Merge branch 'main' into poc/package-modules-v2

This commit is contained in:
Mads Rasmussen
2023-05-11 21:12:18 +02:00
81 changed files with 2427 additions and 695 deletions

View File

@@ -70,7 +70,7 @@ export class UmbExtensionRegistry {
this._extensions.next([...extensionsValues, manifest as ManifestTypes]);
}
registerMany(manifests: Array<ManifestTypes>): void {
registerMany(manifests: Array<ManifestTypes | ManifestKind>): void {
manifests.forEach((manifest) => this.register(manifest));
}

View File

@@ -1,7 +1,13 @@
import type { ManifestModal } from '../models';
import type { UmbModalHandler } from '@umbraco-cms/backoffice/modal';
export interface UmbModalExtensionElement<UmbModalData extends object = object, UmbModalResult = unknown>
extends HTMLElement {
export interface UmbModalExtensionElement<
UmbModalData extends object = object,
UmbModalResult = unknown,
ModalManifestType extends ManifestModal = ManifestModal
> extends HTMLElement {
manifest?: ModalManifestType;
modalHandler?: UmbModalHandler<UmbModalData, UmbModalResult>;
data?: UmbModalData;

View File

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

View File

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

View File

@@ -72,6 +72,9 @@ export class UmbModalHandlerClass<ModalData extends object = object, ModalResult
this.type = config?.type || this.type;
this.size = config?.size || this.size;
const defaultData = modalAlias instanceof UmbModalToken ? modalAlias.getDefaultData() : undefined;
const combinedData = { ...defaultData, ...data } as ModalData;
// TODO: Consider if its right to use Promises, or use another event based system? Would we need to be able to cancel an event, to then prevent the closing..?
this._submitPromise = new Promise((resolve, reject) => {
this._submitResolver = resolve;
@@ -79,7 +82,7 @@ export class UmbModalHandlerClass<ModalData extends object = object, ModalResult
});
this.modalElement = this.#createContainerElement();
this.#observeModal(modalAlias.toString(), data);
this.#observeModal(modalAlias.toString(), combinedData);
}
#createContainerElement() {
@@ -109,6 +112,7 @@ export class UmbModalHandlerClass<ModalData extends object = object, ModalResult
innerElement.data = data;
//innerElement.observable = this.#dataObservable;
innerElement.modalHandler = this;
innerElement.manifest = manifest;
}
return innerElement;

View File

@@ -1,10 +1,13 @@
export interface UmbPickerModalData<T> {
export interface UmbPickerModalData<ItemType> {
multiple?: boolean;
selection?: Array<string | null>;
filter?: (item: T) => boolean;
pickableFilter?: (item: T) => boolean;
filter?: (item: ItemType) => boolean;
pickableFilter?: (item: ItemType) => boolean;
}
export interface UmbPickerModalResult {
selection: Array<string | null>;
}
export interface UmbTreePickerModalData<TreeItemType> extends UmbPickerModalData<TreeItemType> {
treeAlias?: string;
}

View File

@@ -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<FolderTreeItemResponseModel>;
export type UmbDataTypePickerModalData = UmbTreePickerModalData<FolderTreeItemResponseModel>;
export type UmbDataTypePickerModalResult = UmbPickerModalResult;
export const UMB_DATA_TYPE_PICKER_MODAL = new UmbModalToken<UmbDataTypePickerModalData, UmbDataTypePickerModalResult>(
'Umb.Modal.DataTypePicker',
'Umb.Modal.TreePicker',
{
type: 'sidebar',
size: 'small',
},
{
treeAlias: 'Umb.Tree.DataTypes',
}
);

View File

@@ -0,0 +1,20 @@
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export interface UmbDictionaryItemPickerModalData {
multiple: boolean;
selection: string[];
}
export interface UmbDictionaryItemPickerModalResult {
selection: Array<string | null>;
}
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',
});

View File

@@ -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<string | null>;
}
export interface UmbDocumentPickerModalResult {
selection: Array<string | null>;
}
export type UmbDocumentPickerModalData = UmbTreePickerModalData<DocumentTreeItemResponseModel>;
export type UmbDocumentPickerModalResult = UmbPickerModalResult;
export const UMB_DOCUMENT_PICKER_MODAL = new UmbModalToken<UmbDocumentPickerModalData, UmbDocumentPickerModalResult>(
'Umb.Modal.DocumentPicker',
'Umb.Modal.TreePicker',
{
type: 'sidebar',
size: 'small',
},
{
treeAlias: 'Umb.Tree.Documents',
}
);

