Updating foreign keys
This commit is contained in:
@@ -1,15 +1,23 @@
|
|||||||
import {FormContextType, RJSFSchema, WidgetProps} from '@rjsf/utils';
|
import { FormContextType, RJSFSchema, UiSchema, WidgetProps } from '@rjsf/utils';
|
||||||
import { Autocomplete, CircularProgress, TextField } from "@mui/material";
|
import { Autocomplete, Button, CircularProgress, Container, Grid2, InputAdornment, Modal, TextField, Box } from "@mui/material";
|
||||||
|
import ClearIcon from '@mui/icons-material/Clear';
|
||||||
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
|
import NoteAddIcon from '@mui/icons-material/NoteAdd';
|
||||||
import React, { useState, useEffect, useContext } from "react";
|
import React, { useState, useEffect, useContext } from "react";
|
||||||
import { useList, useOne } from "@refinedev/core";
|
import { useList, useOne } from "@refinedev/core";
|
||||||
import { ResourceContext } from "../../contexts/ResourceContext";
|
import { ResourceContext } from "../../contexts/ResourceContext";
|
||||||
|
import { CrudForm } from "../crud-form";
|
||||||
|
|
||||||
|
type ForeignKeyReference = {
|
||||||
|
resource: string,
|
||||||
|
label: string,
|
||||||
|
displayedFields: [string],
|
||||||
|
schema: string
|
||||||
|
}
|
||||||
|
|
||||||
type ForeignKeySchema = RJSFSchema & {
|
type ForeignKeySchema = RJSFSchema & {
|
||||||
foreignKey?: {
|
foreignKey?: {
|
||||||
reference: {
|
reference: ForeignKeyReference
|
||||||
resource: string,
|
|
||||||
label: string
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,26 +27,105 @@ export default function ForeignKeyWidget<T = any, S extends ForeignKeySchema = F
|
|||||||
if (props.schema.foreignKey === undefined) {
|
if (props.schema.foreignKey === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { onChange, value: originalValue } = props;
|
|
||||||
const [initialState, setInitialState] = useState(true);
|
|
||||||
if (originalValue != null && initialState) {
|
|
||||||
return <InitialValue {...props} onClear={() => {setInitialState(false); onChange(null)}}/>
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
const { value: originalValue, onChange } = props;
|
||||||
<RealAutocomplete {...props}/>
|
const [currentValue, setCurrentValue] = useState(originalValue)
|
||||||
)
|
|
||||||
|
if (currentValue) {
|
||||||
|
return <ChosenValue {...props} onClear={() => {setCurrentValue(null); onChange(null)}}/>
|
||||||
|
}
|
||||||
|
return <RealAutocomplete {...props} onChange={(value) => {setCurrentValue(value); onChange(value)}}/>
|
||||||
};
|
};
|
||||||
|
|
||||||
const InitialValue = <T = any, S extends ForeignKeySchema = ForeignKeySchema, F extends FormContextType = any>(
|
const RealAutocomplete = <T = any, S extends ForeignKeySchema = ForeignKeySchema, F extends FormContextType = any>(
|
||||||
|
props: WidgetProps<T, S, F>
|
||||||
|
) => {
|
||||||
|
if (props.schema.foreignKey === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { onChange, label } = props
|
||||||
|
|
||||||
|
const [openFormModal, setOpenFormModal] = useState(false);
|
||||||
|
const [searchString, setSearchString] = useState<string>("");
|
||||||
|
const [debouncedInputValue, setDebouncedInputValue] = useState<string>();
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = setTimeout(() => setDebouncedInputValue(searchString), 300); // Adjust debounce delay as needed
|
||||||
|
return () => clearTimeout(handler);
|
||||||
|
}, [searchString]);
|
||||||
|
|
||||||
|
const { resource, schema, label: labelField = "label" } = props.schema.foreignKey.reference
|
||||||
|
const { basePath } = useContext(ResourceContext)
|
||||||
|
const { data, isLoading } = useList({
|
||||||
|
resource: `${basePath}/${resource}`,
|
||||||
|
pagination: { current: 1, pageSize: 10, mode: "server" },
|
||||||
|
filters: [{ field: "label", operator: "contains", value: debouncedInputValue }],
|
||||||
|
sorters: [{ field: "label", order: "asc" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Autocomplete
|
||||||
|
onChange={(event, value) => {
|
||||||
|
onChange(value ? value.id : null);
|
||||||
|
return true;
|
||||||
|
}}
|
||||||
|
onInputChange={(event, value) => {
|
||||||
|
setSearchString(value)
|
||||||
|
}}
|
||||||
|
options={data ? data.data : []}
|
||||||
|
getOptionLabel={(option) => option ? option[labelField] : ""}
|
||||||
|
loading={isLoading}
|
||||||
|
forcePopupIcon={false}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
label={ label }
|
||||||
|
variant="outlined"
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
...params.InputProps,
|
||||||
|
endAdornment: (
|
||||||
|
<Button variant="outlined" onClick={() => setOpenFormModal(true)} color="success" >
|
||||||
|
<NoteAddIcon />
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Modal
|
||||||
|
open={openFormModal}
|
||||||
|
onClose={() => setOpenFormModal(false)}
|
||||||
|
aria-labelledby="modal-modal-title"
|
||||||
|
aria-describedby="modal-modal-description"
|
||||||
|
>
|
||||||
|
<FormContainer
|
||||||
|
schemaName={schema}
|
||||||
|
resourceBasePath={basePath}
|
||||||
|
resource={resource}
|
||||||
|
uiSchema={{}}
|
||||||
|
onSuccess={(data: any) => {
|
||||||
|
setOpenFormModal(false)
|
||||||
|
onChange(data.data.id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChosenValue = <T = any, S extends ForeignKeySchema = ForeignKeySchema, F extends FormContextType = any>(
|
||||||
props: WidgetProps<T, S, F> & { onClear: () => void }
|
props: WidgetProps<T, S, F> & { onClear: () => void }
|
||||||
) => {
|
) => {
|
||||||
const { onClear, value } = props;
|
const { onClear, value } = props;
|
||||||
|
|
||||||
|
const [openFormModal, setOpenFormModal] = React.useState(false);
|
||||||
|
|
||||||
if (props.schema.foreignKey === undefined) {
|
if (props.schema.foreignKey === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { resource, label: labelField = "label" } = props.schema.foreignKey.reference
|
const { resource, schema, label: labelField = "label", displayedFields } = props.schema.foreignKey.reference
|
||||||
const { basePath } = useContext(ResourceContext)
|
const { basePath } = useContext(ResourceContext)
|
||||||
|
|
||||||
const { data, isLoading } = useOne({
|
const { data, isLoading } = useOne({
|
||||||
@@ -51,60 +138,97 @@ const InitialValue = <T = any, S extends ForeignKeySchema = ForeignKeySchema, F
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Autocomplete
|
<>
|
||||||
value={data.data}
|
<TextField label={ props.label } variant="outlined" disabled={true} value={data.data[labelField]}
|
||||||
onChange={() => onClear()}
|
slotProps={{
|
||||||
onInputChange={() => onClear()}
|
input: {
|
||||||
options={[data.data]}
|
endAdornment: (
|
||||||
getOptionLabel={(option) => option ? option[labelField] : ""}
|
<InputAdornment position="end">
|
||||||
loading={isLoading}
|
<Button variant="outlined" onClick={() => setOpenFormModal(true)} color="primary" >
|
||||||
renderInput={(params) => (
|
<EditIcon />
|
||||||
<TextField {...params} label={ props.label } variant="outlined" />
|
</Button>
|
||||||
)}
|
<Button variant="outlined" onClick={onClear} color="error" >
|
||||||
/>
|
<ClearIcon />
|
||||||
|
</Button>
|
||||||
|
</InputAdornment>),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{ displayedFields && <Preview id={value} basePath={basePath} resource={resource} displayedFields={displayedFields}/>}
|
||||||
|
<Modal
|
||||||
|
open={openFormModal}
|
||||||
|
onClose={() => setOpenFormModal(false)}
|
||||||
|
aria-labelledby="modal-modal-title"
|
||||||
|
aria-describedby="modal-modal-description"
|
||||||
|
>
|
||||||
|
<FormContainer
|
||||||
|
schemaName={schema}
|
||||||
|
resourceBasePath={basePath}
|
||||||
|
resource={resource}
|
||||||
|
uiSchema={{}}
|
||||||
|
id={value}
|
||||||
|
onSuccess={() => setOpenFormModal(false)}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const RealAutocomplete = <T = any, S extends ForeignKeySchema = ForeignKeySchema, F extends FormContextType = any>(
|
const modalStyle = {
|
||||||
props: WidgetProps<T, S, F>
|
position: 'absolute',
|
||||||
) => {
|
top: '50%',
|
||||||
if (props.schema.foreignKey === undefined) {
|
left: '50%',
|
||||||
return;
|
transform: 'translate(-50%, -50%)',
|
||||||
}
|
width: 400,
|
||||||
const { onChange } = props
|
bgcolor: 'background.paper',
|
||||||
|
border: '2px solid #000',
|
||||||
|
boxShadow: 24,
|
||||||
|
pt: 2,
|
||||||
|
px: 4,
|
||||||
|
pb: 3,
|
||||||
|
};
|
||||||
|
|
||||||
const [searchString, setSearchString] = useState<string>("");
|
type FormContainerProps = {
|
||||||
const [debouncedInputValue, setDebouncedInputValue] = useState<string>();
|
schemaName: string,
|
||||||
useEffect(() => {
|
resourceBasePath: string,
|
||||||
const handler = setTimeout(() => setDebouncedInputValue(searchString), 300); // Adjust debounce delay as needed
|
resource: string,
|
||||||
return () => clearTimeout(handler);
|
uiSchema?: UiSchema,
|
||||||
}, [searchString]);
|
id?: string,
|
||||||
|
onSuccess: (data: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
const { resource, label: labelField = "label" } = props.schema.foreignKey.reference
|
const FormContainer = (props: FormContainerProps) => {
|
||||||
const { basePath } = useContext(ResourceContext)
|
const { schemaName, resourceBasePath, resource, uiSchema = {}, id = undefined, onSuccess } = props;
|
||||||
const { data, isLoading } = useList({
|
|
||||||
resource: `${basePath}/${resource}`,
|
|
||||||
pagination: { current: 1, pageSize: 10, mode: "server" },
|
|
||||||
filters: [{ field: "label", operator: "contains", value: debouncedInputValue }],
|
|
||||||
sorters: [{ field: "label", order: "asc" }],
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Autocomplete
|
<Box sx={{ ...modalStyle, width: 800 }}>
|
||||||
onChange={(event, value) => {
|
<CrudForm schemaName={schemaName} resourceBasePath={resourceBasePath} resource={resource} uiSchema={uiSchema} id={id} onSuccess={onSuccess} />
|
||||||
onChange(value ? value.id : null);
|
</Box>
|
||||||
return true;
|
)
|
||||||
}}
|
}
|
||||||
onInputChange={(event, newInputValue) => {
|
|
||||||
setSearchString(newInputValue)
|
const Preview = (props: {id: string, resource: string, basePath: string, displayedFields: [string]}) => {
|
||||||
console.log(newInputValue)
|
const { basePath, resource, id, displayedFields } = props
|
||||||
}}
|
|
||||||
options={data ? data.data : []}
|
const { data, isLoading } = useOne({
|
||||||
getOptionLabel={(option) => option ? option[labelField] : ""}
|
resource: `${basePath}/${resource}`,
|
||||||
loading={isLoading}
|
id
|
||||||
renderInput={(params) => (
|
});
|
||||||
<TextField {...params} label={ props.label } variant="outlined" />
|
|
||||||
)}
|
if (isLoading || data === undefined) {
|
||||||
/>
|
return <CircularProgress />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid2 container spacing={2}>
|
||||||
|
{displayedFields.map((field: string) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Grid2 size={2}><Container>{field}</Container></Grid2>
|
||||||
|
<Grid2 size={9}><Container dangerouslySetInnerHTML={{ __html: data.data[field] }} ></Container></Grid2>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Grid2>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user