import { Property } from "@toolctrl/db/prisma-browser"; import clsx from "clsx"; import { useRef, useState, useEffect, Fragment } from "react"; import { useTranslation } from "react-i18next"; import Constants from "~/application/Constants"; import { PropertyType } from "~/application/enums/entities/PropertyType"; import { ViewFilterCondition } from "~/application/enums/entities/ViewFilterCondition"; import InfoBanner from "~/components/ui/banners/InfoBanner"; import FormGroup from "~/components/ui/forms/FormGroup"; import InputGroup from "~/components/ui/forms/InputGroup"; import InputCheckboxInline from "~/components/ui/input/InputCheckboxInline"; import InputNumber from "~/components/ui/input/InputNumber"; import InputRadioGroupCards from "~/components/ui/input/InputRadioGroupCards"; import InputSelect from "~/components/ui/input/InputSelect"; import InputSelector from "~/components/ui/input/InputSelector"; import InputText, { RefInputText } from "~/components/ui/input/InputText"; import OrderIndexButtons from "~/components/ui/sort/OrderIndexButtons"; import CollapsibleRow from "~/components/ui/tables/CollapsibleRow"; import { EntityViewLayoutTypes } from "~/modules/rows/dtos/EntityViewLayoutType"; import { EntityWithDetails } from "~/utils/db/entities/entities.db.server"; import { EntityViewWithDetails } from "~/utils/db/entities/entityViews.db.server"; import OrderHelper from "~/utils/helpers/OrderHelper"; import RowColumnsHelper from "~/utils/helpers/RowColumnsHelper"; import { updateItemByIdx } from "~/utils/shared/ObjectUtils"; import StringUtils from "~/utils/shared/StringUtils"; import EntityViewLayoutBadge from "./EntityViewLayoutBadge"; import { RowWithDetails } from "~/utils/db/entities/rows.db.server"; import RowHelper from "~/utils/helpers/RowHelper"; import RowsList from "../rows/RowsList"; import { defaultDisplayProperties } from "~/utils/helpers/PropertyHelper"; import { translateEntityTitlePlural, translatePropertyTitle, translateSelectOptionLabel } from "~/utils/i18n/translateLabel"; interface Props { entity: EntityWithDetails | undefined; item?: EntityViewWithDetails | null; canDelete?: boolean; onClose?: () => void; actionNames?: { create: string; update: string; delete: string; }; tenantId: string | null; userId: string | null; isSystem: boolean; showViewType: boolean; } export default function EntityViewForm({ entity, item, canDelete, onClose, actionNames, isSystem, tenantId, userId, showViewType, }: Props & { entity: EntityWithDetails }) { const { t, i18n } = useTranslation(); const inputName = useRef(null); const [possibleProperties, setPossibleProperties] = useState<{ propertyId: string | null; name: string | null; order: number; title?: string }[]>( item?.properties ?? [] ); const [fakeItems, setFakeItems] = useState([]); const [layout, setLayout] = useState<"table" | "board" | "grid" | "card">("table"); const [name, setName] = useState(""); const [title, setTitle] = useState(""); const [order, setOrder] = useState(item?.order ?? OrderHelper.getNextOrder(entity.views)); const [pageSize, setPageSize] = useState(item?.pageSize ?? Constants.DEFAULT_PAGE_SIZE); const [isDefault, setIsDefault] = useState(item?.isDefault ?? OrderHelper.getNextOrder(entity.views) === 1); const [properties, setProperties] = useState<{ propertyId: string | null; name: string | null; order: number; title?: string }[]>([]); const [filters, setFilters] = useState<{ name: string; condition: string; value: string; match: string }[]>(item?.filters ?? []); // const [sort, setSort] = useState<{ name: string; asc: boolean; order: number }[]>(item?.sort ?? []); // Board const [groupBy, setGroupBy] = useState(item?.groupByPropertyId ? "byProperty" : undefined); const [groupByPropertyId, setGroupByPropertyId] = useState(item?.groupByPropertyId ?? undefined); const [groupByOptions, setGroupByOptions] = useState<{ name: string; value: string }[]>([]); const [selectProperties, setSelectProperties] = useState([]); // Grid const [gridColumns, setGridColumns] = useState(item?.gridColumns || 0); const [gridColumnsSm, setGridColumnsSm] = useState(item?.gridColumnsSm || 0); const [gridColumnsMd, setGridColumnsMd] = useState(item?.gridColumnsMd || 0); const [gridColumnsLg, setGridColumnsLg] = useState(item?.gridColumnsLg || 0); const [gridColumnsXl, setGridColumnsXl] = useState(item?.gridColumnsXl || 0); const [gridColumns2xl, setGridColumns2xl] = useState(item?.gridColumns2xl || 0); const [gridGap, setGridGap] = useState(item?.gridGap ?? "sm"); const [filterByProperties, setFilterByProperties] = useState<{ name: string; value: string }[]>([]); useEffect(() => { if (!item && isSystem) { setName("system-" + entity.slug); setTitle(t("models.view.systemTitle", { entity: translateEntityTitlePlural(t, i18n, entity) })); setLayout("card"); } else if (item) { setName(item.name); setTitle(item.title); setLayout(item.layout as "table" | "board" | "grid" | "card"); } }, [entity, isSystem, item, t]); useEffect(() => { const items: RowWithDetails[] = Array.from({ length: 10 }).map((_, idx) => { let nonDefaultProperties = properties.filter((p) => !defaultDisplayProperties.find((dp) => dp.name === p.name)); const item: RowWithDetails = { values: nonDefaultProperties.map(({ name }) => { const property = entity.properties.find((p) => p.name === name); if (!property) return null; return RowHelper.getFakePropertyValue({ property, t, idx: idx + 1 }); }), folio: idx + 1, createdAt: new Date(), createdByUser: { email: "preview@example.invalid", firstName: "Preview", lastName: "User", }, } as RowWithDetails; return item; }); setFakeItems(items); }, [entity.properties, properties, t]); useEffect(() => { const possibleProperties = RowColumnsHelper.getDefaultEntityColumns(entity).map((i, idx) => { const property = entity.properties.find((p) => p.name === i.name); return { propertyId: property?.id ?? null, name: i.name, title: i.title, order: idx + 1, }; }); setPossibleProperties(possibleProperties); }, [entity]); // Initial selected properties useEffect(() => { if (item) { setProperties(item.properties); } else { setProperties( RowColumnsHelper.getDefaultEntityColumns(entity) .filter((f) => f.visible) .map((i, idx) => { return { propertyId: null, name: i.name, title: i.title, order: idx + 1, }; }) ); } }, [entity, item]); useEffect(() => { setTimeout(() => { inputName.current?.input.current?.focus(); }, 100); const selectProperties = entity.properties.filter((f) => [PropertyType.SELECT, PropertyType.MULTI_SELECT].includes(f.type)); setGroupByOptions([ { name: t("models.view.property"), value: "byProperty", }, ]); setSelectProperties(selectProperties); let filterByProperties = entity.properties .filter((f) => f.type === PropertyType.TEXT || f.type === PropertyType.SELECT || f.type === PropertyType.BOOLEAN) .map((property) => { return { name: translatePropertyTitle(t, i18n, entity.name, property), value: property.name, }; }); if (entity.hasTags) { filterByProperties = [...filterByProperties, { name: t("models.row.tags"), value: "tags" }]; } setFilterByProperties(filterByProperties); }, [entity, t]); function getPropertyByName(name: string) { return entity.properties.find((f) => f.name === name); } function getPropertyConditionsByName(name: string) { if (name === "tags") { return [ { value: ViewFilterCondition.equals, name: t("entities.conditions.equals") }, { value: ViewFilterCondition.contains, name: t("entities.conditions.contains") }, { value: ViewFilterCondition.notIn, name: t("entities.conditions.notIn") }, ]; } else { const property = getPropertyByName(name); if (property?.type === PropertyType.BOOLEAN) { return [ { value: ViewFilterCondition.equals, name: t("entities.conditions.equals") }, // { value: ViewFilterCondition.notIn, name: t("entities.conditions.notIn") }, ]; } else if (property?.type === PropertyType.TEXT || property?.type === PropertyType.SELECT) { return [ { value: ViewFilterCondition.equals, name: t("entities.conditions.equals") }, { value: ViewFilterCondition.contains, name: t("entities.conditions.contains") }, { value: ViewFilterCondition.lt, name: t("entities.conditions.lt") }, { value: ViewFilterCondition.lte, name: t("entities.conditions.lte") }, { value: ViewFilterCondition.gt, name: t("entities.conditions.gt") }, { value: ViewFilterCondition.gte, name: t("entities.conditions.gte") }, { value: ViewFilterCondition.startsWith, name: t("entities.conditions.startsWith") }, { value: ViewFilterCondition.endsWith, name: t("entities.conditions.endsWith") }, { value: ViewFilterCondition.in, name: t("entities.conditions.in") }, { value: ViewFilterCondition.notIn, name: t("entities.conditions.notIn") }, ]; } } return []; } function getPropertyTitle(name: string) { return RowColumnsHelper.getDefaultEntityColumns(entity).find((f) => f.name === name)?.title ?? ""; } function getTypeTitle() { if (isSystem) { return t("models.view.typeSummary", { type: t("models.view.types.system") }); } else if (!tenantId && !userId) { return t("models.view.typeSummary", { type: t("models.view.types.default") }); } else if (tenantId && !userId) { return t("models.view.typeSummary", { type: t("models.view.types.tenant") }); } else if (tenantId && userId) { return t("models.view.typeSummary", { type: t("models.view.types.user") }); } return t("models.view.typeSummary", { type: t("models.view.types.tenant") }); } function getTypeDescription() { if (isSystem) { return t("models.view.typeDescriptions.system"); } else if (!tenantId && !userId) { return t("models.view.typeDescriptions.default"); } else if (tenantId && !userId) { return t("models.view.typeDescriptions.tenant"); } else if (tenantId && userId) { return t("models.view.typeDescriptions.user"); } return t("models.view.typeDescriptions.tenant"); } return ( {showViewType && }
setLayout((e?.toString() ?? "table") as "table" | "board" | "grid" | "card")} options={EntityViewLayoutTypes.map((f) => { return { name: t(f.labelKey), value: f.value, renderName: (
{t(f.labelKey)}
), }; })} /> setName(e.toString().toLowerCase())} autoComplete="off" required // lowercase onBlur={() => { if (!item) { let title = StringUtils.capitalize(name.toLowerCase()); // replace - occurrences with spaces title = title.replace(/-/g, " "); setTitle(title); } }} /> {!isSystem && ( )}
{layout === "board" && (
<> { return { name: t(item.title), value: item.id, }; })} />
)} {layout === "grid" && (
setGridGap(e?.toString() ?? "sm")} options={[ { name: "xs", value: "xs" }, { name: "sm", value: "sm" }, { name: "md", value: "md" }, { name: "lg", value: "lg" }, { name: "xl", value: "xl" }, ]} />
)}
{properties.map((item, idx) => ( ))}
{t("shared.all")}
{possibleProperties.map((item, idx) => (
{item.title ? t(item.title) : item.name}
({item?.name})
} value={properties.find((f) => f.name === item.name) !== undefined} setValue={(e) => { if (e) { setProperties([ ...properties, { propertyId: item.propertyId, name: item.name, title: item.title, order: OrderHelper.getNextOrder(properties) }, ]); } else { setProperties(properties.filter((f) => f.name !== item.name)); } }} />
))}
{t("models.view.propertiesDisplayed")}
{properties .sort((a, b) => a.order - b.order) .map((item, idx) => (
{ return { order: item.order, idx: itemIdx, }; })} onChange={(newOrders) => { setProperties( properties.map((item, itemIdx) => { const newOrder = newOrders.find((f) => f.idx === itemIdx); if (newOrder) { return { ...item, order: newOrder.order, }; } else { return item; } }) ); }} />
{item.title ? t(item.title) : t(getPropertyTitle(item.name ?? ""))}{" "} ({item?.name})
))}
{!isSystem && ( {filters.map((item, idx) => ( ))}
{filters.map((item, idx) => (
{item.name}
{item.condition}
{item.value}
} onRemove={() => setFilters(filters.filter((_, i) => i !== idx))} >
updateItemByIdx(filters, setFilters, idx, { match: e, }) } options={[ { name: t("models.view.matchOperators.and"), value: "and" }, { name: t("models.view.matchOperators.or"), value: "or" }, ]} />
updateItemByIdx(filters, setFilters, idx, { name: e, }) } options={filterByProperties} />
updateItemByIdx(filters, setFilters, idx, { condition: e, }) } options={getPropertyConditionsByName(item.name)} />
{getPropertyByName(item.name)?.type === PropertyType.BOOLEAN ? (
updateItemByIdx(filters, setFilters, idx, { value: e })} options={[ { name: t("shared.true"), value: "true" }, { name: t("shared.false"), value: "false" }, ]} />
) : getPropertyByName(item.name)?.type === PropertyType.SELECT ? (
updateItemByIdx(filters, setFilters, idx, { value: e })} options={ getPropertyByName(item.name)?.options.map((i) => { return { name: translateSelectOptionLabel(t, i18n, entity.name, item.name, i), value: i.value, }; }) ?? [] } />
) : (
updateItemByIdx(filters, setFilters, idx, { value: e })} />
)}
))}
{/* {sort.map((item, idx) => ( ))}
{sort .sort((a, b) => a.order - b.order) .map((item, idx) => (
{item.name}
{item.asc ? "asc" : "desc"}
} onRemove={() => setSort(sort.filter((_, i) => i !== idx))} >
updateItemByIdx(sort, setSort, idx, { name: e })} options={entity.properties.map((property) => { return { name: translatePropertyTitle(t, i18n, entity.name, property), value: property.name, }; })} /> updateItemByIdx(sort, setSort, idx, { asc: e?.toString() === "asc" })} options={[ { name: "Ascending", value: "asc", }, { name: "Descending", value: "desc", }, ]} />
))}
*/}
)}
({ name: p.name ?? "", title: p.title ?? "", visible: true, }))} />
); }