View File

@@ -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<string>;
}
export interface UmbDocumentTypePickerModalResult {
selection: Array<string | null>;
}
export type UmbDocumentTypePickerModalData = UmbTreePickerModalData<EntityTreeItemResponseModel>;
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',
}
);

View File

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

View File

@@ -1,18 +0,0 @@
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export interface UmbMediaPickerModalData {
multiple?: boolean;
selection: Array<string>;
}
export interface UmbMediaPickerModalResult {
selection: Array<string | null>;
}
export const UMB_MEDIA_PICKER_MODAL = new UmbModalToken<UmbMediaPickerModalData, UmbMediaPickerModalResult>(
'Umb.Modal.MediaPicker',
{
type: 'sidebar',
size: 'small',
}
);

View File

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

View File

@@ -1,36 +1,38 @@
import { UmbModalConfig } from '../modal.context';
export class UmbModalToken<Data extends object = object, Result = unknown> {
export class UmbModalToken<ModalDataType extends object = object, ModalResultType = unknown> {
/**
* 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<Data extends object = object, Result = unknown> {
public getDefaultConfig(): UmbModalConfig | undefined {
return this.defaultConfig;
}
public getDefaultData(): ModalDataType | undefined {
return this.defaultData;
}
}

View File

@@ -0,0 +1,20 @@
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export interface UmbPartialViewPickerModalData {
multiple: boolean;
selection: string[];
}
export interface UmbPartialViewPickerModalResult {
selection: Array<string | null> | 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',
});

View File

@@ -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<string | null>;
}
export interface UmbTemplatePickerModalResult {
selection: Array<string | null>;
}
export type UmbTemplatePickerModalData = UmbTreePickerModalData<EntityTreeItemResponseModel>;
export type UmbTemplatePickerModalResult = UmbPickerModalResult;
export const UMB_TEMPLATE_PICKER_MODAL = new UmbModalToken<UmbTemplatePickerModalData, UmbTemplatePickerModalResult>(
'Umb.Modal.TemplatePicker',
'Umb.Modal.TreePicker',
{
type: 'sidebar',
size: 'small',
},
{
treeAlias: 'Umb.Tree.Templates',
}
);

View File

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

View File

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

View File

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

View File

@@ -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<string>;
this.value = data.selection?.filter((id) => id !== null && id !== undefined) as Array<string>;
this.selectionUpdated();
}
});

View File

@@ -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<ManifestTypes | ManifestKind> = [
...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) => {

View File

@@ -37,12 +37,6 @@ const modals: Array<ManifestModal> = [
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<ManifestModal> = [
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];

View File

@@ -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<string | null> = [];
@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`
<umb-workspace-editor headline="Select Content">
<uui-box>
<uui-input></uui-input>
<hr />
<umb-tree
alias="Umb.Tree.Templates"
@selected=${this._handleSelectionChange}
.selection=${this._selection}
selectable></umb-tree>
</uui-box>
<div slot="actions">
<uui-button label="Close" @click=${this._close}></uui-button>
<uui-button label="Submit" look="primary" color="positive" @click=${this._submit}></uui-button>
</div>
</umb-workspace-editor>
`;
}
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;
}
}

View File

@@ -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<UmbDataTypePickerModalData, UmbDataTypePickerModalResult>;
@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<TreeItemType extends TreeItemPresentationModel> extends UmbModalBaseElement<
UmbTreePickerModalData<TreeItemType>,
UmbPickerModalResult,
ManifestModalTreePickerKind
> {
@state()
_selection: Array<string | null> = [];
@@ -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 {
<umb-body-layout headline="Select">
<uui-box>
<umb-tree
alias="Umb.Tree.DataTypes"
alias=${this.data?.treeAlias}
@selected=${this.#onSelectionChange}
.selection=${this._selection}
selectable
@@ -67,10 +63,10 @@ export class UmbDataTypePickerModalElement extends UmbLitElement {
static styles = [UUITextStyles, css``];
}
export default UmbDataTypePickerModalElement;
export default UmbTreePickerModalElement;
declare global {
interface HTMLElementTagNameMap {
'umb-data-type-picker-modal': UmbDataTypePickerModalElement;
'umb-tree-picker-modal': UmbTreePickerModalElement<TreeItemPresentationModel>;
}
}

View File

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

View File

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

View File

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

View File

@@ -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<string | null> = [];
@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`
<umb-workspace-editor headline="Select Content">
<uui-box>
<uui-input></uui-input>
<hr />
<umb-tree
alias="Umb.Tree.Documents"
@selected=${this._handleSelectionChange}
.selection=${this._selection}
selectable></umb-tree>
</uui-box>
<div slot="actions">
<uui-button label="Close" @click=${this._close}></uui-button>
<uui-button label="Submit" look="primary" color="positive" @click=${this._submit}></uui-button>
</div>
</umb-workspace-editor>
`;
}
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;
}
}

View File

@@ -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<UmbDocumentPickerModalElement> = () => html`
<!-- TODO: figure out if generics are allowed for properties:
https://github.com/runem/lit-analyzer/issues/149
https://github.com/runem/lit-analyzer/issues/163 -->
<umb-document-picker-modal .data=${data as any}></umb-document-picker-modal>
`;

View File

@@ -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<string | null> = [];
@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`
<umb-workspace-editor headline="Select Content">
<uui-box>
<uui-input></uui-input>
<hr />
<umb-tree
alias="Umb.Tree.DocumentTypes"
@selected=${this._handleSelectionChange}
.selection=${this._selection}
selectable
?multiple=${this._multiple}></umb-tree>
</uui-box>
<div slot="actions">
<uui-button label="Close" @click=${this._close}></uui-button>
<uui-button label="Submit" look="primary" color="positive" @click=${this._submit}></uui-button>
</div>
</umb-workspace-editor>
`;
}
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;
}
}

View File

@@ -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<UmbDocumentTypePickerModalElement> = () => html`
<!-- TODO: figure out if generics are allowed for properties:
https://github.com/runem/lit-analyzer/issues/149
https://github.com/runem/lit-analyzer/issues/163 -->
<umb-document-picker-modal .data=${data as any}></umb-document-picker-modal>
`;

View File

@@ -1,18 +0,0 @@
import type { ManifestModal } from '@umbraco-cms/backoffice/extensions-registry';
const modals: Array<ManifestModal> = [
{
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];

View File

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

View File

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

View File

@@ -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<UmbMediaRepository> {
#modalContext?: UmbModalContext;
@@ -17,7 +17,7 @@ export class UmbMediaMoveEntityBulkAction extends UmbEntityBulkActionBase<UmbMed
async execute() {
// TODO: the picker should be single picker by default
const modalHandler = this.#modalContext?.open(UMB_MEDIA_PICKER_MODAL, {
const modalHandler = this.#modalContext?.open(UMB_MEDIA_TREE_PICKER_MODAL, {
selection: [],
multiple: false,
});

View File

@@ -5,7 +5,6 @@ import { manifests as treeManifests } from './tree/manifests';
import { manifests as workspaceManifests } from './workspace/manifests';
import { manifests as entityActionsManifests } from './entity-actions/manifests';
import { manifests as entityBulkActionsManifests } from './entity-bulk-actions/manifests';
import { manifests as modalManifests } from './modals/manifests';
export const manifests = [
...collectionViewManifests,
@@ -15,5 +14,4 @@ export const manifests = [
...workspaceManifests,
...entityActionsManifests,
...entityBulkActionsManifests,
...modalManifests,
];

View File

@@ -1,12 +0,0 @@
import type { ManifestModal } from '@umbraco-cms/backoffice/extensions-registry';
const modals: Array<ManifestModal> = [
{
type: 'modal',
alias: 'Umb.Modal.MediaPicker',
name: 'Media Picker Modal',
loader: () => import('./media-picker/media-picker-modal.element'),
},
];
export const manifests = [...modals];

View File

@@ -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<string | null> = [];
@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`
<umb-workspace-editor headline="Select Content">
<uui-box>
<uui-input></uui-input>
<hr />
<umb-tree
alias="Umb.Tree.Media"
@selected=${this._handleSelectionChange}
.selection=${this._selection}
selectable
?multiple=${this._multiple}></umb-tree>
</uui-box>
<div slot="actions">
<uui-button label="Close" @click=${this._close}></uui-button>
<uui-button label="Submit" look="primary" color="positive" @click=${this._submit}></uui-button>
</div>
</umb-workspace-editor>
`;
}
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;
}
}

View File

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

View File

@@ -1,12 +0,0 @@
import type { ManifestModal } from '@umbraco-cms/backoffice/extensions-registry';
const modals: Array<ManifestModal> = [
{
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];

View File

@@ -1 +1,2 @@
import './file-system-tree-item/file-system-tree-item.element';
import './insert-menu/templating-insert-menu.element';

View File

@@ -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`
<uui-button-group>
<uui-button look="secondary" @click=${this.#openChooseTypeModal}>
<uui-icon name="umb:add"></uui-icon>Insert</uui-button
>
<umb-button-with-dropdown
look="secondary"
compact
placement="bottom-start"
id="insert-button"
label="open insert menu">
<ul id="insert-menu" slot="dropdown">
<li>
<uui-menu-item
class="insert-menu-item"
target="_blank"
label="Value"
title="Value"
@click=${this.#openInsertValueSidebar}>
</uui-menu-item>
</li>
${this.hidePartialView
? ''
: html` <li>
<uui-menu-item
class="insert-menu-item"
label="Partial view"
title="Partial view"
@click=${this.#openInsertPartialViewSidebar}>
</uui-menu-item>
</li>`}
<li>
<uui-menu-item
class="insert-menu-item"
label="Dictionary item"
title="Dictionary item"
@click=${this.#openInsertDictionaryItemModal}>
</uui-menu-item>
</li>
<li>
<uui-menu-item class="insert-menu-item" label="Macro" title="Macro"> </uui-menu-item>
</li>
</ul>
</umb-button-with-dropdown>
</uui-button-group>
`;
}
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;
}
}

View File

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

View File

@@ -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`
<umb-body-layout headline="Insert">
<div id="main">
<uui-box>
<uui-button @click=${this.#openInsertValueSidebar} look="placeholder" label="Insert value"
><h3>Value</h3>
<p>
Displays the value of a named field from the current page, with options to modify the value or fallback
to alternative values.
</p></uui-button
>
${this.data?.hidePartialViews
? ''
: html`<uui-button @click=${this.#openInsertPartialViewSidebar} look="placeholder" label="Insert value"
><h3>Partial view</h3>
<p>
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.
</p></uui-button
>`}
<uui-button @click=${this._close} look="placeholder" label="Insert Macro"
><h3>Macro</h3>
<p>
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.
</p></uui-button
>
<uui-button @click=${this.#openInsertDictionaryItemModal} look="placeholder" label="Insert Dictionary item"
><h3>Dictionary item</h3>
<p>
A dictionary item is a placeholder for a translatable piece of text, which makes it easy to create
designs for multilingual websites.
</p></uui-button
>
</uui-box>
</div>
<div slot="actions">
<uui-button @click=${this._close} look="secondary">Close</uui-button>
</div>
</umb-body-layout>
`;
}
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;
}
}

View File

@@ -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()}
<h3 @click=${this.click}>${this.checked ? html`<uui-icon name="umb:check"></uui-icon>` : ''}${this.label}</h3>
<div @click=${this.click}>
<slot name="description"><p>here goes some description</p></slot>
</div>
${this.checked && this.showInput
? html`<uui-form>
<form @submit=${this.#preventDefault}>
<uui-form-layout-item>
<uui-label slot="label" for="section-name-input" required>Section name</uui-label>
<uui-input
required
placeholder="Enter section name"
id="section-name-input"></uui-input> </uui-form-layout-item
>${this.showMandatory
? html`<p slot="if-checked">
<uui-checkbox label="Section is mandatory">Section is mandatory </uui-checkbox><br />
<small
>If mandatory, the child template must contain a <code>@section</code> definition, otherwise an
error is shown.</small
>
</p>`
: ''}
</form>
</uui-form>`
: ''}
`;
}
/* 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;
}
}

View File

@@ -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<UmbInsertSectionCheckboxElement>;
@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`
<umb-body-layout headline="Insert">
<div id="main">
<uui-box @change=${this.#chooseSection}>
<umb-insert-section-checkbox label="Render child template" checked>
<p slot="description">
Renders the contents of a child template, by inserting a <code>@RenderBody()</code> placeholder.
</p>
</umb-insert-section-checkbox>
<umb-insert-section-checkbox label="Render a named section" show-mandatory show-input>
<p slot="description">
Renders a named area of a child template, by inserting a <code>@RenderSection(name)</code> placeholder.
This renders an area of a child template which is wrapped in a corresponding
<code>@section [name]{ ... }</code> definition.
</p>
</umb-insert-section-checkbox>
<umb-insert-section-checkbox label="Define a named section" show-input>
<p slot="description">
Defines a part of your template as a named section by wrapping it in <code>@section { ... }</code>. This
can be rendered in a specific area of the parent of this template, by using <code>@RenderSection</code>.
</p>
</umb-insert-section-checkbox>
</uui-box>
</div>
<div slot="actions">
<uui-button @click=${this.#close} look="secondary">Close</uui-button>
<uui-button @click=${this.#submit} look="primary" color="positive">Submit</uui-button>
</div>
</umb-body-layout>
`;
}
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;
}
}

View File

@@ -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<object, string> {
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`
<umb-body-layout headline="Insert">
<div id="main">
<uui-box>
<uui-form-layout-item>
<uui-label slot="label" for="field-selector">Choose field</uui-label>
<uui-combobox id="field-selector" @change=${this.#setField}>
<uui-combobox-list>
<uui-combobox-list-option style="padding: 8px"> apple </uui-combobox-list-option>
<uui-combobox-list-option style="padding: 8px"> orange </uui-combobox-list-option>
<uui-combobox-list-option style="padding: 8px"> lemon </uui-combobox-list-option>
</uui-combobox-list>
</uui-combobox>
</uui-form-layout-item>
${this.showDefaultValueInput
? html` <uui-form-layout-item>
<uui-label slot="label" for="default-value">Default value</uui-label>
<uui-input
id="default-value"
type="text"
name="default-value"
label="default value"
@input=${this.#setDefaultValue}>
</uui-input>
</uui-form-layout-item>`
: html` <uui-button
@click=${() => (this.showDefaultValueInput = true)}
look="placeholder"
label="Add default value "
>Add default value</uui-button
>`}
<uui-form-layout-item>
<uui-label slot="label" for="default-value">Fallback</uui-label>
<uui-checkbox @change=${() => (this.recursive = !this.recursive)}>From ancestors</uui-checkbox>
</uui-form-layout-item>
<uui-form-layout-item>
<uui-label slot="label">Output sample</uui-label>
<uui-code-block>${this.output}</uui-code-block>
</uui-form-layout-item>
</uui-box>
</div>
<div slot="actions">
<uui-button @click=${this._close} look="secondary">Close</uui-button>
<uui-button @click=${this._submit} look="primary">Submit</uui-button>
</div>
</umb-body-layout>
`;
}
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;
}
}

View File

@@ -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<ManifestModal> = [
{
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];

View File

@@ -0,0 +1 @@
//TODO: move tokens here nad import this file somewhere

View File

@@ -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<string | null> = [];
@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`
<umb-body-layout headline="Insert Partial view">
<div id="main">
<uui-box>
<umb-tree
alias="Umb.Tree.PartialViews"
@selected=${this._handleSelectionChange}
.selection=${this._selection}
selectable></umb-tree>
</uui-box>
</div>
<div slot="actions">
<uui-button @click=${this._close} look="secondary">Close</uui-button>
</div>
</umb-body-layout>
`;
}
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;
}
}

View File

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

View File

@@ -0,0 +1,12 @@
import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller';
export class UmbCreateEmptyPartialViewAction<T extends { copy(): Promise<void> }> extends UmbEntityActionBase<T> {
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
}
async execute() {
throw new Error('Method not implemented.');
}
}

View File

@@ -0,0 +1,12 @@
import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller';
export class UmbCreateFromSnippetPartialViewAction<T extends { copy(): Promise<void> }> extends UmbEntityActionBase<T> {
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
}
async execute() {
throw new Error('Method not implemented.');
}
}

View File

@@ -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<ManifestEntityAction> = [
{
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<ManifestEntityAction> = [
{
type: 'entityAction',
alias: 'Umb.EntityAction.PartialViewFolder.Create.New',
name: 'Create PartialView Entity Under Directory Action',
meta: {
icon: 'umb:article',
label: 'New empty partial view',
api: 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];

View File

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

View File

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

View File

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

View File

@@ -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<any>, UmbDetailRepository<any> {
#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<any[]>) | undefined;
}>)
| undefined;
itemsLegacy?: ((uniques: string[]) => Promise<Observable<any[]>>) | undefined;
byId(id: string): Promise<Observable<any>> {
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<string>) {
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<string>) {
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'));
}
}

View File

@@ -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<TemplateResponseModel>([], (x) => x.id)
);
}
/**
* Append a partial view to the store
* @param {Template} template
* @memberof UmbPartialViewsStore
*/
append(template: TemplateResponseModel) {
this._data.append([template]);
}
/**
* Removes partial views in the store with the given uniques
* @param {string[]} uniques
* @memberof UmbPartialViewsStore
*/
remove(uniques: string[]) {
this._data.remove(uniques);
}
}
export const UMB_PARTIAL_VIEWS_STORE_CONTEXT_TOKEN = new UmbContextToken<UmbPartialViewsStore>(
UMB_PARTIAL_VIEW_STORE_CONTEXT_TOKEN_ALIAS
);

View File

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

View File

@@ -0,0 +1,19 @@
import {
FileSystemTreeItemPresentationModel,
PagedFileSystemTreeItemPresentationModel,
} from '@umbraco-cms/backoffice/backend-api';
import type { DataSourceResponse } from '@umbraco-cms/backoffice/repository';
export interface PartialViewsTreeDataSource {
getRootItems(): Promise<DataSourceResponse<PagedFileSystemTreeItemPresentationModel>>;
getChildrenOf({
path,
skip,
take,
}: {
path?: string | undefined;
skip?: number | undefined;
take?: number | undefined;
}): Promise<DataSourceResponse<PagedFileSystemTreeItemPresentationModel>>;
getItem(ids: Array<string>): Promise<DataSourceResponse<FileSystemTreeItemPresentationModel[]>>;
}

View File

@@ -0,0 +1,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<PartialViewDetails, PartialViewDetails, PartialViewDetails, PartialViewDetails>
{
#host: UmbControllerHostElement;
/**
* Creates an instance of UmbPartialViewDetailServerDataSource.
* @param {UmbControllerHostInterface} host
* @memberof UmbPartialViewDetailServerDataSource
*/
constructor(host: UmbControllerHostElement) {
this.#host = host;
}
createScaffold(parentKey: string | null): Promise<DataSourceResponse<PartialViewDetails>> {
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<DataSourceResponse<PartialViewDetails>> {
throw new Error('Method not implemented.');
}
update(unique: string, data: PartialViewDetails): Promise<DataSourceResponse<PartialViewDetails>> {
throw new Error('Method not implemented.');
}
delete(unique: string): Promise<DataSourceResponse> {
throw new Error('Method not implemented.');
}
}

View File

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

View File

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

View File

@@ -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<ManifestWorkspaceAction> = [
{
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];

View File

@@ -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`<umb-body-layout alias="Umb.Workspace.Template">
<uui-input slot="header" .value=${this._name} @input=${this.#onNameInput}></uui-input>
<uui-box>
<uui-button color="danger" look="primary" slot="header" @click=${this.#insertCode}
>Insert "My hovercraft is full of eels"</uui-button
>
<umb-code-editor
language="razor"
id="content"
.code=${this._content ?? ''}
@input=${this.#onCodeEditorInput}></umb-code-editor>
</uui-box>
</umb-body-layout>`;
}
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;
}
}

