Browse Source

Improve content editor.

pull/1039/head
Sebastian 3 years ago
parent
commit
323c9db353
  1. 4
      backend/i18n/frontend_en.json
  2. 4
      backend/i18n/frontend_it.json
  3. 4
      backend/i18n/frontend_nl.json
  4. 4
      backend/i18n/frontend_zh.json
  5. 4
      backend/i18n/source/frontend_en.json
  6. 2
      backend/src/Squidex/wwwroot/editor/squidex-editor.css
  7. 269
      backend/src/Squidex/wwwroot/editor/squidex-editor.js
  8. 14
      backend/src/Squidex/wwwroot/scripts/editor-dialogs.html
  9. 3
      backend/src/Squidex/wwwroot/scripts/editor-sdk.d.ts
  10. 9
      backend/src/Squidex/wwwroot/scripts/editor-sdk.js
  11. 13
      frontend/src/app/declarations.d.ts
  12. 1
      frontend/src/app/features/content/shared/forms/iframe-editor.component.html
  13. 8
      frontend/src/app/features/content/shared/forms/iframe-editor.component.ts
  14. 13
      frontend/src/app/shared/components/forms/rich-editor.component.ts
  15. 9
      frontend/src/app/shared/components/references/content-selector.component.ts

4
backend/i18n/frontend_en.json

@ -843,8 +843,8 @@
"schemas.field.hide": "Hide in API",
"schemas.field.hintsHint": "Describe this field for documentation and the UI.",
"schemas.field.inlineEditable": "Inline Editable",
"schemas.field.isEmbeddable": "Is embedding contents and assets",
"schemas.field.isEmbeddableHint": "With this option a custom format is returned in GraphQL, where the linked assets or contents can be fetched.",
"schemas.field.isEmbeddable": "Embed Contents and Assets",
"schemas.field.isEmbeddableHint": "With this option a custom output is used in GraphQL, and embedded assets and contents can be included.",
"schemas.field.labelHint": "Display name for documentation and the UI.",
"schemas.field.localizable": "Localizable",
"schemas.field.localizableHint": "You can mark the field as localizable. It means that is dependent on the language, for example a city name.",

4
backend/i18n/frontend_it.json

@ -843,8 +843,8 @@
"schemas.field.hide": "Nascondi nelle API",
"schemas.field.hintsHint": "Descrivi questo schema per la documentazione e le interfacce utente.",
"schemas.field.inlineEditable": "Modificabile sulla stessa linea",
"schemas.field.isEmbeddable": "Is embedding contents and assets",
"schemas.field.isEmbeddableHint": "With this option a custom format is returned in GraphQL, where the linked assets or contents can be fetched.",
"schemas.field.isEmbeddable": "Embed Contents and Assets",
"schemas.field.isEmbeddableHint": "With this option a custom output is used in GraphQL, and embedded assets and contents can be included.",
"schemas.field.labelHint": "Nome da visualizzare per la documentazione e le interfacce utente.",
"schemas.field.localizable": "Consente la localizzazione",
"schemas.field.localizableHint": "Puoi impostare il campo per consentire la localizzazione, ossia che dipende dalla lingua che utilizzi come ad esempio i nomi delle città.",

4
backend/i18n/frontend_nl.json

@ -843,8 +843,8 @@
"schemas.field.hide": "Verbergen in API",
"schemas.field.hintsHint": "Beschrijf dit schema voor documentatie en gebruikersinterfaces.",
"schemas.field.inlineEditable": "Inline bewerkbaar",
"schemas.field.isEmbeddable": "Is embedding contents and assets",
"schemas.field.isEmbeddableHint": "With this option a custom format is returned in GraphQL, where the linked assets or contents can be fetched.",
"schemas.field.isEmbeddable": "Embed Contents and Assets",
"schemas.field.isEmbeddableHint": "With this option a custom output is used in GraphQL, and embedded assets and contents can be included.",
"schemas.field.labelHint": "Weergavenaam voor documentatie en gebruikersinterfaces.",
"schemas.field.localizable": "Localizable",
"schemas.field.localizableHint": "Je kunt het veld markeren als lokaliseerbaar. Dit betekent dat het afhankelijk is van de taal, bijvoorbeeld de naam van een stad.",

4
backend/i18n/frontend_zh.json

