@wordrhyme/auto-crud 1.20.0 → 1.21.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 CHANGED
@@ -1982,7 +1982,7 @@ const viewStorage = createLocalViewStorage(`${organizationId}:${userId}`);
1982
1982
 
1983
1983
  `ViewStorage` 和 `TagService` 的完整 TypeScript 接口由包导出。完整交互示例见 Storybook 的 **Auto CRUD / Saved views and tags**;可切换 simple、advanced、command 三种模式。
1984
1984
 
1985
- 展示方式与当前选中视图的 ID 自动存入浏览器 `localStorage`,键为 `auto-crud:view-preferences:${encodeURIComponent(scope)}:${encodeURIComponent(resourceId)}`,按用户/租户和资源隔离,独立于 `ViewStorage` 的本地或服务端实现。刷新或重新进入时恢复展示方式;视图列表加载成功后恢复选中项。URL 已有筛选、搜索、排序或分页参数时保留 URL 查询,否则应用上次视图的保存条件,使用 replace 更新 URL。上次视图已删除或字段不可用时清除其选中记忆,保留 URL 查询。本地数据损坏或存储不可用时使用配置的初始展示方式,仍可在当前页面切换。未保存的查询不另存到本地偏好中。
1985
+ 展示方式、当前选中视图的 ID、显示顺序 `viewOrder` 和隐藏视图 ID `hiddenViewIds` 自动存入浏览器 `localStorage`,键为 `auto-crud:view-preferences:${encodeURIComponent(scope)}:${encodeURIComponent(resourceId)}`,按用户/租户和资源隔离,独立于 `ViewStorage` 的本地或服务端实现。旧偏好无需手动迁移:缺少的新字段表示使用配置顺序、不隐藏视图;浏览器内旧的 `hiddenPresetIds` 会自动转换为 `hiddenViewIds`,已有新字段时以新字段为准。刷新或重新进入时恢复展示偏好;视图列表加载成功后恢复选中项。URL 已有筛选、搜索、排序或分页参数时保留 URL 查询,否则应用上次视图的保存条件,使用 replace 更新 URL。上次视图已删除、已隐藏或字段不可用时清除其选中记忆,保留 URL 查询。本地数据损坏或存储不可用时使用配置的初始展示方式,仍可在当前页面调整显示偏好。未保存的查询不另存到本地偏好中。
1986
1986
 
1987
1987
  ### 代码预设与 Host 保存范围
1988
1988
 
@@ -2008,7 +2008,6 @@ const viewStorage: ViewStorage = {
2008
2008
  create: (resourceId, input) => api.views.create({ resourceId, ...input }),
2009
2009
  update: (resourceId, id, input) => api.views.update({ resourceId, id, ...input }),
2010
2010
  remove: (resourceId, id) => api.views.remove({ resourceId, id }),
2011
- reorder: (resourceId, ids) => api.views.reorder({ resourceId, ids }),
2012
2011
  };
2013
2012
 
2014
2013
  <AutoCrudProvider scope={`${organizationId}:${userId}`} viewStorage={viewStorage}>
@@ -2021,7 +2020,31 @@ const viewStorage: ViewStorage = {
2021
2020
  </AutoCrudProvider>;
2022
2021
  ```
2023
2022
 
2024
- 合并顺序为“全部 → presets 数组顺序 → storage.list 返回顺序”。ID、slug 在合并后的所有来源间共用命名空间:重复 ID、重复 slug 或 slug 与另一项 ID 相同均报错,禁止静默覆盖。预设 schema 不接受 `scope` 或 `capabilities`;其来源由组件根据配置入口识别。预设不可保存覆盖、重命名、删除或参与存储排序,修改查询后可另存;另存为普通持久化视图并取得新的 ID / slug。Host 应在服务端预留代码预设的 ID / slug,防止其他调用方或并发写入产生冲突。
2023
+ 默认合并顺序为“全部 → presets 数组顺序 → storage.list 返回顺序”,随后应用个人显示顺序和隐藏偏好。ID、slug 在合并后的所有来源间共用命名空间:重复 ID、重复 slug 或 slug 与另一项 ID 相同均报错,禁止静默覆盖。预设 schema 不接受 `scope` 或 `capabilities`;其来源由组件根据配置入口识别。预设可隐藏、恢复和调整本地显示顺序;其查询仍由代码维护,不可保存覆盖、重命名、删除或参与存储排序,修改查询后可另存为普通持久化视图并取得新的 ID / slug。隐藏项仍参与 ID / slug 冲突校验。Host 应在服务端预留代码预设的 ID / slug,防止其他调用方或并发写入产生冲突。
2024
+
2025
+ 复用基础页面时,可通过 `views.hiddenViews?: readonly string[]` 按 ID 或 slug 隐藏任意来源的视图,无需修改查询定义。例如通过已知 slug 提前隐藏 WordRhyme 订单预设「已付款」:
2026
+
2027
+ ```tsx
2028
+ <AutoCrudTable
2029
+ id="shop.orders"
2030
+ schema={schema}
2031
+ resource={resource}
2032
+ table={{
2033
+ views: {
2034
+ presets,
2035
+ hiddenViews: ['shop-orders-paid'],
2036
+ selector: 'tabs',
2037
+ tabs: { variant: 'line' },
2038
+ },
2039
+ }}
2040
+ />
2041
+ ```
2042
+
2043
+ 独立 `AutoTable` 同样使用 `views={{ presets, hiddenViews }}`。省略或传空数组表示页面不额外隐藏;数组可混用 ID 与 slug,按大小写精确匹配,不按显示名称匹配。ID 与 slug 已在同一资源内共用唯一命名空间,不会歧义匹配。重复项被忽略,暂未匹配的项不影响加载,视图之后注册或创建并出现在可见集合时自动应用配置;没有 slug 的视图仍可用 ID。修改 slug 后需同步更新对应配置,用户在界面上隐藏时继续按稳定 ID 保存本地偏好。预设、个人及只读共享视图均可隐藏,“全部”仍固定可见。页面配置的隐藏与用户本地隐藏取并集,配置项不进入“恢复隐藏视图”菜单;移除配置后,若用户未自行隐藏该项,就会重新显示。配置本身不写入本地偏好、不修改查询定义、不调用 `ViewStorage` 写入方法,也不是访问权限控制。
2044
+
2045
+ 运行时新增配置使当前选中项被隐藏时,会切换到“全部”并重置查询;初始化时不会选中已隐藏的默认项或本地记忆项。显式 URL 指向配置隐藏项时提示页面已隐藏该视图并保留 URL 查询,不提供用户恢复入口。隐藏项不参与可见视图的数量统计,但仍参与 ID / slug 冲突校验。Storybook 的 **Configured Hidden Views** 可通过 `hiddenViews` 控件体验 ID / slug 混用及移除配置后的显示。
2046
+
2047
+ 基础页面必须将消费侧配置传入 `table.views`。WordRhyme 当前的 `registerViews(id, views)` 只追加预设数组;仅在 OmniDS 插件侧传入隐藏视图的 ID 或 slug 尚不会生效,需由 WordRhyme 的扩展配置入口接收并透传到此处,查询定义继续留在基础项目。
2025
2048
 
2026
2049
  公共 schema 和对应类型均从包导出:`viewQuerySchema` / `ViewQuery`、`viewInputSchema` / `ViewInput`、`savedViewSchema` / `SavedView`、`presetViewSchema` / `PresetView`、`viewScopeSchema` / `ViewScope`、`viewCapabilitiesSchema` / `ViewCapabilities`、`viewStorageCapabilitiesSchema` / `ViewStorageCapabilities`。
2027
2050
 
@@ -2062,7 +2085,7 @@ const capabilities: ViewStorageCapabilities = {
2062
2085
  - `scope` 是 Host 的不透明存储标识,组件不内置个人/组织枚举、展示文案或权限推断。个人/组织的可见性、归属及服务端授权由 Host 实现。`AutoCrudProvider.scope` 仍是用户/租户缓存及偏好命名空间,与视图保存范围不同。
2063
2086
  - 创建省略 `scope` 使用存储适配器默认值;更新省略时保留原范围。更新不能迁移范围,要改变范围请另存。提供 `getCapabilities` 时,组件默认选择返回列表的第一项。Host 可继续使用 `personal` / `organization`,通过返回顺序定义默认行为。
2064
2087
  - 保存弹窗默认只显示名称和 slug。业务需要让用户选择保存范围时,配置 `views.scopeOptions: [{ value, label }]`;值与 `createScopes` 取交集,只有多个可用选项时才展示选择器,文案完全由调用方提供。配置后以首个可用配置项为默认;未配置或只有一个选项时隐藏选择器,仍向适配器传入选定范围。
2065
- - `capabilities.update` 同时控制保存查询、重命名和修改 slug;`delete` 控制删除;`reorder` 控制参与排序。提供 capabilities 时三个布尔值都必须返回。省略时保留旧适配器的可管理行为;Host 必须为有权限差异的项显式返回能力。组件不根据 scope、角色或身份猜权限。
2088
+ - `capabilities.update` 同时控制保存查询、重命名和修改 slug;`delete` 控制删除;`reorder` 表示适配器层面的持久化排序权限,不限制个人本地显示顺序。提供 capabilities 时三个布尔值都必须返回。省略时保留旧适配器的可管理行为;Host 必须为有权限差异的项显式返回能力。组件不根据 scope、角色或身份猜权限。
2066
2089
  - `getCapabilities?: (resourceId: string) => Promise<ViewStorageCapabilities>` 为独立异步读取,不影响 list 的返回形状。旧适配器省略时允许创建且不指定范围;已实现时加载或失败均禁用创建,失败提供重试。Host 需返回当前资源实际允许的范围,在每次写入时再次校验权限。组件成功写入后刷新列表和创建能力。
2067
2090
  - 所有来源共用现有 URL、默认选中、本地偏好、`fieldsReady`、Tab 和异步 counts 机制;范围不写入查询。`createLocalViewStorage` 在给定的浏览器命名空间内保存视图,scope 如有提供则原样保存,不实现跨用户共享。
2068
2091
 
@@ -2078,7 +2101,7 @@ table={{ views: {
2078
2101
 
2079
2102
  之前通过 scope 自动推断只读的适配器,需改为显式返回 `capabilities`。既有 Host 的 list / create / update / remove / reorder 和 `getCapabilities` 形状保持不变;未配置 `scopeOptions` 时不展示范围选择器。
2080
2103
 
2081
- Storybook 的 **Presets And Shared Views** 展示两组固定预设、只读项和可编辑项;**Organization Administrator** 演示由 Host 注入“仅自己/共享”选项和权限。示例 Host 使用内存存储,刷新重置,不代表浏览器本地存储提供共享能力。
2104
+ Storybook 的 **Presets And Shared Views** 展示两组代码预设、只读项和可编辑项,可体验任意项隐藏/恢复与混合排序;**Organization Administrator** 演示由 Host 注入“仅自己/共享”选项和权限。示例 Host 的查询定义使用内存存储,刷新重置;个人显示偏好在当前浏览器保留,不代表浏览器本地存储提供共享能力。
2082
2105
 
2083
2106
  共享视图示例使用 `queryKeys.view: 'sharedView'`,普通示例继续使用 `view`。切换这两组数据源时,示例会清理另一组的视图参数及关联查询,避免 Storybook 保留的旧 URL 指向不同存储中的视图;同组刷新与分享链接仍保留当前查询。
2084
2107
 
@@ -2103,13 +2126,19 @@ Storybook 的 **Presets And Shared Views** 展示两组固定预设、只读项
2103
2126
 
2104
2127
  默认参数为 `?view=vip-customers`,也接受 `?view=view-123`;若配置上例的 `queryKeys.view`,则使用 `?customerView=vip-customers`。切换视图会同步参数并创建浏览器历史记录,前进/后退恢复 URL 指定的视图。有 slug 时写入规范 slug,只有 ID 的旧链接会以 replace 规范化。`?view=` 明确表示“全部”。仅带视图参数的链接会应用保存条件;若同时带筛选、搜索、排序或分页参数,保留完整 URL 查询,便于分享未保存的调整。无权访问、已删除或查询字段失效时显示错误并保留 URL 查询,不回退到另一个视图。链接接收者仍需有权限读取该视图及资源,URL 不授予访问权限。
2105
2128
 
2106
- 下拉和 Tab 都按预设数组顺序、`ViewStorage.list()` 的返回顺序展示,“全部”固定在最前。存储提供可选方法后,设置菜单显示“调整顺序”,拖动手柄(也支持键盘)后点击保存;取消或保存失败不会改变显示顺序或当前选中项:
2129
+ 下拉和 Tab 共用个人显示偏好,“全部”固定在最前,不参与隐藏或排序。设置菜单始终提供“调整顺序”:至少有两个可见视图且列表加载成功后可用,拖动手柄(也支持键盘)后点击保存,取消不会改变顺序或当前选中项。预设与所有已保存项(包括只读共享项)均可混合调整显示顺序,无需实现 `ViewStorage.reorder`,也不受 `capabilities.reorder` 限制;操作仅写本地偏好,不调用服务端排序、不改变他人的顺序、共享查询或存储权限。列表加载失败或配置冲突时禁用;创建权限加载失败仅禁用创建,不阻止已成功加载列表的本地排序。
2130
+
2131
+ 选择任意视图后,在“视图设置 → 隐藏视图”中隐藏;隐藏当前项会切换到“全部”,清空筛选、搜索和排序并重置分页。通过“恢复隐藏视图”子菜单逐项恢复,不自动选中。隐藏项的位置在调整其他视图时保留,恢复后回到该位置。本地隐藏适用于预设、个人和只读共享视图,不受 `capabilities.update/delete/reorder` 限制;共享查询定义及编辑/删除权限保持原有语义。`defaultView` 或上次选中记忆不会重新选择隐藏项;显式 URL 指向用户隐藏项时显示恢复提示并保留 URL 查询,恢复后可继续使用该链接。
2132
+
2133
+ 顺序按稳定 ID 记录;重复或不在当前集合中的 ID 被忽略,新增项按代码/存储默认顺序追加。暂时不可见或插件移除的项不阻止加载,隐藏 ID 会保留供插件恢复时使用。排序弹窗打开后若可见集合变化,保存会提示重新调整,避免漏掉新项。切换展示方式、选中视图或保存查询均保留显示顺序与隐藏偏好。本地存储不可用时,本轮页面内的调整仍然有效,刷新后可能丢失。
2134
+
2135
+ `ViewStorage` 保留可选的适配器排序接口,供需要管理持久化顺序的调用方使用;视图选择器的“调整顺序”不再调用它:
2107
2136
 
2108
2137
  ```ts
2109
2138
  reorder?: (resourceId: string, ids: string[]) => Promise<void>;
2110
2139
  ```
2111
2140
 
2112
- `ids` 必须为该资源当前允许排序的持久化项的完整排列,只包含 `capabilities.reorder` 为 true 的项(旧适配器省略 capabilities 时仍允许排序)。预设和不可排序项从不传入,Host 必须保留其他项在持久化列表中的位置,将新顺序填入可排序项原有位置。例如 list 为 `[只读 A, 可排序 B, 可排序 C]`,提交 `[C, B]` 后应为 `[A, C, B]`。服务端应在事务中校验权限并保存,拒绝重复、未知、无权或缺失 ID;并发新增/删除或权限变化导致集合不一致时返回错误,让用户刷新后重试。旧的全个人列表仍提交全部 ID。成功后组件重新调用 `list()`。排序不修改视图 ID、slug 或查询条件;服务未实现 `reorder` 时不展示排序入口。`createLocalViewStorage` 已实现本地排序持久化。
2141
+ 直接调用该接口时,`ids` 仍必须为该资源当前允许排序的持久化项的完整排列,只包含 `capabilities.reorder` 为 true 的项(旧适配器省略 capabilities 时仍允许排序)。预设和不可排序项从不传入,Host 必须保留其他项在持久化列表中的位置,将新顺序填入可排序项原有位置。例如 list 为 `[只读 A, 可排序 B, 可排序 C]`,提交 `[C, B]` 后应为 `[A, C, B]`。服务端应在事务中校验权限并保存,拒绝重复、未知、无权或缺失 ID;并发新增/删除或权限变化导致集合不一致时返回错误。该接口及 `createLocalViewStorage` 的实现保持兼容,既有存储顺序仍作为没有个人排列偏好时的默认顺序。
2113
2142
 
2114
2143
  Storybook 的 **Default View** 演示按 slug 默认选中,“视图设置 → 调整顺序”可体验排序;所有 Saved views and tags 示例共用本地视图存储,首次加载补齐演示视图;视图、删除操作和排列顺序刷新后保留。客户标签仍为刷新后重置的内存数据。
2115
2144
 
@@ -2135,7 +2164,7 @@ const { options, hasMore } = await tagService.list({
2135
2164
 
2136
2165
  ### 视图数量
2137
2166
 
2138
- 通过可选的 `views.counts` 回调接入数量,Tab 和下拉选项都会显示。组件提供全部预设、组织和个人视图保存的筛选、AND/OR 与搜索条件,并额外提供不带筛选的「全部」查询;查询不包含排序和分页,也不叠加当前尚未保存的筛选。统计独立异步执行,列表不等待统计完成。
2167
+ 通过可选的 `views.counts` 回调接入数量,Tab 和下拉选项都会显示。组件提供所有可见视图(预设、组织和个人项)保存的筛选、AND/OR 与搜索条件,并额外提供不带筛选的「全部」查询;查询不包含排序和分页,也不叠加当前尚未保存的筛选。统计独立异步执行,列表不等待统计完成。
2139
2168
 
2140
2169
  ```tsx
2141
2170
  import type { SavedViewsConfig } from '@wordrhyme/auto-crud';
@@ -2177,7 +2206,6 @@ Storybook 的 **Auto CRUD / Saved views and tags / Tabs With Counts** 与其他
2177
2206
  selector: 'tabs',
2178
2207
  tabs: {
2179
2208
  variant: 'line',
2180
- linePosition: 'bottom',
2181
2209
  classNames: {
2182
2210
  list: 'gap-2',
2183
2211
  trigger: 'text-base data-[state=on]:text-blue-700',
@@ -2191,7 +2219,7 @@ Storybook 的 **Auto CRUD / Saved views and tags / Tabs With Counts** 与其他
2191
2219
 
2192
2220
  `classNames.list` 作用于 Tab 列表,`trigger` 作用于每个 Tab(含「全部」),`indicator` 作用于选中项的线条,可修改颜色和粗细。类名通过 `cn` 在默认类名之后合并;`trigger` 的选中状态可用 `data-[state=on]` 定制。`linePosition` 和 `indicator` 仅在线条样式中生效。独立 `AutoTable` 使用同样的 `views.tabs` 配置。这些外观配置由使用方提供,不写入本地偏好。
2193
2221
 
2194
- Storybook 提供 **Line Tabs**(下方蓝色强调线)和 **Top Line Tabs**(上方强调线)示例,均带视图数量;可通过 Controls 切换样式和线条位置。已有本地展示偏好时,仍可在视图设置中切换到 Tab。
2222
+ Storybook 提供 **Line Tabs**(下方蓝色强调线)和 **Top Line Tabs**(上方强调线)示例,均带预设、共享视图和数量;选中后可在齿轮菜单隐藏、恢复或调整顺序,也可通过 Controls 切换样式和线条位置。已有本地展示偏好时,仍可在视图设置中切换到 Tab。
2195
2223
 
2196
2224
  ### 自动标签接入
2197
2225
 
@@ -2278,7 +2306,7 @@ function EditTags({
2278
2306
 
2279
2307
  独立使用 `TagFilter` 时,在 `AutoCrudProvider` 内传入 `resourceId="crm.customers"` 和 `field="tags"`,默认显示工具栏按钮;表格内部可省略 `resourceId`,自动继承表格的 CRUD id。嵌入自有条件行可传 `variant="input"`;`id` 和 `className` 对应输入样式的触发按钮,`open/onOpenChange` 可控制其展开状态。值仍通过 `value/onChange` 控制,不需要转换成普通多选数组。
2280
2308
 
2281
- `useAutoCrudResource` 返回 `metadataPending`,`AutoCrudTable` 据此等待字段元数据就绪,再恢复上次视图,避免把尚未加载的扩展字段当成已删除字段。自定义资源应在异步字段加载期间提供 `metadataPending: true`;独立 `AutoTable` 使用 `views={{ fieldsReady: false }}` 暂停恢复和视图切换,字段加载完成后改为 `true`。未提供这些状态时沿用同步字段行为。
2309
+ `useAutoCrudResource` 返回 `metadataPending`,`AutoCrudTable` 据此自动等待字段元数据就绪,再恢复上次视图,通常无需配置 `fieldsReady`。自定义资源应在异步字段加载期间提供 `metadataPending: true`,避免把尚未加载的扩展字段当成已删除字段。独立 `AutoTable` 没有资源元数据状态,异步加载字段时使用 `views={{ fieldsReady: false }}` 暂停恢复和视图切换,字段加载完成后改为 `true`。未提供这些状态时沿用同步字段行为。
2282
2310
 
2283
2311
  ## 自定义全局搜索
2284
2312
 
package/dist/index.cjs CHANGED
@@ -1505,10 +1505,33 @@ function reorderSavedViews(views, ids) {
1505
1505
  let index$1 = 0;
1506
1506
  return views.map((view) => byId.has(view.id) ? byId.get(ids[index$1++]) : view);
1507
1507
  }
1508
+ const viewPreferenceIdsSchema = zod.z.array(zod.z.string().min(1)).optional().catch(void 0);
1508
1509
  const viewPreferencesSchema = zod.z.object({
1509
1510
  selector: zod.z.enum(["dropdown", "tabs"]),
1510
- viewId: zod.z.string().min(1).nullable()
1511
+ viewId: zod.z.string().min(1).nullable(),
1512
+ viewOrder: viewPreferenceIdsSchema,
1513
+ hiddenViewIds: viewPreferenceIdsSchema,
1514
+ hiddenPresetIds: viewPreferenceIdsSchema
1515
+ }).transform(({ hiddenPresetIds,...preferences }) => {
1516
+ if (preferences.hiddenViewIds === void 0 && hiddenPresetIds !== void 0) return {
1517
+ ...preferences,
1518
+ hiddenViewIds: hiddenPresetIds
1519
+ };
1520
+ return preferences;
1511
1521
  });
1522
+ /** Ignore removed/duplicate IDs and append new views in their configured order. */
1523
+ function orderViews(views, ids = []) {
1524
+ const remaining = new Map(views.map((view) => [view.id, view]));
1525
+ const ordered = [];
1526
+ for (const id of ids) {
1527
+ const view = remaining.get(id);
1528
+ if (view) {
1529
+ ordered.push(view);
1530
+ remaining.delete(id);
1531
+ }
1532
+ }
1533
+ return [...ordered, ...remaining.values()];
1534
+ }
1512
1535
  function viewPreferencesKey(scope, resourceId) {
1513
1536
  return `auto-crud:view-preferences:${encodeURIComponent(scope)}:${encodeURIComponent(resourceId)}`;
1514
1537
  }
@@ -1603,28 +1626,43 @@ function suggestSlug(name, views) {
1603
1626
  while (references.has(slug)) slug = `${base}-${suffix++}`;
1604
1627
  return slug;
1605
1628
  }
1606
- function SavedViews({ resourceId, table, fieldsReady, defaultView, presets, scopeOptions, selector, tabs, counts, onApply, children }) {
1629
+ function SavedViews(props) {
1607
1630
  const config = useAutoCrudConfig();
1608
1631
  if (!config?.viewStorage) throw new Error("Saved views require AutoCrudProvider.viewStorage");
1609
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ViewMenu, {
1610
- resourceId,
1611
- table,
1612
- fieldsReady,
1613
- defaultView,
1614
- presets,
1615
- scopeOptions,
1616
- onApply,
1617
- selector,
1618
- tabs,
1619
- counts,
1620
- children
1621
- }, `${config.cacheKey}:${resourceId}`);
1632
+ return /* @__PURE__ */ (0, react.createElement)(ViewMenu, {
1633
+ ...props,
1634
+ key: `${config.cacheKey}:${props.resourceId}`
1635
+ });
1622
1636
  }
1623
- function ViewMenu({ resourceId, table, fieldsReady = true, defaultView, presets, scopeOptions, selector = "dropdown", tabs, counts, onApply, children }) {
1637
+ function ViewMenu({ resourceId, table, fieldsReady = true, defaultView, presets, hiddenViews, scopeOptions, selector = "dropdown", tabs, counts, onApply, children }) {
1624
1638
  const config = useAutoCrudConfig();
1625
1639
  const storage = config.viewStorage;
1626
1640
  const client = (0, __tanstack_react_query.useQueryClient)();
1627
1641
  const scopeLabelId = (0, react.useId)();
1642
+ const [display, setDisplay] = (0, react.useState)(selector);
1643
+ const [preferences, setPreferences] = (0, react.useState)();
1644
+ const preferencesRef = (0, react.useRef)(null);
1645
+ (0, react.useEffect)(() => {
1646
+ const stored = readViewPreferences(config.scope, resourceId);
1647
+ preferencesRef.current = stored;
1648
+ setPreferences(stored);
1649
+ if (stored) setDisplay(stored.selector);
1650
+ }, [config.scope, resourceId]);
1651
+ const savePreferences = (0, react.useCallback)((patch) => {
1652
+ const next = {
1653
+ selector,
1654
+ viewId: null,
1655
+ ...preferencesRef.current,
1656
+ ...patch
1657
+ };
1658
+ preferencesRef.current = next;
1659
+ setPreferences(next);
1660
+ writeViewPreferences(config.scope, resourceId, next);
1661
+ }, [
1662
+ config.scope,
1663
+ resourceId,
1664
+ selector
1665
+ ]);
1628
1666
  const queryKey = [
1629
1667
  "auto-crud-views",
1630
1668
  config.cacheKey,
@@ -1658,10 +1696,20 @@ function ViewMenu({ resourceId, table, fieldsReady = true, defaultView, presets,
1658
1696
  };
1659
1697
  }
1660
1698
  }, [presets, views.data]);
1661
- const viewItems = collection.items;
1662
- const savedItems = (0, react.useMemo)(() => viewItems.filter((view) => view.source === "saved"), [viewItems]);
1663
- const reorderableViews = savedItems.filter((view) => getViewCapabilities(view).reorder);
1664
- const ready = views.isSuccess && collection.error === null;
1699
+ const allViewItems = (0, react.useMemo)(() => orderViews(collection.items, preferences?.viewOrder), [collection.items, preferences?.viewOrder]);
1700
+ const configuredHiddenIds = (0, react.useMemo)(() => {
1701
+ const references = new Set(hiddenViews);
1702
+ return new Set(allViewItems.filter((view) => references.has(view.id) || view.slug !== void 0 && references.has(view.slug)).map((view) => view.id));
1703
+ }, [allViewItems, hiddenViews]);
1704
+ const hiddenItems = (0, react.useMemo)(() => allViewItems.filter((view) => configuredHiddenIds.has(view.id) || preferences?.hiddenViewIds?.includes(view.id)), [
1705
+ allViewItems,
1706
+ configuredHiddenIds,
1707
+ preferences?.hiddenViewIds
1708
+ ]);
1709
+ const restorableViews = (0, react.useMemo)(() => hiddenItems.filter((view) => !configuredHiddenIds.has(view.id)), [hiddenItems, configuredHiddenIds]);
1710
+ const viewItems = (0, react.useMemo)(() => allViewItems.filter((view) => !hiddenItems.includes(view)), [allViewItems, hiddenItems]);
1711
+ const savedItems = (0, react.useMemo)(() => collection.items.filter((view) => view.source === "saved"), [collection.items]);
1712
+ const ready = views.isSuccess && collection.error === null && preferences !== void 0;
1665
1713
  const countQueries = (0, react.useMemo)(() => [{
1666
1714
  key: "all",
1667
1715
  filters: [],
@@ -1728,7 +1776,7 @@ function ViewMenu({ resourceId, table, fieldsReady = true, defaultView, presets,
1728
1776
  const valid = typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
1729
1777
  const label = totals.isPending ? "数量加载中" : totals.isError ? "统计失败" : valid ? `${value} 条记录` : "暂无统计";
1730
1778
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1731
- className: "text-muted-foreground tabular-nums",
1779
+ className: "text-xs text-muted-foreground tabular-nums",
1732
1780
  "aria-label": label,
1733
1781
  title: label,
1734
1782
  children: totals.isPending ? "…" : totals.isError || !valid ? "—" : value
@@ -1739,13 +1787,6 @@ function ViewMenu({ resourceId, table, fieldsReady = true, defaultView, presets,
1739
1787
  const setSelected = (view) => setSelectedId(view?.id ?? null);
1740
1788
  const canUpdate = selected?.source === "saved" && getViewCapabilities(selected).update;
1741
1789
  const canDelete = selected?.source === "saved" && getViewCapabilities(selected).delete;
1742
- const [display, setDisplay] = (0, react.useState)(selector);
1743
- const initialPreferences = (0, react.useRef)(null);
1744
- (0, react.useEffect)(() => {
1745
- const preferences = readViewPreferences(config.scope, resourceId);
1746
- initialPreferences.current = preferences;
1747
- if (preferences) setDisplay(preferences.selector);
1748
- }, [config.scope, resourceId]);
1749
1790
  const [dialog, setDialog] = (0, react.useState)(null);
1750
1791
  const [name, setName] = (0, react.useState)("");
1751
1792
  const [slug, setSlug] = (0, react.useState)("");
@@ -1809,25 +1850,48 @@ function ViewMenu({ resourceId, table, fieldsReady = true, defaultView, presets,
1809
1850
  onApply,
1810
1851
  viewKey
1811
1852
  ]);
1853
+ (0, react.useEffect)(() => {
1854
+ if (!ready || !fieldsReady || selectedId === null || !hiddenItems.some((view) => view.id === selectedId)) return;
1855
+ try {
1856
+ apply({
1857
+ filters: [],
1858
+ joinOperator: "and",
1859
+ search: "",
1860
+ sort: []
1861
+ }, "replace", "");
1862
+ setSelectedId(null);
1863
+ savePreferences({ viewId: null });
1864
+ setError(null);
1865
+ } catch (reason) {
1866
+ setError(reason instanceof Error ? reason.message : String(reason));
1867
+ }
1868
+ }, [
1869
+ ready,
1870
+ fieldsReady,
1871
+ selectedId,
1872
+ hiddenItems,
1873
+ apply,
1874
+ savePreferences
1875
+ ]);
1812
1876
  const preferencesRestored = (0, react.useRef)(false);
1813
1877
  (0, react.useEffect)(() => {
1814
- if (!ready || !fieldsReady) return;
1878
+ if (!ready || !fieldsReady || preferences === void 0) return;
1815
1879
  const first = !preferencesRestored.current;
1816
1880
  const urlReference = getUrlParams().get(viewKey);
1817
- if (!first && urlReference === lastViewParam.current) return;
1881
+ if (!first && urlReference === lastViewParam.current && (selectedId !== null || !viewItems.some((view) => view.id === urlReference || view.slug === urlReference))) return;
1818
1882
  const savedViews = viewItems;
1819
1883
  let cancelled = false;
1820
1884
  queueMicrotask(() => {
1821
1885
  if (cancelled) return;
1822
1886
  preferencesRestored.current = true;
1823
1887
  lastViewParam.current = urlReference;
1824
- const preferences = initialPreferences.current;
1888
+ const storedPreferences = preferencesRef.current;
1825
1889
  let reference = urlReference;
1826
- if (first && urlReference === null) reference = preferences ? preferences.viewId : defaultView ?? null;
1890
+ if (first && urlReference === null) reference = storedPreferences ? storedPreferences.viewId : defaultView ?? null;
1827
1891
  if (reference === null || reference === "") {
1828
1892
  setSelected(null);
1829
1893
  setError(null);
1830
- if (!first || urlReference !== null) writeViewPreferences(config.scope, resourceId, {
1894
+ if (!first || urlReference !== null) savePreferences({
1831
1895
  selector: display,
1832
1896
  viewId: null
1833
1897
  });
@@ -1853,22 +1917,24 @@ function ViewMenu({ resourceId, table, fieldsReady = true, defaultView, presets,
1853
1917
  setSearchParams(params, { history: "replace" });
1854
1918
  }
1855
1919
  setSelected(view);
1856
- writeViewPreferences(config.scope, resourceId, {
1920
+ savePreferences({
1857
1921
  selector: display,
1858
1922
  viewId: view.id
1859
1923
  });
1860
1924
  setError(null);
1861
1925
  return;
1862
1926
  }
1863
- if (urlReference !== null && urlReference !== "") setError("链接指定的视图不存在或不可用");
1927
+ if (urlReference !== null && urlReference !== "") {
1928
+ const hiddenView = hiddenItems.find((item) => item.id === reference || item.slug === reference);
1929
+ let message = "链接指定的视图不存在或不可用";
1930
+ if (hiddenView) message = configuredHiddenIds.has(hiddenView.id) ? "此视图已由页面配置隐藏" : "此视图已隐藏,请在视图设置中恢复";
1931
+ setError(message);
1932
+ }
1864
1933
  } catch (reason) {
1865
1934
  setError(reason instanceof Error ? reason.message : String(reason));
1866
1935
  }
1867
1936
  setSelected(null);
1868
- if (first && urlReference === null && preferences) writeViewPreferences(config.scope, resourceId, {
1869
- ...preferences,
1870
- viewId: null
1871
- });
1937
+ if (first && urlReference === null && storedPreferences) savePreferences({ viewId: null });
1872
1938
  });
1873
1939
  return () => {
1874
1940
  cancelled = true;
@@ -1886,17 +1952,16 @@ function ViewMenu({ resourceId, table, fieldsReady = true, defaultView, presets,
1886
1952
  search,
1887
1953
  viewKey,
1888
1954
  defaultView,
1889
- display
1955
+ display,
1956
+ preferences,
1957
+ selectedId,
1958
+ hiddenItems,
1959
+ configuredHiddenIds,
1960
+ savePreferences
1890
1961
  ]);
1891
1962
  const mutation = (0, __tanstack_react_query.useMutation)({
1892
1963
  mutationFn: async (request) => {
1893
1964
  if (!ready) throw new Error("请等待视图列表加载成功");
1894
- if (request.operation === "reorder") {
1895
- if (!storage.reorder) throw new Error("存储服务不支持视图排序");
1896
- reorderSavedViews(savedItems, request.ids);
1897
- await storage.reorder(resourceId, request.ids);
1898
- return null;
1899
- }
1900
1965
  if (request.operation === "delete") {
1901
1966
  if (!canDelete || selected?.source !== "saved") throw new Error("无权删除此视图");
1902
1967
  await storage.remove(resourceId, selected.id);
@@ -1918,28 +1983,26 @@ function ViewMenu({ resourceId, table, fieldsReady = true, defaultView, presets,
1918
1983
  resolveViews(presets ?? [], [...remaining, result]);
1919
1984
  return result;
1920
1985
  },
1921
- onSuccess: async (view, request) => {
1986
+ onSuccess: async (view) => {
1922
1987
  if (view) client.setQueryData(queryKey, (cached = []) => cached.some((item) => item.id === view.id) ? cached.map((item) => item.id === view.id ? view : item) : [...cached, view]);
1923
- if (request.operation !== "reorder") {
1924
- if (!view) apply({
1925
- filters: [],
1926
- joinOperator: "and",
1927
- search: "",
1928
- sort: []
1929
- }, "push", "");
1930
- else {
1931
- const reference = view.slug ?? view.id;
1932
- const params = getUrlParams();
1933
- params.set(viewKey, reference);
1934
- lastViewParam.current = reference;
1935
- setSearchParams(params, { history: "replace" });
1936
- }
1937
- setSelected(view);
1938
- writeViewPreferences(config.scope, resourceId, {
1939
- selector: display,
1940
- viewId: view?.id ?? null
1941
- });
1988
+ if (!view) apply({
1989
+ filters: [],
1990
+ joinOperator: "and",
1991
+ search: "",
1992
+ sort: []
1993
+ }, "push", "");
1994
+ else {
1995
+ const reference = view.slug ?? view.id;
1996
+ const params = getUrlParams();
1997
+ params.set(viewKey, reference);
1998
+ lastViewParam.current = reference;
1999
+ setSearchParams(params, { history: "replace" });
1942
2000
  }
2001
+ setSelected(view);
2002
+ savePreferences({
2003
+ selector: display,
2004
+ viewId: view?.id ?? null
2005
+ });
1943
2006
  setDialog(null);
1944
2007
  setError(null);
1945
2008
  await Promise.all([client.invalidateQueries({ queryKey }), client.invalidateQueries({ queryKey: [
@@ -1959,23 +2022,42 @@ function ViewMenu({ resourceId, table, fieldsReady = true, defaultView, presets,
1959
2022
  sort: []
1960
2023
  }, "push", view?.slug ?? view?.id ?? "");
1961
2024
  setSelected(view);
1962
- writeViewPreferences(config.scope, resourceId, {
2025
+ savePreferences({
1963
2026
  selector: display,
1964
2027
  viewId: view?.id ?? null
1965
2028
  });
1966
2029
  setError(null);
2030
+ return true;
1967
2031
  } catch (reason) {
1968
2032
  setError(reason instanceof Error ? reason.message : String(reason));
2033
+ return false;
1969
2034
  }
1970
2035
  };
2036
+ const hideView = () => {
2037
+ if (!ready || !selected) return;
2038
+ const { id } = selected;
2039
+ if (!select(null)) return;
2040
+ savePreferences({ hiddenViewIds: [...new Set([...preferencesRef.current?.hiddenViewIds ?? [], id])] });
2041
+ };
2042
+ const saveOrder = () => {
2043
+ const ids = orderedViews.map((view) => view.id);
2044
+ if (!ready || ids.length !== viewItems.length || new Set(ids).size !== ids.length || ids.some((id) => !viewItems.some((view) => view.id === id))) {
2045
+ setError("视图列表已变化,请取消后重新调整顺序");
2046
+ return;
2047
+ }
2048
+ let index$1 = 0;
2049
+ savePreferences({ viewOrder: allViewItems.map((view) => hiddenItems.includes(view) ? view.id : ids[index$1++]) });
2050
+ setDialog(null);
2051
+ setError(null);
2052
+ };
1971
2053
  const open = (operation) => {
1972
2054
  setName(operation === "create" ? "" : selected?.name ?? "");
1973
2055
  let nextSlug = "";
1974
2056
  if (operation === "rename") nextSlug = selected?.slug ?? "";
1975
- else if (operation === "duplicate") nextSlug = suggestSlug(selected?.name ?? "", viewItems);
2057
+ else if (operation === "duplicate") nextSlug = suggestSlug(selected?.name ?? "", allViewItems);
1976
2058
  setSlug(nextSlug);
1977
2059
  setScope(availableScopeOptions[0]?.value ?? createScopes[0]);
1978
- if (operation === "reorder") setOrderedViews(reorderableViews);
2060
+ if (operation === "reorder") setOrderedViews(viewItems);
1979
2061
  setError(null);
1980
2062
  setDialog(operation);
1981
2063
  };
@@ -2020,7 +2102,7 @@ function ViewMenu({ resourceId, table, fieldsReady = true, defaultView, presets,
2020
2102
  ]
2021
2103
  }), viewItems.map((view, index$1) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__wordrhyme_shadcn.ToggleGroupItem, {
2022
2104
  value: `view:${view.id}`,
2023
- title: view.source === "preset" ? `${view.name}(固定视图)` : view.name,
2105
+ title: view.source === "preset" ? `${view.name}(预设视图)` : view.name,
2024
2106
  className: tabClassName,
2025
2107
  children: [
2026
2108
  view.name,
@@ -2089,20 +2171,36 @@ function ViewMenu({ resourceId, table, fieldsReady = true, defaultView, presets,
2089
2171
  canDelete && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__wordrhyme_shadcn.DropdownMenuItem, {
2090
2172
  onSelect: () => open("delete"),
2091
2173
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Trash2, { className: "size-4" }), "删除视图"]
2174
+ }),
2175
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__wordrhyme_shadcn.DropdownMenuItem, {
2176
+ disabled: !ready,
2177
+ onSelect: hideView,
2178
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.EyeOff, { className: "size-4" }), "隐藏视图"]
2092
2179
  })
2093
2180
  ] }),
2094
2181
  selected && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.DropdownMenuSeparator, {}),
2095
- storage.reorder && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__wordrhyme_shadcn.DropdownMenuItem, {
2096
- disabled: !ready || reorderableViews.length < 2,
2182
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__wordrhyme_shadcn.DropdownMenuItem, {
2183
+ disabled: !ready || viewItems.length < 2,
2097
2184
  onSelect: () => open("reorder"),
2098
2185
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ArrowDownUp, { className: "size-4" }), "调整顺序"]
2099
2186
  }),
2187
+ restorableViews.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__wordrhyme_shadcn.DropdownMenuSub, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__wordrhyme_shadcn.DropdownMenuSubTrigger, {
2188
+ disabled: !ready,
2189
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Eye, { className: "size-4" }), "恢复隐藏视图"]
2190
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.DropdownMenuSubContent, { children: restorableViews.map((view) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__wordrhyme_shadcn.DropdownMenuItem, {
2191
+ onSelect: () => {
2192
+ preferencesRestored.current = false;
2193
+ savePreferences({ hiddenViewIds: preferencesRef.current?.hiddenViewIds?.filter((id) => id !== view.id) });
2194
+ setError(null);
2195
+ },
2196
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Eye, { className: "size-4" }), view.name]
2197
+ }, view.id)) })] }),
2100
2198
  /* @__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, {
2101
2199
  value: display,
2102
2200
  onValueChange: (value) => {
2103
2201
  if (value === "dropdown" || value === "tabs") {
2104
2202
  setDisplay(value);
2105
- writeViewPreferences(config.scope, resourceId, {
2203
+ savePreferences({
2106
2204
  selector: value,
2107
2205
  viewId: selected?.id ?? null
2108
2206
  });
@@ -2205,10 +2303,7 @@ function ViewMenu({ resourceId, table, fieldsReady = true, defaultView, presets,
2205
2303
  return;
2206
2304
  }
2207
2305
  if (dialog === "reorder") {
2208
- mutation.mutate({
2209
- operation: "reorder",
2210
- ids: orderedViews.map((view) => view.id)
2211
- });
2306
+ saveOrder();
2212
2307
  return;
2213
2308
  }
2214
2309
  if (dialog === "delete") {
@@ -2236,7 +2331,7 @@ function ViewMenu({ resourceId, table, fieldsReady = true, defaultView, presets,
2236
2331
  className: "grid gap-3",
2237
2332
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2238
2333
  className: "text-sm text-muted-foreground",
2239
- children: "拖动手柄调整可排序的已保存视图;预设和其他视图保持原位。保存后生效。"
2334
+ children: "拖动手柄调整所有可见视图的显示顺序,仅对自己生效。“全部”固定在最前。保存后生效。"
2240
2335
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn_ui.Sortable, {
2241
2336
  value: orderedViews,
2242
2337
  onValueChange: setOrderedViews,
@@ -2292,7 +2387,7 @@ function ViewMenu({ resourceId, table, fieldsReady = true, defaultView, presets,
2292
2387
  value: name,
2293
2388
  onChange: (event) => {
2294
2389
  const { value } = event.target;
2295
- if (dialog !== "rename" && slug === suggestSlug(name, viewItems)) setSlug(suggestSlug(value, viewItems));
2390
+ if (dialog !== "rename" && slug === suggestSlug(name, allViewItems)) setSlug(suggestSlug(value, allViewItems));
2296
2391
  setName(value);
2297
2392
  },
2298
2393
  maxLength: 40,
@@ -7975,16 +8070,10 @@ function AutoTable({ resourceId, views, schema, data, getRowId, expandedRow, def
7975
8070
  className: "space-y-4",
7976
8071
  children: [
7977
8072
  viewsEnabled && activeResourceId ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SavedViews, {
8073
+ ...typeof views === "object" ? views : {},
7978
8074
  resourceId: activeResourceId,
7979
8075
  table,
7980
8076
  onApply: clearBatchSearch,
7981
- fieldsReady: typeof views === "object" ? views.fieldsReady : void 0,
7982
- defaultView: typeof views === "object" ? views.defaultView : void 0,
7983
- presets: typeof views === "object" ? views.presets : void 0,
7984
- scopeOptions: typeof views === "object" ? views.scopeOptions : void 0,
7985
- selector: typeof views === "object" ? views.selector : void 0,
7986
- tabs: typeof views === "object" ? views.tabs : void 0,
7987
- counts: typeof views === "object" ? views.counts : void 0,
7988
8077
  children: (controls, display) => display === "tabs" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7989
8078
  className: "space-y-2",
7990
8079
  children: [controls, renderToolbar()]