diff --git a/frontend/src/app/features/content/shared/list/content.component.ts b/frontend/src/app/features/content/shared/list/content.component.ts
index 1094a2b3b..a2f75e384 100644
--- a/frontend/src/app/features/content/shared/list/content.component.ts
+++ b/frontend/src/app/features/content/shared/list/content.component.ts
@@ -6,7 +6,7 @@
*/
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, QueryList, SimpleChanges, ViewChildren } from '@angular/core';
-import { AppLanguageDto, ContentDto, ContentListFieldComponent, ContentsState, ModalModel, PatchContentForm, RootFieldDto, TableField, TableSettings, Types } from '@app/shared';
+import { AppLanguageDto, ContentDto, ContentListFieldComponent, ContentsState, ModalModel, PatchContentForm, TableField, TableSettings } from '@app/shared';
/* tslint:disable: component-selector */
@@ -103,8 +103,8 @@ export class ContentComponent implements OnChanges {
}
public shouldStop(field: TableField) {
- if (Types.is(field, RootFieldDto)) {
- return this.isDirty || (field.isInlineEditable && this.patchAllowed);
+ if (field.rootField) {
+ return this.isDirty || (field.rootField.isInlineEditable && this.patchAllowed);
} else {
return this.isDirty;
}
diff --git a/frontend/src/app/features/content/shared/references/reference-item.component.html b/frontend/src/app/features/content/shared/references/reference-item.component.html
index 942ea103c..8adc687aa 100644
--- a/frontend/src/app/features/content/shared/references/reference-item.component.html
+++ b/frontend/src/app/features/content/shared/references/reference-item.component.html
@@ -3,8 +3,8 @@
- |
-
+ |
+
|
@@ -16,8 +16,8 @@
INVALID
|
-
-
+ |
+
|
diff --git a/frontend/src/app/features/content/shared/references/reference-item.component.ts b/frontend/src/app/features/content/shared/references/reference-item.component.ts
index cea5b83ec..fa8591e09 100644
--- a/frontend/src/app/features/content/shared/references/reference-item.component.ts
+++ b/frontend/src/app/features/content/shared/references/reference-item.component.ts
@@ -8,7 +8,7 @@
/* tslint:disable: component-selector */
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
-import { AppLanguageDto, ContentDto, getContentValue } from '@app/shared';
+import { AppLanguageDto, ContentDto, getContentValue, MetaFields } from '@app/shared';
@Component({
selector: '[sqxReferenceItem][language]',
@@ -17,6 +17,8 @@ import { AppLanguageDto, ContentDto, getContentValue } from '@app/shared';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ReferenceItemComponent implements OnChanges {
+ public readonly metaFields = MetaFields;
+
@Output()
public delete = new EventEmitter();
diff --git a/frontend/src/app/features/schemas/pages/schema/fields/types/string-ui.component.ts b/frontend/src/app/features/schemas/pages/schema/fields/types/string-ui.component.ts
index 271eb1969..5eab5548b 100644
--- a/frontend/src/app/features/schemas/pages/schema/fields/types/string-ui.component.ts
+++ b/frontend/src/app/features/schemas/pages/schema/fields/types/string-ui.component.ts
@@ -8,7 +8,7 @@
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { Observable } from 'rxjs';
-import { FieldDto, ResourceOwner, STRING_FIELD_EDITORS, StringFieldPropertiesDto, valueProjection$, SchemaTagSource } from '@app/shared';
+import { FieldDto, ResourceOwner, SchemaTagSource, STRING_FIELD_EDITORS, StringFieldPropertiesDto, valueProjection$ } from '@app/shared';
@Component({
selector: 'sqx-string-ui[field][fieldForm][properties]',
diff --git a/frontend/src/app/features/schemas/pages/schema/ui/field-list.component.html b/frontend/src/app/features/schemas/pages/schema/ui/field-list.component.html
index 2214749ca..679e00808 100644
--- a/frontend/src/app/features/schemas/pages/schema/ui/field-list.component.html
+++ b/frontend/src/app/features/schemas/pages/schema/ui/field-list.component.html
@@ -10,7 +10,7 @@
- {{field}}
+ {{field.name}}
@@ -23,6 +23,6 @@
- {{field}}
+ {{field.name}}
\ No newline at end of file
diff --git a/frontend/src/app/features/schemas/pages/schema/ui/field-list.component.ts b/frontend/src/app/features/schemas/pages/schema/ui/field-list.component.ts
index 0daf35b58..9512ab04a 100644
--- a/frontend/src/app/features/schemas/pages/schema/ui/field-list.component.ts
+++ b/frontend/src/app/features/schemas/pages/schema/ui/field-list.component.ts
@@ -7,9 +7,9 @@
import { CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
-import { MetaFields, SchemaDto } from '@app/shared';
+import { MetaFields, SchemaDto, TableField } from '@app/shared';
-const META_FIELD_NAMES = Object.values(MetaFields);
+const META_FIELD_NAMES = Object.values(MetaFields).filter(x => x !== MetaFields.empty);
@Component({
selector: 'sqx-field-list[fieldNames][schema]',
@@ -33,21 +33,21 @@ export class FieldListComponent implements OnChanges {
@Output()
public fieldNamesChange = new EventEmitter>();
- public fieldsAdded!: string[];
- public fieldsNotAdded!: string[];
+ public fieldsAdded!: TableField[];
+ public fieldsNotAdded!: TableField[];
public ngOnChanges() {
- let allFields = this.schema.contentFields.map(x => x.name);
+ let allFields = this.schema.contentFields;
if (this.withMetaFields) {
allFields = [...allFields, ...META_FIELD_NAMES];
}
- this.fieldsAdded = this.fieldNames.filter(n => allFields.includes(n));
- this.fieldsNotAdded = allFields.filter(n => !this.fieldNames.includes(n));
+ this.fieldsAdded = allFields.filter(x => this.fieldNames.includes(x.name));
+ this.fieldsNotAdded = allFields.filter(x => !this.fieldNames.includes(x.name));
}
- public drop(event: CdkDragDrop) {
+ public drop(event: CdkDragDrop) {
if (event.previousContainer === event.container) {
moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
} else {
@@ -58,7 +58,7 @@ export class FieldListComponent implements OnChanges {
event.currentIndex);
}
- const newNames = this.fieldsAdded;
+ const newNames = this.fieldsAdded.map(x => x.name);
this.fieldNamesChange.emit(newNames);
}
diff --git a/frontend/src/app/framework/angular/forms/editors/autocomplete.stories.ts b/frontend/src/app/framework/angular/forms/editors/autocomplete.stories.ts
index e3b0caa58..8fdff4ee0 100644
--- a/frontend/src/app/framework/angular/forms/editors/autocomplete.stories.ts
+++ b/frontend/src/app/framework/angular/forms/editors/autocomplete.stories.ts
@@ -86,13 +86,6 @@ Icon.args = {
icon: 'user',
};
-export const IconLoading = Template.bind({});
-
-IconLoading.args = {
- source: new Source(['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing'], 4000),
- icon: 'user',
-};
-
export const StyleEmpty = Template.bind({});
StyleEmpty.args = {
@@ -103,4 +96,11 @@ export const StyleUnderlined = Template.bind({});
StyleUnderlined.args = {
inputStyle: 'underlined',
+};
+
+export const IconLoading = Template.bind({});
+
+IconLoading.args = {
+ source: new Source(['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing'], 4000),
+ icon: 'user',
};
\ No newline at end of file
diff --git a/frontend/src/app/framework/angular/forms/editors/checkbox-group.component.html b/frontend/src/app/framework/angular/forms/editors/checkbox-group.component.html
index 942a6e7c2..b20e70e29 100644
--- a/frontend/src/app/framework/angular/forms/editors/checkbox-group.component.html
+++ b/frontend/src/app/framework/angular/forms/editors/checkbox-group.component.html
@@ -1,5 +1,5 @@
\ No newline at end of file
diff --git a/frontend/src/app/framework/angular/forms/editors/checkbox-group.component.ts b/frontend/src/app/framework/angular/forms/editors/checkbox-group.component.ts
index 9ed2feff9..4cd859cf5 100644
--- a/frontend/src/app/framework/angular/forms/editors/checkbox-group.component.ts
+++ b/frontend/src/app/framework/angular/forms/editors/checkbox-group.component.ts
@@ -7,14 +7,12 @@
import { AfterViewChecked, AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnChanges, ViewChild } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
-import { getTagValues, MathHelper, StatefulControlComponent, TagValue, Types } from '@app/framework/internal';
+import { getTagValues, MathHelper, StatefulControlComponent, TagValue, TextMeasurer, Types } from '@app/framework/internal';
export const SQX_CHECKBOX_GROUP_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CheckboxGroupComponent), multi: true,
};
-let CACHED_FONT: string;
-
interface State {
// The checked values.
checkedValues: ReadonlyArray;
@@ -33,6 +31,7 @@ interface State {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CheckboxGroupComponent extends StatefulControlComponent implements AfterViewInit, AfterViewChecked, OnChanges {
+ private readonly textMeasurer: TextMeasurer;
private childrenWidth = 0;
private checkedValuesRaw: any;
private containerWidth = 0;
@@ -44,7 +43,10 @@ export class CheckboxGroupComponent extends StatefulControlComponent;
@Input()
- public layout: 'Auto' | 'Singletine' | 'Multiline' = 'Auto';
+ public layout: 'Auto' | 'Singleline' | 'Multiline' = 'Auto';
+
+ @Input()
+ public unsorted = true;
@Input()
public set disabled(value: boolean | undefined | null) {
@@ -53,17 +55,23 @@ export class CheckboxGroupComponent extends StatefulControlComponent) {
- this.valuesSorted = getTagValues(value);
+ this.tagValuesUnsorted = getTagValues(value, false);
+ this.tagValuesSorted = this.tagValuesUnsorted.sortedByString(x => x.lowerCaseName);
this.writeValue(this.checkedValuesRaw);
}
- public valuesSorted: ReadonlyArray = [];
+ public get tagValues() {
+ return !this.unsorted ? this.tagValuesSorted : this.tagValuesUnsorted;
+ }
+
+ public tagValuesSorted: ReadonlyArray = [];
+ public tagValuesUnsorted: ReadonlyArray = [];
constructor(changeDetector: ChangeDetectorRef) {
- super(changeDetector, {
- checkedValues: [],
- });
+ super(changeDetector, { checkedValues: [] });
+
+ this.textMeasurer = new TextMeasurer(() => this.containerElement);
}
public ngAfterViewInit() {
@@ -87,50 +95,33 @@ export class CheckboxGroupComponent extends StatefulControlComponent 0) {
- checkedValues = this.valuesSorted.filter(x => obj.includes(x.value));
+ checkedValues = this.tagValuesUnsorted.filter(x => obj.includes(x.value));
}
this.next({ checkedValues });
@@ -191,5 +163,3 @@ export class CheckboxGroupComponent extends StatefulControlComponent = (args: CheckboxGroupComponent) => ({
+ props: args,
+ template: `
+
+
+
+
+ `,
+});
+
+export const Default = Template.bind({});
+
+Default.args = {
+ values: ['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing'],
+ model: [],
+};
+
+export const Unsorted = Template.bind({});
+
+Unsorted.args = {
+ values: ['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing'],
+ unsorted: true,
+};
+
+export const Small = Template.bind({});
+
+Small.args = {
+ values: ['Lorem', 'ipsum', 'dolor'],
+ layout: 'Auto',
+};
+
+export const SmallMultiline = Template.bind({});
+
+SmallMultiline.args = {
+ values: ['Lorem', 'ipsum', 'dolor'],
+ layout: 'Multiline',
+};
+
+export const Disabled = Template.bind({});
+
+Disabled.args = {
+ values: ['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing'],
+ disabled: true,
+};
+
+export const Checked = Template.bind({});
+
+Checked.args = {
+ values: ['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing'],
+ model: ['Lorem', 'ipsum'],
+};
\ No newline at end of file
diff --git a/frontend/src/app/framework/angular/forms/editors/radio-group.component.html b/frontend/src/app/framework/angular/forms/editors/radio-group.component.html
new file mode 100644
index 000000000..7e44abcbd
--- /dev/null
+++ b/frontend/src/app/framework/angular/forms/editors/radio-group.component.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/app/framework/angular/forms/editors/radio-group.component.scss b/frontend/src/app/framework/angular/forms/editors/radio-group.component.scss
new file mode 100644
index 000000000..9269cceb4
--- /dev/null
+++ b/frontend/src/app/framework/angular/forms/editors/radio-group.component.scss
@@ -0,0 +1,11 @@
+@import 'mixins';
+@import 'vars';
+
+.form-check-block {
+ margin-bottom: .5rem;
+ margin-left: 0;
+
+ .form-check-input {
+ margin-top: .3rem;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/app/framework/angular/forms/editors/radio-group.component.ts b/frontend/src/app/framework/angular/forms/editors/radio-group.component.ts
new file mode 100644
index 000000000..c3be42332
--- /dev/null
+++ b/frontend/src/app/framework/angular/forms/editors/radio-group.component.ts
@@ -0,0 +1,135 @@
+/*
+ * Squidex Headless CMS
+ *
+ * @license
+ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
+ */
+
+import { AfterViewChecked, AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnChanges, ViewChild } from '@angular/core';
+import { NG_VALUE_ACCESSOR } from '@angular/forms';
+import { getTagValues, MathHelper, StatefulControlComponent, TagValue, TextMeasurer } from '@app/framework/internal';
+
+export const SQX_RADIO_GROUP_CONTROL_VALUE_ACCESSOR: any = {
+ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RadioGroupComponent), multi: true,
+};
+
+interface State {
+ // True when all checkboxes can be shown as single line.
+ isSingleline?: boolean;
+}
+
+@Component({
+ selector: 'sqx-radio-group',
+ styleUrls: ['./radio-group.component.scss'],
+ templateUrl: './radio-group.component.html',
+ providers: [
+ SQX_RADIO_GROUP_CONTROL_VALUE_ACCESSOR,
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class RadioGroupComponent extends StatefulControlComponent implements AfterViewInit, AfterViewChecked, OnChanges {
+ private readonly textMeasurer: TextMeasurer;
+ private childrenWidth = 0;
+ private containerWidth = 0;
+ private labelsMeasured = false;
+
+ public readonly controlId = MathHelper.guid();
+
+ @ViewChild('container', { static: false })
+ public containerElement!: ElementRef;
+
+ @Input()
+ public layout: 'Auto' | 'Singleline' | 'Multiline' = 'Auto';
+
+ @Input()
+ public unsorted = true;
+
+ @Input()
+ public set disabled(value: boolean | undefined | null) {
+ this.setDisabledState(value === true);
+ }
+
+ @Input()
+ public set values(value: ReadonlyArray) {
+ this.tagValuesUnsorted = getTagValues(value, false);
+ this.tagValuesSorted = this.tagValuesUnsorted.sortedByString(x => x.lowerCaseName);
+ }
+
+ public get tagValues() {
+ return !this.unsorted ? this.tagValuesSorted : this.tagValuesUnsorted;
+ }
+
+ public tagValuesSorted: ReadonlyArray = [];
+ public tagValuesUnsorted: ReadonlyArray = [];
+
+ public valueModel: any;
+
+ constructor(changeDetector: ChangeDetectorRef) {
+ super(changeDetector, {});
+
+ this.textMeasurer = new TextMeasurer(() => this.containerElement);
+ }
+
+ public ngAfterViewInit() {
+ this.calculateWidth();
+ }
+
+ public ngAfterViewChecked() {
+ this.calculateWidth();
+ }
+
+ public ngOnChanges() {
+ this.labelsMeasured = false;
+
+ this.calculateWidth();
+ }
+
+ public updateContainerWidth(width: number) {
+ this.containerWidth = width;
+
+ this.calculateSingleLine();
+ }
+
+ private calculateWidth() {
+ if (this.labelsMeasured) {
+ this.calculateSingleLine();
+ return;
+ }
+
+ let width = 0;
+
+ for (const value of this.tagValuesUnsorted) {
+ width += 40;
+ width += this.textMeasurer.getTextSize(value.name);
+ }
+
+ if (width < 0) {
+ return;
+ }
+
+ this.childrenWidth = width;
+ this.calculateSingleLine();
+
+ this.labelsMeasured = true;
+ }
+
+ private calculateSingleLine() {
+ let isSingleline = false;
+
+ if (this.layout !== 'Auto') {
+ isSingleline = this.layout === 'Singleline';
+ } else {
+ isSingleline = this.childrenWidth < this.containerWidth;
+ }
+
+ this.next({ isSingleline });
+ }
+
+ public writeValue(obj: any) {
+ this.valueModel = obj;
+ }
+
+ public trackByValue(_index: number, tag: TagValue) {
+ return tag.id;
+ }
+}
diff --git a/frontend/src/app/framework/angular/forms/editors/radio-group.stories.ts b/frontend/src/app/framework/angular/forms/editors/radio-group.stories.ts
new file mode 100644
index 000000000..ed9db4759
--- /dev/null
+++ b/frontend/src/app/framework/angular/forms/editors/radio-group.stories.ts
@@ -0,0 +1,91 @@
+/*
+ * Squidex Headless CMS
+ *
+ * @license
+ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
+ */
+
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { moduleMetadata } from '@storybook/angular';
+import { Meta, Story } from '@storybook/angular/types-6-0';
+import { RadioGroupComponent, SqxFrameworkModule } from '@app/framework';
+
+export default {
+ title: 'Framework/RadioGroup',
+ component: RadioGroupComponent,
+ argTypes: {
+ disabled: {
+ control: 'boolean',
+ },
+ unsorted: {
+ control: 'boolean',
+ },
+ },
+ decorators: [
+ moduleMetadata({
+ imports: [
+ BrowserAnimationsModule,
+ SqxFrameworkModule,
+ SqxFrameworkModule.forRoot(),
+ ],
+ }),
+ ],
+} as Meta;
+
+const Template: Story = (args: RadioGroupComponent) => ({
+ props: args,
+ template: `
+
+
+
+
+ `,
+});
+
+export const Default = Template.bind({});
+
+Default.args = {
+ values: ['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing'],
+ model: [],
+};
+
+export const Unsorted = Template.bind({});
+
+Unsorted.args = {
+ values: ['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing'],
+ unsorted: false,
+};
+
+export const Small = Template.bind({});
+
+Small.args = {
+ values: ['Lorem', 'ipsum', 'dolor'],
+ layout: 'Auto',
+};
+
+export const SmallMultiline = Template.bind({});
+
+SmallMultiline.args = {
+ values: ['Lorem', 'ipsum', 'dolor'],
+ layout: 'Multiline',
+};
+
+export const Disabled = Template.bind({});
+
+Disabled.args = {
+ values: ['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing'],
+ disabled: true,
+};
+
+export const Checked = Template.bind({});
+
+Checked.args = {
+ values: ['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing'],
+ model: 'ipsum',
+};
\ No newline at end of file
diff --git a/frontend/src/app/framework/angular/forms/editors/tag-editor.component.ts b/frontend/src/app/framework/angular/forms/editors/tag-editor.component.ts
index da96c29cb..634102b0f 100644
--- a/frontend/src/app/framework/angular/forms/editors/tag-editor.component.ts
+++ b/frontend/src/app/framework/angular/forms/editors/tag-editor.component.ts
@@ -8,14 +8,12 @@
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core';
import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { distinctUntilChanged, map, tap } from 'rxjs/operators';
-import { getTagValues, Keys, ModalModel, StatefulControlComponent, StringConverter, TagValue, Types } from '@app/framework/internal';
+import { getTagValues, Keys, ModalModel, StatefulControlComponent, StringConverter, TagValue, TextMeasurer, Types } from '@app/framework/internal';
export const SQX_TAG_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TagEditorComponent), multi: true,
};
-let CACHED_FONT: string;
-
interface State {
// True, when the item has the focus.
hasFocus: boolean;
@@ -40,6 +38,7 @@ interface State {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TagEditorComponent extends StatefulControlComponent> implements AfterViewInit, OnChanges, OnInit {
+ private readonly textMeasurer: TextMeasurer;
private latestValue: any;
private latestInput?: string;
@@ -136,6 +135,8 @@ export class TagEditorComponent extends StatefulControlComponent this.inputElement);
}
public ngAfterViewInit() {
@@ -243,35 +244,19 @@ export class TagEditorComponent extends StatefulControlComponent {
this.formElement.nativeElement.scrollLeft = this.formElement.nativeElement.scrollWidth;
@@ -279,25 +264,6 @@ export class TagEditorComponent extends StatefulControlComponent | undefined | null) {
+export function getTagValues(values: ReadonlyArray | undefined | null, sorted = true) {
if (!Types.isArray(values)) {
return [];
}
@@ -138,5 +138,9 @@ export function getTagValues(values: ReadonlyArray | undefine
}
}
+ if (!sorted) {
+ return result;
+ }
+
return result.sortByString(x => x.lowerCaseName);
}
diff --git a/frontend/src/app/framework/utils/text-measurer.ts b/frontend/src/app/framework/utils/text-measurer.ts
new file mode 100644
index 000000000..60c5e1afb
--- /dev/null
+++ b/frontend/src/app/framework/utils/text-measurer.ts
@@ -0,0 +1,63 @@
+/*
+ * Squidex Headless CMS
+ *
+ * @license
+ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
+ */
+
+import { ElementRef } from '@angular/core';
+import { Types } from '../internal';
+
+let CANVAS: HTMLCanvasElement | null = null;
+
+export class TextMeasurer {
+ private font?: string;
+
+ constructor(
+ private readonly element: () => any | ElementRef,
+ ) {
+ }
+
+ public getTextSize(text: string) {
+ if (!CANVAS) {
+ CANVAS = document.createElement('canvas');
+ }
+
+ if (!this.font) {
+ let currentElement = this.element();
+
+ if (Types.is(currentElement, ElementRef)) {
+ currentElement = currentElement.nativeElement;
+ }
+
+ if (!currentElement) {
+ return -1000;
+ }
+
+ const style = window.getComputedStyle(currentElement);
+
+ const fontSize = style.getPropertyValue('font-size');
+ const fontFamily = style.getPropertyValue('font-family');
+
+ if (!fontSize || !fontFamily) {
+ return -1000;
+ }
+
+ this.font = `${fontSize} ${fontFamily}`;
+ }
+
+ if (!this.font) {
+ return -1000;
+ }
+
+ const ctx = CANVAS.getContext('2d');
+
+ if (!ctx) {
+ return -1000;
+ }
+
+ ctx.font = this.font;
+
+ return ctx.measureText(text).width;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/app/shared/components/contents/content-list-cell.directive.ts b/frontend/src/app/shared/components/contents/content-list-cell.directive.ts
index 2abc149fa..b75f76abe 100644
--- a/frontend/src/app/shared/components/contents/content-list-cell.directive.ts
+++ b/frontend/src/app/shared/components/contents/content-list-cell.directive.ts
@@ -10,11 +10,7 @@ import { ResourceOwner } from '@app/framework';
import { ContentDto, FieldSizes, MetaFields, RootFieldDto, TableField, TableSettings, Types } from '@app/shared/internal';
export function getCellWidth(field: TableField, sizes: FieldSizes | undefined | null) {
- if (Types.is(field, RootFieldDto)) {
- field = field.name;
- }
-
- const size = sizes?.[field] || 0;
+ const size = sizes?.[field.name] || 0;
if (size > 0) {
return size;
@@ -125,12 +121,9 @@ export class ContentListWidthDirective extends ResourceOwner implements OnChange
export class ContentListCellDirective extends ResourceOwner implements OnChanges {
private sizes?: FieldSizes;
private size = -1;
- private fieldName?: string;
@Input()
- public set field(value: TableField) {
- this.fieldName = Types.is(value, RootFieldDto) ? value.name : value;
- }
+ public field!: TableField;
@Input('fields')
public set tableFields(value: TableSettings | undefined | null) {
@@ -155,11 +148,11 @@ export class ContentListCellDirective extends ResourceOwner implements OnChanges
}
private updateSize() {
- if (!this.fieldName) {
+ if (!this.field.name) {
return;
}
- const size = getCellWidth(this.fieldName, this.sizes);
+ const size = getCellWidth(this.field, this.sizes);
if (size === this.size) {
return;
diff --git a/frontend/src/app/shared/components/contents/content-list-field.component.html b/frontend/src/app/shared/components/contents/content-list-field.component.html
index d292d1e25..1edfbb306 100644
--- a/frontend/src/app/shared/components/contents/content-list-field.component.html
+++ b/frontend/src/app/shared/components/contents/content-list-field.component.html
@@ -1,4 +1,4 @@
-
+
{{content.id}}
@@ -94,8 +94,8 @@
{{content.version.value}}
-
-
+
+
diff --git a/frontend/src/app/shared/components/contents/content-list-field.component.ts b/frontend/src/app/shared/components/contents/content-list-field.component.ts
index 3867f2c53..f35e572be 100644
--- a/frontend/src/app/shared/components/contents/content-list-field.component.ts
+++ b/frontend/src/app/shared/components/contents/content-list-field.component.ts
@@ -7,7 +7,7 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges } from '@angular/core';
import { FormGroup } from '@angular/forms';
-import { ContentDto, FieldValue, getContentValue, LanguageDto, MetaFields, RootFieldDto, StatefulComponent, TableField, TableSettings, Types } from '@app/shared/internal';
+import { ContentDto, FieldValue, getContentValue, LanguageDto, MetaFields, StatefulComponent, TableField, TableSettings } from '@app/shared/internal';
interface State {
// The formatted value.
@@ -21,6 +21,8 @@ interface State {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ContentListFieldComponent extends StatefulComponent implements OnChanges {
+ public readonly metaFields = MetaFields;
+
@Input()
public field!: TableField;
@@ -39,16 +41,8 @@ export class ContentListFieldComponent extends StatefulComponent implemen
@Input()
public language!: LanguageDto;
- public get metaFields() {
- return MetaFields;
- }
-
public get isInlineEditable() {
- return Types.is(this.field, RootFieldDto) ? this.field.isInlineEditable : false;
- }
-
- public get fieldName() {
- return Types.is(this.field, RootFieldDto) ? this.field.name : this.field;
+ return this.field.rootField?.isInlineEditable === true;
}
constructor(changeDetector: ChangeDetectorRef) {
@@ -62,8 +56,8 @@ export class ContentListFieldComponent extends StatefulComponent implemen
}
public reset() {
- if (Types.is(this.field, RootFieldDto)) {
- const { value, formatted } = getContentValue(this.content, this.language, this.field);
+ if (this.field.rootField) {
+ const { value, formatted } = getContentValue(this.content, this.language, this.field.rootField);
if (this.patchForm) {
const formControl = this.patchForm.controls[this.field.name];
diff --git a/frontend/src/app/shared/components/contents/content-list-header.component.html b/frontend/src/app/shared/components/contents/content-list-header.component.html
index cc3d75f53..1d748ab05 100644
--- a/frontend/src/app/shared/components/contents/content-list-header.component.html
+++ b/frontend/src/app/shared/components/contents/content-list-header.component.html
@@ -1,56 +1,9 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
+
\ No newline at end of file
diff --git a/frontend/src/app/shared/components/contents/content-list-header.component.ts b/frontend/src/app/shared/components/contents/content-list-header.component.ts
index 71a59938d..7e23476db 100644
--- a/frontend/src/app/shared/components/contents/content-list-header.component.ts
+++ b/frontend/src/app/shared/components/contents/content-list-header.component.ts
@@ -5,8 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
-import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
-import { LanguageDto, MetaFields, Query, RootFieldDto, TableField, Types } from '@app/shared/internal';
+import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
+import { LanguageDto, MetaFields, Query, SortMode, TableField } from '@app/shared/internal';
@Component({
selector: 'sqx-content-list-header[field][language]',
@@ -14,7 +14,9 @@ import { LanguageDto, MetaFields, Query, RootFieldDto, TableField, Types } from
templateUrl: './content-list-header.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class ContentListHeaderComponent {
+export class ContentListHeaderComponent implements OnChanges {
+ public readonly metaFields = MetaFields;
+
@Input()
public field!: TableField;
@@ -27,29 +29,26 @@ export class ContentListHeaderComponent {
@Input()
public language!: LanguageDto;
- public get metaFields() {
- return MetaFields;
- }
+ public sortPath?: string;
+ public sortDefault?: SortMode;
- public get isSortable() {
- return Types.is(this.field, RootFieldDto) ? this.field.properties.isSortable : false;
- }
-
- public get fieldName() {
- return Types.is(this.field, RootFieldDto) ? this.field.name : this.field;
- }
+ public ngOnChanges() {
+ const { field, language } = this;
- public get fieldDisplayName() {
- return Types.is(this.field, RootFieldDto) ? this.field.displayName : '';
- }
-
- public get fieldPath() {
- if (Types.isString(this.field)) {
- return this.field;
- } else if (this.field.isLocalizable && this.language) {
- return `data.${this.field.name}.${this.language.iso2Code}`;
+ if (field === MetaFields.created) {
+ this.sortPath = 'created';
+ } else if (field === MetaFields.lastModified) {
+ this.sortPath = 'lastModified';
+ } else if (field.rootField?.properties.isSortable !== true) {
+ this.sortPath = undefined;
+ } else if (field.rootField.isLocalizable && language) {
+ this.sortPath = `data.${field.name}.${language.iso2Code}`;
} else {
- return `data.${this.field.name}.iv`;
+ this.sortPath = `data.${field.name}.iv`;
+ }
+
+ if (field === MetaFields.lastModified) {
+ this.sortDefault = 'descending';
}
}
}
diff --git a/frontend/src/app/shared/components/contents/content-value.component.scss b/frontend/src/app/shared/components/contents/content-value.component.scss
index 1f74f9d2a..7d24cc453 100644
--- a/frontend/src/app/shared/components/contents/content-value.component.scss
+++ b/frontend/src/app/shared/components/contents/content-value.component.scss
@@ -46,6 +46,7 @@
}
.value-container {
+ cursor: default;
max-height: 10rem;
overflow-x: hidden;
overflow-y: auto;
diff --git a/frontend/src/app/shared/components/contents/content-value.component.ts b/frontend/src/app/shared/components/contents/content-value.component.ts
index a809b12e1..4b9eb8d3d 100644
--- a/frontend/src/app/shared/components/contents/content-value.component.ts
+++ b/frontend/src/app/shared/components/contents/content-value.component.ts
@@ -7,7 +7,7 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { ResourceOwner } from '@app/framework';
-import { FieldWrappings, HtmlValue, RootFieldDto, TableField, TableSettings, Types } from '@app/shared/internal';
+import { FieldWrappings, HtmlValue, TableField, TableSettings, Types } from '@app/shared/internal';
@Component({
selector: 'sqx-content-value[value]',
@@ -28,7 +28,7 @@ export class ContentValueComponent extends ResourceOwner implements OnChanges {
public wrapping = false;
public get isString() {
- return Types.is(this.field, RootFieldDto) && this.field.properties.fieldType === 'String';
+ return this.field.rootField?.properties.fieldType === 'String';
}
public get isPlain() {
@@ -57,11 +57,11 @@ export class ContentValueComponent extends ResourceOwner implements OnChanges {
}
public toggle() {
- this.fields?.toggleWrapping(this.field?.['name']);
+ this.fields?.toggleWrapping(this.field?.name);
}
private updateWrapping(wrappings: FieldWrappings) {
- const wrapping = wrappings[this.field?.['name']];
+ const wrapping = wrappings[this.field?.name];
if (wrapping === this.wrapping) {
return;
diff --git a/frontend/src/app/shared/components/forms/markdown-editor.component.ts b/frontend/src/app/shared/components/forms/markdown-editor.component.ts
index b279b7953..6c8ecc84b 100644
--- a/frontend/src/app/shared/components/forms/markdown-editor.component.ts
+++ b/frontend/src/app/shared/components/forms/markdown-editor.component.ts
@@ -8,8 +8,7 @@
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, Renderer2, ViewChild } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { marked } from 'marked';
-import { ApiUrlConfig, AssetDto, AssetUploaderState, DialogModel, getContentValue, LanguageDto, ResourceLoaderService, StatefulControlComponent, Types, UploadCanceled } from '@app/shared/internal';
-import { ContentDto } from '@app/shared';
+import { ApiUrlConfig, AssetDto, AssetUploaderState, ContentDto, DialogModel, getContentValue, LanguageDto, ResourceLoaderService, StatefulControlComponent, Types, UploadCanceled } from '@app/shared/internal';
declare const SimpleMDE: any;
@@ -199,7 +198,7 @@ export class MarkdownEditorComponent extends StatefulControlComponent
|
-
-
+ |
+
|
|
-
-
+ |
+
|
\ No newline at end of file
diff --git a/frontend/src/app/shared/components/references/content-selector-item.component.ts b/frontend/src/app/shared/components/references/content-selector-item.component.ts
index e819edfff..b5709f9be 100644
--- a/frontend/src/app/shared/components/references/content-selector-item.component.ts
+++ b/frontend/src/app/shared/components/references/content-selector-item.component.ts
@@ -8,7 +8,7 @@
/* tslint:disable: component-selector */
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
-import { ContentDto, LanguageDto, SchemaDto } from '@app/shared/internal';
+import { ContentDto, LanguageDto, MetaFields, SchemaDto } from '@app/shared/internal';
@Component({
selector: '[sqxContentSelectorItem][language][schema]',
@@ -17,6 +17,8 @@ import { ContentDto, LanguageDto, SchemaDto } from '@app/shared/internal';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ContentSelectorItemComponent {
+ public readonly metaFields = MetaFields;
+
@Output()
public selectedChange = new EventEmitter();
diff --git a/frontend/src/app/shared/components/references/content-selector.component.html b/frontend/src/app/shared/components/references/content-selector.component.html
index 9439f89b9..d3018fbd9 100644
--- a/frontend/src/app/shared/components/references/content-selector.component.html
+++ b/frontend/src/app/shared/components/references/content-selector.component.html
@@ -49,8 +49,8 @@
[ngModel]="selectedAll"
(ngModelChange)="selectAll($event)">
-
-
+ |
+
|
|
-
-
+ |
+
|
diff --git a/frontend/src/app/shared/components/references/content-selector.component.ts b/frontend/src/app/shared/components/references/content-selector.component.ts
index 033a3a423..69b23b5f2 100644
--- a/frontend/src/app/shared/components/references/content-selector.component.ts
+++ b/frontend/src/app/shared/components/references/content-selector.component.ts
@@ -8,7 +8,7 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { BehaviorSubject, of } from 'rxjs';
import { distinctUntilChanged, map, switchMap } from 'rxjs/operators';
-import { ApiUrlConfig, AppsState, ComponentContentsState, ContentDto, LanguageDto, Query, ResourceOwner, SchemaDto, SchemasService, SchemasState } from '@app/shared/internal';
+import { ApiUrlConfig, AppsState, ComponentContentsState, ContentDto, LanguageDto, MetaFields, Query, ResourceOwner, SchemaDto, SchemasService, SchemasState } from '@app/shared/internal';
@Component({
selector: 'sqx-content-selector[language][languages]',
@@ -19,6 +19,8 @@ import { ApiUrlConfig, AppsState, ComponentContentsState, ContentDto, LanguageDt
],
})
export class ContentSelectorComponent extends ResourceOwner implements OnInit {
+ public readonly metaFields = MetaFields;
+
@Output()
public select = new EventEmitter>();
diff --git a/frontend/src/app/shared/components/table-header.component.ts b/frontend/src/app/shared/components/table-header.component.ts
index 521b672c3..e7bd11cb0 100644
--- a/frontend/src/app/shared/components/table-header.component.ts
+++ b/frontend/src/app/shared/components/table-header.component.ts
@@ -25,50 +25,52 @@ export class TableHeaderComponent implements OnChanges {
public text = '';
@Input()
- public fieldPath?: string | undefined | null;
+ public language!: LanguageDto;
@Input()
- public language!: LanguageDto;
+ public sortPath?: string | undefined | null;
@Input()
public sortable?: boolean | null;
@Input()
- public defaultOrder: SortMode | undefined | null;
+ public sortDefault: SortMode | undefined | null;
public order: SortMode | undefined | null;
public ngOnChanges() {
- if (this.sortable) {
- const { sort } = this.query || {};
-
- if (this.fieldPath && sort && sort.length === 1 && sort[0].path === this.fieldPath) {
- this.order = sort[0].order;
- } else if (this.defaultOrder && (!sort || sort.length === 0)) {
- this.order = this.defaultOrder;
- } else {
- this.order = null;
- }
+ const { query, sortDefault, sortable, sortPath } = this;
+
+ if (!sortable) {
+ this.order = null;
+ } else if (sortPath && query?.sort?.length === 1 && query.sort[0].path === sortPath) {
+ this.order = query.sort[0].order;
+ } else if (sortDefault && !query?.sort?.length) {
+ this.order = sortDefault;
} else {
this.order = null;
}
}
public sort() {
- if (this.sortable && this.fieldPath) {
- if (!this.order || this.order !== 'ascending') {
- this.order = 'ascending';
- } else {
- this.order = 'descending';
- }
-
- const newQuery = Types.clone(this.query || {});
+ const { order, query, sortable, sortPath } = this;
- newQuery.sort = [
- { path: this.fieldPath, order: this.order! },
- ];
+ if (!sortable || !sortPath) {
+ return;
+ }
- this.queryChange.emit(newQuery);
+ if (!order || order !== 'ascending') {
+ this.order = 'ascending';
+ } else {
+ this.order = 'descending';
}
+
+ const newQuery = Types.clone(query || {});
+
+ newQuery.sort = [
+ { path: sortPath, order: this.order! },
+ ];
+
+ this.queryChange.emit(newQuery);
}
}
diff --git a/frontend/src/app/shared/services/schemas.service.ts b/frontend/src/app/shared/services/schemas.service.ts
index b50d4b759..0f381074e 100644
--- a/frontend/src/app/shared/services/schemas.service.ts
+++ b/frontend/src/app/shared/services/schemas.service.ts
@@ -14,17 +14,54 @@ import { QueryModel } from './query';
import { createProperties, FieldPropertiesDto } from './schemas.types';
export const MetaFields = {
- id: 'meta.id',
- created: 'meta.created',
- createdByAvatar: 'meta.createdBy.avatar',
- createdByName: 'meta.createdBy.name',
- lastModified: 'meta.lastModified',
- lastModifiedByAvatar: 'meta.lastModifiedBy.avatar',
- lastModifiedByName: 'meta.lastModifiedBy.name',
- status: 'meta.status',
- statusColor: 'meta.status.color',
- statusNext: 'meta.status.next',
- version: 'meta.version',
+ empty: {
+ name: '',
+ label: '',
+ },
+ id: {
+ name: 'meta.id',
+ label: 'i18n:contents.tableHeaders.id',
+ },
+ created: {
+ name: 'meta.created',
+ label: 'i18n:contents.tableHeaders.created',
+ },
+ createdByAvatar: {
+ name: 'meta.createdBy.avatar',
+ label: 'i18n:contents.tableHeaders.createdByShort',
+ },
+ createdByName: {
+ name: 'meta.createdBy.name',
+ label: 'i18n:contents.tableHeaders.createdBy',
+ },
+ lastModified: {
+ name: 'meta.lastModified',
+ label: 'i18n:contents.tableHeaders.lastModified',
+ },
+ lastModifiedByAvatar: {
+ name: 'meta.lastModifiedBy.avatar',
+ label: 'i18n:contents.tableHeaders.lastModifiedByShort',
+ },
+ lastModifiedByName: {
+ name: 'meta.lastModifiedBy.name',
+ label: 'i18n:contents.tableHeaders.lastModifiedBy',
+ },
+ status: {
+ name: 'meta.status',
+ label: 'i18n:contents.tableHeaders.status',
+ },
+ statusColor: {
+ name: 'meta.status.color',
+ label: 'i18n:contents.tableHeaders.status',
+ },
+ statusNext: {
+ name: 'meta.status.next',
+ label: 'i18n:contents.tableHeaders.nextStatus',
+ },
+ version: {
+ name: 'meta.version',
+ label: 'i18n:contents.tableHeaders.version',
+ },
};
export type SchemaType = 'Default' | 'Singleton' | 'Component';
@@ -53,7 +90,7 @@ export class SchemaDto {
public readonly displayName: string;
- public readonly contentFields: ReadonlyArray = [];
+ public readonly contentFields: ReadonlyArray = [];
public readonly defaultListFields: ReadonlyArray = [];
public readonly defaultReferenceFields: ReadonlyArray = [];
@@ -99,17 +136,37 @@ export class SchemaDto {
this.displayName = StringHelper.firstNonEmpty(this.properties.label, this.name);
if (fields) {
- this.contentFields = fields.filter(x => x.properties.isContentField);
+ this.contentFields = fields.filter(x => x.properties.isContentField).map(tableField);
+
+ function tableFields(names: ReadonlyArray, fields: ReadonlyArray): TableField[] {
+ const result: TableField[] = [];
+
+ for (const name of names) {
+ const metaField = MetaFields[name];
+
+ if (metaField) {
+ result.push(metaField);
+ } else {
+ const field = fields.find(x => x.name === name && x.properties.isContentField);
+
+ if (field) {
+ result.push(tableField(field));
+ }
+ }
+ }
+
+ return result;
+ }
- const listFields = findFields(fieldsInLists, this.contentFields);
+ const listFields = tableFields(fieldsInLists, fields);
if (listFields.length === 0) {
listFields.push(MetaFields.lastModifiedByAvatar);
if (fields.length > 0) {
- listFields.push(this.fields[0]);
+ listFields.push(tableField(this.fields[0]));
} else {
- listFields.push('');
+ listFields.push(MetaFields.empty);
}
listFields.push(MetaFields.statusColor);
@@ -118,15 +175,17 @@ export class SchemaDto {
this.defaultListFields = listFields;
- this.defaultReferenceFields = findFields(fieldsInReferences, this.contentFields);
+ const referenceFields = tableFields(fieldsInReferences, fields);
- if (this.defaultReferenceFields.length === 0) {
+ if (referenceFields.length === 0) {
if (fields.length > 0) {
- this.defaultReferenceFields = [fields[0]];
+ referenceFields.push(tableField(this.fields[0]));
} else {
- this.defaultReferenceFields = [''];
+ referenceFields.push(MetaFields.empty);
}
}
+
+ this.defaultReferenceFields = referenceFields;
}
}
@@ -187,22 +246,8 @@ export class SchemaDto {
}
}
-function findFields(names: ReadonlyArray, fields: ReadonlyArray): TableField[] {
- const result: TableField[] = [];
-
- for (const name of names) {
- if (name.startsWith('meta.')) {
- result.push(name);
- } else {
- const field = fields.find(x => x.name === name);
-
- if (field) {
- result.push(field);
- }
- }
- }
-
- return result;
+export function tableField(rootField: RootFieldDto) {
+ return { name: rootField.name, label: rootField.displayName, rootField };
}
export class FieldDto {
@@ -304,7 +349,7 @@ export const FIELD_RULE_ACTIONS: ReadonlyArray = [
type Tags = readonly string[];
-export type TableField = RootFieldDto | string;
+export type TableField = { name: string; label: string; rootField?: RootFieldDto };
export type FieldRuleAction = 'Disable' | 'Hide' | 'Require';
export type FieldRule = { field: string; action: FieldRuleAction; condition: string };
diff --git a/frontend/src/app/shared/services/schemas.spec.ts b/frontend/src/app/shared/services/schemas.spec.ts
index cf994537d..5b7d19f69 100644
--- a/frontend/src/app/shared/services/schemas.spec.ts
+++ b/frontend/src/app/shared/services/schemas.spec.ts
@@ -39,37 +39,57 @@ describe('SchemaDto', () => {
it('should return configured fields as list fields if fields are declared', () => {
const schema = createSchema({ properties: new SchemaPropertiesDto(''), fields: [field1, field2, field3], fieldsInLists: ['field1', 'field3'] });
- expect(schema.defaultListFields).toEqual([field1, field3]);
+ expect(schema.defaultListFields.map(x => x.name)).toEqual([
+ field1.name,
+ field3.name,
+ ]);
+ });
+
+ it('should return configured fields as references fields if fields are declared', () => {
+ const schema = createSchema({ properties: new SchemaPropertiesDto(''), fields: [field1, field2, field3], fieldsInReferences: ['field1', 'field3'] });
+
+ expect(schema.defaultReferenceFields.map(x => x.name)).toEqual([
+ field1.name,
+ field3.name,
+ ]);
});
it('should return first fields as list fields if no field is declared', () => {
const schema = createSchema({ properties: new SchemaPropertiesDto(''), fields: [field1, field2, field3] });
- expect(schema.defaultListFields).toEqual([MetaFields.lastModifiedByAvatar, field1, MetaFields.statusColor, MetaFields.lastModified]);
+ expect(schema.defaultListFields.map(x => x.name)).toEqual([
+ MetaFields.lastModifiedByAvatar.name,
+ field1.name,
+ MetaFields.statusColor.name,
+ MetaFields.lastModified.name,
+ ]);
});
it('should return preset with empty content field as list fields if fields is empty', () => {
const schema = createSchema({ properties: new SchemaPropertiesDto() });
- expect(schema.defaultListFields).toEqual([MetaFields.lastModifiedByAvatar, '', MetaFields.statusColor, MetaFields.lastModified]);
- });
-
- it('should return configured fields as references fields if fields are declared', () => {
- const schema = createSchema({ properties: new SchemaPropertiesDto(''), fields: [field1, field2, field3], fieldsInReferences: ['field1', 'field3'] });
-
- expect(schema.defaultReferenceFields).toEqual([field1, field3]);
+ expect(schema.defaultListFields.map(x => x.name)).toEqual([
+ MetaFields.lastModifiedByAvatar.name,
+ MetaFields.empty.name,
+ MetaFields.statusColor.name,
+ MetaFields.lastModified.name,
+ ]);
});
it('should return first field as reference fields if no field is declared', () => {
const schema = createSchema({ properties: new SchemaPropertiesDto(''), fields: [field1, field2, field3] });
- expect(schema.defaultReferenceFields).toEqual([field1]);
+ expect(schema.defaultReferenceFields.map(x => x.name)).toEqual([
+ field1.name,
+ ]);
});
it('should return noop field as reference field if list is empty', () => {
const schema = createSchema({ properties: new SchemaPropertiesDto() });
- expect(schema.defaultReferenceFields).toEqual(['']);
+ expect(schema.defaultReferenceFields.map(x => x.name)).toEqual([
+ MetaFields.empty.name,
+ ]);
});
});
diff --git a/frontend/src/app/shared/state/contents.forms.ts b/frontend/src/app/shared/state/contents.forms.ts
index 746277343..616cbb9b8 100644
--- a/frontend/src/app/shared/state/contents.forms.ts
+++ b/frontend/src/app/shared/state/contents.forms.ts
@@ -42,7 +42,7 @@ export class PatchContentForm extends Form {
) {
super(new ExtendedFormGroup({}));
- this.editableFields = this.listFields.filter(x => Types.is(x, RootFieldDto) && x.isInlineEditable) as any;
+ this.editableFields = this.listFields.filter(x => x.rootField?.isInlineEditable).map(x => x.rootField!);
for (const field of this.editableFields) {
const validators = FieldsValidators.create(field, this.language.isOptional);
diff --git a/frontend/src/app/shared/state/table-settings.spec.ts b/frontend/src/app/shared/state/table-settings.spec.ts
index 5ba129e69..b9f8150cd 100644
--- a/frontend/src/app/shared/state/table-settings.spec.ts
+++ b/frontend/src/app/shared/state/table-settings.spec.ts
@@ -8,7 +8,7 @@
import { of } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { DateTime, Version } from '@app/framework';
-import { createProperties, FieldSizes, MetaFields, RootFieldDto, SchemaDto, TableField, TableSettings, UIState } from '@app/shared/internal';
+import { createProperties, FieldSizes, MetaFields, RootFieldDto, SchemaDto, TableSettings, UIState } from '@app/shared/internal';
import { FieldWrappings } from '..';
describe('TableSettings', () => {
@@ -33,17 +33,17 @@ describe('TableSettings', () => {
uiState = Mock.ofType();
});
+ const INVALID_FIELD = { name: 'invalid', label: 'invalid' };
const INVALID_CONFIGS = [
{ case: 'blank', fields: [] },
- { case: 'broken', fields: ['invalid'] },
+ { case: 'broken', fields: [{ name: 'invalid', label: 'invalid' }] },
];
const EMPTY = { fields: [], sizes: {}, wrappings: {} };
INVALID_CONFIGS.forEach(test => {
it(`should provide default fields if config is ${test.case}`, () => {
- let listFields: ReadonlyArray;
- let listFieldNames: ReadonlyArray;
+ let listFields: ReadonlyArray;
let fieldSizes: FieldSizes;
let fieldWrappings: FieldWrappings;
@@ -65,11 +65,7 @@ describe('TableSettings', () => {
const tableSettings = new TableSettings(uiState.object, schema);
tableSettings.listFields.subscribe(result => {
- listFields = result;
- });
-
- tableSettings.listFieldNames.subscribe(result => {
- listFieldNames = result;
+ listFields = result.map(x => x.name);
});
tableSettings.fieldSizes.subscribe(result => {
@@ -81,17 +77,10 @@ describe('TableSettings', () => {
});
expect(listFields!).toEqual([
- MetaFields.lastModifiedByAvatar,
- schema.fields[0],
- MetaFields.statusColor,
- MetaFields.lastModified,
- ]);
-
- expect(listFieldNames!).toEqual([
- MetaFields.lastModifiedByAvatar,
+ MetaFields.lastModifiedByAvatar.name,
schema.fields[0].name,
- MetaFields.statusColor,
- MetaFields.lastModified,
+ MetaFields.statusColor.name,
+ MetaFields.lastModified.name,
]);
expect(fieldSizes!).toEqual({
@@ -122,28 +111,19 @@ describe('TableSettings', () => {
});
it('should eliminate invalid fields from the config', () => {
- let listFields: ReadonlyArray;
- let listFieldNames: ReadonlyArray;
+ let listFields: ReadonlyArray;
uiState.setup(x => x.getUser('schemas.my-schema.config', {}))
- .returns(() => of(({ fields: ['invalid', MetaFields.version] })));
+ .returns(() => of(({ fields: ['invalid', MetaFields.version.name] })));
const tableSettings = new TableSettings(uiState.object, schema);
tableSettings.listFields.subscribe(result => {
- listFields = result;
- });
-
- tableSettings.listFieldNames.subscribe(result => {
- listFieldNames = result;
+ listFields = result.map(x => x.name);
});
expect(listFields!).toEqual([
- MetaFields.version,
- ]);
-
- expect(listFieldNames!).toEqual([
- MetaFields.version,
+ MetaFields.version.name,
]);
});
@@ -153,11 +133,11 @@ describe('TableSettings', () => {
const tableSettings = new TableSettings(uiState.object, schema);
- const config = ['invalid', MetaFields.version];
+ const config = [INVALID_FIELD, MetaFields.version];
tableSettings.updateFields(config, true);
- uiState.verify(x => x.set('schemas.my-schema.config', { ...EMPTY, fields: [MetaFields.version] }, true), Times.once());
+ uiState.verify(x => x.set('schemas.my-schema.config', { ...EMPTY, fields: [MetaFields.version.name] }, true), Times.once());
expect().nothing();
});
@@ -181,7 +161,7 @@ describe('TableSettings', () => {
const tableSettings = new TableSettings(uiState.object, schema);
- const config = ['invalid', MetaFields.version];
+ const config = [INVALID_FIELD, MetaFields.version];
tableSettings.updateFields(config, false);
@@ -202,11 +182,11 @@ describe('TableSettings', () => {
fieldSizes = result;
});
- tableSettings.updateSize(MetaFields.version, 100, true);
+ tableSettings.updateSize(MetaFields.version.name, 100, true);
- uiState.verify(x => x.set('schemas.my-schema.config', { ...EMPTY, sizes: { [MetaFields.version]: 100 } }, true), Times.once());
+ uiState.verify(x => x.set('schemas.my-schema.config', { ...EMPTY, sizes: { [MetaFields.version.name]: 100 } }, true), Times.once());
- expect(fieldSizes!).toEqual({ [MetaFields.version]: 100 });
+ expect(fieldSizes!).toEqual({ [MetaFields.version.name]: 100 });
});
it('should update config if sizes are only updated', () => {
@@ -221,11 +201,11 @@ describe('TableSettings', () => {
fieldSizes = result;
});
- tableSettings.updateSize(MetaFields.version, 100, false);
+ tableSettings.updateSize(MetaFields.version.name, 100, false);
uiState.verify(x => x.set('schemas.my-schema.config', It.isAny(), true), Times.never());
- expect(fieldSizes!).toEqual({ [MetaFields.version]: 100 });
+ expect(fieldSizes!).toEqual({ [MetaFields.version.name]: 100 });
});
it('should update config if wrapping is toggled', () => {
@@ -240,11 +220,11 @@ describe('TableSettings', () => {
fieldWrappings = result;
});
- tableSettings.toggleWrapping(MetaFields.version, true);
+ tableSettings.toggleWrapping(MetaFields.version.name, true);
- uiState.verify(x => x.set('schemas.my-schema.config', { ...EMPTY, wrappings: { [MetaFields.version]: true } }, true), Times.once());
+ uiState.verify(x => x.set('schemas.my-schema.config', { ...EMPTY, wrappings: { [MetaFields.version.name]: true } }, true), Times.once());
- expect(fieldWrappings!).toEqual({ [MetaFields.version]: true });
+ expect(fieldWrappings!).toEqual({ [MetaFields.version.name]: true });
});
it('should update config if wrapping is toggled and only updated', () => {
@@ -259,22 +239,21 @@ describe('TableSettings', () => {
fieldWrappings = result;
});
- tableSettings.toggleWrapping(MetaFields.version, false);
+ tableSettings.toggleWrapping(MetaFields.version.name, false);
uiState.verify(x => x.set('schemas.my-schema.config', It.isAny(), true), Times.never());
- expect(fieldWrappings!).toEqual({ [MetaFields.version]: true });
+ expect(fieldWrappings!).toEqual({ [MetaFields.version.name]: true });
});
it('should provide default fields if reset', () => {
- let listFields: ReadonlyArray;
- let listFieldNames: ReadonlyArray;
+ let listFields: ReadonlyArray;
let fieldSizes: FieldSizes;
let fieldWrappings: FieldWrappings;
const config = {
fields: [
- MetaFields.version,
+ MetaFields.version.name,
],
sizes: {
field1: 100,
@@ -292,11 +271,7 @@ describe('TableSettings', () => {
const tableSettings = new TableSettings(uiState.object, schema);
tableSettings.listFields.subscribe(result => {
- listFields = result;
- });
-
- tableSettings.listFieldNames.subscribe(result => {
- listFieldNames = result;
+ listFields = result.map(x => x.name);
});
tableSettings.fieldSizes.subscribe(result => {
@@ -310,17 +285,10 @@ describe('TableSettings', () => {
tableSettings.reset();
expect(listFields!).toEqual([
- MetaFields.lastModifiedByAvatar,
- schema.fields[0],
- MetaFields.statusColor,
- MetaFields.lastModified,
- ]);
-
- expect(listFieldNames!).toEqual([
- MetaFields.lastModifiedByAvatar,
+ MetaFields.lastModifiedByAvatar.name,
schema.fields[0].name,
- MetaFields.statusColor,
- MetaFields.lastModified,
+ MetaFields.statusColor.name,
+ MetaFields.lastModified.name,
]);
expect(fieldSizes!).toEqual({});
diff --git a/frontend/src/app/shared/state/table-settings.ts b/frontend/src/app/shared/state/table-settings.ts
index 3f2ac0a4c..a4e619046 100644
--- a/frontend/src/app/shared/state/table-settings.ts
+++ b/frontend/src/app/shared/state/table-settings.ts
@@ -10,14 +10,14 @@ import { State, Types } from '@app/framework';
import { MetaFields, SchemaDto, TableField } from './../services/schemas.service';
import { UIState } from './ui.state';
-const META_FIELD_NAMES = Object.values(MetaFields);
+const META_FIELD_NAMES = Object.values(MetaFields).filter(x => x !== MetaFields.empty);
export type FieldSizes = { [name: string]: number };
export type FieldWrappings = { [name: string]: boolean };
interface Snapshot {
// The table fields in the right order.
- fields: ReadonlyArray;
+ fields: ReadonlyArray;
// The sizes of the columns if overriden.
sizes: FieldSizes;
@@ -29,8 +29,8 @@ interface Snapshot {
export class TableSettings extends State {
private readonly settingsKey: string;
- public readonly schemaFields: ReadonlyArray;
- public readonly schemaDefaults: ReadonlyArray;
+ public readonly schemaFields: ReadonlyArray;
+ public readonly schemaDefaults: ReadonlyArray;
public fieldSizes =
this.project(x => x.sizes);
@@ -41,11 +41,8 @@ export class TableSettings extends State {
public fields =
this.project(x => x.fields);
- public listFieldNames =
- this.projectFrom(this.fields, x => this.getListFieldNames(x));
-
public listFields =
- this.projectFrom(this.listFieldNames, x => this.getListFields(x));
+ this.projectFrom(this.fields, x => x.length > 0 ? x : this.schemaDefaults);
constructor(
private readonly uiState: UIState,
@@ -53,8 +50,8 @@ export class TableSettings extends State {
) {
super({ fields: [], sizes: {}, wrappings: {} });
- this.schemaFields = [...schema.contentFields.map(x => x.name), ...META_FIELD_NAMES].sort();
- this.schemaDefaults = schema.defaultListFields.map(x => x['name'] || x);
+ this.schemaFields = [...schema.contentFields, ...META_FIELD_NAMES];
+ this.schemaDefaults = schema.defaultListFields;
this.settingsKey = `schemas.${this.schema.name}.config`;
@@ -112,8 +109,8 @@ export class TableSettings extends State {
}
}
- public updateFields(fields: ReadonlyArray, save = true) {
- this.publishFields(fields);
+ public updateFields(fields: ReadonlyArray, save = true) {
+ this.publishFields(fields.map(x => x.name));
if (save) {
this.saveConfig();
@@ -128,8 +125,10 @@ export class TableSettings extends State {
this.next({ wrappings });
}
- private publishFields(fields: ReadonlyArray) {
- this.next({ fields: fields.filter(x => this.schemaFields.includes(x)) });
+ private publishFields(names: ReadonlyArray) {
+ const fields = names.map(n => this.schemaFields.find(f => f.name === n)).filter(x => !!x) as any;
+
+ this.next({ fields });
}
private saveConfig() {
@@ -138,15 +137,9 @@ export class TableSettings extends State {
if (Object.keys(sizes).length === 0 && Object.keys(wrappings).length === 0 && fields.length === 0) {
this.uiState.removeUser(this.settingsKey);
} else {
- this.uiState.set(this.settingsKey, this.snapshot, true);
- }
- }
+ const update = { sizes, wrappings, fields: fields.map(x => x.name) };
- private getListFields(names: ReadonlyArray): ReadonlyArray {
- return names.map(n => this.schema.fields.find(f => f.name === n) || n);
- }
-
- private getListFieldNames(names: ReadonlyArray): ReadonlyArray {
- return names.length === 0 ? this.schemaDefaults : names;
+ this.uiState.set(this.settingsKey, update, true);
+ }
}
}
diff --git a/frontend/src/app/shell/module.ts b/frontend/src/app/shell/module.ts
index 8078446ea..ff3bb7639 100644
--- a/frontend/src/app/shell/module.ts
+++ b/frontend/src/app/shell/module.ts
@@ -7,8 +7,7 @@
import { NgModule } from '@angular/core';
import { SqxFrameworkModule, SqxSharedModule } from '@app/shared';
-import { NotificationDropdownComponent } from '.';
-import { AppAreaComponent, AppsMenuComponent, ForbiddenPageComponent, HomePageComponent, InternalAreaComponent, LeftMenuComponent, LoginPageComponent, LogoComponent, LogoutPageComponent, NotFoundPageComponent, NotificationsMenuComponent, ProfileMenuComponent, SearchMenuComponent } from './declarations';
+import { AppAreaComponent, AppsMenuComponent, ForbiddenPageComponent, HomePageComponent, InternalAreaComponent, LeftMenuComponent, LoginPageComponent, LogoComponent, LogoutPageComponent, NotFoundPageComponent, NotificationDropdownComponent, NotificationsMenuComponent, ProfileMenuComponent, SearchMenuComponent } from './declarations';
@NgModule({
imports: [