Add react i18next to console
Signed-off-by: Jonathan <contact@grafikart.fr>
This commit is contained in:
@@ -18,7 +18,6 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Button,
|
||||
ErrorDetailMessage,
|
||||
@@ -26,6 +25,7 @@ import {
|
||||
ErrorLayout,
|
||||
} from "@probo/ui";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useLocation, useRouteError } from "react-router";
|
||||
|
||||
type Props = {
|
||||
@@ -36,7 +36,7 @@ type Props = {
|
||||
export function PageError({ resetErrorBoundary, error: propsError }: Props) {
|
||||
const routeError = useRouteError();
|
||||
const error = routeError ?? propsError;
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const baseLocation = useRef(location);
|
||||
|
||||
@@ -56,7 +56,7 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) {
|
||||
|
||||
const actions = (
|
||||
<Button asChild>
|
||||
<Link to="/">{__("Go home")}</Link>
|
||||
<Link to="/">{t("pageError.actions.goHome")}</Link>
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -70,8 +70,8 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) {
|
||||
return (
|
||||
<ErrorLayout
|
||||
{...layoutProps}
|
||||
title={__("Page not found")}
|
||||
description={__("The page you are looking for does not exist.")}
|
||||
title={t("pageError.notFound.title")}
|
||||
description={t("pageError.notFound.description")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -80,8 +80,8 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) {
|
||||
return (
|
||||
<ErrorLayout
|
||||
{...layoutProps}
|
||||
title={__("Page not found")}
|
||||
description={__("The page you are looking for does not exist.")}
|
||||
title={t("pageError.notFound.title")}
|
||||
description={t("pageError.notFound.description")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -89,11 +89,11 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) {
|
||||
return (
|
||||
<ErrorLayout
|
||||
{...layoutProps}
|
||||
title={__("Something went wrong")}
|
||||
description={__("We hit an unexpected error. Head back home to continue.")}
|
||||
title={t("pageError.unexpected.title")}
|
||||
description={t("pageError.unexpected.description")}
|
||||
>
|
||||
{error instanceof Error && (
|
||||
<ErrorDetails summary={__("Technical details")}>
|
||||
<ErrorDetails summary={t("pageError.technicalDetails")}>
|
||||
<ErrorDetailMessage>{error.message}</ErrorDetailMessage>
|
||||
</ErrorDetails>
|
||||
)}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Button,
|
||||
IconChevronDown,
|
||||
@@ -35,6 +34,7 @@ import {
|
||||
useContext,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { LoadMoreFn } from "react-relay";
|
||||
import type { OperationType } from "relay-runtime";
|
||||
|
||||
@@ -75,7 +75,7 @@ export function SortableTable({
|
||||
isLoadingNext?: boolean;
|
||||
pageSize?: number;
|
||||
}) {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
const [order, setOrder] = useState(defaultOrder);
|
||||
const changeOrder = (o: Order) => {
|
||||
startTransition(() => {
|
||||
@@ -95,7 +95,7 @@ export function SortableTable({
|
||||
disabled={isLoadingNext}
|
||||
icon={isLoadingNext ? Spinner : IconChevronDown}
|
||||
>
|
||||
{__("Show more")}
|
||||
{t("sortableTable.actions.showMore")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -21,9 +21,7 @@
|
||||
import {
|
||||
getAssetTypeVariant,
|
||||
promisifyMutation,
|
||||
sprintf,
|
||||
} from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
ActionDropdown,
|
||||
Badge,
|
||||
@@ -34,6 +32,7 @@ import {
|
||||
TextCell,
|
||||
useConfirm,
|
||||
} from "@probo/ui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMutation } from "react-relay";
|
||||
import type { usePaginationFragmentHookType } from "react-relay/relay-hooks/usePaginationFragment";
|
||||
import { Link } from "react-router";
|
||||
@@ -90,7 +89,7 @@ export function AssetsTable(props: Props) {
|
||||
const { connectionId, pagination, assets } = props;
|
||||
|
||||
const organizationId = useOrganizationId();
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
const deleteAsset = useDeleteAsset(connectionId);
|
||||
|
||||
return (
|
||||
@@ -100,17 +99,17 @@ export function AssetsTable(props: Props) {
|
||||
pagination={pagination}
|
||||
items={assets}
|
||||
columns={[
|
||||
__("Name"),
|
||||
__("Type"),
|
||||
__("Data Types stored"),
|
||||
__("Amount"),
|
||||
__("Owner"),
|
||||
__("Third parties"),
|
||||
t("assetsTable.columns.name"),
|
||||
t("assetsTable.columns.type"),
|
||||
t("assetsTable.columns.dataTypesStored"),
|
||||
t("assetsTable.columns.amount"),
|
||||
t("assetsTable.columns.owner"),
|
||||
t("assetsTable.columns.thirdParties"),
|
||||
]}
|
||||
schema={schema}
|
||||
updateMutation={updateAssetMutation}
|
||||
createMutation={createAssetMutation}
|
||||
addLabel={__("Add a new asset")}
|
||||
addLabel={t("assetsTable.actions.add")}
|
||||
defaultValue={{
|
||||
...defaultValue,
|
||||
organizationId,
|
||||
@@ -120,7 +119,7 @@ export function AssetsTable(props: Props) {
|
||||
<DropdownItem asChild>
|
||||
<Link to={`/organizations/${organizationId}/assets/${item.id}`}>
|
||||
<IconPencil size={16} />
|
||||
{__("Edit")}
|
||||
{t("assetsTable.actions.edit")}
|
||||
</Link>
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
@@ -128,7 +127,7 @@ export function AssetsTable(props: Props) {
|
||||
variant="danger"
|
||||
icon={IconTrashCan}
|
||||
>
|
||||
{__("Delete")}
|
||||
{t("assetsTable.actions.delete")}
|
||||
</DropdownItem>
|
||||
</ActionDropdown>
|
||||
)}
|
||||
@@ -140,7 +139,9 @@ export function AssetsTable(props: Props) {
|
||||
items={["VIRTUAL", "PHYSICAL"]}
|
||||
itemRenderer={({ item }) => (
|
||||
<Badge variant={getAssetTypeVariant(item ?? "VIRTUAL")}>
|
||||
{item === "PHYSICAL" ? __("Physical") : __("Virtual")}
|
||||
{item === "PHYSICAL"
|
||||
? t("assetsTable.assetTypes.physical")
|
||||
: t("assetsTable.assetTypes.virtual")}
|
||||
</Badge>
|
||||
)}
|
||||
defaultValue={item?.assetType ?? defaultValue.assetType}
|
||||
@@ -174,11 +175,11 @@ export function AssetsTable(props: Props) {
|
||||
const useDeleteAsset = (connectionId: string) => {
|
||||
const [mutate] = useMutation<AssetGraphDeleteMutation>(deleteAssetMutation);
|
||||
const confirm = useConfirm();
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (asset: { id: string; name: string }) => {
|
||||
if (!asset.id || !asset.name) {
|
||||
return alert(__("Failed to delete asset: missing id or name"));
|
||||
return alert(t("assetsTable.delete.missingIdOrName"));
|
||||
}
|
||||
confirm(
|
||||
() =>
|
||||
@@ -191,12 +192,7 @@ const useDeleteAsset = (connectionId: string) => {
|
||||
},
|
||||
}),
|
||||
{
|
||||
message: sprintf(
|
||||
__(
|
||||
"This will permanently delete \"%s\". This action cannot be undone.",
|
||||
),
|
||||
asset.name,
|
||||
),
|
||||
message: t("assetsTable.delete.confirmation", { name: asset.name }),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
// SOFTWARE.
|
||||
|
||||
import { faviconUrl, getAssetTypeVariant } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
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 { OperationType } from "relay-runtime";
|
||||
|
||||
@@ -46,17 +46,17 @@ type Props = {
|
||||
|
||||
export function ReadOnlyAssetsTable(props: Props) {
|
||||
const { pagination, assets } = props;
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SortableTable {...pagination} pageSize={10}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{__("Name")}</Th>
|
||||
<Th>{__("Type")}</Th>
|
||||
<Th>{__("Amount")}</Th>
|
||||
<Th>{__("Owner")}</Th>
|
||||
<Th>{__("Third parties")}</Th>
|
||||
<Th>{t("readOnlyAssetsTable.columns.name")}</Th>
|
||||
<Th>{t("readOnlyAssetsTable.columns.type")}</Th>
|
||||
<Th>{t("readOnlyAssetsTable.columns.amount")}</Th>
|
||||
<Th>{t("readOnlyAssetsTable.columns.owner")}</Th>
|
||||
<Th>{t("readOnlyAssetsTable.columns.thirdParties")}</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
@@ -70,7 +70,7 @@ export function ReadOnlyAssetsTable(props: Props) {
|
||||
|
||||
function AssetRow({ entry }: { entry: AssetEntry }) {
|
||||
const organizationId = useOrganizationId();
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
const thirdParties = entry.thirdParties?.edges.map(edge => edge.node) ?? [];
|
||||
|
||||
return (
|
||||
@@ -78,11 +78,13 @@ function AssetRow({ entry }: { entry: AssetEntry }) {
|
||||
<Td>{entry.name}</Td>
|
||||
<Td>
|
||||
<Badge variant={getAssetTypeVariant(entry.assetType)}>
|
||||
{entry.assetType === "PHYSICAL" ? __("Physical") : __("Virtual")}
|
||||
{entry.assetType === "PHYSICAL"
|
||||
? t("readOnlyAssetsTable.assetTypes.physical")
|
||||
: t("readOnlyAssetsTable.assetTypes.virtual")}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td>{entry.amount}</Td>
|
||||
<Td>{entry.owner?.fullName ?? __("Unassigned")}</Td>
|
||||
<Td>{entry.owner?.fullName ?? t("readOnlyAssetsTable.unassigned")}</Td>
|
||||
<Td>
|
||||
{thirdParties.length > 0
|
||||
? (
|
||||
@@ -110,7 +112,7 @@ function AssetRow({ entry }: { entry: AssetEntry }) {
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<span className="text-txt-secondary text-sm">{__("None")}</span>
|
||||
<span className="text-txt-secondary text-sm">{t("readOnlyAssetsTable.none")}</span>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { getAuditStateVariant, sprintf } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { getAuditStateVariant } from "@probo/helpers";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
@@ -37,6 +36,7 @@ import {
|
||||
} from "@probo/ui";
|
||||
import { clsx } from "clsx";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFragment } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
@@ -78,7 +78,7 @@ type Props<Params> = {
|
||||
};
|
||||
|
||||
export function LinkedAuditsCard<Params>(props: Props<Params>) {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
const [limit, setLimit] = useState<number | null>(4);
|
||||
const audits = useMemo(() => {
|
||||
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]">
|
||||
{variant === "card" && (
|
||||
<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 && (
|
||||
<LinkedAuditsDialog
|
||||
disabled={props.disabled}
|
||||
@@ -125,7 +125,7 @@ export function LinkedAuditsCard<Params>(props: Props<Params>) {
|
||||
onUnlink={onDetach}
|
||||
>
|
||||
<Button variant="tertiary" icon={IconPlusLarge}>
|
||||
{__("Link audit")}
|
||||
{t("linkedAuditsCard.actions.link")}
|
||||
</Button>
|
||||
</LinkedAuditsDialog>
|
||||
)}
|
||||
@@ -134,8 +134,8 @@ export function LinkedAuditsCard<Params>(props: Props<Params>) {
|
||||
<Table className={clsx(variant === "card" && "bg-invert")}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{__("Name")}</Th>
|
||||
<Th>{__("State")}</Th>
|
||||
<Th>{t("linkedAuditsCard.columns.name")}</Th>
|
||||
<Th>{t("linkedAuditsCard.columns.state")}</Th>
|
||||
{!props.readOnly && <Th></Th>}
|
||||
</Tr>
|
||||
</Thead>
|
||||
@@ -146,7 +146,7 @@ export function LinkedAuditsCard<Params>(props: Props<Params>) {
|
||||
colSpan={props.readOnly ? 2 : 3}
|
||||
className="text-center text-txt-secondary"
|
||||
>
|
||||
{__("No audits linked")}
|
||||
{t("linkedAuditsCard.empty")}
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
@@ -166,7 +166,7 @@ export function LinkedAuditsCard<Params>(props: Props<Params>) {
|
||||
onUnlink={onDetach}
|
||||
>
|
||||
<TrButton colspan={3} icon={IconPlusLarge}>
|
||||
{__("Link audit")}
|
||||
{t("linkedAuditsCard.actions.link")}
|
||||
</TrButton>
|
||||
</LinkedAuditsDialog>
|
||||
)}
|
||||
@@ -179,7 +179,9 @@ export function LinkedAuditsCard<Params>(props: Props<Params>) {
|
||||
className="mt-3 mx-auto"
|
||||
icon={IconChevronDown}
|
||||
>
|
||||
{sprintf(__("Show %s more"), props.audits.length - limit)}
|
||||
{t("linkedAuditsCard.actions.showMore", {
|
||||
count: props.audits.length - limit,
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</Wrapper>
|
||||
@@ -193,7 +195,7 @@ function AuditRow(props: {
|
||||
}) {
|
||||
const audit = useFragment(linkedAuditFragment, props.audit);
|
||||
const organizationId = useOrganizationId();
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Tr to={`/organizations/${organizationId}/audits/${audit.id}`}>
|
||||
@@ -217,7 +219,7 @@ function AuditRow(props: {
|
||||
onClick={() => props.onClick(audit.id)}
|
||||
icon={IconTrashCan}
|
||||
>
|
||||
{__("Unlink")}
|
||||
{t("linkedAuditsCard.actions.unlink")}
|
||||
</Button>
|
||||
</Td>
|
||||
)}
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
// SOFTWARE.
|
||||
|
||||
import { getAuditStateVariant } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
@@ -34,6 +33,7 @@ import {
|
||||
Spinner,
|
||||
} from "@probo/ui";
|
||||
import { type ReactNode, Suspense, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLazyLoadQuery, usePaginationFragment } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
@@ -98,16 +98,16 @@ type Props = {
|
||||
};
|
||||
|
||||
export function LinkedAuditsDialog({ children, ...props }: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Dialog trigger={children} title={__("Link audits")}>
|
||||
<Dialog trigger={children} title={t("linkedAuditsDialog.title")}>
|
||||
<DialogContent>
|
||||
<Suspense fallback={<Spinner centered />}>
|
||||
<LinkedAuditsDialogContent {...props} />
|
||||
</Suspense>
|
||||
</DialogContent>
|
||||
<DialogFooter exitLabel={__("Close")} />
|
||||
<DialogFooter exitLabel={t("linkedAuditsDialog.actions.close")} />
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -122,7 +122,7 @@ function LinkedAuditsDialogContent(props: Omit<Props, "children">) {
|
||||
auditsFragment,
|
||||
query.organization as LinkedAuditsDialogFragment$key,
|
||||
);
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
const [search, setSearch] = useState("");
|
||||
const audits = useMemo(
|
||||
() => 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">
|
||||
<Input
|
||||
icon={IconMagnifyingGlass}
|
||||
placeholder={__("Search audits...")}
|
||||
placeholder={t("linkedAuditsDialog.searchPlaceholder")}
|
||||
onValueChange={setSearch}
|
||||
/>
|
||||
</div>
|
||||
@@ -180,7 +180,7 @@ type RowProps = {
|
||||
};
|
||||
|
||||
function AuditRow(props: RowProps) {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isLinked = props.linkedAudits.has(props.audit.id);
|
||||
const onClick = isLinked ? props.onUnlink : props.onLink;
|
||||
@@ -209,7 +209,9 @@ function AuditRow(props: RowProps) {
|
||||
<span>
|
||||
<IconComponent size={16} />
|
||||
{" "}
|
||||
{isLinked ? __("Unlink") : __("Link")}
|
||||
{isLinked
|
||||
? t("linkedAuditsDialog.actions.unlink")
|
||||
: t("linkedAuditsDialog.actions.link")}
|
||||
</span>
|
||||
</Button>
|
||||
</button>
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
// SOFTWARE.
|
||||
|
||||
import { acceptImage } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
@@ -32,6 +31,7 @@ import {
|
||||
useDialogRef,
|
||||
} from "@probo/ui";
|
||||
import { forwardRef, type ReactNode, useImperativeHandle, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { CompliancePageReferenceListItemFragment$data } from "#/__generated__/core/CompliancePageReferenceListItemFragment.graphql";
|
||||
@@ -57,7 +57,7 @@ export type CompliancePageReferenceDialogRef = {
|
||||
|
||||
export const CompliancePageReferenceDialog = forwardRef<CompliancePageReferenceDialogRef, { children?: ReactNode }>(
|
||||
function CompliancePageReferenceDialog({ children }, ref) {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
const dialogRef = useDialogRef();
|
||||
const [mode, setMode] = useState<"create" | "edit">("create");
|
||||
const [compliancePageId, setCompliancePageId] = useState<string>("");
|
||||
@@ -182,7 +182,7 @@ export const CompliancePageReferenceDialog = forwardRef<CompliancePageReferenceD
|
||||
};
|
||||
|
||||
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 (
|
||||
<>
|
||||
@@ -202,45 +202,45 @@ export const CompliancePageReferenceDialog = forwardRef<CompliancePageReferenceD
|
||||
<DialogContent padded className="space-y-6">
|
||||
<Field
|
||||
{...register("name")}
|
||||
label={__("Reference Name")}
|
||||
label={t("trustCenterReferenceDialog.fields.name.label")}
|
||||
type="text"
|
||||
required
|
||||
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
|
||||
{...register("description")}
|
||||
placeholder={__("Brief description of the reference")}
|
||||
placeholder={t("trustCenterReferenceDialog.fields.description.placeholder")}
|
||||
rows={3}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
{...register("websiteUrl")}
|
||||
label={__("Website URL")}
|
||||
label={t("trustCenterReferenceDialog.fields.websiteUrl.label")}
|
||||
type="url"
|
||||
required
|
||||
error={errors.websiteUrl?.message}
|
||||
placeholder={__("https://example.com")}
|
||||
placeholder={t("trustCenterReferenceDialog.fields.websiteUrl.placeholder")}
|
||||
/>
|
||||
|
||||
{mode === "edit" && (
|
||||
<Field
|
||||
{...register("rank", { valueAsNumber: true })}
|
||||
label={__("Rank")}
|
||||
label={t("trustCenterReferenceDialog.fields.rank.label")}
|
||||
type="number"
|
||||
min={1}
|
||||
error={errors.rank?.message}
|
||||
placeholder={__("Display order (1, 2, 3...)")}
|
||||
help={__("Lower numbers appear first")}
|
||||
placeholder={t("trustCenterReferenceDialog.fields.rank.placeholder")}
|
||||
help={t("trustCenterReferenceDialog.fields.rank.help")}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Field label={__("Logo")}>
|
||||
<Field label={t("trustCenterReferenceDialog.fields.logo.label")}>
|
||||
<Dropzone
|
||||
description={__("Upload logo image (PNG, JPG, WEBP, SVG up to 5MB)")}
|
||||
description={t("trustCenterReferenceDialog.fields.logo.description")}
|
||||
isUploading={isSubmitting}
|
||||
onDrop={handleDrop}
|
||||
accept={acceptImage}
|
||||
@@ -249,7 +249,7 @@ export const CompliancePageReferenceDialog = forwardRef<CompliancePageReferenceD
|
||||
{uploadedFile && (
|
||||
<div className="mt-2 p-3 bg-tertiary-subtle rounded-lg">
|
||||
<p className="text-sm font-medium">
|
||||
{__("Selected file")}
|
||||
{t("trustCenterReferenceDialog.fields.logo.selectedFile")}
|
||||
:
|
||||
</p>
|
||||
<p className="text-sm text-txt-secondary">{uploadedFile.name}</p>
|
||||
@@ -258,14 +258,14 @@ export const CompliancePageReferenceDialog = forwardRef<CompliancePageReferenceD
|
||||
{mode === "edit" && !uploadedFile && (
|
||||
<div className="mt-2 p-3 bg-tertiary-subtle rounded-lg">
|
||||
<p className="text-sm text-txt-secondary">
|
||||
{__("Current logo will be kept if no new file is uploaded")}
|
||||
{t("trustCenterReferenceDialog.fields.logo.keepCurrent")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{mode === "create" && !uploadedFile && (
|
||||
<div className="mt-2 p-3 bg-warning-subtle rounded-lg">
|
||||
<p className="text-sm">
|
||||
{__("Logo is required for new references")}
|
||||
{t("trustCenterReferenceDialog.fields.logo.required")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -278,7 +278,7 @@ export const CompliancePageReferenceDialog = forwardRef<CompliancePageReferenceD
|
||||
disabled={isSubmitting || (mode === "create" && !uploadedFile)}
|
||||
icon={isSubmitting ? Spinner : undefined}
|
||||
>
|
||||
{mode === "create" ? __("Add Reference") : __("Update Reference")}
|
||||
{mode === "create" ? t("trustCenterReferenceDialog.actions.add") : t("trustCenterReferenceDialog.actions.update")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { sprintf } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
@@ -29,6 +27,7 @@ import {
|
||||
Spinner,
|
||||
useDialogRef,
|
||||
} from "@probo/ui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useDeleteCompliancePageReferenceMutation } from "#/pages/organizations/compliance-page/_lib/compliancePageReferenceMutations";
|
||||
|
||||
@@ -47,7 +46,7 @@ export function DeleteCompliancePageReferenceDialog({
|
||||
connectionId,
|
||||
onSuccess,
|
||||
}: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
const ref = useDialogRef();
|
||||
|
||||
const [deleteReference, isDeleting] = useDeleteCompliancePageReferenceMutation();
|
||||
@@ -70,18 +69,15 @@ export function DeleteCompliancePageReferenceDialog({
|
||||
<Dialog
|
||||
ref={ref}
|
||||
trigger={children}
|
||||
title={__("Delete Reference")}
|
||||
title={t("deleteTrustCenterReferenceDialog.title")}
|
||||
className="max-w-md"
|
||||
>
|
||||
<DialogContent padded>
|
||||
<p className="text-txt-secondary">
|
||||
{sprintf(
|
||||
__("Are you sure you want to delete the reference \"%s\"?"),
|
||||
referenceName,
|
||||
)}
|
||||
{t("deleteTrustCenterReferenceDialog.description", { referenceName })}
|
||||
</p>
|
||||
<p className="text-txt-secondary mt-2">
|
||||
{__("This action cannot be undone.")}
|
||||
{t("deleteTrustCenterReferenceDialog.warning")}
|
||||
</p>
|
||||
</DialogContent>
|
||||
|
||||
@@ -92,7 +88,7 @@ export function DeleteCompliancePageReferenceDialog({
|
||||
disabled={isDeleting}
|
||||
icon={isDeleting ? Spinner : IconTrashCan}
|
||||
>
|
||||
{isDeleting ? __("Deleting...") : __("Delete")}
|
||||
{isDeleting ? t("deleteTrustCenterReferenceDialog.actions.deleting") : t("deleteTrustCenterReferenceDialog.actions.delete")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
@@ -31,6 +30,7 @@ import {
|
||||
TrButton,
|
||||
} from "@probo/ui";
|
||||
import type { ComponentProps } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFragment } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
@@ -84,7 +84,7 @@ type Props<Params> = {
|
||||
* Reusable component that displays a list of linked controls
|
||||
*/
|
||||
export function LinkedControlsCard<Params>(props: Props<Params>) {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
const controls = props.controls;
|
||||
|
||||
const onDetach = (controlId: string) => {
|
||||
@@ -118,8 +118,8 @@ export function LinkedControlsCard<Params>(props: Props<Params>) {
|
||||
<SortableTable refetch={props.refetch}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<SortableTh field="SECTION_TITLE">{__("Reference")}</SortableTh>
|
||||
<Th>{__("Name")}</Th>
|
||||
<SortableTh field="SECTION_TITLE">{t("linkedControlsCard.columns.reference")}</SortableTh>
|
||||
<Th>{t("linkedControlsCard.columns.name")}</Th>
|
||||
{!props.readOnly && <Th></Th>}
|
||||
</Tr>
|
||||
</Thead>
|
||||
@@ -130,7 +130,7 @@ export function LinkedControlsCard<Params>(props: Props<Params>) {
|
||||
colSpan={props.readOnly ? 2 : 3}
|
||||
className="text-center text-txt-secondary"
|
||||
>
|
||||
{__("No controls linked")}
|
||||
{t("linkedControlsCard.empty")}
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
@@ -151,7 +151,7 @@ export function LinkedControlsCard<Params>(props: Props<Params>) {
|
||||
onLink={onAttach}
|
||||
onUnlink={onDetach}
|
||||
>
|
||||
<TrButton colspan={3}>{__("Link control")}</TrButton>
|
||||
<TrButton colspan={3}>{t("linkedControlsCard.actions.link")}</TrButton>
|
||||
</LinkedControlsDialog>
|
||||
)}
|
||||
</Tbody>
|
||||
@@ -167,7 +167,7 @@ function ControlRow(props: {
|
||||
}) {
|
||||
const control = useFragment(linkedControlFragment, props.control);
|
||||
const organizationId = useOrganizationId();
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Tr
|
||||
@@ -188,7 +188,7 @@ function ControlRow(props: {
|
||||
onClick={() => props.onClick(control.id)}
|
||||
icon={IconTrashCan}
|
||||
>
|
||||
{__("Unlink")}
|
||||
{t("linkedControlsCard.actions.unlink")}
|
||||
</Button>
|
||||
</Td>
|
||||
)}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
@@ -40,6 +39,7 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLazyLoadQuery, usePaginationFragment } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
import { useDebounceCallback } from "usehooks-ts";
|
||||
@@ -107,7 +107,7 @@ type Props = {
|
||||
type SearchRef = RefObject<{ search: (v: string) => void } | null>;
|
||||
|
||||
export function LinkedControlsDialog(props: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
const searchRef: SearchRef = useRef(null);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const [minHeight, setMinHeight] = useState(0);
|
||||
@@ -116,12 +116,12 @@ export function LinkedControlsDialog(props: Props) {
|
||||
searchRef.current?.search(v);
|
||||
};
|
||||
return (
|
||||
<Dialog trigger={props.children} title={__("Link controls")}>
|
||||
<Dialog trigger={props.children} title={t("linkedControlsDialog.title")}>
|
||||
<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">
|
||||
<Input
|
||||
icon={IconMagnifyingGlass}
|
||||
placeholder={__("Search controls...")}
|
||||
placeholder={t("linkedControlsDialog.searchPlaceholder")}
|
||||
onValueChange={onSearch}
|
||||
/>
|
||||
</div>
|
||||
@@ -202,7 +202,7 @@ function ControlRow(
|
||||
controlIds: Set<string>;
|
||||
} & Props,
|
||||
) {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
const isLinked = props.controlIds.has(props.control.id);
|
||||
const onClick = isLinked ? props.onUnlink : props.onLink;
|
||||
const IconComponent = isLinked ? IconTrashCan : IconPlusLarge;
|
||||
@@ -225,7 +225,9 @@ function ControlRow(
|
||||
<span>
|
||||
<IconComponent size={16} />
|
||||
{" "}
|
||||
{isLinked ? __("Unlink") : __("Link")}
|
||||
{isLinked
|
||||
? t("linkedControlsDialog.actions.unlink")
|
||||
: t("linkedControlsDialog.actions.link")}
|
||||
</span>
|
||||
</Button>
|
||||
</button>
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { sprintf } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
@@ -31,6 +29,7 @@ import {
|
||||
useDialogRef,
|
||||
} from "@probo/ui";
|
||||
import { forwardRef, type ReactNode, useImperativeHandle } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
||||
@@ -69,7 +68,7 @@ export type BulkExportDialogRef = {
|
||||
|
||||
export const BulkExportDialog = forwardRef<BulkExportDialogRef, Props>(
|
||||
({ children, onExport, isLoading = false, defaultEmail, selectedCount }, ref) => {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
const dialogRef = useDialogRef();
|
||||
|
||||
const { register, handleSubmit, formState, watch, setValue } = useFormWithSchema(
|
||||
@@ -106,7 +105,7 @@ export const BulkExportDialog = forwardRef<BulkExportDialogRef, Props>(
|
||||
<Dialog
|
||||
className="max-w-md"
|
||||
ref={dialogRef}
|
||||
title={sprintf(__("Export %s Documents"), selectedCount)}
|
||||
title={t("bulkExportDialog.title", { count: selectedCount })}
|
||||
>
|
||||
<form onSubmit={e => void handleSubmit(onSubmit)(e)}>
|
||||
<DialogContent className="space-y-4" padded>
|
||||
@@ -118,10 +117,10 @@ export const BulkExportDialog = forwardRef<BulkExportDialogRef, Props>(
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<label className="text-sm font-medium text-txt-primary cursor-pointer">
|
||||
{__("Include signatures")}
|
||||
{t("bulkExportDialog.includeSignatures.label")}
|
||||
</label>
|
||||
<p className="text-xs text-txt-secondary mt-1">
|
||||
{__("Show signature information and approval details in the PDFs")}
|
||||
{t("bulkExportDialog.includeSignatures.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -133,10 +132,10 @@ export const BulkExportDialog = forwardRef<BulkExportDialogRef, Props>(
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<label className="text-sm font-medium text-txt-primary cursor-pointer">
|
||||
{__("Add watermark")}
|
||||
{t("bulkExportDialog.watermark.label")}
|
||||
</label>
|
||||
<p className="text-xs text-txt-secondary mt-1">
|
||||
{__("Add confidential watermark with email and timestamp to all PDFs")}
|
||||
{t("bulkExportDialog.watermark.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -144,10 +143,10 @@ export const BulkExportDialog = forwardRef<BulkExportDialogRef, Props>(
|
||||
{watchWatermark && (
|
||||
<div className="ml-6">
|
||||
<Field
|
||||
label={__("Watermark email")}
|
||||
label={t("bulkExportDialog.watermark.emailLabel")}
|
||||
{...register("watermarkEmail")}
|
||||
type="email"
|
||||
placeholder={__("Enter email address")}
|
||||
placeholder={t("bulkExportDialog.watermark.emailPlaceholder")}
|
||||
error={formState.errors.watermarkEmail?.message}
|
||||
autoComplete="off"
|
||||
required
|
||||
@@ -158,7 +157,7 @@ export const BulkExportDialog = forwardRef<BulkExportDialogRef, Props>(
|
||||
|
||||
<div className="bg-level-1 p-3 rounded-lg border border-border-subtle">
|
||||
<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>
|
||||
</div>
|
||||
</DialogContent>
|
||||
@@ -171,11 +170,11 @@ export const BulkExportDialog = forwardRef<BulkExportDialogRef, Props>(
|
||||
? (
|
||||
<>
|
||||
<Spinner size={16} />
|
||||
{__("Exporting...")}
|
||||
{t("bulkExportDialog.actions.exporting")}
|
||||
</>
|
||||
)
|
||||
: (
|
||||
__("Export Documents")
|
||||
t("bulkExportDialog.actions.export")
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { sprintf } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -38,6 +36,7 @@ import {
|
||||
} from "@probo/ui";
|
||||
import { clsx } from "clsx";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFragment } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
@@ -92,7 +91,7 @@ type Props<Params> = {
|
||||
* Reusable component that displays a list of linked documents
|
||||
*/
|
||||
export function LinkedDocumentsCard<Params>(props: Props<Params>) {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
const [limit, setLimit] = useState<number | null>(4);
|
||||
const documents = useMemo(() => {
|
||||
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]">
|
||||
{variant === "card" && (
|
||||
<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 && (
|
||||
<LinkedDocumentDialog
|
||||
connectionId={props.connectionId}
|
||||
@@ -140,7 +139,7 @@ export function LinkedDocumentsCard<Params>(props: Props<Params>) {
|
||||
onUnlink={onDetach}
|
||||
>
|
||||
<Button variant="tertiary" icon={IconPlusLarge}>
|
||||
{__("Link document")}
|
||||
{t("linkedDocumentsCard.actions.link")}
|
||||
</Button>
|
||||
</LinkedDocumentDialog>
|
||||
)}
|
||||
@@ -149,9 +148,9 @@ export function LinkedDocumentsCard<Params>(props: Props<Params>) {
|
||||
<Table className={clsx(variant === "card" && "bg-invert")}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{__("Name")}</Th>
|
||||
<Th>{__("Type")}</Th>
|
||||
<Th>{__("State")}</Th>
|
||||
<Th>{t("linkedDocumentsCard.columns.name")}</Th>
|
||||
<Th>{t("linkedDocumentsCard.columns.type")}</Th>
|
||||
<Th>{t("linkedDocumentsCard.columns.state")}</Th>
|
||||
{!props.readOnly && <Th></Th>}
|
||||
</Tr>
|
||||
</Thead>
|
||||
@@ -162,7 +161,7 @@ export function LinkedDocumentsCard<Params>(props: Props<Params>) {
|
||||
colSpan={props.readOnly ? 3 : 4}
|
||||
className="text-center text-txt-secondary"
|
||||
>
|
||||
{__("No documents linked")}
|
||||
{t("linkedDocumentsCard.empty")}
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
@@ -183,7 +182,7 @@ export function LinkedDocumentsCard<Params>(props: Props<Params>) {
|
||||
onUnlink={onDetach}
|
||||
>
|
||||
<TrButton colspan={4} icon={IconPlusLarge}>
|
||||
{__("Link document")}
|
||||
{t("linkedDocumentsCard.actions.link")}
|
||||
</TrButton>
|
||||
</LinkedDocumentDialog>
|
||||
)}
|
||||
@@ -196,7 +195,9 @@ export function LinkedDocumentsCard<Params>(props: Props<Params>) {
|
||||
className="mt-3 mx-auto"
|
||||
icon={IconChevronDown}
|
||||
>
|
||||
{sprintf(__("Show %s more"), props.documents.length - limit)}
|
||||
{t("linkedDocumentsCard.actions.showMore", {
|
||||
count: props.documents.length - limit,
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</Wrapper>
|
||||
@@ -210,7 +211,7 @@ function DocumentRow(props: {
|
||||
}) {
|
||||
const document = useFragment(linkedDocumentFragment, props.document);
|
||||
const organizationId = useOrganizationId();
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Tr to={`/organizations/${organizationId}/documents/${document.id}`}>
|
||||
@@ -239,7 +240,7 @@ function DocumentRow(props: {
|
||||
onClick={() => props.onClick(document.id)}
|
||||
icon={IconTrashCan}
|
||||
>
|
||||
{__("Unlink")}
|
||||
{t("linkedDocumentsCard.actions.unlink")}
|
||||
</Button>
|
||||
</Td>
|
||||
)}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
@@ -33,6 +32,7 @@ import {
|
||||
Spinner,
|
||||
} from "@probo/ui";
|
||||
import { type ReactNode, Suspense, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLazyLoadQuery, usePaginationFragment } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
@@ -101,16 +101,16 @@ type Props = {
|
||||
};
|
||||
|
||||
export function LinkedDocumentDialog({ children, ...props }: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Dialog trigger={children} title={__("Link documents")}>
|
||||
<Dialog trigger={children} title={t("linkedDocumentsDialog.title")}>
|
||||
<DialogContent>
|
||||
<Suspense fallback={<Spinner centered />}>
|
||||
<LinkedDocumentsDialogContent {...props} />
|
||||
</Suspense>
|
||||
</DialogContent>
|
||||
<DialogFooter exitLabel={__("Close")} />
|
||||
<DialogFooter exitLabel={t("linkedDocumentsDialog.actions.close")} />
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -129,7 +129,7 @@ function LinkedDocumentsDialogContent(props: Omit<Props, "children">) {
|
||||
documentsFragment,
|
||||
query.organization as LinkedDocumentsDialogFragment$key,
|
||||
);
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
const [search, setSearch] = useState("");
|
||||
const documents = useMemo(
|
||||
() => 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">
|
||||
<Input
|
||||
icon={IconMagnifyingGlass}
|
||||
placeholder={__("Search documents...")}
|
||||
placeholder={t("linkedDocumentsDialog.searchPlaceholder")}
|
||||
onValueChange={setSearch}
|
||||
/>
|
||||
</div>
|
||||
@@ -187,7 +187,7 @@ type RowProps = {
|
||||
};
|
||||
|
||||
function DocumentRow(props: RowProps) {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isLinked = props.linkedDocuments.has(props.document.id);
|
||||
const onClick = isLinked ? props.onUnlink : props.onLink;
|
||||
@@ -209,7 +209,9 @@ function DocumentRow(props: RowProps) {
|
||||
<span>
|
||||
<IconComponent size={16} />
|
||||
{" "}
|
||||
{isLinked ? __("Unlink") : __("Link")}
|
||||
{isLinked
|
||||
? t("linkedDocumentsDialog.actions.unlink")
|
||||
: t("linkedDocumentsDialog.actions.link")}
|
||||
</span>
|
||||
</Button>
|
||||
</button>
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
@@ -30,6 +29,7 @@ import {
|
||||
useDialogRef,
|
||||
} from "@probo/ui";
|
||||
import { forwardRef, type ReactNode, useImperativeHandle } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
||||
@@ -60,7 +60,7 @@ export type PdfDownloadDialogRef = {
|
||||
|
||||
export const PdfDownloadDialog = forwardRef<PdfDownloadDialogRef, Props>(
|
||||
({ children, onDownload, isLoading = false, defaultEmail }, ref) => {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
const dialogRef = useDialogRef();
|
||||
|
||||
const { register, handleSubmit, formState, watch, setValue }
|
||||
@@ -95,7 +95,7 @@ export const PdfDownloadDialog = forwardRef<PdfDownloadDialogRef, Props>(
|
||||
<Dialog
|
||||
className="max-w-md"
|
||||
ref={dialogRef}
|
||||
title={__("Download PDF Options")}
|
||||
title={t("pdfDownloadDialog.title")}
|
||||
>
|
||||
<form onSubmit={e => void handleSubmit(onSubmit)(e)}>
|
||||
<DialogContent className="space-y-4" padded>
|
||||
@@ -107,12 +107,10 @@ export const PdfDownloadDialog = forwardRef<PdfDownloadDialogRef, Props>(
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<label className="text-sm font-medium text-txt-primary cursor-pointer">
|
||||
{__("Include signatures")}
|
||||
{t("pdfDownloadDialog.includeSignatures.label")}
|
||||
</label>
|
||||
<p className="text-xs text-txt-secondary mt-1">
|
||||
{__(
|
||||
"Show signature information and approval details in the PDF",
|
||||
)}
|
||||
{t("pdfDownloadDialog.includeSignatures.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -124,12 +122,10 @@ export const PdfDownloadDialog = forwardRef<PdfDownloadDialogRef, Props>(
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<label className="text-sm font-medium text-txt-primary cursor-pointer">
|
||||
{__("Add watermark")}
|
||||
{t("pdfDownloadDialog.watermark.label")}
|
||||
</label>
|
||||
<p className="text-xs text-txt-secondary mt-1">
|
||||
{__(
|
||||
"Add confidential watermark with email and timestamp",
|
||||
)}
|
||||
{t("pdfDownloadDialog.watermark.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -137,10 +133,10 @@ export const PdfDownloadDialog = forwardRef<PdfDownloadDialogRef, Props>(
|
||||
{watchWatermark && (
|
||||
<div className="ml-6">
|
||||
<Field
|
||||
label={__("Watermark email")}
|
||||
label={t("pdfDownloadDialog.watermark.emailLabel")}
|
||||
{...register("watermarkEmail")}
|
||||
type="email"
|
||||
placeholder={__("Enter email address")}
|
||||
placeholder={t("pdfDownloadDialog.watermark.emailPlaceholder")}
|
||||
error={formState.errors.watermarkEmail?.message}
|
||||
autoComplete="off"
|
||||
required
|
||||
@@ -155,11 +151,11 @@ export const PdfDownloadDialog = forwardRef<PdfDownloadDialogRef, Props>(
|
||||
? (
|
||||
<>
|
||||
<Spinner size={16} />
|
||||
{__("Downloading...")}
|
||||
{t("pdfDownloadDialog.actions.downloading")}
|
||||
</>
|
||||
)
|
||||
: (
|
||||
__("Download PDF")
|
||||
t("pdfDownloadDialog.actions.download")
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
// SOFTWARE.
|
||||
|
||||
import { getAuditStateLabel, getAuditStateVariant } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Badge, Field, Option, Select } from "@probo/ui";
|
||||
import { type ComponentProps, Suspense } from "react";
|
||||
import {
|
||||
@@ -28,6 +27,7 @@ import {
|
||||
type FieldValues,
|
||||
type Path,
|
||||
} from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { graphql, useLazyLoadQuery } from "react-relay";
|
||||
|
||||
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>(
|
||||
props: Pick<Props<T>, "organizationId" | "control" | "name" | "disabled">,
|
||||
) {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
const { name, organizationId, control } = props;
|
||||
const data = useLazyLoadQuery<AuditSelectFieldQuery>(
|
||||
auditsQuery,
|
||||
@@ -109,7 +109,7 @@ function AuditSelectWithQuery<T extends FieldValues = FieldValues>(
|
||||
disabled={props.disabled}
|
||||
id={name}
|
||||
variant="editor"
|
||||
placeholder={__("Select an audit")}
|
||||
placeholder={t("auditSelectField.placeholder")}
|
||||
onValueChange={value =>
|
||||
field.onChange(value === NONE_VALUE ? "" : value)}
|
||||
key={audits?.length.toString() ?? "0"}
|
||||
@@ -118,7 +118,7 @@ function AuditSelectWithQuery<T extends FieldValues = FieldValues>(
|
||||
value={field.value || NONE_VALUE}
|
||||
>
|
||||
<Option value={NONE_VALUE}>
|
||||
<span className="text-txt-tertiary">{__("None")}</span>
|
||||
<span className="text-txt-tertiary">{t("auditSelectField.none")}</span>
|
||||
</Option>
|
||||
{audits?.map(audit => (
|
||||
<Option key={audit.id} value={audit.id}>
|
||||
@@ -130,7 +130,7 @@ function AuditSelectWithQuery<T extends FieldValues = FieldValues>(
|
||||
</span>
|
||||
<div className="ml-3">
|
||||
<Badge variant={getAuditStateVariant(audit.state)}>
|
||||
{getAuditStateLabel(__, audit.state)}
|
||||
{getAuditStateLabel(t, audit.state)}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,21 +18,18 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import {
|
||||
controlMaturityLevels,
|
||||
getControlMaturityLevelLabel,
|
||||
} from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { controlMaturityLevels } from "@probo/helpers";
|
||||
import { Option } from "@probo/ui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function ControlMaturityLevelOptions() {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
{controlMaturityLevels.map(level => (
|
||||
<Option key={level} value={level}>
|
||||
{getControlMaturityLevelLabel(__, level)}
|
||||
{t(`controlMaturityLevelOptions.levels.${level}`)}
|
||||
</Option>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -18,12 +18,17 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { countries, type CountryCode, getCountryName, getCountryOptions } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { countries, type CountryCode, getCountryName } from "@probo/helpers";
|
||||
import { Badge, IconCrossLargeX, Input } from "@probo/ui";
|
||||
import { clsx } from "clsx";
|
||||
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> = {
|
||||
control: Control<T>;
|
||||
@@ -31,7 +36,11 @@ type Props<T extends FieldValues = FieldValues> = {
|
||||
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 (
|
||||
<Controller
|
||||
control={control}
|
||||
@@ -54,7 +63,7 @@ type CountriesFieldInputProps = {
|
||||
};
|
||||
|
||||
function CountriesFieldInput(props: CountriesFieldInputProps) {
|
||||
const { __ } = useTranslate();
|
||||
const { i18n } = useTranslation();
|
||||
const [animateBadge, setAnimateBadge] = useState(false);
|
||||
|
||||
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",
|
||||
)}
|
||||
>
|
||||
{getCountryName(__, countryCode as CountryCode)}
|
||||
{getCountryName(i18n.language, countryCode as CountryCode)}
|
||||
<div className="w-0 overflow-hidden group-hover:w-4 duration-200">
|
||||
<IconCrossLargeX size={12} />
|
||||
</div>
|
||||
@@ -113,10 +122,13 @@ type CountryInputProps = {
|
||||
};
|
||||
|
||||
function CountryInput({ availableCountries, onAdd }: CountryInputProps) {
|
||||
const { __ } = useTranslate();
|
||||
const { t, i18n } = useTranslation();
|
||||
const [search, setSearch] = useState("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const countryOptions = getCountryOptions(__);
|
||||
const countryOptions = availableCountries.map(code => ({
|
||||
value: code,
|
||||
label: getCountryName(i18n.language, code),
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = isOpen ? "hidden" : "";
|
||||
@@ -126,7 +138,9 @@ function CountryInput({ availableCountries, onAdd }: CountryInputProps) {
|
||||
}, [isOpen]);
|
||||
|
||||
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 }) =>
|
||||
option.label.toLowerCase().includes(search.toLowerCase()),
|
||||
);
|
||||
@@ -145,7 +159,7 @@ function CountryInput({ availableCountries, onAdd }: CountryInputProps) {
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
onFocus={() => setIsOpen(true)}
|
||||
placeholder={__("Search and add countries...")}
|
||||
placeholder={t("countriesField.searchPlaceholder")}
|
||||
className="w-full pr-8"
|
||||
/>
|
||||
<button
|
||||
@@ -186,10 +200,7 @@ function CountryInput({ availableCountries, onAdd }: CountryInputProps) {
|
||||
)}
|
||||
|
||||
{isOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-40"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
<div className="fixed inset-0 z-40" onClick={() => setIsOpen(false)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -18,21 +18,18 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import {
|
||||
documentClassifications,
|
||||
getDocumentClassificationLabel,
|
||||
} from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { documentClassifications } from "@probo/helpers";
|
||||
import { Option } from "@probo/ui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function DocumentClassificationOptions() {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
{documentClassifications.map(classification => (
|
||||
<Option key={classification} value={classification}>
|
||||
{getDocumentClassificationLabel(__, classification)}
|
||||
{t(`documentClassificationOptions.classifications.${classification}`)}
|
||||
</Option>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -18,18 +18,18 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { documentTypes, getDocumentTypeLabel } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { documentTypes } from "@probo/helpers";
|
||||
import { Option } from "@probo/ui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function DocumentTypeOptions() {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
{documentTypes.map(type => (
|
||||
<Option key={type} value={type}>
|
||||
{getDocumentTypeLabel(__, type)}
|
||||
{t(`documentTypeOptions.types.${type}`)}
|
||||
</Option>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -18,10 +18,17 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
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 { useTranslation } from "react-i18next";
|
||||
|
||||
type Props<TFieldValues extends FieldValues = FieldValues> = {
|
||||
disabled: boolean;
|
||||
@@ -32,10 +39,12 @@ type Props<TFieldValues extends FieldValues = FieldValues> = {
|
||||
/**
|
||||
* A field to handle multiple emails
|
||||
*/
|
||||
export function EmailsField<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
>({ control, register, disabled }: Props<TFieldValues>) {
|
||||
const { __ } = useTranslate();
|
||||
export function EmailsField<TFieldValues extends FieldValues = FieldValues>({
|
||||
control,
|
||||
register,
|
||||
disabled,
|
||||
}: Props<TFieldValues>) {
|
||||
const { t } = useTranslation();
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
name: "additionalEmailAddresses" as ArrayPath<TFieldValues>,
|
||||
control,
|
||||
@@ -43,12 +52,14 @@ export function EmailsField<
|
||||
|
||||
return (
|
||||
<fieldset className="space-y-2">
|
||||
{fields.length > 0 && <Label>{__("Additional emails")}</Label>}
|
||||
{fields.length > 0 && <Label>{t("emailsField.additionalEmails")}</Label>}
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className="flex items-stretch">
|
||||
<Input
|
||||
className="w-full"
|
||||
{...register(`additionalEmailAddresses.${index}` as Path<TFieldValues>)}
|
||||
{...register(
|
||||
`additionalEmailAddresses.${index}` as Path<TFieldValues>,
|
||||
)}
|
||||
type="email"
|
||||
disabled={disabled}
|
||||
/>
|
||||
@@ -67,7 +78,7 @@ export function EmailsField<
|
||||
onClick={() => append("" as FieldValue<TFieldValues>)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{__("Add email")}
|
||||
{t("emailsField.actions.add")}
|
||||
</Button>
|
||||
</fieldset>
|
||||
);
|
||||
|
||||
@@ -18,10 +18,15 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Combobox, ComboboxItem, Field } from "@probo/ui";
|
||||
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";
|
||||
|
||||
@@ -38,7 +43,9 @@ type Props<
|
||||
optional?: boolean;
|
||||
} & ComponentProps<typeof Field>;
|
||||
|
||||
export function MeasureSelectField<TFieldValues extends FieldValues = FieldValues>({
|
||||
export function MeasureSelectField<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
>({
|
||||
organizationId,
|
||||
control,
|
||||
disabled,
|
||||
@@ -48,7 +55,11 @@ export function MeasureSelectField<TFieldValues extends FieldValues = FieldValue
|
||||
return (
|
||||
<Field {...props}>
|
||||
<Suspense
|
||||
fallback={<Combobox onSearch={() => {}} placeholder="Loading..." disabled><div /></Combobox>}
|
||||
fallback={(
|
||||
<Combobox onSearch={() => {}} placeholder="Loading..." disabled>
|
||||
<div />
|
||||
</Combobox>
|
||||
)}
|
||||
>
|
||||
<MeasureSelectWithQuery<TFieldValues>
|
||||
organizationId={organizationId}
|
||||
@@ -63,9 +74,12 @@ export function MeasureSelectField<TFieldValues extends FieldValues = FieldValue
|
||||
}
|
||||
|
||||
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 { data } = usePaginatedMeasures(organizationId);
|
||||
const [search, setSearch] = useState("");
|
||||
@@ -91,12 +105,14 @@ function MeasureSelectWithQuery<TFieldValues extends FieldValues = FieldValues>(
|
||||
control={control}
|
||||
name={name}
|
||||
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 (
|
||||
<Combobox
|
||||
id={name}
|
||||
placeholder={__("Select a measure")}
|
||||
placeholder={t("measureSelectField.placeholder")}
|
||||
value={selectedMeasure ? selectedMeasure.name : search}
|
||||
onSearch={setSearch}
|
||||
disabled={disabled}
|
||||
@@ -108,7 +124,7 @@ function MeasureSelectWithQuery<TFieldValues extends FieldValues = FieldValues>(
|
||||
setSearch("");
|
||||
}}
|
||||
>
|
||||
{__("None")}
|
||||
{t("measureSelectField.none")}
|
||||
</ComboboxItem>
|
||||
)}
|
||||
{measures?.map(m => (
|
||||
@@ -123,7 +139,9 @@ function MeasureSelectWithQuery<TFieldValues extends FieldValues = FieldValues>(
|
||||
<div className="max-w-75 ellipsis overflow-hidden whitespace-pre-wrap">
|
||||
{m.name}
|
||||
</div>
|
||||
<div className="text-sm text-txt-secondary">{m.category}</div>
|
||||
<div className="text-sm text-txt-secondary">
|
||||
{m.category}
|
||||
</div>
|
||||
</div>
|
||||
</ComboboxItem>
|
||||
))}
|
||||
|
||||
@@ -18,10 +18,22 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Badge, Button, Field, IconCrossLargeX, Option, Select } from "@probo/ui";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Field,
|
||||
IconCrossLargeX,
|
||||
Option,
|
||||
Select,
|
||||
} from "@probo/ui";
|
||||
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";
|
||||
|
||||
@@ -67,10 +79,24 @@ export function PeopleMultiSelectField<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 { name, organizationId, control, selectedPeople = [], placeholder } = props;
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
name,
|
||||
organizationId,
|
||||
control,
|
||||
selectedPeople = [],
|
||||
placeholder,
|
||||
} = props;
|
||||
const people = usePeople(organizationId, { contractEnded: false });
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
@@ -91,10 +117,16 @@ function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
|
||||
control={control}
|
||||
name={name as Path<T>}
|
||||
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 availablePeople = allPeople.filter(p => !selectedPeopleIds.includes(p.id));
|
||||
const selectedPeople = allPeople.filter(p =>
|
||||
selectedPeopleIds.includes(p.id),
|
||||
);
|
||||
const availablePeople = allPeople.filter(
|
||||
p => !selectedPeopleIds.includes(p.id),
|
||||
);
|
||||
|
||||
const handleAddPerson = (personId: string) => {
|
||||
const newValue = [...selectedPeopleIds, personId];
|
||||
@@ -103,7 +135,9 @@ function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
|
||||
};
|
||||
|
||||
const handleRemovePerson = (personId: string) => {
|
||||
const newValue = selectedPeopleIds.filter((id: string) => id !== personId);
|
||||
const newValue = selectedPeopleIds.filter(
|
||||
(id: string) => id !== personId,
|
||||
);
|
||||
field.onChange(newValue);
|
||||
};
|
||||
|
||||
@@ -114,7 +148,9 @@ function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
|
||||
disabled={props.disabled}
|
||||
id={name}
|
||||
variant="editor"
|
||||
placeholder={placeholder ?? __("Add people...")}
|
||||
placeholder={
|
||||
placeholder ?? t("peopleMultiSelectField.addPlaceholder")
|
||||
}
|
||||
onValueChange={handleAddPerson}
|
||||
key={`${selectedPeopleIds.length}-${people.length}`}
|
||||
className="w-full"
|
||||
@@ -123,7 +159,11 @@ function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
|
||||
onOpenChange={setIsOpen}
|
||||
>
|
||||
{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">
|
||||
<span>{person.fullName}</span>
|
||||
{person.emailAddress && (
|
||||
@@ -140,7 +180,11 @@ function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
|
||||
{selectedPeople.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{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>
|
||||
{!props.disabled && (
|
||||
<Button
|
||||
@@ -158,7 +202,7 @@ function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
|
||||
|
||||
{selectedPeople.length === 0 && availablePeople.length === 0 && (
|
||||
<div className="text-sm text-txt-secondary py-2">
|
||||
{__("No people available")}
|
||||
{t("peopleMultiSelectField.empty")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -18,12 +18,12 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Avatar, Field, Option, Select } from "@probo/ui";
|
||||
import { type ComponentProps, Suspense } from "react";
|
||||
import { type Control, Controller, type FieldPath, type FieldValues } from "react-hook-form";
|
||||
|
||||
import { usePeople } from "#/hooks/graph/PeopleGraph";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type Props<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
@@ -42,7 +42,7 @@ export function PeopleSelectField<TFieldValues extends FieldValues = FieldValues
|
||||
control,
|
||||
...props
|
||||
}: Props<TFieldValues>) {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Field {...props}>
|
||||
@@ -51,7 +51,7 @@ export function PeopleSelectField<TFieldValues extends FieldValues = FieldValues
|
||||
<Select
|
||||
variant="editor"
|
||||
loading
|
||||
placeholder={__("Select an owner")}
|
||||
placeholder={t("peopleSelectField.placeholder")}
|
||||
className="w-full"
|
||||
/>
|
||||
)}
|
||||
@@ -74,7 +74,7 @@ function PeopleSelectWithQuery<TFieldValues extends FieldValues = FieldValues>(
|
||||
"organizationId" | "control" | "name" | "disabled" | "optional"
|
||||
>,
|
||||
) {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
const { name, organizationId, control } = props;
|
||||
const people = usePeople(organizationId, { contractEnded: false });
|
||||
|
||||
@@ -87,14 +87,16 @@ function PeopleSelectWithQuery<TFieldValues extends FieldValues = FieldValues>(
|
||||
disabled={props.disabled}
|
||||
id={name}
|
||||
variant="editor"
|
||||
placeholder={__("Select an owner")}
|
||||
placeholder={t("peopleSelectField.placeholder")}
|
||||
onValueChange={value =>
|
||||
field.onChange(value === "__NONE__" ? null : value)}
|
||||
{...field}
|
||||
className="w-full"
|
||||
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 => (
|
||||
<Option key={p.id} value={p.id} className="flex gap-2">
|
||||
<Avatar name={p.fullName} />
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Option } from "@probo/ui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import type {
|
||||
ProcessingActivityDataProtectionImpactAssessment,
|
||||
@@ -28,16 +28,27 @@ import type {
|
||||
ProcessingActivityTransferImpactAssessment,
|
||||
} from "#/__generated__/core/ProcessingActivityGraphCreateMutation.graphql";
|
||||
|
||||
type Translator = (key: string) => string;
|
||||
|
||||
export function SpecialOrCriminalDataOptions() {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const options: Array<{
|
||||
value: ProcessingActivitySpecialOrCriminalDatum;
|
||||
label: string;
|
||||
}> = [
|
||||
{ value: "YES", label: __("Yes") },
|
||||
{ value: "NO", label: __("No") },
|
||||
{ value: "POSSIBLE", label: __("Possible") },
|
||||
{
|
||||
value: "YES",
|
||||
label: t("processingActivityEnumOptions.specialOrCriminalData.yes"),
|
||||
},
|
||||
{
|
||||
value: "NO",
|
||||
label: t("processingActivityEnumOptions.specialOrCriminalData.no"),
|
||||
},
|
||||
{
|
||||
value: "POSSIBLE",
|
||||
label: t("processingActivityEnumOptions.specialOrCriminalData.possible"),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -52,18 +63,38 @@ export function SpecialOrCriminalDataOptions() {
|
||||
}
|
||||
|
||||
export function LawfulBasisOptions() {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const options: Array<{
|
||||
value: ProcessingActivityLawfulBasis;
|
||||
label: string;
|
||||
}> = [
|
||||
{ value: "CONSENT", label: __("Consent") },
|
||||
{ value: "CONTRACTUAL_NECESSITY", label: __("Contractual Necessity") },
|
||||
{ value: "LEGAL_OBLIGATION", label: __("Legal Obligation") },
|
||||
{ value: "LEGITIMATE_INTEREST", label: __("Legitimate Interest") },
|
||||
{ value: "PUBLIC_TASK", label: __("Public Task") },
|
||||
{ value: "VITAL_INTERESTS", label: __("Vital Interests") },
|
||||
{
|
||||
value: "CONSENT",
|
||||
label: t("processingActivityEnumOptions.lawfulBasis.consent"),
|
||||
},
|
||||
{
|
||||
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 (
|
||||
@@ -79,17 +110,23 @@ export function LawfulBasisOptions() {
|
||||
|
||||
export function getLawfulBasisLabel(
|
||||
value: ProcessingActivityLawfulBasis | null | undefined,
|
||||
__: (key: string) => string,
|
||||
t: Translator,
|
||||
): string {
|
||||
if (!value) return "-";
|
||||
|
||||
const labels = {
|
||||
CONSENT: __("Consent"),
|
||||
CONTRACTUAL_NECESSITY: __("Contractual Necessity"),
|
||||
LEGAL_OBLIGATION: __("Legal Obligation"),
|
||||
LEGITIMATE_INTEREST: __("Legitimate Interest"),
|
||||
PUBLIC_TASK: __("Public Task"),
|
||||
VITAL_INTERESTS: __("Vital Interests"),
|
||||
CONSENT:
|
||||
t("processingActivityEnumOptions.lawfulBasis.consent"),
|
||||
CONTRACTUAL_NECESSITY:
|
||||
t("processingActivityEnumOptions.lawfulBasis.contractualNecessity"),
|
||||
LEGAL_OBLIGATION:
|
||||
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;
|
||||
@@ -97,38 +134,63 @@ export function getLawfulBasisLabel(
|
||||
|
||||
export function getResidualRiskLabel(
|
||||
value: "LOW" | "MEDIUM" | "HIGH" | null | undefined,
|
||||
__: (key: string) => string,
|
||||
t: Translator,
|
||||
): string {
|
||||
if (!value) return "-";
|
||||
|
||||
const labels = {
|
||||
LOW: __("Low"),
|
||||
MEDIUM: __("Medium"),
|
||||
HIGH: __("High"),
|
||||
LOW: t("processingActivityEnumOptions.residualRisk.low") || "Low",
|
||||
MEDIUM: t("processingActivityEnumOptions.residualRisk.medium") || "Medium",
|
||||
HIGH: t("processingActivityEnumOptions.residualRisk.high") || "High",
|
||||
};
|
||||
|
||||
return labels[value] || value;
|
||||
}
|
||||
|
||||
export function TransferSafeguardsOptions() {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const options: Array<{
|
||||
value: string;
|
||||
label: string;
|
||||
}> = [
|
||||
{ value: "__NONE__", label: __("None") },
|
||||
{
|
||||
value: "__NONE__",
|
||||
label: t("processingActivityEnumOptions.transferSafeguards.none"),
|
||||
},
|
||||
{
|
||||
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",
|
||||
label: __("Certification Mechanisms"),
|
||||
label: t(
|
||||
"processingActivityEnumOptions.transferSafeguards.certificationMechanisms",
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -144,14 +206,24 @@ export function TransferSafeguardsOptions() {
|
||||
}
|
||||
|
||||
export function DataProtectionImpactAssessmentOptions() {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const options: Array<{
|
||||
value: ProcessingActivityDataProtectionImpactAssessment;
|
||||
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 (
|
||||
@@ -166,14 +238,22 @@ export function DataProtectionImpactAssessmentOptions() {
|
||||
}
|
||||
|
||||
export function TransferImpactAssessmentOptions() {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const options: Array<{
|
||||
value: ProcessingActivityTransferImpactAssessment;
|
||||
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 (
|
||||
@@ -188,14 +268,20 @@ export function TransferImpactAssessmentOptions() {
|
||||
}
|
||||
|
||||
export function RoleOptions() {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const options: Array<{
|
||||
value: "CONTROLLER" | "PROCESSOR";
|
||||
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 (
|
||||
|
||||
@@ -19,11 +19,28 @@
|
||||
// SOFTWARE.
|
||||
|
||||
import { faviconUrl } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Avatar, Badge, Button, Field, IconCrossLargeX, Option, Select } from "@probo/ui";
|
||||
import {
|
||||
Avatar,
|
||||
Badge,
|
||||
Button,
|
||||
Field,
|
||||
IconCrossLargeX,
|
||||
Option,
|
||||
Select,
|
||||
} from "@probo/ui";
|
||||
import { type ComponentProps, Suspense, useEffect, useState } from "react";
|
||||
import { type Control, Controller, type FieldValues, type Path } from "react-hook-form";
|
||||
import { type PreloadedQuery, usePreloadedQuery, useQueryLoader } from "react-relay";
|
||||
import {
|
||||
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 type { ThirdPartiesMultiSelectFieldQuery } from "#/__generated__/core/ThirdPartiesMultiSelectFieldQuery.graphql";
|
||||
@@ -32,10 +49,7 @@ const thirdPartiesQuery = graphql`
|
||||
query ThirdPartiesMultiSelectFieldQuery($organizationId: ID!) {
|
||||
organization: node(id: $organizationId) {
|
||||
... on Organization {
|
||||
thirdParties(
|
||||
first: 100
|
||||
orderBy: { direction: ASC, field: NAME }
|
||||
) {
|
||||
thirdParties(first: 100, orderBy: { direction: ASC, field: NAME }) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
@@ -64,12 +78,9 @@ type Props<T extends FieldValues = FieldValues> = {
|
||||
selectedThirdParties?: ThirdParty[];
|
||||
} & ComponentProps<typeof Field>;
|
||||
|
||||
export function ThirdPartiesMultiSelectField<T extends FieldValues = FieldValues>({
|
||||
organizationId,
|
||||
control,
|
||||
selectedThirdParties = [],
|
||||
...props
|
||||
}: Props<T>) {
|
||||
export function ThirdPartiesMultiSelectField<
|
||||
T extends FieldValues = FieldValues,
|
||||
>({ organizationId, control, selectedThirdParties = [], ...props }: Props<T>) {
|
||||
const [queryRef, loadQuery]
|
||||
= useQueryLoader<ThirdPartiesMultiSelectFieldQuery>(thirdPartiesQuery);
|
||||
|
||||
@@ -103,14 +114,21 @@ export function ThirdPartiesMultiSelectField<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>;
|
||||
},
|
||||
) {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
const { name, control, selectedThirdParties = [] } = props;
|
||||
const data = usePreloadedQuery<ThirdPartiesMultiSelectFieldQuery>(thirdPartiesQuery, props.queryRef);
|
||||
const thirdParties = data.organization?.thirdParties?.edges.map(edge => edge.node) ?? [];
|
||||
const data = usePreloadedQuery<ThirdPartiesMultiSelectFieldQuery>(
|
||||
thirdPartiesQuery,
|
||||
props.queryRef,
|
||||
);
|
||||
const thirdParties
|
||||
= data.organization?.thirdParties?.edges.map(edge => edge.node) ?? [];
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const allThirdParties: ThirdParty[] = [...thirdParties];
|
||||
@@ -128,10 +146,16 @@ function ThirdPartiesMultiSelectWithQuery<T extends FieldValues = FieldValues>(
|
||||
control={control}
|
||||
name={name as Path<T>}
|
||||
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 availableThirdParties = allThirdParties.filter(v => !selectedThirdPartyIds.includes(v.id));
|
||||
const selectedThirdParties = allThirdParties.filter(v =>
|
||||
selectedThirdPartyIds.includes(v.id),
|
||||
);
|
||||
const availableThirdParties = allThirdParties.filter(
|
||||
v => !selectedThirdPartyIds.includes(v.id),
|
||||
);
|
||||
|
||||
const handleAddThirdParty = (thirdPartyId: string) => {
|
||||
const newValue = [...selectedThirdPartyIds, thirdPartyId];
|
||||
@@ -140,7 +164,9 @@ function ThirdPartiesMultiSelectWithQuery<T extends FieldValues = FieldValues>(
|
||||
};
|
||||
|
||||
const handleRemoveThirdParty = (thirdPartyId: string) => {
|
||||
const newValue = selectedThirdPartyIds.filter((id: string) => id !== thirdPartyId);
|
||||
const newValue = selectedThirdPartyIds.filter(
|
||||
(id: string) => id !== thirdPartyId,
|
||||
);
|
||||
field.onChange(newValue);
|
||||
};
|
||||
|
||||
@@ -151,7 +177,7 @@ function ThirdPartiesMultiSelectWithQuery<T extends FieldValues = FieldValues>(
|
||||
disabled={props.disabled}
|
||||
id={name}
|
||||
variant="editor"
|
||||
placeholder={__("Add third parties...")}
|
||||
placeholder={t("thirdPartiesMultiSelectField.addPlaceholder")}
|
||||
onValueChange={handleAddThirdParty}
|
||||
key={`${selectedThirdPartyIds.length}-${thirdParties.length}`}
|
||||
className="w-full"
|
||||
@@ -160,7 +186,11 @@ function ThirdPartiesMultiSelectWithQuery<T extends FieldValues = FieldValues>(
|
||||
onOpenChange={setIsOpen}
|
||||
>
|
||||
{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
|
||||
name={thirdParty.name}
|
||||
src={faviconUrl(thirdParty.websiteUrl)}
|
||||
@@ -182,7 +212,11 @@ function ThirdPartiesMultiSelectWithQuery<T extends FieldValues = FieldValues>(
|
||||
{selectedThirdParties.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{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
|
||||
name={thirdParty.name}
|
||||
src={faviconUrl(thirdParty.websiteUrl)}
|
||||
@@ -202,9 +236,10 @@ function ThirdPartiesMultiSelectWithQuery<T extends FieldValues = FieldValues>(
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedThirdParties.length === 0 && availableThirdParties.length === 0 && (
|
||||
{selectedThirdParties.length === 0
|
||||
&& availableThirdParties.length === 0 && (
|
||||
<div className="text-sm text-txt-secondary py-2">
|
||||
{__("No third parties available")}
|
||||
{t("thirdPartiesMultiSelectField.empty")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { sprintf } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -37,6 +35,7 @@ import {
|
||||
import { MeasureBadge } from "@probo/ui/src/Molecules/Badge/MeasureBadge";
|
||||
import { clsx } from "clsx";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFragment } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
@@ -83,7 +82,7 @@ type Props<Params> = {
|
||||
* Reusable component that displays a list of linked measures
|
||||
*/
|
||||
export function LinkedMeasuresCard<Params>(props: Props<Params>) {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
const [limit, setLimit] = useState<number | null>(
|
||||
props.variant === "card" ? 4 : null,
|
||||
);
|
||||
@@ -123,7 +122,9 @@ export function LinkedMeasuresCard<Params>(props: Props<Params>) {
|
||||
<Wrapper padded className="space-y-[10px]">
|
||||
{variant === "card" && (
|
||||
<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 && (
|
||||
<LinkedMeasureDialog
|
||||
connectionId={props.connectionId}
|
||||
@@ -133,7 +134,7 @@ export function LinkedMeasuresCard<Params>(props: Props<Params>) {
|
||||
onUnlink={onDetach}
|
||||
>
|
||||
<Button variant="tertiary" icon={IconPlusLarge}>
|
||||
{__("Link measure")}
|
||||
{t("linkedMeasuresCard.actions.link")}
|
||||
</Button>
|
||||
</LinkedMeasureDialog>
|
||||
)}
|
||||
@@ -142,8 +143,8 @@ export function LinkedMeasuresCard<Params>(props: Props<Params>) {
|
||||
<Table className={clsx(variant === "card" && "bg-invert")}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{__("Name")}</Th>
|
||||
<Th>{__("State")}</Th>
|
||||
<Th>{t("linkedMeasuresCard.columns.name")}</Th>
|
||||
<Th>{t("linkedMeasuresCard.columns.state")}</Th>
|
||||
{!props.readOnly && <Th></Th>}
|
||||
</Tr>
|
||||
</Thead>
|
||||
@@ -154,7 +155,7 @@ export function LinkedMeasuresCard<Params>(props: Props<Params>) {
|
||||
colSpan={props.readOnly ? 2 : 3}
|
||||
className="text-center text-txt-secondary"
|
||||
>
|
||||
{__("No measures linked")}
|
||||
{t("linkedMeasuresCard.empty")}
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
@@ -175,7 +176,7 @@ export function LinkedMeasuresCard<Params>(props: Props<Params>) {
|
||||
onUnlink={onDetach}
|
||||
>
|
||||
<TrButton colspan={3} icon={IconPlusLarge}>
|
||||
{__("Link measure")}
|
||||
{t("linkedMeasuresCard.actions.link")}
|
||||
</TrButton>
|
||||
</LinkedMeasureDialog>
|
||||
)}
|
||||
@@ -188,7 +189,9 @@ export function LinkedMeasuresCard<Params>(props: Props<Params>) {
|
||||
className="mt-3 mx-auto"
|
||||
icon={IconChevronDown}
|
||||
>
|
||||
{sprintf(__("Show %s more"), props.measures.length - limit)}
|
||||
{t("linkedMeasuresCard.actions.showMore", {
|
||||
count: props.measures.length - limit,
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</Wrapper>
|
||||
@@ -202,7 +205,7 @@ function MeasureRow(props: {
|
||||
}) {
|
||||
const measure = useFragment(linkedMeasureFragment, props.measure);
|
||||
const organizationId = useOrganizationId();
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Tr to={`/organizations/${organizationId}/measures/${measure.id}`}>
|
||||
@@ -217,7 +220,7 @@ function MeasureRow(props: {
|
||||
onClick={() => props.onClick(measure.id)}
|
||||
icon={IconTrashCan}
|
||||
>
|
||||
{__("Unlink")}
|
||||
{t("linkedMeasuresCard.actions.unlink")}
|
||||
</Button>
|
||||
</Td>
|
||||
)}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
@@ -35,6 +34,7 @@ import {
|
||||
Spinner,
|
||||
} from "@probo/ui";
|
||||
import { type ReactNode, Suspense, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { usePaginatedMeasures } from "#/hooks/graph/usePaginatedMeasures";
|
||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||
@@ -49,16 +49,16 @@ type Props = {
|
||||
};
|
||||
|
||||
export function LinkedMeasureDialog({ children, ...props }: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Dialog trigger={children} title={__("Link measures")}>
|
||||
<Dialog trigger={children} title={t("linkedMeasuresDialog.title")}>
|
||||
<DialogContent>
|
||||
<Suspense fallback={<Spinner centered />}>
|
||||
<LinkedMeasuresDialogContent {...props} />
|
||||
</Suspense>
|
||||
</DialogContent>
|
||||
<DialogFooter exitLabel={__("Close")} />
|
||||
<DialogFooter exitLabel={t("linkedMeasuresDialog.actions.close")} />
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -67,10 +67,13 @@ function LinkedMeasuresDialogContent(props: Omit<Props, "children">) {
|
||||
const organizationId = useOrganizationId();
|
||||
const { data, loadNext, hasNext, isLoadingNext }
|
||||
= usePaginatedMeasures(organizationId);
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
const [search, setSearch] = useState("");
|
||||
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(() => {
|
||||
return new Set(props.linkedMeasures?.map(m => m.id) ?? []);
|
||||
}, [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">
|
||||
<Input
|
||||
icon={IconMagnifyingGlass}
|
||||
placeholder={__("Search measures...")}
|
||||
placeholder={t("linkedMeasuresDialog.searchPlaceholder")}
|
||||
onValueChange={setSearch}
|
||||
/>
|
||||
<Select
|
||||
value={category ?? ""}
|
||||
placeholder={__("All categories")}
|
||||
placeholder={t("linkedMeasuresDialog.allCategories")}
|
||||
onValueChange={setCategory}
|
||||
className="max-w-[180px]"
|
||||
>
|
||||
@@ -141,7 +144,7 @@ type RowProps = {
|
||||
};
|
||||
|
||||
function MeasureRow(props: RowProps) {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isLinked = props.linkedMeasures.has(props.measure.id);
|
||||
const onClick = isLinked ? props.onUnlink : props.onLink;
|
||||
@@ -163,7 +166,9 @@ function MeasureRow(props: RowProps) {
|
||||
<span>
|
||||
<IconComponent size={16} />
|
||||
{" "}
|
||||
{isLinked ? __("Unlink") : __("Link")}
|
||||
{isLinked
|
||||
? t("linkedMeasuresDialog.actions.unlink")
|
||||
: t("linkedMeasuresDialog.actions.link")}
|
||||
</span>
|
||||
</Button>
|
||||
</button>
|
||||
|
||||
@@ -21,9 +21,7 @@
|
||||
import {
|
||||
getObligationStatusLabel,
|
||||
getObligationStatusVariant,
|
||||
sprintf,
|
||||
} from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
@@ -41,6 +39,7 @@ import {
|
||||
} from "@probo/ui";
|
||||
import { clsx } from "clsx";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFragment } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
@@ -84,7 +83,7 @@ type Props<Params> = {
|
||||
};
|
||||
|
||||
export function LinkedObligationsCard<Params>(props: Props<Params>) {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
const [limit, setLimit] = useState<number | null>(
|
||||
props.variant === "card" ? 4 : null,
|
||||
);
|
||||
@@ -126,7 +125,9 @@ export function LinkedObligationsCard<Params>(props: Props<Params>) {
|
||||
<Wrapper padded className="space-y-[10px]">
|
||||
{variant === "card" && (
|
||||
<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 && (
|
||||
<LinkedObligationDialog
|
||||
connectionId={props.connectionId}
|
||||
@@ -136,7 +137,7 @@ export function LinkedObligationsCard<Params>(props: Props<Params>) {
|
||||
onUnlink={onDetach}
|
||||
>
|
||||
<Button variant="tertiary" icon={IconPlusLarge}>
|
||||
{__("Link obligation")}
|
||||
{t("linkedObligationsCard.actions.link")}
|
||||
</Button>
|
||||
</LinkedObligationDialog>
|
||||
)}
|
||||
@@ -145,10 +146,10 @@ export function LinkedObligationsCard<Params>(props: Props<Params>) {
|
||||
<Table className={clsx(variant === "card" && "bg-invert")}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{__("Area")}</Th>
|
||||
<Th>{__("Source")}</Th>
|
||||
<Th>{__("Status")}</Th>
|
||||
<Th>{__("Owner")}</Th>
|
||||
<Th>{t("linkedObligationsCard.columns.area")}</Th>
|
||||
<Th>{t("linkedObligationsCard.columns.source")}</Th>
|
||||
<Th>{t("linkedObligationsCard.columns.status")}</Th>
|
||||
<Th>{t("linkedObligationsCard.columns.owner")}</Th>
|
||||
{!props.readOnly && <Th></Th>}
|
||||
</Tr>
|
||||
</Thead>
|
||||
@@ -159,7 +160,7 @@ export function LinkedObligationsCard<Params>(props: Props<Params>) {
|
||||
colSpan={props.readOnly ? 4 : 5}
|
||||
className="text-center text-txt-secondary"
|
||||
>
|
||||
{__("No obligations linked")}
|
||||
{t("linkedObligationsCard.empty")}
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
@@ -180,7 +181,7 @@ export function LinkedObligationsCard<Params>(props: Props<Params>) {
|
||||
onUnlink={onDetach}
|
||||
>
|
||||
<TrButton colspan={5} icon={IconPlusLarge}>
|
||||
{__("Link obligation")}
|
||||
{t("linkedObligationsCard.actions.link")}
|
||||
</TrButton>
|
||||
</LinkedObligationDialog>
|
||||
)}
|
||||
@@ -193,7 +194,9 @@ export function LinkedObligationsCard<Params>(props: Props<Params>) {
|
||||
className="mt-3 mx-auto"
|
||||
icon={IconChevronDown}
|
||||
>
|
||||
{sprintf(__("Show %s more"), props.obligations.length - limit)}
|
||||
{t("linkedObligationsCard.actions.showMore", {
|
||||
count: props.obligations.length - limit,
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</Wrapper>
|
||||
@@ -205,7 +208,7 @@ function ObligationRow(props: {
|
||||
onClick: (obligationId: string) => void;
|
||||
readOnly?: boolean;
|
||||
}) {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
const obligation = useFragment(linkedObligationFragment, props.obligation);
|
||||
const organizationId = useOrganizationId();
|
||||
|
||||
@@ -217,18 +220,20 @@ function ObligationRow(props: {
|
||||
|
||||
return (
|
||||
<Tr to={detailsUrl}>
|
||||
<Td>{obligation.area || __("No area specified")}</Td>
|
||||
<Td>{obligation.source || __("No source specified")}</Td>
|
||||
<Td>{obligation.area || t("linkedObligationsCard.noArea")}</Td>
|
||||
<Td>{obligation.source || t("linkedObligationsCard.noSource")}</Td>
|
||||
<Td>
|
||||
<Badge variant={getObligationStatusVariant(obligation.status)}>
|
||||
{getObligationStatusLabel(obligation.status)}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td>{obligation.owner?.fullName || __("Unassigned")}</Td>
|
||||
<Td>
|
||||
{obligation.owner?.fullName || t("linkedObligationsCard.unassigned")}
|
||||
</Td>
|
||||
{!props.readOnly && (
|
||||
<Td noLink width={50} className="text-end">
|
||||
<Button variant="secondary" icon={IconTrashCan} onClick={onDetach}>
|
||||
{__("Unlink")}
|
||||
{t("linkedObligationsCard.actions.unlink")}
|
||||
</Button>
|
||||
</Td>
|
||||
)}
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
getObligationStatusLabel,
|
||||
getObligationStatusVariant,
|
||||
} from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
@@ -37,6 +36,7 @@ import {
|
||||
Spinner,
|
||||
} from "@probo/ui";
|
||||
import { type ReactNode, Suspense, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLazyLoadQuery, usePaginationFragment } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
@@ -102,16 +102,16 @@ type Props = {
|
||||
};
|
||||
|
||||
export function LinkedObligationDialog({ children, ...props }: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Dialog trigger={children} title={__("Link obligations")}>
|
||||
<Dialog trigger={children} title={t("linkedObligationsDialog.title")}>
|
||||
<DialogContent>
|
||||
<Suspense fallback={<Spinner centered />}>
|
||||
<LinkedObligationsDialogContent {...props} />
|
||||
</Suspense>
|
||||
</DialogContent>
|
||||
<DialogFooter exitLabel={__("Close")} />
|
||||
<DialogFooter exitLabel={t("linkedObligationsDialog.actions.close")} />
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -125,12 +125,14 @@ function LinkedObligationsDialogContent(props: Omit<Props, "children">) {
|
||||
},
|
||||
{ fetchPolicy: "network-only" },
|
||||
);
|
||||
const { data, loadNext, hasNext, isLoadingNext }
|
||||
= usePaginationFragment<LinkedObligationsDialogQuery_fragment, LinkedObligationsDialogFragment$key>(
|
||||
obligationsFragment,
|
||||
query.organization as LinkedObligationsDialogFragment$key,
|
||||
);
|
||||
const { __ } = useTranslate();
|
||||
const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment<
|
||||
LinkedObligationsDialogQuery_fragment,
|
||||
LinkedObligationsDialogFragment$key
|
||||
>(
|
||||
obligationsFragment,
|
||||
query.organization as LinkedObligationsDialogFragment$key,
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
const [search, setSearch] = useState("");
|
||||
const obligations = useMemo(
|
||||
() => 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">
|
||||
<Input
|
||||
icon={IconMagnifyingGlass}
|
||||
placeholder={__("Search obligations...")}
|
||||
placeholder={t("linkedObligationsDialog.searchPlaceholder")}
|
||||
onValueChange={setSearch}
|
||||
/>
|
||||
</div>
|
||||
@@ -191,7 +193,7 @@ function ObligationRow(props: {
|
||||
onUnlink: (obligationId: string) => void;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
const isLinked = props.linkedObligations.has(props.obligation.id);
|
||||
|
||||
const onToggle = () => {
|
||||
@@ -208,11 +210,12 @@ function ObligationRow(props: {
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium text-txt-primary truncate">
|
||||
{props.obligation.area || __("No area specified")}
|
||||
{props.obligation.source || __("No source specified")}
|
||||
{props.obligation.area || t("linkedObligationsDialog.noArea")}
|
||||
{props.obligation.source || t("linkedObligationsDialog.noSource")}
|
||||
</div>
|
||||
<div className="text-xs text-txt-secondary">
|
||||
{props.obligation.owner?.fullName || __("Unassigned")}
|
||||
{props.obligation.owner?.fullName
|
||||
|| t("linkedObligationsDialog.unassigned")}
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant={getObligationStatusVariant(props.obligation.status)}>
|
||||
@@ -227,7 +230,9 @@ function ObligationRow(props: {
|
||||
disabled={props.disabled}
|
||||
className="ml-6"
|
||||
>
|
||||
{isLinked ? __("Unlink") : __("Link")}
|
||||
{isLinked
|
||||
? t("linkedObligationsDialog.actions.unlink")
|
||||
: t("linkedObligationsDialog.actions.link")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { sprintf } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
@@ -30,6 +28,7 @@ import {
|
||||
useDialogRef,
|
||||
} from "@probo/ui";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type DeleteOrganizationDialogProps = {
|
||||
children: React.ReactNode;
|
||||
@@ -44,7 +43,7 @@ export function DeleteOrganizationDialog({
|
||||
onConfirm,
|
||||
isDeleting = false,
|
||||
}: DeleteOrganizationDialogProps) {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const dialogRef = useDialogRef();
|
||||
const isConfirmDisabled = inputValue !== organizationName || isDeleting;
|
||||
@@ -61,25 +60,21 @@ export function DeleteOrganizationDialog({
|
||||
className="max-w-lg"
|
||||
ref={dialogRef}
|
||||
trigger={children}
|
||||
title={__("Delete Organization")}
|
||||
title={t("deleteOrganizationDialog.title")}
|
||||
>
|
||||
<DialogContent padded className="space-y-4">
|
||||
<p className="text-txt-secondary text-sm">
|
||||
{sprintf(
|
||||
__("This will permanently delete the organization %s and all its data."),
|
||||
organizationName,
|
||||
)}
|
||||
{t("deleteOrganizationDialog.description", { organizationName })}
|
||||
</p>
|
||||
|
||||
<p className="text-red-600 text-sm font-medium">
|
||||
{__("This action cannot be undone.")}
|
||||
{t("deleteOrganizationDialog.warning")}
|
||||
</p>
|
||||
|
||||
<Field
|
||||
label={sprintf(
|
||||
__("To confirm deletion, type \"%s\" below:"),
|
||||
label={t("deleteOrganizationDialog.confirmationLabel", {
|
||||
organizationName,
|
||||
)}
|
||||
})}
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={e => setInputValue(e.target.value)}
|
||||
@@ -96,7 +91,9 @@ export function DeleteOrganizationDialog({
|
||||
onClick={handleConfirm}
|
||||
disabled={isConfirmDisabled}
|
||||
>
|
||||
{isDeleting ? __("Deleting...") : __("Delete Organization")}
|
||||
{isDeleting
|
||||
? t("deleteOrganizationDialog.actions.deleting")
|
||||
: t("deleteOrganizationDialog.actions.delete")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Button,
|
||||
IconTrashCan,
|
||||
@@ -31,6 +30,7 @@ import {
|
||||
Tr,
|
||||
TrButton,
|
||||
} from "@probo/ui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFragment } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
@@ -77,7 +77,7 @@ type Props<Params> = {
|
||||
* Reusable component that displays a list of linked risks
|
||||
*/
|
||||
export function LinkedRisksCard<Params>(props: Props<Params>) {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onAttach = (riskId: string) => {
|
||||
props.onAttach({
|
||||
@@ -108,9 +108,9 @@ export function LinkedRisksCard<Params>(props: Props<Params>) {
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{__("Name")}</Th>
|
||||
<Th>{__("Initial Risk")}</Th>
|
||||
<Th>{__("Residual Risk")}</Th>
|
||||
<Th>{t("linkedRisksCard.columns.name")}</Th>
|
||||
<Th>{t("linkedRisksCard.columns.initialRisk")}</Th>
|
||||
<Th>{t("linkedRisksCard.columns.residualRisk")}</Th>
|
||||
{!props.readOnly && <Th></Th>}
|
||||
</Tr>
|
||||
</Thead>
|
||||
@@ -121,7 +121,7 @@ export function LinkedRisksCard<Params>(props: Props<Params>) {
|
||||
colSpan={props.readOnly ? 3 : 4}
|
||||
className="text-center text-txt-secondary"
|
||||
>
|
||||
{__("No risks linked")}
|
||||
{t("linkedRisksCard.empty")}
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
@@ -141,7 +141,9 @@ export function LinkedRisksCard<Params>(props: Props<Params>) {
|
||||
onLink={onAttach}
|
||||
onUnlink={onDetach}
|
||||
>
|
||||
<TrButton colspan={4}>{__("Link risk")}</TrButton>
|
||||
<TrButton colspan={4}>
|
||||
{t("linkedRisksCard.actions.link")}
|
||||
</TrButton>
|
||||
</LinkedRisksDialog>
|
||||
)}
|
||||
</Tbody>
|
||||
@@ -157,7 +159,7 @@ function RiskRow(props: {
|
||||
}) {
|
||||
const risk = useFragment(linkedRiskFragment, props.risk);
|
||||
const organizationId = useOrganizationId();
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Tr to={`/organizations/${organizationId}/risks/${risk.id}`}>
|
||||
@@ -175,7 +177,7 @@ function RiskRow(props: {
|
||||
onClick={() => props.onClick(risk.id)}
|
||||
icon={IconTrashCan}
|
||||
>
|
||||
{__("Unlink")}
|
||||
{t("linkedRisksCard.actions.unlink")}
|
||||
</Button>
|
||||
</Td>
|
||||
)}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
@@ -34,6 +33,7 @@ import {
|
||||
Spinner,
|
||||
} from "@probo/ui";
|
||||
import { type ReactNode, Suspense, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLazyLoadQuery } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
@@ -70,16 +70,16 @@ type Props = {
|
||||
};
|
||||
|
||||
export function LinkedRisksDialog({ children, ...props }: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Dialog trigger={children} title={__("Link risks")}>
|
||||
<Dialog trigger={children} title={t("linkedRisksDialog.title")}>
|
||||
<DialogContent>
|
||||
<Suspense fallback={<Spinner centered />}>
|
||||
<LinkedRisksDialogContent {...props} />
|
||||
</Suspense>
|
||||
</DialogContent>
|
||||
<DialogFooter exitLabel={__("Close")} />
|
||||
<DialogFooter exitLabel={t("linkedRisksDialog.actions.close")} />
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -89,7 +89,7 @@ function LinkedRisksDialogContent(props: Omit<Props, "children">) {
|
||||
const data = useLazyLoadQuery<LinkedRisksDialogQuery>(risksQuery, {
|
||||
organizationId,
|
||||
});
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
const [search, setSearch] = useState("");
|
||||
const [category, setCategory] = useState<string | null>(null);
|
||||
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">
|
||||
<Input
|
||||
icon={IconMagnifyingGlass}
|
||||
placeholder={__("Search risks...")}
|
||||
placeholder={t("linkedRisksDialog.searchPlaceholder")}
|
||||
onValueChange={setSearch}
|
||||
/>
|
||||
<Select
|
||||
value={category ?? ""}
|
||||
placeholder={__("All categories")}
|
||||
placeholder={t("linkedRisksDialog.allCategories")}
|
||||
onValueChange={setCategory}
|
||||
className="max-w-[180px]"
|
||||
>
|
||||
@@ -165,7 +165,7 @@ type RowProps = {
|
||||
};
|
||||
|
||||
function RiskRow(props: RowProps) {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isLinked = props.linkedRisks.has(props.risk.id);
|
||||
const onClick = isLinked ? props.onUnlink : props.onLink;
|
||||
@@ -187,7 +187,9 @@ function RiskRow(props: RowProps) {
|
||||
<span>
|
||||
<IconComponent size={16} />
|
||||
{" "}
|
||||
{isLinked ? __("Unlink") : __("Link")}
|
||||
{isLinked
|
||||
? t("linkedRisksDialog.actions.unlink")
|
||||
: t("linkedRisksDialog.actions.link")}
|
||||
</span>
|
||||
</Button>
|
||||
</button>
|
||||
|
||||
@@ -18,21 +18,21 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { ActionDropdown, Button, IconPencil, Skeleton } from "@probo/ui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
/**
|
||||
* Skeleton state for the framework control panel
|
||||
*/
|
||||
export function ControlSkeleton() {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between">
|
||||
<Skeleton style={{ width: 72, height: 34 }} className="mb-3" />
|
||||
<div className="flex gap-2">
|
||||
<Button icon={IconPencil} variant="secondary" disabled>
|
||||
{__("Edit control")}
|
||||
{t("controlSkeleton.actions.edit")}
|
||||
</Button>
|
||||
<ActionDropdown variant="secondary" />
|
||||
</div>
|
||||
|
||||
@@ -18,16 +18,16 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Button, IconPlusLarge, PageHeader, Skeleton } from "@probo/ui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function RisksPageSkeleton() {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader title={__("Risks")}>
|
||||
<PageHeader title={t("risksPageSkeleton.title")}>
|
||||
<Button icon={IconPlusLarge} disabled>
|
||||
{__("New Risk")}
|
||||
{t("risksPageSkeleton.actions.create")}
|
||||
</Button>
|
||||
</PageHeader>
|
||||
<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
|
||||
// SOFTWARE.
|
||||
|
||||
import { sprintf } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
@@ -37,6 +35,7 @@ import {
|
||||
} from "@probo/ui";
|
||||
import { clsx } from "clsx";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFragment } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
@@ -46,18 +45,18 @@ import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||
import { LinkedStatementsOfApplicabilityDialog } from "./LinkedStatementsOfApplicabilityDialog";
|
||||
|
||||
const linkedStatementOfApplicabilityFragment = graphql`
|
||||
fragment LinkedStatementsOfApplicabilityCardFragment on ApplicabilityStatement {
|
||||
id
|
||||
statementOfApplicability {
|
||||
id
|
||||
name
|
||||
}
|
||||
control {
|
||||
id
|
||||
}
|
||||
applicability
|
||||
justification
|
||||
fragment LinkedStatementsOfApplicabilityCardFragment on ApplicabilityStatement {
|
||||
id
|
||||
statementOfApplicability {
|
||||
id
|
||||
name
|
||||
}
|
||||
control {
|
||||
id
|
||||
}
|
||||
applicability
|
||||
justification
|
||||
}
|
||||
`;
|
||||
|
||||
type AttachMutation<Params> = (p: {
|
||||
@@ -94,8 +93,10 @@ type Props<Params> = {
|
||||
readOnly?: boolean;
|
||||
};
|
||||
|
||||
export function LinkedStatementsOfApplicabilityCard<Params>(props: Props<Params>) {
|
||||
const { __ } = useTranslate();
|
||||
export function LinkedStatementsOfApplicabilityCard<Params>(
|
||||
props: Props<Params>,
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [limit, setLimit] = useState<number | null>(
|
||||
props.variant === "card" ? 4 : null,
|
||||
@@ -171,7 +172,7 @@ export function LinkedStatementsOfApplicabilityCard<Params>(props: Props<Params>
|
||||
{variant === "card" && (
|
||||
<div className="flex justify-between">
|
||||
<div className="text-lg font-semibold">
|
||||
{__("Statements of Applicability")}
|
||||
{t("linkedStatementsOfApplicabilityCard.title")}
|
||||
</div>
|
||||
{!props.readOnly && (
|
||||
<LinkedStatementsOfApplicabilityDialog
|
||||
@@ -182,7 +183,7 @@ export function LinkedStatementsOfApplicabilityCard<Params>(props: Props<Params>
|
||||
onUnlink={onDetach}
|
||||
>
|
||||
<Button variant="tertiary" icon={IconPlusLarge}>
|
||||
{__("Link statement of applicability")}
|
||||
{t("linkedStatementsOfApplicabilityCard.actions.link")}
|
||||
</Button>
|
||||
</LinkedStatementsOfApplicabilityDialog>
|
||||
)}
|
||||
@@ -191,9 +192,13 @@ export function LinkedStatementsOfApplicabilityCard<Params>(props: Props<Params>
|
||||
<Table className={clsx(variant === "card" && "bg-invert")}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{__("Name")}</Th>
|
||||
<Th>{__("Applicability")}</Th>
|
||||
<Th>{__("Justification")}</Th>
|
||||
<Th>{t("linkedStatementsOfApplicabilityCard.columns.name")}</Th>
|
||||
<Th>
|
||||
{t("linkedStatementsOfApplicabilityCard.columns.applicability")}
|
||||
</Th>
|
||||
<Th>
|
||||
{t("linkedStatementsOfApplicabilityCard.columns.justification")}
|
||||
</Th>
|
||||
{!props.readOnly && <Th></Th>}
|
||||
</Tr>
|
||||
</Thead>
|
||||
@@ -204,7 +209,7 @@ export function LinkedStatementsOfApplicabilityCard<Params>(props: Props<Params>
|
||||
colSpan={props.readOnly ? 3 : 4}
|
||||
className="text-center text-txt-secondary"
|
||||
>
|
||||
{__("No statements of applicability linked")}
|
||||
{t("linkedStatementsOfApplicabilityCard.empty")}
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
@@ -225,7 +230,7 @@ export function LinkedStatementsOfApplicabilityCard<Params>(props: Props<Params>
|
||||
onUnlink={onDetach}
|
||||
>
|
||||
<TrButton colspan={4} icon={IconPlusLarge}>
|
||||
{__("Link statement of applicability")}
|
||||
{t("linkedStatementsOfApplicabilityCard.actions.link")}
|
||||
</TrButton>
|
||||
</LinkedStatementsOfApplicabilityDialog>
|
||||
)}
|
||||
@@ -237,10 +242,9 @@ export function LinkedStatementsOfApplicabilityCard<Params>(props: Props<Params>
|
||||
icon={IconChevronDown}
|
||||
onClick={() => setLimit(null)}
|
||||
>
|
||||
{sprintf(
|
||||
__("Show %d more"),
|
||||
props.statementsOfApplicability.length - limit,
|
||||
)}
|
||||
{t("linkedStatementsOfApplicabilityCard.actions.showMore", {
|
||||
count: props.statementsOfApplicability.length - limit,
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</Wrapper>
|
||||
@@ -256,10 +260,7 @@ function LinkedInfoExtractor(props: {
|
||||
}) {
|
||||
const { onExtracted, fragment } = props;
|
||||
|
||||
const data = useFragment(
|
||||
linkedStatementOfApplicabilityFragment,
|
||||
fragment,
|
||||
);
|
||||
const data = useFragment(linkedStatementOfApplicabilityFragment, fragment);
|
||||
|
||||
useEffect(() => {
|
||||
onExtracted({
|
||||
@@ -283,7 +284,7 @@ function StatementOfApplicabilityRow(props: {
|
||||
props.statementOfApplicability,
|
||||
);
|
||||
const organizationId = useOrganizationId();
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Tr
|
||||
@@ -293,8 +294,8 @@ function StatementOfApplicabilityRow(props: {
|
||||
<Td>
|
||||
<Badge variant={soa.applicability ? "success" : "danger"}>
|
||||
{soa.applicability
|
||||
? __("Applicable")
|
||||
: __("Not Applicable")}
|
||||
? t("linkedStatementsOfApplicabilityCard.applicable")
|
||||
: t("linkedStatementsOfApplicabilityCard.notApplicable")}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td>{soa.justification || "-"}</Td>
|
||||
@@ -303,13 +304,10 @@ function StatementOfApplicabilityRow(props: {
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
props.onClick(
|
||||
soa.statementOfApplicability.id,
|
||||
soa.control.id,
|
||||
)}
|
||||
props.onClick(soa.statementOfApplicability.id, soa.control.id)}
|
||||
icon={IconTrashCan}
|
||||
>
|
||||
{__("Unlink")}
|
||||
{t("linkedStatementsOfApplicabilityCard.actions.unlink")}
|
||||
</Button>
|
||||
</Td>
|
||||
)}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
@@ -30,6 +29,7 @@ import {
|
||||
} from "@probo/ui";
|
||||
import type { ReactNode } from "react";
|
||||
import { Suspense, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLazyLoadQuery } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
@@ -75,13 +75,14 @@ export function LinkedStatementsOfApplicabilityDialog({
|
||||
children,
|
||||
...props
|
||||
}: Props) {
|
||||
const { t } = useTranslation();
|
||||
const dialogRef = useRef<{ open: () => void; close: () => void }>(null);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
ref={dialogRef}
|
||||
trigger={children}
|
||||
title="Link Statement of Applicability"
|
||||
title={t("linkedStatementsOfApplicabilityDialog.title")}
|
||||
>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<LinkedStatementsOfApplicabilityDialogContent
|
||||
@@ -96,7 +97,7 @@ export function LinkedStatementsOfApplicabilityDialog({
|
||||
function LinkedStatementsOfApplicabilityDialogContent(
|
||||
props: Omit<Props, "children"> & { onClose: () => void },
|
||||
) {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
const organizationId = useOrganizationId();
|
||||
const [selectedSOA, setSelectedSOA] = useState<{
|
||||
id: string;
|
||||
@@ -163,12 +164,10 @@ function LinkedStatementsOfApplicabilityDialogContent(
|
||||
? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<div className="text-txt-secondary text-base mb-2">
|
||||
{__("No statements of applicability available")}
|
||||
{t("linkedStatementsOfApplicabilityDialog.empty.title")}
|
||||
</div>
|
||||
<div className="text-txt-tertiary text-sm">
|
||||
{__(
|
||||
"Create a statement of applicability first to link it to this control",
|
||||
)}
|
||||
{t("linkedStatementsOfApplicabilityDialog.empty.description")}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -176,7 +175,7 @@ function LinkedStatementsOfApplicabilityDialogContent(
|
||||
? (
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium mb-2">
|
||||
{__("Select a statement of applicability:")}
|
||||
{t("linkedStatementsOfApplicabilityDialog.select")}
|
||||
</div>
|
||||
{statementsOfApplicability.map((soa) => {
|
||||
const isLinked = linkedSOAIds.has(soa.id);
|
||||
@@ -197,7 +196,7 @@ function LinkedStatementsOfApplicabilityDialogContent(
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<Badge variant="success">
|
||||
{__("Linked")}
|
||||
{t("linkedStatementsOfApplicabilityDialog.linked")}
|
||||
</Badge>
|
||||
<Button
|
||||
variant="danger"
|
||||
@@ -205,7 +204,7 @@ function LinkedStatementsOfApplicabilityDialogContent(
|
||||
handleUnlink(soa.id)}
|
||||
disabled={props.disabled}
|
||||
>
|
||||
{__("Unlink")}
|
||||
{t("linkedStatementsOfApplicabilityDialog.actions.unlink")}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
@@ -220,7 +219,7 @@ function LinkedStatementsOfApplicabilityDialogContent(
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm text-txt-secondary mb-1">
|
||||
{__("Selected:")}
|
||||
{t("linkedStatementsOfApplicabilityDialog.selected")}
|
||||
</div>
|
||||
<div className="text-lg font-medium">
|
||||
{selectedSOA.name}
|
||||
@@ -230,7 +229,7 @@ function LinkedStatementsOfApplicabilityDialogContent(
|
||||
variant="tertiary"
|
||||
onClick={() => setSelectedSOA(null)}
|
||||
>
|
||||
{__("Change")}
|
||||
{t("linkedStatementsOfApplicabilityDialog.actions.change")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -242,16 +241,16 @@ function LinkedStatementsOfApplicabilityDialogContent(
|
||||
setApplicability(checked)}
|
||||
/>
|
||||
<span className="font-medium">
|
||||
{__("Applicable")}
|
||||
{t("linkedStatementsOfApplicabilityDialog.applicable")}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1 block">
|
||||
{__("Justification (optional)")}
|
||||
{t("linkedStatementsOfApplicabilityDialog.justification.label")}
|
||||
</label>
|
||||
<Textarea
|
||||
placeholder={__("Add a justification...")}
|
||||
placeholder={t("linkedStatementsOfApplicabilityDialog.justification.placeholder")}
|
||||
value={justification}
|
||||
onChange={e =>
|
||||
setJustification(e.target.value)}
|
||||
@@ -262,7 +261,7 @@ function LinkedStatementsOfApplicabilityDialogContent(
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogFooter exitLabel={__("Close")}>
|
||||
<DialogFooter exitLabel={t("linkedStatementsOfApplicabilityDialog.actions.close")}>
|
||||
{selectedSOA
|
||||
? (
|
||||
<>
|
||||
@@ -270,14 +269,14 @@ function LinkedStatementsOfApplicabilityDialogContent(
|
||||
variant="secondary"
|
||||
onClick={() => setSelectedSOA(null)}
|
||||
>
|
||||
{__("Back")}
|
||||
{t("linkedStatementsOfApplicabilityDialog.actions.back")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleLink}
|
||||
disabled={props.disabled}
|
||||
>
|
||||
{__("Link")}
|
||||
{t("linkedStatementsOfApplicabilityDialog.actions.link")}
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -19,13 +19,13 @@
|
||||
// SOFTWARE.
|
||||
|
||||
import { useStateWithRef } from "@probo/hooks";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { EditableCell, selectCell, SelectValue, Spinner } from "@probo/ui";
|
||||
import { useEditableCellRef } from "@probo/ui/src/Molecules/Table/EditableCell";
|
||||
import { useEditableRowContext } from "@probo/ui/src/Molecules/Table/EditableRow";
|
||||
import { getKey } from "@probo/ui/src/Molecules/Table/utils";
|
||||
import { Command } from "cmdk";
|
||||
import { type ReactNode, Suspense } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLazyLoadQuery } from "react-relay";
|
||||
import type {
|
||||
GraphQLTaggedNode,
|
||||
@@ -44,14 +44,23 @@ type Props<Q extends OperationType, T> = {
|
||||
| { 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>(
|
||||
props.defaultValue,
|
||||
);
|
||||
const cellRef = useEditableCellRef();
|
||||
const { __ } = useTranslate();
|
||||
const filteredValue = Array.isArray(value) ? value.filter(Boolean) : value ? [value] : [];
|
||||
const usedKeys = new Set<string>(filteredValue.map(getKey).filter(Boolean) as string[]);
|
||||
const { t } = useTranslation();
|
||||
const filteredValue = Array.isArray(value)
|
||||
? value.filter(Boolean)
|
||||
: value
|
||||
? [value]
|
||||
: [];
|
||||
const usedKeys = new Set<string>(
|
||||
filteredValue.map(getKey).filter(Boolean) as string[],
|
||||
);
|
||||
const { onUpdate } = useEditableRowContext();
|
||||
|
||||
const onSelect = (item: T) => {
|
||||
@@ -104,7 +113,7 @@ export function GraphQLCell<Q extends OperationType, T extends NonNullable<unkno
|
||||
{props.multiple && (
|
||||
<Command.Input
|
||||
className={classNames.input()}
|
||||
placeholder={__("Search")}
|
||||
placeholder={t("graphQLCell.searchPlaceholder")}
|
||||
/>
|
||||
)}
|
||||
<Command.List>
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Button,
|
||||
CellHead,
|
||||
@@ -35,6 +34,7 @@ import {
|
||||
useContext,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { LoadMoreFn } from "react-relay";
|
||||
import type { OperationType } from "relay-runtime";
|
||||
|
||||
@@ -75,7 +75,7 @@ export function SortableDataTable({
|
||||
isLoadingNext?: boolean;
|
||||
pageSize?: number;
|
||||
}) {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
const [order, setOrder] = useState(defaultOrder);
|
||||
const onOrderChange = (o: Order) => {
|
||||
startTransition(() => {
|
||||
@@ -95,7 +95,7 @@ export function SortableDataTable({
|
||||
disabled={isLoadingNext}
|
||||
icon={isLoadingNext ? Spinner : IconChevronDown}
|
||||
>
|
||||
{__("Show more")}
|
||||
{t("sortableDataTable.actions.showMore")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
// SOFTWARE.
|
||||
|
||||
import { formatDatetime } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
@@ -40,6 +39,7 @@ import {
|
||||
import { Breadcrumb } from "@probo/ui";
|
||||
import { type ReactNode, useEffect } from "react";
|
||||
import { Controller } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFragment, useRelayEnvironment } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
import { z } from "zod";
|
||||
@@ -143,7 +143,7 @@ type Props = {
|
||||
|
||||
export default function TaskFormDialog(props: Props) {
|
||||
const { children, connection, ref, task: taskKey, measureId, onCompleted } = props;
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
const newRef = useDialogRef();
|
||||
const dialogRef = ref ?? newRef;
|
||||
const organizationId = useOrganizationId();
|
||||
@@ -152,8 +152,8 @@ export default function TaskFormDialog(props: Props) {
|
||||
const [mutate] = useMutationWithToasts(
|
||||
task ? taskUpdateMutation : taskCreateMutation,
|
||||
{
|
||||
successMessage: __(`Task ${task ? "updated" : "created"} successfully.`),
|
||||
errorMessage: __(`Failed to ${task ? "update" : "create"} task`),
|
||||
successMessage: t(task ? "taskFormDialog.messages.updated" : "taskFormDialog.messages.created"),
|
||||
errorMessage: t(task ? "taskFormDialog.errors.update" : "taskFormDialog.errors.create"),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -246,7 +246,7 @@ export default function TaskFormDialog(props: Props) {
|
||||
trigger={children}
|
||||
title={(
|
||||
<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"
|
||||
required
|
||||
variant="title"
|
||||
placeholder={__("Task title")}
|
||||
placeholder={t("taskFormDialog.fields.title.placeholder")}
|
||||
{...register("name")}
|
||||
/>
|
||||
<Textarea
|
||||
id="content"
|
||||
variant="ghost"
|
||||
autogrow
|
||||
placeholder={__("Add description")}
|
||||
placeholder={t("taskFormDialog.fields.description.placeholder")}
|
||||
{...register("description")}
|
||||
/>
|
||||
</div>
|
||||
{/* Properties form */}
|
||||
<div className="py-5 px-6 bg-subtle">
|
||||
<Label>{__("Properties")}</Label>
|
||||
<Label>{t("taskFormDialog.properties")}</Label>
|
||||
{isUpdating && (
|
||||
<PropertyRow
|
||||
label={__("State")}
|
||||
label={t("taskFormDialog.fields.state.label")}
|
||||
error={"state" in formState.errors ? formState.errors.state?.message : undefined}
|
||||
>
|
||||
<Controller
|
||||
@@ -287,19 +287,19 @@ export default function TaskFormDialog(props: Props) {
|
||||
<Option value="TODO">
|
||||
<span className="flex items-center gap-2">
|
||||
<TaskStateIcon state="TODO" />
|
||||
{__("To do")}
|
||||
{t("taskFormDialog.states.todo")}
|
||||
</span>
|
||||
</Option>
|
||||
<Option value="IN_PROGRESS">
|
||||
<span className="flex items-center gap-2">
|
||||
<TaskStateIcon state="IN_PROGRESS" />
|
||||
{__("In progress")}
|
||||
{t("taskFormDialog.states.inProgress")}
|
||||
</span>
|
||||
</Option>
|
||||
<Option value="DONE">
|
||||
<span className="flex items-center gap-2">
|
||||
<TaskStateIcon state="DONE" />
|
||||
{__("Done")}
|
||||
{t("taskFormDialog.states.done")}
|
||||
</span>
|
||||
</Option>
|
||||
</Select>
|
||||
@@ -308,7 +308,7 @@ export default function TaskFormDialog(props: Props) {
|
||||
</PropertyRow>
|
||||
)}
|
||||
<PropertyRow
|
||||
label={__("Priority")}
|
||||
label={t("taskFormDialog.fields.priority.label")}
|
||||
error={formState.errors.priority?.message}
|
||||
>
|
||||
<Controller
|
||||
@@ -322,25 +322,25 @@ export default function TaskFormDialog(props: Props) {
|
||||
<Option value="URGENT">
|
||||
<span className="flex items-center gap-2">
|
||||
<PriorityLevel level="URGENT" />
|
||||
{__("Urgent")}
|
||||
{t("taskFormDialog.priorities.urgent")}
|
||||
</span>
|
||||
</Option>
|
||||
<Option value="HIGH">
|
||||
<span className="flex items-center gap-2">
|
||||
<PriorityLevel level="HIGH" />
|
||||
{__("High")}
|
||||
{t("taskFormDialog.priorities.high")}
|
||||
</span>
|
||||
</Option>
|
||||
<Option value="MEDIUM">
|
||||
<span className="flex items-center gap-2">
|
||||
<PriorityLevel level="MEDIUM" />
|
||||
{__("Medium")}
|
||||
{t("taskFormDialog.priorities.medium")}
|
||||
</span>
|
||||
</Option>
|
||||
<Option value="LOW">
|
||||
<span className="flex items-center gap-2">
|
||||
<PriorityLevel level="LOW" />
|
||||
{__("Low")}
|
||||
{t("taskFormDialog.priorities.low")}
|
||||
</span>
|
||||
</Option>
|
||||
</Select>
|
||||
@@ -348,7 +348,7 @@ export default function TaskFormDialog(props: Props) {
|
||||
/>
|
||||
</PropertyRow>
|
||||
<PropertyRow
|
||||
label={__("Assigned to")}
|
||||
label={t("taskFormDialog.fields.assignedTo.label")}
|
||||
error={formState.errors.assignedToId?.message}
|
||||
>
|
||||
<PeopleSelectField
|
||||
@@ -360,7 +360,7 @@ export default function TaskFormDialog(props: Props) {
|
||||
</PropertyRow>
|
||||
{showMeasure && (
|
||||
<PropertyRow
|
||||
label={__("Measure")}
|
||||
label={t("taskFormDialog.fields.measure.label")}
|
||||
error={formState.errors.measureId?.message}
|
||||
>
|
||||
<MeasureSelectField
|
||||
@@ -372,7 +372,7 @@ export default function TaskFormDialog(props: Props) {
|
||||
</PropertyRow>
|
||||
)}
|
||||
<PropertyRow
|
||||
label={__("Time estimate")}
|
||||
label={t("taskFormDialog.fields.timeEstimate.label")}
|
||||
error={formState.errors.timeEstimate?.message}
|
||||
>
|
||||
<Controller
|
||||
@@ -388,7 +388,7 @@ export default function TaskFormDialog(props: Props) {
|
||||
/>
|
||||
</PropertyRow>
|
||||
<PropertyRow
|
||||
label={__("Deadline")}
|
||||
label={t("taskFormDialog.fields.deadline.label")}
|
||||
error={formState.errors.deadline?.message}
|
||||
>
|
||||
<Input id="deadline" type="date" {...register("deadline")} />
|
||||
@@ -397,7 +397,7 @@ export default function TaskFormDialog(props: Props) {
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<Button type="submit">
|
||||
{isUpdating ? __("Update task") : __("Create task")}
|
||||
{isUpdating ? t("taskFormDialog.actions.update") : t("taskFormDialog.actions.create")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { formatDate, formatDuration, formatError, promisifyMutation } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { formatError, promisifyMutation } from "@probo/helpers";
|
||||
import { dateFormat, formatDuration } from "@probo/i18n";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
useToast,
|
||||
} from "@probo/ui";
|
||||
import { Fragment, type ReactNode, useRef, useState, useTransition } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
graphql,
|
||||
readInlineData,
|
||||
@@ -185,7 +186,7 @@ const updateRankMutation = graphql`
|
||||
`;
|
||||
|
||||
export function TasksCard({ tasks, connectionId, canReorder, refetch }: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const { t } = useTranslation();
|
||||
const hash = useLocation().hash.replace("#", "");
|
||||
const [, startTransition] = useTransition();
|
||||
|
||||
@@ -205,13 +206,13 @@ export function TasksCard({ tasks, connectionId, canReorder, refetch }: Props) {
|
||||
};
|
||||
|
||||
const stateHashes = [
|
||||
{ hash: "todo", label: __("To do"), state: "TODO" },
|
||||
{ hash: "in-progress", label: __("In progress"), state: "IN_PROGRESS" },
|
||||
{ hash: "done", label: __("Done"), state: "DONE" },
|
||||
{ hash: "todo", label: t("tasksCard.states.todo"), state: "TODO" },
|
||||
{ hash: "in-progress", label: t("tasksCard.states.inProgress"), state: "IN_PROGRESS" },
|
||||
{ hash: "done", label: t("tasksCard.states.done"), state: "DONE" },
|
||||
] as const;
|
||||
|
||||
const hashes = [
|
||||
{ hash: "", label: __("All"), state: null },
|
||||
{ hash: "", label: t("tasksCard.states.all"), state: null },
|
||||
...stateHashes,
|
||||
] as const;
|
||||
|
||||
@@ -346,8 +347,8 @@ export function TasksCard({ tasks, connectionId, canReorder, refetch }: Props) {
|
||||
onCompleted: (_, errors) => {
|
||||
if (errors?.length) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: formatError(__("Failed to reorder task."), errors),
|
||||
title: t("tasksCard.error.title"),
|
||||
description: formatError(t("tasksCard.error.reorder"), errors),
|
||||
variant: "error",
|
||||
});
|
||||
}
|
||||
@@ -365,7 +366,7 @@ export function TasksCard({ tasks, connectionId, canReorder, refetch }: Props) {
|
||||
onError: () => {
|
||||
droppedRef.current = false;
|
||||
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">
|
||||
{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>
|
||||
@@ -463,8 +464,8 @@ export function TasksCard({ tasks, connectionId, canReorder, refetch }: Props) {
|
||||
{canDrag && filteredTasks.length > 1 && (
|
||||
<p className="text-sm text-txt-tertiary">
|
||||
{hash === ""
|
||||
? __("Drag and drop to reorder tasks or move them between states")
|
||||
: __("Drag and drop to reorder tasks")}
|
||||
? t("tasksCard.dragInstructions.all")
|
||||
: t("tasksCard.dragInstructions.state")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -521,7 +522,7 @@ const deleteMutation = graphql`
|
||||
function TaskRow(props: TaskRowProps) {
|
||||
const organizationId = useOrganizationId();
|
||||
const dialogRef = useDialogRef();
|
||||
const { __ } = useTranslate();
|
||||
const { t, i18n } = useTranslation();
|
||||
const confirm = useConfirm();
|
||||
const [deleteTask] = useMutation<TasksCardDeleteMutation>(deleteMutation);
|
||||
const params = useParams<{ measureId?: string }>();
|
||||
@@ -542,8 +543,8 @@ function TaskRow(props: TaskRowProps) {
|
||||
icon: typeof IconCircleProgress;
|
||||
className: string;
|
||||
}> = {
|
||||
TODO: { state: "IN_PROGRESS", label: __("Move to In progress"), icon: IconCircleProgress, className: "text-txt-warning" },
|
||||
IN_PROGRESS: { state: "DONE", label: __("Move to Done"), icon: IconCircleCheck, className: "text-txt-accent" },
|
||||
TODO: { state: "IN_PROGRESS", label: t("tasksCard.actions.moveToInProgress"), icon: IconCircleProgress, className: "text-txt-warning" },
|
||||
IN_PROGRESS: { state: "DONE", label: t("tasksCard.actions.moveToDone"), icon: IconCircleCheck, className: "text-txt-accent" },
|
||||
};
|
||||
|
||||
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>
|
||||
)}
|
||||
{task.timeEstimate && (
|
||||
<span>{formatDuration(task.timeEstimate, __)}</span>
|
||||
<span>{formatDuration(task.timeEstimate, t)}</span>
|
||||
)}
|
||||
{task.deadline && (
|
||||
<time dateTime={task.deadline}>
|
||||
{formatDate(task.deadline)}
|
||||
{dateFormat(i18n.language, task.deadline)}
|
||||
</time>
|
||||
)}
|
||||
</div>
|
||||
@@ -677,7 +678,7 @@ function TaskRow(props: TaskRowProps) {
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={IconPencil}
|
||||
title={__("Edit")}
|
||||
title={t("tasksCard.actions.edit")}
|
||||
onClick={() => dialogRef.current?.open()}
|
||||
/>
|
||||
)}
|
||||
@@ -685,7 +686,7 @@ function TaskRow(props: TaskRowProps) {
|
||||
<Button
|
||||
variant="danger"
|
||||
icon={IconTrashCan}
|
||||
title={__("Delete")}
|
||||
title={t("tasksCard.actions.delete")}
|
||||
onClick={onDelete}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user