Browse Source

Dropdown field for references.

pull/361/head
Sebastian Stehle 7 years ago
parent
commit
383e6b8b06
  1. 15
      src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldEditor.cs
  2. 2
      src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs
  3. 6
      src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs
  4. 5
      src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs
  5. 1
      src/Squidex/app/features/content/declarations.ts
  6. 2
      src/Squidex/app/features/content/module.ts
  7. 4
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  8. 25
      src/Squidex/app/features/content/shared/field-editor.component.html
  9. 163
      src/Squidex/app/features/content/shared/references-dropdown.component.ts
  10. 2
      src/Squidex/app/features/schemas/pages/schema/field-wizard.component.ts
  11. 21
      src/Squidex/app/features/schemas/pages/schema/types/references-ui.component.html
  12. 13
      src/Squidex/app/features/schemas/pages/schema/types/references-ui.component.ts
  13. 6
      src/Squidex/app/features/settings/pages/patterns/pattern.component.ts
  14. 9
      src/Squidex/app/framework/state.ts
  15. 7
      src/Squidex/app/shared/services/schemas.types.ts
  16. 2
      src/Squidex/app/shared/state/contents.forms.spec.ts
  17. 14
      tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/ReferencesFieldPropertiesTests.cs

15
src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldEditor.cs

@ -0,0 +1,15 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Schemas
{
public enum ReferencesFieldEditor
{
List,
Dropdown
}
}

2
src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs

@ -17,6 +17,8 @@ namespace Squidex.Domain.Apps.Core.Schemas
public bool AllowDuplicates { get; set; }
public ReferencesFieldEditor Editor { get; set; }
public Guid SchemaId { get; set; }
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)

6
src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs

@ -190,6 +190,12 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
public IEnumerable<ValidationError> Visit(ReferencesFieldProperties properties)
{
if (!properties.Editor.IsEnumValue())
{
yield return new ValidationError(Not.Valid("Editor"),
nameof(properties.Editor));
}
if (properties.MaxItems.HasValue && properties.MinItems.HasValue && properties.MinItems.Value > properties.MaxItems.Value)
{
yield return new ValidationError(Not.GreaterEquals("Max items", "min items"),

5
src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs

@ -28,6 +28,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields
/// </summary>
public bool AllowDuplicates { get; set; }
/// <summary>
/// The editor that is used to manage this field.
/// </summary>
public ReferencesFieldEditor Editor { get; set; }
/// <summary>
/// The id of the referenced schema.
/// </summary>

1
src/Squidex/app/features/content/declarations.ts

@ -24,4 +24,5 @@ export * from './shared/contents-selector.component';
export * from './shared/due-time-selector.component';
export * from './shared/field-editor.component';
export * from './shared/preview-button.component';
export * from './shared/references-dropdown.component';
export * from './shared/references-editor.component';

2
src/Squidex/app/features/content/module.ts

@ -38,6 +38,7 @@ import {
FieldEditorComponent,
FieldLanguagesComponent,
PreviewButtonComponent,
ReferencesDropdownComponent,
ReferencesEditorComponent,
SchemasPageComponent
} from './declarations';
@ -122,6 +123,7 @@ const routes: Routes = [
FieldEditorComponent,
FieldLanguagesComponent,
PreviewButtonComponent,
ReferencesDropdownComponent,
ReferencesEditorComponent,
SchemasPageComponent
]

4
src/Squidex/app/features/content/pages/content/content-page.component.ts

@ -138,14 +138,14 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
if (asProposal) {
this.contentsState.proposeUpdate(this.content, value)
.subscribe(() => {
this.contentForm.submitCompleted();
this.contentForm.submitCompleted({ noReset: true });
}, error => {
this.contentForm.submitFailed(error);
});
} else {
this.contentsState.update(this.content, value)
.subscribe(() => {
this.contentForm.submitCompleted();
this.contentForm.submitCompleted({ noReset: true });
}, error => {
this.contentForm.submitFailed(error);
});

25
src/Squidex/app/features/content/shared/field-editor.component.html

@ -124,13 +124,24 @@
</sqx-array-editor>
</ng-container>
<ng-container *ngSwitchCase="'References'">
<sqx-references-editor
[formControl]="control"
[language]="language"
[languages]="languages"
[schemaId]="field.properties['schemaId']"
[isCompact]="isCompact">
</sqx-references-editor>
<ng-container [ngSwitch]="field.properties['editor']">
<ng-container *ngSwitchCase="'List'">
<sqx-references-editor
[formControl]="control"
[language]="language"
[languages]="languages"
[schemaId]="field.properties['schemaId']"
[isCompact]="isCompact">
</sqx-references-editor>
</ng-container>
<ng-container *ngSwitchCase="'Dropdown'">
<sqx-references-dropdown
[formControl]="control"
[language]="language"
[schemaId]="field.properties['schemaId']">
</sqx-references-dropdown>
</ng-container>
</ng-container>
</ng-container>
</ng-container>
</ng-template>

163
src/Squidex/app/features/content/shared/references-dropdown.component.ts

@ -0,0 +1,163 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core';
import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { throwError } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import {
AppLanguageDto,
AppsState,
ContentDto,
ContentsService,
FieldFormatter,
fieldInvariant,
ImmutableArray,
MathHelper,
RootFieldDto,
SchemaDetailsDto,
SchemasService,
StatefulControlComponent,
Types
} from '@app/shared';
export const SQX_REFERENCES_DROPDOWN_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ReferencesDropdownComponent), multi: true
};
interface State {
schema?: SchemaDetailsDto | null;
contentItems: ImmutableArray<ContentDto>;
contentNames: ImmutableArray<ContentName>;
}
type ContentName = { name: string, id: string };
@Component({
selector: 'sqx-references-dropdown',
template: `
<select class="form-control" [formControl]="selectedId">
<option [ngValue]="null"></option>
<option *ngFor="let content of snapshot.contentNames" [ngValue]="content.id">{{content.name}}</option>
</select>
`,
providers: [SQX_REFERENCES_DROPDOWN_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ReferencesDropdownComponent extends StatefulControlComponent<State, string[]> implements OnInit {
private languageField: AppLanguageDto;
@Input()
public schemaId: string;
@Input()
public set language(value: AppLanguageDto) {
this.languageField = value;
this.next(s => ({ ...s, contentNames: this.createContentNames(s.schema, s.contentItems) }));
}
public selectedId = new FormControl('');
constructor(changeDetector: ChangeDetectorRef,
private readonly appsState: AppsState,
private readonly contentsService: ContentsService,
private readonly schemasService: SchemasService
) {
super(changeDetector, {
schema: null,
contentItems: ImmutableArray.empty(),
contentNames: ImmutableArray.empty()
});
this.own(
this.selectedId.valueChanges
.subscribe(value => {
if (value) {
this.callTouched();
this.callChange([value]);
} else {
this.callTouched();
this.callChange([]);
}
}));
}
public ngOnInit() {
if (this.schemaId === MathHelper.EMPTY_GUID) {
this.selectedId.disable();
return;
}
this.schemasService.getSchema(this.appsState.appName, this.schemaId).pipe(
switchMap(schema => {
if (schema) {
return this.contentsService.getContents(this.appsState.appName, this.schemaId, 100, 0);
} else {
return throwError('Invalid schema');
}
}, (schema, contents) => ({ schema, contents })))
.subscribe(({ schema, contents }) => {
const contentItems = ImmutableArray.of(contents.items);
const contentNames = this.createContentNames(schema, contentItems);
this.next(s => ({ ...s, schema, contentItems, contentNames }));
}, () => {
this.selectedId.disable();
});
}
public writeValue(obj: any) {
if (Types.isArrayOfString(obj)) {
this.selectedId.setValue(obj[0], { emitEvent: false });
} else {
this.selectedId.setValue(undefined, { emitEvent: false });
}
}
private createContentNames(schema: SchemaDetailsDto | undefined | null, contents: ImmutableArray<ContentDto>): ImmutableArray<ContentName> {
if (contents.length === 0 || !schema) {
return ImmutableArray.empty();
}
function getRawValue(field: RootFieldDto, data: any, language: AppLanguageDto): any {
const contentField = data[field.name];
if (contentField) {
if (field.isLocalizable) {
return contentField[language.iso2Code];
} else {
return contentField[fieldInvariant];
}
}
return undefined;
}
return contents.map(content => {
const values: string[] = [];
for (let field of schema.listFields) {
const value = getRawValue(field, content.data, this.languageField);
if (!Types.isUndefined(value)) {
values.push(FieldFormatter.format(field, value));
}
}
const name = values.join(', ');
return { name, id: content.id };
});
}
public trackByContent(content: ContentDto) {
return content.id;
}
}

2
src/Squidex/app/features/schemas/pages/schema/field-wizard.component.ts

@ -75,7 +75,7 @@ export class FieldWizardComponent implements OnInit {
.subscribe(dto => {
this.field = dto;
this.addFieldForm.submitCompleted({ ...DEFAULT_FIELD });
this.addFieldForm.submitCompleted({ newValue: { ...DEFAULT_FIELD } });
if (addNew) {
if (Types.isFunction(this.nameInput.nativeElement.focus)) {

21
src/Squidex/app/features/schemas/pages/schema/types/references-ui.component.html

@ -1,3 +1,22 @@
<div [formGroup]="editForm">
<span>Nothing to setup</span>
<div class="form-group row">
<label class="col-3 col-form-label">Editor</label>
<div class="col-9">
<label class="btn btn-radio" [class.active]="editForm.controls['editor'].value === 'List'">
<input type="radio" class="radio-input" name="editor" formControlName="editor" value="Checkbox" />
<i class="icon-control-Checkboxes"></i>
<span class="radio-label">List</span>
</label>
<label class="btn btn-radio" [class.active]="editForm.controls['editor'].value === 'Dropdown'">
<input type="radio" class="radio-input" name="editor" formControlName="editor" value="Dropdown" />
<i class="icon-control-Dropdown"></i>
<span class="radio-label">Dropdown</span>
</label>
</div>
</div>
</div>

13
src/Squidex/app/features/schemas/pages/schema/types/references-ui.component.ts

@ -5,8 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { Component, Input, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { FieldDto, ReferencesFieldPropertiesDto } from '@app/shared';
@ -15,7 +15,7 @@ import { FieldDto, ReferencesFieldPropertiesDto } from '@app/shared';
styleUrls: ['references-ui.component.scss'],
templateUrl: 'references-ui.component.html'
})
export class ReferencesUIComponent {
export class ReferencesUIComponent implements OnInit {
@Input()
public editForm: FormGroup;
@ -24,4 +24,11 @@ export class ReferencesUIComponent {
@Input()
public properties: ReferencesFieldPropertiesDto;
public ngOnInit() {
this.editForm.setControl('editor',
new FormControl(this.properties.editor, [
Validators.required
]));
}
}

6
src/Squidex/app/features/settings/pages/patterns/pattern.component.ts

@ -36,7 +36,7 @@ export class PatternComponent implements OnInit {
}
public cancel() {
this.editForm.submitCompleted(this.pattern);
this.editForm.submitCompleted({ newValue: this.pattern });
}
public delete() {
@ -49,8 +49,8 @@ export class PatternComponent implements OnInit {
if (value) {
if (this.pattern) {
this.patternsState.update(this.pattern, value)
.subscribe(() => {
this.editForm.submitCompleted();
.subscribe(newPattern => {
this.editForm.submitCompleted({ newValue: newPattern });
}, error => {
this.editForm.submitFailed(error);
});

9
src/Squidex/app/framework/state.ts

@ -79,11 +79,16 @@ export class Form<T extends AbstractControl, V> {
}
}
public submitCompleted(newValue?: V) {
public submitCompleted(options?: { newValue?: V, noReset?: boolean }) {
this.state.next(() => ({ submitted: false, error: null }));
this.enable();
this.setValue(newValue);
if (options && options.noReset) {
this.form.markAsPristine();
} else {
this.setValue(options ? options.newValue : undefined);
}
}
public submitFailed(error?: string | ErrorDto) {

7
src/Squidex/app/shared/services/schemas.types.ts

@ -79,7 +79,7 @@ export function createProperties(fieldType: FieldType, values: Object | null = n
properties = new NumberFieldPropertiesDto('Input');
break;
case 'References':
properties = new ReferencesFieldPropertiesDto();
properties = new ReferencesFieldPropertiesDto('List');
break;
case 'String':
properties = new StringFieldPropertiesDto('Input');
@ -308,6 +308,7 @@ export class ReferencesFieldPropertiesDto extends FieldPropertiesDto {
public readonly minItems?: number;
public readonly maxItems?: number;
public readonly editor: string;
public readonly schemaId?: string;
public readonly allowDuplicates?: boolean;
@ -315,10 +316,10 @@ export class ReferencesFieldPropertiesDto extends FieldPropertiesDto {
return false;
}
constructor(
constructor(editor: string,
props?: Partial<ReferencesFieldPropertiesDto>
) {
super('Default', props);
super(editor, props);
}
public accept<T>(visitor: FieldPropertiesVisitor<T>): T {

2
src/Squidex/app/shared/state/contents.forms.spec.ts

@ -324,7 +324,7 @@ describe('NumberField', () => {
});
describe('ReferencesField', () => {
const field = createField(new ReferencesFieldPropertiesDto({ isRequired: true, minItems: 1, maxItems: 5 }));
const field = createField(new ReferencesFieldPropertiesDto('List', { isRequired: true, minItems: 1, maxItems: 5 }));
it('should create validators', () => {
expect(FieldValidatorsFactory.createValidators(field, false).length).toBe(3);

14
tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/ReferencesFieldPropertiesTests.cs

@ -30,6 +30,20 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards.FieldProperties
});
}
[Fact]
public void Should_add_error_if_editor_is_not_valid()
{
var sut = new ReferencesFieldProperties { Editor = (ReferencesFieldEditor)123 };
var errors = FieldPropertiesValidator.Validate(sut).ToList();
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Editor is not a valid value.", "Editor")
});
}
[Fact]
public void Should_not_add_error_if_min_items_greater_equals_to_max_items()
{

Loading…
Cancel
Save