Browse Source

More stories (#873)

* More stories.

* Fix components.
pull/876/head
Sebastian Stehle 4 years ago
committed by GitHub
parent
commit
377fe120d7
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      frontend/src/app/features/content/shared/forms/array-editor.component.ts
  2. 6
      frontend/src/app/features/content/shared/forms/stock-photo-editor.component.html
  3. 17
      frontend/src/app/features/content/shared/forms/stock-photo-editor.component.scss
  4. 2
      frontend/src/app/features/content/shared/references/reference-dropdown.component.html
  5. 7
      frontend/src/app/features/content/shared/references/reference-dropdown.component.ts
  6. 3
      frontend/src/app/features/content/shared/references/references-tags.component.html
  7. 7
      frontend/src/app/features/content/shared/references/references-tags.component.ts
  8. 2
      frontend/src/app/features/settings/pages/contributors/contributor-add-form.component.html
  9. 6
      frontend/src/app/features/settings/pages/workflows/workflow-step.component.html
  10. 5
      frontend/src/app/features/settings/pages/workflows/workflow-transition.component.html
  11. 8
      frontend/src/app/framework/angular/forms/editors/autocomplete.component.html
  12. 2
      frontend/src/app/framework/angular/forms/editors/autocomplete.component.ts
  13. 106
      frontend/src/app/framework/angular/forms/editors/autocomplete.stories.ts
  14. 22
      frontend/src/app/framework/angular/forms/editors/date-time-editor.stories.ts
  15. 12
      frontend/src/app/framework/angular/forms/editors/dropdown.component.html
  16. 8
      frontend/src/app/framework/angular/forms/editors/dropdown.component.scss
  17. 6
      frontend/src/app/framework/angular/forms/editors/dropdown.component.ts
  18. 175
      frontend/src/app/framework/angular/forms/editors/dropdown.stories.ts
  19. 52
      frontend/src/app/framework/angular/forms/editors/tag-editor.component.html
  20. 6
      frontend/src/app/framework/angular/forms/editors/tag-editor.component.scss
  21. 14
      frontend/src/app/framework/angular/forms/editors/tag-editor.component.ts
  22. 170
      frontend/src/app/framework/angular/forms/editors/tag-editor.stories.ts
  23. 17
      frontend/src/app/framework/angular/layout.component.ts
  24. 2
      frontend/src/app/framework/angular/list-view.component.html
  25. 9
      frontend/src/app/framework/angular/loader.component.html
  26. 22
      frontend/src/app/framework/angular/loader.component.scss
  27. 24
      frontend/src/app/framework/angular/loader.component.ts
  28. 82
      frontend/src/app/framework/angular/loader.stories.ts
  29. 11
      frontend/src/app/framework/angular/modals/modal-placement.directive.ts
  30. 1
      frontend/src/app/framework/declarations.ts
  31. 4
      frontend/src/app/framework/module.ts
  32. 2
      frontend/src/app/shared/components/assets/asset-folder-dropdown-item.component.html
  33. 5
      frontend/src/app/shell/pages/internal/logo.component.html
  34. 5
      frontend/src/app/shell/pages/internal/logo.component.scss
  35. 53
      frontend/src/app/theme/_common.scss

2
frontend/src/app/features/content/shared/forms/array-editor.component.ts

@ -71,7 +71,7 @@ export class ArrayEditorComponent implements OnChanges {
const maxItems = this.formModel.field.properties['maxItems'] || Number.MAX_VALUE;
if (Types.is(this.formModel.field.properties, ComponentsFieldPropertiesDto)) {
this.schemasList = this.formModel.field.properties.schemaIds?.map(x => this.formModel.globals.schemas[x]).defined() || [];
this.schemasList = this.formModel.field.properties.schemaIds?.map(x => this.formModel.globals.schemas[x]).defined().sortedByString(x => x.displayName) || [];
} else {
this.isArray = true;
}

6
frontend/src/app/features/content/shared/forms/stock-photo-editor.component.html

@ -14,7 +14,7 @@
<div *ngIf="stockPhotoThumbnail | async; let url;" class="preview mt-1" [class.hidden-important]="snapshot.thumbnailStatus === 'Failed'">
<img [src]="url" (error)="onThumbnailFailed()" (load)="onThumbnailLoaded()">
<i class="icon-spinner2 spin2" [class.hidden-important]="snapshot.thumbnailStatus === 'Loaded'"></i>
<sqx-loader color="white" *ngIf="snapshot.thumbnailStatus !== 'Loaded'"></sqx-loader>
</div>
<ng-container *sqxModal="searchDialog">
@ -22,7 +22,7 @@
<ng-container title>
<input class="form-control search" [formControl]="stockPhotoSearch" sqxFocusOnInit placeholder="{{ 'contents.stockPhotoSearch' | sqxTranslate }}">
<i *ngIf="snapshot.isLoading" class="icon-spinner2 spin2"></i>
<sqx-loader *ngIf="snapshot.isLoading"></sqx-loader>
</ng-container>
<ng-container content>
@ -44,7 +44,7 @@
<div class="mt-4 text-center" *ngIf="snapshot.hasMore">
<button class="btn btn-outline-secondary" type="button" (click)="loadMore()" [disabled]="snapshot.isLoading">
{{ 'common.loadMore' | sqxTranslate }} <i *ngIf="snapshot.isLoading" class="icon-spinner2 spin2"></i>
{{ 'common.loadMore' | sqxTranslate }} <sqx-loader *ngIf="snapshot.isLoading"></sqx-loader>
</button>
</div>
</ng-container>

17
frontend/src/app/features/content/shared/forms/stock-photo-editor.component.scss

@ -9,12 +9,17 @@ $color-background: #000;
background: $color-background;
border: 0;
border-radius: 0;
color: white;
text-align: center;
i {
margin-top: 1rem;
margin-bottom: 1rem;
& {
position: relative;
max-height: none;
min-height: 2rem;
}
sqx-loader {
@include absolute(50%, null, null, 50%);
margin-left: -8px;
}
img {
@ -22,10 +27,6 @@ $color-background: #000;
}
}
.spin2 {
font-size: 14px;
}
.search {
display: inline-block;
margin-left: 0;

2
frontend/src/app/features/content/shared/references/reference-dropdown.component.html

@ -1,4 +1,4 @@
<sqx-dropdown [formControl]="control" [items]="snapshot.contentNames" valueProperty="id" (open)="onOpened()">
<sqx-dropdown [formControl]="control" [items]="snapshot.contentNames" [itemsLoading]="snapshot.isLoading" valueProperty="id" (open)="onOpened()">
<ng-template let-content="$implicit" let-context="context">
<span class="truncate" [innerHTML]="content.name | sqxHighlight:context | sqxSafeHtml"></span>
</ng-template>

7
frontend/src/app/features/content/shared/references/reference-dropdown.component.ts

@ -21,6 +21,9 @@ interface State {
// The name of the selected item.
selectedItem?: ContentName;
// True when loading.
isLoading?: boolean;
}
type ContentName = { name: string; id?: string };
@ -143,6 +146,8 @@ export class ReferenceDropdownComponent extends StatefulControlComponent<State,
}
private loadMore(observable: Observable<ContentsDto>) {
this.next({ isLoading: true });
observable
.subscribe({
next: ({ items }) => {
@ -196,5 +201,7 @@ export class ReferenceDropdownComponent extends StatefulControlComponent<State,
this.next({ contentNames });
}
this.next({ isLoading: false });
}
}

3
frontend/src/app/features/content/shared/references/references-tags.component.html

@ -4,5 +4,6 @@
[allowDuplicates]="false"
[allowOpen]="true"
[converter]="snapshot.converter"
[suggestions]="snapshot.converter.suggestions">
[suggestions]="snapshot.converter.suggestions"
[suggestionsLoading]="snapshot.isLoading">
</sqx-tag-editor>

7
frontend/src/app/features/content/shared/references/references-tags.component.ts

@ -19,6 +19,9 @@ export const SQX_REFERENCES_TAGS_CONTROL_VALUE_ACCESSOR: any = {
interface State {
// The tags converter.
converter: ReferencesTagsConverter;
// True when loading.
isLoading?: boolean;
}
const NO_EMIT = { emitEvent: false };
@ -126,6 +129,8 @@ export class ReferencesTagsComponent extends StatefulControlComponent<State, Rea
}
private loadMore(observable: Observable<ContentsDto>) {
this.next({ isLoading: true });
observable
.subscribe({
next: ({ items }) => {
@ -165,5 +170,7 @@ export class ReferencesTagsComponent extends StatefulControlComponent<State, Rea
this.next({ converter });
}
this.next({ isLoading: false });
}
}

2
frontend/src/app/features/settings/pages/contributors/contributor-add-form.component.html

@ -4,7 +4,7 @@
<form [formGroup]="assignContributorForm.form" (ngSubmit)="assignContributor()">
<div class="row gx-2">
<div class="col">
<sqx-autocomplete [source]="usersDataSource" formControlName="user" inputName="contributor" placeholder="{{ 'contributors.emailPlaceholder' | sqxTranslate }}" displayProperty="displayName">
<sqx-autocomplete [source]="usersDataSource" formControlName="user" icon="search" inputName="contributor" placeholder="{{ 'contributors.emailPlaceholder' | sqxTranslate }}" displayProperty="displayName">
<ng-template let-user="$implicit">
<span class="autocomplete-user">
<img class="user-picture" [src]="user | sqxUserDtoPicture">

6
frontend/src/app/features/settings/pages/workflows/workflow-step.component.html

@ -91,13 +91,15 @@
<span class="text-decent">{{ 'workflows.syntax.for' | sqxTranslate }}</span>
</div>
<div class="col col-roles">
<sqx-tag-editor [allowDuplicates]="false" [dashed]="true"
<sqx-tag-editor
[allowDuplicates]="false"
[disabled]="!!disabled"
[ngModelOptions]="onBlur"
[ngModel]="step.noUpdateRoles"
(ngModelChange)="changeNoUpdateRoles($event)"
[singleLine]="true"
[suggestions]="roles" placeholder="{{ 'common.role' | sqxTranslate }}">
[suggestions]="roles" placeholder="{{ 'common.role' | sqxTranslate }}"
[styleDashed]="true">
</sqx-tag-editor>
</div>
<div class="col col-button"></div>

5
frontend/src/app/features/settings/pages/workflows/workflow-transition.component.html

@ -22,13 +22,14 @@
<span class="text-decent">{{ 'workflows.syntax.for' | sqxTranslate }}</span>
</div>
<div class="col col-roles">
<sqx-tag-editor [allowDuplicates]="false"
[dashed]="true"
<sqx-tag-editor
[allowDuplicates]="false"
[disabled]="!!disabled"
[ngModelOptions]="onBlur"
[ngModel]="transition.roles"
(ngModelChange)="changeRole($event)"
[singleLine]="true"
[styleDashed]="true"
[suggestions]="roles" placeholder="{{ 'common.role' | sqxTranslate }}">
</sqx-tag-editor>
</div>

8
frontend/src/app/framework/angular/forms/editors/autocomplete.component.html

@ -12,7 +12,13 @@
[formControl]="queryInput">
<div class="icon" *ngIf="icon">
<i class="icon-{{icon}}" [class.icon-spinner2]="snapshot.isLoading" [class.spin2]="snapshot.isLoading"></i>
<ng-container *ngIf="snapshot.isLoading; else notLoading">
<sqx-loader color="input"></sqx-loader>
</ng-container>
<ng-template #notLoading>
<i class="icon-{{icon}}"></i>
</ng-template>
</div>
<div class="btn btn-sm" (click)="openModal()" sqxStopClick *ngIf="allowOpen">

2
frontend/src/app/framework/angular/forms/editors/autocomplete.component.ts

@ -58,7 +58,7 @@ export class AutocompleteComponent extends StatefulControlComponent<State, Reado
public inputStyle?: 'underlined' | 'empty';
@Input()
public allowOpen?: boolean | null = true;
public allowOpen?: boolean | null = false;
@Input()
public displayProperty = '';

106
frontend/src/app/framework/angular/forms/editors/autocomplete.stories.ts

@ -0,0 +1,106 @@
/*
* 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 { map, Observable, timer } from 'rxjs';
import { AutocompleteComponent, LocalizerService, SqxFrameworkModule } from '@app/framework';
import { AutocompleteSource } from './autocomplete.component';
const TRANSLATIONS = {
'common.search': 'Search',
'common.empty': 'Nothing available.',
};
export default {
title: 'Framework/Autocomplete',
component: AutocompleteComponent,
argTypes: {
disabled: {
control: 'boolean',
},
dropdownFullWidth: {
control: 'boolean',
},
},
decorators: [
moduleMetadata({
imports: [
BrowserAnimationsModule,
SqxFrameworkModule,
SqxFrameworkModule.forRoot(),
],
providers: [
{ provide: LocalizerService, useFactory: () => new LocalizerService(TRANSLATIONS) },
],
}),
],
} as Meta;
const Template: Story<AutocompleteComponent & { model: any }> = (args: AutocompleteComponent) => ({
props: args,
template: `
<sqx-root-view>
<sqx-autocomplete
[disabled]="disabled"
[icon]="icon"
[inputStyle]="inputStyle"
[source]="source">
</sqx-autocomplete>
</sqx-root-view>
`,
});
class Source implements AutocompleteSource {
constructor(
private readonly values: string[],
private readonly delay = 0,
) {
}
public find(query: string): Observable<readonly any[]> {
return timer(this.delay).pipe(map(() => this.values.filter(x => x.indexOf(query) >= 0)));
}
}
export const Default = Template.bind({});
Default.args = {
source: new Source(['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing']),
};
export const Disabled = Template.bind({});
Disabled.args = {
disabled: true,
};
export const Icon = Template.bind({});
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 = {
inputStyle: 'empty',
};
export const StyleUnderlined = Template.bind({});
StyleUnderlined.args = {
inputStyle: 'underlined',
};

22
frontend/src/app/framework/angular/forms/editors/date-time-editor.stories.ts

@ -5,10 +5,10 @@
* 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 { LocalizerService, SqxFrameworkModule, UIOptions } from '@app/framework';
import { DateTimeEditorComponent } from './date-time-editor.component';
import { DateTimeEditorComponent, LocalizerService, SqxFrameworkModule, UIOptions } from '@app/framework';
const translations = {
'common.date': 'Date',
@ -23,6 +23,9 @@ export default {
title: 'Framework/DateTimeEditor',
component: DateTimeEditorComponent,
argTypes: {
disabled: {
control: 'boolean',
},
hideClear: {
control: 'boolean',
},
@ -43,6 +46,7 @@ export default {
decorators: [
moduleMetadata({
imports: [
BrowserAnimationsModule,
SqxFrameworkModule,
SqxFrameworkModule.forRoot(),
],
@ -64,8 +68,22 @@ Date.args = {
mode: 'Date',
};
export const DateDisabled = Template.bind({});
DateDisabled.args = {
mode: 'Date',
disabled: true,
};
export const DateTime = Template.bind({});
DateTime.args = {
mode: 'DateTime',
};
export const DateTimeDisabled = Template.bind({});
DateTimeDisabled.args = {
mode: 'DateTime',
disabled: true,
};

12
frontend/src/app/framework/angular/forms/editors/dropdown.component.html

@ -24,7 +24,7 @@
[style]="dropdownStyles"
[position]="dropdownPosition">
<div *ngIf="canSearch" class="search-form">
<input class="form-control search" [formControl]="queryInput" placeholder="{{ 'contributors.search' | sqxTranslate }}" (keydown)="onKeyDown($event)" sqxFocusOnInit>
<input class="form-control search" [formControl]="queryInput" placeholder="{{ 'common.search' | sqxTranslate }}" (keydown)="onKeyDown($event)" sqxFocusOnInit>
</div>
<div class="control-dropdown-items" #container>
@ -38,6 +38,16 @@
<ng-template *ngIf="templateItem" [sqxTemplateWrapper]="templateItem" [item]="item" [index]="i" [context]="snapshot.query"></ng-template>
</div>
<div class="text-decent control-dropdown-item no-events" *ngIf="snapshot.suggestedItems.length === 0">
<ng-container *ngIf="itemsLoading; else notLoading">
<sqx-loader color="input"></sqx-loader>
</ng-container>
<ng-template #notLoading>
<small>{{itemsEmptyText | sqxTranslate}}</small>
</ng-template>
</div>
</div>
</sqx-dropdown-menu>
</ng-container>

8
frontend/src/app/framework/angular/forms/editors/dropdown.component.scss

@ -19,6 +19,10 @@ $color-input-disabled: #eef1f4;
white-space: nowrap;
}
.no-events {
pointer-events: none;
}
.search-form {
padding: .5rem;
}
@ -57,4 +61,8 @@ $color-input-disabled: #eef1f4;
.form-control {
cursor: default;
}
}
sqx-loader {
margin-top: .25rem;
}

6
frontend/src/app/framework/angular/forms/editors/dropdown.component.ts

@ -46,6 +46,12 @@ export class DropdownComponent extends StatefulControlComponent<State, ReadonlyA
@Output()
public close = new EventEmitter();
@Input()
public itemsLoading?: boolean | null;
@Input()
public itemsEmptyText = 'i18n:common.empty';
@Input()
public items: ReadonlyArray<any> | undefined | null = [];

175
frontend/src/app/framework/angular/forms/editors/dropdown.stories.ts

@ -0,0 +1,175 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { moduleMetadata } from '@storybook/angular';
import { Meta, Story } from '@storybook/angular/types-6-0';
import { DropdownComponent, LocalizerService, SqxFrameworkModule } from '@app/framework';
const TRANSLATIONS = {
'common.search': 'Search',
'common.empty': 'Nothing available.',
};
@Component({
selector: 'sqx-dropdown-test',
template: `
<sqx-root-view>
<sqx-dropdown
[allowOpen]="true"
[items]="items"
[itemsLoading]="itemsLoading"
(open)="load()">
</sqx-dropdown>
</sqx-root-view>
`,
})
class TestComponent {
public items: string[] = [];
public itemsLoading = false;
public load() {
this.items = [];
this.itemsLoading = true;
setTimeout(() => {
this.items = ['A', 'B'];
this.itemsLoading = false;
}, 1000);
}
}
export default {
title: 'Framework/Dropdown',
component: DropdownComponent,
argTypes: {
disabled: {
control: 'boolean',
},
dropdownFullWidth: {
control: 'boolean',
},
},
decorators: [
moduleMetadata({
declarations: [
TestComponent,
],
imports: [
BrowserAnimationsModule,
SqxFrameworkModule,
SqxFrameworkModule.forRoot(),
],
providers: [
{ provide: LocalizerService, useFactory: () => new LocalizerService(TRANSLATIONS) },
],
}),
],
} as Meta;
const Template: Story<DropdownComponent & { model: any }> = (args: DropdownComponent) => ({
props: args,
template: `
<sqx-root-view>
<sqx-dropdown
[disabled]="disabled"
[dropdownPosition]="'bottom-left'"
[dropdownFullWidth]="dropdownFullWidth"
[items]="items"
[itemsLoading]="itemsLoading"
[ngModel]="model">
</sqx-dropdown>
</sqx-root-view>
`,
});
const Template2: Story<DropdownComponent & { model: any }> = (args: DropdownComponent) => ({
props: args,
template: `
<sqx-root-view>
<sqx-dropdown
[disabled]="disabled"
[dropdownPosition]="'bottom-left'"
[dropdownFullWidth]="dropdownFullWidth"
[items]="items"
[itemsLoading]="itemsLoading"
[searchProperty]="searchProperty"
[ngModel]="model"
[valueProperty]="valueProperty">
<ng-template let-target="$implicit">
{{target.label}}
</ng-template>
</sqx-dropdown>
</sqx-root-view>
`,
});
const Template3: Story<DropdownComponent & { model: any }> = (args: DropdownComponent) => ({
props: args,
template: `
<sqx-dropdown-test></sqx-dropdown-test>
`,
});
export const Default = Template.bind({});
Default.args = {
items: ['A', 'B', 'C'],
model: 'B',
};
export const Empty = Template.bind({});
Empty.args = {
items: [],
model: 'B',
};
export const EmptyLoading = Template.bind({});
EmptyLoading.args = {
items: [],
itemsLoading: true,
};
export const NoSearch = Template.bind({});
NoSearch.args = {
items: ['A', 'B', 'C'],
canSearch: false,
};
export const FullWidth = Template.bind({});
FullWidth.args = {
items: ['A', 'B', 'C'],
dropdownFullWidth: true,
};
export const ComplexValues = Template2.bind({});
ComplexValues.args = {
searchProperty: 'label',
items: [{
id: 1,
label: 'Lorem',
}, {
id: 2,
label: 'ipsum',
}, {
id: 3,
label: 'dolor',
}, {
id: 4,
label: 'sit',
}],
model: 2,
valueProperty: 'id',
};
export const Lazy = Template3.bind({});

52
frontend/src/app/framework/angular/forms/editors/tag-editor.component.html

@ -9,7 +9,7 @@
[class.multiline]="!singleLine"
[class.focus]="snapshot.hasFocus"
[class.disabled]="snapshot.isDisabled"
[class.dashed]="dashed && !(snapshot.items.length > 0)">
[class.dashed]="styleDashed && !(snapshot.items.length > 0)">
<span class="item" *ngFor="let item of snapshot.items; let i = index" [class.disabled]="addInput.disabled">
{{item}} <i class="icon-close" (click)="remove(i)"></i>
</span>
@ -49,29 +49,37 @@
</sqx-dropdown-menu>
</ng-container>
<ng-container *ngIf="allowOpen && suggestionsSorted.length > 0">
<ng-container *sqxModal="suggestionsModal">
<sqx-dropdown-menu class="control-dropdown suggestions-dropdown"
[sqxAnchoredTo]="form"
[adjustWidth]="false"
[adjustHeight]="false"
[scrollY]="true"
position="bottom-left">
<div class="row">
<div class=" col-6" *ngFor="let item of suggestionsSorted; let i = index">
<div class="form-check form-check">
<input class="form-check-input" type="checkbox" id="tag_{{i}}"
[ngModel]="isSelected(item)"
(ngModelChange)="toggleValue($event, item)"
/>
<ng-container *sqxModal="suggestionsModal">
<sqx-dropdown-menu class="control-dropdown suggestions-dropdown"
[sqxAnchoredTo]="form"
[adjustWidth]="false"
[adjustHeight]="false"
[scrollY]="true"
position="bottom-left">
<div class="row">
<div class=" col-6" *ngFor="let item of suggestionsSorted; let i = index">
<div class="form-check form-check">
<input class="form-check-input" type="checkbox" id="tag_{{i}}"
[ngModel]="isSelected(item)"
(ngModelChange)="toggleValue($event, item)"
/>
<label class="form-check-label" for="tag_{{i}}" title="{{item.name}}" titlePosition="top-left">
<span class="truncate">{{item.name}}</span>
</label>
</div>
<label class="form-check-label" for="tag_{{i}}" title="{{item.name}}" titlePosition="top-left">
<span class="truncate">{{item.name}}</span>
</label>
</div>
</div>
</sqx-dropdown-menu>
</ng-container>
</div>
<div class="text-decent" *ngIf="suggestionsSorted.length === 0">
<ng-container *ngIf="suggestionsLoading; else notLoading">
<sqx-loader color="input"></sqx-loader>
</ng-container>
<ng-template #notLoading>
<small>{{suggestionsEmptyText | sqxTranslate}}</small>
</ng-template>
</div>
</sqx-dropdown-menu>
</ng-container>
</div>

6
frontend/src/app/framework/angular/forms/editors/tag-editor.component.scss

@ -185,5 +185,11 @@ div {
.suggestions-dropdown {
@include force-width(450px);
max-height: none;
min-height: 4rem;
padding: 1rem;
}
sqx-loader {
margin-top: .25rem;
}

14
frontend/src/app/framework/angular/forms/editors/tag-editor.component.ts

@ -68,14 +68,11 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
public acceptEnter?: boolean | null;
@Input()
public allowOpen?: boolean | null = true;
public allowOpen?: boolean | null = false;
@Input()
public allowDuplicates?: boolean | null = true;
@Input()
public dashed?: boolean | null;
@Input()
public itemSeparator?: boolean | null;
@ -85,6 +82,9 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
@Input()
public readonly?: boolean | null;
@Input()
public styleDashed?: boolean | null;
@Input()
public styleBlank?: boolean | null;
@ -97,6 +97,12 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
@Input()
public dropdownWidth = '18rem';
@Input()
public suggestionsLoading?: boolean | null;
@Input()
public suggestionsEmptyText = 'i18n:common.empty';
@Input()
public set disabled(value: boolean | undefined | null) {
this.setDisabledState(value === true);

170
frontend/src/app/framework/angular/forms/editors/tag-editor.stories.ts

@ -0,0 +1,170 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component } from '@angular/core';
import { moduleMetadata } from '@storybook/angular';
import { Meta, Story } from '@storybook/angular/types-6-0';
import { LocalizerService, SqxFrameworkModule, TagEditorComponent } from '@app/framework';
const TRANSLATIONS = {
'common.tagAdd': ', to add tag',
'common.empty': 'Nothing available.',
};
@Component({
selector: 'sqx-tag-editor-test',
template: `
<sqx-root-view>
<sqx-tag-editor
[allowOpen]="true"
[suggestions]="suggestions"
[suggestionsLoading]="suggestionsLoading"
(open)="load()">
</sqx-tag-editor>
</sqx-root-view>
`,
})
class TestComponent {
public suggestions: string[] = [];
public suggestionsLoading = false;
public load() {
this.suggestions = [];
this.suggestionsLoading = true;
setTimeout(() => {
this.suggestions = ['A', 'B'];
this.suggestionsLoading = false;
}, 1000);
}
}
export default {
title: 'Framework/TagEditor',
component: TagEditorComponent,
argTypes: {
dashed: {
control: 'boolean',
},
disabled: {
control: 'boolean',
},
},
decorators: [
moduleMetadata({
declarations: [
TestComponent,
],
imports: [
SqxFrameworkModule,
SqxFrameworkModule.forRoot(),
],
providers: [
{ provide: LocalizerService, useFactory: () => new LocalizerService(TRANSLATIONS) },
],
}),
],
} as Meta;
const Template: Story<TagEditorComponent & { ngModel: any }> = (args: TagEditorComponent) => ({
props: args,
template: `
<sqx-root-view>
<sqx-tag-editor
[allowOpen]="allowOpen"
[disabled]="disabled"
[ngModel]="ngModel"
[singleLine]="singleLine"
[styleBlank]="styleBlank"
[styleDashed]="styleDashed"
[suggestions]="suggestions"
[suggestionsLoading]="suggestionsLoading">
</sqx-tag-editor>
</sqx-root-view>
`,
});
const Template2: Story<TagEditorComponent & { ngModel: any }> = (args: TagEditorComponent) => ({
props: args,
template: `
<sqx-tag-editor-test></sqx-tag-editor-test>
`,
});
export const Default = Template.bind({});
export const Suggestions = Template.bind({});
Suggestions.args = {
suggestions: ['A', 'B', 'C'],
allowOpen: true,
};
export const SuggestionsEmpty = Template.bind({});
SuggestionsEmpty.args = {
suggestions: [],
allowOpen: true,
};
export const SuggestionsLoading = Template.bind({});
SuggestionsLoading.args = {
suggestionsLoading: true,
allowOpen: true,
};
export const Values = Template.bind({});
Values.args = {
suggestions: [],
ngModel: ['A', 'A', 'B'],
};
export const StyleDashed = Template.bind({});
StyleDashed.args = {
styleDashed: true,
ngModel: [],
};
export const StyleDashedValues = Template.bind({});
StyleDashedValues.args = {
styleDashed: true,
ngModel: ['A', 'B', 'C'],
};
export const StyleBlank = Template.bind({});
StyleBlank.args = {
styleBlank: true,
ngModel: [],
};
export const StyleBlankValues = Template.bind({});
StyleBlankValues.args = {
styleBlank: true,
ngModel: ['A', 'B', 'C'],
};
export const Multiline = Template.bind({});
Multiline.args = {
singleLine: false,
ngModel: ['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'do', 'eiusmod', 'tempor', 'incididunt', 'ut', 'labore', 'et', 'dolore', 'magna', 'aliqua'],
};
export const SingleLine = Template.bind({});
SingleLine.args = {
singleLine: true,
ngModel: ['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'do', 'eiusmod', 'tempor', 'incididunt', 'ut', 'labore', 'et', 'dolore', 'magna', 'aliqua'],
};
export const Lazy = Template2.bind({});

17
frontend/src/app/framework/angular/layout.component.ts

@ -9,7 +9,7 @@
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core';
import { ActivatedRoute, NavigationEnd, QueryParamsHandling, Router } from '@angular/router';
import { filter, map, startWith } from 'rxjs';
import { concat, defer, filter, map, of } from 'rxjs';
import { LayoutContainerDirective } from './layout-container.directive';
@Component({
@ -87,13 +87,14 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
}
public firstChild =
this.router.events.pipe(
filter(event => event instanceof NavigationEnd),
map(() => {
return !!this.route.firstChild;
}),
startWith(!!this.route.firstChild),
);
concat(
defer(() => of(!!this.route.firstChild)),
this.router.events.pipe(
filter(event => event instanceof NavigationEnd),
map(() => {
return !!this.route.firstChild;
}),
));
constructor(
private readonly container: LayoutContainerDirective,

2
frontend/src/app/framework/angular/list-view.component.html

@ -37,7 +37,7 @@
</div>
<div class="loader text-center" *ngIf="isLoading">
<i class="icon-spinner2 spin2"></i>
<sqx-loader></sqx-loader>
</div>
<ng-template #contentTemplate>

9
frontend/src/app/framework/angular/loader.component.html

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: none; display: block; shape-rendering: auto;"
[attr.width]="size"
[attr.height]="size"
viewBox="0 0 100 100"
preserveAspectRatio="xMidYMid">
<circle cx="50" cy="50" fill="none" [class]="color" stroke-width="12" r="35" stroke-dasharray="164.93361431346415 56.97787143782138">
<animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="1s" values="0 50 50;360 50 50" keyTimes="0;1"></animateTransform>
</circle>
</svg>

After

Width:  |  Height:  |  Size: 596 B

22
frontend/src/app/framework/angular/loader.component.scss

@ -0,0 +1,22 @@
@import 'mixins';
@import 'vars';
:host {
display: inline-block;
}
.theme {
stroke: $color-theme-brand;
}
.white {
stroke: $color-white;
}
.input {
stroke: $color-input;
}
.text {
stroke: $color-text;
}

24
frontend/src/app/framework/angular/loader.component.ts

@ -0,0 +1,24 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
/* eslint-disable import/no-cycle */
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
@Component({
selector: 'sqx-loader',
styleUrls: ['./loader.component.scss'],
templateUrl: './loader.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LoaderComponent {
@Input()
public size = 18;
@Input()
public color: 'input' | 'theme' | 'white' | 'text' = 'text';
}

82
frontend/src/app/framework/angular/loader.stories.ts

@ -0,0 +1,82 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { moduleMetadata } from '@storybook/angular';
import { Meta, Story } from '@storybook/angular/types-6-0';
import { LoaderComponent, SqxFrameworkModule } from '@app/framework';
export default {
title: 'Framework/Loader',
component: LoaderComponent,
argTypes: {
size: {
control: 'number',
},
color: {
control: 'enum',
options: [
'white',
'theme',
'text',
],
},
},
decorators: [
moduleMetadata({
imports: [
SqxFrameworkModule,
SqxFrameworkModule.forRoot(),
],
}),
],
} as Meta;
const Template: Story<LoaderComponent> = (args: LoaderComponent) => ({
props: args,
});
export const ColorWhite = Template.bind({});
ColorWhite.args = {
color: 'white',
};
export const ColorTheme = Template.bind({});
ColorTheme.args = {
color: 'theme',
};
export const ColorText = Template.bind({});
ColorText.args = {
color: 'text',
};
export const ColorInput = Template.bind({});
ColorInput.args = {
color: 'input',
};
export const SizeSmall = Template.bind({});
SizeSmall.args = {
size: 16,
};
export const SizeMedium = Template.bind({});
SizeMedium.args = {
size: 32,
};
export const SizeLarge = Template.bind({});
SizeLarge.args = {
size: 64,
};

11
frontend/src/app/framework/angular/modals/modal-placement.directive.ts

@ -14,6 +14,7 @@ import { AnchorX, AnchorY, computeAnchors, positionModal, PositionRequest, Relat
})
export class ModalPlacementDirective extends ResourceOwner implements AfterViewInit, OnDestroy {
private targetElement?: Element;
private isViewInit = false;
@Input('sqxAnchoredTo')
public set target(element: Element) {
@ -26,7 +27,9 @@ export class ModalPlacementDirective extends ResourceOwner implements AfterViewI
this.listenToElement(element);
}
this.updatePosition();
if (this.isViewInit) {
this.updatePosition();
}
}
}
@ -79,6 +82,8 @@ export class ModalPlacementDirective extends ResourceOwner implements AfterViewI
private readonly element: ElementRef<HTMLElement>,
) {
super();
renderer.setStyle(element.nativeElement, 'visibility', 'hidden');
}
private listenToElement(element: any) {
@ -110,6 +115,8 @@ export class ModalPlacementDirective extends ResourceOwner implements AfterViewI
}
this.updatePosition();
this.isViewInit = true;
}
private updatePosition() {
@ -186,5 +193,7 @@ export class ModalPlacementDirective extends ResourceOwner implements AfterViewI
if (position.y) {
this.renderer.setStyle(modalRef, 'top', `${position.y}px`);
}
this.renderer.setStyle(modalRef, 'visibility', 'visible');
}
}

