Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V15: Bugfix: Add "Not Found" empty state to detail workspaces #17489

Merged
merged 47 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
8f1a07d
remove custom implementation from data type
madsrasmussen Nov 11, 2024
e571a7b
clear workspace data when entity gets removed from the store
madsrasmussen Nov 11, 2024
a603379
remove unused alias
madsrasmussen Nov 11, 2024
591ecbe
remove custom workspace implementations
madsrasmussen Nov 11, 2024
80b85a5
use data clear method instead
madsrasmussen Nov 11, 2024
2c82944
remove unused imports
madsrasmussen Nov 11, 2024
14ef1e7
wip entity-detail-not-found
madsrasmussen Nov 11, 2024
c94950a
Merge branch 'v15/bugfix/clean-up-unused-workspace-alias' into v15/bu…
madsrasmussen Nov 11, 2024
51c5fab
add loader property + loader bar
madsrasmussen Nov 11, 2024
b391d74
add loader property + loader bar
madsrasmussen Nov 11, 2024
3288390
add loading state manager to entity detail base
madsrasmussen Nov 11, 2024
18c8527
add entity-detail editor incl loading state
madsrasmussen Nov 11, 2024
2e85f92
add localization keys
madsrasmussen Nov 11, 2024
ad70dfa
update keys
madsrasmussen Nov 11, 2024
be8ab4e
render error element
madsrasmussen Nov 11, 2024
00d50ab
clean up
madsrasmussen Nov 11, 2024
bf2cb3c
Merge branch 'v15/dev' into v15/bugfix/handle-deleted-open-workspace-…
madsrasmussen Nov 12, 2024
df6fe95
add language root workspace path pattern
madsrasmussen Nov 12, 2024
cf993c1
use entity detail workspace editor element
madsrasmussen Nov 12, 2024
0a4b07b
pass backPath property
madsrasmussen Nov 12, 2024
6bb4169
use entity detail editor for user workspace
madsrasmussen Nov 12, 2024
fd7fd93
simplify
madsrasmussen Nov 12, 2024
0aec36a
add default value
madsrasmussen Nov 12, 2024
af0b0de
add descriminator
madsrasmussen Nov 12, 2024
8111a0f
add null check
madsrasmussen Nov 12, 2024
22bfae8
add todo
madsrasmussen Nov 12, 2024
b80bd6b
observe unique
madsrasmussen Nov 12, 2024
f619428
fix lint errors
madsrasmussen Nov 12, 2024
26c5b35
render entity actions in entity-detail-editor
madsrasmussen Nov 12, 2024
5835064
implement editor
madsrasmussen Nov 12, 2024
c0e665e
clean up member group
madsrasmussen Nov 12, 2024
d5b5244
use new editor
madsrasmussen Nov 12, 2024
49f376e
use new editor
madsrasmussen Nov 12, 2024
921e3b8
remove unused
madsrasmussen Nov 12, 2024
2d7442c
Merge branch 'v15/dev' into v15/bugfix/handle-deleted-open-workspace-…
madsrasmussen Nov 12, 2024
ce9548e
use new editor
madsrasmussen Nov 12, 2024
7e44d87
Merge branch 'v15/dev' into v15/bugfix/handle-deleted-open-workspace-…
madsrasmussen Nov 13, 2024
3aa9365
Merge branch 'v15/dev' into v15/bugfix/handle-deleted-open-workspace-…
madsrasmussen Nov 13, 2024
a327baf
temp hide the editor instead of removing it
madsrasmussen Nov 13, 2024
39b9b98
update message
madsrasmussen Nov 13, 2024
34d663a
Merge branch 'v15/dev' into v15/bugfix/handle-deleted-open-workspace-…
madsrasmussen Nov 14, 2024
53a870e
Merge branch 'v15/dev' into v15/bugfix/handle-deleted-open-workspace-…
madsrasmussen Nov 15, 2024
f6aa21c
remove unused import
madsrasmussen Nov 15, 2024
e739fb4
Merge branch 'v15/dev' into v15/bugfix/handle-deleted-open-workspace-…
madsrasmussen Nov 18, 2024
dd5311e
Update workspace-context.interface.ts
nielslyngsoe Nov 19, 2024
cad2d70
minor refactor
nielslyngsoe Nov 19, 2024
c5a7756
Merge branch 'v15/dev' into v15/bugfix/handle-deleted-open-workspace-…
nielslyngsoe Nov 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading