Skip to content

Commit 9ee3a60

Browse files
committed
feat: Changable name for asset
1 parent 658c145 commit 9ee3a60

File tree

8 files changed

+149
-33
lines changed

8 files changed

+149
-33
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
3+
import type { Kysely } from "kysely";
4+
5+
export async function up(db: Kysely<any>) {
6+
await db.schema.alterTable("assets").addColumn("name", "text").execute();
7+
}
8+
9+
export async function down(db: Kysely<any>) {
10+
await db.schema.alterTable("assets").dropColumn("name").execute();
11+
}

packages/api/src/db/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,15 @@ export type GroupInsert = Insertable<GroupsTable>;
2424

2525
export interface AssetsTable {
2626
id: string;
27+
name: string | null;
2728
groupId: number | null;
2829
createdAt: ColumnType<Date, never, never>;
2930
}
3031

3132
export type AssetInsert = Insertable<AssetsTable>;
3233

34+
export type AssetUpdate = Updateable<AssetsTable>;
35+
3336
export interface PlayablesTable {
3437
assetId: string;
3538
name: string;

packages/api/src/repositories/assets.ts

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { db } from "../db";
2-
import type { AssetInsert, PlayableInsert } from "../db/types";
2+
import type { AssetInsert, AssetUpdate, PlayableInsert } from "../db/types";
33

44
interface AssetsFilter {
55
page: number;
@@ -12,6 +12,14 @@ export async function createAsset(fields: AssetInsert) {
1212
return await db.insertInto("assets").values(fields).executeTakeFirstOrThrow();
1313
}
1414

15+
export async function updateAsset(id: string, fields: AssetUpdate) {
16+
return await db
17+
.updateTable("assets")
18+
.set(fields)
19+
.where("id", "=", id)
20+
.executeTakeFirst();
21+
}
22+
1523
export async function getAssets(filter: AssetsFilter) {
1624
let orderBy: "id" | "playables" | "groupId" | "createdAt";
1725
if (filter.sortKey === "name") {
@@ -25,6 +33,7 @@ export async function getAssets(filter: AssetsFilter) {
2533
.leftJoin("playables", "playables.assetId", "assets.id")
2634
.select(({ fn }) => [
2735
"assets.id",
36+
"assets.name",
2837
"assets.groupId",
2938
"assets.createdAt",
3039
fn.count<number>("playables.assetId").as("playables"),
@@ -44,7 +53,7 @@ export async function getAssets(filter: AssetsFilter) {
4453

4554
return {
4655
...filter,
47-
items: items.map(formatAsset),
56+
items,
4857
totalPages,
4958
};
5059
}
@@ -84,6 +93,7 @@ export async function getAsset(id: string) {
8493
.leftJoin("playables", "playables.assetId", "assets.id")
8594
.select(({ fn }) => [
8695
"assets.id",
96+
"assets.name",
8797
"assets.groupId",
8898
"assets.createdAt",
8999
fn.count<number>("playables.assetId").as("playables"),
@@ -92,17 +102,5 @@ export async function getAsset(id: string) {
92102
.where("assets.id", "=", id)
93103
.executeTakeFirst();
94104

95-
return asset ? formatAsset(asset) : null;
96-
}
97-
98-
function formatAsset(asset: {
99-
id: string;
100-
groupId: number | null;
101-
createdAt: Date;
102-
playables: number;
103-
}) {
104-
return {
105-
...asset,
106-
name: asset.id,
107-
};
105+
return asset ?? null;
108106
}

packages/api/src/routes/assets.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { Elysia, t } from "elysia";
22
import { auth } from "../auth";
33
import { DeliberateError } from "../errors";
4-
import { getAsset, getAssets, getGroups } from "../repositories/assets";
4+
import {
5+
getAsset,
6+
getAssets,
7+
getGroups,
8+
updateAsset,
9+
} from "../repositories/assets";
510
import { AssetSchema } from "../types";
611
import { mergeProps } from "../utils/type-guard";
712

@@ -76,6 +81,20 @@ export const assets = new Elysia()
7681
},
7782
},
7883
)
84+
.put(
85+
"/assets/:id",
86+
async ({ params, body }) => {
87+
await updateAsset(params.id, body);
88+
},
89+
{
90+
params: t.Object({
91+
id: t.String(),
92+
}),
93+
body: t.Object({
94+
name: t.String(),
95+
}),
96+
},
97+
)
7998
.get(
8099
"/groups",
81100
async () => {

packages/api/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ export type User = Static<typeof UserSchema>;
8282
export const AssetSchema = t.Object(
8383
{
8484
id: t.String({ format: "uuid" }),
85+
name: t.Nullable(t.String()),
8586
groupId: t.Nullable(t.Number()),
86-
name: t.String(),
8787
createdAt: t.Date(),
8888
playables: t.Number(),
8989
},

packages/app/src/components/Form.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Button, Input } from "@heroui/react";
22
import cn from "clsx";
3-
import { useEffect } from "react";
3+
import { useEffect, useState } from "react";
44
import { useForm } from "react-hook-form";
55
import { Controller } from "react-hook-form";
66
import type { InputProps } from "@heroui/input";
@@ -29,6 +29,8 @@ export function Form<T extends FieldRecord>({
2929
onSubmit,
3030
submit,
3131
}: FormProps<T>) {
32+
const [loading, setLoading] = useState(false);
33+
3234
const entries = Object.entries(fields);
3335

3436
const { handleSubmit, setValue, control, getValues } = useForm<FieldMap>({
@@ -50,7 +52,13 @@ export function Form<T extends FieldRecord>({
5052
return (
5153
<form
5254
onSubmit={handleSubmit((values) => {
53-
onSubmit(values as FieldMap<T>);
55+
const promise = onSubmit(values as FieldMap<T>);
56+
if (promise) {
57+
setLoading(true);
58+
promise.then(() => {
59+
setLoading(false);
60+
});
61+
}
5462
})}
5563
className={cn("flex flex-col gap-4", className)}
5664
autoComplete="off"
@@ -76,7 +84,9 @@ export function Form<T extends FieldRecord>({
7684
);
7785
})}
7886
<div>
79-
<Button type="submit">{submit ?? "Submit"}</Button>
87+
<Button isLoading={loading} type="submit">
88+
{submit ?? "Submit"}
89+
</Button>
8090
</div>
8191
</form>
8292
);

packages/app/src/components/FullTable.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
TableRow,
1010
} from "@heroui/table";
1111
import { useInfiniteScroll } from "@heroui/use-infinite-scroll";
12-
import { useState } from "react";
1312
import type { SortDescriptor, TableProps } from "@heroui/table";
1413
import type { ReactNode } from "@tanstack/react-router";
1514

@@ -23,15 +22,15 @@ export interface Column {
2322
export interface Filter {
2423
page: number;
2524
perPage: number;
26-
sortKey: string | number;
25+
sortKey: string;
2726
sortDir: "asc" | "desc";
2827
}
2928

3029
interface FullTableProps<T, F extends Filter> {
3130
classNames?: TableProps["classNames"];
3231
columns: Column[];
3332
items: T[];
34-
mapRow(props: T): { key: string | number; cells: ReactNode[] };
33+
mapRow(props: T): { key: string; cells: ReactNode[] };
3534
filter?: F;
3635
onFilterChange?(filter: F): void;
3736
hasMore?: boolean;
@@ -50,10 +49,13 @@ export function FullTable<T, F extends Filter>({
5049
onLoadMore,
5150
totalPages,
5251
}: FullTableProps<T, F>) {
53-
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
54-
column: filter?.sortKey ?? "",
55-
direction: filter?.sortDir === "asc" ? "ascending" : "descending",
56-
});
52+
let sortDescriptor: SortDescriptor | undefined;
53+
if (filter) {
54+
sortDescriptor = {
55+
column: filter.sortKey,
56+
direction: filter.sortDir === "asc" ? "ascending" : "descending",
57+
};
58+
}
5759

5860
const [loaderRef, scrollerRef] = useInfiniteScroll({
5961
onLoadMore,
@@ -72,7 +74,6 @@ export function FullTable<T, F extends Filter>({
7274
baseRef={scrollerRef}
7375
sortDescriptor={sortDescriptor ?? undefined}
7476
onSortChange={(sd) => {
75-
setSortDescriptor(sd);
7677
updateFilter({
7778
sortKey: sd.column,
7879
sortDir: sd.direction === "ascending" ? "asc" : "desc",

packages/app/src/routes/(dashboard)/_layout/assets.tsx

Lines changed: 79 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
1-
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
1+
import {
2+
Drawer,
3+
DrawerBody,
4+
DrawerContent,
5+
DrawerHeader,
6+
} from "@heroui/drawer";
7+
import {
8+
createFileRoute,
9+
Link,
10+
useNavigate,
11+
useRouter,
12+
} from "@tanstack/react-router";
213
import { zodSearchValidator } from "@tanstack/router-zod-adapter";
3-
import { CircleSlash } from "lucide-react";
14+
import { CircleSlash, SquarePen } from "lucide-react";
15+
import { useState } from "react";
416
import { z } from "zod";
17+
import { useAuth } from "../../../auth";
18+
import { Form } from "../../../components/Form";
519
import { Format } from "../../../components/Format";
620
import { FullTable } from "../../../components/FullTable";
721
import { Uniqolor } from "../../../components/Uniqolor";
8-
import type { Group } from "@superstreamer/api/client";
9-
import type { Asset } from "@superstreamer/api/client";
22+
import type { Asset, Group } from "@superstreamer/api/client";
1023

1124
export const Route = createFileRoute("/(dashboard)/_layout/assets")({
1225
component: RouteComponent,
@@ -33,6 +46,7 @@ function RouteComponent() {
3346
const navigate = useNavigate({ from: Route.fullPath });
3447
const { assets, groups } = Route.useLoaderData();
3548
const filter = Route.useLoaderDeps();
49+
const [editAsset, setEditAsset] = useState<Asset | null>(null);
3650

3751
if (!assets.data || !groups.data) {
3852
return null;
@@ -63,6 +77,10 @@ function RouteComponent() {
6377
label: "Created",
6478
allowsSorting: true,
6579
},
80+
{
81+
id: "actions",
82+
label: "Actions",
83+
},
6684
]}
6785
{...assets.data}
6886
filter={filter}
@@ -72,13 +90,31 @@ function RouteComponent() {
7290
mapRow={(item) => ({
7391
key: item.id,
7492
cells: [
75-
item.name,
93+
<Name asset={item} />,
7694
<Playables asset={item} />,
7795
<GroupTag groups={groups.data} asset={item} />,
7896
<Format format="date" value={item.createdAt} />,
97+
<div className="flex items-center">
98+
<button onClick={() => setEditAsset(item)}>
99+
<SquarePen className="w-4 h-4" />
100+
</button>
101+
</div>,
79102
],
80103
})}
81104
/>
105+
<EditAssetDrawer asset={editAsset} onClose={() => setEditAsset(null)} />
106+
</div>
107+
);
108+
}
109+
110+
function Name({ asset }: { asset: Asset }) {
111+
if (!asset.name) {
112+
return <span>{asset.id}</span>;
113+
}
114+
return (
115+
<div>
116+
{asset.name}
117+
<div className="text-xs opacity-50">{asset.id}</div>
82118
</div>
83119
);
84120
}
@@ -101,3 +137,41 @@ function Playables({ asset }: { asset: Asset }) {
101137
</div>
102138
);
103139
}
140+
141+
function EditAssetDrawer({
142+
asset,
143+
onClose,
144+
}: {
145+
asset: Asset | null;
146+
onClose: () => void;
147+
}) {
148+
const router = useRouter();
149+
const { api } = useAuth();
150+
151+
return (
152+
<Drawer isOpen={asset !== null} onClose={onClose}>
153+
<DrawerContent>
154+
<DrawerHeader>Asset</DrawerHeader>
155+
{asset ? (
156+
<DrawerBody>
157+
<div className="mb-4">{asset.id}</div>
158+
<Form
159+
fields={{
160+
name: {
161+
type: "string",
162+
label: "Name",
163+
value: asset.name,
164+
},
165+
}}
166+
onSubmit={async (values) => {
167+
await api.assets({ id: asset.id }).put(values);
168+
await router.invalidate();
169+
onClose();
170+
}}
171+
/>
172+
</DrawerBody>
173+
) : null}
174+
</DrawerContent>
175+
</Drawer>
176+
);
177+
}

0 commit comments

Comments
 (0)