@wordrhyme/auto-crud 1.18.0 → 1.19.0
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/README.md +33 -2
- package/dist/index.cjs +227 -47
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.js +226 -46
- package/dist/styles.css +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1948,7 +1948,7 @@ import {
|
|
|
1948
1948
|
createLocalViewStorage,
|
|
1949
1949
|
} from '@wordrhyme/auto-crud';
|
|
1950
1950
|
|
|
1951
|
-
//
|
|
1951
|
+
// 服务对象应保持引用稳定。后端存储实现 ViewStorage 的四个必需方法,可选实现 reorder。
|
|
1952
1952
|
const viewStorage = createLocalViewStorage(`${organizationId}:${userId}`);
|
|
1953
1953
|
|
|
1954
1954
|
<QueryClientProvider client={queryClient}>
|
|
@@ -1984,6 +1984,37 @@ const viewStorage = createLocalViewStorage(`${organizationId}:${userId}`);
|
|
|
1984
1984
|
|
|
1985
1985
|
展示方式与当前选中视图的 ID 自动存入浏览器 `localStorage`,键为 `auto-crud:view-preferences:${encodeURIComponent(scope)}:${encodeURIComponent(resourceId)}`,按用户/租户和资源隔离,独立于 `ViewStorage` 的本地或服务端实现。刷新或重新进入时恢复展示方式;视图列表加载成功后恢复选中项。URL 已有筛选、搜索、排序或分页参数时保留 URL 查询,否则应用上次视图的保存条件,使用 replace 更新 URL。上次视图已删除或字段不可用时清除其选中记忆,保留 URL 查询。本地数据损坏或存储不可用时使用配置的初始展示方式,仍可在当前页面切换。未保存的查询不另存到本地偏好中。
|
|
1986
1986
|
|
|
1987
|
+
### 默认视图、分享链接与排列顺序
|
|
1988
|
+
|
|
1989
|
+
```tsx
|
|
1990
|
+
<AutoCrudTable
|
|
1991
|
+
id="crm.customers"
|
|
1992
|
+
schema={schema}
|
|
1993
|
+
resource={resource}
|
|
1994
|
+
table={{
|
|
1995
|
+
views: { defaultView: 'vip-customers', selector: 'tabs' },
|
|
1996
|
+
// 可选;同页多个列表或字段名与 view 冲突时使用不同的 URL 参数名。
|
|
1997
|
+
queryKeys: { view: 'customerView' },
|
|
1998
|
+
}}
|
|
1999
|
+
/>
|
|
2000
|
+
```
|
|
2001
|
+
|
|
2002
|
+
`defaultView` 接受视图的 slug 或 ID,独立 `AutoTable` 使用 `views={{ defaultView: 'vip-customers' }}`。初始化优先级为 URL 指定、用户本地选中记忆、`defaultView`、全部;本地明确记住“全部”也优先于默认值。默认项不存在时回到全部,不自动选择其他视图。`views` 仍须显式开启,本地偏好与 URL 都不会自动开启视图功能。
|
|
2003
|
+
|
|
2004
|
+
视图可包含可选的 `slug`,例如 `{ id: 'view-123', slug: 'vip-customers', name: '重点客户', query }`。新建时按名称建议 slug,可手动修改;重命名保持原 slug。slug 为 1–80 个文字、数字或连接用的短横线,区分大小写。`ViewStorage.create/update` 须保存并返回该字段,`update` 未传 slug 时保留原值;已有数据无需迁移,没有 slug 时继续使用 ID。同一资源内 ID 和 slug 必须唯一,slug 也不能与另一视图的 ID 相同。服务端须在用户/租户/资源边界内校验唯一性;修改 slug 后旧 slug 链接失效,稳定 ID 仍可定位。
|
|
2005
|
+
|
|
2006
|
+
默认参数为 `?view=vip-customers`,也接受 `?view=view-123`;若配置上例的 `queryKeys.view`,则使用 `?customerView=vip-customers`。切换视图会同步参数并创建浏览器历史记录,前进/后退恢复 URL 指定的视图。有 slug 时写入规范 slug,只有 ID 的旧链接会以 replace 规范化。`?view=` 明确表示“全部”。仅带视图参数的链接会应用保存条件;若同时带筛选、搜索、排序或分页参数,保留完整 URL 查询,便于分享未保存的调整。无权访问、已删除或查询字段失效时显示错误并保留 URL 查询,不回退到另一个视图。链接接收者仍需有权限读取该视图及资源,URL 不授予访问权限。
|
|
2007
|
+
|
|
2008
|
+
下拉和 Tab 都按 `ViewStorage.list()` 的返回顺序展示,“全部”固定在最前。存储提供可选方法后,设置菜单显示“调整顺序”,拖动手柄(也支持键盘)后点击保存;取消或保存失败不会改变显示顺序或当前选中项:
|
|
2009
|
+
|
|
2010
|
+
```ts
|
|
2011
|
+
reorder?: (resourceId: string, ids: string[]) => Promise<void>;
|
|
2012
|
+
```
|
|
2013
|
+
|
|
2014
|
+
`ids` 必须包含该资源的全部视图 ID,顺序即新顺序。服务端应在事务中校验并保存,拒绝重复、未知或缺失 ID;并发新增/删除导致列表不一致时返回错误,让用户刷新后重试。成功后组件重新调用 `list()`。排序不修改视图 ID、slug 或查询条件;服务未实现 `reorder` 时不展示排序入口。`createLocalViewStorage` 已实现本地排序持久化。
|
|
2015
|
+
|
|
2016
|
+
Storybook 的 **Default View** 演示按 slug 默认选中,“视图设置 → 调整顺序”可体验排序;所有 Saved views and tags 示例共用本地视图存储,首次加载补齐演示视图;视图、删除操作和排列顺序刷新后保留。客户标签仍为刷新后重置的内存数据。
|
|
2017
|
+
|
|
1987
2018
|
标签列表调用示例:
|
|
1988
2019
|
|
|
1989
2020
|
```ts
|
|
@@ -2032,7 +2063,7 @@ const loadViewCounts: NonNullable<SavedViewsConfig['counts']> = async ({
|
|
|
2032
2063
|
|
|
2033
2064
|
首次加载显示 `…`,零结果显示 `0`,缺失或无效数量显示 `—`;请求失败显示 `—` 和重试入口。保存视图条件后重新统计;表格数据引用或 `counts` 回调引用变化时也会重新获取。数量由查询缓存管理,不写入视图存储。独立 `AutoTable` 使用 `views={{ selector: 'tabs', counts: loadViewCounts }}`。
|
|
2034
2065
|
|
|
2035
|
-
Storybook 的 **Auto CRUD / Saved views and tags / Tabs With Counts**
|
|
2066
|
+
Storybook 的 **Auto CRUD / Saved views and tags / Tabs With Counts** 与其他视图示例共用本地视图存储,模拟 300ms 统计请求延迟;首次加载预置数量为 3、2、1、0 的视图。列表和数量共用筛选逻辑,修改客户标签后会重新计算。视图与排列顺序刷新后保留,客户标签刷新后重置。
|
|
2036
2067
|
|
|
2037
2068
|
### 视图 Tab 样式
|
|
2038
2069
|
|
package/dist/index.cjs
CHANGED
|
@@ -15,12 +15,12 @@ let clsx = require("clsx");
|
|
|
15
15
|
clsx = require_position.__toESM(clsx);
|
|
16
16
|
let tailwind_merge = require("tailwind-merge");
|
|
17
17
|
tailwind_merge = require_position.__toESM(tailwind_merge);
|
|
18
|
+
let __wordrhyme_shadcn_ui = require("@wordrhyme/shadcn-ui");
|
|
19
|
+
__wordrhyme_shadcn_ui = require_position.__toESM(__wordrhyme_shadcn_ui);
|
|
18
20
|
let nanoid = require("nanoid");
|
|
19
21
|
nanoid = require_position.__toESM(nanoid);
|
|
20
22
|
let __tanstack_react_table = require("@tanstack/react-table");
|
|
21
23
|
__tanstack_react_table = require_position.__toESM(__tanstack_react_table);
|
|
22
|
-
let __wordrhyme_shadcn_ui = require("@wordrhyme/shadcn-ui");
|
|
23
|
-
__wordrhyme_shadcn_ui = require_position.__toESM(__wordrhyme_shadcn_ui);
|
|
24
24
|
let __wordrhyme_ui = require("@wordrhyme/ui");
|
|
25
25
|
__wordrhyme_ui = require_position.__toESM(__wordrhyme_ui);
|
|
26
26
|
let __wordrhyme_formily_shadcn = require("@wordrhyme/formily-shadcn");
|
|
@@ -1445,9 +1445,32 @@ const viewQuerySchema = zod.z.object({
|
|
|
1445
1445
|
});
|
|
1446
1446
|
const viewInputSchema = zod.z.object({
|
|
1447
1447
|
name: zod.z.string().trim().min(1).max(40),
|
|
1448
|
+
slug: zod.z.string().trim().min(1).max(80).regex(/^[\p{L}\p{N}]+(?:-[\p{L}\p{N}]+)*$/u, "Slug 只能包含文字、数字和连接用的短横线").optional(),
|
|
1448
1449
|
query: viewQuerySchema
|
|
1449
1450
|
});
|
|
1450
1451
|
const savedViewSchema = viewInputSchema.extend({ id: zod.z.string().min(1) });
|
|
1452
|
+
/** IDs and slugs share a lookup namespace within a resource. */
|
|
1453
|
+
function parseSavedViews(value) {
|
|
1454
|
+
const views = zod.z.array(savedViewSchema).parse(value);
|
|
1455
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1456
|
+
const references = /* @__PURE__ */ new Map();
|
|
1457
|
+
for (const view of views) {
|
|
1458
|
+
if (ids.has(view.id)) throw new Error("视图 ID 重复");
|
|
1459
|
+
ids.add(view.id);
|
|
1460
|
+
for (const reference of [view.id, view.slug].filter((item) => item !== void 0)) {
|
|
1461
|
+
const owner = references.get(reference);
|
|
1462
|
+
if (owner !== void 0 && owner !== view.id) throw new Error("视图 slug 必须唯一,且不能与其他视图的 ID 相同");
|
|
1463
|
+
references.set(reference, view.id);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
return views;
|
|
1467
|
+
}
|
|
1468
|
+
/** Require a complete permutation so stale lists cannot silently drop views. */
|
|
1469
|
+
function reorderSavedViews(views, ids) {
|
|
1470
|
+
const byId = new Map(views.map((view) => [view.id, view]));
|
|
1471
|
+
if (ids.length !== views.length || new Set(ids).size !== ids.length || ids.some((id) => !byId.has(id))) throw new Error("视图列表已变化,请取消后重新调整顺序");
|
|
1472
|
+
return ids.map((id) => byId.get(id));
|
|
1473
|
+
}
|
|
1451
1474
|
const viewPreferencesSchema = zod.z.object({
|
|
1452
1475
|
selector: zod.z.enum(["dropdown", "tabs"]),
|
|
1453
1476
|
viewId: zod.z.string().min(1).nullable()
|
|
@@ -1491,8 +1514,8 @@ function queryFingerprint(query) {
|
|
|
1491
1514
|
function createLocalViewStorage(namespace) {
|
|
1492
1515
|
if (!namespace.trim()) throw new Error("A user/tenant storage namespace is required");
|
|
1493
1516
|
const key = (resourceId) => `auto-crud:views:${encodeURIComponent(namespace)}:${encodeURIComponent(resourceId)}`;
|
|
1494
|
-
const read = (resourceId) =>
|
|
1495
|
-
const write = (resourceId, views) => localStorage.setItem(key(resourceId), JSON.stringify(views));
|
|
1517
|
+
const read = (resourceId) => parseSavedViews(JSON.parse(localStorage.getItem(key(resourceId)) ?? "[]"));
|
|
1518
|
+
const write = (resourceId, views) => localStorage.setItem(key(resourceId), JSON.stringify(parseSavedViews(views)));
|
|
1496
1519
|
return {
|
|
1497
1520
|
async list(resourceId) {
|
|
1498
1521
|
return read(resourceId);
|
|
@@ -1507,9 +1530,12 @@ function createLocalViewStorage(namespace) {
|
|
|
1507
1530
|
},
|
|
1508
1531
|
async update(resourceId, id, input) {
|
|
1509
1532
|
const views = read(resourceId);
|
|
1510
|
-
|
|
1533
|
+
const current = views.find((view$1) => view$1.id === id);
|
|
1534
|
+
if (!current) throw new Error("View not found");
|
|
1535
|
+
const valid = viewInputSchema.parse(input);
|
|
1511
1536
|
const view = {
|
|
1512
|
-
...
|
|
1537
|
+
...valid,
|
|
1538
|
+
slug: valid.slug ?? current.slug,
|
|
1513
1539
|
id
|
|
1514
1540
|
};
|
|
1515
1541
|
write(resourceId, views.map((item) => item.id === id ? view : item));
|
|
@@ -1517,19 +1543,32 @@ function createLocalViewStorage(namespace) {
|
|
|
1517
1543
|
},
|
|
1518
1544
|
async remove(resourceId, id) {
|
|
1519
1545
|
write(resourceId, read(resourceId).filter((view) => view.id !== id));
|
|
1546
|
+
},
|
|
1547
|
+
async reorder(resourceId, ids) {
|
|
1548
|
+
write(resourceId, reorderSavedViews(read(resourceId), ids));
|
|
1520
1549
|
}
|
|
1521
1550
|
};
|
|
1522
1551
|
}
|
|
1523
1552
|
|
|
1524
1553
|
//#endregion
|
|
1525
1554
|
//#region src/components/auto-crud/saved-views.tsx
|
|
1526
|
-
function
|
|
1555
|
+
function suggestSlug(name, views) {
|
|
1556
|
+
if (!name.trim()) return "";
|
|
1557
|
+
const base = name.normalize("NFKC").toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-|-$/gu, "").slice(0, 70).replace(/-$/gu, "") || "view";
|
|
1558
|
+
const references = new Set(views.flatMap((view) => [view.id, view.slug]));
|
|
1559
|
+
let slug = base;
|
|
1560
|
+
let suffix = 2;
|
|
1561
|
+
while (references.has(slug)) slug = `${base}-${suffix++}`;
|
|
1562
|
+
return slug;
|
|
1563
|
+
}
|
|
1564
|
+
function SavedViews({ resourceId, table, fieldsReady, defaultView, selector, tabs, counts, onApply, children }) {
|
|
1527
1565
|
const config = useAutoCrudConfig();
|
|
1528
1566
|
if (!config?.viewStorage) throw new Error("Saved views require AutoCrudProvider.viewStorage");
|
|
1529
1567
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ViewMenu, {
|
|
1530
1568
|
resourceId,
|
|
1531
1569
|
table,
|
|
1532
1570
|
fieldsReady,
|
|
1571
|
+
defaultView,
|
|
1533
1572
|
onApply,
|
|
1534
1573
|
selector,
|
|
1535
1574
|
tabs,
|
|
@@ -1537,7 +1576,7 @@ function SavedViews({ resourceId, table, fieldsReady, selector, tabs, counts, on
|
|
|
1537
1576
|
children
|
|
1538
1577
|
}, `${config.cacheKey}:${resourceId}`);
|
|
1539
1578
|
}
|
|
1540
|
-
function ViewMenu({ resourceId, table, fieldsReady = true, selector = "dropdown", tabs, counts, onApply, children }) {
|
|
1579
|
+
function ViewMenu({ resourceId, table, fieldsReady = true, defaultView, selector = "dropdown", tabs, counts, onApply, children }) {
|
|
1541
1580
|
const config = useAutoCrudConfig();
|
|
1542
1581
|
const storage = config.viewStorage;
|
|
1543
1582
|
const client = (0, __tanstack_react_query.useQueryClient)();
|
|
@@ -1548,7 +1587,7 @@ function ViewMenu({ resourceId, table, fieldsReady = true, selector = "dropdown"
|
|
|
1548
1587
|
];
|
|
1549
1588
|
const views = (0, __tanstack_react_query.useQuery)({
|
|
1550
1589
|
queryKey,
|
|
1551
|
-
queryFn: async () => (await storage.list(resourceId))
|
|
1590
|
+
queryFn: async () => parseSavedViews(await storage.list(resourceId))
|
|
1552
1591
|
});
|
|
1553
1592
|
const countQueries = (0, react.useMemo)(() => [{
|
|
1554
1593
|
key: "all",
|
|
@@ -1632,6 +1671,8 @@ function ViewMenu({ resourceId, table, fieldsReady = true, selector = "dropdown"
|
|
|
1632
1671
|
}, [config.scope, resourceId]);
|
|
1633
1672
|
const [dialog, setDialog] = (0, react.useState)(null);
|
|
1634
1673
|
const [name, setName] = (0, react.useState)("");
|
|
1674
|
+
const [slug, setSlug] = (0, react.useState)("");
|
|
1675
|
+
const [orderedViews, setOrderedViews] = (0, react.useState)([]);
|
|
1635
1676
|
const [error, setError] = (0, react.useState)(null);
|
|
1636
1677
|
const [search, setSearch] = (0, react.useState)(() => getUrlParams().toString());
|
|
1637
1678
|
(0, react.useEffect)(() => {
|
|
@@ -1645,6 +1686,8 @@ function ViewMenu({ resourceId, table, fieldsReady = true, selector = "dropdown"
|
|
|
1645
1686
|
}, []);
|
|
1646
1687
|
const columns = table.getAllColumns().filter((column) => column.getCanFilter());
|
|
1647
1688
|
const keys = table.options.meta?.queryKeys;
|
|
1689
|
+
const viewKey = keys?.view ?? "view";
|
|
1690
|
+
const lastViewParam = (0, react.useRef)(null);
|
|
1648
1691
|
const readQuery = (0, react.useCallback)(() => {
|
|
1649
1692
|
const params = getUrlParams();
|
|
1650
1693
|
return {
|
|
@@ -1665,7 +1708,7 @@ function ViewMenu({ resourceId, table, fieldsReady = true, selector = "dropdown"
|
|
|
1665
1708
|
if (query.filters.some((filter) => !allowed.has(filter.id)) || query.sort.some((sort) => !table.getColumn(sort.id)?.getCanSort())) throw new Error("视图包含已删除或不可用的字段,请修改视图条件");
|
|
1666
1709
|
if (query.filters.some((filter) => (table.getColumn(filter.id)?.columnDef.meta?.variant ?? "text") !== filter.variant)) throw new Error("视图中的字段类型已变化,请重新配置筛选条件");
|
|
1667
1710
|
}, [columns, table]);
|
|
1668
|
-
const apply = (0, react.useCallback)((query, history = "push") => {
|
|
1711
|
+
const apply = (0, react.useCallback)((query, history = "push", reference) => {
|
|
1669
1712
|
validateQuery(query);
|
|
1670
1713
|
const params = getUrlParams();
|
|
1671
1714
|
applyReadableFilters(params, columns, query.filters);
|
|
@@ -1675,43 +1718,76 @@ function ViewMenu({ resourceId, table, fieldsReady = true, selector = "dropdown"
|
|
|
1675
1718
|
if (query.sort.length) params.set(keys?.sort ?? "sort", getSortingStateParser().serialize(query.sort));
|
|
1676
1719
|
else params.set(keys?.sort ?? "sort", "");
|
|
1677
1720
|
params.set(keys?.page ?? "page", "1");
|
|
1721
|
+
if (reference !== void 0) {
|
|
1722
|
+
params.set(viewKey, reference);
|
|
1723
|
+
lastViewParam.current = reference;
|
|
1724
|
+
}
|
|
1678
1725
|
setSearchParams(params, { history });
|
|
1679
1726
|
onApply?.();
|
|
1680
1727
|
}, [
|
|
1681
1728
|
columns,
|
|
1682
1729
|
keys,
|
|
1683
1730
|
validateQuery,
|
|
1684
|
-
onApply
|
|
1731
|
+
onApply,
|
|
1732
|
+
viewKey
|
|
1685
1733
|
]);
|
|
1686
1734
|
const preferencesRestored = (0, react.useRef)(false);
|
|
1687
1735
|
(0, react.useEffect)(() => {
|
|
1688
|
-
if (
|
|
1736
|
+
if (!views.isSuccess || !fieldsReady) return;
|
|
1737
|
+
const first = !preferencesRestored.current;
|
|
1738
|
+
const urlReference = getUrlParams().get(viewKey);
|
|
1739
|
+
if (!first && urlReference === lastViewParam.current) return;
|
|
1689
1740
|
const savedViews = views.data;
|
|
1690
1741
|
let cancelled = false;
|
|
1691
1742
|
queueMicrotask(() => {
|
|
1692
|
-
if (cancelled
|
|
1743
|
+
if (cancelled) return;
|
|
1693
1744
|
preferencesRestored.current = true;
|
|
1745
|
+
lastViewParam.current = urlReference;
|
|
1694
1746
|
const preferences = initialPreferences.current;
|
|
1695
|
-
|
|
1696
|
-
|
|
1747
|
+
let reference = urlReference;
|
|
1748
|
+
if (first && urlReference === null) reference = preferences ? preferences.viewId : defaultView ?? null;
|
|
1749
|
+
if (reference === null || reference === "") {
|
|
1750
|
+
setSelected(null);
|
|
1751
|
+
setError(null);
|
|
1752
|
+
if (!first || urlReference !== null) writeViewPreferences(config.scope, resourceId, {
|
|
1753
|
+
selector: display,
|
|
1754
|
+
viewId: null
|
|
1755
|
+
});
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
const view = savedViews.find((item) => item.id === reference || item.slug === reference);
|
|
1697
1759
|
try {
|
|
1698
1760
|
if (view) {
|
|
1699
1761
|
validateQuery(view.query);
|
|
1700
1762
|
const params = getUrlParams();
|
|
1701
|
-
|
|
1763
|
+
const hasUrlQuery = readQuery().filters.length > 0 || [
|
|
1702
1764
|
"search",
|
|
1703
1765
|
keys?.sort ?? "sort",
|
|
1704
1766
|
keys?.joinOperator ?? "joinOperator",
|
|
1705
1767
|
keys?.page ?? "page",
|
|
1706
1768
|
keys?.perPage ?? "perPage"
|
|
1707
|
-
].some((key) => params.has(key))
|
|
1769
|
+
].some((key) => params.has(key));
|
|
1770
|
+
const canonical = view.slug ?? view.id;
|
|
1771
|
+
if (!hasUrlQuery) apply(view.query, "replace", canonical);
|
|
1772
|
+
else if (urlReference !== canonical) {
|
|
1773
|
+
params.set(viewKey, canonical);
|
|
1774
|
+
lastViewParam.current = canonical;
|
|
1775
|
+
setSearchParams(params, { history: "replace" });
|
|
1776
|
+
}
|
|
1708
1777
|
setSelected(view);
|
|
1778
|
+
writeViewPreferences(config.scope, resourceId, {
|
|
1779
|
+
selector: display,
|
|
1780
|
+
viewId: view.id
|
|
1781
|
+
});
|
|
1782
|
+
setError(null);
|
|
1709
1783
|
return;
|
|
1710
1784
|
}
|
|
1785
|
+
if (urlReference !== null && urlReference !== "") setError("链接指定的视图不存在或不可用");
|
|
1711
1786
|
} catch (reason) {
|
|
1712
1787
|
setError(reason instanceof Error ? reason.message : String(reason));
|
|
1713
1788
|
}
|
|
1714
|
-
|
|
1789
|
+
setSelected(null);
|
|
1790
|
+
if (first && urlReference === null && preferences) writeViewPreferences(config.scope, resourceId, {
|
|
1715
1791
|
...preferences,
|
|
1716
1792
|
viewId: null
|
|
1717
1793
|
});
|
|
@@ -1728,29 +1804,52 @@ function ViewMenu({ resourceId, table, fieldsReady = true, selector = "dropdown"
|
|
|
1728
1804
|
keys,
|
|
1729
1805
|
readQuery,
|
|
1730
1806
|
apply,
|
|
1731
|
-
validateQuery
|
|
1807
|
+
validateQuery,
|
|
1808
|
+
search,
|
|
1809
|
+
viewKey,
|
|
1810
|
+
defaultView,
|
|
1811
|
+
display
|
|
1732
1812
|
]);
|
|
1733
1813
|
const mutation = (0, __tanstack_react_query.useMutation)({
|
|
1734
|
-
mutationFn: async (
|
|
1735
|
-
if (operation === "
|
|
1814
|
+
mutationFn: async (request) => {
|
|
1815
|
+
if (request.operation === "reorder") {
|
|
1816
|
+
if (!storage.reorder) throw new Error("存储服务不支持视图排序");
|
|
1817
|
+
reorderSavedViews(views.data ?? [], request.ids);
|
|
1818
|
+
await storage.reorder(resourceId, request.ids);
|
|
1819
|
+
return null;
|
|
1820
|
+
}
|
|
1821
|
+
if (request.operation === "delete") {
|
|
1736
1822
|
await storage.remove(resourceId, selected.id);
|
|
1737
1823
|
return null;
|
|
1738
1824
|
}
|
|
1739
|
-
const valid = viewInputSchema.parse(input);
|
|
1740
|
-
|
|
1825
|
+
const valid = viewInputSchema.parse(request.input);
|
|
1826
|
+
parseSavedViews([...(views.data ?? []).filter((view) => request.operation === "create" || view.id !== selected.id), {
|
|
1827
|
+
...valid,
|
|
1828
|
+
id: request.operation === "create" ? crypto.randomUUID() : selected.id
|
|
1829
|
+
}]);
|
|
1830
|
+
return savedViewSchema.parse(request.operation === "create" ? await storage.create(resourceId, valid) : await storage.update(resourceId, selected.id, valid));
|
|
1741
1831
|
},
|
|
1742
|
-
onSuccess: async (view) => {
|
|
1743
|
-
if (
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1832
|
+
onSuccess: async (view, request) => {
|
|
1833
|
+
if (request.operation !== "reorder") {
|
|
1834
|
+
if (!view) apply({
|
|
1835
|
+
filters: [],
|
|
1836
|
+
joinOperator: "and",
|
|
1837
|
+
search: "",
|
|
1838
|
+
sort: []
|
|
1839
|
+
}, "push", "");
|
|
1840
|
+
else {
|
|
1841
|
+
const reference = view.slug ?? view.id;
|
|
1842
|
+
const params = getUrlParams();
|
|
1843
|
+
params.set(viewKey, reference);
|
|
1844
|
+
lastViewParam.current = reference;
|
|
1845
|
+
setSearchParams(params, { history: "replace" });
|
|
1846
|
+
}
|
|
1847
|
+
setSelected(view);
|
|
1848
|
+
writeViewPreferences(config.scope, resourceId, {
|
|
1849
|
+
selector: display,
|
|
1850
|
+
viewId: view?.id ?? null
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1754
1853
|
setDialog(null);
|
|
1755
1854
|
setError(null);
|
|
1756
1855
|
await client.invalidateQueries({ queryKey });
|
|
@@ -1764,7 +1863,7 @@ function ViewMenu({ resourceId, table, fieldsReady = true, selector = "dropdown"
|
|
|
1764
1863
|
joinOperator: "and",
|
|
1765
1864
|
search: "",
|
|
1766
1865
|
sort: []
|
|
1767
|
-
});
|
|
1866
|
+
}, "push", view?.slug ?? view?.id ?? "");
|
|
1768
1867
|
setSelected(view);
|
|
1769
1868
|
writeViewPreferences(config.scope, resourceId, {
|
|
1770
1869
|
selector: display,
|
|
@@ -1777,6 +1876,11 @@ function ViewMenu({ resourceId, table, fieldsReady = true, selector = "dropdown"
|
|
|
1777
1876
|
};
|
|
1778
1877
|
const open = (operation) => {
|
|
1779
1878
|
setName(operation === "create" ? "" : selected?.name ?? "");
|
|
1879
|
+
let nextSlug = "";
|
|
1880
|
+
if (operation === "rename") nextSlug = selected?.slug ?? "";
|
|
1881
|
+
else if (operation === "duplicate") nextSlug = suggestSlug(selected?.name ?? "", views.data ?? []);
|
|
1882
|
+
setSlug(nextSlug);
|
|
1883
|
+
if (operation === "reorder") setOrderedViews(views.data ?? []);
|
|
1780
1884
|
setError(null);
|
|
1781
1885
|
setDialog(operation);
|
|
1782
1886
|
};
|
|
@@ -1891,6 +1995,11 @@ function ViewMenu({ resourceId, table, fieldsReady = true, selector = "dropdown"
|
|
|
1891
1995
|
})
|
|
1892
1996
|
] }),
|
|
1893
1997
|
selected && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.DropdownMenuSeparator, {}),
|
|
1998
|
+
storage.reorder && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__wordrhyme_shadcn.DropdownMenuItem, {
|
|
1999
|
+
disabled: views.isError || (views.data?.length ?? 0) < 2,
|
|
2000
|
+
onSelect: () => open("reorder"),
|
|
2001
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ArrowDownUp, { className: "size-4" }), "调整顺序"]
|
|
2002
|
+
}),
|
|
1894
2003
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__wordrhyme_shadcn.DropdownMenuSub, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__wordrhyme_shadcn.DropdownMenuSubTrigger, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.PanelsTopLeft, { className: "size-4" }), "展示方式"] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.DropdownMenuSubContent, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__wordrhyme_shadcn.DropdownMenuRadioGroup, {
|
|
1895
2004
|
value: display,
|
|
1896
2005
|
onValueChange: (value) => {
|
|
@@ -1921,6 +2030,7 @@ function ViewMenu({ resourceId, table, fieldsReady = true, selector = "dropdown"
|
|
|
1921
2030
|
operation: "update",
|
|
1922
2031
|
input: {
|
|
1923
2032
|
name: selected.name,
|
|
2033
|
+
slug: selected.slug,
|
|
1924
2034
|
query: current
|
|
1925
2035
|
}
|
|
1926
2036
|
}),
|
|
@@ -1940,6 +2050,7 @@ function ViewMenu({ resourceId, table, fieldsReady = true, selector = "dropdown"
|
|
|
1940
2050
|
variant: "link",
|
|
1941
2051
|
size: "sm",
|
|
1942
2052
|
onClick: () => {
|
|
2053
|
+
preferencesRestored.current = false;
|
|
1943
2054
|
views.refetch().catch((reason) => setError(reason instanceof Error ? reason.message : String(reason)));
|
|
1944
2055
|
},
|
|
1945
2056
|
children: "重试"
|
|
@@ -1968,17 +2079,30 @@ function ViewMenu({ resourceId, table, fieldsReady = true, selector = "dropdown"
|
|
|
1968
2079
|
delete: "删除视图",
|
|
1969
2080
|
rename: "重命名视图",
|
|
1970
2081
|
create: "另存为新视图",
|
|
1971
|
-
duplicate: "复制视图"
|
|
2082
|
+
duplicate: "复制视图",
|
|
2083
|
+
reorder: "调整视图顺序"
|
|
1972
2084
|
}[dialog] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
1973
2085
|
onSubmit: (event) => {
|
|
1974
2086
|
event.preventDefault();
|
|
2087
|
+
if (mutation.isPending) return;
|
|
2088
|
+
if (dialog === "reorder") {
|
|
2089
|
+
mutation.mutate({
|
|
2090
|
+
operation: "reorder",
|
|
2091
|
+
ids: orderedViews.map((view) => view.id)
|
|
2092
|
+
});
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
if (dialog === "delete") {
|
|
2096
|
+
mutation.mutate({ operation: "delete" });
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
1975
2099
|
let operation = "create";
|
|
1976
|
-
if (dialog === "
|
|
1977
|
-
else if (dialog === "rename") operation = "update";
|
|
2100
|
+
if (dialog === "rename") operation = "update";
|
|
1978
2101
|
mutation.mutate({
|
|
1979
2102
|
operation,
|
|
1980
2103
|
input: {
|
|
1981
2104
|
name,
|
|
2105
|
+
slug: slug.trim() || void 0,
|
|
1982
2106
|
query: dialog === "rename" || dialog === "duplicate" ? selected.query : current
|
|
1983
2107
|
}
|
|
1984
2108
|
});
|
|
@@ -1988,14 +2112,68 @@ function ViewMenu({ resourceId, table, fieldsReady = true, selector = "dropdown"
|
|
|
1988
2112
|
"删除「",
|
|
1989
2113
|
selected?.name,
|
|
1990
2114
|
"」?数据和标签会保留。"
|
|
1991
|
-
] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("
|
|
1992
|
-
className: "grid gap-
|
|
1993
|
-
children: [
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
2115
|
+
] }) : dialog === "reorder" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2116
|
+
className: "grid gap-3",
|
|
2117
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2118
|
+
className: "text-sm text-muted-foreground",
|
|
2119
|
+
children: "拖动手柄调整顺序,“全部”固定在最前。保存后生效。"
|
|
2120
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn_ui.Sortable, {
|
|
2121
|
+
value: orderedViews,
|
|
2122
|
+
onValueChange: setOrderedViews,
|
|
2123
|
+
getItemValue: (view) => view.id,
|
|
2124
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn_ui.SortableContent, {
|
|
2125
|
+
className: "grid max-h-80 gap-2 overflow-y-auto",
|
|
2126
|
+
children: orderedViews.map((view) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__wordrhyme_shadcn_ui.SortableItem, {
|
|
2127
|
+
value: view.id,
|
|
2128
|
+
disabled: mutation.isPending,
|
|
2129
|
+
className: "flex items-center gap-2 rounded-md border bg-background p-2",
|
|
2130
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn_ui.SortableItemHandle, {
|
|
2131
|
+
asChild: true,
|
|
2132
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.Button, {
|
|
2133
|
+
type: "button",
|
|
2134
|
+
variant: "ghost",
|
|
2135
|
+
size: "icon-sm",
|
|
2136
|
+
"aria-label": `拖动排序:${view.name}`,
|
|
2137
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.GripVertical, { className: "size-4" })
|
|
2138
|
+
})
|
|
2139
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2140
|
+
className: "min-w-0 truncate",
|
|
2141
|
+
children: view.name
|
|
2142
|
+
})]
|
|
2143
|
+
}, view.id))
|
|
2144
|
+
})
|
|
2145
|
+
})]
|
|
2146
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2147
|
+
className: "grid gap-4",
|
|
2148
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2149
|
+
className: "grid gap-2",
|
|
2150
|
+
children: ["视图名称", /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.Input, {
|
|
2151
|
+
value: name,
|
|
2152
|
+
onChange: (event) => {
|
|
2153
|
+
const { value } = event.target;
|
|
2154
|
+
if (dialog !== "rename" && slug === suggestSlug(name, views.data ?? [])) setSlug(suggestSlug(value, views.data ?? []));
|
|
2155
|
+
setName(value);
|
|
2156
|
+
},
|
|
2157
|
+
maxLength: 40,
|
|
2158
|
+
required: true,
|
|
2159
|
+
autoFocus: true
|
|
2160
|
+
})]
|
|
2161
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2162
|
+
className: "grid gap-2",
|
|
2163
|
+
children: [
|
|
2164
|
+
"链接标识(slug)",
|
|
2165
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.Input, {
|
|
2166
|
+
value: slug,
|
|
2167
|
+
onChange: (event) => setSlug(event.target.value),
|
|
2168
|
+
maxLength: 80,
|
|
2169
|
+
required: dialog === "rename" && selected?.slug !== void 0,
|
|
2170
|
+
placeholder: "例如 vip-customers"
|
|
2171
|
+
}),
|
|
2172
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2173
|
+
className: "text-sm text-muted-foreground",
|
|
2174
|
+
children: "用于分享链接,同一列表内唯一。修改后旧链接将失效。"
|
|
2175
|
+
})
|
|
2176
|
+
]
|
|
1999
2177
|
})]
|
|
2000
2178
|
}),
|
|
2001
2179
|
error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -7293,6 +7471,7 @@ function useDataTable(props) {
|
|
|
7293
7471
|
meta: {
|
|
7294
7472
|
...tableProps.meta,
|
|
7295
7473
|
queryKeys: {
|
|
7474
|
+
view: queryKeys?.view ?? "view",
|
|
7296
7475
|
page: pageKey,
|
|
7297
7476
|
perPage: perPageKey,
|
|
7298
7477
|
sort: sortKey,
|
|
@@ -7657,6 +7836,7 @@ function AutoTable({ resourceId, views, schema, data, getRowId, expandedRow, def
|
|
|
7657
7836
|
table,
|
|
7658
7837
|
onApply: clearBatchSearch,
|
|
7659
7838
|
fieldsReady: typeof views === "object" ? views.fieldsReady : void 0,
|
|
7839
|
+
defaultView: typeof views === "object" ? views.defaultView : void 0,
|
|
7660
7840
|
selector: typeof views === "object" ? views.selector : void 0,
|
|
7661
7841
|
tabs: typeof views === "object" ? views.tabs : void 0,
|
|
7662
7842
|
counts: typeof views === "object" ? views.counts : void 0,
|
package/dist/index.d.cts
CHANGED
|
@@ -750,6 +750,8 @@ declare module '@tanstack/react-table' {
|
|
|
750
750
|
}
|
|
751
751
|
}
|
|
752
752
|
interface QueryKeys {
|
|
753
|
+
/** Saved view slug or ID. Defaults to "view". */
|
|
754
|
+
view?: string;
|
|
753
755
|
page: string;
|
|
754
756
|
perPage: string;
|
|
755
757
|
sort: string;
|
|
@@ -999,6 +1001,8 @@ interface ViewStorage {
|
|
|
999
1001
|
create: (resourceId: string, input: ViewInput) => Promise<SavedView>;
|
|
1000
1002
|
update: (resourceId: string, id: string, input: ViewInput) => Promise<SavedView>;
|
|
1001
1003
|
remove: (resourceId: string, id: string) => Promise<void>;
|
|
1004
|
+
/** Atomically persist all view IDs in order; reject duplicates, missing or unknown IDs. */
|
|
1005
|
+
reorder?: (resourceId: string, ids: string[]) => Promise<void>;
|
|
1002
1006
|
}
|
|
1003
1007
|
interface TagScope {
|
|
1004
1008
|
resourceId: string;
|
|
@@ -1106,6 +1110,7 @@ declare const viewQuerySchema: z.ZodObject<{
|
|
|
1106
1110
|
}, z.core.$strip>;
|
|
1107
1111
|
declare const viewInputSchema: z.ZodObject<{
|
|
1108
1112
|
name: z.ZodString;
|
|
1113
|
+
slug: z.ZodOptional<z.ZodString>;
|
|
1109
1114
|
query: z.ZodObject<{
|
|
1110
1115
|
filters: z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
1111
1116
|
id: z.ZodString;
|
|
@@ -1164,6 +1169,7 @@ declare const viewInputSchema: z.ZodObject<{
|
|
|
1164
1169
|
}, z.core.$strip>;
|
|
1165
1170
|
declare const savedViewSchema: z.ZodObject<{
|
|
1166
1171
|
name: z.ZodString;
|
|
1172
|
+
slug: z.ZodOptional<z.ZodString>;
|
|
1167
1173
|
query: z.ZodObject<{
|
|
1168
1174
|
filters: z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
1169
1175
|
id: z.ZodString;
|
|
@@ -1237,6 +1243,8 @@ declare function createLocalViewStorage(namespace: string): ViewStorage;
|
|
|
1237
1243
|
//#endregion
|
|
1238
1244
|
//#region src/components/auto-crud/saved-views.d.ts
|
|
1239
1245
|
interface SavedViewsConfig {
|
|
1246
|
+
/** Initial view slug or ID when the URL and local preferences do not select a view. */
|
|
1247
|
+
defaultView?: string;
|
|
1240
1248
|
/** Wait for asynchronous field metadata before restoring or selecting a view. */
|
|
1241
1249
|
fieldsReady?: boolean;
|
|
1242
1250
|
/** Initial display mode when no local preference exists. */
|
package/dist/index.d.ts
CHANGED
|
@@ -5,9 +5,9 @@ import * as react_jsx_runtime17 from "react/jsx-runtime";
|
|
|
5
5
|
import { DropdownMenuTrigger, PopoverContentProps } from "@wordrhyme/shadcn";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { ClassValue } from "clsx";
|
|
8
|
+
import { MultiCombobox, MultiComboboxOption, MultiComboboxProps, MultiComboboxTriggerRenderProps, Select, SelectMode, SelectOption, SelectProps, SelectSearchableDynamicProps, SelectSearchableMultipleProps, SelectSearchableProps, SelectSearchableSingleProps, SelectSimpleProps, SelectTriggerRenderProps } from "@wordrhyme/shadcn-ui";
|
|
8
9
|
import * as _tanstack_react_table0 from "@tanstack/react-table";
|
|
9
10
|
import { Column, ColumnDef, ColumnMeta, ColumnSort, Row, RowData, Table as Table$1, TableOptions, TableState, VisibilityState } from "@tanstack/react-table";
|
|
10
|
-
import { MultiCombobox, MultiComboboxOption, MultiComboboxProps, MultiComboboxTriggerRenderProps, Select, SelectMode, SelectOption, SelectProps, SelectSearchableDynamicProps, SelectSearchableMultipleProps, SelectSearchableProps, SelectSearchableSingleProps, SelectSimpleProps, SelectTriggerRenderProps } from "@wordrhyme/shadcn-ui";
|
|
11
11
|
import { DropdownMenuItem as DropdownMenuItem$1 } from "@wordrhyme/ui";
|
|
12
12
|
import { JsonSchemaFormScope } from "@wordrhyme/formily-shadcn";
|
|
13
13
|
import { ISchema } from "@formily/json-schema";
|
|
@@ -750,6 +750,8 @@ declare module '@tanstack/react-table' {
|
|
|
750
750
|
}
|
|
751
751
|
}
|
|
752
752
|
interface QueryKeys {
|
|
753
|
+
/** Saved view slug or ID. Defaults to "view". */
|
|
754
|
+
view?: string;
|
|
753
755
|
page: string;
|
|
754
756
|
perPage: string;
|
|
755
757
|
sort: string;
|
|
@@ -999,6 +1001,8 @@ interface ViewStorage {
|
|
|
999
1001
|
create: (resourceId: string, input: ViewInput) => Promise<SavedView>;
|
|
1000
1002
|
update: (resourceId: string, id: string, input: ViewInput) => Promise<SavedView>;
|
|
1001
1003
|
remove: (resourceId: string, id: string) => Promise<void>;
|
|
1004
|
+
/** Atomically persist all view IDs in order; reject duplicates, missing or unknown IDs. */
|
|
1005
|
+
reorder?: (resourceId: string, ids: string[]) => Promise<void>;
|
|
1002
1006
|
}
|
|
1003
1007
|
interface TagScope {
|
|
1004
1008
|
resourceId: string;
|
|
@@ -1106,6 +1110,7 @@ declare const viewQuerySchema: z.ZodObject<{
|
|
|
1106
1110
|
}, z.core.$strip>;
|
|
1107
1111
|
declare const viewInputSchema: z.ZodObject<{
|
|
1108
1112
|
name: z.ZodString;
|
|
1113
|
+
slug: z.ZodOptional<z.ZodString>;
|
|
1109
1114
|
query: z.ZodObject<{
|
|
1110
1115
|
filters: z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
1111
1116
|
id: z.ZodString;
|
|
@@ -1164,6 +1169,7 @@ declare const viewInputSchema: z.ZodObject<{
|
|
|
1164
1169
|
}, z.core.$strip>;
|
|
1165
1170
|
declare const savedViewSchema: z.ZodObject<{
|
|
1166
1171
|
name: z.ZodString;
|
|
1172
|
+
slug: z.ZodOptional<z.ZodString>;
|
|
1167
1173
|
query: z.ZodObject<{
|
|
1168
1174
|
filters: z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
1169
1175
|
id: z.ZodString;
|
|
@@ -1237,6 +1243,8 @@ declare function createLocalViewStorage(namespace: string): ViewStorage;
|
|
|
1237
1243
|
//#endregion
|
|
1238
1244
|
//#region src/components/auto-crud/saved-views.d.ts
|
|
1239
1245
|
interface SavedViewsConfig {
|
|
1246
|
+
/** Initial view slug or ID when the URL and local preferences do not select a view. */
|
|
1247
|
+
defaultView?: string;
|
|
1240
1248
|
/** Wait for asynchronous field metadata before restoring or selecting a view. */
|
|
1241
1249
|
fieldsReady?: boolean;
|
|
1242
1250
|
/** Initial display mode when no local preference exists. */
|