create-m5kdev 0.39.0 → 0.40.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/package.json +1 -1
- package/templates/minimal-app/apps/e2e/tests/files.spec.ts.tpl +4 -0
- package/templates/minimal-app/apps/webapp/src/modules/files/FilesRoute.tsx.tpl +24 -0
- package/templates/minimal-app/apps/webapp/src/modules/files/hooks/useFilesRoute.ts.tpl +33 -5
- package/templates/minimal-app/apps/webapp/translations/en/{{APP_SLUG}}.json.tpl +7 -3
- package/templates/minimal-app/pnpm-workspace.yaml.tpl +12 -12
package/package.json
CHANGED
|
@@ -32,4 +32,8 @@ test("upload a file and see it inventoried", async ({ page, request }) => {
|
|
|
32
32
|
await expect(page.getByTestId("file-row").first()).toContainText("hello.png", {
|
|
33
33
|
timeout: 15_000,
|
|
34
34
|
});
|
|
35
|
+
await expect(page.getByTestId("file-download")).toHaveAttribute(
|
|
36
|
+
"href",
|
|
37
|
+
/\/files\/[0-9a-f-]{36}$/i
|
|
38
|
+
);
|
|
35
39
|
});
|
|
@@ -79,6 +79,30 @@ export function FilesRoute() {
|
|
|
79
79
|
</Chip>
|
|
80
80
|
) : null}
|
|
81
81
|
</Card.Header>
|
|
82
|
+
<Card.Content className="flex flex-wrap items-center gap-3">
|
|
83
|
+
<img
|
|
84
|
+
src={row.downloadUrl}
|
|
85
|
+
alt={row.originalName}
|
|
86
|
+
className="h-16 w-16 rounded-md object-cover"
|
|
87
|
+
data-testid="file-preview"
|
|
88
|
+
/>
|
|
89
|
+
<a
|
|
90
|
+
href={row.downloadUrl}
|
|
91
|
+
data-testid="file-download"
|
|
92
|
+
className="text-sm font-medium underline"
|
|
93
|
+
>
|
|
94
|
+
{t("files.row.open")}
|
|
95
|
+
</a>
|
|
96
|
+
<Button
|
|
97
|
+
size="sm"
|
|
98
|
+
variant="ghost"
|
|
99
|
+
data-testid="file-delete"
|
|
100
|
+
isDisabled={files.isDeleting}
|
|
101
|
+
onPress={() => files.remove(row.id)}
|
|
102
|
+
>
|
|
103
|
+
{t("files.row.delete")}
|
|
104
|
+
</Button>
|
|
105
|
+
</Card.Content>
|
|
82
106
|
</Card>
|
|
83
107
|
))}
|
|
84
108
|
</div>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useAppConfig } from "@m5kdev/frontend/modules/app/hooks/useAppConfig";
|
|
2
|
+
import { fileCookieDownloadUrl } from "@m5kdev/frontend/modules/file/hooks/useS3DownloadUrl";
|
|
3
|
+
import { useS3Upload } from "@m5kdev/frontend/modules/file/hooks/useS3Upload";
|
|
2
4
|
import type { AppRouter } from "{{PACKAGE_SCOPE}}/server/types";
|
|
3
5
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
6
|
import type { inferRouterOutputs } from "@trpc/server";
|
|
@@ -9,25 +11,32 @@ import { useTRPC } from "@/utils/trpc";
|
|
|
9
11
|
type FileListOutput = inferRouterOutputs<AppRouter>["file"]["list"];
|
|
10
12
|
type FileRow = FileListOutput["rows"][number];
|
|
11
13
|
|
|
14
|
+
interface FilesRouteRow extends FileRow {
|
|
15
|
+
readonly downloadUrl: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
12
18
|
interface FilesRouteState {
|
|
13
|
-
readonly rows:
|
|
19
|
+
readonly rows: FilesRouteRow[];
|
|
14
20
|
readonly isLoading: boolean;
|
|
15
21
|
readonly isFetching: boolean;
|
|
16
22
|
readonly isUploading: boolean;
|
|
23
|
+
readonly isDeleting: boolean;
|
|
17
24
|
readonly upload: (file: File) => void;
|
|
25
|
+
readonly remove: (fileId: string) => void;
|
|
18
26
|
}
|
|
19
27
|
|
|
20
28
|
export function useFilesRoute(): FilesRouteState {
|
|
21
29
|
const { t } = useTranslation("{{APP_SLUG}}");
|
|
30
|
+
const { serverUrl } = useAppConfig();
|
|
22
31
|
const trpc = useTRPC();
|
|
23
32
|
const queryClient = useQueryClient();
|
|
24
|
-
const fileUpload =
|
|
33
|
+
const fileUpload = useS3Upload({ scope: "organization" });
|
|
25
34
|
|
|
26
35
|
const listQuery = useQuery(trpc.file.list.queryOptions({}));
|
|
27
36
|
|
|
28
37
|
const uploadMutation = useMutation({
|
|
29
38
|
mutationFn: async (file: File) => {
|
|
30
|
-
await fileUpload.upload(
|
|
39
|
+
await fileUpload.upload(file);
|
|
31
40
|
},
|
|
32
41
|
onSuccess: async () => {
|
|
33
42
|
toast.success(t("files.toast.uploaded"));
|
|
@@ -38,13 +47,32 @@ export function useFilesRoute(): FilesRouteState {
|
|
|
38
47
|
},
|
|
39
48
|
});
|
|
40
49
|
|
|
50
|
+
const deleteMutation = useMutation(
|
|
51
|
+
trpc.file.delete.mutationOptions({
|
|
52
|
+
onSuccess: async () => {
|
|
53
|
+
toast.success(t("files.toast.deleted"));
|
|
54
|
+
await queryClient.invalidateQueries(trpc.file.list.queryFilter());
|
|
55
|
+
},
|
|
56
|
+
onError: (error) => {
|
|
57
|
+
toast.error(error instanceof Error ? error.message : t("files.toast.deleteFailed"));
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
);
|
|
61
|
+
|
|
41
62
|
return {
|
|
42
|
-
rows: listQuery.data?.rows ?? []
|
|
63
|
+
rows: (listQuery.data?.rows ?? []).map((row) => ({
|
|
64
|
+
...row,
|
|
65
|
+
downloadUrl: fileCookieDownloadUrl(serverUrl, row.id),
|
|
66
|
+
})),
|
|
43
67
|
isLoading: listQuery.isLoading,
|
|
44
68
|
isFetching: listQuery.isFetching,
|
|
45
69
|
isUploading: uploadMutation.isPending,
|
|
70
|
+
isDeleting: deleteMutation.isPending,
|
|
46
71
|
upload: (file: File) => {
|
|
47
72
|
void uploadMutation.mutateAsync(file).catch(() => undefined);
|
|
48
73
|
},
|
|
74
|
+
remove: (fileId: string) => {
|
|
75
|
+
void deleteMutation.mutateAsync({ fileId }).catch(() => undefined);
|
|
76
|
+
},
|
|
49
77
|
};
|
|
50
78
|
}
|
|
@@ -168,7 +168,7 @@
|
|
|
168
168
|
"hero": {
|
|
169
169
|
"eyebrow": "Files",
|
|
170
170
|
"title": "Upload an image and see it inventoried.",
|
|
171
|
-
"body": "
|
|
171
|
+
"body": "The browser PUTs to S3 after initiate, then finalize inventories the File. Open uses /files/:id.",
|
|
172
172
|
"upload": "Upload image",
|
|
173
173
|
"syncing": "Refreshing",
|
|
174
174
|
"synced": "Live against the files table"
|
|
@@ -178,11 +178,15 @@
|
|
|
178
178
|
"body": "Upload an image to see it listed here."
|
|
179
179
|
},
|
|
180
180
|
"row": {
|
|
181
|
-
"organization": "Organization"
|
|
181
|
+
"organization": "Organization",
|
|
182
|
+
"open": "Open",
|
|
183
|
+
"delete": "Delete"
|
|
182
184
|
},
|
|
183
185
|
"toast": {
|
|
184
186
|
"uploaded": "File uploaded.",
|
|
185
|
-
"failed": "Upload failed."
|
|
187
|
+
"failed": "Upload failed.",
|
|
188
|
+
"deleted": "File deleted.",
|
|
189
|
+
"deleteFailed": "Delete failed."
|
|
186
190
|
}
|
|
187
191
|
},
|
|
188
192
|
"notifications": {
|
|
@@ -7,18 +7,18 @@ catalogs:
|
|
|
7
7
|
"@heroui/react": 3.0.3
|
|
8
8
|
"@heroui/styles": 3.0.3
|
|
9
9
|
"@libsql/client": 0.17.4
|
|
10
|
-
"@m5kdev/backend": 0.
|
|
11
|
-
"@m5kdev/commons": 0.
|
|
12
|
-
"@m5kdev/config": 0.
|
|
13
|
-
"@m5kdev/email": 0.
|
|
14
|
-
"@m5kdev/expo": 0.
|
|
15
|
-
"@m5kdev/frontend": 0.
|
|
16
|
-
"@m5kdev/module-clay": 0.
|
|
17
|
-
"@m5kdev/module-docx": 0.
|
|
18
|
-
"@m5kdev/module-pdf": 0.
|
|
19
|
-
"@m5kdev/module-social": 0.
|
|
20
|
-
"@m5kdev/module-video": 0.
|
|
21
|
-
"@m5kdev/web-ui": 0.
|
|
10
|
+
"@m5kdev/backend": 0.40.0
|
|
11
|
+
"@m5kdev/commons": 0.40.0
|
|
12
|
+
"@m5kdev/config": 0.40.0
|
|
13
|
+
"@m5kdev/email": 0.40.0
|
|
14
|
+
"@m5kdev/expo": 0.40.0
|
|
15
|
+
"@m5kdev/frontend": 0.40.0
|
|
16
|
+
"@m5kdev/module-clay": 0.40.0
|
|
17
|
+
"@m5kdev/module-docx": 0.40.0
|
|
18
|
+
"@m5kdev/module-pdf": 0.40.0
|
|
19
|
+
"@m5kdev/module-social": 0.40.0
|
|
20
|
+
"@m5kdev/module-video": 0.40.0
|
|
21
|
+
"@m5kdev/web-ui": 0.40.0
|
|
22
22
|
"@mastra/core": 1.51.0
|
|
23
23
|
"@openrouter/ai-sdk-provider": 3.0.0
|
|
24
24
|
"@opentelemetry/api": 1.9.0
|