add Tags pages

This commit is contained in:
Alex Holliday committed 2026-05-20 10:28:26 -07:00
1 parent fe74d4b465
commit 1f7344f2e6
3 files changed
+291

No files matched your search

@@ -0,0 +1,103 @@
import { ActionsMenu, type ActionMenuItem } from "@/Components/actions-menu";
import Typography from "@mui/material/Typography";
import type { Header } from "@/Components/design-elements/Table";
import { Table, Chip } from "@/Components/design-elements";
import { Pagination } from "@/Components/design-elements/Table";
import { useClientPagination } from "@/Hooks/useClientPagination";
import type { Tag } from "@/Types/Tag";
import { useNavigate } from "react-router";
import { useTranslation } from "react-i18next";
import { useTheme } from "@mui/material";
interface TagsTableProps {
tags: Tag[];
setSelectedTag: Function;
}
export const TagsTable = ({ tags, setSelectedTag }: TagsTableProps) => {
const navigate = useNavigate();
const { t } = useTranslation();
const theme = useTheme();
const { pagedRows, paginationProps } = useClientPagination(tags);
const getActions = (tag: Tag): ActionMenuItem[] => {
return [
{
id: 1,
label: t("pages.common.monitors.actions.configure"),
action: () => {
navigate(`/tags/configure/${tag.id}`);
},
closeMenu: true,
},
{
id: 2,
label: (
<Typography color={theme.palette.error.main}>
{t("pages.common.monitors.actions.delete")}
</Typography>
),
action: async () => {
setSelectedTag(tag);
},
closeMenu: true,
},
];
};
const getHeaders = () => {
const headers: Header<Tag>[] = [
{
id: "name",
content: t("common.table.headers.name"),
render: (row) => {
return <Typography>{row?.name}</Typography>;
},
},
{
id: "color",
content: t("pages.tags.table.headers.color"),
render: (row) => {
return <Typography textTransform={"capitalize"}>{row?.color}</Typography>;
},
},
{
id: "chip",
content: t("pages.tags.table.headers.chip"),
render: (row) => {
return (
<Chip
text={row.name}
color={row.color}
/>
);
},
},
{
id: "actions",
content: t("common.table.headers.actions"),
render: (row) => {
return <ActionsMenu items={getActions(row)} />;
},
},
];
return headers;
};
return (
<>
<Table
headers={getHeaders()}
data={pagedRows}
onRowClick={(row) => {
navigate(`/tags/configure/${row.id}`);
}}
/>
{tags.length > 0 && <Pagination {...paginationProps} />}
</>
);
};
+116
View File
@@ -0,0 +1,116 @@
import { BasePage, ConfigBox } from "@/Components/design-elements";
import { TextField, Button, ColorInput } from "@/Components/inputs";
import Stack from "@mui/material/Stack";
import { useTheme } from "@mui/material/styles";
import { useEffect } from "react";
import { useParams } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useGet, usePost, usePatch } from "@/Hooks/UseApi";
import type { TagFormData } from "@/Validation/tag";
import type { Tag } from "@/Types/Tag";
import { useTranslation } from "react-i18next";
import { useTagsForm } from "@/Hooks/useTagsForm";
const TagsCreatePage = () => {
const { t } = useTranslation();
const theme = useTheme();
const navigate = useNavigate();
const { tagId } = useParams<{ tagId: string }>();
const isEditMode = Boolean(tagId);
const { data: existingTag } = useGet<Tag>(isEditMode ? `/tags/${tagId}` : null);
const { post, loading: isSubmitting } = usePost<TagFormData, Tag>();
const { patch, loading: isPatching } = usePatch<TagFormData, Tag>();
const { schema, defaults } = useTagsForm({ data: existingTag });
const form = useForm<TagFormData>({
resolver: zodResolver(schema),
defaultValues: defaults,
});
const { control, reset, handleSubmit } = form;
useEffect(() => {
reset(defaults);
}, [defaults, reset]);
const onSubmit = async (data: TagFormData) => {
const result = isEditMode
? await patch(`/tags/${tagId}`, data)
: await post("/tags", data);
if (result) {
navigate("/tags");
}
};
return (
<BasePage
component="form"
onSubmit={handleSubmit(onSubmit)}
>
<ConfigBox
title={t("pages.tags.form.name.title")}
subtitle={t("pages.tags.form.name.description")}
rightContent={
<Controller
name="name"
control={control}
defaultValue={defaults.name}
render={({ field, fieldState }) => (
<TextField
{...field}
type="text"
fieldLabel={t("pages.tags.form.name.optionName")}
placeholder={t("pages.tags.form.name.placeholder")}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
}
/>
<ConfigBox
title={t("pages.tags.form.color.title")}
subtitle={t("pages.tags.form.color.description")}
rightContent={
<Controller
name="color"
control={control}
defaultValue={defaults.color}
render={({ field }) => (
<ColorInput
format="hex"
value={field.value}
onChange={field.onChange}
fieldLabel={t("pages.tags.form.color.optionName")}
/>
)}
/>
}
/>
<Stack
direction="row"
justifyContent="flex-end"
spacing={theme.spacing(2)}
>
<Button
loading={isSubmitting || isPatching}
type="submit"
variant="contained"
color="primary"
>
{t("common.buttons.save")}
</Button>
</Stack>
</BasePage>
);
};
export default TagsCreatePage;
+72
View File
@@ -0,0 +1,72 @@
import { BasePageWithStates } from "@/Components/design-elements";
import { TagsTable } from "@/Pages/Tags/components/TagsTable";
import { Dialog } from "@/Components/inputs";
import { HeaderCreate } from "@/Components/common";
import { useState } from "react";
import { useGet, useDelete } from "@/Hooks/UseApi";
import { useTranslation } from "react-i18next";
import type { Tag } from "@/Types/Tag";
import { useIsAdmin } from "@/Hooks/useIsAdmin";
const TagsPage = () => {
const { t } = useTranslation();
const isAdmin = useIsAdmin();
const [selectedTag, setSelectedTag] = useState<Tag | null>(null);
const isDialogOpen = Boolean(selectedTag);
const {
data: tags,
isLoading,
isValidating,
error,
refetch,
} = useGet<Tag[]>("/tags/team", {}, { keepPreviousData: true });
const { deleteFn, loading: isDeleting } = useDelete();
const handleConfirm = async () => {
if (!selectedTag) return;
await deleteFn(`/tags/${selectedTag.id}`);
setSelectedTag(null);
refetch();
};
const handleCancel = () => {
setSelectedTag(null);
};
return (
<BasePageWithStates
headerKey="tags"
page={t("pages.tags.fallback.title")}
description={t("pages.tags.fallback.description")}
loading={isLoading || isValidating}
error={!!error}
totalCount={tags?.length ?? 0}
actionButtonText={t("pages.tags.fallback.actionButton")}
actionLink="/tags/create"
>
<HeaderCreate
path="/tags/create"
isLoading={isLoading || isValidating}
isAdmin={isAdmin}
/>
<TagsTable
tags={tags ?? []}
setSelectedTag={setSelectedTag}
/>
<Dialog
open={isDialogOpen}
title={t("common.dialogs.delete.title")}
content={t("common.dialogs.delete.description")}
onConfirm={handleConfirm}
onCancel={handleCancel}
loading={isDeleting}
/>
</BasePageWithStates>
);
};
export default TagsPage;