@nocobase/client-v2 2.2.0-beta.6 → 2.2.0-beta.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.
@@ -57,12 +57,11 @@ function getItemLabel(itemModel: any, t: (key: string) => string): string {
57
57
  return '';
58
58
  }
59
59
 
60
- function getLastPathSegment(path: string): string {
61
- const segs = String(path || '')
60
+ function splitFieldPath(path: string): string[] {
61
+ return String(path || '')
62
62
  .split('.')
63
63
  .map((s) => s.trim())
64
64
  .filter(Boolean);
65
- return segs.length ? segs[segs.length - 1] : '';
66
65
  }
67
66
 
68
67
  type CollectionLike = {
@@ -70,6 +69,15 @@ type CollectionLike = {
70
69
  getFields?: () => unknown[];
71
70
  };
72
71
 
72
+ type CollectionFieldLike = {
73
+ name?: unknown;
74
+ title?: unknown;
75
+ interface?: unknown;
76
+ target?: unknown;
77
+ isAssociationField?: () => boolean;
78
+ targetCollection?: CollectionLike | null;
79
+ };
80
+
73
81
  function getCollectionFromFormBlockModel(model: any): CollectionLike | null {
74
82
  if (!model || typeof model !== 'object') return null;
75
83
  const collection = (model as any)?.collection || (model as any)?.context?.collection;
@@ -89,14 +97,7 @@ export function buildFieldAssignCascaderOptionsFromCollection(
89
97
  const out: FieldAssignCascaderOption[] = [];
90
98
  for (const rawField of fields) {
91
99
  if (!rawField) continue;
92
- const f = rawField as {
93
- name?: unknown;
94
- title?: unknown;
95
- interface?: unknown;
96
- target?: unknown;
97
- isAssociationField?: () => boolean;
98
- targetCollection?: any;
99
- };
100
+ const f = rawField as CollectionFieldLike;
100
101
  const fieldInterface = typeof f.interface === 'string' ? f.interface : undefined;
101
102
  if (!fieldInterface) continue;
102
103
  if (fieldInterface === 'formula') continue;
@@ -123,23 +124,143 @@ export function buildFieldAssignCascaderOptionsFromCollection(
123
124
  function mergeRootOptions(
124
125
  configured: FieldAssignCascaderOption[],
125
126
  allFields: FieldAssignCascaderOption[],
127
+ ): FieldAssignCascaderOption[] {
128
+ return mergeCascaderOptions(configured, allFields);
129
+ }
130
+
131
+ function mergeCascaderOptions(
132
+ configured: FieldAssignCascaderOption[],
133
+ allFields: FieldAssignCascaderOption[],
126
134
  ): FieldAssignCascaderOption[] {
127
135
  const configuredList = Array.isArray(configured) ? configured : [];
128
136
  const allList = Array.isArray(allFields) ? allFields : [];
137
+ const result = configuredList.map((item) => cloneCascaderOption(item));
129
138
 
130
- const configuredValues = new Set<string>();
131
- for (const it of configuredList) {
132
- const v = it?.value ? String(it.value) : '';
133
- if (v) configuredValues.add(v);
139
+ for (const item of allList) {
140
+ mergeCascaderOptionInto(result, item);
134
141
  }
135
142
 
136
- const extra = allList.filter((it) => {
137
- const v = it?.value ? String(it.value) : '';
138
- if (!v) return false;
139
- return !configuredValues.has(v);
140
- });
143
+ return result;
144
+ }
145
+
146
+ function cloneCascaderOption(option: FieldAssignCascaderOption): FieldAssignCascaderOption {
147
+ return {
148
+ ...option,
149
+ children: option.children?.map((child) => cloneCascaderOption(child)),
150
+ };
151
+ }
152
+
153
+ function mergeCascaderOptionInto(options: FieldAssignCascaderOption[], source: FieldAssignCascaderOption) {
154
+ const value = source?.value ? String(source.value) : '';
155
+ if (!value) return;
156
+
157
+ const existing = options.find((item) => String(item?.value || '') === value);
158
+ if (!existing) {
159
+ options.push(cloneCascaderOption(source));
160
+ return;
161
+ }
162
+
163
+ if (!existing.label && source.label) {
164
+ existing.label = source.label;
165
+ }
166
+ if (source.isLeaf === false || existing.children?.length || source.children?.length) {
167
+ existing.isLeaf = false;
168
+ }
169
+ if (source.loading) {
170
+ existing.loading = source.loading;
171
+ }
172
+ if (source.children?.length) {
173
+ existing.children = mergeCascaderOptions(existing.children || [], source.children);
174
+ }
175
+ }
176
+
177
+ function getCollectionField(collection: CollectionLike | null, name: string): CollectionFieldLike | undefined {
178
+ const field = typeof collection?.getField === 'function' ? collection.getField(name) : undefined;
179
+ return field && typeof field === 'object' ? (field as CollectionFieldLike) : undefined;
180
+ }
181
+
182
+ function isAssociationFieldLike(field?: CollectionFieldLike | null) {
183
+ return !!(field?.isAssociationField?.() || field?.target || field?.targetCollection);
184
+ }
185
+
186
+ function getFieldLabelFromCollection(collection: CollectionLike | null, name: string, t: (key: string) => string) {
187
+ const field = getCollectionField(collection, name);
188
+ const title = typeof field?.title === 'string' && field.title ? field.title : name;
189
+ return t(title);
190
+ }
191
+
192
+ function getAssociationDepthFromFieldPath(
193
+ rootCollection: CollectionLike | null,
194
+ segments: string[],
195
+ ): number | undefined {
196
+ if (!rootCollection) return undefined;
197
+
198
+ let collection: CollectionLike | null = rootCollection;
199
+ let associationDepth = 0;
200
+ for (let index = 0; index < segments.length; index++) {
201
+ const field = getCollectionField(collection, segments[index]);
202
+ if (!field) return undefined;
203
+
204
+ if (isAssociationFieldLike(field)) {
205
+ associationDepth += 1;
206
+ collection = field.targetCollection || null;
207
+ continue;
208
+ }
209
+
210
+ if (index < segments.length - 1) return undefined;
211
+ }
212
+
213
+ return associationDepth;
214
+ }
215
+
216
+ function shouldIncludeConfiguredFieldPath(options: {
217
+ segments: string[];
218
+ rootCollection: CollectionLike | null;
219
+ leafField?: CollectionFieldLike | null;
220
+ maxAssociationFieldDepth: number;
221
+ }) {
222
+ const resolvedAssociationDepth = getAssociationDepthFromFieldPath(options.rootCollection, options.segments);
223
+ if (typeof resolvedAssociationDepth === 'number') {
224
+ return resolvedAssociationDepth <= options.maxAssociationFieldDepth;
225
+ }
226
+
227
+ const leafIsAssociation = isAssociationFieldLike(options.leafField);
228
+ const maxPathLength = leafIsAssociation ? options.maxAssociationFieldDepth : options.maxAssociationFieldDepth + 1;
229
+ return options.segments.length <= maxPathLength;
230
+ }
231
+
232
+ function buildNestedOptionFromFieldPath(options: {
233
+ targetPath: string;
234
+ leaf: FieldAssignCascaderOption;
235
+ rootCollection: CollectionLike | null;
236
+ t: (key: string) => string;
237
+ }): FieldAssignCascaderOption {
238
+ const segments = splitFieldPath(options.targetPath);
239
+ if (segments.length <= 1) {
240
+ return options.leaf;
241
+ }
242
+
243
+ const build = (index: number, collection: CollectionLike | null): FieldAssignCascaderOption => {
244
+ const value = segments[index];
245
+ const isLast = index === segments.length - 1;
246
+ if (isLast) {
247
+ return {
248
+ ...options.leaf,
249
+ value,
250
+ };
251
+ }
252
+
253
+ const field = getCollectionField(collection, value);
254
+ const nextCollection = field?.targetCollection || null;
255
+ return {
256
+ label: getFieldLabelFromCollection(collection, value, options.t),
257
+ value,
258
+ isLeaf: false,
259
+ children: [build(index + 1, nextCollection)],
260
+ };
261
+ };
141
262
 
142
- return [...configuredList, ...extra];
263
+ return build(0, options.rootCollection);
143
264
  }
144
265
 
145
266
  export function collectFieldAssignCascaderOptions(options: {
@@ -167,7 +288,8 @@ export function collectFieldAssignCascaderOptions(options: {
167
288
  const targetPath = getItemFieldPath(item);
168
289
  if (!targetPath) continue;
169
290
 
170
- const seg = getLastPathSegment(targetPath);
291
+ const segments = splitFieldPath(targetPath);
292
+ const seg = segments[segments.length - 1];
171
293
  if (!seg) continue;
172
294
 
173
295
  const node: FieldAssignCascaderOption = {
@@ -179,10 +301,16 @@ export function collectFieldAssignCascaderOptions(options: {
179
301
  const childItems = fieldModel?.subModels?.grid?.subModels?.items;
180
302
  const cf: (CollectionField & { target?: unknown; targetCollection?: unknown }) | undefined =
181
303
  fieldModel?.context?.collectionField;
182
- const isAssociation = !!(cf?.isAssociationField?.() || cf?.target || cf?.targetCollection);
304
+ const isAssociation = isAssociationFieldLike(cf);
183
305
  const hasTargetCollection = !!cf?.targetCollection;
184
- const associationDepth = isAssociation ? targetPath.split('.').filter(Boolean).length : 0;
185
- if (isAssociation && associationDepth > maxAssociationFieldDepth) {
306
+ if (
307
+ !shouldIncludeConfiguredFieldPath({
308
+ segments,
309
+ rootCollection,
310
+ leafField: cf,
311
+ maxAssociationFieldDepth,
312
+ })
313
+ ) {
186
314
  continue;
187
315
  }
188
316
 
@@ -203,7 +331,7 @@ export function collectFieldAssignCascaderOptions(options: {
203
331
  node.isLeaf = true;
204
332
  }
205
333
  }
206
- out.push(node);
334
+ mergeCascaderOptionInto(out, buildNestedOptionFromFieldPath({ targetPath, leaf: node, rootCollection, t }));
207
335
  continue;
208
336
  }
209
337
 
@@ -215,7 +343,7 @@ export function collectFieldAssignCascaderOptions(options: {
215
343
  node.isLeaf = true;
216
344
  }
217
345
 
218
- out.push(node);
346
+ mergeCascaderOptionInto(out, buildNestedOptionFromFieldPath({ targetPath, leaf: node, rootCollection, t }));
219
347
  }
220
348
 
221
349
  return out;
@@ -355,7 +355,6 @@ export class NocoBaseBuildInPlugin extends Plugin<any, Application> {
355
355
  title: this.app.i18n.t('System settings'),
356
356
  icon: 'SettingOutlined',
357
357
  aclSnippet: 'pm.system-settings.system-settings',
358
- sort: -100,
359
358
  });
360
359
  this.app.pluginSettingsManager.addPageTabItem({
361
360
  menuKey: 'system-settings',
@@ -363,7 +362,6 @@ export class NocoBaseBuildInPlugin extends Plugin<any, Application> {
363
362
  title: this.app.i18n.t('System settings'),
364
363
  componentLoader: () => import('../settings-center/SystemSettingsPage'),
365
364
  aclSnippet: 'pm.system-settings.system-settings',
366
- sort: -100,
367
365
  });
368
366
  // Parent menu for security-related plugin settings (password policy, locked users, etc.). Registered here in the buildin plugin so any pro plugin can attach page tabs to `menuKey: 'security'` without each one re-registering the same parent.
369
367
  this.app.pluginSettingsManager.addMenuItem({
@@ -221,7 +221,6 @@ export const SystemSettingsPage = () => {
221
221
  return (
222
222
  <div
223
223
  style={{
224
- minHeight: '100%',
225
224
  background: token.colorBgContainer,
226
225
  borderRadius: token.borderRadiusLG,
227
226
  padding: token.paddingLG,
@@ -116,7 +116,7 @@ export const PluginCard: FC<PluginCardProps> = ({ data }) => {
116
116
  });
117
117
 
118
118
  const openSettings = useMemoizedFn(() => {
119
- navigate(app.pluginSettingsManager.getRoutePath(name));
119
+ navigate(app.pluginSettingsManager.getPluginSettingsRoutePath(name));
120
120
  });
121
121
 
122
122
  const cardClassName = useMemo(
@@ -180,7 +180,7 @@ export const PluginCard: FC<PluginCardProps> = ({ data }) => {
180
180
  <ReadOutlined /> {t('Docs')}
181
181
  </a>
182
182
  )}
183
- {enabled && app.pluginSettingsManager.has(name) && (
183
+ {enabled && app.pluginSettingsManager.hasPluginSettings(name) && (
184
184
  <a
185
185
  onClick={(e) => {
186
186
  e.stopPropagation();
@@ -85,12 +85,6 @@ export function filterVisibleSettings(settings: readonly PluginSettingsPageType[
85
85
  */
86
86
  export function sortTopLevelSettings(settings: PluginSettingsPageType[] = []) {
87
87
  return [...settings].sort((a, b) => {
88
- if (a.name === SYSTEM_SETTINGS_SETTING_NAME && b.name !== SYSTEM_SETTINGS_SETTING_NAME) {
89
- return -1;
90
- }
91
- if (b.name === SYSTEM_SETTINGS_SETTING_NAME && a.name !== SYSTEM_SETTINGS_SETTING_NAME) {
92
- return 1;
93
- }
94
88
  if ((a.sort || 0) !== (b.sort || 0)) {
95
89
  return (a.sort || 0) - (b.sort || 0);
96
90
  }