Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions apps/api/src/locales/@vitnode/core/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,21 @@
"title": "No roles found",
"description": "Try adjusting your search criteria."
}
},
"create": {
"title": "Create role",
"desc": "Add a new role to your application.",
"submit": "Create",
"success": "Role created"
},
"edit": {
"title": "Edit role",
"submit": "Save changes",
"success": "Role updated"
},
"form": {
"name": "Name",
"color": "Color"
}
},
"staff": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import React from "react";
import { I18nProvider } from "@vitnode/core/components/i18n-provider";
import { DataTableSkeleton } from "@vitnode/core/components/table/data-table";
import { HeaderContent } from "@vitnode/core/components/ui/header-content";
import { ActionsRolesAdmin } from "@vitnode/core/views/admin/views/core/users/roles/actions/actions";

const RolesAdminView = dynamic(async () =>
import("@vitnode/core/views/admin/views/core/users/roles/roles-admin-view").then(
Expand Down Expand Up @@ -35,7 +36,9 @@ export default async function Page(
return (
<I18nProvider namespaces="admin.role">
<div className="p-4">
<HeaderContent desc={t("desc")} h1={tNav("roles")} />
<HeaderContent desc={t("desc")} h1={tNav("roles")}>
<ActionsRolesAdmin />
</HeaderContent>

<React.Suspense fallback={<DataTableSkeleton columns={2} />}>
<RolesAdminView {...props} />
Expand Down
15 changes: 15 additions & 0 deletions apps/docs/src/locales/@vitnode/core/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,21 @@
"title": "No roles found",
"description": "Try adjusting your search criteria."
}
},
"create": {
"title": "Create role",
"desc": "Add a new role to your application.",
"submit": "Create",
"success": "Role created"
},
"edit": {
"title": "Edit role",
"submit": "Save changes",
"success": "Role updated"
},
"form": {
"name": "Name",
"color": "Color"
}
},
"staff": {
Expand Down
2 changes: 1 addition & 1 deletion packages/vitnode/src/api/lib/save-language-words.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const saveLanguageWords = async (
),
);

if (values.length === 0) {
if (!values || values.length === 0) {
Comment thread
aXenDeveloper marked this conversation as resolved.
return;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { buildModule } from "@/api/lib/module";
import { CONFIG_PLUGIN } from "@/config";

import { createRoleAdminRoute } from "./routes/create.route";
import { listRolesAdminRoute } from "./routes/list.route";
import { showRoleAdminRoute } from "./routes/show.route";
import { updateRoleAdminRoute } from "./routes/update.route";

export const rolesAdminModule = buildModule({
pluginId: CONFIG_PLUGIN.pluginId,
name: "roles",
routes: [listRolesAdminRoute, showRoleAdminRoute],
routes: [
listRolesAdminRoute,
showRoleAdminRoute,
createRoleAdminRoute,
updateRoleAdminRoute,
],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { z } from "@hono/zod-openapi";

import { buildRoute } from "@/api/lib/route";
import { saveLanguageWords } from "@/api/lib/save-language-words";
import { CONFIG_PLUGIN } from "@/config";
import { core_roles } from "@/database/roles";

// Role names are not a column on `core_roles` - every translation lives in
// `core_languages_words`, so the name is the full list of per-language values.
export const zodRoleNameSchema = z
.array(
z.object({
languageCode: z.string(),
value: z.string().min(1).max(255),
}),
)
.min(1);

export const zodCreateRoleAdminSchema = z.object({
name: zodRoleNameSchema,
color: z.string().max(19).optional(),
});

export const createRoleAdminRoute = buildRoute({
pluginId: CONFIG_PLUGIN.pluginId,
route: {
method: "post",
description: "Create a new role (Admin only)",
path: "/create",
request: {
body: {
required: true,
content: {
"application/json": {
schema: zodCreateRoleAdminSchema,
},
},
},
},
responses: {
201: {
content: {
"application/json": {
schema: z.object({ id: z.number() }),
},
},
description: "Role created",
},
403: {
description: "Access Denied",
},
},
},
handler: async c => {
const { name, color } = c.req.valid("json");

const [role] = await c
.get("db")
.insert(core_roles)
.values({ color: color?.trim() ? color : null, updatedAt: new Date() })
Comment thread
aXenDeveloper marked this conversation as resolved.
.returning({ id: core_roles.id });

await saveLanguageWords(c, {
pluginCode: "core",
tableName: "core_roles",
variable: "name",
itemId: role.id,
values: name,
});

return c.json({ id: role.id }, 201);
},
});
103 changes: 103 additions & 0 deletions packages/vitnode/src/api/modules/admin/roles/routes/update.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { z } from "@hono/zod-openapi";
import { eq } from "drizzle-orm";

import { buildRoute } from "@/api/lib/route";
import { saveLanguageWords } from "@/api/lib/save-language-words";
import { CONFIG_PLUGIN } from "@/config";
import { core_roles } from "@/database/roles";

import { zodRoleNameSchema } from "./create.route";

export const zodUpdateRoleAdminSchema = z
.object({
name: zodRoleNameSchema,
color: z.string().max(19),
})
.partial()
.refine(body => Object.values(body).some(value => value !== undefined), {
message: "At least one field is required",
});

export const updateRoleAdminRoute = buildRoute({
pluginId: CONFIG_PLUGIN.pluginId,
route: {
method: "patch",
description: "Update a role by id (Admin only)",
path: "/{id}",
request: {
params: z.object({
id: z.string().openapi({ example: "1" }),
}),
body: {
required: true,
content: {
"application/json": {
schema: zodUpdateRoleAdminSchema,
},
},
},
},
responses: {
200: {
content: {
"application/json": {
schema: z.object({ id: z.number() }),
},
},
description: "Role updated",
},
403: {
description: "Access Denied",
},
404: {
content: {
"application/json": {
schema: z.object({ error: z.string() }),
},
},
description: "Role not found",
},
},
},
handler: async c => {
const { id } = c.req.valid("param");
const body = c.req.valid("json");
const db = c.get("db");

const roleId = Number(id);
if (!Number.isInteger(roleId)) {
return c.json({ error: "Role not found" }, 404);
}

const [role] = await db
.select({ id: core_roles.id })
.from(core_roles)
.where(eq(core_roles.id, roleId))
.limit(1);

if (!role) {
return c.json({ error: "Role not found" }, 404);
}

const values: Partial<typeof core_roles.$inferInsert> = {
updatedAt: new Date(),
};
if (body.color !== undefined) {
values.color = body.color.trim() ? body.color : null;
Comment thread
aXenDeveloper marked this conversation as resolved.
}

await db.update(core_roles).set(values).where(eq(core_roles.id, roleId));

if (body.name !== undefined) {
await saveLanguageWords(c, {
pluginCode: "core",
tableName: "core_roles",
variable: "name",
itemId: roleId,
values: body.name,
});
}

return c.json({ id: roleId }, 200);
},
});
17 changes: 12 additions & 5 deletions packages/vitnode/src/lib/helpers/multi-lang.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,19 @@ export const multiLangValueSchema = ({
};

export const getLangValue = (
value: MultiLangValue | undefined,
value: MultiLangValue | string | undefined,
languageCode: string,
): string =>
(Array.isArray(value) ? value : []).find(
item => item.languageCode === languageCode,
)?.value ?? "";
): string => {
if (typeof value === "string") {
return value;
}

return (
(Array.isArray(value) ? value : []).find(
item => item.languageCode === languageCode,
)?.value ?? ""
);
};

export const upsertLangValue = (
value: MultiLangValue | undefined,
Expand Down
15 changes: 15 additions & 0 deletions packages/vitnode/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,21 @@
"title": "No roles found",
"description": "Try adjusting your search criteria."
}
},
"create": {
"title": "Create role",
"desc": "Add a new role to your application.",
"submit": "Create",
"success": "Role created"
},
"edit": {
"title": "Edit role",
"submit": "Save changes",
"success": "Role updated"
},
"form": {
"name": "Name",
"color": "Color"
}
},
"staff": {
Expand Down
5 changes: 4 additions & 1 deletion packages/vitnode/src/routes/admin/core/users/roles/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import React from "react";
import { I18nProvider } from "@/components/i18n-provider";
import { DataTableSkeleton } from "@/components/table/data-table";
import { HeaderContent } from "@/components/ui/header-content";
import { ActionsRolesAdmin } from "@/views/admin/views/core/users/roles/actions/actions";

const RolesAdminView = dynamic(async () =>
import("@/views/admin/views/core/users/roles/roles-admin-view").then(
Expand Down Expand Up @@ -35,7 +36,9 @@ export default async function Page(
return (
<I18nProvider namespaces="admin.role">
<div className="p-4">
<HeaderContent desc={t("desc")} h1={tNav("roles")} />
<HeaderContent desc={t("desc")} h1={tNav("roles")}>
<ActionsRolesAdmin />
</HeaderContent>

<React.Suspense fallback={<DataTableSkeleton columns={2} />}>
<RolesAdminView {...props} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"use client";

import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import dynamic from "next/dynamic";
import React from "react";

import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Loader } from "@/components/ui/loader";

const CreateEditRoleAdmin = dynamic(async () =>
import("./create-edit/create-edit").then(mod => ({
default: mod.CreateEditRoleAdmin,
})),
);

export const ActionsRolesAdmin = () => {
const t = useTranslations("admin.role.create");

return (
<Dialog>
<DialogTrigger render={<Button />}>
<PlusIcon />
{t("title")}
</DialogTrigger>

<DialogContent>
<DialogHeader>
<DialogTitle>{t("title")}</DialogTitle>
<DialogDescription>{t("desc")}</DialogDescription>
</DialogHeader>

<React.Suspense fallback={<Loader />}>
<CreateEditRoleAdmin />
</React.Suspense>
</DialogContent>
</Dialog>
);
};
Loading
Loading