Merge pull request #18154 from umbraco/v15/feature/enable-document-rollback-as-entity-action

Rollback as entity action + Picker data updates
This commit is contained in:
Niels Lyngsø
2025-01-31 12:59:58 +01:00
committed by GitHub
8 changed files with 393 additions and 166 deletions

View File

@@ -42,6 +42,7 @@ import type { UmbModalToken } from '@umbraco-cms/backoffice/modal';
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import {
UmbEntityUpdatedEvent,
UmbRequestReloadChildrenOfEntityEvent,
UmbRequestReloadStructureForEntityEvent,
} from '@umbraco-cms/backoffice/entity-action';
@@ -709,13 +710,21 @@ export abstract class UmbContentDetailWorkspaceContextBase<
);
this._data.setCurrent(newCurrentData);
const unique = this.getUnique()!;
const entityType = this.getEntityType();
const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
const event = new UmbRequestReloadStructureForEntityEvent({
entityType: this.getEntityType(),
unique: this.getUnique()!,
const structureEvent = new UmbRequestReloadStructureForEntityEvent({ unique, entityType });
eventContext.dispatchEvent(structureEvent);
const updatedEvent = new UmbEntityUpdatedEvent({
unique,
entityType,
eventUnique: this._workspaceEventUnique,
});
eventContext.dispatchEvent(event);
eventContext.dispatchEvent(updatedEvent);
this._closeModal();
}

View File

@@ -2,7 +2,9 @@ import { UmbControllerEvent } from '@umbraco-cms/backoffice/controller-api';
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface UmbEntityActionEventArgs extends UmbEntityModel {}
export interface UmbEntityActionEventArgs extends UmbEntityModel {
eventUnique?: string;
}
export class UmbEntityActionEvent<
ArgsType extends UmbEntityActionEventArgs = UmbEntityActionEventArgs,
@@ -21,4 +23,8 @@ export class UmbEntityActionEvent<
getUnique(): string | null {
return this._args.unique;
}
getEventUnique(): string | undefined {
return this._args.eventUnique;
}
}

View File

@@ -0,0 +1,10 @@
import type { UmbEntityActionEventArgs } from './entity-action.event.js';
import { UmbEntityActionEvent } from './entity-action.event.js';
export class UmbEntityUpdatedEvent extends UmbEntityActionEvent {
static readonly TYPE = 'entity-updated';
constructor(args: UmbEntityActionEventArgs) {
super(UmbEntityUpdatedEvent.TYPE, args);
}
}

View File

@@ -5,6 +5,7 @@ export * from './constants.js';
export * from './entity-action-base.js';
export * from './entity-action-list.element.js';
export * from './entity-action.event.js';
export * from './entity-updated.event.js';
export type * from './types.js';
export { UmbRequestReloadStructureForEntityEvent } from './request-reload-structure-for-entity.event.js';

View File

@@ -4,6 +4,8 @@ import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
import { type ManifestRepository, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import { UmbEntityUpdatedEvent } from '@umbraco-cms/backoffice/entity-action';
const ObserveRepositoryAlias = Symbol();
@@ -14,6 +16,7 @@ export class UmbRepositoryItemsManager<ItemType extends { unique: string }> exte
#init: Promise<unknown>;
#currentRequest?: Promise<unknown>;
#eventContext?: typeof UMB_ACTION_EVENT_CONTEXT.TYPE;
// the init promise is used externally for recognizing when the manager is ready.
public get init() {
@@ -70,6 +73,20 @@ export class UmbRepositoryItemsManager<ItemType extends { unique: string }> exte
},
null,
);
this.consumeContext(UMB_ACTION_EVENT_CONTEXT, (context) => {
this.#eventContext = context;
this.#eventContext.removeEventListener(
UmbEntityUpdatedEvent.TYPE,
this.#onEntityUpdatedEvent as unknown as EventListener,
);
this.#eventContext.addEventListener(
UmbEntityUpdatedEvent.TYPE,
this.#onEntityUpdatedEvent as unknown as EventListener,
);
});
}
getUniques(): Array<string> {
@@ -122,6 +139,25 @@ export class UmbRepositoryItemsManager<ItemType extends { unique: string }> exte
}
}
async #reloadItem(unique: string): Promise<void> {
await this.#init;
if (!this.repository) throw new Error('Repository is not initialized');
const { data } = await this.repository.requestItems([unique]);
if (data) {
const items = this.getItems();
const item = items.find((item) => this.#getUnique(item) === unique);
if (item) {
const index = items.indexOf(item);
const newItems = [...items];
newItems[index] = data[0];
this.#items.setValue(this.#sortByUniques(newItems));
}
}
}
#sortByUniques(data: Array<ItemType>): Array<ItemType> {
const uniques = this.getUniques();
return [...data].sort((a, b) => {
@@ -130,4 +166,25 @@ export class UmbRepositoryItemsManager<ItemType extends { unique: string }> exte
return aIndex - bIndex;
});
}
#onEntityUpdatedEvent = (event: UmbEntityUpdatedEvent) => {
const eventUnique = event.getUnique();
const items = this.getItems();
if (items.length === 0) return;
// Ignore events if the entity is not in the list of items.
const item = items.find((item) => this.#getUnique(item) === eventUnique);
if (!item) return;
this.#reloadItem(item.unique);
};
override destroy(): void {
this.#eventContext?.removeEventListener(
UmbEntityUpdatedEvent.TYPE,
this.#onEntityUpdatedEvent as unknown as EventListener,
);
super.destroy();
}
}

