986 lines
36 KiB
TypeScript
986 lines
36 KiB
TypeScript
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<RefRowForm>
|
|
) => {
|
|
const { t, i18n } = useTranslation();
|
|
const submit = useSubmit();
|
|
const navigation = useNavigation();
|
|
const params = useParams();
|
|
// const actionData = useActionData<{ newRow?: RowWithDetails }>();
|
|
|
|
const formGroup = useRef<RefFormGroup>(null);
|
|
|
|
const [searchParams] = useSearchParams();
|
|
const [searchingRelationshipRows, setSearchingRelationshipRows] = useState<EntityRelationshipWithDetails>();
|
|
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<RowValueDto[]>([]);
|
|
|
|
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 (
|
|
<>
|
|
<FormGroup
|
|
ref={formGroup}
|
|
id={item?.id}
|
|
editing={editing}
|
|
canDelete={canDelete}
|
|
onSubmit={submitForm}
|
|
onDelete={onDelete}
|
|
canUpdate={canUpdate}
|
|
canSubmit={canSubmit}
|
|
onCancel={onCancel}
|
|
submitDisabled={!canSubmitForm()}
|
|
onCreatedRedirect={onCreatedRedirect}
|
|
deleteRedirect={EntityHelper.getRoutes({ routes, entity })?.list}
|
|
state={state}
|
|
message={
|
|
createdRow
|
|
? { success: t("shared.created") + ": " + RowHelper.getTextDescription({ entity, item: createdRow, t, defaultsToFolio: true }) }
|
|
: undefined
|
|
}
|
|
>
|
|
{hiddenFields &&
|
|
Object.keys(hiddenFields).map((f) => {
|
|
return <Fragment key={f}>{hiddenFields[f] && <input type="hidden" name={f} value={hiddenFields[f] ?? ""} hidden readOnly />}</Fragment>;
|
|
})}
|
|
{!onSubmit && (
|
|
<>
|
|
{!item ? (
|
|
<input type="hidden" name="redirect" value={EntityHelper.getRoutes({ routes, entity })?.list} hidden readOnly />
|
|
) : (
|
|
<input type="hidden" name="redirect" value={EntityHelper.getRoutes({ routes, entity, item })?.overview} hidden readOnly />
|
|
)}
|
|
</>
|
|
)}
|
|
{onCreatedRedirect && <input type="hidden" name="onCreatedRedirect" value={onCreatedRedirect} hidden readOnly />}
|
|
<RowGroups
|
|
item={item}
|
|
entity={entity}
|
|
rowValues={headers}
|
|
parentEntity={parentEntity}
|
|
allEntities={allEntities}
|
|
relatedRows={relatedRows}
|
|
editing={editing}
|
|
canUpdate={canUpdate}
|
|
routes={routes}
|
|
relationshipRows={relationshipRows}
|
|
setHeaders={setHeaders}
|
|
addRelationshipRow={addRelationshipRow}
|
|
setRelationshipRows={setRelationshipRows}
|
|
onFindEntityRows={onFindEntityRows}
|
|
onRemoveRelatedRow={onRemoveRelatedRow}
|
|
isPropertyVisible={isPropertyVisible}
|
|
children={children}
|
|
canSubmit={canSubmit}
|
|
isAddingOrEditing={isAddingOrEditing()}
|
|
parentEntities={{
|
|
visible: parentEntities.visible,
|
|
hidden: parentEntities.hidden,
|
|
onAddParentEntity: (rel) => {
|
|
setParentEntities((prev) => {
|
|
return {
|
|
visible: [...prev.visible, rel],
|
|
hidden: prev.hidden.filter((f) => f.id !== rel.id),
|
|
};
|
|
});
|
|
},
|
|
}}
|
|
onSaveIfAllSet={onSaveIfAllSet}
|
|
/>
|
|
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-12">
|
|
{childrenEntities.visible.map((relationship) => (
|
|
<div key={relationship.id} className="col-span-12">
|
|
<div className="space-y-2">
|
|
<h3 className="text-foreground text-sm leading-3 font-medium">
|
|
<div className="flex items-center space-x-1">
|
|
<div>
|
|
<span className="font-light italic"></span> {t(RelationshipHelper.getTitle({ fromEntityId: entity.id, relationship }))}
|
|
{/* {relationship.required && <span className="ml-1 text-red-500">*</span>} */}
|
|
</div>
|
|
</div>
|
|
</h3>
|
|
<RelationshipSelector
|
|
fromEntity={entity}
|
|
type="child"
|
|
relationship={relationship}
|
|
relatedRows={relatedRows}
|
|
onFindEntityRows={onFindEntityRows}
|
|
allEntities={allEntities}
|
|
onRemoveRelatedRow={onRemoveRelatedRow}
|
|
readOnly={item?.id !== undefined && (!editing || !canUpdate)}
|
|
routes={routes}
|
|
relationshipRows={relationshipRows}
|
|
addRelationshipRow={addRelationshipRow}
|
|
setRelationshipRows={setRelationshipRows}
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{isAddingOrEditing() && (
|
|
<AddHiddenRelationshipEntities
|
|
items={childrenEntities.hidden}
|
|
onClick={(rel) => {
|
|
setChildrenEntities((prev) => {
|
|
return {
|
|
visible: [...prev.visible, rel],
|
|
hidden: prev.hidden.filter((f) => f.id !== rel.id),
|
|
};
|
|
});
|
|
}}
|
|
type="child"
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{relatedRows.map(({ relationship, rows }) => (
|
|
<Fragment key={relationship.id}>
|
|
{rows.map((row) => (
|
|
// <>
|
|
<input
|
|
key={row.id}
|
|
type="hidden"
|
|
readOnly
|
|
hidden
|
|
name={`${relationship.childId === entity.id ? `parents[${relationship.parent.name}]` : `children[${relationship.child.name}]`}`}
|
|
value={row.id}
|
|
/>
|
|
// </>
|
|
))}
|
|
</Fragment>
|
|
))}
|
|
</FormGroup>
|
|
{/* // <OpenModal className="sm:max-w-4xl" onClose={() => setSearchingRelationshipRows(undefined)}> */}
|
|
<SlideOverWideEmpty
|
|
withTitle={false}
|
|
withClose={false}
|
|
title={t("shared.select")}
|
|
open={searchingRelationshipRows !== undefined}
|
|
onClose={() => setSearchingRelationshipRows(undefined)}
|
|
>
|
|
{selectedRelatedEntity && searchingRelationshipRows && (
|
|
<RowListFetcher
|
|
currentView={selectedRelatedEntity.view}
|
|
listUrl={EntityHelper.getRoutes({ routes, entity: selectedRelatedEntity.entity })?.list + "?view=null"}
|
|
newUrl={EntityHelper.getRoutes({ routes, entity: selectedRelatedEntity.entity })?.new ?? ""}
|
|
parentEntity={entity}
|
|
onSelected={(rows) => {
|
|
addRelationshipRow(searchingRelationshipRows, rows);
|
|
setSearchingRelationshipRows(undefined);
|
|
}}
|
|
multipleSelection={selectedRelatedEntity.multiple}
|
|
allEntities={allEntities}
|
|
/>
|
|
)}
|
|
</SlideOverWideEmpty>
|
|
{/* // </OpenModal> */}
|
|
</>
|
|
);
|
|
};
|
|
|
|
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 (
|
|
<div className={className}>
|
|
{/* <div>selectedRow: {getSelectedRow()}</div>
|
|
<div>
|
|
options:{" "}
|
|
{getOptions()
|
|
.map((f) => f.value)
|
|
.join(",")}
|
|
</div>
|
|
<div>
|
|
{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(",")}
|
|
</div> */}
|
|
{/* {RelationshipHelper.getInputType({ fromEntityId: fromEntity.id, relationship }) === "single-select" ? (
|
|
<InputSelector
|
|
className="mt-1"
|
|
name={relationship.parent.name}
|
|
disabled={readOnly}
|
|
value={getSelectedRow()}
|
|
options={getOptions()}
|
|
setValue={(value) => {
|
|
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 ? (
|
|
<button
|
|
onClick={() => onFindEntityRows(relationship)}
|
|
type="button"
|
|
disabled={readOnly}
|
|
className={clsx(
|
|
"border-border relative block w-full rounded-lg border-2 border-dashed p-4 text-center",
|
|
readOnly ? "bg-secondary/90 cursor-not-allowed" : "bg-background hover:border-border focus:ring-2 focus:ring-gray-500 focus:outline-hidden"
|
|
)}
|
|
>
|
|
<span className="text-muted-foreground flex items-center space-x-1 text-xs font-normal">
|
|
{readOnly ? (
|
|
<div>{t("shared.notSet")}</div>
|
|
) : (
|
|
<>
|
|
{type === "parent" && (
|
|
<>
|
|
<div>{t("shared.select")}</div>
|
|
<div className="lowercase">{translateEntityTitle(t, i18n, relationship.parent)}</div>
|
|
</>
|
|
)}
|
|
{type === "child" && (
|
|
<>
|
|
<div>{t("shared.add")}</div>
|
|
<div className="lowercase">{translateEntityTitle(t, i18n, relationship.child)}</div>
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
</span>
|
|
</button>
|
|
) : (
|
|
<div className="relative space-y-2 overflow-visible">
|
|
<RowsList
|
|
entity={entity.entity}
|
|
items={getRows(relationship) as RowWithDetails[]}
|
|
currentView={entity.view}
|
|
view={(entity.view?.layout ?? "card") as "table" | "board" | "grid" | "card"}
|
|
readOnly={readOnly}
|
|
onRemove={readOnly ? undefined : (row) => onRemoveRelatedRow(relationship, row)}
|
|
ignoreColumns={!readOnly ? [RowDisplayDefaultProperty.FOLIO, "parent." + relationship.parent.name, "child." + relationship.child.name] : []}
|
|
routes={routes}
|
|
/>
|
|
{/* {getRows(relationship).map((item) => (
|
|
<div
|
|
key={item.id}
|
|
className={clsx(
|
|
"group relative w-full overflow-visible truncate rounded-md border border-border px-4 py-3 text-left text-sm",
|
|
!readOnly ? "bg-background hover:border-border" : "bg-secondary/90"
|
|
)}
|
|
>
|
|
<button
|
|
onClick={() => onRemoveRelatedRow(relationship, item)}
|
|
type="button"
|
|
disabled={readOnly}
|
|
className={clsx(
|
|
"absolute right-0 top-0 mr-2 mt-2 hidden origin-top-right justify-center rounded-full bg-background text-muted-foreground",
|
|
readOnly ? "cursor-not-allowed" : "hover:text-red-500 group-hover:flex"
|
|
)}
|
|
>
|
|
<svg className="h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
</button>
|
|
<div className="grid grid-cols-2 gap-1">
|
|
<div>{RowHelper.getTextDescription({ entity, item, t, defaultsToFolio: true })}</div>
|
|
</div>
|
|
</div>
|
|
))} */}
|
|
<button
|
|
onClick={() => onFindEntityRows(relationship)}
|
|
type="button"
|
|
className={clsx(
|
|
"hover:bg-secondary border-border text-muted-foreground hover:border-border relative flex space-x-1 rounded-md border border-dashed px-2 py-1 text-center text-xs focus:ring-1 focus:ring-gray-500 focus:outline-hidden",
|
|
readOnly && "hidden"
|
|
)}
|
|
>
|
|
{type === "parent" && (
|
|
<>
|
|
<div>{t("shared.select")}</div>
|
|
<div className="lowercase">{translateEntityTitle(t, i18n, relationship.parent)}</div>
|
|
</>
|
|
)}
|
|
{type === "child" && (
|
|
<>
|
|
<div>{t("shared.add")}</div>
|
|
<div className="lowercase">{translateEntityTitle(t, i18n, relationship.child)}</div>
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
)}
|
|
{/* </>
|
|
) : null} */}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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<SetStateAction<RowValueDto[]>>;
|
|
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<RefRowValueInput>(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 (
|
|
<InputGroup key={idx} title={group ? t(group) : t("shared.details")}>
|
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-12">
|
|
{headers.map((detailValue, idxDetailValue) => {
|
|
return (
|
|
<div key={detailValue.propertyId} className={clsx("w-full", getPropertyColumnSpan(detailValue.property))}>
|
|
<RowValueInput
|
|
ref={rowValueInput}
|
|
entity={entity}
|
|
textValue={detailValue.textValue}
|
|
numberValue={detailValue.numberValue}
|
|
dateValue={detailValue.dateValue}
|
|
booleanValue={detailValue.booleanValue}
|
|
multiple={detailValue.multiple}
|
|
range={detailValue.range}
|
|
initialOption={detailValue.selectedOption}
|
|
selected={detailValue.property}
|
|
initialMedia={detailValue.media}
|
|
onChange={(e) => {
|
|
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}
|
|
/>
|
|
</div>
|
|
);
|
|
})}
|
|
{/* Show parent entities in Default Properties Group */}
|
|
{!group && (
|
|
<>
|
|
{parentEntities.visible.map((relationship) => (
|
|
<div key={relationship.id} className="col-span-12">
|
|
<label htmlFor={relationship.id} className="text-muted-foreground flex items-center justify-between space-x-2 text-xs font-medium">
|
|
<span className="truncate">
|
|
{t(RelationshipHelper.getTitle({ fromEntityId: entity.id, relationship }))}
|
|
{relationship.required && <span className="ml-1 text-red-500">*</span>}
|
|
</span>
|
|
</label>
|
|
<RelationshipSelector
|
|
fromEntity={entity}
|
|
className="mt-1"
|
|
type="parent"
|
|
relationship={relationship}
|
|
relatedRows={relatedRows}
|
|
onFindEntityRows={onFindEntityRows}
|
|
allEntities={allEntities}
|
|
onRemoveRelatedRow={onRemoveRelatedRow}
|
|
readOnly={item?.id !== undefined && (!editing || !canUpdate)}
|
|
routes={routes}
|
|
relationshipRows={relationshipRows}
|
|
addRelationshipRow={addRelationshipRow}
|
|
setRelationshipRows={setRelationshipRows}
|
|
/>
|
|
</div>
|
|
))}
|
|
|
|
{isAddingOrEditing && (
|
|
<AddHiddenRelationshipEntities items={parentEntities.hidden} onClick={parentEntities.onAddParentEntity} type="parent" />
|
|
)}
|
|
</>
|
|
)}
|
|
{/* Show custom properties in Default Properties Group */}
|
|
{!group && <>{children}</>}
|
|
</div>
|
|
</InputGroup>
|
|
);
|
|
})}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function AddHiddenRelationshipEntities({
|
|
items,
|
|
onClick,
|
|
type,
|
|
}: {
|
|
items: EntityRelationshipWithDetails[];
|
|
onClick: (item: EntityRelationshipWithDetails) => void;
|
|
type: "parent" | "child";
|
|
}) {
|
|
const { t, i18n } = useTranslation();
|
|
return (
|
|
<Fragment>
|
|
{items.length > 0 && (
|
|
<div className="col-span-12 flex flex-wrap items-center">
|
|
{items.map((relationship) => (
|
|
<button
|
|
key={relationship.id}
|
|
type="button"
|
|
onClick={() => onClick(relationship)}
|
|
className="text-muted-foreground bg-secondary/90 m-0.5 w-auto rounded-md px-2 py-1 text-xs font-medium hover:bg-gray-200"
|
|
>
|
|
{t("shared.add")} {type === "parent" ? translateEntityTitle(t, i18n, relationship.parent) : translateEntityTitle(t, i18n, relationship.child)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</Fragment>
|
|
);
|
|
}
|
|
|
|
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);
|