add scripts files

This commit is contained in:
Julia Gru
2023-09-04 11:40:44 +02:00
committed by Jacob Overgaard
parent a0a09a33e1
commit b8a4cf2062
20 changed files with 1023 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
import { ScriptResponseModel } from '@umbraco-cms/backoffice/backend-api';
export type ScriptDetails = ScriptResponseModel;
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_REPOSITORY_ALIAS = 'Umb.Repository.Scripts';
export const SCRIPTS_TREE_ALIAS = 'Umb.Tree.Scripts';
export const UMB_SCRIPTS_TREE_STORE_CONTEXT_TOKEN_ALIAS = 'Umb.Store.Scripts.Tree';
export const UMB_SCRIPTS_STORE_CONTEXT_TOKEN_ALIAS = 'Umb.Store.Scripts';

View File

@@ -0,0 +1,12 @@
import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
export class UmbCreateScriptAction<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/script/create/${this.unique ?? 'null'}`);
}
}

View File

@@ -0,0 +1,70 @@
import {
SCRIPTS_REPOSITORY_ALIAS,
SCRIPTS_ENTITY_TYPE,
SCRIPTS_FOLDER_ENTITY_TYPE,
SCRIPTS_ROOT_ENTITY_TYPE,
SCRIPTS_FOLDER_EMPTY_ENTITY_TYPE,
} 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<ManifestEntityAction> = [
{
type: 'entityAction',
alias: 'Umb.EntityAction.PartialView.Delete',
name: 'Delete PartialView Entity Action',
meta: {
icon: 'umb:trash',
label: 'Delete',
api: UmbDeleteEntityAction,
repositoryAlias: SCRIPTS_REPOSITORY_ALIAS,
entityTypes: [SCRIPTS_ENTITY_TYPE],
},
},
];
const scriptsFolderActions: Array<ManifestEntityAction> = [
{
type: 'entityAction',
alias: 'Umb.EntityAction.PartialViewFolder.Create.New',
name: 'Create PartialView Entity Under Directory Action',
meta: {
icon: 'umb:article',
label: 'New empty partial view',
api: UmbCreateScriptAction,
repositoryAlias: SCRIPTS_REPOSITORY_ALIAS,
entityTypes: [SCRIPTS_FOLDER_ENTITY_TYPE, SCRIPTS_ROOT_ENTITY_TYPE],
},
},
{
type: 'entityAction',
alias: 'Umb.EntityAction.PartialViewFolder.DeleteFolder',
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: 'Umb.EntityAction.PartialViewFolder.CreateFolder',
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];

View File

@@ -0,0 +1 @@
export * from './repository/index.js';

View File

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

View File

@@ -0,0 +1,19 @@
import { SCRIPTS_ENTITY_TYPE, SCRIPTS_TREE_ALIAS } from '../config.js';
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
const menuItem: ManifestTypes = {
type: 'menuItem',
kind: 'tree',
alias: 'Umb.MenuItem.Scripts',
name: 'Scripts Menu Item',
weight: 40,
meta: {
label: 'Scripts',
icon: 'umb:folder',
entityType: SCRIPTS_ENTITY_TYPE,
treeAlias: SCRIPTS_TREE_ALIAS,
menus: ['Umb.Menu.Templating'],
},
};
export const manifests = [menuItem];

View File

@@ -0,0 +1 @@
export * from './scripts.repository.js';

View File

@@ -0,0 +1,28 @@
import { ManifestRepository, ManifestStore, ManifestTreeStore } from '@umbraco-cms/backoffice/extension-registry';
import { SCRIPTS_REPOSITORY_ALIAS } from '../config.js';
const repository: ManifestRepository = {
type: 'repository',
alias: SCRIPTS_REPOSITORY_ALIAS,
name: 'Partial Views Repository',
class: UmbScriptsRepository,
};
export const SCRIPTS_STORE_ALIAS = 'Umb.Store.PartialViews';
export const SCRIPTS_TREE_STORE_ALIAS = 'Umb.Store.PartialViewsTree';
const store: ManifestStore = {
type: 'store',
alias: SCRIPTS_STORE_ALIAS,
name: 'Partial Views Store',
class: UmbScriptsStore,
};
const treeStore: ManifestTreeStore = {
type: 'treeStore',
alias: SCRIPTS_TREE_STORE_ALIAS,
name: 'Partial Views Tree Store',
class: UmbScriptsTreeStore,
};
export const manifests = [repository, store, treeStore];

View File

@@ -0,0 +1,218 @@
import { PARTIAL_VIEW_ROOT_ENTITY_TYPE } from '../../partial-views/config.js';
import { UMB_PARTIAL_VIEW_TREE_STORE_CONTEXT_TOKEN } from '../../partial-views/repository/partial-views.tree.store.js';
import { PartialViewGetFolderResponse } from '../../partial-views/repository/sources/partial-views.folder.server.data.js';
import { UmbScriptsTreeServerDataSource } from './sources/scripts.tree.server.data.js';
import { UmbScriptsServerDataSource } from './sources/scripts.detail.server.data.js';
import { UmbScriptsFolderServerDataSource } from './sources/scripts.folder.server.data.js';
import { 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<FileSystemTreeItemPresentationModel>,
UmbDetailRepository<CreateScriptRequestModel, string, UpdateScriptRequestModel, ScriptResponseModel, string>,
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_PARTIAL_VIEW_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?: PartialViewGetFolderResponse | 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: PARTIAL_VIEW_ROOT_ENTITY_TYPE,
name: 'Partial Views',
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) {
return this.requestRootTreeItems();
}
await this.#init;
const { data, error } = await this.#treeDataSource.getChildrenOf({ path, skip: 0, take: 100 });
if (data) {
this.#treeStore!.appendItems(data.items);
}
return { data, error, asObservable: () => this.#treeStore!.childrenOf(path) };
}
async requestTreeItems(keys: Array<string>) {
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<string>) {
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<DataSourceResponse<any>> {
throw new Error('Method not implemented.');
}
byId(id: string): Promise<Observable<any>> {
throw new Error('Method not implemented.');
}
createScaffold(parentId: string | null, preset: string): Promise<DataSourceResponse<TextFileResponseModelBaseModel>> {
return this.#detailDataSource.createScaffold(parentId, preset);
}
async create(data: CreateScriptRequestModel): Promise<DataSourceResponse<any>> {
const promise = this.#detailDataSource.insert(data);
await promise;
this.requestTreeItemsOf(data.parentPath ? data.parentPath : null);
return promise;
}
save(id: string, requestBody: UpdateScriptRequestModel): Promise<UmbDataSourceErrorResponse> {
return this.#detailDataSource.update(id, requestBody);
}
async delete(id: string): Promise<UmbDataSourceErrorResponse> {
const promise = this.#detailDataSource.delete(id);
const parentPath = id.substring(0, id.lastIndexOf('/'));
this.requestTreeItemsOf(parentPath ? parentPath : null);
return promise;
}
requestItems(keys: Array<string>): Promise<DataSourceResponse<FileItemResponseModelBaseModel[]>> {
return this.#detailDataSource.getItems(keys);
}
//#endregion
}

View File

@@ -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 UmbPartialViewsStore
* @extends {UmbStoreBase}
* @description - Data Store for partial views
*/
export class UmbScriptsStore extends UmbStoreBase {
/**
* Creates an instance of UmbPartialViewsStore.
* @param {UmbControllerHostInterface} host
* @memberof UmbPartialViewsStore
*/
constructor(host: UmbControllerHostElement) {
super(host, UMB_SCRIPTS_STORE_CONTEXT_TOKEN.toString(), new UmbArrayState<TemplateResponseModel>([], (x) => x.id));
}
/**
* Append a partial view to the store
* @param {Template} template
* @memberof UmbPartialViewsStore
*/
append(template: TemplateResponseModel) {
this._data.append([template]);
}
/**
* Removes partial views in the store with the given uniques
* @param {string[]} uniques
* @memberof UmbPartialViewsStore
*/
remove(uniques: string[]) {
this._data.remove(uniques);
}
}
export const UMB_SCRIPTS_STORE_CONTEXT_TOKEN = new UmbContextToken<UmbScriptsStore>(
UMB_SCRIPTS_STORE_CONTEXT_TOKEN_ALIAS,
);

View File

@@ -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<UmbPartialViewsTreeStore>(
UMB_SCRIPTS_TREE_STORE_CONTEXT_TOKEN_ALIAS,
);
/**
* Tree Store for partial views
*
* @export
* @class UmbPartialViewsTreeStore
* @extends {UmbEntityTreeStore}
*/
export class UmbPartialViewsTreeStore extends UmbFileSystemTreeStore {
/**
* Creates an instance of UmbPartialViewsTreeStore.
* @param {UmbControllerHostInterface} host
* @memberof UmbPartialViewsTreeStore
*/
constructor(host: UmbControllerHostElement) {
super(host, UMB_SCRIPTS_TREE_STORE_CONTEXT_TOKEN.toString());
}
}

View File

@@ -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<DataSourceResponse<PagedFileSystemTreeItemPresentationModel>>;
getChildrenOf({
path,
skip,
take,
}: {
path?: string | undefined;
skip?: number | undefined;
take?: number | undefined;
}): Promise<DataSourceResponse<PagedFileSystemTreeItemPresentationModel>>;
getItem(ids: Array<string>): Promise<DataSourceResponse<FileSystemTreeItemPresentationModel[]>>;
}

View File

@@ -0,0 +1,77 @@
import {
CreatePartialViewRequestModel,
CreateScriptRequestModel,
CreateTextFileViewModelBaseModel,
PartialViewItemResponseModel,
ScriptResource,
ScriptResponseModel,
UpdatePartialViewRequestModel,
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<CreateScriptRequestModel, string, UpdateScriptRequestModel, ScriptResponseModel, string>
{
#host: UmbControllerHostElement;
constructor(host: UmbControllerHostElement) {
this.#host = host;
}
createScaffold(
parentId: string | null,
preset?: string | Partial<CreateTextFileViewModelBaseModel> | undefined,
): Promise<DataSourceResponse<CreateTextFileViewModelBaseModel>> {
throw new Error('Method not implemented.');
}
/**
* Fetches a partial view with the given path from the server
* @param {string} path
* @return {*}
* @memberof UmbStylesheetServerDataSource
*/
get(path: string): Promise<DataSourceResponse<ScriptResponseModel>> {
if (!path) throw new Error('Path is missing');
return tryExecuteAndNotify(this.#host, ScriptResource.getScript({ path }));
}
/**
* Creates a new partial view
*
* @param {CreatePartialViewRequestModel} requestBody
* @return {*} {Promise<DataSourceResponse<string>>}
* @memberof UmbPartialViewDetailServerDataSource
*/
insert(requestBody: CreatePartialViewRequestModel): Promise<DataSourceResponse<string>> {
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 partial view
*
* @param {string} [unique='']
* @param {UpdatePartialViewRequestModel} requestBody
* @return {*} {Promise<DataSourceResponse<any>>}
* @memberof UmbPartialViewDetailServerDataSource
*/
update(unique = '', requestBody: UpdatePartialViewRequestModel): Promise<DataSourceResponse<any>> {
return tryExecuteAndNotify(this.#host, ScriptResource.putScript({ requestBody }));
}
/**
* Deletes a partial view
*
* @param {string} path
* @return {*} {Promise<DataSourceResponse>}
* @memberof UmbPartialViewDetailServerDataSource
*/
delete(path: string): Promise<DataSourceResponse> {
return tryExecuteAndNotify(this.#host, ScriptResource.deleteScript({ path }));
}
getItems(keys: Array<string>): Promise<DataSourceResponse<PartialViewItemResponseModel[]>> {
return tryExecuteAndNotify(this.#host, ScriptResource.getScriptItem({ path: keys }));
}
}

View File

@@ -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<DataSourceResponse<FolderResponseModel>> {
throw new Error('Method not implemented.');
}
get(unique: string): Promise<DataSourceResponse<ScriptsGetFolderResponse>> {
return tryExecuteAndNotify(this.#host, ScriptResource.getScriptFolder({ path: unique }));
}
insert(requestBody: CreateFolderRequestModel): Promise<DataSourceResponse<string>> {
return tryExecuteAndNotify(this.#host, ScriptResource.postScriptFolder({ requestBody }));
}
update(unique: string, data: CreateFolderRequestModel): Promise<DataSourceResponse<FolderModelBaseModel>> {
throw new Error('Method not implemented.');
}
delete(path: string): Promise<DataSourceResponse<unknown>> {
return tryExecuteAndNotify(this.#host, ScriptResource.deleteScriptFolder({ path }));
}
}

View File

@@ -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<string>) {
if (!path) {
const error: ProblemDetails = { title: 'Paths are missing' };
return error;
}
return tryExecuteAndNotify(
this.#host,
ScriptResource.getScriptItem({
path,
}),
);
}
}

View File

@@ -0,0 +1,29 @@
import {
SCRIPTS_ENTITY_TYPE,
SCRIPTS_REPOSITORY_ALIAS,
SCRIPTS_ROOT_ENTITY_TYPE,
SCRIPTS_TREE_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: 'Umb.TreeItem.Scripts',
name: 'Scripts Tree Item',
meta: {
entityTypes: [SCRIPTS_ROOT_ENTITY_TYPE, SCRIPTS_ENTITY_TYPE],
},
};
export const manifests = [tree, treeItem];

View File

@@ -0,0 +1,35 @@
import { UmbSaveWorkspaceAction } from '@umbraco-cms/backoffice/workspace';
import type { ManifestWorkspace, ManifestWorkspaceAction } from '@umbraco-cms/backoffice/extension-registry';
const workspace: ManifestWorkspace = {
type: 'workspace',
alias: 'Umb.Workspace.Scripts',
name: 'Partial View Workspace',
loader: () => import('./scripts-workspace.element.js'),
meta: {
entityType: 'partial-view',
},
};
const workspaceActions: Array<ManifestWorkspaceAction> = [
{
type: 'workspaceAction',
alias: 'Umb.WorkspaceAction.Scripts.Save',
name: 'Save Partial View',
weight: 70,
meta: {
look: 'primary',
color: 'positive',
label: 'Save',
api: UmbSaveWorkspaceAction,
},
conditions: [
{
alias: 'Umb.Condition.WorkspaceAlias',
match: workspace.alias,
},
],
},
];
export const manifests = [workspace, ...workspaceActions];

View File

@@ -0,0 +1,171 @@
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 } 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';
@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 _ready?: boolean = false;
@query('umb-code-editor')
private _codeEditor?: UmbCodeEditorElement;
#scriptsWorkspaceContext?: UmbScriptsWorkspaceContext;
private _modalContext?: UmbModalManagerContext;
#isNew = false;
private inputQuery$ = new Subject<string>();
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}.cshtml`);
});
});
}
#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`<umb-code-editor
language="razor"
id="content"
.code=${this._content ?? ''}
@input=${this.#onCodeEditorInput}></umb-code-editor>`;
}
render() {
return html`<umb-workspace-editor alias="Umb.Workspace.Template">
<div id="workspace-header" slot="header">
<uui-input
placeholder="Enter name..."
.value=${this._name}
@input=${this.#onNameInput}
label="template name"></uui-input>
<small>Scripts/${this._path}</small>
</div>
<uui-box>
${this._ready
? this.#renderCodeEditor()
: html`<div id="loader-container">
<uui-loader></uui-loader>
</div>`}
</uui-box>
</umb-workspace-editor>`;
}
static styles = [
UUITextStyles,
css`
:host {
display: block;
width: 100%;
height: 100%;
}
#loader-container {
display: grid;
place-items: center;
min-height: calc(100dvh - 360px);
}
umb-code-editor {
--editor-height: calc(100dvh - 300px);
}
uui-box {
min-height: calc(100dvh - 300px);
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%;
}
#code-editor-menu-container uui-icon:not([name='umb:delete']) {
margin-right: var(--uui-size-space-3);
}
#code-editor-menu-container {
display: flex;
justify-content: flex-end;
gap: var(--uui-size-space-3);
}
`,
];
}
export default UmbScriptsWorkspaceEditElement;
declare global {
interface HTMLElementTagNameMap {
'umb-scripts-workspace-edit': UmbScriptsWorkspaceEditElement;
}
}