@ -843,8 +843,8 @@
"schemas.field.hide": "隐藏在 API",
"schemas.field.hintsHint": "为文档和 UI 描述这个字段。",
"schemas.field.inlineEditable": "内联可编辑",
"schemas.field.isEmbeddable": "Is embedding contents and assets",
"schemas.field.isEmbeddableHint": "With this option a custom format is returned in GraphQL, where the linked assets or contents can be fetched.",
"schemas.field.isEmbeddable": "Embed Contents and Assets",
"schemas.field.isEmbeddableHint": "With this option a custom output is used in GraphQL, and embedded assets and contents can be included.",
"schemas.field.labelHint": "文档和 UI 的显示名称。",
"schemas.field.localizable": "Localizable",
"schemas.field.localizableHint": "您可以将字段标记为可本地化。这意味着这取决于语言,例如城市名称。",

4
backend/i18n/source/frontend_en.json

@ -843,8 +843,8 @@
"schemas.field.hide": "Hide in API",
"schemas.field.hintsHint": "Describe this field for documentation and the UI.",
"schemas.field.inlineEditable": "Inline Editable",
"schemas.field.isEmbeddable": "Is embedding contents and assets",
"schemas.field.isEmbeddableHint": "With this option a custom format is returned in GraphQL, where the linked assets or contents can be fetched.",
"schemas.field.isEmbeddable": "Embed Contents and Assets",
"schemas.field.isEmbeddableHint": "With this option a custom output is used in GraphQL, and embedded assets and contents can be included.",
"schemas.field.labelHint": "Display name for documentation and the UI.",
"schemas.field.localizable": "Localizable",
"schemas.field.localizableHint": "You can mark the field as localizable. It means that is dependent on the language, for example a city name.",

2
backend/src/Squidex/wwwroot/editor/squidex-editor.css

