diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldEditor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldEditor.cs index f6715eb1f..97d80d1ed 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldEditor.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldEditor.cs @@ -13,5 +13,6 @@ public enum ReferencesFieldEditor Dropdown, Tags, Checkboxes, - Input + Input, + Radio } diff --git a/backend/src/Squidex/wwwroot/scripts/editor-context.html b/backend/src/Squidex/wwwroot/scripts/editor-context.html index 246992a91..de08f5d9f 100644 --- a/backend/src/Squidex/wwwroot/scripts/editor-context.html +++ b/backend/src/Squidex/wwwroot/scripts/editor-context.html @@ -43,6 +43,13 @@ grow(element); }); + + // Init is called once with a context that contains the app name, schema name and authentication information. + field.onContextChanged(function (context) { + element.innerHTML = JSON.stringify(context, null, 2); + + grow(element); + }); diff --git a/backend/src/Squidex/wwwroot/scripts/editor-log.html b/backend/src/Squidex/wwwroot/scripts/editor-log.html index 09ae1df7f..8754ab6d0 100644 --- a/backend/src/Squidex/wwwroot/scripts/editor-log.html +++ b/backend/src/Squidex/wwwroot/scripts/editor-log.html @@ -78,7 +78,7 @@ appendLabel('Value of Form'); appendLine(`<${JSON.stringify(field.getFormValue(), 2)}>`); - appendLabel('Disabled: '); + appendLabel('Disabled'); appendLine(field.isDisabled()); console.log(text); @@ -124,6 +124,10 @@ logState('Field language changed'); }); + field.onContextChanged(function () { + logState('Context changed'); + }); + field.onExpanded(function () { logState('Expanded changed'); }); diff --git a/backend/src/Squidex/wwwroot/scripts/editor-sdk.js b/backend/src/Squidex/wwwroot/scripts/editor-sdk.js index b59588756..b2f5d4796 100644 --- a/backend/src/Squidex/wwwroot/scripts/editor-sdk.js +++ b/backend/src/Squidex/wwwroot/scripts/editor-sdk.js @@ -259,6 +259,7 @@ function SquidexWidget(options) { */ function SquidexFormField(options) { var context; + var contextHandler; var currentConfirm; var currentPickAssets; var currentPickContents; @@ -324,6 +325,12 @@ function SquidexFormField(options) { } } + function raiseContextChanged() { + if (contextHandler && context) { + contextHandler(context); + } + } + function raisedMoved() { if (movedHandler && isNumber(index)) { movedHandler(index); @@ -386,6 +393,10 @@ function SquidexFormField(options) { context = event.data.context; raiseInit(); + } else if (type === 'contextChanged') { + context = event.data.context; + + raiseContextChanged(); } else if (type === 'confirmResult') { var correlationId = event.data.correlationId; @@ -631,7 +642,6 @@ function SquidexFormField(options) { } initHandler = callback; - raiseInit(); }, @@ -661,10 +671,23 @@ function SquidexFormField(options) { } disabledHandler = callback; - raiseDisabled(); }, + /** + * Register an function that is called whenever the context has been changed. + * + * @param {function} callback: The callback to invoke. Argument 1: New context. + */ + onContextChanged: function (callback) { + if (!isFunction(callback)) { + return; + } + + contextHandler = callback; + raiseContextChanged(); + }, + /** * Register an function that is called whenever the field language is changed. * @@ -676,7 +699,6 @@ function SquidexFormField(options) { } languageHandler = callback; - raiseLanguageChanged(); }, @@ -691,7 +713,6 @@ function SquidexFormField(options) { } valueHandler = callback; - raiseValueChanged(); }, @@ -706,7 +727,6 @@ function SquidexFormField(options) { } formValueHandler = callback; - raiseFormValueChanged(); }, @@ -721,7 +741,6 @@ function SquidexFormField(options) { } fullscreenHandler = callback; - raiseFullscreen(); }, @@ -736,7 +755,6 @@ function SquidexFormField(options) { } expandedHandler = callback; - raiseExpanded(); }, diff --git a/frontend/src/app/features/content/pages/content/content-page.component.ts b/frontend/src/app/features/content/pages/content/content-page.component.ts index dd5f1a29d..d4f196e79 100644 --- a/frontend/src/app/features/content/pages/content/content-page.component.ts +++ b/frontend/src/app/features/content/pages/content/content-page.component.ts @@ -62,6 +62,7 @@ import { ContentReferencesComponent } from './references/content-references.comp }) export class ContentPageComponent implements CanComponentDeactivate, OnInit { private readonly subscriptions = new Subscriptions(); + private readonly mutableContext: Record; private autoSaveKey!: AutoSaveKey; public schema!: SchemaDto; @@ -69,8 +70,8 @@ export class ContentPageComponent implements CanComponentDeactivate, OnInit { public formContext: any; public contentTab = this.route.queryParams.pipe(map(x => x['tab'] || 'editor')); - public content?: ContentDto | null; public contentId = ''; + public content?: ContentDto | null; public contentVersion: Version | null = null; public contentForm!: EditContentForm; public contentFormCompare: EditContentForm | null = null; @@ -103,7 +104,7 @@ export class ContentPageComponent implements CanComponentDeactivate, OnInit { ) { const role = appsState.snapshot.selectedApp?.roleName; - this.formContext = { + this.mutableContext = { apiUrl: apiUrl.buildUrl('api'), appId: contentsState.appId, appName: contentsState.appName, @@ -158,10 +159,10 @@ export class ContentPageComponent implements CanComponentDeactivate, OnInit { this.schema = schema; const languageKey = this.localStore.get(this.languageKey()); - const language = this.languages.find(x => x.iso2Code === languageKey); + const languageItem = this.languages.find(x => x.iso2Code === languageKey); - if (language) { - this.language = language; + if (languageItem) { + this.language = languageItem; } this.contentForm = new EditContentForm(this.languages, this.schema, this.schemasState.schemaMap, this.formContext); @@ -172,12 +173,9 @@ export class ContentPageComponent implements CanComponentDeactivate, OnInit { .subscribe(content => { const isNewContent = isOtherContent(content, this.content); - this.formContext['languages'] = this.languages; - this.formContext['schema'] = this.schema; - this.formContext['initialContent'] = content; - this.contentForm.setContext(this.formContext); - this.content = content; + this.updateContext(); + this.contentForm.setContext(this.formContext); this.autoSaveKey = { schemaId: this.schema.id, @@ -221,6 +219,14 @@ export class ContentPageComponent implements CanComponentDeactivate, OnInit { })); } + private updateContext() { + this.mutableContext['initialContent'] = this.content; + this.mutableContext['language'] = this.language; + this.mutableContext['languages'] = this.languages; + this.mutableContext['schema'] = this.schema; + this.formContext = { ...this.mutableContext }; + } + public canDeactivate(): Observable { return this.checkPendingChangesBeforeClose().pipe( tap(confirmed => { @@ -308,6 +314,7 @@ export class ContentPageComponent implements CanComponentDeactivate, OnInit { public changeLanguage(language: AppLanguageDto) { this.language = language; + this.updateContext(); this.localStore.set(this.languageKey(), language.iso2Code); } diff --git a/frontend/src/app/features/content/pages/schemas/schemas-page.component.html b/frontend/src/app/features/content/pages/schemas/schemas-page.component.html index aacd1f710..d77922a85 100644 --- a/frontend/src/app/features/content/pages/schemas/schemas-page.component.html +++ b/frontend/src/app/features/content/pages/schemas/schemas-page.component.html @@ -20,7 +20,7 @@ - @for (category of categories | async; track category.name) { + @for (category of categories | async; track category.displayName) { } diff --git a/frontend/src/app/features/content/shared/forms/field-editor.component.html b/frontend/src/app/features/content/shared/forms/field-editor.component.html index fee1a08ed..37d6378cb 100644 --- a/frontend/src/app/features/content/shared/forms/field-editor.component.html +++ b/frontend/src/app/features/content/shared/forms/field-editor.component.html @@ -203,6 +203,12 @@ [language]="language" [schemaId]="field.rawProperties.singleId"> } + @case ("Radio") { + + } } } @case ("String") { diff --git a/frontend/src/app/features/content/shared/forms/field-editor.component.ts b/frontend/src/app/features/content/shared/forms/field-editor.component.ts index 720aa2569..2476967e3 100644 --- a/frontend/src/app/features/content/shared/forms/field-editor.component.ts +++ b/frontend/src/app/features/content/shared/forms/field-editor.component.ts @@ -13,6 +13,7 @@ import { AbstractContentForm, AnnotationCreate, AnnotationsSelect, AppLanguageDt import { ReferenceDropdownComponent } from '../references/reference-dropdown.component'; import { ReferencesCheckboxesComponent } from '../references/references-checkboxes.component'; import { ReferencesEditorComponent } from '../references/references-editor.component'; +import { ReferencesRadioButtonsComponent } from '../references/references-radio-buttons.component'; import { ReferencesTagsComponent } from '../references/references-tags.component'; import { ArrayEditorComponent } from './array-editor.component'; import { AssetsEditorComponent } from './assets-editor.component'; @@ -50,6 +51,7 @@ import { StockPhotoEditorComponent } from './stock-photo-editor.component'; ReferenceInputComponent, ReferencesCheckboxesComponent, ReferencesEditorComponent, + ReferencesRadioButtonsComponent, ReferencesTagsComponent, RichEditorComponent, StarsComponent, diff --git a/frontend/src/app/features/content/shared/forms/iframe-editor.component.ts b/frontend/src/app/features/content/shared/forms/iframe-editor.component.ts index 996946967..349029889 100644 --- a/frontend/src/app/features/content/shared/forms/iframe-editor.component.ts +++ b/frontend/src/app/features/content/shared/forms/iframe-editor.component.ts @@ -126,6 +126,10 @@ export class IFrameEditorComponent extends StatefulComponent implements O this.sendLanguage(); } + if (changes.context) { + this.sendContext(); + } + if (changes.formControlBinding) { this.subscriptions.unsubscribeAll(); @@ -300,6 +304,10 @@ export class IFrameEditorComponent extends StatefulComponent implements O this.sendMessage('expandedChanged', { expanded: this.isExpanded }); } + private sendContext() { + this.sendMessage('contextChanged', { context: this.context }); + } + private sendDisabled() { this.sendMessage('disabled', { isDisabled: this.isDisabled }); } diff --git a/frontend/src/app/features/content/shared/references/references-checkboxes.component.ts b/frontend/src/app/features/content/shared/references/references-checkboxes.component.ts index c424f9bbd..62090a5cd 100644 --- a/frontend/src/app/features/content/shared/references/references-checkboxes.component.ts +++ b/frontend/src/app/features/content/shared/references/references-checkboxes.component.ts @@ -6,9 +6,8 @@ */ import { booleanAttribute, ChangeDetectionStrategy, Component, forwardRef, inject, Input } from '@angular/core'; -import { FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule, UntypedFormControl } from '@angular/forms'; -import { CheckboxGroupComponent } from '@app/shared'; -import { AppsState, ContentDto, ContentsService, LanguageDto, LocalizerService, StatefulControlComponent, Subscriptions, TypedSimpleChanges, UIOptions } from '@app/shared/internal'; +import { FormControl, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms'; +import { AppsState, CheckboxGroupComponent, ContentDto, ContentsService, LanguageDto, LocalizerService, StatefulControlComponent, Subscriptions, TypedSimpleChanges, UIOptions } from '@app/shared'; import { ReferencesTagsConverter } from './references-tag-converter'; export const SQX_REFERENCES_CHECKBOXES_CONTROL_VALUE_ACCESSOR: any = { @@ -37,7 +36,7 @@ const NO_EMIT = { emitEvent: false }; ReactiveFormsModule, ], }) -export class ReferencesCheckboxesComponent extends StatefulControlComponent> { +export class ReferencesCheckboxesComponent extends StatefulControlComponent | null | undefined> { private readonly subscriptions = new Subscriptions(); private readonly itemCount: number = inject(UIOptions).value.referencesDropdownItemCount; private contentItems: ReadonlyArray | null = null; @@ -53,7 +52,7 @@ export class ReferencesCheckboxesComponent extends StatefulControlComponent | null | undefined>([]); public get isValid() { return !!this.schemaId && !!this.language; @@ -68,7 +67,7 @@ export class ReferencesCheckboxesComponent extends StatefulControlComponent { + .subscribe(value => { if (value && value.length > 0) { this.callTouched(); this.callChange(value); @@ -125,7 +124,6 @@ export class ReferencesCheckboxesComponent extends StatefulControlComponent diff --git a/frontend/src/app/features/content/shared/references/references-radio-buttons.component.scss b/frontend/src/app/features/content/shared/references/references-radio-buttons.component.scss new file mode 100644 index 000000000..2742d895e --- /dev/null +++ b/frontend/src/app/features/content/shared/references/references-radio-buttons.component.scss @@ -0,0 +1,2 @@ +@import 'mixins'; +@import 'vars'; \ No newline at end of file diff --git a/frontend/src/app/features/content/shared/references/references-radio-buttons.component.ts b/frontend/src/app/features/content/shared/references/references-radio-buttons.component.ts new file mode 100644 index 000000000..23b269be6 --- /dev/null +++ b/frontend/src/app/features/content/shared/references/references-radio-buttons.component.ts @@ -0,0 +1,135 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { booleanAttribute, ChangeDetectionStrategy, Component, forwardRef, inject, Input } from '@angular/core'; +import { FormControl, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms'; +import { AppsState, ContentDto, ContentsService, LanguageDto, LocalizerService, RadioGroupComponent, StatefulControlComponent, Subscriptions, TypedSimpleChanges, UIOptions } from '@app/shared'; +import { ReferencesTagsConverter } from './references-tag-converter'; + +export const SQX_REFERENCES_RADIO_BUTTONS_CONTROL_VALUE_ACCESSOR: any = { + provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ReferencesRadioButtonsComponent), multi: true, +}; + +interface State { + // The tags converter. + converter: ReferencesTagsConverter; +} + +const NO_EMIT = { emitEvent: false }; + +@Component({ + standalone: true, + selector: 'sqx-references-radio-buttons', + styleUrls: ['./references-radio-buttons.component.scss'], + templateUrl: './references-radio-buttons.component.html', + providers: [ + SQX_REFERENCES_RADIO_BUTTONS_CONTROL_VALUE_ACCESSOR, + ], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + FormsModule, + RadioGroupComponent, + ReactiveFormsModule, + ], +}) +export class ReferencesRadioButtonsComponent extends StatefulControlComponent | null | undefined> { + private readonly subscriptions = new Subscriptions(); + private readonly itemCount: number = inject(UIOptions).value.referencesDropdownItemCount; + private contentItems: ReadonlyArray | null = null; + + @Input({ required: true }) + public schemaId: string | undefined | null; + + @Input({ required: true }) + public language!: LanguageDto; + + @Input({ transform: booleanAttribute }) + public set disabled(value: boolean | undefined | null) { + this.setDisabledState(value === true); + } + + public control = new FormControl(undefined); + + public get isValid() { + return !!this.schemaId && !!this.language; + } + + constructor( + private readonly appsState: AppsState, + private readonly contentsService: ContentsService, + private readonly localizer: LocalizerService, + ) { + super({ converter: new ReferencesTagsConverter(null!, [], localizer) }); + + this.subscriptions.add( + this.control.valueChanges + .subscribe(value => { + if (value) { + this.callTouched(); + this.callChange([value]); + } else { + this.callTouched(); + this.callChange(null); + } + })); + } + + public ngOnChanges(changes: TypedSimpleChanges) { + if (changes.schemaId) { + this.resetState(); + + if (this.isValid) { + this.contentsService.getContents(this.appsState.appName, this.schemaId!, { take: this.itemCount }) + .subscribe({ + next: contents => { + this.contentItems = contents.items; + + this.resetConverterState(); + }, + error: () => { + this.contentItems = null; + + this.resetConverterState(); + }, + }); + } else { + this.contentItems = null; + + this.resetConverterState(); + } + } else { + this.resetConverterState(); + } + } + + public onDisabled(isDisabled: boolean) { + if (isDisabled) { + this.control.disable(NO_EMIT); + } else if (this.isValid) { + this.control.enable(NO_EMIT); + } + } + + public writeValue(obj: ReadonlyArray | null | undefined) { + this.control.setValue(obj?.[0], NO_EMIT); + } + + private resetConverterState() { + const success = this.isValid && this.contentItems && this.contentItems.length > 0; + + this.onDisabled(!success || this.snapshot.isDisabled); + + let converter: ReferencesTagsConverter; + if (success) { + converter = new ReferencesTagsConverter(this.language, this.contentItems!, this.localizer); + } else { + converter = new ReferencesTagsConverter(null!, [], this.localizer); + } + + this.next({ converter }); + } +} diff --git a/frontend/src/app/features/schemas/pages/schemas/schemas-page.component.html b/frontend/src/app/features/schemas/pages/schemas/schemas-page.component.html index e6b55904b..a32655576 100644 --- a/frontend/src/app/features/schemas/pages/schemas/schemas-page.component.html +++ b/frontend/src/app/features/schemas/pages/schemas/schemas-page.component.html @@ -34,7 +34,7 @@
- @for (category of categories | async; track category.name) { + @for (category of categories | async; track category.displayName) { }
diff --git a/frontend/src/app/shared/components/schema-category.component.html b/frontend/src/app/shared/components/schema-category.component.html index 28a5f91d6..bb78523df 100644 --- a/frontend/src/app/shared/components/schema-category.component.html +++ b/frontend/src/app/shared/components/schema-category.component.html @@ -87,7 +87,7 @@
- @for (category of schemaCategory.categories; track category.name) { + @for (category of schemaCategory.categories; track category.displayName) { = [ 'List', @@ -387,6 +387,7 @@ export const REFERENCES_FIELD_EDITORS: ReadonlyArray = [ 'Checkboxes', 'Tags', 'Input', + 'Radio', ]; export class ReferencesFieldPropertiesDto extends FieldPropertiesDto { diff --git a/frontend/src/app/shared/state/schemas.state.ts b/frontend/src/app/shared/state/schemas.state.ts index a5f6f9ff6..b11d9aeca 100644 --- a/frontend/src/app/shared/state/schemas.state.ts +++ b/frontend/src/app/shared/state/schemas.state.ts @@ -447,7 +447,7 @@ export function getCategoryTree(allSchemas: ReadonlyArray, categories schemasFiltered: [], countSchemasInSubtree: 0, countSchemasInSubtreeFiltered: 0, - categories: [], + categories: [] }; const components: SchemaCategory = { @@ -456,7 +456,7 @@ export function getCategoryTree(allSchemas: ReadonlyArray, categories schemasFiltered: [], countSchemasInSubtree: 0, countSchemasInSubtreeFiltered: 0, - categories: [], + categories: [] }; const categoryCache: { [name: string]: SchemaCategory } = {};