Extract inline forms from CategorySection into separate components

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-04-21 10:40:44 +04:00
parent 653b43fc81
commit b29f51c303
4 changed files with 276 additions and 186 deletions

View File

@@ -0,0 +1,80 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { useTranslate } from "@probo/i18n";
import { Button, Input, Td, Tr } from "@probo/ui";
import { useState } from "react";
import type { CookieEntry } from "./CategorySection";
interface AddCookieRowProps {
isUpdating: boolean;
onSave: (cookie: CookieEntry) => void;
onCancel: () => void;
}
export function AddCookieRow({
isUpdating,
onSave,
onCancel,
}: AddCookieRowProps) {
const { __ } = useTranslate();
const [form, setForm] = useState<CookieEntry>({
name: "",
duration: "",
description: "",
});
return (
<Tr>
<Td className="pr-3">
<Input
value={form.name}
onChange={e => setForm({ ...form, name: e.target.value })}
placeholder={__("Cookie name")}
/>
</Td>
<Td className="pr-3">
<Input
value={form.duration}
onChange={e => setForm({ ...form, duration: e.target.value })}
placeholder={__("e.g. 1 year")}
/>
</Td>
<Td className="pr-3">
<Input
value={form.description}
onChange={e => setForm({ ...form, description: e.target.value })}
placeholder={__("Description")}
/>
</Td>
<Td>
<div className="flex items-center gap-2">
<Button
onClick={() => onSave(form)}
disabled={isUpdating}
>
{__("Save")}
</Button>
<Button
variant="secondary"
onClick={onCancel}
>
{__("Cancel")}
</Button>
</div>
</Td>
</Tr>
);
}

View File

