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