Merge pull request #1104 from umbraco/feature/block-catalogue-modal

block catalogue modal
This commit is contained in:
Niels Lyngsø
2024-01-12 11:15:54 +01:00
committed by GitHub
15 changed files with 632 additions and 17 deletions

View File

@@ -1 +1 @@
export { debounce, clamp, camelCase } from 'lodash-es';
export { debounce, clamp, camelCase, groupBy } from 'lodash-es';

View File

@@ -490,6 +490,41 @@ export const data: Array<DataTypeResponseModel | FolderTreeItemResponseModel> =
{
label: 'Mocked Block Type for Block List',
contentElementTypeKey: '4f68ba66-6fb2-4778-83b8-6ab4ca3a7c5c',
icon: 'icon-server-alt',
},
{
label: 'Mocked Coffee Block',
contentElementTypeKey: 'coffee-umbraco-demo-block-id',
iconColor: '#FFFDD0',
backgroundColor: '#633f32',
editorSize: 'medium',
icon: 'icon-coffee',
},
{
label: 'Headline',
contentElementTypeKey: 'headline-umbraco-demo-block-id',
backgroundColor: 'gold',
editorSize: 'medium',
icon: 'icon-edit',
},
{
label: 'Image',
contentElementTypeKey: 'image-umbraco-demo-block-id',
editorSize: 'medium',
icon: 'icon-picture',
},
{
label: 'Rich Text',
contentElementTypeKey: 'rich-text-umbraco-demo-block-id',
editorSize: 'medium',
icon: 'icon-diploma',
},
{
label: 'Two Column Layout',
contentElementTypeKey: 'two-column-layout-umbraco-demo-block-id',
editorSize: 'medium',
icon: 'icon-book-alt',
},
],
},
@@ -560,6 +595,10 @@ export const data: Array<DataTypeResponseModel | FolderTreeItemResponseModel> =
editorAlias: 'Umbraco.BlockGrid',
editorUiAlias: 'Umb.PropertyEditorUi.BlockGrid',
values: [
{
alias: 'blockGroups',
value: [{ key: 'demo-block-group-id', name: 'Demo Blocks' }],
},
{
alias: 'blocks',
value: [
@@ -567,6 +606,45 @@ export const data: Array<DataTypeResponseModel | FolderTreeItemResponseModel> =
label: 'Mocked Block Type for Block Grid',
contentElementTypeKey: '4f68ba66-6fb2-4778-83b8-6ab4ca3a7c5c',
},
{
label: 'Mocked Coffee Block',
contentElementTypeKey: 'coffee-umbraco-demo-block-id',
iconColor: '#FFFDD0',
backgroundColor: '#633f32',
editorSize: 'medium',
icon: 'icon-coffee',
},
{
label: 'Headline',
contentElementTypeKey: 'headline-umbraco-demo-block-id',
backgroundColor: 'gold',
editorSize: 'medium',
icon: 'icon-edit',
groupKey: 'demo-block-group-id',
},
{
label: 'Image',
contentElementTypeKey: 'image-umbraco-demo-block-id',
editorSize: 'medium',
icon: 'icon-picture',
groupKey: 'demo-block-group-id',
},
{
label: 'Rich Text',
contentElementTypeKey: 'rich-text-umbraco-demo-block-id',
editorSize: 'medium',
icon: 'icon-diploma',
groupKey: 'demo-block-group-id',
},
{
label: 'Two Column Layout',
contentElementTypeKey: 'two-column-layout-umbraco-demo-block-id',
editorSize: 'medium',
icon: 'icon-book-alt',
groupKey: 'demo-block-group-id',
},
],
},
],

View File

