Merge pull request #1247 from umbraco/feature/save-publish-variants

Save & Save & Publish modals, part 1
This commit is contained in:
Jacob Overgaard
2024-02-21 09:07:46 +01:00
committed by GitHub
30 changed files with 584 additions and 90 deletions

View File

@@ -76,6 +76,7 @@ export type { CreateTemplateRequestModel } from './models/CreateTemplateRequestM
export type { CreateUserGroupRequestModel } from './models/CreateUserGroupRequestModel';
export type { CreateUserRequestModel } from './models/CreateUserRequestModel';
export type { CreateUserResponseModel } from './models/CreateUserResponseModel';
export type { CultureAndScheduleRequestModel } from './models/CultureAndScheduleRequestModel';
export type { CultureReponseModel } from './models/CultureReponseModel';
export type { CurrentUserResponseModel } from './models/CurrentUserResponseModel';
export type { CurrenUserConfigurationResponseModel } from './models/CurrenUserConfigurationResponseModel';
@@ -264,6 +265,7 @@ export type { PagedMediaCollectionResponseModel } from './models/PagedMediaColle
export type { PagedMediaRecycleBinItemResponseModel } from './models/PagedMediaRecycleBinItemResponseModel';
export type { PagedMediaTreeItemResponseModel } from './models/PagedMediaTreeItemResponseModel';
export type { PagedMediaTypeTreeItemResponseModel } from './models/PagedMediaTypeTreeItemResponseModel';
export type { PagedMemberResponseModel } from './models/PagedMemberResponseModel';
export type { PagedNamedEntityTreeItemResponseModel } from './models/PagedNamedEntityTreeItemResponseModel';
export type { PagedObjectTypeResponseModel } from './models/PagedObjectTypeResponseModel';
export type { PagedPackageDefinitionResponseModel } from './models/PagedPackageDefinitionResponseModel';
@@ -320,6 +322,7 @@ export { RuntimeModeModel } from './models/RuntimeModeModel';
export type { SavedLogSearchPresenationBaseModel } from './models/SavedLogSearchPresenationBaseModel';
export type { SavedLogSearchRequestModel } from './models/SavedLogSearchRequestModel';
export type { SavedLogSearchResponseModel } from './models/SavedLogSearchResponseModel';
export type { ScheduleRequestModel } from './models/ScheduleRequestModel';
export type { ScriptFolderResponseModel } from './models/ScriptFolderResponseModel';
export type { ScriptItemResponseModel } from './models/ScriptItemResponseModel';
export type { ScriptResponseModel } from './models/ScriptResponseModel';

View File

@@ -0,0 +1,12 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ScheduleRequestModel } from './ScheduleRequestModel';
export type CultureAndScheduleRequestModel = {
culture?: string | null;
schedule?: ScheduleRequestModel | null;
};

View File

@@ -0,0 +1,12 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { MemberResponseModel } from './MemberResponseModel';
export type PagedMemberResponseModel = {
total: number;
items: Array<MemberResponseModel>;
};

View File

@@ -3,7 +3,9 @@
/* tslint:disable */
/* eslint-disable */
import type { CultureAndScheduleRequestModel } from './CultureAndScheduleRequestModel';
export type PublishDocumentRequestModel = {
cultures: Array<string>;
publishSchedules: Array<CultureAndScheduleRequestModel>;
};

View File

@@ -3,9 +3,8 @@
/* tslint:disable */
/* eslint-disable */
import type { PublishDocumentRequestModel } from './PublishDocumentRequestModel';
export type PublishDocumentWithDescendantsRequestModel = (PublishDocumentRequestModel & {
export type PublishDocumentWithDescendantsRequestModel = {
includeUnpublishedDescendants: boolean;
});
cultures: Array<string>;
};

View File

@@ -0,0 +1,10 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ScheduleRequestModel = {
publishTime?: string | null;
unpublishTime?: string | null;
};

View File

