Merge remote-tracking branch 'origin/main' into feature/property-editor-ui-config-collection

This commit is contained in:
Niels Lyngsø
2023-09-07 09:50:36 +02:00
27 changed files with 1619 additions and 107 deletions

View File

@@ -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();

View File

@@ -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,
});

View File

@@ -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];

View File

@@ -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()

View File

@@ -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 {

View File

@@ -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: [

View File

@@ -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');

View File

@@ -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';

View File

@@ -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`,
);
}
}

View File

@@ -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`);
}
}

View File

@@ -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];

View File

@@ -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';

View File

@@ -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,
];

View File

@@ -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',

View File

@@ -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.');
}
}

View File

@@ -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 })
);
}
}

View File

@@ -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
}

View File

@@ -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';

View File

@@ -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];

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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 {

View File

@@ -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() {

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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 € £ $ % &amp; (.,;:'"!?)
<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;
}
}

View File

@@ -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;
}
}