@@ -1198,4 +1198,302 @@ export const data: Array<UmbMockDocumentTypeModel> = [
keepLatestVersionPerDayForDays: null,
},
},
{
type: 'document-type',
allowedTemplateIds: [],
defaultTemplateId: null,
id: 'folder-umbraco-demo-blocks-id',
alias: 'folderUmbracoDemoBlocks',
name: 'Umbraco Demo Blocks',
description: null,
icon: 'icon-folder',
allowedAsRoot: true,
variesByCulture: false,
variesBySegment: false,
isElement: false,
hasChildren: true,
isContainer: false,
parentId: null,
isFolder: true,
allowedContentTypes: [],
compositions: [],
cleanup: {
preventCleanup: false,
keepAllVersionsNewerThanDays: null,
keepLatestVersionPerDayForDays: null,
},
properties: [],
containers: [],
},
{
type: 'document-type',
allowedTemplateIds: [],
defaultTemplateId: null,
id: 'coffee-umbraco-demo-block-id',
alias: 'coffeeUmbracoDemoBlock',
name: 'Favorite Coffee',
description: null,
icon: 'icon-coffee',
allowedAsRoot: true,
variesByCulture: false,
variesBySegment: false,
isElement: true,
hasChildren: false,
isContainer: false,
parentId: 'folder-umbraco-demo-blocks-id',
isFolder: false,
allowedContentTypes: [],
compositions: [],
cleanup: {
preventCleanup: false,
keepAllVersionsNewerThanDays: null,
keepLatestVersionPerDayForDays: null,
},
properties: [
{
id: 'coffee-name-id',
containerId: 'coffee-content-group-key',
alias: 'coffeeName',
name: 'Name of Coffee',
description: '',
dataTypeId: 'dt-textBox',
variesByCulture: false,
variesBySegment: false,
sortOrder: 10,
validation: {
mandatory: true,
mandatoryMessage: null,
regEx: null,
regExMessage: null,
},
appearance: {
labelOnTop: false,
},
},
{
id: 'coffee-size-id',
containerId: 'coffee-content-group-key',
alias: 'coffeeSize',
name: 'Amount (deciliter)',
description: '',
dataTypeId: 'dt-integer',
variesByCulture: false,
variesBySegment: false,
sortOrder: 10,
validation: {
mandatory: true,
mandatoryMessage: null,
regEx: null,
regExMessage: null,
},
appearance: {
labelOnTop: false,
},
},
],
containers: [
{
id: 'coffee-content-group-key',
parentId: null,
name: 'Content',
type: 'Group',
sortOrder: 0,
},
],
},
{
type: 'document-type',
allowedTemplateIds: [],
defaultTemplateId: null,
id: 'headline-umbraco-demo-block-id',
alias: 'headlineUmbracoDemoBlock',
name: 'Headline',
description: null,
icon: 'icon-edit',
allowedAsRoot: true,
variesByCulture: false,
variesBySegment: false,
isElement: true,
hasChildren: false,
isContainer: false,
parentId: 'folder-umbraco-demo-blocks-id',
isFolder: false,
allowedContentTypes: [],
compositions: [],
cleanup: {
preventCleanup: false,
keepAllVersionsNewerThanDays: null,
keepLatestVersionPerDayForDays: null,
},
properties: [
{
id: 'headline-id',
containerId: 'headline-content-group-key',
alias: 'headline',
name: 'Headline',
description: '',
dataTypeId: 'dt-textBox',
variesByCulture: false,
variesBySegment: false,
sortOrder: 10,
validation: {
mandatory: true,
mandatoryMessage: null,
regEx: null,
regExMessage: null,
},
appearance: {
labelOnTop: false,
},
},
],
containers: [
{
id: 'headline-content-group-key',
parentId: null,
name: 'Content',
type: 'Group',
sortOrder: 0,
},
],
},
{
type: 'document-type',
allowedTemplateIds: [],
defaultTemplateId: null,
id: 'image-umbraco-demo-block-id',
alias: 'imageUmbracoDemoBlock',
name: 'Image',
description: null,
icon: 'icon-picture',
allowedAsRoot: true,
variesByCulture: false,
variesBySegment: false,
isElement: true,
hasChildren: false,
isContainer: false,
parentId: 'folder-umbraco-demo-blocks-id',
isFolder: false,
allowedContentTypes: [],
compositions: [],
cleanup: {
preventCleanup: false,
keepAllVersionsNewerThanDays: null,
keepLatestVersionPerDayForDays: null,
},
properties: [
{
id: 'image-id',
containerId: 'image-content-group-key',
alias: 'image',
name: 'Image',
description: '',
dataTypeId: 'dt-mediaPicker',
variesByCulture: false,
variesBySegment: false,
sortOrder: 10,
validation: {
mandatory: true,
mandatoryMessage: null,
regEx: null,
regExMessage: null,
},
appearance: {
labelOnTop: false,
},
},
],
containers: [
{
id: 'image-content-group-key',
parentId: null,
name: 'Content',
type: 'Group',
sortOrder: 0,
},
],
},
{
type: 'document-type',
allowedTemplateIds: [],
defaultTemplateId: null,
id: 'rich-text-umbraco-demo-block-id',
alias: 'richTextUmbracoDemoBlock',
name: 'Rich Text',
description: null,
icon: 'icon-diploma',
allowedAsRoot: true,
variesByCulture: false,
variesBySegment: false,
isElement: true,
hasChildren: false,
isContainer: false,
parentId: 'folder-umbraco-demo-blocks-id',
isFolder: false,
allowedContentTypes: [],
compositions: [],
cleanup: {
preventCleanup: false,
keepAllVersionsNewerThanDays: null,
keepLatestVersionPerDayForDays: null,
},
properties: [
{
id: 'rich-text-id',
containerId: 'rich-text-content-group-key',
alias: 'richText',
name: 'Text',
description: '',
dataTypeId: 'dt-richTextEditor',
variesByCulture: false,
variesBySegment: false,
sortOrder: 10,
validation: {
mandatory: true,
mandatoryMessage: null,
regEx: null,
regExMessage: null,
},
appearance: {
labelOnTop: false,
},
},
],
containers: [
{
id: 'rich-text-content-group-key',
parentId: null,
name: 'Content',
type: 'Group',
sortOrder: 0,
},
],
},
{
type: 'document-type',
allowedTemplateIds: [],
defaultTemplateId: null,
id: 'two-column-layout-umbraco-demo-block-id',
alias: 'twoColumnLayoutUmbracoDemoBlock',
name: 'Two Column Layout',
description: null,
icon: 'icon-book-alt',
allowedAsRoot: true,
variesByCulture: false,
variesBySegment: false,
isElement: true,
hasChildren: false,
isContainer: false,
parentId: 'folder-umbraco-demo-blocks-id',
isFolder: false,
allowedContentTypes: [],
compositions: [],
cleanup: {
preventCleanup: false,
keepAllVersionsNewerThanDays: null,
keepLatestVersionPerDayForDays: null,
},
properties: [],
containers: [],
},
];

