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
8 changes: 6 additions & 2 deletions apps/bot/src/review-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { changeSubmissionStatus, prisma } from "@msk-forms/db";
import {
DEFAULT_STATUSES,
parseBotConfig,
parseFormSettings,
type StatusChangeNotification,
} from "@msk-forms/shared";
import {
Expand Down Expand Up @@ -53,7 +54,7 @@ export async function handleReviewButton(interaction: ButtonInteraction): Promis
status: true,
userId: true,
guildId: true,
form: { select: { title: true } },
form: { select: { title: true, settings: true } },
guild: { select: { discordGuildId: true, botConfig: true } },
},
});
Expand Down Expand Up @@ -83,7 +84,10 @@ export async function handleReviewButton(interaction: ButtonInteraction): Promis
});

if (changed && action === "accept" && submission.userId) {
const roleId = parseBotConfig(submission.guild.botConfig).acceptedRoleId;
// Per-form accepted role overrides the guild-wide default.
const roleId =
parseFormSettings(submission.form.settings).acceptedRoleId ??
parseBotConfig(submission.guild.botConfig).acceptedRoleId;
if (roleId) await grantRole(interaction, submission.userId, roleId);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export async function PATCH(
status: input.status,
visibility: input.visibility,
schema: input.spec as Prisma.InputJsonValue,
settings: (input.settings ?? {}) as Prisma.InputJsonValue,
version: { increment: 1 },
},
});
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/app/api/guilds/[guildId]/forms/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export async function POST(
status: input.status,
visibility: input.visibility,
schema: input.spec as Prisma.InputJsonValue,
settings: (input.settings ?? {}) as Prisma.InputJsonValue,
createdById: user.id,
},
select: { id: true },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Card } from "@msk-forms/ui";
import { notFound } from "next/navigation";

import { parseFormSettings } from "@msk-forms/shared";

import { FormBuilder } from "@/components/builder/form-builder";
import { requireUser } from "@/lib/auth";
import { getFormForEdit, parseFormSpec } from "@/lib/forms";
Expand Down Expand Up @@ -46,6 +48,7 @@ export default async function EditFormPage({
slug: form.slug,
status: form.status,
visibility: form.visibility,
acceptedRoleId: parseFormSettings(form.settings).acceptedRoleId ?? "",
pages: spec?.pages.length ? spec.pages : [{ id: "p1", title: "", fields: [] }],
}}
/>
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/app/dashboard/[guildId]/forms/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export default async function NewFormPage({
slug: "",
status: "draft",
visibility: "public",
acceptedRoleId: "",
pages: [{ id: "p1", title: "", fields: [] }],
}}
/>
Expand Down
10 changes: 10 additions & 0 deletions apps/web/src/components/builder/form-builder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface FormBuilderInitial {
slug: string;
status: string;
visibility: string;
acceptedRoleId: string;
pages: FormPage[];
}

