@zat-design/sisyphus-react 4.6.2 → 4.6.3-beta.2

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.
@@ -8,6 +8,7 @@ interface EmptyProps {
8
8
  form?: FormInstance;
9
9
  namePath?: InternalNamePath;
10
10
  emptyBtnText?: string;
11
+ onAdd?: () => unknown | Promise<unknown>;
11
12
  }
12
- declare const Empty: ({ disabled, toolbarProps, operation, form, namePath, emptyBtnText }: EmptyProps) => import("react/jsx-runtime").JSX.Element;
13
+ declare const Empty: ({ disabled, toolbarProps, operation, form, namePath, emptyBtnText, onAdd, }: EmptyProps) => import("react/jsx-runtime").JSX.Element;
13
14
  export default Empty;
@@ -3,12 +3,24 @@ import { Button } from "antd";
3
3
  import isFunction from "lodash/isFunction";
4
4
  import EmptyImg from "../../../../../assets/empty.png";
5
5
  import locale from "../../../../../locale";
6
- const Empty = ({ disabled, toolbarProps, operation, form, namePath, emptyBtnText }) => {
6
+ const Empty = ({
7
+ disabled,
8
+ toolbarProps,
9
+ operation,
10
+ form,
11
+ namePath,
12
+ emptyBtnText,
13
+ onAdd
14
+ }) => {
7
15
  const addConfig = Array.isArray(toolbarProps) ? toolbarProps.find((item) => item.type === "add") : null;
8
16
  const handleAddClick = async () => {
9
17
  var _a;
10
18
  if (!operation || !form || !namePath)
11
19
  return;
20
+ if (onAdd) {
21
+ await onAdd();
22
+ return;
23
+ }
12
24
  const value = form.getFieldValue(namePath);
13
25
  const data = await ((_a = addConfig == null ? void 0 : addConfig.onClick) == null ? void 0 : _a.call(addConfig, value, { operation, form, namePath, index: 0 }));
14
26
  if (!(addConfig == null ? void 0 : addConfig.onClick) || data === true) {
@@ -1,6 +1,6 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
- import { Row, Tabs } from "antd";
3
+ import { Badge, Row, Tabs } from "antd";
4
4
  import { PlusOutlined } from "@ant-design/icons";
5
5
  import { DndContext } from "@dnd-kit/core";
6
6
  import { horizontalListSortingStrategy, SortableContext, useSortable } from "@dnd-kit/sortable";
@@ -11,6 +11,7 @@ import ProIcon from "../../../../../ProIcon";
11
11
  import locale from "../../../../../locale";
12
12
  import RenderFields from "../../../render/RenderFields";
13
13
  import ActionButton from "./ActionButton";
14
+ import Empty from "./Empty";
14
15
  import { memoWith } from "../utils";
15
16
  import {
16
17
  executeToolbarAdd,
@@ -18,7 +19,29 @@ import {
18
19
  isToolbarActionVisible,
19
20
  normalizeToolbarActions
20
21
  } from "../toolbarActions";
22
+ import { subscribeFormListTabsValidation } from "../tabsValidation";
21
23
  const MemoRenderFields = React.memo(RenderFields, memoWith);
24
+ const getTabErrorCounts = (errorFields, fields, namePath) => {
25
+ const uniqueErrors = /* @__PURE__ */ new Map();
26
+ errorFields.forEach((errorField) => {
27
+ var _a;
28
+ if (!((_a = errorField.errors) == null ? void 0 : _a.length) || errorField.name.length <= namePath.length + 1)
29
+ return;
30
+ const matchesList = namePath.every((name, index) => errorField.name[index] === name);
31
+ if (!matchesList)
32
+ return;
33
+ const fieldName = errorField.name[namePath.length];
34
+ const field = fields.find((item) => item.name === fieldName);
35
+ if (!field)
36
+ return;
37
+ const fieldKey = String(field.key);
38
+ const errorPath = JSON.stringify(errorField.name.slice(namePath.length + 1));
39
+ const paths = uniqueErrors.get(fieldKey) || /* @__PURE__ */ new Set();
40
+ paths.add(errorPath);
41
+ uniqueErrors.set(fieldKey, paths);
42
+ });
43
+ return new Map(Array.from(uniqueErrors, ([fieldKey, paths]) => [fieldKey, paths.size]));
44
+ };
22
45
  const SortableTabLabel = ({
23
46
  children,
24
47
  disabled,
@@ -69,6 +92,7 @@ const TabsFields = (props) => {
69
92
  diffConfig,
70
93
  disabled,
71
94
  draggable,
95
+ emptyBtnText,
72
96
  fields,
73
97
  form,
74
98
  isView,
@@ -84,11 +108,17 @@ const TabsFields = (props) => {
84
108
  const fieldKeys = fields.map((field) => String(field.key));
85
109
  const fieldKeysSignature = fieldKeys.join("\0");
86
110
  const [activeKey, setActiveKey] = useState(fieldKeys[0]);
111
+ const [validationErrorFields, setValidationErrorFields] = useState([]);
87
112
  const activeIndexRef = useRef(0);
88
113
  const fieldKeysRef = useRef(fieldKeys);
114
+ const fieldsRef = useRef(fields);
115
+ const namePathRef = useRef(namePath);
89
116
  const pendingAddKeysRef = useRef(null);
90
117
  const validatingRef = useRef(false);
91
118
  fieldKeysRef.current = fieldKeys;
119
+ fieldsRef.current = fields;
120
+ namePathRef.current = namePath;
121
+ const errorCounts = getTabErrorCounts(validationErrorFields, fields, namePath);
92
122
  const currentIndex = activeKey ? fieldKeys.indexOf(activeKey) : -1;
93
123
  if (currentIndex >= 0)
94
124
  activeIndexRef.current = currentIndex;
@@ -106,6 +136,22 @@ const TabsFields = (props) => {
106
136
  const fallbackIndex = Math.min(activeIndexRef.current, fieldKeys.length - 1);
107
137
  setActiveKey(fallbackIndex >= 0 ? fieldKeys[fallbackIndex] : void 0);
108
138
  }, [activeKey, fieldKeysSignature]);
139
+ useEffect(
140
+ () => subscribeFormListTabsValidation(form, (event) => {
141
+ setValidationErrorFields(event.errorFields);
142
+ if (event.reason !== "submitFailed")
143
+ return;
144
+ const counts = getTabErrorCounts(event.errorFields, fieldsRef.current, namePathRef.current);
145
+ const firstErrorIndex = fieldsRef.current.findIndex(
146
+ (field) => counts.has(String(field.key))
147
+ );
148
+ if (firstErrorIndex < 0)
149
+ return;
150
+ activeIndexRef.current = firstErrorIndex;
151
+ setActiveKey(String(fieldsRef.current[firstErrorIndex].key));
152
+ }),
153
+ [form]
154
+ );
109
155
  const validateCurrent = useCallback(async () => {
110
156
  if (!activeKey)
111
157
  return true;
@@ -142,7 +188,8 @@ const TabsFields = (props) => {
142
188
  (item) => item.type === "add" && isToolbarActionVisible(item, form, namePath)
143
189
  );
144
190
  const addLabel = (addAction == null ? void 0 : addAction.label) ?? locale.ProForm.formListTabsAdd;
145
- const hideAdd = !addAction || addAction.disabled || disabled || isView || Boolean(max && max <= fields.length);
191
+ const hasReachedMax = max !== void 0 && max <= fields.length;
192
+ const hideAdd = !addAction || addAction.disabled || disabled || isView || hasReachedMax;
146
193
  const handleAdd = async () => {
147
194
  if (!addAction || hideAdd)
148
195
  return;
@@ -198,30 +245,48 @@ const TabsFields = (props) => {
198
245
  const tabTitle = isFunction(title) ? title(record, index, form) : title;
199
246
  const renderedTitle = tabTitle ?? `Tab ${index + 1}`;
200
247
  const tabActionProps = wrapValidatedActions(actionProps || void 0);
248
+ const errorCount = errorCounts.get(fieldKey) || 0;
201
249
  return {
202
250
  key: fieldKey,
203
251
  closable: false,
204
- label: /* @__PURE__ */ jsxs(SortableTabLabel, { fieldKey, draggable, disabled: disabled || isView, children: [
205
- /* @__PURE__ */ jsx("span", { className: "pro-form-list-tabs-title", children: renderedTitle }),
206
- actionProps !== false && /* @__PURE__ */ jsx(
207
- ActionButton,
208
- {
209
- min,
210
- max,
211
- index,
212
- length: fields.length,
213
- operation,
214
- namePath: itemNamePath,
215
- form,
216
- field: { ...field, key: fieldKey },
217
- fields,
218
- actionProps: tabActionProps,
219
- mode: "tabs",
220
- disabled,
221
- isView
222
- }
223
- )
224
- ] }),
252
+ // 提前注册未访问页签的字段,确保全量提交能定位隐藏卡片错误。
253
+ forceRender: true,
254
+ label: /* @__PURE__ */ jsx(SortableTabLabel, { fieldKey, draggable, disabled: disabled || isView, children: /* @__PURE__ */ jsx(
255
+ Badge,
256
+ {
257
+ className: "pro-form-list-tabs-error-badge",
258
+ count: errorCount,
259
+ overflowCount: 99,
260
+ size: "small",
261
+ children: /* @__PURE__ */ jsxs(
262
+ "span",
263
+ {
264
+ className: `pro-form-list-tabs-title-content${errorCount ? " pro-form-list-tabs-title-content-error" : ""}`,
265
+ children: [
266
+ /* @__PURE__ */ jsx("span", { className: "pro-form-list-tabs-title", children: renderedTitle }),
267
+ actionProps !== false && /* @__PURE__ */ jsx(
268
+ ActionButton,
269
+ {
270
+ min,
271
+ max,
272
+ index,
273
+ length: fields.length,
274
+ operation,
275
+ namePath: itemNamePath,
276
+ form,
277
+ field: { ...field, key: fieldKey },
278
+ fields,
279
+ actionProps: tabActionProps,
280
+ mode: "tabs",
281
+ disabled,
282
+ isView
283
+ }
284
+ )
285
+ ]
286
+ }
287
+ )
288
+ }
289
+ ) }),
225
290
  children: /* @__PURE__ */ jsx("div", { className: "pro-form-list-tabs-content", children: /* @__PURE__ */ jsx(Row, { gutter: 16, className: "pro-form-list-fields", children: /* @__PURE__ */ jsx(
226
291
  MemoRenderFields,
227
292
  {
@@ -235,31 +300,45 @@ const TabsFields = (props) => {
235
300
  ) }) })
236
301
  };
237
302
  });
238
- return /* @__PURE__ */ jsx(DndContext, { onDragEnd: handleDragEnd, children: /* @__PURE__ */ jsx(
303
+ return /* @__PURE__ */ jsx(DndContext, { onDragEnd: handleDragEnd, children: /* @__PURE__ */ jsxs(
239
304
  SortableContext,
240
305
  {
241
306
  items: fieldKeys.map((key) => ({ id: key })),
242
307
  strategy: horizontalListSortingStrategy,
243
- children: /* @__PURE__ */ jsx(
244
- Tabs,
245
- {
246
- className: "pro-form-list-tabs",
247
- type: "editable-card",
248
- size: "small",
249
- activeKey,
250
- addIcon: /* @__PURE__ */ jsxs("span", { className: "pro-form-list-tabs-add", children: [
251
- /* @__PURE__ */ jsx(PlusOutlined, {}),
252
- /* @__PURE__ */ jsx("span", { className: "pro-form-list-tabs-add-label", children: addLabel })
253
- ] }),
254
- hideAdd,
255
- items,
256
- onChange: handleChange,
257
- onEdit: (_, action) => {
258
- if (action === "add")
259
- void handleAdd();
308
+ children: [
309
+ /* @__PURE__ */ jsx(
310
+ Tabs,
311
+ {
312
+ className: "pro-form-list-tabs",
313
+ type: "editable-card",
314
+ size: "small",
315
+ activeKey,
316
+ addIcon: /* @__PURE__ */ jsxs("span", { className: "pro-form-list-tabs-add", children: [
317
+ /* @__PURE__ */ jsx(PlusOutlined, {}),
318
+ /* @__PURE__ */ jsx("span", { className: "pro-form-list-tabs-add-label", children: addLabel })
319
+ ] }),
320
+ hideAdd,
321
+ items,
322
+ onChange: handleChange,
323
+ onEdit: (_, action) => {
324
+ if (action === "add")
325
+ void handleAdd();
326
+ }
260
327
  }
261
- }
262
- )
328
+ ),
329
+ fields.length === 0 && /* @__PURE__ */ jsx(
330
+ Empty,
331
+ {
332
+ disabled: hideAdd,
333
+ toolbarProps: addAction ? [{ ...addAction, show: true }] : false,
334
+ operation,
335
+ form,
336
+ namePath,
337
+ emptyBtnText,
338
+ onAdd: handleAdd
339
+ }
340
+ )
341
+ ]
263
342
  }
264
343
  ) });
265
344
  };
@@ -130,6 +130,17 @@
130
130
  }
131
131
 
132
132
  .pro-form-list-tabs {
133
+ &.@{ant-prefix}-tabs > .@{ant-prefix}-tabs-nav {
134
+ .@{ant-prefix}-tabs-nav-wrap {
135
+ overflow-x: clip;
136
+ overflow-y: visible;
137
+ }
138
+
139
+ .@{ant-prefix}-tabs-nav-list {
140
+ overflow: visible;
141
+ }
142
+ }
143
+
133
144
  &.@{ant-prefix}-tabs-card > .@{ant-prefix}-tabs-nav {
134
145
  &::before {
135
146
  border-bottom-color: var(--ant-color-border, #d9d9d9);
@@ -168,6 +179,31 @@
168
179
  white-space: nowrap;
169
180
  }
170
181
 
182
+ .pro-form-list-tabs-error-badge {
183
+ display: inline-flex;
184
+
185
+ .@{ant-prefix}-badge-count {
186
+ top: 0;
187
+ right: 0;
188
+ z-index: 2;
189
+ pointer-events: none;
190
+ transform: translate(50%, -50%);
191
+ box-shadow: 0 0 0 2px var(--ant-color-bg-container, #fff);
192
+ transition: none;
193
+ animation: none;
194
+
195
+ .@{ant-prefix}-scroll-number-only-unit {
196
+ transition: none;
197
+ }
198
+ }
199
+ }
200
+
201
+ .pro-form-list-tabs-title-content {
202
+ display: inline-flex;
203
+ gap: 4px;
204
+ align-items: center;
205
+ }
206
+
171
207
  .pro-form-list-tabs-drag-handle {
172
208
  display: inline-flex;
173
209
  align-items: center;
@@ -0,0 +1,14 @@
1
+ import type { FormInstance } from 'antd';
2
+ import type { InternalNamePath } from 'antd/es/form/interface';
3
+ export interface FormListTabsErrorField {
4
+ errors?: unknown[];
5
+ name: InternalNamePath;
6
+ }
7
+ export interface FormListTabsValidationEvent {
8
+ errorFields: FormListTabsErrorField[];
9
+ reason: 'fieldsChange' | 'submitFailed' | 'submitSuccess';
10
+ }
11
+ type FormListTabsValidationListener = (event: FormListTabsValidationEvent) => void;
12
+ export declare const subscribeFormListTabsValidation: (form: FormInstance, listener: FormListTabsValidationListener) => () => void;
13
+ export declare const notifyFormListTabsValidation: (form: FormInstance, event: FormListTabsValidationEvent) => void;
14
+ export {};
@@ -0,0 +1,19 @@
1
+ const formListeners = /* @__PURE__ */ new WeakMap();
2
+ const subscribeFormListTabsValidation = (form, listener) => {
3
+ const listeners = formListeners.get(form) || /* @__PURE__ */ new Set();
4
+ listeners.add(listener);
5
+ formListeners.set(form, listeners);
6
+ return () => {
7
+ listeners.delete(listener);
8
+ if (!listeners.size)
9
+ formListeners.delete(form);
10
+ };
11
+ };
12
+ const notifyFormListTabsValidation = (form, event) => {
13
+ var _a;
14
+ (_a = formListeners.get(form)) == null ? void 0 : _a.forEach((listener) => listener(event));
15
+ };
16
+ export {
17
+ notifyFormListTabsValidation,
18
+ subscribeFormListTabsValidation
19
+ };
@@ -29,6 +29,7 @@ import locale from "../locale";
29
29
  import useWatch from "./hooks/useWatch";
30
30
  import FormsProvider, { useContextForms, useForms } from "../FormsProvider";
31
31
  import { useStep } from "../ProStep";
32
+ import { notifyFormListTabsValidation } from "./components/combination/FormList/tabsValidation";
32
33
  const ProForm = (props, ref) => {
33
34
  const {
34
35
  mode = "search",
@@ -144,8 +145,20 @@ const ProForm = (props, ref) => {
144
145
  }
145
146
  };
146
147
  const handleFinish = () => {
148
+ notifyFormListTabsValidation(form, { reason: "submitSuccess", errorFields: [] });
147
149
  onFinish == null ? void 0 : onFinish(form.getFieldsValue());
148
150
  };
151
+ const handleFieldsChange = (changedFields, allFields) => {
152
+ var _a;
153
+ notifyFormListTabsValidation(form, {
154
+ reason: "fieldsChange",
155
+ errorFields: allFields.filter((field) => {
156
+ var _a2;
157
+ return (_a2 = field.errors) == null ? void 0 : _a2.length;
158
+ })
159
+ });
160
+ (_a = otherProps.onFieldsChange) == null ? void 0 : _a.call(otherProps, changedFields, allFields);
161
+ };
149
162
  const cls = classnames({
150
163
  "pro-form": true,
151
164
  "pro-form-view": isView,
@@ -177,7 +190,7 @@ const ProForm = (props, ref) => {
177
190
  otherProps.layout = otherProps.layout || "vertical";
178
191
  }
179
192
  }
180
- const filteredOtherProps = omit(otherProps, ["desensitizationKey"]);
193
+ const filteredOtherProps = omit(otherProps, ["desensitizationKey", "onFieldsChange"]);
181
194
  return /* @__PURE__ */ jsxs(
182
195
  Form,
183
196
  {
@@ -194,10 +207,12 @@ const ProForm = (props, ref) => {
194
207
  ...omit(config, ["isDiffAll"]),
195
208
  ...filteredOtherProps,
196
209
  labelAlign: labelAlign ?? config.labelAlign ?? "left",
210
+ onFieldsChange: handleFieldsChange,
197
211
  onValuesChange: handleValuesChange,
198
212
  onFinish: handleFinish,
199
213
  onFinishFailed: ({ errorFields }) => {
200
214
  var _a;
215
+ notifyFormListTabsValidation(form, { reason: "submitFailed", errorFields });
201
216
  if (scrollToError && (errorFields == null ? void 0 : errorFields.length)) {
202
217
  form.scrollToField((_a = errorFields[0]) == null ? void 0 : _a.name, {
203
218
  block: "center",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zat-design/sisyphus-react",
3
- "version": "4.6.2",
3
+ "version": "4.6.3-beta.2",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public",