deep partial modal data, for more fine grained definitions

This commit is contained in:
Niels Lyngsø
2024-04-24 23:42:36 +02:00
parent fdbe522a8e
commit 433bdc4c74
10 changed files with 80 additions and 26 deletions

View File

@@ -38,7 +38,7 @@ export class UmbModalManagerContext extends UmbContextBase<UmbModalManagerContex
* @memberof UmbModalManagerContext
*/
public open<
ModalData extends object = object,
ModalData extends { [key: string]: any } = { [key: string]: any },
ModalValue = unknown,
ModalAliasTypeAsToken extends UmbModalToken = UmbModalToken<ModalData, ModalValue>,
>(

View File

@@ -6,6 +6,7 @@ import type { UUIModalSidebarSize } from '@umbraco-cms/backoffice/external/uui';
import { UmbId } from '@umbraco-cms/backoffice/id';
import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import { type UmbDeepPartialObject, umbDeepMerge } from '@umbraco-cms/backoffice/utils';
export interface UmbModalRejectReason {
type: string;
@@ -13,7 +14,9 @@ export interface UmbModalRejectReason {
export type UmbModalContextClassArgs<
ModalAliasType extends string | UmbModalToken,
ModalAliasTypeAsToken extends UmbModalToken = ModalAliasType extends UmbModalToken ? ModalAliasType : UmbModalToken,
ModalAliasTypeAsToken extends UmbModalToken = ModalAliasType extends UmbModalToken
? ModalAliasType
: UmbModalToken<never, never>,
> = {
router?: IRouterSlot | null;
data?: ModalAliasTypeAsToken['DATA'];
@@ -22,7 +25,10 @@ export type UmbModalContextClassArgs<
};
// TODO: consider splitting this into two separate handlers
export class UmbModalContext<ModalPreset extends object = object, ModalValue = any> extends UmbControllerBase {
export class UmbModalContext<
ModalPreset extends { [key: string]: any } = { [key: string]: any },
ModalValue = any,
> extends UmbControllerBase {
//
#submitPromise: Promise<ModalValue>;
#submitResolver?: (value: ModalValue) => void;
@@ -60,7 +66,13 @@ export class UmbModalContext<ModalPreset extends object = object, ModalValue = a
this.backdropBackground = args.modal?.backdropBackground || this.backdropBackground;
const defaultData = this.alias instanceof UmbModalToken ? this.alias.getDefaultData() : undefined;
this.data = Object.freeze({ ...defaultData, ...args.data } as ModalPreset);
this.data = Object.freeze(
// If we have both data and defaultData perform a deep merge
args.data && defaultData
? (umbDeepMerge(args.data as UmbDeepPartialObject<ModalPreset>, defaultData) as ModalPreset)
: // otherwise pick one of them:
(args.data as ModalPreset) ?? defaultData,
);
const initValue =
args.value ?? (this.alias instanceof UmbModalToken ? (this.alias as UmbModalToken).getDefaultValue() : undefined);

View File

@@ -1,25 +1,30 @@
import type { UmbModalToken } from '../token/index.js';
import type { UmbModalConfig, UmbModalContext, UmbModalManagerContext, UmbModalRouteRegistration } from '../index.js';
import type { UmbModalContextClassArgs } from '../context/modal.context.js';
import { type Params, type IRouterSlot, UMB_ROUTE_CONTEXT, encodeFolderName } from '@umbraco-cms/backoffice/router';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import { UmbId } from '@umbraco-cms/backoffice/id';
import type { UmbDeepPartialObject } from '@umbraco-cms/backoffice/utils';
export type UmbModalRouteBuilder = (params: { [key: string]: string | number } | null) => string;
export type UmbModalRouteSetupReturn<UmbModalTokenData, UmbModalTokenValue> = UmbModalTokenValue extends undefined
? {
modal?: UmbModalConfig;
data: UmbModalTokenData;
modal?: UmbDeepPartialObject<UmbModalConfig>;
data: UmbDeepPartialObject<UmbModalTokenData>;
value?: UmbModalTokenValue;
}
: {
modal?: UmbModalConfig;
data: UmbModalTokenData;
modal?: UmbDeepPartialObject<UmbModalConfig>;
data: UmbDeepPartialObject<UmbModalTokenData>;
value: UmbModalTokenValue;
};
export class UmbModalRouteRegistrationController<UmbModalTokenData extends object = object, UmbModalTokenValue = any>
export class UmbModalRouteRegistrationController<
UmbModalTokenData extends { [key: string]: any } = { [key: string]: any },
UmbModalTokenValue = any,
>
extends UmbControllerBase
implements UmbModalRouteRegistration<UmbModalTokenData, UmbModalTokenValue>
{
@@ -293,8 +298,8 @@ export class UmbModalRouteRegistrationController<UmbModalTokenData extends objec
modal: {},
...modalData,
router,
};
args.modal.key = this.#key;
} as UmbModalContextClassArgs<UmbModalToken<UmbModalTokenData, UmbModalTokenValue>>;
args.modal!.key = this.#key;
this.#modalContext = modalManagerContext.open(this, this.#modalAlias, args);
this.#modalContext.onSubmit().then(this.#onSubmit, this.#onReject);

View File

@@ -3,7 +3,10 @@ import type { UmbModalContext, UmbModalRouteBuilder } from '../index.js';
import type { UmbModalToken } from '../token/modal-token.js';
import type { IRouterSlot, Params } from '@umbraco-cms/backoffice/router';
export interface UmbModalRouteRegistration<UmbModalTokenData extends object = object, UmbModalTokenValue = any> {
export interface UmbModalRouteRegistration<
UmbModalTokenData extends { [key: string]: any } = { [key: string]: any },
UmbModalTokenValue = any,
> {
key: string;
alias: UmbModalToken<UmbModalTokenData, UmbModalTokenValue> | string;

View File

@@ -1,12 +1,18 @@
import type { UmbModalConfig } from '../context/modal-manager.context.js';
export interface UmbModalTokenDefaults<ModalDataType extends object = object, ModalValueType = unknown> {
export interface UmbModalTokenDefaults<
ModalDataType extends { [key: string]: any } = { [key: string]: any },
ModalValueType = unknown,
> {
modal?: UmbModalConfig;
data?: ModalDataType;
value?: ModalValueType;
}
export class UmbModalToken<ModalDataType extends object = object, ModalValueType = unknown> {
export class UmbModalToken<
ModalDataType extends { [key: string]: any } = { [key: string]: any },
ModalValueType = unknown,
> {
/**
* Get the data type of the token's data.
*

View File

@@ -11,7 +11,7 @@ export interface UmbTreePickerModalCreateActionData<PathGeneratorType extends Um
}
export interface UmbTreePickerModalData<
TreeItemType = any,
TreeItemType,
PathGeneratorType extends UmbPathGeneratorType = UmbPathGeneratorType,
> extends UmbPickerModalData<TreeItemType> {
treeAlias?: string;
@@ -21,7 +21,7 @@ export interface UmbTreePickerModalData<
export interface UmbTreePickerModalValue extends UmbPickerModalValue {}
export const UMB_TREE_PICKER_MODAL = new UmbModalToken<UmbTreePickerModalData, UmbTreePickerModalValue>(
export const UMB_TREE_PICKER_MODAL = new UmbModalToken<UmbTreePickerModalData<unknown>, UmbTreePickerModalValue>(
UMB_TREE_PICKER_MODAL_ALIAS,
{
modal: {

View File

@@ -17,5 +17,5 @@ export * from './string/generate-umbraco-alias.function.js';
export * from './string/increment-string.function.js';
export * from './string/split-string-to-array.js';
export * from './string/to-camel-case/to-camel-case.function.js';
export type * from './type/deep-partial.type.js';
export type * from './type/deep-partial-object.type.js';
export type * from './type/diff.type.js';

View File

@@ -1,17 +1,20 @@
import type { DeepPartial } from '../type/deep-partial.type.js';
import type { UmbDeepPartialObject } from '../type/deep-partial-object.type.js';
/**
* Deep merge two objects.
* @param target
* @param ...sources
*/
export function umbDeepMerge<T extends { [key: string]: any }>(source: DeepPartial<T>, fallback: T) {
export function umbDeepMerge<
T extends { [key: string]: any },
PartialType extends UmbDeepPartialObject<T> = UmbDeepPartialObject<T>,
>(source: PartialType, fallback: T) {
const result = { ...fallback };
for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key) && source[key] !== undefined) {
if (source[key].constructor === Object && fallback[key].constructor === Object) {
result[key] = umbDeepMerge(source[key], fallback[key]);
if (source[key]?.constructor === Object && fallback[key].constructor === Object) {
result[key] = umbDeepMerge(source[key] as any, fallback[key]);
} else {
result[key] = source[key] as any;
}

View File

@@ -0,0 +1,30 @@
/*export type DeepPartial<T> = T extends { [key: string]: any }
? {
[P in keyof T]?: DeepPartial<T[P]>;
}
: T;
*/
// Notice this can be way more complex, but in this case I just wanted to cover pure objects, to match our deep merge function [NL]
// See https://stackoverflow.com/questions/61132262/typescript-deep-partial for more extensive solutions.
/**
* Deep partial object type, making objects and their properties optional, but only until a property of a different type is encountered.
* This means if an object holds a property with an array that holds objects, the array will be made optional, but the properties of the objects inside the array will not be changed.
* @type UmbDeepPartialObject
* @generic T - The object to make partial.
* @returns A type with all properties of objects made optional.
*/
// eslint-disable-next-line @typescript-eslint/ban-types
export type UmbDeepPartialObject<T> = T extends Function
? T
: // Thing extends Array<infer InferredArrayMember>
// ? DeepPartialArray<InferredArrayMember> :
T extends { [key: string]: any }
? UmbDeepPartialObjectProperty<T>
: T | undefined;
//interface DeepPartialArray<Thing> extends Array<DeepPartial<Thing>> {}
type UmbDeepPartialObjectProperty<Thing> = {
[Key in keyof Thing]?: UmbDeepPartialObject<Thing[Key]>;
};

View File

@@ -1,5 +0,0 @@
export type DeepPartial<T> = T extends object
? {
[P in keyof T]?: DeepPartial<T[P]>;
}
: T;