Add Import action for catalog vendors in trackers

A tracker pattern resolved only to a catalog vendor now shows an "Import
to third parties" action in its row menu. It calls
importThirdPartyFromCommon for the pattern's common third party and, on
success, links the imported org vendor onto the row so it stops showing
the "Common catalog" badge.

Sibling patterns of the same vendor are backfilled server-side and pick
up the link on the next fetch of the list, so the store update only
needs to reflect the clicked row.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-06-10 15:39:33 +02:00
parent e2d6972da5
commit 675fcc4824

View File

@@ -12,7 +12,7 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { EyeIcon, EyeSlashIcon } from "@phosphor-icons/react";
import { DownloadSimpleIcon, EyeIcon, EyeSlashIcon } from "@phosphor-icons/react";
import { formatError, getTrackerSourceBadge, getTrackerTypeBadge, type GraphQLError, humanizeSeconds } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import {
@@ -32,8 +32,10 @@ import { ConnectionHandler } from "relay-runtime";
import type { TrackerPatternRowDeleteMutation } from "#/__generated__/core/TrackerPatternRowDeleteMutation.graphql";
import type { TrackerPatternRowFragment$key } from "#/__generated__/core/TrackerPatternRowFragment.graphql";
import type { TrackerPatternRowImportMutation } from "#/__generated__/core/TrackerPatternRowImportMutation.graphql";
import type { TrackerPatternRowMoveMutation } from "#/__generated__/core/TrackerPatternRowMoveMutation.graphql";
import type { TrackerPatternRowUpdateMutation } from "#/__generated__/core/TrackerPatternRowUpdateMutation.graphql";
import { useOrganizationId } from "#/hooks/useOrganizationId";
import { MoveToCategorySelect } from "./MoveToCategorySelect";
import { TrackerPatternRowEdit } from "./TrackerPatternRowEdit";
@@ -130,6 +132,22 @@ const updatePatternMutation = graphql`
}
`;
const importThirdPartyMutation = graphql`
mutation TrackerPatternRowImportMutation(
$input: ImportThirdPartyFromCommonInput!
) {
importThirdPartyFromCommon(input: $input) {
created
thirdPartyEdge {
node {
id
name
}
}
}
}
`;
interface TrackerPatternRowProps {
patternKey: TrackerPatternRowFragment$key;
connectionId: string;
@@ -139,6 +157,7 @@ export function TrackerPatternRow({ patternKey, connectionId }: TrackerPatternRo
const { __ } = useTranslate();
const { toast } = useToast();
const confirm = useConfirm();
const organizationId = useOrganizationId();
const pattern = useFragment(trackerPatternFragment, patternKey);
const [isEditing, setIsEditing] = useState(false);
@@ -149,6 +168,8 @@ export function TrackerPatternRow({ patternKey, connectionId }: TrackerPatternRo
= useMutation<TrackerPatternRowMoveMutation>(movePatternMutation);
const [updatePattern, isUpdating]
= useMutation<TrackerPatternRowUpdateMutation>(updatePatternMutation);
const [importThirdParty, isImporting]
= useMutation<TrackerPatternRowImportMutation>(importThirdPartyMutation);
const handleDelete = () => {
confirm(
@@ -249,6 +270,49 @@ export function TrackerPatternRow({ patternKey, connectionId }: TrackerPatternRo
});
};
const handleImport = () => {
const commonThirdParty = pattern.commonThirdParty;
if (!commonThirdParty || isImporting) {
return;
}
importThirdParty({
variables: {
input: {
organizationId,
commonThirdPartyId: commonThirdParty.id,
},
},
updater(store) {
const node = store
.getRootField("importThirdPartyFromCommon")
?.getLinkedRecord("thirdPartyEdge")
?.getLinkedRecord("node");
if (!node) {
return;
}
// Reflect the import on the clicked pattern immediately. Sibling
// patterns of the same vendor are backfilled server-side and pick
// up the link on the next fetch of the list.
const patternRecord = store.get(pattern.id);
if (patternRecord) {
patternRecord.setLinkedRecord(node, "thirdParty");
}
},
onCompleted(_, errors) {
if (errors?.length) {
toast({ title: __("Error"), description: errors[0].message, variant: "error" });
return;
}
toast({ title: __("Success"), description: __("Third party imported"), variant: "success" });
},
onError(error) {
toast({ title: __("Error"), description: formatError(__("Failed to import third party"), error as GraphQLError), variant: "error" });
},
});
};
const handleSaveEdit = (data: { description: string; maxAgeSeconds: number | null }) => {
updatePattern({
variables: {
@@ -352,6 +416,14 @@ export function TrackerPatternRow({ patternKey, connectionId }: TrackerPatternRo
<IconPencil size={14} />
</button>
<ActionDropdown>
{!pattern.thirdParty && pattern.commonThirdParty && (
<DropdownItem
icon={DownloadSimpleIcon}
onSelect={handleImport}
>
{__("Import to third parties")}
</DropdownItem>
)}
<DropdownItem
icon={pattern.excluded ? EyeIcon : EyeSlashIcon}
onSelect={handleToggleExcluded}