diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/stylesheet.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/stylesheet.data.ts index 05548ef529..26f845a4b1 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/stylesheet.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/stylesheet.data.ts @@ -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 = [ { 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 = [ // 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 { +class UmbStylesheetData extends UmbData { constructor() { super(data); } @@ -71,6 +137,160 @@ class UmbStylesheetData extends UmbEntityData { 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*(?[^*\r\n]*?)\s*\*\/\s*(?[^,{]*?)\s*{\s*(?.*?)\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*(?[^*\r\n]*?)\s*\*\/\s*(?[^,{]*?)\s*{\s*(?.*?)\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*(?[^*\r\n]*?)\s*\*\/\s*(?[^,{]*?)\s*{\s*(?.*?)\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) { + 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(); diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/utils.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/utils.ts index df924c2e71..33adf91412 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/utils.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/utils.ts @@ -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, +}); diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/stylesheet.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/stylesheet.handlers.ts index b4f3c3caff..3ae10d1f33 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/stylesheet.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/stylesheet.handlers.ts @@ -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]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-editor/workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-editor/workspace-editor.element.ts index 6f304dffd0..9ff49f64a2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-editor/workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-editor/workspace-editor.element.ts @@ -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() diff --git a/src/Umbraco.Web.UI.Client/src/packages/settings/languages/app-language-select/app-language-select.element.ts b/src/Umbraco.Web.UI.Client/src/packages/settings/languages/app-language-select/app-language-select.element.ts index a3bd286ada..2219fef411 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/settings/languages/app-language-select/app-language-select.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/settings/languages/app-language-select/app-language-select.element.ts @@ -94,14 +94,13 @@ export class UmbAppLanguageSelectElement extends UmbLitElement { ${repeat( this._languages, (language) => language.isoCode, - (language) => - html` - - ` + (language) => html` + + `, )} `; } @@ -127,6 +126,7 @@ export class UmbAppLanguageSelectElement extends UmbLitElement { align-items: center; justify-content: space-between; cursor: pointer; + font-family: inherit; } #toggle:hover { diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/manifests.ts index aa37da276e..bcc1996c93 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/manifests.ts @@ -16,11 +16,10 @@ const workspaceActions: Array = [ 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: [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace.context.ts index dca98153e4..89792790cc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace.context.ts @@ -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'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/config.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/config.ts new file mode 100644 index 0000000000..cb6e44810e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/config.ts @@ -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'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/create/create-rtf.action.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/create/create-rtf.action.ts new file mode 100644 index 0000000000..9bc1e2ec28 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/create/create-rtf.action.ts @@ -0,0 +1,16 @@ +import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbCreateRTFStylesheetAction }> extends UmbEntityActionBase { + 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`, + ); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/create/create.action.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/create/create.action.ts new file mode 100644 index 0000000000..b5b4e71368 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/create/create.action.ts @@ -0,0 +1,12 @@ +import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbCreateStylesheetAction }> extends UmbEntityActionBase { + 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`); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/manifests.ts new file mode 100644 index 0000000000..cb90d2b298 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/manifests.ts @@ -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 = [ + { + 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 = [ + { + 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]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/index.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/index.ts index 22804ab3ff..ec91672f86 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/index.ts @@ -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'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/manifests.ts index ebc952eb39..12f1863639 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/manifests.ts @@ -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, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/repository/manifests.ts index ef28d9ac67..b645728c43 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/repository/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/repository/manifests.ts @@ -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', diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/repository/sources/stylesheet.folder.server.data.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/repository/sources/stylesheet.folder.server.data.ts new file mode 100644 index 0000000000..a20273ca95 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/repository/sources/stylesheet.folder.server.data.ts @@ -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> { + return tryExecuteAndNotify(this.#host, StylesheetResource.getStylesheetFolder({ path: unique })); + } + insert(requestBody: CreateFolderRequestModel): Promise> { + return tryExecuteAndNotify(this.#host, StylesheetResource.postStylesheetFolder({ requestBody })); + } + delete(path: string): Promise> { + return tryExecuteAndNotify(this.#host, StylesheetResource.deleteStylesheetFolder({ path })); + } + update(unique: string, data: CreateFolderRequestModel): Promise> { + throw new Error('Method not implemented.'); + } + createScaffold(parentId: string | null): Promise> { + throw new Error('Method not implemented.'); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/repository/sources/stylesheet.server.data.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/repository/sources/stylesheet.server.data.ts index e0844ce428..43edfa7ac1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/repository/sources/stylesheet.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/repository/sources/stylesheet.server.data.ts @@ -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 { +export class UmbStylesheetServerDataSource + implements UmbDataSource +{ #host: UmbControllerHostElement; /** @@ -31,17 +44,79 @@ export class UmbStylesheetServerDataSource implements UmbDataSource> { - throw new Error('Method not implemented.'); + /** + * Creates a new Stylesheet + * + * @param {StylesheetDetails} data + * @return {*} {Promise>} + * @memberof UmbStylesheetServerDataSource + */ + insert(data: StylesheetDetails): Promise> { + return tryExecuteAndNotify(this.#host, StylesheetResource.postStylesheet({ requestBody: data })); } + /** + * Updates an existing Stylesheet + * + * @param {string} path + * @param {StylesheetDetails} data + * @return {*} {Promise>} + * @memberof UmbStylesheetServerDataSource + */ update(path: string, data: StylesheetDetails): Promise> { - throw new Error('Method not implemented.'); + return tryExecuteAndNotify(this.#host, StylesheetResource.putStylesheet({ requestBody: data })); } + /** + * Deletes a Stylesheet. + * + * @param {string} path + * @return {*} {Promise} + * @memberof UmbStylesheetServerDataSource + */ delete(path: string): Promise { - 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>)} + * @memberof UmbStylesheetServerDataSource + */ + getStylesheetRichTextRules( + path: string + ): Promise> { + 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>} + * @memberof UmbStylesheetServerDataSource + */ + postStylesheetRichTextExtractRules( + data: ExtractRichTextStylesheetRulesRequestModel + ): Promise> { + 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>} + * @memberof UmbStylesheetServerDataSource + */ + postStylesheetRichTextInterpolateRules( + data: InterpolateRichTextStylesheetRequestModel + ): Promise> { + return tryExecuteAndNotify( + this.#host, + StylesheetResource.postStylesheetRichTextInterpolateRules({ requestBody: data }) + ); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/repository/stylesheet.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/repository/stylesheet.repository.ts index 5e1a4e41b2..5e625766aa 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/repository/stylesheet.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/repository/stylesheet.repository.ts @@ -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 + implements + UmbTreeRepository, + UmbDetailRepository, + 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 | undefined, + ): Promise> { + throw new Error('Method not implemented.'); + } + + async requestById(id: string): Promise> { + 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> { + throw new Error('Method not implemented.'); + } + async create(data: CreateTextFileViewModelBaseModel): Promise> { + const promise = this.#dataSource.insert(data); + await promise; + this.requestTreeItemsOf(data.parentPath ? data.parentPath : null); + return promise; + } + save(id: string, data: UpdateTextFileViewModelBaseModel): Promise { + return this.#dataSource.update(id, data); + } + delete(id: string): Promise { + const promise = this.#dataSource.delete(id); + const parentPath = id.substring(0, id.lastIndexOf('/')); + this.requestTreeItemsOf(parentPath ? parentPath : null); + return promise; + } + + getStylesheetRules( + path: string, + ): Promise> { + return this.#dataSource.getStylesheetRichTextRules(path); + } + /** + * Existing content + array of rules => new content string + * + * @param {InterpolateRichTextStylesheetRequestModel} data + * @return {*} {Promise>} + * @memberof UmbStylesheetRepository + */ + interpolateStylesheetRules( + data: InterpolateRichTextStylesheetRequestModel, + ): Promise> { + return this.#dataSource.postStylesheetRichTextInterpolateRules(data); + } + /** + * content string => array of rules + * + * @param {ExtractRichTextStylesheetRulesRequestModel} data + * @return {*} {Promise>} + * @memberof UmbStylesheetRepository + */ + extractStylesheetRules( + data: ExtractRichTextStylesheetRulesRequestModel, + ): Promise> { + 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) { + async requestItems(paths: Array) { 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 } diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/tree/manifests.ts index 4118b0b889..108889cea8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/tree/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/tree/manifests.ts @@ -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'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/manifests.ts index f341fbee8f..6f6bfff160 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/manifests.ts @@ -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 = []; -const workspaceActions: Array = []; +const workspaceEditorViews: Array = [ + { + 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 = [ + { + 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 = [ + { + 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]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace-edit.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace-edit.element.ts deleted file mode 100644 index 570660fdc4..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace-edit.element.ts +++ /dev/null @@ -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` Stylesheet workspace `; - } - - static styles = [ - UmbTextStyles, - css` - :host { - display: block; - width: 100%; - height: 100%; - } - `, - ]; -} - -export default UmbStylesheetWorkspaceEditElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-stylesheet-workspace-edit': UmbStylesheetWorkspaceEditElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace-editor.element.ts new file mode 100644 index 0000000000..fcea614d8a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace-editor.element.ts @@ -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(); + + 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` + + + +
+ + + Keyboard Shortcuts + + ALT + + + shift + + + k + + +
+
+ `; + } + + 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; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.context.ts index fd1ba1098a..b91f3fc128 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.context.ts @@ -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 implements UmbEntityWorkspaceContextInterface { #data = new UmbObjectState(undefined); + #rules = new UmbArrayState([], (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 { diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.element.ts index 0529dcecdc..5948f25b30 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.element.ts @@ -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() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/views/code-editor/stylesheet-workspace-view-code-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/views/code-editor/stylesheet-workspace-view-code-editor.element.ts new file mode 100644 index 0000000000..5486ff8d61 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/views/code-editor/stylesheet-workspace-view-code-editor.element.ts @@ -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``; + } + + render() { + return html` +
+ ${this._ready + ? this.#renderCodeEditor() + : html`
+ +
`} +
`; + } + + 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; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/views/rich-text-editor/stylesheet-workspace-view-rich-text-editor-rule.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/views/rich-text-editor/stylesheet-workspace-view-rich-text-editor-rule.element.ts new file mode 100644 index 0000000000..71cd598dbf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/views/rich-text-editor/stylesheet-workspace-view-rich-text-editor-rule.element.ts @@ -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` +
${this.rule?.name}
+
+ EditRemove +
+ `; + } + + 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; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/views/rich-text-editor/stylesheet-workspace-view-rich-text-editor-style-sidebar.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/views/rich-text-editor/stylesheet-workspace-view-rich-text-editor-style-sidebar.element.ts new file mode 100644 index 0000000000..5d383ac3c1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/views/rich-text-editor/stylesheet-workspace-view-rich-text-editor-style-sidebar.element.ts @@ -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>; + +@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` + +
+ + +
+ + Name + The name displayed in the editor style selector + + + + + Selector + Uses CSS syntax, e.g. "h1" or ".redHeader" + + + + + Styles + The CSS that should be applied in the rich text editor, e.g. "color:red;" + + + + + Preview + How the text will look like in the rich text editor. +
+ 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 +
+ 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 +
+ 1 2 3 4 5 6 7 8 9 0 € £ $ % & (.,;:'"!?) +
+ Just keep examining every bid quoted for zinc etchings. +
+
+
+
+
+
+
+ Close + Submit +
+
+ `; + } + + 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; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/views/rich-text-editor/stylesheet-workspace-view-rich-text-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/views/rich-text-editor/stylesheet-workspace-view-rich-text-editor.element.ts new file mode 100644 index 0000000000..2375222055 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/views/rich-text-editor/stylesheet-workspace-view-rich-text-editor.element.ts @@ -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 = { + 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` +
+

Define the styles that should be available in the rich text editor for this stylesheet.

+
+
+ ${repeat( + this._rules, + (rule) => rule?.name ?? '' + rule?.sortOrder ?? '', + (rule) => + html``, + )} +
+ this.openModal(null)}>Add +
+
+
`; + } + + 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; + } +}