Browse Source

Menu (#1275)

* Menu

* Move to components.
pull/1276/head
Sebastian Stehle 9 months ago
committed by GitHub
parent
commit
239884275d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 3
      backend/i18n/frontend_de.json
  2. 3
      backend/i18n/frontend_en.json
  3. 3
      backend/i18n/frontend_fr.json
  4. 3
      backend/i18n/frontend_it.json
  5. 3
      backend/i18n/frontend_nl.json
  6. 3
      backend/i18n/frontend_pt.json
  7. 3
      backend/i18n/frontend_zh.json
  8. 3
      backend/i18n/source/frontend_en.json
  9. 1
      frontend/.storybook/main.js
  10. 51
      frontend/src/app/features/content/shared/forms/content-field.component.html
  11. 11
      frontend/src/app/features/content/shared/forms/content-field.component.ts
  12. 84
      frontend/src/app/features/content/shared/forms/field-editor.component.html
  13. 19
      frontend/src/app/features/content/shared/forms/field-editor.component.scss
  14. 17
      frontend/src/app/features/content/shared/forms/field-editor.component.ts
  15. 5
      frontend/src/app/framework/angular/forms/confirm-click.directive.ts
  16. 30
      frontend/src/app/framework/angular/menu-item.component.html
  17. 8
      frontend/src/app/framework/angular/menu-item.component.scss
  18. 65
      frontend/src/app/framework/angular/menu-item.component.ts
  19. 29
      frontend/src/app/framework/angular/menu.component.html
  20. 24
      frontend/src/app/framework/angular/menu.component.scss
  21. 87
      frontend/src/app/framework/angular/menu.component.ts
  22. 130
      frontend/src/app/framework/angular/menu.stories.ts
  23. 2
      frontend/src/app/framework/index.ts

3
backend/i18n/frontend_de.json

@ -270,6 +270,7 @@
"common.event": "Ereignis", "common.event": "Ereignis",
"common.events": "Ereignisse", "common.events": "Ereignisse",
"common.executed": "Ausgeführt", "common.executed": "Ausgeführt",
"common.expand": "Expand",
"common.expertMode": "Expertenmodus", "common.expertMode": "Expertenmodus",
"common.extension": "Erweiterung", "common.extension": "Erweiterung",
"common.failed": "Fehlgeschlagen", "common.failed": "Fehlgeschlagen",
@ -465,7 +466,9 @@
"contents.draftNew": "Neuer Entwurf", "contents.draftNew": "Neuer Entwurf",
"contents.draftStatus": "Neue Version", "contents.draftStatus": "Neue Version",
"contents.editPageTitle": "Inhalt bearbeiten", "contents.editPageTitle": "Inhalt bearbeiten",
"contents.fieldAIMenu": "Generate with AI",
"contents.fieldFullscreen": "Auf dieses Feld fokussieren und Vollbildmodus umschalten.", "contents.fieldFullscreen": "Auf dieses Feld fokussieren und Vollbildmodus umschalten.",
"contents.fieldFullscreenMenu": "Fullscreen mode",
"contents.idPlaceholder": "Definieren Sie eine benutzerdefinierte ID oder lassen Sie das Feld leer, damit Squidex eine generiert.", "contents.idPlaceholder": "Definieren Sie eine benutzerdefinierte ID oder lassen Sie das Feld leer, damit Squidex eine generiert.",
"contents.inspectContent": "Inhalt", "contents.inspectContent": "Inhalt",
"contents.inspectData": "Daten", "contents.inspectData": "Daten",

3
backend/i18n/frontend_en.json

@ -270,6 +270,7 @@
"common.event": "Event", "common.event": "Event",
"common.events": "Events", "common.events": "Events",
"common.executed": "Executed", "common.executed": "Executed",
"common.expand": "Expand",
"common.expertMode": "Expert Mode", "common.expertMode": "Expert Mode",
"common.extension": "Extension", "common.extension": "Extension",
"common.failed": "Failed", "common.failed": "Failed",
@ -465,7 +466,9 @@
"contents.draftNew": "New Draft", "contents.draftNew": "New Draft",
"contents.draftStatus": "New Version", "contents.draftStatus": "New Version",
"contents.editPageTitle": "Edit Content", "contents.editPageTitle": "Edit Content",
"contents.fieldAIMenu": "Generate with AI",
"contents.fieldFullscreen": "Focus on this field and toggle fullscreen mode.", "contents.fieldFullscreen": "Focus on this field and toggle fullscreen mode.",
"contents.fieldFullscreenMenu": "Fullscreen mode",
"contents.idPlaceholder": "Define a custom ID or leave empty to let Squidex generate one.", "contents.idPlaceholder": "Define a custom ID or leave empty to let Squidex generate one.",
"contents.inspectContent": "Content", "contents.inspectContent": "Content",
"contents.inspectData": "Data", "contents.inspectData": "Data",

3
backend/i18n/frontend_fr.json

@ -270,6 +270,7 @@
"common.event": "Événement", "common.event": "Événement",
"common.events": "Événements", "common.events": "Événements",
"common.executed": "Réalisé", "common.executed": "Réalisé",
"common.expand": "Expand",
"common.expertMode": "Mode expert", "common.expertMode": "Mode expert",
"common.extension": "Extension", "common.extension": "Extension",
"common.failed": "Échoué", "common.failed": "Échoué",
@ -465,7 +466,9 @@
"contents.draftNew": "Nouveau brouillon", "contents.draftNew": "Nouveau brouillon",
"contents.draftStatus": "Nouvelle version", "contents.draftStatus": "Nouvelle version",
"contents.editPageTitle": "Modifier le contenu", "contents.editPageTitle": "Modifier le contenu",
"contents.fieldAIMenu": "Generate with AI",
"contents.fieldFullscreen": "Concentrez-vous sur ce champ et basculez en mode plein écran.", "contents.fieldFullscreen": "Concentrez-vous sur ce champ et basculez en mode plein écran.",
"contents.fieldFullscreenMenu": "Fullscreen mode",
"contents.idPlaceholder": "Définissez un ID personnalisé ou laissez vide pour laisser Squidex en générer un.", "contents.idPlaceholder": "Définissez un ID personnalisé ou laissez vide pour laisser Squidex en générer un.",
"contents.inspectContent": "Contenu", "contents.inspectContent": "Contenu",
"contents.inspectData": "Données", "contents.inspectData": "Données",

3
backend/i18n/frontend_it.json

@ -270,6 +270,7 @@
"common.event": "Evento", "common.event": "Evento",
"common.events": "Eventi", "common.events": "Eventi",
"common.executed": "Eseguito", "common.executed": "Eseguito",
"common.expand": "Expand",
"common.expertMode": "Modalità esperto", "common.expertMode": "Modalità esperto",
"common.extension": "Extension", "common.extension": "Extension",
"common.failed": "Fallito", "common.failed": "Fallito",
@ -465,7 +466,9 @@
"contents.draftNew": "Nuova bozza", "contents.draftNew": "Nuova bozza",
"contents.draftStatus": "Nuova versione", "contents.draftStatus": "Nuova versione",
"contents.editPageTitle": "Modifica contenuto", "contents.editPageTitle": "Modifica contenuto",
"contents.fieldAIMenu": "Generate with AI",
"contents.fieldFullscreen": "Focus on this field and toggle fullscreen mode.", "contents.fieldFullscreen": "Focus on this field and toggle fullscreen mode.",
"contents.fieldFullscreenMenu": "Fullscreen mode",
"contents.idPlaceholder": "Define a custom ID or leave empty to let Squidex generate one.", "contents.idPlaceholder": "Define a custom ID or leave empty to let Squidex generate one.",
"contents.inspectContent": "Content", "contents.inspectContent": "Content",
"contents.inspectData": "Data", "contents.inspectData": "Data",

3
backend/i18n/frontend_nl.json

@ -270,6 +270,7 @@
"common.event": "Evenement", "common.event": "Evenement",
"common.events": "Evenementen", "common.events": "Evenementen",
"common.executed": "Uitgevoerd", "common.executed": "Uitgevoerd",
"common.expand": "Expand",
"common.expertMode": "Expert-modus", "common.expertMode": "Expert-modus",
"common.extension": "Uitbreiding", "common.extension": "Uitbreiding",
"common.failed": "Mislukt", "common.failed": "Mislukt",
@ -465,7 +466,9 @@
"contents.draftNew": "Nieuw concept", "contents.draftNew": "Nieuw concept",
"contents.draftStatus": "Nieuwe versie", "contents.draftStatus": "Nieuwe versie",
"contents.editPageTitle": "Inhoud bewerken", "contents.editPageTitle": "Inhoud bewerken",
"contents.fieldAIMenu": "Generate with AI",
"contents.fieldFullscreen": "Focus on this field and toggle fullscreen mode.", "contents.fieldFullscreen": "Focus on this field and toggle fullscreen mode.",
"contents.fieldFullscreenMenu": "Fullscreen mode",
"contents.idPlaceholder": "Definieer een aangepaste ID of laat leeg om Squidex er een te laten genereren.", "contents.idPlaceholder": "Definieer een aangepaste ID of laat leeg om Squidex er een te laten genereren.",
"contents.inspectContent": "Inhoud", "contents.inspectContent": "Inhoud",
"contents.inspectData": "Data", "contents.inspectData": "Data",

3
backend/i18n/frontend_pt.json

@ -270,6 +270,7 @@
"common.event": "Evento", "common.event": "Evento",
"common.events": "Eventos", "common.events": "Eventos",
"common.executed": "Executado", "common.executed": "Executado",
"common.expand": "Expand",
"common.expertMode": "Modo Especialista", "common.expertMode": "Modo Especialista",
"common.extension": "Extensão", "common.extension": "Extensão",
"common.failed": "Falhou", "common.failed": "Falhou",
@ -465,7 +466,9 @@
"contents.draftNew": "Novo Esboço", "contents.draftNew": "Novo Esboço",
"contents.draftStatus": "Nova versão", "contents.draftStatus": "Nova versão",
"contents.editPageTitle": "Editar Conteúdo", "contents.editPageTitle": "Editar Conteúdo",
"contents.fieldAIMenu": "Generate with AI",
"contents.fieldFullscreen": "Concentre-se neste campo e altere o modo de ecrã completo.", "contents.fieldFullscreen": "Concentre-se neste campo e altere o modo de ecrã completo.",
"contents.fieldFullscreenMenu": "Fullscreen mode",
"contents.idPlaceholder": "Defina um ID personalizado ou deixe vazio para permitir que o Squidex gere um.", "contents.idPlaceholder": "Defina um ID personalizado ou deixe vazio para permitir que o Squidex gere um.",
"contents.inspectContent": "Conteúdo", "contents.inspectContent": "Conteúdo",
"contents.inspectData": "Dados", "contents.inspectData": "Dados",

3
backend/i18n/frontend_zh.json

@ -270,6 +270,7 @@
"common.event": "事件", "common.event": "事件",
"common.events": "事件", "common.events": "事件",
"common.executed": "已执行", "common.executed": "已执行",
"common.expand": "Expand",
"common.expertMode": "专家模式", "common.expertMode": "专家模式",
"common.extension": "扩展名", "common.extension": "扩展名",
"common.failed": "失败", "common.failed": "失败",
@ -465,7 +466,9 @@
"contents.draftNew": "新草稿", "contents.draftNew": "新草稿",
"contents.draftStatus": "新版本", "contents.draftStatus": "新版本",
"contents.editPageTitle": "编辑内容", "contents.editPageTitle": "编辑内容",
"contents.fieldAIMenu": "Generate with AI",
"contents.fieldFullscreen": "Focus on this field and toggle fullscreen mode.", "contents.fieldFullscreen": "Focus on this field and toggle fullscreen mode.",
"contents.fieldFullscreenMenu": "Fullscreen mode",
"contents.idPlaceholder": "Define a custom ID or leave empty to let Squidex generate one.", "contents.idPlaceholder": "Define a custom ID or leave empty to let Squidex generate one.",
"contents.inspectContent": "Content", "contents.inspectContent": "Content",
"contents.inspectData": "Data", "contents.inspectData": "Data",

3
backend/i18n/source/frontend_en.json

@ -270,6 +270,7 @@
"common.event": "Event", "common.event": "Event",
"common.events": "Events", "common.events": "Events",
"common.executed": "Executed", "common.executed": "Executed",
"common.expand": "Expand",
"common.expertMode": "Expert Mode", "common.expertMode": "Expert Mode",
"common.extension": "Extension", "common.extension": "Extension",
"common.failed": "Failed", "common.failed": "Failed",
@ -465,7 +466,9 @@
"contents.draftNew": "New Draft", "contents.draftNew": "New Draft",
"contents.draftStatus": "New Version", "contents.draftStatus": "New Version",
"contents.editPageTitle": "Edit Content", "contents.editPageTitle": "Edit Content",
"contents.fieldAIMenu": "Generate with AI",
"contents.fieldFullscreen": "Focus on this field and toggle fullscreen mode.", "contents.fieldFullscreen": "Focus on this field and toggle fullscreen mode.",
"contents.fieldFullscreenMenu": "Fullscreen mode",
"contents.idPlaceholder": "Define a custom ID or leave empty to let Squidex generate one.", "contents.idPlaceholder": "Define a custom ID or leave empty to let Squidex generate one.",
"contents.inspectContent": "Content", "contents.inspectContent": "Content",
"contents.inspectData": "Data", "contents.inspectData": "Data",

1
frontend/.storybook/main.js

@ -21,6 +21,7 @@ class FilterSassWarningsPlugin {
module.exports = { module.exports = {
stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"], stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: [ addons: [
'@storybook/addon-actions',
"@storybook/addon-links", "@storybook/addon-links",
"@storybook/addon-essentials", "@storybook/addon-essentials",
"@storybook/addon-interactions" "@storybook/addon-interactions"

51
frontend/src/app/features/content/shared/forms/content-field.component.html

@ -14,21 +14,15 @@
[formLevel]="formLevel" [formLevel]="formLevel"
[formModel]="formModel.get(language)" [formModel]="formModel.get(language)"
[hasChatBot]="hasChatBot" [hasChatBot]="hasChatBot"
[isComparing]="!!formModelCompare"
[isCollapsed]="isCollapsed" [isCollapsed]="isCollapsed"
[isComparing]="!!formModelCompare"
[language]="language" [language]="language"
[languages]="languages"> [languages]="languages">
<ng-container beforeButtons> <ng-container>
@if (i === 0) { @if (i === 0) {
<ng-container *ngTemplateOutlet="sharedButtons" /> <ng-container *ngTemplateOutlet="sharedButtons" />
} }
</ng-container> </ng-container>
<ng-container afterButtons>
@if (i === 0) {
<ng-container *ngTemplateOutlet="toggleButton" />
}
</ng-container>
</sqx-field-editor> </sqx-field-editor>
</div> </div>
} }
@ -40,16 +34,13 @@
[formLevel]="formLevel" [formLevel]="formLevel"
[formModel]="getControl()" [formModel]="getControl()"
[hasChatBot]="hasChatBot" [hasChatBot]="hasChatBot"
[isComparing]="!!formModelCompare"
[isCollapsed]="isCollapsed" [isCollapsed]="isCollapsed"
[isComparing]="!!formModelCompare"
[language]="language" [language]="language"
[languages]="languages"> [languages]="languages">
<ng-container beforeButtons> <ng-container>
<ng-container *ngTemplateOutlet="sharedButtons" /> <ng-container *ngTemplateOutlet="sharedButtons" />
</ng-container> </ng-container>
<ng-container afterButtons>
<ng-container *ngTemplateOutlet="toggleButton" />
</ng-container>
</sqx-field-editor> </sqx-field-editor>
} }
</div> </div>
@ -81,8 +72,8 @@
[formLevel]="formLevel" [formLevel]="formLevel"
[formModel]="formModelCompare.get(language)" [formModel]="formModelCompare.get(language)"
[hasChatBot]="hasChatBot" [hasChatBot]="hasChatBot"
[isComparing]="!!formModelCompare"
[isCollapsed]="isCollapsed" [isCollapsed]="isCollapsed"
[isComparing]="!!formModelCompare"
[language]="language" [language]="language"
[languages]="languages" /> [languages]="languages" />
</div> </div>
@ -94,8 +85,8 @@
[formLevel]="formLevel" [formLevel]="formLevel"
[formModel]="getControlCompare()!" [formModel]="getControlCompare()!"
[hasChatBot]="hasChatBot" [hasChatBot]="hasChatBot"
[isComparing]="!!formModelCompare"
[isCollapsed]="isCollapsed" [isCollapsed]="isCollapsed"
[isComparing]="!!formModelCompare"
[language]="language" [language]="language"
[languages]="languages" /> [languages]="languages" />
} }
@ -113,23 +104,23 @@
[languages]="languages" [languages]="languages"
[showAllControls]="showAllControls" [showAllControls]="showAllControls"
(showAllControlsChange)="changeShowAllControls($event)" /> (showAllControlsChange)="changeShowAllControls($event)" />
<sqx-field-copy-button [formModel]="formModel" [languages]="languages" /> <sqx-field-copy-button [formModel]="formModel" [languages]="languages" />
@if (isTranslatable) { @if (isTranslatable) {
<button <sqx-menu-item
class="btn btn-sm btn-outline-secondary force no-focus-shadow" (action)="translate()"
(click)="translate()"
[disabled]="formModel.field.isDisabled" [disabled]="formModel.field.isDisabled"
tabindex="-1" icon="translate"
title="i18n:contents.autotranslate" menuLabel="i18n:contents.autotranslateMenu"
type="button"> small
<i class="icon-translate"></i> tabIndex="-1"
</button> tooltip="i18n:contents.autotranslate" />
} }
</ng-template>
<ng-template #toggleButton> @if (isCollapsed) {
<button class="btn btn-sm btn-square btn-outline-secondary" (click)="toggle()" type="button" <sqx-menu-item (action)="toggle()" icon="minus2" menuLabel="i18n:common.collapse" small tooltip="i18n:contents.arrayCollapseItem" />
title="i18n:contents.arrayCollapseItem"> } @else {
<i [class.icon-minus2]="!isCollapsed" [class.icon-plus2]="isCollapsed"></i> <sqx-menu-item (action)="toggle()" icon="plus2" menuLabel="i18n:common.expand" small tooltip="i18n:contents.arrayCollapseItem" />
</button> }
</ng-template> </ng-template>

