@nocobase/client-v2 2.1.21 → 2.1.22

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/client-v2",
3
- "version": "2.1.21",
3
+ "version": "2.1.22",
4
4
  "license": "Apache-2.0",
5
5
  "main": "lib/index.js",
6
6
  "module": "es/index.mjs",
@@ -27,11 +27,11 @@
27
27
  "@formily/antd-v5": "1.2.3",
28
28
  "@formily/react": "^2.2.27",
29
29
  "@formily/shared": "^2.2.27",
30
- "@nocobase/evaluators": "2.1.21",
31
- "@nocobase/flow-engine": "2.1.21",
32
- "@nocobase/sdk": "2.1.21",
33
- "@nocobase/shared": "2.1.21",
34
- "@nocobase/utils": "2.1.21",
30
+ "@nocobase/evaluators": "2.1.22",
31
+ "@nocobase/flow-engine": "2.1.22",
32
+ "@nocobase/sdk": "2.1.22",
33
+ "@nocobase/shared": "2.1.22",
34
+ "@nocobase/utils": "2.1.22",
35
35
  "ahooks": "^3.7.2",
36
36
  "antd": "5.24.2",
37
37
  "antd-style": "3.7.1",
@@ -46,5 +46,5 @@
46
46
  "react-i18next": "^11.15.1",
47
47
  "react-router-dom": "^6.30.1"
48
48
  },
49
- "gitHead": "491141417d8338022651c9e99afbd2d84c1e2b0b"
49
+ "gitHead": "811fc3d272389739babdfa4338faf843d5bf04ff"
50
50
  }
@@ -62,9 +62,25 @@ function isRecord(value: unknown): value is Record<string, unknown> {
62
62
  return !!value && typeof value === 'object' && !Array.isArray(value);
63
63
  }
64
64
 
