feat: attributes page

This commit is contained in:
2025-11-02 16:07:49 +04:00
parent 8020561da6
commit 03be3903cb
16 changed files with 348 additions and 48 deletions

View File

@ -0,0 +1,37 @@
"use client";
import { FC } from "react";
import { Group, TextInput } from "@mantine/core";
import { useAttributesContext } from "@/app/attributes/contexts/AttributesContext";
import InlineButton from "@/components/ui/InlineButton/InlineButton";
import useIsMobile from "@/hooks/utils/useIsMobile";
const AttributesHeader: FC = () => {
const {
attributesActions: { onCreate },
search,
setSearch,
} = useAttributesContext();
const isMobile = useIsMobile();
return (
<Group
wrap={"nowrap"}
mt={isMobile ? "xs" : ""}
mx={isMobile ? "xs" : ""}>
<InlineButton
onClick={onCreate}
w={isMobile ? "100%" : "auto"}>
Создать атрибут
</InlineButton>
<TextInput
value={search}
onChange={e => setSearch(e.currentTarget.value)}
w={isMobile ? "100%" : "auto"}
placeholder={"Поиск..."}
/>
</Group>
);
};
export default AttributesHeader;

View File

@ -0,0 +1,40 @@
"use client";
import { FC } from "react";
import { IconMoodSad } from "@tabler/icons-react";
import { Group, Text } from "@mantine/core";
import { useAttributesContext } from "@/app/attributes/contexts/AttributesContext";
import useAttributesTableColumns from "@/app/attributes/hooks/useAttributesTableColumns";
import BaseTable from "@/components/ui/BaseTable/BaseTable";
import useIsMobile from "@/hooks/utils/useIsMobile";
const AttributesTable: FC = () => {
const isMobile = useIsMobile();
const { attributes } = useAttributesContext();
const columns = useAttributesTableColumns();
return (
<BaseTable
withTableBorder
columns={columns}
records={attributes}
verticalSpacing={"md"}
emptyState={
<Group mt={attributes.length === 0 ? "xl" : 0}>
<Text>Нет атрибутов</Text>
<IconMoodSad />
</Group>
}
groups={undefined}
styles={{
table: {
width: "100%",
},
header: { zIndex: 1 },
}}
mx={isMobile ? "xs" : 0}
/>
);
};
export default AttributesTable;

View File

@ -0,0 +1,26 @@
"use client";
import AttributesHeader from "@/app/attributes/components/AttributesHeader";
import AttributesTable from "@/app/attributes/components/AttributesTable";
import PageBlock from "@/components/layout/PageBlock/PageBlock";
const PageBody = () => (
<PageBlock
style={{ flex: 1, minHeight: 0 }}
fullScreenMobile>
<div
style={{
height: "100%",
display: "flex",
flexDirection: "column",
gap: "var(--mantine-spacing-md)",
}}>
<AttributesHeader />
<div style={{ flex: 1, overflow: "auto" }}>
<AttributesTable />
</div>
</div>
</PageBlock>
);
export default PageBody;

View File

@ -0,0 +1,40 @@
"use client";
import { Dispatch, SetStateAction } from "react";
import useFilteredAttributes from "@/app/attributes/hooks/useFilteredAttributes";
import useAttributesActions, {
AttributesActions,
} from "@/app/module-editor/[moduleId]/hooks/useAttributesActions";
import useAttributesList from "@/app/module-editor/[moduleId]/hooks/useAttributesList";
import { AttributeSchema } from "@/lib/client";
import makeContext from "@/lib/contextFactory/contextFactory";
type AttributesContextState = {
attributes: AttributeSchema[];
refetchAttributes: () => void;
attributesActions: AttributesActions;
search: string;
setSearch: Dispatch<SetStateAction<string>>;
};
const useAttributesContextState = (): AttributesContextState => {
const { attributes, refetch } = useAttributesList();
const attributesActions = useAttributesActions({
refetchAttributes: refetch,
});
const { search, setSearch, filteredAttributes } = useFilteredAttributes({
attributes,
});
return {
attributes: filteredAttributes,
refetchAttributes: refetch,
attributesActions,
search,
setSearch,
};
};
export const [AttributesContextProvider, useAttributesContext] =
makeContext<AttributesContextState>(useAttributesContextState, "Attribute");

View File

@ -0,0 +1,72 @@
"use client";
import { useMemo } from "react";
import { IconCheck, IconX } from "@tabler/icons-react";
import { DataTableColumn } from "mantine-datatable";
import { Box, Center } from "@mantine/core";
import AttributeTableActions from "@/app/module-editor/[moduleId]/components/shared/AttributeTableActions/AttributeTableActions";
import AttributeDefaultValue from "@/components/ui/AttributeDefaultValue/AttributeDefaultValue";
import useIsMobile from "@/hooks/utils/useIsMobile";
import { AttributeSchema } from "@/lib/client";
import { useAttributesContext } from "../contexts/AttributesContext";
const useAttributesTableColumns = () => {
const isMobile = useIsMobile();
const { attributesActions } = useAttributesContext();
const renderCheck = (value: boolean) => (value ? <IconCheck /> : <IconX />);
return useMemo(
() =>
[
{
title: "Название атрибута",
accessor: "label",
},
{
title: "Тип",
accessor: "type.name",
render: attr =>
attr.type.type === "select"
? `Выбор "${attr.label}"`
: attr.type.name,
},
{
title: "Значение по умолчанию",
accessor: "defaultValue",
render: attr => <AttributeDefaultValue attribute={attr} />,
},
{
title: isMobile
? "Синх. в группе"
: "Синхронизировано в группе",
accessor: "isApplicableToGroup",
render: attr => renderCheck(attr.isApplicableToGroup),
},
{
title: "Может быть пустым",
accessor: "isNullable",
render: attr => renderCheck(attr.isNullable),
},
{
title: "Описаниие",
accessor: "description",
render: attr => <Box>{attr.description}</Box>,
},
{
accessor: "actions",
title: <Center>Действия</Center>,
width: "0%",
render: attribute => (
<AttributeTableActions
attribute={attribute}
onUpdate={attributesActions.onUpdate}
onDelete={attributesActions.onDelete}
/>
),
},
] as DataTableColumn<AttributeSchema>[],
[isMobile]
);
};
export default useAttributesTableColumns;

View File

@ -0,0 +1,29 @@
import { useMemo, useState } from "react";
import { AttributeSchema } from "@/lib/client";
type Props = {
attributes: AttributeSchema[];
};
const useFilteredAttributes = ({ attributes }: Props) => {
const [search, setSearch] = useState<string>("");
const filteredAttributes = useMemo(
() =>
attributes.filter(
attr =>
attr.type.name.includes(search) ||
attr.label.includes(search) ||
attr.description.includes(search)
),
[attributes, search]
);
return {
search,
setSearch,
filteredAttributes,
};
};
export default useFilteredAttributes;

View File

@ -0,0 +1,22 @@
import { Suspense } from "react";
import { Center, Loader } from "@mantine/core";
import PageBody from "@/app/attributes/components/PageBody";
import { AttributesContextProvider } from "@/app/attributes/contexts/AttributesContext";
import PageContainer from "@/components/layout/PageContainer/PageContainer";
export default async function AttributesPage() {
return (
<Suspense
fallback={
<Center h="50vh">
<Loader size="lg" />
</Center>
}>
<PageContainer>
<AttributesContextProvider>
<PageBody />
</AttributesContextProvider>
</PageContainer>
</Suspense>
);
}