View File

@@ -0,0 +1,97 @@
import { ScriptDetails } 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 { UpdatePartialViewRequestModel } from '@umbraco-cms/backoffice/backend-api';
export class UmbScriptsWorkspaceContext extends UmbWorkspaceContext<UmbScriptsRepository, ScriptDetails> {
getEntityId(): string | undefined {
return this.getData()?.path;
}
getEntityType(): string {
throw new Error('Method not implemented.');
}
save(): Promise<void> {
const script = this.getData();
if (!script) return Promise.reject('Something went wrong, there is no data for partial view you want to save...');
if (this.getIsNew()) {
const createRequestBody = {
name: script.name,
content: script.content,
parentPath: script.path + '/',
};
this.repository.create(createRequestBody);
console.log('create');
return Promise.resolve();
}
if (!script.path) return Promise.reject('There is no path');
const updateRequestBody: UpdatePartialViewRequestModel = {
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.');
}
#data = new UmbDeepState<ScriptDetails | undefined>(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, 'Umb.Workspace.PartialViews', 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 | null, name = 'Empty') {
const { data } = await this.repository.createScaffold(parentKey, name);
const newPartial = {
...data,
name: '',
path: parentKey ?? '',
};
if (!data) return;
this.setIsNew(true);
this.#data.next(newPartial);
}
}

