wcz-layout 9.4.2 → 9.4.3

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.
@@ -3,12 +3,14 @@ name: client-db
3
3
  description: "Use when: creating or modifying TanStack DB collections, live queries, optimistic collection writes, subset loading, collection preloading."
4
4
  ---
5
5
 
6
+ > Mechanics (`createCollection`, `queryCollectionOptions`, the live-query builder, `createOptimisticAction`) are covered by TanStack's own skills — load `@tanstack/db#db-core/live-queries`, `#db-core/mutations-optimistic`, and `@tanstack/react-db` for the API details. This skill adds only the wcz-layout conventions on top.
7
+
6
8
  ## Rules
7
9
 
8
10
  - Use `eager` syncMode for top-level collections, `on-demand` for child/relational data.
9
11
  - Use `useLiveQuery` for list/grid pages and `useLiveSuspenseQuery` for detail components wrapped in suspense.
10
12
  - For mutations use `createOptimisticAction` — mirror the server mutation in `onMutate`, then call the server function and `collection.utils.refetch()` in `mutationFn`.
11
- - Use `meta.loadSubsetOptions` for relational subset loading.
13
+ - For on-demand subset loading, convert `meta.loadSubsetOptions` with `fromTanDb()` (from `agnostic-query/tanstack-db`) before passing it to the server fn, and set `autoIndex: "eager"` with `defaultIndexType: BasicIndex` (from `@tanstack/react-db`).
12
14
 
13
15
  ## File Placement
14
16
 
@@ -34,15 +36,20 @@ export const librariesCollection = createCollection(
34
36
  }),
35
37
  );
36
38
 
37
- // src/db-collections/<feature>.ts
39
+ // src/db-collections/<feature>.ts — child/relational (on-demand)
40
+ import { BasicIndex } from "@tanstack/react-db";
41
+ import { fromTanDb } from "agnostic-query/tanstack-db";
42
+
38
43
  export const bookCollection = createCollection(
39
44
  queryCollectionOptions({
40
45
  queryKey: ["books"],
41
- queryFn: ({ meta }) => selectBooks({ data: meta?.loadSubsetOptions }),
46
+ queryFn: ({ meta }) => selectBooks({ data: fromTanDb(meta?.loadSubsetOptions) }),
42
47
  getKey: ({ id }) => id,
43
48
  schema: BookSchema,
44
49
  queryClient: queryClient,
45
50
  syncMode: "on-demand",
51
+ autoIndex: "eager",
52
+ defaultIndexType: BasicIndex,
46
53
  }),
47
54
  );
48
55
 
@@ -1,142 +1,142 @@
1
- ---
2
- name: data-grid
3
- description: "Use when: building or configuring MUI X DataGrid tables, editable columns, toolbar actions, row selection, or TanStack DB-backed grids."
4
- ---
5
-
6
- ## Rules
7
-
8
- - Wrap DataGrid in `Fullscreen` when it is the only content on the page.
9
- - Type columns as `Array<GridColDef<RowType>>` with the row interface.
10
- - Use translation for all `headerName` values.
11
- - Use `rows`, `columns`, `showToolbar`, `loading`, `ignoreDiacritics`, `cellSelection`, and `disableRowSelectionOnClick` on grids.
12
- - For enums use `type: "singleSelect"` with `{ value, label }` options array used from enumObject.
13
- - For editable columns, pair `editable: true` with `renderHeader: EditableColumnHeader`.
14
- - Use `EditableColumnHeader`, and router button/grid action components from `wcz-layout/components` when they fit.
15
- - Always set `valueFormatter: (value) => dayjs(value).format("L LT")` for columns type `dateTime` or `format("L")` for columns type `date`.
16
-
17
- ## File Placement
18
-
19
- ```
20
- @mui/x-data-grid-premium — GridColDef, DataGridPremium
21
- src/db-collections/ - TanStack DB collections to get data
22
- @tanstack/react-db - useLiveQuery, useLiveSuspenseQuery
23
- src/server/actions/ - server functions and enums
24
- wcz-layout/components - EditableColumnHeader, Fullscreen
25
- wcz-layout/hooks - useTranslation
26
- ```
27
-
28
- ## Examples
29
-
30
- ```tsx
31
- // src/routes/<feature>s/index.tsx
32
- const { t } = useTranslation();
33
- const { confirm, alert } = useDialogs();
34
- const navigate = useNavigate();
35
- const [cellSelectionModel, setCellSelectionModel] = useState<GridCellSelectionModel>({});
36
-
37
- const { data, isLoading } = useLiveQuery((q) =>
38
- q.from({ feature: featureCollection }).orderBy(({ feature }) => feature.name, "asc"),
39
- );
40
-
41
- const columns: Array<GridColDef<Feature>> = [
42
- {
43
- field: "name",
44
- headerName: t("Feature.Name"),
45
- width: 200,
46
- editable: true,
47
- renderHeader: EditableColumnHeader,
48
- },
49
- {
50
- field: "status",
51
- headerName: t("Feature.Status"),
52
- width: 200,
53
- type: "singleSelect",
54
- valueOptions: featureStatusEnum.enumValues.map((status) => ({
55
- value: status,
56
- label: t(`FeatureStatus.${status}`),
57
- })),
58
- },
59
- ];
60
-
61
- const handleOnDelete = createOptimisticAction<Array<string>>({
62
- onMutate: (ids) => {
63
- ids.forEach((id) => {
64
- featureCollection.delete(id);
65
- });
66
- },
67
- mutationFn: async (ids) => {
68
- deleteFeatures({ data: ids });
69
- await featureCollection.utils.refetch();
70
- },
71
- });
72
-
73
- return (
74
- <Fullscreen>
75
- <DataGridPremium
76
- rows={data}
77
- columns={columns}
78
- showToolbar
79
- loading={isLoading}
80
- ignoreDiacritics
81
- onRowDoubleClick={({ row }) => navigate({ to: "/features/$id", params: { id: row.id } })}
82
- cellSelection
83
- disableRowSelectionOnClick
84
- cellSelectionModel={cellSelectionModel}
85
- onCellSelectionModelChange={(newModel) => setCellSelectionModel(newModel)}
86
- slots={{ toolbar: DataGridToolbar }}
87
- slotProps={{
88
- toolbar: {
89
- title: t("Feature.Features"),
90
- actions: [
91
- <Tooltip key="create" title={t("Create")}>
92
- <RouterIconButton to="/features/create">
93
- <Add fontSize="small" />
94
- </RouterIconButton>
95
- </Tooltip>,
96
- Object.keys(cellSelectionModel).length === 1 && (
97
- <Tooltip key="edit" title={t("Edit")}>
98
- <RouterIconButton
99
- to="/features/edit/$id"
100
- params={{ id: Object.keys(cellSelectionModel)[0].toString() }}
101
- >
102
- <Edit fontSize="small" />
103
- </RouterIconButton>
104
- </Tooltip>
105
- ),
106
- Object.keys(cellSelectionModel).length > 0 && (
107
- <Tooltip key="delete" title={t("Delete")}>
108
- <IconButton
109
- onClick={async () => {
110
- const confirmed = await confirm(
111
- t("DeleteConfirmation", { count: Object.keys(cellSelectionModel).length }),
112
- );
113
- if (confirmed) {
114
- try {
115
- const transaction = handleOnDelete(
116
- Object.keys(cellSelectionModel).map((id) => id.toString()),
117
- );
118
- await transaction.isPersisted.promise;
119
- setCellSelectionModel({});
120
- } catch (error) {
121
- if (error instanceof Error) await alert(error.message);
122
- }
123
- }
124
- }}
125
- >
126
- <Badge
127
- badgeContent={Object.keys(cellSelectionModel).length}
128
- invisible={Object.keys(cellSelectionModel).length <= 1}
129
- color="error"
130
- >
131
- <Delete fontSize="small" />
132
- </Badge>
133
- </IconButton>
134
- </Tooltip>
135
- ),
136
- ],
137
- },
138
- }}
139
- />
140
- </Fullscreen>
141
- );
142
- ```
1
+ ---
2
+ name: data-grid
3
+ description: "Use when: building or configuring MUI X DataGrid tables, editable columns, toolbar actions, row selection, or TanStack DB-backed grids."
4
+ ---
5
+
6
+ ## Rules
7
+
8
+ - Wrap DataGrid in `Fullscreen` when it is the only content on the page.
9
+ - Type columns as `Array<GridColDef<RowType>>` with the row interface.
10
+ - Use translation for all `headerName` values.
11
+ - Use `rows`, `columns`, `showToolbar`, `loading`, `ignoreDiacritics`, `cellSelection`, and `disableRowSelectionOnClick` on grids.
12
+ - For enums use `type: "singleSelect"` with `{ value, label }` options array used from enumObject.
13
+ - For editable columns, pair `editable: true` with `renderHeader: EditableColumnHeader`.
14
+ - Use `EditableColumnHeader`, and router button/grid action components from `wcz-layout/components` when they fit.
15
+ - Always set `valueFormatter: (value) => dayjs(value).format("L LT")` for columns type `dateTime` or `format("L")` for columns type `date`.
16
+
17
+ ## File Placement
18
+
19
+ ```
20
+ @mui/x-data-grid-premium — GridColDef, DataGridPremium
21
+ src/db-collections/ - TanStack DB collections to get data
22
+ @tanstack/react-db - useLiveQuery, useLiveSuspenseQuery
23
+ src/server/actions/ - server functions and enums
24
+ wcz-layout/components - EditableColumnHeader, Fullscreen
25
+ wcz-layout/hooks - useTranslation
26
+ ```
27
+
28
+ ## Examples
29
+
30
+ ```tsx
31
+ // src/routes/<feature>s/index.tsx
32
+ const { t } = useTranslation();
33
+ const { confirm, alert } = useDialogs();
34
+ const navigate = useNavigate();
35
+ const [cellSelectionModel, setCellSelectionModel] = useState<GridCellSelectionModel>({});
36
+
37
+ const { data, isLoading } = useLiveQuery((q) =>
38
+ q.from({ feature: featureCollection }).orderBy(({ feature }) => feature.name, "asc"),
39
+ );
40
+
41
+ const columns: Array<GridColDef<Feature>> = [
42
+ {
43
+ field: "name",
44
+ headerName: t("Feature.Name"),
45
+ width: 200,
46
+ editable: true,
47
+ renderHeader: EditableColumnHeader,
48
+ },
49
+ {
50
+ field: "status",
51
+ headerName: t("Feature.Status"),
52
+ width: 200,
53
+ type: "singleSelect",
54
+ valueOptions: featureStatusEnum.enumValues.map((status) => ({
55
+ value: status,
56
+ label: t(`FeatureStatus.${status}`),
57
+ })),
58
+ },
59
+ ];
60
+
61
+ const handleOnDelete = createOptimisticAction<Array<string>>({
62
+ onMutate: (ids) => {
63
+ ids.forEach((id) => {
64
+ featureCollection.delete(id);
65
+ });
66
+ },
67
+ mutationFn: async (ids) => {
68
+ deleteFeatures({ data: ids });
69
+ await featureCollection.utils.refetch();
70
+ },
71
+ });
72
+
73
+ return (
74
+ <Fullscreen>
75
+ <DataGridPremium
76
+ rows={data}
77
+ columns={columns}
78
+ showToolbar
79
+ loading={isLoading}
80
+ ignoreDiacritics
81
+ onRowDoubleClick={({ row }) => navigate({ to: "/features/$id", params: { id: row.id } })}
82
+ cellSelection
83
+ disableRowSelectionOnClick
84
+ cellSelectionModel={cellSelectionModel}
85
+ onCellSelectionModelChange={(newModel) => setCellSelectionModel(newModel)}
86
+ slots={{ toolbar: DataGridToolbar }}
87
+ slotProps={{
88
+ toolbar: {
89
+ title: t("Feature.Features"),
90
+ actions: [
91
+ <Tooltip key="create" title={t("Create")}>
92
+ <RouterIconButton to="/features/create">
93
+ <Add fontSize="small" />
94
+ </RouterIconButton>
95
+ </Tooltip>,
96
+ Object.keys(cellSelectionModel).length === 1 && (
97
+ <Tooltip key="edit" title={t("Edit")}>
98
+ <RouterIconButton
99
+ to="/features/edit/$id"
100
+ params={{ id: Object.keys(cellSelectionModel)[0].toString() }}
101
+ >
102
+ <Edit fontSize="small" />
103
+ </RouterIconButton>
104
+ </Tooltip>
105
+ ),
106
+ Object.keys(cellSelectionModel).length > 0 && (
107
+ <Tooltip key="delete" title={t("Delete")}>
108
+ <IconButton
109
+ onClick={async () => {
110
+ const confirmed = await confirm(
111
+ t("DeleteConfirmation", { count: Object.keys(cellSelectionModel).length }),
112
+ );
113
+ if (confirmed) {
114
+ try {
115
+ const transaction = handleOnDelete(
116
+ Object.keys(cellSelectionModel).map((id) => id.toString()),
117
+ );
118
+ await transaction.isPersisted.promise;
119
+ setCellSelectionModel({});
120
+ } catch (error) {
121
+ if (error instanceof Error) await alert(error.message);
122
+ }
123
+ }
124
+ }}
125
+ >
126
+ <Badge
127
+ badgeContent={Object.keys(cellSelectionModel).length}
128
+ invisible={Object.keys(cellSelectionModel).length <= 1}
129
+ color="error"
130
+ >
131
+ <Delete fontSize="small" />
132
+ </Badge>
133
+ </IconButton>
134
+ </Tooltip>
135
+ ),
136
+ ],
137
+ },
138
+ }}
139
+ />
140
+ </Fullscreen>
141
+ );
142
+ ```
@@ -8,8 +8,8 @@ description: "Use when: displaying success, error, warning, or informational fee
8
8
  - Use `useNotification()` for lightweight non-blocking user feedback.
9
9
  - Always use translation; add new keys for feature-specific messages.
10
10
  - Notifications are automatically queued by the provider and shown one at a time.
11
- - `autoHideDuration` defaults to 5000 ms; override it (up to 10000 ms) only for messages that need more reading time.
12
- - Use `severity: "success" | "info" | "warning" | "error"` consistently with the outcome.
11
+ - `autoHideDuration` defaults to 5000 ms and is not capped by the library; keep overrides reasonable (~10000 ms) for messages that need more reading time.
12
+ - Use `severity: "success" | "info" | "warning" | "error"` consistently with the outcome. Severity is optional — omitting it renders a plain snackbar (no colored `Alert`).
13
13
  - Use notifications for lightweight feedback; dialogs when the user must decide or acknowledge.
14
14
 
15
15
  ## File Placement
@@ -3,9 +3,11 @@ name: routing
3
3
  description: "Use when: creating or modifying TanStack Router routes, feature folders, navigation, permissions, loaders, suspense flows, or route-scoped components/hooks."
4
4
  ---
5
5
 
6
+ > Mechanics (`createFileRoute`, `beforeLoad`, `redirect`, `loader`, preloading, `pendingComponent`) are covered by TanStack's own skills — load `@tanstack/router-core#router-core/auth-and-guards`, `#router-core/data-loading`, and `#router-core/navigation` for the API details. This skill adds only the wcz-layout conventions on top.
7
+
6
8
  ## Rules
7
9
 
8
- - Always include `requirePermission("all")` in the route's `beforeLoad` function to enforce access control unless the route is meant to be public.
10
+ - Include the `requirePermission(<permissionKey>)` function in the route's `beforeLoad` to enforce access control pass a key defined in your app's `src/lib/auth/permissions.ts`; omit it only for intentionally public routes.
9
11
  - Use `Route.useRouteContext()` to access the authenticated user. User can be `null` if the route doesn't include `requirePermission`.
10
12
  - Use the route `loader` option to preload db collections. Preload only root-level collections.
11
13
  - Create a `pendingComponent` for routes that suspend or preload meaningful data.
@@ -15,6 +17,7 @@ description: "Use when: creating or modifying TanStack Router routes, feature fo
15
17
  - Colocate route-specific components/hooks inside `routes/<feature>/-components` or `-hooks`.
16
18
  - Root-level `components/` and `hooks/` are only for code shared across multiple routes.
17
19
  - After creating a new route, add or ask for a navigation item with a unique icon from `@mui/icons-material` in `src/routes/__root.tsx`.
20
+ - For navigation and redirects, always use the type-safe router components from `wcz-layout/components` (`RouterButton`, `RouterIconButton`, `RouterLink`, `RouterFab`, `RouterTab`, `RouterListItemButton`, `RouterGridActionsCellItem`) rather than raw MUI with a manual `href` — they wrap `createLink`, so `to`/`params`/`search` are type-checked.
18
21
 
19
22
  ## Base Project Structure
20
23
 
@@ -64,7 +67,7 @@ import { requirePermission, uuidv7 } from "wcz-layout/utils";
64
67
  // src/routes/features/create.tsx
65
68
  export const Route = createFileRoute("/features/create")({
66
69
  component: RouteComponent,
67
- beforeLoad: requirePermission("all"),
70
+ beforeLoad: requirePermission("admin"),
68
71
  pendingComponent: FeatureSkeleton,
69
72
  loader: () => {
70
73
  featureCollection.preload();
@@ -119,10 +122,11 @@ const navigation: Navigation = [
119
122
  },
120
123
  ];
121
124
 
122
- // Routes are public by default. Protect a route by adding requirePermission to
123
- // its beforeLoad it redirects to login when signed out, then checks the group.
125
+ // Routes are public unless guarded. By convention, protect every route by adding
126
+ // requirePermission to its beforeLoad (omit only for intentionally public routes)
127
+ // it redirects to login when signed out, then checks the permission group.
124
128
  export const Route = createFileRoute("/feature-name")({
125
129
  component: RouteComponent,
126
- beforeLoad: requirePermission("all"),
130
+ beforeLoad: requirePermission("admin"),
127
131
  });
128
132
  ```
@@ -3,13 +3,15 @@ name: server-functions
3
3
  description: "Use when: creating or modifying TanStack Start server functions, REST routes, middleware, validation, database transactions, external service calls, or permission checks."
4
4
  ---
5
5
 
6
+ > Mechanics (`createServerFn`, `.validator`, `createMiddleware`, `createHandlers`) are covered by TanStack's own skills — load `@tanstack/start-client-core#start-core/server-functions`, `#start-core/middleware`, and `#start-core/server-routes` for the API details. This skill adds only the wcz-layout middleware conventions on top.
7
+
6
8
  ## Rules
7
9
 
8
10
  - Before generating code, clarify whether the feature should be exposed as public REST API routes or implemented as internal TanStack Start server functions (recommended), and what permission should be required for access.
9
11
  - If permission is unknown, use `"all"`.
10
12
  - Every server function MUST include `authorizationMiddleware` — server functions are callable RPC endpoints, so a route-level `requirePermission` in `beforeLoad` only hides the UI and does not protect them.
11
- - Follow the chain order for server functions: `validator` → `middleware` → `handler`.
12
- - In middleware, always follow this order: `validationMiddleware` `authorizationMiddleware` `databaseMiddleware` (reject unauthorized callers before opening a DB transaction).
13
+ - Follow the chain order for server functions: `middleware` → `validator` → `handler`.
14
+ - `authorizationMiddleware` MUST be the LAST middleware in the array so its `context.user` (a non-null `User`) is the value that reaches the handler — `databaseMiddleware` re-derives `user` as nullable, so putting it after would widen the type back to `User | null`. Order: `[databaseMiddleware, authorizationMiddleware(<key>)]`. In REST routes, `validationMiddleware` also comes before `authorizationMiddleware`.
13
15
  - Use `validationMiddleware` only in REST API routes. Server functions use `.validator()`.
14
16
  - `databaseMiddleware` wraps the handler in a DB transaction and sets `app.user_name` so audit triggers can record who made the change.
15
17
  - Reuse schemas from `src/lib/schemas/`.
@@ -31,28 +33,28 @@ src/lib/auth/permissions.ts — permission keys
31
33
  ```ts
32
34
  // src/server/actions/<feature>.ts
33
35
  export const selectFeatures = createServerFn()
34
- .middleware([authorizationMiddleware("all"), databaseMiddleware])
36
+ .middleware([databaseMiddleware, authorizationMiddleware("all")])
35
37
  .handler(async ({ context }) => {
36
38
  return await context.db.select().from(featureTable);
37
39
  });
38
40
 
39
41
  export const insertFeature = createServerFn({ method: "POST" })
42
+ .middleware([databaseMiddleware, authorizationMiddleware("admin")])
40
43
  .validator(FeatureSchema)
41
- .middleware([authorizationMiddleware("admin"), databaseMiddleware])
42
44
  .handler(async ({ data, context }) => {
43
45
  await context.db.insert(featureTable).values(data);
44
46
  });
45
47
 
46
48
  export const updateFeature = createServerFn({ method: "POST" })
49
+ .middleware([databaseMiddleware, authorizationMiddleware("admin")])
47
50
  .validator(FeatureSchema)
48
- .middleware([authorizationMiddleware("admin"), databaseMiddleware])
49
51
  .handler(async ({ data, context }) => {
50
52
  await context.db.update(featureTable).set(data).where(eq(featureTable.id, data.id));
51
53
  });
52
54
 
53
55
  export const deleteFeature = createServerFn({ method: "POST" })
56
+ .middleware([databaseMiddleware, authorizationMiddleware("admin")])
54
57
  .validator(FeatureSchema.pick({ id: true }))
55
- .middleware([authorizationMiddleware("admin"), databaseMiddleware])
56
58
  .handler(async ({ data, context }) => {
57
59
  await context.db.delete(featureTable).where(eq(featureTable.id, data.id));
58
60
  });
@@ -60,21 +62,18 @@ export const deleteFeature = createServerFn({ method: "POST" })
60
62
  // src/routes/api/<feature>s/index.ts
61
63
  export const Route = createFileRoute("/api/features/")({
62
64
  server: {
65
+ middleware: [databaseMiddleware],
63
66
  handlers: ({ createHandlers }) =>
64
67
  createHandlers({
65
68
  GET: {
66
- middleware: [authorizationMiddleware("all"), databaseMiddleware],
69
+ middleware: [authorizationMiddleware("all")],
67
70
  handler: async ({ context }) => {
68
71
  const items = await context.db.select().from(featureTable);
69
72
  return Response.json(items);
70
73
  },
71
74
  },
72
75
  POST: {
73
- middleware: [
74
- validationMiddleware(FeatureSchema),
75
- authorizationMiddleware("admin"),
76
- databaseMiddleware,
77
- ],
76
+ middleware: [validationMiddleware(FeatureSchema), authorizationMiddleware("admin")],
78
77
  handler: async ({ context }) => {
79
78
  const [response] = await context.db
80
79
  .insert(featureTable)
@@ -90,7 +89,7 @@ export const Route = createFileRoute("/api/features/")({
90
89
  // src/routes/api/<feature>s/$id.ts
91
90
  export const Route = createFileRoute("/api/features/$id")({
92
91
  server: {
93
- middleware: [authorizationMiddleware("admin"), databaseMiddleware],
92
+ middleware: [databaseMiddleware, authorizationMiddleware("admin")],
94
93
  handlers: ({ createHandlers }) =>
95
94
  createHandlers({
96
95
  PUT: {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: services
3
- description: "Use when: integrating file, approval, PeopleSoft, email, or attendance services into your workflow."
3
+ description: "Use when: working with file, approval, employee or email services into your workflow."
4
4
  ---
5
5
 
6
6
  ## Info
@@ -9,7 +9,8 @@ You can read about each service in the links below. Services are categorized as
9
9
 
10
10
  - **Client exports** — Contains `queryOptions` and `mutationOptions`; use for data fetching/mutations on the client with React Query.
11
11
  - **Server exports** — Server functions; prefer calling mutations inside other server functions or database transactions.
12
- - All mutation options take `{ queryClient }` so they can invalidate related queries.
12
+ - Invalidating mutation options take `{ queryClient }` so they can invalidate related queries; read-only ones (e.g. `downloadFileMutationOptions()`, `openFileMutationOptions()`) don't require it.
13
+ - Access `queryClient` with `Route.useRouteContext()` in a route file, or `useRouteContext({ from: "path" })` in a component.
13
14
  - To read about a service, open the linked file in the **File** column directly.
14
15
 
15
16
  ## Import Paths
@@ -163,8 +163,8 @@ const ApprovalSchema = z.object({
163
163
  createdBy: z.custom<ApprovalEmployee>(),
164
164
  updated: z.date().optional(),
165
165
  updatedBy: z.custom<ApprovalEmployee>().optional(),
166
- approvalFlows: z.array(ApprovalFlow).min(1),
167
- currentApprovers: z.array(ApprovalEmployee),
166
+ approvalFlows: z.array(z.custom<ApprovalFlow>()).min(1),
167
+ currentApprovers: z.array(z.custom<ApprovalEmployee>()),
168
168
  coRequestors: z.array(ApprovalCoRequestorSchema).optional(),
169
169
  });
170
170
 
@@ -68,14 +68,41 @@ const UploadFileMetaSchema = z.object({
68
68
  ## Examples
69
69
 
70
70
  ```ts
71
- import { fileMetasQueryOptions, uploadFileMutationOptions } from "wcz-layout/data/client";
71
+ import { fileMetasQueryOptions, fileQueryOptions, uploadFileMutationOptions } from "wcz-layout/data/client";
72
+ import { Dropzone, FileViewer } from "wcz-layout/components";
73
+ import { useInView } from "wcz-layout/hooks";
72
74
 
73
- const queryClient = useQueryClient();
75
+ // fetch metas + fetch first file
76
+ const { ref, inView } = useInView();
74
77
 
75
- const { data: files = [] } = useQuery(fileMetasQueryOptions({ appName, subId }));
78
+ const { data: fileMetas = [] } = useQuery({
79
+ ...fileMetasQueryOptions({ appName: packageJson.name, subId }),
80
+ enabled: inView,
81
+ });
82
+
83
+ const { data: file } = useQuery({
84
+ ...fileQueryOptions({ appName: packageJson.name, id: fileMetas[0]?.id ?? "" }),
85
+ enabled: !!fileMetas[0],
86
+ });
76
87
 
77
88
  // upload
78
89
  const [progress, setProgress] = useState(0);
79
- const { mutate: upload, isPending } = useMutation(uploadFileMutationOptions({ queryClient }));
80
- upload({ appName, subId, file, setProgress });
90
+ const { mutateAsync: uploadFile } = useMutation(uploadFileMutationOptions({ queryClient }));
91
+
92
+ // Dropzone
93
+ const onDrop = async (acceptedFiles: File[]) => {
94
+ await Promise.allSettled(
95
+ acceptedFiles.map((file) =>
96
+ uploadFile({ appName: packageJson.name, subId, file, setProgress }),
97
+ ),
98
+ );
99
+ setUploadProgress(0);
100
+ };
101
+
102
+ <Dropzone onDrop={onDrop} progress={progress} />
103
+
104
+ // File Viewer - no manual fetch metas needed (default)
105
+ <FileViewer appName={packageJson.name} subId={subId}>
106
+ {(component) => <component.Grid />} // or component.List
107
+ </FileViewer>
81
108
  ```
@@ -1,6 +1,6 @@
1
1
  # Database Setup
2
2
 
3
- > Values written to `.env.local` here are temporary when the project uses Vault: the Vault Setup step moves them into the Vault `default` secret (only `DATABASE_URL` stays in `.env.local`, for drizzle-studio).
3
+ > Create `.env.local` if it does not exist, then preserve its existing keys when setting database values. Values written here are temporary when the project uses Vault: the Vault Setup step moves them into the Vault `default` secret. Both `DATABASE_URL` and `DATABASE_SCHEMA` stay in `.env.local` drizzle-kit (`db:studio`, `db:generate`) reads both.
4
4
 
5
5
  ## User Input
6
6
 
@@ -34,6 +34,8 @@ docker run --name <project-name>-postgres -e POSTGRES_PASSWORD=<password> -d -p
34
34
  - **Success** → continue.
35
35
  - **Error** → stop and show the full error to the user.
36
36
 
37
- 5. Update `.env.local` → set `DATABASE_URL=postgres://postgres:<password>@localhost:<PORT>/postgres`.
37
+ 5. Update `.env.local`:
38
+ - Set `DATABASE_URL=postgres://postgres:<password>@localhost:<PORT>/postgres`.
39
+ - Set `DATABASE_SCHEMA=public`.
38
40
  6. Output exactly:
39
41
  > ✅ Local Postgres database created successfully on port [PORT]
@@ -1,6 +1,6 @@
1
1
  # Vault Setup
2
2
 
3
- Never print user-provided secret values into chat, files, or logs — reference only the env keys and where each value lives. The one value the AI generates itself is `SESSION_SECRET`: a fresh 64-char hex string via `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`. The template ships only `VITE_MUI_LICENSE_KEY` in `.env`; all project configuration goes into the gitignored `.env.local` created in this step.
3
+ Never print user-provided secret values into chat, files, or logs — reference only the env keys and where each value lives. The one value the AI generates itself is `SESSION_SECRET`: a fresh 64-char hex string via `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`. The template ships only `VITE_MUI_LICENSE_KEY` in `.env`; all project configuration goes into the gitignored `.env.local`, which may have been created by an earlier setup step and is normalized in this step.
4
4
 
5
5
  ## User Input
6
6
 
@@ -21,23 +21,23 @@ Output:
21
21
  > 2. In your app's root path, create a secret named exactly `harborvault`: toggle 'JSON' mode, then copy-paste the JSON values from another app's `harborvault` secret and save.
22
22
  > 3. Open your app's `default` secret, click 'Edit' (toggle 'View as JSON'), fill in these keys, then save. Provide user this in JSON to copy-paste — empty string values, except `SESSION_SECRET`, which you pre-fill with a freshly generated value:
23
23
  >
24
- > | Key | Value source |
25
- > | ----------------------- | --------------------------------------------------------------- |
26
- > | `ENTRA_CLIENT_ID` | the Entra ID step |
27
- > | `ENTRA_TENANT_ID` | same for all Wistron apps — copy from another project |
28
- > | `ENTRA_CLIENT_SECRET` | your Entra ID application |
29
- > | `SESSION_SECRET` | already pre-filled in the JSON — generated by the AI |
30
- > | `DATABASE_SCHEMA` | the database step |
31
- > | `DATABASE_AUTO_MIGRATE` | `true` unless you manage migrations manually |
32
- > | `DATABASE_URL` | the database step |
24
+ > | Key | Value source |
25
+ > | ----------------------- | ----------------------------------------------------- |
26
+ > | `ENTRA_CLIENT_ID` | the Entra ID step |
27
+ > | `ENTRA_TENANT_ID` | same for all Wistron apps — copy from another project |
28
+ > | `ENTRA_CLIENT_SECRET` | your Entra ID application |
29
+ > | `SESSION_SECRET` | already pre-filled in the JSON — generated by the AI |
30
+ > | `DATABASE_SCHEMA` | the database step |
31
+ > | `DATABASE_AUTO_MIGRATE` | `true` unless you manage migrations manually |
32
+ > | `DATABASE_URL` | the database step |
33
33
  >
34
34
  > When finished, type 'done'.
35
35
 
36
36
  Wait for "done".
37
37
 
38
- ### Part 2 — Create `.env.local`
38
+ ### Part 2 — Normalize `.env.local`
39
39
 
40
- Rewrite `.env.local` so it contains **only** the keys below. Never fill in secret values yourself — keep values empty except `DATABASE_URL`, where you keep the value if the database step already set one:
40
+ Rewrite `.env.local` so it contains **only** the keys below. Never fill in secret values yourself — keep values empty except `DATABASE_URL` and `DATABASE_SCHEMA`, where you keep the values if the database step already set them (drizzle-kit reads both):
41
41
 
42
42
  ```dotenv
43
43
  VAULT_ADDRESS=
@@ -46,6 +46,7 @@ VAULT_USERNAME=
46
46
  VAULT_PASSWORD=
47
47
 
48
48
  DATABASE_URL=
49
+ DATABASE_SCHEMA=
49
50
  ```
50
51
 
51
52
  Then ask the user to fill in the empty values directly in `.env.local` (not in chat) and type 'done'. Wait for "done".