@@ -459,7 +459,7 @@ export class DocumentResource {
requestBody,
}: {
id: string,
requestBody?: (PublishDocumentRequestModel | PublishDocumentWithDescendantsRequestModel),
requestBody?: PublishDocumentRequestModel,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'PUT',

View File

@@ -3,8 +3,10 @@
/* tslint:disable */
/* eslint-disable */
import type { CreateMemberRequestModel } from '../models/CreateMemberRequestModel';
import type { DirectionModel } from '../models/DirectionModel';
import type { MemberItemResponseModel } from '../models/MemberItemResponseModel';
import type { MemberResponseModel } from '../models/MemberResponseModel';
import type { PagedMemberResponseModel } from '../models/PagedMemberResponseModel';
import type { UpdateMemberRequestModel } from '../models/UpdateMemberRequestModel';
import type { CancelablePromise } from '../core/CancelablePromise';
@@ -156,6 +158,44 @@ export class MemberResource {
});
}
/**
* @returns PagedMemberResponseModel Success
* @throws ApiError
*/
public static getMemberFilter({
memberTypeId,
orderBy = 'username',
orderDirection,
filter,
skip,
take = 100,
}: {
memberTypeId?: string,
orderBy?: string,
orderDirection?: DirectionModel,
filter?: string,
skip?: number,
take?: number,
}): CancelablePromise<PagedMemberResponseModel> {
return __request(OpenAPI, {
method: 'GET',
url: '/umbraco/management/api/v1/member/filter',
query: {
'memberTypeId': memberTypeId,
'orderBy': orderBy,
'orderDirection': orderDirection,
'filter': filter,
'skip': skip,
'take': take,
},
errors: {
400: `Bad Request`,
401: `The resource is protected and requires an authentication token`,
404: `Not Found`,
},
});
}
/**
* @returns any Success
* @throws ApiError

View File

@@ -617,7 +617,7 @@ export class UserResource {
}
/**
* @returns any Success
* @returns PagedUserResponseModel Success
* @throws ApiError
*/
public static getUserFilter({
@@ -636,7 +636,7 @@ export class UserResource {
userGroupIds?: Array<string>,
userStates?: Array<UserStateModel>,
filter?: string,
}): CancelablePromise<any> {
}): CancelablePromise<PagedUserResponseModel> {
return __request(OpenAPI, {
method: 'GET',
url: '/umbraco/management/api/v1/user/filter',
@@ -650,7 +650,9 @@ export class UserResource {
'filter': filter,
},
errors: {
400: `Bad Request`,
401: `The resource is protected and requires an authentication token`,
404: `Not Found`,
},
});
}

View File

