Browse Source

New editor. (#1034)

* New editor.

* Fix tests
pull/1035/head
Sebastian Stehle 3 years ago
committed by GitHub
parent
commit
0909a21fc7
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      backend/src/Squidex/wwwroot/editor/squidex-editor.css
  2. 5000
      backend/src/Squidex/wwwroot/editor/squidex-editor.js
  3. 13
      backend/src/Squidex/wwwroot/scripts/editor-combined.html
  4. 13
      backend/src/Squidex/wwwroot/scripts/editor-context.html
  5. 15
      backend/src/Squidex/wwwroot/scripts/editor-dialogs.html
  6. 68
      backend/src/Squidex/wwwroot/scripts/editor-editorjs.html
  7. 88
      backend/src/Squidex/wwwroot/scripts/editor-json-schema.html
  8. 20
      backend/src/Squidex/wwwroot/scripts/editor-log.html
  9. 77
      backend/src/Squidex/wwwroot/scripts/editor-monaco.html
  10. 28
      backend/src/Squidex/wwwroot/scripts/editor-plain.html
  11. 5
      backend/src/Squidex/wwwroot/scripts/editor-references.html
  12. 53
      backend/src/Squidex/wwwroot/scripts/editor-sdk.d.ts
  13. 42
      backend/src/Squidex/wwwroot/scripts/editor-sdk.js
  14. 104
      backend/src/Squidex/wwwroot/scripts/editor-simple.html
  15. 12
      backend/src/Squidex/wwwroot/scripts/sidebar-content.html
  16. 13
      backend/src/Squidex/wwwroot/scripts/sidebar-context.html
  17. 116
      backend/src/Squidex/wwwroot/scripts/sidebar-search.html
  18. 15
      backend/src/Squidex/wwwroot/scripts/widget-context.html
  19. 2
      frontend/package.json
  20. 82
      frontend/src/app/declarations.d.ts
  21. 13
      frontend/src/app/features/content/shared/forms/field-editor.component.html
  22. 5
      frontend/src/app/features/content/shared/forms/iframe-editor.component.ts
  23. 7
      frontend/src/app/framework/angular/forms/file-drop.directive.ts
  24. 18
      frontend/src/app/shared/components/forms/markdown-editor.component.html
  25. 18
      frontend/src/app/shared/components/forms/markdown-editor.component.scss
  26. 363
      frontend/src/app/shared/components/forms/markdown-editor.component.ts
  27. 4
      frontend/src/app/shared/components/forms/rich-editor.component.html
  28. 10
      frontend/src/app/shared/components/forms/rich-editor.component.scss
  29. 352
      frontend/src/app/shared/components/forms/rich-editor.component.ts
  30. 1
      frontend/src/app/shared/declarations.ts
  31. 13
      frontend/src/app/shared/guards/must-be-authenticated.guard.spec.ts
  32. 4
      frontend/src/app/shared/guards/must-be-authenticated.guard.ts
  33. 13
      frontend/src/app/shared/guards/must-be-not-authenticated.guard.spec.ts
  34. 4
      frontend/src/app/shared/guards/must-be-not-authenticated.guard.ts
  35. 4
      frontend/src/app/shared/module.ts
  36. 8
      frontend/src/app/shared/state/resolvers.spec.ts
  37. 14
      frontend/src/config/webpack.config.js

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

@ -0,0 +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}.MuiStack-root>.MuiBox-root>button.MuiButtonBase-root{margin-left:-1px}.MuiBox-root>.MuiButtonBase-root{border-color:#dedfe3!important;border-radius:0}.MuiButtonBase-root.Mui-selected:hover{background-color:#3284f4!important}.remirror-editor img{max-width:300px;max-height:300px}.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-floating{border:1px solid #dedfe3}.squidex-editor-floating .MuiBox-root{margin-right:0!important}.squidex-editor-floating 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-floating input:active{border-color:#3389ff}.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}

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

File diff suppressed because one or more lines are too long

13
backend/src/Squidex/wwwroot/scripts/editor-combined.html

@ -6,10 +6,21 @@
<!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js --> <!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js -->
<script src="editor-sdk.js"></script> <script src="editor-sdk.js"></script>
<link rel="stylesheet" type="text/css" href="https://cloud.squidex.io/styles.css">
<style>
body {
background-color: white;
}
textarea {
height: 300px;
}
</style>
</head> </head>
<body> <body>
<textarea style="width: 100%; box-sizing: border-box; height: 40px;" name="content" id="editor"></textarea> <textarea class="form-control" id="editor"></textarea>
<script> <script>
var element = document.getElementById('editor'); var element = document.getElementById('editor');

13
backend/src/Squidex/wwwroot/scripts/editor-context.html

@ -7,13 +7,14 @@
<!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js --> <!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js -->
<script src="editor-sdk.js"></script> <script src="editor-sdk.js"></script>
<link rel="stylesheet" type="text/css" href="https://cloud.squidex.io/styles.css">
<style> <style>
body {
background-color: white;
}
textarea { textarea {
box-sizing: border-box; height: 300px;
resize: none;
overflow-x: hidden;
overflow-y: hidden;
width: 100%;
} }
</style> </style>
</head> </head>
@ -26,7 +27,7 @@
} }
</script> </script>
<textarea oninput="grow(this)" name="content" id="editor"></textarea> <textarea oninput="grow(this)" class="form-control" name="content" id="editor"></textarea>
<script> <script>
var element = document.getElementById('editor'); var element = document.getElementById('editor');

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

@ -6,22 +6,29 @@
<!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js --> <!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js -->
<script src="editor-sdk.js"></script> <script src="editor-sdk.js"></script>
<link rel="stylesheet" type="text/css" href="https://cloud.squidex.io/styles.css">
<style>
body {
background-color: white;
}
</style>
</head> </head>
<body> <body>
<button id="alert"> <button class="btn btn-outline-secondary" id="alert">
ALERT ALERT
</button> </button>
<button id="info"> <button class="btn btn-outline-secondary" id="info">
INFO INFO
</button> </button>
<button id="confirm"> <button class="btn btn-outline-secondary" id="confirm">
CONFIRM CONFIRM
</button> </button>
<button id="pickAssets"> <button class="btn btn-outline-secondary" id="pickAssets">
PICK ASSETS PICK ASSETS
</button> </button>

68
backend/src/Squidex/wwwroot/scripts/editor-editorjs.html

@ -1,68 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js -->
<script src="editor-sdk.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@latest"></script>
<style>
.editor {
border: 1px solid #eee;
border-radius: 4px;
min-height: 50px;
}
</style>
</head>
<body>
<div class="editor">
<div id="editorjs"></div>
</div>
<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();
var editor = new EditorJS({
holder: 'editorjs',
tools: {
header: {
class: Header,
inlineToolbar : true
}
},
minHeight: 50,
data: {},
onReady: function () {
field.onValueChanged(function (value) {
if (value) {
editor.render(value);
}
});
field.onDisabled(function (disabled) {
editor.readOnly.toggle(disabled);
});
},
onChange: function () {
editor.save().then(function (data) {
// Notify the UI that the value has been changed. Will be used to trigger validation.
field.valueChanged(data);
});
}
});
</script>
</body>
</html>

88
backend/src/Squidex/wwwroot/scripts/editor-json-schema.html

@ -1,88 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js -->
<script src="editor-sdk.js"></script>
<script src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@rjsf/core/dist/react-jsonschema-form.js"></script>
<link rel="stylesheet" type="text/css" href="https://cloud.squidex.io/styles.css">
<style>
body {
background: none;
}
#root__title {
display: none;
}
</style>
</head>
<body>
<div id="editor"></div>
<script>
const Form = JSONSchemaForm.default;
// When the field is instantiated it notifies the UI that it has been loaded.
//
// Furthermore it sends the current size to the parent.
const field = new SquidexFormField();
function Editor() {
var stateDisabled = React.useState(false); // Use ES5 language features only.
var stateValue = React.useState(undefined);
var stateSchema = React.useState(undefined);
React.useEffect(function () {
field.onDisabled(function (disabled) {
stateDisabled[1](disabled);
});
field.onValueChanged(function (value) {
stateValue[1](value);
});
if (window.location.hash && window.location.hash.length > 1) {
fetch(window.location.hash.substr(1))
.then(x => x.json())
.then(x => stateSchema[1](x));
}
}, []);
const doBlur = React.useCallback(function () {
field.touched();
}, []);
const doChange = React.useCallback(function (editor) {
if (editor.errors.length === 0) {
field.valueChanged(editor.formData);
}
}, []);
if (!stateSchema[0]) {
return null;
}
return (
React.createElement(Form, {
formData: stateValue[0],
disabled: stateDisabled[0],
onChange: doChange,
onBlur: doBlur,
schema: stateSchema[0],
children: [] // Hide submit button
})
);
}
ReactDOM.render(React.createElement(Editor), document.getElementById("editor"));
</script>
</body>
</html>

20
backend/src/Squidex/wwwroot/scripts/editor-log.html

@ -7,22 +7,26 @@
<!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js --> <!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js -->
<script src="editor-sdk.js"></script> <script src="editor-sdk.js"></script>
<link rel="stylesheet" type="text/css" href="https://cloud.squidex.io/styles.css">
<style> <style>
#textarea { body {
box-sizing: border-box; background-color: white;
min-width: 100%; }
min-height: 200px;
margin-top: 10px; textarea {
height: 300px;
font-family: monospace;
font-size: 12px;
} }
</style> </style>
</head> </head>
<body> <body>
<div> <div class="mb-2">
<button id="button">Generate New Value</button> <button class="btn btn-outline-secondary" id="button">Generate New Value</button>
</div> </div>
<textarea readonly id="textarea"></textarea> <textarea class="form-control" readonly id="textarea"></textarea>
<script> <script>
var numberGenerator = 1; var numberGenerator = 1;

