POC Modal deeplinking

This commit is contained in:
Niels Lyngsø
2023-03-14 12:00:43 +01:00
parent f0692baf54
commit 763121df1f
10 changed files with 117 additions and 49 deletions

View File

@@ -13,7 +13,7 @@ import { ManifestModal } from '@umbraco-cms/extensions-registry';
/**
* Type which omits the real submit method, and replaces it with a submit method which accepts an optional argument depending on the generic type.
*/
export type UmbModalHandler<ModalData = unknown, ModalResult = unknown> = Omit<
export type UmbModalHandler<ModalData extends object = { [key: string]: unknown }, ModalResult = unknown> = Omit<
UmbModalHandlerClass<ModalData, ModalResult>,
'submit'
> &
@@ -35,7 +35,7 @@ type OptionalSubmitArgumentIfUndefined<T> = T extends undefined
};
//TODO consider splitting this into two separate handlers
export class UmbModalHandlerClass<ModalData, ModalResult> {
export class UmbModalHandlerClass<ModalData extends object, ModalResult> {
private _submitPromise: Promise<ModalResult>;
private _submitResolver?: (value: ModalResult) => void;
private _submitRejecter?: () => void;

View File

@@ -76,7 +76,7 @@ export class UmbModalContext {
* @return {*} {UmbModalHandler}
* @memberof UmbModalContext
*/
public open<ModalData = unknown, ModalResult = unknown>(
public open<ModalData extends object = { [key: string]: unknown }, ModalResult = unknown>(
modalAlias: string | UmbModalToken<ModalData, ModalResult>,
data?: ModalData,
config?: UmbModalConfig

View File

@@ -1,6 +1,6 @@
import { UmbModalConfig } from '../modal.context';
export class UmbModalToken<Data = unknown, Result = unknown> {
export class UmbModalToken<Data extends object = { [key: string]: unknown }, Result = unknown> {
/**
* Get the data type of the token's data.
*

View File

@@ -3,10 +3,9 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, property } from 'lit/decorators.js';
import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins';
import { UUIModalSidebarSize } from '@umbraco-ui/uui-modal-sidebar';
import { UmbRouteContext, UMB_ROUTE_CONTEXT_TOKEN } from '@umbraco-cms/router';
import { UmbModalRouteBuilder, UmbRouteContext, UMB_ROUTE_CONTEXT_TOKEN } from '@umbraco-cms/router';
import { UmbLinkPickerLink, UMB_LINK_PICKER_MODAL_TOKEN } from '../../modals/link-picker';
import { UmbLitElement } from '@umbraco-cms/element';
import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal';
/**
* @element umb-input-multi-url-picker
@@ -88,7 +87,7 @@ export class UmbInputMultiUrlPickerElement extends FormControlMixin(UmbLitElemen
@property({ attribute: false })
set urls(data: Array<UmbLinkPickerLink>) {
if (!data) return;
this._urls = data;
this._urls = [...data]; // Unfreeze data coming from State, so we can manipulate it.
super.value = this._urls.map((x) => x.url).join(',');
}
@@ -97,9 +96,11 @@ export class UmbInputMultiUrlPickerElement extends FormControlMixin(UmbLitElemen
}
private _urls: Array<UmbLinkPickerLink> = [];
private _modalContext?: UmbModalContext;
//private _modalContext?: UmbModalContext;
private _routeContext?: UmbRouteContext;
private _linkPickerURL?: UmbModalRouteBuilder;
constructor() {
super();
this.addValidator(
@@ -113,9 +114,11 @@ export class UmbInputMultiUrlPickerElement extends FormControlMixin(UmbLitElemen
() => !!this.max && this.urls.length > this.max
);
/*
this.consumeContext(UMB_MODAL_CONTEXT_TOKEN, (instance) => {
this._modalContext = instance;
});
*/
this.consumeContext(UMB_ROUTE_CONTEXT_TOKEN, (instance) => {
this._routeContext = instance;
@@ -123,16 +126,29 @@ export class UmbInputMultiUrlPickerElement extends FormControlMixin(UmbLitElemen
// Registre the routes of this UI:
// TODO: To avoid retriving the property alias, we might make use of the property context?
// Or maybe its not the property-alias, but something unique? as this might not be in a property?.
this._routeContext.registerModal(UMB_LINK_PICKER_MODAL_TOKEN, {
//path: `${'to-do-myPropertyAlias'}/:index`,
path: `modal`,
onSetup: (routeInfo) => {
this._linkPickerURL = this._routeContext.registerModal(UMB_LINK_PICKER_MODAL_TOKEN, {
path: `${'to-do-myPropertyAlias'}/:index`,
onSetup: (routingInfo) => {
// Get index from routeInfo:
const index = 0;
const indexParam = routingInfo.match.params.index;
if (!indexParam) return false;
let index: number | null = parseInt(routingInfo.match.params.index);
if (Number.isNaN(index)) return false;
// Use the index to find data:
console.log('onSetup modal', routeInfo);
/*
modaldata = {
console.log('onSetup modal index:', index);
let data: UmbLinkPickerLink | null = null;
if (index >= 0 && index < this.urls.length) {
data = this._getItemByIndex(index);
} else {
index = null;
}
console.log('onSetup modal got data:', data);
const modalData = {
index: index,
link: {
name: data?.name,
published: data?.published,
@@ -148,14 +164,12 @@ export class UmbInputMultiUrlPickerElement extends FormControlMixin(UmbLitElemen
overlaySize: this.overlaySize || 'small',
},
};
return modaldata;
*/
return modalData;
},
onSubmit: (newUrl) => {
if (!newUrl) return;
const index = 0; // TODO: get the index in some way?.
this._setSelection(newUrl, index);
onSubmit: (submitData) => {
console.log('On submit in property editor input');
if (!submitData) return;
this._setSelection(submitData.link, submitData.index);
},
onReject: () => {
console.log('User cancelled dialog.');
@@ -169,9 +183,16 @@ export class UmbInputMultiUrlPickerElement extends FormControlMixin(UmbLitElemen
this._dispatchChangeEvent();
}
private _setSelection(selection: UmbLinkPickerLink, index?: number) {
if (index !== undefined && index >= 0) this.urls[index] = selection;
else this.urls.push(selection);
private _getItemByIndex(index: number) {
return this.urls[index];
}
private _setSelection(selection: UmbLinkPickerLink, index: number | null) {
if (index !== null && index >= 0) {
this.urls[index] = selection;
} else {
this.urls.push(selection);
}
this._dispatchChangeEvent();
}
@@ -182,6 +203,8 @@ export class UmbInputMultiUrlPickerElement extends FormControlMixin(UmbLitElemen
}
private _openPicker(data?: UmbLinkPickerLink, index?: number) {
console.log('JS open picker, should fail for now,');
/*
const modalHandler = this._modalContext?.open(UMB_LINK_PICKER_MODAL_TOKEN, {
link: {
name: data?.name,
@@ -203,11 +226,13 @@ export class UmbInputMultiUrlPickerElement extends FormControlMixin(UmbLitElemen
this._setSelection(newUrl, index);
});
*/
}
render() {
return html`${this.urls?.map((link, index) => this._renderItem(link, index))}
<uui-button look="placeholder" label="Add" @click=${this._openPicker}>Add</uui-button>`;
<uui-button look="placeholder" label="Add" .href=${this._linkPickerURL?.({ index: -1 })}>Add</uui-button>`;
// "modal/Umb.Modal.LinkPicker/${'to-do-myPropertyAlias'}/-1"
}
private _renderItem(link: UmbLinkPickerLink, index: number) {
@@ -217,10 +242,11 @@ export class UmbInputMultiUrlPickerElement extends FormControlMixin(UmbLitElemen
@open="${() => this._openPicker(link, index)}">
<uui-icon slot="icon" name="${link.icon || 'umb:link'}"></uui-icon>
<uui-action-bar slot="actions">
<uui-button @click="${() => this._openPicker(link, index)}" label="Edit link">Edit</uui-button>
<uui-button .href=${this._linkPickerURL?.({ index })} label="Edit link">Edit</uui-button>
<uui-button @click="${() => this._removeItem(index)}" label="Remove link">Remove</uui-button>
</uui-action-bar>
</uui-ref-node>`;
// "modal/Umb.Modal.LinkPicker/${'to-do-myPropertyAlias'}/${index}"
}
}

View File

@@ -2,11 +2,12 @@ import { UUIModalSidebarSize } from '@umbraco-ui/uui-modal-sidebar';
import { UmbModalToken } from '@umbraco-cms/modal';
export interface UmbLinkPickerModalData {
index: number | null;
link: UmbLinkPickerLink;
config: UmbLinkPickerConfig;
}
export type UmbLinkPickerModalResult = UmbLinkPickerLink;
export type UmbLinkPickerModalResult = { index: number | null; link: UmbLinkPickerLink };
export interface UmbLinkPickerLink {
icon?: string | null;

View File

@@ -46,6 +46,9 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
@state()
_selectedKey?: string;
@state()
_index: number | null = null;
@state()
_link: UmbLinkPickerLink = {
icon: null,
@@ -76,6 +79,7 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
connectedCallback() {
super.connectedCallback();
if (!this.data) return;
this._index = this.data?.index;
this._link = this.data?.link;
this._layout = this.data?.config;
@@ -105,7 +109,7 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
}
private _submit() {
this.modalHandler?.submit(this._link);
this.modalHandler?.submit({ index: this._index, link: this._link });
}
private _close() {

View File

@@ -53,6 +53,7 @@ export class UmbPropertyEditorUIMultiUrlPickerElement extends UmbLitElement impl
private _onChange(event: CustomEvent) {
this.value = (event.target as UmbInputMultiUrlPickerElement).urls;
console.log(this.value);
this.dispatchEvent(new CustomEvent('property-value-change'));
}

View File

@@ -47,7 +47,17 @@ export const data: Array<DocumentModel> = [
alias: 'multiUrlPicker',
culture: null,
segment: null,
value: null,
value: [
{
name: undefined,
published: undefined,
queryString: undefined,
target: undefined,
trashed: undefined,
udi: 'umb://document/c05da24d7740447b9cdcbd8ce2172e38',
url: 'umb://document/c05da24d7740447b9cdcbd8ce2172e38',
},
],
},
{
alias: 'multiNodeTreePicker',

View File

@@ -1,22 +1,23 @@
import { IRoute } from 'router-slot';
import { IRoute, IRoutingInfo, PARAM_IDENTIFIER, stripSlash } from 'router-slot';
import { UmbContextConsumerController, UmbContextProviderController, UmbContextToken } from '@umbraco-cms/context-api';
import { UmbControllerHostInterface } from '@umbraco-cms/controller';
import { UmbModalToken } from '@umbraco-cms/modal';
// Get the second generic type of UmbModalToken:
type GetResultType<T> = T extends UmbModalToken<infer Data, infer Result> ? Result : unknown;
import { UmbModalToken, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal';
const EmptyDiv = document.createElement('div');
export type UmbModalRoute<UmbModalTokenResult> = {
// TODO: Consider accepting the Token as a generic:
export type UmbModalRoute<UmbModalTokenData extends object, UmbModalTokenResult> = {
path: string;
onSetup: (routeInfo: any) => void;
onSubmit: (data: UmbModalTokenResult) => void;
onSetup: (routingInfo: IRoutingInfo) => UmbModalTokenData | false;
onSubmit: (data: UmbModalTokenResult) => void | PromiseLike<void>;
onReject: () => void;
};
export type UmbModalRouteBuilder = (params: { [key: string]: string | number }) => string;
export class UmbRouteContext {
#host: UmbControllerHostInterface;
#modalContext?: typeof UMB_MODAL_CONTEXT_TOKEN.TYPE;
#contextRoutes: IRoute[] = [];
constructor(host: UmbControllerHostInterface, private _onGotModals: (contextRoutes: any) => void) {
@@ -25,28 +26,54 @@ export class UmbRouteContext {
/*new UmbContextConsumerController(host, UMB_ROUTE_CONTEXT_TOKEN, (context) => {
console.log('got a parent', this === context, this, context);
});*/
new UmbContextConsumerController(host, UMB_MODAL_CONTEXT_TOKEN, (context) => {
this.#modalContext = context;
});
// Consider using this event, to stay up to date with current full-URL. which is necessary for Modal opening.
// window.addEventListener('navigationsuccess', this._onNavigationChanged);
}
public registerModal<T extends UmbModalToken = UmbModalToken, R = GetResultType<T>>(
modalAlias: T,
options: UmbModalRoute<R>
) {
console.log('registerModalRoute', modalAlias.toString(), options);
public registerModal<D extends object = object, R = unknown>(
modalAlias: UmbModalToken<D, R> | string,
options: UmbModalRoute<D, R>
): UmbModalRouteBuilder {
const localPath = `modal/${modalAlias.toString()}/${options.path}`;
this.#contextRoutes.push({
path: options.path,
path: localPath,
pathMatch: 'suffix',
component: EmptyDiv,
setup: (component, info) => {
console.log('modal open?', info);
options.onSetup(info);
const modalData = options.onSetup(info);
if (modalData && this.#modalContext) {
const modalHandler = this.#modalContext.open<D, R>(modalAlias, modalData);
modalHandler.onSubmit().then(
() => this._removeModalPath(info),
() => this._removeModalPath(info)
);
modalHandler.onSubmit().then(options.onSubmit, options.onReject);
}
},
});
//TODO: move to a method:
this._onGotModals(this.#contextRoutes);
return (params: { [key: string]: string | number }) => {
const localRoutePath = stripSlash(
localPath.replace(PARAM_IDENTIFIER, (substring: string, ...args: string[]) => {
return params[args[0]];
//return `([^\/]+)`;
})
);
const baseRoutePath = window.location.href;
return (baseRoutePath.endsWith('/') ? baseRoutePath : baseRoutePath + '/') + localRoutePath;
};
}
private _removeModalPath(info: IRoutingInfo) {
window.history.pushState({}, '', window.location.href.split(info.match.fragments.consumed)[0]);
console.log('ask to remove path', info.match.fragments.consumed);
}
}

View File

@@ -44,7 +44,6 @@ export class UmbRouterSlotElement extends UmbLitElement {
#routeContext = new UmbRouteContext(this, (contextRoutes) => {
(this.#modalRouter as any).routes = contextRoutes;
console.log(this.absoluteRouterPath, this.#router, this.#modalRouter, 'Router got context routes', contextRoutes);
// Force a render?
this.#modalRouter.render();
});