65
+ interface CollectionFieldWithAssociationMarker {
66
+ isAssociationField?: () => boolean;
67
+ target?: unknown;
68
+ }
69
+
70
+ function hasAssociationMarker(value: unknown): value is CollectionFieldWithAssociationMarker {
71
+ return isRecord(value) && (typeof value.isAssociationField === 'function' || 'target' in value);
72
+ }
73
+
74
+ const isAssociationCollectionField = (collectionField: unknown) => {
75
+ if (!hasAssociationMarker(collectionField)) {
76
+ return false;
77
+ }
78
+ return collectionField.isAssociationField ? collectionField.isAssociationField() : Boolean(collectionField.target);
79
+ };
80
+
65
81
  const normalizeAssociationDefaultFilterValue = (value: any, fieldModel: any) => {
66
82
  const collectionField = fieldModel?.context?.collectionField;
67
- if (!collectionField?.isAssociationField?.()) {
83
+ if (!isAssociationCollectionField(collectionField)) {
68
84
  return value;
69
85
  }
70
86
 
@@ -171,8 +187,7 @@ const buildFilterFormFieldItem = ({
171
187
  if (!binding) {
172
188
  return;
173
189
  }
174
- const isAssociation =
175
- typeof field?.isAssociationField === 'function' ? field.isAssociationField() : Boolean(field?.target);
190
+ const isAssociation = isAssociationCollectionField(field);
176
191
  const fieldModel =
177
192
  isAssociation && ctxWithFlags.engine?.getModelClass?.('FilterFormRecordSelectFieldModel')
178
193
  ? 'FilterFormRecordSelectFieldModel'
@@ -507,10 +522,7 @@ export class FilterFormItemModel extends FilterableItemModel<{
507
522
  const formValue = this.context.form?.getFieldValue(this.props.name);
508
523
  const modelValue = fieldModel.getFilterValue ? fieldModel.getFilterValue() : formValue;
509
524
  const collectionField = (fieldModel as any)?.context?.collectionField;
510
- const isAssociationField =
511
- typeof collectionField?.isAssociationField === 'function'
512
- ? collectionField.isAssociationField()
513
- : !!collectionField?.target;
525
+ const isAssociationField = isAssociationCollectionField(collectionField);
514
526
  const shouldUseFormValue =
515
527
  isAssociationField &&
516
528
  !this.mounted &&
@@ -547,11 +559,7 @@ export class FilterFormItemModel extends FilterableItemModel<{
547
559
  return value;
548
560
  }
549
561
  const collectionField = (fieldModel as any)?.context?.collectionField;
550
- const isAssociation =
551
- typeof collectionField?.isAssociationField === 'function'
552
- ? collectionField.isAssociationField()
553
- : !!collectionField?.target;
554
- if (!isAssociation) {
562
+ if (!isAssociationCollectionField(collectionField)) {
555
563
  return value;
556
564
  }
557
565
  const normalizedFieldNames = normalizeAssociationFieldNames(
@@ -798,6 +806,13 @@ FilterFormItemModel.registerFlow({
798
806
  },
799
807
  defaultOperator: {
800
808
  use: 'defaultOperator',
809
+ hideInSettings(ctx) {
810
+ const collectionField =
811
+ ctx.collectionField ||
812
+ ctx.model?.context?.collectionField ||
813
+ ctx.model?.subModels?.field?.context?.collectionField;
814
+ return isAssociationCollectionField(collectionField);
815
+ },
801
816
  },
802
817
  operatorComponentProps: {
803
818
  use: 'operatorComponentProps',
@@ -33,6 +33,42 @@ class DummyCollectionBlockModel extends CollectionBlockModel {
33
33
  }
34
34
 
35
35
  describe('FilterFormItemModel defineChildren association fields', () => {
36
+ it('hides default operator setting for association filter fields', () => {
37
+ const engine = new FlowEngine();
38
+ engine.registerModels({
39
+ FilterFormItemModel,
40
+ });
41
+
42
+ const filterItem = engine.createModel<FilterFormItemModel>({
43
+ uid: 'association-filter-item-settings',
44
+ use: 'FilterFormItemModel',
45
+ });
46
+
47
+ const defaultOperatorStep = filterItem.getFlow('filterFormItemSettings')?.steps?.defaultOperator as {
48
+ hideInSettings?: (ctx: {
49
+ collectionField?: unknown;
50
+ model?: {
51
+ subModels?: {
52
+ field?: {
53
+ context?: {
54
+ collectionField?: unknown;
55
+ };
56
+ };
57
+ };
58
+ };
59
+ }) => boolean;
60
+ };
61
+ expect(defaultOperatorStep?.hideInSettings?.({ collectionField: { isAssociationField: () => true } })).toBe(true);
62
+ expect(
63
+ defaultOperatorStep?.hideInSettings?.({
64
+ model: { subModels: { field: { context: { collectionField: { target: 'departments' } } } } },
65
+ }),
66
+ ).toBe(true);
67
+ expect(defaultOperatorStep?.hideInSettings?.({ collectionField: { interface: 'input', type: 'string' } })).toBe(
68
+ false,
69
+ );
70
+ });
71
+
36
72
  it('groups association target fields and supports recursive paths', async () => {
37
73
  const engine = new FlowEngine();
38
74
  engine.registerModels({
@@ -30,6 +30,37 @@ import { FormItemModel } from './FormItemModel';
30
30
  export const QUICK_EDIT_POPOVER_MAX_HEIGHT = 'calc(100vh - 96px)';
31
31
  export const QUICK_EDIT_FORM_MAX_HEIGHT = 'calc(100vh - 160px)';
32
32
  export const QUICK_EDIT_MARKDOWN_HEIGHT = 'min(480px, calc(100vh - 320px))';
33
+ const QUICK_EDIT_MOBILE_DRAWER_HEIGHT = '50vh';
34
+ const QUICK_EDIT_MOBILE_FORM_MAX_HEIGHT = 'calc(50vh - var(--nb-mobile-page-header-height, 40px) - 132px)';
35
+ const QUICK_EDIT_MOBILE_CONTENT_PADDING = '8px var(--nb-mobile-page-tabs-content-padding, 12px) 0';
36
+ const QUICK_EDIT_MOBILE_ACTIONS_PADDING =
37
+ '8px var(--nb-mobile-page-tabs-content-padding, 12px) calc(80px + env(safe-area-inset-bottom, 0px))';
38
+ const QUICK_EDIT_MOBILE_MEDIA_QUERY = '(max-width: 768px)';
39
+
40
+ type QuickEditViewBeforeClosePayload = {
41
+ result?: unknown;
42
+ force?: boolean;
43
+ [key: string]: unknown;
44
+ };
45
+
46
+ type QuickEditViewBeforeCloseHandler = (
47
+ payload: QuickEditViewBeforeClosePayload,
48
+ ) => Promise<boolean | void> | boolean | void;
49
+
50
+ type QuickEditViewUpdateConfig = {
51
+ preventClose?: boolean;
52
+ [key: string]: unknown;
53
+ };
54
+
55
+ type QuickEditViewContainer = {
56
+ close: (result?: unknown, force?: boolean) => Promise<boolean | void> | boolean | void;
57
+ update?: (newConfig: QuickEditViewUpdateConfig) => unknown;
58
+ beforeClose?: QuickEditViewBeforeCloseHandler;
59
+ };
60
+
61
+ type QuickEditViewContext = {
62
+ defineProperty?: (key: string, options: { value: unknown }) => void;
63
+ };
33
64
 
34
65
  export function getQuickEditFieldProps(collectionField: CollectionField, fieldProps?: Record<string, any>) {
35
66
  const nextProps = { ...collectionField.getComponentProps(), ...(fieldProps || {}) };
@@ -39,13 +70,92 @@ export function getQuickEditFieldProps(collectionField: CollectionField, fieldPr
39
70
  return nextProps;
40
71
  }
41
72
 
73
+ function getQuickEditMobileLayoutState(flowEngine: FlowEngine, sourceFieldModel?: FlowModel) {
74
+ const sourceMobileLayout = sourceFieldModel?.context?.isMobileLayout;
75
+ if (typeof sourceMobileLayout === 'boolean') {
76
+ return { isMobileLayout: sourceMobileLayout, inheritsMobileContext: sourceMobileLayout };
77
+ }
78
+
79
+ const engineMobileLayout = flowEngine.context?.isMobileLayout;
80
+ if (typeof engineMobileLayout === 'boolean') {
81
+ return { isMobileLayout: engineMobileLayout, inheritsMobileContext: engineMobileLayout };
82
+ }
83
+
84
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
85
+ return { isMobileLayout: false, inheritsMobileContext: false };
86
+ }
87
+
88
+ return {
89
+ isMobileLayout: window.matchMedia(QUICK_EDIT_MOBILE_MEDIA_QUERY).matches,
90
+ inheritsMobileContext: false,
91
+ };
92
+ }
93
+
94
+ function getQuickEditTitle(
95
+ flowEngine: FlowEngine,
96
+ dataSourceKey: string,
97
+ collectionName: string,
98
+ fieldPath: string,
99
+ sourceFieldModel?: FlowModel,
100
+ fieldProps?: Record<string, unknown>,
101
+ ): React.ReactNode {
102
+ const sourceColumnTitle = sourceFieldModel?.parent?.props?.title;
103
+ if (sourceColumnTitle) {
104
+ return sourceColumnTitle;
105
+ }
106
+
107
+ const fieldPropsTitle = fieldProps?.title;
108
+ if (fieldPropsTitle) {
109
+ return fieldPropsTitle as React.ReactNode;
110
+ }
111
+
112
+ const collectionField = flowEngine.context.dataSourceManager.getCollectionField(
113
+ `${dataSourceKey}.${collectionName}.${fieldPath}`,
114
+ ) as CollectionField | undefined;
115
+ return collectionField?.title || fieldPath;
116
+ }
117
+
118
+ function createQuickEditViewContainer(view: QuickEditViewContainer): QuickEditViewContainer {
119
+ let preventClose = false;
120
+ let nextBeforeClose = view.beforeClose;
121
+ const beforeClose: QuickEditViewBeforeCloseHandler = async (payload) => {
122
+ if (preventClose && !payload?.force) {
123
+ return false;
124
+ }
125
+
126
+ const result = await nextBeforeClose?.(payload);
127
+ return result !== false;
128
+ };
129
+ view.beforeClose = beforeClose;
130
+
131
+ return {
132
+ ...view,
133
+ close(result, force) {
134
+ return view.close(result, force);
135
+ },
136
+ update(newConfig) {
137
+ if (Object.prototype.hasOwnProperty.call(newConfig, 'preventClose')) {
138
+ preventClose = !!newConfig.preventClose;
139
+ }
140
+ return view.update?.(newConfig);
141
+ },
142
+ get beforeClose() {
143
+ return view.beforeClose;
144
+ },
145
+ set beforeClose(value) {
146
+ nextBeforeClose = value;
147
+ view.beforeClose = beforeClose;
148
+ },
149
+ };
150
+ }
151
+
42
152
  export class QuickEditFormModel extends FlowModel {
43
153
  fieldPath: string;
44
154
 
45
155
  declare resource: SingleRecordResource;
46
156
  declare collection: Collection;
47
157
 
48
- viewContainer: any;
158
+ declare viewContainer: QuickEditViewContainer;
49
159
  __onSubmitSuccess;
50
160
  _fieldProps: any;
51
161
  _onOk: any;
@@ -103,6 +213,59 @@ export class QuickEditFormModel extends FlowModel {
103
213
  sourceFieldModel ? { delegate: sourceFieldModel.context } : undefined,
104
214
  ) as QuickEditFormModel;
105
215
 
216
+ const bodyStyles = {
217
+ maxHeight: QUICK_EDIT_POPOVER_MAX_HEIGHT,
218
+ overflowY: 'auto',
219
+ overscrollBehavior: 'contain',
220
+ };
221
+ const mobileBodyStyles = {
222
+ ...bodyStyles,
223
+ maxHeight: QUICK_EDIT_MOBILE_DRAWER_HEIGHT,
224
+ };
225
+ const content = (view: QuickEditViewContainer, viewContext?: QuickEditViewContext) => {
226
+ if (mobileLayoutState.isMobileLayout) {
227
+ viewContext?.defineProperty?.('isMobileLayout', { value: true });
228
+ }
229
+ model.viewContainer = createQuickEditViewContainer(view);
230
+ model.__onSubmitSuccess = onSuccess;
231
+ model._fieldProps = fieldProps;
232
+ model._onOk = onOk;
233
+ return (
234
+ <FlowModelRenderer
235
+ fallback={<Skeleton.Input size="small" />}
236
+ model={model}
237
+ inputArgs={{ filterByTk, record, sourceFieldModelUid }}
238
+ />
239
+ );
240
+ };
241
+ const mobileLayoutState = getQuickEditMobileLayoutState(flowEngine, sourceFieldModel);
242
+ if (mobileLayoutState.isMobileLayout) {
243
+ model.context.defineProperty('isMobileLayout', { value: true });
244
+ const viewer = sourceFieldModel?.context?.viewer || flowEngine.context.viewer;
245
+ const title = getQuickEditTitle(
246
+ flowEngine,
247
+ dataSourceKey,
248
+ collectionName,
249
+ fieldPath,
250
+ sourceFieldModel,
251
+ fieldProps,
252
+ );
253
+ await viewer.open({
254
+ type: 'drawer',
255
+ title,
256
+ closable: true,
257
+ placement: 'bottom',
258
+ inputArgs: {
259
+ isMobileLayout: true,
260
+ },
261
+ styles: {
262
+ body: mobileBodyStyles,
263
+ },
264
+ content,
265
+ });
266
+ return;
267
+ }
268
+
106
269
  await flowEngine.context.viewer.open({
107
270
  type: 'popover',
108
271
  target,
@@ -110,24 +273,10 @@ export class QuickEditFormModel extends FlowModel {
110
273
  styles: {
111
274
  body: {
112
275
  width: 420,
113
- maxHeight: QUICK_EDIT_POPOVER_MAX_HEIGHT,
114
- overflowY: 'auto',
115
- overscrollBehavior: 'contain',
276
+ ...bodyStyles,
116
277
  },
117
278
  },
118
- content: (popover) => {
119
- model.viewContainer = popover;
120
- model.__onSubmitSuccess = onSuccess;
121
- model._fieldProps = fieldProps;
122
- model._onOk = onOk;
123
- return (
124
- <FlowModelRenderer
125
- fallback={<Skeleton.Input size="small" />}
126
- model={model}
127
- inputArgs={{ filterByTk, record, sourceFieldModelUid }}
128
- />
129
- );
130
- },
279
+ content,
131
280
  });
132
281
  }
133
282
 
@@ -168,13 +317,15 @@ export class QuickEditFormModel extends FlowModel {
168
317
  }
169
318
 
170
319
  render() {
320
+ const isMobileLayout = this.context.isMobileLayout;
171
321
  return (
172
322
  <FormComponent model={this}>
173
323
  <div
174
324
  style={{
175
325
  minHeight: 0,
176
326
  overflowY: 'auto',
177
- maxHeight: QUICK_EDIT_FORM_MAX_HEIGHT,
327
+ maxHeight: isMobileLayout ? QUICK_EDIT_MOBILE_FORM_MAX_HEIGHT : QUICK_EDIT_FORM_MAX_HEIGHT,
328
+ padding: isMobileLayout ? QUICK_EDIT_MOBILE_CONTENT_PADDING : undefined,
178
329
  }}
179
330
  >
180
331
  {this.mapSubModels('fields', (field) => {
@@ -191,7 +342,14 @@ export class QuickEditFormModel extends FlowModel {
191
342
  );
192
343
  })}
193
344
  </div>
194
- <Space style={{ display: 'flex', justifyContent: 'flex-end', flexShrink: 0 }}>
345
+ <Space
346
+ style={{
347
+ display: 'flex',
348
+ justifyContent: 'flex-end',
349
+ flexShrink: 0,
350
+ padding: isMobileLayout ? QUICK_EDIT_MOBILE_ACTIONS_PADDING : undefined,
351
+ }}
352
+ >
195
353
  <Button
196
354
  onClick={() => {
197
355
  this.viewContainer.close();