@ -1 +1 @@
.remirror-theme{width:100%}.remirror-theme *{box-sizing:border-box}.remirror-editor{min-height:300px!important;max-height:500px}.menu{border:1px solid #dedfe3;border-bottom:0}.MuiStack-root{padding:5px}.MuiStack-root>.MuiBox-root{margin-right:5px}.MuiBox-root>.MuiButtonBase-root{border-color:#dedfe3!important;border-radius:0}.MuiBox-root>.MuiButtonBase-root{border-left-width:0}.MuiBox-root>.MuiBox-root:first-child>.MuiButtonBase-root{border-left-width:1px}.MuiButtonBase-root.Mui-selected:hover{background-color:#3284f4!important}.remirror-editor-wrapper{padding-top:0!important}.remirror-editable-image-view{display:inline-block;margin-top:15px;margin-bottom:15px}.remirror-editable-image-view:hover .remirror-editable-image-button{display:block}.remirror-editable-image{max-width:400px;max-height:400px}.remirror-editable-image-button{background-color:#fff;border:1px solid #dedfe3;border-radius:0;bottom:15px;display:none;left:10px;padding:6px 12px}.custom-icon path{fill:#0000008a}.remirror-theme div.ProseMirror{border:1px solid #dedfe3!important;border-radius:0!important;box-shadow:none!important}.MuiTooltip-popper>div{background-color:#1a2129;border-radius:0;font-size:85%;font-weight:400;padding:.5rem}.squidex-editor-disabled{pointer-events:none}.squidex-editor-floating{border:1px solid #dedfe3}.squidex-editor-floating .MuiBox-root{margin-right:0!important}.squidex-editor-floating .MuiButtonBase-root{height:30px}.squidex-editor-counter{border:1px solid #dedfe3;border-top:0;font-size:85%;font-weight:400;opacity:.8;padding:4px 10px 4px 4px;text-align:right}.squidex-editor-input{border:1px solid #dedfe3;border-radius:0;box-sizing:border-box;height:30px;margin-left:0;margin-right:5px;outline:none;padding:6px 12px}.squidex-editor-input:active{border-color:#3389ff}.remirror-theme{position:relative}.squidex-editor-modal-wrapper,.squidex-editor-modal-backdrop{bottom:0;left:0;position:absolute;padding-left:0;padding-right:0;right:0;top:0}.squidex-editor-modal-backdrop{background-color:#00000003}.squidex-editor-modal-window{background-color:#fff;border:1px solid #dedfe3;border-radius:0;box-sizing:border-box;left:50%;margin-top:-50px;margin-left:-150px;position:absolute;top:50%;width:350px}.squidex-editor-modal-body{display:flex;flex-direction:row;flex-grow:1;padding-top:0!important}.squidex-editor-modal-body,.squidex-editor-modal-title{padding:15px}.squidex-editor-modal-title{font-size:85%}.squidex-editor-modal-window input{flex-grow:1}
.remirror-theme{width:100%}.remirror-theme{position:relative}.remirror-theme *{box-sizing:border-box}.remirror-editor{min-height:300px!important;max-height:500px}.MuiStack-root{padding:5px}.MuiStack-root>.MuiBox-root{margin-right:5px}.MuiBox-root>.MuiButtonBase-root{border-color:#dedfe3!important;border-radius:0}.MuiBox-root>.MuiButtonBase-root{border-left-width:0}.MuiBox-root>.MuiBox-root:first-child>.MuiButtonBase-root{border-left-width:1px}.MuiButtonBase-root.Mui-selected:hover{background-color:#3284f4!important}.remirror-editor-wrapper{padding-top:0!important}.custom-icon path{fill:#0000008a}.remirror-theme div.ProseMirror{border:1px solid #dedfe3!important;border-radius:0!important;box-shadow:none!important}.MuiTooltip-popper>div{background-color:#1a2129;border-radius:0;font-size:85%;font-weight:400;padding:.5rem}.squidex-editor-disabled{pointer-events:none}.squidex-editor-menu{border:1px solid #dedfe3;border-bottom:0}.squidex-editor-counter{border:1px solid #dedfe3;border-top:0;font-size:85%;font-weight:400;opacity:.8;padding:4px 10px 4px 4px;text-align:right}.squidex-editor-image-view{border:1px solid #dedfe3;border-radius:0;display:inline-block;margin-top:15px;margin-bottom:15px;overflow:hidden}.squidex-editor-image-view .squidex-editor-button{bottom:10px;left:10px;position:absolute}.squidex-editor-image-element{display:block;max-width:400px;max-height:400px}.squidex-editor-image-info{left:10px;top:10px;position:absolute;background-color:#3284f4;color:#fff;font-size:85%;font-weight:400;padding:2px 6px}.squidex-editor-content-link{align-items:center;border-radius:2px;border:1px solid #dedfe3;padding:10px;display:flex;flex-direction:row;flex-wrap:nowrap;margin-top:10px;margin-bottom:10px}.squidex-editor-content-schema{display:block;overflow-x:hidden;overflow-y:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%;min-width:0;width:auto;border-right:1px solid #c2c4cc;color:#8b8f9d;flex-shrink:0;padding-left:10px;padding-right:10px;width:200px}.squidex-editor-content-name{display:block;overflow-x:hidden;overflow-y:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%;min-width:0;width:auto;padding-left:10px;padding-right:0}.squidex-editor-html{margin-bottom:10px;margin-top:10px;position:relative}.squidex-editor-html-label{left:6px;top:0;position:absolute;color:#8b8f9d;font-size:85%;font-weight:400}.squidex-editor-html textarea{font-family:monospace;padding:30px 20px 20px}.squidex-editor-button{background-color:#fff;border-radius:0;border:1px solid #dedfe3;bottom:10px;font-size:85%;font-weight:400;line-height:1;padding:6px 12px}.squidex-editor-button:hover{background-color:#f5f5f5}.squidex-editor-input{border:1px solid #dedfe3;border-radius:0;box-sizing:border-box;height:30px;margin-left:0;margin-right:5px;outline:none;padding:6px 12px}.squidex-editor-input:active{border-color:#3284f4}.squidex-editor-floating{border:1px solid #dedfe3}.squidex-editor-floating .MuiBox-root{margin-right:0!important}.squidex-editor-floating .MuiButtonBase-root{height:30px}.squidex-editor-modal-wrapper,.squidex-editor-modal-backdrop{bottom:0;left:0;right:0;top:0;position:absolute}.squidex-editor-modal-backdrop{background-color:#00000003}.squidex-editor-modal-window{left:50%;top:50%;background-color:#fff;border:1px solid #dedfe3;border-radius:0;box-sizing:border-box;margin-top:-50px;margin-left:-150px;position:absolute;width:350px}.squidex-editor-modal-body{display:flex;flex-direction:row;flex-grow:1;padding-top:0!important}.squidex-editor-modal-body,.squidex-editor-modal-title{padding:15px}.squidex-editor-modal-title{font-size:85%}.squidex-editor-modal-window input{flex-grow:1}

269
backend/src/Squidex/wwwroot/editor/squidex-editor.js

File diff suppressed because one or more lines are too long

14
backend/src/Squidex/wwwroot/scripts/editor-dialogs.html

@ -32,12 +32,18 @@
PICK ASSETS
</button>
<button class="btn btn-outline-secondary" id="pickContents">
PICK CONTENTS
</button>
<script>
// When the field is instantiated it notifies the UI that it has been loaded.
//
// Furthermore it sends the current size to the parent.
var field = new SquidexFormField();
let selectedContents;
document.getElementById('alert').addEventListener('click', function () {
field.notifyError('ERROR Text');
});
@ -57,6 +63,14 @@
console.log('ASSETS: ' + JSON.stringify(assets, undefined, 2));
});
});
document.getElementById('pickContents').addEventListener('click', function () {
field.pickContents(undefined, function (contents) {
selectedContents = contents.map(x => x.id);
console.log('ASSETS: ' + JSON.stringify(contents, undefined, 2));
}, undefined, selectedContents);
});
</script>
</body>

