@farmzone/fz-template-react 1.0.6 → 1.0.8

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.
Files changed (64) hide show
  1. package/README.md +102 -102
  2. package/bin/create.js +108 -108
  3. package/package.json +24 -24
  4. package/template/.env.example +5 -5
  5. package/template/.prettierrc +9 -9
  6. package/template/eslint.config.js +26 -26
  7. package/template/index.css +32 -32
  8. package/template/index.html +19 -19
  9. package/template/package.json +54 -54
  10. package/template/pnpm-lock.yaml +4214 -4214
  11. package/template/public/mockServiceWorker.js +349 -349
  12. package/template/src/app/App.tsx +26 -26
  13. package/template/src/app/api/api.ts +178 -178
  14. package/template/src/app/api/queries.ts +335 -335
  15. package/template/src/app/api/queryKey.ts +7 -7
  16. package/template/src/app/api/token.ts +8 -7
  17. package/template/src/app/layout/Layout.tsx +33 -33
  18. package/template/src/app/layout/ListContents.tsx +9 -9
  19. package/template/src/app/layout/ListHeader.tsx +41 -41
  20. package/template/src/app/layout/MultiTabNav.tsx +106 -101
  21. package/template/src/app/layout/Sidebar.tsx +33 -33
  22. package/template/src/app/layout/UserInfo.tsx +95 -94
  23. package/template/src/app/layout/menu.ts +79 -55
  24. package/template/src/app/layout/tabSwitchStore.ts +11 -11
  25. package/template/src/app/router/Router.tsx +56 -56
  26. package/template/src/app/store/index.ts +26 -26
  27. package/template/src/index.tsx +21 -21
  28. package/template/src/mocks/browser.ts +17 -17
  29. package/template/src/mocks/handlers.ts +43 -43
  30. package/template/src/mocks/scenarios.ts +57 -57
  31. package/template/src/pages/dashboard/index.tsx +541 -541
  32. package/template/src/pages/error/Error.tsx +29 -29
  33. package/template/src/pages/error/NotFound.tsx +27 -27
  34. package/template/src/pages/login/index.tsx +317 -317
  35. package/template/src/pages/post/PostFormModal.tsx +128 -128
  36. package/template/src/pages/post/detail/index.tsx +545 -545
  37. package/template/src/pages/post/index.tsx +266 -266
  38. package/template/src/pages/sample/SampleFormModal.tsx +188 -188
  39. package/template/src/pages/sample/detail/index.tsx +551 -517
  40. package/template/src/pages/sample/index.tsx +298 -298
  41. package/template/src/pages/sample/modal/index.tsx +308 -308
  42. package/template/src/pages/system/log/index.tsx +173 -173
  43. package/template/src/pages/user/config/columns.tsx +102 -102
  44. package/template/src/pages/user/config/schema.ts +54 -54
  45. package/template/src/pages/user/index.tsx +704 -650
  46. package/template/src/shared/components/CommentInput.tsx +243 -243
  47. package/template/src/shared/components/FilePreviewCard.tsx +71 -71
  48. package/template/src/shared/config/text.ts +27 -27
  49. package/template/src/shared/config/type.ts +40 -40
  50. package/template/src/shared/utils/format.ts +11 -11
  51. package/template/src/types/auth.ts +10 -10
  52. package/template/src/types/comment.ts +33 -33
  53. package/template/src/types/common.ts +19 -19
  54. package/template/src/types/dashboard.ts +53 -53
  55. package/template/src/types/index.ts +16 -16
  56. package/template/src/types/log.ts +21 -21
  57. package/template/src/types/post.ts +32 -32
  58. package/template/src/types/sample.ts +33 -33
  59. package/template/src/types/user.ts +51 -51
  60. package/template/src/vite-env.d.ts +10 -10
  61. package/template/tsconfig.app.json +32 -32
  62. package/template/tsconfig.json +7 -7
  63. package/template/tsconfig.node.json +26 -26
  64. package/template/vite.config.ts +13 -13