View File

@@ -0,0 +1,58 @@
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 './scripts-workspace-edit.element.js';
import { UmbWorkspaceIsNewRedirectController } from '@umbraco-cms/backoffice/workspace';
@customElement('umb-scripts-workspace')
export class UmbScriptsWorkspaceElement extends UmbLitElement {
#scriptsWorkspaceContext = new UmbScriptsWorkspaceContext(this);
#element = document.createElement('umb-scripts-workspace-edit');
@state()
_routes: UmbRoute[] = [
{
path: 'create/:parentKey',
component: () => this.#element,
setup: async (component: PageComponent, info: IRoutingInfo) => {
const parentKey = info.match.params.parentKey;
const decodePath = decodeURIComponent(parentKey);
this.#scriptsWorkspaceContext.create(decodePath === 'null' ? null : parentKey);
new UmbWorkspaceIsNewRedirectController(
this,
this.#scriptsWorkspaceContext,
this.shadowRoot!.querySelector('umb-router-slot')!,
);
},
},
{
path: 'edit/:key',
component: () => this.#element,
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`<umb-router-slot .routes=${this._routes}></umb-router-slot>`;
}
static styles = [UUITextStyles, css``];
}
export default UmbScriptsWorkspaceElement;
declare global {
interface HTMLElementTagNameMap {
'umb-scripts-workspace': UmbScriptsWorkspaceElement;
}
}