diff --git a/src/mocks/browser-handlers.ts b/src/mocks/browser-handlers.ts index a477b869cc..6af78c329c 100644 --- a/src/mocks/browser-handlers.ts +++ b/src/mocks/browser-handlers.ts @@ -31,6 +31,7 @@ import { handlers as stylesheetHandlers } from './handlers/stylesheet.handlers.j import { handlers as partialViewsHandlers } from './handlers/partial-views.handlers.js'; import { handlers as tagHandlers } from './handlers/tag-handlers.js'; import { handlers as configHandlers } from './handlers/config.handlers.js'; +import { handlers as scriptHandlers } from './handlers/scripts.handlers.js'; const handlers = [ serverHandlers.serverVersionHandler, @@ -65,6 +66,7 @@ const handlers = [ ...partialViewsHandlers, ...tagHandlers, ...configHandlers, + ...scriptHandlers, ]; switch (import.meta.env.VITE_UMBRACO_INSTALL_STATUS) { diff --git a/src/mocks/data/scripts.data.ts b/src/mocks/data/scripts.data.ts new file mode 100644 index 0000000000..0b43b6022a --- /dev/null +++ b/src/mocks/data/scripts.data.ts @@ -0,0 +1,238 @@ +import { UmbData } from './data.js'; +import { UmbEntityData } from './entity.data.js'; +import { createFileItemResponseModelBaseModel, createFileSystemTreeItem, createTextFileItem } from './utils.js'; +import { + CreatePathFolderRequestModel, + CreateTextFileViewModelBaseModel, + FileSystemTreeItemPresentationModel, + PagedFileSystemTreeItemPresentationModel, + ScriptItemResponseModel, + ScriptResponseModel, + UpdateScriptRequestModel, +} from '@umbraco-cms/backoffice/backend-api'; + +type ScriptsDataItem = ScriptResponseModel & FileSystemTreeItemPresentationModel; + +export const data: Array = [ + { + path: 'some-folder', + isFolder: true, + name: 'some-folder', + type: 'script', + hasChildren: true, + }, + { + path: 'another-folder', + isFolder: true, + name: 'another-folder', + type: 'script', + hasChildren: true, + }, + { + path: 'very important folder', + isFolder: true, + name: 'very important folder', + type: 'script', + hasChildren: true, + }, + { + path: 'some-folder/ugly script.js', + isFolder: false, + name: 'ugly script.js', + type: 'script', + hasChildren: false, + content: `function makeid(length) { + var result = ''; + var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + var charactersLength = characters.length; + for ( var i = 0; i < length; i++ ) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; + } + + console.log(makeid(5));`, + }, + { + path: 'some-folder/nice script.js', + isFolder: false, + name: 'nice script.js', + type: 'script', + hasChildren: false, + content: `var items = { + "item_1": "1", + "item_2": "2", + "item_3": "3" + } + for (var item in items) { + console.log(items[item]); + }`, + }, + { + path: 'another-folder/only bugs.js', + isFolder: false, + name: 'only bugs.js', + type: 'script', + hasChildren: false, + content: `var my_arr = [4, '', 0, 10, 7, '', false, 10]; + + my_arr = my_arr.filter(Boolean); + + console.log(my_arr);`, + }, + { + path: 'very important folder/no bugs at all.js', + isFolder: false, + name: 'no bugs at all.js', + type: 'script', + hasChildren: false, + content: `const date_str = "07/20/2021"; + const date = new Date(date_str); + const full_day_name = date.toLocaleDateString('default', { weekday: 'long' }); + // -> to get full day name e.g. Tuesday + + const short_day_name = date.toLocaleDateString('default', { weekday: 'short' }); + console.log(short_day_name); + // -> TO get the short day name e.g. Tue`, + }, + { + path: 'very important folder/nope.js', + isFolder: false, + name: 'nope.js', + type: 'script', + hasChildren: false, + content: `// Define an object + const employee = { + "name": "John Deo", + "department": "IT", + "project": "Inventory Manager" + }; + + // Remove a property + delete employee["project"]; + + console.log(employee);`, + }, +]; + +class UmbScriptsData extends UmbData { + constructor() { + super(data); + } + + getTreeRoot(): PagedFileSystemTreeItemPresentationModel { + const items = this.data.filter((item) => item.path?.includes('/') === false); + const treeItems = items.map((item) => createFileSystemTreeItem(item)); + const total = items.length; + return { items: treeItems, total }; + } + + getTreeItemChildren(parentPath: string): PagedFileSystemTreeItemPresentationModel { + const items = this.data.filter((item) => item.path?.startsWith(parentPath)); + const treeItems = items.map((item) => createFileSystemTreeItem(item)); + const total = items.length; + return { items: treeItems, total }; + } + + getTreeItem(paths: Array): Array { + const items = this.data.filter((item) => paths.includes(item.path ?? '')); + return items.map((item) => createFileSystemTreeItem(item)); + } + + getItem(paths: Array): Array { + const items = this.data.filter((item) => paths.includes(item.path ?? '')); + return items.map((item) => createFileItemResponseModelBaseModel(item)); + } + + getFolder(path: string): FileSystemTreeItemPresentationModel { + const items = data.filter((item) => item.isFolder && item.path === path); + return items as FileSystemTreeItemPresentationModel; + } + + postFolder(payload: CreatePathFolderRequestModel) { + const newFolder = { + path: `${payload.parentPath ?? ''}/${payload.name}`, + isFolder: true, + name: payload.name, + type: 'script', + hasChildren: false, + }; + return this.insert(newFolder); + } + + deleteFolder(path: string) { + return this.delete([path]); + } + + getScript(path: string): ScriptResponseModel | undefined { + return createTextFileItem(this.data.find((item) => item.path === path)); + } + + insertScript(item: CreateTextFileViewModelBaseModel) { + const newItem: ScriptsDataItem = { + ...item, + path: `${item.parentPath}/${item.name}.js}`, + isFolder: false, + hasChildren: false, + type: 'script', + }; + + this.insert(newItem); + return newItem; + } + + insert(item: ScriptsDataItem) { + 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: UpdateScriptRequestModel) { + 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 pathsOfItemsToDelete = this.data + .filter((item) => { + if (!item.path) throw new Error('Item has no path'); + return paths.includes(item.path); + }) + .map((item) => item.path); + + this.data = this.data.filter((item) => { + if (!item.path) throw new Error('Item has no path'); + return paths.indexOf(item.path) === -1; + }); + + return pathsOfItemsToDelete; + } +} + +export const umbScriptsData = new UmbScriptsData(); diff --git a/src/mocks/handlers/scripts.handlers.ts b/src/mocks/handlers/scripts.handlers.ts new file mode 100644 index 0000000000..409bc297ee --- /dev/null +++ b/src/mocks/handlers/scripts.handlers.ts @@ -0,0 +1,86 @@ +const { rest } = window.MockServiceWorker; +import { RestHandler, MockedRequest, DefaultBodyType } from 'msw'; +import { umbScriptsData } from '../data/scripts.data.js'; +import { umbracoPath } from '@umbraco-cms/backoffice/utils'; +import { CreatePathFolderRequestModel, CreateTextFileViewModelBaseModel } from '@umbraco-cms/backoffice/backend-api'; + +const treeHandlers = [ + rest.get(umbracoPath('/tree/script/root'), (req, res, ctx) => { + const response = umbScriptsData.getTreeRoot(); + return res(ctx.status(200), ctx.json(response)); + }), + + rest.get(umbracoPath('/tree/script/children'), (req, res, ctx) => { + const path = req.url.searchParams.get('path'); + if (!path) return; + + const response = umbScriptsData.getTreeItemChildren(path); + return res(ctx.status(200), ctx.json(response)); + }), + + rest.get(umbracoPath('/tree/script/item'), (req, res, ctx) => { + const paths = req.url.searchParams.getAll('paths'); + if (!paths) return; + + const items = umbScriptsData.getTreeItem(paths); + return res(ctx.status(200), ctx.json(items)); + }), +]; + +const detailHandlers: RestHandler>[] = [ + rest.get(umbracoPath('/script'), (req, res, ctx) => { + const path = decodeURIComponent(req.url.searchParams.get('path') ?? '').replace('-js', '.js'); + if (!path) return res(ctx.status(400)); + const response = umbScriptsData.getScript(path); + return res(ctx.status(200), ctx.json(response)); + }), + + rest.get(umbracoPath('/script/item'), (req, res, ctx) => { + const path = decodeURIComponent(req.url.searchParams.get('path') ?? '').replace('-js', '.js'); + if (!path) return res(ctx.status(400, 'no body found')); + const response = umbScriptsData.getItem([path]); + return res(ctx.status(200), ctx.json(response)); + }), + + rest.post(umbracoPath('/script'), async (req, res, ctx) => { + const requestBody = (await req.json()) as CreateTextFileViewModelBaseModel; + if (!requestBody) return res(ctx.status(400, 'no body found')); + const response = umbScriptsData.insertScript(requestBody); + return res(ctx.status(200), ctx.json(response)); + }), + + rest.delete(umbracoPath('/script'), (req, res, ctx) => { + const path = req.url.searchParams.get('path'); + if (!path) return res(ctx.status(400)); + const response = umbScriptsData.delete([path]); + return res(ctx.status(200), ctx.json(response)); + }), + rest.put(umbracoPath('/script'), async (req, res, ctx) => { + const requestBody = (await req.json()) as CreateTextFileViewModelBaseModel; + if (!requestBody) return res(ctx.status(400, 'no body found')); + const response = umbScriptsData.updateData(requestBody); + return res(ctx.status(200)); + }), +]; + +const folderHandlers: RestHandler>[] = [ + rest.get(umbracoPath('script/folder'), (req, res, ctx) => { + const path = decodeURIComponent(req.url.searchParams.get('path') ?? '').replace('-js', '.js'); + if (!path) return res(ctx.status(400)); + const response = umbScriptsData.getFolder(path); + return res(ctx.status(200), ctx.json(response)); + }), + rest.post(umbracoPath('script/folder'), (req, res, ctx) => { + const requestBody = req.json() as CreatePathFolderRequestModel; + if (!requestBody) return res(ctx.status(400, 'no body found')); + return res(ctx.status(200)); + }), + rest.delete(umbracoPath('script/folder'), (req, res, ctx) => { + const path = decodeURIComponent(req.url.searchParams.get('path') ?? '').replace('-js', '.js'); + if (!path) return res(ctx.status(400)); + const response = umbScriptsData.deleteFolder(path); + return res(ctx.status(200), ctx.json(response)); + }), +]; + +export const handlers = [...treeHandlers, ...detailHandlers, ...folderHandlers]; diff --git a/src/packages/templating/manifests.ts b/src/packages/templating/manifests.ts index 158a216974..4e83ef08d4 100644 --- a/src/packages/templating/manifests.ts +++ b/src/packages/templating/manifests.ts @@ -2,6 +2,7 @@ import { manifests as menuManifests } from './menu.manifests.js'; import { manifests as templateManifests } from './templates/manifests.js'; import { manifests as stylesheetManifests } from './stylesheets/manifests.js'; import { manifests as partialManifests } from './partial-views/manifests.js'; +import { manifests as scriptsManifest } from './scripts/manifests.js'; import { manifests as modalManifests } from './modals/manifests.js'; export const manifests = [ @@ -10,4 +11,5 @@ export const manifests = [ ...stylesheetManifests, ...partialManifests, ...modalManifests, + ...scriptsManifest, ]; diff --git a/src/packages/templating/scripts/config.ts b/src/packages/templating/scripts/config.ts new file mode 100644 index 0000000000..1cdd3d28da --- /dev/null +++ b/src/packages/templating/scripts/config.ts @@ -0,0 +1,34 @@ +import { ScriptResponseModel } from '@umbraco-cms/backoffice/backend-api'; + +export type ScriptDetails = ScriptResponseModel; + +//ENTITY TYPES +export const SCRIPTS_ENTITY_TYPE = 'script'; +export const SCRIPTS_ROOT_ENTITY_TYPE = 'script-root'; +export const SCRIPTS_FOLDER_ENTITY_TYPE = 'script-folder'; +export const SCRIPTS_FOLDER_EMPTY_ENTITY_TYPE = 'script-folder-empty'; + + +export const SCRIPTS_STORE_ALIAS = 'Umb.Store.Scripts'; +export const UMB_SCRIPTS_STORE_CONTEXT_TOKEN_ALIAS = 'Umb.Store.Scripts.Context.Token'; + +export const SCRIPTS_REPOSITORY_ALIAS = 'Umb.Repository.Scripts'; + +export const SCRIPTS_MENU_ITEM_ALIAS = 'Umb.MenuItem.Scripts'; + +//TREE +export const SCRIPTS_TREE_ALIAS = 'Umb.Tree.Scripts'; +export const SCRIPTS_TREE_ITEM_ALIAS = 'Umb.TreeItem.Scripts'; +export const SCRIPTS_TREE_STORE_ALIAS = 'Umb.Store.Scripts.Tree'; +export const UMB_SCRIPTS_TREE_STORE_CONTEXT_TOKEN_ALIAS = 'Umb.Store.Scripts.Tree.Context.Token'; + +//ENTITY (tree) ACTIONS +export const SCRIPTS_ENTITY_ACTION_DELETE_ALIAS = 'Umb.EntityAction.Scripts.Delete'; +export const SCRIPTS_ENTITY_ACTION_CREATE_NEW_ALIAS = 'Umb.EntityAction.ScriptsFolder.Create.New'; +export const SCRIPTS_ENTITY_ACTION_DELETE_FOLDER_ALIAS = 'Umb.EntityAction.ScriptsFolder.DeleteFolder'; +export const SCRIPTS_ENTITY_ACTION_CREATE_FOLDER_NEW_ALIAS = 'Umb.EntityAction.ScriptsFolder.CreateFolder'; + +//WORKSPACE +export const SCRIPTS_WORKSPACE_ALIAS = 'Umb.Workspace.Scripts'; +export const SCRIPTS_WORKSPACE_ACTION_SAVE_ALIAS = 'Umb.WorkspaceAction.Scripts.Save'; + diff --git a/src/packages/templating/scripts/entity-actions/create/create-empty.action.ts b/src/packages/templating/scripts/entity-actions/create/create-empty.action.ts new file mode 100644 index 0000000000..7421a3c713 --- /dev/null +++ b/src/packages/templating/scripts/entity-actions/create/create-empty.action.ts @@ -0,0 +1,12 @@ +import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbCreateScriptAction }> extends UmbEntityActionBase { + constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) { + super(host, repositoryAlias, unique); + } + + async execute() { + history.pushState(null, '', `section/settings/workspace/script/create/${this.unique ?? 'null'}`); + } +} diff --git a/src/packages/templating/scripts/entity-actions/manifests.ts b/src/packages/templating/scripts/entity-actions/manifests.ts new file mode 100644 index 0000000000..ca8a0b6229 --- /dev/null +++ b/src/packages/templating/scripts/entity-actions/manifests.ts @@ -0,0 +1,74 @@ +import { + SCRIPTS_REPOSITORY_ALIAS, + SCRIPTS_ENTITY_TYPE, + SCRIPTS_FOLDER_ENTITY_TYPE, + SCRIPTS_ROOT_ENTITY_TYPE, + SCRIPTS_FOLDER_EMPTY_ENTITY_TYPE, + SCRIPTS_ENTITY_ACTION_DELETE_ALIAS, + SCRIPTS_ENTITY_ACTION_CREATE_NEW_ALIAS, + SCRIPTS_ENTITY_ACTION_DELETE_FOLDER_ALIAS, + SCRIPTS_ENTITY_ACTION_CREATE_FOLDER_NEW_ALIAS, +} from '../config.js'; +import { UmbCreateScriptAction } from './create/create-empty.action.js'; +import { + UmbCreateFolderEntityAction, + UmbDeleteEntityAction, + UmbDeleteFolderEntityAction, +} from '@umbraco-cms/backoffice/entity-action'; +import { ManifestEntityAction } from '@umbraco-cms/backoffice/extension-registry'; + +const scriptsViewActions: Array = [ + { + type: 'entityAction', + alias: SCRIPTS_ENTITY_ACTION_DELETE_ALIAS, + name: 'Delete Scripts Entity Action', + meta: { + icon: 'umb:trash', + label: 'Delete', + api: UmbDeleteEntityAction, + repositoryAlias: SCRIPTS_REPOSITORY_ALIAS, + entityTypes: [SCRIPTS_ENTITY_TYPE], + }, + }, +]; + +const scriptsFolderActions: Array = [ + { + type: 'entityAction', + alias: SCRIPTS_ENTITY_ACTION_CREATE_NEW_ALIAS, + name: 'Create Scripts Entity Under Directory Action', + meta: { + icon: 'umb:article', + label: 'New empty script', + api: UmbCreateScriptAction, + repositoryAlias: SCRIPTS_REPOSITORY_ALIAS, + entityTypes: [SCRIPTS_FOLDER_ENTITY_TYPE, SCRIPTS_FOLDER_EMPTY_ENTITY_TYPE, SCRIPTS_ROOT_ENTITY_TYPE], + }, + }, + { + type: 'entityAction', + alias: SCRIPTS_ENTITY_ACTION_DELETE_FOLDER_ALIAS, + name: 'Remove empty folder', + meta: { + icon: 'umb:trash', + label: 'Remove folder', + api: UmbDeleteFolderEntityAction, + repositoryAlias: SCRIPTS_REPOSITORY_ALIAS, + entityTypes: [SCRIPTS_FOLDER_EMPTY_ENTITY_TYPE], + }, + }, + { + type: 'entityAction', + alias: SCRIPTS_ENTITY_ACTION_CREATE_FOLDER_NEW_ALIAS, + name: 'Create empty folder', + meta: { + icon: 'umb:add', + label: 'Create folder', + api: UmbCreateFolderEntityAction, + repositoryAlias: SCRIPTS_REPOSITORY_ALIAS, + entityTypes: [SCRIPTS_FOLDER_EMPTY_ENTITY_TYPE, SCRIPTS_FOLDER_ENTITY_TYPE, SCRIPTS_ROOT_ENTITY_TYPE], + }, + }, +]; + +export const manifests = [...scriptsViewActions, ...scriptsFolderActions]; diff --git a/src/packages/templating/scripts/index.ts b/src/packages/templating/scripts/index.ts new file mode 100644 index 0000000000..5b27938d26 --- /dev/null +++ b/src/packages/templating/scripts/index.ts @@ -0,0 +1,2 @@ +export * from './repository/index.js'; +export * from './config.js'; diff --git a/src/packages/templating/scripts/manifests.ts b/src/packages/templating/scripts/manifests.ts new file mode 100644 index 0000000000..1f440270ba --- /dev/null +++ b/src/packages/templating/scripts/manifests.ts @@ -0,0 +1,13 @@ +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 entityActionsManifests } from './entity-actions/manifests.js'; +import { manifests as workspaceManifests } from './workspace/manifests.js'; + +export const manifests = [ + ...repositoryManifests, + ...menuItemManifests, + ...treeManifests, + ...entityActionsManifests, + ...workspaceManifests, +]; diff --git a/src/packages/templating/scripts/menu-item/manifests.ts b/src/packages/templating/scripts/menu-item/manifests.ts new file mode 100644 index 0000000000..2b6d95347a --- /dev/null +++ b/src/packages/templating/scripts/menu-item/manifests.ts @@ -0,0 +1,19 @@ +import { SCRIPTS_ENTITY_TYPE, SCRIPTS_MENU_ITEM_ALIAS, SCRIPTS_TREE_ALIAS } from '../config.js'; +import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; + +const menuItem: ManifestTypes = { + type: 'menuItem', + kind: 'tree', + alias: SCRIPTS_MENU_ITEM_ALIAS, + name: 'Scripts Menu Item', + weight: 10, + meta: { + label: 'Scripts', + icon: 'umb:folder', + entityType: SCRIPTS_ENTITY_TYPE, + treeAlias: SCRIPTS_TREE_ALIAS, + menus: ['Umb.Menu.Templating'], + }, +}; + +export const manifests = [menuItem]; diff --git a/src/packages/templating/scripts/repository/index.ts b/src/packages/templating/scripts/repository/index.ts new file mode 100644 index 0000000000..8b6bcfbff6 --- /dev/null +++ b/src/packages/templating/scripts/repository/index.ts @@ -0,0 +1 @@ +export * from './scripts.repository.js'; diff --git a/src/packages/templating/scripts/repository/manifests.ts b/src/packages/templating/scripts/repository/manifests.ts new file mode 100644 index 0000000000..b0e43e0483 --- /dev/null +++ b/src/packages/templating/scripts/repository/manifests.ts @@ -0,0 +1,28 @@ +import { SCRIPTS_REPOSITORY_ALIAS, SCRIPTS_STORE_ALIAS, SCRIPTS_TREE_STORE_ALIAS } from '../config.js'; +import { UmbScriptsRepository } from './scripts.repository.js'; +import { UmbScriptsStore } from './scripts.store.js'; +import { UmbScriptsTreeStore } from './scripts.tree.store.js'; +import { ManifestRepository, ManifestStore, ManifestTreeStore } from '@umbraco-cms/backoffice/extension-registry'; + +const repository: ManifestRepository = { + type: 'repository', + alias: SCRIPTS_REPOSITORY_ALIAS, + name: 'Scripts Repository', + class: UmbScriptsRepository, +}; + +const store: ManifestStore = { + type: 'store', + alias: SCRIPTS_STORE_ALIAS, + name: 'Scripts Store', + class: UmbScriptsStore, +}; + +const treeStore: ManifestTreeStore = { + type: 'treeStore', + alias: SCRIPTS_TREE_STORE_ALIAS, + name: 'Scripts Tree Store', + class: UmbScriptsTreeStore, +}; + +export const manifests = [repository, store, treeStore]; diff --git a/src/packages/templating/scripts/repository/scripts.repository.ts b/src/packages/templating/scripts/repository/scripts.repository.ts new file mode 100644 index 0000000000..b87d31ad9e --- /dev/null +++ b/src/packages/templating/scripts/repository/scripts.repository.ts @@ -0,0 +1,216 @@ +import { SCRIPTS_ROOT_ENTITY_TYPE } from '../config.js'; +import { UmbScriptsTreeServerDataSource } from './sources/scripts.tree.server.data.js'; +import { UmbScriptsServerDataSource } from './sources/scripts.detail.server.data.js'; +import { ScriptsGetFolderResponse, UmbScriptsFolderServerDataSource } from './sources/scripts.folder.server.data.js'; +import { UMB_SCRIPTS_TREE_STORE_CONTEXT_TOKEN, UmbScriptsTreeStore } from './scripts.tree.store.js'; +import { + DataSourceResponse, + UmbDataSourceErrorResponse, + UmbDetailRepository, + UmbFolderRepository, + UmbTreeRepository, +} from '@umbraco-cms/backoffice/repository'; +import { + CreateFolderRequestModel, + CreateScriptRequestModel, + FileItemResponseModelBaseModel, + FileSystemTreeItemPresentationModel, + FolderModelBaseModel, + FolderResponseModel, + ProblemDetails, + ScriptResponseModel, + TextFileResponseModelBaseModel, + UpdateScriptRequestModel, +} from '@umbraco-cms/backoffice/backend-api'; +import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; +import { Observable } from '@umbraco-cms/backoffice/external/rxjs'; + +export class UmbScriptsRepository + implements + UmbTreeRepository, + UmbDetailRepository, + UmbFolderRepository +{ + #init; + #host: UmbControllerHostElement; + + #treeDataSource: UmbScriptsTreeServerDataSource; + #detailDataSource: UmbScriptsServerDataSource; + #folderDataSource: UmbScriptsFolderServerDataSource; + + #treeStore?: UmbScriptsTreeStore; + + constructor(host: UmbControllerHostElement) { + this.#host = host; + + this.#treeDataSource = new UmbScriptsTreeServerDataSource(this.#host); + this.#detailDataSource = new UmbScriptsServerDataSource(this.#host); + this.#folderDataSource = new UmbScriptsFolderServerDataSource(this.#host); + + this.#init = Promise.all([ + new UmbContextConsumerController(this.#host, UMB_SCRIPTS_TREE_STORE_CONTEXT_TOKEN, (instance) => { + this.#treeStore = instance; + }), + ]); + } + + //#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( + requestBody: CreateFolderRequestModel, + ): Promise<{ data?: string | undefined; error?: ProblemDetails | undefined }> { + await this.#init; + const req = { + parentPath: requestBody.parentId, + name: requestBody.name, + }; + const promise = this.#folderDataSource.insert(req); + await promise; + this.requestTreeItemsOf(requestBody.parentId ? requestBody.parentId : null); + return promise; + } + async requestFolder( + unique: string, + ): Promise<{ data?: ScriptsGetFolderResponse | 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 TREE + + async requestTreeRoot() { + await this.#init; + + const data = { + id: null, + path: null, + type: SCRIPTS_ROOT_ENTITY_TYPE, + name: 'Scripts', + icon: 'umb:folder', + hasChildren: true, + }; + return { data }; + } + + async requestRootTreeItems() { + await this.#init; + + const { data, error } = await this.#treeDataSource.getRootItems(); + + if (data) { + this.#treeStore?.appendItems(data.items); + } + + return { data, error, asObservable: () => this.#treeStore!.rootItems }; + } + + async requestTreeItemsOf(path: string | null) { + if (path === null || path === '/' || path === '') { + return this.requestRootTreeItems(); + } + + await this.#init; + const response = await this.#treeDataSource.getChildrenOf({ path, skip: 0, take: 100 }); + const { data, error } = response; + if (data) { + this.#treeStore!.appendItems(data.items); + } + + return { data, error, asObservable: () => this.#treeStore!.childrenOf(path) }; + } + + async requestTreeItems(keys: Array) { + await this.#init; + + if (!keys) { + const error: ProblemDetails = { title: 'Keys are missing' }; + return { data: undefined, error }; + } + + const { data, error } = await this.#treeDataSource.getItem(keys); + + return { data, error, asObservable: () => this.#treeStore!.items(keys) }; + } + + async rootTreeItems() { + await this.#init; + return this.#treeStore!.rootItems; + } + + async treeItemsOf(parentPath: string | null) { + if (!parentPath) throw new Error('Parent Path is missing'); + await this.#init; + return this.#treeStore!.childrenOf(parentPath); + } + + async treeItems(paths: Array) { + if (!paths) throw new Error('Paths are missing'); + await this.#init; + return this.#treeStore!.items(paths); + } + //#endregion + + //#region DETAILS + async requestByKey(path: string) { + if (!path) throw new Error('Path is missing'); + await this.#init; + const { data, error } = await this.#detailDataSource.get(path); + return { data, error }; + } + + requestById(id: string): Promise> { + throw new Error('Method not implemented.'); + } + byId(id: string): Promise> { + throw new Error('Method not implemented.'); + } + + createScaffold(parentId: string | null, preset: string): Promise> { + return this.#detailDataSource.createScaffold(parentId, preset); + } + async create(data: CreateScriptRequestModel): Promise> { + const promise = this.#detailDataSource.insert(data); + await promise; + this.requestTreeItemsOf(data.parentPath ? data.parentPath : null); + return promise; + } + save(id: string, requestBody: UpdateScriptRequestModel): Promise { + return this.#detailDataSource.update(id, requestBody); + } + async delete(id: string): Promise { + const promise = this.#detailDataSource.delete(id); + const parentPath = id.substring(0, id.lastIndexOf('/')); + this.requestTreeItemsOf(parentPath ? parentPath : null); + return promise; + } + + requestItems(keys: Array): Promise> { + return this.#detailDataSource.getItems(keys); + } + + //#endregion +} diff --git a/src/packages/templating/scripts/repository/scripts.store.ts b/src/packages/templating/scripts/repository/scripts.store.ts new file mode 100644 index 0000000000..43285510f5 --- /dev/null +++ b/src/packages/templating/scripts/repository/scripts.store.ts @@ -0,0 +1,45 @@ +import { UMB_SCRIPTS_STORE_CONTEXT_TOKEN_ALIAS } from '../config.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbStoreBase } from '@umbraco-cms/backoffice/store'; +import type { TemplateResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; + +/** + * @export + * @class UmbScriptsStore + * @extends {UmbStoreBase} + * @description - Data Store for scripts + */ +export class UmbScriptsStore extends UmbStoreBase { + /** + * Creates an instance of UmbScriptsStore. + * @param {UmbControllerHostInterface} host + * @memberof UmbScriptsStore + */ + constructor(host: UmbControllerHostElement) { + super(host, UMB_SCRIPTS_STORE_CONTEXT_TOKEN.toString(), new UmbArrayState([], (x) => x.id)); + } + + /** + * Append a script to the store + * @param {Template} template + * @memberof UmbScriptsStore + */ + append(template: TemplateResponseModel) { + this._data.append([template]); + } + + /** + * Removes scripts in the store with the given uniques + * @param {string[]} uniques + * @memberof UmbScriptsStore + */ + remove(uniques: string[]) { + this._data.remove(uniques); + } +} + +export const UMB_SCRIPTS_STORE_CONTEXT_TOKEN = new UmbContextToken( + UMB_SCRIPTS_STORE_CONTEXT_TOKEN_ALIAS, +); diff --git a/src/packages/templating/scripts/repository/scripts.tree.store.ts b/src/packages/templating/scripts/repository/scripts.tree.store.ts new file mode 100644 index 0000000000..3dd1fe2461 --- /dev/null +++ b/src/packages/templating/scripts/repository/scripts.tree.store.ts @@ -0,0 +1,26 @@ +import { UMB_SCRIPTS_TREE_STORE_CONTEXT_TOKEN_ALIAS } from '../config.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; +import { UmbFileSystemTreeStore } from '@umbraco-cms/backoffice/store'; +import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; + +export const UMB_SCRIPTS_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken( + UMB_SCRIPTS_TREE_STORE_CONTEXT_TOKEN_ALIAS, +); + +/** + * Tree Store for scripts + * + * @export + * @class + * @extends {UmbEntityTreeStore} + */ +export class UmbScriptsTreeStore extends UmbFileSystemTreeStore { + /** + * Creates an instance of UmbScriptsTreeStore. + * @param {UmbControllerHostInterface} host + * @memberof UmbScriptsTreeStore + */ + constructor(host: UmbControllerHostElement) { + super(host, UMB_SCRIPTS_TREE_STORE_CONTEXT_TOKEN.toString()); + } +} diff --git a/src/packages/templating/scripts/repository/sources/index.ts b/src/packages/templating/scripts/repository/sources/index.ts new file mode 100644 index 0000000000..0e00d1bfdd --- /dev/null +++ b/src/packages/templating/scripts/repository/sources/index.ts @@ -0,0 +1,19 @@ +import { + FileSystemTreeItemPresentationModel, + PagedFileSystemTreeItemPresentationModel, +} from '@umbraco-cms/backoffice/backend-api'; +import type { DataSourceResponse } from '@umbraco-cms/backoffice/repository'; + +export interface ScriptsTreeDataSource { + getRootItems(): Promise>; + getChildrenOf({ + path, + skip, + take, + }: { + path?: string | undefined; + skip?: number | undefined; + take?: number | undefined; + }): Promise>; + getItem(ids: Array): Promise>; +} diff --git a/src/packages/templating/scripts/repository/sources/scripts.detail.server.data.ts b/src/packages/templating/scripts/repository/sources/scripts.detail.server.data.ts new file mode 100644 index 0000000000..a7f43557b0 --- /dev/null +++ b/src/packages/templating/scripts/repository/sources/scripts.detail.server.data.ts @@ -0,0 +1,75 @@ +import { + CreateScriptRequestModel, + CreateTextFileViewModelBaseModel, + ScriptItemResponseModel, + ScriptResource, + ScriptResponseModel, + UpdateScriptRequestModel, +} from '@umbraco-cms/backoffice/backend-api'; +import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; +import { DataSourceResponse, UmbDataSource } from '@umbraco-cms/backoffice/repository'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; + +export class UmbScriptsServerDataSource + implements UmbDataSource +{ + #host: UmbControllerHostElement; + + constructor(host: UmbControllerHostElement) { + this.#host = host; + } + createScaffold( + parentId: string | null, + preset?: string | Partial | undefined, + ): Promise> { + throw new Error('Method not implemented.'); + } + + /** + * Fetches a script with the given path from the server + * @param {string} path + * @return {*} + * @memberof UmbScriptsDetailServerDataSource + */ + get(path: string): Promise> { + if (!path) throw new Error('Path is missing'); + return tryExecuteAndNotify(this.#host, ScriptResource.getScript({ path })); + } + /** + * Creates a new script + * + * @param {CreateScriptRequestModel} requestBody + * @return {*} {Promise>} + * @memberof UmbScriptsDetailServerDataSource + */ + insert(requestBody: CreateScriptRequestModel): Promise> { + return tryExecuteAndNotify(this.#host, ScriptResource.postScript({ requestBody })); + } + + //TODO the parameters here are bit ugly, since unique is already in the request body parameter, but it has to be done to marry the UmbDataSource interface an backend API together... maybe come up with some nicer solution + /** + * Updates a script + * + * @param {string} [unique=''] + * @param {UpdateScriptRequestModel} requestBody + * @return {*} {Promise>} + * @memberof UmbScriptsDetailServerDataSource + */ + update(unique = '', requestBody: UpdateScriptRequestModel): Promise> { + return tryExecuteAndNotify(this.#host, ScriptResource.putScript({ requestBody })); + } + /** + * Deletes a script + * + * @param {string} path + * @return {*} {Promise} + * @memberof UmbScriptsDetailServerDataSource + */ + delete(path: string): Promise { + return tryExecuteAndNotify(this.#host, ScriptResource.deleteScript({ path })); + } + + getItems(keys: Array): Promise> { + return tryExecuteAndNotify(this.#host, ScriptResource.getScriptItem({ path: keys })); + } +} diff --git a/src/packages/templating/scripts/repository/sources/scripts.folder.server.data.ts b/src/packages/templating/scripts/repository/sources/scripts.folder.server.data.ts new file mode 100644 index 0000000000..a4a21add6a --- /dev/null +++ b/src/packages/templating/scripts/repository/sources/scripts.folder.server.data.ts @@ -0,0 +1,35 @@ +import { + CreateFolderRequestModel, + FolderModelBaseModel, + FolderResponseModel, + ScriptResource, +} 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 ScriptsGetFolderResponse = { path: string; parentPath: string; name: string }; + +export class UmbScriptsFolderServerDataSource implements UmbFolderDataSource { + #host: UmbControllerHostElement; + + constructor(host: UmbControllerHostElement) { + this.#host = host; + } + createScaffold(parentId: string | null): Promise> { + throw new Error('Method not implemented.'); + } + get(unique: string): Promise> { + return tryExecuteAndNotify(this.#host, ScriptResource.getScriptFolder({ path: unique })); + } + insert(requestBody: CreateFolderRequestModel): Promise> { + return tryExecuteAndNotify(this.#host, ScriptResource.postScriptFolder({ requestBody })); + } + update(unique: string, data: CreateFolderRequestModel): Promise> { + throw new Error('Method not implemented.'); + } + delete(path: string): Promise> { + return tryExecuteAndNotify(this.#host, ScriptResource.deleteScriptFolder({ path })); + } +} diff --git a/src/packages/templating/scripts/repository/sources/scripts.tree.server.data.ts b/src/packages/templating/scripts/repository/sources/scripts.tree.server.data.ts new file mode 100644 index 0000000000..0014bec5f2 --- /dev/null +++ b/src/packages/templating/scripts/repository/sources/scripts.tree.server.data.ts @@ -0,0 +1,54 @@ +import { ScriptsTreeDataSource } from './index.js'; +import { ScriptResource, ProblemDetails } from '@umbraco-cms/backoffice/backend-api'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; + +export class UmbScriptsTreeServerDataSource implements ScriptsTreeDataSource { + #host: UmbControllerHostElement; + + constructor(host: UmbControllerHostElement) { + this.#host = host; + } + + async getRootItems() { + return tryExecuteAndNotify(this.#host, ScriptResource.getTreeScriptRoot({})); + } + + async getChildrenOf({ + path, + skip, + take, + }: { + path?: string | undefined; + skip?: number | undefined; + take?: number | undefined; + }) { + if (!path) { + const error: ProblemDetails = { title: 'Path is missing' }; + return error; + } + + return tryExecuteAndNotify( + this.#host, + ScriptResource.getTreeScriptChildren({ + path, + skip, + take, + }), + ); + } + + async getItem(path: Array) { + if (!path) { + const error: ProblemDetails = { title: 'Paths are missing' }; + return error; + } + + return tryExecuteAndNotify( + this.#host, + ScriptResource.getScriptItem({ + path, + }), + ); + } +} diff --git a/src/packages/templating/scripts/tree/manifests.ts b/src/packages/templating/scripts/tree/manifests.ts new file mode 100644 index 0000000000..dbb150e860 --- /dev/null +++ b/src/packages/templating/scripts/tree/manifests.ts @@ -0,0 +1,30 @@ +import { + SCRIPTS_ENTITY_TYPE, + SCRIPTS_REPOSITORY_ALIAS, + SCRIPTS_ROOT_ENTITY_TYPE, + SCRIPTS_TREE_ALIAS, + SCRIPTS_TREE_ITEM_ALIAS, +} from '../config.js'; +import type { ManifestTree, ManifestTreeItem } from '@umbraco-cms/backoffice/extension-registry'; + +const tree: ManifestTree = { + type: 'tree', + alias: SCRIPTS_TREE_ALIAS, + name: 'Scripts Tree', + weight: 30, + meta: { + repositoryAlias: SCRIPTS_REPOSITORY_ALIAS, + }, +}; + +const treeItem: ManifestTreeItem = { + type: 'treeItem', + kind: 'fileSystem', + alias: SCRIPTS_TREE_ITEM_ALIAS, + name: 'Scripts Tree Item', + meta: { + entityTypes: [SCRIPTS_ROOT_ENTITY_TYPE, SCRIPTS_ENTITY_TYPE], + }, +}; + +export const manifests = [tree, treeItem]; diff --git a/src/packages/templating/scripts/workspace/manifests.ts b/src/packages/templating/scripts/workspace/manifests.ts new file mode 100644 index 0000000000..580cf9eef4 --- /dev/null +++ b/src/packages/templating/scripts/workspace/manifests.ts @@ -0,0 +1,36 @@ +import { SCRIPTS_ENTITY_TYPE, SCRIPTS_WORKSPACE_ACTION_SAVE_ALIAS, SCRIPTS_WORKSPACE_ALIAS } from '../config.js'; +import { UmbSaveWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; +import type { ManifestWorkspace, ManifestWorkspaceAction } from '@umbraco-cms/backoffice/extension-registry'; + +const workspace: ManifestWorkspace = { + type: 'workspace', + alias: SCRIPTS_WORKSPACE_ALIAS, + name: 'Scripts Workspace', + loader: () => import('./scripts-workspace.element.js'), + meta: { + entityType: SCRIPTS_ENTITY_TYPE, + }, +}; + +//TODO: this does not work for some reason +const workspaceActions: Array = [ + { + type: 'workspaceAction', + alias: 'Umb.WorkspaceAction.Scripts.Save', + name: 'Save Script Workspace Action', + meta: { + label: 'Save', + look: 'primary', + color: 'positive', + api: UmbSaveWorkspaceAction, + }, + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: workspace.alias, + }, + ], + }, +]; + +export const manifests = [workspace, ...workspaceActions]; diff --git a/src/packages/templating/scripts/workspace/scripts-workspace-edit.element.ts b/src/packages/templating/scripts/workspace/scripts-workspace-edit.element.ts new file mode 100644 index 0000000000..3db87bf14a --- /dev/null +++ b/src/packages/templating/scripts/workspace/scripts-workspace-edit.element.ts @@ -0,0 +1,185 @@ +import { UmbScriptsWorkspaceContext } from './scripts-workspace.context.js'; +import type { UmbCodeEditorElement } from '@umbraco-cms/backoffice/code-editor'; +import { UUITextStyles, UUIInputElement } from '@umbraco-cms/backoffice/external/uui'; +import { css, html, customElement, query, state, PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import { UMB_MODAL_MANAGER_CONTEXT_TOKEN, UmbModalManagerContext } from '@umbraco-cms/backoffice/modal'; +import { Subject, debounceTime } from '@umbraco-cms/backoffice/external/rxjs'; +import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; +import _ from 'lodash'; + +@customElement('umb-scripts-workspace-edit') +export class UmbScriptsWorkspaceEditElement extends UmbLitElement { + #name: string | undefined = ''; + @state() + private get _name() { + return this.#name; + } + + private set _name(value) { + this.#name = value?.replace('.js', ''); + this.requestUpdate(); + } + + @state() + private _content?: string | null = ''; + + @state() + private _path?: string | null = ''; + + @state() + private _dirName?: string | null = ''; + + @state() + private _ready?: boolean = false; + + @query('umb-code-editor') + private _codeEditor?: UmbCodeEditorElement; + + #scriptsWorkspaceContext?: UmbScriptsWorkspaceContext; + private _modalContext?: UmbModalManagerContext; + + #isNew = false; + + private inputQuery$ = new Subject(); + + constructor() { + super(); + + this.consumeContext(UMB_MODAL_MANAGER_CONTEXT_TOKEN, (instance) => { + this._modalContext = instance; + }); + + this.consumeContext(UMB_WORKSPACE_CONTEXT, (workspaceContext) => { + this.#scriptsWorkspaceContext = workspaceContext as UmbScriptsWorkspaceContext; + this.observe(this.#scriptsWorkspaceContext.name, (name) => { + this._name = name; + }); + + this.observe(this.#scriptsWorkspaceContext.content, (content) => { + this._content = content; + }); + + this.observe(this.#scriptsWorkspaceContext.path, (path) => { + this._path = path; + }); + + this.observe(this.#scriptsWorkspaceContext.isNew, (isNew) => { + this.#isNew = !!isNew; + }); + + this.observe(this.#scriptsWorkspaceContext.isCodeEditorReady, (isReady) => { + this._ready = isReady; + }); + + this.inputQuery$.pipe(debounceTime(250)).subscribe((nameInputValue: string) => { + this.#scriptsWorkspaceContext?.setName(`${nameInputValue}.js`); + }); + }); + } + + protected willUpdate(_changedProperties: PropertyValueMap | Map): void { + if (_changedProperties.has('_path')) { + this._dirName = this._path?.substring(0, this._path?.lastIndexOf('/')); + } + } + + #onNameInput(event: Event) { + const target = event.target as UUIInputElement; + const value = target.value as string; + this.inputQuery$.next(value); + } + + #onCodeEditorInput(event: Event) { + const target = event.target as UmbCodeEditorElement; + const value = target.code as string; + this.#scriptsWorkspaceContext?.setContent(value); + } + + #renderCodeEditor() { + return html``; + } + + render() { + return html` +
+ + Scripts/${this._dirName}${this._name}.js +
+ + +
+ ${this._ready + ? this.#renderCodeEditor() + : html`
+ +
`} +
+
+ + + Keyboard Shortcuts + + ALT + + + shift + + + k + + +
+
`; + } + + static styles = [ + UUITextStyles, + css` + :host { + display: block; + width: 100%; + } + + #loader-container { + display: grid; + place-items: center; + min-height: calc(100dvh - 260px); + } + + umb-code-editor { + --editor-height: calc(100dvh - 260px); + } + + uui-box { + min-height: calc(100dvh - 260px); + 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%; + } + + uui-input { + width: 100%; + } + `, + ]; +} + +export default UmbScriptsWorkspaceEditElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-scripts-workspace-edit': UmbScriptsWorkspaceEditElement; + } +} diff --git a/src/packages/templating/scripts/workspace/scripts-workspace.context.ts b/src/packages/templating/scripts/workspace/scripts-workspace.context.ts new file mode 100644 index 0000000000..13fcc1d344 --- /dev/null +++ b/src/packages/templating/scripts/workspace/scripts-workspace.context.ts @@ -0,0 +1,100 @@ +import { ScriptDetails, SCRIPTS_WORKSPACE_ALIAS } from '../config.js'; +import { UmbScriptsRepository } from '../repository/scripts.repository.js'; +import { createObservablePart, UmbBooleanState, UmbDeepState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; +import { UmbWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; +import { loadCodeEditor } from '@umbraco-cms/backoffice/code-editor'; +import { TextFileResponseModelBaseModel, UpdateScriptRequestModel } from '@umbraco-cms/backoffice/backend-api'; + +export class UmbScriptsWorkspaceContext extends UmbWorkspaceContext { + #data = new UmbDeepState(undefined); + data = this.#data.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, SCRIPTS_WORKSPACE_ALIAS, new UmbScriptsRepository(host)); + this.#loadCodeEditor(); + } + + async #loadCodeEditor() { + try { + await loadCodeEditor(); + this.#isCodeEditorReady.next(true); + } catch (error) { + console.error(error); + } + } + + getData() { + return this.#data.getValue(); + } + + setName(value: string) { + this.#data.next({ ...this.#data.value, name: value }); + } + + setContent(value: string) { + this.#data.next({ ...this.#data.value, content: value }); + } + + async load(entityKey: string) { + const { data } = await this.repository.requestByKey(entityKey); + if (data) { + this.setIsNew(false); + this.#data.next(data); + } + } + + async create(parentKey: string) { + const newScript: TextFileResponseModelBaseModel = { + name: '', + path: parentKey, + content: '', + }; + this.#data.next(newScript); + this.setIsNew(true); + } + + getEntityId(): string | undefined { + return this.getData()?.path; + } + + public async save() { + const script = this.getData(); + + if (!script) { + return Promise.reject('Something went wrong, there is no data for script you want to save...'); + } + if (this.getIsNew()) { + const createRequestBody = { + name: script.name, + content: script.content, + parentPath: script.path + '/', + }; + + this.repository.create(createRequestBody); + return Promise.resolve(); + } + if (!script.path) return Promise.reject('There is no path'); + const updateRequestBody: UpdateScriptRequestModel = { + name: script.name, + existingPath: script.path, + content: script.content, + }; + this.repository.save(script.path, updateRequestBody); + return Promise.resolve(); + } + + destroy(): void { + throw new Error('Method not implemented.'); + } + + getEntityType(): string { + throw new Error('Method not implemented.'); + } +} diff --git a/src/packages/templating/scripts/workspace/scripts-workspace.element.ts b/src/packages/templating/scripts/workspace/scripts-workspace.element.ts new file mode 100644 index 0000000000..edf56fb2f1 --- /dev/null +++ b/src/packages/templating/scripts/workspace/scripts-workspace.element.ts @@ -0,0 +1,52 @@ +import { UmbScriptsWorkspaceContext } from './scripts-workspace.context.js'; +import { UUITextStyles } from '@umbraco-cms/backoffice/external/uui'; +import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import { UmbRoute, IRoutingInfo, PageComponent } from '@umbraco-cms/backoffice/router'; +import { UmbWorkspaceIsNewRedirectController } from '@umbraco-cms/backoffice/workspace'; + +@customElement('umb-scripts-workspace') +export class UmbScriptsWorkspaceElement extends UmbLitElement { + #scriptsWorkspaceContext = new UmbScriptsWorkspaceContext(this); + @state() + _routes: UmbRoute[] = [ + { + path: 'create/:parentKey', + component: import('./scripts-workspace-edit.element.js'), + setup: async (component: PageComponent, info: IRoutingInfo) => { + const parentKey = info.match.params.parentKey; + const decodePath = decodeURIComponent(parentKey); + this.#scriptsWorkspaceContext.create(decodePath === 'null' ? '' : decodePath); + + new UmbWorkspaceIsNewRedirectController( + this, + this.#scriptsWorkspaceContext, + this.shadowRoot!.querySelector('umb-router-slot')!, + ); + }, + }, + { + path: 'edit/:key', + component: import('./scripts-workspace-edit.element.js'), + setup: (component: PageComponent, info: IRoutingInfo) => { + const key = info.match.params.key; + const decodePath = decodeURIComponent(key).replace('-js', '.js'); + this.#scriptsWorkspaceContext.load(decodePath); + }, + }, + ]; + + render() { + return html``; + } + + static styles = [UUITextStyles, css``]; +} + +export default UmbScriptsWorkspaceElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-scripts-workspace': UmbScriptsWorkspaceElement; + } +}