3
backend/src/Squidex/wwwroot/scripts/editor-sdk.d.ts

@ -153,8 +153,9 @@ declare class SquidexFormField {
* @param schemas: The list of schema names.
* @param callback The callback to invoke when the dialog is completed or closed.
* @param query: The initial query that is used in the UI.
* @param selectedIds: The selected ids to mark them as selected in the content selector dialog.
*/
pickContents(schemas: string[], callback: (assets: any[]) => void, query?: string): void;
pickContents(schemas: string[], callback: (assets: any[]) => void, query?: string, selectedIds?: string[]): void;
/**
* Shows a dialog to pick a file.

9
backend/src/Squidex/wwwroot/scripts/editor-sdk.js

@ -546,12 +546,13 @@ function SquidexFormField() {
/**
* Shows the dialog to pick assets.
*
* @param {string} schemas: The list of schema names.
* @param {string[]} schemas: The list of schema names.
* @param {function} callback The callback to invoke when the dialog is completed or closed.
* @param {string} query: The initial filter that is used in the UI.
* @param {string[]} selectedIds: The selected ids to mark them as selected in the content selector dialog.
*/
pickContents: function (schemas, callback, query) {
if (!isFunction(callback) || !isArrayOfStrings(schemas)) {
pickContents: function (schemas, callback, query, selectedIds) {
if (!isFunction(callback)) {
return;
}
@ -560,7 +561,7 @@ function SquidexFormField() {
currentPickContents = { correlationId: correlationId, callback: callback };
if (window.parent) {
window.parent.postMessage({ type: 'pickContents', correlationId: correlationId, schemas: schemas, query: query }, '*');
window.parent.postMessage({ type: 'pickContents', correlationId: correlationId, schemas: schemas, query: query, selectedIds: selectedIds }, '*');
}
},

13
frontend/src/app/declarations.d.ts

@ -35,8 +35,11 @@ type Asset = {
};
type Content = {
// The link of the content item.
href: string;
// The title of the content.
id: string;
// The name of the schema.
schemaName: string;
// The title of the content item.
title: string;
@ -65,6 +68,12 @@ interface EditorProps {
// The incoming value.
value?: string;
// The base url.
baseUrl: string;
// The name to the app.
appName: string;
// Called when the value has been changed.
onChange?: OnChange;

1
frontend/src/app/features/content/shared/forms/iframe-editor.component.html

@ -10,6 +10,7 @@
<sqx-content-selector *sqxModal="contentsDialog"
(contentSelect)="pickContents($event)"
[alreadySelectedIds]="contentsSelectedIds"
[query]="contentsQuery"
[language]="language"
[languages]="languages"

8
frontend/src/app/features/content/shared/forms/iframe-editor.component.ts

@ -84,8 +84,9 @@ export class IFrameEditorComponent extends StatefulComponent<State> implements O
public contentsQuery?: string = undefined;
public contentsCorrelationId: any;
public contentsSchemas?: string[];
public contentsSchemas?: ReadonlyArray<string>;
public contentsDialog = new DialogModel();
public contentsSelectedIds?: ReadonlyArray<string>;
constructor(
private readonly appsState: AppsState,
@ -212,12 +213,13 @@ export class IFrameEditorComponent extends StatefulComponent<State> implements O
this.assetsDialog.show();
}
} else if (type === 'pickContents') {
const { correlationId, schemas, query } = event.data;
const { correlationId, schemas, query, selectedIds } = event.data;
if (correlationId) {
this.contentsQuery = query;
this.contentsCorrelationId = correlationId;
this.contentsSchemas = this.schemaIds && this.schemaIds.length > 0 ? this.schemaIds : schemas;
this.contentsSelectedIds = Types.isArrayOfString(selectedIds) ? selectedIds : undefined;
this.contentsSchemas = this.schemaIds && this.schemaIds.length > 0 ? this.schemaIds : Types.isArrayOfString(schemas) ? schemas : undefined;
this.contentsDialog.show();
}
}

13
frontend/src/app/shared/components/forms/rich-editor.component.ts

@ -8,7 +8,7 @@
import { AfterViewInit, booleanAttribute, ChangeDetectionStrategy, Component, ElementRef, EventEmitter, forwardRef, Input, OnDestroy, Output, ViewChild } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { ContentDto } from '@app/shared';
import { ApiUrlConfig, AssetDto, AssetUploaderState, DialogModel, getContentValue, LanguageDto, ResourceLoaderService, StatefulControlComponent, Types } from '@app/shared/internal';
import { ApiUrlConfig, AppsState, AssetDto, AssetUploaderState, DialogModel, getContentValue, LanguageDto, ResourceLoaderService, StatefulControlComponent, Types } from '@app/shared/internal';
export const SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RichEditorComponent), multi: true,
@ -67,6 +67,7 @@ export class RichEditorComponent extends StatefulControlComponent<{}, string> im
constructor(
private readonly apiUrl: ApiUrlConfig,
private readonly appsState: AppsState,
private readonly assetUploader: AssetUploaderState,
private readonly resourceLoader: ResourceLoaderService,
) {
@ -122,6 +123,8 @@ export class RichEditorComponent extends StatefulControlComponent<{}, string> im
onChange: (value: string | undefined) => {
this.callChange(value);
},
appName: this.appsState.appName,
baseUrl: this.apiUrl.buildUrl(''),
canSelectAIText: this.hasChatBot,
canSelectAssets: true,
canSelectContents: !!this.schemaIds,
@ -208,16 +211,16 @@ export class RichEditorComponent extends StatefulControlComponent<{}, string> im
return requests.map(r => () => uploadFile(r));
}
private buildAsset(asset: AssetDto) {
private buildAsset(asset: AssetDto): Asset {
return { type: asset.mimeType, src: asset.fullUrl(this.apiUrl), fileName: asset.fileName };
}
private buildContent(content: ContentDto) {
return { url: this.apiUrl.buildUrl(content._links['self'].href), name: buildContentName(content, this.language) };
private buildContent(content: ContentDto): Content {
return { ...content, title: buildContentTitle(content, this.language) };
}
}
function buildContentName(content: ContentDto, language: LanguageDto) {
function buildContentTitle(content: ContentDto, language: LanguageDto) {
const name =
content.referenceFields
.map(f => getContentValue(content, language, f, false))

9
frontend/src/app/shared/components/references/content-selector.component.ts

@ -52,7 +52,12 @@ export class ContentSelectorComponent implements OnInit {
public allowDuplicates?: boolean | null;
@Input()
public alreadySelected: ReadonlyArray<ContentDto> | undefined | null;
public alreadySelectedIds: ReadonlyArray<string> | undefined | null;
@Input()
public set alreadySelected(value: ReadonlyArray<ContentDto> | undefined | null) {
this.alreadySelectedIds = value?.map(x => x.id);
}
public schema!: SchemaDto;
public schemas: ReadonlyArray<SchemaDto> = [];
@ -132,7 +137,7 @@ export class ContentSelectorComponent implements OnInit {
}
public isItemAlreadySelected(content: ContentDto) {
return !this.allowDuplicates && this.alreadySelected && !!this.alreadySelected.find(x => x.id === content.id);
return !this.allowDuplicates && this.alreadySelectedIds && this.alreadySelectedIds.indexOf(content.id) >= 0;
}
public emitClose() {

Loading…
Cancel
Save