feat(widgets): wire up tabbed app editing in widget edit modal

- Add hideButtons/formRef props to AppForm for headless embedding
- Add Mantine Tabs to WidgetEditModal for app widgets (permission-gated)
- Pass appId from BoardItemMenu to the modal
- Add translation keys for tab labels and propagation notice
This commit is contained in:
Thomas Camlong committed 2026-05-28 14:55:15 +02:00
1 parent d83e9b2611
commit 3d1bb2f0ba
4 files changed
+167 -93

No files matched your search

+53 -25
View File
@@ -1,7 +1,7 @@
"use client";
import type { ChangeEventHandler } from "react";
import { useEffect, useRef } from "react";
import { useEffect, useImperativeHandle, useRef } from "react";
import { Button, Checkbox, Collapse, Group, Stack, Textarea, TextInput } from "@mantine/core";
import { useDebouncedValue, useDisclosure } from "@mantine/hooks";
import type { z } from "zod/v4";
@@ -17,6 +17,11 @@ import { findBestIconMatch } from "./icon-matcher";
type FormType = z.infer<typeof appManageSchema>;
export interface AppFormHandle {
submit: () => void;
isDirty: () => boolean;
}
interface AppFormProps {
showBackToOverview: boolean;
buttonLabels: {
@@ -26,6 +31,8 @@ interface AppFormProps {
initialValues?: FormType;
handleSubmit: (values: FormType, redirect: boolean, afterSuccess?: () => void) => void;
isPending: boolean;
hideButtons?: boolean;
formRef?: React.Ref<AppFormHandle>;
}
export const AppForm = ({
@@ -34,6 +41,8 @@ export const AppForm = ({
handleSubmit: originalHandleSubmit,
initialValues,
isPending,
hideButtons,
formRef,
}: AppFormProps) => {
const t = useI18n();
@@ -62,6 +71,15 @@ export const AppForm = ({
originalHandleSubmit(values, redirect, afterSuccess);
};
useImperativeHandle(
formRef,
() => ({
submit: () => form.onSubmit(handleSubmit)(),
isDirty: () => form.isDirty(),
}),
[form, handleSubmit],
);
const [opened, { open, close }] = useDisclosure((initialValues?.pingUrl?.length ?? 0) > 0);
const handleClickDifferentUrlPing: ChangeEventHandler<HTMLInputElement> = () => {
@@ -92,32 +110,32 @@ export const AppForm = ({
}
}, [debouncedName, iconsData]);
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack>
<TextInput {...form.getInputProps("name")} withAsterisk label={t("app.field.name.label")} />
<IconPicker {...form.getInputProps("iconUrl")} />
<Textarea
{...form.getInputProps("description")}
label={t("app.field.description.label")}
autosize
minRows={2}
resize="vertical"
/>
<TextInput {...form.getInputProps("href")} label={t("app.field.url.label")} />
const formFields = (
<Stack>
<TextInput {...form.getInputProps("name")} withAsterisk label={t("app.field.name.label")} />
<IconPicker {...form.getInputProps("iconUrl")} />
<Textarea
{...form.getInputProps("description")}
label={t("app.field.description.label")}
autosize
minRows={2}
resize="vertical"
/>
<TextInput {...form.getInputProps("href")} label={t("app.field.url.label")} />
<Checkbox
checked={opened}
onChange={handleClickDifferentUrlPing}
label={t("app.field.useDifferentUrlForPing.checkbox.label")}
description={t("app.field.useDifferentUrlForPing.checkbox.description")}
mt="md"
/>
<Checkbox
checked={opened}
onChange={handleClickDifferentUrlPing}
label={t("app.field.useDifferentUrlForPing.checkbox.label")}
description={t("app.field.useDifferentUrlForPing.checkbox.description")}
mt="md"
/>
<Collapse expanded={opened}>
<TextInput {...form.getInputProps("pingUrl")} />
</Collapse>
<Collapse expanded={opened}>
<TextInput {...form.getInputProps("pingUrl")} />
</Collapse>
{!hideButtons && (
<Group justify="end">
{showBackToOverview && (
<Button variant="default" component={Link} href="/manage/apps">
@@ -139,7 +157,17 @@ export const AppForm = ({
{buttonLabels.submit}
</Button>
</Group>
</Stack>
)}
</Stack>
);
if (hideButtons) {
return formFields;
}
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
{formFields}
</form>
);
};