@@ -24,10 +24,8 @@ import {
IconPencil, IconPencil,
IconPlusSmall, IconPlusSmall,
IconTrashCan, IconTrashCan,
Input,
Tbody, Tbody,
Td, Td,
Textarea,
Th, Th,
Thead, Thead,
Tr, Tr,
@@ -41,6 +39,16 @@ import type { CategorySectionFragment$key } from "#/__generated__/core/CategoryS
import type { CategorySectionMoveCookieMutation } from "#/__generated__/core/CategorySectionMoveCookieMutation.graphql"; import type { CategorySectionMoveCookieMutation } from "#/__generated__/core/CategorySectionMoveCookieMutation.graphql";
import type { CategorySectionUpdateMutation } from "#/__generated__/core/CategorySectionUpdateMutation.graphql"; import type { CategorySectionUpdateMutation } from "#/__generated__/core/CategorySectionUpdateMutation.graphql";
import { AddCookieRow } from "./AddCookieRow";
import { EditCategoryForm } from "./EditCategoryForm";
import { EditCookieRow } from "./EditCookieRow";
export interface CookieEntry {
name: string;
duration: string;
description: string;
}
export const categorySectionFragment = graphql` export const categorySectionFragment = graphql`
fragment CategorySectionFragment on CookieCategory { fragment CategorySectionFragment on CookieCategory {
id id
@@ -134,12 +142,6 @@ const moveCookieMutation = graphql`
} }
`; `;
interface CookieEntry {
name: string;
duration: string;
description: string;
}
interface CategorySectionProps { interface CategorySectionProps {
categoryKey: CategorySectionFragment$key; categoryKey: CategorySectionFragment$key;
onDelete?: () => void; onDelete?: () => void;
@@ -156,18 +158,8 @@ export function CategorySection({ categoryKey, onDelete }: CategorySectionProps)
= useMutation<CategorySectionMoveCookieMutation>(moveCookieMutation); = useMutation<CategorySectionMoveCookieMutation>(moveCookieMutation);
const [isEditingCategory, setIsEditingCategory] = useState(false); const [isEditingCategory, setIsEditingCategory] = useState(false);
const [editName, setEditName] = useState(category.name); const [editingCookieIndex, setEditingCookieIndex] = useState<number | null>(null);
const [editDescription, setEditDescription] = useState(category.description);
const [editingCookieIndex, setEditingCookieIndex] = useState<number | null>(
null,
);
const [isAddingCookie, setIsAddingCookie] = useState(false); const [isAddingCookie, setIsAddingCookie] = useState(false);
const [cookieForm, setCookieForm] = useState<CookieEntry>({
name: "",
duration: "",
description: "",
});
const doUpdate = ( const doUpdate = (
input: Record<string, unknown>, input: Record<string, unknown>,
@@ -209,48 +201,24 @@ export function CategorySection({ categoryKey, onDelete }: CategorySectionProps)
}); });
}; };
const handleSaveCategory = () => { const handleSaveCategory = (name: string, description: string) => {
doUpdate({ name: editName, description: editDescription }, () => { doUpdate({ name, description }, () => {
setIsEditingCategory(false); setIsEditingCategory(false);
}); });
}; };
const handleCancelCategoryEdit = () => { const handleSaveEditCookie = (index: number, cookie: CookieEntry) => {
setEditName(category.name); if (!cookie.name.trim()) return;
setEditDescription(category.description);
setIsEditingCategory(false);
};
const handleStartEditCookie = (index: number) => {
const c = category.cookies[index];
setCookieForm({
name: c.name,
duration: c.duration,
description: c.description,
});
setEditingCookieIndex(index);
setIsAddingCookie(false);
};
const handleSaveEditCookie = () => {
if (editingCookieIndex === null) return;
if (!cookieForm.name.trim()) return;
const newCookies = category.cookies.map((c, i) => const newCookies = category.cookies.map((c, i) =>
i === editingCookieIndex i === index
? { ...cookieForm } ? { ...cookie }
: { name: c.name, duration: c.duration, description: c.description }, : { name: c.name, duration: c.duration, description: c.description },
); );
doUpdate({ cookies: newCookies }, () => { doUpdate({ cookies: newCookies }, () => {
setEditingCookieIndex(null); setEditingCookieIndex(null);
setCookieForm({ name: "", duration: "", description: "" });
}); });
}; };
const handleCancelEditCookie = () => {
setEditingCookieIndex(null);
setCookieForm({ name: "", duration: "", description: "" });
};
const handleDeleteCookie = (index: number) => { const handleDeleteCookie = (index: number) => {
const newCookies = category.cookies const newCookies = category.cookies
.filter((_, i) => i !== index) .filter((_, i) => i !== index)
@@ -262,33 +230,21 @@ export function CategorySection({ categoryKey, onDelete }: CategorySectionProps)
doUpdate({ cookies: newCookies }); doUpdate({ cookies: newCookies });
}; };
const handleStartAddCookie = () => { const handleSaveNewCookie = (cookie: CookieEntry) => {
setCookieForm({ name: "", duration: "", description: "" }); if (!cookie.name.trim()) return;
setIsAddingCookie(true);
setEditingCookieIndex(null);
};
const handleSaveNewCookie = () => {
if (!cookieForm.name.trim()) return;
const newCookies = [ const newCookies = [
...category.cookies.map(c => ({ ...category.cookies.map(c => ({
name: c.name, name: c.name,
duration: c.duration, duration: c.duration,
description: c.description, description: c.description,
})), })),
{ ...cookieForm }, { ...cookie },
]; ];
doUpdate({ cookies: newCookies }, () => { doUpdate({ cookies: newCookies }, () => {
setIsAddingCookie(false); setIsAddingCookie(false);
setCookieForm({ name: "", duration: "", description: "" });
}); });
}; };
const handleCancelAddCookie = () => {
setIsAddingCookie(false);
setCookieForm({ name: "", duration: "", description: "" });
};
const allCategories = category.cookieBanner.categories.edges.map(e => e.node) ?? []; const allCategories = category.cookieBanner.categories.edges.map(e => e.node) ?? [];
const siblingCategories = allCategories.filter(c => c.id !== category.id); const siblingCategories = allCategories.filter(c => c.id !== category.id);
@@ -336,33 +292,13 @@ export function CategorySection({ categoryKey, onDelete }: CategorySectionProps)
<div className="p-4"> <div className="p-4">
{isEditingCategory {isEditingCategory
? ( ? (
<div className="space-y-3"> <EditCategoryForm
<Input name={category.name}
value={editName} description={category.description}
onChange={e => setEditName(e.target.value)} isUpdating={isUpdating}
placeholder={__("Category name")} onSave={handleSaveCategory}
onCancel={() => setIsEditingCategory(false)}
/> />
<Textarea
value={editDescription}
onChange={e => setEditDescription(e.target.value)}
placeholder={__("Category description")}
rows={2}
/>
<div className="flex items-center gap-2">
<Button
onClick={handleSaveCategory}
disabled={isUpdating}
>
{isUpdating ? __("Saving...") : __("Save")}
</Button>
<Button
variant="secondary"
onClick={handleCancelCategoryEdit}
>
{__("Cancel")}
</Button>
</div>
</div>
) )
: ( : (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -420,54 +356,17 @@ export function CategorySection({ categoryKey, onDelete }: CategorySectionProps)
{category.cookies.map((cookie, index) => {category.cookies.map((cookie, index) =>
editingCookieIndex === index editingCookieIndex === index
? ( ? (
<Tr key={index}> <EditCookieRow
<Td className="pr-3"> key={index}
<Input cookie={{
value={cookieForm.name} name: cookie.name,
onChange={e => duration: cookie.duration,
setCookieForm({ ...cookieForm, name: e.target.value })} description: cookie.description,
placeholder={__("Cookie name")} }}
isUpdating={isUpdating}
onSave={updated => handleSaveEditCookie(index, updated)}
onCancel={() => setEditingCookieIndex(null)}
/> />
</Td>
<Td className="pr-3">
<Input
value={cookieForm.duration}
onChange={e =>
setCookieForm({
...cookieForm,
duration: e.target.value,
})}
placeholder={__("e.g. 1 year")}
/>
</Td>
<Td className="pr-3">
<Input
value={cookieForm.description}
onChange={e =>
setCookieForm({
...cookieForm,
description: e.target.value,
})}
placeholder={__("Description")}
/>
</Td>
<Td>
<div className="flex items-center gap-1">
<Button
onClick={handleSaveEditCookie}
disabled={isUpdating}
>
{__("Save")}
</Button>
<Button
variant="secondary"
onClick={handleCancelEditCookie}
>
{__("Cancel")}
</Button>
</div>
</Td>
</Tr>
) )
: ( : (
<Tr key={index}> <Tr key={index}>
@@ -484,7 +383,10 @@ export function CategorySection({ categoryKey, onDelete }: CategorySectionProps)
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<button <button
type="button" type="button"
onClick={() => handleStartEditCookie(index)} onClick={() => {
setEditingCookieIndex(index);
setIsAddingCookie(false);
}}
className="p-1 rounded cursor-pointer" className="p-1 rounded cursor-pointer"
> >
<IconPencil size={14} /> <IconPencil size={14} />
@@ -524,51 +426,11 @@ export function CategorySection({ categoryKey, onDelete }: CategorySectionProps)
), ),
)} )}
{isAddingCookie && ( {isAddingCookie && (
<Tr> <AddCookieRow
<Td className="pr-3"> isUpdating={isUpdating}
<Input onSave={handleSaveNewCookie}
value={cookieForm.name} onCancel={() => setIsAddingCookie(false)}
onChange={e =>
setCookieForm({ ...cookieForm, name: e.target.value })}
placeholder={__("Cookie name")}
/> />
</Td>
<Td className="pr-3">
<Input
value={cookieForm.duration}
onChange={e =>
setCookieForm({ ...cookieForm, duration: e.target.value })}
placeholder={__("e.g. 1 year")}
/>
</Td>
<Td className="pr-3">
<Input
value={cookieForm.description}
onChange={e =>
setCookieForm({
...cookieForm,
description: e.target.value,
})}
placeholder={__("Description")}
/>
</Td>
<Td>
<div className="flex items-center gap-2">
<Button
onClick={handleSaveNewCookie}
disabled={isUpdating}
>
{__("Save")}
</Button>
<Button
variant="secondary"
onClick={handleCancelAddCookie}
>
{__("Cancel")}
</Button>
</div>
</Td>
</Tr>
)} )}
</Tbody> </Tbody>
</table> </table>
@@ -577,7 +439,10 @@ export function CategorySection({ categoryKey, onDelete }: CategorySectionProps)
<div className="p-3 border-t border-border-low"> <div className="p-3 border-t border-border-low">
<Button <Button
variant="secondary" variant="secondary"
onClick={handleStartAddCookie} onClick={() => {
setIsAddingCookie(true);
setEditingCookieIndex(null);
}}
> >
<IconPlusSmall size={14} /> <IconPlusSmall size={14} />
{__("Add Cookie")} {__("Add Cookie")}

View File

@@ -0,0 +1,67 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { useTranslate } from "@probo/i18n";
import { Button, Input, Textarea } from "@probo/ui";
import { useState } from "react";
interface EditCategoryFormProps {
name: string;
description: string;
isUpdating: boolean;
onSave: (name: string, description: string) => void;
onCancel: () => void;
}
export function EditCategoryForm({
name,
description,
isUpdating,
onSave,
onCancel,
}: EditCategoryFormProps) {
const { __ } = useTranslate();
const [editName, setEditName] = useState(name);
const [editDescription, setEditDescription] = useState(description);
return (
<div className="space-y-3">
<Input
value={editName}
onChange={e => setEditName(e.target.value)}
placeholder={__("Category name")}
/>
<Textarea
value={editDescription}
onChange={e => setEditDescription(e.target.value)}
placeholder={__("Category description")}
rows={2}
/>
<div className="flex items-center gap-2">
<Button
onClick={() => onSave(editName, editDescription)}
disabled={isUpdating}
>
{isUpdating ? __("Saving...") : __("Save")}
</Button>
<Button
variant="secondary"
onClick={onCancel}
>
{__("Cancel")}
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,78 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { useTranslate } from "@probo/i18n";
import { Button, Input, Td, Tr } from "@probo/ui";
import { useState } from "react";
import type { CookieEntry } from "./CategorySection";
interface EditCookieRowProps {
cookie: CookieEntry;
isUpdating: boolean;
onSave: (cookie: CookieEntry) => void;
onCancel: () => void;
}
export function EditCookieRow({
cookie,
isUpdating,
onSave,
onCancel,
}: EditCookieRowProps) {
const { __ } = useTranslate();
const [form, setForm] = useState<CookieEntry>(cookie);
return (
<Tr>
<Td className="pr-3">
<Input
value={form.name}
onChange={e => setForm({ ...form, name: e.target.value })}
placeholder={__("Cookie name")}
/>
</Td>
<Td className="pr-3">
<Input
value={form.duration}
onChange={e => setForm({ ...form, duration: e.target.value })}
placeholder={__("e.g. 1 year")}
/>
</Td>
<Td className="pr-3">
<Input
value={form.description}
onChange={e => setForm({ ...form, description: e.target.value })}
placeholder={__("Description")}
/>
</Td>
<Td>
<div className="flex items-center gap-1">
<Button
onClick={() => onSave(form)}
disabled={isUpdating}
>
{__("Save")}
</Button>
<Button
variant="secondary"
onClick={onCancel}
>
{__("Cancel")}
</Button>
</div>
</Td>
</Tr>
);
}