workspaces

This commit is contained in:
Lone Iversen
2023-03-22 10:31:06 +01:00
parent f5048da942
commit 50efb41584
12 changed files with 432 additions and 77 deletions

View File

@@ -61,7 +61,8 @@ export class UmbWorkspaceViewDocumentTypePermissionsElement extends UmbLitElemen
Allow content of the specified types to be created underneath content of this type.
</div>
<div slot="editor">
<umb-input-document-type-picker></umb-input-document-type-picker>
<umb-input-document-type-picker
.currentDocumentType="${this._documentType}"></umb-input-document-type-picker>
</div>
</umb-workspace-property-layout>
<umb-workspace-property-layout alias="VaryByNature" label="Allow vary by culture">

View File

@@ -1,8 +1,8 @@
import { css, html } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, query, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import { customElement, property, state } from 'lit/decorators.js';
import { UmbWorkspaceDocumentTypeContext } from '../../document-type-workspace.context';
import { UmbTemplateCardListElement } from '../../../../../shared/components/template-card/template-card-list.element';
import { UmbLitElement } from '@umbraco-cms/element';
import type { DocumentTypeModel } from '@umbraco-cms/backend-api';
import '../../../../../shared/property-creator/property-creator.element.ts';
@@ -37,9 +37,18 @@ export class UmbWorkspaceViewDocumentTypeTemplatesElement extends UmbLitElement
`,
];
@property()
defaultTemplateKey?: string = '123';
@state()
_documentType?: DocumentTypeModel;
@state()
_templates = [
{ key: '123', name: 'Blog Post Page' },
{ key: '456', name: 'Blog Entry Page' },
];
private _workspaceContext?: UmbWorkspaceDocumentTypeContext;
constructor() {
@@ -60,17 +69,21 @@ export class UmbWorkspaceViewDocumentTypeTemplatesElement extends UmbLitElement
});
}
#changeDefaultTemplate(e: CustomEvent) {
this.defaultTemplateKey = (e.target as UmbTemplateCardListElement).value as string;
console.log('default template key', this.defaultTemplateKey);
}
#removeTemplate(key: string) {
console.log('remove template', key);
}
render() {
return html`<uui-box headline="Templates">
<umb-workspace-property-layout alias="Templates" label="Allowed Templates">
<div slot="description">Choose which templates editors are allowed to use on content of this type</div>
<div id="templates" slot="editor">
<umb-template-card-list>
<umb-template-card value="123"></umb-template-card>
<umb-template-card value="456"></umb-template-card>
</umb-template-card-list>
</div>
<umb-input-template-picker umb-input-template-picker></umb-input-template-picker>
</div>
</umb-workspace-property-layout>
</uui-box>`;

View File

@@ -0,0 +1,103 @@
import { css, html } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, state } from 'lit/decorators.js';
import type { UmbTreeElement } from '../../../../shared/components/tree/tree.element';
import { UmbDocumentTypePickerModalData, UmbDocumentTypePickerModalResult } from '.';
import { UmbModalBaseElement } from '@umbraco-cms/modal';
// TODO: make use of UmbPickerLayoutBase
@customElement('umb-document-type-picker-modal')
export class UmbDocumentTypePickerModalElement extends UmbModalBaseElement<
UmbDocumentTypePickerModalData,
UmbDocumentTypePickerModalResult
> {
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);
}
`,
];
@state()
_selection: Array<string> = [];
@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-layout headline="Select Content">
<uui-box>
<uui-input></uui-input>
<hr />
<umb-tree
alias="Umb.Tree.DocumentTypes"
@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-layout>
`;
}
}
export default UmbDocumentTypePickerModalElement;
declare global {
interface HTMLElementTagNameMap {
'umb-document-type-picker-modal': UmbDocumentTypePickerModalElement;
}
}

View File

@@ -0,0 +1,26 @@
import '../../../../shared/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 './index';
export default {
title: 'API/Modals/Layouts/Content Picker',
component: 'umb-document-picker-modal',
id: 'umb-document-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

@@ -0,0 +1,18 @@
import { UmbModalToken } from '@umbraco-cms/modal';
export interface UmbDocumentTypePickerModalData {
multiple?: boolean;
selection?: Array<string>;
}
export interface UmbDocumentTypePickerModalResult {
selection: Array<string>;
}
export const UMB_DOCUMENT_TYPE_PICKER_MODAL_TOKEN = new UmbModalToken<
UmbDocumentTypePickerModalData,
UmbDocumentTypePickerModalResult
>('Umb.Modal.DocumentTypePicker', {
type: 'sidebar',
size: 'small',
});

View File

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

@@ -28,6 +28,7 @@ import './input-media-picker/input-media-picker.element';
import './input-multi-url-picker/input-multi-url-picker.element';
import './input-slider/input-slider.element';
import './input-toggle/input-toggle.element';
import './input-template-picker/input-template-picker.element';
import './property-type-based-property/property-type-based-property.element';
import './ref-property-editor-ui/ref-property-editor-ui.element';
import './section/section-main/section-main.element';
@@ -47,7 +48,7 @@ import './workspace/workspace-layout/workspace-layout.element';
import './workspace/workspace-footer-layout/workspace-footer-layout.element';
import './template-cards/template-card.element';
import './template-cards/template-card-list.element';
import './template-card/template-card.element';
import './template-card/template-card-list.element';
export const manifests = [...debugManifests];

View File

@@ -3,13 +3,15 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, property, state } from 'lit/decorators.js';
import { ifDefined } from 'lit-html/directives/if-defined.js';
import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins';
import { UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN } from '../../../documents/documents/repository/document.tree.store';
import { UmbDocumentTypeTreeStore } from '../../../documents/document-types/repository/document-type.tree.store';
import {
UmbDocumentTypeTreeStore,
UMB_DOCUMENT_TYPE_TREE_STORE_CONTEXT_TOKEN,
} from '../../../documents/document-types/repository/document-type.tree.store';
import { UMB_CONFIRM_MODAL_TOKEN } from '../../modals/confirm';
import { UMB_DOCUMENT_PICKER_MODAL_TOKEN } from '../../../documents/documents/modals/document-picker';
import { UMB_DOCUMENT_TYPE_PICKER_MODAL_TOKEN } from '../../../documents/documents/modals/document-type-picker';
import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal';
import { UmbLitElement } from '@umbraco-cms/element';
import type { DocumentTypeTreeItemModel, FolderTreeItemModel } from '@umbraco-cms/backend-api';
import type { DocumentTypeModel, DocumentTypeTreeItemModel, FolderTreeItemModel } from '@umbraco-cms/backend-api';
import type { UmbObserverController } from '@umbraco-cms/observable-api';
@customElement('umb-input-document-type-picker')
@@ -20,43 +22,16 @@ export class UmbInputDocumentTypePickerElement extends FormControlMixin(UmbLitEl
#add-button {
width: 100%;
}
#current-node {
background-color: var(--uui-color-surface-alt);
}
#wrapper-nodes {
margin-left: var(--uui-size-space-6);
}
`,
];
/**
* This is a minimum amount of selected items in this input.
* @type {number}
* @attr
* @default undefined
*/
@property({ type: Number })
min?: number;
/**
* Min validation message.
* @type {boolean}
* @attr
* @default
*/
@property({ type: String, attribute: 'min-message' })
minMessage = 'This field need more items';
/**
* This is a maximum amount of selected items in this input.
* @type {number}
* @attr
* @default undefined
*/
@property({ type: Number })
max?: number;
/**
* Max validation message.
* @type {boolean}
* @attr
* @default
*/
@property({ type: String, attribute: 'min-message' })
maxMessage = 'This field exceeds the allowed amount of items';
// TODO: do we need both selectedKeys and value? If we just use value we follow the same pattern as native form controls.
private _selectedKeys: Array<string> = [];
@@ -76,29 +51,20 @@ export class UmbInputDocumentTypePickerElement extends FormControlMixin(UmbLitEl
}
}
@property()
currentDocumentType?: DocumentTypeModel;
@state()
private _items?: Array<DocumentTypeTreeItemModel>;
private _modalContext?: UmbModalContext;
private _documentStore?: UmbDocumentTypeTreeStore;
private _documentTypeStore?: UmbDocumentTypeTreeStore;
private _pickedItemsObserver?: UmbObserverController<FolderTreeItemModel>;
constructor() {
super();
this.addValidator(
'rangeUnderflow',
() => this.minMessage,
() => !!this.min && this._selectedKeys.length < this.min
);
this.addValidator(
'rangeOverflow',
() => this.maxMessage,
() => !!this.max && this._selectedKeys.length > this.max
);
this.consumeContext(UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN, (instance) => {
this._documentStore = instance;
this.consumeContext(UMB_DOCUMENT_TYPE_TREE_STORE_CONTEXT_TOKEN, (instance) => {
this._documentTypeStore = instance;
this._observePickedDocuments();
});
this.consumeContext(UMB_MODAL_CONTEXT_TOKEN, (instance) => {
@@ -113,18 +79,18 @@ export class UmbInputDocumentTypePickerElement extends FormControlMixin(UmbLitEl
private _observePickedDocuments() {
this._pickedItemsObserver?.destroy();
if (!this._documentStore) return;
if (!this._documentTypeStore) return;
// TODO: consider changing this to the list data endpoint when it is available
this._pickedItemsObserver = this.observe(this._documentStore.items(this._selectedKeys), (items) => {
this._pickedItemsObserver = this.observe(this._documentTypeStore.items(this._selectedKeys), (items) => {
this._items = items;
});
}
private _openPicker() {
// We send a shallow copy(good enough as its just an array of keys) of our this._selectedKeys, as we don't want the modal to manipulate our data:
const modalHandler = this._modalContext?.open(UMB_DOCUMENT_PICKER_MODAL_TOKEN, {
multiple: this.max === 1 ? false : true,
const modalHandler = this._modalContext?.open(UMB_DOCUMENT_TYPE_PICKER_MODAL_TOKEN, {
multiple: true,
selection: [...this._selectedKeys],
});
@@ -153,8 +119,13 @@ export class UmbInputDocumentTypePickerElement extends FormControlMixin(UmbLitEl
render() {
return html`
${this._items?.map((item) => this._renderItem(item))}
<uui-button id="add-button" look="placeholder" @click=${this._openPicker} label="open">Add</uui-button>
<uui-ref-node id="current-node" .name="${this.currentDocumentType?.name ?? ''} (current)">
<uui-icon slot="icon" .name="${this.currentDocumentType?.icon ?? 'umb:document'}"></uui-icon>
</uui-ref-node>
<div id="wrapper-nodes">
<uui-ref-list> ${this._items?.map((item) => this._renderItem(item))} </uui-ref-list>
<uui-button id="add-button" look="placeholder" @click=${this._openPicker} label="open">Add</uui-button>
</div>
`;
}
@@ -164,6 +135,7 @@ export class UmbInputDocumentTypePickerElement extends FormControlMixin(UmbLitEl
return html`
<uui-ref-node name=${ifDefined(item.name === null ? undefined : item.name)} detail=${ifDefined(item.key)}>
<uui-icon slot="icon" name="${ifDefined(item.icon)}"></uui-icon>
${tempItem.isTrashed ? html` <uui-tag size="s" slot="tag" color="danger">Trashed</uui-tag> ` : nothing}
<uui-action-bar slot="actions">
<uui-button @click=${() => this._removeItem(item)} label="Remove document ${item.name}">Remove</uui-button>

View File

@@ -0,0 +1,211 @@
import { css, html } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, property, queryAll, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins';
import { UMB_CONFIRM_MODAL_TOKEN } from '../../modals/confirm';
import { UmbTemplateCardElement } from '../template-card/template-card.element';
import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal';
import { UmbLitElement } from '@umbraco-cms/element';
import { TemplateModel, TemplateResource } from '@umbraco-cms/backend-api';
import { tryExecuteAndNotify } from '@umbraco-cms/resources';
type DocTypTemplateModel = TemplateModel & { default: boolean };
@customElement('umb-input-template-picker')
export class UmbInputTemplatePickerElement extends FormControlMixin(UmbLitElement) {
static styles = [
UUITextStyles,
css`
#add-button {
width: 100%;
}
:host {
box-sizing: border-box;
display: flex;
gap: var(--uui-size-space-4);
flex-wrap: wrap;
}
:host > * {
max-width: 180px;
min-width: 180px;
min-height: 150px;
}
.fade-in {
animation: fadeIn 1s;
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
`,
];
/**
* This is a minimum amount of selected items in this input.
* @type {number}
* @attr
* @default undefined
*/
@property({ type: Number })
min?: number;
/**
* Min validation message.
* @type {boolean}
* @attr
* @default
*/
@property({ type: String, attribute: 'min-message' })
minMessage = 'This field need more items';
/**
* This is a maximum amount of selected items in this input.
* @type {number}
* @attr
* @default undefined
*/
@property({ type: Number })
max?: number;
/**
* Max validation message.
* @type {boolean}
* @attr
* @default
*/
@property({ type: String, attribute: 'min-message' })
maxMessage = 'This field exceeds the allowed amount of items';
private _templates: Array<DocTypTemplateModel> = [];
public get templates(): Array<DocTypTemplateModel> {
return this._templates;
}
public set templates(newTemplates: Array<DocTypTemplateModel>) {
const keys = newTemplates.map((template) => template.key);
super.value = keys.join(',');
this._templates = newTemplates;
}
@state()
private _items: Array<any> = [
{ key: '2bf464b6-3aca-4388-b043-4eb439cc2643', name: 'Doc 1', default: false },
{ key: '9a84c0b3-03b4-4dd4-84ac-706740ac0f71', name: 'Test', default: true },
];
private _modalContext?: UmbModalContext;
//private _documentStore?: UmbDocumentTreeStore;
//private _pickedItemsObserver?: UmbObserverController<FolderTreeItemModel>;
@queryAll('.template-card')
private _templateCardElements?: NodeListOf<Element>;
constructor() {
super();
this.addValidator(
'rangeUnderflow',
() => this.minMessage,
() => !!this.min && this._templates.length < this.min
);
this.addValidator(
'rangeOverflow',
() => this.maxMessage,
() => !!this.max && this._templates.length > this.max
);
this.consumeContext(UMB_MODAL_CONTEXT_TOKEN, (instance) => {
this._modalContext = instance;
});
}
connectedCallback(): void {
super.connectedCallback();
this._items = this._items.sort((a, b) => b.default - a.default);
this.#setup();
}
async #setup() {
const templates = await tryExecuteAndNotify(this, TemplateResource.getTreeTemplateRoot({ skip: 0, take: 9999 }));
console.log(templates);
}
protected getFormElement() {
return undefined;
}
#openTemplatePickerModal() {
console.log('template picker modal');
}
#changeSelected() {
console.log('selected');
}
/** Clicking the template card buttons */
#changeDefault(e: CustomEvent) {
const key = (e.target as UmbTemplateCardElement).value;
const oldDefault = this._items.find((x) => x.default === true);
const newDefault = this._items.find((x) => x.key === key);
const items = this._items.map((item) => {
if (item.default === true) return { ...newDefault, default: true };
if (item.key === key) return { ...oldDefault, default: false };
return item;
});
this._items = items;
}
#openTemplate(e: CustomEvent) {
const key = (e.target as UmbTemplateCardElement).value;
console.log('open', key);
}
#removeTemplate(key: string) {
console.log('remove', key);
}
render() {
return html`
${repeat(
this._items,
(template) => template.default,
(template, index) => html`<div class="fade-in">
<umb-template-card
class="template-card"
name="${template.name}"
key="${template.key}"
@default-change="${this.#changeDefault}"
@open="${this.#openTemplate}"
?default="${template.default}">
<uui-button
slot="actions"
label="Remove document ${template.name}"
@click="${() => this.#removeTemplate(template.key)}"
compact>
<uui-icon name="umb:trash"> </uui-icon>
</uui-button>
</umb-template-card>
</div>`
)}
<uui-button id="add-button" look="placeholder" label="open">Add</uui-button>
`;
}
}
export default UmbInputTemplatePickerElement;
declare global {
interface HTMLElementTagNameMap {
'umb-input-template-picker': UmbInputTemplatePickerElement;
}
}

View File

@@ -57,6 +57,7 @@ export class UmbTemplateCardListElement extends FormControlMixin(UmbLitElement)
el.selected = false;
}
});
this.value = newValue;
this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true }));
}

View File

@@ -41,13 +41,16 @@ export class UmbTemplateCardElement extends FormControlMixin(UmbLitElement) {
border-radius: var(--uui-border-radius);
border: 1px solid var(--uui-color-divider-emphasis);
background-color: var(--uui-color-background);
padding: var(--uui-size-space-4);
padding: var(--uui-size-4);
}
:host([selected]) #card {
:host([default]) #card {
border: 1px solid var(--uui-color-selected);
outline: 1px solid var(--uui-color-selected);
}
#card:has(uui-button:hover) {
border: 1px solid var(--uui-color-selected);
}
#bottom {
margin-top: auto;
@@ -117,7 +120,7 @@ export class UmbTemplateCardElement extends FormControlMixin(UmbLitElement) {
name = '';
@property({ type: Boolean, reflect: true })
selected = false;
default = false;
_key = '';
@property({ type: String })
@@ -137,7 +140,7 @@ export class UmbTemplateCardElement extends FormControlMixin(UmbLitElement) {
e.preventDefault();
e.stopPropagation();
//this.selected = true;
this.dispatchEvent(new CustomEvent('selected', { bubbles: true, composed: true }));
this.dispatchEvent(new CustomEvent('default-change', { bubbles: true, composed: true }));
}
#openTemplate(e: KeyboardEvent) {
e.preventDefault();
@@ -152,8 +155,8 @@ export class UmbTemplateCardElement extends FormControlMixin(UmbLitElement) {
<uui-icon class="logo" name="umb:layout"></uui-icon>
<strong>${this.name.length ? this.name : 'Untitled template'}</strong>
</button>
<uui-button id="bottom" label="Default template" ?disabled="${this.selected}" @click="${this.#setSelection}">
${this.selected ? '(Default template)' : 'Set default'}
<uui-button id="bottom" label="Default template" ?disabled="${this.default}" @click="${this.#setSelection}">
${this.default ? '(Default template)' : 'Set default'}
</uui-button>
</div>`;
}