@@ -1,298 +1,298 @@
1
- import { useState } from "react";
2
- import { useNavigate } from "react-router";
3
- import { Badge, Button, confirmModal, PageFilter, Select, Table } from "@farmzone/fz-react-ui";
4
-
5
- import {
6
- checkSampleNameAvailable,
7
- useDeleteSamples,
8
- useGetSamples,
9
- usePostSample,
10
- usePutSample,
11
- } from "@/app/api/queries";
12
- import type { Sample } from "@/types";
13
- import ListContents from "@/app/layout/ListContents";
14
- import ListHeader from "@/app/layout/ListHeader";
15
- import { formatDateTime } from "@/shared/utils/format";
16
- import { SampleFormModal, SAMPLE_FORM_DEFAULT_VALUES } from "./SampleFormModal";
17
- import type { SampleFormData } from "./SampleFormModal";
18
-
19
- const PAGE_SIZE_OPTIONS = [
20
- { label: "15", value: "15" },
21
- { label: "30", value: "30" },
22
- { label: "60", value: "60" },
23
- { label: "100", value: "100" },
24
- ];
25
-
26
- interface SampleFilterParams {
27
- name: string;
28
- category: string;
29
- }
30
-
31
- const FILTER_RESET: SampleFilterParams = { name: "", category: "" };
32
-
33
- const CATEGORY_FILTER_OPTIONS = [
34
- { label: "기본", value: "BASIC" },
35
- { label: "고급", value: "PREMIUM" },
36
- ];
37
-
38
- export default function SamplePage() {
39
- const navigate = useNavigate();
40
- const [page, setPage] = useState(0);
41
- const [pageSize, setPageSize] = useState(15);
42
- const [filterParams, setFilterParams] = useState<SampleFilterParams>(FILTER_RESET);
43
- const [submittedFilter, setSubmittedFilter] = useState<SampleFilterParams>(FILTER_RESET);
44
-
45
- const [modalMode, setModalMode] = useState<"create" | "edit" | null>(null);
46
- const [selectedSample, setSelectedSample] = useState<Sample | null>(null);
47
- const [editDefaultValues, setEditDefaultValues] = useState<SampleFormData | undefined>(undefined);
48
- const [selectedKeys, setSelectedKeys] = useState<Array<string | number>>([]);
49
- const [pendingFiles, setPendingFiles] = useState<Array<File>>([]);
50
- const [isUploading, setIsUploading] = useState(false);
51
- const [sortOption, setSortOption] = useState<{ sortKey: string; sortOrder: "asc" | "desc" } | undefined>(
52
- undefined,
53
- );
54
-
55
- const { data, isLoading } = useGetSamples({
56
- page,
57
- size: pageSize,
58
- name: submittedFilter.name || undefined,
59
- category: submittedFilter.category || undefined,
60
- sortKey: sortOption?.sortKey,
61
- sortOrder: sortOption?.sortOrder,
62
- });
63
-
64
- const { mutateAsync: postSample, isPending: isCreating } = usePostSample();
65
- const { mutateAsync: putSample, isPending: isUpdating } = usePutSample();
66
- const { mutateAsync: deleteSamples } = useDeleteSamples();
67
-
68
- const totalElements = data?.totalElements ?? 0;
69
- const totalPages = data?.totalPages ?? 0;
70
- const content = data?.content ?? [];
71
-
72
- const openCreate = () => {
73
- setEditDefaultValues(undefined);
74
- setSelectedSample(null);
75
- setModalMode("create");
76
- };
77
-
78
- const closeModal = () => {
79
- setModalMode(null);
80
- setSelectedSample(null);
81
- setEditDefaultValues(undefined);
82
- setPendingFiles([]);
83
- };
84
-
85
- const handleSubmit = async (data: SampleFormData) => {
86
- setIsUploading(true);
87
- try {
88
- if (modalMode === "create") {
89
- const isDuplicate = await checkSampleNameAvailable(data.name);
90
- if (isDuplicate) return;
91
- await postSample({
92
- data: {
93
- name: data.name,
94
- description: data.description,
95
- category: data.category as "BASIC" | "ADVANCED",
96
- priority: data.priority,
97
- active: data.active,
98
- },
99
- files: pendingFiles.length > 0 ? pendingFiles : undefined,
100
- });
101
- } else if (modalMode === "edit" && selectedSample) {
102
- if (data.name !== selectedSample.name) {
103
- const isDuplicate = await checkSampleNameAvailable(data.name);
104
- if (isDuplicate) return;
105
- }
106
- await putSample({
107
- id: selectedSample.id,
108
- data: {
109
- name: data.name,
110
- description: data.description,
111
- category: data.category as "BASIC" | "ADVANCED",
112
- priority: data.priority,
113
- active: data.active,
114
- },
115
- files: pendingFiles.length > 0 ? pendingFiles : undefined,
116
- });
117
- }
118
- closeModal();
119
- } finally {
120
- setIsUploading(false);
121
- }
122
- };
123
-
124
- const columns = [
125
- {
126
- key: "index",
127
- title: "No",
128
- width: "30px",
129
- align: "center" as const,
130
- render: (_: unknown, __: unknown, index: number) => totalElements - page * pageSize - index,
131
- },
132
- {
133
- key: "name",
134
- title: "이름",
135
- sortable: true,
136
- sort: sortOption?.sortKey === "name" ? sortOption.sortOrder : null,
137
- },
138
- { key: "description", title: "설명", minWidth: 200 },
139
- {
140
- key: "category",
141
- title: "카테고리",
142
- align: "center" as const,
143
- width: "40px",
144
- render: (_: unknown, record: Sample) => (
145
- <Badge
146
- text={record.category === "BASIC" ? "기본" : "고급"}
147
- className={`scale-90 ${record.category === "BASIC" ? "bg-blue-100 text-blue-700 border-blue-100" : "bg-purple-100 text-purple-700 border-purple-100"}`}
148
- />
149
- ),
150
- },
151
- {
152
- key: "priority",
153
- title: "우선순위",
154
- align: "center" as const,
155
- width: "30px",
156
- render: (_: unknown, record: Sample) => (
157
- <span className="text-sm font-medium text-gray-700">{record.priority}</span>
158
- ),
159
- },
160
- {
161
- key: "active",
162
- title: "상태",
163
- align: "center" as const,
164
- width: "30px",
165
- render: (_: unknown, record: Sample) => (
166
- <Badge
167
- text={record.active ? "활성" : "비활성"}
168
- className={`scale-90 ${record.active ? "bg-green-100 text-green-700 border-green-100" : "bg-red-100 text-red-500 border-red-100"}`}
169
- />
170
- ),
171
- },
172
- {
173
- key: "createdAt",
174
- title: "등록일시",
175
- align: "center" as const,
176
- width: "80px",
177
- render: (_: unknown, record: Sample) => formatDateTime(record.createdAt),
178
- },
179
- ];
180
-
181
- return (
182
- <div className="p-6">
183
- <ListHeader
184
- title="샘플 관리"
185
- rightArea={
186
- <div className="flex items-center gap-2">
187
- <Button variant="save" onClick={openCreate}>
188
- 등록
189
- </Button>
190
- <Button
191
- variant="delete"
192
- disabled={selectedKeys.length === 0}
193
- onClick={() => {
194
- confirmModal({
195
- content: `선택한 ${selectedKeys.length}개 항목을 삭제하시겠습니까?`,
196
- onOk: async () => {
197
- await deleteSamples(selectedKeys.map(Number));
198
- setSelectedKeys([]);
199
- },
200
- onCancel: () => {},
201
- className: "max-w-100",
202
- });
203
- }}
204
- >
205
- 삭제
206
- </Button>
207
- </div>
208
- }
209
- />
210
- <ListContents>
211
- <PageFilter
212
- values={filterParams}
213
- onChange={(updates: Partial<SampleFilterParams>) =>
214
- setFilterParams((prev) => ({ ...prev, ...updates }))
215
- }
216
- onSubmit={() => {
217
- setPage(0);
218
- setSelectedKeys([]);
219
- setSubmittedFilter(filterParams);
220
- }}
221
- onReset={() => {
222
- setFilterParams(FILTER_RESET);
223
- setSubmittedFilter(FILTER_RESET);
224
- setPage(0);
225
- setPageSize(15);
226
- setSelectedKeys([]);
227
- }}
228
- rows={[
229
- {
230
- options: [
231
- { type: "select", key: "category", label: "카테고리", options: CATEGORY_FILTER_OPTIONS },
232
- { type: "input", key: "name", label: "이름", placeholder: "이름 검색" },
233
- ],
234
- },
235
- ]}
236
- />
237
- <Table
238
- rowKey="id"
239
- columns={columns}
240
- data={content}
241
- isLoading={isLoading}
242
- sortOption={sortOption}
243
- onSortChange={(sortKey: string, sortOrder: "asc" | "desc") => {
244
- setSortOption({ sortKey, sortOrder });
245
- setPage(0);
246
- }}
247
- onRowClick={(record: Sample) => navigate(`/sample/${record.id}`)}
248
- paginationInfo={{
249
- page,
250
- totalPages,
251
- onPageChange: (newPage: number) => {
252
- setPage(newPage);
253
- setSelectedKeys([]);
254
- },
255
- }}
256
- checkboxInfo={{
257
- selectedKeys,
258
- onSelectionChange: setSelectedKeys,
259
- }}
260
- renderFooter={{
261
- renderLeft: (
262
- <p className="text-sm text-gray-600">
263
- 총 <span className="font-semibold text-gray-900">{totalElements}</span>건
264
- </p>
265
- ),
266
- renderRight: (
267
- <div className="flex items-center gap-2.5 pr-5">
268
- <p className="text-sm font-medium text-gray-700">페이지 개수</p>
269
- <Select
270
- value={String(pageSize)}
271
- options={PAGE_SIZE_OPTIONS}
272
- onChange={(val: string) => {
273
- setPageSize(Number(val));
274
- setPage(0);
275
- setSelectedKeys([]);
276
- }}
277
- />
278
- </div>
279
- ),
280
- }}
281
- />
282
- </ListContents>
283
-
284
- <SampleFormModal
285
- mode={modalMode ?? "create"}
286
- isOpen={modalMode !== null}
287
- onClose={closeModal}
288
- defaultValues={editDefaultValues ?? SAMPLE_FORM_DEFAULT_VALUES}
289
- onSubmit={handleSubmit}
290
- isPending={isCreating || isUpdating || isUploading}
291
- formKey={selectedSample?.id}
292
- originalName={selectedSample?.name}
293
- pendingFiles={pendingFiles}
294
- onPendingFilesChange={setPendingFiles}
295
- />
296
- </div>
297
- );
298
- }
1
+ import { useState } from "react";
2
+ import { useNavigate } from "react-router";
3
+ import { Badge, Button, confirmModal, PageFilter, Select, Table } from "@farmzone/fz-react-ui";
4
+
5
+ import {
6
+ checkSampleNameAvailable,
7
+ useDeleteSamples,
8
+ useGetSamples,
9
+ usePostSample,
10
+ usePutSample,
11
+ } from "@/app/api/queries";
12
+ import type { Sample } from "@/types";
13
+ import ListContents from "@/app/layout/ListContents";
14
+ import ListHeader from "@/app/layout/ListHeader";
15
+ import { formatDateTime } from "@/shared/utils/format";
16
+ import { SampleFormModal, SAMPLE_FORM_DEFAULT_VALUES } from "./SampleFormModal";
17
+ import type { SampleFormData } from "./SampleFormModal";
18
+
19
+ const PAGE_SIZE_OPTIONS = [
20
+ { label: "15", value: "15" },
21
+ { label: "30", value: "30" },
22
+ { label: "60", value: "60" },
23
+ { label: "100", value: "100" },
24
+ ];
25
+
26
+ interface SampleFilterParams {
27
+ name: string;
28
+ category: string;
29
+ }
30
+
31
+ const FILTER_RESET: SampleFilterParams = { name: "", category: "" };
32
+
33
+ const CATEGORY_FILTER_OPTIONS = [
34
+ { label: "기본", value: "BASIC" },
35
+ { label: "고급", value: "PREMIUM" },
36
+ ];
37
+
38
+ export default function SamplePage() {
39
+ const navigate = useNavigate();
40
+ const [page, setPage] = useState(0);
41
+ const [pageSize, setPageSize] = useState(15);
42
+ const [filterParams, setFilterParams] = useState<SampleFilterParams>(FILTER_RESET);
43
+ const [submittedFilter, setSubmittedFilter] = useState<SampleFilterParams>(FILTER_RESET);
44
+
45
+ const [modalMode, setModalMode] = useState<"create" | "edit" | null>(null);
46
+ const [selectedSample, setSelectedSample] = useState<Sample | null>(null);
47
+ const [editDefaultValues, setEditDefaultValues] = useState<SampleFormData | undefined>(undefined);
48
+ const [selectedKeys, setSelectedKeys] = useState<Array<string | number>>([]);
49
+ const [pendingFiles, setPendingFiles] = useState<Array<File>>([]);
50
+ const [isUploading, setIsUploading] = useState(false);
51
+ const [sortOption, setSortOption] = useState<{ sortKey: string; sortOrder: "asc" | "desc" } | undefined>(
52
+ undefined,
53
+ );
54
+
55
+ const { data, isLoading } = useGetSamples({
56
+ page,
57
+ size: pageSize,
58
+ name: submittedFilter.name || undefined,
59
+ category: submittedFilter.category || undefined,
60
+ sortKey: sortOption?.sortKey,
61
+ sortOrder: sortOption?.sortOrder,
62
+ });
63
+
64
+ const { mutateAsync: postSample, isPending: isCreating } = usePostSample();
65
+ const { mutateAsync: putSample, isPending: isUpdating } = usePutSample();
66
+ const { mutateAsync: deleteSamples } = useDeleteSamples();
67
+
68
+ const totalElements = data?.totalElements ?? 0;
69
+ const totalPages = data?.totalPages ?? 0;
70
+ const content = data?.content ?? [];
71
+
72
+ const openCreate = () => {
73
+ setEditDefaultValues(undefined);
74
+ setSelectedSample(null);
75
+ setModalMode("create");
76
+ };
77
+
78
+ const closeModal = () => {
79
+ setModalMode(null);
80
+ setSelectedSample(null);
81
+ setEditDefaultValues(undefined);
82
+ setPendingFiles([]);
83
+ };
84
+
85
+ const handleSubmit = async (data: SampleFormData) => {
86
+ setIsUploading(true);
87
+ try {
88
+ if (modalMode === "create") {
89
+ const isDuplicate = await checkSampleNameAvailable(data.name);
90
+ if (isDuplicate) return;
91
+ await postSample({
92
+ data: {
93
+ name: data.name,
94
+ description: data.description,
95
+ category: data.category as "BASIC" | "ADVANCED",
96
+ priority: data.priority,
97
+ active: data.active,
98
+ },
99
+ files: pendingFiles.length > 0 ? pendingFiles : undefined,
100
+ });
101
+ } else if (modalMode === "edit" && selectedSample) {
102
+ if (data.name !== selectedSample.name) {
103
+ const isDuplicate = await checkSampleNameAvailable(data.name);
104
+ if (isDuplicate) return;
105
+ }
106
+ await putSample({
107
+ id: selectedSample.id,
108
+ data: {
109
+ name: data.name,
110
+ description: data.description,
111
+ category: data.category as "BASIC" | "ADVANCED",
112
+ priority: data.priority,
113
+ active: data.active,
114
+ },
115
+ files: pendingFiles.length > 0 ? pendingFiles : undefined,
116
+ });
117
+ }
118
+ closeModal();
119
+ } finally {
120
+ setIsUploading(false);
121
+ }
122
+ };
123
+
124
+ const columns = [
125
+ {
126
+ key: "index",
127
+ title: "No",
128
+ width: "30px",
129
+ align: "center" as const,
130
+ render: (_: unknown, __: unknown, index: number) => totalElements - page * pageSize - index,
131
+ },
132
+ {
133
+ key: "name",
134
+ title: "이름",
135
+ sortable: true,
136
+ sort: sortOption?.sortKey === "name" ? sortOption.sortOrder : null,
137
+ },
138
+ { key: "description", title: "설명", minWidth: 200 },
139
+ {
140
+ key: "category",
141
+ title: "카테고리",
142
+ align: "center" as const,
143
+ width: "40px",
144
+ render: (_: unknown, record: Sample) => (
145
+ <Badge
146
+ text={record.category === "BASIC" ? "기본" : "고급"}
147
+ className={`scale-90 ${record.category === "BASIC" ? "bg-blue-100 text-blue-700 border-blue-100" : "bg-purple-100 text-purple-700 border-purple-100"}`}
148
+ />
149
+ ),
150
+ },
151
+ {
152
+ key: "priority",
153
+ title: "우선순위",
154
+ align: "center" as const,
155
+ width: "30px",
156
+ render: (_: unknown, record: Sample) => (
157
+ <span className="text-sm font-medium text-gray-700">{record.priority}</span>
158
+ ),
159
+ },
160
+ {
161
+ key: "active",
162
+ title: "상태",
163
+ align: "center" as const,
164
+ width: "30px",
165
+ render: (_: unknown, record: Sample) => (
166
+ <Badge
167
+ text={record.active ? "활성" : "비활성"}
168
+ className={`scale-90 ${record.active ? "bg-green-100 text-green-700 border-green-100" : "bg-red-100 text-red-500 border-red-100"}`}
169
+ />
170
+ ),
171
+ },
172
+ {
173
+ key: "createdAt",
174
+ title: "등록일시",
175
+ align: "center" as const,
176
+ width: "80px",
177
+ render: (_: unknown, record: Sample) => formatDateTime(record.createdAt),
178
+ },
179
+ ];
180
+
181
+ return (
182
+ <div className="p-6">
183
+ <ListHeader
184
+ title="샘플 관리"
185
+ rightArea={
186
+ <div className="flex items-center gap-2">
187
+ <Button variant="save" onClick={openCreate}>
188
+ 등록
189
+ </Button>
190
+ <Button
191
+ variant="delete"
192
+ disabled={selectedKeys.length === 0}
193
+ onClick={() => {
194
+ confirmModal({
195
+ content: `선택한 ${selectedKeys.length}개 항목을 삭제하시겠습니까?`,
196
+ onOk: async () => {
197
+ await deleteSamples(selectedKeys.map(Number));
198
+ setSelectedKeys([]);
199
+ },
200
+ onCancel: () => {},
201
+ className: "max-w-100",
202
+ });
203
+ }}
204
+ >
205
+ 삭제
206
+ </Button>
207
+ </div>
208
+ }
209
+ />
210
+ <ListContents>
211
+ <PageFilter
212
+ values={filterParams}
213
+ onChange={(updates: Partial<SampleFilterParams>) =>
214
+ setFilterParams((prev) => ({ ...prev, ...updates }))
215
+ }
216
+ onSubmit={() => {
217
+ setPage(0);
218
+ setSelectedKeys([]);
219
+ setSubmittedFilter(filterParams);
220
+ }}
221
+ onReset={() => {
222
+ setFilterParams(FILTER_RESET);
223
+ setSubmittedFilter(FILTER_RESET);
224
+ setPage(0);
225
+ setPageSize(15);
226
+ setSelectedKeys([]);
227
+ }}
228
+ rows={[
229
+ {
230
+ options: [
231
+ { type: "select", key: "category", label: "카테고리", options: CATEGORY_FILTER_OPTIONS },
232
+ { type: "input", key: "name", label: "이름", placeholder: "이름 검색" },
233
+ ],
234
+ },
235
+ ]}
236
+ />
237
+ <Table
238
+ rowKey="id"
239
+ columns={columns}
240
+ data={content}
241
+ isLoading={isLoading}
242
+ sortOption={sortOption}
243
+ onSortChange={(sortKey: string, sortOrder: "asc" | "desc") => {
244
+ setSortOption({ sortKey, sortOrder });
245
+ setPage(0);
246
+ }}
247
+ onRowClick={(record: Sample) => navigate(`/sample/${record.id}`)}
248
+ paginationInfo={{
249
+ page,
250
+ totalPages,
251
+ onPageChange: (newPage: number) => {
252
+ setPage(newPage);
253
+ setSelectedKeys([]);
254
+ },
255
+ }}
256
+ checkboxInfo={{
257
+ selectedKeys,
258
+ onSelectionChange: setSelectedKeys,
259
+ }}
260
+ renderFooter={{
261
+ renderLeft: (
262
+ <p className="text-sm text-gray-600">
263
+ 총 <span className="font-semibold text-gray-900">{totalElements}</span>건
264
+ </p>
265
+ ),
266
+ renderRight: (
267
+ <div className="flex items-center gap-2.5 pr-5">
268
+ <p className="text-sm font-medium text-gray-700">페이지 개수</p>
269
+ <Select
270
+ value={String(pageSize)}
271
+ options={PAGE_SIZE_OPTIONS}
272
+ onChange={(val: string) => {
273
+ setPageSize(Number(val));
274
+ setPage(0);
275
+ setSelectedKeys([]);
276
+ }}
277
+ />
278
+ </div>
279
+ ),
280
+ }}
281
+ />
282
+ </ListContents>
283
+
284
+ <SampleFormModal
285
+ mode={modalMode ?? "create"}
286
+ isOpen={modalMode !== null}
287
+ onClose={closeModal}
288
+ defaultValues={editDefaultValues ?? SAMPLE_FORM_DEFAULT_VALUES}
289
+ onSubmit={handleSubmit}
290
+ isPending={isCreating || isUpdating || isUploading}
291
+ formKey={selectedSample?.id}
292
+ originalName={selectedSample?.name}
293
+ pendingFiles={pendingFiles}
294
+ onPendingFilesChange={setPendingFiles}
295
+ />
296
+ </div>
297
+ );
298
+ }