Skip to content

Commit

Permalink
Fix: Add "Not Found" empty state to detail workspaces (#17489)
Browse files Browse the repository at this point in the history
Co-authored-by: Niels Lyngsø <[email protected]>
Co-authored-by: Niels Lyngsø <[email protected]>
  • Loading branch information
3 people authored Nov 19, 2024
1 parent 7cc8f84 commit 310ccdc
Show file tree
Hide file tree
Showing 29 changed files with 270 additions and 166 deletions.
10 changes: 10 additions & 0 deletions src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,16 @@ export default {
blueprintDescription:
'A Document Blueprint is predefined content that an editor can select to use as the\n basis for creating new content\n ',
},
entityDetail: {
notFoundTitle: (entityType: string) => {
const entityName = entityType ?? 'Item';
return `${entityName} not found`;
},
notFoundDescription: (entityType: string) => {
const entityName = entityType ?? 'item';
return `The requested ${entityName} could not be found. Please check the URL and try again.`;
},
},
media: {
clickToUpload: 'Click to upload',
orClickHereToUpload: 'or click here to choose files',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ export class UmbBodyLayoutElement extends LitElement {
@property({ type: Boolean, reflect: true, attribute: 'header-transparent' })
public headerTransparent = false;

@property({ type: Boolean })
loading = false;

@state()
private _headerSlotHasChildren = false;

Expand Down Expand Up @@ -116,6 +119,7 @@ export class UmbBodyLayoutElement extends LitElement {
<!-- This div should be changed for the uui-scroll-container when it gets updated -->
<div id="main">
${this.loading ? html`<uui-loader-bar></uui-loader-bar>` : nothing}
<slot></slot>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export class UmbWorkspaceEditorElement extends UmbLitElement {
@property({ attribute: 'back-path' })
public backPath?: string;

@property({ type: Boolean })
public loading = false;

@state()
private _workspaceViews: Array<ManifestWorkspaceView> = [];

Expand Down Expand Up @@ -83,7 +86,7 @@ export class UmbWorkspaceEditorElement extends UmbLitElement {

override render() {
return html`
<umb-body-layout main-no-padding .headline=${this.headline}>
<umb-body-layout main-no-padding .headline=${this.headline} ?loading=${this.loading}>
${this.#renderBackButton()}
<slot name="header" slot="header"></slot>
${this.#renderViews()}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,15 @@ export class UmbWorkspaceEntityActionMenuElement extends UmbLitElement {

this.consumeContext(UMB_ENTITY_WORKSPACE_CONTEXT, (context) => {
this._workspaceContext = context;
this._observeInfo();
this.observe(this._workspaceContext.unique, (unique) => {
this._unique = unique;
// TODO: the context does not have an observable for the entity type, so we need to use the
// getEntityType method until we can add an observable for it.
this._entityType = this._workspaceContext?.getEntityType();
});
});
}

private _observeInfo() {
if (!this._workspaceContext) return;
this._unique = this._workspaceContext.getUnique();
this._entityType = this._workspaceContext.getEntityType();
}

#onActionExecuted(event: UmbActionExecutedEvent) {
event.stopPropagation();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { UmbSubmittableWorkspaceContextBase } from '../submittable/index.js';
import { UmbEntityWorkspaceDataManager } from '../entity/entity-workspace-data-manager.js';
import type { UmbEntityDetailWorkspaceContextArgs, UmbEntityDetailWorkspaceContextCreateArgs } from './types.js';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbEntityContext, type UmbEntityModel, type UmbEntityUnique } from '@umbraco-cms/backoffice/entity';
Expand All @@ -12,45 +13,36 @@ import {
import { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api';
import { umbExtensionsRegistry, type ManifestRepository } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbDetailRepository } from '@umbraco-cms/backoffice/repository';
import { UmbStateManager } from '@umbraco-cms/backoffice/utils';

export interface UmbEntityDetailWorkspaceContextArgs {
entityType: string;
workspaceAlias: string;
detailRepositoryAlias: string;
}

/**
* @deprecated Use UmbEntityDetailWorkspaceContextArgs instead
*/
export type UmbEntityWorkspaceContextArgs = UmbEntityDetailWorkspaceContextArgs;

export interface UmbEntityDetailWorkspaceContextCreateArgs<DetailModelType> {
parent: UmbEntityModel;
preset?: Partial<DetailModelType>;
}
const LOADING_STATE_UNIQUE = 'umbLoadingEntityDetail';

export abstract class UmbEntityDetailWorkspaceContextBase<
DetailModelType extends UmbEntityModel,
DetailModelType extends UmbEntityModel = UmbEntityModel,
DetailRepositoryType extends UmbDetailRepository<DetailModelType> = UmbDetailRepository<DetailModelType>,
CreateArgsType extends
UmbEntityDetailWorkspaceContextCreateArgs<DetailModelType> = UmbEntityDetailWorkspaceContextCreateArgs<DetailModelType>,
> extends UmbSubmittableWorkspaceContextBase<DetailModelType> {
// Just for context token safety:
public readonly IS_ENTITY_DETAIL_WORKSPACE_CONTEXT = true;

/**
* @description Data manager for the workspace.
* @protected
* @memberof UmbEntityWorkspaceContextBase
*/
protected readonly _data = new UmbEntityWorkspaceDataManager<DetailModelType>(this);

#entityContext = new UmbEntityContext(this);
public readonly entityType = this.#entityContext.entityType;
public readonly unique = this.#entityContext.unique;

public readonly data = this._data.current;
public readonly loading = new UmbStateManager(this);

protected _getDataPromise?: Promise<any>;
protected _detailRepository?: DetailRepositoryType;

#entityContext = new UmbEntityContext(this);
public readonly entityType = this.#entityContext.entityType;
public readonly unique = this.#entityContext.unique;

#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) =>
Expand Down Expand Up @@ -131,6 +123,7 @@ export abstract class UmbEntityDetailWorkspaceContextBase<

async load(unique: string) {
this.#entityContext.setUnique(unique);
this.loading.addState({ unique: LOADING_STATE_UNIQUE, message: `Loading ${this.getEntityType()} Details` });
await this.#init;
this.resetState();
this._getDataPromise = this._detailRepository!.requestByUnique(unique);
Expand All @@ -142,8 +135,15 @@ export abstract class UmbEntityDetailWorkspaceContextBase<
this._data.setPersisted(data);
this._data.setCurrent(data);
this.setIsNew(false);

this.observe(
response.asObservable(),
(entity) => this.#onDetailStoreChange(entity),
'umbEntityDetailTypeStoreObserver',
);
}

this.loading.removeState(LOADING_STATE_UNIQUE);
return response;
}

Expand All @@ -166,24 +166,28 @@ export abstract class UmbEntityDetailWorkspaceContextBase<
* @returns { Promise<any> | undefined } The data of the scaffold.
*/
public async createScaffold(args: CreateArgsType) {
this.loading.addState({ unique: LOADING_STATE_UNIQUE, message: `Creating ${this.getEntityType()} scaffold` });
await this.#init;
this.resetState();
this.setParent(args.parent);

const request = this._detailRepository!.createScaffold(args.preset);
this._getDataPromise = request;
let { data } = await request;
if (!data) return undefined;

this.#entityContext.setUnique(data.unique);
if (data) {
this.#entityContext.setUnique(data.unique);

if (this.modalContext) {
data = { ...data, ...this.modalContext.data.preset };
}

if (this.modalContext) {
data = { ...data, ...this.modalContext.data.preset };
this.setIsNew(true);
this._data.setPersisted(data);
this._data.setCurrent(data);
}

this.setIsNew(true);
this._data.setPersisted(data);
this._data.setCurrent(data);
this.loading.removeState(LOADING_STATE_UNIQUE);

return data;
}
Expand Down Expand Up @@ -284,9 +288,9 @@ export abstract class UmbEntityDetailWorkspaceContextBase<
}

if (this._checkWillNavigateAway(newUrl) && this._getHasUnpersistedChanges()) {
/* Since ours modals are async while events are synchronous, we need to prevent the default behavior of the event, even if the modal hasn’t been resolved yet.
Once the modal is resolved (the user accepted to discard the changes and navigate away from the route), we will push a new history state.
This push will make the "willchangestate" event happen again and due to this somewhat "backward" behavior,
/* Since ours modals are async while events are synchronous, we need to prevent the default behavior of the event, even if the modal hasn’t been resolved yet.
Once the modal is resolved (the user accepted to discard the changes and navigate away from the route), we will push a new history state.
This push will make the "willchangestate" event happen again and due to this somewhat "backward" behavior,
we set an "allowNavigateAway"-flag to prevent the "discard-changes" functionality from running in a loop.*/
e.preventDefault();
const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
Expand Down Expand Up @@ -342,6 +346,12 @@ export abstract class UmbEntityDetailWorkspaceContextBase<
);
}

#onDetailStoreChange(entity: DetailModelType | undefined) {
if (!entity) {
this._data.clear();
}
}

public override destroy(): void {
window.removeEventListener('willchangestate', this.#onWillNavigate);
this._detailRepository?.destroy();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { UmbWorkspaceContext } from '../workspace-context.interface.js';
import type { UmbEntityDetailWorkspaceContextBase } from './entity-detail-workspace-base.js';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';

export const UMB_ENTITY_DETAIL_WORKSPACE_CONTEXT = new UmbContextToken<
UmbWorkspaceContext,
UmbEntityDetailWorkspaceContextBase
>(
'UmbWorkspaceContext',
undefined,
(context): context is UmbEntityDetailWorkspaceContextBase => (context as any).IS_ENTITY_DETAIL_WORKSPACE_CONTEXT,
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';

@customElement('umb-entity-detail-not-found')
export class UmbEntityDetailNotFoundElement extends UmbLitElement {
@property({ type: String, attribute: 'entity-type' })
entityType = '';

override render() {
return html`
<div class="uui-text">
<h4>${this.localize.term('entityDetail_notFoundTitle', this.entityType)}</h4>
${this.localize.term('entityDetail_notFoundDescription', this.entityType)}
</div>
`;
}

static override styles = [
UmbTextStyles,
css`
:host {
display: block;
width: 100%;
height: 100%;
min-width: 0;
}
:host > div {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
}
@keyframes fadeIn {
100% {
opacity: 100%;
}
}
`,
];
}

declare global {
interface HTMLElementTagNameMap {
'umb-entity-detail-not-found': UmbEntityDetailNotFoundElement;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UMB_ENTITY_DETAIL_WORKSPACE_CONTEXT } from '../entity-detail-workspace.context-token.js';
import { css, customElement, html, ifDefined, nothing, property, state } from '@umbraco-cms/backoffice/external/lit';

@customElement('umb-entity-detail-workspace-editor')
export class UmbEntityDetailWorkspaceEditorElement extends UmbLitElement {
@property({ attribute: 'back-path' })
public backPath?: string;

@state()
private _entityType?: string;

@state()
private _isLoading = false;

@state()
private _exists = false;

@state()
private _isNew? = false;

#context?: typeof UMB_ENTITY_DETAIL_WORKSPACE_CONTEXT.TYPE;

constructor() {
super();

this.consumeContext(UMB_ENTITY_DETAIL_WORKSPACE_CONTEXT, (context) => {
this.#context = context;
this.observe(this.#context?.entityType, (entityType) => (this._entityType = entityType));
this.observe(this.#context?.loading.isOn, (isLoading) => (this._isLoading = isLoading));
this.observe(this.#context?.data, (data) => (this._exists = !!data));
this.observe(this.#context?.isNew, (isNew) => (this._isNew = isNew));
});
}

protected override render() {
return html` ${!this._exists && !this._isLoading
? html`<umb-entity-detail-not-found entity-type=${ifDefined(this._entityType)}></umb-entity-detail-not-found>`
: nothing}
<!-- TODO: It is currently on purpose that the workspace editor is always in the DOM, even when it doesn't have data.
We currently rely on the entity actions to be available to execute, and we ran into an issue when the entity got deleted; then the DOM got cleared, and the delete action couldn't complete.
We need to look into loading the entity actions in the workspace context instead so we don't rely on the DOM.
-->
<umb-workspace-editor
?loading=${this._isLoading}
.backPath=${this.backPath}
class="${this._exists === false ? 'hide' : ''}">
<slot name="header" slot="header"></slot>
${this.#renderEntityActions()}
<slot></slot>
</umb-workspace-editor>`;
}

#renderEntityActions() {
if (this._isNew) return nothing;
return html`<umb-workspace-entity-action-menu slot="action-menu"></umb-workspace-entity-action-menu>`;
}

static override styles = [
css`
umb-workspace-editor {
visibility: visible;
}
umb-workspace-editor.hide {
visibility: hidden;
}
`,
];
}

declare global {
interface HTMLElementTagNameMap {
'umb-entity-detail-workspace-editor': UmbEntityDetailWorkspaceEditorElement;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import './entity-detail-not-found.element.js';
import './entity-detail-workspace-editor.element.js';

export * from './entity-detail-not-found.element.js';
export * from './entity-detail-workspace-editor.element.js';
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
import './global-components/index.js';

export * from './entity-detail-workspace-base.js';
export * from './global-components/index.js';
export type * from './types.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';

export interface UmbEntityDetailWorkspaceContextArgs {
entityType: string;
workspaceAlias: string;
detailRepositoryAlias: string;
}

/**
* @deprecated Use UmbEntityDetailWorkspaceContextArgs instead
*/
export type UmbEntityWorkspaceContextArgs = UmbEntityDetailWorkspaceContextArgs;

export interface UmbEntityDetailWorkspaceContextCreateArgs<DetailModelType> {
parent: UmbEntityModel;
preset?: Partial<DetailModelType>;
}
Loading

0 comments on commit 310ccdc

Please sign in to comment.