@nubase/create 0.1.21 → 0.1.22
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/backend/src/api/routes/ticket.ts +51 -4
- package/templates/backend/src/api/routes/user.ts +139 -2
- package/templates/backend/src/db/schema/ticket.ts +2 -0
- package/templates/frontend/src/config.tsx +7 -1
- package/templates/frontend/src/resources/ticket.ts +57 -2
- package/templates/frontend/src/resources/user.ts +104 -2
- package/templates/schema/src/api-endpoints.ts +13 -1
- package/templates/schema/src/endpoints/index.ts +1 -0
- package/templates/schema/src/endpoints/user/delete-user.ts +12 -0
- package/templates/schema/src/endpoints/user/get-user.ts +9 -0
- package/templates/schema/src/endpoints/user/get-users.ts +9 -0
- package/templates/schema/src/endpoints/user/index.ts +5 -0
- package/templates/schema/src/endpoints/user/lookup-users.ts +2 -2
- package/templates/schema/src/endpoints/user/patch-user.ts +10 -0
- package/templates/schema/src/endpoints/user/post-user.ts +10 -0
- package/templates/schema/src/resources/ticket.ts +7 -0
- package/templates/schema/src/resources/user.ts +26 -6
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHttpHandler, HttpError } from "@nubase/backend";
|
|
2
|
-
import {
|
|
2
|
+
import type { SQL } from "drizzle-orm";
|
|
3
|
+
import { and, eq, ilike, inArray } from "drizzle-orm";
|
|
3
4
|
import { apiEndpoints } from "schema";
|
|
4
5
|
import { getDb } from "../../db/helpers/drizzle";
|
|
5
6
|
import { tickets } from "../../db/schema";
|
|
@@ -7,12 +8,46 @@ import { tickets } from "../../db/schema";
|
|
|
7
8
|
export const ticketHandlers = {
|
|
8
9
|
getTickets: createHttpHandler({
|
|
9
10
|
endpoint: apiEndpoints.getTickets,
|
|
10
|
-
handler: async () => {
|
|
11
|
-
const
|
|
11
|
+
handler: async ({ params }) => {
|
|
12
|
+
const db = getDb();
|
|
13
|
+
|
|
14
|
+
// Build filter conditions
|
|
15
|
+
const conditions: SQL[] = [];
|
|
16
|
+
|
|
17
|
+
// Filter by title (case-insensitive partial match)
|
|
18
|
+
if (params.title) {
|
|
19
|
+
conditions.push(ilike(tickets.title, `%${params.title}%`));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Filter by description (case-insensitive partial match)
|
|
23
|
+
if (params.description) {
|
|
24
|
+
conditions.push(ilike(tickets.description, `%${params.description}%`));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Filter by assigneeId (supports single value or array for multi-select)
|
|
28
|
+
if (params.assigneeId !== undefined) {
|
|
29
|
+
if (Array.isArray(params.assigneeId)) {
|
|
30
|
+
if (params.assigneeId.length > 0) {
|
|
31
|
+
conditions.push(inArray(tickets.assigneeId, params.assigneeId));
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
conditions.push(eq(tickets.assigneeId, params.assigneeId));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const allTickets =
|
|
39
|
+
conditions.length > 0
|
|
40
|
+
? await db
|
|
41
|
+
.select()
|
|
42
|
+
.from(tickets)
|
|
43
|
+
.where(and(...conditions))
|
|
44
|
+
: await db.select().from(tickets);
|
|
45
|
+
|
|
12
46
|
return allTickets.map((ticket) => ({
|
|
13
47
|
id: ticket.id,
|
|
14
48
|
title: ticket.title,
|
|
15
49
|
description: ticket.description ?? undefined,
|
|
50
|
+
assigneeId: ticket.assigneeId ?? undefined,
|
|
16
51
|
}));
|
|
17
52
|
},
|
|
18
53
|
}),
|
|
@@ -33,6 +68,7 @@ export const ticketHandlers = {
|
|
|
33
68
|
id: ticket.id,
|
|
34
69
|
title: ticket.title,
|
|
35
70
|
description: ticket.description ?? undefined,
|
|
71
|
+
assigneeId: ticket.assigneeId ?? undefined,
|
|
36
72
|
};
|
|
37
73
|
},
|
|
38
74
|
}),
|
|
@@ -46,6 +82,7 @@ export const ticketHandlers = {
|
|
|
46
82
|
workspaceId: 1, // TODO: Get from context
|
|
47
83
|
title: body.title,
|
|
48
84
|
description: body.description,
|
|
85
|
+
assigneeId: body.assigneeId,
|
|
49
86
|
})
|
|
50
87
|
.returning();
|
|
51
88
|
|
|
@@ -57,6 +94,7 @@ export const ticketHandlers = {
|
|
|
57
94
|
id: ticket.id,
|
|
58
95
|
title: ticket.title,
|
|
59
96
|
description: ticket.description ?? undefined,
|
|
97
|
+
assigneeId: ticket.assigneeId ?? undefined,
|
|
60
98
|
};
|
|
61
99
|
},
|
|
62
100
|
}),
|
|
@@ -64,7 +102,12 @@ export const ticketHandlers = {
|
|
|
64
102
|
patchTicket: createHttpHandler({
|
|
65
103
|
endpoint: apiEndpoints.patchTicket,
|
|
66
104
|
handler: async ({ params, body }) => {
|
|
67
|
-
const updateData: {
|
|
105
|
+
const updateData: {
|
|
106
|
+
title?: string;
|
|
107
|
+
description?: string;
|
|
108
|
+
assigneeId?: number | null;
|
|
109
|
+
updatedAt: Date;
|
|
110
|
+
} = {
|
|
68
111
|
updatedAt: new Date(),
|
|
69
112
|
};
|
|
70
113
|
|
|
@@ -74,6 +117,9 @@ export const ticketHandlers = {
|
|
|
74
117
|
if (body.description !== undefined) {
|
|
75
118
|
updateData.description = body.description;
|
|
76
119
|
}
|
|
120
|
+
if (body.assigneeId !== undefined) {
|
|
121
|
+
updateData.assigneeId = body.assigneeId;
|
|
122
|
+
}
|
|
77
123
|
|
|
78
124
|
const [ticket] = await getDb()
|
|
79
125
|
.update(tickets)
|
|
@@ -89,6 +135,7 @@ export const ticketHandlers = {
|
|
|
89
135
|
id: ticket.id,
|
|
90
136
|
title: ticket.title,
|
|
91
137
|
description: ticket.description ?? undefined,
|
|
138
|
+
assigneeId: ticket.assigneeId ?? undefined,
|
|
92
139
|
};
|
|
93
140
|
},
|
|
94
141
|
}),
|
|
@@ -1,10 +1,147 @@
|
|
|
1
|
-
import { createHttpHandler } from "@nubase/backend";
|
|
2
|
-
import {
|
|
1
|
+
import { createHttpHandler, HttpError } from "@nubase/backend";
|
|
2
|
+
import type { SQL } from "drizzle-orm";
|
|
3
|
+
import { and, eq, ilike } from "drizzle-orm";
|
|
3
4
|
import { apiEndpoints } from "schema";
|
|
4
5
|
import { getDb } from "../../db/helpers/drizzle";
|
|
5
6
|
import { users } from "../../db/schema";
|
|
6
7
|
|
|
7
8
|
export const userHandlers = {
|
|
9
|
+
/** Get all users with optional filters. */
|
|
10
|
+
getUsers: createHttpHandler({
|
|
11
|
+
endpoint: apiEndpoints.getUsers,
|
|
12
|
+
handler: async ({ params }) => {
|
|
13
|
+
const db = getDb();
|
|
14
|
+
|
|
15
|
+
// Build filter conditions
|
|
16
|
+
const conditions: SQL[] = [];
|
|
17
|
+
|
|
18
|
+
// Filter by displayName (case-insensitive partial match)
|
|
19
|
+
if (params.displayName) {
|
|
20
|
+
conditions.push(ilike(users.displayName, `%${params.displayName}%`));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Filter by email (case-insensitive partial match)
|
|
24
|
+
if (params.email) {
|
|
25
|
+
conditions.push(ilike(users.email, `%${params.email}%`));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const query =
|
|
29
|
+
conditions.length > 0
|
|
30
|
+
? db.select().from(users).where(and(...conditions))
|
|
31
|
+
: db.select().from(users);
|
|
32
|
+
|
|
33
|
+
const results = await query;
|
|
34
|
+
|
|
35
|
+
return results.map((u) => ({
|
|
36
|
+
id: u.id,
|
|
37
|
+
email: u.email,
|
|
38
|
+
displayName: u.displayName,
|
|
39
|
+
}));
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
|
|
43
|
+
/** Get a single user by ID. */
|
|
44
|
+
getUser: createHttpHandler({
|
|
45
|
+
endpoint: apiEndpoints.getUser,
|
|
46
|
+
handler: async ({ params }) => {
|
|
47
|
+
const db = getDb();
|
|
48
|
+
const [user] = await db.select().from(users).where(eq(users.id, params.id));
|
|
49
|
+
|
|
50
|
+
if (!user) {
|
|
51
|
+
throw new HttpError(404, "User not found");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
id: user.id,
|
|
56
|
+
email: user.email,
|
|
57
|
+
displayName: user.displayName,
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
|
|
62
|
+
/** Create a new user. */
|
|
63
|
+
postUser: createHttpHandler({
|
|
64
|
+
endpoint: apiEndpoints.postUser,
|
|
65
|
+
handler: async ({ body }) => {
|
|
66
|
+
const db = getDb();
|
|
67
|
+
|
|
68
|
+
// Check if user with this email already exists
|
|
69
|
+
const [existing] = await db.select().from(users).where(eq(users.email, body.email));
|
|
70
|
+
|
|
71
|
+
if (existing) {
|
|
72
|
+
throw new HttpError(409, "User with this email already exists");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const [user] = await db
|
|
76
|
+
.insert(users)
|
|
77
|
+
.values({
|
|
78
|
+
email: body.email,
|
|
79
|
+
displayName: body.displayName,
|
|
80
|
+
passwordHash: "placeholder-requires-password-reset",
|
|
81
|
+
})
|
|
82
|
+
.returning();
|
|
83
|
+
|
|
84
|
+
if (!user) {
|
|
85
|
+
throw new HttpError(500, "Failed to create user");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
id: user.id,
|
|
90
|
+
email: user.email,
|
|
91
|
+
displayName: user.displayName,
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
95
|
+
|
|
96
|
+
/** Update a user by ID. */
|
|
97
|
+
patchUser: createHttpHandler({
|
|
98
|
+
endpoint: apiEndpoints.patchUser,
|
|
99
|
+
handler: async ({ params, body }) => {
|
|
100
|
+
const db = getDb();
|
|
101
|
+
|
|
102
|
+
const updateData: { email?: string; displayName?: string } = {};
|
|
103
|
+
if (body.email !== undefined) {
|
|
104
|
+
updateData.email = body.email;
|
|
105
|
+
}
|
|
106
|
+
if (body.displayName !== undefined) {
|
|
107
|
+
updateData.displayName = body.displayName;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const [user] = await db
|
|
111
|
+
.update(users)
|
|
112
|
+
.set(updateData)
|
|
113
|
+
.where(eq(users.id, params.id))
|
|
114
|
+
.returning();
|
|
115
|
+
|
|
116
|
+
if (!user) {
|
|
117
|
+
throw new HttpError(404, "User not found");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
id: user.id,
|
|
122
|
+
email: user.email,
|
|
123
|
+
displayName: user.displayName,
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
}),
|
|
127
|
+
|
|
128
|
+
/** Delete a user by ID. */
|
|
129
|
+
deleteUser: createHttpHandler({
|
|
130
|
+
endpoint: apiEndpoints.deleteUser,
|
|
131
|
+
handler: async ({ params }) => {
|
|
132
|
+
const db = getDb();
|
|
133
|
+
|
|
134
|
+
const [deleted] = await db.delete(users).where(eq(users.id, params.id)).returning();
|
|
135
|
+
|
|
136
|
+
if (!deleted) {
|
|
137
|
+
throw new HttpError(404, "User not found");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { success: true };
|
|
141
|
+
},
|
|
142
|
+
}),
|
|
143
|
+
|
|
144
|
+
/** Lookup users for select/autocomplete fields. */
|
|
8
145
|
lookupUsers: createHttpHandler({
|
|
9
146
|
endpoint: apiEndpoints.lookupUsers,
|
|
10
147
|
handler: async ({ params }) => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { integer, pgTable, serial, text, timestamp, varchar } from "drizzle-orm/pg-core";
|
|
2
|
+
import { users } from "./user";
|
|
2
3
|
import { workspaces } from "./workspace";
|
|
3
4
|
|
|
4
5
|
export const tickets = pgTable("tickets", {
|
|
@@ -8,6 +9,7 @@ export const tickets = pgTable("tickets", {
|
|
|
8
9
|
.references(() => workspaces.id, { onDelete: "cascade" }),
|
|
9
10
|
title: varchar("title", { length: 255 }).notNull(),
|
|
10
11
|
description: text("description"),
|
|
12
|
+
assigneeId: integer("assignee_id").references(() => users.id, { onDelete: "set null" }),
|
|
11
13
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
|
12
14
|
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),
|
|
13
15
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { NubaseFrontendConfig } from "@nubase/frontend";
|
|
2
2
|
import { defaultKeybindings, resourceLink } from "@nubase/frontend";
|
|
3
|
-
import { Home, TicketIcon } from "lucide-react";
|
|
3
|
+
import { Home, TicketIcon, UsersIcon } from "lucide-react";
|
|
4
4
|
import { apiEndpoints } from "schema";
|
|
5
5
|
import { __PROJECT_NAME_PASCAL__AuthController } from "./auth/__PROJECT_NAME_PASCAL__AuthController";
|
|
6
6
|
import { analyticsDashboard } from "./dashboards/analytics";
|
|
@@ -26,6 +26,12 @@ export const config: NubaseFrontendConfig<typeof apiEndpoints> = {
|
|
|
26
26
|
label: "Tickets",
|
|
27
27
|
href: resourceLink(ticketResource, "search"),
|
|
28
28
|
},
|
|
29
|
+
{
|
|
30
|
+
id: "users",
|
|
31
|
+
icon: UsersIcon,
|
|
32
|
+
label: "Users",
|
|
33
|
+
href: resourceLink(userResource, "search"),
|
|
34
|
+
},
|
|
29
35
|
],
|
|
30
36
|
resources: {
|
|
31
37
|
[ticketResource.id]: ticketResource,
|
|
@@ -1,8 +1,60 @@
|
|
|
1
|
-
import { createResource } from "@nubase/frontend";
|
|
1
|
+
import { createResource, showToast } from "@nubase/frontend";
|
|
2
|
+
import { TrashIcon } from "lucide-react";
|
|
2
3
|
import { apiEndpoints } from "schema";
|
|
3
4
|
|
|
4
5
|
export const ticketResource = createResource("ticket")
|
|
5
6
|
.withApiEndpoints(apiEndpoints)
|
|
7
|
+
.withActions({
|
|
8
|
+
delete: {
|
|
9
|
+
label: "Delete",
|
|
10
|
+
icon: TrashIcon,
|
|
11
|
+
variant: "destructive" as const,
|
|
12
|
+
onExecute: async ({ selectedIds, context }) => {
|
|
13
|
+
if (!selectedIds || selectedIds.length === 0) {
|
|
14
|
+
showToast("No tickets selected for deletion", "error");
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const ticketCount = selectedIds.length;
|
|
19
|
+
const ticketLabel = ticketCount === 1 ? "ticket" : "tickets";
|
|
20
|
+
|
|
21
|
+
// Show confirmation dialog
|
|
22
|
+
const confirmed = await new Promise<boolean>((resolve) => {
|
|
23
|
+
context.dialog.openDialog({
|
|
24
|
+
title: "Delete Tickets",
|
|
25
|
+
content: `Are you sure you want to delete ${ticketCount} ${ticketLabel}? This action cannot be undone.`,
|
|
26
|
+
confirmText: "Delete",
|
|
27
|
+
confirmVariant: "destructive",
|
|
28
|
+
onConfirm: () => resolve(true),
|
|
29
|
+
onCancel: () => resolve(false),
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (!confirmed) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// Delete all selected tickets in parallel
|
|
39
|
+
await Promise.all(
|
|
40
|
+
selectedIds.map((id) =>
|
|
41
|
+
context.http.deleteTicket({
|
|
42
|
+
params: { id: Number(id) },
|
|
43
|
+
}),
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
showToast(
|
|
48
|
+
`${ticketCount} ${ticketLabel} deleted successfully`,
|
|
49
|
+
"default",
|
|
50
|
+
);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error("Error deleting tickets:", error);
|
|
53
|
+
showToast(`Failed to delete ${ticketLabel}`, "error");
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
})
|
|
6
58
|
.withViews({
|
|
7
59
|
create: {
|
|
8
60
|
type: "resource-create",
|
|
@@ -46,10 +98,13 @@ export const ticketResource = createResource("ticket")
|
|
|
46
98
|
id: "search-tickets",
|
|
47
99
|
title: "Search Tickets",
|
|
48
100
|
schemaGet: (api) => api.getTickets.responseBody,
|
|
101
|
+
schemaFilter: (api) => api.getTickets.requestParams,
|
|
49
102
|
breadcrumbs: () => [{ label: "Tickets", to: "/r/ticket/search" }],
|
|
103
|
+
tableActions: ["delete"],
|
|
104
|
+
rowActions: ["delete"],
|
|
50
105
|
onLoad: async ({ context }) => {
|
|
51
106
|
return context.http.getTickets({
|
|
52
|
-
params: {},
|
|
107
|
+
params: context.params || {},
|
|
53
108
|
});
|
|
54
109
|
},
|
|
55
110
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { createResource } from "@nubase/frontend";
|
|
1
|
+
import { createResource, showToast } from "@nubase/frontend";
|
|
2
|
+
import { TrashIcon } from "lucide-react";
|
|
2
3
|
import { apiEndpoints } from "schema";
|
|
3
4
|
|
|
4
5
|
export const userResource = createResource("user")
|
|
@@ -7,4 +8,105 @@ export const userResource = createResource("user")
|
|
|
7
8
|
onSearch: ({ query, context }) =>
|
|
8
9
|
context.http.lookupUsers({ params: { q: query } }),
|
|
9
10
|
})
|
|
10
|
-
.
|
|
11
|
+
.withActions({
|
|
12
|
+
delete: {
|
|
13
|
+
label: "Remove",
|
|
14
|
+
icon: TrashIcon,
|
|
15
|
+
variant: "destructive" as const,
|
|
16
|
+
onExecute: async ({ selectedIds, context }) => {
|
|
17
|
+
if (!selectedIds || selectedIds.length === 0) {
|
|
18
|
+
showToast("No users selected for removal", "error");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const userCount = selectedIds.length;
|
|
23
|
+
const userLabel = userCount === 1 ? "user" : "users";
|
|
24
|
+
|
|
25
|
+
// Show confirmation dialog
|
|
26
|
+
const confirmed = await new Promise<boolean>((resolve) => {
|
|
27
|
+
context.dialog.openDialog({
|
|
28
|
+
title: "Remove Users",
|
|
29
|
+
content: `Are you sure you want to remove ${userCount} ${userLabel}? This action cannot be undone.`,
|
|
30
|
+
confirmText: "Remove",
|
|
31
|
+
confirmVariant: "destructive",
|
|
32
|
+
onConfirm: () => resolve(true),
|
|
33
|
+
onCancel: () => resolve(false),
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (!confirmed) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// Remove all selected users in parallel
|
|
43
|
+
await Promise.all(
|
|
44
|
+
selectedIds.map((id) =>
|
|
45
|
+
context.http.deleteUser({
|
|
46
|
+
params: { id: Number(id) },
|
|
47
|
+
}),
|
|
48
|
+
),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
showToast(
|
|
52
|
+
`${userCount} ${userLabel} removed successfully`,
|
|
53
|
+
"default",
|
|
54
|
+
);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error("Error removing users:", error);
|
|
57
|
+
showToast(`Failed to remove ${userLabel}`, "error");
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
.withViews({
|
|
63
|
+
create: {
|
|
64
|
+
type: "resource-create",
|
|
65
|
+
id: "create-user",
|
|
66
|
+
title: "Add User",
|
|
67
|
+
schemaPost: (api) => api.postUser.requestBody,
|
|
68
|
+
breadcrumbs: [{ label: "Users", to: "/r/user/search" }, "Add User"],
|
|
69
|
+
onSubmit: async ({ data, context }) => {
|
|
70
|
+
return context.http.postUser({ data });
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
view: {
|
|
74
|
+
type: "resource-view",
|
|
75
|
+
id: "view-user",
|
|
76
|
+
title: "View User",
|
|
77
|
+
schemaGet: (api) => api.getUser.responseBody.omit("id"),
|
|
78
|
+
schemaParams: (api) => api.getUser.requestParams,
|
|
79
|
+
breadcrumbs: ({ context, data }) => [
|
|
80
|
+
{ label: "Users", to: "/r/user/search" },
|
|
81
|
+
{
|
|
82
|
+
label:
|
|
83
|
+
data?.displayName || `User #${context.params?.id || "Unknown"}`,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
onLoad: async ({ context }) => {
|
|
87
|
+
return context.http.getUser({
|
|
88
|
+
params: { id: context.params.id },
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
onPatch: async ({ data, context }) => {
|
|
92
|
+
return context.http.patchUser({
|
|
93
|
+
params: { id: context.params.id },
|
|
94
|
+
data: data,
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
search: {
|
|
99
|
+
type: "resource-search",
|
|
100
|
+
id: "search-users",
|
|
101
|
+
title: "Users",
|
|
102
|
+
schemaGet: (api) => api.getUsers.responseBody,
|
|
103
|
+
breadcrumbs: () => [{ label: "Users", to: "/r/user/search" }],
|
|
104
|
+
tableActions: ["delete"],
|
|
105
|
+
rowActions: ["delete"],
|
|
106
|
+
onLoad: async ({ context }) => {
|
|
107
|
+
return context.http.getUsers({
|
|
108
|
+
params: {},
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
});
|
|
@@ -21,7 +21,14 @@ import {
|
|
|
21
21
|
patchTicketSchema,
|
|
22
22
|
postTicketSchema,
|
|
23
23
|
} from "./endpoints/ticket";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
deleteUserSchema,
|
|
26
|
+
getUserSchema,
|
|
27
|
+
getUsersSchema,
|
|
28
|
+
lookupUsersSchema,
|
|
29
|
+
patchUserSchema,
|
|
30
|
+
postUserSchema,
|
|
31
|
+
} from "./endpoints/user";
|
|
25
32
|
|
|
26
33
|
export const apiEndpoints = {
|
|
27
34
|
// Auth
|
|
@@ -40,6 +47,11 @@ export const apiEndpoints = {
|
|
|
40
47
|
deleteTicket: deleteTicketSchema,
|
|
41
48
|
|
|
42
49
|
// Users
|
|
50
|
+
getUsers: getUsersSchema,
|
|
51
|
+
getUser: getUserSchema,
|
|
52
|
+
postUser: postUserSchema,
|
|
53
|
+
patchUser: patchUserSchema,
|
|
54
|
+
deleteUser: deleteUserSchema,
|
|
43
55
|
lookupUsers: lookupUsersSchema,
|
|
44
56
|
|
|
45
57
|
// Dashboard widgets
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
idNumberSchema,
|
|
3
|
+
type RequestSchema,
|
|
4
|
+
successSchema,
|
|
5
|
+
} from "@nubase/core";
|
|
6
|
+
|
|
7
|
+
export const deleteUserSchema = {
|
|
8
|
+
method: "DELETE" as const,
|
|
9
|
+
path: "/users/:id",
|
|
10
|
+
requestParams: idNumberSchema,
|
|
11
|
+
responseBody: successSchema,
|
|
12
|
+
} satisfies RequestSchema;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { idNumberSchema, type RequestSchema } from "@nubase/core";
|
|
2
|
+
import { userSchema } from "../../resources/user";
|
|
3
|
+
|
|
4
|
+
export const getUserSchema = {
|
|
5
|
+
method: "GET" as const,
|
|
6
|
+
path: "/users/:id",
|
|
7
|
+
requestParams: idNumberSchema,
|
|
8
|
+
responseBody: userSchema,
|
|
9
|
+
} satisfies RequestSchema;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { nu, type RequestSchema } from "@nubase/core";
|
|
2
|
+
import { userSchema } from "../../resources/user";
|
|
3
|
+
|
|
4
|
+
export const getUsersSchema = {
|
|
5
|
+
method: "GET" as const,
|
|
6
|
+
path: "/users",
|
|
7
|
+
requestParams: userSchema.omit("id").partial(),
|
|
8
|
+
responseBody: nu.array(userSchema),
|
|
9
|
+
} satisfies RequestSchema;
|
|
@@ -1 +1,6 @@
|
|
|
1
|
+
export { deleteUserSchema } from "./delete-user";
|
|
2
|
+
export { getUserSchema } from "./get-user";
|
|
3
|
+
export { getUsersSchema } from "./get-users";
|
|
1
4
|
export { lookupUsersSchema } from "./lookup-users";
|
|
5
|
+
export { patchUserSchema } from "./patch-user";
|
|
6
|
+
export { postUserSchema } from "./post-user";
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { createLookupEndpoint } from "@nubase/core";
|
|
1
|
+
import { createLookupEndpoint, nu } from "@nubase/core";
|
|
2
2
|
|
|
3
|
-
export const lookupUsersSchema = createLookupEndpoint("users");
|
|
3
|
+
export const lookupUsersSchema = createLookupEndpoint("users", nu.number());
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { idNumberSchema, type RequestSchema } from "@nubase/core";
|
|
2
|
+
import { userSchema } from "../../resources/user";
|
|
3
|
+
|
|
4
|
+
export const patchUserSchema = {
|
|
5
|
+
method: "PATCH" as const,
|
|
6
|
+
path: "/users/:id",
|
|
7
|
+
requestParams: idNumberSchema,
|
|
8
|
+
requestBody: userSchema.omit("id").partial(),
|
|
9
|
+
responseBody: userSchema,
|
|
10
|
+
} satisfies RequestSchema;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { emptySchema, type RequestSchema } from "@nubase/core";
|
|
2
|
+
import { userSchema } from "../../resources/user";
|
|
3
|
+
|
|
4
|
+
export const postUserSchema = {
|
|
5
|
+
method: "POST" as const,
|
|
6
|
+
path: "/users",
|
|
7
|
+
requestParams: emptySchema,
|
|
8
|
+
requestBody: userSchema.omit("id"),
|
|
9
|
+
responseBody: userSchema,
|
|
10
|
+
} satisfies RequestSchema;
|
|
@@ -12,6 +12,12 @@ export const ticketSchema = nu
|
|
|
12
12
|
description: "Enter the description of the ticket",
|
|
13
13
|
renderer: "multiline",
|
|
14
14
|
}),
|
|
15
|
+
assigneeId: nu.number().optional().withMeta({
|
|
16
|
+
label: "Assignee",
|
|
17
|
+
description: "Select a user to assign this ticket to",
|
|
18
|
+
renderer: "lookup",
|
|
19
|
+
lookupResource: "user",
|
|
20
|
+
}),
|
|
15
21
|
})
|
|
16
22
|
.withId("id")
|
|
17
23
|
.withTableLayouts({
|
|
@@ -20,6 +26,7 @@ export const ticketSchema = nu
|
|
|
20
26
|
{ name: "id", columnWidthPx: 80, pinned: true },
|
|
21
27
|
{ name: "title", columnWidthPx: 300, pinned: true },
|
|
22
28
|
{ name: "description", columnWidthPx: 400 },
|
|
29
|
+
{ name: "assigneeId", columnWidthPx: 150 },
|
|
23
30
|
],
|
|
24
31
|
metadata: {
|
|
25
32
|
linkFields: ["title"],
|
|
@@ -1,10 +1,30 @@
|
|
|
1
1
|
import { nu } from "@nubase/core";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* User schema for authenticated user data
|
|
4
|
+
* User entity schema for authenticated user data
|
|
5
5
|
*/
|
|
6
|
-
export const userSchema = nu
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
export const userSchema = nu
|
|
7
|
+
.object({
|
|
8
|
+
id: nu.number(),
|
|
9
|
+
email: nu.string().withMeta({
|
|
10
|
+
label: "Email",
|
|
11
|
+
description: "The user's email address",
|
|
12
|
+
}),
|
|
13
|
+
displayName: nu.string().withMeta({
|
|
14
|
+
label: "Display Name",
|
|
15
|
+
description: "The user's display name",
|
|
16
|
+
}),
|
|
17
|
+
})
|
|
18
|
+
.withId("id")
|
|
19
|
+
.withTableLayouts({
|
|
20
|
+
default: {
|
|
21
|
+
fields: [
|
|
22
|
+
{ name: "id", columnWidthPx: 80, pinned: true },
|
|
23
|
+
{ name: "displayName", columnWidthPx: 200, pinned: true },
|
|
24
|
+
{ name: "email", columnWidthPx: 300 },
|
|
25
|
+
],
|
|
26
|
+
metadata: {
|
|
27
|
+
linkFields: ["displayName"],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
});
|