View File

@@ -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<UmbTemplateRepository, TemplateResponseModel> {
getEntityId(): string | undefined {
throw new Error('Method not implemented.');
}
getEntityType(): string {
throw new Error('Method not implemented.');
}
save(): Promise<void> {
throw new Error('Method not implemented.');
}
destroy(): void {
throw new Error('Method not implemented.');
}
#data = new UmbDeepState<TemplateResponseModel | undefined>(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);
}
}

View File

@@ -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`<umb-router-slot
.routes=${this._routes}
@init=${(event: UmbRouterSlotInitEvent) => {
this.#routerPath = event.target.absoluteRouterPath;
}}></umb-router-slot>`;
}
static styles = [UUITextStyles, css``];
}
export default UmbPartialViewsWorkspaceElement;
declare global {
interface HTMLElementTagNameMap {
'umb-partial-views-workspace': UmbPartialViewsWorkspaceElement;
}
}

View File

@@ -9,7 +9,7 @@ export class UmbCreateEntityAction<T extends { copy(): Promise<void> }> 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);
}

View File

@@ -1,4 +1,3 @@
import { TEMPLATE_REPOSITORY_ALIAS } from '../repository/manifests';
import { UmbSaveWorkspaceAction } from '@umbraco-cms/backoffice/workspace';
import type {
ManifestWorkspace,

View File

@@ -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`<umb-body-layout alias="Umb.Workspace.Template">
<uui-input slot="header" .value=${this._name} @input=${this.#onNameInput}></uui-input>
<uui-box>
<div slot="header" id="code-editor-menu-container">
<uui-button-group>
<uui-button look="secondary" id="master-template-button" label="Change Master template"
>Master template: something</uui-button
>
<uui-button look="secondary" id="save-button" label="Remove master template" compact
><uui-icon name="umb:delete"></uui-icon
></uui-button>
</uui-button-group>
<umb-templating-insert-menu @insert=${this.#insertCode}></umb-templating-insert-menu>
<uui-button look="secondary" id="query-builder-button" label="Query builder">
<uui-icon name="umb:wand"></uui-icon>Query builder
</uui-button>
<uui-button
look="secondary"
id="sections-button"
label="Query builder"
@click=${this.#openInsertSectionModal}>
<uui-icon name="umb:indent"></uui-icon>Sections
</uui-button>
</div>
<umb-code-editor
language="razor"
id="content"
.code=${this._content ?? ''}
@input=${this.#onCodeEditorInput}></umb-code-editor>
</uui-box>
</umb-body-layout>`;
}
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;
}
}