77
backend/src/Squidex/wwwroot/scripts/editor-monaco.html

@ -1,77 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js -->
<script src="editor-sdk.js"></script>
<script type="text/javascript" src="https://unpkg.com/monaco-editor@latest/min/vs/loader.js"></script>
<style>
.editor {
border: 1px solid #eee;
border-radius: 4px;
height: 500px;
}
</style>
</head>
<body>
<div class="editor" id="editor"></div>
<script>
require.config({ paths: { 'vs': 'https://unpkg.com/monaco-editor@latest/min/vs' }});
// 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();
// Before loading vs/editor/editor.main, define a global MonacoEnvironment that overwrites
// the default worker url location (used when creating WebWorkers). The problem here is that
// HTML5 does not allow cross-domain web workers, so we need to proxy the instantiation of
// a web worker through a same-domain script
window.MonacoEnvironment = {
getWorkerUrl: function(workerId, label) {
return `data:text/javascript;charset=utf-8,${encodeURIComponent(`
self.MonacoEnvironment = {
baseUrl: 'https://unpkg.com/monaco-editor@latest/min/'
};
importScripts('https://unpkg.com/monaco-editor@latest/min/vs/base/worker/workerMain.js');`
)}`;
}
};
require(["vs/editor/editor.main"], function () {
var editor = monaco.editor.create(document.getElementById('editor'), {
value: `function x() {
console.log("Hello world!");
}`,
language: 'html',
theme: 'vs-dark'
});
field.onValueChanged(function (value) {
editor.setValue(value || '');
});
field.onDisabled(function (disabled) {
editor.updateOptions({ readOnly: disabled });
});
editor.getModel().onDidChangeContent((event) => {
var data = editor.getValue();
// Notify the UI that the value has been changed. Will be used to trigger validation.
field.valueChanged(data);
});
window.addEventListener('resize', function () {
editor.layout();
});
});
</script>
</body>
</html>

28
backend/src/Squidex/wwwroot/scripts/editor-plain.html

@ -4,39 +4,31 @@
<!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js --> <!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js -->
<script src="editor-sdk.js"></script> <script src="editor-sdk.js"></script>
<link rel="stylesheet" type="text/css" href="https://cloud.squidex.io/styles.css">
<style> <style>
.editor { body {
border: 1px solid #ddd; background-color: white;
border-radius: 4px;
height: 500px;
width: 100%;
} }
.btn { textarea {
background: white; height: 300px;
border: 1px solid #ddd;
border-radius: 4px;
padding: 8px 15px;
margin-right: 5px;
margin-bottom: 5px;
} }
</style> </style>
</head> </head>
<body style="margin: 0px; padding: 0px;"> <body style="margin: 0px; padding: 0px;">
<div> <div class="mb-2">
<button class="btn" id="expanded"> <button class="btn btn-outline-secondary" id="expanded">
Toggle Expanded Toggle Expanded
</button> </button>
<button class="btn" id="fullscreen"> <button class="btn btn-outline-secondary" id="fullscreen">
Toggle Fullscreen Toggle Fullscreen
</button> </button>
</div> </div>
<div> <div>
<textarea name="content" class="editor" id="editor"></textarea> <textarea class="form-control" id="editor"></textarea>
</div> </div>
<script> <script>

5
backend/src/Squidex/wwwroot/scripts/editor-references.html

@ -6,12 +6,11 @@
<!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js --> <!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js -->
<script src="editor-sdk.js"></script> <script src="editor-sdk.js"></script>
<link rel="stylesheet" type="text/css" href="https://cloud.squidex.io/styles.css">
<link rel="stylesheet" type="text/css" href="https://cloud.squidex.io/styles.css">
<style> <style>
body { body {
height: 38px; background-color: white;
} }
</style> </style>
</head> </head>

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

