import { RowWithDetails, RowWithValues } from "~/utils/db/entities/rows.db.server"; import { EntityWithDetails, PropertyWithDetails } from "~/utils/db/entities/entities.db.server"; import { Dispatch, forwardRef, Fragment, ReactNode, Ref, SetStateAction, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react"; import clsx from "clsx"; import { updateItemByIdx } from "~/utils/shared/ObjectUtils"; import { useNavigation, useParams, useSearchParams, useSubmit } from "react-router"; import { RowValueDto } from "~/application/dtos/entities/RowValueDto"; import FormGroup, { RefFormGroup } from "~/components/ui/forms/FormGroup"; import InputGroup from "~/components/ui/forms/InputGroup"; import RowHelper from "~/utils/helpers/RowHelper"; import RowValueInput, { RefRowValueInput } from "./RowValueInput"; import PropertyAttributeHelper from "~/utils/helpers/PropertyAttributeHelper"; import { PropertyAttributeName } from "~/application/enums/entities/PropertyAttributeName"; import { useTranslation } from "react-i18next"; import { EntityRelationshipWithDetails } from "~/utils/db/entities/entityRelationships.db.server"; import RowListFetcher from "../../../modules/rows/fetchers/RowListFetcher"; import SlideOverWideEmpty from "~/components/ui/slideOvers/SlideOverWideEmpty"; import { EntitiesApi } from "~/utils/api/.server/EntitiesApi"; import EntityHelper from "~/utils/helpers/EntityHelper"; import { RowsApi } from "~/utils/api/.server/RowsApi"; import { RowValueMultipleDto } from "~/application/dtos/entities/RowValueMultipleDto"; import RelationshipHelper from "~/utils/helpers/RelationshipHelper"; import RowsList from "./RowsList"; import { EntityViewWithDetails } from "~/utils/db/entities/entityViews.db.server"; import { RowDisplayDefaultProperty } from "~/utils/helpers/PropertyHelper"; import { PropertyType } from "~/application/enums/entities/PropertyType"; import RowUrlHelper from "~/utils/helpers/RowUrlHelper"; import { translateEntityTitle } from "~/utils/i18n/translateLabel"; export interface RefRowForm { save: () => void; } interface Props { entity: EntityWithDetails; allEntities: EntityWithDetails[]; routes?: EntitiesApi.Routes; item?: RowWithDetails | null; editing?: boolean; adding?: boolean; onSubmit?: (formData: FormData) => void; canUpdate?: boolean; canDelete?: boolean; canSubmit?: boolean; children?: ReactNode; parentEntity?: EntityWithDetails; onCreatedRedirect?: string; onDelete?: () => void; relationshipRows?: RowsApi.GetRelationshipRowsData; hiddenProperties?: string[]; hiddenFields?: { [key: string]: string | null | undefined; }; state?: { loading?: boolean; submitting?: boolean }; createdRow?: RowWithDetails; onCancel?: () => void; onChange?: (values: RowValueDto[]) => void; customSearchParams?: URLSearchParams; template?: { title: string; config: string } | null; } const RowForm = ( { entity, routes, item, editing = false, adding, onSubmit, canUpdate, canDelete, canSubmit = true, children, parentEntity, onCreatedRedirect, allEntities, onDelete, relationshipRows, hiddenProperties, hiddenFields, state, createdRow, onCancel, onChange, customSearchParams, template, }: Props, ref: Ref ) => { const { t, i18n } = useTranslation(); const submit = useSubmit(); const navigation = useNavigation(); const params = useParams(); // const actionData = useActionData<{ newRow?: RowWithDetails }>(); const formGroup = useRef(null); const [searchParams] = useSearchParams(); const [searchingRelationshipRows, setSearchingRelationshipRows] = useState(); const [selectedRelatedEntity, setSelectedRelatedEntity] = useState<{ entity: { slug: string; onEdit: string | null }; view: EntityViewWithDetails | null; multiple: boolean; }>(); const [relatedRows, setRelatedRows] = useState<{ relationship: EntityRelationshipWithDetails; rows: RowWithValues[] }[]>([]); const [headers, setHeaders] = useState([]); const [childrenEntities, setChildrenEntities] = useState<{ visible: EntityRelationshipWithDetails[]; hidden: EntityRelationshipWithDetails[] }>({ visible: [], hidden: [], }); const [parentEntities, setParentEntities] = useState<{ visible: EntityRelationshipWithDetails[]; hidden: EntityRelationshipWithDetails[] }>({ visible: [], hidden: [], }); useEffect(() => { loadInitialFields(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [params.entity, params.group, customSearchParams, template]); useEffect(() => { if (onChange) { onChange(headers); } }, [headers, onChange]); // useEffect(() => { // if (headers.length > 0) { // rowValueInput.current?.focus(); // } // }, [headers]) // useEffect(() => { // if (actionData?.newRow && onCreated) { // onCreated(actionData.newRow); // } // // eslint-disable-next-line react-hooks/exhaustive-deps // }, [actionData]); useEffect(() => { if (searchingRelationshipRows?.parentId === entity.id) { setSelectedRelatedEntity({ entity: searchingRelationshipRows.child, view: searchingRelationshipRows.childEntityView, multiple: true, }); } else if (searchingRelationshipRows?.childId === entity.id) { setSelectedRelatedEntity({ entity: searchingRelationshipRows.parent, view: searchingRelationshipRows.parentEntityView, multiple: false, }); } }, [entity.id, searchingRelationshipRows]); useImperativeHandle(ref, () => ({ save, })); function save() { formGroup.current?.submitForm(); } function loadInitialFields() { const initial: RowValueDto[] = []; if (template && !customSearchParams) { const config = JSON.parse(template.config); const defaultValues: { [key: string]: any } = {}; Object.keys(config).forEach((key) => { defaultValues[key] = config[key]; }); customSearchParams = new URLSearchParams(defaultValues); } entity.properties ?.filter((f) => isPropertyVisible(f)) .forEach((property) => { const existing = item?.values?.find((f) => f?.propertyId === property.id); let urlSearchParams = customSearchParams ?? searchParams; const defaultValueString = RowUrlHelper.getString({ urlSearchParams, property }) ?? PropertyAttributeHelper.getPropertyAttributeValue_String(property, PropertyAttributeName.DefaultValue); const defaultValueNumber = RowUrlHelper.getNumber({ urlSearchParams, property }) ?? PropertyAttributeHelper.getPropertyAttributeValue_Number(property, PropertyAttributeName.DefaultValue); const defaultValueBoolean = RowUrlHelper.getBoolean({ urlSearchParams, property }) ?? PropertyAttributeHelper.getPropertyAttributeValue_Boolean(property, PropertyAttributeName.DefaultValue); let defaultDate = RowUrlHelper.getDate({ urlSearchParams, property }) ?? undefined; const defaultRange = RowUrlHelper.getRange({ urlSearchParams, property }) ?? undefined; const defaultMultiple = RowUrlHelper.getMultiple({ urlSearchParams, property }) ?? undefined; initial.push({ propertyId: property.id, property: property, textValue: existing?.textValue ?? defaultValueString ?? undefined, numberValue: existing?.numberValue ? Number(existing?.numberValue) : defaultValueNumber, dateValue: existing?.dateValue ?? defaultDate, booleanValue: existing ? Boolean(existing?.booleanValue) : defaultValueBoolean, selectedOption: existing?.textValue ?? defaultValueString ?? undefined, media: existing?.media ?? [], multiple: existing?.multiple.sort((a: RowValueMultipleDto, b: RowValueMultipleDto) => a.order - b.order) ?? defaultMultiple ?? [], range: existing?.range ?? defaultRange, }); }); const relatedRows: { relationship: EntityRelationshipWithDetails; rows: RowWithValues[] }[] = []; if (item) { entity.parentEntities.forEach((relationship) => { if (item.parentRows?.length > 0) { relatedRows.push({ relationship, rows: item.parentRows.filter((f) => f.relationshipId === relationship.id).map((i) => i.parent), }); } }); entity.childEntities.forEach((relationship) => { if (item.childRows?.length > 0) { relatedRows.push({ relationship, rows: item.childRows.filter((f) => f.relationshipId === relationship.id).map((i) => i.child), }); } }); } else { entity.parentEntities.forEach((relationship) => { const rowId = customSearchParams?.get(relationship.parent.name) ?? searchParams.get(relationship.parent.name); if (rowId) { const foundRow = relationshipRows ?.filter((f) => f.relationship.id === relationship.id) .map((m) => m.rows) .flat() .find((f) => f.id === rowId); relatedRows.push({ relationship, rows: foundRow ? [foundRow] : [], }); } }); } const allChildren = entity.childEntities.filter((f) => childEntityVisible(f) && allEntities.find((x) => x.id === f.childId)); setChildrenEntities(getVisibleRelatedEntities(allChildren, relatedRows)); const allParents = entity.parentEntities.filter((f) => f.parentId !== parentEntity?.id && allEntities.find((x) => x.id === f.parentId)); setParentEntities(getVisibleRelatedEntities(allParents, relatedRows)); setHeaders(initial); setRelatedRows(relatedRows); } function onFindEntityRows(relationship: EntityRelationshipWithDetails) { if (!routes) { return; } setSearchingRelationshipRows(relationship); } function addRelationshipRow(relationship: EntityRelationshipWithDetails, rows: RowWithDetails[]) { const newRelatedRows = [...relatedRows]; const existing = newRelatedRows.find((f) => f.relationship.id === relationship.id); if (existing) { if (relationship.parentId === entity.id) { const nonExistingRows = rows.filter((f) => !existing.rows.find((ff) => ff.id === f.id)); existing.rows = [...existing.rows, ...nonExistingRows]; } else { existing.rows = rows; } } else { newRelatedRows.push({ relationship, rows }); } setRelatedRows(newRelatedRows); } function setRelationshipRows(relationship: EntityRelationshipWithDetails, rows: RowWithDetails[]) { const newRelatedRows = [...relatedRows]; const existing = newRelatedRows.find((f) => f.relationship.id === relationship.id); if (existing) { existing.rows = rows; } else { newRelatedRows.push({ relationship, rows }); } setRelatedRows(newRelatedRows); } function onRemoveRelatedRow(relationship: EntityRelationshipWithDetails, row: RowWithValues) { const newRelatedRows = [...relatedRows]; const existing = newRelatedRows.find((f) => f.relationship.id === relationship.id); if (existing) { existing.rows = existing.rows.filter((f) => f.id !== row.id); } setRelatedRows(newRelatedRows); } function submitForm(formData: FormData) { if (onSubmit) { onSubmit(formData); } else { submit(formData, { method: "post", }); } } function isPropertyVisible(f: PropertyWithDetails) { if (f.isHidden || (!item && !f.showInCreate) || (!item && f.isReadOnly) || (item && editing && f.isReadOnly)) { return false; } else if (hiddenProperties?.includes(f.name)) { return false; } if (item && editing && !f.canUpdate) { return false; } return true; } function childEntityVisible(f: EntityRelationshipWithDetails) { if (f.readOnly) { if (!item) { return false; } if (item && editing) { return false; } } return true; } function canSubmitForm() { if (!canSubmit) { return false; } const required = headers.filter((f) => f.property.isRequired); let hasError = false; required.forEach((f) => { if (f.property.type === PropertyType.MEDIA) { if (f.media?.length === 0) { hasError = true; } } }); return !hasError; } function isAddingOrEditing() { if (adding) { return true; } if (editing && item && canUpdate && navigation.state === "idle") { return true; } return false; } function onSaveIfAllSet() { return; // if (item) { // return; // } // const missingValues = headers // .filter((f) => isPropertyVisible(f.property)) // .map((header) => { // if ([PropertyType.TEXT, PropertyType.SELECT].includes(header.property.type) && !header.textValue) { // return header; // } else if ([PropertyType.NUMBER].includes(header.property.type) && !header.numberValue) { // return header; // } else if ([PropertyType.DATE].includes(header.property.type) && !header.dateValue) { // return header; // } // // else if ([PropertyType.MEDIA].includes(header.property.type) && (!header.media || header.media.length === 0)) { // // return header; // // } // else if ([PropertyType.MULTI_SELECT].includes(header.property.type) && (!header.multiple || header.multiple.length === 0)) { // return header; // } // return null; // }); // const rowValues = missingValues.filter((f) => f !== null); // if (rowValues.length === 0) { // formGroup.current?.submitForm(); // } } return ( <> {hiddenFields && Object.keys(hiddenFields).map((f) => { return {hiddenFields[f] && }; })} {!onSubmit && ( <> {!item ? ( ) : ( )} )} {onCreatedRedirect && } { setParentEntities((prev) => { return { visible: [...prev.visible, rel], hidden: prev.hidden.filter((f) => f.id !== rel.id), }; }); }, }} onSaveIfAllSet={onSaveIfAllSet} />
{childrenEntities.visible.map((relationship) => (

{t(RelationshipHelper.getTitle({ fromEntityId: entity.id, relationship }))} {/* {relationship.required && *} */}

))} {isAddingOrEditing() && ( { setChildrenEntities((prev) => { return { visible: [...prev.visible, rel], hidden: prev.hidden.filter((f) => f.id !== rel.id), }; }); }} type="child" /> )}
{relatedRows.map(({ relationship, rows }) => ( {rows.map((row) => ( // <> // ))} ))}
{/* // setSearchingRelationshipRows(undefined)}> */} setSearchingRelationshipRows(undefined)} > {selectedRelatedEntity && searchingRelationshipRows && ( { addRelationshipRow(searchingRelationshipRows, rows); setSearchingRelationshipRows(undefined); }} multipleSelection={selectedRelatedEntity.multiple} allEntities={allEntities} /> )} {/* // */} ); }; function RelationshipSelector({ fromEntity, routes, type, relationship, relatedRows, onFindEntityRows, className, allEntities, onRemoveRelatedRow, readOnly, relationshipRows, addRelationshipRow, setRelationshipRows, }: { fromEntity: EntityWithDetails; type: "child" | "parent"; relationship: EntityRelationshipWithDetails; relatedRows: { relationship: EntityRelationshipWithDetails; rows: RowWithValues[] }[]; onFindEntityRows: (relationship: EntityRelationshipWithDetails) => void; className?: string; allEntities: EntityWithDetails[]; onRemoveRelatedRow: (relationship: EntityRelationshipWithDetails, row: RowWithValues) => void; readOnly: boolean; routes?: EntitiesApi.Routes; relationshipRows?: RowsApi.GetRelationshipRowsData; addRelationshipRow: (relationship: EntityRelationshipWithDetails, rows: RowWithDetails[]) => void; setRelationshipRows: (relationship: EntityRelationshipWithDetails, rows: RowWithDetails[]) => void; }) { const { t, i18n } = useTranslation(); const [entity] = useState( type === "parent" ? { entity: getChildEntity(relationship)!, view: relationship.parentEntityView, } : { entity: getParentEntity(relationship)!, view: relationship.childEntityView, } ); function getRows(relationship: EntityRelationshipWithDetails) { const existing = relatedRows.find((f) => f.relationship.id === relationship.id); return existing?.rows.sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) ?? []; } function getParentEntity(relationship: EntityRelationshipWithDetails) { return allEntities.find((f) => f.id === relationship.childId); } function getChildEntity(relationship: EntityRelationshipWithDetails) { return allEntities.find((f) => f.id === relationship.parentId); } return (
{/*
selectedRow: {getSelectedRow()}
options:{" "} {getOptions() .map((f) => f.value) .join(",")}
{relatedRows.filter((f) => f.relationship.id === relationship.id).length > 0 && relatedRows .filter((f) => f.relationship.id === relationship.id)[0] .rows.map((f) => f.id) .join(",")}
*/} {/* {RelationshipHelper.getInputType({ fromEntityId: fromEntity.id, relationship }) === "single-select" ? ( { const row = relationshipRows?.find((f) => f.relationship.id === relationship.id)?.rows.find((f) => f.id === value); if (row) { setRelationshipRows(relationship, [row]); } }} /> ) : RelationshipHelper.getInputType({ fromEntityId: fromEntity.id, relationship }) === "multi-select" ? ( <> */} {getRows(relationship).length === 0 ? ( ) : (
onRemoveRelatedRow(relationship, row)} ignoreColumns={!readOnly ? [RowDisplayDefaultProperty.FOLIO, "parent." + relationship.parent.name, "child." + relationship.child.name] : []} routes={routes} /> {/* {getRows(relationship).map((item) => (
{RowHelper.getTextDescription({ entity, item, t, defaultsToFolio: true })}
))} */}
)} {/* ) : null} */}
); } function RowGroups({ item, entity, rowValues, parentEntity, allEntities, relatedRows, editing, canUpdate, routes, relationshipRows, setHeaders, addRelationshipRow, setRelationshipRows, onFindEntityRows, onRemoveRelatedRow, isPropertyVisible, children, canSubmit, isAddingOrEditing, parentEntities, onSaveIfAllSet, }: { item?: RowWithDetails | null; entity: EntityWithDetails; rowValues: RowValueDto[]; parentEntity?: EntityWithDetails; allEntities: EntityWithDetails[]; relatedRows: { relationship: EntityRelationshipWithDetails; rows: RowWithValues[] }[]; editing?: boolean; canUpdate?: boolean; routes?: EntitiesApi.Routes; relationshipRows?: RowsApi.GetRelationshipRowsData; setHeaders: Dispatch>; addRelationshipRow: (relationship: EntityRelationshipWithDetails, rows: RowWithDetails[]) => void; setRelationshipRows: (relationship: EntityRelationshipWithDetails, rows: RowWithDetails[]) => void; onFindEntityRows: (relationship: EntityRelationshipWithDetails) => void; onRemoveRelatedRow: (relationship: EntityRelationshipWithDetails, row: RowWithValues) => void; isPropertyVisible: (property: PropertyWithDetails) => boolean; children?: ReactNode; canSubmit?: boolean; isAddingOrEditing: boolean; parentEntities: { visible: EntityRelationshipWithDetails[]; hidden: EntityRelationshipWithDetails[]; onAddParentEntity: (item: EntityRelationshipWithDetails) => void; }; onSaveIfAllSet: () => void; }) { const { t } = useTranslation(); const rowValueInput = useRef(null); const groups = useMemo(() => { const groups: { group?: string; headers: RowValueDto[] }[] = []; rowValues.forEach((header) => { const groupName = PropertyAttributeHelper.getPropertyAttributeValue_String(header.property, PropertyAttributeName.Group); let found = groups.find((f) => f.group === groupName); if (!found) { found = groups.find((f) => !f.group && !groupName); } if (found) { found.headers.push(header); } else { groups.push({ group: groupName, headers: [header], }); } }); if (groups.length === 0) { groups.push({ headers: rowValues }); } return groups; }, [rowValues]); function getPropertyColumnSpan(property: PropertyWithDetails) { const columns = PropertyAttributeHelper.getPropertyAttributeValue_Number(property, PropertyAttributeName.Columns); if (columns === undefined || isNaN(columns) || (columns < 1 && columns > 12)) { return "col-span-12"; } return `col-span-${columns}`; } function onChange(rowValue: RowValueDto) { setHeaders((prev) => { return prev.map((f) => { if (f.propertyId === rowValue.propertyId) { return rowValue; } return f; }); }); } return ( <> {groups.map(({ group, headers }, idx) => { return (
{headers.map((detailValue, idxDetailValue) => { return (
{ onChange({ ...detailValue, ...RowHelper.updateFieldValueTypeArray(detailValue, e), }); }} onChangeOption={(e) => { onChange({ ...detailValue, selectedOption: e, textValue: e, }); }} onChangeMedia={(media) => { onChange({ ...detailValue, media: media as any, }); if (media.filter((f) => f.type).length > 0) { onSaveIfAllSet(); } }} onChangeMultiple={(e) => { onChange({ ...detailValue, multiple: e as any[], }); }} onChangeRange={(e) => { onChange({ ...detailValue, range: e as any, }); }} readOnly={ (editing && !detailValue.property.canUpdate) || (item?.id !== undefined && (!editing || !canUpdate)) || detailValue.property?.isReadOnly } autoFocus={idx === 0 && idxDetailValue === 0 && canSubmit} />
); })} {/* Show parent entities in Default Properties Group */} {!group && ( <> {parentEntities.visible.map((relationship) => (
))} {isAddingOrEditing && ( )} )} {/* Show custom properties in Default Properties Group */} {!group && <>{children}}
); })} ); } function AddHiddenRelationshipEntities({ items, onClick, type, }: { items: EntityRelationshipWithDetails[]; onClick: (item: EntityRelationshipWithDetails) => void; type: "parent" | "child"; }) { const { t, i18n } = useTranslation(); return ( {items.length > 0 && (
{items.map((relationship) => ( ))}
)}
); } function getVisibleRelatedEntities( entityRelationships: EntityRelationshipWithDetails[], relatedRows: { relationship: EntityRelationshipWithDetails; rows: RowWithValues[] }[] ) { const visible = entityRelationships.filter((f) => !f.hiddenIfEmpty); const hidden: EntityRelationshipWithDetails[] = []; entityRelationships .filter((f) => f.hiddenIfEmpty) .forEach((relationship) => { const rows = relatedRows.filter((f) => f.relationship.id === relationship.id).flatMap((f) => f.rows); if (rows.length > 0) { visible.push(relationship); } else { hidden.push(relationship); } }); return { visible, hidden, }; } export default forwardRef(RowForm);