View File

@@ -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`<umb-workspace-editor alias="Umb.Workspace.Template">
<uui-input slot="header" .value=${this._name} @input=${this.#onNameInput}></uui-input>
<uui-box>
<uui-button color="danger" look="primary" slot="header" @click=${this.#insertCode}
>Insert "My hovercraft is full of eels"</uui-button
>
<umb-code-editor
language="razor"
id="content"
.code=${this._content ?? ''}
@input=${this.#onCodeEditorInput}></umb-code-editor>
</uui-box>
</umb-workspace-editor>`;
return html`<umb-router-slot
.routes=${this._routes}
@init=${(event: UmbRouterSlotInitEvent) => {
this.#routerPath = event.target.absoluteRouterPath;
}}></umb-router-slot>`;
}
static styles = [

View File

@@ -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 +=
'<ul>\n' +
'\t@foreach (var item in selection)\n' +
'\t{\n' +
'\t\t<li>\n' +
'\t\t\t<a href="@item.Url()">@item.Name()</a>\n' +
'\t\t</li>\n' +
'\t}\n' +
'</ul>\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;
};

View File

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

View File

@@ -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<string | null> = [];
@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`
<umb-body-layout headline="Dictionary item">
<div id="main">
<uui-box>
<umb-tree
alias="Umb.Tree.Dictionary"
@selected=${this._handleSelectionChange}
.selection=${this._selection}
selectable></umb-tree>
</uui-box>
</div>
<div slot="actions">
<uui-button @click=${this._close} look="secondary">Close</uui-button>
</div>
</umb-body-layout>
`;
}
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;
}
}

