diff --git a/apps/pyconkr-admin/src/components/elements/admin_list_filter.tsx b/apps/pyconkr-admin/src/components/elements/admin_list_filter.tsx new file mode 100644 index 0000000..e38a55e --- /dev/null +++ b/apps/pyconkr-admin/src/components/elements/admin_list_filter.tsx @@ -0,0 +1,218 @@ +import { Add, Clear, FilterList, RestartAlt } from "@mui/icons-material"; +import { Box, Button, Chip, FormControl, IconButton, InputLabel, MenuItem, Select, Stack, TextField } from "@mui/material"; +import * as React from "react"; + +import BackendAdminAPISchemas from "../../../../../packages/common/src/schemas/backendAdminAPI"; + +type OpenAPIParameterSchema = BackendAdminAPISchemas.OpenAPIParameterSchema; +type ChoicesResponse = BackendAdminAPISchemas.ChoicesResponse; + +type AdminListFilterProps = { + parameters: OpenAPIParameterSchema[]; + values: Record; + choices?: ChoicesResponse; + onApply: (values: Record) => void; +}; + +export const AdminListFilter: React.FC = ({ parameters, values, choices, onApply }) => { + const [localValues, setLocalValues] = React.useState>(values); + + React.useEffect(() => { + setLocalValues(values); + }, [values]); + + const handleChange = (name: string, value: string) => { + setLocalValues((prev) => ({ ...prev, [name]: value })); + }; + + const handleApply = () => { + const cleaned = Object.fromEntries(Object.entries(localValues).filter(([, v]) => v !== "")); + onApply(cleaned); + }; + + const handleClear = () => { + setLocalValues({}); + onApply({}); + }; + + if (parameters.length === 0) return null; + + return ( + + + + + 필터 + + + {parameters.map((param) => ( + + ))} + + + + + + + + ); +}; + +type ChoiceItem = { const: string | null; title: string }; + +type FilterFieldProps = { + param: OpenAPIParameterSchema; + value: string; + choices?: ChoiceItem[]; + onChange: (name: string, value: string) => void; +}; + +const FilterField: React.FC = ({ param, value, choices, onChange }) => { + const { name, schema, description } = param; + + if (schema?.type === "array") return ; + if (schema?.enum) return ; + + if (choices && choices.length > 0) { + return ( + + {name} + + + ); + } + + const inputType = schema?.type === "integer" || schema?.type === "number" ? "number" : "text"; + const helperText = schema?.format === "uuid" ? "UUID" : description || undefined; + + return ( + onChange(name, e.target.value)} + size="small" + type={inputType} + helperText={helperText} + sx={{ minWidth: 200 }} + /> + ); +}; + +type EnumFilterFieldProps = { + name: string; + options: string[]; + value: string; + onChange: (name: string, value: string) => void; +}; + +const EnumFilterField: React.FC = ({ name, options, value, onChange }) => { + const selectedValues = value ? value.split(",") : []; + + const handleChange = (newValues: string | string[]) => { + const arr = typeof newValues === "string" ? newValues.split(",") : newValues; + onChange(name, arr.filter((v) => v !== "").join(",")); + }; + + return ( + + {name} + + + ); +}; + +type ArrayFilterFieldProps = { + name: string; + items?: { type?: string; enum?: string[] }; + value: string; + onChange: (name: string, value: string) => void; +}; + +const ArrayFilterField: React.FC = ({ name, items, value, onChange }) => { + const values = value ? value.split(",") : []; + + const updateValues = (newValues: string[]) => onChange(name, newValues.filter((v) => v !== "").join(",")); + const handleAdd = () => updateValues([...values, ""]); + const handleRemove = (index: number) => updateValues(values.filter((_, i) => i !== index)); + + const handleItemChange = (index: number, newValue: string) => { + const newValues = [...values]; + newValues[index] = newValue; + updateValues(newValues); + }; + + const inputType = items?.type === "integer" || items?.type === "number" ? "number" : "text"; + + return ( + + + + {name} + + + + + {values.map((v, index) => ( + + {items?.enum ? ( + + + + ) : ( + handleItemChange(index, e.target.value)} size="small" type={inputType} sx={{ minWidth: 150 }} /> + )} + handleRemove(index)}> + + + + ))} + + + ); +}; diff --git a/apps/pyconkr-admin/src/components/layouts/admin_editor.tsx b/apps/pyconkr-admin/src/components/layouts/admin_editor.tsx index bd9e981..9af4007 100644 --- a/apps/pyconkr-admin/src/components/layouts/admin_editor.tsx +++ b/apps/pyconkr-admin/src/components/layouts/admin_editor.tsx @@ -277,6 +277,21 @@ const InnerAdminEditor: React.FC = Err }); const backendAdminClient = Common.Hooks.BackendAdminAPI.useBackendAdminClient(); const { data: schemaInfo } = Common.Hooks.BackendAdminAPI.useSchemaQuery(backendAdminClient, app, resource); + const { data: choicesData } = Common.Hooks.BackendAdminAPI.useChoicesQuery(backendAdminClient, app, resource); + + // Merge choices into schema for FK/M2M fields + React.useMemo(() => { + if (!choicesData || !schemaInfo.schema.properties) return; + for (const [fieldName, items] of Object.entries(choicesData)) { + const prop = (schemaInfo.schema.properties as Record)[fieldName]; + if (!prop) continue; + if (prop.type === "array" && prop.items) { + (prop.items as RJSFSchema).oneOf = items; + } else { + prop.oneOf = items; + } + } + }, [choicesData, schemaInfo.schema]); const setTab = (_: React.SyntheticEvent, tab: number) => setEditorState((ps) => ({ ...ps, tab })); const setFormData = (formData?: Record) => setEditorState((ps) => ({ ...ps, formData })); diff --git a/apps/pyconkr-admin/src/components/layouts/admin_list.tsx b/apps/pyconkr-admin/src/components/layouts/admin_list.tsx index 64ba59a..edd9930 100644 --- a/apps/pyconkr-admin/src/components/layouts/admin_list.tsx +++ b/apps/pyconkr-admin/src/components/layouts/admin_list.tsx @@ -3,8 +3,9 @@ import { Add } from "@mui/icons-material"; import { Box, Button, CircularProgress, Stack, Table, TableBody, TableCell, TableHead, TableRow, Typography } from "@mui/material"; import { ErrorBoundary, Suspense } from "@suspensive/react"; import * as React from "react"; -import { Link, useNavigate } from "react-router-dom"; +import { Link, useNavigate, useSearchParams } from "react-router-dom"; +import { AdminListFilter } from "../elements/admin_list_filter"; import { BackendAdminSignInGuard } from "../elements/admin_signin_guard"; type AdminListProps = { @@ -26,8 +27,21 @@ const InnerAdminList: React.FC = ErrorBoundary.with( { fallback: Common.Components.ErrorFallback }, Suspense.with({ fallback: }, ({ app, resource, hideCreatedAt, hideUpdatedAt, hideCreateNew }) => { const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); const backendAdminClient = Common.Hooks.BackendAdminAPI.useBackendAdminClient(); - const listQuery = Common.Hooks.BackendAdminAPI.useListQuery(backendAdminClient, app, resource); + + const filterParams: Record = Object.fromEntries(searchParams.entries()); + const listQuery = Common.Hooks.BackendAdminAPI.useListQuery(backendAdminClient, app, resource, filterParams); + + const openApiSchemaQuery = Common.Hooks.BackendAdminAPI.useOpenApiSchemaQuery(backendAdminClient); + const queryParameters = React.useMemo( + () => Common.Utils.extractQueryParameters(openApiSchemaQuery.data, app, resource), + [openApiSchemaQuery.data, app, resource] + ); + + const choicesQuery = Common.Hooks.BackendAdminAPI.useChoicesQuery(backendAdminClient, app, resource); + + const handleFilterApply = (newParams: Record) => setSearchParams(newParams, { replace: true }); return ( @@ -35,6 +49,7 @@ const InnerAdminList: React.FC = ErrorBoundary.with( {app.toUpperCase()} > {resource.toUpperCase()} > 목록
+ {!hideCreateNew && (