Fix apps/console lint issues
Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -10,7 +10,7 @@ export function DocumentClassificationOptions() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{documentClassifications.map((classification) => (
|
||||
{documentClassifications.map(classification => (
|
||||
<Option key={classification} value={classification}>
|
||||
{getDocumentClassificationLabel(__, classification)}
|
||||
</Option>
|
||||
|
||||
@@ -7,7 +7,7 @@ export function DocumentTypeOptions() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{documentTypes.map((type) => (
|
||||
{documentTypes.map(type => (
|
||||
<Option key={type} value={type}>
|
||||
{getDocumentTypeLabel(__, type)}
|
||||
</Option>
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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>(
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -7,7 +7,7 @@ export function SnapshotTypeOptions() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{snapshotTypes.map((type) => (
|
||||
{snapshotTypes.map(type => (
|
||||
<Option key={type} value={type}>
|
||||
{getSnapshotTypeLabel(__, type)}
|
||||
</Option>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user