diff --git a/.github/workflows/database-migrations.yml b/.github/workflows/database-migrations.yml new file mode 100644 index 0000000000..e43b13ab58 --- /dev/null +++ b/.github/workflows/database-migrations.yml @@ -0,0 +1,43 @@ +name: Database Migrations + +on: + push: + branches: + - main + paths: + - "packages/db/prisma/schema.prisma" + - "packages/db/prisma/migrations/**" + workflow_dispatch: # Allows manual triggering + +env: + NODE_VERSION: "20.x" + +jobs: + migrate: + name: Run Database Migrations + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Apply database migrations + env: + DATABASE_URL: ${{ secrets.DATABASE_URL_PROD }} + run: | + cd packages/db + npx prisma migrate deploy + + - name: Generate Prisma Client + run: | + cd packages/db + npx prisma generate diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/policies/[id]/Actions/publish-policy.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/policies/[id]/Actions/publish-policy.ts new file mode 100644 index 0000000000..246cd11317 --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/policies/[id]/Actions/publish-policy.ts @@ -0,0 +1,59 @@ +"use server"; + +import { authActionClient } from "@/actions/safe-action"; +import { db, type OrganizationPolicy } from "@bubba/db"; +import { z } from "zod"; + +const schema = z.object({ + id: z.string(), +}); + +export type PublishPolicyResponse = { + success: boolean; + data?: OrganizationPolicy; + error?: string; +}; + +export const publishPolicy = authActionClient + .schema(schema) + .metadata({ + name: "publish-policy", + track: { + event: "publish-policy", + channel: "server", + }, + }) + .action(async ({ ctx, parsedInput }) => { + const { user } = ctx; + const { id } = parsedInput; + + if (!user.organizationId) { + return { + success: false, + error: "Not authorized - no organization found", + }; + } + + try { + const policy = await db.organizationPolicy.update({ + where: { + id, + organizationId: user.organizationId!, + }, + data: { + status: "published", + updatedAt: new Date(), + }, + }); + + return { + success: true, + data: policy, + }; + } catch (error) { + return { + success: false, + error: "Failed to publish policy", + }; + } + }); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/policies/[id]/Components/PolicyOverview.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/policies/[id]/Components/PolicyOverview.tsx new file mode 100644 index 0000000000..2d15cd0097 --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/policies/[id]/Components/PolicyOverview.tsx @@ -0,0 +1,29 @@ +"use client"; + +import type { JSONContent } from "@tiptap/react"; +import PolicyEditor from "@/components/editor/advanced-editor"; +import { usePolicy } from "@/app/[locale]/(app)/(dashboard)/policies/hooks/usePolicy"; +import { Button } from "@bubba/ui/button"; +import { Separator } from "@bubba/ui/separator"; + +export function PolicyOverview({ policyId }: { policyId: string }) { + const { data: policy } = usePolicy({ policyId }); + + if (!policy) return null; + + const content = policy.content as JSONContent; + + if (!content) return null; + + return ( +
+
+ +
+ + +
+ ); +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/policies/[id]/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/policies/[id]/layout.tsx index 13e511a661..a64030c109 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/policies/[id]/layout.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/policies/[id]/layout.tsx @@ -20,7 +20,7 @@ export default async function Layout({ } return ( -
+
{children}
); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/policies/[id]/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/policies/[id]/page.tsx index da98f65d53..b257dd6e28 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/policies/[id]/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/policies/[id]/page.tsx @@ -1,6 +1,4 @@ -import { auth } from "@/auth"; import { PolicyOverview } from "@/components/policies/policy-overview"; -import { redirect } from "next/navigation"; interface PageProps { params: Promise<{ id: string }>; diff --git a/apps/app/src/components/policies/policy-overview.tsx b/apps/app/src/components/policies/policy-overview.tsx index 286b9b5293..7d98cdcd80 100644 --- a/apps/app/src/components/policies/policy-overview.tsx +++ b/apps/app/src/components/policies/policy-overview.tsx @@ -3,9 +3,22 @@ import type { JSONContent } from "@tiptap/react"; import PolicyEditor from "../editor/advanced-editor"; import { usePolicy } from "@/app/[locale]/(app)/(dashboard)/policies/hooks/usePolicy"; +import { Button } from "@bubba/ui/button"; +import { Separator } from "@bubba/ui/separator"; +import { useAction } from "next-safe-action/hooks"; +import { publishPolicy } from "@/app/[locale]/(app)/(dashboard)/policies/[id]/Actions/publish-policy"; +import { toast } from "sonner"; export function PolicyOverview({ policyId }: { policyId: string }) { const { data: policy } = usePolicy({ policyId }); + const { execute, isExecuting } = useAction( + () => publishPolicy({ id: policyId }), + { + onSuccess: () => { + toast.success("Policy published successfully"); + }, + } + ); if (!policy) return null; @@ -14,7 +27,17 @@ export function PolicyOverview({ policyId }: { policyId: string }) { if (!content) return null; return ( -
+
+
+ +
+
);