View File

@@ -7,6 +7,7 @@ import { UmbEntityContext, type UmbEntityModel, type UmbEntityUnique } from '@um
import { UMB_DISCARD_CHANGES_MODAL, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import {
UmbEntityUpdatedEvent,
UmbRequestReloadChildrenOfEntityEvent,
UmbRequestReloadStructureForEntityEvent,
} from '@umbraco-cms/backoffice/entity-action';
@@ -15,6 +16,7 @@ import { umbExtensionsRegistry, type ManifestRepository } from '@umbraco-cms/bac
import type { UmbDetailRepository } from '@umbraco-cms/backoffice/repository';
import { UmbStateManager } from '@umbraco-cms/backoffice/utils';
import { UmbValidationContext } from '@umbraco-cms/backoffice/validation';
import { UmbId } from '@umbraco-cms/backoffice/id';
const LOADING_STATE_UNIQUE = 'umbLoadingEntityDetail';
@@ -45,6 +47,8 @@ export abstract class UmbEntityDetailWorkspaceContextBase<
protected _getDataPromise?: Promise<any>;
protected _detailRepository?: DetailRepositoryType;
#eventContext?: typeof UMB_ACTION_EVENT_CONTEXT.TYPE;
#parent = new UmbObjectState<{ entityType: string; unique: UmbEntityUnique } | undefined>(undefined);
public readonly parentUnique = this.#parent.asObservablePart((parent) => (parent ? parent.unique : undefined));
public readonly parentEntityType = this.#parent.asObservablePart((parent) =>
@@ -85,6 +89,19 @@ export abstract class UmbEntityDetailWorkspaceContextBase<
window.addEventListener('willchangestate', this.#onWillNavigate);
this.#observeRepository(args.detailRepositoryAlias);
this.addValidationContext(this.validationContext);
this.consumeContext(UMB_ACTION_EVENT_CONTEXT, (context) => {
this.#eventContext = context;
this.#eventContext.removeEventListener(
UmbEntityUpdatedEvent.TYPE,
this.#onEntityUpdatedEvent as unknown as EventListener,
);
this.#eventContext.addEventListener(
UmbEntityUpdatedEvent.TYPE,
this.#onEntityUpdatedEvent as unknown as EventListener,
);
});
}
/**
@@ -307,13 +324,21 @@ export abstract class UmbEntityDetailWorkspaceContextBase<
this._data.setPersisted(data);
this._data.setCurrent(data);
const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
const event = new UmbRequestReloadStructureForEntityEvent({
unique: this.getUnique()!,
entityType: this.getEntityType(),
const unique = this.getUnique()!;
const entityType = this.getEntityType();
const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
const event = new UmbRequestReloadStructureForEntityEvent({ unique, entityType });
eventContext.dispatchEvent(event);
const updatedEvent = new UmbEntityUpdatedEvent({
unique,
entityType,
eventUnique: this._workspaceEventUnique,
});
actionEventContext.dispatchEvent(event);
eventContext.dispatchEvent(updatedEvent);
}
#allowNavigateAway = false;
@@ -396,8 +421,30 @@ export abstract class UmbEntityDetailWorkspaceContextBase<
}
}
// Discriminator to identify events from this workspace context
protected readonly _workspaceEventUnique = UmbId.new();
#onEntityUpdatedEvent = (event: UmbEntityUpdatedEvent) => {
const eventEntityUnique = event.getUnique();
const eventEntityType = event.getEntityType();
const eventDiscriminator = event.getEventUnique();
// Ignore events for other entities
if (eventEntityType !== this.getEntityType()) return;
if (eventEntityUnique !== this.getUnique()) return;
// Ignore events from this workspace so we don't reload the data twice. Ex saving this workspace
if (eventDiscriminator === this._workspaceEventUnique) return;
this.reload();
};
public override destroy(): void {
window.removeEventListener('willchangestate', this.#onWillNavigate);
this.#eventContext?.removeEventListener(
UmbEntityUpdatedEvent.TYPE,
this.#onEntityUpdatedEvent as unknown as EventListener,
);
this._detailRepository?.destroy();
this.#entityContext.destroy();
super.destroy();

View File

@@ -1,9 +1,4 @@
import {
UMB_USER_PERMISSION_DOCUMENT_ROLLBACK,
UMB_DOCUMENT_ENTITY_TYPE,
UMB_DOCUMENT_WORKSPACE_ALIAS,
} from '../../constants.js';
import { UMB_WORKSPACE_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace';
import { UMB_USER_PERMISSION_DOCUMENT_ROLLBACK, UMB_DOCUMENT_ENTITY_TYPE } from '../../constants.js';
import { UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS } from '@umbraco-cms/backoffice/recycle-bin';
export const manifests: Array<UmbExtensionManifest> = [
@@ -12,7 +7,7 @@ export const manifests: Array<UmbExtensionManifest> = [
kind: 'default',
alias: 'Umb.EntityAction.Document.Rollback',
name: 'Rollback Document Entity Action',
weight: 500,
weight: 450,
api: () => import('./rollback.action.js'),
forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE],
meta: {
@@ -27,12 +22,6 @@ export const manifests: Array<UmbExtensionManifest> = [
{
alias: UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS,
},
/* Currently the rollback is tightly coupled to the workspace contexts so we only allow it to show up
In the document workspace. */
{
alias: UMB_WORKSPACE_CONDITION_ALIAS,
match: UMB_DOCUMENT_WORKSPACE_ALIAS,
},
],
},
];

