diff --git a/apps/app/src/actions/policies/archive-policy.ts b/apps/app/src/actions/policies/archive-policy.ts index d02f5c1813..8b10299622 100644 --- a/apps/app/src/actions/policies/archive-policy.ts +++ b/apps/app/src/actions/policies/archive-policy.ts @@ -6,70 +6,70 @@ import { authActionClient } from "../safe-action"; import { z } from "zod"; const archivePolicySchema = z.object({ - id: z.string(), - action: z.enum(["archive", "restore"]).optional(), + id: z.string(), + action: z.enum(["archive", "restore"]).optional(), }); export const archivePolicyAction = authActionClient - .schema(archivePolicySchema) - .metadata({ - name: "archive-policy", - track: { - event: "archive-policy", - channel: "server", - }, - }) - .action(async ({ parsedInput, ctx }) => { - const { id, action } = parsedInput; - const { activeOrganizationId } = ctx.session; + .schema(archivePolicySchema) + .metadata({ + name: "archive-policy", + track: { + event: "archive-policy", + channel: "server", + }, + }) + .action(async ({ parsedInput, ctx }) => { + const { id, action } = parsedInput; + const { activeOrganizationId } = ctx.session; - if (!activeOrganizationId) { - return { - success: false, - error: "Not authorized", - }; - } + if (!activeOrganizationId) { + return { + success: false, + error: "Not authorized", + }; + } - try { - const policy = await db.policy.findUnique({ - where: { - id, - organizationId: activeOrganizationId, - }, - }); + try { + const policy = await db.policy.findUnique({ + where: { + id, + organizationId: activeOrganizationId, + }, + }); - if (!policy) { - return { - success: false, - error: "Policy not found", - }; - } + if (!policy) { + return { + success: false, + error: "Policy not found", + }; + } - // Determine if we should archive or restore based on action or current state - const shouldArchive = - action === "archive" || (action === undefined && !policy.isArchived); + // Determine if we should archive or restore based on action or current state + const shouldArchive = + action === "archive" || (action === undefined && !policy.isArchived); - await db.policy.update({ - where: { id }, - data: { - isArchived: shouldArchive, - }, - }); + await db.policy.update({ + where: { id }, + data: { + isArchived: shouldArchive, + }, + }); - revalidatePath(`/${activeOrganizationId}/policies/all/${id}`); - revalidatePath(`/${activeOrganizationId}/policies/all`); - revalidatePath(`/${activeOrganizationId}/policies`); - revalidateTag("policies"); + revalidatePath(`/${activeOrganizationId}/policies/${id}`); + revalidatePath(`/${activeOrganizationId}/policies/all`); + revalidatePath(`/${activeOrganizationId}/policies`); + revalidateTag("policies"); - return { - success: true, - isArchived: shouldArchive, - }; - } catch (error) { - console.error(error); - return { - success: false, - error: "Failed to update policy archive status", - }; - } - }); + return { + success: true, + isArchived: shouldArchive, + }; + } catch (error) { + console.error(error); + return { + success: false, + error: "Failed to update policy archive status", + }; + } + }); diff --git a/apps/app/src/actions/policies/update-policy-form-action.ts b/apps/app/src/actions/policies/update-policy-form-action.ts index 6f2bdad24f..856cc479b1 100644 --- a/apps/app/src/actions/policies/update-policy-form-action.ts +++ b/apps/app/src/actions/policies/update-policy-form-action.ts @@ -10,107 +10,107 @@ import { PolicyStatus } from "@comp/db/types"; // Helper function to calculate next review date based on frequency function calculateNextReviewDate( - frequency: string, - baseDate: Date = new Date() + frequency: string, + baseDate: Date = new Date(), ): Date { - const nextDate = new Date(baseDate); + const nextDate = new Date(baseDate); - switch (frequency) { - case "monthly": - nextDate.setMonth(nextDate.getMonth() + 1); - break; - case "quarterly": - nextDate.setMonth(nextDate.getMonth() + 3); - break; - case "yearly": - nextDate.setFullYear(nextDate.getFullYear() + 1); - break; - default: - // If frequency is not recognized, default to yearly - nextDate.setFullYear(nextDate.getFullYear() + 1); - } + switch (frequency) { + case "monthly": + nextDate.setMonth(nextDate.getMonth() + 1); + break; + case "quarterly": + nextDate.setMonth(nextDate.getMonth() + 3); + break; + case "yearly": + nextDate.setFullYear(nextDate.getFullYear() + 1); + break; + default: + // If frequency is not recognized, default to yearly + nextDate.setFullYear(nextDate.getFullYear() + 1); + } - return nextDate; + return nextDate; } export const updatePolicyFormAction = authActionClient - .schema(updatePolicyFormSchema) - .metadata({ - name: "update-policy-form", - track: { - event: "update-policy-form", - channel: "server", - }, - }) - .action(async ({ parsedInput, ctx }) => { - const { - id, - status, - assigneeId, - department, - review_frequency, - review_date, - isRequiredToSign, - } = parsedInput; - const { user, session } = ctx; + .schema(updatePolicyFormSchema) + .metadata({ + name: "update-policy-form", + track: { + event: "update-policy-form", + channel: "server", + }, + }) + .action(async ({ parsedInput, ctx }) => { + const { + id, + status, + assigneeId, + department, + review_frequency, + review_date, + isRequiredToSign, + } = parsedInput; + const { user, session } = ctx; - if (!user.id || !session.activeOrganizationId) { - throw new Error("Unauthorized"); - } + if (!user.id || !session.activeOrganizationId) { + throw new Error("Unauthorized"); + } - try { - // Get the current policy to check if status is changing to published - const currentPolicy = await db.policy.findUnique({ - where: { - id, - organizationId: session.activeOrganizationId, - }, - select: { - status: true, - }, - }); + try { + // Get the current policy to check if status is changing to published + const currentPolicy = await db.policy.findUnique({ + where: { + id, + organizationId: session.activeOrganizationId, + }, + select: { + status: true, + }, + }); - // Determine if we need to update the review date - let reviewDate = review_date; - let lastPublishedAt = undefined; + // Determine if we need to update the review date + let reviewDate = review_date; + let lastPublishedAt = undefined; - // If status is changing to 'published', calculate next review date based on frequency - if ( - status === PolicyStatus.published && - currentPolicy?.status !== PolicyStatus.published - ) { - reviewDate = calculateNextReviewDate(review_frequency); - lastPublishedAt = new Date(); // Set lastPublishedAt to now when publishing - } + // If status is changing to 'published', calculate next review date based on frequency + if ( + status === PolicyStatus.published && + currentPolicy?.status !== PolicyStatus.published + ) { + reviewDate = calculateNextReviewDate(review_frequency); + lastPublishedAt = new Date(); // Set lastPublishedAt to now when publishing + } - await db.policy.update({ - where: { - id, - organizationId: session.activeOrganizationId, - }, - data: { - status, - assigneeId, - department, - frequency: review_frequency, - reviewDate, - isRequiredToSign: isRequiredToSign === "required", - ...(lastPublishedAt && { lastPublishedAt }), - }, - }); + await db.policy.update({ + where: { + id, + organizationId: session.activeOrganizationId, + }, + data: { + status, + assigneeId, + department, + frequency: review_frequency, + reviewDate, + isRequiredToSign: isRequiredToSign === "required", + ...(lastPublishedAt && { lastPublishedAt }), + }, + }); - revalidatePath(`/${session.activeOrganizationId}/policies`); - revalidatePath(`/${session.activeOrganizationId}/policies/all/${id}`); - revalidateTag("policies"); + revalidatePath(`/${session.activeOrganizationId}/policies`); + revalidatePath(`/${session.activeOrganizationId}/policies/${id}`); + revalidateTag("policies"); - return { - success: true, - }; - } catch (error) { - console.error("Error updating policy:", error); + return { + success: true, + }; + } catch (error) { + console.error("Error updating policy:", error); - return { - success: false, - }; - } - }); + return { + success: false, + }; + } + }); diff --git a/apps/app/src/actions/policies/update-policy-overview-action.ts b/apps/app/src/actions/policies/update-policy-overview-action.ts index 9c3d316ee4..1753e336b7 100644 --- a/apps/app/src/actions/policies/update-policy-overview-action.ts +++ b/apps/app/src/actions/policies/update-policy-overview-action.ts @@ -59,7 +59,7 @@ export const updatePolicyOverviewAction = authActionClient }, }); - revalidatePath(`/${session.activeOrganizationId}/policies/all/${id}`); + revalidatePath(`/${session.activeOrganizationId}/policies/${id}`); revalidatePath(`/${session.activeOrganizationId}/policies/all`); revalidatePath(`/${session.activeOrganizationId}/policies`); diff --git a/apps/app/src/actions/schema.ts b/apps/app/src/actions/schema.ts index ed254a0fbf..d7df9ff709 100644 --- a/apps/app/src/actions/schema.ts +++ b/apps/app/src/actions/schema.ts @@ -1,300 +1,296 @@ import { - Departments, - Frequency, - PolicyStatus, - RiskCategory, - RiskStatus, - VendorCategory, - VendorStatus, - FrameworkId, - TaskStatus, - Impact, - Likelihood, + Departments, + FrameworkId, + Frequency, + Impact, + Likelihood, + PolicyStatus, + RiskCategory, + RiskStatus, + TaskStatus, } from "@comp/db/types"; import { z } from "zod"; export const organizationSchema = z.object({ - name: z.string().min(1, "Name is required"), - fullName: z.string().min(1, "Full name is required"), - website: z.string().url("Must be a valid URL").optional().or(z.literal("")), - frameworks: z - .array(z.nativeEnum(FrameworkId)) - .min(1, "Please select at least one framework to get started with"), + name: z.string().min(1, "Organization name is required"), + frameworks: z + .array(z.nativeEnum(FrameworkId)) + .min(1, "Please select at least one framework to get started with"), }); export type OrganizationSchema = z.infer; export const organizationNameSchema = z.object({ - name: z - .string() - .min(1, "Organization name is required") - .max(255, "Organization name cannot exceed 255 characters"), + name: z + .string() + .min(1, "Organization name is required") + .max(255, "Organization name cannot exceed 255 characters"), }); export const subdomainAvailabilitySchema = z.object({ - subdomain: z - .string() - .min(1, "Subdomain is required") - .max(255, "Subdomain cannot exceed 255 characters") - .regex(/^[a-z0-9-]+$/, { - message: - "Subdomain can only contain lowercase letters, numbers, and hyphens", - }), + subdomain: z + .string() + .min(1, "Subdomain is required") + .max(255, "Subdomain cannot exceed 255 characters") + .regex(/^[a-z0-9-]+$/, { + message: + "Subdomain can only contain lowercase letters, numbers, and hyphens", + }), }); export const uploadSchema = z.object({ - file: z.instanceof(File), - organizationId: z.string(), + file: z.instanceof(File), + organizationId: z.string(), }); export const deleteOrganizationSchema = z.object({ - id: z.string(), - organizationId: z.string(), + id: z.string(), + organizationId: z.string(), }); export const sendFeedbackSchema = z.object({ - feedback: z.string(), + feedback: z.string(), }); export const updaterMenuSchema = z.array( - z.object({ - path: z.string(), - name: z.string(), - }) + z.object({ + path: z.string(), + name: z.string(), + }), ); export const organizationWebsiteSchema = z.object({ - website: z.string().url().max(255), + website: z.string().url().max(255), }); // Risks export const createRiskSchema = z.object({ - title: z - .string({ - required_error: "Risk name is required", - }) - .min(1, { - message: "Risk name should be at least 1 character", - }) - .max(100, { - message: "Risk name should be at most 100 characters", - }), - description: z - .string({ - required_error: "Risk description is required", - }) - .min(1, { - message: "Risk description should be at least 1 character", - }) - .max(255, { - message: "Risk description should be at most 255 characters", - }), - category: z.nativeEnum(RiskCategory, { - required_error: "Risk category is required", - }), - department: z.nativeEnum(Departments, { - required_error: "Risk department is required", - }), - ownerId: z.string({ - required_error: "You must assign an owner to the risk", - }), + title: z + .string({ + required_error: "Risk name is required", + }) + .min(1, { + message: "Risk name should be at least 1 character", + }) + .max(100, { + message: "Risk name should be at most 100 characters", + }), + description: z + .string({ + required_error: "Risk description is required", + }) + .min(1, { + message: "Risk description should be at least 1 character", + }) + .max(255, { + message: "Risk description should be at most 255 characters", + }), + category: z.nativeEnum(RiskCategory, { + required_error: "Risk category is required", + }), + department: z.nativeEnum(Departments, { + required_error: "Risk department is required", + }), + ownerId: z.string({ + required_error: "You must assign an owner to the risk", + }), }); export const updateRiskSchema = z.object({ - id: z.string().min(1, { - message: "Risk ID is required", - }), - title: z.string().min(1, { - message: "Risk title is required", - }), - description: z.string().min(1, { - message: "Risk description is required", - }), - category: z.nativeEnum(RiskCategory, { - required_error: "Risk category is required", - }), - department: z.nativeEnum(Departments, { - required_error: "Risk department is required", - }), - ownerId: z.string({ - required_error: "You must assign an owner to the risk", - }), - status: z.nativeEnum(RiskStatus, { - required_error: "Risk status is required", - }), + id: z.string().min(1, { + message: "Risk ID is required", + }), + title: z.string().min(1, { + message: "Risk title is required", + }), + description: z.string().min(1, { + message: "Risk description is required", + }), + category: z.nativeEnum(RiskCategory, { + required_error: "Risk category is required", + }), + department: z.nativeEnum(Departments, { + required_error: "Risk department is required", + }), + ownerId: z.string({ + required_error: "You must assign an owner to the risk", + }), + status: z.nativeEnum(RiskStatus, { + required_error: "Risk status is required", + }), }); export const createRiskCommentSchema = z.object({ - riskId: z.string().min(1, { - message: "Risk ID is required", - }), - content: z - .string() - .min(1, { - message: "Comment content is required", - }) - .max(1000, { - message: "Comment content should be at most 1000 characters", - }), + riskId: z.string().min(1, { + message: "Risk ID is required", + }), + content: z + .string() + .min(1, { + message: "Comment content is required", + }) + .max(1000, { + message: "Comment content should be at most 1000 characters", + }), }); export const createTaskSchema = z.object({ - riskId: z.string().min(1, { - message: "Risk ID is required", - }), - title: z.string().min(1, { - message: "Task title is required", - }), - description: z.string().min(1, { - message: "Task description is required", - }), - dueDate: z.date().optional(), - ownerId: z - .string({ - required_error: "You must assign an owner to the task", - }) - .min(1, { - message: "You must assign an owner to the task", - }), + riskId: z.string().min(1, { + message: "Risk ID is required", + }), + title: z.string().min(1, { + message: "Task title is required", + }), + description: z.string().min(1, { + message: "Task description is required", + }), + dueDate: z.date().optional(), + ownerId: z + .string({ + required_error: "You must assign an owner to the task", + }) + .min(1, { + message: "You must assign an owner to the task", + }), }); export const updateTaskSchema = z.object({ - id: z.string().min(1, { - message: "Task ID is required", - }), - title: z.string().optional(), - description: z.string().optional(), - dueDate: z.date().optional(), - status: z.nativeEnum(TaskStatus, { - required_error: "Task status is required", - }), - ownerId: z.string({ - required_error: "You must assign an owner to the task", - }), + id: z.string().min(1, { + message: "Task ID is required", + }), + title: z.string().optional(), + description: z.string().optional(), + dueDate: z.date().optional(), + status: z.nativeEnum(TaskStatus, { + required_error: "Task status is required", + }), + ownerId: z.string({ + required_error: "You must assign an owner to the task", + }), }); export const createTaskCommentSchema = z.object({ - riskId: z.string().min(1, { - message: "Risk ID is required", - }), - taskId: z.string().min(1, { - message: "Task ID is required", - }), - content: z - .string() - .min(1, { - message: "Comment content is required", - }) - .max(1000, { - message: "Comment content should be at most 1000 characters", - }), + riskId: z.string().min(1, { + message: "Risk ID is required", + }), + taskId: z.string().min(1, { + message: "Task ID is required", + }), + content: z + .string() + .min(1, { + message: "Comment content is required", + }) + .max(1000, { + message: "Comment content should be at most 1000 characters", + }), }); export const uploadTaskFileSchema = z.object({ - riskId: z.string().min(1, { - message: "Risk ID is required", - }), - taskId: z.string().min(1, { - message: "Task ID is required", - }), + riskId: z.string().min(1, { + message: "Risk ID is required", + }), + taskId: z.string().min(1, { + message: "Task ID is required", + }), }); // Integrations export const deleteIntegrationConnectionSchema = z.object({ - integrationId: z.string().min(1, { - message: "Integration ID is required", - }), + integrationId: z.string().min(1, { + message: "Integration ID is required", + }), }); export const createIntegrationSchema = z.object({ - integrationId: z.string().min(1, { - message: "Integration ID is required", - }), + integrationId: z.string().min(1, { + message: "Integration ID is required", + }), }); // Seed Data export const seedDataSchema = z.object({ - organizationId: z.string(), + organizationId: z.string(), }); export const updateInherentRiskSchema = z.object({ - id: z.string().min(1, { - message: "Risk ID is required", - }), - probability: z.nativeEnum(Likelihood), - impact: z.nativeEnum(Impact), + id: z.string().min(1, { + message: "Risk ID is required", + }), + probability: z.nativeEnum(Likelihood), + impact: z.nativeEnum(Impact), }); export const updateResidualRiskSchema = z.object({ - id: z.string().min(1, { - message: "Risk ID is required", - }), - probability: z.number().min(1).max(10), - impact: z.number().min(1).max(10), + id: z.string().min(1, { + message: "Risk ID is required", + }), + probability: z.number().min(1).max(10), + impact: z.number().min(1).max(10), }); // Policies export const createPolicySchema = z.object({ - title: z.string(), - description: z.string(), - frameworkIds: z.array(z.string()), - controlIds: z.array(z.string()), + title: z.string(), + description: z.string(), + frameworkIds: z.array(z.string()), + controlIds: z.array(z.string()), }); export const updatePolicySchema = z.object({ - id: z.string(), - content: z.any(), + id: z.string(), + content: z.any(), }); export const assistantSettingsSchema = z.object({ - enabled: z.boolean().optional(), + enabled: z.boolean().optional(), }); export const createEmployeeSchema = z.object({ - name: z.string().min(1, "Name is required"), - email: z.string().email("Invalid email address"), - department: z.nativeEnum(Departments, { - required_error: "Department is required", - }), - externalEmployeeId: z.string().optional(), - isActive: z.boolean().default(true), + name: z.string().min(1, "Name is required"), + email: z.string().email("Invalid email address"), + department: z.nativeEnum(Departments, { + required_error: "Department is required", + }), + externalEmployeeId: z.string().optional(), + isActive: z.boolean().default(true), }); export const updatePolicyOverviewSchema = z.object({ - id: z.string(), - title: z.string(), - description: z.string(), - isRequiredToSign: z.enum(["required", "not_required"]).optional(), + id: z.string(), + title: z.string(), + description: z.string(), + isRequiredToSign: z.enum(["required", "not_required"]).optional(), }); export const updatePolicyFormSchema = z.object({ - id: z.string(), - status: z.nativeEnum(PolicyStatus), - assigneeId: z.string().nullable(), - department: z.nativeEnum(Departments), - review_frequency: z.nativeEnum(Frequency), - review_date: z.date(), - isRequiredToSign: z.enum(["required", "not_required"]), + id: z.string(), + status: z.nativeEnum(PolicyStatus), + assigneeId: z.string().nullable(), + department: z.nativeEnum(Departments), + review_frequency: z.nativeEnum(Frequency), + review_date: z.date(), + isRequiredToSign: z.enum(["required", "not_required"]), }); export const apiKeySchema = z.object({ - name: z - .string() - .min(1, { message: "Name is required" }) - .max(64, { message: "Name must be less than 64 characters" }), - expiresAt: z.enum(["30days", "90days", "1year", "never"]), + name: z + .string() + .min(1, { message: "Name is required" }) + .max(64, { message: "Name must be less than 64 characters" }), + expiresAt: z.enum(["30days", "90days", "1year", "never"]), }); export const createPolicyCommentSchema = z.object({ - policyId: z.string().min(1, { - message: "Policy ID is required", - }), - content: z - .string() - .min(1, { - message: "Comment content is required", - }) - .max(1000, { - message: "Comment content should be at most 1000 characters", - }), + policyId: z.string().min(1, { + message: "Policy ID is required", + }), + content: z + .string() + .min(1, { + message: "Comment content is required", + }) + .max(1000, { + message: "Comment content should be at most 1000 characters", + }), }); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/controls/components/controls-table.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/controls/components/controls-table.tsx index 228fdf2a94..6137e3cdee 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/controls/components/controls-table.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/controls/components/controls-table.tsx @@ -9,42 +9,46 @@ import { DataTableSortList } from "@/components/data-table/data-table-sort-list" import { getControlColumns } from "./controls-table-columns"; import { DataTableToolbar } from "@/components/data-table/data-table-toolbar"; import { CreatePolicySheet } from "@/components/sheets/create-policy-sheet"; +import { useParams } from "next/navigation"; interface ControlsTableProps { - promises: Promise<[ - Awaited> - ]>; + promises: Promise<[Awaited>]>; } export function ControlsTable({ promises }: ControlsTableProps) { - const [{ data, pageCount }] = React.use(promises); + const [{ data, pageCount }] = React.use(promises); + const { orgId } = useParams(); + const columns = React.useMemo(() => getControlColumns(), []); - const columns = React.useMemo(() => getControlColumns(), []); + const { table } = useDataTable({ + data, + columns, + pageCount, + initialState: { + sorting: [{ id: "name", desc: true }], + columnPinning: { right: ["actions"] }, + }, + getRowId: (originalRow) => originalRow.id, + shallow: false, + clearOnDefault: true, + }); - const { table } = useDataTable({ - data, - columns, - pageCount, - initialState: { - sorting: [{ id: "name", desc: true }], - columnPinning: { right: ["actions"] }, - }, - getRowId: (originalRow) => originalRow.id, - shallow: false, - clearOnDefault: true, - }); - - return ( - <> - row.id} - > - - - - - - - ); -} \ No newline at end of file + return ( + <> + row.id} + rowClickBasePath={`/${orgId}/controls`} + > + + + + + + + ); +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/(overview)/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/(overview)/layout.tsx deleted file mode 100644 index fa86e00382..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/(overview)/layout.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { auth } from "@comp/auth"; -import { getI18n } from "@/locales/server"; -import { SecondaryMenu } from "@comp/ui/secondary-menu"; -import { headers } from "next/headers"; - -export default async function Layout({ - children, -}: { - children: React.ReactNode; -}) { - const t = await getI18n(); - const session = await auth.api.getSession({ - headers: await headers(), - }); - const user = session?.user; - const orgId = session?.session.activeOrganizationId; - - return ( -
- - -
{children}
-
- ); -} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/[policyId]/editor/actions/get-policy-details.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/[policyId]/editor/actions/get-policy-details.ts similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/[policyId]/editor/actions/get-policy-details.ts rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/[policyId]/editor/actions/get-policy-details.ts diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/[policyId]/editor/actions/update-policy.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/[policyId]/editor/actions/update-policy.ts similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/[policyId]/editor/actions/update-policy.ts rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/[policyId]/editor/actions/update-policy.ts diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/[policyId]/editor/components/PolicyDetails.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/[policyId]/editor/components/PolicyDetails.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/[policyId]/editor/components/PolicyDetails.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/[policyId]/editor/components/PolicyDetails.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/[policyId]/editor/components/PolicyHeader.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/[policyId]/editor/components/PolicyHeader.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/[policyId]/editor/components/PolicyHeader.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/[policyId]/editor/components/PolicyHeader.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/[policyId]/editor/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/[policyId]/editor/page.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/[policyId]/editor/page.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/[policyId]/editor/page.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/[policyId]/editor/types/index.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/[policyId]/editor/types/index.ts similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/[policyId]/editor/types/index.ts rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/[policyId]/editor/types/index.ts diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/[policyId]/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/[policyId]/page.tsx similarity index 82% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/[policyId]/page.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/[policyId]/page.tsx index bd51da859c..4b58dde86d 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/[policyId]/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/[policyId]/page.tsx @@ -1,26 +1,32 @@ -import { cache } from "react"; +import PageWithBreadcrumb from "@/components/pages/PageWithBreadcrumb"; import { PolicyOverview } from "@/components/policies/policy-overview"; import { getI18n } from "@/locales/server"; -import { db } from "@comp/db"; import { auth } from "@comp/auth"; +import { db } from "@comp/db"; import type { Metadata } from "next"; import { setStaticParamsLocale } from "next-international/server"; import { headers } from "next/headers"; - +import { cache } from "react"; export default async function PolicyDetails({ params, }: { - params: Promise<{ locale: string; policyId: string }>; + params: Promise<{ locale: string; policyId: string; orgId: string }>; }) { - const { locale, policyId } = await params; + const { locale, policyId, orgId } = await params; + setStaticParamsLocale(locale); const policy = await getPolicy(policyId); const assignees = await getAssignees(); return ( -
+ -
+ ); } diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/components/policies-table.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/components/policies-table.tsx deleted file mode 100644 index 611c13df8f..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/components/policies-table.tsx +++ /dev/null @@ -1,49 +0,0 @@ -"use client"; - -import * as React from "react"; - -import { DataTable } from "@/components/data-table/data-table"; -import { useDataTable } from "@/hooks/use-data-table"; -import { getPolicies } from "../data/queries"; -import { DataTableSortList } from "@/components/data-table/data-table-sort-list"; -import { getPolicyColumns } from "./policies-table-columns"; -import { DataTableToolbar } from "@/components/data-table/data-table-toolbar"; -import { CreatePolicySheet } from "@/components/sheets/create-policy-sheet"; - -interface PoliciesTableProps { - promises: Promise<[ - Awaited> - ]>; -} - -export function PoliciesTable({ promises }: PoliciesTableProps) { - const [{ data, pageCount }] = React.use(promises); - - const columns = React.useMemo(() => getPolicyColumns(), []); - - const { table } = useDataTable({ - data, - columns, - pageCount, - initialState: { - columnPinning: { right: ["actions"] }, - }, - getRowId: (originalRow) => originalRow.id, - shallow: false, - clearOnDefault: true, - }); - - return ( - <> - row.id} - > - - - - - - - ); -} \ No newline at end of file diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/layout.tsx deleted file mode 100644 index 3df8b9a0cb..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/layout.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { auth } from "@comp/auth"; -import { getI18n } from "@/locales/server"; -import { SecondaryMenu } from "@comp/ui/secondary-menu"; -import { headers } from "next/headers"; - -export default async function Layout({ - children, -}: { - children: React.ReactNode; -}) { - const t = await getI18n(); - const session = await auth.api.getSession({ - headers: await headers(), - }); - const user = session?.user; - const organizationId = session?.session.activeOrganizationId; - - return ( -
- - -
{children}
-
- ); -} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/page.tsx deleted file mode 100644 index 5e7ebf9c75..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/page.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { getI18n } from "@/locales/server"; -import type { Metadata } from "next"; -import { setStaticParamsLocale } from "next-international/server"; -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; -import { Suspense } from "react"; -import type { SearchParams } from "@/types"; -import { searchParamsCache } from "./data/validations"; -import { getValidFilters } from "@/lib/data-table"; -import { PoliciesTable } from "./components/policies-table"; -import { getPolicies } from "./data/queries"; - -interface PolicyTableProps { - params: Promise<{ locale: string }>; - searchParams: Promise; -} - -export default async function PoliciesPage({ - params, - ...props -}: PolicyTableProps) { - const { locale } = await params; - const searchParams = await props.searchParams; - const search = searchParamsCache.parse(searchParams); - const validFilters = getValidFilters(search.filters); - - setStaticParamsLocale(locale); - - const promises = Promise.all([ - getPolicies({ - ...search, - filters: validFilters, - }), - ]); - - return ( - - } - > - - - ); -} - -export async function generateMetadata({ - params, -}: { - params: Promise<{ locale: string }>; -}): Promise { - const { locale } = await params; - - setStaticParamsLocale(locale); - const t = await getI18n(); - - return { - title: t("sidebar.policies"), - }; -} \ No newline at end of file diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/[policyId]/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/[policyId]/layout.tsx deleted file mode 100644 index 315f4096a3..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/[policyId]/layout.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { getI18n } from "@/locales/server"; -import { SecondaryMenu } from "@comp/ui/secondary-menu"; - -interface LayoutProps { - children: React.ReactNode; - params: Promise<{ policyId: string; orgId: string }>; -} - -export default async function Layout({ children, params }: LayoutProps) { - const t = await getI18n(); - const { policyId, orgId } = await params; - - return ( -
- - -
{children}
-
- ); -} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/components/policies-table-action-bar.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/components/policies-table-action-bar.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/components/policies-table-action-bar.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/components/policies-table-action-bar.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/components/policies-table-columns.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/components/policies-table-columns.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/components/policies-table-columns.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/components/policies-table-columns.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/components/policies-table.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/components/policies-table.tsx new file mode 100644 index 0000000000..fbbb297726 --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/components/policies-table.tsx @@ -0,0 +1,55 @@ +"use client"; + +import * as React from "react"; + +import { DataTable } from "@/components/data-table/data-table"; +import { useDataTable } from "@/hooks/use-data-table"; +import { getPolicies } from "../data/queries"; +import { DataTableSortList } from "@/components/data-table/data-table-sort-list"; +import { getPolicyColumns } from "./policies-table-columns"; +import { DataTableToolbar } from "@/components/data-table/data-table-toolbar"; +import { CreatePolicySheet } from "@/components/sheets/create-policy-sheet"; +import { useSession } from "next-auth/react"; +import { useParams } from "next/navigation"; + +interface PoliciesTableProps { + promises: Promise<[Awaited>]>; +} + +export function PoliciesTable({ promises }: PoliciesTableProps) { + const [{ data, pageCount }] = React.use(promises); + const { orgId } = useParams(); + + const columns = React.useMemo(() => getPolicyColumns(), []); + + const { table } = useDataTable({ + data, + columns, + pageCount, + initialState: { + columnPinning: { right: ["actions"] }, + }, + getRowId: (originalRow) => originalRow.id, + shallow: false, + clearOnDefault: true, + }); + + return ( + <> + row.id} + rowClickBasePath={`/${orgId}/policies`} + > + + + + + + + ); +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/data/queries.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/data/queries.ts similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/data/queries.ts rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/data/queries.ts diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/data/validations.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/data/validations.ts similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/data/validations.ts rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/data/validations.ts diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/loading.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/loading.tsx new file mode 100644 index 0000000000..d21bfe214c --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/loading.tsx @@ -0,0 +1,12 @@ +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; + +export default function Loading() { + return ( + + ); +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/page.tsx new file mode 100644 index 0000000000..37f3097bfd --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/page.tsx @@ -0,0 +1,54 @@ +import PageWithBreadcrumb from "@/components/pages/PageWithBreadcrumb"; +import { getValidFilters } from "@/lib/data-table"; +import { getI18n } from "@/locales/server"; +import type { SearchParams } from "@/types"; +import type { Metadata } from "next"; +import { setStaticParamsLocale } from "next-international/server"; +import { PoliciesTable } from "./components/policies-table"; +import { getPolicies } from "./data/queries"; +import { searchParamsCache } from "./data/validations"; + +interface PolicyTableProps { + params: Promise<{ locale: string }>; + searchParams: Promise; +} + +export default async function PoliciesPage({ + params, + ...props +}: PolicyTableProps) { + const { locale } = await params; + const searchParams = await props.searchParams; + const search = searchParamsCache.parse(searchParams); + const validFilters = getValidFilters(search.filters); + + setStaticParamsLocale(locale); + + const promises = Promise.all([ + getPolicies({ + ...search, + filters: validFilters, + }), + ]); + + return ( + + + + ); +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale } = await params; + + setStaticParamsLocale(locale); + const t = await getI18n(); + + return { + title: t("sidebar.policies"), + }; +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/layout.tsx new file mode 100644 index 0000000000..05555fc0fb --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/layout.tsx @@ -0,0 +1,31 @@ +import { getI18n } from "@/locales/server"; +import { SecondaryMenu } from "@comp/ui/secondary-menu"; +import { Separator } from "@comp/ui/separator"; + +interface LayoutProps { + children: React.ReactNode; + params: Promise<{ policyId: string; orgId: string }>; +} + +export default async function Layout({ children, params }: LayoutProps) { + const t = await getI18n(); + const { orgId } = await params; + + return ( +
+ +
{children}
+
+ ); +} diff --git a/apps/app/src/components/data-table/data-table.tsx b/apps/app/src/components/data-table/data-table.tsx index 2f9279a4e1..672f4642ce 100644 --- a/apps/app/src/components/data-table/data-table.tsx +++ b/apps/app/src/components/data-table/data-table.tsx @@ -1,8 +1,9 @@ import { type Table as TanstackTable, flexRender } from "@tanstack/react-table"; +import { useRouter } from "next/navigation"; import type * as React from "react"; -import { usePathname, useRouter } from "next/navigation"; -import { DataTablePagination } from "./data-table-pagination"; +import { getCommonPinningStyles } from "@/lib/data-table"; +import { cn } from "@comp/ui/cn"; import { Table, TableBody, @@ -11,13 +12,13 @@ import { TableHeader, TableRow, } from "@comp/ui/table"; -import { getCommonPinningStyles } from "@/lib/data-table"; -import { cn } from "@comp/ui/cn"; +import { DataTablePagination } from "./data-table-pagination"; interface DataTableProps extends React.ComponentProps<"div"> { table: TanstackTable; actionBar?: React.ReactNode; getRowId?: (row: TData) => string; + rowClickBasePath: string; } export function DataTable({ @@ -26,15 +27,15 @@ export function DataTable({ children, className, getRowId, + rowClickBasePath, ...props }: DataTableProps) { const router = useRouter(); - const currentPath = usePathname(); const handleRowClick = (row: TData) => { if (getRowId) { const id = getRowId(row); - router.push(`${currentPath}/${id}`); + router.push(`${rowClickBasePath}/${id}`); } }; diff --git a/apps/app/src/components/forms/create-organization-form.tsx b/apps/app/src/components/forms/create-organization-form.tsx index f91b1fe481..74a191d990 100644 --- a/apps/app/src/components/forms/create-organization-form.tsx +++ b/apps/app/src/components/forms/create-organization-form.tsx @@ -3,6 +3,7 @@ import { createOrganizationAction } from "@/actions/organization/create-organization-action"; import { organizationSchema } from "@/actions/schema"; import { useI18n } from "@/locales/client"; +import { authClient } from "@comp/auth"; import { frameworks, type FrameworkId } from "@comp/data"; import { Button } from "@comp/ui/button"; import { Checkbox } from "@comp/ui/checkbox"; @@ -19,15 +20,14 @@ import { Icons } from "@comp/ui/icons"; import { Input } from "@comp/ui/input"; import { zodResolver } from "@hookform/resolvers/zod"; import { Loader2 } from "lucide-react"; -import { authClient } from "@comp/auth"; import { useAction } from "next-safe-action/hooks"; import Link from "next/link"; +import { useRouter } from "next/navigation"; import { useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import type { z } from "zod"; import { LogoSpinner } from "../logo-spinner"; -import { useRouter } from "next/navigation"; function OnboardingClient() { const [isCreatingOrganization, setIsCreatingOrganization] = useState(false); @@ -115,27 +115,6 @@ function OnboardingClient() { className="space-y-6" suppressHydrationWarning > - ( - - - {t("onboarding.fields.fullName.label")} - - - - - - - )} - /> - [] { return (
diff --git a/apps/app/src/locales/features/policies.ts b/apps/app/src/locales/features/policies.ts index 03ac23c4c8..0df4ac72ca 100644 --- a/apps/app/src/locales/features/policies.ts +++ b/apps/app/src/locales/features/policies.ts @@ -1,105 +1,105 @@ export const policies = { - dashboard: { - title: "Dashboard", - all: "All Policies", - policy_status: "Policy by Status", - policies_by_assignee: "Policies by Assignee", - policies_by_framework: "Policies by Framework", - sub_pages: { - overview: "Overview", - edit_policy: "Edit Policy", - }, - }, - overview: { - title: "Policy Overview", - form: { - update_policy: "Update Policy", - update_policy_description: "Update the policy title or description.", - update_policy_success: "Policy updated successfully", - update_policy_error: "Failed to update policy", - update_policy_title: "Policy Name", - status: "Status", - status_placeholder: "Select status", - description: "Description", - description_placeholder: "Enter policy description", - review_frequency: "Review Frequency", - review_frequency_placeholder: "Select a review frequency", - review_date: "Review Date", - review_date_placeholder: "Select a review date", - signature_required: "Require employees signature", - signature_not_required: "Do not ask employees to sign", - signature_requirement: "Signature Requirement", - signature_requirement_placeholder: "Select signature requirement", - policy_department: "Department", - policy_department_placeholder: "Select a department", - policy_status: "Policy Status", - policy_status_placeholder: "Select a policy status", - policy_assignee: "Assignee", - policy_assignee_placeholder: "Select an assignee", - }, - }, - new: { - success: "Policy successfully created", - error: "Failed to create policy", - details: "Policy Details", - title: "Enter a title for the policy", - description: "Enter a description for the policy", - }, - table: { - name: "Policy Name", - statuses: { - draft: "Draft", - published: "Published", - archived: "Archived", - }, - filters: { - owner: { - label: "Assignee", - placeholder: "Filter by assignee", - }, - }, - }, - filters: { - search: "Search policies...", - all: "All Policies", - }, - status: { - draft: "Draft", - published: "Published", - needs_review: "Needs Review", - archived: "Archived", - relevant: "Relevant", - "not-relevant": "Not Relevant", - }, - policies: "policies", - title: "Policies", - create_new: "Create New Policy", - search_placeholder: "Search policies...", - status_filter: "Filter by status", - all_statuses: "All statuses", - no_policies_title: "No policies yet", - no_policies_description: "Get started by creating your first policy", - create_first: "Create first policy", - last_updated: "Last updated: {{date}}", - save: "Save", - policy_details: "Policy Details", - archive: { - tooltip: "Archive policy", - restore_tooltip: "Restore policy", - title: "Archive Policy", - restore_title: "Restore Policy", - description: "Are you sure you want to archive this policy?", - restore_description: "Are you sure you want to restore this policy?", - confirm: "Archive", - restore_confirm: "Restore", - cancel: "Cancel", - success: "Policy archived successfully", - restore_success: "Policy restored successfully", - error: "Failed to update policy archive status", - status: "This policy is archived", - archived_on: "Archived on", - }, - edit: { - tooltip: "Edit policy", - }, + dashboard: { + title: "Dashboard", + all: "Policies", + policy_status: "Policy by Status", + policies_by_assignee: "Policies by Assignee", + policies_by_framework: "Policies by Framework", + sub_pages: { + overview: "Overview", + edit_policy: "Edit Policy", + }, + }, + overview: { + title: "Policy Overview", + form: { + update_policy: "Update Policy", + update_policy_description: "Update the policy title or description.", + update_policy_success: "Policy updated successfully", + update_policy_error: "Failed to update policy", + update_policy_title: "Policy Name", + status: "Status", + status_placeholder: "Select status", + description: "Description", + description_placeholder: "Enter policy description", + review_frequency: "Review Frequency", + review_frequency_placeholder: "Select a review frequency", + review_date: "Review Date", + review_date_placeholder: "Select a review date", + signature_required: "Require employees signature", + signature_not_required: "Do not ask employees to sign", + signature_requirement: "Signature Requirement", + signature_requirement_placeholder: "Select signature requirement", + policy_department: "Department", + policy_department_placeholder: "Select a department", + policy_status: "Policy Status", + policy_status_placeholder: "Select a policy status", + policy_assignee: "Assignee", + policy_assignee_placeholder: "Select an assignee", + }, + }, + new: { + success: "Policy successfully created", + error: "Failed to create policy", + details: "Policy Details", + title: "Enter a title for the policy", + description: "Enter a description for the policy", + }, + table: { + name: "Policy Name", + statuses: { + draft: "Draft", + published: "Published", + archived: "Archived", + }, + filters: { + owner: { + label: "Assignee", + placeholder: "Filter by assignee", + }, + }, + }, + filters: { + search: "Search policies...", + all: "All Policies", + }, + status: { + draft: "Draft", + published: "Published", + needs_review: "Needs Review", + archived: "Archived", + relevant: "Relevant", + "not-relevant": "Not Relevant", + }, + policies: "policies", + title: "Policies", + create_new: "Create New Policy", + search_placeholder: "Search policies...", + status_filter: "Filter by status", + all_statuses: "All statuses", + no_policies_title: "No policies yet", + no_policies_description: "Get started by creating your first policy", + create_first: "Create first policy", + last_updated: "Last updated: {{date}}", + save: "Save", + policy_details: "Policy Details", + archive: { + tooltip: "Archive policy", + restore_tooltip: "Restore policy", + title: "Archive Policy", + restore_title: "Restore Policy", + description: "Are you sure you want to archive this policy?", + restore_description: "Are you sure you want to restore this policy?", + confirm: "Archive", + restore_confirm: "Restore", + cancel: "Cancel", + success: "Policy archived successfully", + restore_success: "Policy restored successfully", + error: "Failed to update policy archive status", + status: "This policy is archived", + archived_on: "Archived on", + }, + edit: { + tooltip: "Edit policy", + }, } as const;