Expand Down Expand Up @@ -53,6 +54,7 @@ export function FormBuilder({
const [slug, setSlug] = useState(initial.slug);
const [status, setStatus] = useState(initial.status);
const [visibility, setVisibility] = useState(initial.visibility);
const [acceptedRoleId, setAcceptedRoleId] = useState(initial.acceptedRoleId);
const [pages, setPages] = useState<FormPage[]>(initial.pages);
const [addType, setAddType] = useState<FieldType>("short_text");
const [error, setError] = useState<string | null>(null);
Expand Down Expand Up @@ -122,6 +124,7 @@ export function FormBuilder({
status,
visibility,
spec,
settings: acceptedRoleId.trim() ? { acceptedRoleId: acceptedRoleId.trim() } : {},
};

setSaving(true);
Expand Down Expand Up @@ -196,6 +199,13 @@ export function FormBuilder({
/>
</Field>
</div>
<Field label={t.acceptedRole} hint={t.acceptedRoleHint}>
<Input
value={acceptedRoleId}
placeholder="123456789012345678"
onChange={(e) => setAcceptedRoleId(e.target.value)}
/>
</Field>
</Card>

{pages.map((page, pi) => (
Expand Down
6 changes: 4 additions & 2 deletions apps/web/src/i18n/dictionaries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ const en = {
reviewChannel: "Review channel ID",
reviewChannelHint: "New submissions are posted here for your team to review.",
acceptedRole: "Accepted role ID",
acceptedRoleHint: "Granted to the applicant when a submission is accepted.",
acceptedRoleHint: "Default role granted on acceptance. Each form can override this in its settings.",
idHint: "Enable Developer Mode in Discord (Settings → Advanced), then right-click a channel/role → Copy ID.",
postName: "Posting name",
postNameHint: "When set, the bot posts forms & review embeds under this name with your logo (via a webhook). Needs the Manage Webhooks permission. Leave empty to post as the bot.",
Expand All @@ -150,6 +150,7 @@ const en = {
status: "Status", visibility: "Visibility",
statusDraft: "Draft", statusLive: "Live", statusClosed: "Closed", statusArchived: "Archived",
visPublic: "Public", visAuth: "Login required",
acceptedRole: "Accepted role ID", acceptedRoleHint: "Granted on acceptance for this form (overrides the guild default in Bot settings). Leave empty to use the default.",
addField: "Add field", add: "Add",
page: "Page", addPage: "Add page", removePage: "Remove page", pageTitlePh: "Page title (optional)…",
saving: "Saving…", saveChanges: "Save changes", createForm: "Create form", cancel: "Cancel",
Expand Down Expand Up @@ -330,7 +331,7 @@ const de: Dictionary = {
reviewChannel: "Review-Kanal-ID",
reviewChannelHint: "Neue Einreichungen werden hier für dein Team zur Prüfung gepostet.",
acceptedRole: "Annahme-Rollen-ID",
acceptedRoleHint: "Wird dem Bewerber bei Annahme der Einreichung vergeben.",
acceptedRoleHint: "Standard-Rolle bei Annahme. Jedes Formular kann das in seinen Einstellungen überschreiben.",
idHint: "Aktiviere den Entwicklermodus in Discord (Einstellungen → Erweitert), dann Rechtsklick auf Kanal/Rolle → ID kopieren.",
postName: "Anzeigename beim Posten",
postNameHint: "Wenn gesetzt, postet der Bot Formulare & Review-Embeds unter diesem Namen mit deinem Logo (per Webhook). Benötigt die Berechtigung zum Verwalten von Webhooks. Leer lassen, um als Bot zu posten.",
Expand All @@ -343,6 +344,7 @@ const de: Dictionary = {
status: "Status", visibility: "Sichtbarkeit",
statusDraft: "Entwurf", statusLive: "Live", statusClosed: "Geschlossen", statusArchived: "Archiviert",
visPublic: "Öffentlich", visAuth: "Anmeldung erforderlich",
acceptedRole: "Annahme-Rollen-ID", acceptedRoleHint: "Wird bei Annahme für dieses Formular vergeben (überschreibt den Guild-Standard in den Bot-Einstellungen). Leer lassen = Standard verwenden.",
addField: "Feld hinzufügen", add: "Hinzufügen",
page: "Seite", addPage: "Seite hinzufügen", removePage: "Seite entfernen", pageTitlePh: "Seitentitel (optional)…",
saving: "Wird gespeichert…", saveChanges: "Änderungen speichern", createForm: "Formular erstellen", cancel: "Abbrechen",
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/lib/form-input.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { formSpecSchema } from "@msk-forms/shared";
import { formSettingsSchema, formSpecSchema } from "@msk-forms/shared";
import { z } from "zod";

/**
Expand All @@ -17,6 +17,7 @@ export const formInputSchema = z.object({
status: z.enum(["draft", "live", "closed", "archived"]),
visibility: z.enum(["public", "authenticated", "password", "role_required"]),
spec: formSpecSchema,
settings: formSettingsSchema.optional(),
});

export type FormInput = z.infer<typeof formInputSchema>;
1 change: 1 addition & 0 deletions apps/web/src/lib/forms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export async function getFormForEdit(formId: string, guildId: string) {
status: true,
visibility: true,
schema: true,
settings: true,
},
});
if (!form || form.guildId !== guildId) return null;
Expand Down
20 changes: 20 additions & 0 deletions packages/shared/src/form-settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { z } from "zod";

/**
* Per-form settings stored in `Form.settings` (JSON). Currently a per-form
* accepted role that overrides the guild-wide `botConfig.acceptedRoleId`.
*/
const snowflake = z.string().regex(/^\d{17,20}$/, "Enter a valid Discord ID.");

export const formSettingsSchema = z.object({
/** Role granted on acceptance for THIS form (overrides the guild default). */
acceptedRoleId: snowflake.optional(),
});

export type FormSettings = z.infer<typeof formSettingsSchema>;

/** Parse a stored Form.settings JSON blob, falling back to empty settings. */
export function parseFormSettings(json: unknown): FormSettings {
const result = formSettingsSchema.safeParse(json);
return result.success ? result.data : {};
}
1 change: 1 addition & 0 deletions packages/shared/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Extensionless exports so Turbopack resolves them when web bundles this
// package (a `.js` suffix on a `.ts` source breaks Turbopack resolution).
export * from "./form-spec";
export * from "./form-settings";
export * from "./conditions";
export * from "./constants";
export * from "./notifications";
Expand Down