11
frontend/src/app/features/content/shared/forms/content-field.component.ts

@ -8,7 +8,7 @@
import { AsyncPipe, NgTemplateOutlet } from '@angular/common'; import { AsyncPipe, NgTemplateOutlet } from '@angular/common';
import { booleanAttribute, Component, EventEmitter, HostBinding, inject, Input, numberAttribute, Optional, Output } from '@angular/core'; import { booleanAttribute, Component, EventEmitter, HostBinding, inject, Input, numberAttribute, Optional, Output } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { AppLanguageDto, AppsState, changed$, CommentsState, disabled$, EditContentForm, FieldForm, FocusMarkerComponent, invalid$, LocalStoreService, SchemaDto, Settings, TooltipDirective, TranslateDto, TranslationsService, TypedSimpleChanges, UIOptions } from '@app/shared'; import { AppLanguageDto, AppsState, changed$, CommentsState, disabled$, EditContentForm, FieldForm, FocusMarkerComponent, invalid$, LocalStoreService, MenuItemComponent, SchemaDto, Settings, TranslateDto, TranslationsService, TypedSimpleChanges, UIOptions } from '@app/shared';
import { FieldCopyButtonComponent } from './field-copy-button.component'; import { FieldCopyButtonComponent } from './field-copy-button.component';
import { FieldEditorComponent } from './field-editor.component'; import { FieldEditorComponent } from './field-editor.component';
import { FieldLanguagesComponent } from './field-languages.component'; import { FieldLanguagesComponent } from './field-languages.component';
@ -23,8 +23,8 @@ import { FieldLanguagesComponent } from './field-languages.component';
FieldEditorComponent, FieldEditorComponent,
FieldLanguagesComponent, FieldLanguagesComponent,
FocusMarkerComponent, FocusMarkerComponent,
MenuItemComponent,
NgTemplateOutlet, NgTemplateOutlet,
TooltipDirective,
], ],
}) })
export class ContentFieldComponent { export class ContentFieldComponent {
@ -80,10 +80,13 @@ export class ContentFieldComponent {
return this.formModel.field.properties.isHalfWidth && !this.isCompact && !this.formCompare; return this.formModel.field.properties.isHalfWidth && !this.isCompact && !this.formCompare;
} }
public get isTranslatable() { public get isLocalizable() {
return this.formModel.field.properties.fieldType === 'String' && this.hasTranslator && this.formModel.field.isLocalizable && this.languages.length > 1; return this.formModel.field.isLocalizable;
} }
public get isTranslatable() {
return this.formModel.field.properties.fieldType === 'String' && this.hasTranslator && this.isLocalizable && this.languages.length > 1;
}
constructor( constructor(
@Optional() public readonly commentsState: CommentsState, @Optional() public readonly commentsState: CommentsState,
private readonly appsState: AppsState, private readonly appsState: AppsState,

84
frontend/src/app/features/content/shared/forms/field-editor.component.html

@ -1,53 +1,55 @@
@if (formModel) { @if (formModel) {
<div class="field" [class.expanded]="isExpanded && !isCollapsed"> <div class="field" [class.expanded]="isExpanded && !isCollapsed">
<div class="buttons-container"> <div class="row">
<div class="buttons d-flex gap-1"> <div class="col-4">
<fieldset class="d-flex gap-1" [disabled]="(isDisabled | async) || isCollapsed"> <div class="truncate">
<ng-content select="[beforeButtons]"></ng-content> <label class="mb-1">
{{ field.displayName }} {{ displaySuffix }} <span class="field-required" [class.hidden]="!field.properties.isRequired">*</span>
</label>
@if (field.isDisabled) {
<small class="field-disabled ps-1">Disabled</small>
}
</div>
</div>
<div class="col-8 buttons">
<sqx-menu alignment="right" small>
<fieldset class="d-flex gap-1" [disabled]="(isDisabled | async) || isCollapsed">
<ng-content></ng-content>
</fieldset>
@if (isString) { @if (isString) {
<button <sqx-menu-item
class="btn btn-sm btn-outline-secondary force no-focus-shadow" [disabled]="(isDisabled | async) || isCollapsed || !hasChatBot"
(click)="chatDialog.show()" label="AI"
[disabled]="!hasChatBot || isCollapsed" menuLabel="i18n:contents.fieldAIMenu"
tabindex="-1" small
type="button"> tabIndex="-1" />
AI
</button>
} }
<button <sqx-menu-item
class="btn btn-sm btn-outline-secondary force no-focus-shadow" (action)="this.toggleExpanded()"
(click)="toggleExpanded()" [disabled]="(isDisabled | async) || isCollapsed"
[disabled]="isCollapsed" icon="fullscreen"
tabindex="-1" menuLabel="i18n:contents.fieldFullscreenMenu"
title="i18n:contents.fieldFullscreen" small
type="button"> tabIndex="-1"
<i class="icon-fullscreen"></i> tooltip="i18n:contents.fieldFullscreen" />
</button>
<button <sqx-menu-item
class="btn btn-sm btn-outline-secondary btn-clear force no-focus-shadow" (action)="this.unset()"
confirmRememberKey="unsetValue" confirmRememberKey="unsetValue"
confirmText="i18n:contents.unsetValueConfirmText" confirmText="i18n:contents.unsetValueConfirmText"
confirmTitle="i18n:contents.unsetValueConfirmTitle" confirmTitle="i18n:contents.unsetValueConfirmTitle"
[disabled]="isEmpty | async" [disabled]="(isDisabled | async) || isCollapsed || (isEmpty | async)"
(sqxConfirmClick)="unset()" icon="close"
tabindex="-1" menuLabel="i18n:contents.unsetValue"
title="i18n:contents.unsetValue" small
type="button"> tabIndex="-1"
<i class="icon-close"></i> tooltip="i18n:contents.unsetValue" />
</button> </sqx-menu>
</fieldset>
<ng-content select="[afterButtons]"></ng-content>
</div> </div>
</div> </div>
<label class="mb-1">
{{ field.displayName }} {{ displaySuffix }} <span class="field-required" [class.hidden]="!field.properties.isRequired">*</span>
</label>
@if (field.isDisabled) {
<small class="field-disabled ps-1">Disabled</small>
}
@if (form) { @if (form) {
<sqx-control-errors [fieldName]="field.displayName" [for]="$any(fieldForm)" /> <sqx-control-errors [fieldName]="field.displayName" [for]="$any(fieldForm)" />

19
frontend/src/app/features/content/shared/forms/field-editor.component.scss

@ -27,22 +27,11 @@
} }
.buttons { .buttons {
& { margin: 0;
@include absolute(100%, 0); margin-top: -9px;
margin: 0;
margin-top: -9px;
}
&-container {
position: relative;
}
.btn-clear i {
font-size: $font-smallest;
}
.btn { sqx-menu {
width: 2rem; max-width: 100%;
} }
} }

17
frontend/src/app/features/content/shared/forms/field-editor.component.ts

@ -9,7 +9,7 @@ import { AsyncPipe } from '@angular/common';
import { booleanAttribute, Component, ElementRef, EventEmitter, Input, numberAttribute, Output, ViewChild } from '@angular/core'; import { booleanAttribute, Component, ElementRef, EventEmitter, Input, numberAttribute, Output, ViewChild } from '@angular/core';
import { AbstractControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { AbstractControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { AbstractContentForm, AnnotationCreate, AnnotationsSelect, AnyFieldDto, AppLanguageDto, ChatDialogComponent, CheckboxGroupComponent, CodeEditorComponent, ColorPickerComponent, CommentsState, ConfirmClickDirective, ControlErrorsComponent, DateTimeEditorComponent, DialogModel, disabled$, EditContentForm, FormHintComponent, GeolocationEditorComponent, hasNoValue$, HTTP, IndeterminateValueDirective, MarkdownDirective, MathHelper, MessageBus, ModalDirective, RadioGroupComponent, ReferenceInputComponent, ReferencesFieldPropertiesDto, RichEditorComponent, StarsComponent, TagEditorComponent, ToggleComponent, TooltipDirective, TransformInputDirective, TypedSimpleChanges, Types } from '@app/shared'; import { AbstractContentForm, AnnotationCreate, AnnotationsSelect, AnyFieldDto, AppLanguageDto, ChatDialogComponent, CheckboxGroupComponent, CodeEditorComponent, ColorPickerComponent, CommentsState, ControlErrorsComponent, DateTimeEditorComponent, DialogModel, disabled$, EditContentForm, FormHintComponent, GeolocationEditorComponent, hasNoValue$, HTTP, IndeterminateValueDirective, MarkdownDirective, MathHelper, MenuComponent, MenuItemComponent, MessageBus, ModalDirective, RadioGroupComponent, ReferenceInputComponent, ReferencesFieldPropertiesDto, RichEditorComponent, StarsComponent, TagEditorComponent, ToggleComponent, TransformInputDirective, TypedSimpleChanges, Types } from '@app/shared';
import { ReferenceDropdownComponent } from '../references/reference-dropdown.component'; import { ReferenceDropdownComponent } from '../references/reference-dropdown.component';
import { ReferencesCheckboxesComponent } from '../references/references-checkboxes.component'; import { ReferencesCheckboxesComponent } from '../references/references-checkboxes.component';
import { ReferencesEditorComponent } from '../references/references-editor.component'; import { ReferencesEditorComponent } from '../references/references-editor.component';
@ -34,7 +34,6 @@ import { StockPhotoEditorComponent } from './stock-photo-editor.component';
CodeEditorComponent, CodeEditorComponent,
ColorPickerComponent, ColorPickerComponent,
ComponentComponent, ComponentComponent,
ConfirmClickDirective,
ControlErrorsComponent, ControlErrorsComponent,
DateTimeEditorComponent, DateTimeEditorComponent,
FormHintComponent, FormHintComponent,
@ -44,6 +43,8 @@ import { StockPhotoEditorComponent } from './stock-photo-editor.component';
IndeterminateValueDirective, IndeterminateValueDirective,
MarkdownDirective, MarkdownDirective,
ModalDirective, ModalDirective,
MenuComponent,
MenuItemComponent,
RadioGroupComponent, RadioGroupComponent,
ReactiveFormsModule, ReactiveFormsModule,
ReferenceDropdownComponent, ReferenceDropdownComponent,
@ -57,7 +58,6 @@ import { StockPhotoEditorComponent } from './stock-photo-editor.component';
StockPhotoEditorComponent, StockPhotoEditorComponent,
TagEditorComponent, TagEditorComponent,
ToggleComponent, ToggleComponent,
TooltipDirective,
TransformInputDirective, TransformInputDirective,
], ],
}) })
@ -106,12 +106,10 @@ export class FieldEditorComponent {
@ViewChild('editor', { static: false }) @ViewChild('editor', { static: false })
public editor!: ElementRef; public editor!: ElementRef;
public isEmpty?: Observable<boolean>; public isDisabled!: Observable<boolean>;
public isEmpty!: Observable<boolean>;
public isExpanded = false; public isExpanded = false;
public isDisabled?: Observable<boolean>;
public chatDialog = new DialogModel(); public chatDialog = new DialogModel();
public annotations?: Observable<ReadonlyArray<Annotation>>; public annotations?: Observable<ReadonlyArray<Annotation>>;
public get field() { public get field() {
@ -123,7 +121,7 @@ export class FieldEditorComponent {
} }
public get isString() { public get isString() {
return this.field?.properties.fieldType === 'String'; return this.field.properties.fieldType === 'String';
} }
public get schemaIds() { public get schemaIds() {
@ -137,8 +135,8 @@ export class FieldEditorComponent {
public ngOnChanges(changes: TypedSimpleChanges<this>) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes.formModel) { if (changes.formModel) {
this.isEmpty = hasNoValue$(this.formModel.form);
this.isDisabled = disabled$(this.formModel.form); this.isDisabled = disabled$(this.formModel.form);
this.isEmpty = hasNoValue$(this.formModel.form);
} }
if (changes.formModel || changes.comments) { if (changes.formModel || changes.comments) {
@ -182,6 +180,7 @@ export class FieldEditorComponent {
public unset() { public unset() {
this.formModel.unset(); this.formModel.unset();
} }
public setValue(content: string | HTTP.UploadFile | null | undefined) { public setValue(content: string | HTTP.UploadFile | null | undefined) {
this.chatDialog.hide(); this.chatDialog.hide();

5
frontend/src/app/framework/angular/forms/confirm-click.directive.ts

@ -20,7 +20,7 @@ export class ConfirmClickDirective {
public confirmText: string | undefined | null; public confirmText: string | undefined | null;
@Input() @Input()
public confirmRememberKey = ''; public confirmRememberKey: string | undefined | null;
@Input({ transform: booleanAttribute }) @Input({ transform: booleanAttribute })
public confirmRequired?: boolean | null = true; public confirmRequired?: boolean | null = true;
@ -48,7 +48,8 @@ export class ConfirmClickDirective {
this.beforeClick.emit(); this.beforeClick.emit();
this.dialogs.confirm(this.confirmTitle, this.confirmText, this.confirmRememberKey).pipe(take(1)) const rememberKey = this.confirmRememberKey || '';
this.dialogs.confirm(this.confirmTitle, this.confirmText, rememberKey).pipe(take(1))
.subscribe(confirmed => { .subscribe(confirmed => {
if (confirmed) { if (confirmed) {
for (const destination of destinations) { for (const destination of destinations) {

30
frontend/src/app/framework/angular/menu-item.component.html

@ -0,0 +1,30 @@
<ng-template #dropdownTemplate>
<a
class="dropdown-item"
[attr.disabled]="disabled"
[class.disabled]="disabled"
[confirmRememberKey]="confirmRememberKey"
[confirmText]="confirmText | sqxTranslate"
[confirmTitle]="confirmTitle | sqxTranslate"
(sqxConfirmClick)="action.emit()"
[title]="tooltip | sqxTranslate">
{{ menuLabel || label | sqxTranslate }}
</a>
</ng-template>
<button
class="btn btn-outline-secondary"
[class.btn-sm]="small"
[confirmRememberKey]="confirmRememberKey"
[confirmText]="confirmText | sqxTranslate"
[confirmTitle]="confirmTitle | sqxTranslate"
[disabled]="disabled"
(sqxConfirmClick)="action.emit()"
[tabIndex]="tabIndex"
[title]="tooltip | sqxTranslate">
@if (icon) {
<i class="icon-{{ icon }}"></i>
}
{{ label | sqxTranslate }}
</button>

8
frontend/src/app/framework/angular/menu-item.component.scss

@ -0,0 +1,8 @@
@import 'mixins';
@import 'vars';
.btn-sm {
i {
font-size: $font-smallest;
}
}

65
frontend/src/app/framework/angular/menu-item.component.ts

@ -0,0 +1,65 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { booleanAttribute, ChangeDetectionStrategy, Component, EventEmitter, Input, numberAttribute, Output, TemplateRef, ViewChild } from '@angular/core';
import { ConfirmClickDirective } from './forms/confirm-click.directive';
import { TooltipDirective } from './modals/tooltip.directive';
import { TranslatePipe } from './pipes/translate.pipe';
@Component({
selector: 'sqx-menu-item',
styleUrls: ['./menu-item.component.scss'],
templateUrl: './menu-item.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
ConfirmClickDirective,
TooltipDirective,
TranslatePipe,
],
})
export class MenuItemComponent {
@Input()
public label = '';
@Input()
public menuLabel = '';
@Input()
public icon = '';
@Input()
public tooltip = '';
@Input({ transform: numberAttribute })
public tabIndex = -1;
@Input({ transform: booleanAttribute })
public disabled = false;
@Input({ transform: booleanAttribute })
public small = false;
@Input()
public confirmRememberKey = '';
@Input()
public confirmTitle = '';
@Input()
public confirmText = '';
@Output()
public action = new EventEmitter();
@ViewChild('dropdownTemplate', { static: true })
template!: TemplateRef<any>;
public get showInDropdown() {
return this.label || this.menuLabel;
}
}

29
frontend/src/app/framework/angular/menu.component.html

@ -0,0 +1,29 @@
<div class="d-flex flex-nowrap relative" #container [class.gap-1]="small" [class.gap-2]="!small" [class.justify-content-end]="isRightAligned">
@if (overflowMenuItems) {
<button
class="btn btn-text-secondary"
#buttonOptions
attr.aria-label="{{ 'common.options' | sqxTranslate }}"
[class.btn-sm]="small"
[class.order-1]="isRightAligned"
(click)="overflowDropdown.toggle()"
sqxStopClick
type="button">
<i class="icon-dots"></i>
</button>
<sqx-dropdown-menu
[position]="isRightAligned ? 'bottom-end' : 'bottom-start'"
scrollY="true"
[sqxAnchoredTo]="buttonOptions"
*sqxModal="overflowDropdown; closeAlways: true">
@for (item of overflowMenuItems; track item) {
<ng-container *ngTemplateOutlet="item.template"></ng-container>
}
</sqx-dropdown-menu>
}
<div class="d-flex flex-nowrap" #menu [class.gap-1]="small" [class.gap-2]="!small" [class.overflown-right]="!!overflowMenuItems && isRightAligned" [class.overflown-left]="!!overflowMenuItems && !isRightAligned">
<ng-content />
</div>
</div>

24
frontend/src/app/framework/angular/menu.component.scss

@ -0,0 +1,24 @@
@import 'mixins';
@import 'vars';
:host ::ng-deep {
.overflown-left {
sqx-menu-item {
order: 5;
visibility: hidden;
}
}
.overflown-right {
sqx-menu-item {
order: -5;
visibility: hidden;
}
}
}
.btn-sm {
i {
font-size: $font-smallest;
}
}

87
frontend/src/app/framework/angular/menu.component.ts

@ -0,0 +1,87 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { NgTemplateOutlet } from '@angular/common';
import { AfterViewInit, booleanAttribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChildren, ElementRef, Input, QueryList, ViewChild } from '@angular/core';
import { ModalModel, ResizeListener, ResizeService, Subscriptions } from '@app/framework/internal';
import { DropdownMenuComponent } from './dropdown-menu.component';
import { MenuItemComponent } from './menu-item.component';
import { ModalPlacementDirective } from './modals/modal-placement.directive';
import { ModalDirective } from './modals/modal.directive';
import { TranslatePipe } from './pipes/translate.pipe';
@Component({
selector: 'sqx-menu',
styleUrls: ['./menu.component.scss'],
templateUrl: './menu.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
DropdownMenuComponent,
ModalDirective,
ModalPlacementDirective,
NgTemplateOutlet,
TranslatePipe,
],
})
export class MenuComponent implements AfterViewInit, ResizeListener {
private readonly subscriptions = new Subscriptions();
private measuredContainer?: number;
private measuredMenu?: number;
@Input()
public alignment: 'left' | 'right' = 'left';
@Input({ transform: booleanAttribute })
public small = false;
@Input({ transform: booleanAttribute })
public showCustom = true;
@ViewChild('container', { static: true })
public container!: ElementRef<HTMLDivElement>;
@ViewChild('menu', { static: true })
public menu!: ElementRef<HTMLDivElement>;
@ContentChildren(MenuItemComponent)
public menuItems!: QueryList<MenuItemComponent>;
public overflowMenuItems: MenuItemComponent[] | null = null;
public overflowDropdown = new ModalModel();
public get isRightAligned() {
return this.alignment === 'right';
}
constructor(
private readonly resizeService: ResizeService,
private readonly changeDetector: ChangeDetectorRef,
) {
}
public ngAfterViewInit() {
this.subscriptions.add(this.resizeService.listen(this.container.nativeElement, this));
this.subscriptions.add(this.resizeService.listen(this.menu.nativeElement, this));
}
public onResize(rect: DOMRect, element: Element): void {
if (element === this.container.nativeElement) {
this.measuredContainer = rect.width;
} else {
this.measuredMenu = Math.max(rect.width, element.scrollWidth);
}
if (!this.measuredContainer || !this.measuredMenu) {
return;
}
const isOverlapping = this.measuredMenu > this.measuredContainer;
this.overflowMenuItems = isOverlapping ? this.menuItems.toArray().filter(x => x.showInDropdown) : null;
this.changeDetector.detectChanges();
}
}

130
frontend/src/app/framework/angular/menu.stories.ts

@ -0,0 +1,130 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { Meta, moduleMetadata, StoryObj } from '@storybook/angular';
import { LocalizerService, ResizeService } from '@app/framework/internal';
import { MenuItemComponent } from './menu-item.component';
import { MenuComponent } from './menu.component';
import { RootViewComponent } from './modals/root-view.component';
export default {
title: 'Framework/Menu',
component: MenuComponent,
args: {
small: false,
},
argTypes: {
alignment: {
control: 'radio',
options: [
'left',
'right',
],
},
small: {
control: 'boolean',
},
},
decorators: [
moduleMetadata({
imports: [
BrowserAnimationsModule,
MenuItemComponent,
RootViewComponent,
],
providers: [
{
provide: LocalizerService,
useFactory: () => new LocalizerService({}),
},
ResizeService,
],
}),
],
} as Meta;
type Story = StoryObj<MenuComponent>;
export const Simple: Story = {
render: args => ({
props: args,
template: `
<sqx-root-view>
<div style="width: 400px; padding: 10px; border: 1px solid #ddd;">
<sqx-menu [alignment]="alignment" [items]="items" [small]="small">
<sqx-menu-item label="Menu Item1" />
<sqx-menu-item label="Menu Item2" icon="close" />
<sqx-menu-item label="Menu Item3" disabled />
</sqx-menu>
</div>
</sqx-root-view>
`,
}),
};
export const Overlap: Story = {
render: args => ({
props: args,
template: `
<sqx-root-view>
<div style="width: 400px; padding: 10px; border: 1px solid #ddd;">
<sqx-menu [alignment]="alignment" [items]="items" [small]="small">
<sqx-menu-item label="Menu Item1" />
<sqx-menu-item label="Menu Item2" icon="close" />
<sqx-menu-item label="Menu Item3" disabled />
<sqx-menu-item label="Menu Item4" />
<sqx-menu-item label="Menu Item5" />
</sqx-menu>
</div>
</sqx-root-view>
`,
}),
};
export const Custom: Story = {
render: args => ({
props: args,
template: `
<sqx-root-view>
<div style="width: 500px; padding: 10px; border: 1px solid #ddd;">
<sqx-menu [alignment]="alignment" [items]="items" [small]="small">
<sqx-menu-item label="Menu Item1" />
<button class="btn btn-primary">Custom 1</button>
<button class="btn btn-primary">Custom 2</button>
<sqx-menu-item label="Menu Item2" />
</sqx-menu>
</div>
</sqx-root-view>
`,
}),
};
export const CustomOverlap: Story = {
render: args => ({
props: args,
template: `
<sqx-root-view>
<div style="width: 400px; padding: 10px; border: 1px solid #ddd;">
<sqx-menu [alignment]="alignment" [items]="items" [small]="small">
<sqx-menu-item label="Menu Item1" />
<sqx-menu-item label="Menu Item2" icon="close" />
<sqx-menu-item label="Menu Item3" disabled />
<button class="btn btn-primary">Custom 1</button>
<button class="btn btn-primary">Custom 2</button>
<sqx-menu-item label="Menu Item4" />
<sqx-menu-item label="Menu Item5" />
</sqx-menu>
</div>
</sqx-root-view>
`,
}),
};

2
frontend/src/app/framework/index.ts

@ -60,6 +60,8 @@ export * from './angular/list-view.component';
export * from './angular/loader.component'; export * from './angular/loader.component';
export * from './angular/long-hover.directive'; export * from './angular/long-hover.directive';
export * from './angular/markdown.directive'; export * from './angular/markdown.directive';
export * from './angular/menu.component';
export * from './angular/menu-item.component';
export * from './angular/modals/dialog-renderer.component'; export * from './angular/modals/dialog-renderer.component';
export * from './angular/modals/modal-dialog.component'; export * from './angular/modals/modal-dialog.component';
export * from './angular/modals/modal-placement.directive'; export * from './angular/modals/modal-placement.directive';

Loading…
Cancel
Save