diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-api/registry/extension.registry.ts b/src/Umbraco.Web.UI.Client/libs/extensions-api/registry/extension.registry.ts index 7ced4c0dbf..5a5b779069 100644 --- a/src/Umbraco.Web.UI.Client/libs/extensions-api/registry/extension.registry.ts +++ b/src/Umbraco.Web.UI.Client/libs/extensions-api/registry/extension.registry.ts @@ -70,7 +70,7 @@ export class UmbExtensionRegistry { this._extensions.next([...extensionsValues, manifest as ManifestTypes]); } - registerMany(manifests: Array): void { + registerMany(manifests: Array): void { manifests.forEach((manifest) => this.register(manifest)); } diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-registry/interfaces/modal-extension-element.interface.ts b/src/Umbraco.Web.UI.Client/libs/extensions-registry/interfaces/modal-extension-element.interface.ts index 8c7b0c398a..af019a8c5d 100644 --- a/src/Umbraco.Web.UI.Client/libs/extensions-registry/interfaces/modal-extension-element.interface.ts +++ b/src/Umbraco.Web.UI.Client/libs/extensions-registry/interfaces/modal-extension-element.interface.ts @@ -1,7 +1,13 @@ +import type { ManifestModal } from '../models'; import type { UmbModalHandler } from '@umbraco-cms/backoffice/modal'; -export interface UmbModalExtensionElement - extends HTMLElement { +export interface UmbModalExtensionElement< + UmbModalData extends object = object, + UmbModalResult = unknown, + ModalManifestType extends ManifestModal = ManifestModal +> extends HTMLElement { + manifest?: ModalManifestType; + modalHandler?: UmbModalHandler; data?: UmbModalData; diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-registry/models/index.ts b/src/Umbraco.Web.UI.Client/libs/extensions-registry/models/index.ts index def8693ca7..c0c049bb14 100644 --- a/src/Umbraco.Web.UI.Client/libs/extensions-registry/models/index.ts +++ b/src/Umbraco.Web.UI.Client/libs/extensions-registry/models/index.ts @@ -8,7 +8,7 @@ import type { ManifestHeaderApp, ManifestHeaderAppButtonKind } from './header-ap import type { ManifestHealthCheck } from './health-check.model'; import type { ManifestMenu } from './menu.model'; import type { ManifestMenuItem, ManifestMenuItemTreeKind } from './menu-item.model'; -import type { ManifestModal } from './modal.model'; +import type { ManifestModal, ManifestModalTreePickerKind } from './modal.model'; import type { ManifestPackageView } from './package-view.model'; import type { ManifestPropertyAction } from './property-action.model'; import type { ManifestPropertyEditorUI, ManifestPropertyEditorModel } from './property-editor.model'; @@ -72,6 +72,7 @@ export type ManifestTypes = | ManifestMenuItem | ManifestMenuItemTreeKind | ManifestModal + | ManifestModalTreePickerKind | ManifestPackageView | ManifestPropertyAction | ManifestPropertyEditorModel diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-registry/models/modal.model.ts b/src/Umbraco.Web.UI.Client/libs/extensions-registry/models/modal.model.ts index 2bace8be4d..105312466f 100644 --- a/src/Umbraco.Web.UI.Client/libs/extensions-registry/models/modal.model.ts +++ b/src/Umbraco.Web.UI.Client/libs/extensions-registry/models/modal.model.ts @@ -3,3 +3,13 @@ import type { ManifestElement } from '.'; export interface ManifestModal extends ManifestElement { type: 'modal'; } + +export interface ManifestModalTreePickerKind extends ManifestModal { + type: 'modal'; + kind: 'treePicker'; + meta: MetaModalTreePickerKind; +} + +export interface MetaModalTreePickerKind { + treeAlias: string; +} diff --git a/src/Umbraco.Web.UI.Client/libs/modal/modal-handler.ts b/src/Umbraco.Web.UI.Client/libs/modal/modal-handler.ts index 6a0b00c0bb..d6881eebae 100644 --- a/src/Umbraco.Web.UI.Client/libs/modal/modal-handler.ts +++ b/src/Umbraco.Web.UI.Client/libs/modal/modal-handler.ts @@ -72,6 +72,9 @@ export class UmbModalHandlerClass { this._submitResolver = resolve; @@ -79,7 +82,7 @@ export class UmbModalHandlerClass { +export interface UmbPickerModalData { multiple?: boolean; selection?: Array; - filter?: (item: T) => boolean; - pickableFilter?: (item: T) => boolean; + filter?: (item: ItemType) => boolean; + pickableFilter?: (item: ItemType) => boolean; } - export interface UmbPickerModalResult { selection: Array; } + +export interface UmbTreePickerModalData extends UmbPickerModalData { + treeAlias?: string; +} diff --git a/src/Umbraco.Web.UI.Client/libs/modal/token/data-type-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/libs/modal/token/data-type-picker-modal.token.ts index 49a3b54798..246314f0e3 100644 --- a/src/Umbraco.Web.UI.Client/libs/modal/token/data-type-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/libs/modal/token/data-type-picker-modal.token.ts @@ -1,13 +1,16 @@ import { FolderTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; -import { UmbModalToken, UmbPickerModalData, UmbPickerModalResult } from '@umbraco-cms/backoffice/modal'; +import { UmbModalToken, UmbTreePickerModalData, UmbPickerModalResult } from '@umbraco-cms/backoffice/modal'; -export type UmbDataTypePickerModalData = UmbPickerModalData; +export type UmbDataTypePickerModalData = UmbTreePickerModalData; export type UmbDataTypePickerModalResult = UmbPickerModalResult; export const UMB_DATA_TYPE_PICKER_MODAL = new UmbModalToken( - 'Umb.Modal.DataTypePicker', + 'Umb.Modal.TreePicker', { type: 'sidebar', size: 'small', + }, + { + treeAlias: 'Umb.Tree.DataTypes', } ); diff --git a/src/Umbraco.Web.UI.Client/libs/modal/token/dictionary-item-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/libs/modal/token/dictionary-item-picker-modal.token.ts new file mode 100644 index 0000000000..c4c2288089 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/modal/token/dictionary-item-picker-modal.token.ts @@ -0,0 +1,20 @@ +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export interface UmbDictionaryItemPickerModalData { + multiple: boolean; + selection: string[]; +} + +export interface UmbDictionaryItemPickerModalResult { + selection: Array; +} + +export const UMB_DICTIONARY_ITEM_PICKER_MODAL_ALIAS = 'Umb.Modal.DictionaryItemPicker'; + +export const UMB_DICTIONARY_ITEM_PICKER_MODAL = new UmbModalToken< + UmbDictionaryItemPickerModalData, + UmbDictionaryItemPickerModalResult +>(UMB_DICTIONARY_ITEM_PICKER_MODAL_ALIAS, { + type: 'sidebar', + size: 'small', +}); diff --git a/src/Umbraco.Web.UI.Client/libs/modal/token/document-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/libs/modal/token/document-picker-modal.token.ts index 5209f34110..60f45eafee 100644 --- a/src/Umbraco.Web.UI.Client/libs/modal/token/document-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/libs/modal/token/document-picker-modal.token.ts @@ -1,18 +1,16 @@ -import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; +import { DocumentTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import { UmbModalToken, UmbPickerModalResult, UmbTreePickerModalData } from '@umbraco-cms/backoffice/modal'; -export interface UmbDocumentPickerModalData { - multiple?: boolean; - selection?: Array; -} - -export interface UmbDocumentPickerModalResult { - selection: Array; -} +export type UmbDocumentPickerModalData = UmbTreePickerModalData; +export type UmbDocumentPickerModalResult = UmbPickerModalResult; export const UMB_DOCUMENT_PICKER_MODAL = new UmbModalToken( - 'Umb.Modal.DocumentPicker', + 'Umb.Modal.TreePicker', { type: 'sidebar', size: 'small', + }, + { + treeAlias: 'Umb.Tree.Documents', } ); diff --git a/src/Umbraco.Web.UI.Client/libs/modal/token/document-type-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/libs/modal/token/document-type-picker-modal.token.ts index 6b2d0a6341..23e5a1eff4 100644 --- a/src/Umbraco.Web.UI.Client/libs/modal/token/document-type-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/libs/modal/token/document-type-picker-modal.token.ts @@ -1,18 +1,19 @@ -import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; +import { EntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import { UmbModalToken, UmbPickerModalResult, UmbTreePickerModalData } from '@umbraco-cms/backoffice/modal'; -export interface UmbDocumentTypePickerModalData { - multiple?: boolean; - selection?: Array; -} - -export interface UmbDocumentTypePickerModalResult { - selection: Array; -} +export type UmbDocumentTypePickerModalData = UmbTreePickerModalData; +export type UmbDocumentTypePickerModalResult = UmbPickerModalResult; export const UMB_DOCUMENT_TYPE_PICKER_MODAL = new UmbModalToken< UmbDocumentTypePickerModalData, UmbDocumentTypePickerModalResult ->('Umb.Modal.DocumentTypePicker', { - type: 'sidebar', - size: 'small', -}); +>( + 'Umb.Modal.TreePicker', + { + type: 'sidebar', + size: 'small', + }, + { + treeAlias: 'Umb.Tree.DocumentTypes', + } +); diff --git a/src/Umbraco.Web.UI.Client/libs/modal/token/index.ts b/src/Umbraco.Web.UI.Client/libs/modal/token/index.ts index 284cdc1e6a..8d460e2577 100644 --- a/src/Umbraco.Web.UI.Client/libs/modal/token/index.ts +++ b/src/Umbraco.Web.UI.Client/libs/modal/token/index.ts @@ -16,7 +16,7 @@ export * from './import-dictionary-modal.token'; export * from './invite-user-modal.token'; export * from './language-picker-modal.token'; export * from './link-picker-modal.token'; -export * from './media-picker-modal.token'; +export * from './media-tree-picker-modal.token'; export * from './property-editor-ui-picker-modal.token'; export * from './property-settings-modal.token'; export * from './search-modal.token'; @@ -26,4 +26,6 @@ export * from './template-picker-modal.token'; export * from './user-group-picker-modal.token'; export * from './user-picker-modal.token'; export * from './folder-modal.token'; +export * from './partial-view-picker-modal.token'; +export * from './dictionary-item-picker-modal.token'; export * from './data-type-picker-modal.token'; diff --git a/src/Umbraco.Web.UI.Client/libs/modal/token/media-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/libs/modal/token/media-picker-modal.token.ts deleted file mode 100644 index 7e25303f88..0000000000 --- a/src/Umbraco.Web.UI.Client/libs/modal/token/media-picker-modal.token.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; - -export interface UmbMediaPickerModalData { - multiple?: boolean; - selection: Array; -} - -export interface UmbMediaPickerModalResult { - selection: Array; -} - -export const UMB_MEDIA_PICKER_MODAL = new UmbModalToken( - 'Umb.Modal.MediaPicker', - { - type: 'sidebar', - size: 'small', - } -); diff --git a/src/Umbraco.Web.UI.Client/libs/modal/token/media-tree-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/libs/modal/token/media-tree-picker-modal.token.ts new file mode 100644 index 0000000000..aa050884ca --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/modal/token/media-tree-picker-modal.token.ts @@ -0,0 +1,19 @@ +import { UmbModalToken, UmbPickerModalResult, UmbTreePickerModalData } from '@umbraco-cms/backoffice/modal'; +import { ContentTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; + +export type UmbMediaTreePickerModalData = UmbTreePickerModalData; +export type UmbMediaTreePickerModalResult = UmbPickerModalResult; + +export const UMB_MEDIA_TREE_PICKER_MODAL = new UmbModalToken< + UmbMediaTreePickerModalData, + UmbMediaTreePickerModalResult +>( + 'Umb.Modal.TreePicker', + { + type: 'sidebar', + size: 'small', + }, + { + treeAlias: 'Umb.Tree.Media', + } +); diff --git a/src/Umbraco.Web.UI.Client/libs/modal/token/modal-token.ts b/src/Umbraco.Web.UI.Client/libs/modal/token/modal-token.ts index 99510c47a8..057ffe5508 100644 --- a/src/Umbraco.Web.UI.Client/libs/modal/token/modal-token.ts +++ b/src/Umbraco.Web.UI.Client/libs/modal/token/modal-token.ts @@ -1,36 +1,38 @@ import { UmbModalConfig } from '../modal.context'; -export class UmbModalToken { +export class UmbModalToken { /** * Get the data type of the token's data. * * @public - * @type {Data} + * @type {ModalDataType} * @memberOf UmbModalToken * @example `typeof MyModal.TYPE` * @returns undefined */ - readonly DATA: Data = undefined as never; + readonly DATA: ModalDataType = undefined as never; /** * Get the result type of the token * * @public - * @type {Result} + * @type {ModalResultType} * @memberOf UmbModalToken * @example `typeof MyModal.RESULT` * @returns undefined */ - readonly RESULT: Result = undefined as never; + readonly RESULT: ModalResultType = undefined as never; /** * @param alias Unique identifier for the token, * @param defaultConfig Default configuration for the modal, - * @param _desc Description for the token, - * used only for debugging purposes, - * it should but does not need to be unique + * @param defaultData Default data for the modal, */ - constructor(protected alias: string, protected defaultConfig?: UmbModalConfig, protected _desc?: string) {} + constructor( + protected alias: string, + protected defaultConfig?: UmbModalConfig, + protected defaultData?: ModalDataType + ) {} /** * This method must always return the unique alias of the token since that @@ -45,4 +47,8 @@ export class UmbModalToken { public getDefaultConfig(): UmbModalConfig | undefined { return this.defaultConfig; } + + public getDefaultData(): ModalDataType | undefined { + return this.defaultData; + } } diff --git a/src/Umbraco.Web.UI.Client/libs/modal/token/partial-view-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/libs/modal/token/partial-view-picker-modal.token.ts new file mode 100644 index 0000000000..19419c1187 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/modal/token/partial-view-picker-modal.token.ts @@ -0,0 +1,20 @@ +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export interface UmbPartialViewPickerModalData { + multiple: boolean; + selection: string[]; +} + +export interface UmbPartialViewPickerModalResult { + selection: Array | undefined; +} + +export const UMB_PARTIAL_VIEW_PICKER_MODAL_ALIAS = 'Umb.Modal.PartialViewPicker'; + +export const UMB_PARTIAL_VIEW_PICKER_MODAL = new UmbModalToken< + UmbPartialViewPickerModalData, + UmbPartialViewPickerModalResult +>(UMB_PARTIAL_VIEW_PICKER_MODAL_ALIAS, { + type: 'sidebar', + size: 'small', +}); diff --git a/src/Umbraco.Web.UI.Client/libs/modal/token/template-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/libs/modal/token/template-picker-modal.token.ts index 8f9b122a69..2af6dc02d6 100644 --- a/src/Umbraco.Web.UI.Client/libs/modal/token/template-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/libs/modal/token/template-picker-modal.token.ts @@ -1,18 +1,16 @@ -import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; +import { EntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import { UmbModalToken, UmbPickerModalResult, UmbTreePickerModalData } from '@umbraco-cms/backoffice/modal'; -export interface UmbTemplatePickerModalData { - multiple: boolean; - selection: Array; -} - -export interface UmbTemplatePickerModalResult { - selection: Array; -} +export type UmbTemplatePickerModalData = UmbTreePickerModalData; +export type UmbTemplatePickerModalResult = UmbPickerModalResult; export const UMB_TEMPLATE_PICKER_MODAL = new UmbModalToken( - 'Umb.Modal.TemplatePicker', + 'Umb.Modal.TreePicker', { type: 'sidebar', size: 'small', + }, + { + treeAlias: 'Umb.Tree.Templates', } ); diff --git a/src/Umbraco.Web.UI.Client/libs/package.json b/src/Umbraco.Web.UI.Client/libs/package.json index e5f1036d7d..2224fe9080 100644 --- a/src/Umbraco.Web.UI.Client/libs/package.json +++ b/src/Umbraco.Web.UI.Client/libs/package.json @@ -26,7 +26,7 @@ ], "peerDependencies": { "@types/uuid": "^9.0.1", - "@umbraco-ui/uui": "^1.2.0-rc.0", + "@umbraco-ui/uui": "1.2.1", "rxjs": "^7.8.0" }, "customElements": "custom-elements.json" diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/core/components/button-with-dropdown/button-with-dropdown.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/core/components/button-with-dropdown/button-with-dropdown.element.ts index d029de8155..a3240035b9 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/core/components/button-with-dropdown/button-with-dropdown.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/core/components/button-with-dropdown/button-with-dropdown.element.ts @@ -9,7 +9,6 @@ import { InterfaceColor, InterfaceLook } from '@umbraco-ui/uui-base/lib/types'; @customElement('umb-button-with-dropdown') export class UmbButtonWithDropdownElement extends LitElement { - @property() label = ''; @@ -25,6 +24,9 @@ export class UmbButtonWithDropdownElement extends LitElement { @property() placement: PopoverPlacement = 'bottom-start'; + @property({ type: Boolean }) + compact = false; + @query('#symbol-expand') symbolExpand!: UUISymbolExpandElement; @@ -55,6 +57,7 @@ export class UmbButtonWithDropdownElement extends LitElement { .look=${this.look} .color=${this.color} .label=${this.label} + .compact=${this.compact} id="myPopoverBtn" @click=${this.#togglePopover}> diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/core/components/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/core/components/index.ts index a485d7735e..c2fa96e763 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/core/components/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/core/components/index.ts @@ -73,5 +73,7 @@ import './variant-selector/variant-selector.element'; import './code-editor'; export * from './table'; +export * from './tree/tree.element'; +export * from './code-editor'; export const manifests = [...debugManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/core/components/input-list-base/input-list-base.ts b/src/Umbraco.Web.UI.Client/src/backoffice/core/components/input-list-base/input-list-base.ts index fe8713e0ab..4c61990469 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/core/components/input-list-base/input-list-base.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/core/components/input-list-base/input-list-base.ts @@ -45,7 +45,7 @@ export class UmbInputListBaseElement extends UmbLitElement { modalHandler?.onSubmit().then((data: UmbPickerModalResult) => { if (data) { - this.value = data.selection.filter((id) => id !== null && id !== undefined) as Array; + this.value = data.selection?.filter((id) => id !== null && id !== undefined) as Array; this.selectionUpdated(); } }); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/core/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/core/index.ts index 2350ed6611..d0c5b467c9 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/core/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/core/index.ts @@ -10,14 +10,27 @@ import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/modal'; import { UmbContextProviderController } from '@umbraco-cms/backoffice/context-api'; import type { UmbEntrypointOnInit } from '@umbraco-cms/backoffice/extensions-api'; +import { ManifestKind, ManifestTypes } from '@umbraco-cms/backoffice/extensions-registry'; import './notification'; -export const manifests = [ +export const manifests: Array = [ ...componentManifests, ...propertyActionManifests, ...propertyEditorManifests, ...modalManifests, + // TODO: where should these live? + { + type: 'kind', + alias: 'Umb.Kind.TreePickerModal', + matchKind: 'treePicker', + matchType: 'modal', + manifest: { + type: 'modal', + kind: 'treePicker', + elementName: 'umb-tree-picker-modal', + }, + }, ]; export const onInit: UmbEntrypointOnInit = (host, extensionRegistry) => { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/core/modals/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/core/modals/manifests.ts index a91cbbc426..12151e0fc5 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/core/modals/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/core/modals/manifests.ts @@ -37,12 +37,6 @@ const modals: Array = [ name: 'Section Picker Modal', loader: () => import('./section-picker/section-picker-modal.element'), }, - { - type: 'modal', - alias: 'Umb.Modal.TemplatePicker', - name: 'Template Picker Modal', - loader: () => import('./template-picker/template-picker-modal.element'), - }, { type: 'modal', alias: 'Umb.Modal.Template', @@ -55,6 +49,12 @@ const modals: Array = [ name: 'Embedded Media Modal', loader: () => import('./embedded-media/embedded-media-modal.element'), }, + { + type: 'modal', + alias: 'Umb.Modal.TreePicker', + name: 'Tree Picker Modal', + loader: () => import('./tree-picker/tree-picker-modal.element'), + }, ]; export const manifests = [...modals]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/core/modals/template-picker/template-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/core/modals/template-picker/template-picker-modal.element.ts deleted file mode 100644 index 3ce0e82b15..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/core/modals/template-picker/template-picker-modal.element.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { css, html } from 'lit'; -import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; -import { customElement, state } from 'lit/decorators.js'; -import { UmbTreeElement } from '../../components/tree/tree.element'; -import { UmbTemplatePickerModalData, UmbTemplatePickerModalResult } from '@umbraco-cms/backoffice/modal'; -import { UmbModalBaseElement } from '@umbraco-cms/internal/modal'; - -//TODO: make a default tree-picker that can be used across multiple pickers -// TODO: make use of UmbPickerLayoutBase -@customElement('umb-template-picker-modal') -export class UmbTemplatePickerModalElement extends UmbModalBaseElement< - UmbTemplatePickerModalData, - UmbTemplatePickerModalResult -> { - @state() - _selection: Array = []; - - @state() - _multiple = true; - - connectedCallback() { - super.connectedCallback(); - this._selection = this.data?.selection ?? []; - this._multiple = this.data?.multiple ?? true; - } - - private _handleSelectionChange(e: CustomEvent) { - e.stopPropagation(); - const element = e.target as UmbTreeElement; - this._selection = this._multiple ? element.selection : [element.selection[element.selection.length - 1]]; - } - - private _submit() { - this.modalHandler?.submit({ selection: this._selection }); - } - - private _close() { - this.modalHandler?.reject(); - } - - // TODO: implement search - // TODO: make umb-tree have a disabled option (string array like selection)? - render() { - return html` - - - -
- -
-
- - -
-
- `; - } - - static styles = [ - UUITextStyles, - css` - h3 { - margin-left: var(--uui-size-space-5); - margin-right: var(--uui-size-space-5); - } - - uui-input { - width: 100%; - } - - hr { - border: none; - border-bottom: 1px solid var(--uui-color-divider); - margin: 16px 0; - } - - #content-list { - display: flex; - flex-direction: column; - gap: var(--uui-size-space-3); - } - - .content-item { - cursor: pointer; - } - - .content-item.selected { - background-color: var(--uui-color-selected); - color: var(--uui-color-selected-contrast); - } - `, - ]; -} - -export default UmbTemplatePickerModalElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-template-picker-modal': UmbTemplatePickerModalElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/modal/data-type-picker/data-type-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/core/modals/tree-picker/tree-picker-modal.element.ts similarity index 58% rename from src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/modal/data-type-picker/data-type-picker-modal.element.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/core/modals/tree-picker/tree-picker-modal.element.ts index 30dda0bd0a..fc676a3932 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/modal/data-type-picker/data-type-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/core/modals/tree-picker/tree-picker-modal.element.ts @@ -1,23 +1,18 @@ import { css, html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; -import { customElement, property, state } from 'lit/decorators.js'; -import type { UmbTreeElement } from '../../../../core/components/tree/tree.element'; -import { - UmbDataTypePickerModalData, - UmbDataTypePickerModalResult, - UmbModalHandler, -} from '@umbraco-cms/backoffice/modal'; -import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; - -// TODO: make use of UmbPickerLayoutBase -@customElement('umb-data-type-picker-modal') -export class UmbDataTypePickerModalElement extends UmbLitElement { - @property({ attribute: false }) - modalHandler?: UmbModalHandler; - - @property({ type: Object, attribute: false }) - data?: UmbDataTypePickerModalData; +import { customElement, state } from 'lit/decorators.js'; +import type { UmbTreeElement } from '../../components/tree/tree.element'; +import { ManifestModalTreePickerKind } from '@umbraco-cms/backoffice/extensions-registry'; +import { UmbTreePickerModalData, UmbPickerModalResult } from '@umbraco-cms/backoffice/modal'; +import { UmbModalBaseElement } from '@umbraco-cms/internal/modal'; +import { TreeItemPresentationModel } from '@umbraco-cms/backoffice/backend-api'; +@customElement('umb-tree-picker-modal') +export class UmbTreePickerModalElement extends UmbModalBaseElement< + UmbTreePickerModalData, + UmbPickerModalResult, + ManifestModalTreePickerKind +> { @state() _selection: Array = []; @@ -26,6 +21,7 @@ export class UmbDataTypePickerModalElement extends UmbLitElement { connectedCallback() { super.connectedCallback(); + this._selection = this.data?.selection ?? []; this._multiple = this.data?.multiple ?? false; } @@ -49,7 +45,7 @@ export class UmbDataTypePickerModalElement extends UmbLitElement { ; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/components/input-document-type-picker/input-document-type-picker.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/components/input-document-type-picker/input-document-type-picker.element.ts index 72e51ab375..8fad412c29 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/components/input-document-type-picker/input-document-type-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/components/input-document-type-picker/input-document-type-picker.element.ts @@ -11,7 +11,7 @@ import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN, UMB_CONFIRM_MODAL, - UMB_DOCUMENT_TYPE_PICKER_MODAL, + UMB_DOCUMENT_PICKER_MODAL, } from '@umbraco-cms/backoffice/modal'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import { DocumentTypeResponseModel, EntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; @@ -73,7 +73,7 @@ export class UmbInputDocumentTypePickerElement extends FormControlMixin(UmbLitEl private _openPicker() { // We send a shallow copy(good enough as its just an array of ids) of our this._selectedIds, as we don't want the modal to manipulate our data: - const modalHandler = this._modalContext?.open(UMB_DOCUMENT_TYPE_PICKER_MODAL, { + const modalHandler = this._modalContext?.open(UMB_DOCUMENT_PICKER_MODAL, { multiple: true, selection: [...this._selectedIds], }); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/tree/manifests.ts index 2f2bf1fd5b..83ddb777f1 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/tree/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/tree/manifests.ts @@ -1,9 +1,11 @@ import { DOCUMENT_TYPE_REPOSITORY_ALIAS } from '../repository/manifests'; import type { ManifestTree, ManifestTreeItem } from '@umbraco-cms/backoffice/extensions-registry'; +export const DOCUMENT_TYPE_TREE_ALIAS = 'Umb.Tree.DocumentTypes'; + const tree: ManifestTree = { type: 'tree', - alias: 'Umb.Tree.DocumentTypes', + alias: DOCUMENT_TYPE_TREE_ALIAS, name: 'Document Types Tree', meta: { repositoryAlias: DOCUMENT_TYPE_REPOSITORY_ALIAS, diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/manifests.ts index b06012c704..7e4ce1758a 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/manifests.ts @@ -5,7 +5,6 @@ import { manifests as treeManifests } from './tree/manifests'; import { manifests as workspaceManifests } from './workspace/manifests'; import { manifests as entityActionManifests } from './entity-actions/manifests'; import { manifests as entityBulkActionManifests } from './entity-bulk-actions/manifests'; -import { manifests as modalManifests } from './modals/manifests'; import { manifests as propertyEditorManifests } from './property-editors/manifests'; export const manifests = [ @@ -16,6 +15,5 @@ export const manifests = [ ...workspaceManifests, ...entityActionManifests, ...entityBulkActionManifests, - ...modalManifests, ...propertyEditorManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/document-picker/document-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/document-picker/document-picker-modal.element.ts deleted file mode 100644 index 1b2683e71a..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/document-picker/document-picker-modal.element.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { css, html } from 'lit'; -import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; -import { customElement, state } from 'lit/decorators.js'; -import type { UmbTreeElement } from '../../../../core/components/tree/tree.element'; -import { UmbDocumentPickerModalData, UmbDocumentPickerModalResult } from '@umbraco-cms/backoffice/modal'; -import { UmbModalBaseElement } from '@umbraco-cms/internal/modal'; - -// TODO: make use of UmbPickerLayoutBase -@customElement('umb-document-picker-modal') -export class UmbDocumentPickerModalElement extends UmbModalBaseElement< - UmbDocumentPickerModalData, - UmbDocumentPickerModalResult -> { - @state() - _selection: Array = []; - - @state() - _multiple = true; - - connectedCallback() { - super.connectedCallback(); - this._selection = this.data?.selection ?? []; - this._multiple = this.data?.multiple ?? true; - } - - private _handleSelectionChange(e: CustomEvent) { - e.stopPropagation(); - const element = e.target as UmbTreeElement; - //TODO: Should multiple property be implemented here or be passed down into umb-tree? - this._selection = this._multiple ? element.selection : [element.selection[element.selection.length - 1]]; - } - - private _submit() { - this.modalHandler?.submit({ selection: this._selection }); - } - - private _close() { - this.modalHandler?.reject(); - } - - render() { - return html` - - - -
- -
-
- - -
-
- `; - } - - static styles = [ - UUITextStyles, - css` - h3 { - margin-left: var(--uui-size-space-5); - margin-right: var(--uui-size-space-5); - } - - uui-input { - width: 100%; - } - - hr { - border: none; - border-bottom: 1px solid var(--uui-color-divider); - margin: 16px 0; - } - - #content-list { - display: flex; - flex-direction: column; - gap: var(--uui-size-space-3); - } - - .content-item { - cursor: pointer; - } - - .content-item.selected { - background-color: var(--uui-color-selected); - color: var(--uui-color-selected-contrast); - } - `, - ]; -} - -export default UmbDocumentPickerModalElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-document-picker-modal': UmbDocumentPickerModalElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/document-picker/document-picker-modal.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/document-picker/document-picker-modal.stories.ts deleted file mode 100644 index 293c44f6d5..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/document-picker/document-picker-modal.stories.ts +++ /dev/null @@ -1,26 +0,0 @@ -import '../../../../core/components/body-layout/body-layout.element'; -import './document-picker-modal.element'; - -import { Meta, Story } from '@storybook/web-components'; -import { html } from 'lit'; - -import type { UmbDocumentPickerModalElement } from './document-picker-modal.element'; -import type { UmbDocumentPickerModalData } from '@umbraco-cms/backoffice/modal'; - -export default { - title: 'API/Modals/Layouts/Content Picker', - component: 'umb-document-picker-modal', - id: 'umb-document-picker-modal', -} as Meta; - -const data: UmbDocumentPickerModalData = { - multiple: true, - selection: [], -}; - -export const Overview: Story = () => html` - - -`; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/document-type-picker/document-type-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/document-type-picker/document-type-picker-modal.element.ts deleted file mode 100644 index 69b5da52eb..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/document-type-picker/document-type-picker-modal.element.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { css, html } from 'lit'; -import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; -import { customElement, state } from 'lit/decorators.js'; -import type { UmbTreeElement } from '../../../../core/components/tree/tree.element'; -import { UmbDocumentTypePickerModalData, UmbDocumentTypePickerModalResult } from '@umbraco-cms/backoffice/modal'; -import { UmbModalBaseElement } from '@umbraco-cms/internal/modal'; - -// TODO: make use of UmbPickerLayoutBase -@customElement('umb-document-type-picker-modal') -export class UmbDocumentTypePickerModalElement extends UmbModalBaseElement< - UmbDocumentTypePickerModalData, - UmbDocumentTypePickerModalResult -> { - @state() - _selection: Array = []; - - @state() - _multiple = true; - - connectedCallback() { - super.connectedCallback(); - this._selection = this.data?.selection ?? []; - this._multiple = this.data?.multiple ?? true; - } - - private _handleSelectionChange(e: CustomEvent) { - e.stopPropagation(); - const element = e.target as UmbTreeElement; - this._selection = element.selection; - } - - private _submit() { - this.modalHandler?.submit({ selection: this._selection }); - } - - private _close() { - this.modalHandler?.reject(); - } - - render() { - return html` - - - -
- -
-
- - -
-
- `; - } - - static styles = [ - UUITextStyles, - css` - h3 { - margin-left: var(--uui-size-space-5); - margin-right: var(--uui-size-space-5); - } - - uui-input { - width: 100%; - } - - hr { - border: none; - border-bottom: 1px solid var(--uui-color-divider); - margin: 16px 0; - } - - #content-list { - display: flex; - flex-direction: column; - gap: var(--uui-size-space-3); - } - - .content-item { - cursor: pointer; - } - - .content-item.selected { - background-color: var(--uui-color-selected); - color: var(--uui-color-selected-contrast); - } - `, - ]; -} - -export default UmbDocumentTypePickerModalElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-document-type-picker-modal': UmbDocumentTypePickerModalElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/document-type-picker/document-type-picker-modal.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/document-type-picker/document-type-picker-modal.stories.ts deleted file mode 100644 index f946b89fad..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/document-type-picker/document-type-picker-modal.stories.ts +++ /dev/null @@ -1,26 +0,0 @@ -import '../../../../core/components/body-layout/body-layout.element'; -import './document-type-picker-modal.element'; - -import { Meta, Story } from '@storybook/web-components'; -import { html } from 'lit'; - -import type { UmbDocumentTypePickerModalElement } from './document-type-picker-modal.element'; -import type { UmbDocumentTypePickerModalData } from '@umbraco-cms/backoffice/modal'; - -export default { - title: 'API/Modals/Layouts/Content Picker', - component: 'umb-document-type-picker-modal', - id: 'umb-document-type-picker-modal', -} as Meta; - -const data: UmbDocumentTypePickerModalData = { - multiple: true, - selection: [], -}; - -export const Overview: Story = () => html` - - -`; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/manifests.ts deleted file mode 100644 index 5e9595e6a0..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/modals/manifests.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { ManifestModal } from '@umbraco-cms/backoffice/extensions-registry'; - -const modals: Array = [ - { - type: 'modal', - alias: 'Umb.Modal.DocumentPicker', - name: 'Document Picker Modal', - loader: () => import('./document-picker/document-picker-modal.element'), - }, - { - type: 'modal', - alias: 'Umb.Modal.DocumentTypePicker', - name: 'Document Type Picker Modal', - loader: () => import('./document-type-picker/document-type-picker-modal.element'), - }, -]; - -export const manifests = [...modals]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/manifests.ts index 306b2da976..ddc496c003 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/tree/manifests.ts @@ -1,11 +1,11 @@ import { DOCUMENT_REPOSITORY_ALIAS } from '../repository/manifests'; import type { ManifestTree, ManifestTreeItem } from '@umbraco-cms/backoffice/extensions-registry'; -const treeAlias = 'Umb.Tree.Documents'; +export const DOCUMENT_TREE_ALIAS = 'Umb.Tree.Documents'; const tree: ManifestTree = { type: 'tree', - alias: treeAlias, + alias: DOCUMENT_TREE_ALIAS, name: 'Documents Tree', meta: { repositoryAlias: DOCUMENT_REPOSITORY_ALIAS, diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/components/input-media-picker/input-media-picker.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/components/input-media-picker/input-media-picker.element.ts index 4c66716703..1305c0460e 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/components/input-media-picker/input-media-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/components/input-media-picker/input-media-picker.element.ts @@ -8,7 +8,7 @@ import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN, UMB_CONFIRM_MODAL, - UMB_MEDIA_PICKER_MODAL, + UMB_MEDIA_TREE_PICKER_MODAL, } from '@umbraco-cms/backoffice/modal'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import type { EntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; @@ -120,7 +120,7 @@ export class UmbInputMediaPickerElement extends FormControlMixin(UmbLitElement) private _openPicker() { // We send a shallow copy(good enough as its just an array of ids) of our this._selectedIds, as we don't want the modal to manipulate our data: - const modalHandler = this._modalContext?.open(UMB_MEDIA_PICKER_MODAL, { + const modalHandler = this._modalContext?.open(UMB_MEDIA_TREE_PICKER_MODAL, { multiple: this.max === 1 ? false : true, selection: [...this._selectedIds], }); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/entity-bulk-actions/move/move.action.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/entity-bulk-actions/move/move.action.ts index 6f34b483ca..10d2bf8de5 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/entity-bulk-actions/move/move.action.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/entity-bulk-actions/move/move.action.ts @@ -2,7 +2,7 @@ import type { UmbMediaRepository } from '../../repository/media.repository'; import { UmbEntityBulkActionBase } from '@umbraco-cms/backoffice/entity-action'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; -import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN, UMB_MEDIA_PICKER_MODAL } from '@umbraco-cms/backoffice/modal'; +import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN, UMB_MEDIA_TREE_PICKER_MODAL } from '@umbraco-cms/backoffice/modal'; export class UmbMediaMoveEntityBulkAction extends UmbEntityBulkActionBase { #modalContext?: UmbModalContext; @@ -17,7 +17,7 @@ export class UmbMediaMoveEntityBulkAction extends UmbEntityBulkActionBase = [ - { - type: 'modal', - alias: 'Umb.Modal.MediaPicker', - name: 'Media Picker Modal', - loader: () => import('./media-picker/media-picker-modal.element'), - }, -]; - -export const manifests = [...modals]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/modals/media-picker/media-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/modals/media-picker/media-picker-modal.element.ts deleted file mode 100644 index 51c6822108..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/modals/media-picker/media-picker-modal.element.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { css, html } from 'lit'; -import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; -import { customElement, state } from 'lit/decorators.js'; -import { UmbTreeElement } from '../../../../core/components/tree/tree.element'; -import { UmbMediaPickerModalData, UmbMediaPickerModalResult } from '@umbraco-cms/backoffice/modal'; -import { UmbModalBaseElement } from '@umbraco-cms/internal/modal'; - -@customElement('umb-media-picker-modal') -export class UmbMediaPickerModalElement extends UmbModalBaseElement< - UmbMediaPickerModalData, - UmbMediaPickerModalResult -> { - @state() - _selection: Array = []; - - @state() - _multiple = true; - - connectedCallback() { - super.connectedCallback(); - this._selection = this.data?.selection ?? []; - this._multiple = this.data?.multiple ?? true; - } - - private _handleSelectionChange(e: CustomEvent) { - e.stopPropagation(); - const element = e.target as UmbTreeElement; - this._selection = element.selection; - } - - private _submit() { - this.modalHandler?.submit({ selection: this._selection }); - } - - private _close() { - this.modalHandler?.reject(); - } - - render() { - return html` - - - -
- -
-
- - -
-
- `; - } - - static styles = [ - UUITextStyles, - css` - h3 { - margin-left: var(--uui-size-space-5); - margin-right: var(--uui-size-space-5); - } - - uui-input { - width: 100%; - } - - hr { - border: none; - border-bottom: 1px solid var(--uui-color-divider); - margin: 16px 0; - } - - #content-list { - display: flex; - flex-direction: column; - gap: var(--uui-size-space-3); - } - - .content-item { - cursor: pointer; - } - - .content-item.selected { - background-color: var(--uui-color-selected); - color: var(--uui-color-selected-contrast); - } - `, - ]; -} - -export default UmbMediaPickerModalElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-media-picker-modal': UmbMediaPickerModalElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/manifests.ts index 28e0f18938..fac58d2628 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/manifests.ts @@ -3,7 +3,6 @@ import { manifests as repositoryManifests } from './repository/manifests'; import { manifests as menuItemManifests } from './menu-item/manifests'; import { manifests as treeManifests } from './tree/manifests'; import { manifests as workspaceManifests } from './workspace/manifests'; -import { manifests as modalManifests } from './modal/manifests'; export const manifests = [ ...entityActions, @@ -11,5 +10,4 @@ export const manifests = [ ...menuItemManifests, ...treeManifests, ...workspaceManifests, - ...modalManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/modal/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/modal/manifests.ts deleted file mode 100644 index 838aba346b..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/modal/manifests.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { ManifestModal } from '@umbraco-cms/backoffice/extensions-registry'; - -const modals: Array = [ - { - type: 'modal', - alias: 'Umb.Modal.DataTypePicker', - name: 'Data Type Picker Modal', - loader: () => import('./data-type-picker/data-type-picker-modal.element'), - }, -]; - -export const manifests = [...modals]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/components/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/components/index.ts index 23987fc391..1e8352bcf1 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/templating/components/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/components/index.ts @@ -1 +1,2 @@ import './file-system-tree-item/file-system-tree-item.element'; +import './insert-menu/templating-insert-menu.element'; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/components/insert-menu/templating-insert-menu.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/components/insert-menu/templating-insert-menu.element.ts new file mode 100644 index 0000000000..b9cf26f696 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/components/insert-menu/templating-insert-menu.element.ts @@ -0,0 +1,220 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { UMB_MODAL_TEMPLATING_INSERT_CHOOSE_TYPE_SIDEBAR_ALIAS } from '../../modals/manifests'; +import { UmbDictionaryRepository } from '../../../translation/dictionary/repository/dictionary.repository'; +import { getInsertDictionarySnippet, getInsertPartialSnippet } from '../../utils'; +import { + ChooseInsertTypeModalResult, + CodeSnippetType, + UMB_MODAL_TEMPLATING_INSERT_FIELD_SIDEBAR_MODAL, +} from '../../modals/insert-choose-type-sidebar.element'; +import { + UMB_DICTIONARY_ITEM_PICKER_MODAL, + UMB_MODAL_CONTEXT_TOKEN, + UMB_PARTIAL_VIEW_PICKER_MODAL, + UmbDictionaryItemPickerModalResult, + UmbModalContext, + UmbModalHandler, + UmbModalToken, + UmbPartialViewPickerModalResult, +} from '@umbraco-cms/backoffice/modal'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; + +export const UMB_MODAL_TEMPLATING_INSERT_CHOOSE_TYPE_SIDEBAR_MODAL = new UmbModalToken<{ hidePartialView: boolean }>( + UMB_MODAL_TEMPLATING_INSERT_CHOOSE_TYPE_SIDEBAR_ALIAS, + { + type: 'sidebar', + size: 'small', + } +); + +@customElement('umb-templating-insert-menu') +export class UmbTemplatingInsertMenuElement extends UmbLitElement { + @property() + value = ''; + + private _modalContext?: UmbModalContext; + + #openModal?: UmbModalHandler; + + #dictionaryRepository = new UmbDictionaryRepository(this); + + constructor() { + super(); + this.consumeContext(UMB_MODAL_CONTEXT_TOKEN, (instance) => { + this._modalContext = instance; + }); + } + + async determineInsertValue(modalResult: ChooseInsertTypeModalResult) { + const { type, value } = modalResult; + + switch (type) { + case CodeSnippetType.umbracoField: { + this.#getUmbracoFieldValueSnippet(value as string); + break; + } + case CodeSnippetType.partialView: { + this.#getPartialViewSnippet(value as UmbPartialViewPickerModalResult); + break; + } + case CodeSnippetType.dictionaryItem: { + this.#getDictionaryItemSnippet(value as UmbDictionaryItemPickerModalResult); + break; + } + case CodeSnippetType.macro: { + throw new Error('Not implemented'); + } + } + } + + #getDictionaryItemSnippet = async (modalResult: UmbDictionaryItemPickerModalResult) => { + const id = modalResult.selection[0]; + if (id === null) return; + const { data } = await this.#dictionaryRepository.requestById(id); + this.value = getInsertDictionarySnippet(data?.name ?? ''); + }; + + #getUmbracoFieldValueSnippet = async (value: string) => { + this.value = value; + }; + + #getPartialViewSnippet = async (modalResult: UmbPartialViewPickerModalResult) => { + this.value = getInsertPartialSnippet(modalResult.selection?.[0] ?? ''); + }; + + #openChooseTypeModal = () => { + this.#openModal = this._modalContext?.open(UMB_MODAL_TEMPLATING_INSERT_CHOOSE_TYPE_SIDEBAR_MODAL, { + hidePartialView: this.hidePartialView, + }); + this.#openModal?.onSubmit().then((closedModal: ChooseInsertTypeModalResult) => { + this.determineInsertValue(closedModal); + }); + }; + + #openInsertValueSidebar() { + this.#openModal = this._modalContext?.open(UMB_MODAL_TEMPLATING_INSERT_FIELD_SIDEBAR_MODAL); + this.#openModal?.onSubmit().then((value) => { + this.value = value; + this.#dispatchInsertEvent(); + }); + } + + #openInsertPartialViewSidebar() { + this.#openModal = this._modalContext?.open(UMB_PARTIAL_VIEW_PICKER_MODAL); + this.#openModal?.onSubmit().then((value) => { + this.#getPartialViewSnippet(value).then(() => { + this.#dispatchInsertEvent(); + }); + }); + } + + #openInsertDictionaryItemModal() { + this.#openModal = this._modalContext?.open(UMB_DICTIONARY_ITEM_PICKER_MODAL); + this.#openModal?.onSubmit().then((value) => { + this.#getDictionaryItemSnippet(value).then(() => { + this.#dispatchInsertEvent(); + }); + }); + } + + #dispatchInsertEvent() { + this.dispatchEvent(new CustomEvent('insert', { bubbles: true, cancelable: true, composed: false })); + } + + @property() + hidePartialView = false; + + render() { + return html` + + + Insert + +
    +
  • + + +
  • + ${this.hidePartialView + ? '' + : html`
  • + + +
  • `} +
  • + + +
  • +
  • + +
  • +
+
+
+ `; + } + + static styles = [ + UUITextStyles, + css` + #insert-menu { + margin: 0; + padding: 0; + margin-top: var(--uui-size-space-3); + background-color: var(--uui-color-surface); + box-shadow: var(--uui-shadow-depth-3); + min-width: 150px; + } + + #insert-menu > li, + ul { + padding: 0; + width: 100%; + list-style: none; + } + + ul { + transform: translateX(-100px); + } + + .insert-menu-item { + width: 100%; + } + + umb-button-with-dropdown { + --umb-button-with-dropdown-symbol-expand-margin-left: 0; + } + + uui-icon[name='umb:add'] { + margin-right: var(--uui-size-4); + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-templating-insert-menu': UmbTemplatingInsertMenuElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/index.ts index 5d4a400fda..c53a07eaac 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/templating/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/index.ts @@ -1,12 +1,20 @@ import { manifests as menuManifests } from './menu.manifests'; import { manifests as templateManifests } from './templates/manifests'; import { manifests as stylesheetManifests } from './stylesheets/manifests'; +import { manifests as partialManifests } from './partial-views/manifests'; +import { manifests as modalManifests } from './modals/manifests'; import type { UmbEntrypointOnInit } from '@umbraco-cms/backoffice/extensions-api'; import './components'; import './templates/components'; -export const manifests = [...menuManifests, ...templateManifests, ...stylesheetManifests]; +export const manifests = [ + ...menuManifests, + ...templateManifests, + ...stylesheetManifests, + ...partialManifests, + ...modalManifests, +]; export const onInit: UmbEntrypointOnInit = (_host, extensionRegistry) => { extensionRegistry.registerMany(manifests); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/modals/insert-choose-type-sidebar.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/modals/insert-choose-type-sidebar.element.ts new file mode 100644 index 0000000000..e2bea884cb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/modals/insert-choose-type-sidebar.element.ts @@ -0,0 +1,159 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import { UMB_MODAL_TEMPLATING_INSERT_FIELD_SIDEBAR_ALIAS } from './manifests'; +import { UmbModalBaseElement } from '@umbraco-cms/internal/modal'; +import { + UMB_MODAL_CONTEXT_TOKEN, + UmbModalContext, + UmbModalToken, + UMB_PARTIAL_VIEW_PICKER_MODAL, + UmbModalHandler, + UMB_DICTIONARY_ITEM_PICKER_MODAL, + UmbDictionaryItemPickerModalResult, +} from '@umbraco-cms/backoffice/modal'; + +export const UMB_MODAL_TEMPLATING_INSERT_FIELD_SIDEBAR_MODAL = new UmbModalToken( + UMB_MODAL_TEMPLATING_INSERT_FIELD_SIDEBAR_ALIAS, + { + type: 'sidebar', + size: 'small', + } +); + +export interface ChooseInsertTypeModalData { + hidePartialViews?: boolean; +} + +export enum CodeSnippetType { + partialView = 'partialView', + umbracoField = 'umbracoField', + dictionaryItem = 'dictionaryItem', + macro = 'macro', +} +export interface ChooseInsertTypeModalResult { + value: string | UmbDictionaryItemPickerModalResult; + type: CodeSnippetType; +} + +@customElement('umb-templating-choose-insert-type-modal') +export default class UmbChooseInsertTypeModalElement extends UmbModalBaseElement< + ChooseInsertTypeModalData, + ChooseInsertTypeModalResult +> { + private _close() { + this.modalHandler?.reject(); + } + + private _modalContext?: UmbModalContext; + + constructor() { + super(); + this.consumeContext(UMB_MODAL_CONTEXT_TOKEN, (instance) => { + this._modalContext = instance; + }); + } + + #openModal?: UmbModalHandler; + + #openInsertValueSidebar() { + this.#openModal = this._modalContext?.open(UMB_MODAL_TEMPLATING_INSERT_FIELD_SIDEBAR_MODAL); + this.#openModal?.onSubmit().then((chosenValue) => { + if (chosenValue) this.modalHandler?.submit({ value: chosenValue, type: CodeSnippetType.umbracoField }); + }); + } + + #openInsertPartialViewSidebar() { + this.#openModal = this._modalContext?.open(UMB_PARTIAL_VIEW_PICKER_MODAL); + this.#openModal?.onSubmit().then((partialViewPickerModalResult) => { + if (partialViewPickerModalResult) + this.modalHandler?.submit({ + type: CodeSnippetType.partialView, + value: partialViewPickerModalResult.selection[0], + }); + }); + } + + #openInsertDictionaryItemModal() { + this.#openModal = this._modalContext?.open(UMB_DICTIONARY_ITEM_PICKER_MODAL); + this.#openModal?.onSubmit().then((dictionaryItemPickerModalResult) => { + if (dictionaryItemPickerModalResult) this.modalHandler?.submit({ value: dictionaryItemPickerModalResult, type: CodeSnippetType.dictionaryItem }); + }); + } + + render() { + return html` + +
+ +

Value

+

+ Displays the value of a named field from the current page, with options to modify the value or fallback + to alternative values. +

+ ${this.data?.hidePartialViews + ? '' + : html`

Partial view

+

+ A partial view is a separate template file which can be rendered inside another template, it's great + for reusing markup or for separating complex templates into separate files. +

`} +

Macro

+

+ A Macro is a configurable component which is great for reusable parts of your design, where you need the + option to provide parameters, such as galleries, forms and lists. +

+

Dictionary item

+

+ A dictionary item is a placeholder for a translatable piece of text, which makes it easy to create + designs for multilingual websites. +

+
+
+
+ Close +
+
+ `; + } + + static styles = [ + UUITextStyles, + css` + :host { + display: block; + color: var(--uui-color-text); + } + + #main { + box-sizing: border-box; + padding: var(--uui-size-space-5); + height: calc(100vh - 124px); + } + + #main uui-button:not(:last-of-type) { + display: block; + margin-bottom: var(--uui-size-space-5); + } + + h3, + p { + text-align: left; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-templating-choose-insert-type-modal': UmbChooseInsertTypeModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/modals/insert-section-modal/insert-section-input.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/modals/insert-section-modal/insert-section-input.element.ts new file mode 100644 index 0000000000..77910962ae --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/modals/insert-section-modal/insert-section-input.element.ts @@ -0,0 +1,139 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html } from 'lit'; +import { customElement, property, query } from 'lit/decorators.js'; +import { UUIBooleanInputElement, UUIInputElement } from '@umbraco-ui/uui'; +import { getAddSectionSnippet, getRenderBodySnippet, getRenderSectionSnippet } from '../../utils'; + +@customElement('umb-insert-section-checkbox') +export class UmbInsertSectionCheckboxElement extends UUIBooleanInputElement { + renderCheckbox() { + return html``; + } + + @property({ type: Boolean, attribute: 'show-mandatory' }) + showMandatory = false; + + @property({ type: Boolean, attribute: 'show-input' }) + showInput = false; + + @query('uui-input') + input?: UUIInputElement; + + @query('form') + form?: HTMLFormElement; + + @query('uui-checkbox') + checkbox?: HTMLFormElement; + + validate() { + if (!this.form) return true; + + this.form.requestSubmit(); + return this.form.checkValidity(); + } + + #preventDefault(event: Event) { + event.preventDefault(); + } + + get inputValue() { + return this.input?.value; + } + + get isMandatory() { + return this.checkbox?.checked; + } + + /* eslint-disable lit-a11y/click-events-have-key-events */ + render() { + return html` + ${super.render()} +

${this.checked ? html`` : ''}${this.label}

+
+

here goes some description

+
+ ${this.checked && this.showInput + ? html` +
+ + Section name + ${this.showMandatory + ? html`

+ Section is mandatory
+ If mandatory, the child template must contain a @section definition, otherwise an + error is shown. +

` + : ''} +
+
` + : ''} + `; + } + /* eslint-enable lit-a11y/click-events-have-key-events */ + + static styles = [ + ...UUIBooleanInputElement.styles, + UUITextStyles, + css` + :host { + display: block; + border-style: dashed; + background-color: transparent; + color: var(--uui-color-default-standalone, rgb(28, 35, 59)); + border-color: var(--uui-color-border-standalone, #c2c2c2); + border-radius: var(--uui-border-radius, 3px); + border-width: 1px; + line-height: normal; + padding: 6px 18px; + } + + :host(:hover), + :host(:focus), + :host(:focus-within) { + background-color: var(--uui-button-background-color-hover, transparent); + color: var(--uui-color-default-emphasis, #3544b1); + border-color: var(--uui-color-default-emphasis, #3544b1); + } + + uui-icon { + background-color: var(--uui-color-positive-emphasis); + border-radius: 50%; + padding: 0.2em; + margin-right: 1ch; + color: var(--uui-color-positive-contrast); + font-size: 0.7em; + } + + ::slotted(*) { + line-height: normal; + } + + .label { + display: none; + } + + h3, + p { + text-align: left; + } + + uui-input { + width: 100%; + } + `, + ]; +} + +export default UmbInsertSectionCheckboxElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-insert-section-input': UmbInsertSectionCheckboxElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/modals/insert-section-modal/insert-section-modal.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/modals/insert-section-modal/insert-section-modal.element.ts new file mode 100644 index 0000000000..7f3bb25f16 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/modals/insert-section-modal/insert-section-modal.element.ts @@ -0,0 +1,131 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html } from 'lit'; +import { customElement, queryAll, state } from 'lit/decorators.js'; +import { UMB_MODAL_TEMPLATING_INSERT_SECTION_SIDEBAR_ALIAS } from '../manifests'; +import { UmbModalBaseElement } from '@umbraco-cms/internal/modal'; +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +import './insert-section-input.element'; +import UmbInsertSectionCheckboxElement from './insert-section-input.element'; +import { getAddSectionSnippet, getRenderBodySnippet, getRenderSectionSnippet } from '../../utils'; + +export const UMB_MODAL_TEMPLATING_INSERT_SECTION_MODAL = new UmbModalToken( + UMB_MODAL_TEMPLATING_INSERT_SECTION_SIDEBAR_ALIAS, + { + type: 'sidebar', + size: 'small', + } +); + +export interface InsertSectionModalModalResult { + value?: string; +} + +@customElement('umb-templating-insert-section-modal') +export default class UmbTemplatingInsertSectionModalElement extends UmbModalBaseElement< + object, + InsertSectionModalModalResult +> { + @queryAll('umb-insert-section-checkbox') + checkboxes!: NodeListOf; + + @state() + selectedCheckbox?: UmbInsertSectionCheckboxElement | null = null; + + @state() + snippet = ''; + + #chooseSection(event: Event) { + event.stopPropagation(); + const target = event.target as UmbInsertSectionCheckboxElement; + const checkboxes = Array.from(this.checkboxes); + if (checkboxes.every((checkbox) => checkbox.checked === false)) { + this.selectedCheckbox = null; + return; + } + if (target.checked) { + this.selectedCheckbox = target; + this.snippet = this.snippetMethods[checkboxes.indexOf(target)](target?.inputValue as string, target?.isMandatory); + checkboxes.forEach((checkbox) => { + if (checkbox !== target) { + checkbox.checked = false; + } + }); + } + } + + firstUpdated() { + this.selectedCheckbox = this.checkboxes[0]; + } + + snippetMethods = [getRenderBodySnippet, getRenderSectionSnippet, getAddSectionSnippet]; + + #close() { + this.modalHandler?.reject(); + } + + #submit() { + if (this.selectedCheckbox?.validate()) this.modalHandler?.submit({ value: this.snippet ?? '' }); + } + + render() { + return html` + +
+ + +

+ Renders the contents of a child template, by inserting a @RenderBody() placeholder. +

+
+ + +

+ Renders a named area of a child template, by inserting a @RenderSection(name) placeholder. + This renders an area of a child template which is wrapped in a corresponding + @section [name]{ ... } definition. +

+
+ + +

+ Defines a part of your template as a named section by wrapping it in @section { ... }. This + can be rendered in a specific area of the parent of this template, by using @RenderSection. +

+
+
+
+
+ Close + Submit +
+
+ `; + } + + static styles = [ + UUITextStyles, + css` + :host { + display: block; + color: var(--uui-color-text); + } + + #main { + box-sizing: border-box; + padding: var(--uui-size-space-5); + height: calc(100vh - 124px); + } + + #main umb-insert-section-checkbox:not(:last-of-type) { + margin-bottom: var(--uui-size-space-5); + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-templating-insert-section-modal': UmbTemplatingInsertSectionModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/modals/insert-value-sidebar.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/modals/insert-value-sidebar.element.ts new file mode 100644 index 0000000000..2431406194 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/modals/insert-value-sidebar.element.ts @@ -0,0 +1,132 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { UUIComboboxElement, UUIInputElement } from '@umbraco-ui/uui'; +import { getUmbracoFieldSnippet } from '../utils'; +import { UmbModalBaseElement } from '@umbraco-cms/internal/modal'; + +@customElement('umb-insert-value-sidebar') +export default class UmbInsertValueSidebarElement extends UmbModalBaseElement { + private _close() { + this.modalHandler?.submit(); + } + + private _submit() { + this.modalHandler?.submit(this.output); + } + + @state() + showDefaultValueInput = false; + + @state() + recursive = false; + + @state() + defaultValue: string | null = null; + + @state() + field: string | null = null; + + @state() + output = ''; + + protected willUpdate(): void { + this.output = this.field ? getUmbracoFieldSnippet(this.field, this.defaultValue, this.recursive) : ''; + } + + #setField(event: Event) { + const target = event.target as UUIComboboxElement; + this.field = target.value as string; + } + + #setDefaultValue(event: Event) { + const target = event.target as UUIInputElement; + this.defaultValue = target.value === '' ? null : (target.value as string); + } + + render() { + return html` + +
+ + + Choose field + + + apple + orange + lemon + + + + ${this.showDefaultValueInput + ? html` + Default value + + + ` + : html` (this.showDefaultValueInput = true)} + look="placeholder" + label="Add default value " + >Add default value`} + + Fallback + (this.recursive = !this.recursive)}>From ancestors + + + Output sample + ${this.output} + + +
+
+ Close + Submit +
+
+ `; + } + + static styles = [ + UUITextStyles, + css` + :host { + display: block; + color: var(--uui-color-text); + } + + #main { + box-sizing: border-box; + padding: var(--uui-size-space-5); + height: calc(100vh - 124px); + } + + #main uui-button { + width: 100%; + } + + h3, + p { + text-align: left; + } + + uui-combobox, + uui-input { + width: 100%; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-insert-value-sidebar': UmbInsertValueSidebarElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/modals/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/modals/manifests.ts new file mode 100644 index 0000000000..19a0131992 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/modals/manifests.ts @@ -0,0 +1,35 @@ +import { ManifestModal } from '@umbraco-cms/backoffice/extensions-registry'; +import { UMB_PARTIAL_VIEW_PICKER_MODAL_ALIAS } from '@umbraco-cms/backoffice/modal'; + +export const UMB_MODAL_TEMPLATING_INSERT_CHOOSE_TYPE_SIDEBAR_ALIAS = 'Umb.Modal.Templating.Insert.ChooseType.Sidebar'; +export const UMB_MODAL_TEMPLATING_INSERT_FIELD_SIDEBAR_ALIAS = 'Umb.Modal.Templating.Insert.Value.Sidebar'; +export const UMB_MODAL_TEMPLATING_INSERT_SECTION_SIDEBAR_ALIAS = 'Umb.Modal.Templating.Insert.Section.Sidebar'; + +const modals: Array = [ + { + type: 'modal', + alias: UMB_MODAL_TEMPLATING_INSERT_CHOOSE_TYPE_SIDEBAR_ALIAS, + name: 'Choose insert type sidebar', + loader: () => import('./insert-choose-type-sidebar.element'), + }, + { + type: 'modal', + alias: UMB_MODAL_TEMPLATING_INSERT_FIELD_SIDEBAR_ALIAS, + name: 'Insert value type sidebar', + loader: () => import('./insert-value-sidebar.element'), + }, + { + type: 'modal', + alias: UMB_PARTIAL_VIEW_PICKER_MODAL_ALIAS, + name: 'Partial View Picker Modal', + loader: () => import('../../templating/modals/partial-view-picker-modal.element'), + }, + { + type: 'modal', + alias: UMB_MODAL_TEMPLATING_INSERT_SECTION_SIDEBAR_ALIAS, + name: 'Partial Insert Section Picker Modal', + loader: () => import('./insert-section-modal/insert-section-modal.element'), + }, +]; + +export const manifests = [...modals]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/modals/modal-tokens.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/modals/modal-tokens.ts new file mode 100644 index 0000000000..84d366ca6c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/modals/modal-tokens.ts @@ -0,0 +1 @@ +//TODO: move tokens here nad import this file somewhere diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/modals/partial-view-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/modals/partial-view-picker-modal.element.ts new file mode 100644 index 0000000000..824b2469bf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/modals/partial-view-picker-modal.element.ts @@ -0,0 +1,89 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { UmbPartialViewPickerModalData, UmbPartialViewPickerModalResult } from '@umbraco-cms/backoffice/modal'; +import { UmbModalBaseElement } from '@umbraco-cms/internal/modal'; +import { UmbTreeElement } from '@umbraco-cms/backoffice/core/components'; + +@customElement('umb-partial-view-picker-modal') +export default class UmbPartialViewPickerModalElement extends UmbModalBaseElement< + UmbPartialViewPickerModalData, + UmbPartialViewPickerModalResult +> { + @state() + _selection: Array = []; + + @state() + _multiple = false; + + connectedCallback() { + super.connectedCallback(); + this._selection = this.data?.selection ?? []; + this._multiple = this.data?.multiple ?? true; + } + + private _handleSelectionChange(e: CustomEvent) { + e.stopPropagation(); + const element = e.target as UmbTreeElement; + this._selection = this._multiple ? element.selection : [element.selection[element.selection.length - 1]]; + this._submit(); + } + + private _submit() { + this.modalHandler?.submit({ selection: this._selection }); + } + + private _close() { + this.modalHandler?.reject(); + } + + render() { + return html` + +
+ + + +
+
+ Close +
+
+ `; + } + + static styles = [ + UUITextStyles, + css` + :host { + display: block; + color: var(--uui-color-text); + } + + #main { + box-sizing: border-box; + padding: var(--uui-size-space-5); + height: calc(100vh - 124px); + } + + #main uui-button { + width: 100%; + } + + h3, + p { + text-align: left; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-partial-view-picker-modal': UmbPartialViewPickerModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/config.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/config.ts new file mode 100644 index 0000000000..d50c16cca9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/config.ts @@ -0,0 +1,16 @@ +import { FileSystemTreeItemPresentationModel } from '@umbraco-cms/backoffice/backend-api'; + +// TODO: temp until we have a proper stylesheet model +export interface PartialViewDetails extends FileSystemTreeItemPresentationModel { + content: string; +} + +export const PARTIAL_VIEW_ENTITY_TYPE = 'partial-view'; +export const PARTIAL_VIEW_FOLDER_ENTITY_TYPE = 'partial-view'; + +export const PARTIAL_VIEW_REPOSITORY_ALIAS = 'Umb.Repository.PartialViews'; + +export const PARTIAL_VIEW_TREE_ALIAS = 'Umb.Tree.PartialViews'; + +export const UMB_PARTIAL_VIEW_TREE_STORE_CONTEXT_TOKEN_ALIAS = 'Umb.Store.PartialViews.Tree'; +export const UMB_PARTIAL_VIEW_STORE_CONTEXT_TOKEN_ALIAS = 'Umb.Store.PartialViews'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/entity-actions/create/create-empty.action.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/entity-actions/create/create-empty.action.ts new file mode 100644 index 0000000000..b00aee6830 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/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'; + +export class UmbCreateEmptyPartialViewAction }> extends UmbEntityActionBase { + constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) { + super(host, repositoryAlias, unique); + } + + async execute() { + throw new Error('Method not implemented.'); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/entity-actions/create/create-from-snippet.action.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/entity-actions/create/create-from-snippet.action.ts new file mode 100644 index 0000000000..9ac777fa21 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/entity-actions/create/create-from-snippet.action.ts @@ -0,0 +1,12 @@ +import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; + +export class UmbCreateFromSnippetPartialViewAction }> extends UmbEntityActionBase { + constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) { + super(host, repositoryAlias, unique); + } + + async execute() { + throw new Error('Method not implemented.'); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/entity-actions/manifests.ts new file mode 100644 index 0000000000..50c2cd5b4a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/entity-actions/manifests.ts @@ -0,0 +1,60 @@ +import { PARTIAL_VIEW_ENTITY_TYPE, PARTIAL_VIEW_FOLDER_ENTITY_TYPE, PARTIAL_VIEW_REPOSITORY_ALIAS } from '../config'; +import { UmbCreateFromSnippetPartialViewAction } from './create/create-from-snippet.action'; +import { UmbCreateEmptyPartialViewAction } from './create/create-empty.action'; +import { UmbDeleteEntityAction } from '@umbraco-cms/backoffice/entity-action'; +import { ManifestEntityAction } from '@umbraco-cms/backoffice/extensions-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 partialViewActions: Array = [ + { + type: 'entityAction', + alias: 'Umb.EntityAction.PartialView.Delete', + name: 'Delete PartialView Entity Action', + meta: { + icon: 'umb:trash', + label: 'Delete', + api: UmbDeleteEntityAction, + repositoryAlias: PARTIAL_VIEW_REPOSITORY_ALIAS, + }, + conditions: { + entityTypes: [PARTIAL_VIEW_ENTITY_TYPE], + }, + }, +]; + +//TODO: add create folder action when the generic folder action is implemented +//Actions for directories +const partialViewFolderActions: Array = [ + { + 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: UmbCreateEmptyPartialViewAction, + repositoryAlias: PARTIAL_VIEW_REPOSITORY_ALIAS, + }, + conditions: { + entityTypes: [PARTIAL_VIEW_FOLDER_ENTITY_TYPE], + }, + }, + { + type: 'entityAction', + alias: 'Umb.EntityAction.PartialViewFolder.Create.From.Snippet', + name: 'Create PartialView Entity From Snippet Under Directory Action', + meta: { + icon: 'umb:article', + label: 'New partial view from snippet...', + api: UmbCreateFromSnippetPartialViewAction, + repositoryAlias: PARTIAL_VIEW_REPOSITORY_ALIAS, + }, + conditions: { + entityTypes: [PARTIAL_VIEW_FOLDER_ENTITY_TYPE], + }, + }, +]; + +export const manifests = [...partialViewActions, ...partialViewFolderActions]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/manifests.ts new file mode 100644 index 0000000000..84c48ea0b7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/manifests.ts @@ -0,0 +1,13 @@ +import { manifests as repositoryManifests } from './repository/manifests'; +import { manifests as menuItemManifests } from './menu-item/manifests'; +import { manifests as treeManifests } from './tree/manifests'; +import { manifests as entityActionsManifests } from './entity-actions/manifests'; +import { manifests as workspaceManifests } from './workspace/manifests'; + +export const manifests = [ + ...repositoryManifests, + ...menuItemManifests, + ...treeManifests, + ...entityActionsManifests, + ...workspaceManifests, +]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/menu-item/manifests.ts new file mode 100644 index 0000000000..093c5a0afc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/menu-item/manifests.ts @@ -0,0 +1,21 @@ +import { PARTIAL_VIEW_TREE_ALIAS } from '../config'; +import type { ManifestTypes } from '@umbraco-cms/backoffice/extensions-registry'; + +const menuItem: ManifestTypes = { + type: 'menuItem', + kind: 'tree', + alias: 'Umb.MenuItem.PartialViews', + name: 'Partial View Menu Item', + weight: 40, + meta: { + label: 'Partial Views', + icon: 'umb:folder', + entityType: 'partial-view', + treeAlias: PARTIAL_VIEW_TREE_ALIAS, + }, + conditions: { + menus: ['Umb.Menu.Templating'], + }, +}; + +export const manifests = [menuItem]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/repository/manifests.ts new file mode 100644 index 0000000000..a67699c745 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/repository/manifests.ts @@ -0,0 +1,32 @@ +import { UmbTemplateRepository } from '../repository/partial-views.repository'; +import { PARTIAL_VIEW_REPOSITORY_ALIAS } from '../config'; +import { UmbPartialViewsTreeStore } from './partial-views.tree.store'; +import { UmbPartialViewsStore } from './partial-views.store'; +import { ManifestRepository, ManifestStore, ManifestTreeStore } from '@umbraco-cms/backoffice/extensions-registry'; + + +const repository: ManifestRepository = { + type: 'repository', + alias: PARTIAL_VIEW_REPOSITORY_ALIAS, + name: 'Partial Views Repository', + class: UmbTemplateRepository, +}; + +export const PARTIAL_VIEW_STORE_ALIAS = 'Umb.Store.PartialViews'; +export const PARTIAL_VIEW_TREE_STORE_ALIAS = 'Umb.Store.PartialViewsTree'; + +const store: ManifestStore = { + type: 'store', + alias: PARTIAL_VIEW_STORE_ALIAS, + name: 'Partial Views Store', + class: UmbPartialViewsStore, +}; + +const treeStore: ManifestTreeStore = { + type: 'treeStore', + alias: PARTIAL_VIEW_TREE_STORE_ALIAS, + name: 'Partial Views Tree Store', + class: UmbPartialViewsTreeStore, +}; + +export const manifests = [repository, store, treeStore]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/repository/partial-views.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/repository/partial-views.repository.ts new file mode 100644 index 0000000000..8928ccafcb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/repository/partial-views.repository.ts @@ -0,0 +1,153 @@ +import { UmbPartialViewDetailServerDataSource } from './sources/partial-views.detail.server.data'; +import { UmbPartialViewsTreeServerDataSource } from './sources/partial-views.tree.server.data'; +import { UmbPartialViewsStore, UMB_PARTIAL_VIEWS_STORE_CONTEXT_TOKEN } from './partial-views.store'; +import { UmbPartialViewsTreeStore, UMB_PARTIAL_VIEW_TREE_STORE_CONTEXT_TOKEN } from './partial-views.tree.store'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; +import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification'; +import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; +import { ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api'; +import { UmbDetailRepository, UmbTreeRepository } from '@umbraco-cms/backoffice/repository'; +import { UmbTreeRootEntityModel } from '@umbraco-cms/backoffice/tree'; +import { Observable } from 'rxjs'; + +export class UmbTemplateRepository implements UmbTreeRepository, UmbDetailRepository { + #init; + #host: UmbControllerHostElement; + + #treeDataSource: UmbPartialViewsTreeServerDataSource; + #detailDataSource: UmbPartialViewDetailServerDataSource; + + #treeStore?: UmbPartialViewsTreeStore; + #store?: UmbPartialViewsStore; + + #notificationContext?: UmbNotificationContext; + + constructor(host: UmbControllerHostElement) { + this.#host = host; + + this.#treeDataSource = new UmbPartialViewsTreeServerDataSource(this.#host); + this.#detailDataSource = new UmbPartialViewDetailServerDataSource(this.#host); + + this.#init = Promise.all([ + new UmbContextConsumerController(this.#host, UMB_PARTIAL_VIEW_TREE_STORE_CONTEXT_TOKEN, (instance) => { + this.#treeStore = instance; + }), + + new UmbContextConsumerController(this.#host, UMB_PARTIAL_VIEWS_STORE_CONTEXT_TOKEN, (instance) => { + this.#store = instance; + }), + + new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => { + this.#notificationContext = instance; + }), + ]); + } + + requestTreeRoot(): Promise<{ data?: UmbTreeRootEntityModel | undefined; error?: ProblemDetailsModel | undefined }> { + throw new Error('Method not implemented.'); + } + + requestItemsLegacy?: + | (( + uniques: string[] + ) => Promise<{ + data?: any[] | undefined; + error?: ProblemDetailsModel | undefined; + asObservable?: (() => Observable) | undefined; + }>) + | undefined; + + itemsLegacy?: ((uniques: string[]) => Promise>) | undefined; + + byId(id: string): Promise> { + throw new Error('Method not implemented.'); + } + + requestById(id: string): Promise<{ data?: any; error?: ProblemDetailsModel | undefined }> { + throw new Error('Method not implemented.'); + } + + // TREE: + + 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) throw new Error('Cannot request tree item with missing path'); + + await this.#init; + + const { data, error } = await this.#treeDataSource.getChildrenOf({ path }); + + 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: ProblemDetailsModel = { 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); + } + + // 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 }; + } + + // DETAILS: + + async createScaffold(parentKey: string | null) { + return Promise.reject(new Error('Not implemented')); + } + + async create(patrial: any) { + return Promise.reject(new Error('Not implemented')); + } + + async save(patrial: any) { + return Promise.reject(new Error('Not implemented')); + } + + async delete(key: string) { + return Promise.reject(new Error('Not implemented')); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/repository/partial-views.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/repository/partial-views.store.ts new file mode 100644 index 0000000000..56c444e0d7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/repository/partial-views.store.ts @@ -0,0 +1,49 @@ +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'; +import { UMB_PARTIAL_VIEW_STORE_CONTEXT_TOKEN_ALIAS } from '../config'; + +/** + * @export + * @class UmbPartialViewsStore + * @extends {UmbStoreBase} + * @description - Data Store for partial views + */ +export class UmbPartialViewsStore extends UmbStoreBase { + /** + * Creates an instance of UmbPartialViewsStore. + * @param {UmbControllerHostInterface} host + * @memberof UmbPartialViewsStore + */ + constructor(host: UmbControllerHostElement) { + super( + host, + UMB_PARTIAL_VIEWS_STORE_CONTEXT_TOKEN.toString(), + new UmbArrayState([], (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_PARTIAL_VIEWS_STORE_CONTEXT_TOKEN = new UmbContextToken( + UMB_PARTIAL_VIEW_STORE_CONTEXT_TOKEN_ALIAS +); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/repository/partial-views.tree.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/repository/partial-views.tree.store.ts new file mode 100644 index 0000000000..46979f5686 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/repository/partial-views.tree.store.ts @@ -0,0 +1,26 @@ +import { UMB_PARTIAL_VIEW_TREE_STORE_CONTEXT_TOKEN_ALIAS } from '../config'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; +import { UmbFileSystemTreeStore } from '@umbraco-cms/backoffice/store'; +import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; + +export const UMB_PARTIAL_VIEW_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken( + UMB_PARTIAL_VIEW_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_PARTIAL_VIEW_TREE_STORE_CONTEXT_TOKEN.toString()); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/repository/sources/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/repository/sources/index.ts new file mode 100644 index 0000000000..2a8d8c1290 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/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 PartialViewsTreeDataSource { + getRootItems(): Promise>; + getChildrenOf({ + path, + skip, + take, + }: { + path?: string | undefined; + skip?: number | undefined; + take?: number | undefined; + }): Promise>; + getItem(ids: Array): Promise>; +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/repository/sources/partial-views.detail.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/repository/sources/partial-views.detail.server.data.ts new file mode 100644 index 0000000000..86a65028de --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/repository/sources/partial-views.detail.server.data.ts @@ -0,0 +1,45 @@ +import { PartialViewDetails } from '../../config'; +import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; +import { DataSourceResponse, UmbDataSource } from '@umbraco-cms/backoffice/repository'; + +//TODO Pass proper models +export class UmbPartialViewDetailServerDataSource + implements UmbDataSource +{ + #host: UmbControllerHostElement; + + /** + * Creates an instance of UmbPartialViewDetailServerDataSource. + * @param {UmbControllerHostInterface} host + * @memberof UmbPartialViewDetailServerDataSource + */ + constructor(host: UmbControllerHostElement) { + this.#host = host; + } + + createScaffold(parentKey: string | null): Promise> { + throw new Error('Method not implemented.'); + } + + /** + * Fetches a Stylesheet with the given path from the server + * @param {string} path + * @return {*} + * @memberof UmbStylesheetServerDataSource + */ + async get(path: string) { + if (!path) throw new Error('Path is missing'); + console.log('GET PATRIAL WITH PATH', path); + return { data: undefined, error: undefined }; + } + + insert(data: any): Promise> { + throw new Error('Method not implemented.'); + } + update(unique: string, data: PartialViewDetails): Promise> { + throw new Error('Method not implemented.'); + } + delete(unique: string): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/repository/sources/partial-views.tree.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/repository/sources/partial-views.tree.server.data.ts new file mode 100644 index 0000000000..8ef71f1017 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/repository/sources/partial-views.tree.server.data.ts @@ -0,0 +1,54 @@ +import { PartialViewsTreeDataSource } from '.'; +import { PartialViewResource, ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; + +export class UmbPartialViewsTreeServerDataSource implements PartialViewsTreeDataSource { + #host: UmbControllerHostElement; + + constructor(host: UmbControllerHostElement) { + this.#host = host; + } + + async getRootItems() { + return tryExecuteAndNotify(this.#host, PartialViewResource.getTreePartialViewRoot({})); + } + + async getChildrenOf({ + path, + skip, + take, + }: { + path?: string | undefined; + skip?: number | undefined; + take?: number | undefined; + }) { + if (!path) { + const error: ProblemDetailsModel = { title: 'Path is missing' }; + return error ; + } + + return tryExecuteAndNotify( + this.#host, + PartialViewResource.getTreePartialViewChildren({ + path, + skip, + take, + }) + ); + } + + async getItem(id: Array) { + if (!id) { + const error: ProblemDetailsModel = { title: 'Paths are missing' }; + return error ; + } + + return tryExecuteAndNotify( + this.#host, + PartialViewResource.getPartialViewItem({ + id, + }) + ); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/tree/manifests.ts new file mode 100644 index 0000000000..bad84fac40 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/tree/manifests.ts @@ -0,0 +1,23 @@ +import { PARTIAL_VIEW_ENTITY_TYPE, PARTIAL_VIEW_REPOSITORY_ALIAS, PARTIAL_VIEW_TREE_ALIAS } from '../config'; +import type { ManifestTree, ManifestTreeItem } from '@umbraco-cms/backoffice/extensions-registry'; + +const tree: ManifestTree = { + type: 'tree', + alias: PARTIAL_VIEW_TREE_ALIAS, + name: 'Partial Views Tree', + meta: { + repositoryAlias: PARTIAL_VIEW_REPOSITORY_ALIAS, + }, +}; + +const treeItem: ManifestTreeItem = { + type: 'treeItem', + kind: 'fileSystem', + alias: 'Umb.TreeItem.PartialViews', + name: 'Partial Views Tree Item', + conditions: { + entityTypes: [PARTIAL_VIEW_ENTITY_TYPE], + }, +}; + +export const manifests = [tree, treeItem]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/workspace/manifests.ts new file mode 100644 index 0000000000..ea7c35fe95 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/workspace/manifests.ts @@ -0,0 +1,32 @@ +import { UmbSaveWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; +import type { ManifestWorkspace, ManifestWorkspaceAction } from '@umbraco-cms/backoffice/extensions-registry'; + +const workspace: ManifestWorkspace = { + type: 'workspace', + alias: 'Umb.Workspace.PartialView', + name: 'Partial View Workspace', + loader: () => import('./partial-views-workspace.element'), + meta: { + entityType: 'partial-view', + }, +}; + +const workspaceActions: Array = [ + { + type: 'workspaceAction', + alias: 'Umb.WorkspaceAction.PartialView.Save', + name: 'Save Partial View', + weight: 70, + meta: { + look: 'primary', + color: 'positive', + label: 'Save', + api: UmbSaveWorkspaceAction, + }, + conditions: { + workspaces: ['Umb.Workspace.PartialView'], + }, + }, +]; + +export const manifests = [workspace, ...workspaceActions]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/workspace/partial-views-workspace-edit.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/workspace/partial-views-workspace-edit.element.ts new file mode 100644 index 0000000000..8f7c0a3c88 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/workspace/partial-views-workspace-edit.element.ts @@ -0,0 +1,114 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { css, html } from 'lit'; +import { customElement, query, state } from 'lit/decorators.js'; +import { UUIInputElement } from '@umbraco-ui/uui'; +import { UmbCodeEditorElement } from '../../../core/components/code-editor'; +import { UmbPartialViewsWorkspaceContext } from './partial-views-workspace.context'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; + +@customElement('umb-partial-views-workspace-edit') +export class UmbPartialViewsWorkspaceEditElement extends UmbLitElement { + @state() + private _name?: string | null = ''; + + @state() + private _content?: string | null = ''; + + @query('umb-code-editor') + private _codeEditor?: UmbCodeEditorElement; + + #partialViewsWorkspaceContext?: UmbPartialViewsWorkspaceContext; + #isNew = false; + + constructor() { + super(); + + this.consumeContext('umbWorkspaceContext', (workspaceContext: UmbPartialViewsWorkspaceContext) => { + this.#partialViewsWorkspaceContext = workspaceContext; + this.observe(this.#partialViewsWorkspaceContext.name, (name) => { + this._name = name; + }); + + this.observe(this.#partialViewsWorkspaceContext.content, (content) => { + this._content = content; + }); + + // this.observe(this.#partialViewsWorkspaceContext.isNew, (isNew) => { + // this.#isNew = !!isNew; + // console.log(this.#isNew); + // }); + }); + } + + // TODO: temp code for testing create and save + #onNameInput(event: Event) { + const target = event.target as UUIInputElement; + const value = target.value as string; + this.#partialViewsWorkspaceContext?.setName(value); + } + + //TODO - debounce that + #onCodeEditorInput(event: Event) { + const target = event.target as UmbCodeEditorElement; + const value = target.code as string; + this.#partialViewsWorkspaceContext?.setContent(value); + } + + #insertCode(event: Event) { + const target = event.target as UUIInputElement; + const value = target.value as string; + + this._codeEditor?.insert(`My hovercraft is full of eels`); + } + + render() { + // TODO: add correct UI elements + return html` + + + Insert "My hovercraft is full of eels" + + + + `; + } + + static styles = [ + UUITextStyles, + css` + :host { + display: block; + width: 100%; + height: 100%; + } + + umb-code-editor { + --editor-height: calc(100vh - 300px); + } + + uui-box { + margin: 1em; + --uui-box-default-padding: 0; + } + + uui-input { + width: 100%; + margin: 1em; + } + `, + ]; +} + +export default UmbPartialViewsWorkspaceEditElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-partial-views-workspace-edit': UmbPartialViewsWorkspaceEditElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/workspace/partial-views-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/workspace/partial-views-workspace.context.ts new file mode 100644 index 0000000000..f88f6ea390 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/workspace/partial-views-workspace.context.ts @@ -0,0 +1,55 @@ +import { UmbTemplateRepository } from '../repository/partial-views.repository'; +import { createObservablePart, UmbDeepState } from '@umbraco-cms/backoffice/observable-api'; +import { TemplateResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; +import { UmbWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; + +export class UmbPartialViewsWorkspaceContext extends UmbWorkspaceContext { + getEntityId(): string | undefined { + throw new Error('Method not implemented.'); + } + getEntityType(): string { + throw new Error('Method not implemented.'); + } + save(): Promise { + throw new Error('Method not implemented.'); + } + destroy(): void { + throw new Error('Method not implemented.'); + } + #data = new UmbDeepState(undefined); + data = this.#data.asObservable(); + name = createObservablePart(this.#data, (data) => data?.name); + content = createObservablePart(this.#data, (data) => data?.content); + + constructor(host: UmbControllerHostElement) { + super(host, new UmbTemplateRepository(host)); + } + + getData() { + return this.#data.getValue(); + } + + setName(value: string) { + this.#data.next({ ...this.#data.value, $type: this.#data.value?.$type || '', name: value }); + } + + setContent(value: string) { + this.#data.next({ ...this.#data.value, $type: this.#data.value?.$type || '', content: value }); + } + + async load(entityKey: string) { + const { data } = await this.repository.requestByKey(entityKey); + if (data) { + this.setIsNew(false); + this.#data.next(data); + } + } + + async createScaffold(parentKey: string | null) { + const { data } = await this.repository.createScaffold(parentKey); + if (!data) return; + this.setIsNew(true); + this.#data.next(data); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/workspace/partial-views-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/workspace/partial-views-workspace.element.ts new file mode 100644 index 0000000000..f314361b77 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/partial-views/workspace/partial-views-workspace.element.ts @@ -0,0 +1,57 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { UmbPartialViewsWorkspaceContext } from './partial-views-workspace.context'; +import { UmbRouterSlotInitEvent } from '@umbraco-cms/internal/router'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; + +import './partial-views-workspace-edit.element'; +import { UmbRoute, IRoutingInfo, PageComponent } from '@umbraco-cms/backoffice/router'; + +@customElement('umb-partial-views-workspace') +export class UmbPartialViewsWorkspaceElement extends UmbLitElement { + #partialViewsWorkspaceContext = new UmbPartialViewsWorkspaceContext(this); + + #routerPath? = ''; + + #element = document.createElement('umb-partial-views-workspace-edit'); + #key = ''; + + @state() + _routes: UmbRoute[] = [ + { + path: 'create/:parentKey', + component: () => this.#element, + setup: async (component: PageComponent, info: IRoutingInfo) => { + const parentKey = info.match.params.parentKey; + this.#partialViewsWorkspaceContext.createScaffold(parentKey); + }, + }, + { + path: 'edit/:key', + component: () => this.#element, + setup: (component: PageComponent, info: IRoutingInfo) => { + const key = info.match.params.key; + this.#partialViewsWorkspaceContext.load(key); + }, + }, + ]; + + render() { + return html` { + this.#routerPath = event.target.absoluteRouterPath; + }}>`; + } + + static styles = [UUITextStyles, css``]; +} + +export default UmbPartialViewsWorkspaceElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-partial-views-workspace': UmbPartialViewsWorkspaceElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/entity-actions/create/create.action.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/entity-actions/create/create.action.ts index 97067b0fc3..5ac06abfe4 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/entity-actions/create/create.action.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/entity-actions/create/create.action.ts @@ -9,7 +9,7 @@ export class UmbCreateEntityAction }> extends // TODO: can we make this a generic create action async execute() { // TODO: get entity type from repository? - const url = `section/settings/template/create/${this.unique || 'root'}`; + const url = `section/settings/workspace/template/create/${this.unique || 'root'}`; // TODO: how do we handle this with a href? history.pushState(null, '', url); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/manifests.ts index 4006b83b2e..a4b34186fb 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/manifests.ts @@ -1,4 +1,3 @@ -import { TEMPLATE_REPOSITORY_ALIAS } from '../repository/manifests'; import { UmbSaveWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; import type { ManifestWorkspace, diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/template-workspace-edit.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/template-workspace-edit.element.ts new file mode 100644 index 0000000000..12a410a67f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/template-workspace-edit.element.ts @@ -0,0 +1,178 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { css, html } from 'lit'; +import { customElement, query, state } from 'lit/decorators.js'; +import { UUIInputElement } from '@umbraco-ui/uui'; +import { UmbTemplatingInsertMenuElement } from '../../components/insert-menu/templating-insert-menu.element'; +import { UMB_MODAL_TEMPLATING_INSERT_SECTION_MODAL } from '../../modals/insert-section-modal/insert-section-modal.element'; +import { UmbTemplateWorkspaceContext } from './template-workspace.context'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import { UMB_MODAL_CONTEXT_TOKEN, UmbModalContext } from '@umbraco-cms/backoffice/modal'; +import { UmbCodeEditorElement } from '@umbraco-cms/backoffice/core/components'; + +@customElement('umb-template-workspace-edit') +export class UmbTemplateWorkspaceEditElement extends UmbLitElement { + @state() + private _name?: string | null = ''; + + @state() + private _content?: string | null = ''; + + @query('umb-code-editor') + private _codeEditor?: UmbCodeEditorElement; + + #templateWorkspaceContext?: UmbTemplateWorkspaceContext; + #isNew = false; + + constructor() { + super(); + + this.consumeContext(UMB_MODAL_CONTEXT_TOKEN, (instance) => { + this._modalContext = instance; + }); + + this.consumeContext('UmbEntityWorkspaceContext', (workspaceContext: UmbTemplateWorkspaceContext) => { + this.#templateWorkspaceContext = workspaceContext; + this.observe(this.#templateWorkspaceContext.name, (name) => { + this._name = name; + }); + + this.observe(this.#templateWorkspaceContext.content, (content) => { + this._content = content; + }); + + this.observe(this.#templateWorkspaceContext.isNew, (isNew) => { + this.#isNew = !!isNew; + }); + }); + } + + // TODO: temp code for testing create and save + #onNameInput(event: Event) { + const target = event.target as UUIInputElement; + const value = target.value as string; + this.#templateWorkspaceContext?.setName(value); + } + + //TODO - debounce that + #onCodeEditorInput(event: Event) { + const target = event.target as UmbCodeEditorElement; + const value = target.code as string; + this.#templateWorkspaceContext?.setContent(value); + } + + #insertCode(event: Event) { + const target = event.target as UmbTemplatingInsertMenuElement; + const value = target.value as string; + + this._codeEditor?.insert(value); + } + + private _modalContext?: UmbModalContext; + + #openInsertSectionModal() { + const sectionModal = this._modalContext?.open(UMB_MODAL_TEMPLATING_INSERT_SECTION_MODAL); + sectionModal?.onSubmit().then((insertSectionModalResult) => { + console.log(insertSectionModalResult); + }); + } + + render() { + // TODO: add correct UI elements + return html` + + +
+ + Master template: something + + + + + Query builder + + + + Sections + +
+ + +
+
`; + } + + static styles = [ + UUITextStyles, + css` + :host { + display: block; + width: 100%; + height: 100%; + } + + umb-code-editor { + --editor-height: calc(100vh - 300px); + } + + uui-box { + margin: 1em; + --uui-box-default-padding: 0; + } + + uui-input { + width: 100%; + margin: 1em; + } + + #code-editor-menu-container uui-icon { + margin-right: var(--uui-size-space-3); + } + + #insert-menu { + margin: 0; + padding: 0; + margin-top: var(--uui-size-space-3); + background-color: var(--uui-color-surface); + box-shadow: var(--uui-shadow-depth-3); + min-width: calc(100% + var(--uui-size-8, 24px)); + } + + #insert-menu > li, + ul { + padding: 0; + width: 100%; + list-style: none; + } + + .insert-menu-item { + width: 100%; + } + + #code-editor-menu-container { + display: flex; + justify-content: flex-end; + gap: var(--uui-size-space-3); + } + `, + ]; +} + +export default UmbTemplateWorkspaceEditElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-template-workspace-edit': UmbTemplateWorkspaceEditElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/template-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/template-workspace.element.ts index 7fe89f7f32..4c6afaa6a8 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/template-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/templates/workspace/template-workspace.element.ts @@ -1,83 +1,58 @@ -import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { UUITextStyles } from '@umbraco-ui/uui-css'; import { css, html } from 'lit'; -import { customElement, query, state } from 'lit/decorators.js'; -import { UUIInputElement } from '@umbraco-ui/uui'; -import type { UmbCodeEditorElement } from '../../../core/components/code-editor/code-editor.element'; +import { customElement, state } from 'lit/decorators.js'; import { UmbTemplateWorkspaceContext } from './template-workspace.context'; +import { UmbRouterSlotInitEvent } from '@umbraco-cms/internal/router'; +import type { IRoutingInfo, PageComponent, UmbRoute } from '@umbraco-cms/backoffice/router'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import './template-workspace-edit.element'; + @customElement('umb-template-workspace') export class UmbTemplateWorkspaceElement extends UmbLitElement { public load(entityId: string) { this.#templateWorkspaceContext.load(entityId); } - public create(parentId: string | null) { - this.#isNew = true; - this.#templateWorkspaceContext.createScaffold(parentId); - } - @state() private _name?: string | null = ''; @state() private _content?: string | null = ''; - @query('umb-code-editor') - private _codeEditor?: UmbCodeEditorElement; - #templateWorkspaceContext = new UmbTemplateWorkspaceContext(this); - #isNew = false; - async connectedCallback() { - super.connectedCallback(); + #routerPath? = ''; - this.observe(this.#templateWorkspaceContext.name, (name) => { - this._name = name; - }); + #element = document.createElement('umb-template-workspace-edit'); + #key = ''; - this.observe(this.#templateWorkspaceContext.content, (content) => { - this._content = content; - }); - } - - // TODO: temp code for testing create and save - #onNameInput(event: Event) { - const target = event.target as UUIInputElement; - const value = target.value as string; - this.#templateWorkspaceContext.setName(value); - } - - //TODO - debounce that - #onCodeEditorInput(event: Event) { - const target = event.target as UmbCodeEditorElement; - const value = target.code as string; - this.#templateWorkspaceContext.setContent(value); - } - - #insertCode(event: Event) { - const target = event.target as UUIInputElement; - const value = target.value as string; - - this._codeEditor?.insert(`My hovercraft is full of eels`); - } + @state() + _routes: UmbRoute[] = [ + { + path: 'create/:parentKey', + component: () => this.#element, + setup: (component: PageComponent, info: IRoutingInfo) => { + const parentKey = info.match.params.parentKey; + this.#templateWorkspaceContext.createScaffold(parentKey); + }, + }, + { + path: 'edit/:key', + component: () => this.#element, + setup: (component: PageComponent, info: IRoutingInfo): void => { + const key = info.match.params.key; + this.#templateWorkspaceContext.load(key); + }, + }, + ]; render() { - // TODO: add correct UI elements - return html` - - - Insert "My hovercraft is full of eels" - - - - `; + return html` { + this.#routerPath = event.target.absoluteRouterPath; + }}>`; } static styles = [ diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/templating/utils.ts b/src/Umbraco.Web.UI.Client/src/backoffice/templating/utils.ts index 80214bb7fd..afb8b2c552 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/templating/utils.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/templating/utils.ts @@ -3,3 +3,56 @@ export const urlFriendlyPathFromServerFilePath = (path: string) => encodeURIComp // TODO: we can try and make pretty urls if we want to export const serverFilePathFromUrlFriendlyPath = (unique: string) => decodeURIComponent(unique.replace('-', '.')); + +//Below are a copy of +export const getInsertDictionarySnippet = (nodeName: string) => { + return `@Umbraco.GetDictionaryValue("${nodeName}")`; +} + +export const getInsertPartialSnippet = (nodeName: string) => + `@await Html.PartialAsync("${nodeName.replace('.cshtml', '')}")`; + +export const getQuerySnippet = (queryExpression: string) => { + let code = '\n@{\n' + '\tvar selection = ' + queryExpression + ';\n}\n'; + code += + '
    \n' + + '\t@foreach (var item in selection)\n' + + '\t{\n' + + '\t\t
  • \n' + + '\t\t\t@item.Name()\n' + + '\t\t
  • \n' + + '\t}\n' + + '
\n\n'; + return code; +}; + +export const getRenderBodySnippet = () => '@RenderBody()'; + +export const getRenderSectionSnippet = (sectionName: string, isMandatory: boolean) => + `@RenderSection("${sectionName}", ${isMandatory})`; + +export const getAddSectionSnippet = (sectionName: string) => `@section ${sectionName} +{ + + + +}`; + +export const getUmbracoFieldSnippet = (field: string, defaultValue: string | null = null, recursive = false) => { + let fallback = null; + + if (recursive !== false && defaultValue !== null) { + fallback = 'Fallback.To(Fallback.Ancestors, Fallback.DefaultValue)'; + } else if (recursive !== false) { + fallback = 'Fallback.ToAncestors'; + } else if (defaultValue !== null) { + fallback = 'Fallback.ToDefaultValue'; + } + + const value = `${field !== null ? `@Model.Value("${field}"` : ''}${ + fallback !== null ? `, fallback: ${fallback}` : '' + }${defaultValue !== null ? `, defaultValue: new HtmlString("${defaultValue}")` : ''}${field ? ')' : ''}`; + + return value; +}; + diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/index.ts index d6912d1aee..485f2311cd 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/index.ts @@ -1,8 +1,10 @@ import { manifests as translationSectionManifests } from './section.manifest'; import { manifests as dictionaryManifests } from './dictionary/manifests'; +import { manifests as modalManifests } from './modals/manifests'; + import { UmbEntrypointOnInit } from '@umbraco-cms/backoffice/extensions-api'; -export const manifests = [...translationSectionManifests, ...dictionaryManifests]; +export const manifests = [...modalManifests, ...translationSectionManifests, ...dictionaryManifests]; export const onInit: UmbEntrypointOnInit = (_host, extensionRegistry) => { extensionRegistry.registerMany(manifests); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/modals/dictionary-item-picker/dictionary-item-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/modals/dictionary-item-picker/dictionary-item-picker-modal.element.ts new file mode 100644 index 0000000000..f79cec25cf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/modals/dictionary-item-picker/dictionary-item-picker-modal.element.ts @@ -0,0 +1,89 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { UmbTreeElement } from '../../../core/components/tree/tree.element'; +import { UmbModalBaseElement } from '@umbraco-cms/internal/modal'; +import { UmbDictionaryItemPickerModalData, UmbDictionaryItemPickerModalResult } from '@umbraco-cms/backoffice/modal'; + +@customElement('umb-dictionary-item-picker-modal') +export default class UmbDictionaryItemPickerModalElement extends UmbModalBaseElement< + UmbDictionaryItemPickerModalData, + UmbDictionaryItemPickerModalResult +> { + @state() + _selection: Array = []; + + @state() + _multiple = false; + + connectedCallback() { + super.connectedCallback(); + this._selection = this.data?.selection ?? []; + this._multiple = this.data?.multiple ?? true; + } + + private _handleSelectionChange(e: CustomEvent) { + e.stopPropagation(); + const element = e.target as UmbTreeElement; + this._selection = this._multiple ? element.selection : [element.selection[element.selection.length - 1]]; + this._submit(); + } + + private _submit() { + this.modalHandler?.submit({ selection: this._selection }); + } + + private _close() { + this.modalHandler?.reject(); + } + + render() { + return html` + +
+ + + +
+
+ Close +
+
+ `; + } + + static styles = [ + UUITextStyles, + css` + :host { + display: block; + color: var(--uui-color-text); + } + + #main { + box-sizing: border-box; + padding: var(--uui-size-space-5); + height: calc(100vh - 124px); + } + + #main uui-button { + width: 100%; + } + + h3, + p { + text-align: left; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-dictionary-item-picker-modal': UmbDictionaryItemPickerModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/modals/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/modals/manifests.ts new file mode 100644 index 0000000000..e764a4f350 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/modals/manifests.ts @@ -0,0 +1,13 @@ +import { ManifestModal } from '@umbraco-cms/backoffice/extensions-registry'; +import { UMB_DICTIONARY_ITEM_PICKER_MODAL_ALIAS } from '@umbraco-cms/backoffice/modal'; + +const modals: Array = [ + { + type: 'modal', + alias: UMB_DICTIONARY_ITEM_PICKER_MODAL_ALIAS, + name: 'Dictionary Item Picker Modal', + loader: () => import('./dictionary-item-picker/dictionary-item-picker-modal.element'), + }, +]; + +export const manifests = [...modals]; diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/browser-handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/browser-handlers.ts index 2e709760e0..e6348ff511 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/browser-handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/browser-handlers.ts @@ -29,6 +29,7 @@ import { handlers as logViewerHandlers } from './domains/log-viewer.handlers'; import { handlers as packageHandlers } from './domains/package.handlers'; import { handlers as rteEmbedHandlers } from './domains/rte-embed.handlers'; import { handlers as stylesheetHandlers } from './domains/stylesheet.handlers'; +import { handlers as partialViewsHandlers } from './domains/partial-views.handlers'; import { handlers as tagHandlers } from './domains/tag-handlers'; const handlers = [ @@ -62,6 +63,7 @@ const handlers = [ ...packageHandlers, ...rteEmbedHandlers, ...stylesheetHandlers, + ...partialViewsHandlers, ...tagHandlers, ]; diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/partial-views.data.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/partial-views.data.ts new file mode 100644 index 0000000000..0086e48c8a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/partial-views.data.ts @@ -0,0 +1,104 @@ +import { UmbEntityData } from './entity.data'; +import { createFileSystemTreeItem } from './utils'; +import { + FileSystemTreeItemPresentationModel, + PagedFileSystemTreeItemPresentationModel, +} from '@umbraco-cms/backoffice/backend-api'; + +export const data: Array = [ + { + path: 'blockgrid', + isFolder: true, + name: 'blockgrid', + type: 'partial-view', + icon: 'umb:folder', + hasChildren: true, + }, + { + path: 'blocklist', + isFolder: true, + name: 'blocklist', + type: 'partial-view', + icon: 'umb:folder', + hasChildren: true, + }, + { + path: 'grid', + isFolder: true, + name: 'grid', + type: 'partial-view', + icon: 'umb:folder', + hasChildren: true, + }, + { + path: 'blockgrid/area.cshtml', + isFolder: false, + name: 'area.cshtml', + type: 'partial-view', + icon: 'umb:article', + hasChildren: false, + }, + { + path: 'blockgrid/items.cshtml', + isFolder: false, + name: 'items.cshtml', + type: 'partial-view', + icon: 'umb:article', + hasChildren: false, + }, + { + path: 'blocklist/default.cshtml', + isFolder: false, + name: 'default.cshtml', + type: 'partial-view', + icon: 'umb:article', + hasChildren: false, + }, + { + path: 'grid/editors', + isFolder: false, + name: 'editors', + type: 'partial-view', + icon: 'umb:folder', + hasChildren: false, + }, + { + path: 'grid/default.cshtml', + isFolder: false, + name: 'items.cshtml', + type: 'partial-view', + icon: 'umb:article', + hasChildren: false, + }, +]; + +// Temp mocked database +// 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 UmbPartialViewsData extends UmbEntityData { + 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)); + } +} + +export const umbPartialViewsData = new UmbPartialViewsData(); diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/template.data.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/template.data.ts index 99fbbf8c20..7509e0b331 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/data/template.data.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/template.data.ts @@ -29,7 +29,7 @@ export const data: Array = [ parentId: null, name: 'Doc 1', type: 'template', - icon: 'icon-layout', + icon: 'umb:layout', hasChildren: false, alias: 'Doc1', content: `@using Umbraco.Extensions @@ -53,7 +53,7 @@ export const data: Array = [ parentId: null, name: 'Test', type: 'template', - icon: 'icon-layout', + icon: 'umb:layout', hasChildren: true, alias: 'Test', content: @@ -66,7 +66,7 @@ export const data: Array = [ parentId: '9a84c0b3-03b4-4dd4-84ac-706740ac0f71', name: 'Child', type: 'template', - icon: 'icon-layout', + icon: 'umb:layout', hasChildren: false, alias: 'Test', content: diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/partial-views.handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/partial-views.handlers.ts new file mode 100644 index 0000000000..9f8fc8d4cc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/partial-views.handlers.ts @@ -0,0 +1,26 @@ +import { rest } from 'msw'; +import { umbPartialViewsData } from '../data/partial-views.data'; +import { umbracoPath } from '@umbraco-cms/backoffice/utils'; + +export const handlers = [ + rest.get(umbracoPath('/tree/partial-view/root'), (req, res, ctx) => { + const response = umbPartialViewsData.getTreeRoot(); + return res(ctx.status(200), ctx.json(response)); + }), + + rest.get(umbracoPath('/tree/partial-view/children'), (req, res, ctx) => { + const path = req.url.searchParams.get('path'); + if (!path) return; + + const response = umbPartialViewsData.getTreeItemChildren(path); + return res(ctx.status(200), ctx.json(response)); + }), + + rest.get(umbracoPath('/tree/partial-view/item'), (req, res, ctx) => { + const paths = req.url.searchParams.getAll('paths'); + if (!paths) return; + + const items = umbPartialViewsData.getTreeItem(paths); + return res(ctx.status(200), ctx.json(items)); + }), +]; diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/modal-element.element.ts b/src/Umbraco.Web.UI.Client/src/core/modal/modal-element.element.ts index b9052d6d56..5b6d8b6438 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/modal-element.element.ts +++ b/src/Umbraco.Web.UI.Client/src/core/modal/modal-element.element.ts @@ -1,15 +1,22 @@ import { property } from 'lit/decorators.js'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import { UmbModalHandler } from '@umbraco-cms/backoffice/modal'; -import type { UmbModalExtensionElement } from '@umbraco-cms/backoffice/extensions-registry'; +import type { ManifestModal, UmbModalExtensionElement } from '@umbraco-cms/backoffice/extensions-registry'; -export abstract class UmbModalBaseElement +export abstract class UmbModalBaseElement< + ModalDataType extends object = object, + ModalResultType = unknown, + ModalManifestType extends ManifestModal = ManifestModal + > extends UmbLitElement - implements UmbModalExtensionElement + implements UmbModalExtensionElement { + @property({ type: Array, attribute: false }) + public manifest?: ModalManifestType; + @property({ attribute: false }) - modalHandler?: UmbModalHandler; + public modalHandler?: UmbModalHandler; @property({ type: Object, attribute: false }) - data?: UmbModalData; + public data?: ModalDataType; }