View File

@@ -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<ManifestModal> = [
{
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];

View File

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

View File

@@ -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<FileSystemTreeItemPresentationModel> = [
{
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<FileSystemTreeItemPresentationModel> {
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<string>): Array<FileSystemTreeItemPresentationModel> {
const items = this.data.filter((item) => paths.includes(item.path ?? ''));
return items.map((item) => createFileSystemTreeItem(item));
}
}
export const umbPartialViewsData = new UmbPartialViewsData();

View File

@@ -29,7 +29,7 @@ export const data: Array<TemplateDBItem> = [
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<TemplateDBItem> = [
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<TemplateDBItem> = [
parentId: '9a84c0b3-03b4-4dd4-84ac-706740ac0f71',
name: 'Child',
type: 'template',
icon: 'icon-layout',
icon: 'umb:layout',
hasChildren: false,
alias: 'Test',
content:

View File

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

View File

@@ -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<UmbModalData extends object = object, UmbModalResult = unknown>
export abstract class UmbModalBaseElement<
ModalDataType extends object = object,
ModalResultType = unknown,
ModalManifestType extends ManifestModal = ManifestModal
>
extends UmbLitElement
implements UmbModalExtensionElement<UmbModalData, UmbModalResult>
implements UmbModalExtensionElement<ModalDataType, ModalResultType, ModalManifestType>
{
@property({ type: Array, attribute: false })
public manifest?: ModalManifestType;
@property({ attribute: false })
modalHandler?: UmbModalHandler<UmbModalData, UmbModalResult>;
public modalHandler?: UmbModalHandler<ModalDataType, ModalResultType>;
@property({ type: Object, attribute: false })
data?: UmbModalData;
public data?: ModalDataType;
}