Promote useMutation to the @probo/relay package

Extract the awaitable useMutation into @probo/relay as a
createUseMutation factory that delegates feedback to an injected
MutationNotifier, keeping the package free of UI and i18n
dependencies. compliance-portal binds it to its Base UI toast +
i18next + formatError stack and imports it by explicit path
(#/lib/relay/useMutation), dropping the lone intra-app barrel; a
compliance-portal-scoped no-restricted-imports rule forbids
react-relay's useMutation.

Bring packages/relay and packages/routes into the shared ESLint
scope and fix the violations that surfaced, and deprecate the
legacy withQueryRef / loaderFromQueryLoader helpers. Document the
shared-hook pattern and the "index.ts for package entrypoints only"
rule in the relay, hooks, and app-arborescence guides.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-06-24 10:28:57 +02:00
parent e93faf4caa
commit ff966b462e
14 changed files with 684 additions and 122 deletions

View File

@@ -10,12 +10,16 @@
"preview": "vite preview"
},
"dependencies": {
"@base-ui/react": "^1.6.0",
"@probo/helpers": "1.0.0",
"@probo/react-lazy": "1.0.0",
"@probo/relay": "1.0.0",
"@probo/routes": "1.0.0",
"@probo/ui": "1.0.0",
"clsx": "^2.1.1",
"i18next": "^26.3.1",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-i18next": "^17.0.8",
"react-relay": "^21.0.1",
"react-router": "^8.0.0",
"relay-runtime": "^21.0.1"

View File

@@ -0,0 +1,53 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { Toast } from "@base-ui/react/toast";
import { formatError, type GraphQLError } from "@probo/helpers";
import { createUseMutation, type MutationNotifier } from "@probo/relay";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
/**
* Binds the shared awaitable useMutation (`@probo/relay`) to this app's
* feedback stack: Base UI toasts, i18next titles, and `formatError`
* descriptions. This is the only place those opinions are wired.
*
* Always import useMutation from `#/lib/relay/useMutation` — never useMutation
* from react-relay.
*/
function useMutationNotifier(): MutationNotifier {
const toast = Toast.useToastManager();
const { t } = useTranslation();
return useMemo<MutationNotifier>(
() => ({
notifySuccess: (title) => {
toast.add({ title, type: "success" });
},
notifyError: (error, title) => {
const finalTitle = title ?? t("common.error");
toast.add({
title: finalTitle,
description: formatError(finalTitle, error as GraphQLError),
type: "error",
});
},
}),
[toast, t],
);
}
export type { MutationFeedback } from "@probo/relay";
export const useMutation = createUseMutation(useMutationNotifier);