@nocobase/client-v2 2.2.0-beta.2 → 2.2.0-beta.5
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/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +2 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +6 -0
- package/es/flow/utils/dataScopeRowSnapshot.d.ts +59 -0
- package/es/flow/utils/dateTimeDisplayProps.d.ts +49 -0
- package/es/flow/utils/formValueDeps.d.ts +32 -0
- package/es/index.mjs +113 -93
- package/lib/index.js +104 -84
- package/package.json +7 -7
- package/src/components/AppComponents.tsx +19 -3
- package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +1022 -0
- package/src/flow/actions/__tests__/subFormFieldLinkageRules.inputArgs.test.ts +2 -2
- package/src/flow/actions/dateTimeFormat.tsx +42 -28
- package/src/flow/actions/linkageRules.tsx +1 -1
- package/src/flow/actions/linkageRulesFormValueRefresh.ts +22 -130
- package/src/flow/admin-shell/admin-layout/HelpLite.tsx +1 -3
- package/src/flow/components/DynamicFlowsIcon.tsx +447 -126
- package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +148 -2
- package/src/flow/flows/editMarkdownFlow.tsx +1 -1
- package/src/flow/index.ts +1 -1
- package/src/flow/internal/utils/operatorSchemaHelper.ts +15 -1
- package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +56 -1
- package/src/flow/models/blocks/table/TableColumnModel.tsx +36 -3
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +159 -1
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +16 -4
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +141 -19
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +197 -0
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +23 -0
- package/src/flow/models/fields/ClickableFieldModel.tsx +5 -0
- package/src/flow/models/fields/DisplayDateTimeFieldModel.tsx +29 -1
- package/src/flow/models/fields/InputFieldModel.tsx +48 -2
- package/src/flow/models/fields/SelectFieldModel.tsx +6 -4
- package/src/flow/models/fields/TextareaFieldModel.tsx +49 -3
- package/src/flow/models/fields/__tests__/DisplayDateTimeFieldModel.test.tsx +96 -0
- package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +100 -1
- package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +43 -0
- package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +100 -0
- package/src/flow/utils/__tests__/dateTimeFormat.test.ts +258 -0
- package/src/flow/utils/dataScopeFormValueClear.ts +218 -81
- package/src/flow/utils/dataScopeRowSnapshot.ts +616 -0
- package/src/flow/utils/dateTimeDisplayProps.ts +135 -0
- package/src/flow/utils/formValueDeps.ts +170 -0
- package/src/settings-center/AdminSettingsLayout.tsx +7 -2
|
@@ -13,7 +13,7 @@ import { subFormFieldLinkageRules } from '../linkageRules';
|
|
|
13
13
|
import { SubFormFieldModel, SubFormListFieldModel } from '../../models/fields/AssociationFieldModel/SubFormFieldModel';
|
|
14
14
|
|
|
15
15
|
describe('subFormFieldLinkageRules action', () => {
|
|
16
|
-
it('passes inputArgs to fork runtime context', async () => {
|
|
16
|
+
it('passes inputArgs to fork runtime context when grid forks are stored in a Set', async () => {
|
|
17
17
|
const linkageAssignHandler = vi.fn();
|
|
18
18
|
const engine = new FlowEngine();
|
|
19
19
|
const forkModel = new FlowModel({ uid: 'fork-model', flowEngine: engine }) as any;
|
|
@@ -38,7 +38,7 @@ describe('subFormFieldLinkageRules action', () => {
|
|
|
38
38
|
hidden: false,
|
|
39
39
|
subModels: {
|
|
40
40
|
grid: {
|
|
41
|
-
forks: [forkModel],
|
|
41
|
+
forks: new Set([forkModel]),
|
|
42
42
|
},
|
|
43
43
|
},
|
|
44
44
|
},
|
|
@@ -11,13 +11,36 @@ import { css } from '@emotion/css';
|
|
|
11
11
|
import { defineAction, tExpr } from '@nocobase/flow-engine';
|
|
12
12
|
import { getPickerFormat } from '@nocobase/utils/client';
|
|
13
13
|
import { DateFormatCom, ExpiresRadio } from '../components';
|
|
14
|
+
import {
|
|
15
|
+
getDateTimeFormatCollectionField,
|
|
16
|
+
isDateOnlyCollectionField,
|
|
17
|
+
isTimeCollectionField,
|
|
18
|
+
resolveDateTimeDisplayProps,
|
|
19
|
+
} from '../utils/dateTimeDisplayProps';
|
|
20
|
+
|
|
21
|
+
const isTableColumnFieldSubModel = (model) => {
|
|
22
|
+
const parent = model?.parent;
|
|
23
|
+
return (
|
|
24
|
+
parent?.subModels?.field === model &&
|
|
25
|
+
(parent?.use === 'TableColumnModel' || parent?.constructor?.name === 'TableColumnModel')
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const syncTableColumnDateTimeFormatProps = (ctx, props) => {
|
|
30
|
+
const model = ctx.model;
|
|
31
|
+
if (!isTableColumnFieldSubModel(model) || !model?.parent?.collectionField?.isAssociationField?.()) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
model.parent.setProps(props);
|
|
36
|
+
};
|
|
14
37
|
|
|
15
38
|
export const dateTimeFormat = defineAction({
|
|
16
39
|
title: tExpr('Date display format'),
|
|
17
40
|
name: 'dateDisplayFormat',
|
|
18
41
|
uiSchema: (ctx) => {
|
|
19
|
-
const
|
|
20
|
-
const isTimeField = collectionField
|
|
42
|
+
const collectionField = getDateTimeFormatCollectionField({ model: ctx.model });
|
|
43
|
+
const isTimeField = isTimeCollectionField(collectionField);
|
|
21
44
|
const timeFormatField = {
|
|
22
45
|
type: 'string',
|
|
23
46
|
title: '{{t("Time format")}}',
|
|
@@ -43,7 +66,7 @@ export const dateTimeFormat = defineAction({
|
|
|
43
66
|
(field) => {
|
|
44
67
|
if (!isTimeField) {
|
|
45
68
|
const { showTime, picker } = field.form.values || {};
|
|
46
|
-
field.hidden = !showTime || picker !== 'date';
|
|
69
|
+
field.hidden = isDateOnlyCollectionField(collectionField) || !showTime || picker !== 'date';
|
|
47
70
|
}
|
|
48
71
|
},
|
|
49
72
|
],
|
|
@@ -146,10 +169,11 @@ export const dateTimeFormat = defineAction({
|
|
|
146
169
|
},
|
|
147
170
|
},
|
|
148
171
|
(field) => {
|
|
149
|
-
const
|
|
172
|
+
const collectionField = getDateTimeFormatCollectionField({ model: ctx.model });
|
|
150
173
|
const { picker } = field.form.values || {};
|
|
151
|
-
|
|
152
|
-
|
|
174
|
+
const isDateOnlyField = isDateOnlyCollectionField(collectionField);
|
|
175
|
+
field.hidden = isDateOnlyField || picker !== 'date';
|
|
176
|
+
if (isDateOnlyField || picker !== 'date') {
|
|
153
177
|
field.value = false;
|
|
154
178
|
}
|
|
155
179
|
},
|
|
@@ -159,34 +183,24 @@ export const dateTimeFormat = defineAction({
|
|
|
159
183
|
};
|
|
160
184
|
},
|
|
161
185
|
defaultParams: (ctx: any) => {
|
|
162
|
-
const { showTime, dateFormat,
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
};
|
|
166
|
-
const collectionField = ctx.model.context.collectionField;
|
|
167
|
-
const isTimeField = collectionField.type === 'time' || collectionField.interface === 'time';
|
|
186
|
+
const { showTime, dateFormat, timeFormat, picker } = resolveDateTimeDisplayProps({
|
|
187
|
+
model: ctx.model,
|
|
188
|
+
withDefaults: true,
|
|
189
|
+
});
|
|
168
190
|
return {
|
|
169
191
|
picker: picker || 'date',
|
|
170
192
|
dateFormat: dateFormat || 'YYYY-MM-DD',
|
|
171
|
-
timeFormat: timeFormat ||
|
|
193
|
+
timeFormat: timeFormat || 'HH:mm:ss',
|
|
172
194
|
showTime,
|
|
173
195
|
};
|
|
174
196
|
},
|
|
197
|
+
async beforeParamsSave(ctx: any, params) {
|
|
198
|
+
const props = resolveDateTimeDisplayProps({ model: ctx.model, params });
|
|
199
|
+
ctx.model.setProps(props);
|
|
200
|
+
syncTableColumnDateTimeFormatProps(ctx, props);
|
|
201
|
+
await ctx.model.save?.();
|
|
202
|
+
},
|
|
175
203
|
handler(ctx: any, params) {
|
|
176
|
-
|
|
177
|
-
const isTimeField = collectionField.type === 'time' || collectionField.interface === 'time';
|
|
178
|
-
if (isTimeField) {
|
|
179
|
-
const timeFormat = params?.timeFormat || params?.format || 'HH:mm:ss';
|
|
180
|
-
ctx.model.setProps({
|
|
181
|
-
...params,
|
|
182
|
-
timeFormat,
|
|
183
|
-
format: timeFormat,
|
|
184
|
-
});
|
|
185
|
-
} else {
|
|
186
|
-
ctx.model.setProps({
|
|
187
|
-
...params,
|
|
188
|
-
format: params?.showTime ? `${params.dateFormat} ${params.timeFormat}` : params.dateFormat,
|
|
189
|
-
});
|
|
190
|
-
}
|
|
204
|
+
ctx.model.setProps(resolveDateTimeDisplayProps({ model: ctx.model, params }));
|
|
191
205
|
},
|
|
192
206
|
});
|
|
@@ -2842,7 +2842,7 @@ export const subFormFieldLinkageRules = defineAction({
|
|
|
2842
2842
|
}
|
|
2843
2843
|
} else {
|
|
2844
2844
|
await Promise.all(
|
|
2845
|
-
(grid.forks || []).map(async (forkModel: FlowModel) => {
|
|
2845
|
+
Array.from(grid.forks || []).map(async (forkModel: FlowModel) => {
|
|
2846
2846
|
if (forkModel.hidden) {
|
|
2847
2847
|
return;
|
|
2848
2848
|
}
|
|
@@ -15,17 +15,29 @@ import {
|
|
|
15
15
|
isRunJSValue,
|
|
16
16
|
} from '@nocobase/flow-engine';
|
|
17
17
|
import _ from 'lodash';
|
|
18
|
-
import { namePathToPathKey
|
|
18
|
+
import { namePathToPathKey } from '../models/blocks/form/value-runtime/path';
|
|
19
19
|
import {
|
|
20
20
|
collectStaticDepsFromRunJSValue,
|
|
21
21
|
collectStaticDepsFromTemplateValue,
|
|
22
22
|
recordDep,
|
|
23
23
|
type DepCollector,
|
|
24
24
|
} from '../models/blocks/form/value-runtime/deps';
|
|
25
|
+
import {
|
|
26
|
+
buildItemListRootPath,
|
|
27
|
+
buildItemRowPath,
|
|
28
|
+
dedupeNamePaths,
|
|
29
|
+
findFormValueChangeSource,
|
|
30
|
+
getChangedPathsFromPayload,
|
|
31
|
+
getFieldIndexEntriesFromContext,
|
|
32
|
+
isNamePathPrefix,
|
|
33
|
+
isSameNamePath,
|
|
34
|
+
minimizeNamePaths,
|
|
35
|
+
parseDependencyPath,
|
|
36
|
+
parsePathKey,
|
|
37
|
+
type NamePath,
|
|
38
|
+
} from '../utils/formValueDeps';
|
|
25
39
|
import { linkageRulesRefresh } from './linkageRulesRefresh';
|
|
26
40
|
|
|
27
|
-
type NamePath = Array<string | number>;
|
|
28
|
-
|
|
29
41
|
type LinkageRefreshDeps = {
|
|
30
42
|
wildcard: boolean;
|
|
31
43
|
valuePaths: NamePath[];
|
|
@@ -40,75 +52,11 @@ type LinkageRefreshBinding = {
|
|
|
40
52
|
dispose: () => void;
|
|
41
53
|
};
|
|
42
54
|
|
|
43
|
-
type FieldIndexEntry = {
|
|
44
|
-
name: string;
|
|
45
|
-
index: number;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
55
|
const FORM_VALUES_CHANGE_EVENT = 'formValuesChange';
|
|
49
56
|
const LINKAGE_REFRESH_BINDINGS_KEY = '__formValueDrivenLinkageRefreshBindings';
|
|
50
57
|
|
|
51
|
-
function isSameNamePath(a: NamePath, b: NamePath) {
|
|
52
|
-
return a.length === b.length && a.every((seg, index) => seg === b[index]);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function isNamePathPrefix(prefix: NamePath, path: NamePath) {
|
|
56
|
-
if (prefix.length > path.length) return false;
|
|
57
|
-
return prefix.every((seg, index) => seg === path[index]);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function dedupeNamePaths(paths: NamePath[]) {
|
|
61
|
-
const byKey = new Map<string, NamePath>();
|
|
62
|
-
for (const path of paths) {
|
|
63
|
-
if (!path?.length) continue;
|
|
64
|
-
byKey.set(namePathToPathKey(path), path);
|
|
65
|
-
}
|
|
66
|
-
return Array.from(byKey.values());
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function minimizeValueNamePaths(paths: NamePath[]) {
|
|
70
|
-
const deduped = dedupeNamePaths(paths);
|
|
71
|
-
return deduped.filter((path, index) => {
|
|
72
|
-
return !deduped.some((other, otherIndex) => otherIndex !== index && isNamePathPrefix(path, other));
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function parseFieldIndexEntries(fieldIndex: unknown): FieldIndexEntry[] {
|
|
77
|
-
const arr = Array.isArray(fieldIndex) ? fieldIndex : [];
|
|
78
|
-
const entries: FieldIndexEntry[] = [];
|
|
79
|
-
for (const it of arr) {
|
|
80
|
-
if (typeof it !== 'string') continue;
|
|
81
|
-
const [name, indexStr] = it.split(':');
|
|
82
|
-
const index = Number(indexStr);
|
|
83
|
-
if (!name || Number.isNaN(index)) continue;
|
|
84
|
-
entries.push({ name, index });
|
|
85
|
-
}
|
|
86
|
-
return entries;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function getFieldIndexEntriesFromContext(ctx: any): FieldIndexEntry[] {
|
|
90
|
-
return parseFieldIndexEntries(ctx?.model?.context?.fieldIndex ?? ctx?.fieldIndex);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function buildItemRowPath(entries: FieldIndexEntry[], parentDepth: number): NamePath | null {
|
|
94
|
-
const targetIndex = entries.length - 1 - parentDepth;
|
|
95
|
-
if (targetIndex < 0) return null;
|
|
96
|
-
|
|
97
|
-
const out: NamePath = [];
|
|
98
|
-
for (let i = 0; i <= targetIndex; i++) {
|
|
99
|
-
out.push(entries[i].name, entries[i].index);
|
|
100
|
-
}
|
|
101
|
-
return out;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function buildItemListRootPath(entries: FieldIndexEntry[], parentDepth: number): NamePath | null {
|
|
105
|
-
const rowPath = buildItemRowPath(entries, parentDepth);
|
|
106
|
-
if (!rowPath?.length) return null;
|
|
107
|
-
return rowPath.slice(0, -1);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
58
|
function resolveItemDependencyPath(ctx: FlowContext, depPath: NamePath): LinkageRefreshDeps {
|
|
111
|
-
const entries = getFieldIndexEntriesFromContext(ctx
|
|
59
|
+
const entries = getFieldIndexEntriesFromContext(ctx);
|
|
112
60
|
if (!entries.length) {
|
|
113
61
|
return { wildcard: true, valuePaths: [], structuralPaths: [] };
|
|
114
62
|
}
|
|
@@ -171,8 +119,7 @@ function addRunjsUsageToCollector(script: string, collector: DepCollector) {
|
|
|
171
119
|
collector.wildcard = true;
|
|
172
120
|
continue;
|
|
173
121
|
}
|
|
174
|
-
|
|
175
|
-
recordDep(segs, collector);
|
|
122
|
+
recordDep(parseDependencyPath(String(subPath)), collector);
|
|
176
123
|
continue;
|
|
177
124
|
}
|
|
178
125
|
if (varName === 'item') {
|
|
@@ -233,13 +180,13 @@ function collectLinkageRefreshDeps(ctx: FlowContext, params: any): LinkageRefres
|
|
|
233
180
|
wildcard = true;
|
|
234
181
|
continue;
|
|
235
182
|
}
|
|
236
|
-
valuePaths.push(
|
|
183
|
+
valuePaths.push(parsePathKey(inner));
|
|
237
184
|
continue;
|
|
238
185
|
}
|
|
239
186
|
|
|
240
187
|
if (depKey === 'ctx:item' || depKey.startsWith('ctx:item:')) {
|
|
241
188
|
const subPath = depKey === 'ctx:item' ? '' : depKey.slice('ctx:item:'.length);
|
|
242
|
-
const depPath = subPath ? (
|
|
189
|
+
const depPath = subPath ? parseDependencyPath(subPath) : [];
|
|
243
190
|
const resolved = resolveItemDependencyPath(ctx, depPath);
|
|
244
191
|
wildcard ||= resolved.wildcard;
|
|
245
192
|
valuePaths.push(...resolved.valuePaths);
|
|
@@ -249,7 +196,7 @@ function collectLinkageRefreshDeps(ctx: FlowContext, params: any): LinkageRefres
|
|
|
249
196
|
|
|
250
197
|
return {
|
|
251
198
|
wildcard,
|
|
252
|
-
valuePaths:
|
|
199
|
+
valuePaths: minimizeNamePaths(valuePaths),
|
|
253
200
|
structuralPaths: dedupeNamePaths(structuralPaths),
|
|
254
201
|
};
|
|
255
202
|
}
|
|
@@ -258,44 +205,9 @@ function hasLinkageRefreshDeps(deps: LinkageRefreshDeps) {
|
|
|
258
205
|
return deps.wildcard || deps.valuePaths.length > 0 || deps.structuralPaths.length > 0;
|
|
259
206
|
}
|
|
260
207
|
|
|
261
|
-
function getChangedPathsFromPayload(payload: any): NamePath[] {
|
|
262
|
-
const rawChangedPaths = Array.isArray(payload?.changedPaths) ? payload.changedPaths : [];
|
|
263
|
-
const out: NamePath[] = [];
|
|
264
|
-
|
|
265
|
-
for (const path of rawChangedPaths) {
|
|
266
|
-
if (Array.isArray(path)) {
|
|
267
|
-
if (path.length === 1 && typeof path[0] === 'string') {
|
|
268
|
-
const namePath = pathKeyToNamePath(path[0]);
|
|
269
|
-
if (namePath.length) out.push(namePath);
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
const segs = path.filter((seg) => typeof seg === 'string' || typeof seg === 'number') as NamePath;
|
|
273
|
-
if (segs.length) out.push(segs);
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
if (typeof path === 'string' && path) {
|
|
277
|
-
out.push(pathKeyToNamePath(path));
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (out.length) {
|
|
282
|
-
return out;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const changedValues = payload?.changedValues;
|
|
286
|
-
if (changedValues && typeof changedValues === 'object') {
|
|
287
|
-
for (const key of Object.keys(changedValues)) {
|
|
288
|
-
const namePath = pathKeyToNamePath(key);
|
|
289
|
-
if (namePath.length) out.push(namePath);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
return out;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
208
|
function linkageRefreshDepsMatchPayload(deps: LinkageRefreshDeps, payload: any) {
|
|
297
209
|
if (!hasLinkageRefreshDeps(deps)) return false;
|
|
298
|
-
const changedPaths = getChangedPathsFromPayload(payload);
|
|
210
|
+
const changedPaths = getChangedPathsFromPayload(payload, { includeArrayChangedValues: true });
|
|
299
211
|
if (deps.wildcard) return true;
|
|
300
212
|
if (!changedPaths.length) return true;
|
|
301
213
|
|
|
@@ -330,28 +242,8 @@ function getLinkageRefreshBindings(model: any): Map<string, LinkageRefreshBindin
|
|
|
330
242
|
return (model[LINKAGE_REFRESH_BINDINGS_KEY] ||= new Map<string, LinkageRefreshBinding>());
|
|
331
243
|
}
|
|
332
244
|
|
|
333
|
-
function isFormBlockForLinkageRefresh(model: any) {
|
|
334
|
-
if (!model || typeof model !== 'object') return false;
|
|
335
|
-
if (!model.emitter || typeof model.emitter.on !== 'function' || typeof model.emitter.off !== 'function') return false;
|
|
336
|
-
return !!model.formValueRuntime || !!model.context?.form || typeof model.context?.setFormValues === 'function';
|
|
337
|
-
}
|
|
338
|
-
|
|
339
245
|
function findFormBlockForLinkageRefresh(ctx: FlowContext): any | null {
|
|
340
|
-
|
|
341
|
-
const push = (model: any) => {
|
|
342
|
-
if (model && !candidates.includes(model)) candidates.push(model);
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
push((ctx.model as any)?.context?.blockModel);
|
|
346
|
-
push(ctx.model);
|
|
347
|
-
|
|
348
|
-
let cursor: any = (ctx.model as any)?.parent;
|
|
349
|
-
while (cursor) {
|
|
350
|
-
push(cursor);
|
|
351
|
-
cursor = cursor?.parent;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
return candidates.find(isFormBlockForLinkageRefresh) || null;
|
|
246
|
+
return findFormValueChangeSource(ctx) as any;
|
|
355
247
|
}
|
|
356
248
|
|
|
357
249
|
function disposeLinkageRefreshBinding(model: any, key: string) {
|
|
@@ -70,9 +70,7 @@ const SettingsMenu: React.FC = () => {
|
|
|
70
70
|
key: 'userManual',
|
|
71
71
|
label: (
|
|
72
72
|
<a
|
|
73
|
-
href={
|
|
74
|
-
isSimplifiedChinese ? 'https://v2.docs.nocobase.com/cn/guide/' : 'https://v2.docs.nocobase.com/guide/'
|
|
75
|
-
}
|
|
73
|
+
href={isSimplifiedChinese ? 'https://docs.nocobase.com/cn/guide/' : 'https://docs.nocobase.com/guide/'}
|
|
76
74
|
target="_blank"
|
|
77
75
|
rel="noreferrer"
|
|
78
76
|
>
|