View File

@@ -1,5 +1,7 @@
import { UMB_DOCUMENT_WORKSPACE_CONTEXT, UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN } from '../../constants.js';
import { UMB_DOCUMENT_ENTITY_TYPE } from '../../constants.js';
import { UmbRollbackRepository } from '../repository/rollback.repository.js';
import { UmbDocumentDetailRepository } from '../../repository/index.js';
import type { UmbDocumentDetailModel } from '../../types.js';
import type { UmbRollbackModalData, UmbRollbackModalValue } from './types.js';
import { diffWords, type Change } from '@umbraco-cms/backoffice/external/diff';
import { css, customElement, html, nothing, repeat, state, unsafeHTML } from '@umbraco-cms/backoffice/external/lit';
@@ -8,8 +10,13 @@ import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbUserItemRepository } from '@umbraco-cms/backoffice/user';
import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property';
import type { UUISelectEvent } from '@umbraco-cms/backoffice/external/uui';
import { UMB_APP_LANGUAGE_CONTEXT, UmbLanguageItemRepository } from '@umbraco-cms/backoffice/language';
import { UMB_ENTITY_CONTEXT } from '@umbraco-cms/backoffice/entity';
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import '../../modals/shared/document-variant-language-picker.element.js';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import { UmbEntityUpdatedEvent, UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/entity-action';
type DocumentVersion = {
id: string;
@@ -22,10 +29,10 @@ type DocumentVersion = {
@customElement('umb-rollback-modal')
export class UmbRollbackModalElement extends UmbModalBaseElement<UmbRollbackModalData, UmbRollbackModalValue> {
@state()
versions: DocumentVersion[] = [];
_versions: DocumentVersion[] = [];
@state()
currentVersion?: {
_selectedVersion?: {
date: string;
name: string;
user: string;
@@ -37,18 +44,20 @@ export class UmbRollbackModalElement extends UmbModalBaseElement<UmbRollbackModa
};
@state()
currentCulture?: string;
_selectedCulture: string | null = null;
@state()
availableVariants: Option[] = [];
_isInvariant = true;
@state()
_availableVariants: Option[] = [];
@state()
_diffs: Array<{ alias: string; diff: Change[] }> = [];
#rollbackRepository = new UmbRollbackRepository(this);
#userItemRepository = new UmbUserItemRepository(this);
#workspaceContext?: typeof UMB_DOCUMENT_WORKSPACE_CONTEXT.TYPE;
#propertyDatasetContext?: typeof UMB_PROPERTY_DATASET_CONTEXT.TYPE;
#localizeDateOptions: Intl.DateTimeFormatOptions = {
day: 'numeric',
month: 'long',
@@ -56,37 +65,76 @@ export class UmbRollbackModalElement extends UmbModalBaseElement<UmbRollbackModa
minute: '2-digit',
};
#currentDocument: UmbDocumentDetailModel | undefined;
#currentAppCulture: string | undefined;
#currentDatasetCulture: string | undefined;
constructor() {
super();
this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (instance) => {
this.#propertyDatasetContext = instance;
this.currentCulture = instance.getVariantId().culture ?? undefined;
this.#requestVersions();
this.#currentDatasetCulture = instance.getVariantId().culture ?? undefined;
this.#selectCulture();
});
this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, (instance) => {
this.#workspaceContext = instance;
this.consumeContext(UMB_APP_LANGUAGE_CONTEXT, (instance) => {
this.#currentAppCulture = instance.getAppCulture();
this.#selectCulture();
});
this.observe(instance.variantOptions, (options) => {
this.availableVariants = options.map((option) => {
this.consumeContext(UMB_ENTITY_CONTEXT, async (instance) => {
if (instance.getEntityType() !== UMB_DOCUMENT_ENTITY_TYPE) {
throw new Error(`Entity type is not ${UMB_DOCUMENT_ENTITY_TYPE}`);
}
const unique = instance?.getUnique();
if (!unique) {
throw new Error('Document unique is not set');
}
const { data } = await new UmbDocumentDetailRepository(this).requestByUnique(unique);
if (!data) return;
this.#currentDocument = data;
const itemVariants = this.#currentDocument?.variants ?? [];
this._isInvariant = itemVariants.length === 1 && new UmbVariantId(itemVariants[0].culture).isInvariant();
this.#selectCulture();
const cultures = itemVariants.map((x) => x.culture).filter((x) => x !== null) as string[];
const { data: languageItems } = await new UmbLanguageItemRepository(this).requestItems(cultures);
if (languageItems) {
this._availableVariants = languageItems.map((language) => {
return {
name: option.language.name,
value: option.language.unique,
selected: option.language.unique === this.currentCulture,
name: language.name,
value: language.unique,
selected: language.unique === this._selectedCulture,
};
});
});
} else {
this._availableVariants = [];
}
this.#requestVersions();
});
}
#selectCulture() {
const contextCulture = this.#currentDatasetCulture ?? this.#currentAppCulture ?? null;
this._selectedCulture = this._isInvariant ? null : contextCulture;
}
async #requestVersions() {
if (!this.#propertyDatasetContext) return;
if (!this.#currentDocument?.unique) {
throw new Error('Document unique is not set');
}
const documentId = this.#propertyDatasetContext.getUnique();
if (!documentId) return;
const { data } = await this.#rollbackRepository.requestVersionsByDocumentId(documentId, this.currentCulture);
const { data } = await this.#rollbackRepository.requestVersionsByDocumentId(
this.#currentDocument?.unique,
this._selectedCulture ?? undefined,
);
if (!data) return;
const tempItems: DocumentVersion[] = [];
@@ -108,28 +156,38 @@ export class UmbRollbackModalElement extends UmbModalBaseElement<UmbRollbackModa
});
});
this.versions = tempItems;
this._versions = tempItems;
const id = tempItems.find((item) => item.isCurrentlyPublishedVersion)?.id;
if (id) {
this.#setCurrentVersion(id);
this.#selectVersion(id);
}
}
async #setCurrentVersion(id: string) {
const version = this.versions.find((item) => item.id === id);
if (!version) return;
async #selectVersion(id: string) {
const version = this._versions.find((item) => item.id === id);
if (!version) {
this._selectedVersion = undefined;
this._diffs = [];
return;
}
const { data } = await this.#rollbackRepository.requestVersionById(id);
if (!data) return;
this.currentVersion = {
if (!data) {
this._selectedVersion = undefined;
this._diffs = [];
return;
}
this._selectedVersion = {
date: version.date,
user: version.user,
name: data.variants.find((x) => x.culture === this.currentCulture)?.name || data.variants[0].name,
name: data.variants.find((x) => x.culture === this._selectedCulture)?.name || data.variants[0].name,
id: data.id,
properties: data.values
.filter((x) => x.culture === this.currentCulture || !x.culture) // When invariant, culture is undefined or null.
.filter((x) => x.culture === this._selectedCulture || !x.culture) // When invariant, culture is undefined or null.
.map((value: any) => {
return {
alias: value.alias,
@@ -137,20 +195,35 @@ export class UmbRollbackModalElement extends UmbModalBaseElement<UmbRollbackModa
};
}),
};
await this.#setDiffs();
}
#onRollback() {
if (!this.currentVersion) return;
async #onRollback() {
if (!this._selectedVersion) return;
const id = this.currentVersion.id;
const culture = this.availableVariants.length > 1 ? this.currentCulture : undefined;
this.#rollbackRepository.rollback(id, culture);
const id = this._selectedVersion.id;
const culture = this._selectedCulture ?? undefined;
const docUnique = this.#workspaceContext?.getUnique() ?? '';
// TODO Use the load method on the context instead of location.href, when it works.
// this.#workspaceContext?.load(docUnique);
location.href = UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN.generateAbsolute({ unique: docUnique });
this.modalContext?.reject();
const { error } = await this.#rollbackRepository.rollback(id, culture);
if (error) return;
const unique = this.#currentDocument?.unique;
const entityType = this.#currentDocument?.entityType;
if (!unique || !entityType) {
throw new Error('Document unique or entity type is not set');
}
const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
const reloadStructureEvent = new UmbRequestReloadStructureForEntityEvent({ unique, entityType });
actionEventContext.dispatchEvent(reloadStructureEvent);
const entityUpdatedEvent = new UmbEntityUpdatedEvent({ unique, entityType });
actionEventContext.dispatchEvent(entityUpdatedEvent);
this.modalContext?.submit();
}
#onCancel() {
@@ -158,7 +231,7 @@ export class UmbRollbackModalElement extends UmbModalBaseElement<UmbRollbackModa
}
#onVersionClicked(id: string) {
this.#setCurrentVersion(id);
this.#selectVersion(id);
}
#onPreventCleanup(event: Event, id: string, preventCleanup: boolean) {
@@ -166,7 +239,7 @@ export class UmbRollbackModalElement extends UmbModalBaseElement<UmbRollbackModa
event.stopImmediatePropagation();
this.#rollbackRepository.setPreventCleanup(id, preventCleanup);
const version = this.versions.find((item) => item.id === id);
const version = this._versions.find((item) => item.id === id);
if (!version) return;
version.preventCleanup = preventCleanup;
@@ -176,125 +249,147 @@ export class UmbRollbackModalElement extends UmbModalBaseElement<UmbRollbackModa
#onChangeCulture(event: UUISelectEvent) {
const value = event.target.value;
this.currentCulture = value.toString();
this._selectedCulture = value.toString();
this.#requestVersions();
}
#trimQuotes(str: string): string {
return str.replace(/^['"]|['"]$/g, '');
}
#renderCultureSelect() {
if (this.availableVariants.length < 2) return nothing;
return html`
<div id="language-select">
<b>${this.localize.term('general_language')}</b>
<uui-select @change=${this.#onChangeCulture} .options=${this.availableVariants}></uui-select>
</div>
<uui-select
id="language-select"
@change=${this.#onChangeCulture}
.options=${this._availableVariants}></uui-select>
`;
}
#renderVersions() {
return html` ${this.#renderCultureSelect()}
${repeat(
this.versions,
(item) => item.id,
(item) => {
return html`
<div
@click=${() => this.#onVersionClicked(item.id)}
@keydown=${() => {}}
class="rollback-item ${this.currentVersion?.id === item.id ? 'active' : ''}">
<div>
<p class="rollback-item-date">
<umb-localize-date date="${item.date}" .options=${this.#localizeDateOptions}></umb-localize-date>
</p>
<p>${item.user}</p>
<p>${item.isCurrentlyPublishedVersion ? this.localize.term('rollback_currentPublishedVersion') : ''}</p>
if (!this._versions.length) {
return html`<uui-box headline=${this.localize.term('rollback_versions')}>No versions available</uui-box>`;
}
return html` <uui-box id="versions-box" headline=${this.localize.term('rollback_versions')}>
${repeat(
this._versions,
(item) => item.id,
(item) => {
return html`
<div
@click=${() => this.#onVersionClicked(item.id)}
@keydown=${() => {}}
class="rollback-item ${this._selectedVersion?.id === item.id ? 'active' : ''}">
<div>
<p class="rollback-item-date">
<umb-localize-date date="${item.date}" .options=${this.#localizeDateOptions}></umb-localize-date>
</p>
<p>${item.user}</p>
<p>${item.isCurrentlyPublishedVersion ? this.localize.term('rollback_currentPublishedVersion') : ''}</p>
</div>
<uui-button
look="secondary"
@click=${(event: Event) => this.#onPreventCleanup(event, item.id, !item.preventCleanup)}
label=${item.preventCleanup
? this.localize.term('contentTypeEditor_historyCleanupEnableCleanup')
: this.localize.term('contentTypeEditor_historyCleanupPreventCleanup')}></uui-button>
</div>
<uui-button
look="secondary"
@click=${(event: Event) => this.#onPreventCleanup(event, item.id, !item.preventCleanup)}
label=${item.preventCleanup
? this.localize.term('contentTypeEditor_historyCleanupEnableCleanup')
: this.localize.term('contentTypeEditor_historyCleanupPreventCleanup')}></uui-button>
</div>
`;
},
)}`;
`;
},
)}</uui-box
>`;
}
#renderCurrentVersion() {
if (!this.currentVersion) return;
async #setDiffs() {
if (!this._selectedVersion) return;
let draftValues =
(this.#workspaceContext?.getData()?.values as Array<{ alias: string; culture: string; value: any }>) ?? [];
const currentPropertyValues = this.#currentDocument?.values.filter(
(x) => x.culture === this._selectedCulture || !x.culture,
); // When invariant, culture is undefined or null.
draftValues = draftValues.filter((x) => x.culture === this.currentCulture || !x.culture); // When invariant, culture is undefined or null.
if (!currentPropertyValues) {
throw new Error('Current property values are not set');
}
const currentName = this.#currentDocument?.variants.find((x) => x.culture === this._selectedCulture)?.name;
if (!currentName) {
throw new Error('Current name is not set');
}
const diffs: Array<{ alias: string; diff: Change[] }> = [];
const nameDiff = diffWords(this.#workspaceContext?.getName() ?? '', this.currentVersion.name);
const nameDiff = diffWords(currentName, this._selectedVersion.name);
diffs.push({ alias: 'name', diff: nameDiff });
this.currentVersion.properties.forEach((item) => {
const draftValue = draftValues.find((x) => x.alias === item.alias);
this._selectedVersion.properties.forEach((item) => {
const draftValue = currentPropertyValues.find((x) => x.alias === item.alias);
if (!draftValue) return;
const draftValueString = trimQuotes(JSON.stringify(draftValue.value));
const versionValueString = trimQuotes(JSON.stringify(item.value));
const draftValueString = this.#trimQuotes(JSON.stringify(draftValue.value));
const versionValueString = this.#trimQuotes(JSON.stringify(item.value));
const diff = diffWords(draftValueString, versionValueString);
diffs.push({ alias: item.alias, diff });
});
/**
*
* @param str
*/
function trimQuotes(str: string): string {
return str.replace(/^['"]|['"]$/g, '');
}
this._diffs = [...diffs];
}
#renderSelectedVersion() {
if (!this._selectedVersion)
return html`
<uui-box id="box-right" style="display: flex; align-items: center; justify-content: center;"
>No selected version</uui-box
>
`;
return html`
${unsafeHTML(this.localize.term('rollback_diffHelp'))}
<uui-table>
<uui-table-column style="width: 0"></uui-table-column>
<uui-table-column></uui-table-column>
<uui-box headline=${this.currentVersionHeader} id="box-right">
${unsafeHTML(this.localize.term('rollback_diffHelp'))}
<uui-table>
<uui-table-column style="width: 0"></uui-table-column>
<uui-table-column></uui-table-column>
<uui-table-head>
<uui-table-head-cell>${this.localize.term('general_alias')}</uui-table-head-cell>
<uui-table-head-cell>${this.localize.term('general_value')}</uui-table-head-cell>
</uui-table-head>
${repeat(
diffs,
(item) => item.alias,
(item) => {
const diff = diffs.find((x) => x?.alias === item.alias);
return html`
<uui-table-row>
<uui-table-cell>${item.alias}</uui-table-cell>
<uui-table-cell>
${diff
? diff.diff.map((part) =>
part.added
? html`<span class="diff-added">${part.value}</span>`
: part.removed
? html`<span class="diff-removed">${part.value}</span>`
: part.value,
)
: nothing}
</uui-table-cell>
</uui-table-row>
`;
},
)}
</uui-table>
<uui-table-head>
<uui-table-head-cell>${this.localize.term('general_alias')}</uui-table-head-cell>
<uui-table-head-cell>${this.localize.term('general_value')}</uui-table-head-cell>
</uui-table-head>
${repeat(
this._diffs,
(item) => item.alias,
(item) => {
const diff = this._diffs.find((x) => x?.alias === item.alias);
return html`
<uui-table-row>
<uui-table-cell>${item.alias}</uui-table-cell>
<uui-table-cell>
${diff
? diff.diff.map((part) =>
part.added
? html`<span class="diff-added">${part.value}</span>`
: part.removed
? html`<span class="diff-removed">${part.value}</span>`
: part.value,
)
: nothing}
</uui-table-cell>
</uui-table-row>
`;
},
)}
</uui-table>
</uui-box>
`;
}
get currentVersionHeader() {
return (
this.localize.date(this.currentVersion?.date ?? new Date(), this.#localizeDateOptions) +
this.localize.date(this._selectedVersion?.date ?? new Date(), this.#localizeDateOptions) +
' - ' +
this.currentVersion?.user
this._selectedVersion?.user
);
}
@@ -302,10 +397,17 @@ export class UmbRollbackModalElement extends UmbModalBaseElement<UmbRollbackModa
return html`
<umb-body-layout headline="Rollback">
<div id="main">
<uui-box headline=${this.localize.term('rollback_versions')} id="box-left">
<div>${this.#renderVersions()}</div>
</uui-box>
<uui-box headline=${this.currentVersionHeader} id="box-right"> ${this.#renderCurrentVersion()} </uui-box>
<div id="box-left">
${this._availableVariants.length
? html`
<uui-box id="language-box" headline=${this.localize.term('general_language')}>
${this.#renderCultureSelect()}
</uui-box>
`
: nothing}
${this.#renderVersions()}
</div>
${this.#renderSelectedVersion()}
</div>
<umb-footer-layout slot="footer">
<uui-button
@@ -317,7 +419,8 @@ export class UmbRollbackModalElement extends UmbModalBaseElement<UmbRollbackModa
slot="actions"
look="primary"
@click=${this.#onRollback}
label=${this.localize.term('actions_rollback')}></uui-button>
label=${this.localize.term('actions_rollback')}
?disabled=${!this._selectedVersion}></uui-button>
</umb-footer-layout>
</umb-body-layout>
`;
@@ -329,14 +432,15 @@ export class UmbRollbackModalElement extends UmbModalBaseElement<UmbRollbackModa
:host {
color: var(--uui-color-text);
}
#language-select {
display: flex;
flex-direction: column;
padding: var(--uui-size-space-5);
padding-bottom: 0;
gap: var(--uui-size-space-2);
font-size: 15px;
#language-box {
margin-bottom: var(--uui-size-space-2);
}
#language-select {
width: 100%;
}
uui-table {
--uui-table-cell-padding: var(--uui-size-space-1) var(--uui-size-space-4);
margin-top: var(--uui-size-space-5);
@@ -410,15 +514,19 @@ export class UmbRollbackModalElement extends UmbModalBaseElement<UmbRollbackModa
.rollback-item uui-button {
white-space: nowrap;
}
#main {
display: flex;
gap: var(--uui-size-space-4);
gap: var(--uui-size-space-5);
width: 100%;
height: 100%;
}
#box-left {
#versions-box {
--uui-box-default-padding: 0;
}
#box-left {
max-width: 500px;
flex: 1;
overflow: auto;