@nocobase/client-v2 3.0.0-alpha.5 → 3.0.0-alpha.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,13 +9,14 @@
9
9
 
10
10
  import { LockOutlined, QuestionCircleOutlined } from '@ant-design/icons';
11
11
  import { css } from '@emotion/css';
12
- import type { PropertyMetaFactory } from '@nocobase/flow-engine';
12
+ import type { FlowModelOptions, PropertyMetaFactory } from '@nocobase/flow-engine';
13
13
  import {
14
14
  Droppable,
15
15
  tExpr,
16
16
  FlowsFloatContextMenu,
17
17
  DragHandler,
18
18
  MemoFlowModelRenderer,
19
+ createCurrentRecordMetaFactory,
19
20
  createRecordMetaFactory,
20
21
  createRecordResolveOnServerWithLocal,
21
22
  ElementProxy,
@@ -51,6 +52,14 @@ function getRecordRenderSignature(record: any) {
51
52
  export class JSColumnModel extends TableCustomColumnModel {
52
53
  // Stable per‑instance render component to avoid remounts across re-renders
53
54
  private _RenderComponent?: React.ComponentType;
55
+
56
+ onInit(options: FlowModelOptions): void {
57
+ super.onInit(options);
58
+ this.context.defineProperty('record', {
59
+ meta: createCurrentRecordMetaFactory(this.context, () => this.context.collection),
60
+ });
61
+ }
62
+
54
63
  renderHiddenInConfig() {
55
64
  return (
56
65
  <Tooltip
@@ -11,17 +11,62 @@ import { FlowEngine } from '@nocobase/flow-engine';
11
11
  import { describe, expect, it, vi } from 'vitest';
12
12
  import { JSColumnModel } from '../JSColumnModel';
13
13
 
14
- describe('JSColumnModel row refresh', () => {
14
+ describe('JSColumnModel', () => {
15
+ it('declares current record metadata before rendering any row', async () => {
16
+ const engine = new FlowEngine();
17
+ engine.registerModels({ JSColumnModel });
18
+ const model = engine.createModel<JSColumnModel>({
19
+ use: 'JSColumnModel',
20
+ uid: 'js-column-record-meta',
21
+ props: {
22
+ width: 200,
23
+ title: 'JS column',
24
+ },
25
+ });
26
+
27
+ engine.context.dataSourceManager.getDataSource('main').addCollection({
28
+ name: 'users',
29
+ filterTargetKey: 'id',
30
+ fields: [
31
+ { name: 'id', type: 'integer', interface: 'number' },
32
+ { name: 'age', type: 'integer', interface: 'integer' },
33
+ ],
34
+ });
35
+
36
+ const collection = engine.context.dataSourceManager.getCollection('main', 'users');
37
+ model.context.defineProperty('collection', {
38
+ value: collection,
39
+ });
40
+
41
+ const recordNode = model.context.getPropertyMetaTree().find((node) => node.name === 'record');
42
+ const recordFields =
43
+ typeof recordNode?.children === 'function' ? await recordNode.children() : recordNode?.children || [];
44
+
45
+ expect(model.context.record).toBeUndefined();
46
+ expect(recordNode).toMatchObject({
47
+ name: 'record',
48
+ title: 'Current record',
49
+ paths: ['record'],
50
+ });
51
+ expect(recordFields).toEqual(
52
+ expect.arrayContaining([
53
+ expect.objectContaining({ name: 'id', paths: ['record', 'id'] }),
54
+ expect.objectContaining({ name: 'age', paths: ['record', 'age'] }),
55
+ ]),
56
+ );
57
+ });
58
+
15
59
  it('changes renderer key and invalidates beforeRender cache when row content changes', () => {
16
60
  const engine = new FlowEngine();
17
- const model = new JSColumnModel({
61
+ engine.registerModels({ JSColumnModel });
62
+ const model = engine.createModel<JSColumnModel>({
63
+ use: 'JSColumnModel',
18
64
  uid: 'js-column-row-refresh',
19
- flowEngine: engine,
20
65
  props: {
21
66
  width: 200,
22
67
  title: 'JS column',
23
68
  },
24
- } as any);
69
+ });
25
70
 
26
71
  engine.context.dataSourceManager.getDataSource('main').addCollection({
27
72
  name: 'users',
@@ -39,12 +84,14 @@ describe('JSColumnModel row refresh', () => {
39
84
  });
40
85
 
41
86
  const column = model.getColumnProps();
42
- const first = column.render(null, { id: 1, age: 3, workyears: 39.2 }, 0) as any;
87
+ const firstRecord = { id: 1, age: 3, workyears: 39.2 };
88
+ const first = column.render(null, firstRecord, 0) as any;
43
89
  const fork = model.getFork('1') as any;
44
90
  const invalidateFlowCache = vi.fn();
45
91
  fork.invalidateFlowCache = invalidateFlowCache;
46
92
  const second = column.render(null, { id: 1, age: 37, workyears: 39.2 }, 0) as any;
47
93
 
94
+ expect(fork.context.record).toEqual({ id: 1, age: 37, workyears: 39.2 });
48
95
  expect(first.key).not.toBe(second.key);
49
96
  expect(invalidateFlowCache).toHaveBeenCalledWith('beforeRender');
50
97
  });
@@ -243,6 +243,52 @@ describe('RecordPickerFieldModel item context', () => {
243
243
  expect(associationOpts.serverOnlyWhenContextParams).toBe(true);
244
244
  });
245
245
 
246
+ it('builds whole-association descriptors for current and recursive parent item values', async () => {
247
+ const people = {
248
+ name: 'people',
249
+ dataSourceKey: 'main',
250
+ filterTargetKey: 'id',
251
+ getFields: () => [],
252
+ getField: () => undefined,
253
+ } as any;
254
+ const owner = {
255
+ name: 'owner',
256
+ target: 'people',
257
+ targetCollection: people,
258
+ isAssociationField: () => true,
259
+ } as any;
260
+ const collection = {
261
+ name: 'tasks',
262
+ dataSourceKey: 'main',
263
+ getFields: () => [owner],
264
+ getField: (name: string) => (name === 'owner' ? owner : undefined),
265
+ } as any;
266
+ const options = createAssociationItemChainContextPropertyOptions({
267
+ t: (value) => value,
268
+ title: 'Current item',
269
+ collectionAccessor: () => collection,
270
+ propertiesAccessor: (ctx) => ctx.item.value,
271
+ resolverPropertiesAccessor: () => ({ owner: { id: 7 } }),
272
+ parentCollectionAccessor: () => collection,
273
+ parentAccessors: {
274
+ parentPropertiesAccessor: () => ({ owner: { id: 8 } }),
275
+ parentItemMetaAccessor: () => undefined,
276
+ parentItemResolverAccessor: () => undefined,
277
+ },
278
+ });
279
+ const meta = await options.meta();
280
+ const params = await meta.buildVariablesParams({
281
+ item: { value: { owner: { id: 7 } }, parentItem: { value: { owner: { id: 8 } } } },
282
+ });
283
+
284
+ expect(params).toEqual({
285
+ parentItem: { value: { owner: { collection: 'people', dataSourceKey: 'main', filterByTk: 8 } } },
286
+ value: { owner: { collection: 'people', dataSourceKey: 'main', filterByTk: 7 } },
287
+ });
288
+ expect(options.resolveOnServer('value.owner.name')).toBe(true);
289
+ expect(options.resolveOnServer('parentItem.value.owner.name')).toBe(true);
290
+ });
291
+
246
292
  it('disables current item attributes for select-record popup item context', async () => {
247
293
  const parentCtx = new FlowContext();
248
294
  const departmentsCollection = {
@@ -18,6 +18,7 @@ import {
18
18
  } from '../flow/models/topbar/UserCenterTopbarActionModel';
19
19
  import { useApp } from '../hooks/useApp';
20
20
  import { useCurrentUserAuthStatus } from '../nocobase-buildin-plugin/currentUserAuthStatus';
21
+ import { addCustomAlgorithmToTheme } from '../theme/customAlgorithm';
21
22
  import { SettingsBrand } from './SettingsBrand';
22
23
  import { SettingsGroupNav } from './SettingsGroupNav';
23
24
  import { SettingsSearch } from './SettingsSearch';
@@ -79,17 +80,31 @@ export const SettingsShell: FC = ({ children }) => {
79
80
  const { token } = antdTheme.useToken();
80
81
  // 设置中心的外观由主题编辑器里那条「简约」主题记录约束;读不到才用代码里的中性配色兜底。
81
82
  const storedThemeConfig = useSettingsThemeConfig();
83
+ const normalizedStoredThemeConfig = useMemo(
84
+ () =>
85
+ storedThemeConfig
86
+ ? addCustomAlgorithmToTheme({
87
+ ...storedThemeConfig,
88
+ algorithm: Array.isArray(storedThemeConfig.algorithm)
89
+ ? [...storedThemeConfig.algorithm]
90
+ : storedThemeConfig.algorithm,
91
+ })
92
+ : storedThemeConfig,
93
+ [storedThemeConfig],
94
+ );
82
95
  // 顶栏和补丁样式要按**设置中心自己这套主题**算色,不能直接用上面那个 `token`——
83
96
  // 这个组件在自己的 ConfigProvider 外面,`token` 是业务端主题的。用它的话,业务端一切暗黑,
84
97
  // 顶栏就跟着变黑,而内容区还是简约的白,一半黑一半白。
85
98
  const settingsToken = useMemo(
86
- () => (storedThemeConfig ? antdTheme.getDesignToken(storedThemeConfig) : token),
87
- [storedThemeConfig, token],
99
+ () => (normalizedStoredThemeConfig ? antdTheme.getDesignToken(normalizedStoredThemeConfig) : token),
100
+ [normalizedStoredThemeConfig, token],
88
101
  );
89
102
  const settingsShellTheme = useMemo<ThemeConfig>(
90
103
  () =>
91
- storedThemeConfig ? withSettingsHeaderTheme(storedThemeConfig, settingsToken) : buildSettingsNeutralTheme(token),
92
- [settingsToken, storedThemeConfig, token],
104
+ normalizedStoredThemeConfig
105
+ ? withSettingsHeaderTheme(normalizedStoredThemeConfig, settingsToken)
106
+ : buildSettingsNeutralTheme(token),
107
+ [normalizedStoredThemeConfig, settingsToken, token],
93
108
  );
94
109
  const headerColors = useMemo(() => getSettingsHeaderColors(settingsToken), [settingsToken]);
95
110
  const settingsGlobalCss = useMemo(() => buildSettingsGlobalCss(settingsToken), [settingsToken]);