@@ -17,7 +17,7 @@ export class UmbMockDocumentPublishingManager {
const document: UmbMockDocumentModel = this.#documentDb.detail.read(id);
document?.variants?.forEach((variant) => {
const hasCulture = variant.culture && data.cultures?.includes(variant.culture);
const hasCulture = variant.culture && data.publishSchedules.find((x) => x.culture === variant.culture);
if (hasCulture) {
variant.state = DocumentVariantStateModel.PUBLISHED;

View File

@@ -1,4 +1,8 @@
export type variantObject = { culture: string | null; segment: string | null };
export type variantObject = {
culture: string | null;
segment: string | null;
schedule?: { publishTime?: string | null; unpublishTime?: string | null };
};
export const UMB_INVARIANT_CULTURE = 'invariant';
@@ -13,10 +17,12 @@ export class UmbVariantId {
public readonly culture: string | null = null;
public readonly segment: string | null = null;
public readonly schedule: { publishTime?: string | null; unpublishTime?: string | null } | null = null;
constructor(variantData: variantObject) {
this.culture = (variantData.culture === UMB_INVARIANT_CULTURE ? null : variantData.culture) ?? null;
this.segment = variantData.segment ?? null;
this.schedule = variantData.schedule ?? null;
}
public compare(obj: variantObject): boolean {

View File

@@ -9,6 +9,7 @@ export * from './components/index.js';
export * from './entity.js';
export * from './entity-actions/index.js';
export * from './conditions/index.js';
export * from './modals/index.js';
export { UMB_DOCUMENT_TREE_ALIAS } from './tree/index.js';
export { UMB_CONTENT_MENU_ALIAS } from './menu.manifests.js';

View File

@@ -8,6 +8,7 @@ import { manifests as propertyEditorManifests } from './property-editors/manifes
import { manifests as recycleBinManifests } from './recycle-bin/manifests.js';
import { manifests as repositoryManifests } from './repository/manifests.js';
import { manifests as trackedReferenceManifests } from './tracked-reference/manifests.js';
import { manifests as modalManifests } from './modals/manifests.js';
import { manifests as treeManifests } from './tree/manifests.js';
import { manifests as userPermissionManifests } from './user-permissions/manifests.js';
import { manifests as workspaceManifests } from './workspace/manifests.js';
@@ -23,6 +24,7 @@ export const manifests = [
...recycleBinManifests,
...repositoryManifests,
...trackedReferenceManifests,
...modalManifests,
...treeManifests,
...userPermissionManifests,
...workspaceManifests,

View File

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

View File

@@ -0,0 +1,14 @@
import type { ManifestModal } from '@umbraco-cms/backoffice/extension-registry';
export const UMB_DOCUMENT_VARIANT_PICKER_MODAL_ALIAS = 'Umb.Modal.DocumentVariantPicker';
const modals: Array<ManifestModal> = [
{
type: 'modal',
alias: UMB_DOCUMENT_VARIANT_PICKER_MODAL_ALIAS,
name: 'Document Variant Picker Modal',
js: () => import('./variant-picker/document-variant-picker-modal.element.js'),
},
];
export const manifests = [...modals];

View File

@@ -0,0 +1,164 @@
import { type UmbDocumentVariantModel, UmbDocumentVariantState } from '../../types.js';
import type {
UmbDocumentVariantPickerModalValue,
UmbDocumentVariantPickerModalData,
} from './document-variant-picker-modal.token.js';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, customElement, repeat } from '@umbraco-cms/backoffice/external/lit';
import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils';
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
@customElement('umb-document-variant-picker-modal')
export class UmbDocumentVariantPickerModalElement extends UmbModalBaseElement<
UmbDocumentVariantPickerModalData,
UmbDocumentVariantPickerModalValue
> {
#selectionManager = new UmbSelectionManager(this);
connectedCallback(): void {
super.connectedCallback();
this.#selectionManager.setSelectable(true);
this.#selectionManager.setMultiple(true);
// Make sure all mandatory variants are selected when not in unpublish mode
this.#selectionManager.setSelection(this.value?.selection ?? []);
if (this.data?.type !== 'unpublish') {
this.#selectMandatoryVariants();
}
}
#selectMandatoryVariants() {
this.data?.variants.forEach((variant) => {
if (variant.isMandatory) {
this.#selectionManager.select(variant.culture);
}
});
}
get #headline(): string {
switch (this.data?.type) {
case 'publish':
return 'content_readyToPublish';
case 'unpublish':
return 'content_unpublish';
case 'schedule':
return 'content_readyToPublish';
default:
return 'content_readyToSave';
}
}
get #subtitle(): string {
switch (this.data?.type) {
case 'publish':
return 'content_variantsToPublish';
case 'unpublish':
return 'content_languagesToUnpublish';
case 'schedule':
return 'content_languagesToSchedule';
default:
return 'content_variantsToSave';
}
}
get #confirmLabel(): string {
switch (this.data?.type) {
case 'publish':
return 'buttons_saveAndPublish';
case 'unpublish':
return 'actions_unpublish';
case 'schedule':
return 'buttons_schedulePublish';
default:
return 'buttons_saveAndClose';
}
}
#submit() {
this.value = { selection: this.#selectionManager.getSelection() };
this.modalContext?.submit();
}
#close() {
this.modalContext?.reject();
}
render() {
return html`<umb-body-layout headline=${this.localize.term(this.#headline)}>
<p id="subtitle">${this.localize.term(this.#subtitle)}</p>
${repeat(
this.data?.variants ?? [],
(item) => item.culture,
(item) => html`
<uui-menu-item
selectable
label=${item.name}
@selected=${() => this.#selectionManager.select(item.culture)}
@deselected=${() => this.#selectionManager.deselect(item.culture)}
?selected=${this.#selectionManager.isSelected(item.culture)}>
<uui-icon slot="icon" name="icon-globe"></uui-icon>
${this.#renderLabel(item)}
</uui-menu-item>
`,
)}
${this.data?.type === 'publish' ? html`<p>${this.localize.term('content_variantsWillBeSaved')}</p>` : ''}
<div slot="actions">
<uui-button label=${this.localize.term('general_close')} @click=${this.#close}></uui-button>
<uui-button
label="${this.localize.term(this.#confirmLabel)}"
look="primary"
color="positive"
@click=${this.#submit}></uui-button>
</div>
</umb-body-layout> `;
}
#renderLabel(variant: UmbDocumentVariantModel) {
return html`<div class="label" slot="label">
<strong>${variant.segment ? variant.segment + ' - ' : ''}${variant.name}</strong>
<div class="label-status">${this.#renderVariantStatus(variant)}</div>
${variant.isMandatory && variant.state !== UmbDocumentVariantState.PUBLISHED
? html`<div class="label-status">
<umb-localize key="languages_mandatoryLanguage">Mandatory language</umb-localize>
</div>`
: ''}
</div>`;
}
#renderVariantStatus(variant: UmbDocumentVariantModel) {
switch (variant.state) {
case UmbDocumentVariantState.PUBLISHED:
return this.localize.term('content_published');
case UmbDocumentVariantState.PUBLISHED_PENDING_CHANGES:
return this.localize.term('content_publishedPendingChanges');
case UmbDocumentVariantState.NOT_CREATED:
case UmbDocumentVariantState.DRAFT:
default:
return this.localize.term('content_unpublished');
}
}
static styles = [
UmbTextStyles,
css`
#subtitle {
margin-top: 0;
}
.label {
padding: 0.5rem 0;
}
.label-status {
font-size: 0.8rem;
}
`,
];
}
export default UmbDocumentVariantPickerModalElement;
declare global {
interface HTMLElementTagNameMap {
'umb-document-variant-picker-modal': UmbDocumentVariantPickerModalElement;
}
}

View File

@@ -0,0 +1,133 @@
import '../../../../core/components/body-layout/body-layout.element.js';
import './document-variant-picker-modal.element.js';
import type { Meta, StoryObj } from '@storybook/web-components';
import { UmbDocumentVariantState } from '../../types.js';
import type { UmbDocumentVariantPickerModalElement } from './document-variant-picker-modal.element.js';
import type {
UmbDocumentVariantPickerModalData,
UmbDocumentVariantPickerModalValue,
} from './document-variant-picker-modal.token.js';
import { html } from '@umbraco-cms/backoffice/external/lit';
const modalData: UmbDocumentVariantPickerModalData = {
type: 'save',
variants: [
{
name: 'English',
culture: 'en-us',
state: UmbDocumentVariantState.PUBLISHED,
createDate: '2021-08-25T14:00:00Z',
publishDate: null,
updateDate: null,
segment: null,
isMandatory: true,
},
{
name: 'English',
culture: 'en-us',
state: UmbDocumentVariantState.DRAFT,
createDate: '2021-08-25T14:00:00Z',
publishDate: null,
updateDate: null,
segment: 'GTM',
isMandatory: true,
},
{
name: 'Danish',
culture: 'da-dk',
state: UmbDocumentVariantState.NOT_CREATED,
createDate: null,
publishDate: null,
updateDate: null,
segment: null,
isMandatory: false,
},
],
};
const modalValue: UmbDocumentVariantPickerModalValue = {
selection: ['en-us'],
};
const meta: Meta<UmbDocumentVariantPickerModalElement> = {
title: 'Workspaces/Document/Modals/Variant Picker',
component: 'umb-document-variant-picker-modal',
id: 'umb-document-variant-picker-modal',
args: {
data: modalData,
value: modalValue,
},
decorators: [(Story) => html`<div style="width: 500px; border: 1px solid #000;">${Story()}</div>`],
parameters: {
layout: 'centered',
docs: {
source: {
code: `
import { UMB_DOCUMENT_LANGUAGE_PICKER_MODAL, UmbDocumentVariantState } from '@umbraco-cms/backoffice/document';
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (modalManager) => {
modalManager.open(UMB_DOCUMENT_LANGUAGE_PICKER_MODAL, {
data: {
type: 'save',
variants: [
{
name: 'English',
culture: 'en-us',
state: UmbDocumentVariantState.PUBLISHED,
createDate: '2021-08-25T14:00:00Z',
publishDate: '2021-08-25T14:00:00Z',
updateDate: null,
segment: null,
isMandatory: true,
},
{
name: 'English',
culture: 'en-us',
state: UmbDocumentVariantState.PUBLISHED,
createDate: '2021-08-25T14:00:00Z',
publishDate: '2021-08-25T14:00:00Z',
updateDate: null,
segment: 'GTM',
isMandatory: false,
},
{
name: 'Danish',
culture: 'da-dk',
state: UmbDocumentVariantState.NOT_CREATED,
createDate: null,
publishDate: null,
updateDate: null,
segment: null,
isMandatory: false,
},
],
}
}
});
`,
},
},
},
};
export default meta;
type Story = StoryObj<UmbDocumentVariantPickerModalElement>;
export const Save: Story = {};
export const Publish: Story = {
args: {
data: { ...modalData, type: 'publish' },
},
};
export const Schedule: Story = {
args: {
data: { ...modalData, type: 'schedule' },
},
};
export const Unpublish: Story = {
args: {
data: { ...modalData, type: 'unpublish' },
},
};

View File

@@ -0,0 +1,22 @@
import { UMB_DOCUMENT_VARIANT_PICKER_MODAL_ALIAS } from '../manifests.js';
import type { UmbDocumentVariantModel } from '../../types.js';
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export interface UmbDocumentVariantPickerModalData {
type: 'save' | 'publish' | 'schedule' | 'unpublish';
variants: Array<UmbDocumentVariantModel>;
}
export interface UmbDocumentVariantPickerModalValue {
selection: Array<string | null>;
}
export const UMB_DOCUMENT_LANGUAGE_PICKER_MODAL = new UmbModalToken<
UmbDocumentVariantPickerModalData,
UmbDocumentVariantPickerModalValue
>(UMB_DOCUMENT_VARIANT_PICKER_MODAL_ALIAS, {
modal: {
type: 'dialog',
size: 'small',
},
});

View File

@@ -0,0 +1 @@
export * from './document-variant-picker-modal.token.js';

View File

@@ -56,6 +56,7 @@ export class UmbDocumentServerDataSource implements UmbDetailDataSource<UmbDocum
publishDate: null,
createDate: null,
updateDate: null,
isMandatory: false,
},
],
...preset,
@@ -101,6 +102,7 @@ export class UmbDocumentServerDataSource implements UmbDetailDataSource<UmbDocum
publishDate: variant.publishDate || null,
createDate: variant.createDate,
updateDate: variant.updateDate,
isMandatory: false, // TODO: this is not correct. It will be solved when we know where to get the isMandatory from
};
}),
urls: data.urls.map((url) => {

View File

@@ -1,4 +1,5 @@
import type {
CultureAndScheduleRequestModel,
PublishDocumentRequestModel,
UnpublishDocumentRequestModel,
} from '@umbraco-cms/backoffice/external/backend-api';
@@ -35,11 +36,18 @@ export class UmbDocumentPublishingServerDataSource {
async publish(unique: string, variantIds: Array<UmbVariantId>) {
if (!unique) throw new Error('Id is missing');
const publishSchedules: CultureAndScheduleRequestModel[] = variantIds.map<CultureAndScheduleRequestModel>(
(variant) => {
return {
culture: variant.isCultureInvariant() ? null : variant.toCultureString(),
schedule: variant.schedule,
};
},
);
// TODO: THIS DOES NOT TAKE SEGMENTS INTO ACCOUNT!!!!!!
const requestBody: PublishDocumentRequestModel = {
cultures: variantIds
.map((variant) => (variant.isCultureInvariant() ? null : variant.toCultureString()))
.filter((x) => x !== null) as Array<string>,
publishSchedules,
};
return tryExecuteAndNotify(this.#host, DocumentResource.putDocumentByIdPublish({ id: unique, requestBody }));
@@ -57,7 +65,7 @@ export class UmbDocumentPublishingServerDataSource {
// TODO: THIS DOES NOT TAKE SEGMENTS INTO ACCOUNT!!!!!!
const requestBody: UnpublishDocumentRequestModel = {
culture: variantIds.map((variant) => variant.toCultureString())[0],
culture: variantIds.map((variant) => (variant.isCultureInvariant() ? null : variant.toCultureString()))[0],
};
return tryExecuteAndNotify(this.#host, DocumentResource.putDocumentByIdUnpublish({ id: unique, requestBody }));

View File

@@ -1,6 +1,7 @@
import type { UmbDocumentEntityType } from './entity.js';
import type { UmbVariantModel } from '@umbraco-cms/backoffice/variant';
import type { DocumentVariantStateModel } from '@umbraco-cms/backoffice/external/backend-api';
import { DocumentVariantStateModel as UmbDocumentVariantState } from '@umbraco-cms/backoffice/external/backend-api';
export { UmbDocumentVariantState };
export interface UmbDocumentDetailModel {
documentType: {
@@ -18,8 +19,9 @@ export interface UmbDocumentDetailModel {
}
export interface UmbDocumentVariantModel extends UmbVariantModel {
state: DocumentVariantStateModel | null;
state: UmbDocumentVariantState | null;
publishDate: string | null;
isMandatory: boolean;
}
export interface UmbDocumentUrlInfoModel {

View File

@@ -0,0 +1,16 @@
export const UMB_USER_PERMISSION_DOCUMENT_CREATE = 'Umb.UserPermission.Document.Create';
export const UMB_USER_PERMISSION_DOCUMENT_READ = 'Umb.UserPermission.Document.Read';
export const UMB_USER_PERMISSION_DOCUMENT_UPDATE = 'Umb.UserPermission.Document.Update';
export const UMB_USER_PERMISSION_DOCUMENT_DELETE = 'Umb.UserPermission.Document.Delete';
export const UMB_USER_PERMISSION_DOCUMENT_CREATE_BLUEPRINT = 'Umb.UserPermission.Document.CreateBlueprint';
export const UMB_USER_PERMISSION_DOCUMENT_NOTIFICATIONS = 'Umb.UserPermission.Document.Notifications';
export const UMB_USER_PERMISSION_DOCUMENT_PUBLISH = 'Umb.UserPermission.Document.Publish';
export const UMB_USER_PERMISSION_DOCUMENT_PERMISSIONS = 'Umb.UserPermission.Document.Permissions';
export const UMB_USER_PERMISSION_DOCUMENT_SEND_FOR_APPROVAL = 'Umb.UserPermission.Document.SendForApproval';
export const UMB_USER_PERMISSION_DOCUMENT_UNPUBLISH = 'Umb.UserPermission.Document.Unpublish';
export const UMB_USER_PERMISSION_DOCUMENT_COPY = 'Umb.UserPermission.Document.Copy';
export const UMB_USER_PERMISSION_DOCUMENT_MOVE = 'Umb.UserPermission.Document.Move';
export const UMB_USER_PERMISSION_DOCUMENT_SORT = 'Umb.UserPermission.Document.Sort';
export const UMB_USER_PERMISSION_DOCUMENT_CULTURE_AND_HOSTNAMES = 'Umb.UserPermission.Document.CultureAndHostnames';
export const UMB_USER_PERMISSION_DOCUMENT_PUBLIC_ACCESS = 'Umb.UserPermission.Document.PublicAccess';
export const UMB_USER_PERMISSION_DOCUMENT_ROLLBACK = 'Umb.UserPermission.Document.Rollback';

View File

@@ -1,2 +1,2 @@
export * from './manifests.js';
export * from './repository/index.js';
export * from './constants.js';

View File

@@ -1,26 +1,27 @@
import {
UMB_USER_PERMISSION_DOCUMENT_READ,
UMB_USER_PERMISSION_DOCUMENT_CREATE_BLUEPRINT,
UMB_USER_PERMISSION_DOCUMENT_DELETE,
UMB_USER_PERMISSION_DOCUMENT_CREATE,
UMB_USER_PERMISSION_DOCUMENT_NOTIFICATIONS,
UMB_USER_PERMISSION_DOCUMENT_PUBLISH,
UMB_USER_PERMISSION_DOCUMENT_PERMISSIONS,
UMB_USER_PERMISSION_DOCUMENT_SEND_FOR_APPROVAL,
UMB_USER_PERMISSION_DOCUMENT_UNPUBLISH,
UMB_USER_PERMISSION_DOCUMENT_UPDATE,
UMB_USER_PERMISSION_DOCUMENT_COPY,
UMB_USER_PERMISSION_DOCUMENT_MOVE,
UMB_USER_PERMISSION_DOCUMENT_SORT,
UMB_USER_PERMISSION_DOCUMENT_CULTURE_AND_HOSTNAMES,
UMB_USER_PERMISSION_DOCUMENT_PUBLIC_ACCESS,
UMB_USER_PERMISSION_DOCUMENT_ROLLBACK,
} from './constants.js';
import { manifests as repositoryManifests } from './repository/manifests.js';
import type {
ManifestUserGranularPermission,
ManifestUserPermission,
} from '@umbraco-cms/backoffice/extension-registry';
export const UMB_USER_PERMISSION_DOCUMENT_CREATE = 'Umb.UserPermission.Document.Create';
export const UMB_USER_PERMISSION_DOCUMENT_READ = 'Umb.UserPermission.Document.Read';
export const UMB_USER_PERMISSION_DOCUMENT_UPDATE = 'Umb.UserPermission.Document.Update';
export const UMB_USER_PERMISSION_DOCUMENT_DELETE = 'Umb.UserPermission.Document.Delete';
export const UMB_USER_PERMISSION_DOCUMENT_CREATE_BLUEPRINT = 'Umb.UserPermission.Document.CreateBlueprint';
export const UMB_USER_PERMISSION_DOCUMENT_NOTIFICATIONS = 'Umb.UserPermission.Document.Notifications';
export const UMB_USER_PERMISSION_DOCUMENT_PUBLISH = 'Umb.UserPermission.Document.Publish';
export const UMB_USER_PERMISSION_DOCUMENT_PERMISSIONS = 'Umb.UserPermission.Document.Permissions';
export const UMB_USER_PERMISSION_DOCUMENT_SEND_FOR_APPROVAL = 'Umb.UserPermission.Document.SendForApproval';
export const UMB_USER_PERMISSION_DOCUMENT_UNPUBLISH = 'Umb.UserPermission.Document.Unpublish';
export const UMB_USER_PERMISSION_DOCUMENT_COPY = 'Umb.UserPermission.Document.Copy';
export const UMB_USER_PERMISSION_DOCUMENT_MOVE = 'Umb.UserPermission.Document.Move';
export const UMB_USER_PERMISSION_DOCUMENT_SORT = 'Umb.UserPermission.Document.Sort';
export const UMB_USER_PERMISSION_DOCUMENT_CULTURE_AND_HOSTNAMES = 'Umb.UserPermission.Document.CultureAndHostnames';
export const UMB_USER_PERMISSION_DOCUMENT_PUBLIC_ACCESS = 'Umb.UserPermission.Document.PublicAccess';
export const UMB_USER_PERMISSION_DOCUMENT_ROLLBACK = 'Umb.UserPermission.Document.Rollback';
const permissions: Array<ManifestUserPermission> = [
{
type: 'userPermission',

View File

@@ -3,7 +3,9 @@ import { UmbDocumentPropertyDataContext } from '../property-dataset-context/docu
import { UMB_DOCUMENT_ENTITY_TYPE } from '../entity.js';
import { UmbDocumentDetailRepository } from '../repository/index.js';
import type { UmbDocumentDetailModel } from '../types.js';
import { type UmbDocumentVariantPickerModalData, UMB_DOCUMENT_LANGUAGE_PICKER_MODAL } from '../modals/index.js';
import { UmbDocumentPublishingRepository } from '../repository/publishing/index.js';
import { UMB_DOCUMENT_WORKSPACE_ALIAS } from './manifests.js';
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import { UmbContentTypePropertyStructureManager } from '@umbraco-cms/backoffice/content-type';
import {
@@ -14,6 +16,7 @@ import {
} from '@umbraco-cms/backoffice/workspace';
import { appendToFrozenArray, partialUpdateFrozenArray, UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
type EntityType = UmbDocumentDetailModel;
export class UmbDocumentWorkspaceContext
@@ -45,9 +48,14 @@ export class UmbDocumentWorkspaceContext
readonly structure = new UmbContentTypePropertyStructureManager(this, new UmbDocumentTypeDetailRepository(this));
readonly splitView = new UmbWorkspaceSplitViewManager();
#modalManagerContext?: typeof UMB_MODAL_MANAGER_CONTEXT.TYPE;
constructor(host: UmbControllerHost) {
// TODO: Get Workspace Alias via Manifest.
super(host, 'Umb.Workspace.Document');
super(host, UMB_DOCUMENT_WORKSPACE_ALIAS);
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (instance) => {
this.#modalManagerContext = instance;
});
this.observe(this.contentTypeUnique, (unique) => this.structure.loadType(unique));
@@ -149,7 +157,7 @@ export class UmbDocumentWorkspaceContext
* @returns The value or undefined if not set or found.
*/
getPropertyValue<ReturnType = unknown>(alias: string, variantId?: UmbVariantId) {
const currentData = this.#currentData.value;
const currentData = this.getData();
if (currentData) {
const newDataSet = currentData.values?.find(
(x) => x.alias === alias && (variantId ? variantId.compare(x) : true),
@@ -166,7 +174,7 @@ export class UmbDocumentWorkspaceContext
if (!variantId) throw new Error('VariantId is missing');
const entry = { ...variantId.toObject(), alias, value };
const currentData = this.#currentData.value;
const currentData = this.getData();
if (currentData) {
const values = appendToFrozenArray(
currentData.values || [],
@@ -177,27 +185,91 @@ export class UmbDocumentWorkspaceContext
}
}
async #createOrSave() {
if (!this.#currentData.value?.unique) throw new Error('Unique is missing');
async #selectVariants(type: UmbDocumentVariantPickerModalData['type']): Promise<UmbVariantId[]> {
const currentData = this.getData();
if (!currentData) throw new Error('Data is missing');
const variants = currentData.variants;
// If there is only one variant, we don't need to select anything.
if (variants.length === 1) {
return [UmbVariantId.Create(variants[0])];
}
if (!this.#modalManagerContext) throw new Error('Modal manager context is missing');
const modalData: UmbDocumentVariantPickerModalData = {
type,
variants, // TODO: Filter out variants that do not have any changes unless it is the current variant.
};
const activeVariants = this.splitView.getActiveVariants();
const activeVariant = activeVariants[activeVariants.length - 1];
const modalContext = this.#modalManagerContext.open(UMB_DOCUMENT_LANGUAGE_PICKER_MODAL, {
data: modalData,
value: { selection: activeVariant ? [activeVariant.culture] : [] },
});
const result = await modalContext.onSubmit().catch(() => undefined);
if (!result?.selection.length) return [];
const selectedVariants = result.selection.map((x) => x?.toLowerCase() ?? '');
// Match the result to the available variants.
const variantIds = variants.filter((x) => selectedVariants.includes(x.culture!)).map((x) => UmbVariantId.Create(x));
return variantIds;
}
async #createOrSave(type: UmbDocumentVariantPickerModalData['type']): Promise<UmbVariantId[]> {
const data = this.getData();
if (!data) throw new Error('Data is missing');
if (!data.unique) throw new Error('Unique is missing');
const selectedVariants = await this.#selectVariants(type);
// If no variants are selected, we don't save anything.
if (!selectedVariants.length) return [];
if (this.getIsNew()) {
const value = this.#currentData.value;
if ((await this.repository.create(value)).data !== undefined) {
if ((await this.repository.create(data)).data !== undefined) {
this.setIsNew(false);
}
} else {
await this.repository.save(this.#currentData.value);
await this.repository.save(data);
}
return selectedVariants;
}
async save() {
const data = this.getData();
if (!data) throw new Error('Data is missing');
await this.#createOrSave();
await this.#createOrSave('save');
this.saveComplete(data);
}
public async publish() {
const variantIds = await this.#createOrSave('publish');
const unique = this.getEntityId();
if (variantIds.length && unique) {
await this.publishingRepository.publish(unique, variantIds);
}
}
public async saveAndPublish() {
await this.publish();
}
public async unpublish() {
const variantIds = await this.#selectVariants('unpublish');
const unique = this.getEntityId();
if (variantIds.length && unique) {
await this.publishingRepository.unpublish(unique, variantIds);
}
}
async delete() {
const id = this.getEntityId();
if (id) {
@@ -205,43 +277,6 @@ export class UmbDocumentWorkspaceContext
}
}
public async saveAndPublish() {
await this.#createOrSave();
// TODO: This might be right to publish all, but we need a method that just saves and publishes a declared range of variants.
const currentData = this.#currentData.value;
if (currentData) {
const variantIds = currentData.variants?.map((x) => UmbVariantId.Create(x));
const unique = currentData.unique;
if (variantIds && unique) {
await this.publishingRepository.publish(unique, variantIds);
}
}
}
public async publish() {
// TODO: This might be right to publish all, but we need a method that just publishes a declared range of variants.
const currentData = this.#currentData.value;
if (currentData) {
const variantIds = currentData.variants?.map((x) => UmbVariantId.Create(x));
const unique = this.getEntityId();
if (variantIds && unique) {
await this.publishingRepository.publish(unique, variantIds);
}
}
}
public async unpublish() {
// TODO: This might be right to unpublish all, but we need a method that just publishes a declared range of variants.
const currentData = this.#currentData.value;
if (currentData) {
const variantIds = currentData.variants?.map((x) => UmbVariantId.Create(x));
const unique = this.getEntityId();
if (variantIds && unique) {
await this.publishingRepository.unpublish(unique, variantIds);
}
}
}
/*
concept notes:

View File

@@ -10,9 +10,11 @@ import type {
ManifestWorkspaceView,
} from '@umbraco-cms/backoffice/extension-registry';
export const UMB_DOCUMENT_WORKSPACE_ALIAS = 'Umb.Workspace.Document';
const workspace: ManifestWorkspace = {
type: 'workspace',
alias: 'Umb.Workspace.Document',
alias: UMB_DOCUMENT_WORKSPACE_ALIAS,
name: 'Document Workspace',
element: () => import('./document-workspace.element.js'),
api: () => import('./document-workspace.context.js'),

View File

@@ -36,12 +36,12 @@ export class UmbLanguageCollectionServerDataSource implements UmbCollectionDataS
if (data) {
const items = data.items.map((item) => {
const model: UmbLanguageDetailModel = {
unique: item.isoCode,
unique: item.isoCode.toLowerCase(),
name: item.name,
entityType: UMB_LANGUAGE_ENTITY_TYPE,
isDefault: item.isDefault,
isMandatory: item.isMandatory,
fallbackIsoCode: item.fallbackIsoCode || null,
fallbackIsoCode: item.fallbackIsoCode?.toLowerCase() || null,
};
return model;

View File

@@ -38,6 +38,10 @@ export class UmbUserCollectionServerDataSource implements UmbCollectionDataSourc
return { error };
}
if (!data) {
return { data: { items: [], total: 0 } };
}
const { items, total } = data;
const mappedItems: Array<UmbUserDetailModel> = items.map((item: UserResponseModel) => {

View File

@@ -174,14 +174,14 @@ A modal can be opened by calling the open method on the UmbModalManagerContext.
```ts
import { html, LitElement } from '@umbraco-cms/backoffice/external/lit';
import { UmbElementMixin } from '@umbraco-cms/element';
import { UmbModalManagerContext, UMB_MODAL_CONTEXT_ALIAS } from '@umbraco-cms/modal';
import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api';
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
class MyElement extends UmbElementMixin(LitElement) {
#modalManagerContext?: UmbmodalManagerContext;
#modalManagerContext?: typeof UMB_MODAL_MANAGER_CONTEXT.TYPE;
constructor() {
super();
this.consumeContext(UMB_MODAL_CONTEXT, (instance) => {
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (instance) => {
this.#modalManagerContext = instance;
// modalManagerContext is now ready to be used
});
@@ -190,7 +190,7 @@ class MyElement extends UmbElementMixin(LitElement) {
#onClick() {
const data = {'data goes here'};
const value = {'initial value go here'};
const modalContext = this.#modalManagerContext?.open(MY_MODAL_TOKEN), {data: data, value: value});
const modalContext = this.#modalManagerContext?.open(MY_MODAL_TOKEN, {data: data, value: value});
modalContext?.onSubmit().then((value) => {
// if modal submitted, then data is supplied here.