Merge remote-tracking branch 'origin/main' into feature/property-editor-ui-config-collection
This commit is contained in:
@@ -1,46 +1,112 @@
|
||||
import { UmbEntityData } from './entity.data.js';
|
||||
import { createFileSystemTreeItem } from './utils.js';
|
||||
import { UmbData } from './data.js';
|
||||
import { createFileSystemTreeItem, createFileItemResponseModelBaseModel, createTextFileItem } from './utils.js';
|
||||
import {
|
||||
CreateTextFileViewModelBaseModel,
|
||||
ExtractRichTextStylesheetRulesRequestModel,
|
||||
ExtractRichTextStylesheetRulesResponseModel,
|
||||
FileSystemTreeItemPresentationModel,
|
||||
InterpolateRichTextStylesheetRequestModel,
|
||||
PagedFileSystemTreeItemPresentationModel,
|
||||
PagedStylesheetOverviewResponseModel,
|
||||
StylesheetResponseModel,
|
||||
UpdateStylesheetRequestModel,
|
||||
} from '@umbraco-cms/backoffice/backend-api';
|
||||
|
||||
type StylesheetDBItem = FileSystemTreeItemPresentationModel & {
|
||||
content: string;
|
||||
};
|
||||
//prettier-ignore
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
|
||||
type StylesheetDBItem = StylesheetResponseModel & FileSystemTreeItemPresentationModel & { icon?: string };
|
||||
|
||||
export const data: Array<StylesheetDBItem> = [
|
||||
{
|
||||
path: 'Stylesheet File 1.css',
|
||||
icon: 'style',
|
||||
isFolder: false,
|
||||
name: 'Stylesheet File 1.css',
|
||||
type: 'stylesheet',
|
||||
hasChildren: false,
|
||||
content: `Stylesheet content 1`,
|
||||
content: `
|
||||
/** Stylesheet 1 */
|
||||
|
||||
h1 {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
/**umb_name:bjjh*/
|
||||
h1 {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
/**umb_name:comeone*/
|
||||
h1 {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
/**umb_name:lol*/
|
||||
h1 {
|
||||
color: blue;
|
||||
}`,
|
||||
},
|
||||
{
|
||||
path: 'Stylesheet File 2.css',
|
||||
isFolder: false,
|
||||
icon: 'style',
|
||||
name: 'Stylesheet File 2.css',
|
||||
type: 'stylesheet',
|
||||
hasChildren: false,
|
||||
content: `Stylesheet content 2`,
|
||||
content: `
|
||||
/** Stylesheet 2 */
|
||||
h1 {
|
||||
color: green;
|
||||
}
|
||||
|
||||
/**umb_name:HELLO*/
|
||||
h1 {
|
||||
color: green;
|
||||
}
|
||||
|
||||
/**umb_name:SOMETHING*/
|
||||
h1 {
|
||||
color: green;
|
||||
}
|
||||
|
||||
/**umb_name:NIOCE*/
|
||||
h1 {
|
||||
color: green;
|
||||
}`,
|
||||
},
|
||||
{
|
||||
path: 'Folder 1',
|
||||
isFolder: true,
|
||||
name: 'Folder 1',
|
||||
isFolder: true,
|
||||
icon: 'folder',
|
||||
type: 'stylesheet',
|
||||
hasChildren: true,
|
||||
content: `Stylesheet content 3`,
|
||||
},
|
||||
{
|
||||
path: 'Folder 1/Stylesheet File 3.css',
|
||||
isFolder: false,
|
||||
name: 'Stylesheet File 3.css',
|
||||
type: 'stylesheet',
|
||||
hasChildren: false,
|
||||
content: `Stylesheet content 3`,
|
||||
hasChildren: true,
|
||||
isFolder: false,
|
||||
content: `h1 {
|
||||
color: pink;
|
||||
}
|
||||
|
||||
/**umb_name:ONE*/
|
||||
h1 {
|
||||
color: pink;
|
||||
}
|
||||
|
||||
/**umb_name:TWO*/
|
||||
h1 {
|
||||
color: pink;
|
||||
}
|
||||
|
||||
/**umb_name:THREE*/
|
||||
h1 {
|
||||
color: pink;
|
||||
}`,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -48,7 +114,7 @@ export const data: Array<StylesheetDBItem> = [
|
||||
// TODO: all properties are optional in the server schema. I don't think this is correct.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
class UmbStylesheetData extends UmbEntityData<StylesheetDBItem> {
|
||||
class UmbStylesheetData extends UmbData<StylesheetDBItem> {
|
||||
constructor() {
|
||||
super(data);
|
||||
}
|
||||
@@ -71,6 +137,160 @@ class UmbStylesheetData extends UmbEntityData<StylesheetDBItem> {
|
||||
const items = this.data.filter((item) => paths.includes(item.path ?? ''));
|
||||
return items.map((item) => createFileSystemTreeItem(item));
|
||||
}
|
||||
|
||||
getStylesheetItem(path: string): StylesheetDBItem | undefined {
|
||||
return createFileItemResponseModelBaseModel(this.data.find((item) => item.path === path));
|
||||
}
|
||||
|
||||
getStylesheet(path: string): StylesheetResponseModel | undefined {
|
||||
return createTextFileItem(this.data.find((item) => item.path === path));
|
||||
}
|
||||
|
||||
getAllStylesheets(): PagedStylesheetOverviewResponseModel {
|
||||
return {
|
||||
items: this.data.map((item) => createTextFileItem(item)),
|
||||
total: this.data.map((item) => !item.isFolder).length,
|
||||
};
|
||||
}
|
||||
|
||||
getFolder(path: string): StylesheetDBItem | undefined {
|
||||
return this.data.find((item) => item.path === path && item.isFolder === true);
|
||||
}
|
||||
|
||||
getRules(path: string): ExtractRichTextStylesheetRulesResponseModel {
|
||||
const regex = /\*\*\s*umb_name:\s*(?<name>[^*\r\n]*?)\s*\*\/\s*(?<selector>[^,{]*?)\s*{\s*(?<styles>.*?)\s*}/gis;
|
||||
const item = this.data.find((item) => item.path === path);
|
||||
if (!item) throw Error('item not found');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
// eslint-disable-next-line no-unsafe-optional-chaining
|
||||
const rules = [...item.content?.matchAll(regex)].map((match) => match.groups);
|
||||
return { rules };
|
||||
}
|
||||
|
||||
async extractRules({ requestBody }: { requestBody?: ExtractRichTextStylesheetRulesRequestModel }) {
|
||||
const regex = /\*\*\s*umb_name:\s*(?<name>[^*\r\n]*?)\s*\*\/\s*(?<selector>[^,{]*?)\s*{\s*(?<styles>.*?)\s*}/gis;
|
||||
|
||||
if (!requestBody) {
|
||||
throw Error('No request body');
|
||||
}
|
||||
const { content } = await requestBody;
|
||||
if (!content) return { rules: [] };
|
||||
const rules = [...content.matchAll(regex)].map((match) => match.groups);
|
||||
return { rules };
|
||||
}
|
||||
|
||||
interpolateRules({ requestBody }: { requestBody?: InterpolateRichTextStylesheetRequestModel }) {
|
||||
const regex = /\/\*\*\s*umb_name:\s*(?<name>[^*\r\n]*?)\s*\*\/\s*(?<selector>[^,{]*?)\s*{\s*(?<styles>.*?)\s*}/gis;
|
||||
if (!requestBody) {
|
||||
throw Error('No request body');
|
||||
}
|
||||
const { content, rules } = requestBody;
|
||||
if (!content && !rules) return { content: '' };
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
const cleanedContent = content?.replaceAll(regex, '');
|
||||
|
||||
const newContent = `${cleanedContent.replace(/[\r\n]+$/, '')}
|
||||
|
||||
${rules
|
||||
?.map(
|
||||
(rule) =>
|
||||
`/**umb_name:${rule.name}*/
|
||||
${rule.selector} {
|
||||
${rule.styles}
|
||||
}
|
||||
|
||||
`,
|
||||
)
|
||||
.join('')}`;
|
||||
|
||||
return { content: newContent };
|
||||
}
|
||||
|
||||
insertFolder(item: CreateTextFileViewModelBaseModel) {
|
||||
const newItem: StylesheetDBItem = {
|
||||
...item,
|
||||
path: `${item.parentPath}/${item.name}`,
|
||||
isFolder: true,
|
||||
hasChildren: false,
|
||||
type: 'stylesheet',
|
||||
icon: 'folder',
|
||||
};
|
||||
|
||||
this.insert(newItem);
|
||||
return newItem;
|
||||
}
|
||||
|
||||
insertStyleSheet(item: CreateTextFileViewModelBaseModel) {
|
||||
const newItem: StylesheetDBItem = {
|
||||
...item,
|
||||
path: `${item.parentPath}/${item.name}.css`,
|
||||
isFolder: false,
|
||||
hasChildren: false,
|
||||
type: 'stylesheet',
|
||||
icon: 'style',
|
||||
};
|
||||
|
||||
this.insert(newItem);
|
||||
return newItem;
|
||||
}
|
||||
|
||||
|
||||
insert(item: StylesheetDBItem) {
|
||||
const exits = this.data.find((i) => i.path === item.path);
|
||||
|
||||
if (exits) {
|
||||
throw new Error(`Item with path ${item.path} already exists`);
|
||||
}
|
||||
|
||||
this.data.push(item);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
updateData(updateItem: UpdateStylesheetRequestModel) {
|
||||
const itemIndex = this.data.findIndex((item) => item.path === updateItem.existingPath);
|
||||
const item = this.data[itemIndex];
|
||||
if (!item) return;
|
||||
|
||||
// TODO: revisit this code, seems like something we can solve smarter/type safer now:
|
||||
const itemKeys = Object.keys(item);
|
||||
const newItem = { ...item };
|
||||
|
||||
for (const [key] of Object.entries(updateItem)) {
|
||||
if (itemKeys.indexOf(key) !== -1) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
newItem[key] = updateItem[key];
|
||||
}
|
||||
}
|
||||
// Specific to fileSystem, we need to update path based on name:
|
||||
const dirName = updateItem.existingPath?.substring(0, updateItem.existingPath.lastIndexOf('/'));
|
||||
newItem.path = `${dirName}${dirName ? '/' : ''}${updateItem.name}`;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
this.data[itemIndex] = newItem;
|
||||
}
|
||||
|
||||
delete(paths: Array<string>) {
|
||||
const deletedPaths = this.data
|
||||
.filter((item) => {
|
||||
if (!item.path) throw new Error('Item has no path');
|
||||
paths.includes(item.path);
|
||||
})
|
||||
.map((item) => item.path);
|
||||
|
||||
this.data = this.data.filter((item) => {
|
||||
if (!item.path) throw new Error('Item has no path');
|
||||
paths.indexOf(item.path) === -1;
|
||||
});
|
||||
|
||||
return deletedPaths;
|
||||
}
|
||||
}
|
||||
|
||||
export const umbStylesheetData = new UmbStylesheetData();
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
FileSystemTreeItemPresentationModel,
|
||||
DocumentResponseModel,
|
||||
TextFileResponseModelBaseModel,
|
||||
FileItemResponseModelBaseModel,
|
||||
} from '@umbraco-cms/backoffice/backend-api';
|
||||
|
||||
export const createEntityTreeItem = (item: any): EntityTreeItemResponseModel => {
|
||||
@@ -40,7 +41,7 @@ export const createContentTreeItem = (item: any): ContentTreeItemResponseModel &
|
||||
|
||||
// TODO: remove isTrashed type extension when we have found a solution to trashed items
|
||||
export const createDocumentTreeItem = (
|
||||
item: DocumentResponseModel
|
||||
item: DocumentResponseModel,
|
||||
): DocumentTreeItemResponseModel & { isTrashed: boolean } => {
|
||||
return {
|
||||
...createContentTreeItem(item),
|
||||
@@ -80,3 +81,9 @@ export const createTextFileItem = (item: any): TextFileResponseModelBaseModel =>
|
||||
name: item.name,
|
||||
content: item.content,
|
||||
});
|
||||
|
||||
export const createFileItemResponseModelBaseModel = (item: any): FileItemResponseModelBaseModel => ({
|
||||
path: item.path,
|
||||
name: item.name,
|
||||
icon: item.icon,
|
||||
});
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
const { rest } = window.MockServiceWorker;
|
||||
import { umbStylesheetData } from '../data/stylesheet.data.js';
|
||||
import {
|
||||
CreateTextFileViewModelBaseModel,
|
||||
ExtractRichTextStylesheetRulesRequestModel,
|
||||
InterpolateRichTextStylesheetRequestModel,
|
||||
UpdateStylesheetRequestModel,
|
||||
} from '@umbraco-cms/backoffice/backend-api';
|
||||
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
|
||||
|
||||
export const handlers = [
|
||||
const treeHandlers = [
|
||||
rest.get(umbracoPath('/tree/stylesheet/root'), (req, res, ctx) => {
|
||||
const response = umbStylesheetData.getTreeRoot();
|
||||
return res(ctx.status(200), ctx.json(response));
|
||||
@@ -24,3 +30,99 @@ export const handlers = [
|
||||
return res(ctx.status(200), ctx.json(items));
|
||||
}),
|
||||
];
|
||||
|
||||
const detailHandlers = [
|
||||
rest.get(umbracoPath('/stylesheet'), (req, res, ctx) => {
|
||||
const path = req.url.searchParams.get('path');
|
||||
if (!path) return;
|
||||
|
||||
const response = umbStylesheetData.getStylesheet(path);
|
||||
return res(ctx.status(200), ctx.json(response));
|
||||
}),
|
||||
rest.post(umbracoPath('/stylesheet'), (req, res, ctx) => {
|
||||
const requestBody = req.json() as CreateTextFileViewModelBaseModel;
|
||||
if (!requestBody) return res(ctx.status(400, 'no body found'));
|
||||
const response = umbStylesheetData.insertStyleSheet(requestBody);
|
||||
return res(ctx.status(200), ctx.json(response));
|
||||
}),
|
||||
|
||||
rest.delete(umbracoPath('/stylesheet'), (req, res, ctx) => {
|
||||
const path = req.url.searchParams.get('path');
|
||||
if (!path) return res(ctx.status(400));
|
||||
const response = umbStylesheetData.delete([path]);
|
||||
return res(ctx.status(200), ctx.json(response));
|
||||
}),
|
||||
rest.put(umbracoPath('/stylesheet'), async (req, res, ctx) => {
|
||||
const requestBody = await req.json() as UpdateStylesheetRequestModel;
|
||||
if (!requestBody) return res(ctx.status(400, 'no body found'));
|
||||
umbStylesheetData.updateData(requestBody);
|
||||
return res(ctx.status(200));
|
||||
}),
|
||||
|
||||
rest.get(umbracoPath('/v1/stylesheet/all'), (req, res, ctx) => {
|
||||
const path = req.url.searchParams.get('path');
|
||||
if (!path) return;
|
||||
|
||||
const response = umbStylesheetData.getAllStylesheets();
|
||||
return res(ctx.status(200), ctx.json(response));
|
||||
}),
|
||||
rest.get(umbracoPath('/v1/stylesheet/item'), (req, res, ctx) => {
|
||||
const paths = req.url.searchParams.getAll('path');
|
||||
if (!paths) return;
|
||||
|
||||
const items = umbStylesheetData.getStylesheetItem(paths[0]);
|
||||
return res(ctx.status(200), ctx.json(items));
|
||||
}),
|
||||
];
|
||||
|
||||
|
||||
const rulesHandlers = [
|
||||
rest.post(umbracoPath('/stylesheet/rich-text/extract-rules'), async (req, res, ctx) => {
|
||||
const requestBody = req.json() as ExtractRichTextStylesheetRulesRequestModel;
|
||||
if (!requestBody) return res(ctx.status(400, 'no body found'));
|
||||
const response = await umbStylesheetData.extractRules({ requestBody });
|
||||
return res(ctx.status(200), ctx.json(response));
|
||||
}),
|
||||
|
||||
rest.post(umbracoPath('/stylesheet/rich-text/interpolate-rules'), async (req, res, ctx) => {
|
||||
const requestBody = (await req.json()) as InterpolateRichTextStylesheetRequestModel;
|
||||
if (!requestBody) return res(ctx.status(400, 'no body found'));
|
||||
const response = umbStylesheetData.interpolateRules({ requestBody });
|
||||
return res(ctx.status(200), ctx.json(response));
|
||||
}),
|
||||
|
||||
rest.get(umbracoPath('/stylesheet/rich-text/rules'), (req, res, ctx) => {
|
||||
const path = req.url.searchParams.get('path');
|
||||
if (!path) return res(ctx.status(400));
|
||||
try {
|
||||
const response = umbStylesheetData.getRules(path);
|
||||
return res(ctx.status(200), ctx.json(response));
|
||||
} catch (e) {
|
||||
return res(ctx.status(404));
|
||||
}
|
||||
}),
|
||||
];
|
||||
|
||||
const folderHandlers = [
|
||||
rest.get(umbracoPath('/v1/stylesheet/all'), (req, res, ctx) => {
|
||||
const path = req.url.searchParams.get('path');
|
||||
if (!path) return;
|
||||
|
||||
const response = umbStylesheetData.getFolder(path);
|
||||
return res(ctx.status(200), ctx.json(response));
|
||||
}),
|
||||
rest.post(umbracoPath('/stylesheet/folder'), (req, res, ctx) => {
|
||||
const requestBody = req.json() as CreateTextFileViewModelBaseModel;
|
||||
if (!requestBody) return res(ctx.status(400, 'no body found'));
|
||||
const response = umbStylesheetData.insertFolder(requestBody);
|
||||
return res(ctx.status(200), ctx.json(response));
|
||||
}),
|
||||
rest.delete(umbracoPath('/stylesheet/folder'), (req, res, ctx) => {
|
||||
const path = req.url.searchParams.get('path');
|
||||
if (!path) return res(ctx.status(400));
|
||||
const response = umbStylesheetData.delete([path]);
|
||||
return res(ctx.status(200), ctx.json(response));
|
||||
}),
|
||||
];
|
||||
|
||||
export const handlers = [...treeHandlers, ...detailHandlers, ...folderHandlers, ...rulesHandlers];
|
||||
|
||||
@@ -24,7 +24,7 @@ import { componentHasManifestProperty } from '@umbraco-cms/backoffice/utils';
|
||||
* @class UmbWorkspaceLayout
|
||||
* @extends {UmbLitElement}
|
||||
*/
|
||||
// TODO: stop naming this something with layout. as its not just an layout. it hooks up with extensions.
|
||||
// TODO: This element has a bug in the tabs. After the url changes - for example a new entity/file is chosen in the tree and loaded to the workspace the links in the tabs still point to the previous url and therefore views do not change correctly
|
||||
@customElement('umb-workspace-editor')
|
||||
export class UmbWorkspaceEditorElement extends UmbLitElement {
|
||||
@property()
|
||||
|
||||
@@ -94,14 +94,13 @@ export class UmbAppLanguageSelectElement extends UmbLitElement {
|
||||
${repeat(
|
||||
this._languages,
|
||||
(language) => language.isoCode,
|
||||
(language) =>
|
||||
html`
|
||||
<uui-menu-item
|
||||
label=${ifDefined(language.name)}
|
||||
@click-label=${this.#onLabelClick}
|
||||
data-iso-code=${ifDefined(language.isoCode)}
|
||||
?active=${language.isoCode === this._appLanguage?.isoCode}></uui-menu-item>
|
||||
`
|
||||
(language) => html`
|
||||
<uui-menu-item
|
||||
label=${ifDefined(language.name)}
|
||||
@click-label=${this.#onLabelClick}
|
||||
data-iso-code=${ifDefined(language.isoCode)}
|
||||
?active=${language.isoCode === this._appLanguage?.isoCode}></uui-menu-item>
|
||||
`,
|
||||
)}
|
||||
</div>`;
|
||||
}
|
||||
@@ -127,6 +126,7 @@ export class UmbAppLanguageSelectElement extends UmbLitElement {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
#toggle:hover {
|
||||
|
||||
@@ -16,11 +16,10 @@ const workspaceActions: Array<ManifestWorkspaceAction> = [
|
||||
type: 'workspaceAction',
|
||||
alias: 'Umb.WorkspaceAction.PartialView.Save',
|
||||
name: 'Save Partial View',
|
||||
weight: 70,
|
||||
meta: {
|
||||
label: 'Save',
|
||||
look: 'primary',
|
||||
color: 'positive',
|
||||
label: 'Save',
|
||||
api: UmbSaveWorkspaceAction,
|
||||
},
|
||||
conditions: [
|
||||
|
||||
@@ -30,7 +30,6 @@ export class UmbPartialViewWorkspaceContext extends UmbWorkspaceContext<
|
||||
};
|
||||
|
||||
this.repository.create(createRequestBody);
|
||||
console.log('create');
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (!partialView.path) return Promise.reject('There is no path');
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export const STYLESHEET_ENTITY_TYPE = 'stylesheet';
|
||||
|
||||
export const STYLESHEET_ROOT_ENTITY_TYPE = 'stylesheet-root';
|
||||
export const STYLESHEET_FOLDER_ENTITY_TYPE = 'stylesheet-folder';
|
||||
export const STYLESHEET_FOLDER_EMPTY_ENTITY_TYPE = 'stylesheet-folder-empty';
|
||||
|
||||
export const STYLESHEET_REPOSITORY_ALIAS = 'Umb.Repository.Stylesheet';
|
||||
|
||||
export const STYLESHEET_TREE_ALIAS = 'Umb.Tree.Stylesheet';
|
||||
|
||||
export const UMB_STYLESHEET_TREE_STORE_CONTEXT_TOKEN_ALIAS = 'Umb.Store.Stylesheet.Tree';
|
||||
export const UMB_STYLESHEET_STORE_CONTEXT_TOKEN_ALIAS = 'Umb.Store.Stylesheet';
|
||||
|
||||
export const STYLESHEET_STORE_ALIAS = 'Umb.Store.Stylesheet';
|
||||
export const STYLESHEET_TREE_STORE_ALIAS = 'Umb.Store.StylesheetTree';
|
||||
@@ -0,0 +1,16 @@
|
||||
import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
|
||||
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
export class UmbCreateRTFStylesheetAction<T extends { copy(): Promise<void> }> extends UmbEntityActionBase<T> {
|
||||
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
|
||||
super(host, repositoryAlias, unique);
|
||||
}
|
||||
|
||||
async execute() {
|
||||
history.pushState(
|
||||
null,
|
||||
'',
|
||||
`section/settings/workspace/stylesheet/create/${this.unique ?? 'null'}/view/rich-text-editor`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
|
||||
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
export class UmbCreateStylesheetAction<T extends { copy(): Promise<void> }> extends UmbEntityActionBase<T> {
|
||||
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
|
||||
super(host, repositoryAlias, unique);
|
||||
}
|
||||
|
||||
async execute() {
|
||||
history.pushState(null, '', `section/settings/workspace/stylesheet/create/${this.unique ?? 'null'}/view/code`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
STYLESHEET_ENTITY_TYPE,
|
||||
STYLESHEET_FOLDER_EMPTY_ENTITY_TYPE,
|
||||
STYLESHEET_FOLDER_ENTITY_TYPE,
|
||||
STYLESHEET_REPOSITORY_ALIAS,
|
||||
STYLESHEET_ROOT_ENTITY_TYPE,
|
||||
} from '../config.js';
|
||||
import { UmbCreateRTFStylesheetAction } from './create/create-rtf.action.js';
|
||||
import { UmbCreateStylesheetAction } from './create/create.action.js';
|
||||
import {
|
||||
UmbCreateFolderEntityAction,
|
||||
UmbDeleteEntityAction,
|
||||
UmbDeleteFolderEntityAction,
|
||||
} from '@umbraco-cms/backoffice/entity-action';
|
||||
import { ManifestEntityAction } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
//TODO: this is temporary until we have a proper way of registering actions for folder types in a specific tree
|
||||
|
||||
//Actions for partial view files
|
||||
const stylesheetActions: Array<ManifestEntityAction> = [
|
||||
{
|
||||
type: 'entityAction',
|
||||
alias: 'Umb.EntityAction.Stylesheet.Delete',
|
||||
name: 'Delete Stylesheet Entity Action',
|
||||
meta: {
|
||||
icon: 'umb:trash',
|
||||
label: 'Delete',
|
||||
api: UmbDeleteEntityAction,
|
||||
repositoryAlias: STYLESHEET_REPOSITORY_ALIAS,
|
||||
entityTypes: [STYLESHEET_ENTITY_TYPE],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
//TODO: add create folder action when the generic folder action is implemented
|
||||
//Actions for directories
|
||||
const stylesheetFolderActions: Array<ManifestEntityAction> = [
|
||||
{
|
||||
type: 'entityAction',
|
||||
alias: 'Umb.EntityAction.Stylesheet.Folder.Create',
|
||||
name: 'Create Stylesheet Entity Under Directory Action',
|
||||
meta: {
|
||||
icon: 'umb:script',
|
||||
label: 'New stylesheet file',
|
||||
api: UmbCreateStylesheetAction,
|
||||
repositoryAlias: STYLESHEET_REPOSITORY_ALIAS,
|
||||
entityTypes: [STYLESHEET_FOLDER_ENTITY_TYPE, STYLESHEET_FOLDER_EMPTY_ENTITY_TYPE, STYLESHEET_ROOT_ENTITY_TYPE],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'entityAction',
|
||||
alias: 'Umb.EntityAction.Stylesheet.Folder.Create.RTF',
|
||||
name: 'Create RTF Stylesheet Entity Under Directory Action',
|
||||
meta: {
|
||||
icon: 'umb:script',
|
||||
label: 'New Rich Text Editor style sheet file',
|
||||
api: UmbCreateRTFStylesheetAction,
|
||||
repositoryAlias: STYLESHEET_REPOSITORY_ALIAS,
|
||||
entityTypes: [STYLESHEET_FOLDER_ENTITY_TYPE, STYLESHEET_FOLDER_EMPTY_ENTITY_TYPE, STYLESHEET_ROOT_ENTITY_TYPE],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'entityAction',
|
||||
alias: 'Umb.EntityAction.Stylesheet.Folder.DeleteFolder',
|
||||
name: 'Remove empty folder',
|
||||
meta: {
|
||||
icon: 'umb:trash',
|
||||
label: 'Remove folder',
|
||||
api: UmbDeleteFolderEntityAction,
|
||||
repositoryAlias: STYLESHEET_REPOSITORY_ALIAS,
|
||||
entityTypes: [STYLESHEET_FOLDER_EMPTY_ENTITY_TYPE],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'entityAction',
|
||||
alias: 'Umb.EntityAction.Stylesheet.Folder.CreateFolder',
|
||||
name: 'Create empty folder',
|
||||
meta: {
|
||||
icon: 'umb:add',
|
||||
label: 'Create folder',
|
||||
api: UmbCreateFolderEntityAction,
|
||||
repositoryAlias: STYLESHEET_REPOSITORY_ALIAS,
|
||||
entityTypes: [STYLESHEET_FOLDER_EMPTY_ENTITY_TYPE, STYLESHEET_FOLDER_ENTITY_TYPE, STYLESHEET_ROOT_ENTITY_TYPE],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const manifests = [...stylesheetActions, ...stylesheetFolderActions];
|
||||
@@ -1,9 +1,5 @@
|
||||
import { FileSystemTreeItemPresentationModel } from '@umbraco-cms/backoffice/backend-api';
|
||||
import { StylesheetResponseModel } from '@umbraco-cms/backoffice/backend-api';
|
||||
|
||||
// TODO: temp until we have a proper stylesheet model
|
||||
export interface StylesheetDetails extends FileSystemTreeItemPresentationModel {
|
||||
content: string;
|
||||
}
|
||||
export type StylesheetDetails = StylesheetResponseModel;
|
||||
|
||||
export const STYLESHEET_ENTITY_TYPE = 'stylesheet';
|
||||
export * from './repository/index.js';
|
||||
|
||||
@@ -2,5 +2,12 @@ import { manifests as repositoryManifests } from './repository/manifests.js';
|
||||
import { manifests as menuItemManifests } from './menu-item/manifests.js';
|
||||
import { manifests as treeManifests } from './tree/manifests.js';
|
||||
import { manifests as workspaceManifests } from './workspace/manifests.js';
|
||||
import { manifests as entityActionManifests } from './entity-actions/manifests.js';
|
||||
|
||||
export const manifests = [...repositoryManifests, ...menuItemManifests, ...treeManifests, ...workspaceManifests];
|
||||
export const manifests = [
|
||||
...repositoryManifests,
|
||||
...menuItemManifests,
|
||||
...treeManifests,
|
||||
...workspaceManifests,
|
||||
...entityActionManifests,
|
||||
];
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { STYLESHEET_REPOSITORY_ALIAS, STYLESHEET_TREE_STORE_ALIAS } from '../config.js';
|
||||
import { UmbStylesheetRepository } from './stylesheet.repository.js';
|
||||
import { UmbStylesheetTreeStore } from './stylesheet.tree.store.js';
|
||||
import { ManifestRepository, ManifestTreeStore } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
export const STYLESHEET_REPOSITORY_ALIAS = 'Umb.Repository.Stylesheet';
|
||||
|
||||
const repository: ManifestRepository = {
|
||||
type: 'repository',
|
||||
@@ -11,8 +11,6 @@ const repository: ManifestRepository = {
|
||||
class: UmbStylesheetRepository,
|
||||
};
|
||||
|
||||
export const STYLESHEET_STORE_ALIAS = 'Umb.Store.Stylesheet';
|
||||
export const STYLESHEET_TREE_STORE_ALIAS = 'Umb.Store.StylesheetTree';
|
||||
|
||||
const treeStore: ManifestTreeStore = {
|
||||
type: 'treeStore',
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import {
|
||||
CreateFolderRequestModel,
|
||||
FolderModelBaseModel,
|
||||
FolderResponseModel,
|
||||
StylesheetResource,
|
||||
} from '@umbraco-cms/backoffice/backend-api';
|
||||
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { DataSourceResponse, UmbFolderDataSource } from '@umbraco-cms/backoffice/repository';
|
||||
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
|
||||
|
||||
//! this is of any type in the backend-api
|
||||
export type StylesheetGetFolderResponse = { path: string; parentPath: string; name: string };
|
||||
|
||||
export class UmbStylesheetFolderServerDataSource implements UmbFolderDataSource {
|
||||
#host: UmbControllerHostElement;
|
||||
|
||||
constructor(host: UmbControllerHostElement) {
|
||||
this.#host = host;
|
||||
}
|
||||
|
||||
get(unique: string): Promise<DataSourceResponse<StylesheetGetFolderResponse>> {
|
||||
return tryExecuteAndNotify(this.#host, StylesheetResource.getStylesheetFolder({ path: unique }));
|
||||
}
|
||||
insert(requestBody: CreateFolderRequestModel): Promise<DataSourceResponse<string>> {
|
||||
return tryExecuteAndNotify(this.#host, StylesheetResource.postStylesheetFolder({ requestBody }));
|
||||
}
|
||||
delete(path: string): Promise<DataSourceResponse<unknown>> {
|
||||
return tryExecuteAndNotify(this.#host, StylesheetResource.deleteStylesheetFolder({ path }));
|
||||
}
|
||||
update(unique: string, data: CreateFolderRequestModel): Promise<DataSourceResponse<FolderModelBaseModel>> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
createScaffold(parentId: string | null): Promise<DataSourceResponse<FolderResponseModel>> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,17 @@
|
||||
import type { StylesheetDetails } from '../../index.js';
|
||||
import { DataSourceResponse, UmbDataSource } from '@umbraco-cms/backoffice/repository';
|
||||
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
|
||||
import {
|
||||
CreateStylesheetRequestModel,
|
||||
ExtractRichTextStylesheetRulesRequestModel,
|
||||
ExtractRichTextStylesheetRulesResponseModel,
|
||||
InterpolateRichTextStylesheetRequestModel,
|
||||
InterpolateRichTextStylesheetResponseModel,
|
||||
RichTextStylesheetRulesResponseModel,
|
||||
StylesheetResource,
|
||||
UpdateStylesheetRequestModel,
|
||||
} from '@umbraco-cms/backoffice/backend-api';
|
||||
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
|
||||
|
||||
/**
|
||||
* A data source for the Stylesheet that fetches data from the server
|
||||
@@ -8,7 +19,9 @@ import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api
|
||||
* @class UmbStylesheetServerDataSource
|
||||
* @implements {UmbStylesheetServerDataSource}
|
||||
*/
|
||||
export class UmbStylesheetServerDataSource implements UmbDataSource<any, any, any, StylesheetDetails> {
|
||||
export class UmbStylesheetServerDataSource
|
||||
implements UmbDataSource<CreateStylesheetRequestModel, string, UpdateStylesheetRequestModel, StylesheetDetails>
|
||||
{
|
||||
#host: UmbControllerHostElement;
|
||||
|
||||
/**
|
||||
@@ -31,17 +44,79 @@ export class UmbStylesheetServerDataSource implements UmbDataSource<any, any, an
|
||||
*/
|
||||
async get(path: string) {
|
||||
if (!path) throw new Error('Path is missing');
|
||||
console.log('GET STYLESHEET WITH PATH', path);
|
||||
return { data: undefined, error: undefined };
|
||||
return tryExecuteAndNotify(this.#host, StylesheetResource.getStylesheet({ path }));
|
||||
}
|
||||
|
||||
insert(data: StylesheetDetails): Promise<DataSourceResponse<StylesheetDetails>> {
|
||||
throw new Error('Method not implemented.');
|
||||
/**
|
||||
* Creates a new Stylesheet
|
||||
*
|
||||
* @param {StylesheetDetails} data
|
||||
* @return {*} {Promise<DataSourceResponse<any>>}
|
||||
* @memberof UmbStylesheetServerDataSource
|
||||
*/
|
||||
insert(data: StylesheetDetails): Promise<DataSourceResponse<any>> {
|
||||
return tryExecuteAndNotify(this.#host, StylesheetResource.postStylesheet({ requestBody: data }));
|
||||
}
|
||||
/**
|
||||
* Updates an existing Stylesheet
|
||||
*
|
||||
* @param {string} path
|
||||
* @param {StylesheetDetails} data
|
||||
* @return {*} {Promise<DataSourceResponse<StylesheetDetails>>}
|
||||
* @memberof UmbStylesheetServerDataSource
|
||||
*/
|
||||
update(path: string, data: StylesheetDetails): Promise<DataSourceResponse<StylesheetDetails>> {
|
||||
throw new Error('Method not implemented.');
|
||||
return tryExecuteAndNotify(this.#host, StylesheetResource.putStylesheet({ requestBody: data }));
|
||||
}
|
||||
/**
|
||||
* Deletes a Stylesheet.
|
||||
*
|
||||
* @param {string} path
|
||||
* @return {*} {Promise<DataSourceResponse>}
|
||||
* @memberof UmbStylesheetServerDataSource
|
||||
*/
|
||||
delete(path: string): Promise<DataSourceResponse> {
|
||||
throw new Error('Method not implemented.');
|
||||
return tryExecuteAndNotify(this.#host, StylesheetResource.deleteStylesheet({ path }));
|
||||
}
|
||||
/**
|
||||
* Get's the rich text rules for a stylesheet
|
||||
*
|
||||
* @param {string} path
|
||||
* @return {*} {(Promise<DataSourceResponse<RichTextStylesheetRulesResponseModel | ExtractRichTextStylesheetRulesResponseModel>>)}
|
||||
* @memberof UmbStylesheetServerDataSource
|
||||
*/
|
||||
getStylesheetRichTextRules(
|
||||
path: string
|
||||
): Promise<DataSourceResponse<RichTextStylesheetRulesResponseModel | ExtractRichTextStylesheetRulesResponseModel>> {
|
||||
return tryExecuteAndNotify(this.#host, StylesheetResource.getStylesheetRichTextRules({ path }));
|
||||
}
|
||||
/**
|
||||
* Extracts the rich text rules from a stylesheet string. In simple words: takes a stylesheet string and returns a array of rules.
|
||||
*
|
||||
* @param {ExtractRichTextStylesheetRulesRequestModel} data
|
||||
* @return {*} {Promise<DataSourceResponse<ExtractRichTextStylesheetRulesResponseModel>>}
|
||||
* @memberof UmbStylesheetServerDataSource
|
||||
*/
|
||||
postStylesheetRichTextExtractRules(
|
||||
data: ExtractRichTextStylesheetRulesRequestModel
|
||||
): Promise<DataSourceResponse<ExtractRichTextStylesheetRulesResponseModel>> {
|
||||
return tryExecuteAndNotify(
|
||||
this.#host,
|
||||
StylesheetResource.postStylesheetRichTextExtractRules({ requestBody: data })
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Interpolates the rich text rules from a stylesheet string. In simple words: takes a array of rules and existing content. Returns new content with the rules applied.
|
||||
*
|
||||
* @param {InterpolateRichTextStylesheetRequestModel} data
|
||||
* @return {*} {Promise<DataSourceResponse<InterpolateRichTextStylesheetResponseModel>>}
|
||||
* @memberof UmbStylesheetServerDataSource
|
||||
*/
|
||||
postStylesheetRichTextInterpolateRules(
|
||||
data: InterpolateRichTextStylesheetRequestModel
|
||||
): Promise<DataSourceResponse<InterpolateRichTextStylesheetResponseModel>> {
|
||||
return tryExecuteAndNotify(
|
||||
this.#host,
|
||||
StylesheetResource.postStylesheetRichTextInterpolateRules({ requestBody: data })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,51 @@
|
||||
import { Observable } from 'rxjs';
|
||||
import { StylesheetDetails } from '../index.js';
|
||||
import { UmbStylesheetTreeStore, UMB_STYLESHEET_TREE_STORE_CONTEXT_TOKEN } from './stylesheet.tree.store.js';
|
||||
import { UmbStylesheetTreeServerDataSource } from './sources/stylesheet.tree.server.data.js';
|
||||
import { UmbStylesheetServerDataSource } from './sources/stylesheet.server.data.js';
|
||||
import {
|
||||
StylesheetGetFolderResponse,
|
||||
UmbStylesheetFolderServerDataSource,
|
||||
} from './sources/stylesheet.folder.server.data.js';
|
||||
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api';
|
||||
import { UmbTreeRepository } from '@umbraco-cms/backoffice/repository';
|
||||
import { FileSystemTreeItemPresentationModel } from '@umbraco-cms/backoffice/backend-api';
|
||||
import {
|
||||
DataSourceResponse,
|
||||
UmbDataSourceErrorResponse,
|
||||
UmbDetailRepository,
|
||||
UmbFolderRepository,
|
||||
UmbTreeRepository,
|
||||
} from '@umbraco-cms/backoffice/repository';
|
||||
import {
|
||||
CreateFolderRequestModel,
|
||||
CreateStylesheetRequestModel,
|
||||
CreateTextFileViewModelBaseModel,
|
||||
ExtractRichTextStylesheetRulesRequestModel,
|
||||
ExtractRichTextStylesheetRulesResponseModel,
|
||||
FileSystemTreeItemPresentationModel,
|
||||
FolderModelBaseModel,
|
||||
FolderResponseModel,
|
||||
InterpolateRichTextStylesheetRequestModel,
|
||||
InterpolateRichTextStylesheetResponseModel,
|
||||
ProblemDetails,
|
||||
RichTextStylesheetRulesResponseModel,
|
||||
TextFileResponseModelBaseModel,
|
||||
UpdateStylesheetRequestModel,
|
||||
UpdateTextFileViewModelBaseModel,
|
||||
} from '@umbraco-cms/backoffice/backend-api';
|
||||
import type { UmbTreeRootFileSystemModel } from '@umbraco-cms/backoffice/tree';
|
||||
|
||||
export class UmbStylesheetRepository
|
||||
implements UmbTreeRepository<FileSystemTreeItemPresentationModel, UmbTreeRootFileSystemModel>
|
||||
implements
|
||||
UmbTreeRepository<FileSystemTreeItemPresentationModel, UmbTreeRootFileSystemModel>,
|
||||
UmbDetailRepository<CreateStylesheetRequestModel, string, UpdateStylesheetRequestModel, StylesheetDetails>,
|
||||
UmbFolderRepository
|
||||
{
|
||||
#host;
|
||||
#dataSource;
|
||||
#treeDataSource;
|
||||
#treeStore?: UmbStylesheetTreeStore;
|
||||
#folderDataSource;
|
||||
#init;
|
||||
|
||||
constructor(host: UmbControllerHostElement) {
|
||||
@@ -22,13 +54,128 @@ export class UmbStylesheetRepository
|
||||
// TODO: figure out how spin up get the correct data source
|
||||
this.#dataSource = new UmbStylesheetServerDataSource(this.#host);
|
||||
this.#treeDataSource = new UmbStylesheetTreeServerDataSource(this.#host);
|
||||
this.#folderDataSource = new UmbStylesheetFolderServerDataSource(this.#host);
|
||||
|
||||
this.#init = new UmbContextConsumerController(this.#host, UMB_STYLESHEET_TREE_STORE_CONTEXT_TOKEN, (instance) => {
|
||||
this.#treeStore = instance;
|
||||
}).asPromise();
|
||||
}
|
||||
|
||||
// TREE:
|
||||
//#region FOLDER:
|
||||
|
||||
createFolderScaffold(
|
||||
parentId: string | null,
|
||||
): Promise<{ data?: FolderResponseModel | undefined; error?: ProblemDetails | undefined }> {
|
||||
const data: FolderResponseModel = {
|
||||
name: '',
|
||||
parentId,
|
||||
};
|
||||
return Promise.resolve({ data, error: undefined });
|
||||
}
|
||||
|
||||
async createFolder(
|
||||
folderRequest: CreateFolderRequestModel,
|
||||
): Promise<{ data?: string | undefined; error?: ProblemDetails | undefined }> {
|
||||
await this.#init;
|
||||
const req = {
|
||||
parentPath: folderRequest.parentId,
|
||||
name: folderRequest.name,
|
||||
};
|
||||
const promise = this.#folderDataSource.insert(req);
|
||||
await promise;
|
||||
this.requestTreeItemsOf(folderRequest.parentId ? folderRequest.parentId : null);
|
||||
return promise;
|
||||
}
|
||||
async requestFolder(
|
||||
unique: string,
|
||||
): Promise<{ data?: StylesheetGetFolderResponse | undefined; error?: ProblemDetails | undefined }> {
|
||||
await this.#init;
|
||||
return this.#folderDataSource.get(unique);
|
||||
}
|
||||
updateFolder(
|
||||
unique: string,
|
||||
folder: FolderModelBaseModel,
|
||||
): Promise<{ data?: FolderModelBaseModel | undefined; error?: ProblemDetails | undefined }> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
async deleteFolder(path: string): Promise<{ error?: ProblemDetails | undefined }> {
|
||||
await this.#init;
|
||||
const { data } = await this.requestFolder(path);
|
||||
const promise = this.#folderDataSource.delete(path);
|
||||
await promise;
|
||||
this.requestTreeItemsOf(data?.parentPath ? data?.parentPath : null);
|
||||
return promise;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region DETAIL:
|
||||
|
||||
createScaffold(
|
||||
parentId: string | null,
|
||||
preset?: Partial<CreateTextFileViewModelBaseModel> | undefined,
|
||||
): Promise<DataSourceResponse<CreateTextFileViewModelBaseModel>> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
async requestById(id: string): Promise<DataSourceResponse<TextFileResponseModelBaseModel | undefined>> {
|
||||
if (!id) throw new Error('id is missing');
|
||||
await this.#init;
|
||||
const { data, error } = await this.#dataSource.get(id);
|
||||
return { data, error };
|
||||
}
|
||||
byId(id: string): Promise<Observable<TextFileResponseModelBaseModel | undefined>> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
async create(data: CreateTextFileViewModelBaseModel): Promise<DataSourceResponse<string>> {
|
||||
const promise = this.#dataSource.insert(data);
|
||||
await promise;
|
||||
this.requestTreeItemsOf(data.parentPath ? data.parentPath : null);
|
||||
return promise;
|
||||
}
|
||||
save(id: string, data: UpdateTextFileViewModelBaseModel): Promise<UmbDataSourceErrorResponse> {
|
||||
return this.#dataSource.update(id, data);
|
||||
}
|
||||
delete(id: string): Promise<UmbDataSourceErrorResponse> {
|
||||
const promise = this.#dataSource.delete(id);
|
||||
const parentPath = id.substring(0, id.lastIndexOf('/'));
|
||||
this.requestTreeItemsOf(parentPath ? parentPath : null);
|
||||
return promise;
|
||||
}
|
||||
|
||||
getStylesheetRules(
|
||||
path: string,
|
||||
): Promise<DataSourceResponse<RichTextStylesheetRulesResponseModel | ExtractRichTextStylesheetRulesResponseModel>> {
|
||||
return this.#dataSource.getStylesheetRichTextRules(path);
|
||||
}
|
||||
/**
|
||||
* Existing content + array of rules => new content string
|
||||
*
|
||||
* @param {InterpolateRichTextStylesheetRequestModel} data
|
||||
* @return {*} {Promise<DataSourceResponse<InterpolateRichTextStylesheetResponseModel>>}
|
||||
* @memberof UmbStylesheetRepository
|
||||
*/
|
||||
interpolateStylesheetRules(
|
||||
data: InterpolateRichTextStylesheetRequestModel,
|
||||
): Promise<DataSourceResponse<InterpolateRichTextStylesheetResponseModel>> {
|
||||
return this.#dataSource.postStylesheetRichTextInterpolateRules(data);
|
||||
}
|
||||
/**
|
||||
* content string => array of rules
|
||||
*
|
||||
* @param {ExtractRichTextStylesheetRulesRequestModel} data
|
||||
* @return {*} {Promise<DataSourceResponse<ExtractRichTextStylesheetRulesResponseModel>>}
|
||||
* @memberof UmbStylesheetRepository
|
||||
*/
|
||||
extractStylesheetRules(
|
||||
data: ExtractRichTextStylesheetRulesRequestModel,
|
||||
): Promise<DataSourceResponse<ExtractRichTextStylesheetRulesResponseModel>> {
|
||||
return this.#dataSource.postStylesheetRichTextExtractRules(data);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region TREE:
|
||||
async requestTreeRoot() {
|
||||
await this.#init;
|
||||
|
||||
@@ -69,7 +216,7 @@ export class UmbStylesheetRepository
|
||||
return { data, error, asObservable: () => this.#treeStore!.childrenOf(path) };
|
||||
}
|
||||
|
||||
async requestItemsLegacy(paths: Array<string>) {
|
||||
async requestItems(paths: Array<string>) {
|
||||
if (!paths) throw new Error('Paths are missing');
|
||||
await this.#init;
|
||||
const { data, error } = await this.#treeDataSource.getItems(paths);
|
||||
@@ -93,11 +240,5 @@ export class UmbStylesheetRepository
|
||||
return this.#treeStore!.items(paths);
|
||||
}
|
||||
|
||||
// DETAILS
|
||||
async requestByPath(path: string) {
|
||||
if (!path) throw new Error('Path is missing');
|
||||
await this.#init;
|
||||
const { data, error } = await this.#dataSource.get(path);
|
||||
return { data, error };
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { STYLESHEET_ENTITY_TYPE } from '../index.js';
|
||||
import { STYLESHEET_REPOSITORY_ALIAS } from '../repository/manifests.js';
|
||||
import { STYLESHEET_ENTITY_TYPE, STYLESHEET_REPOSITORY_ALIAS } from '../config.js';
|
||||
import type { ManifestTree, ManifestTreeItem } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
export const STYLESHEET_TREE_ALIAS = 'Umb.Tree.Stylesheet';
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type {
|
||||
ManifestModal,
|
||||
ManifestWorkspace,
|
||||
ManifestWorkspaceAction,
|
||||
ManifestWorkspaceEditorView,
|
||||
} from '@umbraco-cms/backoffice/extension-registry';
|
||||
import { UmbSaveWorkspaceAction } from '@umbraco-cms/backoffice/workspace';
|
||||
|
||||
const workspace: ManifestWorkspace = {
|
||||
type: 'workspace',
|
||||
@@ -14,7 +16,75 @@ const workspace: ManifestWorkspace = {
|
||||
},
|
||||
};
|
||||
|
||||
const workspaceViews: Array<ManifestWorkspaceEditorView> = [];
|
||||
const workspaceActions: Array<ManifestWorkspaceAction> = [];
|
||||
const workspaceEditorViews: Array<ManifestWorkspaceEditorView> = [
|
||||
{
|
||||
type: 'workspaceEditorView',
|
||||
alias: 'Umb.WorkspaceView.Stylesheet.CodeEditor',
|
||||
name: 'Stylesheet Workspace Code Editor View',
|
||||
loader: () => import('./views/code-editor/stylesheet-workspace-view-code-editor.element.js'),
|
||||
weight: 700,
|
||||
meta: {
|
||||
label: 'Code',
|
||||
pathname: 'code',
|
||||
icon: 'umb:brackets',
|
||||
},
|
||||
conditions: [
|
||||
{
|
||||
alias: 'Umb.Condition.WorkspaceAlias',
|
||||
match: workspace.alias,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'workspaceEditorView',
|
||||
alias: 'Umb.WorkspaceView.Stylesheet.RichTextEditor',
|
||||
name: 'Stylesheet Workspace Rich Text Editor View',
|
||||
loader: () => import('./views/rich-text-editor/stylesheet-workspace-view-rich-text-editor.element.js'),
|
||||
weight: 800,
|
||||
meta: {
|
||||
label: 'Rich Text Editor',
|
||||
pathname: 'rich-text-editor',
|
||||
icon: 'umb:font',
|
||||
},
|
||||
conditions: [
|
||||
{
|
||||
alias: 'Umb.Condition.WorkspaceAlias',
|
||||
match: workspace.alias,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const workspaceActions: Array<ManifestWorkspaceAction> = [
|
||||
{
|
||||
type: 'workspaceAction',
|
||||
alias: 'Umb.WorkspaceAction.Stylesheet.Save',
|
||||
name: 'Save Stylesheet Workspace Action',
|
||||
meta: {
|
||||
label: 'Save',
|
||||
look: 'primary',
|
||||
color: 'positive',
|
||||
api: UmbSaveWorkspaceAction,
|
||||
},
|
||||
conditions: [
|
||||
{
|
||||
alias: 'Umb.Condition.WorkspaceAlias',
|
||||
match: workspace.alias,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const manifests = [workspace, ...workspaceViews, ...workspaceActions];
|
||||
export const UMB_MODAL_TEMPLATING_STYLESHEET_RTF_STYLE_SIDEBAR =
|
||||
'Umb.Modal.Templating.Stylesheet.RichTextEditorStyle.Sidebar';
|
||||
|
||||
const modals: Array<ManifestModal> = [
|
||||
{
|
||||
type: 'modal',
|
||||
alias: UMB_MODAL_TEMPLATING_STYLESHEET_RTF_STYLE_SIDEBAR,
|
||||
name: 'Rich text editor style modal',
|
||||
loader: () =>
|
||||
import('./views/rich-text-editor/stylesheet-workspace-view-rich-text-editor-style-sidebar.element.js'),
|
||||
},
|
||||
];
|
||||
|
||||
export const manifests = [workspace, ...workspaceEditorViews, ...workspaceActions, ...modals];
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
|
||||
import { css, html, LitElement, customElement } from '@umbraco-cms/backoffice/external/lit';
|
||||
|
||||
@customElement('umb-stylesheet-workspace-edit')
|
||||
export class UmbStylesheetWorkspaceEditElement extends LitElement {
|
||||
render() {
|
||||
return html` <umb-workspace-editor alias="Umb.Workspace.Stylesheet">Stylesheet workspace</umb-workspace-editor> `;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
UmbTextStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
export default UmbStylesheetWorkspaceEditElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-stylesheet-workspace-edit': UmbStylesheetWorkspaceEditElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { UmbStylesheetWorkspaceContext } from './stylesheet-workspace.context.js';
|
||||
import { UUIInputElement, UUIInputEvent, UUITextStyles } from '@umbraco-cms/backoffice/external/uui';
|
||||
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
|
||||
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
|
||||
import { Subject, debounceTime } from '@umbraco-cms/backoffice/external/rxjs';
|
||||
|
||||
@customElement('umb-stylesheet-workspace-editor')
|
||||
export class UmbStylesheetWorkspaceEditorElement extends UmbLitElement {
|
||||
#workspaceContext?: UmbStylesheetWorkspaceContext;
|
||||
|
||||
#name: string | undefined = '';
|
||||
@state()
|
||||
private get _name() {
|
||||
return this.#name;
|
||||
}
|
||||
|
||||
private set _name(value) {
|
||||
this.#name = value?.replace('.css', '');
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
@state()
|
||||
private _path?: string;
|
||||
|
||||
private inputQuery$ = new Subject<string>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.consumeContext(UMB_WORKSPACE_CONTEXT, (instance) => {
|
||||
this.#workspaceContext = instance as unknown as UmbStylesheetWorkspaceContext;
|
||||
this.#observeNameAndPath();
|
||||
this.inputQuery$.pipe(debounceTime(250)).subscribe((nameInputValue: string) => {
|
||||
this.#workspaceContext?.setName(`${nameInputValue}.css`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#observeNameAndPath() {
|
||||
if (!this.#workspaceContext) return;
|
||||
this.observe(this.#workspaceContext.name, (name) => (this._name = name ?? ''), '_observeName');
|
||||
this.observe(this.#workspaceContext.path, (path) => (this._path = path ?? ''), '_observePath');
|
||||
}
|
||||
|
||||
#onNameChange(event: UUIInputEvent) {
|
||||
const target = event.target as UUIInputElement;
|
||||
const value = target.value as string;
|
||||
this.inputQuery$.next(value);
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<umb-workspace-editor alias="Umb.Workspace.StyleSheet">
|
||||
<div id="header" slot="header">
|
||||
<uui-input
|
||||
placeholder="Enter stylesheet name..."
|
||||
label="stylesheet name"
|
||||
id="name"
|
||||
.value=${this._name}
|
||||
@input="${this.#onNameChange}">
|
||||
</uui-input>
|
||||
<small>/css/${this._path}</small>
|
||||
</div>
|
||||
|
||||
<div slot="footer-info">
|
||||
<!-- TODO: Shortcuts Modal? -->
|
||||
<uui-button label="Show keyboard shortcuts">
|
||||
Keyboard Shortcuts
|
||||
<uui-keyboard-shortcut>
|
||||
<uui-key>ALT</uui-key>
|
||||
+
|
||||
<uui-key>shift</uui-key>
|
||||
+
|
||||
<uui-key>k</uui-key>
|
||||
</uui-keyboard-shortcut>
|
||||
</uui-button>
|
||||
</div>
|
||||
</umb-workspace-editor>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#header {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#name {
|
||||
width: 100%;
|
||||
flex: 1 1 auto;
|
||||
align-items: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
export default UmbStylesheetWorkspaceEditorElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-stylesheet-workspace-editor': UmbStylesheetWorkspaceEditorElement;
|
||||
}
|
||||
}
|
||||
@@ -2,39 +2,163 @@ import { UmbStylesheetRepository } from '../repository/stylesheet.repository.js'
|
||||
import { StylesheetDetails } from '../index.js';
|
||||
import { UmbEntityWorkspaceContextInterface, UmbWorkspaceContext } from '@umbraco-cms/backoffice/workspace';
|
||||
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
|
||||
import { UmbArrayState, UmbBooleanState, UmbObjectState, createObservablePart } from '@umbraco-cms/backoffice/observable-api';
|
||||
import { loadCodeEditor } from '@umbraco-cms/backoffice/code-editor';
|
||||
import { RichTextRuleModel, UpdateStylesheetRequestModel } from '@umbraco-cms/backoffice/backend-api';
|
||||
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
|
||||
|
||||
export type RichTextRuleModelSortable = RichTextRuleModel & { sortOrder?: number };
|
||||
|
||||
export class UmbStylesheetWorkspaceContext extends UmbWorkspaceContext<UmbStylesheetRepository, StylesheetDetails> implements UmbEntityWorkspaceContextInterface {
|
||||
#data = new UmbObjectState<StylesheetDetails | undefined>(undefined);
|
||||
#rules = new UmbArrayState<RichTextRuleModelSortable>([], (rule) => rule.name);
|
||||
data = this.#data.asObservable();
|
||||
rules = this.#rules.asObservable();
|
||||
name = createObservablePart(this.#data, (data) => data?.name);
|
||||
content = createObservablePart(this.#data, (data) => data?.content);
|
||||
path = createObservablePart(this.#data, (data) => data?.path);
|
||||
|
||||
#isCodeEditorReady = new UmbBooleanState(false);
|
||||
isCodeEditorReady = this.#isCodeEditorReady.asObservable();
|
||||
|
||||
constructor(host: UmbControllerHostElement) {
|
||||
super(host, 'Umb.Workspace.Stylesheet', new UmbStylesheetRepository(host));
|
||||
super(host, 'Umb.Workspace.StyleSheet', new UmbStylesheetRepository(host));
|
||||
this.#rules.sortBy((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
||||
this.#loadCodeEditor();
|
||||
}
|
||||
|
||||
async #loadCodeEditor() {
|
||||
try {
|
||||
await loadCodeEditor();
|
||||
this.#isCodeEditorReady.next(true);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
getEntityType(): string {
|
||||
return 'stylesheet';
|
||||
}
|
||||
|
||||
getData() {
|
||||
return this.#data.getValue();
|
||||
}
|
||||
|
||||
getEntityId() {
|
||||
return this.getData()?.path || '';
|
||||
}
|
||||
|
||||
getData() {
|
||||
return this.#data.getValue();
|
||||
}
|
||||
|
||||
getRules() {
|
||||
return this.#rules.getValue();
|
||||
}
|
||||
|
||||
updateRule(unique: string, rule: RichTextRuleModelSortable) {
|
||||
this.#rules.updateOne(unique, rule);
|
||||
this.sendRulesGetContent();
|
||||
|
||||
}
|
||||
|
||||
setRules(rules: RichTextRuleModelSortable[]) {
|
||||
const newRules = rules.map((r, i) => ({ ...r, sortOrder: i }));
|
||||
this.#rules.next(newRules);
|
||||
this.sendRulesGetContent();
|
||||
}
|
||||
|
||||
setName(value: string) {
|
||||
this.#data.next({ ...this.#data.value, name: value });
|
||||
}
|
||||
|
||||
setContent(value: string) {
|
||||
this.#data.next({ ...this.#data.value, content: value });
|
||||
}
|
||||
|
||||
async load(path: string) {
|
||||
const { data } = await this.repository.requestByPath(path);
|
||||
const [{ data }, rules] = await Promise.all([
|
||||
this.repository.requestById(path),
|
||||
this.repository.getStylesheetRules(path),
|
||||
]);
|
||||
|
||||
if (data) {
|
||||
this.setIsNew(false);
|
||||
this.#data.update(data);
|
||||
} else {
|
||||
this.#data.update(undefined);
|
||||
}
|
||||
|
||||
if (rules.data) {
|
||||
const x = rules.data.rules?.map((r, i) => ({ ...r, sortOrder: i })) ?? [];
|
||||
this.#rules.next(x);
|
||||
} else {
|
||||
this.#rules.next([]);
|
||||
}
|
||||
}
|
||||
|
||||
async sendRulesGetContent() {
|
||||
const requestBody = {
|
||||
content: this.getData()?.content,
|
||||
rules: this.getRules(),
|
||||
};
|
||||
const { data } = await this.repository.interpolateStylesheetRules(requestBody);
|
||||
this.setContent(data?.content ?? '');
|
||||
}
|
||||
|
||||
async sendContentGetRules() {
|
||||
|
||||
if (!this.getData()?.content) return;
|
||||
|
||||
const requestBody = {
|
||||
content: this.getData()?.content,
|
||||
};
|
||||
|
||||
const { data } = await this.repository.extractStylesheetRules(requestBody);
|
||||
this.setRules(data?.rules ?? []);
|
||||
}
|
||||
|
||||
findNewSortOrder(rule: RichTextRuleModel, newIndex: number) {
|
||||
const rules = [...this.getRules()].sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
||||
const oldIndex = rules.findIndex((r) => r.name === rule.name);
|
||||
|
||||
if (oldIndex === -1) return false;
|
||||
rules.splice(oldIndex, 1);
|
||||
rules.splice(newIndex, 0, rule);
|
||||
this.setRules(rules.map((r, i) => ({ ...r, sortOrder: i })));
|
||||
return true;
|
||||
}
|
||||
|
||||
public async save() {
|
||||
throw new Error('Save method not implemented.');
|
||||
const stylesheet = this.getData();
|
||||
|
||||
if (!stylesheet) {
|
||||
return Promise.reject('Something went wrong, there is no data for partial view you want to save...');
|
||||
}
|
||||
if (this.getIsNew()) {
|
||||
const createRequestBody = {
|
||||
name: stylesheet.name,
|
||||
content: stylesheet.content,
|
||||
parentPath: stylesheet.path === 'null' ? '' : stylesheet.path + '/',
|
||||
};
|
||||
|
||||
this.repository.create(createRequestBody);
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (!stylesheet.path) return Promise.reject('There is no path');
|
||||
const updateRequestBody: UpdateStylesheetRequestModel = {
|
||||
name: stylesheet.name,
|
||||
existingPath: stylesheet.path,
|
||||
content: stylesheet.content,
|
||||
};
|
||||
this.repository.save(stylesheet.path, updateRequestBody);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async create(parentKey: string | null) {
|
||||
const newStylesheet = {
|
||||
name: '',
|
||||
path: parentKey ?? '',
|
||||
content: '',
|
||||
};
|
||||
this.#data.next(newStylesheet);
|
||||
this.setIsNew(true);
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { serverFilePathFromUrlFriendlyPath } from '../../utils.js';
|
||||
import { UmbStylesheetWorkspaceEditElement } from './stylesheet-workspace-edit.element.js';
|
||||
import { UmbStylesheetWorkspaceEditorElement } from './stylesheet-workspace-editor.element.js';
|
||||
import { UmbStylesheetWorkspaceContext } from './stylesheet-workspace.context.js';
|
||||
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
|
||||
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
@@ -10,25 +10,35 @@ import { UmbWorkspaceIsNewRedirectController } from '@umbraco-cms/backoffice/wor
|
||||
@customElement('umb-stylesheet-workspace')
|
||||
export class UmbStylesheetWorkspaceElement extends UmbLitElement {
|
||||
#workspaceContext = new UmbStylesheetWorkspaceContext(this);
|
||||
#element = new UmbStylesheetWorkspaceEditElement();
|
||||
#element = new UmbStylesheetWorkspaceEditorElement();
|
||||
|
||||
@state()
|
||||
_routes: UmbRoute[] = [
|
||||
{
|
||||
path: 'edit/:path',
|
||||
component: () => this.#element,
|
||||
setup: (_component, info) => {
|
||||
const path = info.match.params.path;
|
||||
const serverPath = serverFilePathFromUrlFriendlyPath(path);
|
||||
this.#workspaceContext.load(serverPath);
|
||||
path: 'create/:path',
|
||||
component: import('./stylesheet-workspace-editor.element.js'),
|
||||
setup: async (_component, info) => {
|
||||
const path = info.match.params.path === 'null' ? null : info.match.params.path;
|
||||
const serverPath = path === null ? null : serverFilePathFromUrlFriendlyPath(path);
|
||||
await this.#workspaceContext.create(serverPath);
|
||||
|
||||
new UmbWorkspaceIsNewRedirectController(
|
||||
this,
|
||||
this.#workspaceContext,
|
||||
this.shadowRoot!.querySelector('umb-router-slot')!
|
||||
this.shadowRoot!.querySelector('umb-router-slot')!,
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'edit/:path',
|
||||
component: import('./stylesheet-workspace-editor.element.js'),
|
||||
setup: (_component, info) => {
|
||||
this.removeControllerByAlias('_observeIsNew');
|
||||
const path = info.match.params.path;
|
||||
const serverPath = serverFilePathFromUrlFriendlyPath(path);
|
||||
this.#workspaceContext.load(serverPath);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
render() {
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css';
|
||||
import { css, html } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { UMB_STYLESHEET_WORKSPACE_CONTEXT, UmbStylesheetWorkspaceContext } from '../../stylesheet-workspace.context.js';
|
||||
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
|
||||
import { UmbCodeEditorElement } from '@umbraco-cms/backoffice/code-editor';
|
||||
|
||||
@customElement('umb-stylesheet-workspace-view-code-editor')
|
||||
export class UmbStylesheetWorkspaceViewCodeEditorElement extends UmbLitElement {
|
||||
@state()
|
||||
private _content?: string | null = '';
|
||||
|
||||
@state()
|
||||
private _ready?: boolean = false;
|
||||
|
||||
#stylesheetWorkspaceContext?: UmbStylesheetWorkspaceContext;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.consumeContext(UMB_STYLESHEET_WORKSPACE_CONTEXT, (workspaceContext) => {
|
||||
this.#stylesheetWorkspaceContext = workspaceContext;
|
||||
|
||||
this.observe(this.#stylesheetWorkspaceContext.content, (content) => {
|
||||
this._content = content;
|
||||
});
|
||||
|
||||
this.observe(this.#stylesheetWorkspaceContext.isCodeEditorReady, (isReady) => {
|
||||
this._ready = isReady;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.#stylesheetWorkspaceContext?.sendContentGetRules();
|
||||
}
|
||||
|
||||
#onCodeEditorInput(event: Event) {
|
||||
const target = event.target as UmbCodeEditorElement;
|
||||
const value = target.code as string;
|
||||
this.#stylesheetWorkspaceContext?.setContent(value);
|
||||
}
|
||||
|
||||
#renderCodeEditor() {
|
||||
return html`<umb-code-editor
|
||||
language="css"
|
||||
id="content"
|
||||
.code=${this._content ?? ''}
|
||||
@input=${this.#onCodeEditorInput}></umb-code-editor>`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html` <uui-box>
|
||||
<div slot="header" id="code-editor-menu-container"></div>
|
||||
${this._ready
|
||||
? this.#renderCodeEditor()
|
||||
: html`<div id="loader-container">
|
||||
<uui-loader></uui-loader>
|
||||
</div>`}
|
||||
</uui-box>`;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#loader-container {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: calc(100dvh - 300px);
|
||||
}
|
||||
|
||||
umb-code-editor {
|
||||
--editor-height: calc(100dvh - 300px);
|
||||
}
|
||||
|
||||
uui-box {
|
||||
min-height: calc(100dvh - 360px);
|
||||
margin: var(--uui-size-layout-1);
|
||||
--uui-box-default-padding: 0;
|
||||
/* remove header border bottom as code editor looks better in this box */
|
||||
--uui-color-divider-standalone: transparent;
|
||||
}
|
||||
|
||||
#workspace-header {
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
export default UmbStylesheetWorkspaceViewCodeEditorElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-stylesheet-workspace-view-code-editor': UmbStylesheetWorkspaceViewCodeEditorElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { UmbStylesheetWorkspaceContext } from '../../stylesheet-workspace.context.js';
|
||||
import { UMB_MODAL_TEMPLATING_STYLESHEET_RTF_STYLE_SIDEBAR_MODAL } from './stylesheet-workspace-view-rich-text-editor.element.js';
|
||||
import { UUITextStyles } from '@umbraco-cms/backoffice/external/uui';
|
||||
import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { RichTextRuleModel } from '@umbraco-cms/backoffice/backend-api';
|
||||
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
|
||||
import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
|
||||
import { UMB_MODAL_MANAGER_CONTEXT_TOKEN, UmbModalManagerContext } from '@umbraco-cms/backoffice/modal';
|
||||
|
||||
@customElement('umb-stylesheet-rich-text-editor-rule')
|
||||
export default class UmbStylesheetRichTextEditorRuleElement extends UmbLitElement {
|
||||
@property({ type: Object, attribute: false })
|
||||
private rule: RichTextRuleModel | null = null;
|
||||
|
||||
#context?: UmbStylesheetWorkspaceContext;
|
||||
private _modalContext?: UmbModalManagerContext;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.consumeContext(UMB_WORKSPACE_CONTEXT, (workspaceContext) => {
|
||||
this.#context = workspaceContext as UmbStylesheetWorkspaceContext;
|
||||
});
|
||||
|
||||
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT_TOKEN, (instance) => {
|
||||
this._modalContext = instance;
|
||||
});
|
||||
}
|
||||
|
||||
openModal = () => {
|
||||
if (!this._modalContext) throw new Error('Modal context not found');
|
||||
const modal = this._modalContext.open(UMB_MODAL_TEMPLATING_STYLESHEET_RTF_STYLE_SIDEBAR_MODAL, {
|
||||
rule: this.rule,
|
||||
});
|
||||
modal?.onSubmit().then((result) => {
|
||||
if (result.rule && this.rule?.name) {
|
||||
this.#context?.updateRule(this.rule?.name, result.rule);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
removeRule = () => {
|
||||
//TODO: SPORTER BREAKS THAT - rules are removed from the data but not from the DOM
|
||||
if (!this.#context) throw new Error('Context not found');
|
||||
this.#context.setRules(this.#context.getRules().filter((r) => r.name !== this.rule?.name));
|
||||
};
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="rule-name"><uui-icon name="umb:navigation"></uui-icon>${this.rule?.name}</div>
|
||||
<div class="rule-actions">
|
||||
<uui-button label="Edit" look="secondary" @click=${this.openModal}>Edit</uui-button
|
||||
><uui-button label="Remove" look="secondary" color="danger" @click=${this.removeRule}>Remove</uui-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
padding: var(--uui-size-2);
|
||||
align-items: center;
|
||||
border-radius: var(--uui-border-radius);
|
||||
background-color: var(--uui-color-surface-alt);
|
||||
margin-bottom: var(--uui-size-space-4);
|
||||
}
|
||||
|
||||
.rule-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--uui-size-2);
|
||||
padding-left: var(--uui-size-2);
|
||||
font-weight: bold;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-stylesheet-rich-text-editor-rule': UmbStylesheetRichTextEditorRuleElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import { RichTextRuleModelSortable } from '../../stylesheet-workspace.context.js';
|
||||
import { UUITextStyles } from '@umbraco-cms/backoffice/external/uui';
|
||||
import { css, html, customElement, ifDefined, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbModalBaseElement } from '@umbraco-cms/internal/modal';
|
||||
import { RichTextRuleModel } from '@umbraco-cms/backoffice/backend-api';
|
||||
|
||||
export interface StylesheetRichTextEditorStyleModalData {
|
||||
rule: RichTextRuleModelSortable | null;
|
||||
}
|
||||
|
||||
export type StylesheetRichTextEditorStyleModalResult = NonNullable<Required<StylesheetRichTextEditorStyleModalData>>;
|
||||
|
||||
@customElement('umb-stylesheet-rich-text-editor-style-modal')
|
||||
export default class UmbStylesheetRichTextEditorStyleModalElement extends UmbModalBaseElement<
|
||||
StylesheetRichTextEditorStyleModalData,
|
||||
StylesheetRichTextEditorStyleModalResult
|
||||
> {
|
||||
private _close() {
|
||||
this.modalContext?.reject();
|
||||
}
|
||||
|
||||
#submit() {
|
||||
this.modalContext?.submit({ rule: this._rule });
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._rule = this.data?.rule ?? null;
|
||||
}
|
||||
|
||||
@state()
|
||||
private _rule: RichTextRuleModel | null = null;
|
||||
|
||||
#updateName(event: Event) {
|
||||
const name = (event.target as HTMLInputElement).value;
|
||||
|
||||
this._rule = {
|
||||
...this._rule,
|
||||
name,
|
||||
};
|
||||
}
|
||||
|
||||
#updateSelector(event: Event) {
|
||||
const selector = (event.target as HTMLInputElement).value;
|
||||
|
||||
this._rule = {
|
||||
...this._rule,
|
||||
selector,
|
||||
};
|
||||
}
|
||||
|
||||
#updateStyles(event: Event) {
|
||||
const styles = (event.target as HTMLInputElement).value;
|
||||
|
||||
this._rule = {
|
||||
...this._rule,
|
||||
styles,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<umb-body-layout headline="Edit style">
|
||||
<div id="main">
|
||||
<uui-box>
|
||||
<uui-form>
|
||||
<form id="MyForm" name="myForm">
|
||||
<uui-form-layout-item>
|
||||
<uui-label for="name" slot="label" required>Name</uui-label>
|
||||
<span slot="description">The name displayed in the editor style selector</span>
|
||||
<uui-input
|
||||
id="name"
|
||||
name="name"
|
||||
.value=${this._rule?.name ?? ''}
|
||||
label="Rule name"
|
||||
required
|
||||
@input=${this.#updateName}>
|
||||
</uui-input>
|
||||
</uui-form-layout-item>
|
||||
<uui-form-layout-item>
|
||||
<uui-label for="selector" slot="label" required>Selector</uui-label>
|
||||
<span slot="description">Uses CSS syntax, e.g. "h1" or ".redHeader"</span>
|
||||
<uui-input
|
||||
id="selector"
|
||||
name="selector"
|
||||
.value=${this._rule?.selector ?? ''}
|
||||
label="Rule selector"
|
||||
@input=${this.#updateSelector}
|
||||
required>
|
||||
</uui-input>
|
||||
</uui-form-layout-item>
|
||||
<uui-form-layout-item>
|
||||
<uui-label for="styles" slot="label" required="">Styles</uui-label>
|
||||
<span slot="description"
|
||||
>The CSS that should be applied in the rich text editor, e.g. "color:red;"</span
|
||||
>
|
||||
<uui-textarea
|
||||
@input=${this.#updateStyles}
|
||||
id="styles"
|
||||
name="styles"
|
||||
.value=${this._rule?.styles ?? ''}
|
||||
label="Rule styles">
|
||||
</uui-textarea>
|
||||
</uui-form-layout-item>
|
||||
<uui-form-layout-item>
|
||||
<uui-label for="styles" slot="label" required="">Preview</uui-label>
|
||||
<span slot="description">How the text will look like in the rich text editor.</span>
|
||||
<div style="${ifDefined(this._rule?.styles)}">
|
||||
a b c d e f g h i j k l m n o p q r s t u v w x t z
|
||||
<br />
|
||||
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
|
||||
<br />
|
||||
1 2 3 4 5 6 7 8 9 0 € £ $ % & (.,;:'"!?)
|
||||
<br />
|
||||
Just keep examining every bid quoted for zinc etchings.
|
||||
</div>
|
||||
</uui-form-layout-item>
|
||||
</form>
|
||||
</uui-form>
|
||||
</uui-box>
|
||||
</div>
|
||||
<div slot="actions">
|
||||
<uui-button @click=${this._close} look="secondary" label="Close">Close</uui-button>
|
||||
<uui-button @click=${this.#submit} look="primary" color="positive" label="Submit">Submit</uui-button>
|
||||
</div>
|
||||
</umb-body-layout>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
color: var(--uui-color-text);
|
||||
--umb-header-layout-height: 70px;
|
||||
}
|
||||
|
||||
#main {
|
||||
box-sizing: border-box;
|
||||
height: calc(
|
||||
100dvh - var(--umb-header-layout-height) - var(--umb-footer-layout-height) - 2 * var(--uui-size-layout-1)
|
||||
);
|
||||
}
|
||||
|
||||
#main uui-button:not(:last-of-type) {
|
||||
display: block;
|
||||
margin-bottom: var(--uui-size-space-5);
|
||||
}
|
||||
|
||||
uui-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#styles {
|
||||
font-family:
|
||||
Monaco,
|
||||
Menlo,
|
||||
Consolas,
|
||||
Courier New,
|
||||
monospace;
|
||||
--uui-textarea-min-height: 100px;
|
||||
resize: none;
|
||||
width: 300px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-stylesheet-rich-text-editor-style-modal': UmbStylesheetRichTextEditorStyleModalElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css';
|
||||
import { css, html } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { RichTextRuleModelSortable, UmbStylesheetWorkspaceContext } from '../../stylesheet-workspace.context.js';
|
||||
import { UMB_MODAL_TEMPLATING_STYLESHEET_RTF_STYLE_SIDEBAR } from '../../manifests.js';
|
||||
import {
|
||||
StylesheetRichTextEditorStyleModalData,
|
||||
StylesheetRichTextEditorStyleModalResult,
|
||||
} from './stylesheet-workspace-view-rich-text-editor-style-sidebar.element.js';
|
||||
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
|
||||
import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
|
||||
import { UMB_MODAL_MANAGER_CONTEXT_TOKEN, UmbModalManagerContext, UmbModalToken } from '@umbraco-cms/backoffice/modal';
|
||||
import { RichTextRuleModel } from '@umbraco-cms/backoffice/backend-api';
|
||||
import { UmbSorterConfig, UmbSorterController } from '@umbraco-cms/backoffice/sorter';
|
||||
import { ifDefined, repeat } from '@umbraco-cms/backoffice/external/lit';
|
||||
|
||||
import './stylesheet-workspace-view-rich-text-editor-rule.element.js';
|
||||
|
||||
export const UMB_MODAL_TEMPLATING_STYLESHEET_RTF_STYLE_SIDEBAR_MODAL = new UmbModalToken<
|
||||
StylesheetRichTextEditorStyleModalData,
|
||||
StylesheetRichTextEditorStyleModalResult
|
||||
>(UMB_MODAL_TEMPLATING_STYLESHEET_RTF_STYLE_SIDEBAR, {
|
||||
type: 'sidebar',
|
||||
size: 'medium',
|
||||
});
|
||||
|
||||
const SORTER_CONFIG: UmbSorterConfig<RichTextRuleModel> = {
|
||||
compareElementToModel: (element: HTMLElement, model: RichTextRuleModel) => {
|
||||
return element.getAttribute('data-umb-rule-name') === model.name;
|
||||
},
|
||||
querySelectModelToElement: (container: HTMLElement, modelEntry: RichTextRuleModel) => {
|
||||
return container.querySelector('data-umb-rule-name[' + modelEntry.name + ']');
|
||||
},
|
||||
identifier: 'stylesheet-rules-sorter',
|
||||
itemSelector: 'umb-stylesheet-rich-text-editor-rule',
|
||||
containerSelector: '#rules-container',
|
||||
};
|
||||
|
||||
@customElement('umb-stylesheet-workspace-view-rich-text-editor')
|
||||
export class UmbStylesheetWorkspaceViewRichTextEditorElement extends UmbLitElement {
|
||||
@state()
|
||||
_rules: RichTextRuleModelSortable[] = [];
|
||||
|
||||
#context?: UmbStylesheetWorkspaceContext;
|
||||
private _modalContext?: UmbModalManagerContext;
|
||||
|
||||
#sorter = new UmbSorterController(this, {
|
||||
...SORTER_CONFIG,
|
||||
performItemInsert: ({ item, newIndex }) => {
|
||||
return this.#context?.findNewSortOrder(item, newIndex) ?? false;
|
||||
},
|
||||
performItemRemove: () => {
|
||||
//defined so the default does not run
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.consumeContext(UMB_WORKSPACE_CONTEXT, (workspaceContext) => {
|
||||
this.#context = workspaceContext as UmbStylesheetWorkspaceContext;
|
||||
|
||||
this.observe(this.#context.rules, (rules) => {
|
||||
this._rules = rules;
|
||||
this.#sorter.setModel(this._rules);
|
||||
});
|
||||
});
|
||||
|
||||
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT_TOKEN, (instance) => {
|
||||
this._modalContext = instance;
|
||||
});
|
||||
}
|
||||
|
||||
openModal = (rule: RichTextRuleModelSortable | null = null) => {
|
||||
if (!this._modalContext) throw new Error('Modal context not found');
|
||||
const modal = this._modalContext.open(UMB_MODAL_TEMPLATING_STYLESHEET_RTF_STYLE_SIDEBAR_MODAL, {
|
||||
rule,
|
||||
});
|
||||
modal?.onSubmit().then((result) => {
|
||||
if (result.rule) {
|
||||
this.#context?.setRules([...this._rules, { ...result.rule, sortOrder: this._rules.length }]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
removeRule = (rule: RichTextRuleModelSortable) => {
|
||||
const rules = this._rules?.filter((r) => r.name !== rule.name);
|
||||
this.#context?.setRules(rules);
|
||||
};
|
||||
|
||||
render() {
|
||||
return html` <uui-box headline="Rich text editor styles">
|
||||
<div id="box-row">
|
||||
<p id="description">Define the styles that should be available in the rich text editor for this stylesheet.</p>
|
||||
<div id="rules">
|
||||
<div id="rules-container">
|
||||
${repeat(
|
||||
this._rules,
|
||||
(rule) => rule?.name ?? '' + rule?.sortOrder ?? '',
|
||||
(rule) =>
|
||||
html`<umb-stylesheet-rich-text-editor-rule
|
||||
.rule=${rule}
|
||||
data-umb-rule-name="${ifDefined(rule?.name)}"></umb-stylesheet-rich-text-editor-rule>`,
|
||||
)}
|
||||
</div>
|
||||
<uui-button label="Add rule" look="primary" @click=${() => this.openModal(null)}>Add</uui-button>
|
||||
</div>
|
||||
</div>
|
||||
</uui-box>`;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#box-row {
|
||||
display: flex;
|
||||
gap: var(--uui-size-layout-1);
|
||||
}
|
||||
|
||||
#description {
|
||||
margin-top: 0;
|
||||
flex: 0 0 250px;
|
||||
}
|
||||
|
||||
#rules {
|
||||
flex: 1 1 auto;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
uui-box {
|
||||
margin: var(--uui-size-layout-1);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
export default UmbStylesheetWorkspaceViewRichTextEditorElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-stylesheet-workspace-view-rich-text-editor': UmbStylesheetWorkspaceViewRichTextEditorElement;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user