388 lines
14 KiB
TypeScript
388 lines
14 KiB
TypeScript
import { Fragment } from "react";
|
|
import { ColumnDto } from "~/application/dtos/data/ColumnDto";
|
|
import { EntityWithDetails, PropertyWithDetails } from "../db/entities/entities.db.server";
|
|
import { RowWithDetails } from "../db/entities/rows.db.server";
|
|
import DateUtils from "../shared/DateUtils";
|
|
import RowHelper from "./RowHelper";
|
|
import { RowHeaderDisplayDto } from "~/application/dtos/data/RowHeaderDisplayDto";
|
|
import { EntitiesApi } from "../api/.server/EntitiesApi";
|
|
import EntityHelper from "./EntityHelper";
|
|
import SimpleBadge from "~/components/ui/badges/SimpleBadge";
|
|
import { Colors } from "~/application/enums/shared/Colors";
|
|
import { TFunction, i18n as I18nextInstance } from "i18next";
|
|
import { RowDisplayDefaultProperty } from "./PropertyHelper";
|
|
import DateCell from "~/components/ui/dates/DateCell";
|
|
import RowCreatedByCell from "~/components/entities/rows/cells/RowCreatedByCell";
|
|
import RowFolioCell from "~/components/entities/rows/cells/RowFolioCell";
|
|
import RowTagsCell from "~/components/entities/rows/cells/RowTagsCell";
|
|
import RowOrderButtons from "~/components/entities/rows/RowOrderButtons";
|
|
import RowRelationshipRow from "~/components/entities/rows/cells/RowRelationshipRow";
|
|
import RowRelationshipRowsByEntity from "~/components/entities/rows/cells/RowRelationshipRowsByEntity";
|
|
import { translateEntityTitle, translateEntityTitlePlural, translatePropertyTitle } from "~/utils/i18n/translateLabel";
|
|
|
|
function isColumnVisible(columns?: ColumnDto[], name?: string) {
|
|
if (!columns) {
|
|
return true;
|
|
}
|
|
const column = columns?.find((f) => f.name === name);
|
|
if (!columns || !column) {
|
|
return false;
|
|
}
|
|
return column.visible;
|
|
}
|
|
|
|
function displayTenant(t: TFunction): RowHeaderDisplayDto<RowWithDetails> {
|
|
return {
|
|
name: RowDisplayDefaultProperty.TENANT,
|
|
title: "models.tenant.object",
|
|
value: (item) => <div>{item.tenant === null ? <div className="italic">{t("shared.admin")}</div> : <div>{item.tenant.name}</div>}</div>,
|
|
// href: (item) => item.tenant?.id ?? "",
|
|
breakpoint: "sm",
|
|
};
|
|
}
|
|
|
|
function displayFolio(entity: EntityWithDetails, routes?: EntitiesApi.Routes, onClick?: (item: RowWithDetails) => void): RowHeaderDisplayDto<RowWithDetails> {
|
|
return {
|
|
name: RowDisplayDefaultProperty.FOLIO,
|
|
title: "models.row.folio",
|
|
value: (item) => (
|
|
<RowFolioCell
|
|
prefix={entity.prefix}
|
|
folio={item.folio}
|
|
href={routes && onClick === undefined ? EntityHelper.getRoutes({ routes, entity, item })?.overview : undefined}
|
|
onClick={onClick !== undefined ? () => onClick(item) : undefined}
|
|
/>
|
|
),
|
|
// breakpoint: "sm",
|
|
sortable: true,
|
|
};
|
|
}
|
|
|
|
function displayOrder(): RowHeaderDisplayDto<RowWithDetails> {
|
|
return {
|
|
name: RowDisplayDefaultProperty.ORDER,
|
|
title: "",
|
|
className: "w-12",
|
|
value: (item) => <RowOrderButtons id={item.id} order={item.order} />,
|
|
};
|
|
}
|
|
|
|
function displayTags(): RowHeaderDisplayDto<RowWithDetails> {
|
|
return {
|
|
name: RowDisplayDefaultProperty.TAGS,
|
|
title: "models.tag.plural",
|
|
value: (item) => <RowTagsCell items={item.tags} />,
|
|
breakpoint: "sm",
|
|
sortable: true,
|
|
};
|
|
}
|
|
|
|
function translatedEntityTitleKey(
|
|
t: TFunction,
|
|
i18n: Pick<I18nextInstance, "exists"> | null | undefined,
|
|
entity: { name: string; title?: string | null }
|
|
): string {
|
|
const key = `models.${entity.name}.object`;
|
|
return i18n?.exists(key) ? key : translateEntityTitle(t, i18n, entity);
|
|
}
|
|
|
|
function translatedEntityTitlePluralKey(
|
|
t: TFunction,
|
|
i18n: Pick<I18nextInstance, "exists"> | null | undefined,
|
|
entity: { name: string; titlePlural?: string | null }
|
|
): string {
|
|
const key = `models.${entity.name}.plural`;
|
|
return i18n?.exists(key) ? key : translateEntityTitlePlural(t, i18n, entity);
|
|
}
|
|
|
|
function translatedPropertyTitleKey(
|
|
t: TFunction,
|
|
i18n: Pick<I18nextInstance, "exists"> | null | undefined,
|
|
entityName: string,
|
|
property: { name: string; title?: string | null }
|
|
): string {
|
|
const key = `models.${entityName}.${property.name}`;
|
|
return i18n?.exists(key) ? key : translatePropertyTitle(t, i18n, entityName, property);
|
|
}
|
|
|
|
function displayProperty(
|
|
entity: EntityWithDetails,
|
|
property: PropertyWithDetails,
|
|
i18n: Pick<I18nextInstance, "exists"> | null | undefined,
|
|
t: TFunction,
|
|
layout?: string
|
|
): RowHeaderDisplayDto<RowWithDetails> {
|
|
return {
|
|
name: property.name,
|
|
title: translatedPropertyTitleKey(t, i18n, entity.name, property),
|
|
value: (item) => RowHelper.getPropertyValue({ entity, item, property }),
|
|
formattedValue: (item) => <div className="max-w-sm truncate">{RowHelper.getCellValue({ entity, property, item })}</div>,
|
|
};
|
|
}
|
|
|
|
function displayCreatedAt(): RowHeaderDisplayDto<RowWithDetails> {
|
|
return {
|
|
name: RowDisplayDefaultProperty.CREATED_AT,
|
|
title: "shared.createdAt",
|
|
value: (item) => DateUtils.dateAgo(item.createdAt),
|
|
formattedValue: (item) => <DateCell date={item.createdAt} />,
|
|
className: "text-muted-foreground text-xs",
|
|
breakpoint: "sm",
|
|
};
|
|
}
|
|
|
|
function displayCreatedBy(): RowHeaderDisplayDto<RowWithDetails> {
|
|
return {
|
|
name: RowDisplayDefaultProperty.CREATED_BY,
|
|
title: "shared.createdBy",
|
|
value: (item) => <RowCreatedByCell user={item.createdByUser} apiKey={item.createdByApiKey} />,
|
|
className: "text-muted-foreground text-xs",
|
|
breakpoint: "sm",
|
|
};
|
|
}
|
|
|
|
function getDisplayedHeaders({
|
|
entity,
|
|
columns,
|
|
layout,
|
|
allEntities,
|
|
routes,
|
|
onFolioClick,
|
|
onEditClick,
|
|
onRelatedRowClick,
|
|
t,
|
|
i18n,
|
|
}: {
|
|
entity: EntityWithDetails;
|
|
columns?: ColumnDto[];
|
|
layout?: string;
|
|
allEntities: EntityWithDetails[];
|
|
routes?: EntitiesApi.Routes;
|
|
onFolioClick?: (item: RowWithDetails) => void;
|
|
onEditClick?: (item: RowWithDetails) => void;
|
|
onRelatedRowClick?: (item: RowWithDetails) => void;
|
|
t: TFunction;
|
|
i18n?: Pick<I18nextInstance, "exists"> | null;
|
|
}): RowHeaderDisplayDto<RowWithDetails>[] {
|
|
let headers: RowHeaderDisplayDto<RowWithDetails>[] = [];
|
|
|
|
if (columns && isColumnVisible(columns, RowDisplayDefaultProperty.ID)) {
|
|
headers.push({
|
|
name: RowDisplayDefaultProperty.ID,
|
|
title: "ID",
|
|
value: (item) => (
|
|
<div>
|
|
{onEditClick !== undefined ? (
|
|
<button
|
|
type="button"
|
|
onClick={() => onEditClick(item)}
|
|
className="focus:bg-secondary/90 hover:border-border rounded-md border-b border-dashed border-transparent"
|
|
>
|
|
{item.id}
|
|
</button>
|
|
) : (
|
|
<div>{item.id}</div>
|
|
)}
|
|
</div>
|
|
),
|
|
href: (item) => (onEditClick === undefined ? item.id : undefined),
|
|
breakpoint: "sm",
|
|
});
|
|
}
|
|
if (columns && isColumnVisible(columns, RowDisplayDefaultProperty.ENTITY)) {
|
|
headers.push({
|
|
name: RowDisplayDefaultProperty.ENTITY,
|
|
title: "models.row.entity",
|
|
value: () => translateEntityTitle(t, i18n, entity),
|
|
});
|
|
}
|
|
if (columns && isColumnVisible(columns, RowDisplayDefaultProperty.TENANT)) {
|
|
headers.push(displayTenant(t));
|
|
}
|
|
if (columns && isColumnVisible(columns, RowDisplayDefaultProperty.ORDER) && layout === "table") {
|
|
headers.push(displayOrder());
|
|
}
|
|
if (isColumnVisible(columns, RowDisplayDefaultProperty.FOLIO)) {
|
|
headers.push(displayFolio(entity, routes, onFolioClick));
|
|
}
|
|
|
|
entity.properties
|
|
.filter((f) => !f.isHidden && isColumnVisible(columns, f.name))
|
|
.forEach((property) => {
|
|
headers.push(displayProperty(entity, property, i18n, t, layout));
|
|
});
|
|
|
|
entity.parentEntities
|
|
.filter((f) => !f.hiddenIfEmpty)
|
|
.forEach((relationship) => {
|
|
const parentEntity = allEntities.find((f) => f.id === relationship.parentId);
|
|
if (parentEntity && isColumnVisible(columns, `parent.${parentEntity.name}`)) {
|
|
headers.push({
|
|
name: `parent.${parentEntity.name}`,
|
|
title: translatedEntityTitleKey(t, i18n, parentEntity),
|
|
value: (item) => (
|
|
<div className="flex max-w-xs items-center space-x-2 overflow-x-auto">
|
|
{!item.parentRows?.length || item.parentRows.length === 0 ? (
|
|
<div className="text-muted-foreground px-1 pt-1 pb-0.5 text-center text-sm italic">{t("shared.none")}</div>
|
|
) : (
|
|
<Fragment>
|
|
{item.parentRows
|
|
?.filter((f) => f.relationshipId === relationship.id)
|
|
.map(({ parent }) => {
|
|
return (
|
|
<Fragment key={parent.id}>
|
|
<RowRelationshipRow
|
|
entity={parentEntity}
|
|
item={parent}
|
|
onRelatedRowClick={onRelatedRowClick ? () => onRelatedRowClick(item) : undefined}
|
|
routes={routes}
|
|
t={t}
|
|
/>
|
|
</Fragment>
|
|
);
|
|
})}
|
|
</Fragment>
|
|
)}
|
|
</div>
|
|
),
|
|
});
|
|
}
|
|
});
|
|
|
|
entity.childEntities
|
|
.filter((f) => !f.hiddenIfEmpty)
|
|
.forEach((relationship) => {
|
|
const childEntity = allEntities.find((f) => f.id === relationship.childId);
|
|
if (childEntity && isColumnVisible(columns, `child.${childEntity.name}`)) {
|
|
headers.push({
|
|
name: `child.${childEntity.name}`,
|
|
title: translatedEntityTitlePluralKey(t, i18n, childEntity),
|
|
value: (item) => (
|
|
<ul className="flex max-h-20 max-w-lg flex-col space-y-1 overflow-auto">
|
|
{!item.childRows?.length || item.childRows.length === 0 ? (
|
|
<div className="text-muted-foreground px-1 pt-1 pb-0.5 text-sm italic">{t("shared.none")}</div>
|
|
) : (
|
|
<Fragment>
|
|
{childEntity.properties.filter((f) => !f.isDefault && f.isDisplay).length === 0 ? (
|
|
<div>
|
|
<SimpleBadge title={item.childRows?.length.toString()} color={Colors.INDIGO} />
|
|
</div>
|
|
) : (
|
|
<Fragment>
|
|
{item.childRows
|
|
?.filter((f) => f.relationshipId === relationship.id)
|
|
.map(({ child }) => {
|
|
return (
|
|
<li key={child.id}>
|
|
<RowRelationshipRow
|
|
entity={childEntity}
|
|
item={child}
|
|
onRelatedRowClick={onRelatedRowClick ? () => onRelatedRowClick(item) : undefined}
|
|
routes={routes}
|
|
t={t}
|
|
/>
|
|
</li>
|
|
);
|
|
})}
|
|
</Fragment>
|
|
)}
|
|
</Fragment>
|
|
)}
|
|
</ul>
|
|
),
|
|
});
|
|
}
|
|
});
|
|
|
|
if (isColumnVisible(columns, RowDisplayDefaultProperty.HIDDEN_PARENTS)) {
|
|
headers.push({
|
|
name: RowDisplayDefaultProperty.HIDDEN_PARENTS,
|
|
title: "",
|
|
value: (item) => (
|
|
<RowRelationshipRowsByEntity
|
|
entity={entity}
|
|
item={item}
|
|
onRelatedRowClick={onRelatedRowClick ? () => onRelatedRowClick(item) : undefined}
|
|
routes={routes}
|
|
allEntities={allEntities}
|
|
type="parents"
|
|
/>
|
|
),
|
|
});
|
|
}
|
|
if (isColumnVisible(columns, RowDisplayDefaultProperty.HIDDEN_CHILDREN)) {
|
|
headers.push({
|
|
name: RowDisplayDefaultProperty.HIDDEN_CHILDREN,
|
|
title: "",
|
|
value: (item) => (
|
|
<RowRelationshipRowsByEntity
|
|
entity={entity}
|
|
item={item}
|
|
onRelatedRowClick={onRelatedRowClick ? () => onRelatedRowClick(item) : undefined}
|
|
routes={routes}
|
|
allEntities={allEntities}
|
|
type="children"
|
|
/>
|
|
),
|
|
});
|
|
}
|
|
|
|
if (isColumnVisible(columns, RowDisplayDefaultProperty.CREATED_AT)) {
|
|
headers.push(displayCreatedAt());
|
|
}
|
|
if (isColumnVisible(columns, RowDisplayDefaultProperty.CREATED_BY)) {
|
|
headers.push(displayCreatedBy());
|
|
}
|
|
if (columns && isColumnVisible(columns, RowDisplayDefaultProperty.PERMISSIONS)) {
|
|
headers.push({
|
|
name: RowDisplayDefaultProperty.PERMISSIONS,
|
|
title: "models.row.permissions",
|
|
value: () => <div className="text-muted-foreground italic">{t("shared.notAvailable")}</div>,
|
|
});
|
|
}
|
|
if (columns && entity.hasTags && isColumnVisible(columns, RowDisplayDefaultProperty.TAGS)) {
|
|
headers.push(displayTags());
|
|
}
|
|
if (columns && entity.hasTasks && isColumnVisible(columns, RowDisplayDefaultProperty.TASKS)) {
|
|
headers.push({
|
|
name: RowDisplayDefaultProperty.TASKS,
|
|
title: "models.row.tasks",
|
|
value: () => <div className="text-muted-foreground italic">{t("shared.notAvailable")}</div>,
|
|
});
|
|
}
|
|
if (columns && entity.hasComments && isColumnVisible(columns, RowDisplayDefaultProperty.COMMENTS)) {
|
|
headers.push({
|
|
name: RowDisplayDefaultProperty.COMMENTS,
|
|
title: "models.row.comments",
|
|
value: () => <div className="text-muted-foreground italic">{t("shared.notAvailable")}</div>,
|
|
});
|
|
}
|
|
|
|
if (columns && columns.length > 0) {
|
|
const newOrder: RowHeaderDisplayDto<RowWithDetails>[] = [];
|
|
columns.forEach((column) => {
|
|
const header = headers.find((f) => f.name === column.name);
|
|
if (header && !newOrder.includes(header)) {
|
|
newOrder.push(header);
|
|
}
|
|
});
|
|
headers = newOrder;
|
|
}
|
|
|
|
let nonDefaultHeaders = headers.filter((f) => !f.name?.startsWith("default."));
|
|
if (nonDefaultHeaders.length === 1) {
|
|
nonDefaultHeaders[0].className = "w-full";
|
|
}
|
|
return headers;
|
|
}
|
|
|
|
export default {
|
|
isColumnVisible,
|
|
displayTenant,
|
|
displayFolio,
|
|
displayProperty,
|
|
displayCreatedAt,
|
|
displayCreatedBy,
|
|
getDisplayedHeaders,
|
|
};
|