View File

@@ -33,6 +33,9 @@ export class UmbPropertyEditorUIBlockListBlockElement extends UmbLitElement impl
this.observe(this.#context.label, (label) => {
this._label = label;
});
this.observe(this.#context.layout, (layout) => {
console.log('layout', layout);
});
}
#requestDelete() {
@@ -81,8 +84,9 @@ export class UmbPropertyEditorUIBlockListBlockElement extends UmbLitElement impl
}
uui-action-bar {
position: absolute;
top: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
right: var(--uui-size-2);
}
`,
];

View File

@@ -1,10 +1,11 @@
import { UMB_BLOCK_LIST_PROPERTY_EDITOR_ALIAS } from './manifests.js';
import { html, customElement, property, state, styleMap, repeat, css } from '@umbraco-cms/backoffice/external/lit';
import { html, customElement, property, state, repeat, css, nothing } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
import {
UMB_BLOCK_CATALOGUE_MODAL,
UmbBlockLayoutBaseModel,
UmbBlockManagerContext,
UmbBlockTypeBase,
@@ -14,6 +15,7 @@ import '../../components/block-list-block/index.js';
import { buildUdi } from '@umbraco-cms/backoffice/utils';
import { UmbId } from '@umbraco-cms/backoffice/id';
import type { NumberRangeValueType } from '@umbraco-cms/backoffice/models';
import { UMB_MODAL_MANAGER_CONTEXT_TOKEN, UmbModalManagerContext } from '@umbraco-cms/backoffice/modal';
export interface UmbBlockListLayoutModel extends UmbBlockLayoutBaseModel {}
@@ -67,13 +69,23 @@ export class UmbPropertyEditorUIBlockListElement extends UmbLitElement implement
@state()
private _limitMax?: number;
@state()
private _blocks?: Array<UmbBlockTypeBase>;
#context = new UmbBlockManagerContext(this);
@state()
_layouts: Array<UmbBlockLayoutBaseModel> = [];
#modalContext?: UmbModalManagerContext;
constructor() {
super();
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT_TOKEN, (instance) => {
this.#modalContext = instance;
});
// TODO: Prevent initial notification from these observes:
this.observe(this.#context.layouts, (layouts) => {
this._value.layout[UMB_BLOCK_LIST_PROPERTY_EDITOR_ALIAS] = layouts;
@@ -93,24 +105,34 @@ export class UmbPropertyEditorUIBlockListElement extends UmbLitElement implement
// Notify that the value has changed.
//console.log('settings changed', this._value);
});
this.observe(this.#context.blockTypes, (blockTypes) => {
this._blocks = blockTypes;
});
}
#openBlockCatalogue() {
// Open modal.
async #openBlockCatalogue(openClipboard: boolean = false) {
//Open modal
const modalContext = this.#modalContext?.open(UMB_BLOCK_CATALOGUE_MODAL, {
data: { blocks: this._blocks ?? [], openClipboard },
});
// TEMP Hack:
const data = await modalContext?.onSubmit();
const contentElementTypeKey = this.#context.getBlockTypes()[0]!.contentElementTypeKey;
/**TODO: Insert next modal for data */
console.log('submitted', data);
const contentUdi = buildUdi('element', UmbId.new());
const settingsUdi = buildUdi('element', UmbId.new());
if (!data) return;
const block = this._blocks?.find((x) => x.contentElementTypeKey === data.key);
if (!block?.contentElementTypeKey) return;
this.#context.createBlock(
{
contentUdi,
settingsUdi,
contentUdi: buildUdi('element', UmbId.new()),
settingsUdi: buildUdi('element', UmbId.new()),
},
contentElementTypeKey,
block.contentElementTypeKey,
);
}
@@ -119,21 +141,46 @@ export class UmbPropertyEditorUIBlockListElement extends UmbLitElement implement
this._layouts,
(x) => x.contentUdi,
(layoutEntry) =>
html` <uui-button-inline-create></uui-button-inline-create>
html`<uui-button-inline-create></uui-button-inline-create>
<umb-property-editor-ui-block-list-block .layout=${layoutEntry}>
</umb-property-editor-ui-block-list-block>`,
</umb-property-editor-ui-block-list-block> `,
)}
<uui-button id="add-button" look="placeholder" @click=${this.#openBlockCatalogue} label="open">Add</uui-button>`;
<uui-button-group>
<uui-button
id="add-button"
look="placeholder"
label=${this.localize.term('content_createEmpty')}
@click=${() => this.#openBlockCatalogue()}>
${this.localize.term('content_createEmpty')}
</uui-button>
<uui-button
label=${this.localize.term('content_createFromClipboard')}
look="placeholder"
@click=${() => this.#openBlockCatalogue(true)}>
<uui-icon name="icon-paste-in"></uui-icon>
</uui-button>
</uui-button-group>`;
}
static styles = [
UmbTextStyles,
css`
:host {
display: grid;
gap: 1px;
}
> div {
display: flex;
flex-direction: column;
align-items: stretch;
}
uui-button-group {
padding-top: 1px;
display: grid;
grid-template-columns: 1fr auto;
}
`,
];
}

View File

@@ -9,4 +9,14 @@ export interface UmbBlockTypeBase {
iconColor?: string;
backgroundColor?: string;
editorSize?: UUIModalSidebarSize;
icon?: string; // remove later
}
export interface UmbBlockTypeGroup {
name?: string | null;
key: string;
}
export interface UmbBlockTypeWithGroupKey extends UmbBlockTypeBase {
groupKey?: string | null;
}

View File

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

View File

@@ -0,0 +1,3 @@
import { manifests as modalManifests } from './modals/manifests.js';
export const manifests = [...modalManifests];

View File

@@ -0,0 +1,129 @@
import {
UmbBlockCatalogueModalData,
UmbBlockCatalogueModalValue,
UmbBlockTypeWithGroupKey,
} from '@umbraco-cms/backoffice/block';
import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/document';
import { css, html, customElement, state, repeat, ifDefined, nothing } from '@umbraco-cms/backoffice/external/lit';
import { groupBy } from '@umbraco-cms/backoffice/external/lodash';
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
@customElement('umb-block-catalogue-modal')
export class UmbBlockCatalogueModalElement extends UmbModalBaseElement<
UmbBlockCatalogueModalData,
UmbBlockCatalogueModalValue
> {
@state()
private _blocks: Array<UmbBlockTypeWithGroupKey> = [];
@state()
private _blockGroups: Array<{ key: string; name: string }> = [];
@state()
openClipboard?: boolean;
connectedCallback() {
super.connectedCallback();
if (!this.data) return;
this.openClipboard = this.data.openClipboard ?? false;
this._blocks = this.data.blocks ?? [];
this._blockGroups = this.data.blockGroups ?? [];
}
#onClickBlock(contentElementTypeKey: string) {
this.modalContext?.updateValue({ key: contentElementTypeKey });
this.modalContext?.submit();
}
render() {
return html`
<umb-body-layout headline="${this.localize.term('blockEditor_addBlock')}">
${this.#renderViews()} ${this.openClipboard ? this.#renderClipboard() : this.#renderCreateEmpty()}
<div slot="actions">
<uui-button label=${this.localize.term('general_close')} @click=${this._rejectModal}></uui-button>
<uui-button
label=${this.localize.term('general_submit')}
look="primary"
color="positive"
@click=${this._submitModal}></uui-button>
</div>
</umb-body-layout>
`;
}
#renderClipboard() {
return html`Clipboard`;
}
#renderCreateEmpty() {
const blockArrays = groupBy(this._blocks, 'groupKey');
const mappedGroupsAndBlocks = Object.entries(blockArrays).map(([key, value]) => {
const group = this._blockGroups.find((group) => group.key === key);
return { name: group?.name, blocks: value };
});
return html`
${mappedGroupsAndBlocks.map(
(group) => html`
${group.name ? html`<h2>${group.name}</h2>` : nothing}
<div class="blockGroup">
${repeat(
group.blocks,
(block) => block.contentElementTypeKey,
(block) => html`
<uui-card-block-type
name=${ifDefined(block.label)}
background=${ifDefined(block.backgroundColor)}
style="color: ${block.iconColor}"
@open=${() => this.#onClickBlock(block.contentElementTypeKey)}>
<uui-icon .name=${block.icon ?? ''}></uui-icon>
</uui-card-block-type>
`,
)}
</div>
`,
)}
`;
}
#renderViews() {
return html`
<uui-tab-group slot="navigation">
<uui-tab label="Create Empty" ?active=${!this.openClipboard} @click=${() => (this.openClipboard = false)}>
Create Empty
<uui-icon slot="icon" name="icon-add"></uui-icon>
</uui-tab>
<uui-tab label="Clipboard" ?active=${this.openClipboard} @click=${() => (this.openClipboard = true)}>
Clipboard
<uui-icon slot="icon" name="icon-paste-in"></uui-icon>
</uui-tab>
</uui-tab-group>
`;
}
static styles = [
css`
.blockGroup {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(min(150px, 100%), 1fr));
}
uui-tab-group {
--uui-tab-divider: var(--uui-color-border);
border-left: 1px solid var(--uui-color-border);
border-right: 1px solid var(--uui-color-border);
}
`,
];
}
export default UmbBlockCatalogueModalElement;
declare global {
interface HTMLElementTagNameMap {
'umb-block-catalogue-modal': UmbBlockCatalogueModalElement;
}
}

View File

@@ -0,0 +1,22 @@
import { UmbBlockTypeBase } from '@umbraco-cms/backoffice/block';
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export interface UmbBlockCatalogueModalData {
blocks: Array<UmbBlockTypeBase>;
blockGroups?: Array<{ name: string; key: string }>;
openClipboard?: boolean;
}
export interface UmbBlockCatalogueModalValue {
key: string;
}
export const UMB_BLOCK_CATALOGUE_MODAL = new UmbModalToken<UmbBlockCatalogueModalData, UmbBlockCatalogueModalValue>(
'Umb.Modal.BlockCatalogue',
{
modal: {
type: 'sidebar',
size: 'small',
},
},
);

View File

@@ -0,0 +1,2 @@
export * from './block-catalogue-modal.element.js';
export * from './block-catalogue-modal.token.js';

View File

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

View File

@@ -0,0 +1,12 @@
import type { ManifestModal } from '@umbraco-cms/backoffice/extension-registry';
const modals: Array<ManifestModal> = [
{
type: 'modal',
alias: 'Umb.Modal.BlockCatalogue',
name: 'Block Catalogue Modal',
js: () => import('./block-catalogue/block-catalogue-modal.element.js'),
},
];
export const manifests = [...modals];

View File

@@ -1,3 +1,4 @@
export * from './block/index.js';
export * from './block-grid/index.js';
export * from './block-list/index.js';
export * from './block-manager/index.js';

View File

@@ -1,6 +1,13 @@
import { manifests as blockManifests } from './block/manifests.js';
import { manifests as blockGridManifests } from './block-grid/manifests.js';
import { manifests as blockListManifests } from './block-list/manifests.js';
import { manifests as blockRteManifests } from './block-rte/manifests.js';
import { manifests as blockTypeManifests } from './block-type/manifests.js';
export const manifests = [...blockTypeManifests, ...blockListManifests, ...blockGridManifests, ...blockRteManifests];
export const manifests = [
...blockManifests,
...blockTypeManifests,
...blockListManifests,
...blockGridManifests,
...blockRteManifests,
];