DataTable component

Signed-off-by: Jonathan <contact@grafikart.fr>
This commit is contained in:
Jonathan
2025-11-12 12:29:54 +01:00
committed by Émile Ré
parent 6dc6e2a626
commit 3953a05cf3
12 changed files with 395 additions and 70 deletions

View File

@@ -36,7 +36,10 @@ export function PeopleSelectField({
}
function PeopleSelectWithQuery(
props: Pick<Props, "organizationId" | "control" | "name" | "disabled" | "optional">
props: Pick<
Props,
"organizationId" | "control" | "name" | "disabled" | "optional"
>,
) {
const { __ } = useTranslate();
const { name, organizationId, control } = props;
@@ -53,15 +56,15 @@ function PeopleSelectWithQuery(
id={name}
variant="editor"
placeholder={__("Select an owner")}
onValueChange={(value) => field.onChange(value === "__NONE__" ? null : value)}
onValueChange={(value) =>
field.onChange(value === "__NONE__" ? null : value)
}
key={people?.length.toString() ?? "0"}
{...field}
className="w-full"
value={field.value ?? (props.optional ? "__NONE__" : "")}
>
{props.optional && (
<Option value="__NONE__">{__("None")}</Option>
)}
{props.optional && <Option value="__NONE__">{__("None")}</Option>}
{people?.map((p) => (
<Option key={p.id} value={p.id} className="flex gap-2">
<Avatar name={p.fullName} />
@@ -74,3 +77,44 @@ function PeopleSelectWithQuery(
</>
);
}
type OptionsProps = {
organizationId: string;
optional?: boolean;
} & ComponentProps<typeof Field>;
export function PeopleSelectOptions({
organizationId,
...props
}: OptionsProps) {
return (
<Suspense
fallback={<Select variant="editor" loading placeholder="Loading..." />}
>
<PeopleSelectOptionsWithQuery
organizationId={organizationId}
optional={props.optional}
/>
</Suspense>
);
}
function PeopleSelectOptionsWithQuery(
props: Pick<Props, "organizationId" | "disabled" | "optional">,
) {
const { __ } = useTranslate();
const { organizationId } = props;
const people = usePeople(organizationId, { excludeContractEnded: true });
return (
<>
{props.optional && <Option value="__NONE__">{__("None")}</Option>}
{people?.map((p) => (
<Option key={p.id} value={p.id} className="flex gap-2">
<Avatar name={p.fullName} />
{p.fullName}
</Option>
))}
</>
);
}

View File

@@ -12,6 +12,12 @@ import {
DropdownItem,
IconTrashCan,
Avatar,
EditableCell,
Select,
Option,
DataTable,
CellHead,
Cell,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { usePageTitle } from "@probo/hooks";
@@ -20,11 +26,16 @@ import {
usePaginationFragment,
usePreloadedQuery,
type PreloadedQuery,
useMutation,
} from "react-relay";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { useParams } from "react-router";
import { CreateAssetDialog } from "./dialogs/CreateAssetDialog";
import { useDeleteAsset, assetsQuery } from "../../../hooks/graph/AssetGraph";
import {
useDeleteAsset,
assetsQuery,
updateAssetMutation,
} from "../../../hooks/graph/AssetGraph";
import type { AssetGraphListQuery } from "/hooks/graph/__generated__/AssetGraphListQuery.graphql";
import { faviconUrl } from "@probo/helpers";
import type { NodeOf } from "/types";
@@ -37,6 +48,7 @@ import { SortableTable } from "/components/SortableTable";
import { SnapshotBanner } from "/components/SnapshotBanner";
import { Authorized } from "/permissions";
import { isAuthorized } from "/permissions";
import { PeopleSelectOptions } from "/components/form/PeopleSelectField.tsx";
const paginatedAssetsFragment = graphql`
fragment AssetsPageFragment on Organization
@@ -101,7 +113,7 @@ export default function AssetsPage(props: Props) {
const data = usePreloadedQuery(assetsQuery, props.queryRef);
const pagination = usePaginationFragment(
paginatedAssetsFragment,
data.node as AssetsPageFragment$key
data.node as AssetsPageFragment$key,
);
const assets = pagination.data.assets?.edges.map((edge) => edge.node);
const connectionId = pagination.data.assets.__id;
@@ -119,7 +131,7 @@ export default function AssetsPage(props: Props) {
<PageHeader
title={__("Assets")}
description={__(
"Manage your organization's assets and their classifications."
"Manage your organization's assets and their classifications.",
)}
>
{!isSnapshotMode && (
@@ -133,28 +145,17 @@ export default function AssetsPage(props: Props) {
</Authorized>
)}
</PageHeader>
<SortableTable {...pagination}>
<Thead>
<Tr>
<Th>{__("Name")}</Th>
<Th>{__("Type")}</Th>
<Th>{__("Amount")}</Th>
<Th>{__("Owner")}</Th>
<Th>{__("Vendors")}</Th>
{hasAnyAction && <Th></Th>}
</Tr>
</Thead>
<Tbody>
{assets.map((entry) => (
<AssetRow
key={entry.id}
entry={entry}
connectionId={connectionId}
hasAnyAction={hasAnyAction}
/>
))}
</Tbody>
</SortableTable>
<DataTable columns={6}>
<CellHead>{__("Name")}</CellHead>
<CellHead>{__("Type")}</CellHead>
<CellHead>{__("Amount")}</CellHead>
<CellHead>{__("Owner")}</CellHead>
<CellHead>{__("Vendors")}</CellHead>
<CellHead></CellHead>
{assets.map((entry) => (
<AssetRow key={entry.id} entry={entry} connectionId={connectionId} />
))}
</DataTable>
</div>
);
}
@@ -162,11 +163,9 @@ export default function AssetsPage(props: Props) {
function AssetRow({
entry,
connectionId,
hasAnyAction,
}: {
entry: AssetEntry;
connectionId: string;
hasAnyAction: boolean;
}) {
const organizationId = useOrganizationId();
const { __ } = useTranslate();
@@ -175,21 +174,71 @@ function AssetRow({
const deleteAsset = useDeleteAsset(entry, connectionId);
const vendors = entry.vendors?.edges.map((edge) => edge.node) ?? [];
const assetUrl = isSnapshotMode && snapshotId
? `/organizations/${organizationId}/snapshots/${snapshotId}/assets/${entry.id}`
: `/organizations/${organizationId}/assets/${entry.id}`;
const assetUrl =
isSnapshotMode && snapshotId
? `/organizations/${organizationId}/snapshots/${snapshotId}/assets/${entry.id}`
: `/organizations/${organizationId}/assets/${entry.id}`;
const [mutate, isLoading] = useMutation(updateAssetMutation);
const updater = (fieldName: keyof typeof entry) => (value: string) => {
// Only send an update if the value changed
if (entry[fieldName] === value) {
return;
}
mutate({
variables: {
input: {
id: entry.id,
[fieldName]: value,
},
},
});
};
return (
<Tr to={assetUrl}>
<Td>{entry.name}</Td>
<Td>
<>
<EditableCell
type="text"
defaultValue={entry.name}
onValueChange={updater("name")}
/>
<EditableCell
type="select"
isLoading={isLoading}
onValueChange={updater("assetType")}
options={
<>
<Option value="VIRTUAL">
<Badge variant={getAssetTypeVariant("VIRTUAL")}>
{__("Virtual")}
</Badge>
</Option>
<Option value="PHYSICAL">
<Badge variant={getAssetTypeVariant("PHYSICAL")}>
{__("Physical")}
</Badge>
</Option>
</>
}
>
<Badge variant={getAssetTypeVariant(entry.assetType)}>
{entry.assetType === "PHYSICAL" ? __("Physical") : __("Virtual")}
</Badge>
</Td>
<Td>{entry.amount}</Td>
<Td>{entry.owner?.fullName ?? __("Unassigned")}</Td>
<Td>
</EditableCell>
<EditableCell
type="text"
defaultValue={entry.amount}
onValueChange={updater("amount")}
/>
<EditableCell
type="select"
isLoading={isLoading}
onValueChange={updater("owner")}
options={<PeopleSelectOptions organizationId={organizationId} />}
>
{entry.owner?.fullName ?? __("Unassigned")}
</EditableCell>
<Cell>
{vendors.length > 0 ? (
<div className="flex flex-wrap gap-1">
{vendors.slice(0, 3).map((vendor) => (
@@ -215,22 +264,20 @@ function AssetRow({
) : (
<span className="text-txt-secondary text-sm">{__("None")}</span>
)}
</Td>
{hasAnyAction && (
<Td noLink width={50} className="text-end">
<Authorized entity="Asset" action="deleteAsset">
<ActionDropdown>
<DropdownItem
onClick={deleteAsset}
variant="danger"
icon={IconTrashCan}
>
{__("Delete")}
</DropdownItem>
</ActionDropdown>
</Authorized>
</Td>
)}
</Tr>
</Cell>
<Cell className="text-end">
{!isSnapshotMode && (
<ActionDropdown>
<DropdownItem
onClick={deleteAsset}
variant="danger"
icon={IconTrashCan}
>
{__("Delete")}
</DropdownItem>
</ActionDropdown>
)}
</Cell>
</>
);
}