1
frontend/src/app/framework/declarations.ts

@ -47,6 +47,7 @@ export * from './angular/image-source.directive';
export * from './angular/image-url.directive';
export * from './angular/if-once.directive';
export * from './angular/language-selector.component';
export * from './angular/loader.component';
export * from './angular/layout-container.directive';
export * from './angular/layout.component';
export * from './angular/list-view.component';

4
frontend/src/app/framework/module.ts

@ -11,7 +11,7 @@ import { ModuleWithProviders, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { ColorPickerModule } from 'ngx-color-picker';
import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterceptor, CanDeactivateGuard, CheckboxGroupComponent, ClipboardService, CodeComponent, CodeEditorComponent, ColorPickerComponent, CompensateScrollbarDirective, ConfirmClickDirective, ControlErrorsComponent, ControlErrorsMessagesComponent, CopyDirective, DarkenPipe, DatePipe, DateTimeEditorComponent, DayOfWeekPipe, DayPipe, DialogRendererComponent, DialogService, DisplayNamePipe, DropdownComponent, DropdownMenuComponent, DurationPipe, EditableTitleComponent, ExternalLinkDirective, FileDropDirective, FileSizePipe, FocusOnInitDirective, FormAlertComponent, FormErrorComponent, FormHintComponent, FromNowPipe, FullDateTimePipe, HighlightPipe, HoverBackgroundDirective, IfOnceDirective, ImageSourceDirective, ImageUrlDirective, IndeterminateValueDirective, ISODatePipe, JoinPipe, KeysPipe, KNumberPipe, LanguageSelectorComponent, LayoutComponent, LayoutContainerDirective, LightenPipe, ListViewComponent, LoadingInterceptor, LoadingService, LocalizedInputComponent, LocalStoreService, MarkdownDirective, MarkdownInlinePipe, MarkdownPipe, MessageBus, ModalDialogComponent, ModalDirective, ModalPlacementDirective, MonthPipe, OnboardingService, OnboardingTooltipComponent, PagerComponent, ParentLinkDirective, ProgressBarComponent, ResizedDirective, ResizeService, ResourceLoaderService, RootViewComponent, SafeHtmlPipe, SafeResourceUrlPipe, SafeUrlPipe, ScrollActiveDirective, ShortcutComponent, ShortcutDirective, ShortcutService, ShortDatePipe, ShortTimePipe, StarsComponent, StatusIconComponent, StopClickDirective, StopDragDirective, SyncScollingDirective, SyncWidthDirective, TabRouterlinkDirective, TagEditorComponent, TemplateWrapperDirective, TempService, TitleComponent, TitleService, ToggleComponent, ToolbarComponent, TooltipDirective, TransformInputDirective, TranslatePipe, VideoPlayerComponent } from './declarations';
import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterceptor, CanDeactivateGuard, CheckboxGroupComponent, ClipboardService, CodeComponent, CodeEditorComponent, ColorPickerComponent, CompensateScrollbarDirective, ConfirmClickDirective, ControlErrorsComponent, ControlErrorsMessagesComponent, CopyDirective, DarkenPipe, DatePipe, DateTimeEditorComponent, DayOfWeekPipe, DayPipe, DialogRendererComponent, DialogService, DisplayNamePipe, DropdownComponent, DropdownMenuComponent, DurationPipe, EditableTitleComponent, ExternalLinkDirective, FileDropDirective, FileSizePipe, FocusOnInitDirective, FormAlertComponent, FormErrorComponent, FormHintComponent, FromNowPipe, FullDateTimePipe, HighlightPipe, HoverBackgroundDirective, IfOnceDirective, ImageSourceDirective, ImageUrlDirective, IndeterminateValueDirective, ISODatePipe, JoinPipe, KeysPipe, KNumberPipe, LanguageSelectorComponent, LayoutComponent, LayoutContainerDirective, LightenPipe, ListViewComponent, LoaderComponent, LoadingInterceptor, LoadingService, LocalizedInputComponent, LocalStoreService, MarkdownDirective, MarkdownInlinePipe, MarkdownPipe, MessageBus, ModalDialogComponent, ModalDirective, ModalPlacementDirective, MonthPipe, OnboardingService, OnboardingTooltipComponent, PagerComponent, ParentLinkDirective, ProgressBarComponent, ResizedDirective, ResizeService, ResourceLoaderService, RootViewComponent, SafeHtmlPipe, SafeResourceUrlPipe, SafeUrlPipe, ScrollActiveDirective, ShortcutComponent, ShortcutDirective, ShortcutService, ShortDatePipe, ShortTimePipe, StarsComponent, StatusIconComponent, StopClickDirective, StopDragDirective, SyncScollingDirective, SyncWidthDirective, TabRouterlinkDirective, TagEditorComponent, TemplateWrapperDirective, TempService, TitleComponent, TitleService, ToggleComponent, ToolbarComponent, TooltipDirective, TransformInputDirective, TranslatePipe, VideoPlayerComponent } from './declarations';
@NgModule({
imports: [
@ -68,6 +68,7 @@ import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterc
LayoutContainerDirective,
LightenPipe,
ListViewComponent,
LoaderComponent,
LocalizedInputComponent,
MarkdownDirective,
MarkdownInlinePipe,
@ -155,6 +156,7 @@ import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterc
LayoutContainerDirective,
LightenPipe,
ListViewComponent,
LoaderComponent,
LocalizedInputComponent,
MarkdownDirective,
MarkdownInlinePipe,

2
frontend/src/app/shared/components/assets/asset-folder-dropdown-item.component.html

@ -2,7 +2,7 @@
<ng-container *ngIf="node.isLoading; else notLoading" class="loader">
<button type="button" class="btn btn-sm btn-decent btn-text-secondary">
<i class="icon-spinner2 spin2"></i>
<sqx-loader></sqx-loader>>
</button>
</ng-container>

5
frontend/src/app/shell/pages/internal/logo.component.html

@ -1,3 +1,6 @@
<img [class.hidden]="!isLoading" class="loader" src="./images/loader-white.svg" />
<div [class.hidden]="!isLoading" class="loader" >
<sqx-loader color="white" [size]="28"></sqx-loader>
</div>
<i [class.hidden]="isLoading" class="icon-logo"></i>

5
frontend/src/app/shell/pages/internal/logo.component.scss

@ -5,5 +5,8 @@
@include absolute(50%, null, null, 50%);
margin-left: -14px;
margin-top: -14px;
width: 28px;
sqx-loader {
display: block;
}
}

53
frontend/src/app/theme/_common.scss

@ -346,57 +346,4 @@ hr {
hr {
margin: .5rem 0;
}
}
//
// Animations
//
.spin {
animation: spin 3s infinite linear;
}
.spin2 {
animation: spin2 1s infinite linear;
}
i {
&.spin {
display: inline-block;
}
&.spin2 {
display: inline-block;
}
}
@keyframes spin2 {
50% {
transform: rotate(180deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes spin {
20% {
transform: rotate(0deg);
}
30% {
transform: rotate(180deg);
}
70% {
transform: rotate(180deg);
}
80% {
transform: rotate(360deg);
}
100% {
transform: rotate(360deg);
}
}
Loading…
Cancel
Save