Merge branch 'main' into next

This commit is contained in:
nocobase[bot]
2026-07-22 06:52:12 +00:00
8 changed files with 1826 additions and 295 deletions
@@ -225,6 +225,108 @@ describe('ensureFormValueDrivenDataScopeClear', () => {
expect(onChange).toHaveBeenCalledWith(null);
});
it('does not clear a popup form field when data scope depends on the external parent item', () => {
const emitter = new EventEmitter();
const formBlock = {
uid: 'form-1',
disposed: false,
emitter,
context: {
form: {},
formValues: { staff_m2o: null },
},
};
const onChange = vi.fn();
const model: any = {
disposed: false,
props: {
value: { id: 10 },
onChange,
},
context: {
blockModel: formBlock,
},
};
const ctx: any = {
model,
flowKey: 'selectSettings',
};
const filter = {
logic: '$and',
items: [{ path: 'orgId', operator: '$eq', value: '{{ ctx.item.parentItem.value.org_m2o.id }}' }],
};
ensureFormValueDrivenDataScopeClear(ctx, filter);
emitter.emit('formValuesChange', {
changedPaths: [['staff_m2o']],
allValues: { staff_m2o: { id: 10 } },
});
expect(onChange).not.toHaveBeenCalled();
});
it('maps popup current item dependencies to the popup form root', () => {
const emitter = new EventEmitter();
const formBlock = {
uid: 'form-1',
disposed: false,
emitter,
context: {
form: {},
formValues: {
org_m2o: { id: 1 },
staff_m2o: { id: 10 },
},
},
};
const onChange = vi.fn();
const model: any = {
disposed: false,
props: {
value: { id: 10 },
onChange,
},
context: {
blockModel: formBlock,
},
};
const ctx: any = {
model,
flowKey: 'selectSettings',
};
const filter = {
logic: '$and',
items: [{ path: 'orgId', operator: '$eq', value: '{{ ctx.item.value.org_m2o.id }}' }],
};
ensureFormValueDrivenDataScopeClear(ctx, filter);
emitter.emit('formValuesChange', {
changedPaths: [['staff_m2o']],
allValues: {
org_m2o: { id: 1 },
staff_m2o: { id: 10 },
},
});
expect(onChange).not.toHaveBeenCalled();
emitter.emit('formValuesChange', {
changedPaths: [['org_m2o']],
allValues: {
org_m2o: { id: 2 },
staff_m2o: { id: 10 },
},
});
expect(onChange).toHaveBeenCalledWith(null);
});
it('does not clear a row field when a sibling field changes but the item dependency value is unchanged', () => {
const emitter = new EventEmitter();
const formBlock = {
@@ -105,9 +105,6 @@ function resolveItemDependencyPath(ctx: FlowContext, depPath: NamePath): DataSco
ctx,
parseFieldIndexEntries((ctx.model as any)?.context?.fieldIndex ?? (ctx as any)?.fieldIndex),
);
if (!entries.length) {
return wildcardDeps();
}
let parentDepth = 0;
let cursor = [...depPath];
@@ -121,6 +118,10 @@ function resolveItemDependencyPath(ctx: FlowContext, depPath: NamePath): DataSco
return wildcardDeps();
}
if (!entries.length) {
return parentDepth === 0 ? resolveRootItemDependencyPath(cursor) : emptyDeps();
}
if (parentDepth === entries.length) {
return resolveRootItemDependencyPath(cursor);
}
@@ -98,6 +98,138 @@ type CalendarPopupActionOptions = {
persist?: boolean;
};
type CalendarPopupSettingsOptions = {
preferActionSettings?: boolean;
};
type CalendarPopupActionKey = 'quickCreateAction' | 'eventViewAction';
type CalendarPopupTemplateContextFlags = {
hasFilterByTk: boolean;
hasSourceId: boolean;
};
type CalendarPopupTemplateActionScene = 'record' | 'collection' | 'both' | undefined;
type CalendarPopupTemplateRow = {
useModel?: unknown;
filterByTk?: unknown;
sourceId?: unknown;
};
type CalendarPopupSettingsStepKey = 'quickCreatePopupSettings' | 'eventPopupSettings';
const CALENDAR_POPUP_SETTINGS_STEP_ACTIONS: Record<CalendarPopupSettingsStepKey, CalendarPopupActionKey> = {
quickCreatePopupSettings: 'quickCreateAction',
eventPopupSettings: 'eventViewAction',
};
const POPUP_TEMPLATE_SETTING_KEYS = [
'uid',
'dataSourceKey',
'collectionName',
'associationName',
'filterByTk',
'sourceId',
'popupTemplateUid',
'popupTemplateMode',
'popupTemplateContext',
'popupTemplateHasFilterByTk',
'popupTemplateHasSourceId',
];
const clearPopupTemplateParams = (params: Record<string, any>) => {
delete params.popupTemplateUid;
delete params.popupTemplateContext;
delete params.popupTemplateHasFilterByTk;
delete params.popupTemplateHasSourceId;
delete params.popupTemplateUseModel;
delete params.popupTemplateMode;
delete params.associationName;
delete params.filterByTk;
delete params.sourceId;
delete params.uid;
};
const clearPopupRecordScopeParams = (params: Record<string, any>) => {
delete params.filterByTk;
delete params.sourceId;
};
const normalizePopupTemplateString = (value: unknown): string => {
if (typeof value === 'string') {
return value.trim();
}
if (typeof value === 'number' || typeof value === 'boolean') {
return String(value).trim();
}
return '';
};
const resolveCalendarPopupTemplateActionScene = (
flowEngine: any,
useModel: unknown,
): CalendarPopupTemplateActionScene => {
const useKey = normalizePopupTemplateString(useModel);
if (!useKey) {
return undefined;
}
const ModelClass = flowEngine?.getModelClass?.(useKey);
const isScene = ModelClass?._isScene;
if (typeof isScene !== 'function') {
return undefined;
}
const isRecord = !!isScene.call(ModelClass, 'record');
const isCollection = !!isScene.call(ModelClass, 'collection');
if (isRecord && isCollection) {
return 'both';
}
if (isRecord) {
return 'record';
}
if (isCollection) {
return 'collection';
}
return undefined;
};
const inferCalendarPopupTemplateContextFlags = (
flowEngine: any,
template: CalendarPopupTemplateRow,
): CalendarPopupTemplateContextFlags => {
const scene = resolveCalendarPopupTemplateActionScene(flowEngine, template?.useModel);
const filterByTk = normalizePopupTemplateString(template?.filterByTk);
const sourceId = normalizePopupTemplateString(template?.sourceId);
const isCollectionOnly = scene === 'collection';
const isRecordOnly = scene === 'record';
let hasFilterByTk = false;
if (filterByTk) {
hasFilterByTk = !(isCollectionOnly && filterByTk.includes('ctx.record'));
} else if (isRecordOnly) {
hasFilterByTk = true;
}
let hasSourceId = false;
if (sourceId) {
hasSourceId = !(isCollectionOnly && sourceId.includes('ctx.resource'));
}
return { hasFilterByTk, hasSourceId };
};
const replaceCalendarActionOpenViewParams = (action: any, params: Record<string, any>) => {
if (!action) {
return;
}
action.stepParams = action.stepParams || {};
action.stepParams.popupSettings = action.stepParams.popupSettings || {};
action.stepParams.popupSettings.openView = { ...params };
action.emitter?.emit?.('onStepParamsChanged');
};
const isCalendarPopupTemplateCopyMode = (params?: Record<string, any>) => {
return params?.popupTemplateContext === true;
};
const DRAG_HANDLER_TOOLBAR_ITEMS = [
{
key: 'drag-handler',
@@ -301,6 +433,10 @@ export class CalendarBlockModel extends CollectionBlockModel {
return this.subModels?.eventViewAction as any;
}
getPopupAction(actionKey: CalendarPopupActionKey) {
return actionKey === 'quickCreateAction' ? this.getQuickCreateAction() : this.getEventViewAction();
}
getPopupSettingsDefaults(actionUid?: string) {
return {
mode: normalizeEventOpenMode(this.props?.eventOpenMode),
@@ -316,9 +452,130 @@ export class CalendarBlockModel extends CollectionBlockModel {
return actionKey === 'quickCreateAction' ? 'quickCreatePopupSettings' : 'eventPopupSettings';
}
getStoredPopupSettings(actionKey: 'quickCreateAction' | 'eventViewAction') {
getPopupActionSettings(action: any) {
return action?.getStepParams?.('popupSettings', 'openView') || {};
}
getRawStoredPopupSettings(actionKey: CalendarPopupActionKey) {
const propKey = this.getPopupSettingsPropKey(actionKey);
return this.props?.[propKey] || {};
if (this.props?.[propKey]) {
return this.props[propKey];
}
try {
return super.getStepParams('calendarSettings', propKey) || {};
} catch {
return {};
}
}
getStoredPopupSettings(actionKey: 'quickCreateAction' | 'eventViewAction') {
return this.getRawStoredPopupSettings(actionKey);
}
getPopupSettingsStepParams(stepKey: CalendarPopupSettingsStepKey) {
const actionKey = CALENDAR_POPUP_SETTINGS_STEP_ACTIONS[stepKey];
const action = this.getPopupAction(actionKey);
return this.getPopupSettings(action, actionKey, action?.uid);
}
getStepParams(flowKey: string, stepKey: string): any | undefined;
getStepParams(flowKey: string): Record<string, any> | undefined;
getStepParams(): any;
getStepParams(flowKey?: string, stepKey?: string): any {
if (flowKey === 'calendarSettings' && stepKey && stepKey in CALENDAR_POPUP_SETTINGS_STEP_ACTIONS) {
return this.getPopupSettingsStepParams(stepKey as CalendarPopupSettingsStepKey);
}
if (flowKey === 'calendarSettings' && !stepKey) {
const params = super.getStepParams(flowKey) || {};
return {
...params,
quickCreatePopupSettings: this.getPopupSettingsStepParams('quickCreatePopupSettings'),
eventPopupSettings: this.getPopupSettingsStepParams('eventPopupSettings'),
};
}
if (flowKey && stepKey) {
return super.getStepParams(flowKey, stepKey);
}
if (flowKey) {
return super.getStepParams(flowKey);
}
return super.getStepParams();
}
async preparePopupSettingsForFlowSettings(flowKey?: string, stepKey?: string) {
if (flowKey && flowKey !== 'calendarSettings') {
return;
}
const stepKeys =
stepKey && stepKey in CALENDAR_POPUP_SETTINGS_STEP_ACTIONS
? [stepKey as CalendarPopupSettingsStepKey]
: (Object.keys(CALENDAR_POPUP_SETTINGS_STEP_ACTIONS) as CalendarPopupSettingsStepKey[]);
for (const currentStepKey of stepKeys) {
const actionKey = CALENDAR_POPUP_SETTINGS_STEP_ACTIONS[currentStepKey];
const action = await this.ensurePopupAction(actionKey);
await this.syncPopupActionSettings(action, actionKey);
}
}
async openFlowSettings(options?: Parameters<CollectionBlockModel['openFlowSettings']>[0]) {
await this.preparePopupSettingsForFlowSettings(options?.flowKey, options?.stepKey);
return super.openFlowSettings(options);
}
async openStepSettingsDialog(flowKey: string, stepKey: string) {
await this.preparePopupSettingsForFlowSettings(flowKey, stepKey);
return super.openStepSettingsDialog(flowKey, stepKey);
}
clearStoredPopupSettings(actionKey: 'quickCreateAction' | 'eventViewAction') {
const propKey = this.getPopupSettingsPropKey(actionKey);
this.setProps({
[propKey]: undefined,
});
this.setStepParams('calendarSettings', {
[propKey]: {},
});
}
hasPopupTemplateState(popupSettings?: Record<string, any>) {
if (!popupSettings || typeof popupSettings !== 'object') {
return false;
}
const popupTemplateUid =
typeof popupSettings.popupTemplateUid === 'string'
? popupSettings.popupTemplateUid.trim()
: popupSettings.popupTemplateUid;
return !!popupTemplateUid || popupSettings.popupTemplateContext === true;
}
mergePopupTemplateSettings(baseSettings: Record<string, any>, popupSettings?: Record<string, any>) {
if (!this.hasPopupTemplateState(popupSettings)) {
return baseSettings;
}
const nextSettings = { ...baseSettings };
POPUP_TEMPLATE_SETTING_KEYS.forEach((key) => {
if (Object.prototype.hasOwnProperty.call(popupSettings, key)) {
nextSettings[key] = popupSettings[key];
}
});
if (popupSettings?.popupTemplateContext === true) {
delete nextSettings.popupTemplateUid;
delete nextSettings.popupTemplateHasFilterByTk;
delete nextSettings.popupTemplateHasSourceId;
} else if (popupSettings?.popupTemplateUid) {
delete nextSettings.popupTemplateContext;
}
return nextSettings;
}
normalizePopupSettings(actionKey: 'quickCreateAction' | 'eventViewAction', popupSettings?: Record<string, any>) {
@@ -331,27 +588,65 @@ export class CalendarBlockModel extends CollectionBlockModel {
if (
popupTemplateUidProvided &&
(popupTemplateUid === undefined || popupTemplateUid === null || popupTemplateUid === '')
(popupTemplateUid === undefined || popupTemplateUid === null || popupTemplateUid === '') &&
!isCalendarPopupTemplateCopyMode(nextParams)
) {
clearPopupTemplateParams(nextParams);
}
if (
popupTemplateUidProvided &&
(popupTemplateUid === undefined || popupTemplateUid === null || popupTemplateUid === '') &&
isCalendarPopupTemplateCopyMode(nextParams)
) {
delete nextParams.popupTemplateUid;
delete nextParams.popupTemplateContext;
delete nextParams.popupTemplateHasFilterByTk;
delete nextParams.popupTemplateHasSourceId;
delete nextParams.uid;
}
// Quick create is collection-scene; keep it isolated from record-scoped template state.
if (actionKey === 'quickCreateAction' && nextParams.popupTemplateHasFilterByTk) {
delete nextParams.popupTemplateUid;
delete nextParams.popupTemplateContext;
delete nextParams.popupTemplateHasFilterByTk;
delete nextParams.popupTemplateHasSourceId;
delete nextParams.uid;
if (
actionKey === 'quickCreateAction' &&
(nextParams.popupTemplateHasFilterByTk === true || nextParams.popupTemplateHasSourceId === true)
) {
clearPopupTemplateParams(nextParams);
} else if (actionKey === 'quickCreateAction') {
clearPopupRecordScopeParams(nextParams);
}
delete nextParams.popupTemplateUseModel;
return nextParams;
}
async normalizeQuickCreatePopupTemplateContext(
actionKey: CalendarPopupActionKey,
popupSettings?: Record<string, any>,
defaultUid?: string,
) {
const nextSettings = { ...(popupSettings || {}) };
const templateUid =
typeof nextSettings.popupTemplateUid === 'string'
? nextSettings.popupTemplateUid.trim()
: nextSettings.popupTemplateUid;
if (actionKey !== 'quickCreateAction' || !templateUid) {
return nextSettings;
}
try {
const res = await this.context?.api?.resource?.('flowModelTemplates')?.get?.({ filterByTk: templateUid });
const template = res?.data?.data || {};
const templateContext = inferCalendarPopupTemplateContextFlags(this.flowEngine, template);
if (templateContext.hasFilterByTk || templateContext.hasSourceId) {
clearPopupTemplateParams(nextSettings);
if (defaultUid) {
nextSettings.uid = defaultUid;
}
}
} catch {
// Keep current params when template metadata cannot be loaded.
}
return nextSettings;
}
setPopupSettings(actionKey: 'quickCreateAction' | 'eventViewAction', popupSettings?: Record<string, any>) {
const propKey = this.getPopupSettingsPropKey(actionKey);
const normalized = this.normalizePopupSettings(actionKey, popupSettings);
@@ -361,17 +656,40 @@ export class CalendarBlockModel extends CollectionBlockModel {
return normalized;
}
getPopupSettings(action: any, actionKey: 'quickCreateAction' | 'eventViewAction', actionUid?: string) {
getPopupSettings(
action: any,
actionKey: 'quickCreateAction' | 'eventViewAction',
actionUid?: string,
options: CalendarPopupSettingsOptions = {},
) {
const defaults = this.getPopupSettingsDefaults(action?.uid || actionUid);
const currentParams = this.getStoredPopupSettings(actionKey);
return {
const actionParams = this.getPopupActionSettings(action);
const currentParams =
options.preferActionSettings !== false && Object.keys(actionParams).length > 0
? actionParams
: this.getStoredPopupSettings(actionKey);
const popupSettings = {
...defaults,
...currentParams,
uid: currentParams.uid || defaults.uid,
collectionName: currentParams.collectionName || defaults.collectionName,
dataSourceKey: currentParams.dataSourceKey || defaults.dataSourceKey,
};
const normalizeWithDefaults = (settings: Record<string, any>) => {
const normalized = this.normalizePopupSettings(actionKey, settings);
return {
...normalized,
uid: normalized.uid || defaults.uid,
collectionName: normalized.collectionName || defaults.collectionName,
dataSourceKey: normalized.dataSourceKey || defaults.dataSourceKey,
};
};
if (options.preferActionSettings === false) {
return normalizeWithDefaults(popupSettings);
}
return normalizeWithDefaults(this.mergePopupTemplateSettings(popupSettings, actionParams));
}
async syncPopupActionSettings(
@@ -383,17 +701,40 @@ export class CalendarBlockModel extends CollectionBlockModel {
return;
}
const nextSettings = this.getPopupSettings(action, actionKey, action?.uid);
const currentParams = action.getStepParams?.('popupSettings', 'openView') || {};
if (JSON.stringify(currentParams) === JSON.stringify(nextSettings)) {
const nextSettings = this.getPopupSettings(action, actionKey, action?.uid, {
preferActionSettings: !options.persist,
});
const checkedSettings = await this.normalizeQuickCreatePopupTemplateContext(actionKey, nextSettings, action?.uid);
const currentParams = this.getPopupActionSettings(action);
if (JSON.stringify(currentParams) === JSON.stringify(checkedSettings)) {
return;
}
action.setStepParams('popupSettings', 'openView', nextSettings);
await this.setPopupActionSettings(action, actionKey, checkedSettings, options);
}
async setPopupActionSettings(
action: any,
actionKey: 'quickCreateAction' | 'eventViewAction',
popupSettings?: Record<string, any>,
options: CalendarPopupActionOptions = {},
) {
if (!action) {
return {};
}
const normalized = this.normalizePopupSettings(actionKey, popupSettings);
const nextSettings = await this.normalizeQuickCreatePopupTemplateContext(actionKey, normalized, action?.uid);
replaceCalendarActionOpenViewParams(action, nextSettings);
if (options.persist && this.context.flowSettingsEnabled && action?.saveStepParams) {
if (action?.save) {
await action.save();
}
await action.saveStepParams();
}
return nextSettings;
}
async loadPopupAction(actionKey: 'quickCreateAction' | 'eventViewAction') {
@@ -426,10 +767,6 @@ export class CalendarBlockModel extends CollectionBlockModel {
action = this.subModels?.[actionKey] as any;
}
if (options.persist && this.context.flowSettingsEnabled && action?.save) {
await action.save();
}
await this.syncPopupActionSettings(action, actionKey, options);
return action;
@@ -458,11 +795,6 @@ export class CalendarBlockModel extends CollectionBlockModel {
target: this.context?.layoutContentElement,
};
if (typeof this.context?.openView === 'function' && action.uid) {
await this.context.openView(action.uid, inputArgs);
return;
}
await action.dispatchEvent('click', inputArgs, { debounce: true });
}
@@ -483,11 +815,6 @@ export class CalendarBlockModel extends CollectionBlockModel {
target: this.context?.layoutContentElement,
};
if (typeof this.context?.openView === 'function' && action.uid) {
await this.context.openView(action.uid, inputArgs);
return;
}
await action.dispatchEvent('click', inputArgs, { debounce: true });
}
}
@@ -828,16 +1155,34 @@ CalendarBlockModel.registerFlow({
async defaultParams(ctx) {
const model = ctx.model as CalendarBlockModel;
const action = await model.ensurePopupAction('quickCreateAction');
return model.getPopupSettings(action, 'quickCreateAction', action?.uid);
const popupSettings = model.getPopupSettings(action, 'quickCreateAction', action?.uid);
return model.normalizeQuickCreatePopupTemplateContext('quickCreateAction', popupSettings, action?.uid);
},
async handler(ctx, params) {
const model = ctx.model as CalendarBlockModel;
model.setPopupSettings('quickCreateAction', params);
const action = typeof model.getQuickCreateAction === 'function' ? model.getQuickCreateAction() : undefined;
if (action) {
await model.setPopupActionSettings?.(action, 'quickCreateAction', params);
} else {
model.setPopupSettings('quickCreateAction', params);
}
},
async beforeParamsSave(ctx, params) {
async beforeParamsSave(ctx, params, previousParams) {
const model = ctx.model as CalendarBlockModel;
model.setPopupSettings('quickCreateAction', params);
await model.ensurePopupAction('quickCreateAction', { persist: true });
const action = await model.ensurePopupAction('quickCreateAction');
const storedParams =
typeof model.getPopupActionSettings === 'function'
? model.getPopupActionSettings(action)
: action?.getStepParams?.('popupSettings', 'openView') || {};
await model
.getAction?.('openView')
?.beforeParamsSave?.(ctx, params, Object.keys(storedParams).length > 0 ? storedParams : previousParams || {});
if (typeof model.setPopupActionSettings === 'function') {
await model.setPopupActionSettings(action, 'quickCreateAction', params, { persist: true });
} else {
model.setPopupSettings('quickCreateAction', params);
}
model.clearStoredPopupSettings?.('quickCreateAction');
},
},
eventPopupSettings: {
@@ -850,12 +1195,29 @@ CalendarBlockModel.registerFlow({
},
async handler(ctx, params) {
const model = ctx.model as CalendarBlockModel;
model.setPopupSettings('eventViewAction', params);
const action = typeof model.getEventViewAction === 'function' ? model.getEventViewAction() : undefined;
if (action) {
await model.setPopupActionSettings?.(action, 'eventViewAction', params);
} else {
model.setPopupSettings('eventViewAction', params);
}
},
async beforeParamsSave(ctx, params) {
async beforeParamsSave(ctx, params, previousParams) {
const model = ctx.model as CalendarBlockModel;
model.setPopupSettings('eventViewAction', params);
await model.ensurePopupAction('eventViewAction', { persist: true });
const action = await model.ensurePopupAction('eventViewAction');
const storedParams =
typeof model.getPopupActionSettings === 'function'
? model.getPopupActionSettings(action)
: action?.getStepParams?.('popupSettings', 'openView') || {};
await model
.getAction?.('openView')
?.beforeParamsSave?.(ctx, params, Object.keys(storedParams).length > 0 ? storedParams : previousParams || {});
if (typeof model.setPopupActionSettings === 'function') {
await model.setPopupActionSettings(action, 'eventViewAction', params, { persist: true });
} else {
model.setPopupSettings('eventViewAction', params);
}
model.clearStoredPopupSettings?.('eventViewAction');
},
},
showLunar: {
@@ -292,6 +292,92 @@ describe('calendarPopupModels', () => {
expect(step?.hideInSettings?.({ model } as any)).toBe(true);
});
it('should normalize quick create popup settings default params before rendering settings', async () => {
const flow: any = (CalendarBlockModel as any).globalFlowRegistry.getFlow('calendarSettings');
const step = flow?.steps?.quickCreatePopupSettings;
const action = { uid: 'quick-action-uid' };
const popupSettings = { popupTemplateUid: 'record-template-uid', uid: 'record-template-target-uid' };
const normalizedSettings = { uid: 'quick-action-uid' };
const model = {
ensurePopupAction: vi.fn().mockResolvedValue(action),
getPopupSettings: vi.fn(() => popupSettings),
normalizeQuickCreatePopupTemplateContext: vi.fn().mockResolvedValue(normalizedSettings),
};
await expect(step?.defaultParams?.({ model } as any)).resolves.toBe(normalizedSettings);
expect(model.getPopupSettings).toHaveBeenCalledWith(action, 'quickCreateAction', action.uid);
expect(model.normalizeQuickCreatePopupTemplateContext).toHaveBeenCalledWith(
'quickCreateAction',
popupSettings,
action.uid,
);
});
it('should expose popup settings from hidden actions instead of stale calendar step params', () => {
const model = Object.create(CalendarBlockModel.prototype) as CalendarBlockModel;
Object.defineProperty(model, 'collection', {
value: {
name: 'events',
dataSourceKey: 'main',
},
configurable: true,
});
Object.defineProperty(model, 'props', {
value: {},
configurable: true,
});
Object.defineProperty(model, 'stepParams', {
value: {
calendarSettings: {
quickCreatePopupSettings: {
uid: 'stale-template-target-uid',
popupTemplateUid: 'stale-template-uid',
},
eventPopupSettings: {
uid: 'stale-event-template-target-uid',
popupTemplateUid: 'stale-event-template-uid',
},
},
},
configurable: true,
});
Object.defineProperty(model, 'subModels', {
value: {
quickCreateAction: {
uid: 'quick-create-action-uid',
getStepParams: vi.fn(() => ({
mode: 'drawer',
uid: 'quick-create-action-uid',
})),
},
eventViewAction: {
uid: 'event-view-action-uid',
getStepParams: vi.fn(() => ({
mode: 'dialog',
uid: 'event-view-action-uid',
})),
},
},
configurable: true,
});
expect(model.getStepParams('calendarSettings', 'quickCreatePopupSettings')).toMatchObject({
mode: 'drawer',
uid: 'quick-create-action-uid',
collectionName: 'events',
dataSourceKey: 'main',
});
expect(model.getStepParams('calendarSettings', 'quickCreatePopupSettings')).not.toMatchObject({
popupTemplateUid: 'stale-template-uid',
});
expect(model.getStepParams('calendarSettings', 'eventPopupSettings')).toMatchObject({
mode: 'dialog',
uid: 'event-view-action-uid',
collectionName: 'events',
dataSourceKey: 'main',
});
});
it('should persist popup actions only from popup settings save hooks', async () => {
const flow: any = (CalendarBlockModel as any).globalFlowRegistry.getFlow('calendarSettings');
const quickCreateStep = flow?.steps?.quickCreatePopupSettings;
@@ -310,8 +396,46 @@ describe('calendarPopupModels', () => {
await quickCreateStep.beforeParamsSave({ model } as any, { mode: 'drawer' });
await eventStep.beforeParamsSave({ model } as any, { mode: 'dialog' });
expect(ensurePopupAction).toHaveBeenCalledWith('quickCreateAction', { persist: true });
expect(ensurePopupAction).toHaveBeenCalledWith('eventViewAction', { persist: true });
expect(ensurePopupAction).toHaveBeenCalledWith('quickCreateAction');
expect(ensurePopupAction).toHaveBeenCalledWith('eventViewAction');
});
it('should delegate calendar popup settings save to openView beforeParamsSave', async () => {
const flow: any = (CalendarBlockModel as any).globalFlowRegistry.getFlow('calendarSettings');
const eventStep = flow?.steps?.eventPopupSettings;
const beforeParamsSave = vi.fn(async (_ctx, params) => {
delete params.popupTemplateUid;
delete params.popupTemplateContext;
delete params.popupTemplateHasFilterByTk;
delete params.uid;
});
const setPopupSettings = vi.fn();
const hiddenActionParams = {
popupTemplateUid: 'hidden-template',
uid: 'hidden-template-target',
};
const ensurePopupAction = vi.fn().mockResolvedValue({
uid: 'calendar-action-uid',
getStepParams: vi.fn(() => hiddenActionParams),
});
const params = {
mode: 'dialog',
popupTemplateUid: undefined,
popupTemplateContext: true,
popupTemplateHasFilterByTk: true,
uid: 'stale-template-target',
};
const model = {
getAction: vi.fn(() => ({ beforeParamsSave })),
setPopupSettings,
ensurePopupAction,
};
await eventStep.beforeParamsSave({ model } as any, params, { popupTemplateUid: 'template-1' });
expect(beforeParamsSave).toHaveBeenCalledWith({ model }, params, hiddenActionParams);
expect(setPopupSettings).toHaveBeenCalledWith('eventViewAction', { mode: 'dialog' });
expect(ensurePopupAction).toHaveBeenCalledWith('eventViewAction');
});
it('should build quick-create formData from the selected slot', () => {
@@ -513,22 +637,65 @@ describe('calendarPopupModels', () => {
configurable: true,
});
const setStepParams = vi.fn();
const action = {
uid: 'calendar-action-uid',
getStepParams: vi.fn(() => ({})),
setStepParams,
stepParams: {},
};
await model.syncPopupActionSettings(action, 'eventViewAction');
expect(setStepParams).toHaveBeenCalledWith('popupSettings', 'openView', {
expect(action.stepParams).toEqual({
popupSettings: {
openView: {
mode: 'dialog',
size: 'large',
pageModelClass: 'ChildPageModel',
uid: 'popup-template-target-uid',
popupTemplateUid: 'popup-template-uid',
popupTemplateHasFilterByTk: true,
collectionName: 'events',
dataSourceKey: 'main',
},
},
});
});
it('should prefer hidden popup action settings over legacy calendar popup props', async () => {
const model = Object.create(CalendarBlockModel.prototype) as CalendarBlockModel;
Object.defineProperty(model, 'collection', {
value: {
name: 'events',
dataSourceKey: 'main',
},
configurable: true,
});
Object.defineProperty(model, 'props', {
value: {
eventPopupSettings: {
mode: 'drawer',
size: 'medium',
uid: 'legacy-template-target',
popupTemplateUid: 'legacy-template',
},
},
configurable: true,
});
const action = {
uid: 'calendar-action-uid',
getStepParams: vi.fn(() => ({
mode: 'dialog',
size: 'large',
uid: 'calendar-action-uid',
})),
};
expect(model.getPopupSettings(action, 'eventViewAction')).toEqual({
mode: 'dialog',
size: 'large',
pageModelClass: 'ChildPageModel',
uid: 'popup-template-target-uid',
popupTemplateUid: 'popup-template-uid',
popupTemplateHasFilterByTk: true,
uid: 'calendar-action-uid',
collectionName: 'events',
dataSourceKey: 'main',
});
@@ -567,16 +734,55 @@ describe('calendarPopupModels', () => {
configurable: true,
});
const setStepParams = vi.fn();
const action = {
uid: 'calendar-quick-create-action-uid',
getStepParams: vi.fn(() => ({})),
setStepParams,
stepParams: {},
};
await model.syncPopupActionSettings(action, 'quickCreateAction');
expect(setStepParams).toHaveBeenCalledWith('popupSettings', 'openView', {
expect(action.stepParams).toEqual({
popupSettings: {
openView: {
mode: 'dialog',
size: 'large',
pageModelClass: 'ChildPageModel',
uid: 'calendar-quick-create-action-uid',
collectionName: 'events',
dataSourceKey: 'main',
},
},
});
});
it('should not expose event popup template params in quick create popup settings', () => {
const model = Object.create(CalendarBlockModel.prototype) as CalendarBlockModel;
Object.defineProperty(model, 'collection', {
value: {
name: 'events',
dataSourceKey: 'main',
},
configurable: true,
});
Object.defineProperty(model, 'props', {
value: {},
configurable: true,
});
const action = {
uid: 'calendar-quick-create-action-uid',
getStepParams: vi.fn(() => ({
mode: 'dialog',
size: 'large',
uid: 'event-popup-template-target-uid',
popupTemplateUid: 'event-popup-template-uid',
filterByTk: '{{ ctx.record.id }}',
popupTemplateHasFilterByTk: true,
})),
};
expect(model.getPopupSettings(action, 'quickCreateAction')).toEqual({
mode: 'dialog',
size: 'large',
pageModelClass: 'ChildPageModel',
@@ -586,6 +792,205 @@ describe('calendarPopupModels', () => {
});
});
it('should clear persisted record-scoped popup template from quick create action', async () => {
const model = Object.create(CalendarBlockModel.prototype) as CalendarBlockModel;
Object.defineProperty(model, 'collection', {
value: {
name: 'events',
dataSourceKey: 'main',
},
configurable: true,
});
Object.defineProperty(model, 'context', {
value: {
flowSettingsEnabled: false,
api: {
resource: vi.fn(() => ({
get: vi.fn(async () => ({
data: {
data: {
uid: 'event-template-uid',
filterByTk: '{{ ctx.record.id }}',
},
},
})),
})),
},
},
configurable: true,
});
Object.defineProperty(model, 'props', {
value: {},
configurable: true,
});
const action = {
uid: 'calendar-quick-create-action-uid',
getStepParams: vi.fn(() => ({
mode: 'drawer',
size: 'medium',
uid: 'event-popup-template-target-uid',
popupTemplateUid: 'event-template-uid',
collectionName: 'events',
dataSourceKey: 'main',
})),
stepParams: {},
};
await model.syncPopupActionSettings(action, 'quickCreateAction');
expect(action.stepParams).toEqual({
popupSettings: {
openView: {
mode: 'drawer',
size: 'medium',
pageModelClass: 'ChildPageModel',
uid: 'calendar-quick-create-action-uid',
collectionName: 'events',
dataSourceKey: 'main',
},
},
});
});
it('should keep collection-scene quick create popup template with historical record default filterByTk', async () => {
const model = Object.create(CalendarBlockModel.prototype) as CalendarBlockModel;
Object.defineProperty(model, 'collection', {
value: {
name: 'events',
dataSourceKey: 'main',
},
configurable: true,
});
Object.defineProperty(model, 'flowEngine', {
value: {
getModelClass: vi.fn((use: string) => {
if (use !== 'AddNewActionModel') {
return undefined;
}
return class AddNewActionModel {
static _isScene(scene: string) {
return scene === 'collection';
}
};
}),
},
configurable: true,
});
Object.defineProperty(model, 'context', {
value: {
flowSettingsEnabled: false,
api: {
resource: vi.fn(() => ({
get: vi.fn(async () => ({
data: {
data: {
uid: 'collection-template-uid',
useModel: 'AddNewActionModel',
filterByTk: '{{ ctx.record.id }}',
},
},
})),
})),
},
},
configurable: true,
});
Object.defineProperty(model, 'props', {
value: {},
configurable: true,
});
const action = {
uid: 'calendar-quick-create-action-uid',
getStepParams: vi.fn(() => ({
mode: 'drawer',
size: 'medium',
uid: 'collection-template-target-uid',
popupTemplateUid: 'collection-template-uid',
filterByTk: '{{ ctx.record.id }}',
collectionName: 'events',
dataSourceKey: 'main',
})),
stepParams: {},
};
await model.syncPopupActionSettings(action, 'quickCreateAction');
expect(action.stepParams).toEqual({
popupSettings: {
openView: {
mode: 'drawer',
size: 'medium',
pageModelClass: 'ChildPageModel',
uid: 'collection-template-target-uid',
popupTemplateUid: 'collection-template-uid',
collectionName: 'events',
dataSourceKey: 'main',
},
},
});
});
it('should keep calendar popup template copy mode when the template uid is empty', async () => {
const model = Object.create(CalendarBlockModel.prototype) as CalendarBlockModel;
Object.defineProperty(model, 'collection', {
value: {
name: 'events',
dataSourceKey: 'main',
},
configurable: true,
});
Object.defineProperty(model, 'context', {
value: {
flowSettingsEnabled: false,
},
configurable: true,
});
Object.defineProperty(model, 'props', {
value: {},
writable: true,
configurable: true,
});
(model as any).setProps = function setProps(next: Record<string, any>) {
this.props = {
...(this.props || {}),
...next,
};
};
const action = {
uid: 'calendar-action-uid',
getStepParams: vi.fn(() => ({})),
stepParams: {},
};
model.setPopupSettings('eventViewAction', {
mode: 'dialog',
size: 'large',
popupTemplateUid: undefined,
popupTemplateContext: true,
uid: 'copied-popup-uid',
dataSourceKey: 'main',
collectionName: 'template_events',
});
await model.syncPopupActionSettings(action, 'eventViewAction', { persist: true });
expect(action.stepParams).toEqual({
popupSettings: {
openView: {
mode: 'dialog',
size: 'large',
pageModelClass: 'ChildPageModel',
popupTemplateContext: true,
uid: 'copied-popup-uid',
collectionName: 'template_events',
dataSourceKey: 'main',
},
},
});
});
it('should clear stale popup template params when event template is removed', async () => {
const model = Object.create(CalendarBlockModel.prototype) as CalendarBlockModel;
Object.defineProperty(model, 'collection', {
@@ -623,7 +1028,6 @@ describe('calendarPopupModels', () => {
};
};
const setStepParams = vi.fn();
const action = {
uid: 'calendar-action-uid',
getStepParams: vi.fn(() => ({
@@ -635,7 +1039,19 @@ describe('calendarPopupModels', () => {
popupTemplateHasFilterByTk: true,
popupTemplateHasSourceId: true,
})),
setStepParams,
stepParams: {
popupSettings: {
openView: {
mode: 'drawer',
size: 'medium',
uid: 'popup-template-target-uid',
popupTemplateUid: 'popup-template-uid',
popupTemplateContext: true,
popupTemplateHasFilterByTk: true,
popupTemplateHasSourceId: true,
},
},
},
};
model.setPopupSettings('eventViewAction', {
@@ -643,15 +1059,19 @@ describe('calendarPopupModels', () => {
size: 'large',
popupTemplateUid: undefined,
});
await model.syncPopupActionSettings(action, 'eventViewAction');
await model.syncPopupActionSettings(action, 'eventViewAction', { persist: true });
expect(setStepParams).toHaveBeenCalledWith('popupSettings', 'openView', {
mode: 'dialog',
size: 'large',
pageModelClass: 'ChildPageModel',
uid: 'calendar-action-uid',
collectionName: 'events',
dataSourceKey: 'main',
expect(action.stepParams).toEqual({
popupSettings: {
openView: {
mode: 'dialog',
size: 'large',
pageModelClass: 'ChildPageModel',
uid: 'calendar-action-uid',
collectionName: 'events',
dataSourceKey: 'main',
},
},
});
});
@@ -687,7 +1107,7 @@ describe('calendarPopupModels', () => {
const action = {
uid: 'u_event_popup',
getStepParams: vi.fn(() => ({})),
setStepParams: vi.fn(),
stepParams: {},
save,
saveStepParams,
};
@@ -720,13 +1140,17 @@ describe('calendarPopupModels', () => {
expect(save).not.toHaveBeenCalled();
expect(saveStepParams).not.toHaveBeenCalled();
expect(action.setStepParams).toHaveBeenCalledWith('popupSettings', 'openView', {
mode: 'drawer',
size: 'medium',
pageModelClass: 'ChildPageModel',
uid: 'u_event_popup',
collectionName: 'events',
dataSourceKey: 'main',
expect(action.stepParams).toEqual({
popupSettings: {
openView: {
mode: 'drawer',
size: 'medium',
pageModelClass: 'ChildPageModel',
uid: 'u_event_popup',
collectionName: 'events',
dataSourceKey: 'main',
},
},
});
});
@@ -828,9 +1252,9 @@ describe('calendarPopupModels', () => {
expect(destroy).not.toHaveBeenCalled();
});
it('should open quick-create drawer through flow context openView with selected slot data', async () => {
const openView = vi.fn().mockResolvedValue(undefined);
const ensurePopupAction = vi.fn().mockResolvedValue({ uid: 'u_quick_create_popup' });
it('should open quick-create drawer through the hidden action with selected slot data', async () => {
const dispatchEvent = vi.fn().mockResolvedValue(undefined);
const ensurePopupAction = vi.fn().mockResolvedValue({ uid: 'u_quick_create_popup', dispatchEvent });
const slotInfo = {
start: new Date(2026, 3, 20, 9, 30, 0),
end: new Date(2026, 3, 20, 10, 30, 0),
@@ -840,7 +1264,6 @@ describe('calendarPopupModels', () => {
{
props: {},
context: {
openView,
layoutContentElement: { id: 'layout-root' },
},
collection: {
@@ -857,26 +1280,28 @@ describe('calendarPopupModels', () => {
);
expect(ensurePopupAction).toHaveBeenCalledWith('quickCreateAction');
expect(openView).toHaveBeenCalledWith('u_quick_create_popup', {
formData: {
startsAt: '2026-04-20 09:30:00',
endsAt: '2026-04-20 10:30:00',
expect(dispatchEvent).toHaveBeenCalledWith(
'click',
{
formData: {
startsAt: '2026-04-20 09:30:00',
endsAt: '2026-04-20 10:30:00',
},
dataSourceKey: 'main',
collectionName: 'events',
target: { id: 'layout-root' },
},
dataSourceKey: 'main',
collectionName: 'events',
target: { id: 'layout-root' },
});
expect(openView.mock.calls[0][0]).not.toContain('quickCreateAction');
{ debounce: true },
);
});
it('should open event drawer through flow context openView with record filter key', async () => {
const openView = vi.fn().mockResolvedValue(undefined);
const ensurePopupAction = vi.fn().mockResolvedValue({ uid: 'u_event_view_popup' });
it('should open event drawer through the hidden action with record filter key', async () => {
const dispatchEvent = vi.fn().mockResolvedValue(undefined);
const ensurePopupAction = vi.fn().mockResolvedValue({ uid: 'u_event_view_popup', dispatchEvent });
await CalendarBlockModel.prototype.openEvent.call(
{
context: {
openView,
layoutContentElement: { id: 'layout-root' },
},
collection: {
@@ -891,12 +1316,15 @@ describe('calendarPopupModels', () => {
);
expect(ensurePopupAction).toHaveBeenCalledWith('eventViewAction');
expect(openView).toHaveBeenCalledWith('u_event_view_popup', {
dataSourceKey: 'main',
collectionName: 'events',
filterByTk: 7,
target: { id: 'layout-root' },
});
expect(openView.mock.calls[0][0]).not.toContain('eventViewAction');
expect(dispatchEvent).toHaveBeenCalledWith(
'click',
{
dataSourceKey: 'main',
collectionName: 'events',
filterByTk: 7,
target: { id: 'layout-root' },
},
{ debounce: true },
);
});
});
@@ -201,14 +201,13 @@ describe('KanbanBlockModel.filterCollection', () => {
).toBe('zh:At least one option is required');
});
test('card click uses flow context openView with the latest card-item props', async () => {
const openView = vi.fn().mockResolvedValue(undefined);
const ensureCardViewAction = vi.fn().mockResolvedValue({ uid: 'u_card_view_popup' });
test('card click dispatches the hidden action with the latest card-item props', async () => {
const dispatchEvent = vi.fn().mockResolvedValue(undefined);
const ensureCardViewAction = vi.fn().mockResolvedValue({ uid: 'u_card_view_popup', dispatchEvent });
await KanbanBlockModel.prototype.openCard.call(
{
context: {
openView,
layoutContentElement: { id: 'layout-root' },
},
subModels: {
@@ -229,11 +228,15 @@ describe('KanbanBlockModel.filterCollection', () => {
);
expect(ensureCardViewAction).toHaveBeenCalledTimes(1);
expect(openView).toHaveBeenCalledWith('u_card_view_popup', {
mode: 'dialog',
filterByTk: 1,
target: { id: 'layout-root' },
});
expect(dispatchEvent).toHaveBeenCalledWith(
'click',
{
mode: 'dialog',
filterByTk: 1,
target: { id: 'layout-root' },
},
{ debounce: true },
);
});
test('configured select grouping keeps enum colors from the collection field', () => {
@@ -264,16 +267,16 @@ describe('KanbanBlockModel.filterCollection', () => {
});
test('syncPopupAction persists pageModelClass alongside other popup settings', async () => {
const setStepParams = vi.fn();
const action = {
getStepParams: () => ({ mode: 'drawer' }),
stepParams: {},
};
await KanbanBlockModel.prototype.syncPopupAction.call(
{
context: { flowSettingsEnabled: false },
},
{
getStepParams: () => ({ mode: 'drawer' }),
setStepParams,
},
action,
{
mode: 'dialog',
size: 'large',
@@ -283,27 +286,31 @@ describe('KanbanBlockModel.filterCollection', () => {
},
);
expect(setStepParams).toHaveBeenCalledWith('popupSettings', 'openView', {
mode: 'dialog',
size: 'large',
popupTemplateUid: 'template-1',
uid: 'popup-1',
pageModelClass: 'PopupPageModel',
expect(action.stepParams).toEqual({
popupSettings: {
openView: {
mode: 'dialog',
size: 'large',
popupTemplateUid: 'template-1',
uid: 'popup-1',
pageModelClass: 'PopupPageModel',
},
},
});
});
test('syncPopupAction persists collection context for popup add-block menus', async () => {
const setStepParams = vi.fn();
const action = {
uid: 'card-view-action',
getStepParams: () => ({ mode: 'drawer' }),
stepParams: {},
};
await KanbanBlockModel.prototype.syncPopupAction.call(
{
context: { flowSettingsEnabled: false },
},
{
uid: 'card-view-action',
getStepParams: () => ({ mode: 'drawer' }),
setStepParams,
},
action,
{
mode: 'drawer',
size: 'medium',
@@ -313,14 +320,17 @@ describe('KanbanBlockModel.filterCollection', () => {
},
);
expect(setStepParams).toHaveBeenCalledWith('popupSettings', 'openView', {
mode: 'drawer',
size: 'medium',
popupTemplateUid: undefined,
uid: 'card-view-action',
pageModelClass: undefined,
dataSourceKey: 'main',
collectionName: 'tasks',
expect(action.stepParams).toEqual({
popupSettings: {
openView: {
mode: 'drawer',
size: 'medium',
uid: 'card-view-action',
pageModelClass: undefined,
dataSourceKey: 'main',
collectionName: 'tasks',
},
},
});
});
@@ -330,7 +340,7 @@ describe('KanbanBlockModel.filterCollection', () => {
const action = {
uid: 'kanban-block-quick-create-action',
getStepParams: vi.fn(() => ({})),
setStepParams: vi.fn(),
stepParams: {},
save,
saveStepParams,
};
@@ -364,7 +374,18 @@ describe('KanbanBlockModel.filterCollection', () => {
expect(save).not.toHaveBeenCalled();
expect(saveStepParams).not.toHaveBeenCalled();
expect(action.setStepParams).toHaveBeenCalled();
expect(action.stepParams).toEqual({
popupSettings: {
openView: {
mode: 'drawer',
size: 'medium',
uid: 'kanban-block-quick-create-action',
pageModelClass: undefined,
dataSourceKey: 'main',
collectionName: 'tasks',
},
},
});
});
test('persists hidden popup actions only when kanban popup settings are saved', async () => {
@@ -517,73 +538,157 @@ describe('KanbanBlockModel.filterCollection', () => {
expect(destroy).not.toHaveBeenCalled();
});
test('syncPopupAction overwrites removed popup settings with undefined so stale template params do not survive merges', async () => {
const setStepParams = vi.fn();
test('syncPopupAction replaces removed popup settings so stale template params do not survive', async () => {
const action = {
uid: 'kanban-quick-create-action',
getStepParams: () => ({
mode: 'drawer',
size: 'medium',
popupTemplateUid: 'template-1',
pageModelClass: 'PopupPageModel',
}),
stepParams: {
popupSettings: {
openView: {
mode: 'drawer',
size: 'medium',
popupTemplateUid: 'template-1',
pageModelClass: 'PopupPageModel',
},
},
},
};
await KanbanBlockModel.prototype.syncPopupAction.call(
{
context: { flowSettingsEnabled: false },
},
{
uid: 'kanban-quick-create-action',
getStepParams: () => ({
mode: 'drawer',
size: 'medium',
popupTemplateUid: 'template-1',
pageModelClass: 'PopupPageModel',
}),
setStepParams,
},
action,
{
mode: 'drawer',
size: 'medium',
},
{ persist: true },
);
expect(setStepParams).toHaveBeenCalledWith('popupSettings', 'openView', {
mode: 'drawer',
size: 'medium',
popupTemplateUid: undefined,
pageModelClass: undefined,
uid: 'kanban-quick-create-action',
expect(action.stepParams).toEqual({
popupSettings: {
openView: {
mode: 'drawer',
size: 'medium',
uid: 'kanban-quick-create-action',
pageModelClass: undefined,
},
},
});
});
test('syncPopupAction drops stale template-derived popup params after the template is cleared', async () => {
const setStepParams = vi.fn();
const action = {
uid: 'kanban-block-1-quick-create-action',
getStepParams: () => ({
mode: 'drawer',
size: 'medium',
popupTemplateUid: 'template-1',
uid: 'template-popup-1',
dataSourceKey: 'secondary',
collectionName: 'archived_tasks',
associationName: 'users.tasks',
popupTemplateContext: true,
}),
stepParams: {
popupSettings: {
openView: {
mode: 'drawer',
size: 'medium',
popupTemplateUid: 'template-1',
uid: 'template-popup-1',
dataSourceKey: 'secondary',
collectionName: 'archived_tasks',
associationName: 'users.tasks',
popupTemplateContext: true,
},
},
},
};
await KanbanBlockModel.prototype.syncPopupAction.call(
{
uid: 'kanban-block-1',
context: { flowSettingsEnabled: false },
},
{
uid: 'kanban-block-1-quick-create-action',
getStepParams: () => ({
mode: 'drawer',
size: 'medium',
popupTemplateUid: 'template-1',
uid: 'template-popup-1',
dataSourceKey: 'secondary',
collectionName: 'archived_tasks',
associationName: 'users.tasks',
popupTemplateContext: true,
}),
setStepParams,
},
action,
{
mode: 'drawer',
size: 'medium',
uid: 'template-popup-1',
},
{ persist: true },
);
expect(setStepParams).toHaveBeenCalledWith('popupSettings', 'openView', {
mode: 'drawer',
size: 'medium',
popupTemplateUid: undefined,
pageModelClass: undefined,
expect(action.stepParams).toEqual({
popupSettings: {
openView: {
mode: 'drawer',
size: 'medium',
uid: 'kanban-block-1-quick-create-action',
pageModelClass: undefined,
},
},
});
});
test('syncPopupAction keeps popup template copy mode during runtime sync', async () => {
const action = {
uid: 'kanban-block-1-quick-create-action',
getStepParams: () => ({
mode: 'drawer',
size: 'medium',
uid: 'copied-popup-1',
dataSourceKey: 'secondary',
collectionName: 'template_tasks',
popupTemplateContext: true,
}),
stepParams: {
popupSettings: {
openView: {
mode: 'drawer',
size: 'medium',
uid: 'copied-popup-1',
dataSourceKey: 'secondary',
collectionName: 'template_tasks',
popupTemplateContext: true,
},
},
},
};
await KanbanBlockModel.prototype.syncPopupAction.call(
{
uid: 'kanban-block-1',
context: { flowSettingsEnabled: false },
},
action,
{
mode: 'dialog',
size: 'large',
dataSourceKey: 'main',
collectionName: 'tasks',
},
);
expect(action.stepParams).toEqual({
popupSettings: {
openView: {
mode: 'dialog',
size: 'large',
uid: 'copied-popup-1',
pageModelClass: undefined,
dataSourceKey: 'secondary',
collectionName: 'template_tasks',
popupTemplateContext: true,
},
},
});
});
@@ -630,7 +735,6 @@ describe('KanbanBlockModel.filterCollection', () => {
const setProps = vi.fn(function (this: any, nextProps) {
Object.assign(this.props, nextProps);
});
const setStepParams = vi.fn();
const action = {
uid: 'quick-create-action',
getStepParams: () => ({
@@ -639,7 +743,16 @@ describe('KanbanBlockModel.filterCollection', () => {
popupTemplateUid: 'template-1',
uid: 'template-popup-1',
}),
setStepParams,
stepParams: {
popupSettings: {
openView: {
mode: 'drawer',
size: 'medium',
popupTemplateUid: 'template-1',
uid: 'template-popup-1',
},
},
},
};
const model: any = {
props: {
@@ -694,16 +807,103 @@ describe('KanbanBlockModel.filterCollection', () => {
expect(model.props.popupTargetUid).toBeUndefined();
expect(model.stepParams.kanbanSettings.popup.uid).toBeUndefined();
expect(setStepParams).toHaveBeenCalledWith('popupSettings', 'openView', {
mode: 'drawer',
size: 'medium',
popupTemplateUid: undefined,
pageModelClass: undefined,
uid: 'quick-create-action',
expect(action.stepParams).toEqual({
popupSettings: {
openView: {
mode: 'drawer',
size: 'medium',
pageModelClass: undefined,
uid: 'quick-create-action',
},
},
});
expect(model.ensureQuickCreateAction).toHaveBeenCalledWith({ persist: true });
});
test('popup settings beforeParamsSave keeps copied popup template state', async () => {
const flow: any = (KanbanBlockModel as any).globalFlowRegistry.getFlow('kanbanSettings');
const step: any = flow?.steps?.popup;
const setProps = vi.fn(function (this: any, nextProps) {
Object.assign(this.props, nextProps);
});
const action = {
uid: 'quick-create-action',
getStepParams: () => ({}),
stepParams: {},
};
const model: any = {
props: {
popupTemplateUid: 'template-1',
popupTargetUid: 'copied-popup-1',
},
stepParams: {},
collection: {
dataSourceKey: 'main',
name: 'tasks',
},
context: { flowSettingsEnabled: false },
emitter: { emit: vi.fn() },
setProps,
getPopupTemplateUid() {
return this.props.popupTemplateUid;
},
getPopupTargetUid() {
return this.props.popupTargetUid;
},
getAction: () => ({ beforeParamsSave: vi.fn().mockResolvedValue(undefined) }),
ensureQuickCreateAction: vi.fn(async function (this: any, options) {
await this.syncQuickCreateAction(action, options);
return action;
}),
syncQuickCreateAction: KanbanBlockModel.prototype.syncQuickCreateAction,
syncPopupAction: KanbanBlockModel.prototype.syncPopupAction,
getPopupMode: () => 'drawer',
getPopupSize: () => 'medium',
getPopupPageModelClass: () => undefined,
getQuickCreateActionUid: () => 'quick-create-action',
uid: 'kanban-block-1',
};
await step.beforeParamsSave(
{
model,
} as any,
{
mode: 'drawer',
size: 'medium',
popupTemplateUid: undefined,
popupTemplateContext: true,
uid: 'copied-popup-1',
dataSourceKey: 'main',
collectionName: 'template_tasks',
},
{
popupTemplateUid: 'template-1',
uid: 'copied-popup-1',
},
);
expect(model.props.popupTargetUid).toBe('copied-popup-1');
expect(model.stepParams.kanbanSettings.popup).toMatchObject({
popupTemplateContext: true,
uid: 'copied-popup-1',
collectionName: 'template_tasks',
});
expect(action.stepParams).toEqual({
popupSettings: {
openView: {
mode: 'drawer',
size: 'medium',
pageModelClass: undefined,
popupTemplateContext: true,
uid: 'copied-popup-1',
dataSourceKey: 'main',
collectionName: 'template_tasks',
},
},
});
});
test('card popup getter treats an explicitly cleared item template as higher priority than legacy block props', () => {
expect(
KanbanBlockModel.prototype.getCardPopupTemplateUid.call({
@@ -1804,6 +2004,86 @@ describe('KanbanBlockModel.filterCollection', () => {
expect(syncQuickCreateAction).not.toHaveBeenCalled();
});
test('block popup settings default params load hidden quick-create action template params', async () => {
const flow: any = (KanbanBlockModel as any).globalFlowRegistry.getFlow('kanbanSettings');
const step: any = flow?.steps?.popup;
const hiddenActionParams = {
mode: 'drawer',
size: 'medium',
uid: 'template-popup-1',
dataSourceKey: 'main',
collectionName: 'template_tasks',
popupTemplateContext: true,
};
const ensureQuickCreateAction = vi.fn().mockResolvedValue({
uid: 'quick-create-action',
getStepParams: vi.fn(() => hiddenActionParams),
});
const defaultParams = await step.defaultParams({
model: {
props: {},
ensureQuickCreateAction,
getPopupMode: () => 'dialog',
getPopupSize: () => 'large',
getPopupTemplateUid: () => undefined,
getPopupTargetUid: () => undefined,
getPopupPageModelClass: () => undefined,
},
} as any);
expect(ensureQuickCreateAction).toHaveBeenCalledWith();
expect(defaultParams).toMatchObject({
mode: 'drawer',
size: 'medium',
uid: 'template-popup-1',
dataSourceKey: 'main',
collectionName: 'template_tasks',
popupTemplateContext: true,
});
expect(defaultParams).not.toHaveProperty('popupTemplateUid');
});
test('block popup settings save delegates previous params from hidden quick-create action', async () => {
const flow: any = (KanbanBlockModel as any).globalFlowRegistry.getFlow('kanbanSettings');
const step: any = flow?.steps?.popup;
const openViewBeforeParamsSave = vi.fn().mockResolvedValue(undefined);
const hiddenActionParams = {
popupTemplateUid: 'template-1',
uid: 'template-popup-1',
};
const ensureQuickCreateAction = vi.fn().mockResolvedValue({
uid: 'quick-create-action',
getStepParams: vi.fn(() => hiddenActionParams),
});
const setProps = vi.fn();
await step.beforeParamsSave(
{
model: {
props: {},
context: { flowSettingsEnabled: false },
setProps,
ensureQuickCreateAction,
getAction: () => ({ beforeParamsSave: openViewBeforeParamsSave }),
},
} as any,
{
mode: 'drawer',
size: 'medium',
},
{
popupTemplateUid: 'settings-step-template',
},
);
expect(openViewBeforeParamsSave).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ mode: 'drawer', size: 'medium' }),
hiddenActionParams,
);
});
test('kanban model wraps openView popup template selector without changing the global action', async () => {
const engine = new FlowEngine();
engine.registerModels({ KanbanBlockModel });
@@ -1898,18 +2178,18 @@ describe('KanbanBlockModel.filterCollection', () => {
});
test('syncPopupAction falls back to the popup action uid when the target uid resolves to the kanban block itself', async () => {
const setStepParams = vi.fn();
const action = {
uid: 'kanban-block-1-quick-create-action',
getStepParams: () => ({ mode: 'drawer', uid: 'kanban-block-1' }),
stepParams: {},
};
await KanbanBlockModel.prototype.syncPopupAction.call(
{
uid: 'kanban-block-1',
context: { flowSettingsEnabled: false },
},
{
uid: 'kanban-block-1-quick-create-action',
getStepParams: () => ({ mode: 'drawer', uid: 'kanban-block-1' }),
setStepParams,
},
action,
{
mode: 'drawer',
size: 'medium',
@@ -1917,47 +2197,56 @@ describe('KanbanBlockModel.filterCollection', () => {
},
);
expect(setStepParams).toHaveBeenCalledWith('popupSettings', 'openView', {
mode: 'drawer',
size: 'medium',
uid: 'kanban-block-1-quick-create-action',
expect(action.stepParams).toEqual({
popupSettings: {
openView: {
mode: 'drawer',
size: 'medium',
uid: 'kanban-block-1-quick-create-action',
pageModelClass: undefined,
},
},
});
});
test('syncPopupAction keeps the popup action uid when there is no external popup target uid', async () => {
const setStepParams = vi.fn();
const action = {
uid: 'kanban-block-1-quick-create-action',
getStepParams: () => ({ mode: 'drawer' }),
stepParams: {},
};
await KanbanBlockModel.prototype.syncPopupAction.call(
{
uid: 'kanban-block-1',
context: { flowSettingsEnabled: false },
},
{
uid: 'kanban-block-1-quick-create-action',
getStepParams: () => ({ mode: 'drawer' }),
setStepParams,
},
action,
{
mode: 'drawer',
size: 'medium',
},
);
expect(setStepParams).toHaveBeenCalledWith('popupSettings', 'openView', {
mode: 'drawer',
size: 'medium',
uid: 'kanban-block-1-quick-create-action',
expect(action.stepParams).toEqual({
popupSettings: {
openView: {
mode: 'drawer',
size: 'medium',
uid: 'kanban-block-1-quick-create-action',
pageModelClass: undefined,
},
},
});
});
test('quick create uses flow context openView with the prefilled form data', async () => {
const openView = vi.fn().mockResolvedValue(undefined);
const ensureQuickCreateAction = vi.fn().mockResolvedValue({ uid: 'u_quick_create_popup' });
test('quick create dispatches the hidden action with the prefilled form data', async () => {
const dispatchEvent = vi.fn().mockResolvedValue(undefined);
const ensureQuickCreateAction = vi.fn().mockResolvedValue({ uid: 'u_quick_create_popup', dispatchEvent });
await KanbanBlockModel.prototype.openQuickCreate.call(
{
context: {
openView,
layoutContentElement: { id: 'layout-root' },
},
props: {
@@ -1971,21 +2260,24 @@ describe('KanbanBlockModel.filterCollection', () => {
);
expect(ensureQuickCreateAction).toHaveBeenCalledTimes(1);
expect(openView).toHaveBeenCalledWith('u_quick_create_popup', {
formData: { status: 'todo' },
target: { id: 'layout-root' },
});
expect(dispatchEvent).toHaveBeenCalledWith(
'click',
{
formData: { status: 'todo' },
target: { id: 'layout-root' },
},
{ debounce: true },
);
});
test('quick create falls back to an empty popup shell when the popup action open fails', async () => {
const open = vi.fn().mockResolvedValue(undefined);
const openView = vi.fn().mockRejectedValue(new Error('open failed'));
const ensureQuickCreateAction = vi.fn().mockResolvedValue({ uid: 'u_quick_create_popup' });
const dispatchEvent = vi.fn().mockRejectedValue(new Error('open failed'));
const ensureQuickCreateAction = vi.fn().mockResolvedValue({ uid: 'u_quick_create_popup', dispatchEvent });
await KanbanBlockModel.prototype.openQuickCreate.call(
{
context: {
openView,
viewer: { open },
layoutContentElement: { id: 'layout-root' },
},
@@ -2004,10 +2296,14 @@ describe('KanbanBlockModel.filterCollection', () => {
);
expect(ensureQuickCreateAction).toHaveBeenCalledTimes(1);
expect(openView).toHaveBeenCalledWith('u_quick_create_popup', {
formData: { status: 'todo' },
target: { id: 'layout-root' },
});
expect(dispatchEvent).toHaveBeenCalledWith(
'click',
{
formData: { status: 'todo' },
target: { id: 'layout-root' },
},
{ debounce: true },
);
expect(open).toHaveBeenCalledWith(
expect.objectContaining({
type: 'drawer',
@@ -2020,13 +2316,12 @@ describe('KanbanBlockModel.filterCollection', () => {
test('card click falls back to an empty popup shell when the popup action open fails', async () => {
const open = vi.fn().mockResolvedValue(undefined);
const openView = vi.fn().mockRejectedValue(new Error('open failed'));
const ensureCardViewAction = vi.fn().mockResolvedValue({ uid: 'u_card_view_popup' });
const dispatchEvent = vi.fn().mockRejectedValue(new Error('open failed'));
const ensureCardViewAction = vi.fn().mockResolvedValue({ uid: 'u_card_view_popup', dispatchEvent });
await KanbanBlockModel.prototype.openCard.call(
{
context: {
openView,
viewer: { open },
layoutContentElement: { id: 'layout-root' },
},
@@ -2047,13 +2342,17 @@ describe('KanbanBlockModel.filterCollection', () => {
);
expect(ensureCardViewAction).toHaveBeenCalledTimes(1);
expect(openView).toHaveBeenCalledWith('u_card_view_popup', {
mode: 'dialog',
dataSourceKey: 'main',
collectionName: 'tasks',
filterByTk: 1,
target: { id: 'layout-root' },
});
expect(dispatchEvent).toHaveBeenCalledWith(
'click',
{
mode: 'dialog',
dataSourceKey: 'main',
collectionName: 'tasks',
filterByTk: 1,
target: { id: 'layout-root' },
},
{ debounce: true },
);
expect(open).toHaveBeenCalledWith(
expect.objectContaining({
type: 'dialog',
@@ -102,6 +102,41 @@ describe('KanbanCardItemModel.cardSettings', () => {
});
});
test('popup default params load hidden card-view action template params', async () => {
const flow: any = (KanbanCardItemModel as any).globalFlowRegistry.getFlow('cardSettings');
const hiddenActionParams = {
mode: 'drawer',
size: 'medium',
uid: 'template-card-popup',
dataSourceKey: 'main',
collectionName: 'template_tasks',
popupTemplateContext: true,
};
const ensureCardViewAction = vi.fn().mockResolvedValue({
uid: 'card-view-action',
getStepParams: vi.fn(() => hiddenActionParams),
});
const cardItem = {
props: {},
parent: {
ensureCardViewAction,
},
};
const defaultParams = await flow.steps.popup.defaultParams({ model: cardItem } as any);
expect(ensureCardViewAction).toHaveBeenCalledWith();
expect(defaultParams).toMatchObject({
mode: 'drawer',
size: 'medium',
uid: 'template-card-popup',
dataSourceKey: 'main',
collectionName: 'template_tasks',
popupTemplateContext: true,
});
expect(defaultParams).not.toHaveProperty('popupTemplateUid');
});
test('popup handler clears a stale card popup target uid when removing the popup template', async () => {
const flow: any = (KanbanCardItemModel as any).globalFlowRegistry.getFlow('cardSettings');
const syncCardViewAction = vi.fn();
@@ -196,6 +231,104 @@ describe('KanbanCardItemModel.cardSettings', () => {
expect(ensureCardViewAction).toHaveBeenCalledWith({ persist: true });
});
test('popup beforeParamsSave keeps copied card popup template state', async () => {
const flow: any = (KanbanCardItemModel as any).globalFlowRegistry.getFlow('cardSettings');
const ensureCardViewAction = vi.fn().mockResolvedValue({ uid: 'card-view-action' });
const masterBlock = {
subModels: {
cardViewAction: { uid: 'card-view-action' },
},
ensureCardViewAction,
setProps: vi.fn(),
};
const masterItem: any = {
props: {
popupTemplateUid: 'tpl-card',
popupTargetUid: 'copied-card-popup',
},
stepParams: {},
emitter: { emit: vi.fn() },
parent: masterBlock,
setProps: vi.fn(function (this: any, nextProps) {
Object.assign(this.props, nextProps);
}),
getAction: () => ({ beforeParamsSave: vi.fn().mockResolvedValue(undefined) }),
};
await flow.steps.popup.beforeParamsSave(
{
model: masterItem,
} as any,
{
mode: 'dialog',
size: 'large',
popupTemplateUid: undefined,
popupTemplateContext: true,
uid: 'copied-card-popup',
dataSourceKey: 'main',
collectionName: 'template_tasks',
},
{
popupTemplateUid: 'tpl-card',
uid: 'copied-card-popup',
},
);
expect(masterItem.props.popupTargetUid).toBe('copied-card-popup');
expect(masterItem.stepParams.cardSettings.popup).toMatchObject({
popupTemplateContext: true,
uid: 'copied-card-popup',
collectionName: 'template_tasks',
});
expect(ensureCardViewAction).toHaveBeenCalledWith({ persist: true });
});
test('popup beforeParamsSave delegates previous params from hidden card-view action', async () => {
const flow: any = (KanbanCardItemModel as any).globalFlowRegistry.getFlow('cardSettings');
const openViewBeforeParamsSave = vi.fn().mockResolvedValue(undefined);
const hiddenActionParams = {
popupTemplateUid: 'tpl-card',
uid: 'popup-card-1',
};
const ensureCardViewAction = vi.fn().mockResolvedValue({
uid: 'card-view-action',
getStepParams: vi.fn(() => hiddenActionParams),
});
const masterBlock = {
ensureCardViewAction,
setProps: vi.fn(),
};
const masterItem: any = {
props: {},
stepParams: {},
emitter: { emit: vi.fn() },
parent: masterBlock,
setProps: vi.fn(function (this: any, nextProps) {
Object.assign(this.props, nextProps);
}),
getAction: () => ({ beforeParamsSave: openViewBeforeParamsSave }),
};
await flow.steps.popup.beforeParamsSave(
{
model: masterItem,
} as any,
{
mode: 'dialog',
size: 'large',
},
{
popupTemplateUid: 'settings-step-template',
},
);
expect(openViewBeforeParamsSave).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ mode: 'dialog', size: 'large' }),
hiddenActionParams,
);
});
test('layout changes persist from card forks and propagate to details items', () => {
const flow: any = (KanbanCardItemModel as any).globalFlowRegistry.getFlow('cardSettings');
const detailsItem = {
@@ -88,6 +88,20 @@ type KanbanPopupActionOptions = {
persist?: boolean;
};
const POPUP_TEMPLATE_SETTING_KEYS = [
'uid',
'dataSourceKey',
'collectionName',
'associationName',
'filterByTk',
'sourceId',
'popupTemplateUid',
'popupTemplateMode',
'popupTemplateContext',
'popupTemplateHasFilterByTk',
'popupTemplateHasSourceId',
];
const DRAG_SORT_FIELD_TIP =
'Choose the sorting field that matches the current grouping field. Other sorting fields cannot be used for drag sorting.';
const DRAG_SORT_FIELD_TIP_EXPR = tExpr(DRAG_SORT_FIELD_TIP, { ns: 'kanban' });
@@ -135,6 +149,73 @@ const replaceModelStepParams = (model: any, flowKey: string, stepKey: string, pa
model.emitter?.emit?.('onStepParamsChanged');
};
const getModelStepParams = (model: any, flowKey: string, stepKey: string) => {
const stepParams = model?.stepParams?.[flowKey]?.[stepKey];
if (stepParams) {
return stepParams;
}
try {
return model?.getStepParams?.(flowKey, stepKey) || {};
} catch {
return {};
}
};
const hasKanbanPopupTemplateState = (params?: Record<string, any>) => {
if (!params || typeof params !== 'object') {
return false;
}
return !!normalizeKanbanPopupTemplateUid(params.popupTemplateUid) || params.popupTemplateContext === true;
};
const isKanbanPopupTemplateCopyMode = (params?: Record<string, any>) => {
return params?.popupTemplateContext === true;
};
const mergeKanbanPopupTemplateSettings = (baseSettings: Record<string, any>, popupSettings?: Record<string, any>) => {
if (!hasKanbanPopupTemplateState(popupSettings)) {
return baseSettings;
}
const nextSettings = { ...baseSettings };
POPUP_TEMPLATE_SETTING_KEYS.forEach((key) => {
if (Object.prototype.hasOwnProperty.call(popupSettings, key)) {
nextSettings[key] = popupSettings[key];
}
});
if (popupSettings?.popupTemplateContext === true) {
delete nextSettings.popupTemplateUid;
delete nextSettings.popupTemplateHasFilterByTk;
delete nextSettings.popupTemplateHasSourceId;
} else if (popupSettings?.popupTemplateUid) {
delete nextSettings.popupTemplateContext;
}
return nextSettings;
};
const getKanbanPopupActionSettings = (action: any) => getModelStepParams(action, 'popupSettings', 'openView');
const buildKanbanPopupSettingsFromAction = (baseSettings: Record<string, any>, action: any) => {
const actionParams = getKanbanPopupActionSettings(action);
if (Object.keys(actionParams).length === 0) {
return baseSettings;
}
const popupSettings = {
...baseSettings,
...actionParams,
uid: actionParams.uid || baseSettings.uid,
collectionName: actionParams.collectionName || baseSettings.collectionName,
dataSourceKey: actionParams.dataSourceKey || baseSettings.dataSourceKey,
};
return mergeKanbanPopupTemplateSettings(popupSettings, actionParams);
};
const setKanbanModelProps = (model: any, props: Record<string, any>) => {
model.setProps(props);
model._options = model._options || {};
@@ -211,13 +292,18 @@ const resolveKanbanPopupTargetUid = ({
nextPopupTargetUid,
currentPopupTemplateUid,
currentPopupTargetUid,
popupTemplateCopyMode,
}: {
nextPopupTemplateUid?: string;
nextPopupTargetUid?: string;
currentPopupTemplateUid?: string;
currentPopupTargetUid?: string;
popupTemplateCopyMode?: boolean;
}) => {
return !nextPopupTemplateUid && currentPopupTemplateUid && nextPopupTargetUid === currentPopupTargetUid
return !nextPopupTemplateUid &&
!popupTemplateCopyMode &&
currentPopupTemplateUid &&
nextPopupTargetUid === currentPopupTargetUid
? undefined
: nextPopupTargetUid;
};
@@ -242,6 +328,7 @@ const applyKanbanBlockPopupSettings = async (
nextPopupTargetUid,
currentPopupTemplateUid,
currentPopupTargetUid,
popupTemplateCopyMode: isKanbanPopupTemplateCopyMode(params),
});
const normalizedParams = {
...params,
@@ -867,6 +954,7 @@ export class KanbanBlockModel extends CollectionBlockModel<{
pageModelClass?: string;
dataSourceKey?: string;
collectionName?: string;
popupTemplateParams?: Record<string, any>;
},
syncOptions: KanbanPopupActionOptions = {},
) {
@@ -882,41 +970,48 @@ export class KanbanBlockModel extends CollectionBlockModel<{
const currentParams = action.getStepParams?.('popupSettings', 'openView') || {};
const currentPopupTemplateUid = normalizeKanbanPopupTemplateUid(currentParams.popupTemplateUid);
const currentUid = normalizeKanbanPopupTargetUid(currentParams.uid);
const nextTemplateParams = hasKanbanPopupTemplateState(options.popupTemplateParams)
? options.popupTemplateParams
: undefined;
const currentTemplateParams =
!syncOptions.persist && hasKanbanPopupTemplateState(currentParams) ? currentParams : undefined;
const templateParams = nextTemplateParams || currentTemplateParams;
const templateUid = normalizeKanbanPopupTargetUid(templateParams?.uid);
let sanitizedUid = nextUid && nextUid !== this.uid && nextUid !== selfUid ? nextUid : undefined;
if (!nextPopupTemplateUid && currentPopupTemplateUid && sanitizedUid === currentUid) {
sanitizedUid = undefined;
}
const resolvedUid = sanitizedUid || (nextPopupTemplateUid ? currentUid || selfUid : selfUid);
const resolvedUid = sanitizedUid || templateUid || (nextPopupTemplateUid ? currentUid || selfUid : selfUid);
const nextPageModelClass = options.pageModelClass || undefined;
const nextDataSourceKey = options.dataSourceKey || undefined;
const nextCollectionName = options.collectionName || undefined;
if (
currentParams.mode === nextMode &&
currentParams.size === nextSize &&
currentParams.popupTemplateUid === nextPopupTemplateUid &&
normalizeKanbanPopupTargetUid(currentParams.uid) === resolvedUid &&
currentParams.pageModelClass === nextPageModelClass &&
currentParams.dataSourceKey === nextDataSourceKey &&
currentParams.collectionName === nextCollectionName
) {
const nextParams = {
...(templateParams || {}),
mode: nextMode,
size: nextSize,
uid: resolvedUid,
pageModelClass: nextPageModelClass,
...(nextPopupTemplateUid ? { popupTemplateUid: nextPopupTemplateUid } : {}),
...(templateParams?.dataSourceKey || nextDataSourceKey
? { dataSourceKey: templateParams?.dataSourceKey || nextDataSourceKey }
: {}),
...(templateParams?.collectionName || nextCollectionName
? { collectionName: templateParams?.collectionName || nextCollectionName }
: {}),
};
if (!nextPopupTemplateUid && templateParams?.popupTemplateContext === true) {
delete nextParams.popupTemplateUid;
}
if (JSON.stringify(currentParams) === JSON.stringify(nextParams)) {
return;
}
const nextParams = {
...(nextPopupTemplateUid ? currentParams : {}),
mode: nextMode,
size: nextSize,
popupTemplateUid: nextPopupTemplateUid,
uid: resolvedUid,
pageModelClass: nextPageModelClass,
...(nextDataSourceKey ? { dataSourceKey: nextDataSourceKey } : {}),
...(nextCollectionName ? { collectionName: nextCollectionName } : {}),
};
action.setStepParams('popupSettings', 'openView', nextParams);
replaceModelStepParams(action, 'popupSettings', 'openView', nextParams);
if (syncOptions.persist && this.context.flowSettingsEnabled && action?.saveStepParams) {
await action.saveStepParams();
@@ -934,6 +1029,7 @@ export class KanbanBlockModel extends CollectionBlockModel<{
pageModelClass: this.getCardPopupPageModelClass(),
dataSourceKey: this.collection?.dataSourceKey,
collectionName: this.collection?.name,
popupTemplateParams: getModelStepParams(this.subModels?.item, 'cardSettings', 'popup'),
},
options,
);
@@ -996,6 +1092,7 @@ export class KanbanBlockModel extends CollectionBlockModel<{
pageModelClass: this.getPopupPageModelClass(),
dataSourceKey: this.collection?.dataSourceKey,
collectionName: this.collection?.name,
popupTemplateParams: getModelStepParams(this, 'kanbanSettings', 'popup'),
},
options,
);
@@ -1059,16 +1156,6 @@ export class KanbanBlockModel extends CollectionBlockModel<{
}
try {
if (typeof this.context?.openView === 'function') {
await this.context.openView(action.uid, {
formData: this.buildQuickCreateFormData(column),
...(this.collection?.dataSourceKey ? { dataSourceKey: this.collection.dataSourceKey } : {}),
...(this.collection?.name ? { collectionName: this.collection.name } : {}),
target: this.context.layoutContentElement,
});
return;
}
await action.dispatchEvent(
'click',
{
@@ -1109,17 +1196,6 @@ export class KanbanBlockModel extends CollectionBlockModel<{
}
try {
if (typeof this.context?.openView === 'function' && action.uid) {
await this.context.openView(action.uid, {
mode: this.getCardOpenMode(),
...(this.collection?.dataSourceKey ? { dataSourceKey: this.collection.dataSourceKey } : {}),
...(this.collection?.name ? { collectionName: this.collection.name } : {}),
filterByTk,
target: this.context.layoutContentElement,
});
return;
}
await action.dispatchEvent(
'click',
{
@@ -1527,26 +1603,46 @@ KanbanBlockModel.registerFlow({
return !(enabled ?? defaultEnabled);
},
async defaultParams(ctx) {
const model = ctx.model as KanbanBlockModel;
const commonParams = await resolveKanbanOpenViewDefaultParams(ctx as any);
const action =
typeof model.ensureQuickCreateAction === 'function'
? await model.ensureQuickCreateAction()
: typeof model.getQuickCreateAction === 'function'
? model.getQuickCreateAction()
: undefined;
const popupPageModelClass =
typeof (ctx.model as KanbanBlockModel).getPopupPageModelClass === 'function'
? (ctx.model as KanbanBlockModel).getPopupPageModelClass()
typeof model.getPopupPageModelClass === 'function'
? model.getPopupPageModelClass()
: ctx.model?.props?.popupPageModelClass;
return {
...commonParams,
mode: (ctx.model as KanbanBlockModel).getPopupMode(),
size: (ctx.model as KanbanBlockModel).getPopupSize(),
popupTemplateUid: (ctx.model as KanbanBlockModel).getPopupTemplateUid(),
pageModelClass: popupPageModelClass || commonParams.pageModelClass,
uid: (ctx.model as KanbanBlockModel).getPopupTargetUid(),
};
return buildKanbanPopupSettingsFromAction(
{
...commonParams,
mode: model.getPopupMode(),
size: model.getPopupSize(),
popupTemplateUid: model.getPopupTemplateUid(),
pageModelClass: popupPageModelClass || commonParams.pageModelClass,
uid: model.getPopupTargetUid(),
},
action,
);
},
async handler(ctx, params) {
await applyKanbanBlockPopupSettings(ctx.model as KanbanBlockModel, params, { persist: false });
},
async beforeParamsSave(ctx, params, previousParams) {
await ctx.model?.getAction?.('openView')?.beforeParamsSave?.(ctx, params, previousParams);
await applyKanbanBlockPopupSettings(ctx.model as KanbanBlockModel, params, { persist: true });
const model = ctx.model as KanbanBlockModel;
const action =
typeof model.ensureQuickCreateAction === 'function'
? await model.ensureQuickCreateAction()
: typeof model.getQuickCreateAction === 'function'
? model.getQuickCreateAction()
: undefined;
const storedParams = getKanbanPopupActionSettings(action);
await ctx.model
?.getAction?.('openView')
?.beforeParamsSave?.(ctx, params, Object.keys(storedParams).length > 0 ? storedParams : previousParams);
await applyKanbanBlockPopupSettings(model, params, { persist: true });
},
},
pageSize: {
@@ -67,6 +67,102 @@ const replaceModelStepParams = (model: any, flowKey: string, stepKey: string, pa
model.emitter?.emit?.('onStepParamsChanged');
};
const getModelStepParams = (model: any, flowKey: string, stepKey: string) => {
const stepParams = model?.stepParams?.[flowKey]?.[stepKey];
if (stepParams) {
return stepParams;
}
try {
return model?.getStepParams?.(flowKey, stepKey) || {};
} catch {
return {};
}
};
const isKanbanPopupTemplateCopyMode = (params?: Record<string, any>) => {
return params?.popupTemplateContext === true;
};
const hasKanbanPopupTemplateState = (params?: Record<string, any>) => {
if (!params || typeof params !== 'object') {
return false;
}
return !!normalizeKanbanPopupTemplateUid(params.popupTemplateUid) || params.popupTemplateContext === true;
};
const POPUP_TEMPLATE_SETTING_KEYS = [
'uid',
'dataSourceKey',
'collectionName',
'associationName',
'filterByTk',
'sourceId',
'popupTemplateUid',
'popupTemplateMode',
'popupTemplateContext',
'popupTemplateHasFilterByTk',
'popupTemplateHasSourceId',
];
const mergeKanbanCardPopupTemplateSettings = (
baseSettings: Record<string, any>,
popupSettings?: Record<string, any>,
) => {
if (!hasKanbanPopupTemplateState(popupSettings)) {
return baseSettings;
}
const nextSettings = { ...baseSettings };
POPUP_TEMPLATE_SETTING_KEYS.forEach((key) => {
if (Object.prototype.hasOwnProperty.call(popupSettings, key)) {
nextSettings[key] = popupSettings[key];
}
});
if (popupSettings?.popupTemplateContext === true) {
delete nextSettings.popupTemplateUid;
delete nextSettings.popupTemplateHasFilterByTk;
delete nextSettings.popupTemplateHasSourceId;
} else if (popupSettings?.popupTemplateUid) {
delete nextSettings.popupTemplateContext;
}
return nextSettings;
};
const getKanbanCardPopupActionSettings = (model: any, action?: any) => {
const parentModel = getKanbanCardPersistentParentModel(model);
return getModelStepParams(action || parentModel?.subModels?.cardViewAction, 'popupSettings', 'openView');
};
const ensureKanbanCardPopupAction = async (model: any) => {
const parentModel = getKanbanCardPersistentParentModel(model);
if (typeof parentModel?.ensureCardViewAction === 'function') {
return await parentModel.ensureCardViewAction();
}
return parentModel?.subModels?.cardViewAction;
};
const buildKanbanCardPopupSettingsFromAction = (model: any, baseSettings: Record<string, any>, action?: any) => {
const actionParams = getKanbanCardPopupActionSettings(model, action);
if (Object.keys(actionParams).length === 0) {
return baseSettings;
}
const popupSettings = {
...baseSettings,
...actionParams,
uid: actionParams.uid || baseSettings.uid,
collectionName: actionParams.collectionName || baseSettings.collectionName,
dataSourceKey: actionParams.dataSourceKey || baseSettings.dataSourceKey,
};
return mergeKanbanCardPopupTemplateSettings(popupSettings, actionParams);
};
const applyKanbanCardPopupSettings = async (
model: any,
params: Record<string, any>,
@@ -82,7 +178,10 @@ const applyKanbanCardPopupSettings = async (
getKanbanCardPopupProp(model, 'popupTargetUid', 'cardPopupTargetUid'),
);
const resolvedPopupTargetUid =
!nextPopupTemplateUid && currentPopupTemplateUid && nextPopupTargetUid === currentPopupTargetUid
!nextPopupTemplateUid &&
!isKanbanPopupTemplateCopyMode(params) &&
currentPopupTemplateUid &&
nextPopupTargetUid === currentPopupTargetUid
? undefined
: nextPopupTargetUid;
const normalizedParams = {
@@ -256,22 +355,33 @@ KanbanCardItemModel.registerFlow({
use: 'openView',
async defaultParams(ctx) {
const commonParams = await resolveKanbanOpenViewDefaultParams(ctx as any);
return {
...commonParams,
mode: normalizeKanbanCardOpenMode(getKanbanCardPopupProp(ctx.model, 'openMode', 'cardOpenMode')),
size: normalizeKanbanPopupSize(getKanbanCardPopupProp(ctx.model, 'popupSize', 'cardPopupSize')),
popupTemplateUid: normalizeKanbanPopupTemplateUid(
getKanbanCardPopupProp(ctx.model, 'popupTemplateUid', 'cardPopupTemplateUid'),
),
pageModelClass: getKanbanCardPopupProp(ctx.model, 'pageModelClass', 'cardPopupPageModelClass'),
uid: normalizeKanbanPopupTargetUid(getKanbanCardPopupProp(ctx.model, 'popupTargetUid', 'cardPopupTargetUid')),
};
const action = await ensureKanbanCardPopupAction(ctx.model);
return buildKanbanCardPopupSettingsFromAction(
ctx.model,
{
...commonParams,
mode: normalizeKanbanCardOpenMode(getKanbanCardPopupProp(ctx.model, 'openMode', 'cardOpenMode')),
size: normalizeKanbanPopupSize(getKanbanCardPopupProp(ctx.model, 'popupSize', 'cardPopupSize')),
popupTemplateUid: normalizeKanbanPopupTemplateUid(
getKanbanCardPopupProp(ctx.model, 'popupTemplateUid', 'cardPopupTemplateUid'),
),
pageModelClass: getKanbanCardPopupProp(ctx.model, 'pageModelClass', 'cardPopupPageModelClass'),
uid: normalizeKanbanPopupTargetUid(
getKanbanCardPopupProp(ctx.model, 'popupTargetUid', 'cardPopupTargetUid'),
),
},
action,
);
},
async handler(ctx, params) {
await applyKanbanCardPopupSettings(ctx.model, params, { persist: false });
},
async beforeParamsSave(ctx, params, previousParams) {
await ctx.model?.getAction?.('openView')?.beforeParamsSave?.(ctx, params, previousParams);
const action = await ensureKanbanCardPopupAction(ctx.model);
const storedParams = getKanbanCardPopupActionSettings(ctx.model, action);
await ctx.model
?.getAction?.('openView')
?.beforeParamsSave?.(ctx, params, Object.keys(storedParams).length > 0 ? storedParams : previousParams);
await applyKanbanCardPopupSettings(ctx.model, params, { persist: true });
},
},