Fix apps/console lint issues

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-01-22 12:51:08 +04:00
parent b4a73f2eea
commit dd23449cc0
244 changed files with 7685 additions and 8029 deletions

View File

@@ -74,10 +74,10 @@ function AuditSelectWithQuery<T extends FieldValues = FieldValues>(
{ organizationId },
{ fetchPolicy: "network-only" },
);
const audits =
data?.organization?.audits?.edges
?.map((edge) => edge.node)
.filter((node) => node !== null) ?? [];
const audits
= data?.organization?.audits?.edges
?.map(edge => edge.node)
.filter(node => node !== null) ?? [];
const NONE_VALUE = "__NONE__";
@@ -91,9 +91,8 @@ function AuditSelectWithQuery<T extends FieldValues = FieldValues>(
id={name}
variant="editor"
placeholder={__("Select an audit")}
onValueChange={(value) =>
field.onChange(value === NONE_VALUE ? "" : value)
}
onValueChange={value =>
field.onChange(value === NONE_VALUE ? "" : value)}
key={audits?.length.toString() ?? "0"}
{...field}
className="w-full"
@@ -102,7 +101,7 @@ function AuditSelectWithQuery<T extends FieldValues = FieldValues>(
<Option value={NONE_VALUE}>
<span className="text-txt-tertiary">{__("None")}</span>
</Option>
{audits?.map((audit) => (
{audits?.map(audit => (
<Option key={audit.id} value={audit.id}>
<div className="flex items-center justify-between w-full">
<span>

View File

@@ -1,18 +1,25 @@
import type { ComponentProps } from "react";
import { Field } from "@probo/ui";
import { Controller, type FieldValues } from "react-hook-form";
import { Controller, type FieldPath, type FieldValues } from "react-hook-form";
import { Select } from "@probo/ui";
type Props<T extends typeof Field | typeof Select, TFieldValues extends FieldValues = FieldValues> =
ComponentProps<T> & Omit<ComponentProps<typeof Controller<TFieldValues>>, "render">;
type Props<
T extends typeof Field | typeof Select,
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>
= ComponentProps<T> & Omit<ComponentProps<typeof Controller<TFieldValues, TName>>, "render">;
export function ControlledField<TFieldValues extends FieldValues = FieldValues>({
export function ControlledField<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
control,
name,
...props
}: Props<typeof Field, TFieldValues>) {
}: Props<typeof Field, TFieldValues, TName>) {
return (
<Controller
<Controller<TFieldValues, TName>
control={control}
name={name}
render={({ field }) => (
@@ -21,7 +28,7 @@ export function ControlledField<TFieldValues extends FieldValues = FieldValues>(
{...props}
{...field}
// TODO : Find a better way to handle this case (comparing number and string for select create issues)
value={field.value ? field.value.toString() : ""}
value={field.value ? (field.value as readonly string[] | string | number).toString() : ""}
onValueChange={field.onChange}
/>
</>

View File

@@ -1,5 +1,5 @@
import { useTranslate } from "@probo/i18n";
import { useEffect, useRef, useState } from "react";
import { useEffect, useState } from "react";
import { Badge, Input, IconCrossLargeX } from "@probo/ui";
import { type Control, Controller, type FieldPath, type FieldValues } from "react-hook-form";
import { getCountryName, getCountryOptions, countries, type CountryCode } from "@probo/helpers";
@@ -35,16 +35,16 @@ type CountriesFieldInputProps = {
function CountriesFieldInput(props: CountriesFieldInputProps) {
const { __ } = useTranslate();
const animateBadge = useRef(false);
const [animateBadge, setAnimateBadge] = useState(false);
const addCountry = (code: string) => {
animateBadge.current = true;
setAnimateBadge(true);
props.onValueChange([...props.value, code]);
};
const removeCountry = (code: string) => {
animateBadge.current = true;
props.onValueChange(props.value.filter((v) => v !== code));
setAnimateBadge(true);
props.onValueChange(props.value.filter(v => v !== code));
};
return (
@@ -52,7 +52,7 @@ function CountriesFieldInput(props: CountriesFieldInputProps) {
{props.value.length > 0 && (
<div className="space-y-2">
<div className="flex flex-wrap gap-2">
{props.value.map((countryCode) => (
{props.value.map(countryCode => (
<Badge asChild size="md" key={countryCode}>
<button
onClick={() => removeCountry(countryCode)}
@@ -61,8 +61,8 @@ function CountriesFieldInput(props: CountriesFieldInputProps) {
className={clsx(
"hover:bg-subtle-hover cursor-pointer",
props.disabled && "opacity-50 cursor-not-allowed",
animateBadge.current &&
"starting:opacity-0 starting:w-0 w-max transition-all duration-500 starting:bg-accent"
animateBadge
&& "starting:opacity-0 starting:w-0 w-max transition-all duration-500 starting:bg-accent",
)}
>
{getCountryName(__, countryCode as CountryCode)}
@@ -78,7 +78,7 @@ function CountriesFieldInput(props: CountriesFieldInputProps) {
{!props.disabled && (
<CountryInput
availableCountries={countries.filter(
(c: CountryCode) => !props.value.includes(c)
(c: CountryCode) => !props.value.includes(c),
)}
onAdd={addCountry}
/>
@@ -108,7 +108,7 @@ function CountryInput({ availableCountries, onAdd }: CountryInputProps) {
const filteredCountries = countryOptions
.filter((option: { value: string; label: string }) => availableCountries.includes(option.value as CountryCode))
.filter((option: { value: string; label: string }) =>
option.label.toLowerCase().includes(search.toLowerCase())
option.label.toLowerCase().includes(search.toLowerCase()),
);
const handleCountryClick = (value: string) => {
@@ -123,7 +123,7 @@ function CountryInput({ availableCountries, onAdd }: CountryInputProps) {
<Input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
onChange={e => setSearch(e.target.value)}
onFocus={() => setIsOpen(true)}
placeholder={__("Search and add countries...")}
className="w-full pr-8"

View File

@@ -10,7 +10,7 @@ export function DocumentClassificationOptions() {
return (
<>
{documentClassifications.map((classification) => (
{documentClassifications.map(classification => (
<Option key={classification} value={classification}>
{getDocumentClassificationLabel(__, classification)}
</Option>

View File

@@ -7,7 +7,7 @@ export function DocumentTypeOptions() {
return (
<>
{documentTypes.map((type) => (
{documentTypes.map(type => (
<Option key={type} value={type}>
{getDocumentTypeLabel(__, type)}
</Option>

View File

@@ -12,7 +12,9 @@ type Props<TFieldValues extends FieldValues = FieldValues> = {
/**
* A field to handle multiple emails
*/
export function EmailsField<TFieldValues extends FieldValues = FieldValues>({ control, register }: Props<TFieldValues>) {
export function EmailsField<
TFieldValues extends FieldValues = FieldValues,
>({ control, register }: Props<TFieldValues>) {
const { __ } = useTranslate();
const { fields, append, remove } = useFieldArray({
name: "additionalEmailAddresses" as ArrayPath<TFieldValues>,

View File

@@ -4,7 +4,10 @@ import { useTranslate } from "@probo/i18n";
import { type Control, Controller, type FieldPath, type FieldValues } from "react-hook-form";
import { usePaginatedMeasures } from "/hooks/graph/usePaginatedMeasures";
type Props<TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>> = {
type Props<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
organizationId: string;
control: Control<TFieldValues>;
name: TName;
@@ -39,7 +42,7 @@ 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 { name, organizationId, control, disabled, optional } = props;
@@ -49,16 +52,16 @@ function MeasureSelectWithQuery<TFieldValues extends FieldValues = FieldValues>(
return (
data?.measures.edges
?.filter(
(edge) =>
edge.node.name.toLowerCase().includes(search.toLowerCase()) ||
edge.node.description?.toLowerCase().includes(search.toLowerCase())
edge =>
edge.node.name.toLowerCase().includes(search.toLowerCase())
|| edge.node.description?.toLowerCase().includes(search.toLowerCase()),
)
.map((edge) => edge.node) ?? []
.map(edge => edge.node) ?? []
);
}, [data?.measures.edges, search]);
const allMeasures = useMemo(() => {
return data?.measures.edges?.map((edge) => edge.node) ?? [];
return data?.measures.edges?.map(edge => edge.node) ?? [];
}, [data?.measures.edges]);
return (
@@ -67,7 +70,7 @@ 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
@@ -87,7 +90,7 @@ function MeasureSelectWithQuery<TFieldValues extends FieldValues = FieldValues>(
{__("None")}
</ComboboxItem>
)}
{measures?.map((m) => (
{measures?.map(m => (
<ComboboxItem
key={m.id}
onClick={() => {

View File

@@ -44,7 +44,7 @@ export function PeopleMultiSelectField<T extends FieldValues = FieldValues>({
}
function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
props: Pick<Props<T>, "organizationId" | "control" | "name" | "disabled" | "selectedPeople">
props: Pick<Props<T>, "organizationId" | "control" | "name" | "disabled" | "selectedPeople">,
) {
const { __ } = useTranslate();
const { name, organizationId, control, selectedPeople = [] } = props;
@@ -52,7 +52,7 @@ function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
const [isOpen, setIsOpen] = useState(false);
const allPeople = [...people];
selectedPeople.forEach(selectedPerson => {
selectedPeople.forEach((selectedPerson) => {
if (!allPeople.find(p => p.id === selectedPerson.id)) {
allPeople.push({
id: selectedPerson.id,
@@ -99,7 +99,7 @@ function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
open={isOpen}
onOpenChange={setIsOpen}
>
{availablePeople.map((person) => (
{availablePeople.map(person => (
<Option key={person.id} value={person.id} className="flex gap-2">
<Avatar
name={person.fullName}
@@ -120,7 +120,7 @@ function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
{selectedPeople.length > 0 && (
<div className="flex flex-wrap gap-2">
{selectedPeople.map((person) => (
{selectedPeople.map(person => (
<Badge key={person.id} variant="neutral" className="flex items-center gap-2">
<Avatar
name={person.fullName}
@@ -153,4 +153,3 @@ function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
</>
);
}

View File

@@ -4,7 +4,10 @@ import { useTranslate } from "@probo/i18n";
import { type Control, Controller, type FieldPath, type FieldValues } from "react-hook-form";
import { usePeople } from "/hooks/graph/PeopleGraph.ts";
type Props<TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>> = {
type Props<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
organizationId: string;
control: Control<TFieldValues>;
name: TName;
@@ -55,16 +58,15 @@ function PeopleSelectWithQuery<TFieldValues extends FieldValues = FieldValues>(
id={name}
variant="editor"
placeholder={__("Select an owner")}
onValueChange={(value) =>
field.onChange(value === "__NONE__" ? null : value)
}
onValueChange={value =>
field.onChange(value === "__NONE__" ? null : value)}
key={people?.length.toString() ?? "0"}
{...field}
className="w-full"
value={field.value ?? (props.optional ? "__NONE__" : "")}
>
{props.optional && <Option value="__NONE__">{__("None")}</Option>}
{people?.map((p) => (
{people?.map(p => (
<Option key={p.id} value={p.id} className="flex gap-2">
<Avatar name={p.fullName} />
{p.fullName}

View File

@@ -21,7 +21,7 @@ export function SpecialOrCriminalDataOptions() {
return (
<>
{options.map((option) => (
{options.map(option => (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
@@ -47,7 +47,7 @@ export function LawfulBasisOptions() {
return (
<>
{options.map((option) => (
{options.map(option => (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
@@ -113,7 +113,7 @@ export function TransferSafeguardsOptions() {
return (
<>
{options.map((option) => (
{options.map(option => (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
@@ -135,7 +135,7 @@ export function DataProtectionImpactAssessmentOptions() {
return (
<>
{options.map((option) => (
{options.map(option => (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
@@ -157,7 +157,7 @@ export function TransferImpactAssessmentOptions() {
return (
<>
{options.map((option) => (
{options.map(option => (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
@@ -179,7 +179,7 @@ export function RoleOptions() {
return (
<>
{options.map((option) => (
{options.map(option => (
<Option key={option.value} value={option.value}>
{option.label}
</Option>

View File

@@ -7,7 +7,7 @@ export function SnapshotTypeOptions() {
return (
<>
{snapshotTypes.map((type) => (
{snapshotTypes.map(type => (
<Option key={type} value={type}>
{getSnapshotTypeLabel(__, type)}
</Option>

View File

@@ -1,681 +0,0 @@
import { useTranslate } from "@probo/i18n";
import {
Field,
Select,
Option,
Checkbox,
Textarea,
Spinner,
Button,
IconChevronDown,
IconChevronUp,
IconTrashCan,
IconPlusLarge,
} from "@probo/ui";
import { Suspense, useState, useMemo, useEffect } from "react";
import {
Controller,
type Control,
type UseFormSetValue,
type FieldValues,
type Path,
type PathValue,
} from "react-hook-form";
import { useLazyLoadQuery } from "react-relay";
import { graphql } from "relay-runtime";
import { useOrganizationId } from "/hooks/useOrganizationId";
import type { StateOfApplicabilityControlsFieldFrameworksQuery } from "/__generated__/core/StateOfApplicabilityControlsFieldFrameworksQuery.graphql";
import type { StateOfApplicabilityControlsFieldFrameworkControlsQuery } from "/__generated__/core/StateOfApplicabilityControlsFieldFrameworkControlsQuery.graphql";
const frameworksQuery = graphql`
query StateOfApplicabilityControlsFieldFrameworksQuery(
$organizationId: ID!
) {
organization: node(id: $organizationId) {
... on Organization {
frameworks(first: 100) {
edges {
node {
id
name
}
}
}
}
}
}
`;
const frameworkControlsQuery = graphql`
query StateOfApplicabilityControlsFieldFrameworkControlsQuery(
$frameworkId: ID!
) {
framework: node(id: $frameworkId) {
... on Framework {
id
controls(
first: 500
orderBy: { field: SECTION_TITLE, direction: ASC }
) {
edges {
node {
id
sectionTitle
name
}
}
}
}
}
}
`;
type ControlSelection = {
controlId: string;
state: "EXCLUDED" | "IMPLEMENTED" | "NOT_IMPLEMENTED";
exclusionJustification?: string;
};
type FrameworkData = {
id: string;
name: string;
controls: Array<{
id: string;
sectionTitle: string;
name: string;
}>;
};
type Props<T extends FieldValues = FieldValues> = {
control: Control<T>;
setValue: UseFormSetValue<T>;
name: string;
initialControls?: ControlSelection[];
initialFrameworkIds?: Set<string>;
};
export function StateOfApplicabilityControlsField<
T extends FieldValues = FieldValues,
>({ control, setValue, name, initialControls, initialFrameworkIds }: Props<T>) {
const { __ } = useTranslate();
const organizationId = useOrganizationId();
// Initialize form value with initialControls
useEffect(() => {
if (initialControls && initialControls.length > 0) {
setValue(name as Path<T>, initialControls as PathValue<T, Path<T>>);
}
}, [initialControls, setValue, name]);
// Initialize framework selection from initialFrameworkIds
const [selectedFrameworkIds, setSelectedFrameworkIds] = useState<
Set<string>
>(initialFrameworkIds || new Set());
const [expandedFrameworks, setExpandedFrameworks] = useState<Set<string>>(
initialFrameworkIds || new Set(),
);
const [frameworkDataMap, setFrameworkDataMap] = useState<
Map<string, FrameworkData>
>(new Map());
const [newFrameworkId, setNewFrameworkId] = useState<string>("");
// Update framework selection when initialFrameworkIds changes
useEffect(() => {
if (initialFrameworkIds) {
setSelectedFrameworkIds(initialFrameworkIds);
setExpandedFrameworks(initialFrameworkIds);
}
}, [initialFrameworkIds]);
const addFramework = (frameworkId: string) => {
if (!frameworkId || selectedFrameworkIds.has(frameworkId)) return;
setSelectedFrameworkIds(
new Set([...selectedFrameworkIds, frameworkId]),
);
setExpandedFrameworks(new Set([...expandedFrameworks, frameworkId]));
setNewFrameworkId("");
};
const removeFramework = (frameworkId: string) => {
const newSet = new Set(selectedFrameworkIds);
newSet.delete(frameworkId);
setSelectedFrameworkIds(newSet);
const newExpanded = new Set(expandedFrameworks);
newExpanded.delete(frameworkId);
setExpandedFrameworks(newExpanded);
const newMap = new Map(frameworkDataMap);
newMap.delete(frameworkId);
setFrameworkDataMap(newMap);
};
const toggleFramework = (frameworkId: string) => {
const newExpanded = new Set(expandedFrameworks);
if (newExpanded.has(frameworkId)) {
newExpanded.delete(frameworkId);
} else {
newExpanded.add(frameworkId);
}
setExpandedFrameworks(newExpanded);
};
return (
<div className="space-y-4">
<div>
<h4 className="font-medium text-txt-primary mb-4">
{__("Select Controls")}
</h4>
<div className="space-y-3">
<Field label={__("Add Framework")}>
<Suspense
fallback={
<Select
variant="editor"
disabled
placeholder={__("Loading...")}
/>
}
>
<FrameworkSelect
organizationId={organizationId}
selectedFrameworkIds={selectedFrameworkIds}
value={newFrameworkId}
onValueChange={setNewFrameworkId}
onAdd={addFramework}
/>
</Suspense>
</Field>
{Array.from(selectedFrameworkIds).map((frameworkId) => (
<Suspense
key={frameworkId}
fallback={
<div className="flex items-center justify-center py-8">
<Spinner />
</div>
}
>
<FrameworkSection
frameworkId={frameworkId}
isExpanded={expandedFrameworks.has(frameworkId)}
onToggle={() => toggleFramework(frameworkId)}
onRemove={() => removeFramework(frameworkId)}
control={control}
name={name}
frameworkDataMap={frameworkDataMap}
setFrameworkDataMap={setFrameworkDataMap}
/>
</Suspense>
))}
</div>
</div>
</div>
);
}
function FrameworkSelect({
organizationId,
selectedFrameworkIds,
value,
onValueChange,
onAdd,
}: {
organizationId: string;
selectedFrameworkIds: Set<string>;
value: string;
onValueChange: (value: string) => void;
onAdd: (frameworkId: string) => void;
}) {
const { __ } = useTranslate();
const data =
useLazyLoadQuery<StateOfApplicabilityControlsFieldFrameworksQuery>(
frameworksQuery,
{ organizationId },
{ fetchPolicy: "network-only" },
);
const frameworks: Array<{ id: string; name: string }> =
(data?.organization &&
"frameworks" in data.organization &&
data.organization.frameworks?.edges
?.map((edge) => edge.node)
.filter(
(node): node is NonNullable<typeof node> => node !== null,
)) ||
[];
const availableFrameworks = frameworks.filter(
(framework: { id: string; name: string }) =>
!selectedFrameworkIds.has(framework.id),
);
return (
<div className="flex gap-2">
<Select
variant="editor"
placeholder={__("Select a framework")}
onValueChange={onValueChange}
value={value}
className="flex-1"
>
{availableFrameworks.map(
(framework: { id: string; name: string }) => (
<Option key={framework.id} value={framework.id}>
{framework.name}
</Option>
),
)}
</Select>
<Button
type="button"
variant="secondary"
icon={IconPlusLarge}
onClick={() => onAdd(value)}
disabled={!value || selectedFrameworkIds.has(value)}
>
{__("Add")}
</Button>
</div>
);
}
function FrameworkSection<T extends FieldValues = FieldValues>({
frameworkId,
isExpanded,
onToggle,
onRemove,
control,
name,
frameworkDataMap,
setFrameworkDataMap,
}: {
frameworkId: string;
isExpanded: boolean;
onToggle: () => void;
onRemove: () => void;
control: Control<T>;
name: string;
frameworkDataMap: Map<string, FrameworkData>;
setFrameworkDataMap: React.Dispatch<
React.SetStateAction<Map<string, FrameworkData>>
>;
}) {
const { __ } = useTranslate();
const data =
useLazyLoadQuery<StateOfApplicabilityControlsFieldFrameworkControlsQuery>(
frameworkControlsQuery,
{ frameworkId },
{ fetchPolicy: "network-only" },
);
const framework =
data?.framework && "controls" in data.framework ? data.framework : null;
const frameworkName: string =
framework && "name" in framework && typeof framework.name === "string"
? framework.name
: "";
const controls: Array<{ id: string; sectionTitle: string; name: string }> =
useMemo(
() =>
framework?.controls?.edges
?.map((edge) => edge.node)
.filter(
(node): node is NonNullable<typeof node> =>
node !== null,
) ?? [],
[framework],
);
useEffect(() => {
if (framework && !frameworkDataMap.has(frameworkId)) {
setFrameworkDataMap((prev) => {
const newMap = new Map(prev);
newMap.set(frameworkId, {
id: frameworkId,
name: frameworkName,
controls,
});
return newMap;
});
}
}, [
framework,
frameworkId,
frameworkName,
controls,
frameworkDataMap,
setFrameworkDataMap,
]);
const cachedData = frameworkDataMap.get(frameworkId);
const displayName: string = cachedData?.name || frameworkName;
const displayControls: Array<{
id: string;
sectionTitle: string;
name: string;
}> = cachedData?.controls || controls;
return (
<div className="border border-border-low rounded-lg">
<div className="flex items-center justify-between p-4">
<button
type="button"
onClick={onToggle}
className="flex items-center gap-2 flex-1 text-left hover:bg-subtle -m-4 p-4 rounded-lg"
>
{isExpanded ? (
<IconChevronUp
size={16}
className="text-txt-tertiary"
/>
) : (
<IconChevronDown
size={16}
className="text-txt-tertiary"
/>
)}
<span className="font-medium text-txt-primary">
{displayName}
</span>
<span className="text-sm text-txt-tertiary">
({displayControls.length} {__("controls")})
</span>
</button>
<Button
type="button"
variant="secondary"
icon={IconTrashCan}
onClick={onRemove}
className="ml-2"
>
{__("Remove")}
</Button>
</div>
{isExpanded && (
<div className="border-t border-border-low">
<Controller
control={control}
name={name as Path<T>}
render={({ field }) => {
const selectedControls: ControlSelection[] = (
Array.isArray(field.value) ? field.value : []
) as ControlSelection[];
const selectedControlIds = new Set(
selectedControls.map(
(c: ControlSelection) => c.controlId,
),
);
const toggleControl = (controlId: string) => {
const isSelected =
selectedControlIds.has(controlId);
if (isSelected) {
field.onChange(
selectedControls.filter(
(c: ControlSelection) =>
c.controlId !== controlId,
),
);
} else {
field.onChange([
...selectedControls,
{
controlId,
state: "IMPLEMENTED" as const,
exclusionJustification: undefined,
},
]);
}
};
const updateControlState = (
controlId: string,
state:
| "EXCLUDED"
| "IMPLEMENTED"
| "NOT_IMPLEMENTED",
) => {
field.onChange(
selectedControls.map(
(c: ControlSelection) =>
c.controlId === controlId
? { ...c, state }
: c,
),
);
};
const updateJustification = (
controlId: string,
exclusionJustification: string,
) => {
field.onChange(
selectedControls.map(
(c: ControlSelection) =>
c.controlId === controlId
? {
...c,
exclusionJustification,
}
: c,
),
);
};
const getControlState = (controlId: string) => {
const selected = selectedControls.find(
(c: ControlSelection) =>
c.controlId === controlId,
);
return selected?.state || "IMPLEMENTED";
};
const getExclusionJustification = (
controlId: string,
) => {
const selected = selectedControls.find(
(c: ControlSelection) =>
c.controlId === controlId,
);
return selected?.exclusionJustification || "";
};
const selectAll = () => {
const newSelectedControls: ControlSelection[] =
[...selectedControls];
displayControls.forEach((ctrl) => {
if (!selectedControlIds.has(ctrl.id)) {
newSelectedControls.push({
controlId: ctrl.id,
state: "IMPLEMENTED" as const,
exclusionJustification: undefined,
});
}
});
field.onChange(newSelectedControls);
};
const deselectAll = () => {
const controlIdsToRemove = new Set(
displayControls.map((ctrl) => ctrl.id),
);
const newSelectedControls =
selectedControls.filter(
(c: ControlSelection) =>
!controlIdsToRemove.has(
c.controlId,
),
);
field.onChange(newSelectedControls);
};
const allSelected =
displayControls.length > 0 &&
displayControls.every((ctrl) =>
selectedControlIds.has(ctrl.id),
);
return (
<div className="p-4 space-y-4">
{displayControls.length > 0 && (
<div className="flex justify-end">
<Button
type="button"
variant="quaternary"
onClick={
allSelected
? deselectAll
: selectAll
}
className="text-xs h-7 min-h-7"
>
{allSelected
? __("Deselect All")
: __("Select All")}
</Button>
</div>
)}
<div className="border border-border-low rounded-lg max-h-96 overflow-y-auto">
{displayControls.length === 0 ? (
<div className="p-4 text-center text-txt-tertiary">
{__(
"No controls found in this framework",
)}
</div>
) : (
<div className="divide-y divide-border-low">
{displayControls.map((ctrl) => {
const isSelected =
selectedControlIds.has(
ctrl.id,
);
const state =
getControlState(
ctrl.id,
);
const exclusionJustification =
getExclusionJustification(
ctrl.id,
);
return (
<div
key={ctrl.id}
className="p-4 space-y-3"
>
<div className="flex items-start gap-3">
<Checkbox
checked={
isSelected
}
onChange={() =>
toggleControl(
ctrl.id,
)
}
/>
<div className="flex-1 min-w-0">
<div className="font-medium">
{
ctrl.sectionTitle
}
:{" "}
{
ctrl.name
}
</div>
</div>
</div>
{isSelected && (
<div className="ml-7 space-y-2">
<Field
label={__(
"State",
)}
>
<Select
variant="editor"
value={
state
}
onValueChange={(
value,
) =>
updateControlState(
ctrl.id,
value as
| "EXCLUDED"
| "IMPLEMENTED"
| "NOT_IMPLEMENTED",
)
}
className="w-full"
>
<Option value="IMPLEMENTED">
{__(
"Implemented",
)}
</Option>
<Option value="NOT_IMPLEMENTED">
{__(
"Not Implemented",
)}
</Option>
<Option value="EXCLUDED">
{__(
"Excluded",
)}
</Option>
</Select>
</Field>
{state ===
"EXCLUDED" ||
(state ===
"NOT_IMPLEMENTED" && (
<Field
label={__(
"Justification",
)}
>
<Textarea
value={
exclusionJustification
}
onChange={(
e,
) =>
updateJustification(
ctrl.id,
e
.target
.value,
)
}
placeholder={__(
"Reason for exclusion",
)}
autogrow
/>
</Field>
))}
</div>
)}
</div>
);
})}
</div>
)}
</div>
</div>
);
}}
/>
</div>
)}
</div>
);
}

View File

@@ -45,7 +45,7 @@ export function VendorsMultiSelectField<T extends FieldValues = FieldValues>({
}
function VendorsMultiSelectWithQuery<T extends FieldValues = FieldValues>(
props: Pick<Props<T>, "organizationId" | "control" | "name" | "disabled" | "selectedVendors">
props: Pick<Props<T>, "organizationId" | "control" | "name" | "disabled" | "selectedVendors">,
) {
const { __ } = useTranslate();
const { name, organizationId, control, selectedVendors = [] } = props;
@@ -54,7 +54,7 @@ function VendorsMultiSelectWithQuery<T extends FieldValues = FieldValues>(
const allVendors = [...vendors];
if (props.disabled) {
selectedVendors.forEach(selectedVendor => {
selectedVendors.forEach((selectedVendor) => {
if (!allVendors.find(v => v.id === selectedVendor.id)) {
allVendors.push(selectedVendor);
}
@@ -98,7 +98,7 @@ function VendorsMultiSelectWithQuery<T extends FieldValues = FieldValues>(
open={isOpen}
onOpenChange={setIsOpen}
>
{availableVendors.map((vendor) => (
{availableVendors.map(vendor => (
<Option key={vendor.id} value={vendor.id} className="flex gap-2">
<Avatar
name={vendor.name}
@@ -120,7 +120,7 @@ function VendorsMultiSelectWithQuery<T extends FieldValues = FieldValues>(
{selectedVendors.length > 0 && (
<div className="flex flex-wrap gap-2">
{selectedVendors.map((vendor) => (
{selectedVendors.map(vendor => (
<Badge key={vendor.id} variant="neutral" className="flex items-center gap-2">
<Avatar
name={vendor.name}