Add react i18next to console
Signed-off-by: Jonathan <contact@grafikart.fr>
This commit is contained in:
@@ -20,7 +20,7 @@
|
|||||||
"pdfjs-dist": "^5.4.296",
|
"pdfjs-dist": "^5.4.296",
|
||||||
"react": "^19.2.7",
|
"react": "^19.2.7",
|
||||||
"react-dom": "^19.2.7",
|
"react-dom": "^19.2.7",
|
||||||
"react-i18next": "^17.0.8",
|
"react-i18next": "^17.0.10",
|
||||||
"react-pdf": "^10.3.0",
|
"react-pdf": "^10.3.0",
|
||||||
"react-relay": "^21.0.1",
|
"react-relay": "^21.0.1",
|
||||||
"react-router": "^8.1.0",
|
"react-router": "^8.1.0",
|
||||||
|
|||||||
1
apps/console/.gitignore
vendored
1
apps/console/.gitignore
vendored
@@ -11,6 +11,7 @@ lerna-debug.log*
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
|
scripts
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
|
|||||||
@@ -22,11 +22,13 @@
|
|||||||
"@probo/ui": "1.0.0",
|
"@probo/ui": "1.0.0",
|
||||||
"@tanstack/react-query": "^5.76.1",
|
"@tanstack/react-query": "^5.76.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"i18next": "^26.3.6",
|
||||||
"react": "^19.2.7",
|
"react": "^19.2.7",
|
||||||
"react-dom": "^19.2.7",
|
"react-dom": "^19.2.7",
|
||||||
"react-dropzone": "^15.0.0",
|
"react-dropzone": "^15.0.0",
|
||||||
"react-error-boundary": "^6.0.0",
|
"react-error-boundary": "^6.0.0",
|
||||||
"react-hook-form": "^7.56.4",
|
"react-hook-form": "^7.56.4",
|
||||||
|
"react-i18next": "^17.0.10",
|
||||||
"react-pdf": "^10.3.0",
|
"react-pdf": "^10.3.0",
|
||||||
"react-relay": "^21.0.1",
|
"react-relay": "^21.0.1",
|
||||||
"react-router": "^8.1.0",
|
"react-router": "^8.1.0",
|
||||||
|
|||||||
2469
apps/console/src/_locales/en-US.json
Normal file
2469
apps/console/src/_locales/en-US.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,6 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ErrorDetailMessage,
|
ErrorDetailMessage,
|
||||||
@@ -26,6 +25,7 @@ import {
|
|||||||
ErrorLayout,
|
ErrorLayout,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link, useLocation, useRouteError } from "react-router";
|
import { Link, useLocation, useRouteError } from "react-router";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -36,7 +36,7 @@ type Props = {
|
|||||||
export function PageError({ resetErrorBoundary, error: propsError }: Props) {
|
export function PageError({ resetErrorBoundary, error: propsError }: Props) {
|
||||||
const routeError = useRouteError();
|
const routeError = useRouteError();
|
||||||
const error = routeError ?? propsError;
|
const error = routeError ?? propsError;
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const baseLocation = useRef(location);
|
const baseLocation = useRef(location);
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) {
|
|||||||
|
|
||||||
const actions = (
|
const actions = (
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link to="/">{__("Go home")}</Link>
|
<Link to="/">{t("pageError.actions.goHome")}</Link>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -70,8 +70,8 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) {
|
|||||||
return (
|
return (
|
||||||
<ErrorLayout
|
<ErrorLayout
|
||||||
{...layoutProps}
|
{...layoutProps}
|
||||||
title={__("Page not found")}
|
title={t("pageError.notFound.title")}
|
||||||
description={__("The page you are looking for does not exist.")}
|
description={t("pageError.notFound.description")}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -80,8 +80,8 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) {
|
|||||||
return (
|
return (
|
||||||
<ErrorLayout
|
<ErrorLayout
|
||||||
{...layoutProps}
|
{...layoutProps}
|
||||||
title={__("Page not found")}
|
title={t("pageError.notFound.title")}
|
||||||
description={__("The page you are looking for does not exist.")}
|
description={t("pageError.notFound.description")}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -89,11 +89,11 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) {
|
|||||||
return (
|
return (
|
||||||
<ErrorLayout
|
<ErrorLayout
|
||||||
{...layoutProps}
|
{...layoutProps}
|
||||||
title={__("Something went wrong")}
|
title={t("pageError.unexpected.title")}
|
||||||
description={__("We hit an unexpected error. Head back home to continue.")}
|
description={t("pageError.unexpected.description")}
|
||||||
>
|
>
|
||||||
{error instanceof Error && (
|
{error instanceof Error && (
|
||||||
<ErrorDetails summary={__("Technical details")}>
|
<ErrorDetails summary={t("pageError.technicalDetails")}>
|
||||||
<ErrorDetailMessage>{error.message}</ErrorDetailMessage>
|
<ErrorDetailMessage>{error.message}</ErrorDetailMessage>
|
||||||
</ErrorDetails>
|
</ErrorDetails>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
IconChevronDown,
|
IconChevronDown,
|
||||||
@@ -35,6 +34,7 @@ import {
|
|||||||
useContext,
|
useContext,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import type { LoadMoreFn } from "react-relay";
|
import type { LoadMoreFn } from "react-relay";
|
||||||
import type { OperationType } from "relay-runtime";
|
import type { OperationType } from "relay-runtime";
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ export function SortableTable({
|
|||||||
isLoadingNext?: boolean;
|
isLoadingNext?: boolean;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
}) {
|
}) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const [order, setOrder] = useState(defaultOrder);
|
const [order, setOrder] = useState(defaultOrder);
|
||||||
const changeOrder = (o: Order) => {
|
const changeOrder = (o: Order) => {
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
@@ -95,7 +95,7 @@ export function SortableTable({
|
|||||||
disabled={isLoadingNext}
|
disabled={isLoadingNext}
|
||||||
icon={isLoadingNext ? Spinner : IconChevronDown}
|
icon={isLoadingNext ? Spinner : IconChevronDown}
|
||||||
>
|
>
|
||||||
{__("Show more")}
|
{t("sortableTable.actions.showMore")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,9 +21,7 @@
|
|||||||
import {
|
import {
|
||||||
getAssetTypeVariant,
|
getAssetTypeVariant,
|
||||||
promisifyMutation,
|
promisifyMutation,
|
||||||
sprintf,
|
|
||||||
} from "@probo/helpers";
|
} from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
ActionDropdown,
|
ActionDropdown,
|
||||||
Badge,
|
Badge,
|
||||||
@@ -34,6 +32,7 @@ import {
|
|||||||
TextCell,
|
TextCell,
|
||||||
useConfirm,
|
useConfirm,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMutation } from "react-relay";
|
import { useMutation } from "react-relay";
|
||||||
import type { usePaginationFragmentHookType } from "react-relay/relay-hooks/usePaginationFragment";
|
import type { usePaginationFragmentHookType } from "react-relay/relay-hooks/usePaginationFragment";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
@@ -90,7 +89,7 @@ export function AssetsTable(props: Props) {
|
|||||||
const { connectionId, pagination, assets } = props;
|
const { connectionId, pagination, assets } = props;
|
||||||
|
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const deleteAsset = useDeleteAsset(connectionId);
|
const deleteAsset = useDeleteAsset(connectionId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -100,17 +99,17 @@ export function AssetsTable(props: Props) {
|
|||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
items={assets}
|
items={assets}
|
||||||
columns={[
|
columns={[
|
||||||
__("Name"),
|
t("assetsTable.columns.name"),
|
||||||
__("Type"),
|
t("assetsTable.columns.type"),
|
||||||
__("Data Types stored"),
|
t("assetsTable.columns.dataTypesStored"),
|
||||||
__("Amount"),
|
t("assetsTable.columns.amount"),
|
||||||
__("Owner"),
|
t("assetsTable.columns.owner"),
|
||||||
__("Third parties"),
|
t("assetsTable.columns.thirdParties"),
|
||||||
]}
|
]}
|
||||||
schema={schema}
|
schema={schema}
|
||||||
updateMutation={updateAssetMutation}
|
updateMutation={updateAssetMutation}
|
||||||
createMutation={createAssetMutation}
|
createMutation={createAssetMutation}
|
||||||
addLabel={__("Add a new asset")}
|
addLabel={t("assetsTable.actions.add")}
|
||||||
defaultValue={{
|
defaultValue={{
|
||||||
...defaultValue,
|
...defaultValue,
|
||||||
organizationId,
|
organizationId,
|
||||||
@@ -120,7 +119,7 @@ export function AssetsTable(props: Props) {
|
|||||||
<DropdownItem asChild>
|
<DropdownItem asChild>
|
||||||
<Link to={`/organizations/${organizationId}/assets/${item.id}`}>
|
<Link to={`/organizations/${organizationId}/assets/${item.id}`}>
|
||||||
<IconPencil size={16} />
|
<IconPencil size={16} />
|
||||||
{__("Edit")}
|
{t("assetsTable.actions.edit")}
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
@@ -128,7 +127,7 @@ export function AssetsTable(props: Props) {
|
|||||||
variant="danger"
|
variant="danger"
|
||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
>
|
>
|
||||||
{__("Delete")}
|
{t("assetsTable.actions.delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
)}
|
)}
|
||||||
@@ -140,7 +139,9 @@ export function AssetsTable(props: Props) {
|
|||||||
items={["VIRTUAL", "PHYSICAL"]}
|
items={["VIRTUAL", "PHYSICAL"]}
|
||||||
itemRenderer={({ item }) => (
|
itemRenderer={({ item }) => (
|
||||||
<Badge variant={getAssetTypeVariant(item ?? "VIRTUAL")}>
|
<Badge variant={getAssetTypeVariant(item ?? "VIRTUAL")}>
|
||||||
{item === "PHYSICAL" ? __("Physical") : __("Virtual")}
|
{item === "PHYSICAL"
|
||||||
|
? t("assetsTable.assetTypes.physical")
|
||||||
|
: t("assetsTable.assetTypes.virtual")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
defaultValue={item?.assetType ?? defaultValue.assetType}
|
defaultValue={item?.assetType ?? defaultValue.assetType}
|
||||||
@@ -174,11 +175,11 @@ export function AssetsTable(props: Props) {
|
|||||||
const useDeleteAsset = (connectionId: string) => {
|
const useDeleteAsset = (connectionId: string) => {
|
||||||
const [mutate] = useMutation<AssetGraphDeleteMutation>(deleteAssetMutation);
|
const [mutate] = useMutation<AssetGraphDeleteMutation>(deleteAssetMutation);
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (asset: { id: string; name: string }) => {
|
return (asset: { id: string; name: string }) => {
|
||||||
if (!asset.id || !asset.name) {
|
if (!asset.id || !asset.name) {
|
||||||
return alert(__("Failed to delete asset: missing id or name"));
|
return alert(t("assetsTable.delete.missingIdOrName"));
|
||||||
}
|
}
|
||||||
confirm(
|
confirm(
|
||||||
() =>
|
() =>
|
||||||
@@ -191,12 +192,7 @@ const useDeleteAsset = (connectionId: string) => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
message: sprintf(
|
message: t("assetsTable.delete.confirmation", { name: asset.name }),
|
||||||
__(
|
|
||||||
"This will permanently delete \"%s\". This action cannot be undone.",
|
|
||||||
),
|
|
||||||
asset.name,
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,8 +19,8 @@
|
|||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { faviconUrl, getAssetTypeVariant } from "@probo/helpers";
|
import { faviconUrl, getAssetTypeVariant } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Avatar, Badge, Tbody, Td, Th, Thead, Tr } from "@probo/ui";
|
import { Avatar, Badge, Tbody, Td, Th, Thead, Tr } from "@probo/ui";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import type { usePaginationFragmentHookType } from "react-relay/relay-hooks/usePaginationFragment";
|
import type { usePaginationFragmentHookType } from "react-relay/relay-hooks/usePaginationFragment";
|
||||||
import type { OperationType } from "relay-runtime";
|
import type { OperationType } from "relay-runtime";
|
||||||
|
|
||||||
@@ -46,17 +46,17 @@ type Props = {
|
|||||||
|
|
||||||
export function ReadOnlyAssetsTable(props: Props) {
|
export function ReadOnlyAssetsTable(props: Props) {
|
||||||
const { pagination, assets } = props;
|
const { pagination, assets } = props;
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SortableTable {...pagination} pageSize={10}>
|
<SortableTable {...pagination} pageSize={10}>
|
||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>{__("Name")}</Th>
|
<Th>{t("readOnlyAssetsTable.columns.name")}</Th>
|
||||||
<Th>{__("Type")}</Th>
|
<Th>{t("readOnlyAssetsTable.columns.type")}</Th>
|
||||||
<Th>{__("Amount")}</Th>
|
<Th>{t("readOnlyAssetsTable.columns.amount")}</Th>
|
||||||
<Th>{__("Owner")}</Th>
|
<Th>{t("readOnlyAssetsTable.columns.owner")}</Th>
|
||||||
<Th>{__("Third parties")}</Th>
|
<Th>{t("readOnlyAssetsTable.columns.thirdParties")}</Th>
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
@@ -70,7 +70,7 @@ export function ReadOnlyAssetsTable(props: Props) {
|
|||||||
|
|
||||||
function AssetRow({ entry }: { entry: AssetEntry }) {
|
function AssetRow({ entry }: { entry: AssetEntry }) {
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const thirdParties = entry.thirdParties?.edges.map(edge => edge.node) ?? [];
|
const thirdParties = entry.thirdParties?.edges.map(edge => edge.node) ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -78,11 +78,13 @@ function AssetRow({ entry }: { entry: AssetEntry }) {
|
|||||||
<Td>{entry.name}</Td>
|
<Td>{entry.name}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<Badge variant={getAssetTypeVariant(entry.assetType)}>
|
<Badge variant={getAssetTypeVariant(entry.assetType)}>
|
||||||
{entry.assetType === "PHYSICAL" ? __("Physical") : __("Virtual")}
|
{entry.assetType === "PHYSICAL"
|
||||||
|
? t("readOnlyAssetsTable.assetTypes.physical")
|
||||||
|
: t("readOnlyAssetsTable.assetTypes.virtual")}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>{entry.amount}</Td>
|
<Td>{entry.amount}</Td>
|
||||||
<Td>{entry.owner?.fullName ?? __("Unassigned")}</Td>
|
<Td>{entry.owner?.fullName ?? t("readOnlyAssetsTable.unassigned")}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
{thirdParties.length > 0
|
{thirdParties.length > 0
|
||||||
? (
|
? (
|
||||||
@@ -110,7 +112,7 @@ function AssetRow({ entry }: { entry: AssetEntry }) {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<span className="text-txt-secondary text-sm">{__("None")}</span>
|
<span className="text-txt-secondary text-sm">{t("readOnlyAssetsTable.none")}</span>
|
||||||
)}
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
|
|||||||
@@ -18,8 +18,7 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { getAuditStateVariant, sprintf } from "@probo/helpers";
|
import { getAuditStateVariant } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
@@ -37,6 +36,7 @@ import {
|
|||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { clsx } from "clsx";
|
import { clsx } from "clsx";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFragment } from "react-relay";
|
import { useFragment } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ type Props<Params> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function LinkedAuditsCard<Params>(props: Props<Params>) {
|
export function LinkedAuditsCard<Params>(props: Props<Params>) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const [limit, setLimit] = useState<number | null>(4);
|
const [limit, setLimit] = useState<number | null>(4);
|
||||||
const audits = useMemo(() => {
|
const audits = useMemo(() => {
|
||||||
return limit ? props.audits.slice(0, limit) : props.audits;
|
return limit ? props.audits.slice(0, limit) : props.audits;
|
||||||
@@ -116,7 +116,7 @@ export function LinkedAuditsCard<Params>(props: Props<Params>) {
|
|||||||
<Wrapper padded className="space-y-[10px]">
|
<Wrapper padded className="space-y-[10px]">
|
||||||
{variant === "card" && (
|
{variant === "card" && (
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<div className="text-lg font-semibold">{__("Audits")}</div>
|
<div className="text-lg font-semibold">{t("linkedAuditsCard.title")}</div>
|
||||||
{!props.readOnly && (
|
{!props.readOnly && (
|
||||||
<LinkedAuditsDialog
|
<LinkedAuditsDialog
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
@@ -125,7 +125,7 @@ export function LinkedAuditsCard<Params>(props: Props<Params>) {
|
|||||||
onUnlink={onDetach}
|
onUnlink={onDetach}
|
||||||
>
|
>
|
||||||
<Button variant="tertiary" icon={IconPlusLarge}>
|
<Button variant="tertiary" icon={IconPlusLarge}>
|
||||||
{__("Link audit")}
|
{t("linkedAuditsCard.actions.link")}
|
||||||
</Button>
|
</Button>
|
||||||
</LinkedAuditsDialog>
|
</LinkedAuditsDialog>
|
||||||
)}
|
)}
|
||||||
@@ -134,8 +134,8 @@ export function LinkedAuditsCard<Params>(props: Props<Params>) {
|
|||||||
<Table className={clsx(variant === "card" && "bg-invert")}>
|
<Table className={clsx(variant === "card" && "bg-invert")}>
|
||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>{__("Name")}</Th>
|
<Th>{t("linkedAuditsCard.columns.name")}</Th>
|
||||||
<Th>{__("State")}</Th>
|
<Th>{t("linkedAuditsCard.columns.state")}</Th>
|
||||||
{!props.readOnly && <Th></Th>}
|
{!props.readOnly && <Th></Th>}
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
@@ -146,7 +146,7 @@ export function LinkedAuditsCard<Params>(props: Props<Params>) {
|
|||||||
colSpan={props.readOnly ? 2 : 3}
|
colSpan={props.readOnly ? 2 : 3}
|
||||||
className="text-center text-txt-secondary"
|
className="text-center text-txt-secondary"
|
||||||
>
|
>
|
||||||
{__("No audits linked")}
|
{t("linkedAuditsCard.empty")}
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
)}
|
||||||
@@ -166,7 +166,7 @@ export function LinkedAuditsCard<Params>(props: Props<Params>) {
|
|||||||
onUnlink={onDetach}
|
onUnlink={onDetach}
|
||||||
>
|
>
|
||||||
<TrButton colspan={3} icon={IconPlusLarge}>
|
<TrButton colspan={3} icon={IconPlusLarge}>
|
||||||
{__("Link audit")}
|
{t("linkedAuditsCard.actions.link")}
|
||||||
</TrButton>
|
</TrButton>
|
||||||
</LinkedAuditsDialog>
|
</LinkedAuditsDialog>
|
||||||
)}
|
)}
|
||||||
@@ -179,7 +179,9 @@ export function LinkedAuditsCard<Params>(props: Props<Params>) {
|
|||||||
className="mt-3 mx-auto"
|
className="mt-3 mx-auto"
|
||||||
icon={IconChevronDown}
|
icon={IconChevronDown}
|
||||||
>
|
>
|
||||||
{sprintf(__("Show %s more"), props.audits.length - limit)}
|
{t("linkedAuditsCard.actions.showMore", {
|
||||||
|
count: props.audits.length - limit,
|
||||||
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
@@ -193,7 +195,7 @@ function AuditRow(props: {
|
|||||||
}) {
|
}) {
|
||||||
const audit = useFragment(linkedAuditFragment, props.audit);
|
const audit = useFragment(linkedAuditFragment, props.audit);
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tr to={`/organizations/${organizationId}/audits/${audit.id}`}>
|
<Tr to={`/organizations/${organizationId}/audits/${audit.id}`}>
|
||||||
@@ -217,7 +219,7 @@ function AuditRow(props: {
|
|||||||
onClick={() => props.onClick(audit.id)}
|
onClick={() => props.onClick(audit.id)}
|
||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
>
|
>
|
||||||
{__("Unlink")}
|
{t("linkedAuditsCard.actions.unlink")}
|
||||||
</Button>
|
</Button>
|
||||||
</Td>
|
</Td>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { getAuditStateVariant } from "@probo/helpers";
|
import { getAuditStateVariant } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
@@ -34,6 +33,7 @@ import {
|
|||||||
Spinner,
|
Spinner,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { type ReactNode, Suspense, useMemo, useState } from "react";
|
import { type ReactNode, Suspense, useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useLazyLoadQuery, usePaginationFragment } from "react-relay";
|
import { useLazyLoadQuery, usePaginationFragment } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
@@ -98,16 +98,16 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function LinkedAuditsDialog({ children, ...props }: Props) {
|
export function LinkedAuditsDialog({ children, ...props }: Props) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog trigger={children} title={__("Link audits")}>
|
<Dialog trigger={children} title={t("linkedAuditsDialog.title")}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Suspense fallback={<Spinner centered />}>
|
<Suspense fallback={<Spinner centered />}>
|
||||||
<LinkedAuditsDialogContent {...props} />
|
<LinkedAuditsDialogContent {...props} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogFooter exitLabel={__("Close")} />
|
<DialogFooter exitLabel={t("linkedAuditsDialog.actions.close")} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -122,7 +122,7 @@ function LinkedAuditsDialogContent(props: Omit<Props, "children">) {
|
|||||||
auditsFragment,
|
auditsFragment,
|
||||||
query.organization as LinkedAuditsDialogFragment$key,
|
query.organization as LinkedAuditsDialogFragment$key,
|
||||||
);
|
);
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const audits = useMemo(
|
const audits = useMemo(
|
||||||
() => data.audits?.edges?.map(edge => edge.node) ?? [],
|
() => data.audits?.edges?.map(edge => edge.node) ?? [],
|
||||||
@@ -143,7 +143,7 @@ function LinkedAuditsDialogContent(props: Omit<Props, "children">) {
|
|||||||
<div className="flex items-center gap-2 sticky top-0 relative py-4 bg-linear-to-b from-50% from-level-2 to-level-2/0 px-6">
|
<div className="flex items-center gap-2 sticky top-0 relative py-4 bg-linear-to-b from-50% from-level-2 to-level-2/0 px-6">
|
||||||
<Input
|
<Input
|
||||||
icon={IconMagnifyingGlass}
|
icon={IconMagnifyingGlass}
|
||||||
placeholder={__("Search audits...")}
|
placeholder={t("linkedAuditsDialog.searchPlaceholder")}
|
||||||
onValueChange={setSearch}
|
onValueChange={setSearch}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -180,7 +180,7 @@ type RowProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function AuditRow(props: RowProps) {
|
function AuditRow(props: RowProps) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const isLinked = props.linkedAudits.has(props.audit.id);
|
const isLinked = props.linkedAudits.has(props.audit.id);
|
||||||
const onClick = isLinked ? props.onUnlink : props.onLink;
|
const onClick = isLinked ? props.onUnlink : props.onLink;
|
||||||
@@ -209,7 +209,9 @@ function AuditRow(props: RowProps) {
|
|||||||
<span>
|
<span>
|
||||||
<IconComponent size={16} />
|
<IconComponent size={16} />
|
||||||
{" "}
|
{" "}
|
||||||
{isLinked ? __("Unlink") : __("Link")}
|
{isLinked
|
||||||
|
? t("linkedAuditsDialog.actions.unlink")
|
||||||
|
: t("linkedAuditsDialog.actions.link")}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { acceptImage } from "@probo/helpers";
|
import { acceptImage } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -32,6 +31,7 @@ import {
|
|||||||
useDialogRef,
|
useDialogRef,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { forwardRef, type ReactNode, useImperativeHandle, useState } from "react";
|
import { forwardRef, type ReactNode, useImperativeHandle, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import type { CompliancePageReferenceListItemFragment$data } from "#/__generated__/core/CompliancePageReferenceListItemFragment.graphql";
|
import type { CompliancePageReferenceListItemFragment$data } from "#/__generated__/core/CompliancePageReferenceListItemFragment.graphql";
|
||||||
@@ -57,7 +57,7 @@ export type CompliancePageReferenceDialogRef = {
|
|||||||
|
|
||||||
export const CompliancePageReferenceDialog = forwardRef<CompliancePageReferenceDialogRef, { children?: ReactNode }>(
|
export const CompliancePageReferenceDialog = forwardRef<CompliancePageReferenceDialogRef, { children?: ReactNode }>(
|
||||||
function CompliancePageReferenceDialog({ children }, ref) {
|
function CompliancePageReferenceDialog({ children }, ref) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const dialogRef = useDialogRef();
|
const dialogRef = useDialogRef();
|
||||||
const [mode, setMode] = useState<"create" | "edit">("create");
|
const [mode, setMode] = useState<"create" | "edit">("create");
|
||||||
const [compliancePageId, setCompliancePageId] = useState<string>("");
|
const [compliancePageId, setCompliancePageId] = useState<string>("");
|
||||||
@@ -182,7 +182,7 @@ export const CompliancePageReferenceDialog = forwardRef<CompliancePageReferenceD
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isSubmitting = isCreating || isUpdating;
|
const isSubmitting = isCreating || isUpdating;
|
||||||
const title = mode === "create" ? __("Add Reference") : __("Edit Reference");
|
const title = mode === "create" ? t("trustCenterReferenceDialog.actions.add") : t("trustCenterReferenceDialog.actions.edit");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -202,45 +202,45 @@ export const CompliancePageReferenceDialog = forwardRef<CompliancePageReferenceD
|
|||||||
<DialogContent padded className="space-y-6">
|
<DialogContent padded className="space-y-6">
|
||||||
<Field
|
<Field
|
||||||
{...register("name")}
|
{...register("name")}
|
||||||
label={__("Reference Name")}
|
label={t("trustCenterReferenceDialog.fields.name.label")}
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
error={errors.name?.message}
|
error={errors.name?.message}
|
||||||
placeholder={__("Company or organization name")}
|
placeholder={t("trustCenterReferenceDialog.fields.name.placeholder")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Field label={__("Description")} error={errors.description?.message}>
|
<Field label={t("trustCenterReferenceDialog.fields.description.label")} error={errors.description?.message}>
|
||||||
<Textarea
|
<Textarea
|
||||||
{...register("description")}
|
{...register("description")}
|
||||||
placeholder={__("Brief description of the reference")}
|
placeholder={t("trustCenterReferenceDialog.fields.description.placeholder")}
|
||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
{...register("websiteUrl")}
|
{...register("websiteUrl")}
|
||||||
label={__("Website URL")}
|
label={t("trustCenterReferenceDialog.fields.websiteUrl.label")}
|
||||||
type="url"
|
type="url"
|
||||||
required
|
required
|
||||||
error={errors.websiteUrl?.message}
|
error={errors.websiteUrl?.message}
|
||||||
placeholder={__("https://example.com")}
|
placeholder={t("trustCenterReferenceDialog.fields.websiteUrl.placeholder")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{mode === "edit" && (
|
{mode === "edit" && (
|
||||||
<Field
|
<Field
|
||||||
{...register("rank", { valueAsNumber: true })}
|
{...register("rank", { valueAsNumber: true })}
|
||||||
label={__("Rank")}
|
label={t("trustCenterReferenceDialog.fields.rank.label")}
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
error={errors.rank?.message}
|
error={errors.rank?.message}
|
||||||
placeholder={__("Display order (1, 2, 3...)")}
|
placeholder={t("trustCenterReferenceDialog.fields.rank.placeholder")}
|
||||||
help={__("Lower numbers appear first")}
|
help={t("trustCenterReferenceDialog.fields.rank.help")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Field label={__("Logo")}>
|
<Field label={t("trustCenterReferenceDialog.fields.logo.label")}>
|
||||||
<Dropzone
|
<Dropzone
|
||||||
description={__("Upload logo image (PNG, JPG, WEBP, SVG up to 5MB)")}
|
description={t("trustCenterReferenceDialog.fields.logo.description")}
|
||||||
isUploading={isSubmitting}
|
isUploading={isSubmitting}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
accept={acceptImage}
|
accept={acceptImage}
|
||||||
@@ -249,7 +249,7 @@ export const CompliancePageReferenceDialog = forwardRef<CompliancePageReferenceD
|
|||||||
{uploadedFile && (
|
{uploadedFile && (
|
||||||
<div className="mt-2 p-3 bg-tertiary-subtle rounded-lg">
|
<div className="mt-2 p-3 bg-tertiary-subtle rounded-lg">
|
||||||
<p className="text-sm font-medium">
|
<p className="text-sm font-medium">
|
||||||
{__("Selected file")}
|
{t("trustCenterReferenceDialog.fields.logo.selectedFile")}
|
||||||
:
|
:
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-txt-secondary">{uploadedFile.name}</p>
|
<p className="text-sm text-txt-secondary">{uploadedFile.name}</p>
|
||||||
@@ -258,14 +258,14 @@ export const CompliancePageReferenceDialog = forwardRef<CompliancePageReferenceD
|
|||||||
{mode === "edit" && !uploadedFile && (
|
{mode === "edit" && !uploadedFile && (
|
||||||
<div className="mt-2 p-3 bg-tertiary-subtle rounded-lg">
|
<div className="mt-2 p-3 bg-tertiary-subtle rounded-lg">
|
||||||
<p className="text-sm text-txt-secondary">
|
<p className="text-sm text-txt-secondary">
|
||||||
{__("Current logo will be kept if no new file is uploaded")}
|
{t("trustCenterReferenceDialog.fields.logo.keepCurrent")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{mode === "create" && !uploadedFile && (
|
{mode === "create" && !uploadedFile && (
|
||||||
<div className="mt-2 p-3 bg-warning-subtle rounded-lg">
|
<div className="mt-2 p-3 bg-warning-subtle rounded-lg">
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
{__("Logo is required for new references")}
|
{t("trustCenterReferenceDialog.fields.logo.required")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -278,7 +278,7 @@ export const CompliancePageReferenceDialog = forwardRef<CompliancePageReferenceD
|
|||||||
disabled={isSubmitting || (mode === "create" && !uploadedFile)}
|
disabled={isSubmitting || (mode === "create" && !uploadedFile)}
|
||||||
icon={isSubmitting ? Spinner : undefined}
|
icon={isSubmitting ? Spinner : undefined}
|
||||||
>
|
>
|
||||||
{mode === "create" ? __("Add Reference") : __("Update Reference")}
|
{mode === "create" ? t("trustCenterReferenceDialog.actions.add") : t("trustCenterReferenceDialog.actions.update")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -18,8 +18,6 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { sprintf } from "@probo/helpers";
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -29,6 +27,7 @@ import {
|
|||||||
Spinner,
|
Spinner,
|
||||||
useDialogRef,
|
useDialogRef,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { useDeleteCompliancePageReferenceMutation } from "#/pages/organizations/compliance-page/_lib/compliancePageReferenceMutations";
|
import { useDeleteCompliancePageReferenceMutation } from "#/pages/organizations/compliance-page/_lib/compliancePageReferenceMutations";
|
||||||
|
|
||||||
@@ -47,7 +46,7 @@ export function DeleteCompliancePageReferenceDialog({
|
|||||||
connectionId,
|
connectionId,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const ref = useDialogRef();
|
const ref = useDialogRef();
|
||||||
|
|
||||||
const [deleteReference, isDeleting] = useDeleteCompliancePageReferenceMutation();
|
const [deleteReference, isDeleting] = useDeleteCompliancePageReferenceMutation();
|
||||||
@@ -70,18 +69,15 @@ export function DeleteCompliancePageReferenceDialog({
|
|||||||
<Dialog
|
<Dialog
|
||||||
ref={ref}
|
ref={ref}
|
||||||
trigger={children}
|
trigger={children}
|
||||||
title={__("Delete Reference")}
|
title={t("deleteTrustCenterReferenceDialog.title")}
|
||||||
className="max-w-md"
|
className="max-w-md"
|
||||||
>
|
>
|
||||||
<DialogContent padded>
|
<DialogContent padded>
|
||||||
<p className="text-txt-secondary">
|
<p className="text-txt-secondary">
|
||||||
{sprintf(
|
{t("deleteTrustCenterReferenceDialog.description", { referenceName })}
|
||||||
__("Are you sure you want to delete the reference \"%s\"?"),
|
|
||||||
referenceName,
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
<p className="text-txt-secondary mt-2">
|
<p className="text-txt-secondary mt-2">
|
||||||
{__("This action cannot be undone.")}
|
{t("deleteTrustCenterReferenceDialog.warning")}
|
||||||
</p>
|
</p>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
||||||
@@ -92,7 +88,7 @@ export function DeleteCompliancePageReferenceDialog({
|
|||||||
disabled={isDeleting}
|
disabled={isDeleting}
|
||||||
icon={isDeleting ? Spinner : IconTrashCan}
|
icon={isDeleting ? Spinner : IconTrashCan}
|
||||||
>
|
>
|
||||||
{isDeleting ? __("Deleting...") : __("Delete")}
|
{isDeleting ? t("deleteTrustCenterReferenceDialog.actions.deleting") : t("deleteTrustCenterReferenceDialog.actions.delete")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
@@ -31,6 +30,7 @@ import {
|
|||||||
TrButton,
|
TrButton,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFragment } from "react-relay";
|
import { useFragment } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ type Props<Params> = {
|
|||||||
* Reusable component that displays a list of linked controls
|
* Reusable component that displays a list of linked controls
|
||||||
*/
|
*/
|
||||||
export function LinkedControlsCard<Params>(props: Props<Params>) {
|
export function LinkedControlsCard<Params>(props: Props<Params>) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const controls = props.controls;
|
const controls = props.controls;
|
||||||
|
|
||||||
const onDetach = (controlId: string) => {
|
const onDetach = (controlId: string) => {
|
||||||
@@ -118,8 +118,8 @@ export function LinkedControlsCard<Params>(props: Props<Params>) {
|
|||||||
<SortableTable refetch={props.refetch}>
|
<SortableTable refetch={props.refetch}>
|
||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<SortableTh field="SECTION_TITLE">{__("Reference")}</SortableTh>
|
<SortableTh field="SECTION_TITLE">{t("linkedControlsCard.columns.reference")}</SortableTh>
|
||||||
<Th>{__("Name")}</Th>
|
<Th>{t("linkedControlsCard.columns.name")}</Th>
|
||||||
{!props.readOnly && <Th></Th>}
|
{!props.readOnly && <Th></Th>}
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
@@ -130,7 +130,7 @@ export function LinkedControlsCard<Params>(props: Props<Params>) {
|
|||||||
colSpan={props.readOnly ? 2 : 3}
|
colSpan={props.readOnly ? 2 : 3}
|
||||||
className="text-center text-txt-secondary"
|
className="text-center text-txt-secondary"
|
||||||
>
|
>
|
||||||
{__("No controls linked")}
|
{t("linkedControlsCard.empty")}
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
)}
|
||||||
@@ -151,7 +151,7 @@ export function LinkedControlsCard<Params>(props: Props<Params>) {
|
|||||||
onLink={onAttach}
|
onLink={onAttach}
|
||||||
onUnlink={onDetach}
|
onUnlink={onDetach}
|
||||||
>
|
>
|
||||||
<TrButton colspan={3}>{__("Link control")}</TrButton>
|
<TrButton colspan={3}>{t("linkedControlsCard.actions.link")}</TrButton>
|
||||||
</LinkedControlsDialog>
|
</LinkedControlsDialog>
|
||||||
)}
|
)}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
@@ -167,7 +167,7 @@ function ControlRow(props: {
|
|||||||
}) {
|
}) {
|
||||||
const control = useFragment(linkedControlFragment, props.control);
|
const control = useFragment(linkedControlFragment, props.control);
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tr
|
<Tr
|
||||||
@@ -188,7 +188,7 @@ function ControlRow(props: {
|
|||||||
onClick={() => props.onClick(control.id)}
|
onClick={() => props.onClick(control.id)}
|
||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
>
|
>
|
||||||
{__("Unlink")}
|
{t("linkedControlsCard.actions.unlink")}
|
||||||
</Button>
|
</Button>
|
||||||
</Td>
|
</Td>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
@@ -40,6 +39,7 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useLazyLoadQuery, usePaginationFragment } from "react-relay";
|
import { useLazyLoadQuery, usePaginationFragment } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
import { useDebounceCallback } from "usehooks-ts";
|
import { useDebounceCallback } from "usehooks-ts";
|
||||||
@@ -107,7 +107,7 @@ type Props = {
|
|||||||
type SearchRef = RefObject<{ search: (v: string) => void } | null>;
|
type SearchRef = RefObject<{ search: (v: string) => void } | null>;
|
||||||
|
|
||||||
export function LinkedControlsDialog(props: Props) {
|
export function LinkedControlsDialog(props: Props) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const searchRef: SearchRef = useRef(null);
|
const searchRef: SearchRef = useRef(null);
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
const [minHeight, setMinHeight] = useState(0);
|
const [minHeight, setMinHeight] = useState(0);
|
||||||
@@ -116,12 +116,12 @@ export function LinkedControlsDialog(props: Props) {
|
|||||||
searchRef.current?.search(v);
|
searchRef.current?.search(v);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Dialog trigger={props.children} title={__("Link controls")}>
|
<Dialog trigger={props.children} title={t("linkedControlsDialog.title")}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<div className="flex items-center gap-2 sticky top-0 py-4 bg-linear-to-b from-50% from-level-2 to-level-2/0 px-6">
|
<div className="flex items-center gap-2 sticky top-0 py-4 bg-linear-to-b from-50% from-level-2 to-level-2/0 px-6">
|
||||||
<Input
|
<Input
|
||||||
icon={IconMagnifyingGlass}
|
icon={IconMagnifyingGlass}
|
||||||
placeholder={__("Search controls...")}
|
placeholder={t("linkedControlsDialog.searchPlaceholder")}
|
||||||
onValueChange={onSearch}
|
onValueChange={onSearch}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -202,7 +202,7 @@ function ControlRow(
|
|||||||
controlIds: Set<string>;
|
controlIds: Set<string>;
|
||||||
} & Props,
|
} & Props,
|
||||||
) {
|
) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const isLinked = props.controlIds.has(props.control.id);
|
const isLinked = props.controlIds.has(props.control.id);
|
||||||
const onClick = isLinked ? props.onUnlink : props.onLink;
|
const onClick = isLinked ? props.onUnlink : props.onLink;
|
||||||
const IconComponent = isLinked ? IconTrashCan : IconPlusLarge;
|
const IconComponent = isLinked ? IconTrashCan : IconPlusLarge;
|
||||||
@@ -225,7 +225,9 @@ function ControlRow(
|
|||||||
<span>
|
<span>
|
||||||
<IconComponent size={16} />
|
<IconComponent size={16} />
|
||||||
{" "}
|
{" "}
|
||||||
{isLinked ? __("Unlink") : __("Link")}
|
{isLinked
|
||||||
|
? t("linkedControlsDialog.actions.unlink")
|
||||||
|
: t("linkedControlsDialog.actions.link")}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -18,8 +18,6 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { sprintf } from "@probo/helpers";
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
@@ -31,6 +29,7 @@ import {
|
|||||||
useDialogRef,
|
useDialogRef,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { forwardRef, type ReactNode, useImperativeHandle } from "react";
|
import { forwardRef, type ReactNode, useImperativeHandle } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
||||||
@@ -69,7 +68,7 @@ export type BulkExportDialogRef = {
|
|||||||
|
|
||||||
export const BulkExportDialog = forwardRef<BulkExportDialogRef, Props>(
|
export const BulkExportDialog = forwardRef<BulkExportDialogRef, Props>(
|
||||||
({ children, onExport, isLoading = false, defaultEmail, selectedCount }, ref) => {
|
({ children, onExport, isLoading = false, defaultEmail, selectedCount }, ref) => {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const dialogRef = useDialogRef();
|
const dialogRef = useDialogRef();
|
||||||
|
|
||||||
const { register, handleSubmit, formState, watch, setValue } = useFormWithSchema(
|
const { register, handleSubmit, formState, watch, setValue } = useFormWithSchema(
|
||||||
@@ -106,7 +105,7 @@ export const BulkExportDialog = forwardRef<BulkExportDialogRef, Props>(
|
|||||||
<Dialog
|
<Dialog
|
||||||
className="max-w-md"
|
className="max-w-md"
|
||||||
ref={dialogRef}
|
ref={dialogRef}
|
||||||
title={sprintf(__("Export %s Documents"), selectedCount)}
|
title={t("bulkExportDialog.title", { count: selectedCount })}
|
||||||
>
|
>
|
||||||
<form onSubmit={e => void handleSubmit(onSubmit)(e)}>
|
<form onSubmit={e => void handleSubmit(onSubmit)(e)}>
|
||||||
<DialogContent className="space-y-4" padded>
|
<DialogContent className="space-y-4" padded>
|
||||||
@@ -118,10 +117,10 @@ export const BulkExportDialog = forwardRef<BulkExportDialogRef, Props>(
|
|||||||
/>
|
/>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<label className="text-sm font-medium text-txt-primary cursor-pointer">
|
<label className="text-sm font-medium text-txt-primary cursor-pointer">
|
||||||
{__("Include signatures")}
|
{t("bulkExportDialog.includeSignatures.label")}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs text-txt-secondary mt-1">
|
<p className="text-xs text-txt-secondary mt-1">
|
||||||
{__("Show signature information and approval details in the PDFs")}
|
{t("bulkExportDialog.includeSignatures.description")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -133,10 +132,10 @@ export const BulkExportDialog = forwardRef<BulkExportDialogRef, Props>(
|
|||||||
/>
|
/>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<label className="text-sm font-medium text-txt-primary cursor-pointer">
|
<label className="text-sm font-medium text-txt-primary cursor-pointer">
|
||||||
{__("Add watermark")}
|
{t("bulkExportDialog.watermark.label")}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs text-txt-secondary mt-1">
|
<p className="text-xs text-txt-secondary mt-1">
|
||||||
{__("Add confidential watermark with email and timestamp to all PDFs")}
|
{t("bulkExportDialog.watermark.description")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -144,10 +143,10 @@ export const BulkExportDialog = forwardRef<BulkExportDialogRef, Props>(
|
|||||||
{watchWatermark && (
|
{watchWatermark && (
|
||||||
<div className="ml-6">
|
<div className="ml-6">
|
||||||
<Field
|
<Field
|
||||||
label={__("Watermark email")}
|
label={t("bulkExportDialog.watermark.emailLabel")}
|
||||||
{...register("watermarkEmail")}
|
{...register("watermarkEmail")}
|
||||||
type="email"
|
type="email"
|
||||||
placeholder={__("Enter email address")}
|
placeholder={t("bulkExportDialog.watermark.emailPlaceholder")}
|
||||||
error={formState.errors.watermarkEmail?.message}
|
error={formState.errors.watermarkEmail?.message}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
required
|
required
|
||||||
@@ -158,7 +157,7 @@ export const BulkExportDialog = forwardRef<BulkExportDialogRef, Props>(
|
|||||||
|
|
||||||
<div className="bg-level-1 p-3 rounded-lg border border-border-subtle">
|
<div className="bg-level-1 p-3 rounded-lg border border-border-subtle">
|
||||||
<p className="text-sm text-txt-secondary">
|
<p className="text-sm text-txt-secondary">
|
||||||
{__("The documents will be exported as individual PDFs in a ZIP file. You will receive an email when the export is ready for download.")}
|
{t("bulkExportDialog.notice")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -171,11 +170,11 @@ export const BulkExportDialog = forwardRef<BulkExportDialogRef, Props>(
|
|||||||
? (
|
? (
|
||||||
<>
|
<>
|
||||||
<Spinner size={16} />
|
<Spinner size={16} />
|
||||||
{__("Exporting...")}
|
{t("bulkExportDialog.actions.exporting")}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
__("Export Documents")
|
t("bulkExportDialog.actions.export")
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -18,8 +18,6 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { sprintf } from "@probo/helpers";
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
@@ -38,6 +36,7 @@ import {
|
|||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { clsx } from "clsx";
|
import { clsx } from "clsx";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFragment } from "react-relay";
|
import { useFragment } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
@@ -92,7 +91,7 @@ type Props<Params> = {
|
|||||||
* Reusable component that displays a list of linked documents
|
* Reusable component that displays a list of linked documents
|
||||||
*/
|
*/
|
||||||
export function LinkedDocumentsCard<Params>(props: Props<Params>) {
|
export function LinkedDocumentsCard<Params>(props: Props<Params>) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const [limit, setLimit] = useState<number | null>(4);
|
const [limit, setLimit] = useState<number | null>(4);
|
||||||
const documents = useMemo(() => {
|
const documents = useMemo(() => {
|
||||||
return limit ? props.documents.slice(0, limit) : props.documents;
|
return limit ? props.documents.slice(0, limit) : props.documents;
|
||||||
@@ -130,7 +129,7 @@ export function LinkedDocumentsCard<Params>(props: Props<Params>) {
|
|||||||
<Wrapper padded className="space-y-[10px]">
|
<Wrapper padded className="space-y-[10px]">
|
||||||
{variant === "card" && (
|
{variant === "card" && (
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<div className="text-lg font-semibold">{__("Documents")}</div>
|
<div className="text-lg font-semibold">{t("linkedDocumentsCard.title")}</div>
|
||||||
{!props.readOnly && (
|
{!props.readOnly && (
|
||||||
<LinkedDocumentDialog
|
<LinkedDocumentDialog
|
||||||
connectionId={props.connectionId}
|
connectionId={props.connectionId}
|
||||||
@@ -140,7 +139,7 @@ export function LinkedDocumentsCard<Params>(props: Props<Params>) {
|
|||||||
onUnlink={onDetach}
|
onUnlink={onDetach}
|
||||||
>
|
>
|
||||||
<Button variant="tertiary" icon={IconPlusLarge}>
|
<Button variant="tertiary" icon={IconPlusLarge}>
|
||||||
{__("Link document")}
|
{t("linkedDocumentsCard.actions.link")}
|
||||||
</Button>
|
</Button>
|
||||||
</LinkedDocumentDialog>
|
</LinkedDocumentDialog>
|
||||||
)}
|
)}
|
||||||
@@ -149,9 +148,9 @@ export function LinkedDocumentsCard<Params>(props: Props<Params>) {
|
|||||||
<Table className={clsx(variant === "card" && "bg-invert")}>
|
<Table className={clsx(variant === "card" && "bg-invert")}>
|
||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>{__("Name")}</Th>
|
<Th>{t("linkedDocumentsCard.columns.name")}</Th>
|
||||||
<Th>{__("Type")}</Th>
|
<Th>{t("linkedDocumentsCard.columns.type")}</Th>
|
||||||
<Th>{__("State")}</Th>
|
<Th>{t("linkedDocumentsCard.columns.state")}</Th>
|
||||||
{!props.readOnly && <Th></Th>}
|
{!props.readOnly && <Th></Th>}
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
@@ -162,7 +161,7 @@ export function LinkedDocumentsCard<Params>(props: Props<Params>) {
|
|||||||
colSpan={props.readOnly ? 3 : 4}
|
colSpan={props.readOnly ? 3 : 4}
|
||||||
className="text-center text-txt-secondary"
|
className="text-center text-txt-secondary"
|
||||||
>
|
>
|
||||||
{__("No documents linked")}
|
{t("linkedDocumentsCard.empty")}
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
)}
|
||||||
@@ -183,7 +182,7 @@ export function LinkedDocumentsCard<Params>(props: Props<Params>) {
|
|||||||
onUnlink={onDetach}
|
onUnlink={onDetach}
|
||||||
>
|
>
|
||||||
<TrButton colspan={4} icon={IconPlusLarge}>
|
<TrButton colspan={4} icon={IconPlusLarge}>
|
||||||
{__("Link document")}
|
{t("linkedDocumentsCard.actions.link")}
|
||||||
</TrButton>
|
</TrButton>
|
||||||
</LinkedDocumentDialog>
|
</LinkedDocumentDialog>
|
||||||
)}
|
)}
|
||||||
@@ -196,7 +195,9 @@ export function LinkedDocumentsCard<Params>(props: Props<Params>) {
|
|||||||
className="mt-3 mx-auto"
|
className="mt-3 mx-auto"
|
||||||
icon={IconChevronDown}
|
icon={IconChevronDown}
|
||||||
>
|
>
|
||||||
{sprintf(__("Show %s more"), props.documents.length - limit)}
|
{t("linkedDocumentsCard.actions.showMore", {
|
||||||
|
count: props.documents.length - limit,
|
||||||
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
@@ -210,7 +211,7 @@ function DocumentRow(props: {
|
|||||||
}) {
|
}) {
|
||||||
const document = useFragment(linkedDocumentFragment, props.document);
|
const document = useFragment(linkedDocumentFragment, props.document);
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tr to={`/organizations/${organizationId}/documents/${document.id}`}>
|
<Tr to={`/organizations/${organizationId}/documents/${document.id}`}>
|
||||||
@@ -239,7 +240,7 @@ function DocumentRow(props: {
|
|||||||
onClick={() => props.onClick(document.id)}
|
onClick={() => props.onClick(document.id)}
|
||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
>
|
>
|
||||||
{__("Unlink")}
|
{t("linkedDocumentsCard.actions.unlink")}
|
||||||
</Button>
|
</Button>
|
||||||
</Td>
|
</Td>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -33,6 +32,7 @@ import {
|
|||||||
Spinner,
|
Spinner,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { type ReactNode, Suspense, useMemo, useState } from "react";
|
import { type ReactNode, Suspense, useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useLazyLoadQuery, usePaginationFragment } from "react-relay";
|
import { useLazyLoadQuery, usePaginationFragment } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
@@ -101,16 +101,16 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function LinkedDocumentDialog({ children, ...props }: Props) {
|
export function LinkedDocumentDialog({ children, ...props }: Props) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog trigger={children} title={__("Link documents")}>
|
<Dialog trigger={children} title={t("linkedDocumentsDialog.title")}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Suspense fallback={<Spinner centered />}>
|
<Suspense fallback={<Spinner centered />}>
|
||||||
<LinkedDocumentsDialogContent {...props} />
|
<LinkedDocumentsDialogContent {...props} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogFooter exitLabel={__("Close")} />
|
<DialogFooter exitLabel={t("linkedDocumentsDialog.actions.close")} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -129,7 +129,7 @@ function LinkedDocumentsDialogContent(props: Omit<Props, "children">) {
|
|||||||
documentsFragment,
|
documentsFragment,
|
||||||
query.organization as LinkedDocumentsDialogFragment$key,
|
query.organization as LinkedDocumentsDialogFragment$key,
|
||||||
);
|
);
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const documents = useMemo(
|
const documents = useMemo(
|
||||||
() => data.documents?.edges?.map(edge => edge.node) ?? [],
|
() => data.documents?.edges?.map(edge => edge.node) ?? [],
|
||||||
@@ -150,7 +150,7 @@ function LinkedDocumentsDialogContent(props: Omit<Props, "children">) {
|
|||||||
<div className="flex items-center gap-2 sticky top-0 relative py-4 bg-linear-to-b from-50% from-level-2 to-level-2/0 px-6">
|
<div className="flex items-center gap-2 sticky top-0 relative py-4 bg-linear-to-b from-50% from-level-2 to-level-2/0 px-6">
|
||||||
<Input
|
<Input
|
||||||
icon={IconMagnifyingGlass}
|
icon={IconMagnifyingGlass}
|
||||||
placeholder={__("Search documents...")}
|
placeholder={t("linkedDocumentsDialog.searchPlaceholder")}
|
||||||
onValueChange={setSearch}
|
onValueChange={setSearch}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -187,7 +187,7 @@ type RowProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function DocumentRow(props: RowProps) {
|
function DocumentRow(props: RowProps) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const isLinked = props.linkedDocuments.has(props.document.id);
|
const isLinked = props.linkedDocuments.has(props.document.id);
|
||||||
const onClick = isLinked ? props.onUnlink : props.onLink;
|
const onClick = isLinked ? props.onUnlink : props.onLink;
|
||||||
@@ -209,7 +209,9 @@ function DocumentRow(props: RowProps) {
|
|||||||
<span>
|
<span>
|
||||||
<IconComponent size={16} />
|
<IconComponent size={16} />
|
||||||
{" "}
|
{" "}
|
||||||
{isLinked ? __("Unlink") : __("Link")}
|
{isLinked
|
||||||
|
? t("linkedDocumentsDialog.actions.unlink")
|
||||||
|
: t("linkedDocumentsDialog.actions.link")}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
@@ -30,6 +29,7 @@ import {
|
|||||||
useDialogRef,
|
useDialogRef,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { forwardRef, type ReactNode, useImperativeHandle } from "react";
|
import { forwardRef, type ReactNode, useImperativeHandle } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
||||||
@@ -60,7 +60,7 @@ export type PdfDownloadDialogRef = {
|
|||||||
|
|
||||||
export const PdfDownloadDialog = forwardRef<PdfDownloadDialogRef, Props>(
|
export const PdfDownloadDialog = forwardRef<PdfDownloadDialogRef, Props>(
|
||||||
({ children, onDownload, isLoading = false, defaultEmail }, ref) => {
|
({ children, onDownload, isLoading = false, defaultEmail }, ref) => {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const dialogRef = useDialogRef();
|
const dialogRef = useDialogRef();
|
||||||
|
|
||||||
const { register, handleSubmit, formState, watch, setValue }
|
const { register, handleSubmit, formState, watch, setValue }
|
||||||
@@ -95,7 +95,7 @@ export const PdfDownloadDialog = forwardRef<PdfDownloadDialogRef, Props>(
|
|||||||
<Dialog
|
<Dialog
|
||||||
className="max-w-md"
|
className="max-w-md"
|
||||||
ref={dialogRef}
|
ref={dialogRef}
|
||||||
title={__("Download PDF Options")}
|
title={t("pdfDownloadDialog.title")}
|
||||||
>
|
>
|
||||||
<form onSubmit={e => void handleSubmit(onSubmit)(e)}>
|
<form onSubmit={e => void handleSubmit(onSubmit)(e)}>
|
||||||
<DialogContent className="space-y-4" padded>
|
<DialogContent className="space-y-4" padded>
|
||||||
@@ -107,12 +107,10 @@ export const PdfDownloadDialog = forwardRef<PdfDownloadDialogRef, Props>(
|
|||||||
/>
|
/>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<label className="text-sm font-medium text-txt-primary cursor-pointer">
|
<label className="text-sm font-medium text-txt-primary cursor-pointer">
|
||||||
{__("Include signatures")}
|
{t("pdfDownloadDialog.includeSignatures.label")}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs text-txt-secondary mt-1">
|
<p className="text-xs text-txt-secondary mt-1">
|
||||||
{__(
|
{t("pdfDownloadDialog.includeSignatures.description")}
|
||||||
"Show signature information and approval details in the PDF",
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -124,12 +122,10 @@ export const PdfDownloadDialog = forwardRef<PdfDownloadDialogRef, Props>(
|
|||||||
/>
|
/>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<label className="text-sm font-medium text-txt-primary cursor-pointer">
|
<label className="text-sm font-medium text-txt-primary cursor-pointer">
|
||||||
{__("Add watermark")}
|
{t("pdfDownloadDialog.watermark.label")}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs text-txt-secondary mt-1">
|
<p className="text-xs text-txt-secondary mt-1">
|
||||||
{__(
|
{t("pdfDownloadDialog.watermark.description")}
|
||||||
"Add confidential watermark with email and timestamp",
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -137,10 +133,10 @@ export const PdfDownloadDialog = forwardRef<PdfDownloadDialogRef, Props>(
|
|||||||
{watchWatermark && (
|
{watchWatermark && (
|
||||||
<div className="ml-6">
|
<div className="ml-6">
|
||||||
<Field
|
<Field
|
||||||
label={__("Watermark email")}
|
label={t("pdfDownloadDialog.watermark.emailLabel")}
|
||||||
{...register("watermarkEmail")}
|
{...register("watermarkEmail")}
|
||||||
type="email"
|
type="email"
|
||||||
placeholder={__("Enter email address")}
|
placeholder={t("pdfDownloadDialog.watermark.emailPlaceholder")}
|
||||||
error={formState.errors.watermarkEmail?.message}
|
error={formState.errors.watermarkEmail?.message}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
required
|
required
|
||||||
@@ -155,11 +151,11 @@ export const PdfDownloadDialog = forwardRef<PdfDownloadDialogRef, Props>(
|
|||||||
? (
|
? (
|
||||||
<>
|
<>
|
||||||
<Spinner size={16} />
|
<Spinner size={16} />
|
||||||
{__("Downloading...")}
|
{t("pdfDownloadDialog.actions.downloading")}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
__("Download PDF")
|
t("pdfDownloadDialog.actions.download")
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { getAuditStateLabel, getAuditStateVariant } from "@probo/helpers";
|
import { getAuditStateLabel, getAuditStateVariant } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Badge, Field, Option, Select } from "@probo/ui";
|
import { Badge, Field, Option, Select } from "@probo/ui";
|
||||||
import { type ComponentProps, Suspense } from "react";
|
import { type ComponentProps, Suspense } from "react";
|
||||||
import {
|
import {
|
||||||
@@ -28,6 +27,7 @@ import {
|
|||||||
type FieldValues,
|
type FieldValues,
|
||||||
type Path,
|
type Path,
|
||||||
} from "react-hook-form";
|
} from "react-hook-form";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { graphql, useLazyLoadQuery } from "react-relay";
|
import { graphql, useLazyLoadQuery } from "react-relay";
|
||||||
|
|
||||||
import type { AuditSelectFieldQuery } from "#/__generated__/core/AuditSelectFieldQuery.graphql";
|
import type { AuditSelectFieldQuery } from "#/__generated__/core/AuditSelectFieldQuery.graphql";
|
||||||
@@ -86,7 +86,7 @@ export function AuditSelectField<T extends FieldValues = FieldValues>({
|
|||||||
function AuditSelectWithQuery<T extends FieldValues = FieldValues>(
|
function AuditSelectWithQuery<T extends FieldValues = FieldValues>(
|
||||||
props: Pick<Props<T>, "organizationId" | "control" | "name" | "disabled">,
|
props: Pick<Props<T>, "organizationId" | "control" | "name" | "disabled">,
|
||||||
) {
|
) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const { name, organizationId, control } = props;
|
const { name, organizationId, control } = props;
|
||||||
const data = useLazyLoadQuery<AuditSelectFieldQuery>(
|
const data = useLazyLoadQuery<AuditSelectFieldQuery>(
|
||||||
auditsQuery,
|
auditsQuery,
|
||||||
@@ -109,7 +109,7 @@ function AuditSelectWithQuery<T extends FieldValues = FieldValues>(
|
|||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
id={name}
|
id={name}
|
||||||
variant="editor"
|
variant="editor"
|
||||||
placeholder={__("Select an audit")}
|
placeholder={t("auditSelectField.placeholder")}
|
||||||
onValueChange={value =>
|
onValueChange={value =>
|
||||||
field.onChange(value === NONE_VALUE ? "" : value)}
|
field.onChange(value === NONE_VALUE ? "" : value)}
|
||||||
key={audits?.length.toString() ?? "0"}
|
key={audits?.length.toString() ?? "0"}
|
||||||
@@ -118,7 +118,7 @@ function AuditSelectWithQuery<T extends FieldValues = FieldValues>(
|
|||||||
value={field.value || NONE_VALUE}
|
value={field.value || NONE_VALUE}
|
||||||
>
|
>
|
||||||
<Option value={NONE_VALUE}>
|
<Option value={NONE_VALUE}>
|
||||||
<span className="text-txt-tertiary">{__("None")}</span>
|
<span className="text-txt-tertiary">{t("auditSelectField.none")}</span>
|
||||||
</Option>
|
</Option>
|
||||||
{audits?.map(audit => (
|
{audits?.map(audit => (
|
||||||
<Option key={audit.id} value={audit.id}>
|
<Option key={audit.id} value={audit.id}>
|
||||||
@@ -130,7 +130,7 @@ function AuditSelectWithQuery<T extends FieldValues = FieldValues>(
|
|||||||
</span>
|
</span>
|
||||||
<div className="ml-3">
|
<div className="ml-3">
|
||||||
<Badge variant={getAuditStateVariant(audit.state)}>
|
<Badge variant={getAuditStateVariant(audit.state)}>
|
||||||
{getAuditStateLabel(__, audit.state)}
|
{getAuditStateLabel(t, audit.state)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,21 +18,18 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import {
|
import { controlMaturityLevels } from "@probo/helpers";
|
||||||
controlMaturityLevels,
|
|
||||||
getControlMaturityLevelLabel,
|
|
||||||
} from "@probo/helpers";
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Option } from "@probo/ui";
|
import { Option } from "@probo/ui";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export function ControlMaturityLevelOptions() {
|
export function ControlMaturityLevelOptions() {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{controlMaturityLevels.map(level => (
|
{controlMaturityLevels.map(level => (
|
||||||
<Option key={level} value={level}>
|
<Option key={level} value={level}>
|
||||||
{getControlMaturityLevelLabel(__, level)}
|
{t(`controlMaturityLevelOptions.levels.${level}`)}
|
||||||
</Option>
|
</Option>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -18,12 +18,17 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { countries, type CountryCode, getCountryName, getCountryOptions } from "@probo/helpers";
|
import { countries, type CountryCode, getCountryName } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Badge, IconCrossLargeX, Input } from "@probo/ui";
|
import { Badge, IconCrossLargeX, Input } from "@probo/ui";
|
||||||
import { clsx } from "clsx";
|
import { clsx } from "clsx";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { type Control, Controller, type FieldPath, type FieldValues } from "react-hook-form";
|
import {
|
||||||
|
type Control,
|
||||||
|
Controller,
|
||||||
|
type FieldPath,
|
||||||
|
type FieldValues,
|
||||||
|
} from "react-hook-form";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
type Props<T extends FieldValues = FieldValues> = {
|
type Props<T extends FieldValues = FieldValues> = {
|
||||||
control: Control<T>;
|
control: Control<T>;
|
||||||
@@ -31,7 +36,11 @@ type Props<T extends FieldValues = FieldValues> = {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CountriesField<T extends FieldValues = FieldValues>({ control, name, disabled }: Props<T>) {
|
export function CountriesField<T extends FieldValues = FieldValues>({
|
||||||
|
control,
|
||||||
|
name,
|
||||||
|
disabled,
|
||||||
|
}: Props<T>) {
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
@@ -54,7 +63,7 @@ type CountriesFieldInputProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function CountriesFieldInput(props: CountriesFieldInputProps) {
|
function CountriesFieldInput(props: CountriesFieldInputProps) {
|
||||||
const { __ } = useTranslate();
|
const { i18n } = useTranslation();
|
||||||
const [animateBadge, setAnimateBadge] = useState(false);
|
const [animateBadge, setAnimateBadge] = useState(false);
|
||||||
|
|
||||||
const addCountry = (code: string) => {
|
const addCountry = (code: string) => {
|
||||||
@@ -85,7 +94,7 @@ function CountriesFieldInput(props: CountriesFieldInputProps) {
|
|||||||
&& "starting:opacity-0 starting:w-0 w-max transition-all duration-500 starting:bg-accent",
|
&& "starting:opacity-0 starting:w-0 w-max transition-all duration-500 starting:bg-accent",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{getCountryName(__, countryCode as CountryCode)}
|
{getCountryName(i18n.language, countryCode as CountryCode)}
|
||||||
<div className="w-0 overflow-hidden group-hover:w-4 duration-200">
|
<div className="w-0 overflow-hidden group-hover:w-4 duration-200">
|
||||||
<IconCrossLargeX size={12} />
|
<IconCrossLargeX size={12} />
|
||||||
</div>
|
</div>
|
||||||
@@ -113,10 +122,13 @@ type CountryInputProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function CountryInput({ availableCountries, onAdd }: CountryInputProps) {
|
function CountryInput({ availableCountries, onAdd }: CountryInputProps) {
|
||||||
const { __ } = useTranslate();
|
const { t, i18n } = useTranslation();
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const countryOptions = getCountryOptions(__);
|
const countryOptions = availableCountries.map(code => ({
|
||||||
|
value: code,
|
||||||
|
label: getCountryName(i18n.language, code),
|
||||||
|
}));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.body.style.overflow = isOpen ? "hidden" : "";
|
document.body.style.overflow = isOpen ? "hidden" : "";
|
||||||
@@ -126,7 +138,9 @@ function CountryInput({ availableCountries, onAdd }: CountryInputProps) {
|
|||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
const filteredCountries = countryOptions
|
const filteredCountries = countryOptions
|
||||||
.filter((option: { value: string; label: string }) => availableCountries.includes(option.value as CountryCode))
|
.filter((option: { value: string; label: string }) =>
|
||||||
|
availableCountries.includes(option.value as CountryCode),
|
||||||
|
)
|
||||||
.filter((option: { value: string; label: string }) =>
|
.filter((option: { value: string; label: string }) =>
|
||||||
option.label.toLowerCase().includes(search.toLowerCase()),
|
option.label.toLowerCase().includes(search.toLowerCase()),
|
||||||
);
|
);
|
||||||
@@ -145,7 +159,7 @@ function CountryInput({ availableCountries, onAdd }: CountryInputProps) {
|
|||||||
value={search}
|
value={search}
|
||||||
onChange={e => setSearch(e.target.value)}
|
onChange={e => setSearch(e.target.value)}
|
||||||
onFocus={() => setIsOpen(true)}
|
onFocus={() => setIsOpen(true)}
|
||||||
placeholder={__("Search and add countries...")}
|
placeholder={t("countriesField.searchPlaceholder")}
|
||||||
className="w-full pr-8"
|
className="w-full pr-8"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@@ -186,10 +200,7 @@ function CountryInput({ availableCountries, onAdd }: CountryInputProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div
|
<div className="fixed inset-0 z-40" onClick={() => setIsOpen(false)} />
|
||||||
className="fixed inset-0 z-40"
|
|
||||||
onClick={() => setIsOpen(false)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,21 +18,18 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import {
|
import { documentClassifications } from "@probo/helpers";
|
||||||
documentClassifications,
|
|
||||||
getDocumentClassificationLabel,
|
|
||||||
} from "@probo/helpers";
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Option } from "@probo/ui";
|
import { Option } from "@probo/ui";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export function DocumentClassificationOptions() {
|
export function DocumentClassificationOptions() {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{documentClassifications.map(classification => (
|
{documentClassifications.map(classification => (
|
||||||
<Option key={classification} value={classification}>
|
<Option key={classification} value={classification}>
|
||||||
{getDocumentClassificationLabel(__, classification)}
|
{t(`documentClassificationOptions.classifications.${classification}`)}
|
||||||
</Option>
|
</Option>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -18,18 +18,18 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { documentTypes, getDocumentTypeLabel } from "@probo/helpers";
|
import { documentTypes } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Option } from "@probo/ui";
|
import { Option } from "@probo/ui";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export function DocumentTypeOptions() {
|
export function DocumentTypeOptions() {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{documentTypes.map(type => (
|
{documentTypes.map(type => (
|
||||||
<Option key={type} value={type}>
|
<Option key={type} value={type}>
|
||||||
{getDocumentTypeLabel(__, type)}
|
{t(`documentTypeOptions.types.${type}`)}
|
||||||
</Option>
|
</Option>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -18,10 +18,17 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Button, IconPlusLarge, IconTrashCan, Input, Label } from "@probo/ui";
|
import { Button, IconPlusLarge, IconTrashCan, Input, Label } from "@probo/ui";
|
||||||
import type { ArrayPath, Control, FieldValue, FieldValues, Path, UseFormRegister } from "react-hook-form";
|
import type {
|
||||||
|
ArrayPath,
|
||||||
|
Control,
|
||||||
|
FieldValue,
|
||||||
|
FieldValues,
|
||||||
|
Path,
|
||||||
|
UseFormRegister,
|
||||||
|
} from "react-hook-form";
|
||||||
import { useFieldArray } from "react-hook-form";
|
import { useFieldArray } from "react-hook-form";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
type Props<TFieldValues extends FieldValues = FieldValues> = {
|
type Props<TFieldValues extends FieldValues = FieldValues> = {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
@@ -32,10 +39,12 @@ type Props<TFieldValues extends FieldValues = FieldValues> = {
|
|||||||
/**
|
/**
|
||||||
* A field to handle multiple emails
|
* A field to handle multiple emails
|
||||||
*/
|
*/
|
||||||
export function EmailsField<
|
export function EmailsField<TFieldValues extends FieldValues = FieldValues>({
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
control,
|
||||||
>({ control, register, disabled }: Props<TFieldValues>) {
|
register,
|
||||||
const { __ } = useTranslate();
|
disabled,
|
||||||
|
}: Props<TFieldValues>) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { fields, append, remove } = useFieldArray({
|
const { fields, append, remove } = useFieldArray({
|
||||||
name: "additionalEmailAddresses" as ArrayPath<TFieldValues>,
|
name: "additionalEmailAddresses" as ArrayPath<TFieldValues>,
|
||||||
control,
|
control,
|
||||||
@@ -43,12 +52,14 @@ export function EmailsField<
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<fieldset className="space-y-2">
|
<fieldset className="space-y-2">
|
||||||
{fields.length > 0 && <Label>{__("Additional emails")}</Label>}
|
{fields.length > 0 && <Label>{t("emailsField.additionalEmails")}</Label>}
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
<div key={field.id} className="flex items-stretch">
|
<div key={field.id} className="flex items-stretch">
|
||||||
<Input
|
<Input
|
||||||
className="w-full"
|
className="w-full"
|
||||||
{...register(`additionalEmailAddresses.${index}` as Path<TFieldValues>)}
|
{...register(
|
||||||
|
`additionalEmailAddresses.${index}` as Path<TFieldValues>,
|
||||||
|
)}
|
||||||
type="email"
|
type="email"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
@@ -67,7 +78,7 @@ export function EmailsField<
|
|||||||
onClick={() => append("" as FieldValue<TFieldValues>)}
|
onClick={() => append("" as FieldValue<TFieldValues>)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{__("Add email")}
|
{t("emailsField.actions.add")}
|
||||||
</Button>
|
</Button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,10 +18,15 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Combobox, ComboboxItem, Field } from "@probo/ui";
|
import { Combobox, ComboboxItem, Field } from "@probo/ui";
|
||||||
import { type ComponentProps, Suspense, useMemo, useState } from "react";
|
import { type ComponentProps, Suspense, useMemo, useState } from "react";
|
||||||
import { type Control, Controller, type FieldPath, type FieldValues } from "react-hook-form";
|
import {
|
||||||
|
type Control,
|
||||||
|
Controller,
|
||||||
|
type FieldPath,
|
||||||
|
type FieldValues,
|
||||||
|
} from "react-hook-form";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { usePaginatedMeasures } from "#/hooks/graph/usePaginatedMeasures";
|
import { usePaginatedMeasures } from "#/hooks/graph/usePaginatedMeasures";
|
||||||
|
|
||||||
@@ -38,7 +43,9 @@ type Props<
|
|||||||
optional?: boolean;
|
optional?: boolean;
|
||||||
} & ComponentProps<typeof Field>;
|
} & ComponentProps<typeof Field>;
|
||||||
|
|
||||||
export function MeasureSelectField<TFieldValues extends FieldValues = FieldValues>({
|
export function MeasureSelectField<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
>({
|
||||||
organizationId,
|
organizationId,
|
||||||
control,
|
control,
|
||||||
disabled,
|
disabled,
|
||||||
@@ -48,7 +55,11 @@ export function MeasureSelectField<TFieldValues extends FieldValues = FieldValue
|
|||||||
return (
|
return (
|
||||||
<Field {...props}>
|
<Field {...props}>
|
||||||
<Suspense
|
<Suspense
|
||||||
fallback={<Combobox onSearch={() => {}} placeholder="Loading..." disabled><div /></Combobox>}
|
fallback={(
|
||||||
|
<Combobox onSearch={() => {}} placeholder="Loading..." disabled>
|
||||||
|
<div />
|
||||||
|
</Combobox>
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<MeasureSelectWithQuery<TFieldValues>
|
<MeasureSelectWithQuery<TFieldValues>
|
||||||
organizationId={organizationId}
|
organizationId={organizationId}
|
||||||
@@ -63,9 +74,12 @@ export function MeasureSelectField<TFieldValues extends FieldValues = FieldValue
|
|||||||
}
|
}
|
||||||
|
|
||||||
function MeasureSelectWithQuery<TFieldValues extends FieldValues = FieldValues>(
|
function MeasureSelectWithQuery<TFieldValues extends FieldValues = FieldValues>(
|
||||||
props: Pick<Props<TFieldValues>, "organizationId" | "control" | "name" | "disabled" | "optional">,
|
props: Pick<
|
||||||
|
Props<TFieldValues>,
|
||||||
|
"organizationId" | "control" | "name" | "disabled" | "optional"
|
||||||
|
>,
|
||||||
) {
|
) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const { name, organizationId, control, disabled, optional } = props;
|
const { name, organizationId, control, disabled, optional } = props;
|
||||||
const { data } = usePaginatedMeasures(organizationId);
|
const { data } = usePaginatedMeasures(organizationId);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
@@ -91,12 +105,14 @@ function MeasureSelectWithQuery<TFieldValues extends FieldValues = FieldValues>(
|
|||||||
control={control}
|
control={control}
|
||||||
name={name}
|
name={name}
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
const selectedMeasure = field.value ? allMeasures?.find(m => m.id === field.value) : null;
|
const selectedMeasure = field.value
|
||||||
|
? allMeasures?.find(m => m.id === field.value)
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Combobox
|
<Combobox
|
||||||
id={name}
|
id={name}
|
||||||
placeholder={__("Select a measure")}
|
placeholder={t("measureSelectField.placeholder")}
|
||||||
value={selectedMeasure ? selectedMeasure.name : search}
|
value={selectedMeasure ? selectedMeasure.name : search}
|
||||||
onSearch={setSearch}
|
onSearch={setSearch}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -108,7 +124,7 @@ function MeasureSelectWithQuery<TFieldValues extends FieldValues = FieldValues>(
|
|||||||
setSearch("");
|
setSearch("");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{__("None")}
|
{t("measureSelectField.none")}
|
||||||
</ComboboxItem>
|
</ComboboxItem>
|
||||||
)}
|
)}
|
||||||
{measures?.map(m => (
|
{measures?.map(m => (
|
||||||
@@ -123,7 +139,9 @@ function MeasureSelectWithQuery<TFieldValues extends FieldValues = FieldValues>(
|
|||||||
<div className="max-w-75 ellipsis overflow-hidden whitespace-pre-wrap">
|
<div className="max-w-75 ellipsis overflow-hidden whitespace-pre-wrap">
|
||||||
{m.name}
|
{m.name}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-txt-secondary">{m.category}</div>
|
<div className="text-sm text-txt-secondary">
|
||||||
|
{m.category}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ComboboxItem>
|
</ComboboxItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -18,10 +18,22 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
import {
|
||||||
import { Badge, Button, Field, IconCrossLargeX, Option, Select } from "@probo/ui";
|
Badge,
|
||||||
|
Button,
|
||||||
|
Field,
|
||||||
|
IconCrossLargeX,
|
||||||
|
Option,
|
||||||
|
Select,
|
||||||
|
} from "@probo/ui";
|
||||||
import { type ComponentProps, Suspense, useState } from "react";
|
import { type ComponentProps, Suspense, useState } from "react";
|
||||||
import { type Control, Controller, type FieldValues, type Path } from "react-hook-form";
|
import {
|
||||||
|
type Control,
|
||||||
|
Controller,
|
||||||
|
type FieldValues,
|
||||||
|
type Path,
|
||||||
|
} from "react-hook-form";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { usePeople } from "#/hooks/graph/PeopleGraph";
|
import { usePeople } from "#/hooks/graph/PeopleGraph";
|
||||||
|
|
||||||
@@ -67,10 +79,24 @@ export function PeopleMultiSelectField<T extends FieldValues = FieldValues>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
|
function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
|
||||||
props: Pick<Props<T>, "organizationId" | "control" | "name" | "disabled" | "selectedPeople" | "placeholder">,
|
props: Pick<
|
||||||
|
Props<T>,
|
||||||
|
| "organizationId"
|
||||||
|
| "control"
|
||||||
|
| "name"
|
||||||
|
| "disabled"
|
||||||
|
| "selectedPeople"
|
||||||
|
| "placeholder"
|
||||||
|
>,
|
||||||
) {
|
) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const { name, organizationId, control, selectedPeople = [], placeholder } = props;
|
const {
|
||||||
|
name,
|
||||||
|
organizationId,
|
||||||
|
control,
|
||||||
|
selectedPeople = [],
|
||||||
|
placeholder,
|
||||||
|
} = props;
|
||||||
const people = usePeople(organizationId, { contractEnded: false });
|
const people = usePeople(organizationId, { contractEnded: false });
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
@@ -91,10 +117,16 @@ function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
|
|||||||
control={control}
|
control={control}
|
||||||
name={name as Path<T>}
|
name={name as Path<T>}
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
const selectedPeopleIds = (Array.isArray(field.value) ? field.value : []) as string[];
|
const selectedPeopleIds = (
|
||||||
|
Array.isArray(field.value) ? field.value : []
|
||||||
|
) as string[];
|
||||||
|
|
||||||
const selectedPeople = allPeople.filter(p => selectedPeopleIds.includes(p.id));
|
const selectedPeople = allPeople.filter(p =>
|
||||||
const availablePeople = allPeople.filter(p => !selectedPeopleIds.includes(p.id));
|
selectedPeopleIds.includes(p.id),
|
||||||
|
);
|
||||||
|
const availablePeople = allPeople.filter(
|
||||||
|
p => !selectedPeopleIds.includes(p.id),
|
||||||
|
);
|
||||||
|
|
||||||
const handleAddPerson = (personId: string) => {
|
const handleAddPerson = (personId: string) => {
|
||||||
const newValue = [...selectedPeopleIds, personId];
|
const newValue = [...selectedPeopleIds, personId];
|
||||||
@@ -103,7 +135,9 @@ function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRemovePerson = (personId: string) => {
|
const handleRemovePerson = (personId: string) => {
|
||||||
const newValue = selectedPeopleIds.filter((id: string) => id !== personId);
|
const newValue = selectedPeopleIds.filter(
|
||||||
|
(id: string) => id !== personId,
|
||||||
|
);
|
||||||
field.onChange(newValue);
|
field.onChange(newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -114,7 +148,9 @@ function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
|
|||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
id={name}
|
id={name}
|
||||||
variant="editor"
|
variant="editor"
|
||||||
placeholder={placeholder ?? __("Add people...")}
|
placeholder={
|
||||||
|
placeholder ?? t("peopleMultiSelectField.addPlaceholder")
|
||||||
|
}
|
||||||
onValueChange={handleAddPerson}
|
onValueChange={handleAddPerson}
|
||||||
key={`${selectedPeopleIds.length}-${people.length}`}
|
key={`${selectedPeopleIds.length}-${people.length}`}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
@@ -123,7 +159,11 @@ function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
|
|||||||
onOpenChange={setIsOpen}
|
onOpenChange={setIsOpen}
|
||||||
>
|
>
|
||||||
{availablePeople.map(person => (
|
{availablePeople.map(person => (
|
||||||
<Option key={person.id} value={person.id} className="flex gap-2">
|
<Option
|
||||||
|
key={person.id}
|
||||||
|
value={person.id}
|
||||||
|
className="flex gap-2"
|
||||||
|
>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span>{person.fullName}</span>
|
<span>{person.fullName}</span>
|
||||||
{person.emailAddress && (
|
{person.emailAddress && (
|
||||||
@@ -140,7 +180,11 @@ function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
|
|||||||
{selectedPeople.length > 0 && (
|
{selectedPeople.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{selectedPeople.map(person => (
|
{selectedPeople.map(person => (
|
||||||
<Badge key={person.id} variant="neutral" className="flex items-center gap-2">
|
<Badge
|
||||||
|
key={person.id}
|
||||||
|
variant="neutral"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
<span>{person.fullName}</span>
|
<span>{person.fullName}</span>
|
||||||
{!props.disabled && (
|
{!props.disabled && (
|
||||||
<Button
|
<Button
|
||||||
@@ -158,7 +202,7 @@ function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
|
|||||||
|
|
||||||
{selectedPeople.length === 0 && availablePeople.length === 0 && (
|
{selectedPeople.length === 0 && availablePeople.length === 0 && (
|
||||||
<div className="text-sm text-txt-secondary py-2">
|
<div className="text-sm text-txt-secondary py-2">
|
||||||
{__("No people available")}
|
{t("peopleMultiSelectField.empty")}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,12 +18,12 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Avatar, Field, Option, Select } from "@probo/ui";
|
import { Avatar, Field, Option, Select } from "@probo/ui";
|
||||||
import { type ComponentProps, Suspense } from "react";
|
import { type ComponentProps, Suspense } from "react";
|
||||||
import { type Control, Controller, type FieldPath, type FieldValues } from "react-hook-form";
|
import { type Control, Controller, type FieldPath, type FieldValues } from "react-hook-form";
|
||||||
|
|
||||||
import { usePeople } from "#/hooks/graph/PeopleGraph";
|
import { usePeople } from "#/hooks/graph/PeopleGraph";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
type Props<
|
type Props<
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
@@ -42,7 +42,7 @@ export function PeopleSelectField<TFieldValues extends FieldValues = FieldValues
|
|||||||
control,
|
control,
|
||||||
...props
|
...props
|
||||||
}: Props<TFieldValues>) {
|
}: Props<TFieldValues>) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Field {...props}>
|
<Field {...props}>
|
||||||
@@ -51,7 +51,7 @@ export function PeopleSelectField<TFieldValues extends FieldValues = FieldValues
|
|||||||
<Select
|
<Select
|
||||||
variant="editor"
|
variant="editor"
|
||||||
loading
|
loading
|
||||||
placeholder={__("Select an owner")}
|
placeholder={t("peopleSelectField.placeholder")}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -74,7 +74,7 @@ function PeopleSelectWithQuery<TFieldValues extends FieldValues = FieldValues>(
|
|||||||
"organizationId" | "control" | "name" | "disabled" | "optional"
|
"organizationId" | "control" | "name" | "disabled" | "optional"
|
||||||
>,
|
>,
|
||||||
) {
|
) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const { name, organizationId, control } = props;
|
const { name, organizationId, control } = props;
|
||||||
const people = usePeople(organizationId, { contractEnded: false });
|
const people = usePeople(organizationId, { contractEnded: false });
|
||||||
|
|
||||||
@@ -87,14 +87,16 @@ function PeopleSelectWithQuery<TFieldValues extends FieldValues = FieldValues>(
|
|||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
id={name}
|
id={name}
|
||||||
variant="editor"
|
variant="editor"
|
||||||
placeholder={__("Select an owner")}
|
placeholder={t("peopleSelectField.placeholder")}
|
||||||
onValueChange={value =>
|
onValueChange={value =>
|
||||||
field.onChange(value === "__NONE__" ? null : value)}
|
field.onChange(value === "__NONE__" ? null : value)}
|
||||||
{...field}
|
{...field}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
value={field.value ?? (props.optional ? "__NONE__" : "")}
|
value={field.value ?? (props.optional ? "__NONE__" : "")}
|
||||||
>
|
>
|
||||||
{props.optional && <Option value="__NONE__">{__("None")}</Option>}
|
{props.optional && (
|
||||||
|
<Option value="__NONE__">{t("peopleSelectField.none")}</Option>
|
||||||
|
)}
|
||||||
{people?.map(p => (
|
{people?.map(p => (
|
||||||
<Option key={p.id} value={p.id} className="flex gap-2">
|
<Option key={p.id} value={p.id} className="flex gap-2">
|
||||||
<Avatar name={p.fullName} />
|
<Avatar name={p.fullName} />
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Option } from "@probo/ui";
|
import { Option } from "@probo/ui";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ProcessingActivityDataProtectionImpactAssessment,
|
ProcessingActivityDataProtectionImpactAssessment,
|
||||||
@@ -28,16 +28,27 @@ import type {
|
|||||||
ProcessingActivityTransferImpactAssessment,
|
ProcessingActivityTransferImpactAssessment,
|
||||||
} from "#/__generated__/core/ProcessingActivityGraphCreateMutation.graphql";
|
} from "#/__generated__/core/ProcessingActivityGraphCreateMutation.graphql";
|
||||||
|
|
||||||
|
type Translator = (key: string) => string;
|
||||||
|
|
||||||
export function SpecialOrCriminalDataOptions() {
|
export function SpecialOrCriminalDataOptions() {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const options: Array<{
|
const options: Array<{
|
||||||
value: ProcessingActivitySpecialOrCriminalDatum;
|
value: ProcessingActivitySpecialOrCriminalDatum;
|
||||||
label: string;
|
label: string;
|
||||||
}> = [
|
}> = [
|
||||||
{ value: "YES", label: __("Yes") },
|
{
|
||||||
{ value: "NO", label: __("No") },
|
value: "YES",
|
||||||
{ value: "POSSIBLE", label: __("Possible") },
|
label: t("processingActivityEnumOptions.specialOrCriminalData.yes"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "NO",
|
||||||
|
label: t("processingActivityEnumOptions.specialOrCriminalData.no"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "POSSIBLE",
|
||||||
|
label: t("processingActivityEnumOptions.specialOrCriminalData.possible"),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -52,18 +63,38 @@ export function SpecialOrCriminalDataOptions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function LawfulBasisOptions() {
|
export function LawfulBasisOptions() {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const options: Array<{
|
const options: Array<{
|
||||||
value: ProcessingActivityLawfulBasis;
|
value: ProcessingActivityLawfulBasis;
|
||||||
label: string;
|
label: string;
|
||||||
}> = [
|
}> = [
|
||||||
{ value: "CONSENT", label: __("Consent") },
|
{
|
||||||
{ value: "CONTRACTUAL_NECESSITY", label: __("Contractual Necessity") },
|
value: "CONSENT",
|
||||||
{ value: "LEGAL_OBLIGATION", label: __("Legal Obligation") },
|
label: t("processingActivityEnumOptions.lawfulBasis.consent"),
|
||||||
{ value: "LEGITIMATE_INTEREST", label: __("Legitimate Interest") },
|
},
|
||||||
{ value: "PUBLIC_TASK", label: __("Public Task") },
|
{
|
||||||
{ value: "VITAL_INTERESTS", label: __("Vital Interests") },
|
value: "CONTRACTUAL_NECESSITY",
|
||||||
|
label: t(
|
||||||
|
"processingActivityEnumOptions.lawfulBasis.contractualNecessity",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "LEGAL_OBLIGATION",
|
||||||
|
label: t("processingActivityEnumOptions.lawfulBasis.legalObligation"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "LEGITIMATE_INTEREST",
|
||||||
|
label: t("processingActivityEnumOptions.lawfulBasis.legitimateInterest"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "PUBLIC_TASK",
|
||||||
|
label: t("processingActivityEnumOptions.lawfulBasis.publicTask"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "VITAL_INTERESTS",
|
||||||
|
label: t("processingActivityEnumOptions.lawfulBasis.vitalInterests"),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -79,17 +110,23 @@ export function LawfulBasisOptions() {
|
|||||||
|
|
||||||
export function getLawfulBasisLabel(
|
export function getLawfulBasisLabel(
|
||||||
value: ProcessingActivityLawfulBasis | null | undefined,
|
value: ProcessingActivityLawfulBasis | null | undefined,
|
||||||
__: (key: string) => string,
|
t: Translator,
|
||||||
): string {
|
): string {
|
||||||
if (!value) return "-";
|
if (!value) return "-";
|
||||||
|
|
||||||
const labels = {
|
const labels = {
|
||||||
CONSENT: __("Consent"),
|
CONSENT:
|
||||||
CONTRACTUAL_NECESSITY: __("Contractual Necessity"),
|
t("processingActivityEnumOptions.lawfulBasis.consent"),
|
||||||
LEGAL_OBLIGATION: __("Legal Obligation"),
|
CONTRACTUAL_NECESSITY:
|
||||||
LEGITIMATE_INTEREST: __("Legitimate Interest"),
|
t("processingActivityEnumOptions.lawfulBasis.contractualNecessity"),
|
||||||
PUBLIC_TASK: __("Public Task"),
|
LEGAL_OBLIGATION:
|
||||||
VITAL_INTERESTS: __("Vital Interests"),
|
t("processingActivityEnumOptions.lawfulBasis.legalObligation"),
|
||||||
|
LEGITIMATE_INTEREST:
|
||||||
|
t("processingActivityEnumOptions.lawfulBasis.legitimateInterest"),
|
||||||
|
PUBLIC_TASK:
|
||||||
|
t("processingActivityEnumOptions.lawfulBasis.publicTask"),
|
||||||
|
VITAL_INTERESTS:
|
||||||
|
t("processingActivityEnumOptions.lawfulBasis.vitalInterests"),
|
||||||
};
|
};
|
||||||
|
|
||||||
return labels[value] || value;
|
return labels[value] || value;
|
||||||
@@ -97,38 +134,63 @@ export function getLawfulBasisLabel(
|
|||||||
|
|
||||||
export function getResidualRiskLabel(
|
export function getResidualRiskLabel(
|
||||||
value: "LOW" | "MEDIUM" | "HIGH" | null | undefined,
|
value: "LOW" | "MEDIUM" | "HIGH" | null | undefined,
|
||||||
__: (key: string) => string,
|
t: Translator,
|
||||||
): string {
|
): string {
|
||||||
if (!value) return "-";
|
if (!value) return "-";
|
||||||
|
|
||||||
const labels = {
|
const labels = {
|
||||||
LOW: __("Low"),
|
LOW: t("processingActivityEnumOptions.residualRisk.low") || "Low",
|
||||||
MEDIUM: __("Medium"),
|
MEDIUM: t("processingActivityEnumOptions.residualRisk.medium") || "Medium",
|
||||||
HIGH: __("High"),
|
HIGH: t("processingActivityEnumOptions.residualRisk.high") || "High",
|
||||||
};
|
};
|
||||||
|
|
||||||
return labels[value] || value;
|
return labels[value] || value;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TransferSafeguardsOptions() {
|
export function TransferSafeguardsOptions() {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const options: Array<{
|
const options: Array<{
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
}> = [
|
}> = [
|
||||||
{ value: "__NONE__", label: __("None") },
|
{
|
||||||
|
value: "__NONE__",
|
||||||
|
label: t("processingActivityEnumOptions.transferSafeguards.none"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: "STANDARD_CONTRACTUAL_CLAUSES",
|
value: "STANDARD_CONTRACTUAL_CLAUSES",
|
||||||
label: __("Standard Contractual Clauses"),
|
label: t(
|
||||||
|
"processingActivityEnumOptions.transferSafeguards.standardContractualClauses",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "BINDING_CORPORATE_RULES",
|
||||||
|
label: t(
|
||||||
|
"processingActivityEnumOptions.transferSafeguards.bindingCorporateRules",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "ADEQUACY_DECISION",
|
||||||
|
label: t(
|
||||||
|
"processingActivityEnumOptions.transferSafeguards.adequacyDecision",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "DEROGATIONS",
|
||||||
|
label: t("processingActivityEnumOptions.transferSafeguards.derogations"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "CODES_OF_CONDUCT",
|
||||||
|
label: t(
|
||||||
|
"processingActivityEnumOptions.transferSafeguards.codesOfConduct",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{ value: "BINDING_CORPORATE_RULES", label: __("Binding Corporate Rules") },
|
|
||||||
{ value: "ADEQUACY_DECISION", label: __("Adequacy Decision") },
|
|
||||||
{ value: "DEROGATIONS", label: __("Derogations") },
|
|
||||||
{ value: "CODES_OF_CONDUCT", label: __("Codes of Conduct") },
|
|
||||||
{
|
{
|
||||||
value: "CERTIFICATION_MECHANISMS",
|
value: "CERTIFICATION_MECHANISMS",
|
||||||
label: __("Certification Mechanisms"),
|
label: t(
|
||||||
|
"processingActivityEnumOptions.transferSafeguards.certificationMechanisms",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -144,14 +206,24 @@ export function TransferSafeguardsOptions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function DataProtectionImpactAssessmentOptions() {
|
export function DataProtectionImpactAssessmentOptions() {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const options: Array<{
|
const options: Array<{
|
||||||
value: ProcessingActivityDataProtectionImpactAssessment;
|
value: ProcessingActivityDataProtectionImpactAssessment;
|
||||||
label: string;
|
label: string;
|
||||||
}> = [
|
}> = [
|
||||||
{ value: "NEEDED", label: __("Needed") },
|
{
|
||||||
{ value: "NOT_NEEDED", label: __("Not Needed") },
|
value: "NEEDED",
|
||||||
|
label: t(
|
||||||
|
"processingActivityEnumOptions.dataProtectionImpactAssessment.needed",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "NOT_NEEDED",
|
||||||
|
label: t(
|
||||||
|
"processingActivityEnumOptions.dataProtectionImpactAssessment.notNeeded",
|
||||||
|
),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -166,14 +238,22 @@ export function DataProtectionImpactAssessmentOptions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function TransferImpactAssessmentOptions() {
|
export function TransferImpactAssessmentOptions() {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const options: Array<{
|
const options: Array<{
|
||||||
value: ProcessingActivityTransferImpactAssessment;
|
value: ProcessingActivityTransferImpactAssessment;
|
||||||
label: string;
|
label: string;
|
||||||
}> = [
|
}> = [
|
||||||
{ value: "NEEDED", label: __("Needed") },
|
{
|
||||||
{ value: "NOT_NEEDED", label: __("Not Needed") },
|
value: "NEEDED",
|
||||||
|
label: t("processingActivityEnumOptions.transferImpactAssessment.needed"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "NOT_NEEDED",
|
||||||
|
label: t(
|
||||||
|
"processingActivityEnumOptions.transferImpactAssessment.notNeeded",
|
||||||
|
),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -188,14 +268,20 @@ export function TransferImpactAssessmentOptions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function RoleOptions() {
|
export function RoleOptions() {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const options: Array<{
|
const options: Array<{
|
||||||
value: "CONTROLLER" | "PROCESSOR";
|
value: "CONTROLLER" | "PROCESSOR";
|
||||||
label: string;
|
label: string;
|
||||||
}> = [
|
}> = [
|
||||||
{ value: "CONTROLLER", label: __("Controller") },
|
{
|
||||||
{ value: "PROCESSOR", label: __("Processor") },
|
value: "CONTROLLER",
|
||||||
|
label: t("processingActivityEnumOptions.roles.controller"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "PROCESSOR",
|
||||||
|
label: t("processingActivityEnumOptions.roles.processor"),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -19,11 +19,28 @@
|
|||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { faviconUrl } from "@probo/helpers";
|
import { faviconUrl } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
import {
|
||||||
import { Avatar, Badge, Button, Field, IconCrossLargeX, Option, Select } from "@probo/ui";
|
Avatar,
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Field,
|
||||||
|
IconCrossLargeX,
|
||||||
|
Option,
|
||||||
|
Select,
|
||||||
|
} from "@probo/ui";
|
||||||
import { type ComponentProps, Suspense, useEffect, useState } from "react";
|
import { type ComponentProps, Suspense, useEffect, useState } from "react";
|
||||||
import { type Control, Controller, type FieldValues, type Path } from "react-hook-form";
|
import {
|
||||||
import { type PreloadedQuery, usePreloadedQuery, useQueryLoader } from "react-relay";
|
type Control,
|
||||||
|
Controller,
|
||||||
|
type FieldValues,
|
||||||
|
type Path,
|
||||||
|
} from "react-hook-form";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
type PreloadedQuery,
|
||||||
|
usePreloadedQuery,
|
||||||
|
useQueryLoader,
|
||||||
|
} from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
import type { ThirdPartiesMultiSelectFieldQuery } from "#/__generated__/core/ThirdPartiesMultiSelectFieldQuery.graphql";
|
import type { ThirdPartiesMultiSelectFieldQuery } from "#/__generated__/core/ThirdPartiesMultiSelectFieldQuery.graphql";
|
||||||
@@ -32,10 +49,7 @@ const thirdPartiesQuery = graphql`
|
|||||||
query ThirdPartiesMultiSelectFieldQuery($organizationId: ID!) {
|
query ThirdPartiesMultiSelectFieldQuery($organizationId: ID!) {
|
||||||
organization: node(id: $organizationId) {
|
organization: node(id: $organizationId) {
|
||||||
... on Organization {
|
... on Organization {
|
||||||
thirdParties(
|
thirdParties(first: 100, orderBy: { direction: ASC, field: NAME }) {
|
||||||
first: 100
|
|
||||||
orderBy: { direction: ASC, field: NAME }
|
|
||||||
) {
|
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
@@ -64,12 +78,9 @@ type Props<T extends FieldValues = FieldValues> = {
|
|||||||
selectedThirdParties?: ThirdParty[];
|
selectedThirdParties?: ThirdParty[];
|
||||||
} & ComponentProps<typeof Field>;
|
} & ComponentProps<typeof Field>;
|
||||||
|
|
||||||
export function ThirdPartiesMultiSelectField<T extends FieldValues = FieldValues>({
|
export function ThirdPartiesMultiSelectField<
|
||||||
organizationId,
|
T extends FieldValues = FieldValues,
|
||||||
control,
|
>({ organizationId, control, selectedThirdParties = [], ...props }: Props<T>) {
|
||||||
selectedThirdParties = [],
|
|
||||||
...props
|
|
||||||
}: Props<T>) {
|
|
||||||
const [queryRef, loadQuery]
|
const [queryRef, loadQuery]
|
||||||
= useQueryLoader<ThirdPartiesMultiSelectFieldQuery>(thirdPartiesQuery);
|
= useQueryLoader<ThirdPartiesMultiSelectFieldQuery>(thirdPartiesQuery);
|
||||||
|
|
||||||
@@ -103,14 +114,21 @@ export function ThirdPartiesMultiSelectField<T extends FieldValues = FieldValues
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ThirdPartiesMultiSelectWithQuery<T extends FieldValues = FieldValues>(
|
function ThirdPartiesMultiSelectWithQuery<T extends FieldValues = FieldValues>(
|
||||||
props: Pick<Props<T>, "control" | "name" | "disabled" | "selectedThirdParties"> & {
|
props: Pick<
|
||||||
|
Props<T>,
|
||||||
|
"control" | "name" | "disabled" | "selectedThirdParties"
|
||||||
|
> & {
|
||||||
queryRef: PreloadedQuery<ThirdPartiesMultiSelectFieldQuery>;
|
queryRef: PreloadedQuery<ThirdPartiesMultiSelectFieldQuery>;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const { name, control, selectedThirdParties = [] } = props;
|
const { name, control, selectedThirdParties = [] } = props;
|
||||||
const data = usePreloadedQuery<ThirdPartiesMultiSelectFieldQuery>(thirdPartiesQuery, props.queryRef);
|
const data = usePreloadedQuery<ThirdPartiesMultiSelectFieldQuery>(
|
||||||
const thirdParties = data.organization?.thirdParties?.edges.map(edge => edge.node) ?? [];
|
thirdPartiesQuery,
|
||||||
|
props.queryRef,
|
||||||
|
);
|
||||||
|
const thirdParties
|
||||||
|
= data.organization?.thirdParties?.edges.map(edge => edge.node) ?? [];
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const allThirdParties: ThirdParty[] = [...thirdParties];
|
const allThirdParties: ThirdParty[] = [...thirdParties];
|
||||||
@@ -128,10 +146,16 @@ function ThirdPartiesMultiSelectWithQuery<T extends FieldValues = FieldValues>(
|
|||||||
control={control}
|
control={control}
|
||||||
name={name as Path<T>}
|
name={name as Path<T>}
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
const selectedThirdPartyIds = (Array.isArray(field.value) ? field.value : []) as string[];
|
const selectedThirdPartyIds = (
|
||||||
|
Array.isArray(field.value) ? field.value : []
|
||||||
|
) as string[];
|
||||||
|
|
||||||
const selectedThirdParties = allThirdParties.filter(v => selectedThirdPartyIds.includes(v.id));
|
const selectedThirdParties = allThirdParties.filter(v =>
|
||||||
const availableThirdParties = allThirdParties.filter(v => !selectedThirdPartyIds.includes(v.id));
|
selectedThirdPartyIds.includes(v.id),
|
||||||
|
);
|
||||||
|
const availableThirdParties = allThirdParties.filter(
|
||||||
|
v => !selectedThirdPartyIds.includes(v.id),
|
||||||
|
);
|
||||||
|
|
||||||
const handleAddThirdParty = (thirdPartyId: string) => {
|
const handleAddThirdParty = (thirdPartyId: string) => {
|
||||||
const newValue = [...selectedThirdPartyIds, thirdPartyId];
|
const newValue = [...selectedThirdPartyIds, thirdPartyId];
|
||||||
@@ -140,7 +164,9 @@ function ThirdPartiesMultiSelectWithQuery<T extends FieldValues = FieldValues>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveThirdParty = (thirdPartyId: string) => {
|
const handleRemoveThirdParty = (thirdPartyId: string) => {
|
||||||
const newValue = selectedThirdPartyIds.filter((id: string) => id !== thirdPartyId);
|
const newValue = selectedThirdPartyIds.filter(
|
||||||
|
(id: string) => id !== thirdPartyId,
|
||||||
|
);
|
||||||
field.onChange(newValue);
|
field.onChange(newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -151,7 +177,7 @@ function ThirdPartiesMultiSelectWithQuery<T extends FieldValues = FieldValues>(
|
|||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
id={name}
|
id={name}
|
||||||
variant="editor"
|
variant="editor"
|
||||||
placeholder={__("Add third parties...")}
|
placeholder={t("thirdPartiesMultiSelectField.addPlaceholder")}
|
||||||
onValueChange={handleAddThirdParty}
|
onValueChange={handleAddThirdParty}
|
||||||
key={`${selectedThirdPartyIds.length}-${thirdParties.length}`}
|
key={`${selectedThirdPartyIds.length}-${thirdParties.length}`}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
@@ -160,7 +186,11 @@ function ThirdPartiesMultiSelectWithQuery<T extends FieldValues = FieldValues>(
|
|||||||
onOpenChange={setIsOpen}
|
onOpenChange={setIsOpen}
|
||||||
>
|
>
|
||||||
{availableThirdParties.map(thirdParty => (
|
{availableThirdParties.map(thirdParty => (
|
||||||
<Option key={thirdParty.id} value={thirdParty.id} className="flex gap-2">
|
<Option
|
||||||
|
key={thirdParty.id}
|
||||||
|
value={thirdParty.id}
|
||||||
|
className="flex gap-2"
|
||||||
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
name={thirdParty.name}
|
name={thirdParty.name}
|
||||||
src={faviconUrl(thirdParty.websiteUrl)}
|
src={faviconUrl(thirdParty.websiteUrl)}
|
||||||
@@ -182,7 +212,11 @@ function ThirdPartiesMultiSelectWithQuery<T extends FieldValues = FieldValues>(
|
|||||||
{selectedThirdParties.length > 0 && (
|
{selectedThirdParties.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{selectedThirdParties.map(thirdParty => (
|
{selectedThirdParties.map(thirdParty => (
|
||||||
<Badge key={thirdParty.id} variant="neutral" className="flex items-center gap-2">
|
<Badge
|
||||||
|
key={thirdParty.id}
|
||||||
|
variant="neutral"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
name={thirdParty.name}
|
name={thirdParty.name}
|
||||||
src={faviconUrl(thirdParty.websiteUrl)}
|
src={faviconUrl(thirdParty.websiteUrl)}
|
||||||
@@ -202,9 +236,10 @@ function ThirdPartiesMultiSelectWithQuery<T extends FieldValues = FieldValues>(
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedThirdParties.length === 0 && availableThirdParties.length === 0 && (
|
{selectedThirdParties.length === 0
|
||||||
|
&& availableThirdParties.length === 0 && (
|
||||||
<div className="text-sm text-txt-secondary py-2">
|
<div className="text-sm text-txt-secondary py-2">
|
||||||
{__("No third parties available")}
|
{t("thirdPartiesMultiSelectField.empty")}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,8 +18,6 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { sprintf } from "@probo/helpers";
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
@@ -37,6 +35,7 @@ import {
|
|||||||
import { MeasureBadge } from "@probo/ui/src/Molecules/Badge/MeasureBadge";
|
import { MeasureBadge } from "@probo/ui/src/Molecules/Badge/MeasureBadge";
|
||||||
import { clsx } from "clsx";
|
import { clsx } from "clsx";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFragment } from "react-relay";
|
import { useFragment } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
@@ -83,7 +82,7 @@ type Props<Params> = {
|
|||||||
* Reusable component that displays a list of linked measures
|
* Reusable component that displays a list of linked measures
|
||||||
*/
|
*/
|
||||||
export function LinkedMeasuresCard<Params>(props: Props<Params>) {
|
export function LinkedMeasuresCard<Params>(props: Props<Params>) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const [limit, setLimit] = useState<number | null>(
|
const [limit, setLimit] = useState<number | null>(
|
||||||
props.variant === "card" ? 4 : null,
|
props.variant === "card" ? 4 : null,
|
||||||
);
|
);
|
||||||
@@ -123,7 +122,9 @@ export function LinkedMeasuresCard<Params>(props: Props<Params>) {
|
|||||||
<Wrapper padded className="space-y-[10px]">
|
<Wrapper padded className="space-y-[10px]">
|
||||||
{variant === "card" && (
|
{variant === "card" && (
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<div className="text-lg font-semibold">{__("Measures")}</div>
|
<div className="text-lg font-semibold">
|
||||||
|
{t("linkedMeasuresCard.title")}
|
||||||
|
</div>
|
||||||
{!props.readOnly && (
|
{!props.readOnly && (
|
||||||
<LinkedMeasureDialog
|
<LinkedMeasureDialog
|
||||||
connectionId={props.connectionId}
|
connectionId={props.connectionId}
|
||||||
@@ -133,7 +134,7 @@ export function LinkedMeasuresCard<Params>(props: Props<Params>) {
|
|||||||
onUnlink={onDetach}
|
onUnlink={onDetach}
|
||||||
>
|
>
|
||||||
<Button variant="tertiary" icon={IconPlusLarge}>
|
<Button variant="tertiary" icon={IconPlusLarge}>
|
||||||
{__("Link measure")}
|
{t("linkedMeasuresCard.actions.link")}
|
||||||
</Button>
|
</Button>
|
||||||
</LinkedMeasureDialog>
|
</LinkedMeasureDialog>
|
||||||
)}
|
)}
|
||||||
@@ -142,8 +143,8 @@ export function LinkedMeasuresCard<Params>(props: Props<Params>) {
|
|||||||
<Table className={clsx(variant === "card" && "bg-invert")}>
|
<Table className={clsx(variant === "card" && "bg-invert")}>
|
||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>{__("Name")}</Th>
|
<Th>{t("linkedMeasuresCard.columns.name")}</Th>
|
||||||
<Th>{__("State")}</Th>
|
<Th>{t("linkedMeasuresCard.columns.state")}</Th>
|
||||||
{!props.readOnly && <Th></Th>}
|
{!props.readOnly && <Th></Th>}
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
@@ -154,7 +155,7 @@ export function LinkedMeasuresCard<Params>(props: Props<Params>) {
|
|||||||
colSpan={props.readOnly ? 2 : 3}
|
colSpan={props.readOnly ? 2 : 3}
|
||||||
className="text-center text-txt-secondary"
|
className="text-center text-txt-secondary"
|
||||||
>
|
>
|
||||||
{__("No measures linked")}
|
{t("linkedMeasuresCard.empty")}
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
)}
|
||||||
@@ -175,7 +176,7 @@ export function LinkedMeasuresCard<Params>(props: Props<Params>) {
|
|||||||
onUnlink={onDetach}
|
onUnlink={onDetach}
|
||||||
>
|
>
|
||||||
<TrButton colspan={3} icon={IconPlusLarge}>
|
<TrButton colspan={3} icon={IconPlusLarge}>
|
||||||
{__("Link measure")}
|
{t("linkedMeasuresCard.actions.link")}
|
||||||
</TrButton>
|
</TrButton>
|
||||||
</LinkedMeasureDialog>
|
</LinkedMeasureDialog>
|
||||||
)}
|
)}
|
||||||
@@ -188,7 +189,9 @@ export function LinkedMeasuresCard<Params>(props: Props<Params>) {
|
|||||||
className="mt-3 mx-auto"
|
className="mt-3 mx-auto"
|
||||||
icon={IconChevronDown}
|
icon={IconChevronDown}
|
||||||
>
|
>
|
||||||
{sprintf(__("Show %s more"), props.measures.length - limit)}
|
{t("linkedMeasuresCard.actions.showMore", {
|
||||||
|
count: props.measures.length - limit,
|
||||||
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
@@ -202,7 +205,7 @@ function MeasureRow(props: {
|
|||||||
}) {
|
}) {
|
||||||
const measure = useFragment(linkedMeasureFragment, props.measure);
|
const measure = useFragment(linkedMeasureFragment, props.measure);
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tr to={`/organizations/${organizationId}/measures/${measure.id}`}>
|
<Tr to={`/organizations/${organizationId}/measures/${measure.id}`}>
|
||||||
@@ -217,7 +220,7 @@ function MeasureRow(props: {
|
|||||||
onClick={() => props.onClick(measure.id)}
|
onClick={() => props.onClick(measure.id)}
|
||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
>
|
>
|
||||||
{__("Unlink")}
|
{t("linkedMeasuresCard.actions.unlink")}
|
||||||
</Button>
|
</Button>
|
||||||
</Td>
|
</Td>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
@@ -35,6 +34,7 @@ import {
|
|||||||
Spinner,
|
Spinner,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { type ReactNode, Suspense, useMemo, useState } from "react";
|
import { type ReactNode, Suspense, useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { usePaginatedMeasures } from "#/hooks/graph/usePaginatedMeasures";
|
import { usePaginatedMeasures } from "#/hooks/graph/usePaginatedMeasures";
|
||||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||||
@@ -49,16 +49,16 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function LinkedMeasureDialog({ children, ...props }: Props) {
|
export function LinkedMeasureDialog({ children, ...props }: Props) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog trigger={children} title={__("Link measures")}>
|
<Dialog trigger={children} title={t("linkedMeasuresDialog.title")}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Suspense fallback={<Spinner centered />}>
|
<Suspense fallback={<Spinner centered />}>
|
||||||
<LinkedMeasuresDialogContent {...props} />
|
<LinkedMeasuresDialogContent {...props} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogFooter exitLabel={__("Close")} />
|
<DialogFooter exitLabel={t("linkedMeasuresDialog.actions.close")} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -67,10 +67,13 @@ function LinkedMeasuresDialogContent(props: Omit<Props, "children">) {
|
|||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const { data, loadNext, hasNext, isLoadingNext }
|
const { data, loadNext, hasNext, isLoadingNext }
|
||||||
= usePaginatedMeasures(organizationId);
|
= usePaginatedMeasures(organizationId);
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [category, setCategory] = useState<string | null>(null);
|
const [category, setCategory] = useState<string | null>(null);
|
||||||
const measures = useMemo(() => data.measures?.edges?.map(edge => edge.node) ?? [], [data.measures]);
|
const measures = useMemo(
|
||||||
|
() => data.measures?.edges?.map(edge => edge.node) ?? [],
|
||||||
|
[data.measures],
|
||||||
|
);
|
||||||
const linkedIds = useMemo(() => {
|
const linkedIds = useMemo(() => {
|
||||||
return new Set(props.linkedMeasures?.map(m => m.id) ?? []);
|
return new Set(props.linkedMeasures?.map(m => m.id) ?? []);
|
||||||
}, [props.linkedMeasures]);
|
}, [props.linkedMeasures]);
|
||||||
@@ -94,12 +97,12 @@ function LinkedMeasuresDialogContent(props: Omit<Props, "children">) {
|
|||||||
<div className="flex items-center gap-2 sticky top-0 relative py-4 bg-linear-to-b from-50% from-level-2 to-level-2/0 px-6">
|
<div className="flex items-center gap-2 sticky top-0 relative py-4 bg-linear-to-b from-50% from-level-2 to-level-2/0 px-6">
|
||||||
<Input
|
<Input
|
||||||
icon={IconMagnifyingGlass}
|
icon={IconMagnifyingGlass}
|
||||||
placeholder={__("Search measures...")}
|
placeholder={t("linkedMeasuresDialog.searchPlaceholder")}
|
||||||
onValueChange={setSearch}
|
onValueChange={setSearch}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
value={category ?? ""}
|
value={category ?? ""}
|
||||||
placeholder={__("All categories")}
|
placeholder={t("linkedMeasuresDialog.allCategories")}
|
||||||
onValueChange={setCategory}
|
onValueChange={setCategory}
|
||||||
className="max-w-[180px]"
|
className="max-w-[180px]"
|
||||||
>
|
>
|
||||||
@@ -141,7 +144,7 @@ type RowProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function MeasureRow(props: RowProps) {
|
function MeasureRow(props: RowProps) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const isLinked = props.linkedMeasures.has(props.measure.id);
|
const isLinked = props.linkedMeasures.has(props.measure.id);
|
||||||
const onClick = isLinked ? props.onUnlink : props.onLink;
|
const onClick = isLinked ? props.onUnlink : props.onLink;
|
||||||
@@ -163,7 +166,9 @@ function MeasureRow(props: RowProps) {
|
|||||||
<span>
|
<span>
|
||||||
<IconComponent size={16} />
|
<IconComponent size={16} />
|
||||||
{" "}
|
{" "}
|
||||||
{isLinked ? __("Unlink") : __("Link")}
|
{isLinked
|
||||||
|
? t("linkedMeasuresDialog.actions.unlink")
|
||||||
|
: t("linkedMeasuresDialog.actions.link")}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -21,9 +21,7 @@
|
|||||||
import {
|
import {
|
||||||
getObligationStatusLabel,
|
getObligationStatusLabel,
|
||||||
getObligationStatusVariant,
|
getObligationStatusVariant,
|
||||||
sprintf,
|
|
||||||
} from "@probo/helpers";
|
} from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
@@ -41,6 +39,7 @@ import {
|
|||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { clsx } from "clsx";
|
import { clsx } from "clsx";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFragment } from "react-relay";
|
import { useFragment } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
@@ -84,7 +83,7 @@ type Props<Params> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function LinkedObligationsCard<Params>(props: Props<Params>) {
|
export function LinkedObligationsCard<Params>(props: Props<Params>) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const [limit, setLimit] = useState<number | null>(
|
const [limit, setLimit] = useState<number | null>(
|
||||||
props.variant === "card" ? 4 : null,
|
props.variant === "card" ? 4 : null,
|
||||||
);
|
);
|
||||||
@@ -126,7 +125,9 @@ export function LinkedObligationsCard<Params>(props: Props<Params>) {
|
|||||||
<Wrapper padded className="space-y-[10px]">
|
<Wrapper padded className="space-y-[10px]">
|
||||||
{variant === "card" && (
|
{variant === "card" && (
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<div className="text-lg font-semibold">{__("Obligations")}</div>
|
<div className="text-lg font-semibold">
|
||||||
|
{t("linkedObligationsCard.title")}
|
||||||
|
</div>
|
||||||
{!props.readOnly && (
|
{!props.readOnly && (
|
||||||
<LinkedObligationDialog
|
<LinkedObligationDialog
|
||||||
connectionId={props.connectionId}
|
connectionId={props.connectionId}
|
||||||
@@ -136,7 +137,7 @@ export function LinkedObligationsCard<Params>(props: Props<Params>) {
|
|||||||
onUnlink={onDetach}
|
onUnlink={onDetach}
|
||||||
>
|
>
|
||||||
<Button variant="tertiary" icon={IconPlusLarge}>
|
<Button variant="tertiary" icon={IconPlusLarge}>
|
||||||
{__("Link obligation")}
|
{t("linkedObligationsCard.actions.link")}
|
||||||
</Button>
|
</Button>
|
||||||
</LinkedObligationDialog>
|
</LinkedObligationDialog>
|
||||||
)}
|
)}
|
||||||
@@ -145,10 +146,10 @@ export function LinkedObligationsCard<Params>(props: Props<Params>) {
|
|||||||
<Table className={clsx(variant === "card" && "bg-invert")}>
|
<Table className={clsx(variant === "card" && "bg-invert")}>
|
||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>{__("Area")}</Th>
|
<Th>{t("linkedObligationsCard.columns.area")}</Th>
|
||||||
<Th>{__("Source")}</Th>
|
<Th>{t("linkedObligationsCard.columns.source")}</Th>
|
||||||
<Th>{__("Status")}</Th>
|
<Th>{t("linkedObligationsCard.columns.status")}</Th>
|
||||||
<Th>{__("Owner")}</Th>
|
<Th>{t("linkedObligationsCard.columns.owner")}</Th>
|
||||||
{!props.readOnly && <Th></Th>}
|
{!props.readOnly && <Th></Th>}
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
@@ -159,7 +160,7 @@ export function LinkedObligationsCard<Params>(props: Props<Params>) {
|
|||||||
colSpan={props.readOnly ? 4 : 5}
|
colSpan={props.readOnly ? 4 : 5}
|
||||||
className="text-center text-txt-secondary"
|
className="text-center text-txt-secondary"
|
||||||
>
|
>
|
||||||
{__("No obligations linked")}
|
{t("linkedObligationsCard.empty")}
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
)}
|
||||||
@@ -180,7 +181,7 @@ export function LinkedObligationsCard<Params>(props: Props<Params>) {
|
|||||||
onUnlink={onDetach}
|
onUnlink={onDetach}
|
||||||
>
|
>
|
||||||
<TrButton colspan={5} icon={IconPlusLarge}>
|
<TrButton colspan={5} icon={IconPlusLarge}>
|
||||||
{__("Link obligation")}
|
{t("linkedObligationsCard.actions.link")}
|
||||||
</TrButton>
|
</TrButton>
|
||||||
</LinkedObligationDialog>
|
</LinkedObligationDialog>
|
||||||
)}
|
)}
|
||||||
@@ -193,7 +194,9 @@ export function LinkedObligationsCard<Params>(props: Props<Params>) {
|
|||||||
className="mt-3 mx-auto"
|
className="mt-3 mx-auto"
|
||||||
icon={IconChevronDown}
|
icon={IconChevronDown}
|
||||||
>
|
>
|
||||||
{sprintf(__("Show %s more"), props.obligations.length - limit)}
|
{t("linkedObligationsCard.actions.showMore", {
|
||||||
|
count: props.obligations.length - limit,
|
||||||
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
@@ -205,7 +208,7 @@ function ObligationRow(props: {
|
|||||||
onClick: (obligationId: string) => void;
|
onClick: (obligationId: string) => void;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const obligation = useFragment(linkedObligationFragment, props.obligation);
|
const obligation = useFragment(linkedObligationFragment, props.obligation);
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
|
|
||||||
@@ -217,18 +220,20 @@ function ObligationRow(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tr to={detailsUrl}>
|
<Tr to={detailsUrl}>
|
||||||
<Td>{obligation.area || __("No area specified")}</Td>
|
<Td>{obligation.area || t("linkedObligationsCard.noArea")}</Td>
|
||||||
<Td>{obligation.source || __("No source specified")}</Td>
|
<Td>{obligation.source || t("linkedObligationsCard.noSource")}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<Badge variant={getObligationStatusVariant(obligation.status)}>
|
<Badge variant={getObligationStatusVariant(obligation.status)}>
|
||||||
{getObligationStatusLabel(obligation.status)}
|
{getObligationStatusLabel(obligation.status)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>{obligation.owner?.fullName || __("Unassigned")}</Td>
|
<Td>
|
||||||
|
{obligation.owner?.fullName || t("linkedObligationsCard.unassigned")}
|
||||||
|
</Td>
|
||||||
{!props.readOnly && (
|
{!props.readOnly && (
|
||||||
<Td noLink width={50} className="text-end">
|
<Td noLink width={50} className="text-end">
|
||||||
<Button variant="secondary" icon={IconTrashCan} onClick={onDetach}>
|
<Button variant="secondary" icon={IconTrashCan} onClick={onDetach}>
|
||||||
{__("Unlink")}
|
{t("linkedObligationsCard.actions.unlink")}
|
||||||
</Button>
|
</Button>
|
||||||
</Td>
|
</Td>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import {
|
|||||||
getObligationStatusLabel,
|
getObligationStatusLabel,
|
||||||
getObligationStatusVariant,
|
getObligationStatusVariant,
|
||||||
} from "@probo/helpers";
|
} from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
@@ -37,6 +36,7 @@ import {
|
|||||||
Spinner,
|
Spinner,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { type ReactNode, Suspense, useMemo, useState } from "react";
|
import { type ReactNode, Suspense, useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useLazyLoadQuery, usePaginationFragment } from "react-relay";
|
import { useLazyLoadQuery, usePaginationFragment } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
@@ -102,16 +102,16 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function LinkedObligationDialog({ children, ...props }: Props) {
|
export function LinkedObligationDialog({ children, ...props }: Props) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog trigger={children} title={__("Link obligations")}>
|
<Dialog trigger={children} title={t("linkedObligationsDialog.title")}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Suspense fallback={<Spinner centered />}>
|
<Suspense fallback={<Spinner centered />}>
|
||||||
<LinkedObligationsDialogContent {...props} />
|
<LinkedObligationsDialogContent {...props} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogFooter exitLabel={__("Close")} />
|
<DialogFooter exitLabel={t("linkedObligationsDialog.actions.close")} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -125,12 +125,14 @@ function LinkedObligationsDialogContent(props: Omit<Props, "children">) {
|
|||||||
},
|
},
|
||||||
{ fetchPolicy: "network-only" },
|
{ fetchPolicy: "network-only" },
|
||||||
);
|
);
|
||||||
const { data, loadNext, hasNext, isLoadingNext }
|
const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment<
|
||||||
= usePaginationFragment<LinkedObligationsDialogQuery_fragment, LinkedObligationsDialogFragment$key>(
|
LinkedObligationsDialogQuery_fragment,
|
||||||
|
LinkedObligationsDialogFragment$key
|
||||||
|
>(
|
||||||
obligationsFragment,
|
obligationsFragment,
|
||||||
query.organization as LinkedObligationsDialogFragment$key,
|
query.organization as LinkedObligationsDialogFragment$key,
|
||||||
);
|
);
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const obligations = useMemo(
|
const obligations = useMemo(
|
||||||
() => data.obligations?.edges?.map(edge => edge.node) ?? [],
|
() => data.obligations?.edges?.map(edge => edge.node) ?? [],
|
||||||
@@ -156,7 +158,7 @@ function LinkedObligationsDialogContent(props: Omit<Props, "children">) {
|
|||||||
<div className="flex items-center gap-2 sticky top-0 relative py-4 bg-linear-to-b from-50% from-level-2 to-level-2/0 px-6">
|
<div className="flex items-center gap-2 sticky top-0 relative py-4 bg-linear-to-b from-50% from-level-2 to-level-2/0 px-6">
|
||||||
<Input
|
<Input
|
||||||
icon={IconMagnifyingGlass}
|
icon={IconMagnifyingGlass}
|
||||||
placeholder={__("Search obligations...")}
|
placeholder={t("linkedObligationsDialog.searchPlaceholder")}
|
||||||
onValueChange={setSearch}
|
onValueChange={setSearch}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -191,7 +193,7 @@ function ObligationRow(props: {
|
|||||||
onUnlink: (obligationId: string) => void;
|
onUnlink: (obligationId: string) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const isLinked = props.linkedObligations.has(props.obligation.id);
|
const isLinked = props.linkedObligations.has(props.obligation.id);
|
||||||
|
|
||||||
const onToggle = () => {
|
const onToggle = () => {
|
||||||
@@ -208,11 +210,12 @@ function ObligationRow(props: {
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="text-sm font-medium text-txt-primary truncate">
|
<div className="text-sm font-medium text-txt-primary truncate">
|
||||||
{props.obligation.area || __("No area specified")}
|
{props.obligation.area || t("linkedObligationsDialog.noArea")}
|
||||||
{props.obligation.source || __("No source specified")}
|
{props.obligation.source || t("linkedObligationsDialog.noSource")}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-txt-secondary">
|
<div className="text-xs text-txt-secondary">
|
||||||
{props.obligation.owner?.fullName || __("Unassigned")}
|
{props.obligation.owner?.fullName
|
||||||
|
|| t("linkedObligationsDialog.unassigned")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant={getObligationStatusVariant(props.obligation.status)}>
|
<Badge variant={getObligationStatusVariant(props.obligation.status)}>
|
||||||
@@ -227,7 +230,9 @@ function ObligationRow(props: {
|
|||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
className="ml-6"
|
className="ml-6"
|
||||||
>
|
>
|
||||||
{isLinked ? __("Unlink") : __("Link")}
|
{isLinked
|
||||||
|
? t("linkedObligationsDialog.actions.unlink")
|
||||||
|
: t("linkedObligationsDialog.actions.link")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,8 +18,6 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { sprintf } from "@probo/helpers";
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -30,6 +28,7 @@ import {
|
|||||||
useDialogRef,
|
useDialogRef,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
type DeleteOrganizationDialogProps = {
|
type DeleteOrganizationDialogProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -44,7 +43,7 @@ export function DeleteOrganizationDialog({
|
|||||||
onConfirm,
|
onConfirm,
|
||||||
isDeleting = false,
|
isDeleting = false,
|
||||||
}: DeleteOrganizationDialogProps) {
|
}: DeleteOrganizationDialogProps) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const [inputValue, setInputValue] = useState("");
|
const [inputValue, setInputValue] = useState("");
|
||||||
const dialogRef = useDialogRef();
|
const dialogRef = useDialogRef();
|
||||||
const isConfirmDisabled = inputValue !== organizationName || isDeleting;
|
const isConfirmDisabled = inputValue !== organizationName || isDeleting;
|
||||||
@@ -61,25 +60,21 @@ export function DeleteOrganizationDialog({
|
|||||||
className="max-w-lg"
|
className="max-w-lg"
|
||||||
ref={dialogRef}
|
ref={dialogRef}
|
||||||
trigger={children}
|
trigger={children}
|
||||||
title={__("Delete Organization")}
|
title={t("deleteOrganizationDialog.title")}
|
||||||
>
|
>
|
||||||
<DialogContent padded className="space-y-4">
|
<DialogContent padded className="space-y-4">
|
||||||
<p className="text-txt-secondary text-sm">
|
<p className="text-txt-secondary text-sm">
|
||||||
{sprintf(
|
{t("deleteOrganizationDialog.description", { organizationName })}
|
||||||
__("This will permanently delete the organization %s and all its data."),
|
|
||||||
organizationName,
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="text-red-600 text-sm font-medium">
|
<p className="text-red-600 text-sm font-medium">
|
||||||
{__("This action cannot be undone.")}
|
{t("deleteOrganizationDialog.warning")}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
label={sprintf(
|
label={t("deleteOrganizationDialog.confirmationLabel", {
|
||||||
__("To confirm deletion, type \"%s\" below:"),
|
|
||||||
organizationName,
|
organizationName,
|
||||||
)}
|
})}
|
||||||
type="text"
|
type="text"
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={e => setInputValue(e.target.value)}
|
onChange={e => setInputValue(e.target.value)}
|
||||||
@@ -96,7 +91,9 @@ export function DeleteOrganizationDialog({
|
|||||||
onClick={handleConfirm}
|
onClick={handleConfirm}
|
||||||
disabled={isConfirmDisabled}
|
disabled={isConfirmDisabled}
|
||||||
>
|
>
|
||||||
{isDeleting ? __("Deleting...") : __("Delete Organization")}
|
{isDeleting
|
||||||
|
? t("deleteOrganizationDialog.actions.deleting")
|
||||||
|
: t("deleteOrganizationDialog.actions.delete")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
IconTrashCan,
|
IconTrashCan,
|
||||||
@@ -31,6 +30,7 @@ import {
|
|||||||
Tr,
|
Tr,
|
||||||
TrButton,
|
TrButton,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFragment } from "react-relay";
|
import { useFragment } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ type Props<Params> = {
|
|||||||
* Reusable component that displays a list of linked risks
|
* Reusable component that displays a list of linked risks
|
||||||
*/
|
*/
|
||||||
export function LinkedRisksCard<Params>(props: Props<Params>) {
|
export function LinkedRisksCard<Params>(props: Props<Params>) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const onAttach = (riskId: string) => {
|
const onAttach = (riskId: string) => {
|
||||||
props.onAttach({
|
props.onAttach({
|
||||||
@@ -108,9 +108,9 @@ export function LinkedRisksCard<Params>(props: Props<Params>) {
|
|||||||
<Table>
|
<Table>
|
||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>{__("Name")}</Th>
|
<Th>{t("linkedRisksCard.columns.name")}</Th>
|
||||||
<Th>{__("Initial Risk")}</Th>
|
<Th>{t("linkedRisksCard.columns.initialRisk")}</Th>
|
||||||
<Th>{__("Residual Risk")}</Th>
|
<Th>{t("linkedRisksCard.columns.residualRisk")}</Th>
|
||||||
{!props.readOnly && <Th></Th>}
|
{!props.readOnly && <Th></Th>}
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
@@ -121,7 +121,7 @@ export function LinkedRisksCard<Params>(props: Props<Params>) {
|
|||||||
colSpan={props.readOnly ? 3 : 4}
|
colSpan={props.readOnly ? 3 : 4}
|
||||||
className="text-center text-txt-secondary"
|
className="text-center text-txt-secondary"
|
||||||
>
|
>
|
||||||
{__("No risks linked")}
|
{t("linkedRisksCard.empty")}
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
)}
|
||||||
@@ -141,7 +141,9 @@ export function LinkedRisksCard<Params>(props: Props<Params>) {
|
|||||||
onLink={onAttach}
|
onLink={onAttach}
|
||||||
onUnlink={onDetach}
|
onUnlink={onDetach}
|
||||||
>
|
>
|
||||||
<TrButton colspan={4}>{__("Link risk")}</TrButton>
|
<TrButton colspan={4}>
|
||||||
|
{t("linkedRisksCard.actions.link")}
|
||||||
|
</TrButton>
|
||||||
</LinkedRisksDialog>
|
</LinkedRisksDialog>
|
||||||
)}
|
)}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
@@ -157,7 +159,7 @@ function RiskRow(props: {
|
|||||||
}) {
|
}) {
|
||||||
const risk = useFragment(linkedRiskFragment, props.risk);
|
const risk = useFragment(linkedRiskFragment, props.risk);
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tr to={`/organizations/${organizationId}/risks/${risk.id}`}>
|
<Tr to={`/organizations/${organizationId}/risks/${risk.id}`}>
|
||||||
@@ -175,7 +177,7 @@ function RiskRow(props: {
|
|||||||
onClick={() => props.onClick(risk.id)}
|
onClick={() => props.onClick(risk.id)}
|
||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
>
|
>
|
||||||
{__("Unlink")}
|
{t("linkedRisksCard.actions.unlink")}
|
||||||
</Button>
|
</Button>
|
||||||
</Td>
|
</Td>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
@@ -34,6 +33,7 @@ import {
|
|||||||
Spinner,
|
Spinner,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { type ReactNode, Suspense, useMemo, useState } from "react";
|
import { type ReactNode, Suspense, useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useLazyLoadQuery } from "react-relay";
|
import { useLazyLoadQuery } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
@@ -70,16 +70,16 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function LinkedRisksDialog({ children, ...props }: Props) {
|
export function LinkedRisksDialog({ children, ...props }: Props) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog trigger={children} title={__("Link risks")}>
|
<Dialog trigger={children} title={t("linkedRisksDialog.title")}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Suspense fallback={<Spinner centered />}>
|
<Suspense fallback={<Spinner centered />}>
|
||||||
<LinkedRisksDialogContent {...props} />
|
<LinkedRisksDialogContent {...props} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogFooter exitLabel={__("Close")} />
|
<DialogFooter exitLabel={t("linkedRisksDialog.actions.close")} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -89,7 +89,7 @@ function LinkedRisksDialogContent(props: Omit<Props, "children">) {
|
|||||||
const data = useLazyLoadQuery<LinkedRisksDialogQuery>(risksQuery, {
|
const data = useLazyLoadQuery<LinkedRisksDialogQuery>(risksQuery, {
|
||||||
organizationId,
|
organizationId,
|
||||||
});
|
});
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [category, setCategory] = useState<string | null>(null);
|
const [category, setCategory] = useState<string | null>(null);
|
||||||
const risks = useMemo(
|
const risks = useMemo(
|
||||||
@@ -119,12 +119,12 @@ function LinkedRisksDialogContent(props: Omit<Props, "children">) {
|
|||||||
<div className="flex items-center gap-2 sticky top-0 relative py-4 bg-linear-to-b from-50% from-level-2 to-level-2/0 px-6">
|
<div className="flex items-center gap-2 sticky top-0 relative py-4 bg-linear-to-b from-50% from-level-2 to-level-2/0 px-6">
|
||||||
<Input
|
<Input
|
||||||
icon={IconMagnifyingGlass}
|
icon={IconMagnifyingGlass}
|
||||||
placeholder={__("Search risks...")}
|
placeholder={t("linkedRisksDialog.searchPlaceholder")}
|
||||||
onValueChange={setSearch}
|
onValueChange={setSearch}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
value={category ?? ""}
|
value={category ?? ""}
|
||||||
placeholder={__("All categories")}
|
placeholder={t("linkedRisksDialog.allCategories")}
|
||||||
onValueChange={setCategory}
|
onValueChange={setCategory}
|
||||||
className="max-w-[180px]"
|
className="max-w-[180px]"
|
||||||
>
|
>
|
||||||
@@ -165,7 +165,7 @@ type RowProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function RiskRow(props: RowProps) {
|
function RiskRow(props: RowProps) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const isLinked = props.linkedRisks.has(props.risk.id);
|
const isLinked = props.linkedRisks.has(props.risk.id);
|
||||||
const onClick = isLinked ? props.onUnlink : props.onLink;
|
const onClick = isLinked ? props.onUnlink : props.onLink;
|
||||||
@@ -187,7 +187,9 @@ function RiskRow(props: RowProps) {
|
|||||||
<span>
|
<span>
|
||||||
<IconComponent size={16} />
|
<IconComponent size={16} />
|
||||||
{" "}
|
{" "}
|
||||||
{isLinked ? __("Unlink") : __("Link")}
|
{isLinked
|
||||||
|
? t("linkedRisksDialog.actions.unlink")
|
||||||
|
: t("linkedRisksDialog.actions.link")}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -18,21 +18,21 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { ActionDropdown, Button, IconPencil, Skeleton } from "@probo/ui";
|
import { ActionDropdown, Button, IconPencil, Skeleton } from "@probo/ui";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Skeleton state for the framework control panel
|
* Skeleton state for the framework control panel
|
||||||
*/
|
*/
|
||||||
export function ControlSkeleton() {
|
export function ControlSkeleton() {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<Skeleton style={{ width: 72, height: 34 }} className="mb-3" />
|
<Skeleton style={{ width: 72, height: 34 }} className="mb-3" />
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button icon={IconPencil} variant="secondary" disabled>
|
<Button icon={IconPencil} variant="secondary" disabled>
|
||||||
{__("Edit control")}
|
{t("controlSkeleton.actions.edit")}
|
||||||
</Button>
|
</Button>
|
||||||
<ActionDropdown variant="secondary" />
|
<ActionDropdown variant="secondary" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,16 +18,16 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Button, IconPlusLarge, PageHeader, Skeleton } from "@probo/ui";
|
import { Button, IconPlusLarge, PageHeader, Skeleton } from "@probo/ui";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export function RisksPageSkeleton() {
|
export function RisksPageSkeleton() {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<PageHeader title={__("Risks")}>
|
<PageHeader title={t("risksPageSkeleton.title")}>
|
||||||
<Button icon={IconPlusLarge} disabled>
|
<Button icon={IconPlusLarge} disabled>
|
||||||
{__("New Risk")}
|
{t("risksPageSkeleton.actions.create")}
|
||||||
</Button>
|
</Button>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
|||||||
@@ -18,8 +18,6 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { sprintf } from "@probo/helpers";
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
@@ -37,6 +35,7 @@ import {
|
|||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { clsx } from "clsx";
|
import { clsx } from "clsx";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFragment } from "react-relay";
|
import { useFragment } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
@@ -94,8 +93,10 @@ type Props<Params> = {
|
|||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function LinkedStatementsOfApplicabilityCard<Params>(props: Props<Params>) {
|
export function LinkedStatementsOfApplicabilityCard<Params>(
|
||||||
const { __ } = useTranslate();
|
props: Props<Params>,
|
||||||
|
) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [limit, setLimit] = useState<number | null>(
|
const [limit, setLimit] = useState<number | null>(
|
||||||
props.variant === "card" ? 4 : null,
|
props.variant === "card" ? 4 : null,
|
||||||
@@ -171,7 +172,7 @@ export function LinkedStatementsOfApplicabilityCard<Params>(props: Props<Params>
|
|||||||
{variant === "card" && (
|
{variant === "card" && (
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<div className="text-lg font-semibold">
|
<div className="text-lg font-semibold">
|
||||||
{__("Statements of Applicability")}
|
{t("linkedStatementsOfApplicabilityCard.title")}
|
||||||
</div>
|
</div>
|
||||||
{!props.readOnly && (
|
{!props.readOnly && (
|
||||||
<LinkedStatementsOfApplicabilityDialog
|
<LinkedStatementsOfApplicabilityDialog
|
||||||
@@ -182,7 +183,7 @@ export function LinkedStatementsOfApplicabilityCard<Params>(props: Props<Params>
|
|||||||
onUnlink={onDetach}
|
onUnlink={onDetach}
|
||||||
>
|
>
|
||||||
<Button variant="tertiary" icon={IconPlusLarge}>
|
<Button variant="tertiary" icon={IconPlusLarge}>
|
||||||
{__("Link statement of applicability")}
|
{t("linkedStatementsOfApplicabilityCard.actions.link")}
|
||||||
</Button>
|
</Button>
|
||||||
</LinkedStatementsOfApplicabilityDialog>
|
</LinkedStatementsOfApplicabilityDialog>
|
||||||
)}
|
)}
|
||||||
@@ -191,9 +192,13 @@ export function LinkedStatementsOfApplicabilityCard<Params>(props: Props<Params>
|
|||||||
<Table className={clsx(variant === "card" && "bg-invert")}>
|
<Table className={clsx(variant === "card" && "bg-invert")}>
|
||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>{__("Name")}</Th>
|
<Th>{t("linkedStatementsOfApplicabilityCard.columns.name")}</Th>
|
||||||
<Th>{__("Applicability")}</Th>
|
<Th>
|
||||||
<Th>{__("Justification")}</Th>
|
{t("linkedStatementsOfApplicabilityCard.columns.applicability")}
|
||||||
|
</Th>
|
||||||
|
<Th>
|
||||||
|
{t("linkedStatementsOfApplicabilityCard.columns.justification")}
|
||||||
|
</Th>
|
||||||
{!props.readOnly && <Th></Th>}
|
{!props.readOnly && <Th></Th>}
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
@@ -204,7 +209,7 @@ export function LinkedStatementsOfApplicabilityCard<Params>(props: Props<Params>
|
|||||||
colSpan={props.readOnly ? 3 : 4}
|
colSpan={props.readOnly ? 3 : 4}
|
||||||
className="text-center text-txt-secondary"
|
className="text-center text-txt-secondary"
|
||||||
>
|
>
|
||||||
{__("No statements of applicability linked")}
|
{t("linkedStatementsOfApplicabilityCard.empty")}
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
)}
|
||||||
@@ -225,7 +230,7 @@ export function LinkedStatementsOfApplicabilityCard<Params>(props: Props<Params>
|
|||||||
onUnlink={onDetach}
|
onUnlink={onDetach}
|
||||||
>
|
>
|
||||||
<TrButton colspan={4} icon={IconPlusLarge}>
|
<TrButton colspan={4} icon={IconPlusLarge}>
|
||||||
{__("Link statement of applicability")}
|
{t("linkedStatementsOfApplicabilityCard.actions.link")}
|
||||||
</TrButton>
|
</TrButton>
|
||||||
</LinkedStatementsOfApplicabilityDialog>
|
</LinkedStatementsOfApplicabilityDialog>
|
||||||
)}
|
)}
|
||||||
@@ -237,10 +242,9 @@ export function LinkedStatementsOfApplicabilityCard<Params>(props: Props<Params>
|
|||||||
icon={IconChevronDown}
|
icon={IconChevronDown}
|
||||||
onClick={() => setLimit(null)}
|
onClick={() => setLimit(null)}
|
||||||
>
|
>
|
||||||
{sprintf(
|
{t("linkedStatementsOfApplicabilityCard.actions.showMore", {
|
||||||
__("Show %d more"),
|
count: props.statementsOfApplicability.length - limit,
|
||||||
props.statementsOfApplicability.length - limit,
|
})}
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
@@ -256,10 +260,7 @@ function LinkedInfoExtractor(props: {
|
|||||||
}) {
|
}) {
|
||||||
const { onExtracted, fragment } = props;
|
const { onExtracted, fragment } = props;
|
||||||
|
|
||||||
const data = useFragment(
|
const data = useFragment(linkedStatementOfApplicabilityFragment, fragment);
|
||||||
linkedStatementOfApplicabilityFragment,
|
|
||||||
fragment,
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onExtracted({
|
onExtracted({
|
||||||
@@ -283,7 +284,7 @@ function StatementOfApplicabilityRow(props: {
|
|||||||
props.statementOfApplicability,
|
props.statementOfApplicability,
|
||||||
);
|
);
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tr
|
<Tr
|
||||||
@@ -293,8 +294,8 @@ function StatementOfApplicabilityRow(props: {
|
|||||||
<Td>
|
<Td>
|
||||||
<Badge variant={soa.applicability ? "success" : "danger"}>
|
<Badge variant={soa.applicability ? "success" : "danger"}>
|
||||||
{soa.applicability
|
{soa.applicability
|
||||||
? __("Applicable")
|
? t("linkedStatementsOfApplicabilityCard.applicable")
|
||||||
: __("Not Applicable")}
|
: t("linkedStatementsOfApplicabilityCard.notApplicable")}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>{soa.justification || "-"}</Td>
|
<Td>{soa.justification || "-"}</Td>
|
||||||
@@ -303,13 +304,10 @@ function StatementOfApplicabilityRow(props: {
|
|||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
props.onClick(
|
props.onClick(soa.statementOfApplicability.id, soa.control.id)}
|
||||||
soa.statementOfApplicability.id,
|
|
||||||
soa.control.id,
|
|
||||||
)}
|
|
||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
>
|
>
|
||||||
{__("Unlink")}
|
{t("linkedStatementsOfApplicabilityCard.actions.unlink")}
|
||||||
</Button>
|
</Button>
|
||||||
</Td>
|
</Td>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
@@ -30,6 +29,7 @@ import {
|
|||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { Suspense, useRef, useState } from "react";
|
import { Suspense, useRef, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useLazyLoadQuery } from "react-relay";
|
import { useLazyLoadQuery } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
@@ -75,13 +75,14 @@ export function LinkedStatementsOfApplicabilityDialog({
|
|||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const dialogRef = useRef<{ open: () => void; close: () => void }>(null);
|
const dialogRef = useRef<{ open: () => void; close: () => void }>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
ref={dialogRef}
|
ref={dialogRef}
|
||||||
trigger={children}
|
trigger={children}
|
||||||
title="Link Statement of Applicability"
|
title={t("linkedStatementsOfApplicabilityDialog.title")}
|
||||||
>
|
>
|
||||||
<Suspense fallback={<div>Loading...</div>}>
|
<Suspense fallback={<div>Loading...</div>}>
|
||||||
<LinkedStatementsOfApplicabilityDialogContent
|
<LinkedStatementsOfApplicabilityDialogContent
|
||||||
@@ -96,7 +97,7 @@ export function LinkedStatementsOfApplicabilityDialog({
|
|||||||
function LinkedStatementsOfApplicabilityDialogContent(
|
function LinkedStatementsOfApplicabilityDialogContent(
|
||||||
props: Omit<Props, "children"> & { onClose: () => void },
|
props: Omit<Props, "children"> & { onClose: () => void },
|
||||||
) {
|
) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const [selectedSOA, setSelectedSOA] = useState<{
|
const [selectedSOA, setSelectedSOA] = useState<{
|
||||||
id: string;
|
id: string;
|
||||||
@@ -163,12 +164,10 @@ function LinkedStatementsOfApplicabilityDialogContent(
|
|||||||
? (
|
? (
|
||||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||||
<div className="text-txt-secondary text-base mb-2">
|
<div className="text-txt-secondary text-base mb-2">
|
||||||
{__("No statements of applicability available")}
|
{t("linkedStatementsOfApplicabilityDialog.empty.title")}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-txt-tertiary text-sm">
|
<div className="text-txt-tertiary text-sm">
|
||||||
{__(
|
{t("linkedStatementsOfApplicabilityDialog.empty.description")}
|
||||||
"Create a statement of applicability first to link it to this control",
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -176,7 +175,7 @@ function LinkedStatementsOfApplicabilityDialogContent(
|
|||||||
? (
|
? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="text-sm font-medium mb-2">
|
<div className="text-sm font-medium mb-2">
|
||||||
{__("Select a statement of applicability:")}
|
{t("linkedStatementsOfApplicabilityDialog.select")}
|
||||||
</div>
|
</div>
|
||||||
{statementsOfApplicability.map((soa) => {
|
{statementsOfApplicability.map((soa) => {
|
||||||
const isLinked = linkedSOAIds.has(soa.id);
|
const isLinked = linkedSOAIds.has(soa.id);
|
||||||
@@ -197,7 +196,7 @@ function LinkedStatementsOfApplicabilityDialogContent(
|
|||||||
onClick={e => e.stopPropagation()}
|
onClick={e => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<Badge variant="success">
|
<Badge variant="success">
|
||||||
{__("Linked")}
|
{t("linkedStatementsOfApplicabilityDialog.linked")}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Button
|
<Button
|
||||||
variant="danger"
|
variant="danger"
|
||||||
@@ -205,7 +204,7 @@ function LinkedStatementsOfApplicabilityDialogContent(
|
|||||||
handleUnlink(soa.id)}
|
handleUnlink(soa.id)}
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
>
|
>
|
||||||
{__("Unlink")}
|
{t("linkedStatementsOfApplicabilityDialog.actions.unlink")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -220,7 +219,7 @@ function LinkedStatementsOfApplicabilityDialogContent(
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-txt-secondary mb-1">
|
<div className="text-sm text-txt-secondary mb-1">
|
||||||
{__("Selected:")}
|
{t("linkedStatementsOfApplicabilityDialog.selected")}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-lg font-medium">
|
<div className="text-lg font-medium">
|
||||||
{selectedSOA.name}
|
{selectedSOA.name}
|
||||||
@@ -230,7 +229,7 @@ function LinkedStatementsOfApplicabilityDialogContent(
|
|||||||
variant="tertiary"
|
variant="tertiary"
|
||||||
onClick={() => setSelectedSOA(null)}
|
onClick={() => setSelectedSOA(null)}
|
||||||
>
|
>
|
||||||
{__("Change")}
|
{t("linkedStatementsOfApplicabilityDialog.actions.change")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -242,16 +241,16 @@ function LinkedStatementsOfApplicabilityDialogContent(
|
|||||||
setApplicability(checked)}
|
setApplicability(checked)}
|
||||||
/>
|
/>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{__("Applicable")}
|
{t("linkedStatementsOfApplicabilityDialog.applicable")}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium mb-1 block">
|
<label className="text-sm font-medium mb-1 block">
|
||||||
{__("Justification (optional)")}
|
{t("linkedStatementsOfApplicabilityDialog.justification.label")}
|
||||||
</label>
|
</label>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder={__("Add a justification...")}
|
placeholder={t("linkedStatementsOfApplicabilityDialog.justification.placeholder")}
|
||||||
value={justification}
|
value={justification}
|
||||||
onChange={e =>
|
onChange={e =>
|
||||||
setJustification(e.target.value)}
|
setJustification(e.target.value)}
|
||||||
@@ -262,7 +261,7 @@ function LinkedStatementsOfApplicabilityDialogContent(
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogFooter exitLabel={__("Close")}>
|
<DialogFooter exitLabel={t("linkedStatementsOfApplicabilityDialog.actions.close")}>
|
||||||
{selectedSOA
|
{selectedSOA
|
||||||
? (
|
? (
|
||||||
<>
|
<>
|
||||||
@@ -270,14 +269,14 @@ function LinkedStatementsOfApplicabilityDialogContent(
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => setSelectedSOA(null)}
|
onClick={() => setSelectedSOA(null)}
|
||||||
>
|
>
|
||||||
{__("Back")}
|
{t("linkedStatementsOfApplicabilityDialog.actions.back")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={handleLink}
|
onClick={handleLink}
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
>
|
>
|
||||||
{__("Link")}
|
{t("linkedStatementsOfApplicabilityDialog.actions.link")}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,13 +19,13 @@
|
|||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useStateWithRef } from "@probo/hooks";
|
import { useStateWithRef } from "@probo/hooks";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { EditableCell, selectCell, SelectValue, Spinner } from "@probo/ui";
|
import { EditableCell, selectCell, SelectValue, Spinner } from "@probo/ui";
|
||||||
import { useEditableCellRef } from "@probo/ui/src/Molecules/Table/EditableCell";
|
import { useEditableCellRef } from "@probo/ui/src/Molecules/Table/EditableCell";
|
||||||
import { useEditableRowContext } from "@probo/ui/src/Molecules/Table/EditableRow";
|
import { useEditableRowContext } from "@probo/ui/src/Molecules/Table/EditableRow";
|
||||||
import { getKey } from "@probo/ui/src/Molecules/Table/utils";
|
import { getKey } from "@probo/ui/src/Molecules/Table/utils";
|
||||||
import { Command } from "cmdk";
|
import { Command } from "cmdk";
|
||||||
import { type ReactNode, Suspense } from "react";
|
import { type ReactNode, Suspense } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useLazyLoadQuery } from "react-relay";
|
import { useLazyLoadQuery } from "react-relay";
|
||||||
import type {
|
import type {
|
||||||
GraphQLTaggedNode,
|
GraphQLTaggedNode,
|
||||||
@@ -44,14 +44,23 @@ type Props<Q extends OperationType, T> = {
|
|||||||
| { defaultValue: T[]; multiple: true }
|
| { defaultValue: T[]; multiple: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
export function GraphQLCell<Q extends OperationType, T extends NonNullable<unknown>>(props: Props<Q, T>) {
|
export function GraphQLCell<
|
||||||
|
Q extends OperationType,
|
||||||
|
T extends NonNullable<unknown>,
|
||||||
|
>(props: Props<Q, T>) {
|
||||||
const [value, setValue, valueRef] = useStateWithRef<T | T[] | undefined>(
|
const [value, setValue, valueRef] = useStateWithRef<T | T[] | undefined>(
|
||||||
props.defaultValue,
|
props.defaultValue,
|
||||||
);
|
);
|
||||||
const cellRef = useEditableCellRef();
|
const cellRef = useEditableCellRef();
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const filteredValue = Array.isArray(value) ? value.filter(Boolean) : value ? [value] : [];
|
const filteredValue = Array.isArray(value)
|
||||||
const usedKeys = new Set<string>(filteredValue.map(getKey).filter(Boolean) as string[]);
|
? value.filter(Boolean)
|
||||||
|
: value
|
||||||
|
? [value]
|
||||||
|
: [];
|
||||||
|
const usedKeys = new Set<string>(
|
||||||
|
filteredValue.map(getKey).filter(Boolean) as string[],
|
||||||
|
);
|
||||||
const { onUpdate } = useEditableRowContext();
|
const { onUpdate } = useEditableRowContext();
|
||||||
|
|
||||||
const onSelect = (item: T) => {
|
const onSelect = (item: T) => {
|
||||||
@@ -104,7 +113,7 @@ export function GraphQLCell<Q extends OperationType, T extends NonNullable<unkno
|
|||||||
{props.multiple && (
|
{props.multiple && (
|
||||||
<Command.Input
|
<Command.Input
|
||||||
className={classNames.input()}
|
className={classNames.input()}
|
||||||
placeholder={__("Search")}
|
placeholder={t("graphQLCell.searchPlaceholder")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Command.List>
|
<Command.List>
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
CellHead,
|
CellHead,
|
||||||
@@ -35,6 +34,7 @@ import {
|
|||||||
useContext,
|
useContext,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import type { LoadMoreFn } from "react-relay";
|
import type { LoadMoreFn } from "react-relay";
|
||||||
import type { OperationType } from "relay-runtime";
|
import type { OperationType } from "relay-runtime";
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ export function SortableDataTable({
|
|||||||
isLoadingNext?: boolean;
|
isLoadingNext?: boolean;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
}) {
|
}) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const [order, setOrder] = useState(defaultOrder);
|
const [order, setOrder] = useState(defaultOrder);
|
||||||
const onOrderChange = (o: Order) => {
|
const onOrderChange = (o: Order) => {
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
@@ -95,7 +95,7 @@ export function SortableDataTable({
|
|||||||
disabled={isLoadingNext}
|
disabled={isLoadingNext}
|
||||||
icon={isLoadingNext ? Spinner : IconChevronDown}
|
icon={isLoadingNext ? Spinner : IconChevronDown}
|
||||||
>
|
>
|
||||||
{__("Show more")}
|
{t("sortableDataTable.actions.showMore")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { formatDatetime } from "@probo/helpers";
|
import { formatDatetime } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -40,6 +39,7 @@ import {
|
|||||||
import { Breadcrumb } from "@probo/ui";
|
import { Breadcrumb } from "@probo/ui";
|
||||||
import { type ReactNode, useEffect } from "react";
|
import { type ReactNode, useEffect } from "react";
|
||||||
import { Controller } from "react-hook-form";
|
import { Controller } from "react-hook-form";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFragment, useRelayEnvironment } from "react-relay";
|
import { useFragment, useRelayEnvironment } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -143,7 +143,7 @@ type Props = {
|
|||||||
|
|
||||||
export default function TaskFormDialog(props: Props) {
|
export default function TaskFormDialog(props: Props) {
|
||||||
const { children, connection, ref, task: taskKey, measureId, onCompleted } = props;
|
const { children, connection, ref, task: taskKey, measureId, onCompleted } = props;
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const newRef = useDialogRef();
|
const newRef = useDialogRef();
|
||||||
const dialogRef = ref ?? newRef;
|
const dialogRef = ref ?? newRef;
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
@@ -152,8 +152,8 @@ export default function TaskFormDialog(props: Props) {
|
|||||||
const [mutate] = useMutationWithToasts(
|
const [mutate] = useMutationWithToasts(
|
||||||
task ? taskUpdateMutation : taskCreateMutation,
|
task ? taskUpdateMutation : taskCreateMutation,
|
||||||
{
|
{
|
||||||
successMessage: __(`Task ${task ? "updated" : "created"} successfully.`),
|
successMessage: t(task ? "taskFormDialog.messages.updated" : "taskFormDialog.messages.created"),
|
||||||
errorMessage: __(`Failed to ${task ? "update" : "create"} task`),
|
errorMessage: t(task ? "taskFormDialog.errors.update" : "taskFormDialog.errors.create"),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -246,7 +246,7 @@ export default function TaskFormDialog(props: Props) {
|
|||||||
trigger={children}
|
trigger={children}
|
||||||
title={(
|
title={(
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
items={[__("Tasks"), isUpdating ? __("Edit Task") : __("New Task")]}
|
items={[t("taskFormDialog.breadcrumb.tasks"), isUpdating ? t("taskFormDialog.breadcrumb.edit") : t("taskFormDialog.breadcrumb.new")]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -257,23 +257,23 @@ export default function TaskFormDialog(props: Props) {
|
|||||||
id="title"
|
id="title"
|
||||||
required
|
required
|
||||||
variant="title"
|
variant="title"
|
||||||
placeholder={__("Task title")}
|
placeholder={t("taskFormDialog.fields.title.placeholder")}
|
||||||
{...register("name")}
|
{...register("name")}
|
||||||
/>
|
/>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="content"
|
id="content"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
autogrow
|
autogrow
|
||||||
placeholder={__("Add description")}
|
placeholder={t("taskFormDialog.fields.description.placeholder")}
|
||||||
{...register("description")}
|
{...register("description")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* Properties form */}
|
{/* Properties form */}
|
||||||
<div className="py-5 px-6 bg-subtle">
|
<div className="py-5 px-6 bg-subtle">
|
||||||
<Label>{__("Properties")}</Label>
|
<Label>{t("taskFormDialog.properties")}</Label>
|
||||||
{isUpdating && (
|
{isUpdating && (
|
||||||
<PropertyRow
|
<PropertyRow
|
||||||
label={__("State")}
|
label={t("taskFormDialog.fields.state.label")}
|
||||||
error={"state" in formState.errors ? formState.errors.state?.message : undefined}
|
error={"state" in formState.errors ? formState.errors.state?.message : undefined}
|
||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
@@ -287,19 +287,19 @@ export default function TaskFormDialog(props: Props) {
|
|||||||
<Option value="TODO">
|
<Option value="TODO">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<TaskStateIcon state="TODO" />
|
<TaskStateIcon state="TODO" />
|
||||||
{__("To do")}
|
{t("taskFormDialog.states.todo")}
|
||||||
</span>
|
</span>
|
||||||
</Option>
|
</Option>
|
||||||
<Option value="IN_PROGRESS">
|
<Option value="IN_PROGRESS">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<TaskStateIcon state="IN_PROGRESS" />
|
<TaskStateIcon state="IN_PROGRESS" />
|
||||||
{__("In progress")}
|
{t("taskFormDialog.states.inProgress")}
|
||||||
</span>
|
</span>
|
||||||
</Option>
|
</Option>
|
||||||
<Option value="DONE">
|
<Option value="DONE">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<TaskStateIcon state="DONE" />
|
<TaskStateIcon state="DONE" />
|
||||||
{__("Done")}
|
{t("taskFormDialog.states.done")}
|
||||||
</span>
|
</span>
|
||||||
</Option>
|
</Option>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -308,7 +308,7 @@ export default function TaskFormDialog(props: Props) {
|
|||||||
</PropertyRow>
|
</PropertyRow>
|
||||||
)}
|
)}
|
||||||
<PropertyRow
|
<PropertyRow
|
||||||
label={__("Priority")}
|
label={t("taskFormDialog.fields.priority.label")}
|
||||||
error={formState.errors.priority?.message}
|
error={formState.errors.priority?.message}
|
||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
@@ -322,25 +322,25 @@ export default function TaskFormDialog(props: Props) {
|
|||||||
<Option value="URGENT">
|
<Option value="URGENT">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<PriorityLevel level="URGENT" />
|
<PriorityLevel level="URGENT" />
|
||||||
{__("Urgent")}
|
{t("taskFormDialog.priorities.urgent")}
|
||||||
</span>
|
</span>
|
||||||
</Option>
|
</Option>
|
||||||
<Option value="HIGH">
|
<Option value="HIGH">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<PriorityLevel level="HIGH" />
|
<PriorityLevel level="HIGH" />
|
||||||
{__("High")}
|
{t("taskFormDialog.priorities.high")}
|
||||||
</span>
|
</span>
|
||||||
</Option>
|
</Option>
|
||||||
<Option value="MEDIUM">
|
<Option value="MEDIUM">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<PriorityLevel level="MEDIUM" />
|
<PriorityLevel level="MEDIUM" />
|
||||||
{__("Medium")}
|
{t("taskFormDialog.priorities.medium")}
|
||||||
</span>
|
</span>
|
||||||
</Option>
|
</Option>
|
||||||
<Option value="LOW">
|
<Option value="LOW">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<PriorityLevel level="LOW" />
|
<PriorityLevel level="LOW" />
|
||||||
{__("Low")}
|
{t("taskFormDialog.priorities.low")}
|
||||||
</span>
|
</span>
|
||||||
</Option>
|
</Option>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -348,7 +348,7 @@ export default function TaskFormDialog(props: Props) {
|
|||||||
/>
|
/>
|
||||||
</PropertyRow>
|
</PropertyRow>
|
||||||
<PropertyRow
|
<PropertyRow
|
||||||
label={__("Assigned to")}
|
label={t("taskFormDialog.fields.assignedTo.label")}
|
||||||
error={formState.errors.assignedToId?.message}
|
error={formState.errors.assignedToId?.message}
|
||||||
>
|
>
|
||||||
<PeopleSelectField
|
<PeopleSelectField
|
||||||
@@ -360,7 +360,7 @@ export default function TaskFormDialog(props: Props) {
|
|||||||
</PropertyRow>
|
</PropertyRow>
|
||||||
{showMeasure && (
|
{showMeasure && (
|
||||||
<PropertyRow
|
<PropertyRow
|
||||||
label={__("Measure")}
|
label={t("taskFormDialog.fields.measure.label")}
|
||||||
error={formState.errors.measureId?.message}
|
error={formState.errors.measureId?.message}
|
||||||
>
|
>
|
||||||
<MeasureSelectField
|
<MeasureSelectField
|
||||||
@@ -372,7 +372,7 @@ export default function TaskFormDialog(props: Props) {
|
|||||||
</PropertyRow>
|
</PropertyRow>
|
||||||
)}
|
)}
|
||||||
<PropertyRow
|
<PropertyRow
|
||||||
label={__("Time estimate")}
|
label={t("taskFormDialog.fields.timeEstimate.label")}
|
||||||
error={formState.errors.timeEstimate?.message}
|
error={formState.errors.timeEstimate?.message}
|
||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
@@ -388,7 +388,7 @@ export default function TaskFormDialog(props: Props) {
|
|||||||
/>
|
/>
|
||||||
</PropertyRow>
|
</PropertyRow>
|
||||||
<PropertyRow
|
<PropertyRow
|
||||||
label={__("Deadline")}
|
label={t("taskFormDialog.fields.deadline.label")}
|
||||||
error={formState.errors.deadline?.message}
|
error={formState.errors.deadline?.message}
|
||||||
>
|
>
|
||||||
<Input id="deadline" type="date" {...register("deadline")} />
|
<Input id="deadline" type="date" {...register("deadline")} />
|
||||||
@@ -397,7 +397,7 @@ export default function TaskFormDialog(props: Props) {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button type="submit">
|
<Button type="submit">
|
||||||
{isUpdating ? __("Update task") : __("Create task")}
|
{isUpdating ? t("taskFormDialog.actions.update") : t("taskFormDialog.actions.create")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { formatDate, formatDuration, formatError, promisifyMutation } from "@probo/helpers";
|
import { formatError, promisifyMutation } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
import { dateFormat, formatDuration } from "@probo/i18n";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
useToast,
|
useToast,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { Fragment, type ReactNode, useRef, useState, useTransition } from "react";
|
import { Fragment, type ReactNode, useRef, useState, useTransition } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
graphql,
|
graphql,
|
||||||
readInlineData,
|
readInlineData,
|
||||||
@@ -185,7 +186,7 @@ const updateRankMutation = graphql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export function TasksCard({ tasks, connectionId, canReorder, refetch }: Props) {
|
export function TasksCard({ tasks, connectionId, canReorder, refetch }: Props) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const hash = useLocation().hash.replace("#", "");
|
const hash = useLocation().hash.replace("#", "");
|
||||||
const [, startTransition] = useTransition();
|
const [, startTransition] = useTransition();
|
||||||
|
|
||||||
@@ -205,13 +206,13 @@ export function TasksCard({ tasks, connectionId, canReorder, refetch }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const stateHashes = [
|
const stateHashes = [
|
||||||
{ hash: "todo", label: __("To do"), state: "TODO" },
|
{ hash: "todo", label: t("tasksCard.states.todo"), state: "TODO" },
|
||||||
{ hash: "in-progress", label: __("In progress"), state: "IN_PROGRESS" },
|
{ hash: "in-progress", label: t("tasksCard.states.inProgress"), state: "IN_PROGRESS" },
|
||||||
{ hash: "done", label: __("Done"), state: "DONE" },
|
{ hash: "done", label: t("tasksCard.states.done"), state: "DONE" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const hashes = [
|
const hashes = [
|
||||||
{ hash: "", label: __("All"), state: null },
|
{ hash: "", label: t("tasksCard.states.all"), state: null },
|
||||||
...stateHashes,
|
...stateHashes,
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
@@ -346,8 +347,8 @@ export function TasksCard({ tasks, connectionId, canReorder, refetch }: Props) {
|
|||||||
onCompleted: (_, errors) => {
|
onCompleted: (_, errors) => {
|
||||||
if (errors?.length) {
|
if (errors?.length) {
|
||||||
toast({
|
toast({
|
||||||
title: __("Error"),
|
title: t("tasksCard.error.title"),
|
||||||
description: formatError(__("Failed to reorder task."), errors),
|
description: formatError(t("tasksCard.error.reorder"), errors),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -365,7 +366,7 @@ export function TasksCard({ tasks, connectionId, canReorder, refetch }: Props) {
|
|||||||
onError: () => {
|
onError: () => {
|
||||||
droppedRef.current = false;
|
droppedRef.current = false;
|
||||||
resetDragState();
|
resetDragState();
|
||||||
toast({ title: __("Error"), description: __("Failed to reorder task."), variant: "error" });
|
toast({ title: t("tasksCard.error.title"), description: t("tasksCard.error.reorder"), variant: "error" });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -413,7 +414,7 @@ export function TasksCard({ tasks, connectionId, canReorder, refetch }: Props) {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{tasks.length === 0
|
{tasks.length === 0
|
||||||
? (
|
? (
|
||||||
<p className="text-center py-6 text-txt-secondary">{__("No tasks")}</p>
|
<p className="text-center py-6 text-txt-secondary">{t("tasksCard.empty")}</p>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<Card>
|
<Card>
|
||||||
@@ -463,8 +464,8 @@ export function TasksCard({ tasks, connectionId, canReorder, refetch }: Props) {
|
|||||||
{canDrag && filteredTasks.length > 1 && (
|
{canDrag && filteredTasks.length > 1 && (
|
||||||
<p className="text-sm text-txt-tertiary">
|
<p className="text-sm text-txt-tertiary">
|
||||||
{hash === ""
|
{hash === ""
|
||||||
? __("Drag and drop to reorder tasks or move them between states")
|
? t("tasksCard.dragInstructions.all")
|
||||||
: __("Drag and drop to reorder tasks")}
|
: t("tasksCard.dragInstructions.state")}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -521,7 +522,7 @@ const deleteMutation = graphql`
|
|||||||
function TaskRow(props: TaskRowProps) {
|
function TaskRow(props: TaskRowProps) {
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const dialogRef = useDialogRef();
|
const dialogRef = useDialogRef();
|
||||||
const { __ } = useTranslate();
|
const { t, i18n } = useTranslation();
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const [deleteTask] = useMutation<TasksCardDeleteMutation>(deleteMutation);
|
const [deleteTask] = useMutation<TasksCardDeleteMutation>(deleteMutation);
|
||||||
const params = useParams<{ measureId?: string }>();
|
const params = useParams<{ measureId?: string }>();
|
||||||
@@ -542,8 +543,8 @@ function TaskRow(props: TaskRowProps) {
|
|||||||
icon: typeof IconCircleProgress;
|
icon: typeof IconCircleProgress;
|
||||||
className: string;
|
className: string;
|
||||||
}> = {
|
}> = {
|
||||||
TODO: { state: "IN_PROGRESS", label: __("Move to In progress"), icon: IconCircleProgress, className: "text-txt-warning" },
|
TODO: { state: "IN_PROGRESS", label: t("tasksCard.actions.moveToInProgress"), icon: IconCircleProgress, className: "text-txt-warning" },
|
||||||
IN_PROGRESS: { state: "DONE", label: __("Move to Done"), icon: IconCircleCheck, className: "text-txt-accent" },
|
IN_PROGRESS: { state: "DONE", label: t("tasksCard.actions.moveToDone"), icon: IconCircleCheck, className: "text-txt-accent" },
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAdvance = async () => {
|
const onAdvance = async () => {
|
||||||
@@ -581,7 +582,7 @@ function TaskRow(props: TaskRowProps) {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
message: "Are you sure you want to delete this task?",
|
message: t("tasksCard.deleteConfirmation"),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -642,11 +643,11 @@ function TaskRow(props: TaskRowProps) {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{task.timeEstimate && (
|
{task.timeEstimate && (
|
||||||
<span>{formatDuration(task.timeEstimate, __)}</span>
|
<span>{formatDuration(task.timeEstimate, t)}</span>
|
||||||
)}
|
)}
|
||||||
{task.deadline && (
|
{task.deadline && (
|
||||||
<time dateTime={task.deadline}>
|
<time dateTime={task.deadline}>
|
||||||
{formatDate(task.deadline)}
|
{dateFormat(i18n.language, task.deadline)}
|
||||||
</time>
|
</time>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -677,7 +678,7 @@ function TaskRow(props: TaskRowProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
icon={IconPencil}
|
icon={IconPencil}
|
||||||
title={__("Edit")}
|
title={t("tasksCard.actions.edit")}
|
||||||
onClick={() => dialogRef.current?.open()}
|
onClick={() => dialogRef.current?.open()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -685,7 +686,7 @@ function TaskRow(props: TaskRowProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="danger"
|
variant="danger"
|
||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
title={__("Delete")}
|
title={t("tasksCard.actions.delete")}
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFragment } from "react-relay";
|
import { useFragment } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -88,11 +88,11 @@ const thirdPartyUpdateQuery = graphql`
|
|||||||
|
|
||||||
export function useThirdPartyForm(thirdPartyKey: useThirdPartyFormFragment$key) {
|
export function useThirdPartyForm(thirdPartyKey: useThirdPartyFormFragment$key) {
|
||||||
const thirdParty = useFragment(thirdPartyFormFragment, thirdPartyKey);
|
const thirdParty = useFragment(thirdPartyFormFragment, thirdPartyKey);
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [mutate] = useMutationWithToasts(thirdPartyUpdateQuery, {
|
const [mutate] = useMutationWithToasts(thirdPartyUpdateQuery, {
|
||||||
successMessage: __("Third party updated successfully."),
|
successMessage: t("thirdPartyForm.messages.updated"),
|
||||||
errorMessage: __("Failed to update third party"),
|
errorMessage: t("thirdPartyForm.messages.updateError"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultValues = useMemo(
|
const defaultValues = useMemo(
|
||||||
|
|||||||
@@ -18,9 +18,9 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { promisifyMutation, sprintf } from "@probo/helpers";
|
import { promisifyMutation } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { useConfirm } from "@probo/ui";
|
import { useConfirm } from "@probo/ui";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMutation } from "react-relay";
|
import { useMutation } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
@@ -163,11 +163,11 @@ export const useDeleteAsset = (
|
|||||||
) => {
|
) => {
|
||||||
const [mutate] = useMutation<AssetGraphDeleteMutation>(deleteAssetMutation);
|
const [mutate] = useMutation<AssetGraphDeleteMutation>(deleteAssetMutation);
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (!asset.id || !asset.name) {
|
if (!asset.id || !asset.name) {
|
||||||
return alert(__("Failed to delete asset: missing id or name"));
|
return alert(t("assetGraph.errors.deleteMissingIdOrName"));
|
||||||
}
|
}
|
||||||
confirm(
|
confirm(
|
||||||
() =>
|
() =>
|
||||||
@@ -180,12 +180,7 @@ export const useDeleteAsset = (
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
message: sprintf(
|
message: t("assetGraph.deleteConfirmation", { name: asset.name }),
|
||||||
__(
|
|
||||||
"This will permanently delete \"%s\". This action cannot be undone.",
|
|
||||||
),
|
|
||||||
asset.name,
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -193,7 +188,7 @@ export const useDeleteAsset = (
|
|||||||
|
|
||||||
export const useCreateAsset = (connectionId: string) => {
|
export const useCreateAsset = (connectionId: string) => {
|
||||||
const [mutate, isMutating] = useMutation<AssetGraphCreateMutation>(createAssetMutation);
|
const [mutate, isMutating] = useMutation<AssetGraphCreateMutation>(createAssetMutation);
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
(input: {
|
(input: {
|
||||||
@@ -206,18 +201,16 @@ export const useCreateAsset = (connectionId: string) => {
|
|||||||
dataTypesStored: string;
|
dataTypesStored: string;
|
||||||
}) => {
|
}) => {
|
||||||
if (!input.name?.trim()) {
|
if (!input.name?.trim()) {
|
||||||
return alert(__("Failed to create asset: name is required"));
|
return alert(t("assetGraph.errors.createNameRequired"));
|
||||||
}
|
}
|
||||||
if (!input.ownerId) {
|
if (!input.ownerId) {
|
||||||
return alert(__("Failed to create asset: owner is required"));
|
return alert(t("assetGraph.errors.createOwnerRequired"));
|
||||||
}
|
}
|
||||||
if (!input.organizationId) {
|
if (!input.organizationId) {
|
||||||
return alert(__("Failed to create asset: organization is required"));
|
return alert(t("assetGraph.errors.createOrganizationRequired"));
|
||||||
}
|
}
|
||||||
if (!input.dataTypesStored) {
|
if (!input.dataTypesStored) {
|
||||||
return alert(
|
return alert(t("assetGraph.errors.createDataTypesStoredRequired"));
|
||||||
__("Failed to create asset: data types stored is required"),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return promisifyMutation(mutate)({
|
return promisifyMutation(mutate)({
|
||||||
@@ -240,7 +233,7 @@ export const useCreateAsset = (connectionId: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useUpdateAsset = () => {
|
export const useUpdateAsset = () => {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const [mutate] = useMutation<AssetGraphUpdateMutation>(updateAssetMutation);
|
const [mutate] = useMutation<AssetGraphUpdateMutation>(updateAssetMutation);
|
||||||
|
|
||||||
return (input: {
|
return (input: {
|
||||||
@@ -253,7 +246,7 @@ export const useUpdateAsset = () => {
|
|||||||
thirdPartyIds?: string[];
|
thirdPartyIds?: string[];
|
||||||
}) => {
|
}) => {
|
||||||
if (!input.id) {
|
if (!input.id) {
|
||||||
return alert(__("Failed to update asset: asset ID is required"));
|
return alert(t("assetGraph.errors.updateIdRequired"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return promisifyMutation(mutate)({
|
return promisifyMutation(mutate)({
|
||||||
|
|||||||
@@ -18,9 +18,9 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { promisifyMutation, sprintf } from "@probo/helpers";
|
import { promisifyMutation } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { useConfirm } from "@probo/ui";
|
import { useConfirm } from "@probo/ui";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMutation } from "react-relay";
|
import { useMutation } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
@@ -148,10 +148,10 @@ export const useDeleteAudit = (
|
|||||||
connectionId: string,
|
connectionId: string,
|
||||||
onSuccess?: () => void,
|
onSuccess?: () => void,
|
||||||
) => {
|
) => {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const [mutate] = useMutationWithToasts(deleteAuditMutation, {
|
const [mutate] = useMutationWithToasts(deleteAuditMutation, {
|
||||||
successMessage: __("Audit deleted successfully"),
|
successMessage: t("auditGraph.messages.deleted"),
|
||||||
errorMessage: __("Failed to delete audit"),
|
errorMessage: t("auditGraph.errors.delete"),
|
||||||
});
|
});
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
|
|
||||||
@@ -169,12 +169,7 @@ export const useDeleteAudit = (
|
|||||||
onSuccess?.();
|
onSuccess?.();
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: sprintf(
|
message: t("auditGraph.deleteConfirmation", { frameworkName: audit.framework?.name ?? "" }),
|
||||||
__(
|
|
||||||
"This will permanently delete the audit for %s. This action cannot be undone.",
|
|
||||||
),
|
|
||||||
audit.framework?.name ?? "",
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -183,7 +178,7 @@ export const useDeleteAudit = (
|
|||||||
export const useCreateAudit = (connectionId: string) => {
|
export const useCreateAudit = (connectionId: string) => {
|
||||||
// eslint-disable-next-line relay/generated-typescript-types
|
// eslint-disable-next-line relay/generated-typescript-types
|
||||||
const [mutate] = useMutation(createAuditMutation);
|
const [mutate] = useMutation(createAuditMutation);
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (input: {
|
return (input: {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
@@ -196,10 +191,10 @@ export const useCreateAudit = (connectionId: string) => {
|
|||||||
file?: File | null;
|
file?: File | null;
|
||||||
}) => {
|
}) => {
|
||||||
if (!input.organizationId) {
|
if (!input.organizationId) {
|
||||||
return alert(__("Failed to create audit: organization is required"));
|
return alert(t("auditGraph.errors.createOrganizationRequired"));
|
||||||
}
|
}
|
||||||
if (!input.frameworkId) {
|
if (!input.frameworkId) {
|
||||||
return alert(__("Failed to create audit: framework is required"));
|
return alert(t("auditGraph.errors.createFrameworkRequired"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return promisifyMutation(mutate)({
|
return promisifyMutation(mutate)({
|
||||||
@@ -224,7 +219,7 @@ export const useCreateAudit = (connectionId: string) => {
|
|||||||
export const useUpdateAudit = () => {
|
export const useUpdateAudit = () => {
|
||||||
// eslint-disable-next-line relay/generated-typescript-types
|
// eslint-disable-next-line relay/generated-typescript-types
|
||||||
const [mutate] = useMutation(updateAuditMutation);
|
const [mutate] = useMutation(updateAuditMutation);
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (input: {
|
return (input: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -234,7 +229,7 @@ export const useUpdateAudit = () => {
|
|||||||
state?: string;
|
state?: string;
|
||||||
}) => {
|
}) => {
|
||||||
if (!input.id) {
|
if (!input.id) {
|
||||||
return alert(__("Failed to update audit: audit ID is required"));
|
return alert(t("auditGraph.errors.updateIdRequired"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return promisifyMutation(mutate)({
|
return promisifyMutation(mutate)({
|
||||||
@@ -263,15 +258,15 @@ export const uploadAuditReportMutation = graphql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const useUploadAuditReport = () => {
|
export const useUploadAuditReport = () => {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const [mutate, isLoading] = useMutationWithToasts(uploadAuditReportMutation, {
|
const [mutate, isLoading] = useMutationWithToasts(uploadAuditReportMutation, {
|
||||||
successMessage: __("Audit report uploaded successfully"),
|
successMessage: t("auditGraph.messages.reportUploaded"),
|
||||||
errorMessage: __("Failed to upload audit report"),
|
errorMessage: t("auditGraph.errors.uploadReport"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const uploadAuditReport = (input: { auditId: string; file: File }) => {
|
const uploadAuditReport = (input: { auditId: string; file: File }) => {
|
||||||
if (!input.auditId) {
|
if (!input.auditId) {
|
||||||
return alert(__("Failed to upload report: audit ID is required"));
|
return alert(t("auditGraph.errors.uploadReportIdRequired"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return mutate({
|
return mutate({
|
||||||
@@ -308,10 +303,10 @@ export const deleteAuditReportMutation = graphql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const useDeleteAuditReport = () => {
|
export const useDeleteAuditReport = () => {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const [mutate] = useMutationWithToasts(deleteAuditReportMutation, {
|
const [mutate] = useMutationWithToasts(deleteAuditReportMutation, {
|
||||||
successMessage: __("Audit report deleted successfully"),
|
successMessage: t("auditGraph.messages.reportDeleted"),
|
||||||
errorMessage: __("Failed to delete audit report"),
|
errorMessage: t("auditGraph.errors.deleteReport"),
|
||||||
});
|
});
|
||||||
|
|
||||||
return (input: { auditId: string }) => {
|
return (input: { auditId: string }) => {
|
||||||
|
|||||||
@@ -18,9 +18,9 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { promisifyMutation, sprintf } from "@probo/helpers";
|
import { promisifyMutation } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { useConfirm } from "@probo/ui";
|
import { useConfirm } from "@probo/ui";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMutation } from "react-relay";
|
import { useMutation } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
@@ -154,11 +154,11 @@ export const useDeleteDatum = (
|
|||||||
// eslint-disable-next-line relay/generated-typescript-types
|
// eslint-disable-next-line relay/generated-typescript-types
|
||||||
const [mutate] = useMutation(deleteDatumMutation);
|
const [mutate] = useMutation(deleteDatumMutation);
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (!datum.id || !datum.name) {
|
if (!datum.id || !datum.name) {
|
||||||
return alert(__("Failed to delete data: missing id or name"));
|
return alert(t("datumGraph.errors.deleteMissingIdOrName"));
|
||||||
}
|
}
|
||||||
confirm(
|
confirm(
|
||||||
() =>
|
() =>
|
||||||
@@ -171,12 +171,7 @@ export const useDeleteDatum = (
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
message: sprintf(
|
message: t("datumGraph.deleteConfirmation", { name: datum.name }),
|
||||||
__(
|
|
||||||
"This will permanently delete \"%s\". This action cannot be undone.",
|
|
||||||
),
|
|
||||||
datum.name,
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -185,7 +180,7 @@ export const useDeleteDatum = (
|
|||||||
export const useCreateDatum = (connectionId: string) => {
|
export const useCreateDatum = (connectionId: string) => {
|
||||||
// eslint-disable-next-line relay/generated-typescript-types
|
// eslint-disable-next-line relay/generated-typescript-types
|
||||||
const [mutate] = useMutation(createDatumMutation);
|
const [mutate] = useMutation(createDatumMutation);
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (input: {
|
return (input: {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -195,13 +190,13 @@ export const useCreateDatum = (connectionId: string) => {
|
|||||||
thirdPartyIds?: string[];
|
thirdPartyIds?: string[];
|
||||||
}) => {
|
}) => {
|
||||||
if (!input.name?.trim()) {
|
if (!input.name?.trim()) {
|
||||||
return alert(__("Failed to create data: name is required"));
|
return alert(t("datumGraph.errors.createNameRequired"));
|
||||||
}
|
}
|
||||||
if (!input.ownerId) {
|
if (!input.ownerId) {
|
||||||
return alert(__("Failed to create data: owner is required"));
|
return alert(t("datumGraph.errors.createOwnerRequired"));
|
||||||
}
|
}
|
||||||
if (!input.organizationId) {
|
if (!input.organizationId) {
|
||||||
return alert(__("Failed to create data: organization is required"));
|
return alert(t("datumGraph.errors.createOrganizationRequired"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return promisifyMutation(mutate)({
|
return promisifyMutation(mutate)({
|
||||||
@@ -216,7 +211,7 @@ export const useCreateDatum = (connectionId: string) => {
|
|||||||
export const useUpdateDatum = () => {
|
export const useUpdateDatum = () => {
|
||||||
// eslint-disable-next-line relay/generated-typescript-types
|
// eslint-disable-next-line relay/generated-typescript-types
|
||||||
const [mutate] = useMutation(updateDatumMutation);
|
const [mutate] = useMutation(updateDatumMutation);
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (input: {
|
return (input: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -226,7 +221,7 @@ export const useUpdateDatum = () => {
|
|||||||
thirdPartyIds?: string[];
|
thirdPartyIds?: string[];
|
||||||
}) => {
|
}) => {
|
||||||
if (!input.id) {
|
if (!input.id) {
|
||||||
return alert(__("Failed to update data: missing id"));
|
return alert(t("datumGraph.errors.updateMissingId"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return promisifyMutation(mutate)({
|
return promisifyMutation(mutate)({
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslation } from "react-i18next";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
import type { DocumentGraphBulkExportDocumentsMutation } from "#/__generated__/core/DocumentGraphBulkExportDocumentsMutation.graphql";
|
import type { DocumentGraphBulkExportDocumentsMutation } from "#/__generated__/core/DocumentGraphBulkExportDocumentsMutation.graphql";
|
||||||
@@ -40,13 +40,13 @@ const deleteDocumentMutation = graphql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export function useDeleteDocumentMutation() {
|
export function useDeleteDocumentMutation() {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return useMutationWithToasts<DocumentGraphDeleteMutation>(
|
return useMutationWithToasts<DocumentGraphDeleteMutation>(
|
||||||
deleteDocumentMutation,
|
deleteDocumentMutation,
|
||||||
{
|
{
|
||||||
successMessage: __("Document deleted successfully."),
|
successMessage: t("documentGraph.messages.deleted"),
|
||||||
errorMessage: __("Failed to delete document"),
|
errorMessage: t("documentGraph.errors.delete"),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -62,11 +62,11 @@ const bulkDeleteDocumentsMutation = graphql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export function useBulkDeleteDocumentsMutation() {
|
export function useBulkDeleteDocumentsMutation() {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return useMutationWithToasts(bulkDeleteDocumentsMutation, {
|
return useMutationWithToasts(bulkDeleteDocumentsMutation, {
|
||||||
successMessage: __("Documents deleted successfully."),
|
successMessage: t("documentGraph.messages.bulkDeleted"),
|
||||||
errorMessage: __("Failed to delete documents"),
|
errorMessage: t("documentGraph.errors.bulkDelete"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,15 +81,13 @@ const bulkExportDocumentsMutation = graphql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export function useBulkExportDocumentsMutation() {
|
export function useBulkExportDocumentsMutation() {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return useMutationWithToasts<DocumentGraphBulkExportDocumentsMutation>(
|
return useMutationWithToasts<DocumentGraphBulkExportDocumentsMutation>(
|
||||||
bulkExportDocumentsMutation,
|
bulkExportDocumentsMutation,
|
||||||
{
|
{
|
||||||
successMessage: __(
|
successMessage: t("documentGraph.messages.exportStarted"),
|
||||||
"Document export started successfully. You will receive an email when the export is ready.",
|
errorMessage: t("documentGraph.errors.export"),
|
||||||
),
|
|
||||||
errorMessage: __("Failed to start document export"),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,10 +18,9 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { sprintf } from "@probo/helpers";
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { useConfirm } from "@probo/ui";
|
import { useConfirm } from "@probo/ui";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
import { useMutationWithToasts } from "../useMutationWithToasts";
|
import { useMutationWithToasts } from "../useMutationWithToasts";
|
||||||
@@ -68,12 +67,12 @@ export const useDeleteFrameworkMutation = (
|
|||||||
framework: { id: string; name: string },
|
framework: { id: string; name: string },
|
||||||
connectionId: string,
|
connectionId: string,
|
||||||
) => {
|
) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [commitDelete] = useMutationWithToasts(deleteFrameworkMutation, {
|
const [commitDelete] = useMutationWithToasts(deleteFrameworkMutation, {
|
||||||
errorMessage: "Failed to delete framework",
|
errorMessage: t("frameworkGraph.errors.delete"),
|
||||||
successMessage: "Framework deleted successfully",
|
successMessage: t("frameworkGraph.messages.deleted"),
|
||||||
});
|
});
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const { __ } = useTranslate();
|
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
(options?: { onSuccess?: () => void }) => {
|
(options?: { onSuccess?: () => void }) => {
|
||||||
@@ -90,16 +89,13 @@ export const useDeleteFrameworkMutation = (
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: sprintf(
|
message: t("frameworkGraph.deleteConfirmation", {
|
||||||
__(
|
name: framework.name,
|
||||||
"This will permanently delete framework \"%s\". This action cannot be undone.",
|
}),
|
||||||
),
|
|
||||||
framework.name,
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[framework, connectionId, commitDelete, confirm, __],
|
[framework, connectionId, commitDelete, confirm, t],
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslation } from "react-i18next";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
import type { MeasureGraphDeleteMutation } from "#/__generated__/core/MeasureGraphDeleteMutation.graphql";
|
import type { MeasureGraphDeleteMutation } from "#/__generated__/core/MeasureGraphDeleteMutation.graphql";
|
||||||
@@ -39,13 +39,13 @@ const deleteMeasureMutation = graphql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export function useDeleteMeasureMutation() {
|
export function useDeleteMeasureMutation() {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return useMutationWithToasts<MeasureGraphDeleteMutation>(
|
return useMutationWithToasts<MeasureGraphDeleteMutation>(
|
||||||
deleteMeasureMutation,
|
deleteMeasureMutation,
|
||||||
{
|
{
|
||||||
successMessage: __("Measure deleted successfully."),
|
successMessage: t("measureGraph.messages.deleted"),
|
||||||
errorMessage: __("Failed to delete measure"),
|
errorMessage: t("measureGraph.errors.delete"),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -61,10 +61,10 @@ const measureUpdateMutation = graphql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const useUpdateMeasure = () => {
|
export const useUpdateMeasure = () => {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return useMutationWithToasts(measureUpdateMutation, {
|
return useMutationWithToasts(measureUpdateMutation, {
|
||||||
successMessage: __("Measure updated successfully."),
|
successMessage: t("measureGraph.messages.updated"),
|
||||||
errorMessage: __("Failed to update measure"),
|
errorMessage: t("measureGraph.errors.update"),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,8 +19,8 @@
|
|||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { promisifyMutation } from "@probo/helpers";
|
import { promisifyMutation } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { useConfirm } from "@probo/ui";
|
import { useConfirm } from "@probo/ui";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMutation } from "react-relay";
|
import { useMutation } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
@@ -149,10 +149,10 @@ export const useDeleteObligation = (
|
|||||||
obligation: { id: string },
|
obligation: { id: string },
|
||||||
connectionId: string,
|
connectionId: string,
|
||||||
) => {
|
) => {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const [mutate] = useMutationWithToasts(deleteObligationMutation, {
|
const [mutate] = useMutationWithToasts(deleteObligationMutation, {
|
||||||
successMessage: __("Obligation deleted successfully"),
|
successMessage: t("obligationGraph.messages.deleted"),
|
||||||
errorMessage: __("Failed to delete obligation"),
|
errorMessage: t("obligationGraph.errors.delete"),
|
||||||
});
|
});
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
|
|
||||||
@@ -168,9 +168,7 @@ export const useDeleteObligation = (
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
message: __(
|
message: t("obligationGraph.deleteConfirmation"),
|
||||||
"This will permanently delete this obligation. This action cannot be undone.",
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -179,7 +177,7 @@ export const useDeleteObligation = (
|
|||||||
export const useCreateObligation = (connectionId: string) => {
|
export const useCreateObligation = (connectionId: string) => {
|
||||||
// eslint-disable-next-line relay/generated-typescript-types
|
// eslint-disable-next-line relay/generated-typescript-types
|
||||||
const [mutate] = useMutation(createObligationMutation);
|
const [mutate] = useMutation(createObligationMutation);
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (input: {
|
return (input: {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
@@ -195,10 +193,10 @@ export const useCreateObligation = (connectionId: string) => {
|
|||||||
status: string;
|
status: string;
|
||||||
}) => {
|
}) => {
|
||||||
if (!input.organizationId) {
|
if (!input.organizationId) {
|
||||||
return alert(__("Failed to create obligation: organization is required"));
|
return alert(t("obligationGraph.errors.createOrganizationRequired"));
|
||||||
}
|
}
|
||||||
if (!input.ownerId) {
|
if (!input.ownerId) {
|
||||||
return alert(__("Failed to create obligation: owner is required"));
|
return alert(t("obligationGraph.errors.createOwnerRequired"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return promisifyMutation(mutate)({
|
return promisifyMutation(mutate)({
|
||||||
@@ -225,7 +223,7 @@ export const useCreateObligation = (connectionId: string) => {
|
|||||||
export const useUpdateObligation = () => {
|
export const useUpdateObligation = () => {
|
||||||
// eslint-disable-next-line relay/generated-typescript-types
|
// eslint-disable-next-line relay/generated-typescript-types
|
||||||
const [mutate] = useMutation(updateObligationMutation);
|
const [mutate] = useMutation(updateObligationMutation);
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (input: {
|
return (input: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -241,7 +239,7 @@ export const useUpdateObligation = () => {
|
|||||||
status?: string;
|
status?: string;
|
||||||
}) => {
|
}) => {
|
||||||
if (!input.id) {
|
if (!input.id) {
|
||||||
return alert(__("Failed to update obligation: ID is required"));
|
return alert(t("obligationGraph.errors.updateIdRequired"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return promisifyMutation(mutate)({
|
return promisifyMutation(mutate)({
|
||||||
|
|||||||
@@ -18,9 +18,9 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { promisifyMutation, sprintf } from "@probo/helpers";
|
import { promisifyMutation } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { useConfirm } from "@probo/ui";
|
import { useConfirm } from "@probo/ui";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMutation } from "react-relay";
|
import { useMutation } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
@@ -272,10 +272,10 @@ export const useDeleteProcessingActivity = (
|
|||||||
processingActivity: { id: string; name: string },
|
processingActivity: { id: string; name: string },
|
||||||
connectionId: string,
|
connectionId: string,
|
||||||
) => {
|
) => {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const [mutate] = useMutationWithToasts(deleteProcessingActivityMutation, {
|
const [mutate] = useMutationWithToasts(deleteProcessingActivityMutation, {
|
||||||
successMessage: __("Processing activity deleted successfully"),
|
successMessage: t("processingActivityGraph.messages.deleted"),
|
||||||
errorMessage: __("Failed to delete processing activity"),
|
errorMessage: t("processingActivityGraph.errors.delete"),
|
||||||
});
|
});
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
|
|
||||||
@@ -291,12 +291,9 @@ export const useDeleteProcessingActivity = (
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
message: sprintf(
|
message: t("processingActivityGraph.deleteConfirmation", {
|
||||||
__(
|
name: processingActivity.name,
|
||||||
"This will permanently delete the processing activity %s. This action cannot be undone.",
|
}),
|
||||||
),
|
|
||||||
processingActivity.name,
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -305,7 +302,7 @@ export const useDeleteProcessingActivity = (
|
|||||||
export const useCreateProcessingActivity = (connectionId?: string) => {
|
export const useCreateProcessingActivity = (connectionId?: string) => {
|
||||||
// eslint-disable-next-line relay/generated-typescript-types
|
// eslint-disable-next-line relay/generated-typescript-types
|
||||||
const [mutate] = useMutation(createProcessingActivityMutation);
|
const [mutate] = useMutation(createProcessingActivityMutation);
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (input: {
|
return (input: {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
@@ -332,12 +329,12 @@ export const useCreateProcessingActivity = (connectionId?: string) => {
|
|||||||
}) => {
|
}) => {
|
||||||
if (!input.organizationId) {
|
if (!input.organizationId) {
|
||||||
return alert(
|
return alert(
|
||||||
__("Failed to create processing activity: organization is required"),
|
t("processingActivityGraph.errors.createOrganizationRequired"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!input.name) {
|
if (!input.name) {
|
||||||
return alert(
|
return alert(
|
||||||
__("Failed to create processing activity: name is required"),
|
t("processingActivityGraph.errors.createNameRequired"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,7 +373,7 @@ export const useCreateProcessingActivity = (connectionId?: string) => {
|
|||||||
export const useUpdateProcessingActivity = () => {
|
export const useUpdateProcessingActivity = () => {
|
||||||
// eslint-disable-next-line relay/generated-typescript-types
|
// eslint-disable-next-line relay/generated-typescript-types
|
||||||
const [mutate] = useMutation(updateProcessingActivityMutation);
|
const [mutate] = useMutation(updateProcessingActivityMutation);
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (input: {
|
return (input: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -402,7 +399,7 @@ export const useUpdateProcessingActivity = () => {
|
|||||||
thirdPartyIds?: string[];
|
thirdPartyIds?: string[];
|
||||||
}) => {
|
}) => {
|
||||||
if (!input.id) {
|
if (!input.id) {
|
||||||
return alert(__("Failed to update processing activity: ID is required"));
|
return alert(t("processingActivityGraph.errors.updateIdRequired"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return promisifyMutation(mutate)({
|
return promisifyMutation(mutate)({
|
||||||
@@ -489,7 +486,7 @@ export const deleteDataProtectionImpactAssessmentMutation = graphql`
|
|||||||
export const useCreateDataProtectionImpactAssessment = () => {
|
export const useCreateDataProtectionImpactAssessment = () => {
|
||||||
// eslint-disable-next-line relay/generated-typescript-types
|
// eslint-disable-next-line relay/generated-typescript-types
|
||||||
const [mutate] = useMutation(createDataProtectionImpactAssessmentMutation);
|
const [mutate] = useMutation(createDataProtectionImpactAssessmentMutation);
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (input: {
|
return (input: {
|
||||||
processingActivityId: string;
|
processingActivityId: string;
|
||||||
@@ -501,7 +498,7 @@ export const useCreateDataProtectionImpactAssessment = () => {
|
|||||||
}) => {
|
}) => {
|
||||||
if (!input.processingActivityId) {
|
if (!input.processingActivityId) {
|
||||||
return alert(
|
return alert(
|
||||||
__("Failed to create DPIA: Processing Activity ID is required"),
|
t("processingActivityGraph.errors.createDpiaProcessingActivityIdRequired"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,7 +513,7 @@ export const useCreateDataProtectionImpactAssessment = () => {
|
|||||||
export const useUpdateDataProtectionImpactAssessment = () => {
|
export const useUpdateDataProtectionImpactAssessment = () => {
|
||||||
// eslint-disable-next-line relay/generated-typescript-types
|
// eslint-disable-next-line relay/generated-typescript-types
|
||||||
const [mutate] = useMutation(updateDataProtectionImpactAssessmentMutation);
|
const [mutate] = useMutation(updateDataProtectionImpactAssessmentMutation);
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (input: {
|
return (input: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -527,7 +524,7 @@ export const useUpdateDataProtectionImpactAssessment = () => {
|
|||||||
residualRisk?: ProcessingActivityDPIAResidualRisk;
|
residualRisk?: ProcessingActivityDPIAResidualRisk;
|
||||||
}) => {
|
}) => {
|
||||||
if (!input.id) {
|
if (!input.id) {
|
||||||
return alert(__("Failed to update DPIA: ID is required"));
|
return alert(t("processingActivityGraph.errors.updateDpiaIdRequired"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return promisifyMutation(mutate)({
|
return promisifyMutation(mutate)({
|
||||||
@@ -542,12 +539,12 @@ export const useDeleteDataProtectionImpactAssessment = (
|
|||||||
dpia: { id: string },
|
dpia: { id: string },
|
||||||
options?: { onSuccess?: () => void },
|
options?: { onSuccess?: () => void },
|
||||||
) => {
|
) => {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const [mutate] = useMutationWithToasts(
|
const [mutate] = useMutationWithToasts(
|
||||||
deleteDataProtectionImpactAssessmentMutation,
|
deleteDataProtectionImpactAssessmentMutation,
|
||||||
{
|
{
|
||||||
successMessage: __("DPIA deleted successfully"),
|
successMessage: t("processingActivityGraph.messages.dpiaDeleted"),
|
||||||
errorMessage: __("Failed to delete DPIA"),
|
errorMessage: t("processingActivityGraph.errors.deleteDpia"),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
@@ -564,9 +561,7 @@ export const useDeleteDataProtectionImpactAssessment = (
|
|||||||
onSuccess: options?.onSuccess,
|
onSuccess: options?.onSuccess,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
message: __(
|
message: t("processingActivityGraph.dpiaDeleteConfirmation"),
|
||||||
"This will permanently delete this Data Protection Impact Assessment. This action cannot be undone.",
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -648,7 +643,7 @@ export const deleteTransferImpactAssessmentMutation = graphql`
|
|||||||
export const useCreateTransferImpactAssessment = () => {
|
export const useCreateTransferImpactAssessment = () => {
|
||||||
// eslint-disable-next-line relay/generated-typescript-types
|
// eslint-disable-next-line relay/generated-typescript-types
|
||||||
const [mutate] = useMutation(createTransferImpactAssessmentMutation);
|
const [mutate] = useMutation(createTransferImpactAssessmentMutation);
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (input: {
|
return (input: {
|
||||||
processingActivityId: string;
|
processingActivityId: string;
|
||||||
@@ -660,7 +655,7 @@ export const useCreateTransferImpactAssessment = () => {
|
|||||||
}) => {
|
}) => {
|
||||||
if (!input.processingActivityId) {
|
if (!input.processingActivityId) {
|
||||||
return alert(
|
return alert(
|
||||||
__("Failed to create TIA: Processing Activity ID is required"),
|
t("processingActivityGraph.errors.createTiaProcessingActivityIdRequired"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -675,7 +670,7 @@ export const useCreateTransferImpactAssessment = () => {
|
|||||||
export const useUpdateTransferImpactAssessment = () => {
|
export const useUpdateTransferImpactAssessment = () => {
|
||||||
// eslint-disable-next-line relay/generated-typescript-types
|
// eslint-disable-next-line relay/generated-typescript-types
|
||||||
const [mutate] = useMutation(updateTransferImpactAssessmentMutation);
|
const [mutate] = useMutation(updateTransferImpactAssessmentMutation);
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (input: {
|
return (input: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -686,7 +681,7 @@ export const useUpdateTransferImpactAssessment = () => {
|
|||||||
supplementaryMeasures?: string;
|
supplementaryMeasures?: string;
|
||||||
}) => {
|
}) => {
|
||||||
if (!input.id) {
|
if (!input.id) {
|
||||||
return alert(__("Failed to update TIA: ID is required"));
|
return alert(t("processingActivityGraph.errors.updateTiaIdRequired"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return promisifyMutation(mutate)({
|
return promisifyMutation(mutate)({
|
||||||
@@ -701,12 +696,12 @@ export const useDeleteTransferImpactAssessment = (
|
|||||||
tia: { id: string },
|
tia: { id: string },
|
||||||
options?: { onSuccess?: () => void },
|
options?: { onSuccess?: () => void },
|
||||||
) => {
|
) => {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const [mutate] = useMutationWithToasts(
|
const [mutate] = useMutationWithToasts(
|
||||||
deleteTransferImpactAssessmentMutation,
|
deleteTransferImpactAssessmentMutation,
|
||||||
{
|
{
|
||||||
successMessage: __("TIA deleted successfully"),
|
successMessage: t("processingActivityGraph.messages.tiaDeleted"),
|
||||||
errorMessage: __("Failed to delete TIA"),
|
errorMessage: t("processingActivityGraph.errors.deleteTia"),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
@@ -723,9 +718,7 @@ export const useDeleteTransferImpactAssessment = (
|
|||||||
onSuccess: options?.onSuccess,
|
onSuccess: options?.onSuccess,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
message: __(
|
message: t("processingActivityGraph.tiaDeleteConfirmation"),
|
||||||
"This will permanently delete this Transfer Impact Assessment. This action cannot be undone.",
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,9 +18,9 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { promisifyMutation, sprintf } from "@probo/helpers";
|
import { promisifyMutation } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { useConfirm } from "@probo/ui";
|
import { useConfirm } from "@probo/ui";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMutation } from "react-relay";
|
import { useMutation } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
@@ -126,10 +126,10 @@ export const useDeleteRightsRequest = (
|
|||||||
request: { id: string },
|
request: { id: string },
|
||||||
connectionId: string,
|
connectionId: string,
|
||||||
) => {
|
) => {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const [mutate] = useMutationWithToasts(deleteRightsRequestMutation, {
|
const [mutate] = useMutationWithToasts(deleteRightsRequestMutation, {
|
||||||
successMessage: __("Rights request deleted successfully"),
|
successMessage: t("rightsRequestGraph.messages.deleted"),
|
||||||
errorMessage: __("Failed to delete rights request"),
|
errorMessage: t("rightsRequestGraph.errors.delete"),
|
||||||
});
|
});
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
|
|
||||||
@@ -145,11 +145,7 @@ export const useDeleteRightsRequest = (
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
message: sprintf(
|
message: t("rightsRequestGraph.deleteConfirmation"),
|
||||||
__(
|
|
||||||
"This will permanently delete the rights request. This action cannot be undone.",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -158,7 +154,7 @@ export const useDeleteRightsRequest = (
|
|||||||
export const useCreateRightsRequest = (connectionId: string) => {
|
export const useCreateRightsRequest = (connectionId: string) => {
|
||||||
// eslint-disable-next-line relay/generated-typescript-types
|
// eslint-disable-next-line relay/generated-typescript-types
|
||||||
const [mutate] = useMutation(createRightsRequestMutation);
|
const [mutate] = useMutation(createRightsRequestMutation);
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (input: {
|
return (input: {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
@@ -172,17 +168,17 @@ export const useCreateRightsRequest = (connectionId: string) => {
|
|||||||
}) => {
|
}) => {
|
||||||
if (!input.organizationId) {
|
if (!input.organizationId) {
|
||||||
return alert(
|
return alert(
|
||||||
__("Failed to create rights request: organization is required"),
|
t("rightsRequestGraph.errors.createOrganizationRequired"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!input.requestType) {
|
if (!input.requestType) {
|
||||||
return alert(
|
return alert(
|
||||||
__("Failed to create rights request: request type is required"),
|
t("rightsRequestGraph.errors.createRequestTypeRequired"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!input.requestState) {
|
if (!input.requestState) {
|
||||||
return alert(
|
return alert(
|
||||||
__("Failed to create rights request: request state is required"),
|
t("rightsRequestGraph.errors.createRequestStateRequired"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +203,7 @@ export const useCreateRightsRequest = (connectionId: string) => {
|
|||||||
export const useUpdateRightsRequest = () => {
|
export const useUpdateRightsRequest = () => {
|
||||||
// eslint-disable-next-line relay/generated-typescript-types
|
// eslint-disable-next-line relay/generated-typescript-types
|
||||||
const [mutate] = useMutation(updateRightsRequestMutation);
|
const [mutate] = useMutation(updateRightsRequestMutation);
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (input: {
|
return (input: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -220,7 +216,7 @@ export const useUpdateRightsRequest = () => {
|
|||||||
actionTaken?: string;
|
actionTaken?: string;
|
||||||
}) => {
|
}) => {
|
||||||
if (!input.id) {
|
if (!input.id) {
|
||||||
return alert(__("Failed to update rights request: ID is required"));
|
return alert(t("rightsRequestGraph.errors.updateIdRequired"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return promisifyMutation(mutate)({
|
return promisifyMutation(mutate)({
|
||||||
|
|||||||
@@ -19,9 +19,9 @@
|
|||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { formatError } from "@probo/helpers";
|
import { formatError } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { useToast } from "@probo/ui";
|
import { useToast } from "@probo/ui";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
useMutation,
|
useMutation,
|
||||||
type UseMutationConfig,
|
type UseMutationConfig,
|
||||||
@@ -55,7 +55,7 @@ export function useMutationWithIncrement<T extends MutationParameters>(
|
|||||||
const [mutate, isLoading] = useMutation<T>(query);
|
const [mutate, isLoading] = useMutation<T>(query);
|
||||||
const relayEnv = useRelayEnvironment();
|
const relayEnv = useRelayEnvironment();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const options = { ...defaultOptions, ...baseOptions };
|
const options = { ...defaultOptions, ...baseOptions };
|
||||||
const mutateAndIncrement = useCallback(
|
const mutateAndIncrement = useCallback(
|
||||||
(queryOptions: UseMutationConfig<T>) => {
|
(queryOptions: UseMutationConfig<T>) => {
|
||||||
@@ -63,9 +63,9 @@ export function useMutationWithIncrement<T extends MutationParameters>(
|
|||||||
...queryOptions,
|
...queryOptions,
|
||||||
onCompleted: (response, error) => {
|
onCompleted: (response, error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
const errorTitle = options.errorMessage ?? __("Failed to commit this operation");
|
const errorTitle = options.errorMessage ?? t("mutation.errors.commit");
|
||||||
toast({
|
toast({
|
||||||
title: __("Error"),
|
title: t("common.error"),
|
||||||
description: formatError(errorTitle, error),
|
description: formatError(errorTitle, error),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
@@ -81,9 +81,9 @@ export function useMutationWithIncrement<T extends MutationParameters>(
|
|||||||
queryOptions.onCompleted?.(response, error);
|
queryOptions.onCompleted?.(response, error);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
const errorTitle = options.errorMessage ?? __("Failed to commit this operation");
|
const errorTitle = options.errorMessage ?? t("mutation.errors.commit");
|
||||||
toast({
|
toast({
|
||||||
title: __("Error"),
|
title: t("common.error"),
|
||||||
description: formatError(errorTitle, error),
|
description: formatError(errorTitle, error),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
@@ -91,7 +91,7 @@ export function useMutationWithIncrement<T extends MutationParameters>(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[mutate, options.id, options.node, options.field, options.value, options.errorMessage, relayEnv, toast, __],
|
[mutate, options.id, options.node, options.field, options.value, options.errorMessage, relayEnv, toast, t],
|
||||||
);
|
);
|
||||||
|
|
||||||
return [mutateAndIncrement, isLoading] as const;
|
return [mutateAndIncrement, isLoading] as const;
|
||||||
|
|||||||
@@ -19,9 +19,9 @@
|
|||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { formatError } from "@probo/helpers";
|
import { formatError } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { useToast } from "@probo/ui";
|
import { useToast } from "@probo/ui";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMutation, type UseMutationConfig } from "react-relay";
|
import { useMutation, type UseMutationConfig } from "react-relay";
|
||||||
import type { GraphQLTaggedNode, MutationParameters } from "relay-runtime";
|
import type { GraphQLTaggedNode, MutationParameters } from "relay-runtime";
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ export function useMutationWithToasts<T extends MutationParameters>(
|
|||||||
) {
|
) {
|
||||||
const [mutate, isLoading] = useMutation<T>(query);
|
const [mutate, isLoading] = useMutation<T>(query);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const mutateWithToast = useCallback(
|
const mutateWithToast = useCallback(
|
||||||
(
|
(
|
||||||
queryOptions: UseMutationConfig<T> & {
|
queryOptions: UseMutationConfig<T> & {
|
||||||
@@ -53,9 +53,9 @@ export function useMutationWithToasts<T extends MutationParameters>(
|
|||||||
onCompleted: (response, error) => {
|
onCompleted: (response, error) => {
|
||||||
options.onCompleted?.(response, error);
|
options.onCompleted?.(response, error);
|
||||||
if (error) {
|
if (error) {
|
||||||
const errorTitle = options.errorMessage ?? __("Failed to commit this operation");
|
const errorTitle = options.errorMessage ?? t("mutation.errors.commit");
|
||||||
toast({
|
toast({
|
||||||
title: __("Error"),
|
title: t("common.error"),
|
||||||
description: formatError(errorTitle, error),
|
description: formatError(errorTitle, error),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
@@ -67,19 +67,19 @@ export function useMutationWithToasts<T extends MutationParameters>(
|
|||||||
: options.successMessage;
|
: options.successMessage;
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: __("Success"),
|
title: t("common.success"),
|
||||||
description:
|
description:
|
||||||
successMessage
|
successMessage
|
||||||
?? __("Operation completed successfully"),
|
?? t("mutation.messages.completed"),
|
||||||
variant: "success",
|
variant: "success",
|
||||||
});
|
});
|
||||||
options.onSuccess?.();
|
options.onSuccess?.();
|
||||||
resolve();
|
resolve();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
const errorTitle = options.errorMessage ?? __("Failed to commit this operation");
|
const errorTitle = options.errorMessage ?? t("mutation.errors.commit");
|
||||||
toast({
|
toast({
|
||||||
title: __("Error"),
|
title: t("common.error"),
|
||||||
description: formatError(errorTitle, error),
|
description: formatError(errorTitle, error),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
@@ -88,7 +88,7 @@ export function useMutationWithToasts<T extends MutationParameters>(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[mutate, toast, __, baseOptions],
|
[mutate, toast, t, baseOptions],
|
||||||
);
|
);
|
||||||
|
|
||||||
return [mutateWithToast, isLoading] as const;
|
return [mutateWithToast, isLoading] as const;
|
||||||
|
|||||||
84
apps/console/src/lib/i18n/backend.ts
Normal file
84
apps/console/src/lib/i18n/backend.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
// SOFTWARE.
|
||||||
|
|
||||||
|
import type { BackendModule, ReadCallback, ResourceKey } from "i18next";
|
||||||
|
|
||||||
|
// The default namespace for app-wide chrome strings, sourced from src/_locales/.
|
||||||
|
export const DEFAULT_NAMESPACE = "app";
|
||||||
|
|
||||||
|
type CatalogModule = { default: ResourceKey };
|
||||||
|
|
||||||
|
// Vite turns each translation JSON into its own lazily-imported chunk. The keys
|
||||||
|
// are project-root-absolute paths, e.g.
|
||||||
|
// "/src/pages/organizations/measures/_locales/en-US.json".
|
||||||
|
const catalogs = import.meta.glob<CatalogModule>("/src/**/_locales/*.json");
|
||||||
|
|
||||||
|
const CATALOG_PATH = /^\/src\/(.*)_locales\/([^/]+)\.json$/;
|
||||||
|
|
||||||
|
// Map "<namespace>\u0000<language>" -> lazy importer for that catalog chunk.
|
||||||
|
function buildLookup(): Map<string, () => Promise<CatalogModule>> {
|
||||||
|
const lookup = new Map<string, () => Promise<CatalogModule>>();
|
||||||
|
|
||||||
|
for (const [path, importer] of Object.entries(catalogs)) {
|
||||||
|
const match = CATALOG_PATH.exec(path);
|
||||||
|
if (!match) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, prefix, language] = match;
|
||||||
|
// prefix is the path between "src/" and "_locales/", e.g. "pages/foo/".
|
||||||
|
// Drop the leading "pages/" and trailing slash; an empty prefix (src/_locales)
|
||||||
|
// is the app-wide default namespace.
|
||||||
|
const namespace
|
||||||
|
= prefix.replace(/^pages\//, "").replace(/\/$/, "") || DEFAULT_NAMESPACE;
|
||||||
|
|
||||||
|
lookup.set(catalogKey(namespace, language), importer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lookup;
|
||||||
|
}
|
||||||
|
|
||||||
|
function catalogKey(namespace: string, language: string): string {
|
||||||
|
return `${namespace}\u0000${language}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lookup = buildLookup();
|
||||||
|
|
||||||
|
// Custom i18next backend that resolves a (language, namespace) pair to its lazy
|
||||||
|
// JSON chunk. A missing catalog resolves to an empty resource so i18next falls
|
||||||
|
// through to fallbackLng rather than treating it as a hard load error.
|
||||||
|
export const globBackend: BackendModule = {
|
||||||
|
type: "backend",
|
||||||
|
init() {},
|
||||||
|
read(language: string, namespace: string, callback: ReadCallback) {
|
||||||
|
const importer = lookup.get(catalogKey(namespace, language));
|
||||||
|
|
||||||
|
if (!importer) {
|
||||||
|
callback(null, {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
importer().then(
|
||||||
|
module => callback(null, module.default),
|
||||||
|
(error: unknown) =>
|
||||||
|
callback(error instanceof Error ? error : new Error(String(error)), null),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
47
apps/console/src/lib/i18n/i18n.ts
Normal file
47
apps/console/src/lib/i18n/i18n.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
// SOFTWARE.
|
||||||
|
|
||||||
|
import { createInstance } from "i18next";
|
||||||
|
import { initReactI18next } from "react-i18next";
|
||||||
|
|
||||||
|
import { DEFAULT_NAMESPACE, globBackend } from "./backend";
|
||||||
|
import { resolveLanguage, SUPPORTED_LANGUAGES } from "./resolveLanguage";
|
||||||
|
|
||||||
|
// Build a dedicated instance rather than mutating i18next's global singleton.
|
||||||
|
// Initializing it through initReactI18next still registers it as the instance
|
||||||
|
// react-i18next's hooks read from, so no I18nextProvider is required.
|
||||||
|
const i18n = createInstance();
|
||||||
|
|
||||||
|
void i18n
|
||||||
|
.use(globBackend)
|
||||||
|
.use(initReactI18next)
|
||||||
|
.init({
|
||||||
|
lng: resolveLanguage(),
|
||||||
|
fallbackLng: "en-US",
|
||||||
|
supportedLngs: SUPPORTED_LANGUAGES,
|
||||||
|
load: "currentOnly",
|
||||||
|
defaultNS: DEFAULT_NAMESPACE,
|
||||||
|
fallbackNS: DEFAULT_NAMESPACE,
|
||||||
|
ns: [DEFAULT_NAMESPACE],
|
||||||
|
interpolation: { escapeValue: false },
|
||||||
|
react: { useSuspense: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
export { i18n };
|
||||||
76
apps/console/src/lib/i18n/resolveLanguage.ts
Normal file
76
apps/console/src/lib/i18n/resolveLanguage.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
// SOFTWARE.
|
||||||
|
|
||||||
|
export const SUPPORTED_LANGUAGES = [
|
||||||
|
"en-US",
|
||||||
|
"fr-FR",
|
||||||
|
"de-DE",
|
||||||
|
"es-ES",
|
||||||
|
"id-ID",
|
||||||
|
"it-IT",
|
||||||
|
"ja-JP",
|
||||||
|
"ko-KR",
|
||||||
|
"pl-PL",
|
||||||
|
"pt-PT",
|
||||||
|
"tr-TR",
|
||||||
|
"uk-UA",
|
||||||
|
"zh-CN",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number];
|
||||||
|
|
||||||
|
// Maps a two-letter language prefix (lowercased) to its canonical supported
|
||||||
|
// tag, e.g. any "de*" browser tag resolves to "de-DE".
|
||||||
|
const PREFIX_TO_LANGUAGE: Record<string, SupportedLanguage> = {
|
||||||
|
en: "en-US",
|
||||||
|
fr: "fr-FR",
|
||||||
|
de: "de-DE",
|
||||||
|
es: "es-ES",
|
||||||
|
id: "id-ID",
|
||||||
|
it: "it-IT",
|
||||||
|
ja: "ja-JP",
|
||||||
|
ko: "ko-KR",
|
||||||
|
pl: "pl-PL",
|
||||||
|
pt: "pt-PT",
|
||||||
|
tr: "tr-TR",
|
||||||
|
uk: "uk-UA",
|
||||||
|
zh: "zh-CN",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Collapse the browser's preferred languages to one of our supported locales
|
||||||
|
// by matching the two-letter language prefix (e.g. any "fr*" tag maps to
|
||||||
|
// fr-FR). en-US is the ultimate fallback when nothing matches. Resolving to a
|
||||||
|
// canonical supported tag here means i18next is never asked to load an
|
||||||
|
// unsupported locale; fallbackLng only has to cover individual missing keys.
|
||||||
|
export function resolveLanguage(): SupportedLanguage {
|
||||||
|
const candidates = navigator.languages?.length
|
||||||
|
? navigator.languages
|
||||||
|
: [navigator.language];
|
||||||
|
|
||||||
|
for (const tag of candidates) {
|
||||||
|
const prefix = tag.toLowerCase().split("-")[0];
|
||||||
|
const language = PREFIX_TO_LANGUAGE[prefix];
|
||||||
|
if (language) {
|
||||||
|
return language;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "en-US";
|
||||||
|
}
|
||||||
@@ -13,10 +13,10 @@
|
|||||||
// PERFORMANCE OF THIS SOFTWARE.
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
import { formatError } from "@probo/helpers";
|
import { formatError } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { createUseMutation, type MutationNotifier } from "@probo/relay";
|
import { createUseMutation, type MutationNotifier } from "@probo/relay";
|
||||||
import { useToast } from "@probo/ui";
|
import { useToast } from "@probo/ui";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Binds the shared awaitable useMutation (`@probo/relay`) to this app's
|
* Binds the shared awaitable useMutation (`@probo/relay`) to this app's
|
||||||
@@ -28,7 +28,7 @@ import { useMemo } from "react";
|
|||||||
*/
|
*/
|
||||||
function useMutationNotifier(): MutationNotifier {
|
function useMutationNotifier(): MutationNotifier {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return useMemo<MutationNotifier>(
|
return useMemo<MutationNotifier>(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -36,7 +36,7 @@ function useMutationNotifier(): MutationNotifier {
|
|||||||
toast({ title, description: "", variant: "success" });
|
toast({ title, description: "", variant: "success" });
|
||||||
},
|
},
|
||||||
notifyError: (error, title) => {
|
notifyError: (error, title) => {
|
||||||
const finalTitle = title ?? __("Error");
|
const finalTitle = title ?? t("common.error");
|
||||||
toast({
|
toast({
|
||||||
title: finalTitle,
|
title: finalTitle,
|
||||||
description: formatError(finalTitle, error),
|
description: formatError(finalTitle, error),
|
||||||
@@ -44,7 +44,7 @@ function useMutationNotifier(): MutationNotifier {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[toast, __],
|
[toast, t],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { createRoot } from "react-dom/client";
|
|||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
import { App } from "./App";
|
import { App } from "./App";
|
||||||
import { TranslatorProvider } from "./providers/TranslatorProvider";
|
import "./lib/i18n/i18n";
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
@@ -36,8 +36,6 @@ const queryClient = new QueryClient({
|
|||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<TranslatorProvider>
|
|
||||||
<App />
|
<App />
|
||||||
</TranslatorProvider>
|
|
||||||
</QueryClientProvider>,
|
</QueryClientProvider>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslation } from "react-i18next";
|
||||||
import { graphql, type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
import { graphql, type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
||||||
|
|
||||||
import type { APIKeysPageQuery } from "#/__generated__/iam/APIKeysPageQuery.graphql";
|
import type { APIKeysPageQuery } from "#/__generated__/iam/APIKeysPageQuery.graphql";
|
||||||
@@ -37,13 +37,13 @@ export function APIKeysPage(props: {
|
|||||||
queryRef: PreloadedQuery<APIKeysPageQuery>;
|
queryRef: PreloadedQuery<APIKeysPageQuery>;
|
||||||
}) {
|
}) {
|
||||||
const { queryRef } = props;
|
const { queryRef } = props;
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const data = usePreloadedQuery<APIKeysPageQuery>(apiKeysPageQuery, queryRef);
|
const data = usePreloadedQuery<APIKeysPageQuery>(apiKeysPageQuery, queryRef);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 w-full py-6">
|
<div className="space-y-6 w-full py-6">
|
||||||
<h1 className="text-3xl font-bold text-center">{__("API Keys")}</h1>
|
<h1 className="text-3xl font-bold text-center">{t("apiKeys.title")}</h1>
|
||||||
{data.viewer && <PersonalAPIKeyList fKey={data.viewer} />}
|
{data.viewer && <PersonalAPIKeyList fKey={data.viewer} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { formatError } from "@probo/helpers";
|
import { formatError } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
Button,
|
Button,
|
||||||
@@ -37,6 +36,7 @@ import {
|
|||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller } from "react-hook-form";
|
import { Controller } from "react-hook-form";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
ConnectionHandler,
|
ConnectionHandler,
|
||||||
graphql,
|
graphql,
|
||||||
@@ -120,7 +120,7 @@ export function PersonalAPIKeyList(props: {
|
|||||||
fKey: PersonalAPIKeyListFragment$key;
|
fKey: PersonalAPIKeyListFragment$key;
|
||||||
}) {
|
}) {
|
||||||
const { fKey } = props;
|
const { fKey } = props;
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const createDialogRef = useDialogRef();
|
const createDialogRef = useDialogRef();
|
||||||
const tokenDialogRef = useDialogRef();
|
const tokenDialogRef = useDialogRef();
|
||||||
@@ -162,8 +162,8 @@ export function PersonalAPIKeyList(props: {
|
|||||||
},
|
},
|
||||||
onCompleted: (response) => {
|
onCompleted: (response) => {
|
||||||
toast({
|
toast({
|
||||||
title: __("Success"),
|
title: t("common.success"),
|
||||||
description: __("API key created successfully."),
|
description: t("personalApiKeyList.messages.created"),
|
||||||
variant: "success",
|
variant: "success",
|
||||||
});
|
});
|
||||||
const newToken = response.createPersonalAPIKey?.token;
|
const newToken = response.createPersonalAPIKey?.token;
|
||||||
@@ -176,8 +176,8 @@ export function PersonalAPIKeyList(props: {
|
|||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast({
|
toast({
|
||||||
title: __("Error"),
|
title: t("common.error"),
|
||||||
description: formatError(__("Failed to create API key."), error),
|
description: formatError(t("personalApiKeyList.errors.create"), error),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -188,9 +188,9 @@ export function PersonalAPIKeyList(props: {
|
|||||||
<>
|
<>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<h2 className="text-base font-medium">{__("API Keys")}</h2>
|
<h2 className="text-base font-medium">{t("apiKeys.title")}</h2>
|
||||||
<Button onClick={() => createDialogRef.current?.open()}>
|
<Button onClick={() => createDialogRef.current?.open()}>
|
||||||
{__("Create API Key")}
|
{t("personalApiKeyList.actions.create")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -199,10 +199,10 @@ export function PersonalAPIKeyList(props: {
|
|||||||
<Card padded>
|
<Card padded>
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||||
{__("No API keys")}
|
{t("personalApiKeyList.empty.title")}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-600 mb-6">
|
<p className="text-gray-600 mb-6">
|
||||||
{__("Create an API key to authenticate programmatic access.")}
|
{t("personalApiKeyList.empty.description")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -219,21 +219,21 @@ export function PersonalAPIKeyList(props: {
|
|||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
ref={createDialogRef}
|
ref={createDialogRef}
|
||||||
title={<Breadcrumb items={[__("API Keys"), __("Create")]} />}
|
title={<Breadcrumb items={[t("apiKeys.title"), t("common.create")]} />}
|
||||||
onClose={() => reset()}
|
onClose={() => reset()}
|
||||||
>
|
>
|
||||||
<form onSubmit={e => void handleSubmit(handleCreate)(e)}>
|
<form onSubmit={e => void handleSubmit(handleCreate)(e)}>
|
||||||
<DialogContent padded className="space-y-5">
|
<DialogContent padded className="space-y-5">
|
||||||
<Field error={formState.errors.name?.message}>
|
<Field error={formState.errors.name?.message}>
|
||||||
<Label>{__("Name")}</Label>
|
<Label>{t("personalApiKeyList.fields.name.label")}</Label>
|
||||||
<Input
|
<Input
|
||||||
{...register("name")}
|
{...register("name")}
|
||||||
placeholder={__("e.g., Production API Key")}
|
placeholder={t("personalApiKeyList.fields.name.placeholder")}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field error={formState.errors.expiresIn?.message}>
|
<Field error={formState.errors.expiresIn?.message}>
|
||||||
<Label>{__("Expires In")}</Label>
|
<Label>{t("personalApiKeyList.fields.expiresIn.label")}</Label>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="expiresIn"
|
name="expiresIn"
|
||||||
@@ -243,10 +243,18 @@ export function PersonalAPIKeyList(props: {
|
|||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
>
|
>
|
||||||
<Option value="1month">{__("1 Month")}</Option>
|
<Option value="1month">
|
||||||
<Option value="3months">{__("3 Months")}</Option>
|
{t("personalApiKeyList.expirations.month", { count: 1 })}
|
||||||
<Option value="6months">{__("6 Months")}</Option>
|
</Option>
|
||||||
<Option value="1year">{__("1 Year")}</Option>
|
<Option value="3months">
|
||||||
|
{t("personalApiKeyList.expirations.month", { count: 3 })}
|
||||||
|
</Option>
|
||||||
|
<Option value="6months">
|
||||||
|
{t("personalApiKeyList.expirations.month", { count: 6 })}
|
||||||
|
</Option>
|
||||||
|
<Option value="1year">
|
||||||
|
{t("personalApiKeyList.expirations.year", { count: 1 })}
|
||||||
|
</Option>
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -254,7 +262,7 @@ export function PersonalAPIKeyList(props: {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button type="submit" disabled={isCreating}>
|
<Button type="submit" disabled={isCreating}>
|
||||||
{isCreating ? __("Creating...") : __("Create")}
|
{isCreating ? t("personalApiKeyList.actions.creating") : t("common.create")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -18,11 +18,12 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { formatDate, formatError } from "@probo/helpers";
|
import { formatError } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
import { dateFormat } from "@probo/i18n";
|
||||||
import { Button, Spinner, Td, Tr, useConfirm, useToast } from "@probo/ui";
|
import { Button, Spinner, Td, Tr, useConfirm, useToast } from "@probo/ui";
|
||||||
import { clsx } from "clsx";
|
import { clsx } from "clsx";
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFragment, useMutation } from "react-relay";
|
import { useFragment, useMutation } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@ export function PersonalAPIKeyRow(props: {
|
|||||||
connectionId: string;
|
connectionId: string;
|
||||||
}) {
|
}) {
|
||||||
const { fKey, connectionId } = props;
|
const { fKey, connectionId } = props;
|
||||||
const { __ } = useTranslate();
|
const { t, i18n } = useTranslation();
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -84,28 +85,28 @@ export function PersonalAPIKeyRow(props: {
|
|||||||
onCompleted: (_response, errors) => {
|
onCompleted: (_response, errors) => {
|
||||||
if (errors?.length) {
|
if (errors?.length) {
|
||||||
toast({
|
toast({
|
||||||
title: __("Error"),
|
title: t("common.error"),
|
||||||
description: formatError(
|
description: formatError(
|
||||||
__("Failed to revoke API key."),
|
t("personalApiKeyRow.errors.revoke"),
|
||||||
errors,
|
errors,
|
||||||
),
|
),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
reject(new Error(errors[0]?.message ?? __("Failed to revoke API key.")));
|
reject(new Error(errors[0]?.message ?? t("personalApiKeyRow.errors.revoke")));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
toast({
|
toast({
|
||||||
title: __("Success"),
|
title: t("common.success"),
|
||||||
description: __("API key revoked successfully."),
|
description: t("personalApiKeyRow.messages.revoked"),
|
||||||
variant: "success",
|
variant: "success",
|
||||||
});
|
});
|
||||||
resolve();
|
resolve();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast({
|
toast({
|
||||||
title: __("Error"),
|
title: t("common.error"),
|
||||||
description: formatError(
|
description: formatError(
|
||||||
__("Failed to revoke API key."),
|
t("personalApiKeyRow.errors.revoke"),
|
||||||
error,
|
error,
|
||||||
),
|
),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
@@ -116,11 +117,9 @@ export function PersonalAPIKeyRow(props: {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: __("Revoke API Key"),
|
title: t("personalApiKeyRow.revoke.title"),
|
||||||
message: __(
|
message: t("personalApiKeyRow.revoke.confirmation", { name: key.name }),
|
||||||
`Are you sure you want to revoke the API key "${key.name}"? This action cannot be undone.`,
|
label: t("personalApiKeyRow.actions.revoke"),
|
||||||
),
|
|
||||||
label: __("Revoke"),
|
|
||||||
variant: "danger",
|
variant: "danger",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -131,22 +130,22 @@ export function PersonalAPIKeyRow(props: {
|
|||||||
<Td>
|
<Td>
|
||||||
<div className="font-medium text-txt-primary">{key.name}</div>
|
<div className="font-medium text-txt-primary">{key.name}</div>
|
||||||
<div className="text-xs text-txt-tertiary">
|
<div className="text-xs text-txt-tertiary">
|
||||||
{expired ? __("Expired") : __("Active")}
|
{expired ? t("personalApiKeyRow.status.expired") : t("personalApiKeyRow.status.active")}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<span className="text-sm text-txt-secondary">
|
<span className="text-sm text-txt-secondary">
|
||||||
{key.lastUsedAt ? formatDate(key.lastUsedAt) : __("Never")}
|
{key.lastUsedAt ? dateFormat(i18n.language, key.lastUsedAt) : t("personalApiKeyRow.never")}
|
||||||
</span>
|
</span>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<span className="text-sm text-txt-secondary">
|
<span className="text-sm text-txt-secondary">
|
||||||
{formatDate(key.createdAt)}
|
{dateFormat(i18n.language, key.createdAt)}
|
||||||
</span>
|
</span>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<span className="text-sm text-txt-secondary">
|
<span className="text-sm text-txt-secondary">
|
||||||
{formatDate(key.expiresAt)}
|
{dateFormat(i18n.language, key.expiresAt)}
|
||||||
</span>
|
</span>
|
||||||
</Td>
|
</Td>
|
||||||
<Td width={140} className="text-end">
|
<Td width={140} className="text-end">
|
||||||
@@ -155,7 +154,7 @@ export function PersonalAPIKeyRow(props: {
|
|||||||
<PersonalAPIKeyTokenAction fKey={fKey} disabled={isRevoking} />
|
<PersonalAPIKeyTokenAction fKey={fKey} disabled={isRevoking} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<Button variant="danger" onClick={handleRevoke} disabled={isRevoking}>
|
<Button variant="danger" onClick={handleRevoke} disabled={isRevoking}>
|
||||||
{__("Revoke")}
|
{t("personalApiKeyRow.actions.revoke")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
|
|||||||
@@ -19,8 +19,8 @@
|
|||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { formatError } from "@probo/helpers";
|
import { formatError } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Button, useDialogRef, useToast } from "@probo/ui";
|
import { Button, useDialogRef, useToast } from "@probo/ui";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useRefetchableFragment } from "react-relay";
|
import { useRefetchableFragment } from "react-relay";
|
||||||
|
|
||||||
import type { PersonalAPIKeyRowFragment$key } from "#/__generated__/iam/PersonalAPIKeyRowFragment.graphql";
|
import type { PersonalAPIKeyRowFragment$key } from "#/__generated__/iam/PersonalAPIKeyRowFragment.graphql";
|
||||||
@@ -34,7 +34,7 @@ export function PersonalAPIKeyTokenAction(props: {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { fKey, disabled } = props;
|
const { fKey, disabled } = props;
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const dialogRef = useDialogRef();
|
const dialogRef = useDialogRef();
|
||||||
|
|
||||||
@@ -53,9 +53,9 @@ export function PersonalAPIKeyTokenAction(props: {
|
|||||||
onComplete: (error) => {
|
onComplete: (error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
toast({
|
toast({
|
||||||
title: __("Error"),
|
title: t("common.error"),
|
||||||
description: formatError(
|
description: formatError(
|
||||||
__("Failed to load API key token."),
|
t("personalApiKeyTokenAction.errors.load"),
|
||||||
error,
|
error,
|
||||||
),
|
),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
@@ -70,7 +70,7 @@ export function PersonalAPIKeyTokenAction(props: {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button variant="secondary" onClick={handleShow} disabled={!!disabled}>
|
<Button variant="secondary" onClick={handleShow} disabled={!!disabled}>
|
||||||
{__("Show")}
|
{t("personalApiKeyTokenAction.actions.show")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<PersonalAPIKeyTokenDialog
|
<PersonalAPIKeyTokenDialog
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useCopy } from "@probo/hooks";
|
import { useCopy } from "@probo/hooks";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
Button,
|
Button,
|
||||||
@@ -27,6 +26,7 @@ import {
|
|||||||
DialogContent,
|
DialogContent,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export function PersonalAPIKeyTokenDialog(props: {
|
export function PersonalAPIKeyTokenDialog(props: {
|
||||||
dialogRef: React.RefObject<{ open: () => void; close: () => void } | null>;
|
dialogRef: React.RefObject<{ open: () => void; close: () => void } | null>;
|
||||||
@@ -34,13 +34,13 @@ export function PersonalAPIKeyTokenDialog(props: {
|
|||||||
onDone: () => void;
|
onDone: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { dialogRef, token, onDone } = props;
|
const { dialogRef, token, onDone } = props;
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const [isCopied, copy] = useCopy();
|
const [isCopied, copy] = useCopy();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
ref={dialogRef}
|
ref={dialogRef}
|
||||||
title={<Breadcrumb items={[__("API Keys"), __("Token")]} />}
|
title={<Breadcrumb items={[t("apiKeys.title"), t("personalApiKeyTokenDialog.title")]} />}
|
||||||
>
|
>
|
||||||
<DialogContent padded className="space-y-4">
|
<DialogContent padded className="space-y-4">
|
||||||
<div className="bg-gray-100 p-4 rounded-lg flex items-center gap-2">
|
<div className="bg-gray-100 p-4 rounded-lg flex items-center gap-2">
|
||||||
@@ -50,12 +50,12 @@ export function PersonalAPIKeyTokenDialog(props: {
|
|||||||
onClick={() => copy(token)}
|
onClick={() => copy(token)}
|
||||||
disabled={!token}
|
disabled={!token}
|
||||||
>
|
>
|
||||||
{isCopied ? __("Copied") : __("Copy")}
|
{isCopied ? t("personalApiKeyTokenDialog.actions.copied") : t("personalApiKeyTokenDialog.actions.copy")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button onClick={onDone}>{__("Done")}</Button>
|
<Button onClick={onDone}>{t("personalApiKeyTokenDialog.actions.done")}</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Table, Tbody, Th, Thead, Tr } from "@probo/ui";
|
import { Table, Tbody, Th, Thead, Tr } from "@probo/ui";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import type { PersonalAPIKeyListFragment$data } from "#/__generated__/iam/PersonalAPIKeyListFragment.graphql";
|
import type { PersonalAPIKeyListFragment$data } from "#/__generated__/iam/PersonalAPIKeyListFragment.graphql";
|
||||||
|
|
||||||
@@ -30,16 +30,16 @@ export function PersonalAPIKeysTable(props: {
|
|||||||
connectionId: string;
|
connectionId: string;
|
||||||
}) {
|
}) {
|
||||||
const { edges, connectionId } = props;
|
const { edges, connectionId } = props;
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table>
|
<Table>
|
||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>{__("Name")}</Th>
|
<Th>{t("personalApiKeysTable.columns.name")}</Th>
|
||||||
<Th>{__("Last used")}</Th>
|
<Th>{t("personalApiKeysTable.columns.lastUsed")}</Th>
|
||||||
<Th>{__("Created")}</Th>
|
<Th>{t("personalApiKeysTable.columns.created")}</Th>
|
||||||
<Th>{__("Expires")}</Th>
|
<Th>{t("personalApiKeysTable.columns.expires")}</Th>
|
||||||
<Th></Th>
|
<Th></Th>
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
|
|||||||
@@ -20,9 +20,9 @@
|
|||||||
|
|
||||||
import { formatError, type GraphQLError } from "@probo/helpers";
|
import { formatError, type GraphQLError } from "@probo/helpers";
|
||||||
import { usePageTitle } from "@probo/hooks";
|
import { usePageTitle } from "@probo/hooks";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { useToast } from "@probo/ui";
|
import { useToast } from "@probo/ui";
|
||||||
import { useCallback, useEffect, useRef } from "react";
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMutation } from "react-relay";
|
import { useMutation } from "react-relay";
|
||||||
import { Link, useNavigate, useSearchParams } from "react-router";
|
import { Link, useNavigate, useSearchParams } from "react-router";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
@@ -42,14 +42,14 @@ const activateAccountMutation = graphql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export default function ActivateAccountPage() {
|
export default function ActivateAccountPage() {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const submittedRef = useRef<boolean>(false);
|
const submittedRef = useRef<boolean>(false);
|
||||||
const safeContinueUrl = useSafeContinueUrl();
|
const safeContinueUrl = useSafeContinueUrl();
|
||||||
|
|
||||||
usePageTitle(__("Activate Account"));
|
usePageTitle(t("activateAccountPage.pageTitle"));
|
||||||
|
|
||||||
const [activateAccount] = useMutation<ActivateAccountPageMutation>(activateAccountMutation);
|
const [activateAccount] = useMutation<ActivateAccountPageMutation>(activateAccountMutation);
|
||||||
|
|
||||||
@@ -72,8 +72,8 @@ export default function ActivateAccountPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
toast({
|
toast({
|
||||||
title: __("Activation failed"),
|
title: t("activateAccountPage.errors.activationFailed"),
|
||||||
description: formatError(__("Activation failed"), errors),
|
description: formatError(t("activateAccountPage.errors.activationFailed"), errors),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -81,10 +81,8 @@ export default function ActivateAccountPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: __("Success"),
|
title: t("common.success"),
|
||||||
description: __(
|
description: t("activateAccountPage.messages.activated"),
|
||||||
"Account activated successfully.",
|
|
||||||
),
|
|
||||||
variant: "success",
|
variant: "success",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -125,13 +123,13 @@ export default function ActivateAccountPage() {
|
|||||||
},
|
},
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
toast({
|
toast({
|
||||||
title: __("Activation failed"),
|
title: t("activateAccountPage.errors.activationFailed"),
|
||||||
description: e.message,
|
description: e.message,
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [__, toast, activateAccount, navigate, safeContinueUrl]);
|
}, [t, toast, activateAccount, navigate, safeContinueUrl]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const token = searchParams.get("token");
|
const token = searchParams.get("token");
|
||||||
@@ -144,9 +142,9 @@ export default function ActivateAccountPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6 w-full max-w-md mx-auto pt-8">
|
<div className="space-y-6 w-full max-w-md mx-auto pt-8">
|
||||||
<div className="space-y-2 text-center">
|
<div className="space-y-2 text-center">
|
||||||
<h1 className="text-3xl font-bold">{__("Account Activation")}</h1>
|
<h1 className="text-3xl font-bold">{t("activateAccountPage.title")}</h1>
|
||||||
<p className="text-txt-tertiary">
|
<p className="text-txt-tertiary">
|
||||||
{__("Activating your account…")}
|
{t("activateAccountPage.activating")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center mt-6 text-sm text-txt-secondary">
|
<div className="text-center mt-6 text-sm text-txt-secondary">
|
||||||
@@ -154,7 +152,7 @@ export default function ActivateAccountPage() {
|
|||||||
to="/auth/login"
|
to="/auth/login"
|
||||||
className="underline hover:text-txt-primary"
|
className="underline hover:text-txt-primary"
|
||||||
>
|
>
|
||||||
{__("Go back")}
|
{t("activateAccountPage.actions.goBack")}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
|
|
||||||
import { formatError } from "@probo/helpers";
|
import { formatError } from "@probo/helpers";
|
||||||
import { usePageTitle } from "@probo/hooks";
|
import { usePageTitle } from "@probo/hooks";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
IconArrowsClockwise,
|
IconArrowsClockwise,
|
||||||
@@ -34,6 +33,7 @@ import {
|
|||||||
useToast,
|
useToast,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { type PreloadedQuery, useMutation, usePreloadedQuery } from "react-relay";
|
import { type PreloadedQuery, useMutation, usePreloadedQuery } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
@@ -65,10 +65,10 @@ const approveConsentMutation = graphql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const scopeLabels: Record<string, string> = {
|
const scopeLabels: Record<string, string> = {
|
||||||
openid: "Verify your identity",
|
openid: "consentPage.scopes.openid",
|
||||||
email: "View your email address",
|
email: "consentPage.scopes.email",
|
||||||
profile: "View your profile information",
|
profile: "consentPage.scopes.profile",
|
||||||
offline_access: "Stay signed in and access your data while you're away",
|
offline_access: "consentPage.scopes.offlineAccess",
|
||||||
};
|
};
|
||||||
|
|
||||||
const scopeIcons: Record<string, React.ReactNode> = {
|
const scopeIcons: Record<string, React.ReactNode> = {
|
||||||
@@ -82,8 +82,9 @@ function scopeIcon(name: string): React.ReactNode {
|
|||||||
return scopeIcons[name] ?? <IconKey size={18} className="shrink-0 text-txt-tertiary" />;
|
return scopeIcons[name] ?? <IconKey size={18} className="shrink-0 text-txt-tertiary" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function scopeLabel(name: string): string {
|
function scopeLabel(name: string, translate: (key: string) => string): string {
|
||||||
return scopeLabels[name] ?? formatApiScopeLabel(name);
|
const key = scopeLabels[name];
|
||||||
|
return key ? translate(key) : formatApiScopeLabel(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isApiScope(scope: string): boolean {
|
function isApiScope(scope: string): boolean {
|
||||||
@@ -110,8 +111,7 @@ function ConsentScopeRow(props: {
|
|||||||
translate: (label: string) => string;
|
translate: (label: string) => string;
|
||||||
nested?: boolean;
|
nested?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const label = scopeLabel(props.scope);
|
const translated = scopeLabel(props.scope, props.translate);
|
||||||
const translated = label !== props.scope ? props.translate(label) : label;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
@@ -163,7 +163,7 @@ function ConsentApiScopesAccordion(props: {
|
|||||||
export default function ConsentPage(props: {
|
export default function ConsentPage(props: {
|
||||||
queryRef: PreloadedQuery<ConsentPageQuery>;
|
queryRef: PreloadedQuery<ConsentPageQuery>;
|
||||||
}) {
|
}) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [deviceResult, setDeviceResult] = useState<"authorized" | "denied" | null>(null);
|
const [deviceResult, setDeviceResult] = useState<"authorized" | "denied" | null>(null);
|
||||||
const [pendingAction, setPendingAction] = useState<"allow" | "deny" | null>(null);
|
const [pendingAction, setPendingAction] = useState<"allow" | "deny" | null>(null);
|
||||||
@@ -173,7 +173,7 @@ export default function ConsentPage(props: {
|
|||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
const data = usePreloadedQuery<ConsentPageQuery>(consentPageQuery, props.queryRef);
|
const data = usePreloadedQuery<ConsentPageQuery>(consentPageQuery, props.queryRef);
|
||||||
usePageTitle(__("Authorize Application"));
|
usePageTitle(t("consentPage.pageTitle"));
|
||||||
|
|
||||||
const { node: consent } = data;
|
const { node: consent } = data;
|
||||||
|
|
||||||
@@ -185,8 +185,8 @@ export default function ConsentPage(props: {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const apiScopesSummary = useMemo(
|
const apiScopesSummary = useMemo(
|
||||||
() => `${__("API access")} (${apiScopes.length})`,
|
() => t("consentPage.apiAccess", { count: apiScopes.length }),
|
||||||
[__, apiScopes.length],
|
[t, apiScopes.length],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -212,9 +212,9 @@ export default function ConsentPage(props: {
|
|||||||
if (errors) {
|
if (errors) {
|
||||||
setPendingAction(null);
|
setPendingAction(null);
|
||||||
toast({
|
toast({
|
||||||
title: __("Authorization failed"),
|
title: t("consentPage.errors.authorizationFailed"),
|
||||||
description: formatError(
|
description: formatError(
|
||||||
__("Something went wrong. Please try again."),
|
t("consentPage.errors.generic"),
|
||||||
errors,
|
errors,
|
||||||
),
|
),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
@@ -225,8 +225,8 @@ export default function ConsentPage(props: {
|
|||||||
if (!response.approveConsent) {
|
if (!response.approveConsent) {
|
||||||
setPendingAction(null);
|
setPendingAction(null);
|
||||||
toast({
|
toast({
|
||||||
title: __("Authorization failed"),
|
title: t("consentPage.errors.authorizationFailed"),
|
||||||
description: __("Something went wrong. Please try again."),
|
description: t("consentPage.errors.generic"),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -247,23 +247,23 @@ export default function ConsentPage(props: {
|
|||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
setPendingAction(null);
|
setPendingAction(null);
|
||||||
toast({
|
toast({
|
||||||
title: __("Error"),
|
title: t("common.error"),
|
||||||
description:
|
description:
|
||||||
err.message || __("Something went wrong. Please try again."),
|
err.message || t("consentPage.errors.generic"),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[consent, approveConsent, __, toast, pendingAction],
|
[consent, approveConsent, t, toast, pendingAction],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!consent.application || !consent.scopes) {
|
if (!consent.application || !consent.scopes) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-md mx-auto pt-8 space-y-6 text-center">
|
<div className="w-full max-w-md mx-auto pt-8 space-y-6 text-center">
|
||||||
<h1 className="text-2xl font-bold">{__("Invalid Request")}</h1>
|
<h1 className="text-2xl font-bold">{t("consentPage.invalidRequest.title")}</h1>
|
||||||
<p className="text-txt-tertiary">
|
<p className="text-txt-tertiary">
|
||||||
{__("This consent request is invalid or has expired.")}
|
{t("consentPage.invalidRequest.description")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -272,9 +272,9 @@ export default function ConsentPage(props: {
|
|||||||
if (deviceResult === "authorized") {
|
if (deviceResult === "authorized") {
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-md mx-auto pt-8 space-y-6 text-center">
|
<div className="w-full max-w-md mx-auto pt-8 space-y-6 text-center">
|
||||||
<h1 className="text-2xl font-bold">{__("Device Authorized")}</h1>
|
<h1 className="text-2xl font-bold">{t("consentPage.deviceAuthorized.title")}</h1>
|
||||||
<p className="text-txt-tertiary">
|
<p className="text-txt-tertiary">
|
||||||
{__("Your device has been successfully authorized. You can close this window and return to your device.")}
|
{t("consentPage.deviceAuthorized.description")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -283,9 +283,9 @@ export default function ConsentPage(props: {
|
|||||||
if (deviceResult === "denied") {
|
if (deviceResult === "denied") {
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-md mx-auto pt-8 space-y-6 text-center">
|
<div className="w-full max-w-md mx-auto pt-8 space-y-6 text-center">
|
||||||
<h1 className="text-2xl font-bold">{__("Access Denied")}</h1>
|
<h1 className="text-2xl font-bold">{t("consentPage.accessDenied.title")}</h1>
|
||||||
<p className="text-txt-tertiary">
|
<p className="text-txt-tertiary">
|
||||||
{__("You have denied the authorization request. You can close this window.")}
|
{t("consentPage.accessDenied.description")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -297,10 +297,10 @@ export default function ConsentPage(props: {
|
|||||||
<Spinner size={24} centered className="text-txt-tertiary" />
|
<Spinner size={24} centered className="text-txt-tertiary" />
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h1 className="text-2xl font-bold">
|
<h1 className="text-2xl font-bold">
|
||||||
{redirectState.approved ? __("Authorization Complete") : __("Access Denied")}
|
{redirectState.approved ? t("consentPage.authorizationComplete") : t("consentPage.accessDenied.title")}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-txt-tertiary">
|
<p className="text-txt-tertiary">
|
||||||
{__("You will be redirected to")}
|
{t("consentPage.redirectingTo")}
|
||||||
{" "}
|
{" "}
|
||||||
<span className="font-medium text-txt-secondary">
|
<span className="font-medium text-txt-secondary">
|
||||||
{consent.application.name}
|
{consent.application.name}
|
||||||
@@ -321,14 +321,12 @@ export default function ConsentPage(props: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-bold">
|
<h1 className="text-2xl font-bold">
|
||||||
{__("Authorize")}
|
{t("consentPage.authorize")}
|
||||||
{" "}
|
{" "}
|
||||||
<span className="font-bold">{consent.application.name}</span>
|
<span className="font-bold">{consent.application.name}</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-txt-tertiary text-sm">
|
<p className="text-txt-tertiary text-sm">
|
||||||
{__(
|
{t("consentPage.description")}
|
||||||
"This application is requesting access to your account with the following permissions:",
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -339,7 +337,7 @@ export default function ConsentPage(props: {
|
|||||||
<ConsentScopeRow
|
<ConsentScopeRow
|
||||||
key={scope}
|
key={scope}
|
||||||
scope={scope}
|
scope={scope}
|
||||||
translate={__}
|
translate={t}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -347,7 +345,7 @@ export default function ConsentPage(props: {
|
|||||||
|
|
||||||
<ConsentApiScopesAccordion
|
<ConsentApiScopesAccordion
|
||||||
scopes={apiScopes}
|
scopes={apiScopes}
|
||||||
translate={__}
|
translate={t}
|
||||||
summaryLabel={apiScopesSummary}
|
summaryLabel={apiScopesSummary}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -360,7 +358,7 @@ export default function ConsentPage(props: {
|
|||||||
icon={pendingAction === "deny" ? Spinner : undefined}
|
icon={pendingAction === "deny" ? Spinner : undefined}
|
||||||
onClick={() => handleAction(false)}
|
onClick={() => handleAction(false)}
|
||||||
>
|
>
|
||||||
{__("Deny")}
|
{t("consentPage.actions.deny")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="flex-1 h-10"
|
className="flex-1 h-10"
|
||||||
@@ -368,12 +366,12 @@ export default function ConsentPage(props: {
|
|||||||
icon={pendingAction === "allow" ? Spinner : undefined}
|
icon={pendingAction === "allow" ? Spinner : undefined}
|
||||||
onClick={() => handleAction(true)}
|
onClick={() => handleAction(true)}
|
||||||
>
|
>
|
||||||
{__("Allow")}
|
{t("consentPage.actions.allow")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-center text-xs text-txt-tertiary">
|
<p className="text-center text-xs text-txt-tertiary">
|
||||||
{__("You can revoke access at any time from your account settings.")}
|
{t("consentPage.revokeNotice")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Component, type ReactNode, useEffect } from "react";
|
import { Component, type ReactNode, useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useQueryLoader } from "react-relay";
|
import { useQueryLoader } from "react-relay";
|
||||||
import { useSearchParams } from "react-router";
|
import { useSearchParams } from "react-router";
|
||||||
|
|
||||||
@@ -61,13 +61,13 @@ class ConsentErrorBoundary extends Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ConsentErrorFallback() {
|
function ConsentErrorFallback() {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-md mx-auto pt-8 space-y-6 text-center">
|
<div className="w-full max-w-md mx-auto pt-8 space-y-6 text-center">
|
||||||
<h1 className="text-2xl font-bold">{__("Invalid Request")}</h1>
|
<h1 className="text-2xl font-bold">{t("consentPage.invalidRequest.title")}</h1>
|
||||||
<p className="text-txt-tertiary">
|
<p className="text-txt-tertiary">
|
||||||
{__("This consent request is invalid or has expired.")}
|
{t("consentPage.invalidRequest.description")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,8 +20,8 @@
|
|||||||
|
|
||||||
import { formatError } from "@probo/helpers";
|
import { formatError } from "@probo/helpers";
|
||||||
import { usePageTitle } from "@probo/hooks";
|
import { usePageTitle } from "@probo/hooks";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Button, Field, useToast } from "@probo/ui";
|
import { Button, Field, useToast } from "@probo/ui";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMutation } from "react-relay";
|
import { useMutation } from "react-relay";
|
||||||
import { Link, useNavigate, useSearchParams } from "react-router";
|
import { Link, useNavigate, useSearchParams } from "react-router";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
@@ -43,12 +43,12 @@ const schema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default function CreatePasswordPage() {
|
export default function CreatePasswordPage() {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
usePageTitle(__("Create Password"));
|
usePageTitle(t("createPasswordPage.pageTitle"));
|
||||||
|
|
||||||
const { register, handleSubmit, formState } = useFormWithSchema(schema, {
|
const { register, handleSubmit, formState } = useFormWithSchema(schema, {
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -69,16 +69,16 @@ export default function CreatePasswordPage() {
|
|||||||
onCompleted: (_, e) => {
|
onCompleted: (_, e) => {
|
||||||
if (e) {
|
if (e) {
|
||||||
toast({
|
toast({
|
||||||
title: __("Password creation failed"),
|
title: t("createPasswordPage.errors.creationFailed"),
|
||||||
description: formatError(__("Password creation failed"), e),
|
description: formatError(t("createPasswordPage.errors.creationFailed"), e),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: __("Success"),
|
title: t("common.success"),
|
||||||
description: __("Account created successfully"),
|
description: t("createPasswordPage.messages.created"),
|
||||||
variant: "success",
|
variant: "success",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ export default function CreatePasswordPage() {
|
|||||||
},
|
},
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
toast({
|
toast({
|
||||||
title: __("Password creation failed"),
|
title: t("createPasswordPage.errors.creationFailed"),
|
||||||
description: e.message,
|
description: e.message,
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
@@ -103,15 +103,15 @@ export default function CreatePasswordPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6 w-full max-w-md mx-auto pt-8">
|
<div className="space-y-6 w-full max-w-md mx-auto pt-8">
|
||||||
<div className="space-y-2 text-center">
|
<div className="space-y-2 text-center">
|
||||||
<h1 className="text-3xl font-bold">{__("Create a password")}</h1>
|
<h1 className="text-3xl font-bold">{t("createPasswordPage.title")}</h1>
|
||||||
<p className="text-txt-tertiary">
|
<p className="text-txt-tertiary">
|
||||||
{__("Set a password for your account, with at least 8 characters")}
|
{t("createPasswordPage.description")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={e => void handleSubmit(onSubmit)(e)} className="space-y-4">
|
<form onSubmit={e => void handleSubmit(onSubmit)(e)} className="space-y-4">
|
||||||
<Field
|
<Field
|
||||||
label={__("Password")}
|
label={t("createPasswordPage.fields.password")}
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
{...register("password")}
|
{...register("password")}
|
||||||
@@ -120,19 +120,19 @@ export default function CreatePasswordPage() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Button type="submit" className="w-xs h-10 mx-auto mt-6" disabled={formState.isLoading || isCreatingPassword}>
|
<Button type="submit" className="w-xs h-10 mx-auto mt-6" disabled={formState.isLoading || isCreatingPassword}>
|
||||||
{__("Save")}
|
{t("createPasswordPage.actions.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-sm text-txt-tertiary">
|
<p className="text-sm text-txt-tertiary">
|
||||||
{__("Already have an account?")}
|
{t("createPasswordPage.alreadyHaveAccount")}
|
||||||
{" "}
|
{" "}
|
||||||
<Link
|
<Link
|
||||||
to="/auth/login"
|
to="/auth/login"
|
||||||
className="underline text-txt-primary hover:text-txt-secondary"
|
className="underline text-txt-primary hover:text-txt-secondary"
|
||||||
>
|
>
|
||||||
{__("Log in here")}
|
{t("createPasswordPage.actions.logIn")}
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
|
|
||||||
import { formatError } from "@probo/helpers";
|
import { formatError } from "@probo/helpers";
|
||||||
import { usePageTitle } from "@probo/hooks";
|
import { usePageTitle } from "@probo/hooks";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Button, useToast } from "@probo/ui";
|
import { Button, useToast } from "@probo/ui";
|
||||||
import {
|
import {
|
||||||
type ClipboardEvent,
|
type ClipboardEvent,
|
||||||
@@ -29,6 +28,7 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { type PreloadedQuery, useMutation, usePreloadedQuery } from "react-relay";
|
import { type PreloadedQuery, useMutation, usePreloadedQuery } from "react-relay";
|
||||||
import { useNavigate, useSearchParams } from "react-router";
|
import { useNavigate, useSearchParams } from "react-router";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
@@ -56,13 +56,13 @@ const authorizeDeviceMutation = graphql`
|
|||||||
export default function DeviceActivationPage(props: {
|
export default function DeviceActivationPage(props: {
|
||||||
queryRef: PreloadedQuery<DeviceActivationPageQuery>;
|
queryRef: PreloadedQuery<DeviceActivationPageQuery>;
|
||||||
}) {
|
}) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
usePreloadedQuery<DeviceActivationPageQuery>(deviceActivationPageQuery, props.queryRef);
|
usePreloadedQuery<DeviceActivationPageQuery>(deviceActivationPageQuery, props.queryRef);
|
||||||
usePageTitle(__("Device Activation"));
|
usePageTitle(t("deviceActivationPage.pageTitle"));
|
||||||
|
|
||||||
const preset = (searchParams.get("user_code") ?? "").replace(/-/g, "");
|
const preset = (searchParams.get("user_code") ?? "").replace(/-/g, "");
|
||||||
const [values, setValues] = useState<string[]>(() => {
|
const [values, setValues] = useState<string[]>(() => {
|
||||||
@@ -132,9 +132,9 @@ export default function DeviceActivationPage(props: {
|
|||||||
onCompleted: (response, errors) => {
|
onCompleted: (response, errors) => {
|
||||||
if (errors) {
|
if (errors) {
|
||||||
toast({
|
toast({
|
||||||
title: __("Authorization failed"),
|
title: t("deviceActivationPage.errors.authorizationFailed"),
|
||||||
description: formatError(
|
description: formatError(
|
||||||
__("The code is invalid or has expired."),
|
t("deviceActivationPage.errors.invalidCode"),
|
||||||
errors,
|
errors,
|
||||||
),
|
),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
@@ -153,14 +153,14 @@ export default function DeviceActivationPage(props: {
|
|||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
toast({
|
toast({
|
||||||
title: __("Error"),
|
title: t("common.error"),
|
||||||
description: err.message || __("Something went wrong. Please try again."),
|
description: err.message || t("deviceActivationPage.errors.generic"),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[values, authorizeDevice, __, toast, navigate],
|
[values, authorizeDevice, t, toast, navigate],
|
||||||
);
|
);
|
||||||
|
|
||||||
const isFilled = values.every(v => v.length === 1);
|
const isFilled = values.every(v => v.length === 1);
|
||||||
@@ -168,9 +168,9 @@ export default function DeviceActivationPage(props: {
|
|||||||
if (status === "success") {
|
if (status === "success") {
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-md mx-auto pt-8 space-y-6 text-center">
|
<div className="w-full max-w-md mx-auto pt-8 space-y-6 text-center">
|
||||||
<h1 className="text-2xl font-bold">{__("Device Authorized")}</h1>
|
<h1 className="text-2xl font-bold">{t("deviceActivationPage.authorized.title")}</h1>
|
||||||
<p className="text-txt-tertiary">
|
<p className="text-txt-tertiary">
|
||||||
{__("Your device has been successfully authorized. You can close this window and return to your device.")}
|
{t("deviceActivationPage.authorized.description")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -179,9 +179,9 @@ export default function DeviceActivationPage(props: {
|
|||||||
return (
|
return (
|
||||||
<div className="w-full max-w-md mx-auto pt-8 space-y-6">
|
<div className="w-full max-w-md mx-auto pt-8 space-y-6">
|
||||||
<div className="space-y-2 text-center">
|
<div className="space-y-2 text-center">
|
||||||
<h1 className="text-2xl font-bold">{__("Device Activation")}</h1>
|
<h1 className="text-2xl font-bold">{t("deviceActivationPage.title")}</h1>
|
||||||
<p className="text-txt-tertiary">
|
<p className="text-txt-tertiary">
|
||||||
{__("Enter the code displayed on your device")}
|
{t("deviceActivationPage.description")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -206,7 +206,7 @@ export default function DeviceActivationPage(props: {
|
|||||||
autoCapitalize="characters"
|
autoCapitalize="characters"
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
autoFocus={idx === 0}
|
autoFocus={idx === 0}
|
||||||
aria-label={`${__("Code character")} ${idx + 1}`}
|
aria-label={t("deviceActivationPage.codeCharacter", { count: idx + 1 })}
|
||||||
className="w-11 h-13 text-center text-lg font-mono font-medium uppercase rounded-lg border border-border-mid bg-level-1 text-txt-primary outline-none transition-colors focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20"
|
className="w-11 h-13 text-center text-lg font-mono font-medium uppercase rounded-lg border border-border-mid bg-level-1 text-txt-primary outline-none transition-colors focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -218,12 +218,12 @@ export default function DeviceActivationPage(props: {
|
|||||||
className="w-full h-10"
|
className="w-full h-10"
|
||||||
disabled={!isFilled || isInFlight}
|
disabled={!isFilled || isInFlight}
|
||||||
>
|
>
|
||||||
{isInFlight ? __("Authorizing...") : __("Continue")}
|
{isInFlight ? t("deviceActivationPage.actions.authorizing") : t("deviceActivationPage.actions.continue")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p className="text-center text-sm text-txt-tertiary">
|
<p className="text-center text-sm text-txt-tertiary">
|
||||||
{__("Make sure this code matches the one on your device.")}
|
{t("deviceActivationPage.notice")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,9 +20,9 @@
|
|||||||
|
|
||||||
import { formatError } from "@probo/helpers";
|
import { formatError } from "@probo/helpers";
|
||||||
import { usePageTitle } from "@probo/hooks";
|
import { usePageTitle } from "@probo/hooks";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Button, Field, useToast } from "@probo/ui";
|
import { Button, Field, useToast } from "@probo/ui";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMutation } from "react-relay";
|
import { useMutation } from "react-relay";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
@@ -45,9 +45,9 @@ const schema = z.object({
|
|||||||
|
|
||||||
export default function ForgotPasswordPage() {
|
export default function ForgotPasswordPage() {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
usePageTitle(__("Forgot Password"));
|
usePageTitle(t("forgotPasswordPage.pageTitle"));
|
||||||
|
|
||||||
const [instructionsSent, setInstructionsSent] = useState<boolean>();
|
const [instructionsSent, setInstructionsSent] = useState<boolean>();
|
||||||
const { register, handleSubmit, formState } = useFormWithSchema(schema, {
|
const { register, handleSubmit, formState } = useFormWithSchema(schema, {
|
||||||
@@ -67,7 +67,7 @@ export default function ForgotPasswordPage() {
|
|||||||
},
|
},
|
||||||
onError: (e: Error) => {
|
onError: (e: Error) => {
|
||||||
toast({
|
toast({
|
||||||
title: __("Request failed"),
|
title: t("forgotPasswordPage.errors.requestFailed"),
|
||||||
description: e.message,
|
description: e.message,
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
@@ -75,9 +75,9 @@ export default function ForgotPasswordPage() {
|
|||||||
onCompleted: (_, e) => {
|
onCompleted: (_, e) => {
|
||||||
if (e) {
|
if (e) {
|
||||||
toast({
|
toast({
|
||||||
title: __("Request failed"),
|
title: t("forgotPasswordPage.errors.requestFailed"),
|
||||||
description: formatError(
|
description: formatError(
|
||||||
__("Failed to send reset instructions"),
|
t("forgotPasswordPage.errors.sendInstructions"),
|
||||||
e,
|
e,
|
||||||
),
|
),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
@@ -86,8 +86,8 @@ export default function ForgotPasswordPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: __("Success"),
|
title: t("common.success"),
|
||||||
description: __("Password reset instructions sent to your email"),
|
description: t("forgotPasswordPage.messages.instructionsSent"),
|
||||||
variant: "success",
|
variant: "success",
|
||||||
});
|
});
|
||||||
setInstructionsSent(true);
|
setInstructionsSent(true);
|
||||||
@@ -99,34 +99,34 @@ export default function ForgotPasswordPage() {
|
|||||||
? (
|
? (
|
||||||
<div className="space-y-6 w-full max-w-md mx-auto pt-8">
|
<div className="space-y-6 w-full max-w-md mx-auto pt-8">
|
||||||
<div className="space-y-2 text-center">
|
<div className="space-y-2 text-center">
|
||||||
<h1 className="text-3xl font-bold">{__("Check your email")}</h1>
|
<h1 className="text-3xl font-bold">{t("forgotPasswordPage.sent.title")}</h1>
|
||||||
<p className="text-txt-tertiary">
|
<p className="text-txt-tertiary">
|
||||||
{__("We've sent password reset instructions to your email address")}
|
{t("forgotPasswordPage.sent.description")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-sm text-txt-tertiary">
|
<p className="text-sm text-txt-tertiary">
|
||||||
{__("Didn't receive the email?")}
|
{t("forgotPasswordPage.sent.didNotReceive")}
|
||||||
{" "}
|
{" "}
|
||||||
<button
|
<button
|
||||||
onClick={() => setInstructionsSent(false)}
|
onClick={() => setInstructionsSent(false)}
|
||||||
className="underline text-txt-primary hover:text-txt-secondary"
|
className="underline text-txt-primary hover:text-txt-secondary"
|
||||||
>
|
>
|
||||||
{__("Try again")}
|
{t("forgotPasswordPage.actions.tryAgain")}
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-sm text-txt-tertiary">
|
<p className="text-sm text-txt-tertiary">
|
||||||
{__("Remember your password?")}
|
{t("forgotPasswordPage.rememberPassword")}
|
||||||
{" "}
|
{" "}
|
||||||
<Link
|
<Link
|
||||||
to="/auth/login"
|
to="/auth/login"
|
||||||
className="underline text-txt-primary hover:text-txt-secondary"
|
className="underline text-txt-primary hover:text-txt-secondary"
|
||||||
>
|
>
|
||||||
{__("Back to login")}
|
{t("forgotPasswordPage.actions.backToLogin")}
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -135,19 +135,17 @@ export default function ForgotPasswordPage() {
|
|||||||
: (
|
: (
|
||||||
<div className="space-y-6 w-full max-w-md mx-auto pt-8">
|
<div className="space-y-6 w-full max-w-md mx-auto pt-8">
|
||||||
<div className="space-y-2 text-center">
|
<div className="space-y-2 text-center">
|
||||||
<h1 className="text-3xl font-bold">{__("Forgot Password")}</h1>
|
<h1 className="text-3xl font-bold">{t("forgotPasswordPage.title")}</h1>
|
||||||
<p className="text-txt-tertiary">
|
<p className="text-txt-tertiary">
|
||||||
{__(
|
{t("forgotPasswordPage.description")}
|
||||||
"Enter your email address and we'll send you instructions to reset your password",
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={e => void onSubmit(e)} className="space-y-4">
|
<form onSubmit={e => void onSubmit(e)} className="space-y-4">
|
||||||
<Field
|
<Field
|
||||||
label={__("Email")}
|
label={t("forgotPasswordPage.fields.email")}
|
||||||
type="email"
|
type="email"
|
||||||
placeholder={__("name@example.com")}
|
placeholder={t("forgotPasswordPage.fields.emailPlaceholder")}
|
||||||
{...register("email")}
|
{...register("email")}
|
||||||
required
|
required
|
||||||
error={formState.errors.email?.message}
|
error={formState.errors.email?.message}
|
||||||
@@ -159,20 +157,20 @@ export default function ForgotPasswordPage() {
|
|||||||
disabled={formState.isSubmitting}
|
disabled={formState.isSubmitting}
|
||||||
>
|
>
|
||||||
{formState.isSubmitting
|
{formState.isSubmitting
|
||||||
? __("Sending instructions...")
|
? t("forgotPasswordPage.actions.sendingInstructions")
|
||||||
: __("Send reset instructions")}
|
: t("forgotPasswordPage.actions.sendInstructions")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-sm text-txt-tertiary">
|
<p className="text-sm text-txt-tertiary">
|
||||||
{__("Remember your password?")}
|
{t("forgotPasswordPage.rememberPassword")}
|
||||||
{" "}
|
{" "}
|
||||||
<Link
|
<Link
|
||||||
to="/auth/login"
|
to="/auth/login"
|
||||||
className="underline text-txt-primary hover:text-txt-secondary"
|
className="underline text-txt-primary hover:text-txt-secondary"
|
||||||
>
|
>
|
||||||
{__("Back to login")}
|
{t("forgotPasswordPage.actions.backToLogin")}
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,8 +20,8 @@
|
|||||||
|
|
||||||
import { formatError } from "@probo/helpers";
|
import { formatError } from "@probo/helpers";
|
||||||
import { usePageTitle } from "@probo/hooks";
|
import { usePageTitle } from "@probo/hooks";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Button, Field, useToast } from "@probo/ui";
|
import { Button, Field, useToast } from "@probo/ui";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMutation } from "react-relay";
|
import { useMutation } from "react-relay";
|
||||||
import { Link, useNavigate, useSearchParams } from "react-router";
|
import { Link, useNavigate, useSearchParams } from "react-router";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
@@ -49,13 +49,13 @@ const schema = z
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default function ResetPasswordPage() {
|
export default function ResetPasswordPage() {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const token = searchParams.get("token");
|
const token = searchParams.get("token");
|
||||||
|
|
||||||
usePageTitle(__("Reset password"));
|
usePageTitle(t("resetPasswordPage.pageTitle"));
|
||||||
|
|
||||||
const { register, handleSubmit, formState } = useFormWithSchema(schema, {
|
const { register, handleSubmit, formState } = useFormWithSchema(schema, {
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -71,8 +71,8 @@ export default function ResetPasswordPage() {
|
|||||||
const onSubmit = handleSubmit((data) => {
|
const onSubmit = handleSubmit((data) => {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
toast({
|
toast({
|
||||||
title: __("Reset failed"),
|
title: t("resetPasswordPage.errors.resetFailed"),
|
||||||
description: __("Invalid or missing reset token"),
|
description: t("resetPasswordPage.errors.invalidToken"),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -87,7 +87,7 @@ export default function ResetPasswordPage() {
|
|||||||
},
|
},
|
||||||
onError: (e: Error) => {
|
onError: (e: Error) => {
|
||||||
toast({
|
toast({
|
||||||
title: __("Reset failed"),
|
title: t("resetPasswordPage.errors.resetFailed"),
|
||||||
description: e.message,
|
description: e.message,
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
@@ -95,9 +95,9 @@ export default function ResetPasswordPage() {
|
|||||||
onCompleted: (_, e) => {
|
onCompleted: (_, e) => {
|
||||||
if (e) {
|
if (e) {
|
||||||
toast({
|
toast({
|
||||||
title: __("Reset failed"),
|
title: t("resetPasswordPage.errors.resetFailed"),
|
||||||
description: formatError(
|
description: formatError(
|
||||||
__("Password reset failed"),
|
t("resetPasswordPage.errors.reset"),
|
||||||
e,
|
e,
|
||||||
),
|
),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
@@ -105,8 +105,8 @@ export default function ResetPasswordPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
toast({
|
toast({
|
||||||
title: __("Success"),
|
title: t("common.success"),
|
||||||
description: __("Password reset successfully"),
|
description: t("resetPasswordPage.messages.reset"),
|
||||||
variant: "success",
|
variant: "success",
|
||||||
});
|
});
|
||||||
void navigate("/auth/login", { replace: true });
|
void navigate("/auth/login", { replace: true });
|
||||||
@@ -117,15 +117,15 @@ export default function ResetPasswordPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6 w-full max-w-md mx-auto pt-8">
|
<div className="space-y-6 w-full max-w-md mx-auto pt-8">
|
||||||
<div className="space-y-2 text-center">
|
<div className="space-y-2 text-center">
|
||||||
<h1 className="text-3xl font-bold">{__("Reset password")}</h1>
|
<h1 className="text-3xl font-bold">{t("resetPasswordPage.title")}</h1>
|
||||||
<p className="text-txt-tertiary">
|
<p className="text-txt-tertiary">
|
||||||
{__("Enter your new password to reset your account")}
|
{t("resetPasswordPage.description")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={e => void onSubmit(e)} className="space-y-4">
|
<form onSubmit={e => void onSubmit(e)} className="space-y-4">
|
||||||
<Field
|
<Field
|
||||||
label={__("New Password")}
|
label={t("resetPasswordPage.fields.newPassword")}
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
{...register("password")}
|
{...register("password")}
|
||||||
@@ -134,7 +134,7 @@ export default function ResetPasswordPage() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
label={__("Confirm Password")}
|
label={t("resetPasswordPage.fields.confirmPassword")}
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
{...register("confirmPassword")}
|
{...register("confirmPassword")}
|
||||||
@@ -144,20 +144,20 @@ export default function ResetPasswordPage() {
|
|||||||
|
|
||||||
<Button type="submit" className="w-xs h-10 mx-auto mt-6" disabled={formState.isLoading}>
|
<Button type="submit" className="w-xs h-10 mx-auto mt-6" disabled={formState.isLoading}>
|
||||||
{formState.isLoading
|
{formState.isLoading
|
||||||
? __("Resetting password...")
|
? t("resetPasswordPage.actions.resetting")
|
||||||
: __("Reset password")}
|
: t("resetPasswordPage.actions.reset")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-sm text-txt-tertiary">
|
<p className="text-sm text-txt-tertiary">
|
||||||
{__("Remember your password?")}
|
{t("resetPasswordPage.rememberPassword")}
|
||||||
{" "}
|
{" "}
|
||||||
<Link
|
<Link
|
||||||
to="/auth/login"
|
to="/auth/login"
|
||||||
className="underline text-txt-primary hover:text-txt-secondary"
|
className="underline text-txt-primary hover:text-txt-secondary"
|
||||||
>
|
>
|
||||||
{__("Log in here")}
|
{t("resetPasswordPage.actions.logIn")}
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,9 +20,9 @@
|
|||||||
|
|
||||||
import { formatError } from "@probo/helpers";
|
import { formatError } from "@probo/helpers";
|
||||||
import { usePageTitle } from "@probo/hooks";
|
import { usePageTitle } from "@probo/hooks";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Button, Field, useToast } from "@probo/ui";
|
import { Button, Field, useToast } from "@probo/ui";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMutation, usePreloadedQuery, useQueryLoader } from "react-relay";
|
import { useMutation, usePreloadedQuery, useQueryLoader } from "react-relay";
|
||||||
import { Link, useNavigate } from "react-router";
|
import { Link, useNavigate } from "react-router";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
@@ -57,11 +57,11 @@ const schema = z.object({
|
|||||||
type FormData = z.infer<typeof schema>;
|
type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
function SignUpPageContent(props: { queryRef: NonNullable<ReturnType<typeof useQueryLoader<SignUpPageQuery>>[0]> }) {
|
function SignUpPageContent(props: { queryRef: NonNullable<ReturnType<typeof useQueryLoader<SignUpPageQuery>>[0]> }) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
usePageTitle(__("Sign up"));
|
usePageTitle(t("signUpPage.pageTitle"));
|
||||||
|
|
||||||
const data = usePreloadedQuery<SignUpPageQuery>(signUpPageQuery, props.queryRef);
|
const data = usePreloadedQuery<SignUpPageQuery>(signUpPageQuery, props.queryRef);
|
||||||
|
|
||||||
@@ -87,23 +87,22 @@ function SignUpPageContent(props: { queryRef: NonNullable<ReturnType<typeof useQ
|
|||||||
onCompleted: (_, e) => {
|
onCompleted: (_, e) => {
|
||||||
if (e) {
|
if (e) {
|
||||||
toast({
|
toast({
|
||||||
title: __("Sign up failed"),
|
title: t("signUpPage.errors.failed"),
|
||||||
description: formatError(__("Sign up failed"), e),
|
description: formatError(t("signUpPage.errors.failed"), e),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: __("Success"),
|
title: t("common.success"), description: t("signUpPage.messages.created"),
|
||||||
description: __("Account created successfully"),
|
|
||||||
variant: "success",
|
variant: "success",
|
||||||
});
|
});
|
||||||
void navigate("/", { replace: true });
|
void navigate("/", { replace: true });
|
||||||
},
|
},
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
toast({
|
toast({
|
||||||
title: __("Sign up failed"),
|
title: t("signUpPage.errors.failed"),
|
||||||
description: e.message,
|
description: e.message,
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
@@ -115,9 +114,9 @@ function SignUpPageContent(props: { queryRef: NonNullable<ReturnType<typeof useQ
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6 w-full max-w-md mx-auto pt-8 text-center">
|
<div className="space-y-6 w-full max-w-md mx-auto pt-8 text-center">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h1 className="text-3xl font-bold">{__("Registration unavailable")}</h1>
|
<h1 className="text-3xl font-bold">{t("signUpPage.unavailable.title")}</h1>
|
||||||
<p className="text-txt-tertiary">
|
<p className="text-txt-tertiary">
|
||||||
{__("New account registration is currently disabled. Please contact your administrator or reach out to Probo for assistance.")}
|
{t("signUpPage.unavailable.description")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -127,7 +126,7 @@ function SignUpPageContent(props: { queryRef: NonNullable<ReturnType<typeof useQ
|
|||||||
className="w-xs h-10 mx-auto"
|
className="w-xs h-10 mx-auto"
|
||||||
to="/auth/login"
|
to="/auth/login"
|
||||||
>
|
>
|
||||||
{__("Back to login")}
|
{t("signUpPage.actions.backToLogin")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -137,33 +136,33 @@ function SignUpPageContent(props: { queryRef: NonNullable<ReturnType<typeof useQ
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6 w-full max-w-md mx-auto pt-8">
|
<div className="space-y-6 w-full max-w-md mx-auto pt-8">
|
||||||
<div className="space-y-2 text-center">
|
<div className="space-y-2 text-center">
|
||||||
<h1 className="text-3xl font-bold">{__("Sign up")}</h1>
|
<h1 className="text-3xl font-bold">{t("signUpPage.title")}</h1>
|
||||||
<p className="text-txt-tertiary">
|
<p className="text-txt-tertiary">
|
||||||
{__("Enter your information to create an account")}
|
{t("signUpPage.description")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={e => void handleSubmit(onSubmit)(e)} className="space-y-4">
|
<form onSubmit={e => void handleSubmit(onSubmit)(e)} className="space-y-4">
|
||||||
<Field
|
<Field
|
||||||
label={__("Full Name")}
|
label={t("signUpPage.fields.fullName")}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={__("John Doe")}
|
placeholder={t("signUpPage.fields.fullNamePlaceholder")}
|
||||||
{...register("fullName")}
|
{...register("fullName")}
|
||||||
required
|
required
|
||||||
error={formState.errors.fullName?.message}
|
error={formState.errors.fullName?.message}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
label={__("Email")}
|
label={t("signUpPage.fields.email")}
|
||||||
type="email"
|
type="email"
|
||||||
placeholder={__("name@example.com")}
|
placeholder={t("signUpPage.fields.emailPlaceholder")}
|
||||||
{...register("email")}
|
{...register("email")}
|
||||||
required
|
required
|
||||||
error={formState.errors.email?.message}
|
error={formState.errors.email?.message}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
label={__("Password")}
|
label={t("signUpPage.fields.password")}
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
{...register("password")}
|
{...register("password")}
|
||||||
@@ -173,20 +172,20 @@ function SignUpPageContent(props: { queryRef: NonNullable<ReturnType<typeof useQ
|
|||||||
|
|
||||||
<Button type="submit" className="w-xs h-10 mx-auto mt-6" disabled={formState.isLoading}>
|
<Button type="submit" className="w-xs h-10 mx-auto mt-6" disabled={formState.isLoading}>
|
||||||
{formState.isLoading
|
{formState.isLoading
|
||||||
? __("Creating account...")
|
? t("signUpPage.actions.creating")
|
||||||
: __("Sign up with email")}
|
: t("signUpPage.actions.signUpWithEmail")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-sm text-txt-tertiary">
|
<p className="text-sm text-txt-tertiary">
|
||||||
{__("Already have an account?")}
|
{t("signUpPage.alreadyHaveAccount")}
|
||||||
{" "}
|
{" "}
|
||||||
<Link
|
<Link
|
||||||
to="/auth/login"
|
to="/auth/login"
|
||||||
className="underline text-txt-primary hover:text-txt-secondary"
|
className="underline text-txt-primary hover:text-txt-secondary"
|
||||||
>
|
>
|
||||||
{__("Log in here")}
|
{t("signUpPage.actions.logIn")}
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,9 +20,9 @@
|
|||||||
|
|
||||||
import { formatError } from "@probo/helpers";
|
import { formatError } from "@probo/helpers";
|
||||||
import { usePageTitle } from "@probo/hooks";
|
import { usePageTitle } from "@probo/hooks";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Button, Field, useToast } from "@probo/ui";
|
import { Button, Field, useToast } from "@probo/ui";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMutation } from "react-relay";
|
import { useMutation } from "react-relay";
|
||||||
import { Link, useSearchParams } from "react-router";
|
import { Link, useSearchParams } from "react-router";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
@@ -44,11 +44,11 @@ const confirmEmailSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default function VerifyEmailPage() {
|
export default function VerifyEmailPage() {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
usePageTitle(__("Confirm Email"));
|
usePageTitle(t("verifyEmailPage.pageTitle"));
|
||||||
|
|
||||||
const [isConfirmed, setIsConfirmed] = useState(false);
|
const [isConfirmed, setIsConfirmed] = useState(false);
|
||||||
|
|
||||||
@@ -71,8 +71,7 @@ export default function VerifyEmailPage() {
|
|||||||
onCompleted: (_, errors) => {
|
onCompleted: (_, errors) => {
|
||||||
if (errors) {
|
if (errors) {
|
||||||
toast({
|
toast({
|
||||||
title: __("Error"),
|
title: t("common.error"), description: formatError(t("verifyEmailPage.errors.confirm"), errors),
|
||||||
description: formatError(__("Failed to confirm email"), errors),
|
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -80,15 +79,13 @@ export default function VerifyEmailPage() {
|
|||||||
|
|
||||||
setIsConfirmed(true);
|
setIsConfirmed(true);
|
||||||
toast({
|
toast({
|
||||||
title: __("Success"),
|
title: t("common.success"), description: t("verifyEmailPage.messages.confirmed"),
|
||||||
description: __("Your email has been confirmed successfully"),
|
|
||||||
variant: "success",
|
variant: "success",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
toast({
|
toast({
|
||||||
title: __("Error"),
|
title: t("common.error"), description: err.message || t("verifyEmailPage.errors.confirm"),
|
||||||
description: err.message || __("Failed to confirm email"),
|
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -98,9 +95,9 @@ export default function VerifyEmailPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6 w-full max-w-md mx-auto pt-8">
|
<div className="space-y-6 w-full max-w-md mx-auto pt-8">
|
||||||
<div className="space-y-2 text-center">
|
<div className="space-y-2 text-center">
|
||||||
<h1 className="text-3xl font-bold">{__("Email Confirmation")}</h1>
|
<h1 className="text-3xl font-bold">{t("verifyEmailPage.title")}</h1>
|
||||||
<p className="text-txt-tertiary">
|
<p className="text-txt-tertiary">
|
||||||
{__("Confirm your email address to complete registration")}
|
{t("verifyEmailPage.description")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -108,25 +105,23 @@ export default function VerifyEmailPage() {
|
|||||||
? (
|
? (
|
||||||
<div className="space-y-4 text-center">
|
<div className="space-y-4 text-center">
|
||||||
<p className="text-green-600 dark:text-green-400">
|
<p className="text-green-600 dark:text-green-400">
|
||||||
{__("Your email has been confirmed successfully!")}
|
{t("verifyEmailPage.messages.confirmedWithExclamation")}
|
||||||
</p>
|
</p>
|
||||||
<Button to="/auth/login" className="w-full">
|
<Button to="/auth/login" className="w-full">
|
||||||
{__("Proceed to Login")}
|
{t("verifyEmailPage.actions.proceedToLogin")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<form onSubmit={e => void handleSubmit(e)} className="space-y-4">
|
<form onSubmit={e => void handleSubmit(e)} className="space-y-4">
|
||||||
<Field
|
<Field
|
||||||
label={__("Confirmation Token")}
|
label={t("verifyEmailPage.fields.token")}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={__("Enter your confirmation token")}
|
placeholder={t("verifyEmailPage.fields.tokenPlaceholder")}
|
||||||
{...form.register("token")}
|
{...form.register("token")}
|
||||||
error={form.formState.errors.token?.message}
|
error={form.formState.errors.token?.message}
|
||||||
disabled={form.formState.isSubmitting}
|
disabled={form.formState.isSubmitting}
|
||||||
help={__(
|
help={t("verifyEmailPage.fields.tokenHelp")}
|
||||||
"The token has been automatically filled from the URL if available",
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@@ -135,8 +130,8 @@ export default function VerifyEmailPage() {
|
|||||||
disabled={form.formState.isSubmitting}
|
disabled={form.formState.isSubmitting}
|
||||||
>
|
>
|
||||||
{form.formState.isSubmitting
|
{form.formState.isSubmitting
|
||||||
? __("Confirming...")
|
? t("verifyEmailPage.actions.confirming")
|
||||||
: __("Confirm Email")}
|
: t("verifyEmailPage.actions.confirm")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
@@ -148,7 +143,7 @@ export default function VerifyEmailPage() {
|
|||||||
to="/auth/login"
|
to="/auth/login"
|
||||||
className="underline text-txt-primary hover:text-txt-secondary"
|
className="underline text-txt-primary hover:text-txt-secondary"
|
||||||
>
|
>
|
||||||
{__("Back to Login")}
|
{t("verifyEmailPage.actions.backToLogin")}
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -19,9 +19,9 @@
|
|||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { formatError } from "@probo/helpers";
|
import { formatError } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Button, Field, IconChevronLeft, useToast } from "@probo/ui";
|
import { Button, Field, IconChevronLeft, useToast } from "@probo/ui";
|
||||||
import type { FormEventHandler } from "react";
|
import type { FormEventHandler } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMutation } from "react-relay";
|
import { useMutation } from "react-relay";
|
||||||
import { Link, matchPath, useLocation } from "react-router";
|
import { Link, matchPath, useLocation } from "react-router";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
@@ -43,7 +43,7 @@ export default function PasswordSignInPage() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const postAuthRedirectUrl = usePostAuthRedirectUrl();
|
const postAuthRedirectUrl = usePostAuthRedirectUrl();
|
||||||
|
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const [signIn, isSigningIn]
|
const [signIn, isSigningIn]
|
||||||
@@ -74,9 +74,9 @@ export default function PasswordSignInPage() {
|
|||||||
onCompleted: (_, error) => {
|
onCompleted: (_, error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
toast({
|
toast({
|
||||||
title: __("Error"),
|
title: t("common.error"),
|
||||||
description: formatError(
|
description: formatError(
|
||||||
__("Failed to login"),
|
t("passwordSignInPage.errors.login"),
|
||||||
error,
|
error,
|
||||||
),
|
),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
@@ -88,7 +88,7 @@ export default function PasswordSignInPage() {
|
|||||||
},
|
},
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
toast({
|
toast({
|
||||||
title: __("Error"),
|
title: t("common.error"),
|
||||||
description: e.message,
|
description: e.message,
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
@@ -103,55 +103,55 @@ export default function PasswordSignInPage() {
|
|||||||
className="flex items-center gap-2 text-txt-secondary hover:text-txt-primary transition-colors mb-4"
|
className="flex items-center gap-2 text-txt-secondary hover:text-txt-primary transition-colors mb-4"
|
||||||
>
|
>
|
||||||
<IconChevronLeft size={20} />
|
<IconChevronLeft size={20} />
|
||||||
<span className="text-sm">{__("Back")}</span>
|
<span className="text-sm">{t("passwordSignInPage.actions.back")}</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<h1 className="text-center text-2xl font-bold">
|
<h1 className="text-center text-2xl font-bold">
|
||||||
{__("Sign in with password")}
|
{t("passwordSignInPage.title")}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-center text-txt-tertiary mt-1 mb-6">
|
<p className="text-center text-txt-tertiary mt-1 mb-6">
|
||||||
{__("Enter your email and password")}
|
{t("passwordSignInPage.description")}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Field
|
<Field
|
||||||
required
|
required
|
||||||
placeholder={__("Email")}
|
placeholder={t("passwordSignInPage.fields.email")}
|
||||||
name="email"
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
label={__("Email")}
|
label={t("passwordSignInPage.fields.email")}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
required
|
required
|
||||||
placeholder={__("Password")}
|
placeholder={t("passwordSignInPage.fields.password")}
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
label={__("Password")}
|
label={t("passwordSignInPage.fields.password")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button className="w-xs h-10 mx-auto mt-6" disabled={isSigningIn}>
|
<Button className="w-xs h-10 mx-auto mt-6" disabled={isSigningIn}>
|
||||||
{isSigningIn ? __("Logging in...") : __("Login")}
|
{isSigningIn ? t("passwordSignInPage.actions.loggingIn") : t("passwordSignInPage.actions.login")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="text-center mt-6 text-sm text-txt-secondary">
|
<div className="text-center mt-6 text-sm text-txt-secondary">
|
||||||
{__("Don't have an account ?")}
|
{t("passwordSignInPage.noAccount")}
|
||||||
{" "}
|
{" "}
|
||||||
<Link to={{ pathname: "/auth/register", search: location.search }} className="underline hover:text-txt-primary">
|
<Link to={{ pathname: "/auth/register", search: location.search }} className="underline hover:text-txt-primary">
|
||||||
{__("Register")}
|
{t("passwordSignInPage.actions.register")}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-center text-sm text-txt-secondary">
|
<div className="text-center text-sm text-txt-secondary">
|
||||||
{__("Forgot password?")}
|
{t("passwordSignInPage.forgotPassword")}
|
||||||
{" "}
|
{" "}
|
||||||
<Link
|
<Link
|
||||||
to="/auth/forgot-password"
|
to="/auth/forgot-password"
|
||||||
className="underline hover:text-txt-primary"
|
className="underline hover:text-txt-primary"
|
||||||
>
|
>
|
||||||
{__("Reset password")}
|
{t("passwordSignInPage.actions.resetPassword")}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -18,9 +18,9 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Button, Field, IconChevronLeft, useToast } from "@probo/ui";
|
import { Button, Field, IconChevronLeft, useToast } from "@probo/ui";
|
||||||
import { type FormEventHandler, useEffect, useState } from "react";
|
import { type FormEventHandler, useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
type PreloadedQuery,
|
type PreloadedQuery,
|
||||||
usePreloadedQuery,
|
usePreloadedQuery,
|
||||||
@@ -40,7 +40,7 @@ const ssoAvailabilityQuery = graphql`
|
|||||||
|
|
||||||
export default function SSOSignInPage() {
|
export default function SSOSignInPage() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [queryRef, loadQuery]
|
const [queryRef, loadQuery]
|
||||||
= useQueryLoader<SSOSignInPageQuery>(ssoAvailabilityQuery);
|
= useQueryLoader<SSOSignInPageQuery>(ssoAvailabilityQuery);
|
||||||
@@ -65,37 +65,37 @@ export default function SSOSignInPage() {
|
|||||||
className="flex items-center gap-2 text-txt-secondary hover:text-txt-primary transition-colors mb-4"
|
className="flex items-center gap-2 text-txt-secondary hover:text-txt-primary transition-colors mb-4"
|
||||||
>
|
>
|
||||||
<IconChevronLeft size={20} />
|
<IconChevronLeft size={20} />
|
||||||
<span className="text-sm">{__("Back")}</span>
|
<span className="text-sm">{t("ssoSignInPage.actions.back")}</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<h1 className="text-center text-2xl font-bold">
|
<h1 className="text-center text-2xl font-bold">
|
||||||
{__("Login with SSO")}
|
{t("ssoSignInPage.title")}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-center text-txt-tertiary mt-1 mb-6">
|
<p className="text-center text-txt-tertiary mt-1 mb-6">
|
||||||
{__("Enter your work email to continue with SSO")}
|
{t("ssoSignInPage.description")}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
required
|
required
|
||||||
placeholder={__("Work Email")}
|
placeholder={t("ssoSignInPage.fields.workEmail")}
|
||||||
name="email"
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
label={__("Work Email")}
|
label={t("ssoSignInPage.fields.workEmail")}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button className="w-xs h-10 mx-auto mt-6" disabled={checking}>
|
<Button className="w-xs h-10 mx-auto mt-6" disabled={checking}>
|
||||||
{checking ? __("Checking...") : __("Continue with SSO")}
|
{checking ? t("ssoSignInPage.actions.checking") : t("ssoSignInPage.actions.continue")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="text-center mt-6 text-sm text-txt-secondary">
|
<div className="text-center mt-6 text-sm text-txt-secondary">
|
||||||
{__("Don't have an account ?")}
|
{t("ssoSignInPage.noAccount")}
|
||||||
{" "}
|
{" "}
|
||||||
<Link
|
<Link
|
||||||
to={{ pathname: "/auth/register", search: location.search }}
|
to={{ pathname: "/auth/register", search: location.search }}
|
||||||
className="underline hover:text-txt-primary"
|
className="underline hover:text-txt-primary"
|
||||||
>
|
>
|
||||||
{__("Register")}
|
{t("ssoSignInPage.actions.register")}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -118,7 +118,7 @@ function NavigateToSSOLoginURL(props: {
|
|||||||
}) {
|
}) {
|
||||||
const { queryRef, loginSearch } = props;
|
const { queryRef, loginSearch } = props;
|
||||||
|
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -132,11 +132,11 @@ function NavigateToSSOLoginURL(props: {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ssoLoginURL.ok) {
|
if (!ssoLoginURL.ok) {
|
||||||
toast({
|
toast({
|
||||||
title: __("Error"),
|
title: t("common.error"),
|
||||||
description:
|
description:
|
||||||
ssoLoginURL.errors[0] instanceof Error
|
ssoLoginURL.errors[0] instanceof Error
|
||||||
? ssoLoginURL.errors[0].message
|
? ssoLoginURL.errors[0].message
|
||||||
: __("SSO not available for this email domain"),
|
: t("ssoSignInPage.errors.unavailable"),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -146,8 +146,7 @@ function NavigateToSSOLoginURL(props: {
|
|||||||
|
|
||||||
if (!ssoLoginURL.value) {
|
if (!ssoLoginURL.value) {
|
||||||
toast({
|
toast({
|
||||||
title: __("Error"),
|
title: t("common.error"), description: t("ssoSignInPage.errors.unavailable"),
|
||||||
description: __("SSO not available for this email domain"),
|
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -162,7 +161,7 @@ function NavigateToSSOLoginURL(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.location.href = url.toString();
|
window.location.href = url.toString();
|
||||||
}, [__, loginSearch, navigate, postAuthRedirectUrl, searchParams, ssoLoginURL, toast]);
|
}, [t, loginSearch, navigate, postAuthRedirectUrl, searchParams, ssoLoginURL, toast]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,8 @@
|
|||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { usePageTitle } from "@probo/hooks";
|
import { usePageTitle } from "@probo/hooks";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Button } from "@probo/ui";
|
import { Button } from "@probo/ui";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
||||||
import { Link, useLocation, useSearchParams } from "react-router";
|
import { Link, useLocation, useSearchParams } from "react-router";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
@@ -52,7 +52,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function SignInPage(props: Props) {
|
export default function SignInPage(props: Props) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const postAuthRedirectUrl = usePostAuthRedirectUrl();
|
const postAuthRedirectUrl = usePostAuthRedirectUrl();
|
||||||
@@ -64,15 +64,15 @@ export default function SignInPage(props: Props) {
|
|||||||
|
|
||||||
const clientBranding = data.oauthClientBranding;
|
const clientBranding = data.oauthClientBranding;
|
||||||
const authorizeHeading = clientBranding?.name
|
const authorizeHeading = clientBranding?.name
|
||||||
? __("Sign in")
|
? t("auth.actions.signIn")
|
||||||
: __("Sign in to continue");
|
: t("signInPage.authorize.title");
|
||||||
|
|
||||||
usePageTitle(
|
usePageTitle(
|
||||||
isAuthorizeFlow
|
isAuthorizeFlow
|
||||||
? clientBranding?.name
|
? clientBranding?.name
|
||||||
? `${__("Sign in to")} ${clientBranding.name}`
|
? t("signInPage.authorize.titleWithClient", { name: clientBranding.name })
|
||||||
: authorizeHeading
|
: authorizeHeading
|
||||||
: __("Sign in to your account"),
|
: t("signInPage.title"),
|
||||||
);
|
);
|
||||||
|
|
||||||
const oidcContinueURL = isAuthorizeFlow ? postAuthRedirectUrl : undefined;
|
const oidcContinueURL = isAuthorizeFlow ? postAuthRedirectUrl : undefined;
|
||||||
@@ -94,11 +94,11 @@ export default function SignInPage(props: Props) {
|
|||||||
<h1 className="text-2xl font-bold">
|
<h1 className="text-2xl font-bold">
|
||||||
{isAuthorizeFlow
|
{isAuthorizeFlow
|
||||||
? authorizeHeading
|
? authorizeHeading
|
||||||
: __("Sign in to your account")}
|
: t("signInPage.title")}
|
||||||
</h1>
|
</h1>
|
||||||
{isAuthorizeFlow && (
|
{isAuthorizeFlow && (
|
||||||
<p className="text-txt-tertiary">
|
<p className="text-txt-tertiary">
|
||||||
{__("Use your email or a connected account to continue")}
|
{t("signInPage.authorize.description")}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -114,33 +114,35 @@ export default function SignInPage(props: Props) {
|
|||||||
|
|
||||||
<MagicLinkForm />
|
<MagicLinkForm />
|
||||||
|
|
||||||
<Divider>{__("Or")}</Divider>
|
<Divider>{t("signInPage.or")}</Divider>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="w-full h-10"
|
className="w-full h-10"
|
||||||
to={{ pathname: "/auth/sso-login", search: location.search }}
|
to={{ pathname: "/auth/sso-login", search: location.search }}
|
||||||
>
|
>
|
||||||
{__("Sign in with SSO")}
|
{t("signInPage.actions.signInWithSso")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Divider>{t("signInPage.or")}</Divider>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="w-full h-10"
|
className="w-full h-10"
|
||||||
to={{ pathname: "/auth/password-login", search: location.search }}
|
to={{ pathname: "/auth/password-login", search: location.search }}
|
||||||
>
|
>
|
||||||
{__("Sign in with password")}
|
{t("signInPage.actions.signInWithEmail")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-center text-sm text-txt-secondary">
|
<p className="mt-8 text-center text-sm text-txt-secondary">
|
||||||
{__("New to Probo?")}
|
{t("signInPage.newToProbo")}
|
||||||
{" "}
|
{" "}
|
||||||
<Link
|
<Link
|
||||||
to={{ pathname: "/auth/register", search: location.search }}
|
to={{ pathname: "/auth/register", search: location.search }}
|
||||||
className="underline hover:text-txt-primary"
|
className="underline hover:text-txt-primary"
|
||||||
>
|
>
|
||||||
{__("Create account")}
|
{t("signInPage.actions.createAccount")}
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,9 +18,9 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Button, Field, useToast } from "@probo/ui";
|
import { Button, Field, useToast } from "@probo/ui";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
||||||
@@ -35,7 +35,7 @@ type FormData = z.infer<typeof schema>;
|
|||||||
const timerDurationSeconds = 60;
|
const timerDurationSeconds = 60;
|
||||||
|
|
||||||
export function MagicLinkForm() {
|
export function MagicLinkForm() {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const postAuthRedirectUrl = usePostAuthRedirectUrl();
|
const postAuthRedirectUrl = usePostAuthRedirectUrl();
|
||||||
|
|
||||||
@@ -83,8 +83,8 @@ export function MagicLinkForm() {
|
|||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
toast({
|
toast({
|
||||||
title: __("Error"),
|
title: t("common.error"),
|
||||||
description: __("Cannot send magic link"),
|
description: t("magicLinkForm.errors.send"),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -92,16 +92,16 @@ export function MagicLinkForm() {
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
toast({
|
toast({
|
||||||
title: __("Error"),
|
title: t("common.error"),
|
||||||
description: __("Cannot send magic link"),
|
description: t("magicLinkForm.errors.send"),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: __("Success"),
|
title: t("common.success"),
|
||||||
description: __("Magic link sent!"),
|
description: t("magicLinkForm.messages.sent"),
|
||||||
variant: "success",
|
variant: "success",
|
||||||
});
|
});
|
||||||
setTimer(timerDurationSeconds);
|
setTimer(timerDurationSeconds);
|
||||||
@@ -111,7 +111,7 @@ export function MagicLinkForm() {
|
|||||||
return (
|
return (
|
||||||
<form onSubmit={e => void handleSubmit(e)} className="space-y-4">
|
<form onSubmit={e => void handleSubmit(e)} className="space-y-4">
|
||||||
<Field
|
<Field
|
||||||
label={__("Email")}
|
label={t("magicLinkForm.fields.email")}
|
||||||
placeholder="john.doe@acme.com"
|
placeholder="john.doe@acme.com"
|
||||||
{...register("email")}
|
{...register("email")}
|
||||||
type="email"
|
type="email"
|
||||||
@@ -121,9 +121,7 @@ export function MagicLinkForm() {
|
|||||||
|
|
||||||
{magicLinkSent && (
|
{magicLinkSent && (
|
||||||
<p className="text-txt-primary text-sm">
|
<p className="text-txt-primary text-sm">
|
||||||
{__(
|
{t("magicLinkForm.sentDescription")}
|
||||||
"Magic link sent! Check your email and use the link to continue.",
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -134,9 +132,9 @@ export function MagicLinkForm() {
|
|||||||
>
|
>
|
||||||
{magicLinkSent
|
{magicLinkSent
|
||||||
? timer === 0
|
? timer === 0
|
||||||
? __("Resend Link")
|
? t("magicLinkForm.actions.resend")
|
||||||
: `${__("Resend Link in")} ${timer}s`
|
: t("magicLinkForm.actions.resendIn", { count: timer })
|
||||||
: __("Send Magic Link")}
|
: t("magicLinkForm.actions.send")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,9 +18,9 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Button, Google, Microsoft } from "@probo/ui";
|
import { Button, Google, Microsoft } from "@probo/ui";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFragment } from "react-relay";
|
import { useFragment } from "react-relay";
|
||||||
import { useSearchParams } from "react-router";
|
import { useSearchParams } from "react-router";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
@@ -50,7 +50,7 @@ export function OIDCButton({
|
|||||||
providerRef: OIDCButtonFragment$key;
|
providerRef: OIDCButtonFragment$key;
|
||||||
continueURL?: string;
|
continueURL?: string;
|
||||||
}) {
|
}) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const safeContinueUrl = useSafeContinueUrl();
|
const safeContinueUrl = useSafeContinueUrl();
|
||||||
const provider = useFragment(fragment, providerRef);
|
const provider = useFragment(fragment, providerRef);
|
||||||
@@ -74,7 +74,9 @@ export function OIDCButton({
|
|||||||
>
|
>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
{Icon && <Icon width={18} height={18} />}
|
{Icon && <Icon width={18} height={18} />}
|
||||||
{__(`Sign in with ${provider.name.charAt(0).toUpperCase() + provider.name.slice(1)}`)}
|
{t("oidcButton.signInWith", {
|
||||||
|
provider: provider.name.charAt(0).toUpperCase() + provider.name.slice(1),
|
||||||
|
})}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { usePageTitle } from "@probo/hooks";
|
import { usePageTitle } from "@probo/hooks";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
@@ -28,6 +27,7 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { graphql, type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
import { graphql, type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
||||||
|
|
||||||
import type { MembershipsPageQuery } from "#/__generated__/iam/MembershipsPageQuery.graphql";
|
import type { MembershipsPageQuery } from "#/__generated__/iam/MembershipsPageQuery.graphql";
|
||||||
@@ -67,10 +67,10 @@ export const membershipsPageQuery = graphql`
|
|||||||
export function MembershipsPage(props: {
|
export function MembershipsPage(props: {
|
||||||
queryRef: PreloadedQuery<MembershipsPageQuery>;
|
queryRef: PreloadedQuery<MembershipsPageQuery>;
|
||||||
}) {
|
}) {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
usePageTitle(__("Select an organization"));
|
usePageTitle(t("membershipsPage.pageTitle"));
|
||||||
|
|
||||||
const { queryRef } = props;
|
const { queryRef } = props;
|
||||||
const {
|
const {
|
||||||
@@ -93,13 +93,13 @@ export function MembershipsPage(props: {
|
|||||||
<>
|
<>
|
||||||
<div className="space-y-6 w-full py-6">
|
<div className="space-y-6 w-full py-6">
|
||||||
<h1 className="text-3xl font-bold text-center">
|
<h1 className="text-3xl font-bold text-center">
|
||||||
{__("Select an organization")}
|
{t("membershipsPage.title")}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="space-y-4 w-full">
|
<div className="space-y-4 w-full">
|
||||||
{invitingOrganizations.length > 0 && (
|
{invitingOrganizations.length > 0 && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h2 className="text-xl font-semibold">
|
<h2 className="text-xl font-semibold">
|
||||||
{__("Pending invitations")}
|
{t("membershipsPage.pendingInvitations")}
|
||||||
</h2>
|
</h2>
|
||||||
{invitingOrganizations.map(organization => (
|
{invitingOrganizations.map(organization => (
|
||||||
<InvitingOrganizationCard key={organization.id} fKey={organization} />
|
<InvitingOrganizationCard key={organization.id} fKey={organization} />
|
||||||
@@ -109,12 +109,12 @@ export function MembershipsPage(props: {
|
|||||||
{initialProfiles.length > 0 && (
|
{initialProfiles.length > 0 && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h2 className="text-xl font-semibold">
|
<h2 className="text-xl font-semibold">
|
||||||
{__("Your organizations")}
|
{t("membershipsPage.yourOrganizations")}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Input
|
<Input
|
||||||
icon={IconMagnifyingGlass}
|
icon={IconMagnifyingGlass}
|
||||||
placeholder={__("Search organizations...")}
|
placeholder={t("membershipsPage.searchPlaceholder")}
|
||||||
value={search}
|
value={search}
|
||||||
onValueChange={setSearch}
|
onValueChange={setSearch}
|
||||||
/>
|
/>
|
||||||
@@ -122,7 +122,7 @@ export function MembershipsPage(props: {
|
|||||||
{profiles.length === 0
|
{profiles.length === 0
|
||||||
? (
|
? (
|
||||||
<div className="text-center text-txt-secondary py-4">
|
<div className="text-center text-txt-secondary py-4">
|
||||||
{__("No organizations found")}
|
{t("membershipsPage.empty")}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
@@ -138,10 +138,10 @@ export function MembershipsPage(props: {
|
|||||||
)}
|
)}
|
||||||
<Card padded>
|
<Card padded>
|
||||||
<h2 className="text-xl font-semibold mb-1">
|
<h2 className="text-xl font-semibold mb-1">
|
||||||
{__("Create an organization")}
|
{t("membershipsPage.createOrganization.title")}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-txt-tertiary mb-4">
|
<p className="text-txt-tertiary mb-4">
|
||||||
{__("Add a new organization to your account")}
|
{t("membershipsPage.createOrganization.description")}
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
to="/organizations/new"
|
to="/organizations/new"
|
||||||
@@ -149,7 +149,7 @@ export function MembershipsPage(props: {
|
|||||||
icon={IconPlusLarge}
|
icon={IconPlusLarge}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
{__("Create organization")}
|
{t("membershipsPage.createOrganization.action")}
|
||||||
</Button>
|
</Button>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Badge, Card, IconMail } from "@probo/ui";
|
import { Badge, Card, IconMail } from "@probo/ui";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFragment } from "react-relay";
|
import { useFragment } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ interface InvitingOrganizationCardProps {
|
|||||||
|
|
||||||
export function InvitingOrganizationCard(props: InvitingOrganizationCardProps) {
|
export function InvitingOrganizationCard(props: InvitingOrganizationCardProps) {
|
||||||
const { fKey } = props;
|
const { fKey } = props;
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const organization = useFragment<InvitingOrganizationCardFragment$key>(
|
const organization = useFragment<InvitingOrganizationCardFragment$key>(
|
||||||
fragment,
|
fragment,
|
||||||
@@ -50,7 +50,7 @@ export function InvitingOrganizationCard(props: InvitingOrganizationCardProps) {
|
|||||||
<h2 className="font-semibold text-xl">{organization.name}</h2>
|
<h2 className="font-semibold text-xl">{organization.name}</h2>
|
||||||
<Badge variant="neutral" className="flex items-center gap-1">
|
<Badge variant="neutral" className="flex items-center gap-1">
|
||||||
<IconMail size={14} />
|
<IconMail size={14} />
|
||||||
{__("Check your email")}
|
{t("invitingOrganizationCard.checkEmail")}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { parseDate } from "@probo/helpers";
|
import { parseDate } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Badge,
|
Badge,
|
||||||
@@ -29,6 +28,7 @@ import {
|
|||||||
IconClock,
|
IconClock,
|
||||||
IconLock,
|
IconLock,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFragment } from "react-relay";
|
import { useFragment } from "react-relay";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
@@ -65,7 +65,7 @@ interface MembershipCardProps {
|
|||||||
|
|
||||||
export function MembershipCard(props: MembershipCardProps) {
|
export function MembershipCard(props: MembershipCardProps) {
|
||||||
const { fKey, organizationFragmentRef } = props;
|
const { fKey, organizationFragmentRef } = props;
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { membership, ...user } = useFragment<MembershipCardFragment$key>(
|
const { membership, ...user } = useFragment<MembershipCardFragment$key>(
|
||||||
fragment,
|
fragment,
|
||||||
@@ -84,21 +84,21 @@ export function MembershipCard(props: MembershipCardProps) {
|
|||||||
return (
|
return (
|
||||||
<Badge variant="success" className="flex items-center gap-1">
|
<Badge variant="success" className="flex items-center gap-1">
|
||||||
<IconCheckmark1 size={14} />
|
<IconCheckmark1 size={14} />
|
||||||
{__("Authenticated")}
|
{t("membershipCard.status.authenticated")}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
} else if (isExpired) {
|
} else if (isExpired) {
|
||||||
return (
|
return (
|
||||||
<Badge variant="warning" className="flex items-center gap-1">
|
<Badge variant="warning" className="flex items-center gap-1">
|
||||||
<IconClock size={14} />
|
<IconClock size={14} />
|
||||||
{__("Session expired")}
|
{t("membershipCard.status.sessionExpired")}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Badge variant="neutral" className="flex items-center gap-1">
|
<Badge variant="neutral" className="flex items-center gap-1">
|
||||||
<IconLock size={14} />
|
<IconLock size={14} />
|
||||||
{__("Authentication required")}
|
{t("membershipCard.status.authenticationRequired")}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -123,12 +123,12 @@ export function MembershipCard(props: MembershipCardProps) {
|
|||||||
? (
|
? (
|
||||||
<Link to={`/organizations/${organization.id}`}>
|
<Link to={`/organizations/${organization.id}`}>
|
||||||
{isAssuming
|
{isAssuming
|
||||||
? <Button variant="secondary">{__("Start")}</Button>
|
? <Button variant="secondary">{t("membershipCard.actions.start")}</Button>
|
||||||
: <Button>{__("Login")}</Button>}
|
: <Button>{t("membershipCard.actions.login")}</Button>}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<Button variant="secondary" disabled>{__("Account deactivated")}</Button>
|
<Button variant="secondary" disabled>{t("membershipCard.accountDeactivated")}</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { formatError } from "@probo/helpers";
|
import { formatError } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
DropdownSeparator,
|
DropdownSeparator,
|
||||||
IconArrowBoxLeft,
|
IconArrowBoxLeft,
|
||||||
@@ -29,6 +28,7 @@ import {
|
|||||||
UserDropdownItem,
|
UserDropdownItem,
|
||||||
useToast,
|
useToast,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFragment, useMutation } from "react-relay";
|
import { useFragment, useMutation } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ const signOutMutation = graphql`
|
|||||||
export function ViewerDropdown(props: { fKey: ViewerDropdownFragment$key }) {
|
export function ViewerDropdown(props: { fKey: ViewerDropdownFragment$key }) {
|
||||||
const { fKey } = props;
|
const { fKey } = props;
|
||||||
|
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { canListAPIKeys, canListOAuth2AccessTokens, email, fullName }
|
const { canListAPIKeys, canListOAuth2AccessTokens, email, fullName }
|
||||||
@@ -70,8 +70,8 @@ export function ViewerDropdown(props: { fKey: ViewerDropdownFragment$key }) {
|
|||||||
onCompleted: (_, e) => {
|
onCompleted: (_, e) => {
|
||||||
if (e) {
|
if (e) {
|
||||||
toast({
|
toast({
|
||||||
title: __("Request failed"),
|
title: t("viewerDropdown.errors.requestFailed"),
|
||||||
description: formatError(__("Cannot sign out"), e),
|
description: formatError(t("viewerDropdown.errors.cannotSignOut"), e),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -80,7 +80,7 @@ export function ViewerDropdown(props: { fKey: ViewerDropdownFragment$key }) {
|
|||||||
},
|
},
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
toast({
|
toast({
|
||||||
title: __("Error"),
|
title: t("common.error"),
|
||||||
description: e.message,
|
description: e.message,
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
@@ -94,27 +94,27 @@ export function ViewerDropdown(props: { fKey: ViewerDropdownFragment$key }) {
|
|||||||
<UserDropdownItem
|
<UserDropdownItem
|
||||||
to="/me/api-keys"
|
to="/me/api-keys"
|
||||||
icon={IconKey}
|
icon={IconKey}
|
||||||
label={__("API Keys")}
|
label={t("apiKeys.title")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{canListOAuth2AccessTokens && (
|
{canListOAuth2AccessTokens && (
|
||||||
<UserDropdownItem
|
<UserDropdownItem
|
||||||
to="/me/oauth-tokens"
|
to="/me/oauth-tokens"
|
||||||
icon={IconKey}
|
icon={IconKey}
|
||||||
label={__("OAuth tokens")}
|
label={t("viewerDropdown.actions.oauthTokens")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<UserDropdownItem
|
<UserDropdownItem
|
||||||
to="mailto:support@probo.com"
|
to="mailto:support@probo.com"
|
||||||
icon={IconCircleQuestionmark}
|
icon={IconCircleQuestionmark}
|
||||||
label={__("Help")}
|
label={t("viewerDropdown.actions.help")}
|
||||||
/>
|
/>
|
||||||
<DropdownSeparator />
|
<DropdownSeparator />
|
||||||
<UserDropdownItem
|
<UserDropdownItem
|
||||||
variant="danger"
|
variant="danger"
|
||||||
to="/logout"
|
to="/logout"
|
||||||
icon={IconArrowBoxLeft}
|
icon={IconArrowBoxLeft}
|
||||||
label="Logout"
|
label={t("viewerDropdown.actions.logout")}
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
/>
|
/>
|
||||||
</UserDropdown>
|
</UserDropdown>
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { usePageTitle } from "@probo/hooks";
|
import { usePageTitle } from "@probo/hooks";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
Button,
|
Button,
|
||||||
@@ -35,6 +34,7 @@ import {
|
|||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Controller } from "react-hook-form";
|
import { Controller } from "react-hook-form";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { ConnectionHandler, useLazyLoadQuery } from "react-relay";
|
import { ConnectionHandler, useLazyLoadQuery } from "react-relay";
|
||||||
import { Link, useNavigate } from "react-router";
|
import { Link, useNavigate } from "react-router";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
@@ -106,12 +106,12 @@ function computeExpiresAt(expiresIn: CreateFormData["expiresIn"]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function NewOAuthTokenPage() {
|
export function NewOAuthTokenPage() {
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const tokenDialogRef = useDialogRef();
|
const tokenDialogRef = useDialogRef();
|
||||||
const [token, setToken] = useState("");
|
const [token, setToken] = useState("");
|
||||||
|
|
||||||
usePageTitle(__("New OAuth token"));
|
usePageTitle(t("newOAuthTokenPage.pageTitle"));
|
||||||
|
|
||||||
const data = useLazyLoadQuery<NewOAuthTokenPageQuery>(pageQuery, {});
|
const data = useLazyLoadQuery<NewOAuthTokenPageQuery>(pageQuery, {});
|
||||||
|
|
||||||
@@ -136,8 +136,8 @@ export function NewOAuthTokenPage() {
|
|||||||
const [create, isCreating] = useMutationWithToasts<NewOAuthTokenPageCreateMutation>(
|
const [create, isCreating] = useMutationWithToasts<NewOAuthTokenPageCreateMutation>(
|
||||||
createMutation,
|
createMutation,
|
||||||
{
|
{
|
||||||
successMessage: "OAuth token created successfully.",
|
successMessage: t("newOAuthTokenPage.messages.created"),
|
||||||
errorMessage: "Failed to create OAuth token",
|
errorMessage: t("newOAuthTokenPage.errors.create"),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -202,25 +202,23 @@ export function NewOAuthTokenPage() {
|
|||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
label: __("OAuth tokens"),
|
label: t("oauthTokensPage.title"),
|
||||||
to: "/me/oauth-tokens",
|
to: "/me/oauth-tokens",
|
||||||
},
|
},
|
||||||
{ label: __("New token") },
|
{ label: t("newOAuthTokenPage.breadcrumb") },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={__("New OAuth token")}
|
title={t("newOAuthTokenPage.title")}
|
||||||
description={__(
|
description={t("newOAuthTokenPage.description")}
|
||||||
"Create a bearer token with scoped access to the Probo API.",
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Card padded>
|
<Card padded>
|
||||||
<form className="space-y-6" onSubmit={e => void handleSubmit(handleCreate)(e)}>
|
<form className="space-y-6" onSubmit={e => void handleSubmit(handleCreate)(e)}>
|
||||||
<div className="max-w-xl space-y-6">
|
<div className="max-w-xl space-y-6">
|
||||||
<Field>
|
<Field>
|
||||||
<Label htmlFor="name">{__("Name")}</Label>
|
<Label htmlFor="name">{t("newOAuthTokenPage.fields.name")}</Label>
|
||||||
<Input id="name" {...register("name")} />
|
<Input id="name" {...register("name")} />
|
||||||
{formState.errors.name && (
|
{formState.errors.name && (
|
||||||
<p className="text-sm text-danger mt-1">
|
<p className="text-sm text-danger mt-1">
|
||||||
@@ -230,7 +228,7 @@ export function NewOAuthTokenPage() {
|
|||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field>
|
<Field>
|
||||||
<Label htmlFor="expiresIn">{__("Expiration")}</Label>
|
<Label htmlFor="expiresIn">{t("newOAuthTokenPage.fields.expiration")}</Label>
|
||||||
<Controller
|
<Controller
|
||||||
name="expiresIn"
|
name="expiresIn"
|
||||||
control={control}
|
control={control}
|
||||||
@@ -240,10 +238,10 @@ export function NewOAuthTokenPage() {
|
|||||||
value={field.value}
|
value={field.value}
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
>
|
>
|
||||||
<Option value="1month">{__("1 month")}</Option>
|
<Option value="1month">{t("newOAuthTokenPage.expirations.month", { count: 1 })}</Option>
|
||||||
<Option value="3months">{__("3 months")}</Option>
|
<Option value="3months">{t("newOAuthTokenPage.expirations.month", { count: 3 })}</Option>
|
||||||
<Option value="6months">{__("6 months")}</Option>
|
<Option value="6months">{t("newOAuthTokenPage.expirations.month", { count: 6 })}</Option>
|
||||||
<Option value="1year">{__("1 year")}</Option>
|
<Option value="1year">{t("newOAuthTokenPage.expirations.year", { count: 1 })}</Option>
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -251,7 +249,7 @@ export function NewOAuthTokenPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Field>
|
<Field>
|
||||||
<Label>{__("Scopes")}</Label>
|
<Label>{t("newOAuthTokenPage.fields.scopes")}</Label>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 mt-2">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 mt-2">
|
||||||
{supportedScopes.map(scope => (
|
{supportedScopes.map(scope => (
|
||||||
<label key={scope} className="flex items-start gap-2">
|
<label key={scope} className="flex items-start gap-2">
|
||||||
@@ -279,18 +277,18 @@ export function NewOAuthTokenPage() {
|
|||||||
|
|
||||||
<div className="flex gap-3 max-w-xl">
|
<div className="flex gap-3 max-w-xl">
|
||||||
<Button type="submit" disabled={isCreating}>
|
<Button type="submit" disabled={isCreating}>
|
||||||
{__("Create token")}
|
{t("newOAuthTokenPage.actions.create")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={toggleAllScopes}
|
onClick={toggleAllScopes}
|
||||||
>
|
>
|
||||||
{allScopesSelected ? __("Deselect all") : __("Select all")}
|
{allScopesSelected ? t("newOAuthTokenPage.actions.deselectAll") : t("newOAuthTokenPage.actions.selectAll")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="secondary" asChild>
|
<Button variant="secondary" asChild>
|
||||||
<Link to="/me/oauth-tokens">
|
<Link to="/me/oauth-tokens">
|
||||||
{__("Cancel")}
|
{t("newOAuthTokenPage.actions.cancel")}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { usePageTitle } from "@probo/hooks";
|
import { usePageTitle } from "@probo/hooks";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
@@ -32,6 +31,7 @@ import {
|
|||||||
Thead,
|
Thead,
|
||||||
Tr,
|
Tr,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
graphql,
|
graphql,
|
||||||
type PreloadedQuery,
|
type PreloadedQuery,
|
||||||
@@ -87,9 +87,9 @@ export function OAuthTokensPage(props: {
|
|||||||
queryRef: PreloadedQuery<OAuthTokensPageQuery>;
|
queryRef: PreloadedQuery<OAuthTokensPageQuery>;
|
||||||
}) {
|
}) {
|
||||||
const { queryRef } = props;
|
const { queryRef } = props;
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
usePageTitle(__("OAuth tokens"));
|
usePageTitle(t("oauthTokensPage.pageTitle"));
|
||||||
|
|
||||||
const { viewer } = usePreloadedQuery<OAuthTokensPageQuery>(
|
const { viewer } = usePreloadedQuery<OAuthTokensPageQuery>(
|
||||||
oauthTokensPageQuery,
|
oauthTokensPageQuery,
|
||||||
@@ -106,15 +106,13 @@ export function OAuthTokensPage(props: {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6 w-full py-6">
|
<div className="space-y-6 w-full py-6">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={__("OAuth tokens")}
|
title={t("oauthTokensPage.title")}
|
||||||
description={__(
|
description={t("oauthTokensPage.description")}
|
||||||
"Create bearer tokens with scoped API access for your account.",
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{viewer.canCreateOAuth2AccessToken && (
|
{viewer.canCreateOAuth2AccessToken && (
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link to="/me/oauth-tokens/new">
|
<Link to="/me/oauth-tokens/new">
|
||||||
{__("Create token")}
|
{t("oauthTokensPage.actions.create")}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -125,12 +123,10 @@ export function OAuthTokensPage(props: {
|
|||||||
<Card padded>
|
<Card padded>
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||||
{__("No OAuth tokens")}
|
{t("oauthTokensPage.empty.title")}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-600">
|
<p className="text-gray-600">
|
||||||
{__(
|
{t("oauthTokensPage.empty.description")}
|
||||||
"Create a token to authenticate API requests on your behalf.",
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -139,16 +135,16 @@ export function OAuthTokensPage(props: {
|
|||||||
<Card padded className="space-y-4">
|
<Card padded className="space-y-4">
|
||||||
{totalCount > tokens.length && (
|
{totalCount > tokens.length && (
|
||||||
<p className="text-sm text-txt-tertiary">
|
<p className="text-sm text-txt-tertiary">
|
||||||
{`${__("Showing")} ${tokens.length} ${__("of")} ${totalCount}`}
|
{t("oauthTokensPage.showing", { shown: tokens.length, total: totalCount })}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<Table>
|
<Table>
|
||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>{__("Name")}</Th>
|
<Th>{t("oauthTokensPage.columns.name")}</Th>
|
||||||
<Th>{__("Scopes")}</Th>
|
<Th>{t("oauthTokensPage.columns.scopes")}</Th>
|
||||||
<Th>{__("Created")}</Th>
|
<Th>{t("oauthTokensPage.columns.created")}</Th>
|
||||||
<Th>{__("Expires")}</Th>
|
<Th>{t("oauthTokensPage.columns.expires")}</Th>
|
||||||
<Th className="w-0" />
|
<Th className="w-0" />
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
@@ -170,7 +166,7 @@ export function OAuthTokensPage(props: {
|
|||||||
disabled={isLoadingNext}
|
disabled={isLoadingNext}
|
||||||
icon={isLoadingNext ? Spinner : IconChevronDown}
|
icon={isLoadingNext ? Spinner : IconChevronDown}
|
||||||
>
|
>
|
||||||
{__("Show more")}
|
{t("oauthTokensPage.actions.showMore")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useCopy } from "@probo/hooks";
|
import { useCopy } from "@probo/hooks";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
Button,
|
Button,
|
||||||
@@ -31,6 +30,7 @@ import {
|
|||||||
IconWarning,
|
IconWarning,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { clsx } from "clsx";
|
import { clsx } from "clsx";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export function OAuthTokenCredentialsDialog(props: {
|
export function OAuthTokenCredentialsDialog(props: {
|
||||||
dialogRef: React.RefObject<{ open: () => void; close: () => void } | null>;
|
dialogRef: React.RefObject<{ open: () => void; close: () => void } | null>;
|
||||||
@@ -38,21 +38,19 @@ export function OAuthTokenCredentialsDialog(props: {
|
|||||||
onDone: () => void;
|
onDone: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { dialogRef, token, onDone } = props;
|
const { dialogRef, token, onDone } = props;
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const [isCopied, copy] = useCopy();
|
const [isCopied, copy] = useCopy();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
ref={dialogRef}
|
ref={dialogRef}
|
||||||
title={<Breadcrumb items={[__("OAuth tokens"), __("Token")]} />}
|
title={<Breadcrumb items={[t("oauthTokensPage.title"), t("oauthTokenCredentialsDialog.title")]} />}
|
||||||
>
|
>
|
||||||
<DialogContent padded className="space-y-4">
|
<DialogContent padded className="space-y-4">
|
||||||
<div className="flex items-start gap-2 rounded-lg border border-border-danger bg-danger px-4 py-3 text-sm text-txt-danger">
|
<div className="flex items-start gap-2 rounded-lg border border-border-danger bg-danger px-4 py-3 text-sm text-txt-danger">
|
||||||
<IconWarning size={16} className="shrink-0 mt-0.5" />
|
<IconWarning size={16} className="shrink-0 mt-0.5" />
|
||||||
<p>
|
<p>
|
||||||
{__(
|
{t("oauthTokenCredentialsDialog.description")}
|
||||||
"Copy this bearer token now. You will not be able to see it again.",
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<code className="flex items-start gap-2 rounded-lg bg-subtle p-4 font-mono text-sm">
|
<code className="flex items-start gap-2 rounded-lg bg-subtle p-4 font-mono text-sm">
|
||||||
@@ -65,8 +63,8 @@ export function OAuthTokenCredentialsDialog(props: {
|
|||||||
)}
|
)}
|
||||||
onClick={() => copy(token)}
|
onClick={() => copy(token)}
|
||||||
disabled={!token}
|
disabled={!token}
|
||||||
aria-label={isCopied ? __("Copied") : __("Copy")}
|
aria-label={isCopied ? t("oauthTokenCredentialsDialog.actions.copied") : t("oauthTokenCredentialsDialog.actions.copy")}
|
||||||
title={isCopied ? __("Copied") : __("Copy")}
|
title={isCopied ? t("oauthTokenCredentialsDialog.actions.copied") : t("oauthTokenCredentialsDialog.actions.copy")}
|
||||||
>
|
>
|
||||||
{isCopied
|
{isCopied
|
||||||
? <IconCheckmark1 size={16} />
|
? <IconCheckmark1 size={16} />
|
||||||
@@ -75,7 +73,7 @@ export function OAuthTokenCredentialsDialog(props: {
|
|||||||
</code>
|
</code>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button onClick={onDone}>{__("Done")}</Button>
|
<Button onClick={onDone}>{t("oauthTokenCredentialsDialog.actions.done")}</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,8 +18,7 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { formatError } from "@probo/helpers";
|
import { formatDate, formatError } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
@@ -31,6 +30,7 @@ import {
|
|||||||
useToast,
|
useToast,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import * as Popover from "@radix-ui/react-popover";
|
import * as Popover from "@radix-ui/react-popover";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { ConnectionHandler, graphql, useFragment, useMutation } from "react-relay";
|
import { ConnectionHandler, graphql, useFragment, useMutation } from "react-relay";
|
||||||
|
|
||||||
import type { OAuthTokenRowFragment$key } from "#/__generated__/iam/OAuthTokenRowFragment.graphql";
|
import type { OAuthTokenRowFragment$key } from "#/__generated__/iam/OAuthTokenRowFragment.graphql";
|
||||||
@@ -67,7 +67,7 @@ export function OAuthTokenRow(props: {
|
|||||||
identityId: string;
|
identityId: string;
|
||||||
}) {
|
}) {
|
||||||
const { tokenKey, identityId } = props;
|
const { tokenKey, identityId } = props;
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const token = useFragment(fragment, tokenKey);
|
const token = useFragment(fragment, tokenKey);
|
||||||
@@ -90,9 +90,9 @@ export function OAuthTokenRow(props: {
|
|||||||
onCompleted: (_response, errors) => {
|
onCompleted: (_response, errors) => {
|
||||||
if (errors?.length) {
|
if (errors?.length) {
|
||||||
toast({
|
toast({
|
||||||
title: __("Error"),
|
title: t("common.error"),
|
||||||
description: formatError(
|
description: formatError(
|
||||||
__("Failed to revoke OAuth token."),
|
t("oauthTokenRow.errors.revoke"),
|
||||||
errors,
|
errors,
|
||||||
),
|
),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
@@ -100,15 +100,13 @@ export function OAuthTokenRow(props: {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
toast({
|
toast({
|
||||||
title: __("Success"),
|
title: t("common.success"), description: t("oauthTokenRow.messages.revoked"),
|
||||||
description: __("OAuth token revoked."),
|
|
||||||
variant: "success",
|
variant: "success",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast({
|
toast({
|
||||||
title: __("Error"),
|
title: t("common.error"), description: formatError(t("oauthTokenRow.errors.revoke"), error),
|
||||||
description: formatError(__("Failed to revoke OAuth token."), error),
|
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -154,28 +152,26 @@ export function OAuthTokenRow(props: {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>{new Date(token.createdAt).toLocaleDateString()}</Td>
|
<Td>{formatDate(token.createdAt)}</Td>
|
||||||
<Td>{new Date(token.expiresAt).toLocaleDateString()}</Td>
|
<Td>{formatDate(token.expiresAt)}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
{token.canDelete && (
|
{token.canDelete && (
|
||||||
<Dialog
|
<Dialog
|
||||||
title={__("Revoke OAuth token")}
|
title={t("oauthTokenRow.revoke.title")}
|
||||||
trigger={(
|
trigger={(
|
||||||
<Button variant="danger" disabled={isRevoking}>
|
<Button variant="danger" disabled={isRevoking}>
|
||||||
{__("Revoke")}
|
{t("oauthTokenRow.actions.revoke")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<DialogContent padded>
|
<DialogContent padded>
|
||||||
<p>
|
<p>
|
||||||
{__(
|
{t("oauthTokenRow.revoke.description")}
|
||||||
"This token will stop working immediately. This action cannot be undone.",
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="danger" onClick={handleRevoke} disabled={isRevoking}>
|
<Button variant="danger" onClick={handleRevoke} disabled={isRevoking}>
|
||||||
{__("Revoke token")}
|
{t("oauthTokenRow.actions.revokeToken")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -18,9 +18,9 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { UnAuthenticatedError } from "@probo/relay";
|
import { UnAuthenticatedError } from "@probo/relay";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { type PreloadedQuery, useMutation, usePreloadedQuery } from "react-relay";
|
import { type PreloadedQuery, useMutation, usePreloadedQuery } from "react-relay";
|
||||||
import { useNavigate, useSearchParams } from "react-router";
|
import { useNavigate, useSearchParams } from "react-router";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
@@ -67,7 +67,7 @@ export function AssumePage(props: { queryRef: PreloadedQuery<AssumePageQuery> })
|
|||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const safeContinueUrl = useSafeContinueUrl(`/organizations/${organizationId}`);
|
const safeContinueUrl = useSafeContinueUrl(`/organizations/${organizationId}`);
|
||||||
|
|
||||||
@@ -126,9 +126,9 @@ export function AssumePage(props: { queryRef: PreloadedQuery<AssumePageQuery> })
|
|||||||
<AuthLayout>
|
<AuthLayout>
|
||||||
<div className="space-y-6 w-full max-w-md mx-auto pt-8">
|
<div className="space-y-6 w-full max-w-md mx-auto pt-8">
|
||||||
<div className="space-y-2 text-center">
|
<div className="space-y-2 text-center">
|
||||||
<h1 className="text-3xl font-bold">{__("Sign in Redirection")}</h1>
|
<h1 className="text-3xl font-bold">{t("assumePage.title")}</h1>
|
||||||
<p className="text-txt-tertiary">
|
<p className="text-txt-tertiary">
|
||||||
{__("Redirecting you to your authentication URL…")}
|
{t("assumePage.description")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { formatError } from "@probo/helpers";
|
import { formatError } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
@@ -29,6 +28,7 @@ import {
|
|||||||
useToast,
|
useToast,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import type { FormEventHandler } from "react";
|
import type { FormEventHandler } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { graphql, useMutation } from "react-relay";
|
import { graphql, useMutation } from "react-relay";
|
||||||
import { Link, useLocation, useNavigate } from "react-router";
|
import { Link, useLocation, useNavigate } from "react-router";
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ function NewOrganizationPageInner() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [createOrganization, isCreating]
|
const [createOrganization, isCreating]
|
||||||
= useMutation<NewOrganizationPageMutation>(createOrganizationMutation);
|
= useMutation<NewOrganizationPageMutation>(createOrganizationMutation);
|
||||||
@@ -61,8 +61,7 @@ function NewOrganizationPageInner() {
|
|||||||
const name = formData.get("name") ? (formData.get("name") as string).toString() : "";
|
const name = formData.get("name") ? (formData.get("name") as string).toString() : "";
|
||||||
if (!name) {
|
if (!name) {
|
||||||
toast({
|
toast({
|
||||||
title: __("Error"),
|
title: t("common.error"), description: t("newOrganizationPage.errors.nameRequired"),
|
||||||
description: __("Name is required"),
|
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -77,8 +76,7 @@ function NewOrganizationPageInner() {
|
|||||||
onCompleted: (r, e) => {
|
onCompleted: (r, e) => {
|
||||||
if (e) {
|
if (e) {
|
||||||
toast({
|
toast({
|
||||||
title: __("Error"),
|
title: t("common.error"), description: formatError(t("newOrganizationPage.errors.create"), e),
|
||||||
description: formatError(__("Failed to create organization"), e),
|
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -87,14 +85,13 @@ function NewOrganizationPageInner() {
|
|||||||
const org = r.createOrganization!.organization;
|
const org = r.createOrganization!.organization;
|
||||||
void navigate(`/organizations/${org!.id}`);
|
void navigate(`/organizations/${org!.id}`);
|
||||||
toast({
|
toast({
|
||||||
title: __("Success"),
|
title: t("common.success"), description: t("newOrganizationPage.messages.created"),
|
||||||
description: __("Organization has been created successfully"),
|
|
||||||
variant: "success",
|
variant: "success",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
toast({
|
toast({
|
||||||
title: __("Error"),
|
title: t("common.error"),
|
||||||
description: e.message,
|
description: e.message,
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
@@ -109,34 +106,30 @@ function NewOrganizationPageInner() {
|
|||||||
className="mb-4 inline-flex gap-2 items-center"
|
className="mb-4 inline-flex gap-2 items-center"
|
||||||
>
|
>
|
||||||
<IconChevronLeft size={16} />
|
<IconChevronLeft size={16} />
|
||||||
{__("Back")}
|
{t("newOrganizationPage.actions.back")}
|
||||||
</Link>
|
</Link>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={__("Create Organization")}
|
title={t("newOrganizationPage.title")}
|
||||||
description={__(
|
description={t("newOrganizationPage.description")}
|
||||||
"Create a new organization to manage your compliance and security needs.",
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
<Card padded asChild>
|
<Card padded asChild>
|
||||||
<form onSubmit={e => void handleSubmit(e)} className="space-y-4">
|
<form onSubmit={e => void handleSubmit(e)} className="space-y-4">
|
||||||
<h2 className="text-xl font-semibold mb-1">
|
<h2 className="text-xl font-semibold mb-1">
|
||||||
{__("Organization Details")}
|
{t("newOrganizationPage.details")}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-txt-tertiary text-sm mb-4">
|
<p className="text-txt-tertiary text-sm mb-4">
|
||||||
{__("Enter the basic information about your organization.")}
|
{t("newOrganizationPage.detailsDescription")}
|
||||||
</p>
|
</p>
|
||||||
<Field
|
<Field
|
||||||
required
|
required
|
||||||
name="name"
|
name="name"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={__("Organization name")}
|
placeholder={t("newOrganizationPage.fields.name")}
|
||||||
label={__("Organization name")}
|
label={t("newOrganizationPage.fields.name")}
|
||||||
help={__(
|
help={t("newOrganizationPage.fields.nameHelp")}
|
||||||
"The name of your organization as it will appear throughout the platform.",
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
<Button disabled={isCreating} type="submit" className="w-full">
|
<Button disabled={isCreating} type="submit" className="w-full">
|
||||||
{__("Create Organization")}
|
{t("newOrganizationPage.actions.create")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
@@ -30,6 +29,7 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { Suspense, useCallback, useState } from "react";
|
import { Suspense, useCallback, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFragment, useQueryLoader } from "react-relay";
|
import { useFragment, useQueryLoader } from "react-relay";
|
||||||
import { Link, useLocation } from "react-router";
|
import { Link, useLocation } from "react-router";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
@@ -54,7 +54,7 @@ export function MembershipsDropdown(props: {
|
|||||||
const { organizationFKey } = props;
|
const { organizationFKey } = props;
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
const currentOrganization
|
const currentOrganization
|
||||||
@@ -90,7 +90,7 @@ export function MembershipsDropdown(props: {
|
|||||||
<div className="px-3 py-2">
|
<div className="px-3 py-2">
|
||||||
<Input
|
<Input
|
||||||
icon={IconMagnifyingGlass}
|
icon={IconMagnifyingGlass}
|
||||||
placeholder={__("Search organizations...")}
|
placeholder={t("membershipsDropdown.searchPlaceholder")}
|
||||||
value={search}
|
value={search}
|
||||||
onValueChange={setSearch}
|
onValueChange={setSearch}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
@@ -104,7 +104,7 @@ export function MembershipsDropdown(props: {
|
|||||||
<Suspense
|
<Suspense
|
||||||
fallback={(
|
fallback={(
|
||||||
<div className="px-3 py-2 text-gray-500">
|
<div className="px-3 py-2 text-gray-500">
|
||||||
{__("Loading organizations...")}
|
{t("membershipsDropdown.loading")}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -116,7 +116,7 @@ export function MembershipsDropdown(props: {
|
|||||||
<DropdownItem asChild>
|
<DropdownItem asChild>
|
||||||
<Link to="/organizations/new" state={{ from: location.pathname }}>
|
<Link to="/organizations/new" state={{ from: location.pathname }}>
|
||||||
<IconPlusLarge size={16} />
|
<IconPlusLarge size={16} />
|
||||||
{__("Add organization")}
|
{t("membershipsDropdown.addOrganization")}
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { IconMail } from "@probo/ui";
|
import { IconMail } from "@probo/ui";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFragment } from "react-relay";
|
import { useFragment } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ export function MembershipsDropdownInvitingItem(props: {
|
|||||||
fKey: MembershipsDropdownInvitingItemFragment$key;
|
fKey: MembershipsDropdownInvitingItemFragment$key;
|
||||||
}) {
|
}) {
|
||||||
const { fKey } = props;
|
const { fKey } = props;
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const organization = useFragment<MembershipsDropdownInvitingItemFragment$key>(
|
const organization = useFragment<MembershipsDropdownInvitingItemFragment$key>(
|
||||||
fragment,
|
fragment,
|
||||||
@@ -45,7 +45,7 @@ export function MembershipsDropdownInvitingItem(props: {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="text-txt-primary flex items-center gap-2 p-2 cursor-default"
|
className="text-txt-primary flex items-center gap-2 p-2 cursor-default"
|
||||||
title={__("Check your email to accept the invitation")}
|
title={t("membershipsDropdownInvitingItem.title")}
|
||||||
>
|
>
|
||||||
<div className="bg-border-mid text-txt-invert! rounded-full size-6 flex items-center justify-center flex-none">
|
<div className="bg-border-mid text-txt-invert! rounded-full size-6 flex items-center justify-center flex-none">
|
||||||
<IconMail size={16} />
|
<IconMail size={16} />
|
||||||
|
|||||||
@@ -18,9 +18,9 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { DropdownSeparator } from "@probo/ui";
|
import { DropdownSeparator } from "@probo/ui";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { graphql, type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
import { graphql, type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
||||||
|
|
||||||
import type { MembershipsDropdownMenuQuery } from "#/__generated__/iam/MembershipsDropdownMenuQuery.graphql";
|
import type { MembershipsDropdownMenuQuery } from "#/__generated__/iam/MembershipsDropdownMenuQuery.graphql";
|
||||||
@@ -65,7 +65,7 @@ interface MembershipsDropdownMenuProps {
|
|||||||
|
|
||||||
export function MembershipsDropdownMenu(props: MembershipsDropdownMenuProps) {
|
export function MembershipsDropdownMenu(props: MembershipsDropdownMenuProps) {
|
||||||
const { queryRef, search } = props;
|
const { queryRef, search } = props;
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
viewer: {
|
viewer: {
|
||||||
@@ -102,7 +102,7 @@ export function MembershipsDropdownMenu(props: MembershipsDropdownMenuProps) {
|
|||||||
{invitingOrganizations.length > 0 && (
|
{invitingOrganizations.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div className="px-3 py-1 text-xs text-txt-tertiary uppercase">
|
<div className="px-3 py-1 text-xs text-txt-tertiary uppercase">
|
||||||
{__("Pending invitations")}
|
{t("membershipsDropdownMenu.pendingInvitations")}
|
||||||
</div>
|
</div>
|
||||||
{invitingOrganizations.map(organization => (
|
{invitingOrganizations.map(organization => (
|
||||||
<MembershipsDropdownInvitingItem key={organization.id} fKey={organization} />
|
<MembershipsDropdownInvitingItem key={organization.id} fKey={organization} />
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { CookieIcon, LaptopIcon } from "@phosphor-icons/react";
|
import { CookieIcon, LaptopIcon } from "@phosphor-icons/react";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
IconBank,
|
IconBank,
|
||||||
IconBook,
|
IconBook,
|
||||||
@@ -42,6 +41,7 @@ import {
|
|||||||
IconTodo,
|
IconTodo,
|
||||||
SidebarItem,
|
SidebarItem,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFragment } from "react-relay";
|
import { useFragment } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ const fragment = graphql`
|
|||||||
export function Sidebar(props: { fKey: SidebarFragment$key }) {
|
export function Sidebar(props: { fKey: SidebarFragment$key }) {
|
||||||
const { fKey } = props;
|
const { fKey } = props;
|
||||||
|
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
|
|
||||||
const organization = useFragment<SidebarFragment$key>(fragment, fKey);
|
const organization = useFragment<SidebarFragment$key>(fragment, fKey);
|
||||||
@@ -95,28 +95,28 @@ export function Sidebar(props: { fKey: SidebarFragment$key }) {
|
|||||||
<ul className="space-y-[2px]">
|
<ul className="space-y-[2px]">
|
||||||
{organization.canGetContext && (
|
{organization.canGetContext && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Context")}
|
label={t("sidebar.context")}
|
||||||
icon={IconPageTextSolid}
|
icon={IconPageTextSolid}
|
||||||
to={`${prefix}/context`}
|
to={`${prefix}/context`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{organization.canListTasks && (
|
{organization.canListTasks && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Tasks")}
|
label={t("sidebar.tasks")}
|
||||||
icon={IconInboxEmpty}
|
icon={IconInboxEmpty}
|
||||||
to={`${prefix}/tasks`}
|
to={`${prefix}/tasks`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{organization.canListMeasures && (
|
{organization.canListMeasures && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Measures")}
|
label={t("sidebar.measures")}
|
||||||
icon={IconTodo}
|
icon={IconTodo}
|
||||||
to={`${prefix}/measures`}
|
to={`${prefix}/measures`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{organization.canListRisks && (
|
{organization.canListRisks && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Risks")}
|
label={t("sidebar.risks")}
|
||||||
icon={IconFire3}
|
icon={IconFire3}
|
||||||
to={`${prefix}/risks`}
|
to={`${prefix}/risks`}
|
||||||
/>
|
/>
|
||||||
@@ -124,119 +124,119 @@ export function Sidebar(props: { fKey: SidebarFragment$key }) {
|
|||||||
|
|
||||||
{organization.canListFrameworks && (
|
{organization.canListFrameworks && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Frameworks")}
|
label={t("sidebar.frameworks")}
|
||||||
icon={IconBank}
|
icon={IconBank}
|
||||||
to={`${prefix}/frameworks`}
|
to={`${prefix}/frameworks`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{organization.canListMembers && (
|
{organization.canListMembers && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("People")}
|
label={t("sidebar.people")}
|
||||||
icon={IconGroup1}
|
icon={IconGroup1}
|
||||||
to={`${prefix}/people`}
|
to={`${prefix}/people`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{organization.canListThirdParties && (
|
{organization.canListThirdParties && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Third parties")}
|
label={t("sidebar.thirdParties")}
|
||||||
icon={IconStore}
|
icon={IconStore}
|
||||||
to={`${prefix}/third-parties`}
|
to={`${prefix}/third-parties`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{organization.canListDocuments && (
|
{organization.canListDocuments && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Documents")}
|
label={t("sidebar.documents")}
|
||||||
icon={IconPageTextLine}
|
icon={IconPageTextLine}
|
||||||
to={`${prefix}/documents`}
|
to={`${prefix}/documents`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{organization.canListAssets && (
|
{organization.canListAssets && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Assets")}
|
label={t("sidebar.assets")}
|
||||||
icon={IconBox}
|
icon={IconBox}
|
||||||
to={`${prefix}/assets`}
|
to={`${prefix}/assets`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{organization.canListDevices && (
|
{organization.canListDevices && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Devices")}
|
label={t("sidebar.devices")}
|
||||||
icon={LaptopIcon}
|
icon={LaptopIcon}
|
||||||
to={`${prefix}/devices`}
|
to={`${prefix}/devices`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{organization.canListData && (
|
{organization.canListData && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Data")}
|
label={t("sidebar.data")}
|
||||||
icon={IconListStack}
|
icon={IconListStack}
|
||||||
to={`${prefix}/data`}
|
to={`${prefix}/data`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{organization.canListAudits && (
|
{organization.canListAudits && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Audits")}
|
label={t("sidebar.audits")}
|
||||||
icon={IconMedal}
|
icon={IconMedal}
|
||||||
to={`${prefix}/audits`}
|
to={`${prefix}/audits`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{organization.canListFindings && (
|
{organization.canListFindings && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Findings")}
|
label={t("sidebar.findings")}
|
||||||
icon={IconMagnifyingGlass}
|
icon={IconMagnifyingGlass}
|
||||||
to={`${prefix}/findings`}
|
to={`${prefix}/findings`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{organization.canListObligations && (
|
{organization.canListObligations && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Obligations")}
|
label={t("sidebar.obligations")}
|
||||||
icon={IconBook}
|
icon={IconBook}
|
||||||
to={`${prefix}/obligations`}
|
to={`${prefix}/obligations`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{organization.canListProcessingActivities && (
|
{organization.canListProcessingActivities && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Processing Activities")}
|
label={t("sidebar.processingActivities")}
|
||||||
icon={IconCircleProgress}
|
icon={IconCircleProgress}
|
||||||
to={`${prefix}/processing-activities`}
|
to={`${prefix}/processing-activities`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{organization.canListStatementsOfApplicability && (
|
{organization.canListStatementsOfApplicability && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Statements of Applicability")}
|
label={t("sidebar.statementsOfApplicability")}
|
||||||
icon={IconPageCheck}
|
icon={IconPageCheck}
|
||||||
to={`${prefix}/statements-of-applicability`}
|
to={`${prefix}/statements-of-applicability`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{organization.canListRightsRequests && (
|
{organization.canListRightsRequests && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Rights Requests")}
|
label={t("sidebar.rightsRequests")}
|
||||||
icon={IconLock}
|
icon={IconLock}
|
||||||
to={`${prefix}/rights-requests`}
|
to={`${prefix}/rights-requests`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{organization.canListAccessReviewCampaigns && (
|
{organization.canListAccessReviewCampaigns && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Access Reviews")}
|
label={t("sidebar.accessReviews")}
|
||||||
icon={IconKey}
|
icon={IconKey}
|
||||||
to={`${prefix}/access-reviews`}
|
to={`${prefix}/access-reviews`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{organization.canGetCompliancePage && (
|
{organization.canGetCompliancePage && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Compliance Page")}
|
label={t("sidebar.compliancePage")}
|
||||||
icon={IconShield}
|
icon={IconShield}
|
||||||
to={`${prefix}/compliance-page`}
|
to={`${prefix}/compliance-page`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{organization.canListCookieBanners && (
|
{organization.canListCookieBanners && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Cookie Banners")}
|
label={t("sidebar.cookieBanners")}
|
||||||
icon={CookieIcon}
|
icon={CookieIcon}
|
||||||
to={`${prefix}/cookie-banners`}
|
to={`${prefix}/cookie-banners`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{organization.canUpdateOrganization && (
|
{organization.canUpdateOrganization && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Settings")}
|
label={t("sidebar.settings")}
|
||||||
icon={IconSettingsGear2}
|
icon={IconSettingsGear2}
|
||||||
to={`${prefix}/settings`}
|
to={`${prefix}/settings`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { formatError } from "@probo/helpers";
|
import { formatError } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
import {
|
||||||
DropdownSeparator,
|
DropdownSeparator,
|
||||||
IconArrowBoxLeft,
|
IconArrowBoxLeft,
|
||||||
@@ -30,6 +29,7 @@ import {
|
|||||||
UserDropdownItem,
|
UserDropdownItem,
|
||||||
useToast,
|
useToast,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFragment, useMutation } from "react-relay";
|
import { useFragment, useMutation } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ export function ViewerMembershipDropdown(props: {
|
|||||||
}) {
|
}) {
|
||||||
const { fKey } = props;
|
const { fKey } = props;
|
||||||
|
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
@@ -85,8 +85,8 @@ export function ViewerMembershipDropdown(props: {
|
|||||||
onCompleted: (_, e) => {
|
onCompleted: (_, e) => {
|
||||||
if (e) {
|
if (e) {
|
||||||
toast({
|
toast({
|
||||||
title: __("Request failed"),
|
title: t("viewerMembershipDropdown.errors.requestFailed"),
|
||||||
description: formatError(__("Cannot sign out"), e),
|
description: formatError(t("viewerMembershipDropdown.errors.cannotSignOut"), e),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -95,7 +95,7 @@ export function ViewerMembershipDropdown(props: {
|
|||||||
},
|
},
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
toast({
|
toast({
|
||||||
title: __("Error"),
|
title: t("common.error"),
|
||||||
description: e.message,
|
description: e.message,
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
@@ -109,32 +109,32 @@ export function ViewerMembershipDropdown(props: {
|
|||||||
<UserDropdownItem
|
<UserDropdownItem
|
||||||
to="/me/api-keys"
|
to="/me/api-keys"
|
||||||
icon={IconKey}
|
icon={IconKey}
|
||||||
label={__("API Keys")}
|
label={t("viewerMembershipDropdown.actions.apiKeys")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{canListOAuth2AccessTokens && (
|
{canListOAuth2AccessTokens && (
|
||||||
<UserDropdownItem
|
<UserDropdownItem
|
||||||
to="/me/oauth-tokens"
|
to="/me/oauth-tokens"
|
||||||
icon={IconKey}
|
icon={IconKey}
|
||||||
label={__("OAuth tokens")}
|
label={t("viewerMembershipDropdown.actions.oauthTokens")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<UserDropdownItem
|
<UserDropdownItem
|
||||||
to={`/organizations/${organizationId}/employee`}
|
to={`/organizations/${organizationId}/employee`}
|
||||||
icon={IconPageTextLine}
|
icon={IconPageTextLine}
|
||||||
label={__("Employee Portal")}
|
label={t("viewerMembershipDropdown.actions.employeePortal")}
|
||||||
/>
|
/>
|
||||||
<UserDropdownItem
|
<UserDropdownItem
|
||||||
to="mailto:support@probo.com"
|
to="mailto:support@probo.com"
|
||||||
icon={IconCircleQuestionmark}
|
icon={IconCircleQuestionmark}
|
||||||
label={__("Help")}
|
label={t("viewerMembershipDropdown.actions.help")}
|
||||||
/>
|
/>
|
||||||
<DropdownSeparator />
|
<DropdownSeparator />
|
||||||
<UserDropdownItem
|
<UserDropdownItem
|
||||||
variant="danger"
|
variant="danger"
|
||||||
to="/logout"
|
to="/logout"
|
||||||
icon={IconArrowBoxLeft}
|
icon={IconArrowBoxLeft}
|
||||||
label="Logout"
|
label={t("viewerMembershipDropdown.actions.logout")}
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
/>
|
/>
|
||||||
</UserDropdown>
|
</UserDropdown>
|
||||||
|
|||||||
@@ -18,9 +18,9 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Button, PageHeader } from "@probo/ui";
|
import { Button, PageHeader } from "@probo/ui";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
||||||
import { ConnectionHandler, type DataID, graphql } from "relay-runtime";
|
import { ConnectionHandler, type DataID, graphql } from "relay-runtime";
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ export function PeoplePage(props: {
|
|||||||
const { queryRef } = props;
|
const { queryRef } = props;
|
||||||
|
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [connectionId, setConnectionId] = useState<DataID>(
|
const [connectionId, setConnectionId] = useState<DataID>(
|
||||||
ConnectionHandler.getConnectionID(
|
ConnectionHandler.getConnectionID(
|
||||||
@@ -69,11 +69,11 @@ export function PeoplePage(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<PageHeader title={__("People")}>
|
<PageHeader title={t("peoplePage.title")}>
|
||||||
{organization.canCreateUser
|
{organization.canCreateUser
|
||||||
&& (
|
&& (
|
||||||
<AddPersonDialog connectionId={connectionId}>
|
<AddPersonDialog connectionId={connectionId}>
|
||||||
<Button variant="secondary">{__("Add Person")}</Button>
|
<Button variant="secondary">{t("peoplePage.actions.add")}</Button>
|
||||||
</AddPersonDialog>
|
</AddPersonDialog>
|
||||||
)}
|
)}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|||||||
@@ -18,9 +18,8 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { sprintf } from "@probo/helpers";
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { ActionDropdown, Avatar, Badge, Breadcrumb, Card, DropdownItem, IconArchive, IconTrashCan, useConfirm } from "@probo/ui";
|
import { ActionDropdown, Avatar, Badge, Breadcrumb, Card, DropdownItem, IconArchive, IconTrashCan, useConfirm } from "@probo/ui";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
@@ -73,7 +72,7 @@ export function PersonPage(props: { queryRef: PreloadedQuery<PersonPageQuery> })
|
|||||||
const { queryRef } = props;
|
const { queryRef } = props;
|
||||||
|
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -85,15 +84,15 @@ export function PersonPage(props: { queryRef: PreloadedQuery<PersonPageQuery> })
|
|||||||
const [archiveUser, isArchiving] = useMutationWithToasts(
|
const [archiveUser, isArchiving] = useMutationWithToasts(
|
||||||
archiveUserMutation,
|
archiveUserMutation,
|
||||||
{
|
{
|
||||||
successMessage: __("Person archived successfully"),
|
successMessage: t("personPage.messages.archived"),
|
||||||
errorMessage: __("Failed to archive person"),
|
errorMessage: t("personPage.errors.archive"),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const [removeUser, isRemoving] = useMutationWithToasts(
|
const [removeUser, isRemoving] = useMutationWithToasts(
|
||||||
removeUserMutation,
|
removeUserMutation,
|
||||||
{
|
{
|
||||||
successMessage: __("Person removed successfully"),
|
successMessage: t("personPage.messages.removed"),
|
||||||
errorMessage: __("Failed to remove person"),
|
errorMessage: t("personPage.errors.remove"),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const isMutating = isArchiving || isRemoving;
|
const isMutating = isArchiving || isRemoving;
|
||||||
@@ -114,10 +113,7 @@ export function PersonPage(props: { queryRef: PreloadedQuery<PersonPageQuery> })
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: sprintf(
|
message: t("personPage.confirmations.archive", { name: person.fullName }),
|
||||||
__("Are you sure you want to archive %s?"),
|
|
||||||
person.fullName,
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -138,10 +134,7 @@ export function PersonPage(props: { queryRef: PreloadedQuery<PersonPageQuery> })
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: sprintf(
|
message: t("personPage.confirmations.remove", { name: person.fullName }),
|
||||||
__("Are you sure you want to remove %s?"),
|
|
||||||
person.fullName,
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -154,7 +147,7 @@ export function PersonPage(props: { queryRef: PreloadedQuery<PersonPageQuery> })
|
|||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
label: __("People"),
|
label: t("personPage.breadcrumb.people"),
|
||||||
to: `/organizations/${organizationId}/people`,
|
to: `/organizations/${organizationId}/people`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -181,7 +174,7 @@ export function PersonPage(props: { queryRef: PreloadedQuery<PersonPageQuery> })
|
|||||||
onClick={handleArchive}
|
onClick={handleArchive}
|
||||||
disabled={isMutating}
|
disabled={isMutating}
|
||||||
>
|
>
|
||||||
{__("Archive")}
|
{t("personPage.actions.archive")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
)}
|
)}
|
||||||
{canRemove && (
|
{canRemove && (
|
||||||
@@ -191,7 +184,7 @@ export function PersonPage(props: { queryRef: PreloadedQuery<PersonPageQuery> })
|
|||||||
onClick={handleRemove}
|
onClick={handleRemove}
|
||||||
disabled={isMutating}
|
disabled={isMutating}
|
||||||
>
|
>
|
||||||
{__("Remove")}
|
{t("personPage.actions.remove")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
)}
|
)}
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
|
|||||||
@@ -18,9 +18,9 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Dialog, DialogContent, useDialogRef } from "@probo/ui";
|
import { Dialog, DialogContent, useDialogRef } from "@probo/ui";
|
||||||
import { type PropsWithChildren } from "react";
|
import { type PropsWithChildren } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import type { DataID } from "relay-runtime";
|
import type { DataID } from "relay-runtime";
|
||||||
|
|
||||||
import { PersonForm } from "./PersonForm";
|
import { PersonForm } from "./PersonForm";
|
||||||
@@ -30,11 +30,11 @@ export function AddPersonDialog(props: PropsWithChildren<{
|
|||||||
}>) {
|
}>) {
|
||||||
const { children, connectionId } = props;
|
const { children, connectionId } = props;
|
||||||
const dialogRef = useDialogRef();
|
const dialogRef = useDialogRef();
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
title={__("Add Person")}
|
title={t("addPersonDialog.title")}
|
||||||
trigger={children}
|
trigger={children}
|
||||||
className="max-w-xl"
|
className="max-w-xl"
|
||||||
ref={dialogRef}
|
ref={dialogRef}
|
||||||
|
|||||||
@@ -19,10 +19,10 @@
|
|||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { getAssignableRoles } from "@probo/helpers";
|
import { getAssignableRoles } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Tbody, Td, Th, Thead, Tr } from "@probo/ui";
|
import { Tbody, Td, Th, Thead, Tr } from "@probo/ui";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
import { use } from "react";
|
import { use } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { ConnectionHandler, graphql, usePaginationFragment } from "react-relay";
|
import { ConnectionHandler, graphql, usePaginationFragment } from "react-relay";
|
||||||
|
|
||||||
import type { PeopleListFragment$key } from "#/__generated__/iam/PeopleListFragment.graphql";
|
import type { PeopleListFragment$key } from "#/__generated__/iam/PeopleListFragment.graphql";
|
||||||
@@ -72,7 +72,7 @@ export function PeopleList(props: {
|
|||||||
const { fKey, onConnectionIdChange } = props;
|
const { fKey, onConnectionIdChange } = props;
|
||||||
|
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const { __ } = useTranslate();
|
const { t } = useTranslation();
|
||||||
const { role } = use(CurrentUser);
|
const { role } = use(CurrentUser);
|
||||||
const canManageRoles = getAssignableRoles(role).length > 0;
|
const canManageRoles = getAssignableRoles(role).length > 0;
|
||||||
|
|
||||||
@@ -107,11 +107,11 @@ export function PeopleList(props: {
|
|||||||
>
|
>
|
||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<SortableTh field="FULL_NAME" onOrderChange={handleOrderChange}>{__("Name")}</SortableTh>
|
<SortableTh field="FULL_NAME" onOrderChange={handleOrderChange}>{t("peopleList.columns.name")}</SortableTh>
|
||||||
<SortableTh field="STATE">{__("Status")}</SortableTh>
|
<SortableTh field="STATE">{t("peopleList.columns.status")}</SortableTh>
|
||||||
<SortableTh field="EMAIL_ADDRESS" onOrderChange={handleOrderChange}>{__("Email")}</SortableTh>
|
<SortableTh field="EMAIL_ADDRESS" onOrderChange={handleOrderChange}>{t("peopleList.columns.email")}</SortableTh>
|
||||||
{canManageRoles && <Th>{__("Role")}</Th>}
|
{canManageRoles && <Th>{t("peopleList.columns.role")}</Th>}
|
||||||
<SortableTh field="CREATED_AT" onOrderChange={handleOrderChange}>{__("Created on")}</SortableTh>
|
<SortableTh field="CREATED_AT" onOrderChange={handleOrderChange}>{t("peopleList.columns.createdOn")}</SortableTh>
|
||||||
<Th></Th>
|
<Th></Th>
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
@@ -120,7 +120,7 @@ export function PeopleList(props: {
|
|||||||
? (
|
? (
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td colSpan={7} className="text-center text-txt-secondary">
|
<Td colSpan={7} className="text-center text-txt-secondary">
|
||||||
{__("No people")}
|
{t("peopleList.empty")}
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
)
|
)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user