From 1f6da09cf78416d1b85a4006939c1f803e28abe5 Mon Sep 17 00:00:00 2001 From: Zeke Zhang <958414905@qq.com> Date: Sat, 28 Feb 2026 02:02:04 +0800 Subject: [PATCH 1/2] fix(filter-form): preserve SQL filter titles (#8724) * fix(filter-form): preserve SQL filter titles * fix(filter-form): apply SQL fallback field props --- .../filter-form/FilterFormItemModel.tsx | 92 +++++++-- ...FilterFormItemModel.defineChildren.test.ts | 176 ++++++++++++++++++ 2 files changed, 256 insertions(+), 12 deletions(-) diff --git a/packages/core/client/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx b/packages/core/client/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx index 8c8fec8717d..66798d127b4 100644 --- a/packages/core/client/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +++ b/packages/core/client/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx @@ -9,20 +9,20 @@ import { Collection, - tExpr, FieldModelRenderer, FilterableItemModel, FlowModelContext, FormItem, + tExpr, } from '@nocobase/flow-engine'; import { Empty } from 'antd'; import _, { cloneDeep, debounce, isEqual } from 'lodash'; import React from 'react'; import { CollectionBlockModel, FieldModel } from '../../base'; import { RecordSelectFieldModel } from '../../fields/AssociationFieldModel/RecordSelectFieldModel'; +import { FilterManager } from '../filter-manager'; import { getAllDataModels, getDefaultOperator } from '../filter-manager/utils'; import { FilterFormFieldModel } from './fields'; -import { FilterManager } from '../filter-manager'; import { normalizeFilterValueByOperator } from './valueNormalization'; const getAssociationTargetCollection = (field: any, collection?: Collection, model?: CollectionBlockModel) => { @@ -89,6 +89,37 @@ const normalizeAssociationDefaultFilterValue = (value: any, fieldModel: any) => return pickValue(value); }; +const buildVirtualFilterCollectionField = (ctx: FlowModelContext, filterField: any) => { + if (!filterField) { + return; + } + + const resolvedTitle = filterField?.title || filterField?.name; + const interfaceName = filterField?.interface; + const interfaceConfig = interfaceName + ? // @ts-ignore + ctx?.dataSourceManager?.collectionFieldInterfaceManager?.getFieldInterface?.(interfaceName) + : undefined; + + const result = { + ...filterField, + title: resolvedTitle, + uiSchema: { + ...(filterField?.uiSchema || {}), + title: resolvedTitle, + }, + filterable: filterField?.filterable || interfaceConfig?.filterable, + isAssociationField: + typeof filterField?.isAssociationField === 'function' + ? filterField.isAssociationField + : () => Boolean(filterField?.target), + getComponentProps: + typeof filterField?.getComponentProps === 'function' ? filterField.getComponentProps : () => ({}), + }; + + return result; +}; + const buildFilterFormFieldItem = ({ model, collection, @@ -410,6 +441,16 @@ export class FilterFormItemModel extends FilterableItemModel<{ super.onInit(options); // 创建防抖的 doFilter 方法,延迟 300ms this.debouncedDoFilter = debounce(this.doFilter.bind(this), 300); + + const initFilterField = this.getStepParams('filterFormItemSettings', 'init')?.filterField; + if (initFilterField) { + this.context.defineProperty('filterField', { + value: { + ...initFilterField, + title: initFilterField?.title || initFilterField?.name, + }, + }); + } } onMount(): void { @@ -548,7 +589,8 @@ FilterFormItemModel.registerFlow({ 'x-decorator': 'FormItem', 'x-reactions': (field) => { const model = ctx.model; - const originTitle = model.collectionField?.title || ctx.filterField?.title; + const originTitle = + model.collectionField?.title || ctx.filterField?.title || ctx.filterField?.name || model.fieldPath; field.decoratorProps = { ...field.decoratorProps, extra: model.context.t('Original field title: ') + originTitle, @@ -559,11 +601,12 @@ FilterFormItemModel.registerFlow({ }, defaultParams: (ctx) => { return { - label: ctx.collectionField?.title || ctx.filterField?.title, + label: ctx.collectionField?.title || ctx.filterField?.title || ctx.filterField?.name, }; }, handler(ctx, params) { - if (params.label && params.label === ctx.collectionField?.title) { + const originTitle = ctx.collectionField?.title || ctx.filterField?.title || ctx.filterField?.name; + if (params.label && params.label === originTitle) { ctx.model.setProps({ label: params.label }); } else { ctx.model.setProps({ label: ctx.t(params.label, { ns: 'lm-flow-engine' }) }); @@ -575,7 +618,38 @@ FilterFormItemModel.registerFlow({ // }, init: { async handler(ctx, params) { - const collectionField = ctx.model.collectionField; + const normalizedFilterField = params.filterField + ? { + ...params.filterField, + title: params.filterField?.title || params.filterField?.name, + } + : params.filterField; + ctx.model.context.defineProperty('filterField', { + value: normalizedFilterField, + }); + + ctx.model.setProps({ + name: `${ctx.model.fieldPath}_${ctx.model.uid}`, // 确保每个字段的名称唯一 + }); + + let collectionField = ctx.model.collectionField; + // SQL 图表筛选等场景下没有 collectionName/dataSourceKey,无法解析真实 collectionField。 + // 此时用 filterField 元数据注入一个虚拟字段,避免误显示“字段已删除”占位。 + const fieldSettingsInitParams = ctx.model.getFieldSettingsInitParams?.() || {}; + const hasCollectionContext = !!( + // @ts-ignore + (fieldSettingsInitParams?.dataSourceKey && fieldSettingsInitParams?.collectionName) + ); + if (!hasCollectionContext && !collectionField && normalizedFilterField) { + const virtualField = buildVirtualFilterCollectionField(ctx, normalizedFilterField); + if (virtualField) { + ctx.model.context.defineProperty('collectionField', { + value: virtualField, + }); + collectionField = virtualField as any; + } + } + if (collectionField?.getComponentProps) { const componentProps = collectionField.getComponentProps(); const fieldModel = ctx.model.subModels?.field; @@ -589,12 +663,6 @@ FilterFormItemModel.registerFlow({ required: undefined, }); } - ctx.model.setProps({ - name: `${ctx.model.fieldPath}_${ctx.model.uid}`, // 确保每个字段的名称唯一 - }); - ctx.model.context.defineProperty('filterField', { - value: params.filterField, - }); }, }, diff --git a/packages/core/client/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts b/packages/core/client/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts index 533014781d3..2be03bbc1a2 100644 --- a/packages/core/client/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +++ b/packages/core/client/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts @@ -150,4 +150,180 @@ describe('FilterFormItemModel defineChildren association fields', () => { expect(filterItem.fieldPath).toBe('department.manager.name'); expect(filterItem.collectionField).toBeTruthy(); }); + + it('provides fallback field metadata for sql fields without collection context', async () => { + const engine = new FlowEngine(); + engine.registerModels({ + FilterFormItemModel, + NumberFieldModel, + }); + + const filterItem = engine.createModel({ + uid: 'sql-filter-item', + use: 'FilterFormItemModel', + stepParams: { + fieldSettings: { + init: { + fieldPath: 'id', + }, + }, + filterFormItemSettings: { + init: { + filterField: { + name: 'id', + title: 'id', + interface: 'number', + type: 'integer', + }, + }, + }, + }, + subModels: { + field: { + use: 'NumberFieldModel', + }, + }, + } as any); + + await filterItem.dispatchEvent('beforeRender'); + + expect(filterItem.collectionField).toBeTruthy(); + expect(filterItem.collectionField?.name).toBe('id'); + expect(filterItem.collectionField?.interface).toBe('number'); + }); + + it('keeps deleted-field detection when collection context exists', async () => { + const engine = new FlowEngine(); + engine.registerModels({ + FilterFormItemModel, + NumberFieldModel, + }); + + const ds = engine.dataSourceManager.getDataSource('main'); + ds.addCollection({ + name: 'users', + filterTargetKey: 'id', + fields: [{ name: 'id', type: 'integer', interface: 'number', filterable: { operators: [] } }], + }); + + const filterItem = engine.createModel({ + uid: 'deleted-field-item', + use: 'FilterFormItemModel', + stepParams: { + fieldSettings: { + init: { + dataSourceKey: 'main', + collectionName: 'users', + fieldPath: 'not_exists', + }, + }, + filterFormItemSettings: { + init: { + filterField: { + name: 'not_exists', + title: 'not_exists', + interface: 'number', + type: 'integer', + }, + }, + }, + }, + subModels: { + field: { + use: 'NumberFieldModel', + }, + }, + } as any); + + await filterItem.dispatchEvent('beforeRender'); + + expect(filterItem.collectionField).toBeFalsy(); + }); + + it('uses field name as fallback label when sql filter field title is missing', async () => { + const engine = new FlowEngine(); + engine.registerModels({ + FilterFormItemModel, + NumberFieldModel, + }); + + const filterItem = engine.createModel({ + uid: 'sql-filter-item-no-title', + use: 'FilterFormItemModel', + stepParams: { + fieldSettings: { + init: { + fieldPath: 'id', + }, + }, + filterFormItemSettings: { + init: { + filterField: { + name: 'id', + interface: 'number', + type: 'integer', + }, + }, + }, + }, + subModels: { + field: { + use: 'NumberFieldModel', + }, + }, + } as any); + + await filterItem.dispatchEvent('beforeRender'); + + expect(filterItem.props.label).toBe('id'); + }); + + it('applies getComponentProps from fallback sql field metadata', async () => { + const engine = new FlowEngine(); + engine.registerModels({ + FilterFormItemModel, + NumberFieldModel, + }); + + const filterItem = engine.createModel({ + uid: 'sql-filter-item-component-props', + use: 'FilterFormItemModel', + stepParams: { + fieldSettings: { + init: { + fieldPath: 'id', + }, + }, + filterFormItemSettings: { + init: { + filterField: { + name: 'id', + interface: 'number', + type: 'integer', + getComponentProps: () => ({ + placeholder: 'sql-id', + allowMultiple: true, + multiple: true, + required: true, + rules: [{ required: true }], + }), + }, + }, + }, + }, + subModels: { + field: { + use: 'NumberFieldModel', + }, + }, + } as any); + + await filterItem.dispatchEvent('beforeRender'); + + expect(filterItem.props.placeholder).toBe('sql-id'); + expect(filterItem.props.allowMultiple).toBe(true); + expect(filterItem.props.multiple).toBe(true); + expect(filterItem.props.required).toBeUndefined(); + expect(filterItem.props.rules).toBeUndefined(); + }); }); From 650fd8f9840b3a8cec15b1d85f2164e00274fe74 Mon Sep 17 00:00:00 2001 From: chenos Date: Sat, 28 Feb 2026 18:35:22 +0800 Subject: [PATCH 2/2] fix: resolve cache issue by appending hash to assets (#8730) * fix: pkg url hash * Update packages/core/server/src/plugin-manager/options/resource.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/plugin-manager/options/resource.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/core/server/src/plugin-manager/options/resource.ts b/packages/core/server/src/plugin-manager/options/resource.ts index 567124b5044..cd94ab7b6f9 100644 --- a/packages/core/server/src/plugin-manager/options/resource.ts +++ b/packages/core/server/src/plugin-manager/options/resource.ts @@ -14,6 +14,7 @@ import fse from 'fs-extra'; import path from 'path'; import Application from '../../application'; import PluginManager from '../plugin-manager'; +import crypto from 'crypto'; class PackageUrls { static items = {}; @@ -33,7 +34,22 @@ class PackageUrls { const distExists = await fse.exists(dist); if (distExists) { const fsState = await fse.stat(distExists ? dist : pkgPath); - t = `?t=${fsState.mtime.getTime()}`; + const appKey = process.env.APP_KEY || ''; + let version = ''; + try { + const pkgJson = await fse.readJson(path.resolve(pkgPath, 'package.json')); + if (pkgJson && typeof pkgJson.version === 'string') { + version = pkgJson.version; + } + } catch (error) { + // Ignore errors reading package.json and fall back to empty version + } + const hash = crypto + .createHash('sha256') + .update(fsState.mtime.getTime() + appKey + version) + .digest('hex') + .slice(0, 8); + t = `?hash=${hash}`; } const cdnBaseUrl = process.env.CDN_BASE_URL.replace(/\/+$/, ''); const url = `${cdnBaseUrl}${'/static/plugins/'}${packageName}/${PLUGIN_CLIENT_ENTRY_FILE}${t}`;