diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/field-dropdown-list/field-dropdown-list.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/field-dropdown-list/field-dropdown-list.element.ts new file mode 100644 index 0000000000..f469b3953d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/field-dropdown-list/field-dropdown-list.element.ts @@ -0,0 +1,216 @@ +import type { UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type'; +import { UmbDocumentTypeDetailRepository } from '@umbraco-cms/backoffice/document-type'; +import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; +import { + css, + html, + customElement, + property, + state, + repeat, + ifDefined, + nothing, + query, +} from '@umbraco-cms/backoffice/external/lit'; +import type { UUIComboboxEvent, UUIComboboxElement } from '@umbraco-cms/backoffice/external/uui'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbMediaTypeDetailRepository } from '@umbraco-cms/backoffice/media-type'; +import { + UMB_DOCUMENT_TYPE_PICKER_MODAL, + UMB_MEDIA_TYPE_PICKER_MODAL, + UMB_MODAL_MANAGER_CONTEXT, + type UmbModalManagerContext, +} from '@umbraco-cms/backoffice/modal'; + +interface FieldPickerValue { + alias: string; + label: string; +} + +enum FieldType { + MEDIA_TYPE = 'media-type', + DOCUMENT_TYPE = 'document-type', + SYSTEM = 'system', +} + +@customElement('umb-field-dropdown-list') +export class UmbFieldDropdownListElement extends UmbLitElement { + @property({ type: Boolean, attribute: 'exclude-media-type', reflect: true }) + public excludeMediaType = false; + + private _value: FieldPickerValue | undefined; + @property({ type: Object }) + public get value(): FieldPickerValue | undefined { + return this._value; + } + public set value(val: FieldPickerValue | undefined) { + const oldVal = this._value; + this._value = val; + this.requestUpdate('value', oldVal); + this.dispatchEvent(new UmbChangeEvent()); + } + + @state() + private _type?: FieldType; + + @state() + private _uniqueName?: string; + + @state() + private _unique?: string; + + @query('#value') + private _valueElement?: UUIComboboxElement; + + #documentTypeDetailRepository = new UmbDocumentTypeDetailRepository(this); + #mediaTypeDetailRepository = new UmbMediaTypeDetailRepository(this); + #modalManager?: UmbModalManagerContext; + + @state() + private _customFields: Array> = []; + + private _systemFields: Array> = [ + { alias: 'sortOrder', name: this.localize.term('general_sort') }, + { alias: 'updateDate', name: this.localize.term('content_updateDate') }, + { alias: 'updater', name: this.localize.term('content_updatedBy') }, + { alias: 'createDate', name: this.localize.term('content_createDate') }, + { alias: 'owner', name: this.localize.term('content_createBy') }, + { alias: 'published', name: this.localize.term('content_isPublished') }, + { alias: 'contentTypeAlias', name: this.localize.term('content_documentType') }, + ]; + + constructor() { + super(); + this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (modalManager) => { + this.#modalManager = modalManager; + }); + } + + async #getDocumentTypeFields() { + if (!this.#modalManager) return; + const modalContext = this.#modalManager.open(UMB_DOCUMENT_TYPE_PICKER_MODAL, { + data: { + hideTreeRoot: true, + multiple: false, + }, + }); + + const modalValue = await modalContext.onSubmit(); + const unique = modalValue.selection[0] ?? ''; + + const { data } = await this.#documentTypeDetailRepository.requestByUnique(unique); + if (!data) return; + + this._unique = data.unique; + this._uniqueName = data.name; + this._customFields = data.properties; + } + + async #getMediaTypeFields() { + if (!this.#modalManager) return; + const modalContext = this.#modalManager.open(UMB_MEDIA_TYPE_PICKER_MODAL, { + data: { + hideTreeRoot: true, + multiple: false, + }, + }); + + const modalValue = await modalContext.onSubmit(); + const unique = modalValue.selection[0] ?? ''; + + const { data } = await this.#mediaTypeDetailRepository.requestByUnique(unique); + if (!data) return; + + this._unique = data.unique; + this._uniqueName = data.name; + this._customFields = data.properties; + } + + #onChange(e: UUIComboboxEvent) { + this._type = (e.composedPath()[0] as UUIComboboxElement).value as FieldType; + this.value = undefined; + if (this._valueElement) this._valueElement.value = ''; + + switch (this._type) { + case FieldType.DOCUMENT_TYPE: + this.#getDocumentTypeFields(); + break; + case FieldType.MEDIA_TYPE: + this.#getMediaTypeFields(); + break; + default: + this._uniqueName = ''; + this._unique = ''; + this._customFields = this._systemFields; + break; + } + } + + #onChangeValue(e: UUIComboboxEvent) { + e.stopPropagation(); + const alias = (e.composedPath()[0] as UUIComboboxElement).value as FieldType; + this.value = this._customFields.find((field) => field.alias === alias) as FieldPickerValue; + } + + render() { + return html` + + + + ${this.localize.term('formSettings_systemFields')} + + + ${this.localize.term('content_documentType')} + ${this.localize.term('defaultdialogs_treepicker')} + + ${!this.excludeMediaType + ? html` + ${this.localize.term('content_mediatype')} + ${this.localize.term('defaultdialogs_treepicker')} + ` + : nothing} + + + ${this.#renderAliasDropdown()} + `; + } + + #renderAliasDropdown() { + if (this._type !== FieldType.SYSTEM && !this._unique) return; + return html`${this._uniqueName} + + + ${repeat( + this._customFields, + (field) => field.alias, + (field) => + html`${field.alias}`, + )} + + `; + } + + static styles = [ + css` + uui-combobox { + width: 100%; + } + strong { + display: block; + } + uui-combobox-list-option { + padding: calc(var(--uui-size-2, 6px) + 1px); + } + `, + ]; +} + +export default UmbFieldDropdownListElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-field-dropdown-list': UmbFieldDropdownListElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/field-dropdown-list/field-dropdown-list.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/field-dropdown-list/field-dropdown-list.stories.ts new file mode 100644 index 0000000000..f9e84c4665 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/field-dropdown-list/field-dropdown-list.stories.ts @@ -0,0 +1,17 @@ +import type { Meta, StoryObj } from '@storybook/web-components'; +import './field-dropdown-list.element.js'; +import type { UmbFieldDropdownListElement } from './field-dropdown-list.element.js'; + +const meta: Meta = { + title: 'Components/Inputs/Field Dropdown List', + component: 'umb-field-dropdown-list', +}; + +export default meta; +type Story = StoryObj; + +export const Overview: Story = { + args: { + excludeMediaType: false, + }, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/field-dropdown-list/field-dropdown-list.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/field-dropdown-list/field-dropdown-list.test.ts new file mode 100644 index 0000000000..2f741562a7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/field-dropdown-list/field-dropdown-list.test.ts @@ -0,0 +1,20 @@ +import { expect, fixture, html } from '@open-wc/testing'; +import { UmbFieldDropdownListElement } from './field-dropdown-list.element.js'; +import { type UmbTestRunnerWindow, defaultA11yConfig } from '@umbraco-cms/internal/test-utils'; +describe('UmbInputDateElement', () => { + let element: UmbFieldDropdownListElement; + + beforeEach(async () => { + element = await fixture(html` `); + }); + + it('is defined with its own instance', () => { + expect(element).to.be.instanceOf(UmbFieldDropdownListElement); + }); + + if ((window as UmbTestRunnerWindow).__UMBRACO_TEST_RUN_A11Y_TEST) { + it('passes the a11y audit', async () => { + await expect(element).shadowDom.to.be.accessible(defaultA11yConfig); + }); + } +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/field-dropdown-list/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/field-dropdown-list/index.ts new file mode 100644 index 0000000000..2864ccf8fc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/field-dropdown-list/index.ts @@ -0,0 +1 @@ +export * from './field-dropdown-list.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts index dc4b249b61..ea96ecaaaa 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts @@ -7,6 +7,7 @@ export * from './code-block/index.js'; export * from './dropdown/index.js'; export * from './entity-actions-bundle/index.js'; export * from './extension-slot/index.js'; +export * from './field-dropdown-list/index.js'; export * from './footer-layout/index.js'; export * from './header-app/index.js'; export * from './history/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/publish.action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/publish.action.ts index f5b25bef73..c78c81227a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/publish.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/publish.action.ts @@ -1,17 +1,17 @@ import { umbPickDocumentVariantModal } from '../modals/pick-document-variant-modal.controller.js'; -import { type UmbDocumentDetailRepository, UmbDocumentPublishingRepository } from '../repository/index.js'; +import { UmbDocumentDetailRepository, UmbDocumentPublishingRepository } from '../repository/index.js'; import { UmbDocumentVariantState } from '../types.js'; import { UmbLanguageCollectionRepository } from '@umbraco-cms/backoffice/language'; import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action'; import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; -export class UmbPublishDocumentEntityAction extends UmbEntityActionBase { +export class UmbPublishDocumentEntityAction extends UmbEntityActionBase { async execute() { - if (!this.repository) throw new Error('Document repository not set'); - const languageRepository = new UmbLanguageCollectionRepository(this._host); const { data: languageData } = await languageRepository.requestCollection({}); - const { data: documentData } = await this.repository.requestByUnique(this.unique); + + const documentRepository = new UmbDocumentDetailRepository(this._host); + const { data: documentData } = await documentRepository.requestByUnique(this.unique); if (!documentData) throw new Error('The document was not found'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/unpublish.action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/unpublish.action.ts index 4eebb4348c..03b6925fd2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/unpublish.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/unpublish.action.ts @@ -1,17 +1,17 @@ import { umbPickDocumentVariantModal } from '../modals/pick-document-variant-modal.controller.js'; -import { type UmbDocumentDetailRepository, UmbDocumentPublishingRepository } from '../repository/index.js'; +import { UmbDocumentDetailRepository, UmbDocumentPublishingRepository } from '../repository/index.js'; import { UmbDocumentVariantState } from '../types.js'; import { UmbLanguageCollectionRepository } from '@umbraco-cms/backoffice/language'; import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action'; import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; -export class UmbUnpublishDocumentEntityAction extends UmbEntityActionBase { +export class UmbUnpublishDocumentEntityAction extends UmbEntityActionBase { async execute() { - if (!this.repository) throw new Error('Document repository not set'); - const languageRepository = new UmbLanguageCollectionRepository(this._host); const { data: languageData } = await languageRepository.requestCollection({}); - const { data: documentData } = await this.repository.requestByUnique(this.unique); + + const documentRepository = new UmbDocumentDetailRepository(this._host); + const { data: documentData } = await documentRepository.requestByUnique(this.unique); if (!documentData) throw new Error('The document was not found'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/manifests.ts index fda8b13029..f1fc3d5a4a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/manifests.ts @@ -1,5 +1,5 @@ import type { UmbCollectionBulkActionPermissions } from '../../../core/collection/types.js'; -import { UMB_DOCUMENT_DETAIL_REPOSITORY_ALIAS } from '../repository/index.js'; +import { UMB_DOCUMENT_DETAIL_REPOSITORY_ALIAS, UMB_DOCUMENT_PUBLISHING_REPOSITORY_ALIAS } from '../repository/index.js'; import { UMB_DOCUMENT_COLLECTION_ALIAS } from '../collection/index.js'; import { UmbDocumentCopyEntityBulkAction } from './copy/copy.action.js'; import { UmbDocumentDeleteEntityBulkAction } from './delete/delete.action.js'; @@ -21,7 +21,7 @@ export const manifests: Array = [ api: UmbDocumentPublishEntityBulkAction, meta: { label: 'Publish', - repositoryAlias: UMB_DOCUMENT_DETAIL_REPOSITORY_ALIAS, + repositoryAlias: UMB_DOCUMENT_PUBLISHING_REPOSITORY_ALIAS, }, conditions: [ { @@ -42,7 +42,7 @@ export const manifests: Array = [ api: UmbDocumentUnpublishEntityBulkAction, meta: { label: 'Unpublish', - repositoryAlias: UMB_DOCUMENT_DETAIL_REPOSITORY_ALIAS, + repositoryAlias: UMB_DOCUMENT_PUBLISHING_REPOSITORY_ALIAS, }, conditions: [ { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/publish/publish.action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/publish/publish.action.ts index c20657de25..4c5afc39c4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/publish/publish.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/publish/publish.action.ts @@ -1,14 +1,36 @@ -import type { UmbDocumentDetailRepository } from '../../repository/index.js'; +import type { UmbDocumentPublishingRepository } from '../../repository/index.js'; +import { UmbPublishDocumentEntityAction } from '../../entity-actions/publish.action.js'; +import type { UmbDocumentVariantOptionModel } from '../../types.js'; +import { umbPickDocumentVariantModal } from '../../modals/index.js'; import { UmbEntityBulkActionBase } from '@umbraco-cms/backoffice/entity-bulk-action'; -import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; - -export class UmbDocumentPublishEntityBulkAction extends UmbEntityBulkActionBase { - constructor(host: UmbControllerHostElement, repositoryAlias: string, selection: Array) { - super(host, repositoryAlias, selection); - } +import { UmbLanguageCollectionRepository } from '@umbraco-cms/backoffice/language'; +import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +export class UmbDocumentPublishEntityBulkAction extends UmbEntityBulkActionBase { async execute() { - console.log(`execute publish for: ${this.selection}`); - //await this.repository?.publish(); + // If there is only one selection, we can refer to the regular publish entity action: + if (this.selection.length === 1) { + const action = new UmbPublishDocumentEntityAction(this._host, '', this.selection[0], ''); + await action.execute(); + return; + } + + if (!this.repository) throw new Error('Document publishing repository not set'); + + const languageRepository = new UmbLanguageCollectionRepository(this._host); + const { data: languageData } = await languageRepository.requestCollection({}); + + const options: UmbDocumentVariantOptionModel[] = (languageData?.items ?? []).map((language) => ({ + language, + unique: new UmbVariantId(language.unique, null).toString(), + })); + + const selectedVariants = await umbPickDocumentVariantModal(this, { type: 'publish', options }); + + if (selectedVariants.length) { + for (const unique of this.selection) { + await this.repository.publish(unique, selectedVariants); + } + } } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/unpublish/unpublish.action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/unpublish/unpublish.action.ts index 03e9d01c7a..27137e073e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/unpublish/unpublish.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/unpublish/unpublish.action.ts @@ -1,14 +1,36 @@ -import type { UmbDocumentDetailRepository } from '../../repository/index.js'; +import { UmbUnpublishDocumentEntityAction } from '../../entity-actions/unpublish.action.js'; +import { umbPickDocumentVariantModal } from '../../modals/index.js'; +import type { UmbDocumentPublishingRepository } from '../../repository/index.js'; +import type { UmbDocumentVariantOptionModel } from '../../types.js'; import { UmbEntityBulkActionBase } from '@umbraco-cms/backoffice/entity-bulk-action'; -import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; - -export class UmbDocumentUnpublishEntityBulkAction extends UmbEntityBulkActionBase { - constructor(host: UmbControllerHostElement, repositoryAlias: string, selection: Array) { - super(host, repositoryAlias, selection); - } +import { UmbLanguageCollectionRepository } from '@umbraco-cms/backoffice/language'; +import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +export class UmbDocumentUnpublishEntityBulkAction extends UmbEntityBulkActionBase { async execute() { - console.log(`execute unpublish for: ${this.selection}`); - //await this.repository?.unpublish(); + // If there is only one selection, we can refer to the regular publish entity action: + if (this.selection.length === 1) { + const action = new UmbUnpublishDocumentEntityAction(this._host, '', this.selection[0], ''); + await action.execute(); + return; + } + + if (!this.repository) throw new Error('Document publishing repository not set'); + + const languageRepository = new UmbLanguageCollectionRepository(this._host); + const { data: languageData } = await languageRepository.requestCollection({}); + + const options: UmbDocumentVariantOptionModel[] = (languageData?.items ?? []).map((language) => ({ + language, + unique: new UmbVariantId(language.unique, null).toString(), + })); + + const selectedVariants = await umbPickDocumentVariantModal(this, { type: 'unpublish', options }); + + if (selectedVariants.length) { + for (const unique of this.selection) { + await this.repository.unpublish(unique, selectedVariants); + } + } } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/modals/templating-page-field-builder/templating-page-field-builder-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/modals/templating-page-field-builder/templating-page-field-builder-modal.element.ts index 37afe148ca..da381042ae 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/modals/templating-page-field-builder/templating-page-field-builder-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/modals/templating-page-field-builder/templating-page-field-builder-modal.element.ts @@ -6,6 +6,7 @@ import type { import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import type { UmbFieldDropdownListElement } from '@umbraco-cms/backoffice/components'; @customElement('umb-templating-page-field-builder-modal') export class UmbTemplatingPageFieldBuilderModalElement extends UmbModalBaseElement< @@ -36,6 +37,10 @@ export class UmbTemplatingPageFieldBuilderModalElement extends UmbModalBaseEleme /** TODO: Implement "Choose field" */ + #onChangeFieldValue(e: Event) { + this._field = (e.target as UmbFieldDropdownListElement).value?.alias; + } + render() { return html` @@ -44,7 +49,7 @@ export class UmbTemplatingPageFieldBuilderModalElement extends UmbModalBaseEleme Choose field - (Not implemented yet) + Default value