@ -12,14 +12,14 @@ declare class SquidexSidebar {
/** /**
* Register an function that is called when the sidebar is initialized. * Register an function that is called when the sidebar is initialized.
* *
* @param {function} callback: The callback to invoke. * @param callback: The callback to invoke.
*/ */
onInit(callback: () => void): void; onInit(callback: () => void): void;
/** /**
* Register an function that is called whenever the value of the content has changed. * Register an function that is called whenever the value of the content has changed.
* *
* @param {function} callback: The callback to invoke. Argument 1: Content value (any). * @param callback: The callback to invoke. Argument 1: Content value (any).
*/ */
onContentChanged(callback: (content: any) => void): void; onContentChanged(callback: (content: any) => void): void;
@ -38,7 +38,7 @@ declare class SquidexWidget {
/** /**
* Register an function that is called when the sidebar is initialized. * Register an function that is called when the sidebar is initialized.
* *
* @param {function} callback: The callback to invoke. * @param callback: The callback to invoke.
*/ */
onInit(callback: () => void): void; onInit(callback: () => void): void;
/** /**
@ -96,119 +96,124 @@ declare class SquidexFormField {
/** /**
* Notifies the parent to navigate to the path. * Notifies the parent to navigate to the path.
* *
* @param {string} url: The url to navigate to. * @param url: The url to navigate to.
*/ */
navigate(url: string): void; navigate(url: string): void;
/** /**
* Notifies the parent to go to fullscreen mode. * Notifies the parent to toggle the fullscreen mode.
*/ */
toggleFullscreen(): void; toggleFullscreen(): void;
/** /**
* Notifies the parent to go to expanded mode. * Notifies the parent to toggle the expanded mode.
*/ */
toggleExpanded(): void; toggleExpanded(): void;
/** /**
* Notifies the control container that the value has been changed. * Notifies the control container that the value has been changed.
* *
* @param {any} newValue: The new field value. * @param newValue: The new field value.
*/ */
valueChanged(newValue: any): void; valueChanged(newValue: any): void;
/** /**
* Shows an info alert. * Shows an info alert.
* *
* @param {string} text: The info text. * @param text: The info text.
*/ */
notifyInfo(text: string): void; notifyInfo(text: string): void;
/** /**
* Shows an error alert. * Shows an error alert.
* *
* @param {string} text: error info text. * @param text: error info text.
*/ */
notifyError(text: string): void; notifyError(text: string): void;
/** /**
* Shows an confirm dialog. * Shows an confirm dialog.
* *
* @param {string} title The title of the dialog. * @param title The title of the dialog.
* @param {string} text The text of the dialog. * @param text The text of the dialog.
* @param {function} callback The callback to invoke when the dialog is completed or closed. * @param callback The callback to invoke when the dialog is completed or closed.
*/ */
confirm(title: string, text: string, callback: (result: boolean) => void): void; confirm(title: string, text: string, callback: (result: boolean) => void): void;
/** /**
* Shows the dialog to pick assets. * Shows the dialog to pick assets.
* *
* @param {function} callback The callback to invoke when the dialog is completed or closed. * @param callback The callback to invoke when the dialog is completed or closed.
*/ */
pickAssets(callback: (assets: any[]) => void): void; pickAssets(callback: (assets: any[]) => void): void;
/** /**
* Shows the dialog to pick contents. * Shows the dialog to pick contents.
* *
* @param {string} schemas: The list of schema names. * @param schemas: The list of schema names.
* @param {function} callback The callback to invoke when the dialog is completed or closed. * @param callback The callback to invoke when the dialog is completed or closed.
* @param {string} query: The initial query that is used in the UI. * @param query: The initial query that is used in the UI.
*/ */
pickContents(schemas: string[], callback: (assets: any[]) => void, query?: string): void; pickContents(schemas: string[], callback: (assets: any[]) => void, query?: string): void;
/**
* Shows a dialog to pick a file.
*/
pickFile(): void;
/** /**
* Register an function that is called when the field is initialized. * Register an function that is called when the field is initialized.
* *
* @param {function} callback: The callback to invoke. * @param callback: The callback to invoke.
*/ */
onInit(callback: () => void): void; onInit(callback: () => void): void;
/** /**
* Register an function that is called when the field is moved. * Register an function that is called when the field is moved.
* *
* @param {function} callback: The callback to invoke. Argument 1: New position (number). * @param callback: The callback to invoke. Argument 1: New position (number).
*/ */
onMoved(callback: (index: number) => void): void; onMoved(callback: (index: number) => void): void;
/** /**
* Register an function that is called whenever the field is disabled or enabled. * Register an function that is called whenever the field is disabled or enabled.
* *
* @param {function} callback: The callback to invoke. Argument 1: New disabled state (boolean, disabled = true, enabled = false). * @param callback: The callback to invoke. Argument 1: New disabled state (boolean, disabled = true, enabled = false).
*/ */
onDisabled(callback: (isDisabled: boolean) => void): void; onDisabled(callback: (isDisabled: boolean) => void): void;
/** /**
* Register an function that is called whenever the field language is changed. * Register an function that is called whenever the field language is changed.
* *
* @param {function} callback: The callback to invoke. Argument 1: Language code (string). * @param callback: The callback to invoke. Argument 1: Language code (string).
*/ */
onLanguageChanged(callback: (language: string) => void): void; onLanguageChanged(callback: (language: string) => void): void;
/** /**
* Register an function that is called whenever the value of the field has changed. * Register an function that is called whenever the value of the field has changed.
* *
* @param {function} callback: The callback to invoke. Argument 1: Field value (any). * @param callback: The callback to invoke. Argument 1: Field value (any).
*/ */
onValueChanged(callback: (value: any) => void): void; onValueChanged(callback: (value: any) => void): void;
/** /**
* Register an function that is called whenever the value of the content has changed. * Register an function that is called whenever the value of the content has changed.
* *
* @param {function} callback: The callback to invoke. Argument 1: Content value (any). * @param callback: The callback to invoke. Argument 1: Content value (any).
*/ */
onFormValueChanged(callback: (value: any) => void): void; onFormValueChanged(callback: (value: any) => void): void;
/** /**
* Register an function that is called whenever the fullscreen mode has changed. * Register an function that is called whenever the fullscreen mode has changed.
* *
* @param {function} callback: The callback to invoke. Argument 1: Fullscreen state (boolean, fullscreen on = true, fullscreen off = false). * @param callback: The callback to invoke. Argument 1: Fullscreen state (boolean, fullscreen on = true, fullscreen off = false).
*/ */
onFullscreen(callback: (isFullscreen: boolean) => void): void; onFullscreen(callback: (isFullscreen: boolean) => void): void;
/** /**
* Register an function that is called whenever the expanded mode has changed. * Register an function that is called whenever the expanded mode has changed.
* *
* @param {function} callback: The callback to invoke. Argument 1: Expanded state (boolean, expanded on = true, expanded off = false). * @param callback: The callback to invoke. Argument 1: Expanded state (boolean, expanded on = true, expanded off = false).
*/ */
onExpanded(callback: (isExpanded: boolean) => void): void; onExpanded(callback: (isExpanded: boolean) => void): void;

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

@ -242,6 +242,13 @@ function SquidexFormField() {
var value; var value;
var valueHandler; var valueHandler;
function raiseInit() {
if (initHandler && !initCalled && context) {
initHandler(context);
initCalled = true;
}
}
function raiseDisabled() { function raiseDisabled() {
if (disabledHandler) { if (disabledHandler) {
disabledHandler(disabled); disabledHandler(disabled);
@ -284,13 +291,6 @@ function SquidexFormField() {
} }
} }
function raiseInit() {
if (initHandler && !initCalled && context) {
initHandler(context);
initCalled = true;
}
}
function eventListener(event) { function eventListener(event) {
if (event.source !== window) { if (event.source !== window) {
var type = event.data.type; var type = event.data.type;
@ -451,7 +451,7 @@ function SquidexFormField() {
}, },
/** /**
* Notifies the parent to go to fullscreen mode. * Notifies the parent to toggle the fullscreen mode.
*/ */
toggleFullscreen: function () { toggleFullscreen: function () {
if (window.parent) { if (window.parent) {
@ -460,7 +460,7 @@ function SquidexFormField() {
}, },
/** /**
* Notifies the parent to go to expanded mode. * Notifies the parent to toggle the expanded mode.
*/ */
toggleExpanded: function () { toggleExpanded: function () {
if (window.parent) { if (window.parent) {
@ -517,10 +517,7 @@ function SquidexFormField() {
var correlationId = new Date().getTime().toString(); var correlationId = new Date().getTime().toString();
currentConfirm = { currentConfirm = { correlationId: correlationId, callback: callback };
correlationId: correlationId,
callback: callback
};
if (window.parent) { if (window.parent) {
window.parent.postMessage({ type: 'confirm', title: title, text: text, correlationId: correlationId }, '*'); window.parent.postMessage({ type: 'confirm', title: title, text: text, correlationId: correlationId }, '*');
@ -539,10 +536,7 @@ function SquidexFormField() {
var correlationId = new Date().getTime().toString(); var correlationId = new Date().getTime().toString();
currentPickAssets = { currentPickAssets = { correlationId: correlationId, callback: callback };
correlationId: correlationId,
callback: callback
};
if (window.parent) { if (window.parent) {
window.parent.postMessage({ type: 'pickAssets', correlationId: correlationId }, '*'); window.parent.postMessage({ type: 'pickAssets', correlationId: correlationId }, '*');
@ -563,16 +557,22 @@ function SquidexFormField() {
var correlationId = new Date().getTime().toString(); var correlationId = new Date().getTime().toString();
currentPickContents = { currentPickContents = { correlationId: correlationId, callback: callback };
correlationId: correlationId,
callback: callback,
};
if (window.parent) { 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 }, '*');
} }
}, },
/**
* Shows a dialog to pick a file.
*/
pickFile: function () {
if (window.parent) {
window.parent.postMessage({ type: 'pickFile' }, '*');
}
},
/** /**
* Register an function that is called when the field is initialized. * Register an function that is called when the field is initialized.
* *

104
backend/src/Squidex/wwwroot/scripts/editor-simple.html

@ -1,104 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js -->
<script src="editor-sdk.js"></script>
<script src="https://cdn.ckeditor.com/ckeditor5/10.0.0/classic/ckeditor.js"></script>
<style>
.ck-editor__editable {
min-height: 250px;
}
.fullscreen {
height: 100vh;
}
.fullscreen .ck-editor {
position: fixed !important;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
}
.fullscreen .ck-editor .ck-editor__editable.ck-rounded-corners.ck-editor__editable_inline,
.fullscreen .ck-editor .ck.ck-editor__main {
height: 100%;
}
#fullscreenButton {
position: absolute;
z-index: 10000;
right: 10px;
top: 10px;
}
</style>
</head>
<body>
<a href="#" id="fullscreenButton">Toggle fullscreen</a>
<textarea name="content" id="editor"></textarea>
<script>
var element = document.getElementById('editor');
ClassicEditor
.create(element)
.catch(error => {
console.error(error);
})
.then(editor => {
// 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();
// Handle the value change event and set the text to the editor.
field.onValueChanged(function (value) {
if (value) {
editor.setData(value);
}
});
// Disable the editor when it should be disabled.
field.onDisabled(function (disabled) {
editor.set('isReadOnly', disabled);
});
field.onFullscreen(function (fullscreen) {
if (fullscreen) {
document.body.classList.add('fullscreen');
} else {
document.body.classList.remove('fullscreen');
}
});
editor.model.document.on('change', function () {
var data = editor.getData();
// Notify the UI that the value has been changed. Will be used to trigger validation.
field.valueChanged(data);
});
editor.ui.focusTracker.on('change:isFocused', function (event, name, isFocused) {
if (!isFocused) {
// Notify the UI that the value has been touched.
field.touched();
}
});
document.getElementById('fullscreenButton')
.addEventListener('click', function () {
field.toggleFullscreen();
});
});
</script>
</body>
</html>

12
backend/src/Squidex/wwwroot/scripts/sidebar-content.html

@ -7,14 +7,10 @@
<!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js --> <!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js -->
<script src="editor-sdk.js"></script> <script src="editor-sdk.js"></script>
<link rel="stylesheet" type="text/css" href="https://cloud.squidex.io/styles.css">
<style> <style>
textarea { body {
box-sizing: border-box; background-color: white;
border: 0;
border-radius: 0;
resize: none;
overflow: hidden;
width: 100%;
} }
</style> </style>
</head> </head>
@ -27,7 +23,7 @@
} }
</script> </script>
<textarea oninput="grow(this)" name="content" id="editor"></textarea> <textarea class="form-control" oninput="grow(this)" id="editor"></textarea>
<script> <script>
var element = document.getElementById('editor'); var element = document.getElementById('editor');

13
backend/src/Squidex/wwwroot/scripts/sidebar-context.html

@ -7,15 +7,10 @@
<!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js --> <!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js -->
<script src="editor-sdk.js"></script> <script src="editor-sdk.js"></script>
<link rel="stylesheet" type="text/css" href="https://cloud.squidex.io/styles.css">
<style> <style>
textarea { body {
box-sizing: border-box; background-color: white;
border: 0;
border-radius: 0;
resize: none;
overflow-x: hidden;
overflow-y: hidden;
width: 100%;
} }
</style> </style>
</head> </head>
@ -28,7 +23,7 @@
} }
</script> </script>
<textarea oninput="grow(this)" name="content" id="editor"></textarea> <textarea class="form-control" oninput="grow(this)" id="editor"></textarea>
<script> <script>
var element = document.getElementById('editor'); var element = document.getElementById('editor');

116
backend/src/Squidex/wwwroot/scripts/sidebar-search.html

@ -1,116 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js -->
<script src="editor-sdk.js"></script>
<script src="https://cdn.jsdelivr.net/npm/algoliasearch@4.0.0/dist/algoliasearch-lite.umd.js" integrity="sha256-MfeKq2Aw9VAkaE9Caes2NOxQf6vUa8Av0JqcUXUGkd0=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/instantsearch.js@4.0.0/dist/instantsearch.production.min.js" integrity="sha256-6S7q0JJs/Kx4kb/fv0oMjS855QTz5Rc2hh9AkIUjUsk=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/instantsearch.css@7.3.1/themes/algolia-min.css" integrity="sha256-HB49n/BZjuqiCtQQf49OdZn63XuKFaxcIHWf0HNKte8=" crossorigin="anonymous">
<style>
.container {
min-height: 400px;
}
.ais-Hits {
margin-top: 1rem;
}
.ais-Hits-item {
margin-right: 0;
margin-top: 5px;
width: 100%;
}
.button-click {
margin-top: 5px;
color: #3389ff;
cursor: pointer;
display: inline-block;
}
.button-click:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<script>
function grow(element) {
element.style.height = "5px";
element.style.height = (element.scrollHeight)+"px";
}
</script>
<div class="container">
<div id="searchbox"></div>
<div id="hits"></div>
</div>
<script>
var element = document.getElementById('editor');
// When the field is instantiated it notifies the UI that it has been loaded.
//
// Furthermore it sends the current size to the parent.
var plugin = new SquidexPlugin();
// Init is called once with a context that contains the app name, schema name and authentication information.
plugin.onInit(function (context) {
var searchClient = algoliasearch('CFNTEE51PJ', 'afa3a7605277b85348c6fa160fb5cecc');
var search = instantsearch({ indexName: 'test', searchClient });
document.addEventListener('click', event => {
if (event.target.matches('.button-click')) {
// We cannot directly navigate to in the iframe because it would only change the URL of the iframe.
plugin.navigate(`/app/${context.appName}/content/${context.schemaName}`);
// plugin.navigate(`/app/${context.appName}/content/${context.schemaName}/${id}`);
}
});
search.addWidgets([
instantsearch.widgets.searchBox({
container: '#searchbox'
}),
instantsearch.widgets.hits({
container: '#hits',
templates: {
item(hit, bindEvent) {
return `
<div>
<div class="hit-name">
${instantsearch.highlight({
attribute: 'firstname',
hit
})}
${instantsearch.highlight({
attribute: 'lastname',
hit
})}
<div>
<a data-object-id="${hit.objectID}" class="button-click">EDIT</a>
</div>
</div>
</div>
`;
}
}
})
]);
search.start();
});
</script>
</body>
</html>

15
backend/src/Squidex/wwwroot/scripts/widget-context.html

@ -7,23 +7,16 @@
<!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js --> <!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js -->
<script src="editor-sdk.js"></script> <script src="editor-sdk.js"></script>
<link rel="stylesheet" type="text/css" href="https://cloud.squidex.io/styles.css">
<style> <style>
textarea { body {
border-radius: 0; background-color: white;
border: 0;
bottom: 0;
box-sizing: border-box;
left: 0;
position: fixed;
resize: none;
right: 0;
top: 0;
} }
</style> </style>
</head> </head>
<body> <body>
<textarea name="content" id="editor"></textarea> <textarea class="form-control" id="editor"></textarea>
<script> <script>
var element = document.getElementById('editor'); var element = document.getElementById('editor');

2
frontend/package.json

@ -66,7 +66,6 @@
"rxjs": "7.8.1", "rxjs": "7.8.1",
"simplemde": "1.11.2", "simplemde": "1.11.2",
"slugify": "1.6.6", "slugify": "1.6.6",
"tinymce": "5.10.2",
"tslib": "2.6.1", "tslib": "2.6.1",
"tui-calendar": "^1.15.3", "tui-calendar": "^1.15.3",
"typemoq": "^2.1.0", "typemoq": "^2.1.0",
@ -102,7 +101,6 @@
"@types/react-dom": "18.2.7", "@types/react-dom": "18.2.7",
"@types/simplemde": "1.11.8", "@types/simplemde": "1.11.8",
"@types/tapable": "2.2.3", "@types/tapable": "2.2.3",
"@types/tinymce": "4.6.5",
"@types/ws": "8.5.5", "@types/ws": "8.5.5",
"@typescript-eslint/eslint-plugin": "6.2.1", "@typescript-eslint/eslint-plugin": "6.2.1",
"@typescript-eslint/parser": "6.2.1", "@typescript-eslint/parser": "6.2.1",

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

@ -6,4 +6,84 @@
*/ */
declare module 'pikaday/pikaday'; declare module 'pikaday/pikaday';
declare module 'progressbar.js'; declare module 'progressbar.js';
declare class SquidexEditorWrapper {
constructor(element: HTMLElement, props: EditorProps);
update(newProps: Partial<EditorProps>): void;
setValue(value: string): void;
setIsDisabled(isDisabled: boolean): void;
destroy(): void;
}
type Asset = {
// The alternative text of the image.
alt?: string;
// The src to the asset.
src: string;
// The mime type.
type: string;
// The file name of the asset.
fileName: string;
};
type Content = {
// The link of the content item.
href: string;
// The title of the content item.
title: string;
};
type OnSelectAssets = () => Promise<Asset[]>;
type OnSelectContents = () => Promise<Content[]>;
type OnChange = (value: string | undefined) => void;
type SquidexEditorMode = 'Html' | 'Markdown';
interface UploadRequest {
// The file to upload.
file: File;
// The upload progress to update.
progress: (progress: number) => void;
}
interface EditorProps {
// The mode of the editor.
mode: SquidexEditorMode;
// The incoming value.
value?: string;
// Called when the value has been changed.
onChange?: OnChange;
// Called when assets are selected.
onSelectAssets: OnSelectAssets;
// Called when content items should be selected.
onSelectContents: OnSelectContents;
// Called when a file needs to be uploaded.
onUpload?: (images: UploadRequest[]) => DelayedPromiseCreator<Asset>[];
// True, if disabled.
isDisabled?: boolean;
// Indicates whether assets can be selected.
canSelectAssets?: boolean;
// Indicates whether content items can be selected.
canSelectContents?: boolean;
}
type DelayedPromiseCreator<T> = (context: unknown) => Promise<T>;

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

@ -39,7 +39,8 @@
[formIndex]="index" [formIndex]="index"
[formField]="formModel.field.name" [formField]="formModel.field.name"
[language]="language" [language]="language"
[languages]="languages"> [languages]="languages"
[schemaIds]="field.rawProperties.schemaIds">
</sqx-iframe-editor> </sqx-iframe-editor>
</ng-container> </ng-container>
@ -192,7 +193,9 @@
<textarea class="form-control" [formControl]="$any(fieldForm)" [placeholder]="field.displayPlaceholder" rows="5"></textarea> <textarea class="form-control" [formControl]="$any(fieldForm)" [placeholder]="field.displayPlaceholder" rows="5"></textarea>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'RichText'"> <ng-container *ngSwitchCase="'RichText'">
<sqx-rich-editor [formControl]="$any(fieldForm)" #editor <sqx-rich-editor #editor
mode="Html"
[formControl]="$any(fieldForm)"
[folderId]="field.rawProperties.folderId" [folderId]="field.rawProperties.folderId"
[language]="language" [language]="language"
[languages]="languages" [languages]="languages"
@ -203,12 +206,14 @@
<sqx-code-editor [formControl]="$any(fieldForm)" #editor mode="ace/mode/html" [height]="350" ></sqx-code-editor> <sqx-code-editor [formControl]="$any(fieldForm)" #editor mode="ace/mode/html" [height]="350" ></sqx-code-editor>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'Markdown'"> <ng-container *ngSwitchCase="'Markdown'">
<sqx-markdown-editor [formControl]="$any(fieldForm)" #editor <sqx-rich-editor #editor
mode="Markdown"
[formControl]="$any(fieldForm)"
[folderId]="field.rawProperties.folderId" [folderId]="field.rawProperties.folderId"
[language]="language" [language]="language"
[languages]="languages" [languages]="languages"
[schemaIds]="field.rawProperties.schemaIds"> [schemaIds]="field.rawProperties.schemaIds">
</sqx-markdown-editor> </sqx-rich-editor>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'StockPhoto'"> <ng-container *ngSwitchCase="'StockPhoto'">
<sqx-stock-photo-editor [formControl]="$any(fieldForm)"></sqx-stock-photo-editor> <sqx-stock-photo-editor [formControl]="$any(fieldForm)"></sqx-stock-photo-editor>

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

@ -42,6 +42,9 @@ export class IFrameEditorComponent extends StatefulComponent<State> implements
@Input({ transform: booleanAttribute }) @Input({ transform: booleanAttribute })
public isExpanded = false; public isExpanded = false;
@Input({ required: true })
public schemaIds?: ReadonlyArray<string>;
@Input({ required: true }) @Input({ required: true })
public context: any = {}; public context: any = {};
@ -213,7 +216,7 @@ export class IFrameEditorComponent extends StatefulComponent<State> implements
if (correlationId) { if (correlationId) {
this.contentsQuery = query; this.contentsQuery = query;
this.contentsCorrelationId = correlationId; this.contentsCorrelationId = correlationId;
this.contentsSchemas = schemas; this.contentsSchemas = this.schemaIds && this.schemaIds.length > 0 ? this.schemaIds : schemas;
this.contentsDialog.show(); this.contentsDialog.show();
} }
} }

7
frontend/src/app/framework/angular/forms/file-drop.directive.ts

@ -121,7 +121,6 @@ export class FileDropDirective {
} }
const files: File[] = []; const files: File[] = [];
const items = getItems(dataTransfer); const items = getItems(dataTransfer);
// Loop over files first, otherwise Chromes deletes them in the async call. // Loop over files first, otherwise Chromes deletes them in the async call.
@ -139,7 +138,7 @@ export class FileDropDirective {
if (webkitEntry && webkitEntry.isDirectory) { if (webkitEntry && webkitEntry.isDirectory) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await this.transferWebkitTree(webkitEntry, files); await this.traverseWebkitTree(webkitEntry, files);
} }
} }
} }
@ -151,7 +150,7 @@ export class FileDropDirective {
return files; return files;
} }
private async transferWebkitTree(item: any, files: File[]) { private async traverseWebkitTree(item: any, files: File[]) {
if (item.isFile) { if (item.isFile) {
const file = await getFilePromise(item); const file = await getFilePromise(item);
@ -163,7 +162,7 @@ export class FileDropDirective {
for (const entry of entries) { for (const entry of entries) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await this.transferWebkitTree(entry, files); await this.traverseWebkitTree(entry, files);
} }
} }
} }

18
frontend/src/app/shared/components/forms/markdown-editor.component.html

@ -1,18 +0,0 @@
<div #container class="drop-container"
(sqxDropFile)="insertFiles($event)"
sqxDropOnlyImages="true">
<div #inner [class.fullscreen]="snapshot.isFullscreen">
<textarea class="form-control editor" #editor></textarea>
</div>
</div>
<sqx-asset-selector *sqxModal="assetsDialog"
(select)="insertAssets($event)">
</sqx-asset-selector>
<sqx-content-selector *sqxModal="contentsDialog"
[language]="language"
[languages]="languages"
[schemaIds]="schemaIds"
(select)="insertContents($event)">
</sqx-content-selector>

18
frontend/src/app/shared/components/forms/markdown-editor.component.scss

@ -1,18 +0,0 @@
@import 'mixins';
@import 'vars';
$background: #fff;
:host ::ng-deep {
/* stylelint-disable-next-line selector-class-pattern */
.CodeMirror {
height: 300px;
padding-bottom: 10px;
padding-top: 10px;
}
}
.fullscreen {
@include fixed(0, 0, 0, 0);
z-index: 1040;
}

363
frontend/src/app/shared/components/forms/markdown-editor.component.ts

@ -1,363 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { AfterViewInit, booleanAttribute, ChangeDetectionStrategy, Component, ElementRef, forwardRef, Input, Renderer2, ViewChild } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { marked } from 'marked';
import { ApiUrlConfig, AssetDto, AssetUploaderState, ContentDto, DialogModel, getContentValue, LanguageDto, ResourceLoaderService, StatefulControlComponent, Types, UploadCanceled } from '@app/shared/internal';
declare const SimpleMDE: any;
export const SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => MarkdownEditorComponent), multi: true,
};
interface State {
// True, when the editor is shown as fullscreen.
isFullscreen: boolean;
}
@Component({
selector: 'sqx-markdown-editor',
styleUrls: ['./markdown-editor.component.scss'],
templateUrl: './markdown-editor.component.html',
providers: [
SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MarkdownEditorComponent extends StatefulControlComponent<State, string> implements AfterViewInit {
private simplemde: any;
private value?: string;
@Input()
public schemaIds?: ReadonlyArray<string>;
@Input()
public language!: LanguageDto;
@Input()
public languages!: ReadonlyArray<LanguageDto>;
@Input()
public folderId?: string;
@Input({ transform: booleanAttribute })
public set disabled(value: boolean | undefined | null) {
this.setDisabledState(value === true);
}
@ViewChild('editor', { static: false })
public editor!: ElementRef;
@ViewChild('container', { static: false })
public container!: ElementRef;
@ViewChild('inner', { static: false })
public inner!: ElementRef;
public assetsDialog = new DialogModel();
public contentsDialog = new DialogModel();
constructor(
private readonly apiUrl: ApiUrlConfig,
private readonly assetUploader: AssetUploaderState,
private readonly renderer: Renderer2,
private readonly resourceLoader: ResourceLoaderService,
) {
super({ isFullscreen: false });
}
public writeValue(obj: any) {
const newValue = Types.isString(obj) ? obj : '';
if (newValue === this.value) {
return;
}
this.value = newValue;
if (this.simplemde) {
this.simplemde.value(this.value);
}
}
public onDisabled(isDisabled: boolean) {
if (this.simplemde) {
this.simplemde.codemirror.setOption('readOnly', isDisabled);
}
}
private showAssetSelector = () => {
if (this.snapshot.isDisabled) {
return;
}
this.assetsDialog.show();
};
private showContentsSelector = () => {
if (this.snapshot.isDisabled) {
return;
}
this.contentsDialog.show();
};
public ngAfterViewInit() {
Promise.all([
this.resourceLoader.loadLocalStyle('dependencies/simplemde/simplemde.min.css'),
this.resourceLoader.loadLocalStyle('dependencies/font-awesome/css/font-awesome.min.css'),
this.resourceLoader.loadLocalScript('dependencies/simplemde/simplemde.min.js'),
]).then(() => {
const options = {
previewRender: (text: string) => {
return marked(text, { pedantic: true, mangle: false, headerIds: false });
},
autoDownloadFontAwesome: true,
spellChecker: false,
status: ['lines', 'words', 'cursor'],
toolbar: [
{
name: 'bold',
action: SimpleMDE.toggleBold,
className: 'fa fa-bold',
title: 'Bold',
}, {
name: 'italic',
action: SimpleMDE.toggleItalic,
className: 'fa fa-italic',
title: 'Italic',
}, {
name: 'heading',
action: SimpleMDE.toggleHeadingSmaller,
className: 'fa fa-header',
title: 'Heading',
}, {
name: 'quote',
action: SimpleMDE.toggleBlockquote,
className: 'fa fa-quote-left',
title: 'Quote',
}, {
name: 'unordered-list',
action: SimpleMDE.toggleUnorderedList,
className: 'fa fa-list-ul',
title: 'Generic List',
}, {
name: 'ordered-list',
action: SimpleMDE.toggleOrderedList,
className: 'fa fa-list-ol',
title: 'Numbered List',
},
'|',
{
name: 'link',
action: SimpleMDE.drawLink,
className: 'fa fa-link',
title: 'Create Link',
}, {
name: 'image',
action: SimpleMDE.drawImage,
className: 'fa fa-picture-o',
title: 'Insert Image',
},
'|',
{
name: 'preview',
action: SimpleMDE.togglePreview,
className: 'fa fa-eye no-disable',
title: 'Toggle Preview',
}, {
name: 'fullscreen',
action: SimpleMDE.toggleFullScreen,
className: 'fa fa-arrows-alt no-disable no-mobile',
title: 'Toggle Fullscreen',
}, {
name: 'side-by-side',
action: SimpleMDE.toggleSideBySide,
className: 'fa fa-columns no-disable no-mobile',
title: 'Toggle Side by Side',
},
'|',
{
name: 'guide',
action: 'https://simplemde.com/markdown-guide',
className: 'fa fa-question-circle',
title: 'Markdown Guide',
},
'|',
{
name: 'assets',
action: this.showAssetSelector,
className: 'icon-assets icon-bold',
title: 'Insert Assets',
},
],
element: this.editor.nativeElement,
};
if (this.schemaIds && this.schemaIds.length > 0) {
options.toolbar.push({
name: 'contents',
action: this.showContentsSelector,
className: 'icon-contents icon-bold',
title: 'Insert Contents',
});
}
this.simplemde = new SimpleMDE(options);
this.simplemde.value(this.value || '');
this.simplemde.codemirror.setOption('readOnly', this.snapshot.isDisabled);
this.simplemde.codemirror.on('change', () => {
const value = this.simplemde.value();
if (this.value !== value) {
this.value = value;
this.callChange(value);
}
});
this.simplemde.codemirror.on('refresh', () => {
const isFullscreen = this.simplemde.isFullscreenActive();
this.toggleFullscreen(isFullscreen);
});
this.simplemde.codemirror.on('blur', () => {
this.callTouched();
});
});
}
public insertAssets(assets: ReadonlyArray<AssetDto>) {
const content = this.buildAssetsMarkup(assets);
if (content.length > 0) {
this.simplemde.codemirror.replaceSelection(content);
}
this.assetsDialog.hide();
}
public insertContents(contents: ReadonlyArray<ContentDto>) {
const content = this.buildContentsMarkup(contents);
if (content.length > 0) {
this.simplemde.codemirror.replaceSelection(content);
}
this.contentsDialog.hide();
}
public insertFiles(files: ReadonlyArray<File>) {
const doc = this.simplemde.codemirror.getDoc();
for (const file of files) {
this.uploadFile(doc, file);
}
}
private uploadFile(doc: any, file: File) {
if (this.snapshot.isDisabled) {
return;
}
const uploadCursor = doc.getCursor();
const uploadText = `![Uploading file...${new Date()}]()`;
doc.replaceSelection(uploadText);
const replaceText = (replacement: string) => {
const cursor = doc.getCursor();
const text = doc.getValue().replace(uploadText, replacement);
doc.setValue(text);
if (uploadCursor && uploadCursor.line === cursor.line) {
const offset = replacement.length - uploadText.length;
doc.setCursor({ line: cursor.line, ch: cursor.ch + offset });
} else {
doc.setCursor(cursor);
}
};
this.assetUploader.uploadFile(file, this.folderId)
.subscribe({
next: asset => {
if (Types.is(asset, AssetDto)) {
replaceText(this.buildAssetMarkup(asset));
}
},
error: error => {
if (!Types.is(error, UploadCanceled)) {
replaceText('FAILED');
}
},
});
}
private toggleFullscreen(isFullscreen: boolean) {
let target = this.container.nativeElement;
if (isFullscreen) {
target = document.body;
}
this.renderer.appendChild(target, this.inner.nativeElement);
this.next({ isFullscreen });
}
private buildAssetsMarkup(assets: ReadonlyArray<AssetDto>) {
let markup = '';
for (const asset of assets) {
markup += this.buildAssetMarkup(asset);
}
return markup;
}
private buildContentsMarkup(contents: ReadonlyArray<ContentDto>) {
let markup = '';
for (const content of contents) {
markup += this.buildContentMarkup(content);
}
return markup;
}
private buildContentMarkup(content: ContentDto) {
const name =
content.referenceFields
.map(f => getContentValue(content, this.language, f, false))
.map(v => v.formatted)
.defined()
.join(', ')
|| 'content';
return `[${name}](${this.apiUrl.buildUrl(content._links['self'].href)})`;
}
private buildAssetMarkup(asset: AssetDto) {
const name = asset.fileNameWithoutExtension;
if (asset.type === 'Image' || asset.mimeType === 'image/svg+xml' || asset.fileName.endsWith('.svg')) {
return `![${name}](${asset.fullUrl(this.apiUrl)} '${name}')`;
} else if (asset.type === 'Video') {
return `[${name}](${asset.fullUrl(this.apiUrl)})`;
} else {
return `[${name}](${asset.fullUrl(this.apiUrl)})`;
}
}
}

4
frontend/src/app/shared/components/forms/rich-editor.component.html

@ -1,6 +1,4 @@
<div class="drop-container" (sqxDropFile)="insertFiles($event)" sqxDropOnlyImages="true"> <div class="editor" #editor></div>
<div class="editor" #editor></div>
</div>
<sqx-asset-selector *sqxModal="assetsDialog" <sqx-asset-selector *sqxModal="assetsDialog"
(select)="insertAssets($event)"> (select)="insertAssets($event)">

10
frontend/src/app/shared/components/forms/rich-editor.component.scss

@ -1,10 +1,2 @@
@import 'mixins'; @import 'mixins';
@import 'vars'; @import 'vars';
.drop-container {
min-height: 401px;
}
.editor {
height: 400px;
}

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

@ -8,9 +8,7 @@
import { AfterViewInit, booleanAttribute, ChangeDetectionStrategy, Component, ElementRef, EventEmitter, forwardRef, Input, OnDestroy, Output, ViewChild } from '@angular/core'; import { AfterViewInit, booleanAttribute, ChangeDetectionStrategy, Component, ElementRef, EventEmitter, forwardRef, Input, OnDestroy, Output, ViewChild } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { ContentDto } from '@app/shared'; import { ContentDto } from '@app/shared';
import { ApiUrlConfig, AssetDto, AssetUploaderState, DialogModel, getContentValue, LanguageDto, ResourceLoaderService, StatefulControlComponent, Types, UploadCanceled } from '@app/shared/internal'; import { ApiUrlConfig, AssetDto, AssetUploaderState, DialogModel, getContentValue, LanguageDto, ResourceLoaderService, StatefulControlComponent, Types } from '@app/shared/internal';
declare const tinymce: any;
export const SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR: any = { export const SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RichEditorComponent), multi: true, provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RichEditorComponent), multi: true,
@ -26,8 +24,10 @@ export const SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class RichEditorComponent extends StatefulControlComponent<{}, string> implements AfterViewInit, OnDestroy { export class RichEditorComponent extends StatefulControlComponent<{}, string> implements AfterViewInit, OnDestroy {
private tinyEditor: any; private editorWrapper: any;
private value?: string; private value?: string;
private currentContents?: ResolvablePromise<any>;
private currentAssets?: ResolvablePromise<any>;
@Output() @Output()
public assetPluginClick = new EventEmitter<any>(); public assetPluginClick = new EventEmitter<any>();
@ -44,6 +44,9 @@ export class RichEditorComponent extends StatefulControlComponent<{}, string> im
@Input() @Input()
public folderId = ''; public folderId = '';
@Input({ required: true })
public mode: SquidexEditorMode = 'Html';
@Input({ transform: booleanAttribute }) @Input({ transform: booleanAttribute })
public set disabled(value: boolean | undefined | null) { public set disabled(value: boolean | undefined | null) {
this.setDisabledState(value === true); this.setDisabledState(value === true);
@ -65,23 +68,48 @@ export class RichEditorComponent extends StatefulControlComponent<{}, string> im
} }
public ngOnDestroy() { public ngOnDestroy() {
if (this.tinyEditor) { if (this.editorWrapper) {
this.tinyEditor.destroy(); this.editorWrapper.destroy?.();
this.tinyEditor = null; this.editorWrapper = null;
} }
} }
public ngAfterViewInit() { public ngAfterViewInit() {
this.resourceLoader.loadLocalScript('dependencies/tinymce/tinymce.min.js').then(() => { this.resourceLoader.loadLocalStyle('editor/squidex-editor.css');
const timer = setInterval(() => { this.resourceLoader.loadLocalScript('editor/squidex-editor.js').then(() => {
const target = this.editor.nativeElement; this.editorWrapper = new SquidexEditorWrapper(this.editor.nativeElement, {
value: this.value || '',
isDisabled: this.snapshot.isDisabled,
onSelectAssets: async () => {
if (this.snapshot.isDisabled) {
return;
}
this.currentAssets = new ResolvablePromise<any>();
this.assetsDialog.show();
return await this.currentAssets.promise;
},
onSelectContents: async () => {
if (this.snapshot.isDisabled) {
return;
}
if (document.body.contains(target)) { this.currentContents = new ResolvablePromise<any>();
tinymce.init(this.getEditorOptions(target)); this.contentsDialog.show();
clearInterval(timer); return await this.currentContents.promise;
} },
}, 10); onUpload: (requests: UploadRequest[]) => {
return this.uploadFiles(requests);
},
onChange: (value: string | undefined) => {
this.callChange(value);
},
canSelectAssets: true,
canSelectContents: !!this.schemaIds,
mode: this.mode,
});
}); });
} }
@ -93,273 +121,87 @@ export class RichEditorComponent extends StatefulControlComponent<{}, string> im
}); });
} }
private showAssetsSelector = () => {
if (this.snapshot.isDisabled) {
return;
}
this.assetsDialog.show();
};
private showContentsSelector = () => {
if (this.snapshot.isDisabled) {
return;
}
this.contentsDialog.show();
};
private getEditorOptions(target: any): any {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
return {
...DEFAULT_PROPS,
images_upload_handler: (blob: any, success: (url: string) => void, failure: (message: string) => void) => {
const file = new File([blob.blob()], blob.filename(), { lastModified: new Date().getTime() });
self.assetUploader.uploadFile(file, this.folderId)
.subscribe({
next: asset => {
if (Types.is(asset, AssetDto)) {
success(asset.fullUrl(self.apiUrl));
}
},
error: error => {
if (!Types.is(error, UploadCanceled)) {
failure('Failed');
}
},
});
},
setup: (editor: any) => {
editor.ui.registry.addButton('assets', {
onAction: self.showAssetsSelector,
icon: 'gallery',
text: '',
tooltip: 'Insert Assets',
});
if (this.schemaIds && this.schemaIds.length > 0) {
editor.ui.registry.addButton('contents', {
onAction: self.showContentsSelector,
icon: 'duplicate',
text: '',
tooltip: 'Insert Contents',
});
}
editor.on('init', () => {
self.tinyEditor = editor;
self.setContent();
self.setReadOnly();
});
editor.on('change', () => {
self.onValueChanged();
});
editor.on('paste', (event: ClipboardEvent) => {
let hasFileDropped = false;
if (event.clipboardData) {
for (let i = 0; i < event.clipboardData.items.length; i++) {
const file = event.clipboardData.items[i].getAsFile();
if (file) {
self.uploadFile(file);
hasFileDropped = true;
}
}
}
if (!hasFileDropped) {
self.onValueChanged();
} else {
return false;
}
return undefined;
});
editor.on('drop', (event: DragEvent) => {
let hasFileDropped = false;
if (event.dataTransfer) {
for (let i = 0; i < event.dataTransfer.files.length; i++) {
const file = event.dataTransfer.files.item(i);
if (file) {
self.uploadFile(file);
hasFileDropped = true;
}
}
}
if (!hasFileDropped) {
self.onValueChanged();
}
return false;
});
editor.on('blur', () => {
self.callTouched();
});
},
target,
};
}
private onValueChanged() {
const value = this.tinyEditor.getContent();
if (this.value !== value) {
this.value = value;
this.callChange(value);
}
}
public writeValue(obj: any) { public writeValue(obj: any) {
const newValue = Types.isString(obj) ? obj : ''; this.editorWrapper?.setValue(obj);
if (newValue == this.value) {
return;
}
this.value = newValue;
if (this.tinyEditor && this.tinyEditor.initialized) {
this.setContent();
}
} }
public onDisabled() { public onDisabled() {
if (this.tinyEditor && this.tinyEditor.initialized) { this.editorWrapper?.setIsDisabled(this.snapshot.isDisabled);
this.setReadOnly();
}
}
private setContent() {
this.tinyEditor.setContent(this.value || '');
}
private setReadOnly() {
this.tinyEditor.setMode(this.snapshot.isDisabled ? 'readonly' : 'design');
} }
public insertAssets(assets: ReadonlyArray<AssetDto>) { public insertAssets(assets: ReadonlyArray<AssetDto>) {
const content = this.buildAssetsMarkup(assets); this.assetsDialog.hide();
if (content.length > 0) { if (!this.currentAssets) {
this.tinyEditor.execCommand('mceInsertContent', false, content); return;
} }
this.assetsDialog.hide(); const items = assets.map(a => this.buildAsset(a));
this.currentAssets.resolve(items);
this.currentAssets = undefined;
} }
public insertContents(contents: ReadonlyArray<ContentDto>) { public insertContents(contents: ReadonlyArray<ContentDto>) {
const content = this.buildContentsMarkup(contents);
if (content.length > 0) {
this.tinyEditor.execCommand('mceInsertContent', false, content);
}
this.contentsDialog.hide(); this.contentsDialog.hide();
} if (!this.currentContents) {
return;
public insertFiles(files: ReadonlyArray<File>) {
for (const file of files) {
this.uploadFile(file);
} }
}
private uploadFile(file: File) {
const uploadText = `[Uploading file...${new Date()}]`;
this.tinyEditor.execCommand('mceInsertContent', false, uploadText); const items = contents.map(c => this.buildContent(c));
const replaceText = (replacement: string) => { this.currentContents.resolve(items);
const content = this.tinyEditor.getContent().replace(uploadText, replacement); this.currentContents = undefined;
}
this.tinyEditor.setContent(content); private uploadFiles(requests: UploadRequest[]) {
const uploadFile = (request: UploadRequest) => {
return new Promise<any>((resolve, reject) => {
this.assetUploader.uploadFile(request.file, this.folderId)
.subscribe({
next: value => {
if (Types.is(value, AssetDto)) {
resolve(this.buildAsset(value));
} else {
request.progress(value / 100);
}
},
error: reject,
});
});
}; };
this.assetUploader.uploadFile(file, this.folderId) return requests.map(r => () => uploadFile(r));
.subscribe({
next: asset => {
if (Types.is(asset, AssetDto)) {
replaceText(this.buildAssetMarkup(asset));
}
},
error: error => {
if (!Types.is(error, UploadCanceled)) {
replaceText('FAILED');
}
},
});
} }
private buildAssetsMarkup(assets: ReadonlyArray<AssetDto>) { private buildAsset(asset: AssetDto) {
let markup = ''; return { type: asset.mimeType, src: asset.fullUrl(this.apiUrl), fileName: asset.fileName };
for (const asset of assets) {
markup += this.buildAssetMarkup(asset);
}
return markup;
} }
private buildContentsMarkup(contents: ReadonlyArray<ContentDto>) { private buildContent(content: ContentDto) {
let markup = ''; return { url: this.apiUrl.buildUrl(content._links['self'].href), name: buildContentName(content, this.language) };
for (const content of contents) {
markup += this.buildContentMarkup(content);
}
return markup;
} }
}
private buildContentMarkup(content: ContentDto) { function buildContentName(content: ContentDto, language: LanguageDto) {
const name = const name =
content.referenceFields content.referenceFields
.map(f => getContentValue(content, this.language, f, false)) .map(f => getContentValue(content, language, f, false))
.map(v => v.formatted) .map(v => v.formatted)
.defined() .defined()
.join(', ') .join(', ');
|| 'content';
return `<a href="${this.apiUrl.buildUrl(content._links['self'].href)}" alt="${name}">${name}</a>`; return name || 'Content';
} }
private buildAssetMarkup(asset: AssetDto) { class ResolvablePromise<T> {
const name = asset.fileNameWithoutExtension; private resolver?: (value: T) => void;
if (asset.type === 'Image' || asset.mimeType === 'image/svg+xml' || asset.fileName.endsWith('.svg')) { public readonly promise = new Promise<T>(resolve => {
return `<img src="${asset.fullUrl(this.apiUrl)}" alt="${name}" />`; this.resolver = resolve;
} else if (asset.type === 'Video') { });
return `<video src="${asset.fullUrl(this.apiUrl)}" />`;
} else {
return `<a href="${asset.fullUrl(this.apiUrl)}" alt="${name}">${name}</a>`;
}
}
}
const DEFAULT_PROPS = { public resolve(value: T) {
convert_fonts_to_spans: true, this.resolver?.(value);
convert_urls: false, }
paste_data_images: true, }
plugins: 'code image media link lists advlist paste',
min_height: 400,
max_height: 800,
removed_menuitems: 'newdocument',
resize: true,
toolbar: 'undo redo | styleselect | bold italic | alignleft aligncenter | bullist numlist outdent indent | link image media | assets contents',
};

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

@ -43,7 +43,6 @@ export * from './components/contents/content-value-editor.component';
export * from './components/contents/content-value.component'; export * from './components/contents/content-value.component';
export * from './components/contents/translation-status.component'; export * from './components/contents/translation-status.component';
export * from './components/forms/geolocation-editor.component'; export * from './components/forms/geolocation-editor.component';
export * from './components/forms/markdown-editor.component';
export * from './components/forms/rich-editor.component'; export * from './components/forms/rich-editor.component';
export * from './components/help/help-markdown.pipe'; export * from './components/help/help-markdown.pipe';
export * from './components/help/help.component'; export * from './components/help/help.component';

13
frontend/src/app/shared/guards/must-be-authenticated.guard.spec.ts

@ -6,6 +6,7 @@
*/ */
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import { TestBed } from '@angular/core/testing';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { firstValueFrom, of } from 'rxjs'; import { firstValueFrom, of } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq'; import { IMock, It, Mock, Times } from 'typemoq';
@ -24,13 +25,15 @@ describe('MustBeAuthenticatedGuard', () => {
uiOptions.value.redirectToLogin = false; uiOptions.value.redirectToLogin = false;
location = Mock.ofType<Location>(); location = Mock.ofType<Location>();
location.setup(x => x.path(true)).returns(() => '/my-path');
location.setup(x => x.path(true))
.returns(() => '/my-path');
router = Mock.ofType<Router>(); router = Mock.ofType<Router>();
authService = Mock.ofType<AuthService>();
authGuard = new MustBeAuthenticatedGuard(authService.object, location.object, router.object, uiOptions); TestBed.configureTestingModule({ providers: [{ provide: UIOptions, useValue: uiOptions }] });
TestBed.runInInjectionContext(() => {
authService = Mock.ofType<AuthService>();
authGuard = new MustBeAuthenticatedGuard(authService.object, location.object, router.object);
});
}); });
it('should navigate to default page if not authenticated', async () => { it('should navigate to default page if not authenticated', async () => {

4
frontend/src/app/shared/guards/must-be-authenticated.guard.ts

@ -15,7 +15,7 @@ import { AuthService } from './../services/auth.service';
@Injectable() @Injectable()
export class MustBeAuthenticatedGuard { export class MustBeAuthenticatedGuard {
private readonly redirectToLogin = inject(UIOptions).value.redirectToLogin; private readonly uiOptions = inject(UIOptions);
constructor( constructor(
private readonly authService: AuthService, private readonly authService: AuthService,
@ -34,7 +34,7 @@ export class MustBeAuthenticatedGuard {
const redirectPath = this.location.path(true); const redirectPath = this.location.path(true);
if (this.redirectToLogin) { if (this.uiOptions.value.redirectToLogin) {
this.authService.loginRedirect(redirectPath); this.authService.loginRedirect(redirectPath);
} else { } else {
this.router.navigate([''], { queryParams: { redirectPath } }); this.router.navigate([''], { queryParams: { redirectPath } });

13
frontend/src/app/shared/guards/must-be-not-authenticated.guard.spec.ts

@ -6,6 +6,7 @@
*/ */
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import { TestBed } from '@angular/core/testing';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { firstValueFrom, of } from 'rxjs'; import { firstValueFrom, of } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq'; import { IMock, It, Mock, Times } from 'typemoq';
@ -24,13 +25,15 @@ describe('MustBeNotAuthenticatedGuard', () => {
uiOptions.value.redirectToLogin = false; uiOptions.value.redirectToLogin = false;
location = Mock.ofType<Location>(); location = Mock.ofType<Location>();
location.setup(x => x.path(true)).returns(() => '/my-path');
location.setup(x => x.path(true))
.returns(() => '/my-path');
router = Mock.ofType<Router>(); router = Mock.ofType<Router>();
authService = Mock.ofType<AuthService>();
authGuard = new MustBeNotAuthenticatedGuard(authService.object, location.object, router.object, uiOptions); TestBed.configureTestingModule({ providers: [{ provide: UIOptions, useValue: uiOptions }] });
TestBed.runInInjectionContext(() => {
authService = Mock.ofType<AuthService>();
authGuard = new MustBeNotAuthenticatedGuard(authService.object, location.object, router.object);
});
}); });
it('should navigate to app page if authenticated', async () => { it('should navigate to app page if authenticated', async () => {

4
frontend/src/app/shared/guards/must-be-not-authenticated.guard.ts

@ -15,7 +15,7 @@ import { AuthService } from './../services/auth.service';
@Injectable() @Injectable()
export class MustBeNotAuthenticatedGuard { export class MustBeNotAuthenticatedGuard {
private readonly redirectToLogin = inject(UIOptions).value.redirectToLogin; private readonly options = inject(UIOptions);
constructor( constructor(
private readonly authService: AuthService, private readonly authService: AuthService,
@ -25,7 +25,7 @@ export class MustBeNotAuthenticatedGuard {
} }
public canActivate(snapshot: ActivatedRouteSnapshot): Observable<boolean> { public canActivate(snapshot: ActivatedRouteSnapshot): Observable<boolean> {
const redirect = this.redirectToLogin && !snapshot.queryParams.logout; const redirect = this.options.value.redirectToLogin && !snapshot.queryParams.logout;
return this.authService.userChanges.pipe( return this.authService.userChanges.pipe(
take(1), take(1),

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

@ -13,7 +13,7 @@ import { MentionModule } from 'angular-mentions';
import { NgChartsModule } from 'ng2-charts'; import { NgChartsModule } from 'ng2-charts';
import { NgxDocViewerModule } from 'ngx-doc-viewer'; import { NgxDocViewerModule } from 'ngx-doc-viewer';
import { SqxFrameworkModule } from '@app/framework'; import { SqxFrameworkModule } from '@app/framework';
import { ApiCallsCardComponent, ApiCallsSummaryCardComponent, ApiPerformanceCardComponent, ApiTrafficCardComponent, ApiTrafficSummaryCardComponent, AppFormComponent, AppLanguagesService, AppMustExistGuard, AppsService, AppsState, AssetComponent, AssetDialogComponent, AssetFolderComponent, AssetFolderDialogComponent, AssetFolderDropdownComponent, AssetFolderDropdownItemComponent, AssetHistoryComponent, AssetPathComponent, AssetPreviewUrlPipe, AssetScriptsState, AssetSelectorComponent, AssetsListComponent, AssetsService, AssetsState, AssetTextEditorComponent, AssetUploaderComponent, AssetUploaderState, AssetUploadsCountCardComponent, AssetUploadsSizeCardComponent, AssetUploadsSizeSummaryCardComponent, AssetUrlPipe, AuthInterceptor, AuthService, AutoSaveService, BackupsService, BackupsState, buildTasks, ClientsService, ClientsState, CommentComponent, CommentsComponent, CommentsService, ContentListCellDirective, ContentListCellResizeDirective, ContentListFieldComponent, ContentListHeaderComponent, ContentListWidthDirective, ContentMustExistGuard, ContentsColumnsPipe, ContentSelectorComponent, ContentSelectorItemComponent, ContentsService, ContentsState, ContentStatusComponent, ContentValueComponent, ContentValueEditorComponent, ContributorsService, ContributorsState, FileIconPipe, FilterComparisonComponent, FilterLogicalComponent, FilterNodeComponent, FilterOperatorPipe, GeolocationEditorComponent, HelpComponent, HelpMarkdownPipe, HelpService, HistoryComponent, HistoryListComponent, HistoryMessagePipe, HistoryService, IFrameCardComponent, ImageCropperComponent, ImageFocusPointComponent, LanguagesService, LanguagesState, LoadAppsGuard, LoadLanguagesGuard, LoadSchemasGuard, LoadSettingsGuard, LoadTeamsGuard, MarkdownEditorComponent, MustBeAuthenticatedGuard, MustBeNotAuthenticatedGuard, NewsService, NotifoComponent, PlansService, PlansState, PreviewableType, QueryComponent, QueryListComponent, QueryPathComponent, RandomCatCardComponent, RandomDogCardComponent, ReferenceInputComponent, RichEditorComponent, RolesService, RolesState, RuleEventsState, RuleMustExistGuard, RuleSimulatorState, RulesService, RulesState, SavedQueriesComponent, SchemaCategoryComponent, SchemaMustExistGuard, SchemaMustExistPublishedGuard, SchemaMustNotBeSingletonGuard, SchemasService, SchemasState, SchemaTagSource, ScriptNamePipe, SearchFormComponent, SearchService, SortingComponent, StockPhotoService, SupportCardComponent, TableHeaderComponent, TASK_CONFIGURATION, TeamFormComponent, TeamMustExistGuard, TeamsService, TeamsState, TemplatesService, TemplatesState, TourGuideComponent, TourHintDirective, TourState, TranslationsService, TranslationStatusComponent, UIService, UIState, UnsetAppGuard, UnsetTeamGuard, UsagesService, UserDtoPicture, UserIdPicturePipe, UserNamePipe, UserNameRefPipe, UserPicturePipe, UserPictureRefPipe, UsersProviderService, UsersService, WatchingUsersComponent, WorkflowsService, WorkflowsState } from './declarations'; import { ApiCallsCardComponent, ApiCallsSummaryCardComponent, ApiPerformanceCardComponent, ApiTrafficCardComponent, ApiTrafficSummaryCardComponent, AppFormComponent, AppLanguagesService, AppMustExistGuard, AppsService, AppsState, AssetComponent, AssetDialogComponent, AssetFolderComponent, AssetFolderDialogComponent, AssetFolderDropdownComponent, AssetFolderDropdownItemComponent, AssetHistoryComponent, AssetPathComponent, AssetPreviewUrlPipe, AssetScriptsState, AssetSelectorComponent, AssetsListComponent, AssetsService, AssetsState, AssetTextEditorComponent, AssetUploaderComponent, AssetUploaderState, AssetUploadsCountCardComponent, AssetUploadsSizeCardComponent, AssetUploadsSizeSummaryCardComponent, AssetUrlPipe, AuthInterceptor, AuthService, AutoSaveService, BackupsService, BackupsState, buildTasks, ClientsService, ClientsState, CommentComponent, CommentsComponent, CommentsService, ContentListCellDirective, ContentListCellResizeDirective, ContentListFieldComponent, ContentListHeaderComponent, ContentListWidthDirective, ContentMustExistGuard, ContentsColumnsPipe, ContentSelectorComponent, ContentSelectorItemComponent, ContentsService, ContentsState, ContentStatusComponent, ContentValueComponent, ContentValueEditorComponent, ContributorsService, ContributorsState, FileIconPipe, FilterComparisonComponent, FilterLogicalComponent, FilterNodeComponent, FilterOperatorPipe, GeolocationEditorComponent, HelpComponent, HelpMarkdownPipe, HelpService, HistoryComponent, HistoryListComponent, HistoryMessagePipe, HistoryService, IFrameCardComponent, ImageCropperComponent, ImageFocusPointComponent, LanguagesService, LanguagesState, LoadAppsGuard, LoadLanguagesGuard, LoadSchemasGuard, LoadSettingsGuard, LoadTeamsGuard, MustBeAuthenticatedGuard, MustBeNotAuthenticatedGuard, NewsService, NotifoComponent, PlansService, PlansState, PreviewableType, QueryComponent, QueryListComponent, QueryPathComponent, RandomCatCardComponent, RandomDogCardComponent, ReferenceInputComponent, RichEditorComponent, RolesService, RolesState, RuleEventsState, RuleMustExistGuard, RuleSimulatorState, RulesService, RulesState, SavedQueriesComponent, SchemaCategoryComponent, SchemaMustExistGuard, SchemaMustExistPublishedGuard, SchemaMustNotBeSingletonGuard, SchemasService, SchemasState, SchemaTagSource, ScriptNamePipe, SearchFormComponent, SearchService, SortingComponent, StockPhotoService, SupportCardComponent, TableHeaderComponent, TASK_CONFIGURATION, TeamFormComponent, TeamMustExistGuard, TeamsService, TeamsState, TemplatesService, TemplatesState, TourGuideComponent, TourHintDirective, TourState, TranslationsService, TranslationStatusComponent, UIService, UIState, UnsetAppGuard, UnsetTeamGuard, UsagesService, UserDtoPicture, UserIdPicturePipe, UserNamePipe, UserNameRefPipe, UserPicturePipe, UserPictureRefPipe, UsersProviderService, UsersService, WatchingUsersComponent, WorkflowsService, WorkflowsState } from './declarations';
@NgModule({ @NgModule({
imports: [ imports: [
@ -75,7 +75,6 @@ import { ApiCallsCardComponent, ApiCallsSummaryCardComponent, ApiPerformanceCard
IFrameCardComponent, IFrameCardComponent,
ImageCropperComponent, ImageCropperComponent,
ImageFocusPointComponent, ImageFocusPointComponent,
MarkdownEditorComponent,
NotifoComponent, NotifoComponent,
PreviewableType, PreviewableType,
QueryComponent, QueryComponent,
@ -147,7 +146,6 @@ import { ApiCallsCardComponent, ApiCallsSummaryCardComponent, ApiPerformanceCard
HistoryListComponent, HistoryListComponent,
HistoryMessagePipe, HistoryMessagePipe,
IFrameCardComponent, IFrameCardComponent,
MarkdownEditorComponent,
NotifoComponent, NotifoComponent,
PreviewableType, PreviewableType,
QueryListComponent, QueryListComponent,

8
frontend/src/app/shared/state/resolvers.spec.ts

@ -5,6 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { TestBed } from '@angular/core/testing';
import { firstValueFrom, of, throwError } from 'rxjs'; import { firstValueFrom, of, throwError } from 'rxjs';
import { IMock, Mock, Times } from 'typemoq'; import { IMock, Mock, Times } from 'typemoq';
import { UIOptions } from '@app/framework'; import { UIOptions } from '@app/framework';
@ -34,8 +35,11 @@ describe('ResolveContents', () => {
]; ];
beforeEach(() => { beforeEach(() => {
contentsService = Mock.ofType<ContentsService>(); TestBed.configureTestingModule({ providers: [{ provide: UIOptions, useValue: uiOptions }] });
contentsResolver = new ResolveContents(uiOptions, appsState.object, contentsService.object); TestBed.runInInjectionContext(() => {
contentsService = Mock.ofType<ContentsService>();
contentsResolver = new ResolveContents(appsState.object, contentsService.object);
});
}); });
it('should not resolve contents immediately', () => { it('should not resolve contents immediately', () => {

14
frontend/src/config/webpack.config.js

@ -15,20 +15,6 @@ module.exports = (config, _, options) => {
*/ */
config.plugins.push(new plugins.CopyPlugin({ config.plugins.push(new plugins.CopyPlugin({
patterns: [ patterns: [
{ from: './node_modules/simplemde/dist', to: 'dependencies/simplemde' },
{ from: './node_modules/tinymce/icons/default/icons.min.js', to: 'dependencies/tinymce/icons/default' },
{ from: './node_modules/tinymce/plugins/advlist', to: 'dependencies/tinymce/plugins/advlist' },
{ from: './node_modules/tinymce/plugins/code', to: 'dependencies/tinymce/plugins/code' },
{ from: './node_modules/tinymce/plugins/image', to: 'dependencies/tinymce/plugins/image' },
{ from: './node_modules/tinymce/plugins/link', to: 'dependencies/tinymce/plugins/link' },
{ from: './node_modules/tinymce/plugins/lists', to: 'dependencies/tinymce/plugins/lists' },
{ from: './node_modules/tinymce/plugins/media', to: 'dependencies/tinymce/plugins/media' },
{ from: './node_modules/tinymce/plugins/paste', to: 'dependencies/tinymce/plugins/paste' },
{ from: './node_modules/tinymce/skins', to: 'dependencies/tinymce/skins' },
{ from: './node_modules/tinymce/themes/silver', to: 'dependencies/tinymce/themes/silver' },
{ from: './node_modules/tinymce/tinymce.min.js', to: 'dependencies/tinymce' },
{ from: './node_modules/tui-code-snippet/dist', to: 'dependencies/tui-calendar' }, { from: './node_modules/tui-code-snippet/dist', to: 'dependencies/tui-calendar' },
{ from: './node_modules/tui-calendar/dist', to: 'dependencies/tui-calendar' }, { from: './node_modules/tui-calendar/dist', to: 'dependencies/tui-calendar' },

Loading…
Cancel
Save