Refactor third parties frontend to page arborescence
Mirror the risks refactor (c78a713): colocate routes.ts, split the
detail layout query so each child route owns its Loader + Page, rename
tabs/*Tab to resource folders with *Page, move dialogs into
_components/, and extract ThirdPartyRow with its own fragment.
Remove outlet context data passing and deprecated
loaderFromQueryLoader. Delete the monolithic ThirdPartyGraph hook,
colocating each GraphQL operation with its consumer: the create
mutation in CreateThirdPartyDialog (now useMutation + useToast) and
the third-party list queries in ThirdPartiesCell and
ThirdPartiesMultiSelectField (now useQueryLoader + usePreloadedQuery).
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
This commit is contained in:
@@ -15,16 +15,38 @@
|
||||
import { faviconUrl } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Avatar, Badge, Button, Field, IconCrossLargeX, Option, Select } from "@probo/ui";
|
||||
import { type ComponentProps, Suspense, useState } from "react";
|
||||
import { type ComponentProps, Suspense, useEffect, useState } from "react";
|
||||
import { type Control, Controller, type FieldValues, type Path } from "react-hook-form";
|
||||
import { type PreloadedQuery, usePreloadedQuery, useQueryLoader } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
import { useThirdParties } from "#/hooks/graph/ThirdPartyGraph";
|
||||
import type { ThirdPartiesMultiSelectFieldQuery } from "#/__generated__/core/ThirdPartiesMultiSelectFieldQuery.graphql";
|
||||
|
||||
const thirdPartiesQuery = graphql`
|
||||
query ThirdPartiesMultiSelectFieldQuery($organizationId: ID!) {
|
||||
organization: node(id: $organizationId) {
|
||||
... on Organization {
|
||||
thirdParties(
|
||||
first: 100
|
||||
orderBy: { direction: ASC, field: NAME }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
websiteUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type ThirdParty = {
|
||||
id: string;
|
||||
name: string;
|
||||
websiteUrl: string | null | undefined;
|
||||
level?: number;
|
||||
};
|
||||
|
||||
type Props<T extends FieldValues = FieldValues> = {
|
||||
@@ -42,29 +64,47 @@ export function ThirdPartiesMultiSelectField<T extends FieldValues = FieldValues
|
||||
selectedThirdParties = [],
|
||||
...props
|
||||
}: Props<T>) {
|
||||
const [queryRef, loadQuery]
|
||||
= useQueryLoader<ThirdPartiesMultiSelectFieldQuery>(thirdPartiesQuery);
|
||||
|
||||
useEffect(() => {
|
||||
loadQuery({ organizationId }, { fetchPolicy: "network-only" });
|
||||
}, [loadQuery, organizationId]);
|
||||
|
||||
const loadingState = (
|
||||
<Select variant="editor" disabled placeholder="Loading..." />
|
||||
);
|
||||
|
||||
return (
|
||||
<Field {...props}>
|
||||
<Suspense
|
||||
fallback={<Select variant="editor" disabled placeholder="Loading..." />}
|
||||
>
|
||||
<ThirdPartiesMultiSelectWithQuery
|
||||
organizationId={organizationId}
|
||||
control={control}
|
||||
name={props.name}
|
||||
disabled={props.disabled}
|
||||
selectedThirdParties={selectedThirdParties}
|
||||
/>
|
||||
</Suspense>
|
||||
{queryRef
|
||||
? (
|
||||
<Suspense fallback={loadingState}>
|
||||
<ThirdPartiesMultiSelectWithQuery
|
||||
queryRef={queryRef}
|
||||
control={control}
|
||||
name={props.name}
|
||||
disabled={props.disabled}
|
||||
selectedThirdParties={selectedThirdParties}
|
||||
/>
|
||||
</Suspense>
|
||||
)
|
||||
: (
|
||||
loadingState
|
||||
)}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
function ThirdPartiesMultiSelectWithQuery<T extends FieldValues = FieldValues>(
|
||||
props: Pick<Props<T>, "organizationId" | "control" | "name" | "disabled" | "selectedThirdParties">,
|
||||
props: Pick<Props<T>, "control" | "name" | "disabled" | "selectedThirdParties"> & {
|
||||
queryRef: PreloadedQuery<ThirdPartiesMultiSelectFieldQuery>;
|
||||
},
|
||||
) {
|
||||
const { __ } = useTranslate();
|
||||
const { name, organizationId, control, selectedThirdParties = [] } = props;
|
||||
const thirdParties = useThirdParties(organizationId);
|
||||
const { name, control, selectedThirdParties = [] } = props;
|
||||
const data = usePreloadedQuery<ThirdPartiesMultiSelectFieldQuery>(thirdPartiesQuery, props.queryRef);
|
||||
const thirdParties = data.organization?.thirdParties?.edges.map(edge => edge.node) ?? [];
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const allThirdParties: ThirdParty[] = [...thirdParties];
|
||||
|
||||
@@ -14,10 +14,31 @@
|
||||
|
||||
import { faviconUrl } from "@probo/helpers";
|
||||
import { Avatar, Badge, IconCrossLargeX } from "@probo/ui";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
import type { ThirdPartyGraphSelectQuery } from "#/__generated__/core/ThirdPartyGraphSelectQuery.graphql";
|
||||
import type { ThirdPartiesCellQuery } from "#/__generated__/core/ThirdPartiesCellQuery.graphql";
|
||||
import { GraphQLCell } from "#/components/table/GraphQLCell";
|
||||
import { thirdPartiesSelectQuery } from "#/hooks/graph/ThirdPartyGraph";
|
||||
|
||||
const thirdPartiesCellQuery = graphql`
|
||||
query ThirdPartiesCellQuery($organizationId: ID!) {
|
||||
organization: node(id: $organizationId) {
|
||||
... on Organization {
|
||||
thirdParties(
|
||||
first: 100
|
||||
orderBy: { direction: ASC, field: NAME }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
websiteUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type ThirdParty = {
|
||||
id: string;
|
||||
@@ -35,15 +56,19 @@ const empty = [] as ThirdParty[];
|
||||
|
||||
export function ThirdPartiesCell(props: Props) {
|
||||
return (
|
||||
<GraphQLCell<ThirdPartyGraphSelectQuery, ThirdParty>
|
||||
<GraphQLCell<ThirdPartiesCellQuery, ThirdParty>
|
||||
multiple
|
||||
name={props.name}
|
||||
query={thirdPartiesSelectQuery}
|
||||
query={thirdPartiesCellQuery}
|
||||
variables={{
|
||||
organizationId: props.organizationId,
|
||||
}}
|
||||
items={data =>
|
||||
data.organization?.thirdParties?.edges?.map(edge => edge.node) ?? []}
|
||||
data.organization?.thirdParties?.edges?.map(edge => ({
|
||||
id: edge.node.id,
|
||||
name: edge.node.name,
|
||||
websiteUrl: edge.node.websiteUrl,
|
||||
})) ?? []}
|
||||
itemRenderer={({ item, onRemove }) => (
|
||||
<ThirdPartyBadge thirdParty={item} onRemove={onRemove} />
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user