Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2025-05-03 18:19:20 -07:00
parent 625343dada
commit 14e338b4c4
70 changed files with 3070 additions and 1228 deletions

View File

@@ -26,7 +26,7 @@ posthog.init(process.env.POSTHOG_KEY!, {
}); });
const OrganizationSelectionPage = lazy( const OrganizationSelectionPage = lazy(
() => import("./pages/OrganizationSelectionPage") () => import("./pages/OrganizationSelectionPage"),
); );
function App() { function App() {

View File

@@ -5,7 +5,7 @@ import { OrganizationSwitcherSkeleton } from "./OrganizationSwitcherSkeleton";
import { Sidebar } from "./ui/sidebar"; import { Sidebar } from "./ui/sidebar";
export function AppSidebarSkeleton( export function AppSidebarSkeleton(
props: React.ComponentProps<typeof Sidebar> props: React.ComponentProps<typeof Sidebar>,
) { ) {
return ( return (
<AppSidebarShell <AppSidebarShell

View File

@@ -44,7 +44,7 @@ export function EditableField({
} }
className={cn( className={cn(
"w-full resize-none", "w-full resize-none",
required && !value && "border-red-500" required && !value && "border-red-500",
)} )}
placeholder={`Enter ${label.toLowerCase()}`} placeholder={`Enter ${label.toLowerCase()}`}
rows={4} rows={4}

View File

@@ -53,7 +53,7 @@ export function NavMain() {
if (item.items?.length) { if (item.items?.length) {
return item.items.some((subItem) => return item.items.some((subItem) =>
location.pathname.startsWith(subItem.url) location.pathname.startsWith(subItem.url),
); );
} }
@@ -93,7 +93,7 @@ export function NavMain() {
<SidebarMenuSub> <SidebarMenuSub>
{item.items?.map((subItem) => { {item.items?.map((subItem) => {
const subItemActive = location.pathname.startsWith( const subItemActive = location.pathname.startsWith(
subItem.url subItem.url,
); );
return ( return (

View File

@@ -23,7 +23,7 @@ const navItems = [
]; ];
export function NavSecondary( export function NavSecondary(
props: React.ComponentPropsWithoutRef<typeof SidebarGroup> props: React.ComponentPropsWithoutRef<typeof SidebarGroup>,
) { ) {
return ( return (
<SidebarGroup {...props}> <SidebarGroup {...props}>

View File

@@ -82,7 +82,7 @@ export function OrganizationSwitcher({
useEffect(() => { useEffect(() => {
if (hasOrganizations) { if (hasOrganizations) {
const org = data.organizations.edges.find( const org = data.organizations.edges.find(
(edge) => edge.node.id === organizationId (edge) => edge.node.id === organizationId,
); );
if (org) { if (org) {
setCurrentOrganization(org.node); setCurrentOrganization(org.node);
@@ -104,7 +104,7 @@ export function OrganizationSwitcher({
variant="ghost" variant="ghost"
className={cn( className={cn(
"gap-2.5", "gap-2.5",
!currentOrganization && "border border-dashed" !currentOrganization && "border border-dashed",
)} )}
> >
<div className="flex aspect-square size-8 items-center justify-center rounded-sm bg-highlight-bg"> <div className="flex aspect-square size-8 items-center justify-center rounded-sm bg-highlight-bg">

View File

@@ -66,7 +66,7 @@ export default function PeopleSelector({
<SelectItem key={edge.node.id} value={edge.node.id}> <SelectItem key={edge.node.id} value={edge.node.id}>
{edge.node.fullName} ({edge.node.primaryEmailAddress}) {edge.node.fullName} ({edge.node.primaryEmailAddress})
</SelectItem> </SelectItem>
) ),
)} )}
</SelectContent> </SelectContent>
</Select> </Select>

View File

@@ -11,7 +11,7 @@ const Avatar = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className className,
)} )}
{...props} {...props}
/> />
@@ -38,7 +38,7 @@ const AvatarFallback = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-subtle-bg", "flex h-full w-full items-center justify-center rounded-full bg-subtle-bg",
className className,
)} )}
{...props} {...props}
/> />

View File

@@ -20,7 +20,7 @@ const badgeVariants = cva(
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
}, },
} },
); );
export interface BadgeProps export interface BadgeProps

View File

@@ -20,7 +20,7 @@ const BreadcrumbList = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"flex flex-wrap items-center gap-3 break-words text-sm text-secondary sm:gap-2.5", "flex flex-wrap items-center gap-3 break-words text-sm text-secondary sm:gap-2.5",
className className,
)} )}
{...props} {...props}
/> />

View File

@@ -31,7 +31,7 @@ const buttonVariants = cva(
variant: "default", variant: "default",
size: "default", size: "default",
}, },
} },
); );
export interface ButtonProps export interface ButtonProps
@@ -50,7 +50,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
{...props} {...props}
/> />
); );
} },
); );
Button.displayName = "Button"; Button.displayName = "Button";

View File

@@ -12,7 +12,7 @@ const Checkbox = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-level-0 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-active-b focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-invert", "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-level-0 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-active-b focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-invert",
className className,
)} )}
{...props} {...props}
> >

View File

@@ -19,7 +19,7 @@ const Command = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-level-0 text-primary", "flex h-full w-full flex-col overflow-hidden rounded-md bg-level-0 text-primary",
className className,
)} )}
{...props} {...props}
/> />
@@ -48,7 +48,7 @@ const CommandInput = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-tertiary disabled:cursor-not-allowed disabled:opacity-50", "flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-tertiary disabled:cursor-not-allowed disabled:opacity-50",
className className,
)} )}
{...props} {...props}
/> />
@@ -91,7 +91,7 @@ const CommandGroup = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"overflow-hidden p-1 text-primary [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-tertiary", "overflow-hidden p-1 text-primary [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-tertiary",
className className,
)} )}
{...props} {...props}
/> />
@@ -119,7 +119,7 @@ const CommandItem = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent-bg aria-selected:text-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-secondary-bg transition-colors", "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent-bg aria-selected:text-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-secondary-bg transition-colors",
className className,
)} )}
{...props} {...props}
/> />

View File

@@ -19,7 +19,7 @@ const DialogOverlay = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"fixed inset-0 z-50 bg-black/50 backdrop-blur-xs data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", "fixed inset-0 z-50 bg-black/50 backdrop-blur-xs data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className className,
)} )}
{...props} {...props}
/> />
@@ -36,7 +36,7 @@ const DialogContent = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-mid-b bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-mid-b bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className className,
)} )}
{...props} {...props}
> >
@@ -57,7 +57,7 @@ const DialogHeader = ({
<div <div
className={cn( className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left", "flex flex-col space-y-1.5 text-center sm:text-left",
className className,
)} )}
{...props} {...props}
/> />
@@ -71,7 +71,7 @@ const DialogFooter = ({
<div <div
className={cn( className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className className,
)} )}
{...props} {...props}
/> />
@@ -86,7 +86,7 @@ const DialogTitle = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"text-lg font-semibold leading-none tracking-tight", "text-lg font-semibold leading-none tracking-tight",
className className,
)} )}
{...props} {...props}
/> />

View File

@@ -29,7 +29,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
className={cn( className={cn(
"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent-bg data-[state=open]:bg-accent-bg [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", "flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent-bg data-[state=open]:bg-accent-bg [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8", inset && "pl-8",
className className,
)} )}
{...props} {...props}
> >
@@ -48,7 +48,7 @@ const DropdownMenuSubContent = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-level-1 p-1 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-level-1 p-1 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className className,
)} )}
{...props} {...props}
/> />
@@ -67,7 +67,7 @@ const DropdownMenuContent = React.forwardRef<
className={cn( className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-level-1 p-1 shadow-md", "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-level-1 p-1 shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className className,
)} )}
{...props} {...props}
/> />
@@ -86,7 +86,7 @@ const DropdownMenuItem = React.forwardRef<
className={cn( className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden transition-colors focus:bg-active-bg focus:text-active data-disabled:pointer-events-none data-disabled:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0", "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden transition-colors focus:bg-active-bg focus:text-active data-disabled:pointer-events-none data-disabled:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
inset && "pl-8", inset && "pl-8",
className className,
)} )}
{...props} {...props}
/> />
@@ -101,7 +101,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden transition-colors focus:bg-accent-bg focus:text-accent data-disabled:pointer-events-none data-disabled:opacity-50", "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden transition-colors focus:bg-accent-bg focus:text-accent data-disabled:pointer-events-none data-disabled:opacity-50",
className className,
)} )}
checked={checked} checked={checked}
{...props} {...props}
@@ -125,7 +125,7 @@ const DropdownMenuRadioItem = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden transition-colors focus:bg-accent-bg focus:text-accent data-disabled:pointer-events-none data-disabled:opacity-50", "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden transition-colors focus:bg-accent-bg focus:text-accent data-disabled:pointer-events-none data-disabled:opacity-50",
className className,
)} )}
{...props} {...props}
> >
@@ -150,7 +150,7 @@ const DropdownMenuLabel = React.forwardRef<
className={cn( className={cn(
"px-2 py-1.5 text-sm font-semibold", "px-2 py-1.5 text-sm font-semibold",
inset && "pl-8", inset && "pl-8",
className className,
)} )}
{...props} {...props}
/> />

View File

@@ -9,13 +9,13 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
type={type} type={type}
className={cn( className={cn(
"flex h-9 w-full rounded-md border border-solid-b bg-invert-bg px-3 py-1 text-base shadow-xs transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-primary placeholder:text-tertiary focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-active-b disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "flex h-9 w-full rounded-md border border-solid-b bg-invert-bg px-3 py-1 text-base shadow-xs transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-primary placeholder:text-tertiary focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-active-b disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className className,
)} )}
ref={ref} ref={ref}
{...props} {...props}
/> />
); );
} },
); );
Input.displayName = "Input"; Input.displayName = "Input";

View File

@@ -18,7 +18,7 @@ const PopoverContent = React.forwardRef<
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 w-72 rounded-md border bg-level-0 p-4 text-primary shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "z-50 w-72 rounded-md border bg-level-0 p-4 text-primary shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className className,
)} )}
{...props} {...props}
/> />

View File

@@ -27,7 +27,7 @@ const RadioGroupItem = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-level-0 focus:outline-hidden focus-visible:ring-2 focus-visible:ring-active-b focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", "aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-level-0 focus:outline-hidden focus-visible:ring-2 focus-visible:ring-active-b focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className className,
)} )}
{...props} {...props}
> >

View File

@@ -18,7 +18,7 @@ const SelectTrigger = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-solid-b bg-invert-bg px-3 py-2 text-sm ring-offset-level-0 placeholder:text-tertiary focus:outline-hidden focus:ring focus:ring-active-b disabled:cursor-not-allowed disabled:opacity-50", "flex h-10 w-full items-center justify-between rounded-md border border-solid-b bg-invert-bg px-3 py-2 text-sm ring-offset-level-0 placeholder:text-tertiary focus:outline-hidden focus:ring focus:ring-active-b disabled:cursor-not-allowed disabled:opacity-50",
className className,
)} )}
{...props} {...props}
> >
@@ -39,7 +39,7 @@ const SelectContent = React.forwardRef<
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-invert-bg text-primary shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-invert-bg text-primary shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" && position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className className,
)} )}
position={position} position={position}
{...props} {...props}
@@ -48,7 +48,7 @@ const SelectContent = React.forwardRef<
className={cn( className={cn(
"p-1", "p-1",
position === "popper" && position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]" "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)} )}
> >
{children} {children}
@@ -66,7 +66,7 @@ const SelectItem = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden focus:bg-active-bg focus:text-active data-disabled:pointer-events-none data-disabled:opacity-50", "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden focus:bg-active-bg focus:text-active data-disabled:pointer-events-none data-disabled:opacity-50",
className className,
)} )}
{...props} {...props}
> >

View File

@@ -9,7 +9,7 @@ const Separator = React.forwardRef<
>( >(
( (
{ className, orientation = "horizontal", decorative = true, ...props }, { className, orientation = "horizontal", decorative = true, ...props },
ref ref,
) => ( ) => (
<SeparatorPrimitive.Root <SeparatorPrimitive.Root
ref={ref} ref={ref}
@@ -18,11 +18,11 @@ const Separator = React.forwardRef<
className={cn( className={cn(
"shrink-0 bg-solid-b", "shrink-0 bg-solid-b",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className className,
)} )}
{...props} {...props}
/> />
) ),
); );
Separator.displayName = SeparatorPrimitive.Root.displayName; Separator.displayName = SeparatorPrimitive.Root.displayName;

View File

@@ -22,7 +22,7 @@ const SheetOverlay = React.forwardRef<
<SheetPrimitive.Overlay <SheetPrimitive.Overlay
className={cn( className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className className,
)} )}
{...props} {...props}
ref={ref} ref={ref}
@@ -46,7 +46,7 @@ const sheetVariants = cva(
defaultVariants: { defaultVariants: {
side: "right", side: "right",
}, },
} },
); );
interface SheetContentProps interface SheetContentProps
@@ -81,7 +81,7 @@ const SheetHeader = ({
<div <div
className={cn( className={cn(
"flex flex-col space-y-2 text-center sm:text-left", "flex flex-col space-y-2 text-center sm:text-left",
className className,
)} )}
{...props} {...props}
/> />
@@ -95,7 +95,7 @@ const SheetFooter = ({
<div <div
className={cn( className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className className,
)} )}
{...props} {...props}
/> />

View File

@@ -63,7 +63,7 @@ const SidebarProvider = React.forwardRef<
children, children,
...props ...props
}, },
ref ref,
) => { ) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false); const [openMobile, setOpenMobile] = React.useState(false);
@@ -84,7 +84,7 @@ const SidebarProvider = React.forwardRef<
// This sets the cookie to keep the sidebar state. // This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
}, },
[setOpenProp, open] [setOpenProp, open],
); );
// Helper to toggle the sidebar. // Helper to toggle the sidebar.
@@ -124,7 +124,15 @@ const SidebarProvider = React.forwardRef<
setOpenMobile, setOpenMobile,
toggleSidebar, toggleSidebar,
}), }),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] [
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
],
); );
return ( return (
@@ -140,7 +148,7 @@ const SidebarProvider = React.forwardRef<
} }
className={cn( className={cn(
"group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-level-0", "group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-level-0",
className className,
)} )}
ref={ref} ref={ref}
{...props} {...props}
@@ -150,7 +158,7 @@ const SidebarProvider = React.forwardRef<
</TooltipProvider> </TooltipProvider>
</SidebarContext.Provider> </SidebarContext.Provider>
); );
} },
); );
SidebarProvider.displayName = "SidebarProvider"; SidebarProvider.displayName = "SidebarProvider";
@@ -171,7 +179,7 @@ const Sidebar = React.forwardRef<
children, children,
...props ...props
}, },
ref ref,
) => { ) => {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
@@ -180,7 +188,7 @@ const Sidebar = React.forwardRef<
<div <div
className={cn( className={cn(
"flex h-full w-(--sidebar-width) flex-col bg-level-0 text-secondary", "flex h-full w-(--sidebar-width) flex-col bg-level-0 text-secondary",
className className,
)} )}
ref={ref} ref={ref}
{...props} {...props}
@@ -227,7 +235,7 @@ const Sidebar = React.forwardRef<
"group-data-[side=right]:rotate-180", "group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset" variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]" ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)" : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
)} )}
/> />
<div <div
@@ -240,7 +248,7 @@ const Sidebar = React.forwardRef<
variant === "floating" || variant === "inset" variant === "floating" || variant === "inset"
? "p-4 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]" ? "p-4 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l", : "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
className className,
)} )}
{...props} {...props}
> >
@@ -253,7 +261,7 @@ const Sidebar = React.forwardRef<
</div> </div>
</div> </div>
); );
} },
); );
Sidebar.displayName = "Sidebar"; Sidebar.displayName = "Sidebar";
@@ -304,7 +312,7 @@ const SidebarRail = React.forwardRef<
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-level-0", "group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-level-0",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2", "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2", "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className className,
)} )}
{...props} {...props}
/> />
@@ -322,7 +330,7 @@ const SidebarInset = React.forwardRef<
className={cn( className={cn(
"relative flex min-h-svh flex-1 flex-col bg-level-0", "relative flex min-h-svh flex-1 flex-col bg-level-0",
"peer-data-[variant=inset]:min-h-[calc(100svh-(--spacing(4)))] md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm", "peer-data-[variant=inset]:min-h-[calc(100svh-(--spacing(4)))] md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm",
className className,
)} )}
{...props} {...props}
/> />
@@ -340,7 +348,7 @@ const SidebarInput = React.forwardRef<
data-sidebar="input" data-sidebar="input"
className={cn( className={cn(
"h-8 w-full bg-level-0 shadow-none focus-visible:ring-2 focus-visible:ring-active-b", "h-8 w-full bg-level-0 shadow-none focus-visible:ring-2 focus-visible:ring-active-b",
className className,
)} )}
{...props} {...props}
/> />
@@ -403,7 +411,7 @@ const SidebarContent = React.forwardRef<
data-sidebar="content" data-sidebar="content"
className={cn( className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden", "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className className,
)} )}
{...props} {...props}
/> />
@@ -439,7 +447,7 @@ const SidebarGroupLabel = React.forwardRef<
className={cn( className={cn(
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-secondary/70 outline-hidden ring-active-b transition-[margin,opa] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", "flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-secondary/70 outline-hidden ring-active-b transition-[margin,opa] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className className,
)} )}
{...props} {...props}
/> />
@@ -462,7 +470,7 @@ const SidebarGroupAction = React.forwardRef<
// Increases the hit area of the button on mobile. // Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden", "after:absolute after:-inset-2 md:after:hidden",
"group-data-[collapsible=icon]:hidden", "group-data-[collapsible=icon]:hidden",
className className,
)} )}
{...props} {...props}
/> />
@@ -531,7 +539,7 @@ const sidebarMenuButtonVariants = cva(
variant: "default", variant: "default",
size: "default", size: "default",
}, },
} },
); );
const SidebarMenuButton = React.forwardRef< const SidebarMenuButton = React.forwardRef<
@@ -552,7 +560,7 @@ const SidebarMenuButton = React.forwardRef<
className, className,
...props ...props
}, },
ref ref,
) => { ) => {
const Comp = asChild ? Slot : "button"; const Comp = asChild ? Slot : "button";
const { isMobile, state } = useSidebar(); const { isMobile, state } = useSidebar();
@@ -589,7 +597,7 @@ const SidebarMenuButton = React.forwardRef<
/> />
</Tooltip> </Tooltip>
); );
} },
); );
SidebarMenuButton.displayName = "SidebarMenuButton"; SidebarMenuButton.displayName = "SidebarMenuButton";
@@ -616,7 +624,7 @@ const SidebarMenuAction = React.forwardRef<
"group-data-[collapsible=icon]:hidden", "group-data-[collapsible=icon]:hidden",
showOnHover && showOnHover &&
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-accent md:opacity-0", "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-accent md:opacity-0",
className className,
)} )}
{...props} {...props}
/> />
@@ -638,7 +646,7 @@ const SidebarMenuBadge = React.forwardRef<
"peer-data-[size=default]/menu-button:top-1.5", "peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5", "peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden", "group-data-[collapsible=icon]:hidden",
className className,
)} )}
{...props} {...props}
/> />
@@ -693,7 +701,7 @@ const SidebarMenuSub = React.forwardRef<
className={cn( className={cn(
"ml-5 flex min-w-0 flex-col gap-1 border-l border-solid-b px-2.5 py-1", "ml-5 flex min-w-0 flex-col gap-1 border-l border-solid-b px-2.5 py-1",
"group-data-[collapsible=icon]:hidden", "group-data-[collapsible=icon]:hidden",
className className,
)} )}
{...props} {...props}
/> />
@@ -728,7 +736,7 @@ const SidebarMenuSubButton = React.forwardRef<
size === "sm" && "text-xs", size === "sm" && "text-xs",
size === "md" && "text-sm", size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden", "group-data-[collapsible=icon]:hidden",
className className,
)} )}
{...props} {...props}
/> />

View File

@@ -1,7 +1,7 @@
import * as React from "react" import * as React from "react";
import * as SwitchPrimitive from "@radix-ui/react-switch" import * as SwitchPrimitive from "@radix-ui/react-switch";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Switch({ function Switch({
className, className,
@@ -12,18 +12,18 @@ function Switch({
data-slot="switch" data-slot="switch"
className={cn( className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", "peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className className,
)} )}
{...props} {...props}
> >
<SwitchPrimitive.Thumb <SwitchPrimitive.Thumb
data-slot="switch-thumb" data-slot="switch-thumb"
className={cn( className={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0" "bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0",
)} )}
/> />
</SwitchPrimitive.Root> </SwitchPrimitive.Root>
) );
} }
export { Switch } export { Switch };

View File

@@ -56,7 +56,7 @@ const TableRow = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"border-b transition-colors hover:bg-h-subtle-bg data-[state=selected]:bg-subtle-bg", "border-b transition-colors hover:bg-h-subtle-bg data-[state=selected]:bg-subtle-bg",
className className,
)} )}
{...props} {...props}
/> />
@@ -71,7 +71,7 @@ const TableHead = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"h-12 px-4 text-left align-middle font-medium text-tertiary [&:has([role=checkbox])]:pr-0", "h-12 px-4 text-left align-middle font-medium text-tertiary [&:has([role=checkbox])]:pr-0",
className className,
)} )}
{...props} {...props}
/> />

View File

@@ -12,7 +12,7 @@ const TabsList = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"inline-flex h-10 items-center rounded-md p-1 text-tertiary", "inline-flex h-10 items-center rounded-md p-1 text-tertiary",
className className,
)} )}
{...props} {...props}
/> />
@@ -27,7 +27,7 @@ const TabsTrigger = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"border-b-2 border-transparent cursor-pointer inline-flex items-center justify-center whitespace-nowrap px-3 py-1.5 text-sm font-medium ring-offset-level-0 transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-active-b focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:text-active data-[state=active]:border-primary", "border-b-2 border-transparent cursor-pointer inline-flex items-center justify-center whitespace-nowrap px-3 py-1.5 text-sm font-medium ring-offset-level-0 transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-active-b focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:text-active data-[state=active]:border-primary",
className className,
)} )}
{...props} {...props}
/> />
@@ -42,7 +42,7 @@ const TabsContent = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"mt-2 ring-offset-level-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-active-b focus-visible:ring-offset-2", "mt-2 ring-offset-level-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-active-b focus-visible:ring-offset-2",
className className,
)} )}
{...props} {...props}
/> />

View File

@@ -12,7 +12,7 @@ const Textarea = React.forwardRef<
<textarea <textarea
className={cn( className={cn(
"flex min-h-[80px] w-full rounded-md border border-solid-b bg-invert-bg px-3 py-2 text-sm ring-offset-level-0 placeholder:text-tertiary focus-visible:outline-hidden focus-visible:ring focus-visible:ring-active-b disabled:cursor-not-allowed disabled:opacity-50", "flex min-h-[80px] w-full rounded-md border border-solid-b bg-invert-bg px-3 py-2 text-sm ring-offset-level-0 placeholder:text-tertiary focus-visible:outline-hidden focus-visible:ring focus-visible:ring-active-b disabled:cursor-not-allowed disabled:opacity-50",
className className,
)} )}
ref={ref} ref={ref}
{...props} {...props}

View File

@@ -15,7 +15,7 @@ const ToastViewport = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"fixed top-0 z-100 flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]", "fixed top-0 z-100 flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className className,
)} )}
{...props} {...props}
/> />
@@ -35,7 +35,7 @@ const toastVariants = cva(
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
}, },
} },
); );
const Toast = React.forwardRef< const Toast = React.forwardRef<
@@ -61,7 +61,7 @@ const ToastAction = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-invert focus:outline-hidden focus:ring-1 focus:ring-active-b disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-solid-b/40 hover:group-[.destructive]:border-danger-b/30 hover:group-[.destructive]:bg-danger-bg hover:group-[.destructive]:text-danger focus:group-[.destructive]:ring-danger-b", "inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-invert focus:outline-hidden focus:ring-1 focus:ring-active-b disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-solid-b/40 hover:group-[.destructive]:border-danger-b/30 hover:group-[.destructive]:bg-danger-bg hover:group-[.destructive]:text-danger focus:group-[.destructive]:ring-danger-b",
className className,
)} )}
{...props} {...props}
/> />
@@ -76,7 +76,7 @@ const ToastClose = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"absolute right-1 top-1 rounded-md p-1 text-primary/50 opacity-0 transition-opacity hover:text-primary focus:opacity-100 focus:outline-hidden focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 hover:group-[.destructive]:text-red-50 focus:group-[.destructive]:ring-red-400 focus:group-[.destructive]:ring-offset-red-600", "absolute right-1 top-1 rounded-md p-1 text-primary/50 opacity-0 transition-opacity hover:text-primary focus:opacity-100 focus:outline-hidden focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 hover:group-[.destructive]:text-red-50 focus:group-[.destructive]:ring-red-400 focus:group-[.destructive]:ring-offset-red-600",
className className,
)} )}
toast-close="" toast-close=""
{...props} {...props}

View File

@@ -19,7 +19,7 @@ const TooltipContent = React.forwardRef<
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-invert animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-invert animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className className,
)} )}
{...props} {...props}
/> />

View File

@@ -33,11 +33,11 @@ export default function OrganizationSelectionPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const data = useLazyLoadQuery<OrganizationSelectionPageQuery>( const data = useLazyLoadQuery<OrganizationSelectionPageQuery>(
organizationSelectionQuery, organizationSelectionQuery,
{} {},
); );
const organizations = data.viewer.organizations.edges.map( const organizations = data.viewer.organizations.edges.map(
(edge) => edge.node (edge) => edge.node,
); );
useEffect(() => { useEffect(() => {

View File

@@ -3,7 +3,14 @@ import { useSearchParams } from "react-router";
import { Helmet } from "react-helmet-async"; import { Helmet } from "react-helmet-async";
import { buildEndpoint } from "../utils"; import { buildEndpoint } from "../utils";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
@@ -40,13 +47,16 @@ export default function SigningRequestsPage() {
async function fetchDocuments() { async function fetchDocuments() {
try { try {
const response = await fetch(buildEndpoint("/api/console/v1/policies/signing-requests"), { const response = await fetch(
buildEndpoint("/api/console/v1/policies/signing-requests"),
{
method: "GET", method: "GET",
headers: { headers: {
"Authorization": `Bearer ${token}`, Authorization: `Bearer ${token}`,
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
}); },
);
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to fetch signing documents"); throw new Error("Failed to fetch signing documents");
@@ -54,18 +64,20 @@ export default function SigningRequestsPage() {
const documents: Document[] = await response.json(); const documents: Document[] = await response.json();
// Transform the API response to match our internal structure // Transform the API response to match our internal structure
const enhancedDocuments = documents.map(doc => ({ const enhancedDocuments = documents.map((doc) => ({
...doc, ...doc,
signed: false signed: false,
})); }));
setSigningData({ setSigningData({
documents: enhancedDocuments, documents: enhancedDocuments,
requesterName: "Requester", // Default values as they're not in the API response requesterName: "Requester", // Default values as they're not in the API response
requesterOrganization: "Organization" requesterOrganization: "Organization",
}); });
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "An unknown error occurred"); setError(
err instanceof Error ? err.message : "An unknown error occurred",
);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -81,13 +93,18 @@ export default function SigningRequestsPage() {
const docToSign = signingData.documents[currentDocIndex]; const docToSign = signingData.documents[currentDocIndex];
try { try {
const response = await fetch(buildEndpoint(`/api/console/v1/policies/signing-requests/${docToSign.policy_version_id}/sign`), { const response = await fetch(
buildEndpoint(
`/api/console/v1/policies/signing-requests/${docToSign.policy_version_id}/sign`,
),
{
method: "POST", method: "POST",
headers: { headers: {
"Authorization": `Bearer ${token}`, Authorization: `Bearer ${token}`,
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
}); },
);
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to sign document"); throw new Error("Failed to sign document");
@@ -124,7 +141,7 @@ export default function SigningRequestsPage() {
// Calculate progress // Calculate progress
const getSignedCount = () => { const getSignedCount = () => {
if (!signingData) return 0; if (!signingData) return 0;
return signingData.documents.filter(doc => doc.signed).length; return signingData.documents.filter((doc) => doc.signed).length;
}; };
const getProgressPercentage = () => { const getProgressPercentage = () => {
@@ -139,7 +156,9 @@ export default function SigningRequestsPage() {
<Card className="w-full max-w-3xl"> <Card className="w-full max-w-3xl">
<CardHeader> <CardHeader>
<CardTitle>Loading Signing Requests</CardTitle> <CardTitle>Loading Signing Requests</CardTitle>
<CardDescription>Please wait while we fetch your documents...</CardDescription> <CardDescription>
Please wait while we fetch your documents...
</CardDescription>
</CardHeader> </CardHeader>
</Card> </Card>
</div> </div>
@@ -170,7 +189,9 @@ export default function SigningRequestsPage() {
<Card className="w-full max-w-3xl"> <Card className="w-full max-w-3xl">
<CardHeader> <CardHeader>
<CardTitle>No Documents to Sign</CardTitle> <CardTitle>No Documents to Sign</CardTitle>
<CardDescription>There are no documents requiring your signature at this time.</CardDescription> <CardDescription>
There are no documents requiring your signature at this time.
</CardDescription>
</CardHeader> </CardHeader>
</Card> </Card>
</div> </div>
@@ -191,15 +212,19 @@ export default function SigningRequestsPage() {
<div className="mb-8"> <div className="mb-8">
<h1 className="text-2xl font-bold mb-2">Document Signing Request</h1> <h1 className="text-2xl font-bold mb-2">Document Signing Request</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
From {signingData.requesterName} at {signingData.requesterOrganization} From {signingData.requesterName} at{" "}
{signingData.requesterOrganization}
</p> </p>
<div className="mt-4"> <div className="mt-4">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{getSignedCount()} of {signingData.documents.length} documents signed {getSignedCount()} of {signingData.documents.length} documents
signed
</span>
<span className="text-sm font-medium">
{Math.round(getProgressPercentage())}%
</span> </span>
<span className="text-sm font-medium">{Math.round(getProgressPercentage())}%</span>
</div> </div>
<Progress value={getProgressPercentage()} className="h-2" /> <Progress value={getProgressPercentage()} className="h-2" />
</div> </div>
@@ -216,7 +241,9 @@ export default function SigningRequestsPage() {
<CardContent> <CardContent>
<div className="border rounded-md p-4 min-h-[400px] bg-muted/20"> <div className="border rounded-md p-4 min-h-[400px] bg-muted/20">
<div className="prose prose-olive max-w-none"> <div className="prose prose-olive max-w-none">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{currentDoc.content}</ReactMarkdown> <ReactMarkdown remarkPlugins={[remarkGfm]}>
{currentDoc.content}
</ReactMarkdown>
</div> </div>
</div> </div>
</CardContent> </CardContent>
@@ -226,15 +253,11 @@ export default function SigningRequestsPage() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="text-sm text-green-600 font-medium">✓ Signed</div> <div className="text-sm text-green-600 font-medium">✓ Signed</div>
{!isLastDocument && ( {!isLastDocument && (
<Button onClick={handleNextDocument}> <Button onClick={handleNextDocument}>Next Document</Button>
Next Document
</Button>
)} )}
</div> </div>
) : ( ) : (
<Button onClick={handleSignDocument}> <Button onClick={handleSignDocument}>Sign Document</Button>
Sign Document
</Button>
)} )}
{allSigned && ( {allSigned && (

View File

@@ -88,7 +88,7 @@ export default function ConfirmEmailPage() {
setError( setError(
error instanceof Error error instanceof Error
? error.message ? error.message
: "Failed to confirm email. Please try again." : "Failed to confirm email. Please try again.",
); );
setIsLoading(false); setIsLoading(false);
} }

View File

@@ -79,7 +79,7 @@ export default function ConfirmInvitationPage() {
token: token.trim(), token: token.trim(),
password: password, password: password,
}), }),
} },
); );
const data = await response.json(); const data = await response.json();
@@ -97,7 +97,7 @@ export default function ConfirmInvitationPage() {
setError( setError(
error instanceof Error error instanceof Error
? error.message ? error.message
: "Failed to confirm invitation. Please try again." : "Failed to confirm invitation. Please try again.",
); );
} finally { } finally {
setIsLoading(false); setIsLoading(false);

View File

@@ -34,7 +34,7 @@ export default function ForgotPasswordPage() {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ email }), body: JSON.stringify({ email }),
} },
); );
if (!response.ok) { if (!response.ok) {

View File

@@ -37,7 +37,7 @@ export default function LoginPage() {
}, },
credentials: "include", credentials: "include",
body: JSON.stringify({ email, password }), body: JSON.stringify({ email, password }),
} },
); );
if (!response.ok) { if (!response.ok) {

View File

@@ -39,7 +39,7 @@ export default function RegisterPage() {
}, },
credentials: "include", credentials: "include",
body: JSON.stringify({ email, password, fullName }), body: JSON.stringify({ email, password, fullName }),
} },
); );
if (!response.ok) { if (!response.ok) {

View File

@@ -78,7 +78,7 @@ export default function ResetPasswordPage() {
token: token.trim(), token: token.trim(),
password: password, password: password,
}), }),
} },
); );
const data = await response.json(); const data = await response.json();
@@ -96,7 +96,7 @@ export default function ResetPasswordPage() {
setError( setError(
error instanceof Error error instanceof Error
? error.message ? error.message
: "Failed to reset password. Please try again." : "Failed to reset password. Please try again.",
); );
} finally { } finally {
setIsLoading(false); setIsLoading(false);

View File

@@ -86,11 +86,11 @@ export default function CreateOrganizationView() {
const { toast } = useToast(); const { toast } = useToast();
const data = useLazyLoadQuery<CreateOrganizationViewViewerQuery>( const data = useLazyLoadQuery<CreateOrganizationViewViewerQuery>(
viewerQuery, viewerQuery,
{} {},
); );
const [createOrganization] = const [createOrganization] =
useMutation<CreateOrganizationViewCreateOrganizationMutation>( useMutation<CreateOrganizationViewCreateOrganizationMutation>(
createOrganizationMutation createOrganizationMutation,
); );
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: "", name: "",
@@ -114,7 +114,7 @@ export default function CreateOrganizationView() {
connections: [ connections: [
ConnectionHandler.getConnectionID( ConnectionHandler.getConnectionID(
data.viewer.id, data.viewer.id,
"OrganizationSwitcher_organizations" "OrganizationSwitcher_organizations",
), ),
], ],
}, },

View File

@@ -31,7 +31,7 @@ const BreadCrumbNavLink = ({
className={cn( className={cn(
"text-quaternary", "text-quaternary",
isActive && "text-primary", isActive && "text-primary",
className className,
)} )}
> >
{children} {children}

View File

@@ -60,7 +60,7 @@ const BreadcrumbNavLink = ({
className={cn( className={cn(
"text-quaternary", "text-quaternary",
isActive && "text-primary", isActive && "text-primary",
className className,
)} )}
> >
{children} {children}
@@ -98,7 +98,7 @@ function BreadCrumbOrganization() {
} }
`, `,
{ organizationId: organizationId! }, { organizationId: organizationId! },
{ fetchPolicy: "store-or-network" } { fetchPolicy: "store-or-network" },
); );
return ( return (
@@ -146,7 +146,7 @@ function BreadcrumbFrameworkOverview() {
} }
} }
`, `,
{ frameworkId: frameworkId! } { frameworkId: frameworkId! },
); );
return ( return (
@@ -196,7 +196,7 @@ function BreadcrumbVendorOverview() {
} }
`, `,
{ vendorId: vendorId! }, { vendorId: vendorId! },
{ fetchPolicy: "store-or-network" } { fetchPolicy: "store-or-network" },
); );
return ( return (
@@ -246,7 +246,7 @@ function BreadcrumbPeopleOverview() {
} }
`, `,
{ peopleId: peopleId! }, { peopleId: peopleId! },
{ fetchPolicy: "store-or-network" } { fetchPolicy: "store-or-network" },
); );
return ( return (
@@ -296,7 +296,7 @@ function BreadcrumbPolicyOverview() {
} }
`, `,
{ policyId: policyId! }, { policyId: policyId! },
{ fetchPolicy: "store-or-network" } { fetchPolicy: "store-or-network" },
); );
return ( return (
@@ -335,7 +335,9 @@ function BreadcrumbMeasureView() {
const data = const data =
useLazyLoadQuery<OrganizationBreadcrumbBreadcrumbMeasureViewQuery>( useLazyLoadQuery<OrganizationBreadcrumbBreadcrumbMeasureViewQuery>(
graphql` graphql`
query OrganizationBreadcrumbBreadcrumbMeasureViewQuery($measureId: ID!) { query OrganizationBreadcrumbBreadcrumbMeasureViewQuery(
$measureId: ID!
) {
measure: node(id: $measureId) { measure: node(id: $measureId) {
id id
... on Measure { ... on Measure {
@@ -345,7 +347,7 @@ function BreadcrumbMeasureView() {
} }
} }
`, `,
{ measureId: measureId! } { measureId: measureId! },
); );
return ( return (
@@ -385,7 +387,7 @@ function BreadcrumbControl() {
} }
`, `,
{ controlId: controlId! }, { controlId: controlId! },
{ fetchPolicy: "store-or-network" } { fetchPolicy: "store-or-network" },
); );
return ( return (
@@ -431,7 +433,7 @@ function BreadcrumbRiskShow() {
} }
`, `,
{ riskId: riskId! }, { riskId: riskId! },
{ fetchPolicy: "store-or-network" } { fetchPolicy: "store-or-network" },
); );
return ( return (

View File

@@ -9,10 +9,10 @@ import ErrorBoundary from "@/components/ErrorBoundary";
export default function OrganizationLayout() { export default function OrganizationLayout() {
const controlMatch = useMatch( const controlMatch = useMatch(
"organizations/:organizationId/frameworks/:frameworkId/controls/*" "organizations/:organizationId/frameworks/:frameworkId/controls/*",
); );
const frameworkMatch = useMatch( const frameworkMatch = useMatch(
"organizations/:organizationId/frameworks/:frameworkId" "organizations/:organizationId/frameworks/:frameworkId",
); );
return ( return (
@@ -28,7 +28,7 @@ export default function OrganizationLayout() {
"p-8 h-screen overflow-auto", "p-8 h-screen overflow-auto",
controlMatch || frameworkMatch controlMatch || frameworkMatch
? "w-full" ? "w-full"
: "mx-auto w-2xl md:w-3xl lg:w-5xl xl:w-6xl 2xl:w-7xl" : "mx-auto w-2xl md:w-3xl lg:w-5xl xl:w-6xl 2xl:w-7xl",
)} )}
> >
<header className="flex shrink-0 items-center gap-2 mb-7"> <header className="flex shrink-0 items-center gap-2 mb-7">

View File

@@ -54,7 +54,10 @@ export function OrganizationsRoutes() {
<Route path="vendors/:vendorId" element={<VendorPage />} /> <Route path="vendors/:vendorId" element={<VendorPage />} />
<Route path="policies" element={<PolicyListPage />} /> <Route path="policies" element={<PolicyListPage />} />
<Route path="policies/:policyId" element={<ShowPolicyPage />} /> <Route path="policies/:policyId" element={<ShowPolicyPage />} />
<Route path="policies/:policyId/versions/:versionId/edit" element={<EditPolicyPage />} /> <Route
path="policies/:policyId/versions/:versionId/edit"
element={<EditPolicyPage />}
/>
<Route path="risks" element={<ListRiskPage />} /> <Route path="risks" element={<ListRiskPage />} />
<Route path="risks/new" element={<NewRiskPage />} /> <Route path="risks/new" element={<NewRiskPage />} />
<Route path="risks/:riskId" element={<ShowRiskView />} /> <Route path="risks/:riskId" element={<ShowRiskView />} />

View File

@@ -42,7 +42,6 @@ import type { SettingsViewRemoveUserMutation as SettingsViewRemoveUserMutationTy
import { PageTemplate } from "@/components/PageTemplate"; import { PageTemplate } from "@/components/PageTemplate";
import { SettingsViewSkeleton } from "./SettingsPage"; import { SettingsViewSkeleton } from "./SettingsPage";
interface AvailableConnector { interface AvailableConnector {
id: string; id: string;
name: string; name: string;
@@ -120,7 +119,8 @@ function SettingsViewContent({
const data = usePreloadedQuery(settingsViewQuery, queryRef); const data = usePreloadedQuery(settingsViewQuery, queryRef);
const organization = data.organization; const organization = data.organization;
const users = organization.users?.edges.map((edge) => edge.node) || []; const users = organization.users?.edges.map((edge) => edge.node) || [];
const connectors = organization.connectors?.edges.map((edge) => edge.node) || []; const connectors =
organization.connectors?.edges.map((edge) => edge.node) || [];
const { toast } = useToast(); const { toast } = useToast();
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
@@ -130,26 +130,36 @@ function SettingsViewContent({
const [inviteFullName, setInviteFullName] = useState(""); const [inviteFullName, setInviteFullName] = useState("");
const [isInviting, setIsInviting] = useState(false); const [isInviting, setIsInviting] = useState(false);
const [organizationName, setOrganizationName] = useState( const [organizationName, setOrganizationName] = useState(
organization.name || "" organization.name || "",
); );
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const [isRemoving, setIsRemoving] = useState(false); const [isRemoving, setIsRemoving] = useState(false);
// Available connectors // Available connectors
const availableConnectors: AvailableConnector[] = [ const availableConnectors: AvailableConnector[] = [
{ id: "github", name: "GitHub", type: "oauth2", description: "Connect to GitHub repositories and issues" }, {
{ id: "slack", name: "Slack", type: "oauth2", description: "Connect to Slack workspace and channels" }, id: "github",
name: "GitHub",
type: "oauth2",
description: "Connect to GitHub repositories and issues",
},
{
id: "slack",
name: "Slack",
type: "oauth2",
description: "Connect to Slack workspace and channels",
},
]; ];
// Filter out connectors that are already connected // Filter out connectors that are already connected
const connectedConnectorIds = connectors.map((connector) => connector.name); const connectedConnectorIds = connectors.map((connector) => connector.name);
const notConnectedConnectors = availableConnectors.filter( const notConnectedConnectors = availableConnectors.filter(
(connector) => !connectedConnectorIds.includes(connector.id) (connector) => !connectedConnectorIds.includes(connector.id),
); );
const [updateOrganization] = const [updateOrganization] =
useMutation<SettingsViewUpdateOrganizationMutationType>( useMutation<SettingsViewUpdateOrganizationMutationType>(
updateOrganizationMutation updateOrganizationMutation,
); );
const [inviteUser] = const [inviteUser] =
@@ -490,7 +500,9 @@ function SettingsViewContent({
<div className="space-y-6"> <div className="space-y-6">
{connectors.length > 0 && ( {connectors.length > 0 && (
<div> <div>
<h3 className="mb-4 text-sm font-medium">Connected services</h3> <h3 className="mb-4 text-sm font-medium">
Connected services
</h3>
<div className="space-y-4"> <div className="space-y-4">
{connectors.map((connector) => ( {connectors.map((connector) => (
<div <div
@@ -506,7 +518,10 @@ function SettingsViewContent({
{connector.name} {connector.name}
</span> </span>
<span className="text-sm text-tertiary"> <span className="text-sm text-tertiary">
{connector.type} · Connected on {new Date(connector.createdAt).toLocaleDateString()} {connector.type} · Connected on{" "}
{new Date(
connector.createdAt,
).toLocaleDateString()}
</span> </span>
</div> </div>
</div> </div>
@@ -518,7 +533,9 @@ function SettingsViewContent({
{notConnectedConnectors.length > 0 && ( {notConnectedConnectors.length > 0 && (
<div> <div>
<h3 className="mb-4 text-sm font-medium">Available services</h3> <h3 className="mb-4 text-sm font-medium">
Available services
</h3>
<div className="space-y-4"> <div className="space-y-4">
{notConnectedConnectors.map((connector) => ( {notConnectedConnectors.map((connector) => (
<div <div
@@ -541,11 +558,25 @@ function SettingsViewContent({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<a <a
href={(() => { href={(() => {
const baseUrl = process.env.API_SERVER_HOST || window.location.origin; const baseUrl =
const url = new URL('/api/console/v1/connectors/initiate', baseUrl); process.env.API_SERVER_HOST ||
url.searchParams.append('organization_id', organization.id); window.location.origin;
url.searchParams.append('connector_id', connector.id); const url = new URL(
url.searchParams.append('continue', window.location.href); "/api/console/v1/connectors/initiate",
baseUrl,
);
url.searchParams.append(
"organization_id",
organization.id,
);
url.searchParams.append(
"connector_id",
connector.id,
);
url.searchParams.append(
"continue",
window.location.href,
);
return url.toString(); return url.toString();
})()} })()}
className="inline-flex items-center justify-center h-9 px-3 text-sm font-medium rounded-md border border-input bg-background hover:bg-accent hover:text-accent-foreground" className="inline-flex items-center justify-center h-9 px-3 text-sm font-medium rounded-md border border-input bg-background hover:bg-accent hover:text-accent-foreground"
@@ -559,14 +590,18 @@ function SettingsViewContent({
</div> </div>
)} )}
{connectors.length === 0 && notConnectedConnectors.length === 0 && ( {connectors.length === 0 &&
notConnectedConnectors.length === 0 && (
<div className="flex flex-col items-center justify-center p-8 text-center"> <div className="flex flex-col items-center justify-center p-8 text-center">
<div className="rounded-full bg-subtle-bg p-3"> <div className="rounded-full bg-subtle-bg p-3">
<Building2 className="h-6 w-6 text-tertiary" /> <Building2 className="h-6 w-6 text-tertiary" />
</div> </div>
<h3 className="mt-4 text-lg font-medium">No integrations available</h3> <h3 className="mt-4 text-lg font-medium">
No integrations available
</h3>
<p className="mt-2 text-sm text-tertiary"> <p className="mt-2 text-sm text-tertiary">
There are currently no integrations available for your workspace. There are currently no integrations available for your
workspace.
</p> </p>
</div> </div>
)} )}

View File

@@ -86,7 +86,7 @@ function EditableField({
} }
className={cn( className={cn(
"w-full resize-none", "w-full resize-none",
required && !value && "border-red-500" required && !value && "border-red-500",
)} )}
placeholder={`Enter ${label.toLowerCase()}`} placeholder={`Enter ${label.toLowerCase()}`}
rows={4} rows={4}
@@ -133,7 +133,7 @@ function EditFrameworkViewContent({
const [commit, isInFlight] = const [commit, isInFlight] =
useMutation<EditFrameworkViewUpdateFrameworkMutation>( useMutation<EditFrameworkViewUpdateFrameworkMutation>(
updateFrameworkMutation updateFrameworkMutation,
); );
const handleFieldChange = (field: keyof typeof formData, value: unknown) => { const handleFieldChange = (field: keyof typeof formData, value: unknown) => {

View File

@@ -37,7 +37,7 @@ export function FrameworkLayoutViewSkeleton() {
export function FrameworkLayout() { export function FrameworkLayout() {
const match = useMatch( const match = useMatch(
"organizations/:organizationId/frameworks/:frameworkId/*" "organizations/:organizationId/frameworks/:frameworkId/*",
); );
return ( return (

View File

@@ -75,7 +75,7 @@ function FrameworkLayoutViewContent({
// Setup delete mutation // Setup delete mutation
const [commitDeleteMutation] = useMutation<FrameworkLayoutViewDeleteMutation>( const [commitDeleteMutation] = useMutation<FrameworkLayoutViewDeleteMutation>(
DeleteFrameworkMutation DeleteFrameworkMutation,
); );
const handleDeleteFramework = useCallback(() => { const handleDeleteFramework = useCallback(() => {
@@ -83,7 +83,7 @@ function FrameworkLayoutViewContent({
const connectionId = ConnectionHandler.getConnectionID( const connectionId = ConnectionHandler.getConnectionID(
organizationId!, organizationId!,
"FrameworkListView_frameworks" "FrameworkListView_frameworks",
); );
commitDeleteMutation({ commitDeleteMutation({
@@ -189,7 +189,7 @@ function FrameworkLayoutViewContent({
export default function FrameworkLayoutView() { export default function FrameworkLayoutView() {
const { frameworkId } = useParams(); const { frameworkId } = useParams();
const [queryRef, loadQuery] = useQueryLoader<FrameworkLayoutViewQueryType>( const [queryRef, loadQuery] = useQueryLoader<FrameworkLayoutViewQueryType>(
FrameworkLayoutViewQuery FrameworkLayoutViewQuery,
); );
useEffect(() => { useEffect(() => {

View File

@@ -34,7 +34,7 @@ export function ControlList(props: ControlListProps) {
const { className, fragmentKey } = props; const { className, fragmentKey } = props;
const { controls } = useFragment<ControlList_List$key>( const { controls } = useFragment<ControlList_List$key>(
controlListFragment, controlListFragment,
fragmentKey fragmentKey,
); );
if (controls.edges.length === 0) { if (controls.edges.length === 0) {
@@ -45,7 +45,7 @@ export function ControlList(props: ControlListProps) {
<aside <aside
className={cn( className={cn(
"shrink-0 w-70 border-r h-full overflow-x-hidden overflow-y-auto [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-track]:bg-level-0 [&::-webkit-scrollbar-thumb]:bg-highlight-bg [&::-webkit-scrollbar-thumb]:rounded-xs", "shrink-0 w-70 border-r h-full overflow-x-hidden overflow-y-auto [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-track]:bg-level-0 [&::-webkit-scrollbar-thumb]:bg-highlight-bg [&::-webkit-scrollbar-thumb]:rounded-xs",
className className,
)} )}
> >
{controls.edges.map(({ node: control }, i) => ( {controls.edges.map(({ node: control }, i) => (
@@ -55,7 +55,7 @@ export function ControlList(props: ControlListProps) {
className={({ isActive }) => className={({ isActive }) =>
cn( cn(
"block pl-8 pr-4 py-4 border-b hover:bg-h-subtle-bg", "block pl-8 pr-4 py-4 border-b hover:bg-h-subtle-bg",
(isActive || (i === 0 && !controlId)) && "bg-subtle-bg" (isActive || (i === 0 && !controlId)) && "bg-subtle-bg",
) )
} }
> >
@@ -65,7 +65,7 @@ export function ControlList(props: ControlListProps) {
<div <div
className={cn( className={cn(
"inline-block font-mono text-sm px-1 py-0.25 rounded-sm bg-highlight-bg border font-semibold", "inline-block font-mono text-sm px-1 py-0.25 rounded-sm bg-highlight-bg border font-semibold",
isActive && "font-bold bg-active-bg border-mid-b" isActive && "font-bold bg-active-bg border-mid-b",
)} )}
> >
{control.referenceId} {control.referenceId}
@@ -73,7 +73,7 @@ export function ControlList(props: ControlListProps) {
<div <div
className={cn( className={cn(
"text-sm leading-none break-words mt-2", "text-sm leading-none break-words mt-2",
isActive && "font-medium" isActive && "font-medium",
)} )}
> >
{control.name.length > maxControlNameLength {control.name.length > maxControlNameLength

View File

@@ -89,7 +89,7 @@ function FrameworkListViewContent({
export default function FrameworkListView() { export default function FrameworkListView() {
const { organizationId } = useParams(); const { organizationId } = useParams();
const [queryRef, loadQuery] = useQueryLoader<FrameworkListViewQueryType>( const [queryRef, loadQuery] = useQueryLoader<FrameworkListViewQueryType>(
FrameworkListViewQuery FrameworkListViewQuery,
); );
if (!queryRef) { if (!queryRef) {

View File

@@ -39,7 +39,7 @@ function FrameworkViewContent({
}) { }) {
const data = usePreloadedQuery<FrameworkViewQuery>( const data = usePreloadedQuery<FrameworkViewQuery>(
frameworkViewQuery, frameworkViewQuery,
queryRef queryRef,
); );
if (!data?.node) { if (!data?.node) {

View File

@@ -40,7 +40,7 @@ export function FrameworkImportDropdown() {
const [loadingFramework, setLoadingFramework] = useState<string | null>(null); const [loadingFramework, setLoadingFramework] = useState<string | null>(null);
const [commit, isInFlight] = const [commit, isInFlight] =
useMutation<ImportFrameworkDialogImportFrameworkMutation>( useMutation<ImportFrameworkDialogImportFrameworkMutation>(
importFrameworkMutation importFrameworkMutation,
); );
const handleImport = async (frameworkId: string) => { const handleImport = async (frameworkId: string) => {
@@ -56,12 +56,12 @@ export function FrameworkImportDropdown() {
`${frameworkId}.json`, `${frameworkId}.json`,
{ {
type: "application/json", type: "application/json",
} },
); );
const connectionId = ConnectionHandler.getConnectionID( const connectionId = ConnectionHandler.getConnectionID(
organizationId!, organizationId!,
"FrameworkListView_frameworks" "FrameworkListView_frameworks",
); );
commit({ commit({

View File

@@ -70,7 +70,7 @@ function EditableField({
} }
className={cn( className={cn(
"w-full resize-none", "w-full resize-none",
required && !value && "border-red-500" required && !value && "border-red-500",
)} )}
placeholder={`Enter ${label.toLowerCase()}`} placeholder={`Enter ${label.toLowerCase()}`}
rows={4} rows={4}
@@ -102,7 +102,7 @@ function NewFrameworkViewContent() {
const [commit, isInFlight] = const [commit, isInFlight] =
useMutation<NewFrameworkViewCreateFrameworkMutation>( useMutation<NewFrameworkViewCreateFrameworkMutation>(
createFrameworkMutation createFrameworkMutation,
); );
const handleFieldChange = (field: keyof typeof formData, value: unknown) => { const handleFieldChange = (field: keyof typeof formData, value: unknown) => {
@@ -126,7 +126,7 @@ function NewFrameworkViewContent() {
const connectionId = ConnectionHandler.getConnectionID( const connectionId = ConnectionHandler.getConnectionID(
organizationId!, organizationId!,
"FrameworkListPage_frameworks" "FrameworkListPage_frameworks",
); );
commit({ commit({
@@ -154,7 +154,7 @@ function NewFrameworkViewContent() {
}); });
navigate( navigate(
`/organizations/${organizationId}/frameworks/${data.createFramework.frameworkEdge.node.id}` `/organizations/${organizationId}/frameworks/${data.createFramework.frameworkEdge.node.id}`,
); );
}, },
onError(error) { onError(error) {

View File

@@ -233,8 +233,12 @@ export function Control({
const [isUnlinkingPolicy, setIsUnlinkingPolicy] = useState(false); const [isUnlinkingPolicy, setIsUnlinkingPolicy] = useState(false);
// Create mutation hooks // Create mutation hooks
const [commitCreateMeasureMapping] = useMutation(createMeasureMappingMutation); const [commitCreateMeasureMapping] = useMutation(
const [commitDeleteMeasureMapping] = useMutation(deleteMeasureMappingMutation); createMeasureMappingMutation,
);
const [commitDeleteMeasureMapping] = useMutation(
deleteMeasureMappingMutation,
);
const [commitCreatePolicyMapping] = useMutation(createPolicyMappingMutation); const [commitCreatePolicyMapping] = useMutation(createPolicyMappingMutation);
const [commitDeletePolicyMapping] = useMutation(deletePolicyMappingMutation); const [commitDeletePolicyMapping] = useMutation(deletePolicyMappingMutation);
@@ -269,16 +273,20 @@ export function Control({
organizationMeasuresQuery, organizationMeasuresQuery,
{ {
organizationId, organizationId,
} },
).subscribe({ ).subscribe({
next: (data) => { next: (data) => {
setOrganizationMeasuresData(data); setOrganizationMeasuresData(data);
}, },
complete: () => { complete: () => {
// Fetch linked measures for this control // Fetch linked measures for this control
fetchQuery<ControlLinkedMeasuresQuery>(environment, linkedMeasuresQuery, { fetchQuery<ControlLinkedMeasuresQuery>(
environment,
linkedMeasuresQuery,
{
controlId: control.id, controlId: control.id,
}).subscribe({ },
).subscribe({
next: (data) => { next: (data) => {
setLinkedMeasuresData(data); setLinkedMeasuresData(data);
setIsLoadingMeasures(false); setIsLoadingMeasures(false);
@@ -310,7 +318,7 @@ export function Control({
const getMeasures = useCallback(() => { const getMeasures = useCallback(() => {
if (!organizationMeasuresData?.organization?.measures?.edges) return []; if (!organizationMeasuresData?.organization?.measures?.edges) return [];
return organizationMeasuresData.organization.measures.edges.map( return organizationMeasuresData.organization.measures.edges.map(
(edge) => edge.node (edge) => edge.node,
); );
}, [organizationMeasuresData]); }, [organizationMeasuresData]);
@@ -324,7 +332,7 @@ export function Control({
const linkedMeasures = getLinkedMeasures(); const linkedMeasures = getLinkedMeasures();
return linkedMeasures.some((measure) => measure.id === measureId); return linkedMeasures.some((measure) => measure.id === measureId);
}, },
[getLinkedMeasures] [getLinkedMeasures],
); );
const getMeasureCategories = useCallback(() => { const getMeasureCategories = useCallback(() => {
@@ -397,7 +405,7 @@ export function Control({
linkedMeasuresQuery, linkedMeasuresQuery,
{ {
controlId: control.id, controlId: control.id,
} },
).subscribe({ ).subscribe({
next: (data) => { next: (data) => {
setLinkedMeasuresData(data); setLinkedMeasuresData(data);
@@ -423,7 +431,7 @@ export function Control({
}, },
}); });
}, },
[commitCreateMeasureMapping, control.id, environment, toast] [commitCreateMeasureMapping, control.id, environment, toast],
); );
const handleUnlinkMeasure = useCallback( const handleUnlinkMeasure = useCallback(
@@ -458,7 +466,7 @@ export function Control({
linkedMeasuresQuery, linkedMeasuresQuery,
{ {
controlId: control.id, controlId: control.id,
} },
).subscribe({ ).subscribe({
next: (data) => { next: (data) => {
setLinkedMeasuresData(data); setLinkedMeasuresData(data);
@@ -484,7 +492,7 @@ export function Control({
}, },
}); });
}, },
[commitDeleteMeasureMapping, control.id, environment, toast] [commitDeleteMeasureMapping, control.id, environment, toast],
); );
const handleOpenMeasureMappingDialog = useCallback(() => { const handleOpenMeasureMappingDialog = useCallback(() => {
@@ -523,7 +531,7 @@ export function Control({
organizationPoliciesQuery, organizationPoliciesQuery,
{ {
organizationId, organizationId,
} },
).subscribe({ ).subscribe({
next: (data) => { next: (data) => {
setOrganizationPoliciesData(data); setOrganizationPoliciesData(data);
@@ -535,7 +543,7 @@ export function Control({
linkedPoliciesQuery, linkedPoliciesQuery,
{ {
controlId: control.id, controlId: control.id,
} },
).subscribe({ ).subscribe({
next: (data) => { next: (data) => {
setLinkedPoliciesData(data); setLinkedPoliciesData(data);
@@ -568,7 +576,7 @@ export function Control({
const getPolicies = useCallback(() => { const getPolicies = useCallback(() => {
if (!organizationPoliciesData?.organization?.policies?.edges) return []; if (!organizationPoliciesData?.organization?.policies?.edges) return [];
return organizationPoliciesData.organization.policies.edges.map( return organizationPoliciesData.organization.policies.edges.map(
(edge) => edge.node (edge) => edge.node,
); );
}, [organizationPoliciesData]); }, [organizationPoliciesData]);
@@ -582,7 +590,7 @@ export function Control({
const linkedPolicies = getLinkedPolicies(); const linkedPolicies = getLinkedPolicies();
return linkedPolicies.some((policy) => policy.id === policyId); return linkedPolicies.some((policy) => policy.id === policyId);
}, },
[getLinkedPolicies] [getLinkedPolicies],
); );
const filteredPolicies = useCallback(() => { const filteredPolicies = useCallback(() => {
@@ -634,7 +642,7 @@ export function Control({
linkedPoliciesQuery, linkedPoliciesQuery,
{ {
controlId: control.id, controlId: control.id,
} },
).subscribe({ ).subscribe({
next: (data) => { next: (data) => {
setLinkedPoliciesData(data); setLinkedPoliciesData(data);
@@ -660,7 +668,7 @@ export function Control({
}, },
}); });
}, },
[commitCreatePolicyMapping, control.id, environment, toast] [commitCreatePolicyMapping, control.id, environment, toast],
); );
const handleUnlinkPolicy = useCallback( const handleUnlinkPolicy = useCallback(
@@ -695,7 +703,7 @@ export function Control({
linkedPoliciesQuery, linkedPoliciesQuery,
{ {
controlId: control.id, controlId: control.id,
} },
).subscribe({ ).subscribe({
next: (data) => { next: (data) => {
setLinkedPoliciesData(data); setLinkedPoliciesData(data);
@@ -721,7 +729,7 @@ export function Control({
}, },
}); });
}, },
[commitDeletePolicyMapping, control.id, environment, toast] [commitDeletePolicyMapping, control.id, environment, toast],
); );
const handleOpenPolicyMappingDialog = useCallback(() => { const handleOpenPolicyMappingDialog = useCallback(() => {
@@ -871,7 +879,7 @@ export function Control({
<td className="py-3 px-4"> <td className="py-3 px-4">
<div <div
className={`px-2 py-0.5 rounded-full text-xs ${getStateColor( className={`px-2 py-0.5 rounded-full text-xs ${getStateColor(
measure.state measure.state,
)} inline-block`} )} inline-block`}
> >
{formatState(measure.state)} {formatState(measure.state)}
@@ -984,7 +992,7 @@ export function Control({
<td className="py-3 px-4"> <td className="py-3 px-4">
<div <div
className={`px-2 py-0.5 rounded-full text-xs ${getStateColor( className={`px-2 py-0.5 rounded-full text-xs ${getStateColor(
measure.state measure.state,
)} inline-block`} )} inline-block`}
> >
{formatState(measure.state)} {formatState(measure.state)}
@@ -1076,7 +1084,9 @@ export function Control({
<thead className="sticky top-0 bg-white"> <thead className="sticky top-0 bg-white">
<tr className="border-b text-left text-sm text-secondary bg-invert-bg"> <tr className="border-b text-left text-sm text-secondary bg-invert-bg">
<th className="py-3 px-4 font-medium">Name</th> <th className="py-3 px-4 font-medium">Name</th>
<th className="py-3 px-4 font-medium">Review Date</th> <th className="py-3 px-4 font-medium">
Review Date
</th>
<th className="py-3 px-4 font-medium text-right"> <th className="py-3 px-4 font-medium text-right">
Actions Actions
</th> </th>
@@ -1103,7 +1113,7 @@ export function Control({
<td className="py-3 px-4"> <td className="py-3 px-4">
{policy.updatedAt {policy.updatedAt
? new Date( ? new Date(
policy.updatedAt policy.updatedAt,
).toLocaleDateString() ).toLocaleDateString()
: "Not set"} : "Not set"}
</td> </td>

View File

@@ -26,7 +26,7 @@ function ControlViewContent({
}) { }) {
const { node } = usePreloadedQuery<ControlViewQuery>( const { node } = usePreloadedQuery<ControlViewQuery>(
controlViewQuery, controlViewQuery,
queryRef queryRef,
); );
return <Control controlKey={node} />; return <Control controlKey={node} />;

View File

@@ -58,7 +58,7 @@ function EditMeasureViewContent({
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: measure.name ?? "", name: measure.name ?? "",
description: measure.description ?? "", description: measure.description ?? "",
category: measure.category ?? "" category: measure.category ?? "",
}); });
const [commit, isInFlight] = const [commit, isInFlight] =
@@ -89,7 +89,7 @@ function EditMeasureViewContent({
id: measureId!, id: measureId!,
name: formData.name, name: formData.name,
description: formData.description, description: formData.description,
category: formData.category category: formData.category,
}, },
}, },
onCompleted(data, errors) { onCompleted(data, errors) {
@@ -154,7 +154,9 @@ function EditMeasureViewContent({
type="button" type="button"
variant="outline" variant="outline"
onClick={() => onClick={() =>
navigate(`/organizations/${organizationId}/measures/${measureId}`) navigate(
`/organizations/${organizationId}/measures/${measureId}`,
)
} }
> >
Cancel Cancel

View File

@@ -121,7 +121,7 @@ function MeasureListContent({
}) { }) {
const data = usePreloadedQuery<MeasureListViewQueryType>( const data = usePreloadedQuery<MeasureListViewQueryType>(
measureListViewQuery, measureListViewQuery,
queryRef queryRef,
) as unknown as OrganizationData; ) as unknown as OrganizationData;
const { organizationId } = useParams(); const { organizationId } = useParams();
@@ -129,14 +129,15 @@ function MeasureListContent({
const [isImporting, setIsImporting] = useState(false); const [isImporting, setIsImporting] = useState(false);
const { toast } = useToast(); const { toast } = useToast();
const [importMeasure] = const [importMeasure] = useMutation<MeasureListViewImportMeasureMutationType>(
useMutation<MeasureListViewImportMeasureMutationType>(importMeasureMutation); importMeasureMutation,
);
// Monitor URL hash for changes and update state accordingly // Monitor URL hash for changes and update state accordingly
const [initialHashCategory] = useState(() => const [initialHashCategory] = useState(() =>
window.location.hash.substring(1) window.location.hash.substring(1)
? decodeURIComponent(window.location.hash.substring(1)) ? decodeURIComponent(window.location.hash.substring(1))
: "" : "",
); );
// Keep track of manually expanded categories // Keep track of manually expanded categories
@@ -202,10 +203,10 @@ function MeasureListContent({
// Calculate global progress // Calculate global progress
const implementedCount = processedMeasures.filter( const implementedCount = processedMeasures.filter(
(measure: Measure) => measure.status === "complete" (measure: Measure) => measure.status === "complete",
).length; ).length;
const notApplicableCount = processedMeasures.filter( const notApplicableCount = processedMeasures.filter(
(measure: Measure) => measure.status === "not-applicable" (measure: Measure) => measure.status === "not-applicable",
).length; ).length;
const totalMeasures = processedMeasures.length; const totalMeasures = processedMeasures.length;
@@ -223,7 +224,7 @@ function MeasureListContent({
} }
return acc; return acc;
}, },
{} as Record<string, number> {} as Record<string, number>,
); );
// Group measures by category // Group measures by category
@@ -236,7 +237,7 @@ function MeasureListContent({
acc[measure.category].push(measure); acc[measure.category].push(measure);
return acc; return acc;
}, },
{} as Record<string, Measure[]> {} as Record<string, Measure[]>,
); );
// Function to toggle a category's expanded state // Function to toggle a category's expanded state
@@ -253,10 +254,10 @@ function MeasureListContent({
const categories: Category[] = Object.entries(measuresByCategory) const categories: Category[] = Object.entries(measuresByCategory)
.map(([categoryName, categoryMeasures]: [string, Measure[]]) => { .map(([categoryName, categoryMeasures]: [string, Measure[]]) => {
const catImplementedCount = categoryMeasures.filter( const catImplementedCount = categoryMeasures.filter(
(measure) => measure.status === "complete" (measure) => measure.status === "complete",
).length; ).length;
const catNotApplicableCount = categoryMeasures.filter( const catNotApplicableCount = categoryMeasures.filter(
(measure) => measure.status === "not-applicable" (measure) => measure.status === "not-applicable",
).length; ).length;
// Consider both "complete" and "not-applicable" as done for category progress // Consider both "complete" and "not-applicable" as done for category progress
const catDoneCount = catImplementedCount + catNotApplicableCount; const catDoneCount = catImplementedCount + catNotApplicableCount;
@@ -271,7 +272,7 @@ function MeasureListContent({
description: `Measures related to ${categoryName.toLowerCase()}`, description: `Measures related to ${categoryName.toLowerCase()}`,
progress: progress, progress: progress,
measures: categoryMeasures.sort((a, b) => measures: categoryMeasures.sort((a, b) =>
(a.name || "").localeCompare(b.name || "") (a.name || "").localeCompare(b.name || ""),
), ),
doneCount: catDoneCount, doneCount: catDoneCount,
totalCount: catTotalCount, totalCount: catTotalCount,
@@ -308,7 +309,7 @@ function MeasureListContent({
connections: [ connections: [
ConnectionHandler.getConnectionID( ConnectionHandler.getConnectionID(
data.organization.id, data.organization.id,
"MeasureListView_measures" "MeasureListView_measures",
), ),
], ],
input: { input: {
@@ -591,28 +592,28 @@ function MeasureListContent({
</p> </p>
<p> <p>
{measure.description.startsWith( {measure.description.startsWith(
"##" "##",
) )
? measure.description ? measure.description
.split("\n") .split("\n")
.find((line) => .find((line) =>
line.startsWith( line.startsWith(
"## Why" "## Why",
) ),
) )
?.replace("## Why?", "") ?.replace("## Why?", "")
?.replace("## Why", "") ?.replace("## Why", "")
?.trim() || ?.trim() ||
measure.description.split( measure.description.split(
"\n" "\n",
)[1] || )[1] ||
"" ""
: measure.description.substring( : measure.description.substring(
0, 0,
180 180,
) + ) +
(measure.description.length > (measure.description
180 .length > 180
? "..." ? "..."
: "")} : "")}
</p> </p>

File diff suppressed because it is too large Load Diff

View File

@@ -36,11 +36,12 @@ export default function NewMeasureView() {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: "", name: "",
description: "", description: "",
category: "" category: "",
}); });
const [commit, isInFlight] = const [commit, isInFlight] = useMutation<NewMeasureViewCreateMeasureMutation>(
useMutation<NewMeasureViewCreateMeasureMutation>(createMeasureMutation); createMeasureMutation,
);
const handleFieldChange = (field: keyof typeof formData, value: unknown) => { const handleFieldChange = (field: keyof typeof formData, value: unknown) => {
setFormData((prev) => ({ setFormData((prev) => ({
@@ -63,7 +64,7 @@ export default function NewMeasureView() {
const connectionId = ConnectionHandler.getConnectionID( const connectionId = ConnectionHandler.getConnectionID(
organizationId!, organizationId!,
"MeasureListView_measures" "MeasureListView_measures",
); );
commit({ commit({
@@ -72,7 +73,7 @@ export default function NewMeasureView() {
organizationId: organizationId!, organizationId: organizationId!,
name: formData.name, name: formData.name,
description: formData.description, description: formData.description,
category: formData.category category: formData.category,
}, },
connections: [connectionId], connections: [connectionId],
}, },
@@ -92,7 +93,7 @@ export default function NewMeasureView() {
}); });
navigate( navigate(
`/organizations/${organizationId}/measures/${data.createMeasure.measureEdge.node.id}` `/organizations/${organizationId}/measures/${data.createMeasure.measureEdge.node.id}`,
); );
}, },
onError(error) { onError(error) {

View File

@@ -100,7 +100,7 @@ function NewPeopleViewContent() {
direction: "ASC", direction: "ASC",
field: "FULL_NAME", field: "FULL_NAME",
}, },
} },
), ),
ConnectionHandler.getConnectionID( ConnectionHandler.getConnectionID(
organizationId!, organizationId!,
@@ -110,7 +110,7 @@ function NewPeopleViewContent() {
direction: "ASC", direction: "ASC",
field: "FULL_NAME", field: "FULL_NAME",
}, },
} },
), ),
ConnectionHandler.getConnectionID( ConnectionHandler.getConnectionID(
organizationId!, organizationId!,
@@ -120,7 +120,7 @@ function NewPeopleViewContent() {
direction: "ASC", direction: "ASC",
field: "FULL_NAME", field: "FULL_NAME",
}, },
} },
), ),
], ],
input: { input: {
@@ -196,7 +196,7 @@ function NewPeopleViewContent() {
onClick={() => { onClick={() => {
const newEmails = const newEmails =
formData.additionalEmailAddresses.filter( formData.additionalEmailAddresses.filter(
(_, i) => i !== index (_, i) => i !== index,
); );
handleFieldChange("additionalEmailAddresses", newEmails); handleFieldChange("additionalEmailAddresses", newEmails);
}} }}
@@ -243,7 +243,7 @@ function NewPeopleViewContent() {
"rounded-full px-4 py-1 text-sm transition-colors", "rounded-full px-4 py-1 text-sm transition-colors",
formData.kind === "EMPLOYEE" formData.kind === "EMPLOYEE"
? "bg-blue-100 text-blue-900 ring-2 ring-blue-600 ring-offset-2" ? "bg-blue-100 text-blue-900 ring-2 ring-blue-600 ring-offset-2"
: "bg-secondary-bg text-primary hover:bg-h-secondary-bg" : "bg-secondary-bg text-primary hover:bg-h-secondary-bg",
)} )}
> >
Employee Employee
@@ -255,7 +255,7 @@ function NewPeopleViewContent() {
"rounded-full px-4 py-1 text-sm transition-colors", "rounded-full px-4 py-1 text-sm transition-colors",
formData.kind === "CONTRACTOR" formData.kind === "CONTRACTOR"
? "bg-purple-100 text-purple-900 ring-2 ring-purple-600 ring-offset-2" ? "bg-purple-100 text-purple-900 ring-2 ring-purple-600 ring-offset-2"
: "bg-secondary-bg text-primary hover:bg-h-secondary-bg" : "bg-secondary-bg text-primary hover:bg-h-secondary-bg",
)} )}
> >
Contractor Contractor
@@ -269,7 +269,7 @@ function NewPeopleViewContent() {
"rounded-full px-4 py-1 text-sm transition-colors", "rounded-full px-4 py-1 text-sm transition-colors",
formData.kind === "SERVICE_ACCOUNT" formData.kind === "SERVICE_ACCOUNT"
? "bg-green-100 text-green-900 ring-2 ring-green-600 ring-offset-2" ? "bg-green-100 text-green-900 ring-2 ring-green-600 ring-offset-2"
: "bg-secondary-bg text-primary hover:bg-h-secondary-bg" : "bg-secondary-bg text-primary hover:bg-h-secondary-bg",
)} )}
> >
Service Account Service Account

View File

@@ -148,7 +148,7 @@ function PeopleListContent({
}) { }) {
const data = usePreloadedQuery<PeopleListViewQueryType>( const data = usePreloadedQuery<PeopleListViewQueryType>(
peopleListViewQuery, peopleListViewQuery,
queryRef queryRef,
); );
const [, setSearchParams] = useSearchParams(); const [, setSearchParams] = useSearchParams();
const [, startTransition] = useTransition(); const [, startTransition] = useTransition();
@@ -230,7 +230,7 @@ function PeopleListContent({
e.preventDefault(); e.preventDefault();
if ( if (
window.confirm( window.confirm(
"Are you sure you want to delete this person?" "Are you sure you want to delete this person?",
) )
) { ) {
deletePeople({ deletePeople({

View File

@@ -202,7 +202,7 @@ function PeopleViewContent({
onClick={() => { onClick={() => {
const newEmails = const newEmails =
formData.additionalEmailAddresses.filter( formData.additionalEmailAddresses.filter(
(_, i) => i !== index (_, i) => i !== index,
); );
handleFieldChange("additionalEmailAddresses", newEmails); handleFieldChange("additionalEmailAddresses", newEmails);
}} }}

View File

@@ -19,7 +19,11 @@ import { PageTemplate } from "@/components/PageTemplate";
import { EditPolicyViewSkeleton } from "./EditPolicyPage"; import { EditPolicyViewSkeleton } from "./EditPolicyPage";
const editPolicyViewQuery = graphql` const editPolicyViewQuery = graphql`
query EditPolicyViewQuery($policyId: ID!, $organizationId: ID!, $policyVersionId: ID!) { query EditPolicyViewQuery(
$policyId: ID!
$organizationId: ID!
$policyVersionId: ID!
) {
policyVersion: node(id: $policyVersionId) { policyVersion: node(id: $policyVersionId) {
id id
... on PolicyVersion { ... on PolicyVersion {
@@ -63,7 +67,7 @@ function EditPolicyViewContent({
const { organizationId, policyId, versionId } = useParams(); const { organizationId, policyId, versionId } = useParams();
const data = usePreloadedQuery<EditPolicyViewQuery>( const data = usePreloadedQuery<EditPolicyViewQuery>(
editPolicyViewQuery, editPolicyViewQuery,
queryRef queryRef,
); );
const [content, setContent] = useState(data.policyVersion.content || ""); const [content, setContent] = useState(data.policyVersion.content || "");
@@ -140,7 +144,7 @@ function EditPolicyViewContent({
variant="outline" variant="outline"
onClick={() => onClick={() =>
navigate( navigate(
`/organizations/${organizationId}/policies/${policyId}` `/organizations/${organizationId}/policies/${policyId}`,
) )
} }
> >

View File

@@ -26,7 +26,10 @@ import {
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { format } from "date-fns"; import { format } from "date-fns";
import type { PolicyListViewQuery, PolicyListViewQuery$data } from "./__generated__/PolicyListViewQuery.graphql"; import type {
PolicyListViewQuery,
PolicyListViewQuery$data,
} from "./__generated__/PolicyListViewQuery.graphql";
import type { PolicyListViewDeleteMutation } from "./__generated__/PolicyListViewDeleteMutation.graphql"; import type { PolicyListViewDeleteMutation } from "./__generated__/PolicyListViewDeleteMutation.graphql";
import type { PolicyListViewCreateMutation } from "./__generated__/PolicyListViewCreateMutation.graphql"; import type { PolicyListViewCreateMutation } from "./__generated__/PolicyListViewCreateMutation.graphql";
import type { PolicyListViewSendSigningNotificationsMutation } from "./__generated__/PolicyListViewSendSigningNotificationsMutation.graphql"; import type { PolicyListViewSendSigningNotificationsMutation } from "./__generated__/PolicyListViewSendSigningNotificationsMutation.graphql";
@@ -54,7 +57,8 @@ const policyListViewQuery = graphql`
organization: node(id: $organizationId) { organization: node(id: $organizationId) {
... on Organization { ... on Organization {
...PeopleSelector_organization ...PeopleSelector_organization
policies(first: 50, orderBy: {field: TITLE, direction: ASC}) @connection(key: "PolicyListView_policies") { policies(first: 50, orderBy: { field: TITLE, direction: ASC })
@connection(key: "PolicyListView_policies") {
edges { edges {
node { node {
id id
@@ -127,7 +131,9 @@ const createPolicyMutation = graphql`
`; `;
const sendSigningNotificationsMutation = graphql` const sendSigningNotificationsMutation = graphql`
mutation PolicyListViewSendSigningNotificationsMutation($input: SendSigningNotificationsInput!) { mutation PolicyListViewSendSigningNotificationsMutation(
$input: SendSigningNotificationsInput!
) {
sendSigningNotifications(input: $input) { sendSigningNotifications(input: $input) {
success success
} }
@@ -137,12 +143,15 @@ function PolicyTableRow({
policy, policy,
organizationId, organizationId,
}: { }: {
policy: NonNullable<PolicyListViewQuery$data["organization"]["policies"]>["edges"][0]["node"]; policy: NonNullable<
PolicyListViewQuery$data["organization"]["policies"]
>["edges"][0]["node"];
organizationId: string; organizationId: string;
}) { }) {
const navigate = useNavigate(); const navigate = useNavigate();
const { toast } = useToast(); const { toast } = useToast();
const [deletePolicy, isDeleting] = useMutation<PolicyListViewDeleteMutation>(DeletePolicyMutation); const [deletePolicy, isDeleting] =
useMutation<PolicyListViewDeleteMutation>(DeletePolicyMutation);
const latestVersion = policy.versions?.edges[0]?.node; const latestVersion = policy.versions?.edges[0]?.node;
const status = latestVersion?.status || "DRAFT"; const status = latestVersion?.status || "DRAFT";
@@ -151,12 +160,16 @@ function PolicyTableRow({
const signatureEdges = latestVersion?.signatures?.edges || []; const signatureEdges = latestVersion?.signatures?.edges || [];
const totalSignatures = signatureEdges.length; const totalSignatures = signatureEdges.length;
const signedCount = signatureEdges.filter( const signedCount = signatureEdges.filter(
edge => edge?.node?.state === "SIGNED" (edge) => edge?.node?.state === "SIGNED",
).length; ).length;
const handleDeletePolicy = (e: React.MouseEvent) => { const handleDeletePolicy = (e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
if (window.confirm("Are you sure you want to delete this policy? This action cannot be undone.")) { if (
window.confirm(
"Are you sure you want to delete this policy? This action cannot be undone.",
)
) {
deletePolicy({ deletePolicy({
variables: { variables: {
input: { input: {
@@ -165,7 +178,7 @@ function PolicyTableRow({
connections: [ connections: [
ConnectionHandler.getConnectionID( ConnectionHandler.getConnectionID(
organizationId, organizationId,
"PolicyListView_policies" "PolicyListView_policies",
), ),
], ],
}, },
@@ -228,7 +241,9 @@ function PolicyTableRow({
</td> </td>
<td className="py-4 px-6"> <td className="py-4 px-6">
{status === "PUBLISHED" ? ( {status === "PUBLISHED" ? (
<span className="text-sm text-primary">{signedCount} / {totalSignatures}</span> <span className="text-sm text-primary">
{signedCount} / {totalSignatures}
</span>
) : ( ) : (
<span className="text-sm text-tertiary">-</span> <span className="text-sm text-tertiary">-</span>
)} )}
@@ -292,7 +307,8 @@ function CreatePolicyModal({
const [content, setContent] = useState(""); const [content, setContent] = useState("");
const [ownerId, setOwnerId] = useState<string | null>(null); const [ownerId, setOwnerId] = useState<string | null>(null);
const [createPolicy, isCreating] = useMutation<PolicyListViewCreateMutation>(createPolicyMutation); const [createPolicy, isCreating] =
useMutation<PolicyListViewCreateMutation>(createPolicyMutation);
// Reset form fields // Reset form fields
const resetForm = () => { const resetForm = () => {
@@ -334,7 +350,7 @@ function CreatePolicyModal({
ConnectionHandler.getConnectionID( ConnectionHandler.getConnectionID(
organizationId, organizationId,
"PolicyListView_policies", "PolicyListView_policies",
{orderBy: {field: "TITLE", direction: "ASC"}} { orderBy: { field: "TITLE", direction: "ASC" } },
), ),
], ],
}, },
@@ -379,7 +395,10 @@ function CreatePolicyModal({
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[1080px] h-[700px] max-h-[700px] p-0 gap-0 flex flex-col"> <DialogContent className="sm:max-w-[1080px] h-[700px] max-h-[700px] p-0 gap-0 flex flex-col">
<DialogTitle className="sr-only">Create new policy</DialogTitle> <DialogTitle className="sr-only">Create new policy</DialogTitle>
<DialogDescription className="sr-only">Form to create a new policy with title, content, and owner information.</DialogDescription> <DialogDescription className="sr-only">
Form to create a new policy with title, content, and owner
information.
</DialogDescription>
<div className="flex justify-between items-center py-2 px-4 border-b border-solid-b h-[40px]"> <div className="flex justify-between items-center py-2 px-4 border-b border-solid-b h-[40px]">
<div className="flex items-center gap-1 text-sm"> <div className="flex items-center gap-1 text-sm">
<span className="text-tertiary">Policies</span> <span className="text-tertiary">Policies</span>
@@ -392,22 +411,22 @@ function CreatePolicyModal({
<div className="flex-1 p-6 overflow-y-auto"> <div className="flex-1 p-6 overflow-y-auto">
<div className="mb-6"> <div className="mb-6">
<h1 <h1
className={`text-4xl leading-tight font-bold outline-none focus:outline-none ${!title ? 'text-gray-400' : 'text-black'}`} className={`text-4xl leading-tight font-bold outline-none focus:outline-none ${!title ? "text-gray-400" : "text-black"}`}
contentEditable contentEditable
suppressContentEditableWarning suppressContentEditableWarning
onInput={(e) => { onInput={(e) => {
const newText = e.currentTarget.textContent || ""; const newText = e.currentTarget.textContent || "";
setTitle(newText); setTitle(newText);
}} }}
style={{ WebkitTapHighlightColor: 'transparent' }} style={{ WebkitTapHighlightColor: "transparent" }}
onClick={(e) => { onClick={(e) => {
if (!title) { if (!title) {
e.currentTarget.textContent = ''; e.currentTarget.textContent = "";
} }
}} }}
onFocus={(e) => { onFocus={(e) => {
if (e.currentTarget.textContent === "Enter policy title...") { if (e.currentTarget.textContent === "Enter policy title...") {
e.currentTarget.textContent = ''; e.currentTarget.textContent = "";
} }
}} }}
onBlur={(e) => { onBlur={(e) => {
@@ -421,8 +440,7 @@ function CreatePolicyModal({
el.textContent = title || "Enter policy title..."; el.textContent = title || "Enter policy title...";
} }
}} }}
> ></h1>
</h1>
</div> </div>
<Textarea <Textarea
placeholder="This Privacy Policy outlines how NovaSoft collects, uses, and protects personal information provided by users of its services. By accessing or using our platform, you agree to the collection and use of information in accordance with this policy..." placeholder="This Privacy Policy outlines how NovaSoft collects, uses, and protects personal information provided by users of its services. By accessing or using our platform, you agree to the collection and use of information in accordance with this policy..."
@@ -436,7 +454,9 @@ function CreatePolicyModal({
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex justify-between items-center py-2 border-t border-[rgba(2,42,2,0.08)]"> <div className="flex justify-between items-center py-2 border-t border-[rgba(2,42,2,0.08)]">
<span className="text-sm font-medium text-tertiary">Status</span> <span className="text-sm font-medium text-tertiary">
Status
</span>
<div className="bg-[rgba(0,39,0,0.05)] py-1.5 px-2 rounded-lg"> <div className="bg-[rgba(0,39,0,0.05)] py-1.5 px-2 rounded-lg">
<span className="text-sm font-medium">Draft</span> <span className="text-sm font-medium">Draft</span>
</div> </div>
@@ -455,8 +475,14 @@ function CreatePolicyModal({
</div> </div>
<div className="flex justify-between items-center py-2 border-t border-[rgba(2,42,2,0.08)]"> <div className="flex justify-between items-center py-2 border-t border-[rgba(2,42,2,0.08)]">
<span className="text-sm font-medium text-tertiary">Review date</span> <span className="text-sm font-medium text-tertiary">
<Button size="icon" variant="outline" className="h-8 w-8 rounded-full bg-[rgba(0,39,0,0.05)] border-none"> Review date
</span>
<Button
size="icon"
variant="outline"
className="h-8 w-8 rounded-full bg-[rgba(0,39,0,0.05)] border-none"
>
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
</Button> </Button>
</div> </div>
@@ -492,7 +518,7 @@ function PolicyListViewContent({
}) { }) {
const data = usePreloadedQuery<PolicyListViewQuery>( const data = usePreloadedQuery<PolicyListViewQuery>(
policyListViewQuery, policyListViewQuery,
queryRef queryRef,
); );
const { organizationId } = useParams(); const { organizationId } = useParams();
const policies = const policies =
@@ -501,7 +527,10 @@ function PolicyListViewContent({
const [createModalOpen, setCreateModalOpen] = useState(false); const [createModalOpen, setCreateModalOpen] = useState(false);
const [confirmSigningModalOpen, setConfirmSigningModalOpen] = useState(false); const [confirmSigningModalOpen, setConfirmSigningModalOpen] = useState(false);
const [sendSigningNotifications, isSendingNotifications] = useMutation<PolicyListViewSendSigningNotificationsMutation>(sendSigningNotificationsMutation); const [sendSigningNotifications, isSendingNotifications] =
useMutation<PolicyListViewSendSigningNotificationsMutation>(
sendSigningNotificationsMutation,
);
const { toast } = useToast(); const { toast } = useToast();
const handleOpenModal = () => { const handleOpenModal = () => {
@@ -524,7 +553,8 @@ function PolicyListViewContent({
console.error("Error sending signing notifications:", errors); console.error("Error sending signing notifications:", errors);
toast({ toast({
title: "Error", title: "Error",
description: "Failed to send signing notifications. Please try again.", description:
"Failed to send signing notifications. Please try again.",
variant: "destructive", variant: "destructive",
}); });
return; return;
@@ -540,7 +570,8 @@ function PolicyListViewContent({
console.error("Error sending signing notifications:", error); console.error("Error sending signing notifications:", error);
toast({ toast({
title: "Error", title: "Error",
description: "Failed to send signing notifications. Please try again.", description:
"Failed to send signing notifications. Please try again.",
variant: "destructive", variant: "destructive",
}); });
}, },
@@ -637,11 +668,15 @@ function PolicyListViewContent({
/> />
{/* Confirmation Modal for Sending Signing Notifications */} {/* Confirmation Modal for Sending Signing Notifications */}
<Dialog open={confirmSigningModalOpen} onOpenChange={setConfirmSigningModalOpen}> <Dialog
open={confirmSigningModalOpen}
onOpenChange={setConfirmSigningModalOpen}
>
<DialogContent className="sm:max-w-[425px]"> <DialogContent className="sm:max-w-[425px]">
<DialogTitle>Send Signing Notifications</DialogTitle> <DialogTitle>Send Signing Notifications</DialogTitle>
<DialogDescription> <DialogDescription>
This will send signing notifications to all users who have pending policies to sign. Are you sure you want to continue? This will send signing notifications to all users who have pending
policies to sign. Are you sure you want to continue?
</DialogDescription> </DialogDescription>
<DialogFooter className="gap-2 mt-4"> <DialogFooter className="gap-2 mt-4">
<Button <Button

View File

@@ -7,7 +7,15 @@ import {
useQueryLoader, useQueryLoader,
useMutation, useMutation,
} from "react-relay"; } from "react-relay";
import { Clock, Download, Edit, Trash2, MoreHorizontal, X, FileSignature } from "lucide-react"; import {
Clock,
Download,
Edit,
Trash2,
MoreHorizontal,
X,
FileSignature,
} from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { PageTemplate } from "@/components/PageTemplate"; import { PageTemplate } from "@/components/PageTemplate";
@@ -15,7 +23,7 @@ import type { ShowPolicyViewQuery } from "./__generated__/ShowPolicyViewQuery.gr
import { ShowPolicyViewPublishMutation } from "./__generated__/ShowPolicyViewPublishMutation.graphql"; import { ShowPolicyViewPublishMutation } from "./__generated__/ShowPolicyViewPublishMutation.graphql";
import { ShowPolicyViewCreateDraftMutation } from "./__generated__/ShowPolicyViewCreateDraftMutation.graphql"; import { ShowPolicyViewCreateDraftMutation } from "./__generated__/ShowPolicyViewCreateDraftMutation.graphql";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import remarkGfm from 'remark-gfm' import remarkGfm from "remark-gfm";
import { ShowPolicyViewSkeleton } from "./ShowPolicyPage"; import { ShowPolicyViewSkeleton } from "./ShowPolicyPage";
import { format } from "date-fns"; import { format } from "date-fns";
import { import {
@@ -96,7 +104,9 @@ const publishPolicyVersionMutation = graphql`
`; `;
const createDraftPolicyVersionMutation = graphql` const createDraftPolicyVersionMutation = graphql`
mutation ShowPolicyViewCreateDraftMutation($input: CreateDraftPolicyVersionInput!) { mutation ShowPolicyViewCreateDraftMutation(
$input: CreateDraftPolicyVersionInput!
) {
createDraftPolicyVersion(input: $input) { createDraftPolicyVersion(input: $input) {
policyVersionEdge { policyVersionEdge {
node { node {
@@ -114,19 +124,27 @@ function ShowPolicyContent({
}: { }: {
queryRef: PreloadedQuery<ShowPolicyViewQuery>; queryRef: PreloadedQuery<ShowPolicyViewQuery>;
}) { }) {
const data = usePreloadedQuery<ShowPolicyViewQuery>(policyViewQuery, queryRef); const data = usePreloadedQuery<ShowPolicyViewQuery>(
policyViewQuery,
queryRef,
);
const policy = data.node; const policy = data.node;
const { organizationId } = useParams(); const { organizationId } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const { toast } = useToast(); const { toast } = useToast();
const [queryRef2, loadQuery] = useQueryLoader<ShowPolicyViewQuery>(policyViewQuery); const [queryRef2, loadQuery] =
useQueryLoader<ShowPolicyViewQuery>(policyViewQuery);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);
const [isVersionHistoryOpen, setIsVersionHistoryOpen] = useState(false); const [isVersionHistoryOpen, setIsVersionHistoryOpen] = useState(false);
const [isSignaturesModalOpen, setIsSignaturesModalOpen] = useState(false); const [isSignaturesModalOpen, setIsSignaturesModalOpen] = useState(false);
const [publishDraft, isPublishInFlight] = useMutation<ShowPolicyViewPublishMutation>(publishPolicyVersionMutation); const [publishDraft, isPublishInFlight] =
const [createDraft, isCreateDraftInFlight] = useMutation<ShowPolicyViewCreateDraftMutation>(createDraftPolicyVersionMutation); useMutation<ShowPolicyViewPublishMutation>(publishPolicyVersionMutation);
const [createDraft, isCreateDraftInFlight] =
useMutation<ShowPolicyViewCreateDraftMutation>(
createDraftPolicyVersionMutation,
);
const latestVersionEdge = policy.latestVersion?.edges[0]; const latestVersionEdge = policy.latestVersion?.edges[0];
const latestVersionNode = latestVersionEdge?.node; const latestVersionNode = latestVersionEdge?.node;
@@ -137,7 +155,6 @@ function ShowPolicyContent({
// No need to update selectedVersion state since we're using the VersionHistoryModal component // No need to update selectedVersion state since we're using the VersionHistoryModal component
}, []); }, []);
// Handle delete policy // Handle delete policy
const handleDeletePolicy = useCallback(() => { const handleDeletePolicy = useCallback(() => {
setIsDeleteDialogOpen(true); setIsDeleteDialogOpen(true);
@@ -165,15 +182,15 @@ function ShowPolicyContent({
publishDraft({ publishDraft({
variables: { variables: {
input: { input: {
policyId: policy.id policyId: policy.id,
} },
}, },
onCompleted: (_, errors) => { onCompleted: (_, errors) => {
if (errors) { if (errors) {
toast({ toast({
title: "Error publishing policy", title: "Error publishing policy",
description: errors[0]?.message || "An unknown error occurred", description: errors[0]?.message || "An unknown error occurred",
variant: "destructive" variant: "destructive",
}); });
return; return;
} }
@@ -190,9 +207,9 @@ function ShowPolicyContent({
toast({ toast({
title: "Error publishing policy", title: "Error publishing policy",
description: error.message || "An unknown error occurred", description: error.message || "An unknown error occurred",
variant: "destructive" variant: "destructive",
}); });
} },
}); });
}, [policy.id, publishDraft, toast, loadQuery]); }, [policy.id, publishDraft, toast, loadQuery]);
@@ -202,7 +219,8 @@ function ShowPolicyContent({
}, []); }, []);
// Restore version // Restore version
const handleRestoreVersion = useCallback((versionNumber: number) => { const handleRestoreVersion = useCallback(
(versionNumber: number) => {
// Here you would implement the logic to restore a version // Here you would implement the logic to restore a version
toast({ toast({
title: "Version restored", title: "Version restored",
@@ -213,7 +231,9 @@ function ShowPolicyContent({
if (policy.id) { if (policy.id) {
loadQuery({ policyId: policy.id }); loadQuery({ policyId: policy.id });
} }
}, [policy.id, loadQuery, toast]); },
[policy.id, loadQuery, toast],
);
// Handle edit policy // Handle edit policy
const handleEditPolicy = useCallback(() => { const handleEditPolicy = useCallback(() => {
@@ -224,35 +244,47 @@ function ShowPolicyContent({
createDraft({ createDraft({
variables: { variables: {
input: { input: {
policyID: policy.id policyID: policy.id,
} },
}, },
onCompleted: (response, errors) => { onCompleted: (response, errors) => {
if (errors) { if (errors) {
toast({ toast({
title: "Error creating draft", title: "Error creating draft",
description: errors[0]?.message || "An unknown error occurred", description: errors[0]?.message || "An unknown error occurred",
variant: "destructive" variant: "destructive",
}); });
return; return;
} }
const newDraftId = response.createDraftPolicyVersion.policyVersionEdge.node.id; const newDraftId =
navigate(`/organizations/${organizationId}/policies/${policy.id}/versions/${newDraftId}/edit`); response.createDraftPolicyVersion.policyVersionEdge.node.id;
navigate(
`/organizations/${organizationId}/policies/${policy.id}/versions/${newDraftId}/edit`,
);
}, },
onError: (error) => { onError: (error) => {
toast({ toast({
title: "Error creating draft", title: "Error creating draft",
description: error.message || "An unknown error occurred", description: error.message || "An unknown error occurred",
variant: "destructive" variant: "destructive",
}); });
} },
}); });
} else { } else {
// Navigate directly to edit if it's already a draft // Navigate directly to edit if it's already a draft
navigate(`/organizations/${organizationId}/policies/${policy.id}/versions/${latestVersionNode?.id}/edit`); navigate(
`/organizations/${organizationId}/policies/${policy.id}/versions/${latestVersionNode?.id}/edit`,
);
} }
}, [policy.id, latestVersionNode, createDraft, navigate, organizationId, toast]); }, [
policy.id,
latestVersionNode,
createDraft,
navigate,
organizationId,
toast,
]);
// Format date helper // Format date helper
const formatDate = (dateString?: string) => { const formatDate = (dateString?: string) => {
@@ -312,7 +344,9 @@ function ShowPolicyContent({
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<div onClick={handleEditPolicy}> <div onClick={handleEditPolicy}>
<Edit className="mr-2 h-4 w-4" /> <Edit className="mr-2 h-4 w-4" />
{latestVersionNode?.status === "PUBLISHED" ? "Create new draft" : "Edit draft policy"} {latestVersionNode?.status === "PUBLISHED"
? "Create new draft"
: "Edit draft policy"}
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
@@ -332,7 +366,9 @@ function ShowPolicyContent({
<div className="bg-white rounded-lg border border-solid-b shadow-sm p-6"> <div className="bg-white rounded-lg border border-solid-b shadow-sm p-6">
{latestVersionNode.changelog && ( {latestVersionNode.changelog && (
<div className="mb-6 p-4 bg-level-1 rounded-md border border-solid-b"> <div className="mb-6 p-4 bg-level-1 rounded-md border border-solid-b">
<div className="text-xs text-tertiary uppercase font-medium mb-1">Change summary</div> <div className="text-xs text-tertiary uppercase font-medium mb-1">
Change summary
</div>
<div className="text-sm text-secondary"> <div className="text-sm text-secondary">
{latestVersionNode.changelog} {latestVersionNode.changelog}
</div> </div>
@@ -348,8 +384,8 @@ function ShowPolicyContent({
<div className="mt-8 pt-4 border-t border-solid-b text-xs text-tertiary flex justify-between items-center"> <div className="mt-8 pt-4 border-t border-solid-b text-xs text-tertiary flex justify-between items-center">
<div> <div>
{latestVersionNode.status === "PUBLISHED" {latestVersionNode.status === "PUBLISHED"
? `Published on ${formatDate(latestVersionNode.publishedAt || "")}${latestVersionNode.publishedBy ? ` by ${latestVersionNode.publishedBy.fullName}` : ''}` ? `Published on ${formatDate(latestVersionNode.publishedAt || "")}${latestVersionNode.publishedBy ? ` by ${latestVersionNode.publishedBy.fullName}` : ""}`
: `Last modified on ${formatDate(latestVersionNode.updatedAt)} by ${policy.owner?.fullName || 'Unknown'}`} : `Last modified on ${formatDate(latestVersionNode.updatedAt)} by ${policy.owner?.fullName || "Unknown"}`}
</div> </div>
</div> </div>
</div> </div>
@@ -382,7 +418,8 @@ function ShowPolicyContent({
<DialogHeader> <DialogHeader>
<DialogTitle>Delete Policy</DialogTitle> <DialogTitle>Delete Policy</DialogTitle>
<DialogDescription> <DialogDescription>
Are you sure you want to delete the policy "{policy.title}"? This action cannot be undone. Are you sure you want to delete the policy "{policy.title}"? This
action cannot be undone.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<DialogFooter> <DialogFooter>
@@ -408,7 +445,8 @@ function ShowPolicyContent({
} }
export default function ShowPolicyView() { export default function ShowPolicyView() {
const [queryRef, loadQuery] = useQueryLoader<ShowPolicyViewQuery>(policyViewQuery); const [queryRef, loadQuery] =
useQueryLoader<ShowPolicyViewQuery>(policyViewQuery);
const { policyId } = useParams(); const { policyId } = useParams();
useEffect(() => { useEffect(() => {

View File

@@ -27,7 +27,10 @@ export function SignaturesList({ signatures }: SignaturesListProps) {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{signatures.map((signature) => ( {signatures.map((signature) => (
<div key={signature.id} className="bg-white rounded-lg border border-solid-b shadow-sm p-6"> <div
key={signature.id}
className="bg-white rounded-lg border border-solid-b shadow-sm p-6"
>
<div className="flex items-center gap-3 mb-4"> <div className="flex items-center gap-3 mb-4">
<Avatar className="h-10 w-10"> <Avatar className="h-10 w-10">
<AvatarImage src="" alt={signature.signedBy.fullName} /> <AvatarImage src="" alt={signature.signedBy.fullName} />
@@ -48,11 +51,13 @@ export function SignaturesList({ signatures }: SignaturesListProps) {
<div className="space-y-2"> <div className="space-y-2">
<div className="text-sm"> <div className="text-sm">
<span className="text-muted-foreground">Status:</span>{" "} <span className="text-muted-foreground">Status:</span>{" "}
<span className={`px-2 py-0.5 rounded-full ${ <span
className={`px-2 py-0.5 rounded-full ${
signature.state === "SIGNED" signature.state === "SIGNED"
? "bg-green-100 text-green-800" ? "bg-green-100 text-green-800"
: "bg-yellow-100 text-yellow-800" : "bg-yellow-100 text-yellow-800"
}`}> }`}
>
{signature.state} {signature.state}
</span> </span>
</div> </div>

View File

@@ -3,8 +3,18 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { format } from "date-fns"; import { format } from "date-fns";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { graphql, useFragment, useMutation, useQueryLoader, usePreloadedQuery, ConnectionHandler } from "react-relay"; import {
import type { SignaturesModal_policyVersions$data, SignaturesModal_policyVersions$key } from "./__generated__/SignaturesModal_policyVersions.graphql"; graphql,
useFragment,
useMutation,
useQueryLoader,
usePreloadedQuery,
ConnectionHandler,
} from "react-relay";
import type {
SignaturesModal_policyVersions$data,
SignaturesModal_policyVersions$key,
} from "./__generated__/SignaturesModal_policyVersions.graphql";
import type { SignaturesModalRequestSignatureMutation } from "./__generated__/SignaturesModalRequestSignatureMutation.graphql"; import type { SignaturesModalRequestSignatureMutation } from "./__generated__/SignaturesModalRequestSignatureMutation.graphql";
import type { SignaturesModalOrganizationQuery } from "./__generated__/SignaturesModalOrganizationQuery.graphql"; import type { SignaturesModalOrganizationQuery } from "./__generated__/SignaturesModalOrganizationQuery.graphql";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
@@ -26,7 +36,8 @@ export const policyVersionsFragment = graphql`
publishedBy { publishedBy {
fullName fullName
} }
signatures(first: 100) @connection(key: "SignaturesModal_policyVersions_signatures") { signatures(first: 100)
@connection(key: "SignaturesModal_policyVersions_signatures") {
edges { edges {
node { node {
id id
@@ -50,7 +61,10 @@ export const policyVersionsFragment = graphql`
`; `;
const requestSignatureMutation = graphql` const requestSignatureMutation = graphql`
mutation SignaturesModalRequestSignatureMutation($input: RequestSignatureInput!, $connections: [ID!]!) { mutation SignaturesModalRequestSignatureMutation(
$input: RequestSignatureInput!
$connections: [ID!]!
) {
requestSignature(input: $input) { requestSignature(input: $input) {
policyVersionSignatureEdge @prependEdge(connections: $connections) { policyVersionSignatureEdge @prependEdge(connections: $connections) {
node { node {
@@ -108,29 +122,40 @@ export function SignaturesModal({
}: SignaturesModalProps) { }: SignaturesModalProps) {
const data = useFragment<SignaturesModal_policyVersions$key>( const data = useFragment<SignaturesModal_policyVersions$key>(
policyVersionsFragment, policyVersionsFragment,
policyRef policyRef,
); );
const versionNodes = (data?.policyVersions?.edges?.map(edge => edge.node) || []); const versionNodes =
data?.policyVersions?.edges?.map((edge) => edge.node) || [];
const publishedVersions = versionNodes const publishedVersions = versionNodes
.filter(v => v.status === "PUBLISHED") .filter((v) => v.status === "PUBLISHED")
.sort((a, b) => b.version - a.version); .sort((a, b) => b.version - a.version);
const [selectedVersion, setSelectedVersion] = useState<number>( const [selectedVersion, setSelectedVersion] = useState<number>(
publishedVersions[0]?.version || 0 publishedVersions[0]?.version || 0,
); );
const selectedVersionData = versionNodes.find(v => v.version === selectedVersion); const selectedVersionData = versionNodes.find(
(v) => v.version === selectedVersion,
);
const { organizationId } = useParams(); const { organizationId } = useParams();
const [queryRef, loadQuery] = useQueryLoader<SignaturesModalOrganizationQuery>(organizationQuery); const [queryRef, loadQuery] =
useQueryLoader<SignaturesModalOrganizationQuery>(organizationQuery);
const { toast } = useToast(); const { toast } = useToast();
const [isRequesting, setIsRequesting] = useState<string | null>(null); const [isRequesting, setIsRequesting] = useState<string | null>(null);
const [commitRequestSignature] = useMutation<SignaturesModalRequestSignatureMutation>(requestSignatureMutation); const [commitRequestSignature] =
useMutation<SignaturesModalRequestSignatureMutation>(
requestSignatureMutation,
);
useEffect(() => { useEffect(() => {
if (isOpen && organizationId && selectedVersionData?.status === "PUBLISHED") { if (
isOpen &&
organizationId &&
selectedVersionData?.status === "PUBLISHED"
) {
loadQuery({ organizationId }); loadQuery({ organizationId });
} }
}, [isOpen, organizationId, loadQuery, selectedVersionData]); }, [isOpen, organizationId, loadQuery, selectedVersionData]);
@@ -141,7 +166,8 @@ export function SignaturesModal({
return format(date, "h:mm a • MMM d, yyyy"); return format(date, "h:mm a • MMM d, yyyy");
}; };
const signatures = selectedVersionData?.signatures?.edges?.map(edge => edge.node) || []; const signatures =
selectedVersionData?.signatures?.edges?.map((edge) => edge.node) || [];
const handleRequestSignature = (personId: string) => { const handleRequestSignature = (personId: string) => {
if (!selectedVersionData) return; if (!selectedVersionData) return;
@@ -157,7 +183,7 @@ export function SignaturesModal({
connections: [ connections: [
ConnectionHandler.getConnectionID( ConnectionHandler.getConnectionID(
selectedVersionData.id, selectedVersionData.id,
"SignaturesModal_policyVersions_signatures" "SignaturesModal_policyVersions_signatures",
), ),
], ],
}, },
@@ -188,7 +214,7 @@ export function SignaturesModal({
<h2 className="text-lg font-semibold p-6 pb-4">Version history</h2> <h2 className="text-lg font-semibold p-6 pb-4">Version history</h2>
<div className="overflow-y-auto"> <div className="overflow-y-auto">
{versionNodes.length > 0 ? ( {versionNodes.length > 0 ? (
versionNodes.map(version => { versionNodes.map((version) => {
const isPublished = version.status === "PUBLISHED"; const isPublished = version.status === "PUBLISHED";
const isSelected = selectedVersion === version.version; const isSelected = selectedVersion === version.version;
@@ -196,19 +222,36 @@ export function SignaturesModal({
<div <div
key={version.id} key={version.id}
className={`flex items-center gap-3 p-6 py-4 ${ className={`flex items-center gap-3 p-6 py-4 ${
isPublished ? 'cursor-pointer' : 'cursor-not-allowed opacity-50' isPublished
} ${isSelected ? 'bg-slate-50' : ''}`} ? "cursor-pointer"
onClick={() => isPublished && setSelectedVersion(version.version)} : "cursor-not-allowed opacity-50"
} ${isSelected ? "bg-slate-50" : ""}`}
onClick={() =>
isPublished && setSelectedVersion(version.version)
}
> >
<Avatar className="h-10 w-10"> <Avatar className="h-10 w-10">
<AvatarImage src="" alt={version.publishedBy?.fullName || owner?.fullName || ""} /> <AvatarImage
src=""
alt={
version.publishedBy?.fullName ||
owner?.fullName ||
""
}
/>
<AvatarFallback> <AvatarFallback>
{(version.publishedBy?.fullName || owner?.fullName || "").charAt(0)} {(
version.publishedBy?.fullName ||
owner?.fullName ||
""
).charAt(0)}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="font-medium">Version {version.version}</span> <span className="font-medium">
Version {version.version}
</span>
{isPublished ? ( {isPublished ? (
<span className="text-xs px-2 py-0.5 rounded-full bg-green-100 text-green-800"> <span className="text-xs px-2 py-0.5 rounded-full bg-green-100 text-green-800">
Published Published
@@ -220,7 +263,13 @@ export function SignaturesModal({
)} )}
</div> </div>
<div className="text-sm text-tertiary"> <div className="text-sm text-tertiary">
{version.publishedBy?.fullName || owner?.fullName || "Unknown"} • {formatDateTime(version.publishedAt || version.updatedAt)} {version.publishedBy?.fullName ||
owner?.fullName ||
"Unknown"}{" "}
•{" "}
{formatDateTime(
version.publishedAt || version.updatedAt,
)}
</div> </div>
</div> </div>
</div> </div>
@@ -239,11 +288,14 @@ export function SignaturesModal({
{/* Section header */} {/* Section header */}
<div className="p-6 border-b border-solid-b"> <div className="p-6 border-b border-solid-b">
<h2 className="text-2xl font-semibold mb-2"> <h2 className="text-2xl font-semibold mb-2">
{selectedVersionData?.status === "PUBLISHED" ? "Signatures & Requests" : "Signatures"} {selectedVersionData?.status === "PUBLISHED"
? "Signatures & Requests"
: "Signatures"}
</h2> </h2>
{selectedVersionData?.status === "PUBLISHED" && ( {selectedVersionData?.status === "PUBLISHED" && (
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Click request to ask for a signature from people in your organization Click request to ask for a signature from people in your
organization
</p> </p>
)} )}
</div> </div>
@@ -263,17 +315,23 @@ export function SignaturesModal({
<div className="space-y-4"> <div className="space-y-4">
{signatures.length > 0 ? ( {signatures.length > 0 ? (
<div className="divide-y divide-border"> <div className="divide-y divide-border">
{signatures.map(signature => ( {signatures.map((signature) => (
<div key={signature.id} className="py-4"> <div key={signature.id} className="py-4">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Avatar className="h-10 w-10"> <Avatar className="h-10 w-10">
<AvatarFallback> <AvatarFallback>
{signature.signedBy?.fullName?.charAt(0) || signature.requestedBy?.fullName?.charAt(0)} {signature.signedBy?.fullName?.charAt(0) ||
signature.requestedBy?.fullName?.charAt(
0,
)}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
<div> <div>
<div className="font-medium">{signature.signedBy?.fullName || signature.requestedBy?.fullName}</div> <div className="font-medium">
{signature.signedBy?.fullName ||
signature.requestedBy?.fullName}
</div>
<div className="text-sm text-tertiary"> <div className="text-sm text-tertiary">
{signature.state === "SIGNED" {signature.state === "SIGNED"
? `Signed on ${formatDateTime(signature.signedAt)}` ? `Signed on ${formatDateTime(signature.signedAt)}`
@@ -281,12 +339,16 @@ export function SignaturesModal({
</div> </div>
</div> </div>
</div> </div>
<span className={`text-xs px-2 py-0.5 rounded-full ${ <span
className={`text-xs px-2 py-0.5 rounded-full ${
signature.state === "SIGNED" signature.state === "SIGNED"
? 'bg-green-100 text-green-800' ? "bg-green-100 text-green-800"
: 'bg-yellow-100 text-yellow-800' : "bg-yellow-100 text-yellow-800"
}`}> }`}
{signature.state === "SIGNED" ? "Signed" : "Pending"} >
{signature.state === "SIGNED"
? "Signed"
: "Pending"}
</span> </span>
</div> </div>
</div> </div>
@@ -294,9 +356,13 @@ export function SignaturesModal({
</div> </div>
) : ( ) : (
<div className="text-center py-8"> <div className="text-center py-8">
<p className="text-tertiary">No signatures available for this version</p> <p className="text-tertiary">
No signatures available for this version
</p>
{selectedVersionData.status !== "PUBLISHED" && ( {selectedVersionData.status !== "PUBLISHED" && (
<p className="text-sm text-muted-foreground mt-2">Only published versions can have signatures</p> <p className="text-sm text-muted-foreground mt-2">
Only published versions can have signatures
</p>
)} )}
</div> </div>
)} )}
@@ -319,11 +385,7 @@ export function SignaturesModal({
</div> </div>
<div className="flex justify-end p-4 border-t border-solid-b mt-auto"> <div className="flex justify-end p-4 border-t border-solid-b mt-auto">
<Button <Button variant="outline" onClick={onClose} className="mr-2">
variant="outline"
onClick={onClose}
className="mr-2"
>
Close Close
</Button> </Button>
</div> </div>
@@ -340,25 +402,23 @@ function PeopleAndSignaturesList({
existingSignatures, existingSignatures,
formatDateTime, formatDateTime,
}: { }: {
queryRef: PreloadedQuery<SignaturesModalOrganizationQuery>, queryRef: PreloadedQuery<SignaturesModalOrganizationQuery>;
onRequestSignature: (personId: string) => void, onRequestSignature: (personId: string) => void;
requestingId: string | null, requestingId: string | null;
existingSignatures: Array<SignaturesModal_policyVersions$data["policyVersions"]["edges"][0]["node"]["signatures"]["edges"][0]["node"]>, existingSignatures: Array<
formatDateTime: (date?: string | null) => string, SignaturesModal_policyVersions$data["policyVersions"]["edges"][0]["node"]["signatures"]["edges"][0]["node"]
>;
formatDateTime: (date?: string | null) => string;
}) { }) {
const data = usePreloadedQuery<SignaturesModalOrganizationQuery>( const data = usePreloadedQuery<SignaturesModalOrganizationQuery>(
organizationQuery, organizationQuery,
queryRef queryRef,
); );
const people = data?.organization?.peoples?.edges || []; const people = data?.organization?.peoples?.edges || [];
if (!data?.organization) { if (!data?.organization) {
return ( return <div className="text-center py-8">Organization not found</div>;
<div className="text-center py-8">
Organization not found
</div>
);
} }
if (people.length === 0) { if (people.length === 0) {
@@ -371,7 +431,7 @@ function PeopleAndSignaturesList({
// Create lookup for signatures by person ID // Create lookup for signatures by person ID
const signaturesByPersonId = new Map(); const signaturesByPersonId = new Map();
existingSignatures.forEach(sig => { existingSignatures.forEach((sig) => {
if (sig.signedBy?.id) { if (sig.signedBy?.id) {
signaturesByPersonId.set(sig.signedBy.id, sig); signaturesByPersonId.set(sig.signedBy.id, sig);
} }
@@ -389,9 +449,7 @@ function PeopleAndSignaturesList({
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Avatar className="h-10 w-10"> <Avatar className="h-10 w-10">
<AvatarFallback> <AvatarFallback>{person.fullName.charAt(0)}</AvatarFallback>
{person.fullName.charAt(0)}
</AvatarFallback>
</Avatar> </Avatar>
<div> <div>
<div className="font-medium">{person.fullName}</div> <div className="font-medium">{person.fullName}</div>
@@ -400,12 +458,16 @@ function PeopleAndSignaturesList({
signature.state === "SIGNED" ? ( signature.state === "SIGNED" ? (
<div className="flex items-center gap-1 text-green-700"> <div className="flex items-center gap-1 text-green-700">
<CheckCircle2 className="h-3.5 w-3.5" /> <CheckCircle2 className="h-3.5 w-3.5" />
<span>Signed on {formatDateTime(signature.signedAt)}</span> <span>
Signed on {formatDateTime(signature.signedAt)}
</span>
</div> </div>
) : ( ) : (
<div className="flex items-center gap-1 text-amber-700"> <div className="flex items-center gap-1 text-amber-700">
<Clock className="h-3.5 w-3.5" /> <Clock className="h-3.5 w-3.5" />
<span>Requested on {formatDateTime(signature.requestedAt)}</span> <span>
Requested on {formatDateTime(signature.requestedAt)}
</span>
</div> </div>
) )
) : ( ) : (
@@ -416,11 +478,13 @@ function PeopleAndSignaturesList({
</div> </div>
{signature ? ( {signature ? (
<span className={`text-xs px-2 py-0.5 rounded-full ${ <span
className={`text-xs px-2 py-0.5 rounded-full ${
signature.state === "SIGNED" signature.state === "SIGNED"
? 'bg-green-100 text-green-800' ? "bg-green-100 text-green-800"
: 'bg-yellow-100 text-yellow-800' : "bg-yellow-100 text-yellow-800"
}`}> }`}
>
{signature.state === "SIGNED" ? "Signed" : "Pending"} {signature.state === "SIGNED" ? "Signed" : "Pending"}
</span> </span>
) : ( ) : (

View File

@@ -44,15 +44,16 @@ export function VersionHistoryModal({
isOpen, isOpen,
onClose, onClose,
policyRef, policyRef,
onRestoreVersion onRestoreVersion,
}: VersionHistoryModalProps) { }: VersionHistoryModalProps) {
const data = useFragment<VersionHistoryModal_policyVersions$key>( const data = useFragment<VersionHistoryModal_policyVersions$key>(
policyVersionsFragment, policyVersionsFragment,
policyRef policyRef,
); );
// Safely access and extract version nodes // Safely access and extract version nodes
const versionNodes = data?.versionHistory?.edges?.map(edge => edge.node) || []; const versionNodes =
data?.versionHistory?.edges?.map((edge) => edge.node) || [];
// Sort versions by version number (descending) // Sort versions by version number (descending)
const versions = [...versionNodes].sort((a, b) => b.version - a.version); const versions = [...versionNodes].sort((a, b) => b.version - a.version);
@@ -78,18 +79,31 @@ export function VersionHistoryModal({
{versions.map((version) => ( {versions.map((version) => (
<div <div
key={version.id} key={version.id}
className={`flex items-center gap-3 p-6 py-4 cursor-pointer ${selectedVersion === version.version ? 'bg-slate-50' : ''}`} className={`flex items-center gap-3 p-6 py-4 cursor-pointer ${selectedVersion === version.version ? "bg-slate-50" : ""}`}
onClick={() => setSelectedVersion(version.version)} onClick={() => setSelectedVersion(version.version)}
> >
<Avatar className="h-10 w-10"> <Avatar className="h-10 w-10">
<AvatarImage src="" alt={version.publishedBy?.fullName || data.owner?.fullName || ""} /> <AvatarImage
src=""
alt={
version.publishedBy?.fullName ||
data.owner?.fullName ||
""
}
/>
<AvatarFallback> <AvatarFallback>
{(version.publishedBy?.fullName || data.owner?.fullName || "").charAt(0)} {(
version.publishedBy?.fullName ||
data.owner?.fullName ||
""
).charAt(0)}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="font-medium">Version {version.version}</span> <span className="font-medium">
Version {version.version}
</span>
{version.status === "PUBLISHED" ? ( {version.status === "PUBLISHED" ? (
<span className="text-xs px-2 py-0.5 rounded-full bg-green-100 text-green-800"> <span className="text-xs px-2 py-0.5 rounded-full bg-green-100 text-green-800">
Published Published
@@ -101,7 +115,11 @@ export function VersionHistoryModal({
)} )}
</div> </div>
<div className="text-sm text-tertiary"> <div className="text-sm text-tertiary">
{version.publishedBy?.fullName || data.owner?.fullName || "Unknown"} • {formatDateTime(version.publishedAt || version.updatedAt)} {version.publishedBy?.fullName ||
data.owner?.fullName ||
"Unknown"}{" "}
•{" "}
{formatDateTime(version.publishedAt || version.updatedAt)}
</div> </div>
</div> </div>
</div> </div>
@@ -116,7 +134,8 @@ export function VersionHistoryModal({
<div className="prose prose-olive max-w-none"> <div className="prose prose-olive max-w-none">
{selectedVersion && ( {selectedVersion && (
<ReactMarkdown remarkPlugins={[remarkGfm]}> <ReactMarkdown remarkPlugins={[remarkGfm]}>
{versions.find(v => v.version === selectedVersion)?.content || "No content available"} {versions.find((v) => v.version === selectedVersion)
?.content || "No content available"}
</ReactMarkdown> </ReactMarkdown>
)} )}
</div> </div>
@@ -124,11 +143,7 @@ export function VersionHistoryModal({
</div> </div>
<div className="flex justify-end p-4 border-t border-solid-b mt-auto"> <div className="flex justify-end p-4 border-t border-solid-b mt-auto">
<Button <Button variant="outline" onClick={onClose} className="mr-2">
variant="outline"
onClick={onClose}
className="mr-2"
>
Cancel Cancel
</Button> </Button>

View File

@@ -56,7 +56,7 @@ const RISK_CATEGORIES = [
"Reputational", "Reputational",
"Strategic", "Strategic",
"Technology", "Technology",
"Other" "Other",
] as const; ] as const;
const listRiskViewQuery = graphql` const listRiskViewQuery = graphql`
@@ -143,7 +143,9 @@ const calculateRiskLevel = (score: number): string => {
}; };
// Helper function to get color for risk level that matches the matrix colors // Helper function to get color for risk level that matches the matrix colors
const getRiskLevelColor = (level: string): {backgroundColor: string, color: string} => { const getRiskLevelColor = (
level: string,
): { backgroundColor: string; color: string } => {
switch (level) { switch (level) {
case "High": case "High":
return { backgroundColor: "#ef4444", color: "#ffffff" }; // red-500 return { backgroundColor: "#ef4444", color: "#ffffff" }; // red-500
@@ -234,7 +236,6 @@ const emptyRiskMatrixColors = {
high: "bg-red-50 text-black", high: "bg-red-50 text-black",
}; };
// Risk Matrix Component // Risk Matrix Component
function RiskMatrix({ function RiskMatrix({
risks, risks,
@@ -278,10 +279,10 @@ function RiskMatrix({
const getCellContent = (impactValue: number, likelihoodValue: number) => { const getCellContent = (impactValue: number, likelihoodValue: number) => {
return risks.filter((risk) => { return risks.filter((risk) => {
const likelihood = isResidual const likelihood = isResidual
? risk.residualLikelihood ?? risk.inherentLikelihood ? (risk.residualLikelihood ?? risk.inherentLikelihood)
: risk.inherentLikelihood; : risk.inherentLikelihood;
const impact = isResidual const impact = isResidual
? risk.residualImpact ?? risk.inherentImpact ? (risk.residualImpact ?? risk.inherentImpact)
: risk.inherentImpact; : risk.inherentImpact;
return likelihood === likelihoodValue && impact === impactValue; return likelihood === likelihoodValue && impact === impactValue;
@@ -298,13 +299,37 @@ function RiskMatrix({
// This follows the 5x5 grid with rows representing impact (5 to 1) and columns representing likelihood (1 to 5) // This follows the 5x5 grid with rows representing impact (5 to 1) and columns representing likelihood (1 to 5)
const colorMatrix = [ const colorMatrix = [
// Impact 5 (Catastrophic) - first row // Impact 5 (Catastrophic) - first row
[colorSet.low, colorSet.medium, colorSet.high, colorSet.high, colorSet.high], [
colorSet.low,
colorSet.medium,
colorSet.high,
colorSet.high,
colorSet.high,
],
// Impact 4 (Significant) - second row // Impact 4 (Significant) - second row
[colorSet.low, colorSet.medium, colorSet.medium, colorSet.high, colorSet.high], [
colorSet.low,
colorSet.medium,
colorSet.medium,
colorSet.high,
colorSet.high,
],
// Impact 3 (Moderate) - third row // Impact 3 (Moderate) - third row
[colorSet.low, colorSet.low, colorSet.medium, colorSet.medium, colorSet.high], [
colorSet.low,
colorSet.low,
colorSet.medium,
colorSet.medium,
colorSet.high,
],
// Impact 2 (Low) - fourth row // Impact 2 (Low) - fourth row
[colorSet.low, colorSet.low, colorSet.low, colorSet.medium, colorSet.medium], [
colorSet.low,
colorSet.low,
colorSet.low,
colorSet.medium,
colorSet.medium,
],
// Impact 1 (Negligible) - fifth row // Impact 1 (Negligible) - fifth row
[colorSet.low, colorSet.low, colorSet.low, colorSet.low, colorSet.low], [colorSet.low, colorSet.low, colorSet.low, colorSet.low, colorSet.low],
]; ];
@@ -373,11 +398,14 @@ function RiskMatrix({
<div className="text-sm pt-1 border-t border-gray-200 mt-1"> <div className="text-sm pt-1 border-t border-gray-200 mt-1">
<span className="text-muted-foreground">Risk Level:</span> <span className="text-muted-foreground">Risk Level:</span>
<span className="font-bold ml-1"> <span className="font-bold ml-1">
{calculateRiskLevel(impactValue * likelihoodValue)} ({impactValue * likelihoodValue}) {calculateRiskLevel(impactValue * likelihoodValue)} (
{impactValue * likelihoodValue})
</span> </span>
<span <span
className="ml-2 px-2 py-0.5 text-xs rounded-full font-medium inline-block" className="ml-2 px-2 py-0.5 text-xs rounded-full font-medium inline-block"
style={getRiskLevelColor(calculateRiskLevel(impactValue * likelihoodValue))} style={getRiskLevelColor(
calculateRiskLevel(impactValue * likelihoodValue),
)}
> >
{calculateRiskLevel(impactValue * likelihoodValue)} {calculateRiskLevel(impactValue * likelihoodValue)}
</span> </span>
@@ -393,15 +421,15 @@ function RiskMatrix({
key={risk.id} key={risk.id}
className="text-sm border-l-2 pl-2" className="text-sm border-l-2 pl-2"
style={{ style={{
borderColor: borderColor: getRiskLevelColor(
getRiskLevelColor(
calculateRiskLevel( calculateRiskLevel(
isResidual isResidual
? (risk.residualImpact || risk.inherentImpact) * ? (risk.residualImpact || risk.inherentImpact) *
(risk.residualLikelihood || risk.inherentLikelihood) (risk.residualLikelihood ||
: risk.inherentImpact * risk.inherentLikelihood risk.inherentLikelihood)
) : risk.inherentImpact * risk.inherentLikelihood,
).backgroundColor ),
).backgroundColor,
}} }}
> >
<Link <Link
@@ -516,7 +544,9 @@ function ListRiskViewContent({
const [showResidualRisk, setShowResidualRisk] = useState(false); const [showResidualRisk, setShowResidualRisk] = useState(false);
// State for category filters // State for category filters
const [selectedCategories, setSelectedCategories] = useState<Set<string>>(new Set()); const [selectedCategories, setSelectedCategories] = useState<Set<string>>(
new Set(),
);
// Setup delete mutation // Setup delete mutation
const [commitDeleteMutation] = const [commitDeleteMutation] =
@@ -540,16 +570,19 @@ function ListRiskViewContent({
const connectionId = risksConnection?.risks?.__id; const connectionId = risksConnection?.risks?.__id;
// Get unique categories from existing risks // Get unique categories from existing risks
const availableCategories = Array.from(new Set(risks.map(risk => risk.category))).sort(); const availableCategories = Array.from(
new Set(risks.map((risk) => risk.category)),
).sort();
// Filter risks based on selected categories // Filter risks based on selected categories
const filteredRisks = selectedCategories.size === 0 const filteredRisks =
selectedCategories.size === 0
? risks ? risks
: risks.filter(risk => selectedCategories.has(risk.category)); : risks.filter((risk) => selectedCategories.has(risk.category));
// Handle category toggle // Handle category toggle
const toggleCategory = (category: string) => { const toggleCategory = (category: string) => {
setSelectedCategories(prev => { setSelectedCategories((prev) => {
const newSet = new Set(prev); const newSet = new Set(prev);
if (newSet.has(category)) { if (newSet.has(category)) {
newSet.delete(category); newSet.delete(category);
@@ -761,9 +794,16 @@ function ListRiskViewContent({
> >
<span <span
className="px-2 py-0.5 text-xs rounded-full font-medium inline-block" className="px-2 py-0.5 text-xs rounded-full font-medium inline-block"
style={getRiskLevelColor(calculateRiskLevel(risk.inherentLikelihood * risk.inherentImpact))} style={getRiskLevelColor(
calculateRiskLevel(
risk.inherentLikelihood * risk.inherentImpact,
),
)}
> >
{calculateRiskLevel(risk.inherentLikelihood * risk.inherentImpact)} ({risk.inherentLikelihood * risk.inherentImpact}) {calculateRiskLevel(
risk.inherentLikelihood * risk.inherentImpact,
)}{" "}
({risk.inherentLikelihood * risk.inherentImpact})
</span> </span>
</Link> </Link>
</td> </td>
@@ -783,9 +823,18 @@ function ListRiskViewContent({
{risk.residualLikelihood && risk.residualImpact ? ( {risk.residualLikelihood && risk.residualImpact ? (
<span <span
className="px-2 py-0.5 text-xs rounded-full font-medium inline-block" className="px-2 py-0.5 text-xs rounded-full font-medium inline-block"
style={getRiskLevelColor(calculateRiskLevel(risk.residualLikelihood * risk.residualImpact))} style={getRiskLevelColor(
calculateRiskLevel(
risk.residualLikelihood *
risk.residualImpact,
),
)}
> >
{calculateRiskLevel(risk.residualLikelihood * risk.residualImpact)} ({risk.residualLikelihood * risk.residualImpact}) {calculateRiskLevel(
risk.residualLikelihood * risk.residualImpact,
)}{" "}
({risk.residualLikelihood * risk.residualImpact}
)
</span> </span>
) : ( ) : (
"Not set" "Not set"

View File

@@ -107,7 +107,7 @@ function NewRiskForm({
const [createRisk, isInFlight] = useMutation(createRiskMutation); const [createRisk, isInFlight] = useMutation(createRiskMutation);
const categories = useMemo( const categories = useMemo(
() => Array.from(new Set(riskTemplates.map(t => t.category))), () => Array.from(new Set(riskTemplates.map((t) => t.category))),
[riskTemplates], [riskTemplates],
); );
@@ -115,13 +115,13 @@ function NewRiskForm({
() => () =>
riskTemplates riskTemplates
.map((t, idx) => ({ ...t, originalIndex: idx })) .map((t, idx) => ({ ...t, originalIndex: idx }))
.filter(t => !selectedCategory || t.category === selectedCategory), .filter((t) => !selectedCategory || t.category === selectedCategory),
[riskTemplates, selectedCategory], [riskTemplates, selectedCategory],
); );
// Handle category selection // Handle category selection
const selectCategory = (category: string) => { const selectCategory = (category: string) => {
setSelectedCategory(prev => { setSelectedCategory((prev) => {
if (prev !== category) setSelectedTemplate(""); if (prev !== category) setSelectedTemplate("");
return category; return category;
}); });
@@ -226,7 +226,7 @@ function NewRiskForm({
connections: [ connections: [
ConnectionHandler.getConnectionID( ConnectionHandler.getConnectionID(
organizationId!, organizationId!,
"ListRiskView_risks" "ListRiskView_risks",
), ),
], ],
}, },
@@ -289,7 +289,9 @@ function NewRiskForm({
categories.map((category) => ( categories.map((category) => (
<Button <Button
key={category} key={category}
variant={selectedCategory === category ? "default" : "outline"} variant={
selectedCategory === category ? "default" : "outline"
}
size="sm" size="sm"
onClick={() => selectCategory(category)} onClick={() => selectCategory(category)}
className="rounded-full" className="rounded-full"
@@ -319,7 +321,8 @@ function NewRiskForm({
</SelectContent> </SelectContent>
</Select> </Select>
<p className="text-sm text-tertiary"> <p className="text-sm text-tertiary">
Select a risk template to prefill the form or create a custom risk. Select a risk template to prefill the form or create a custom
risk.
</p> </p>
</div> </div>
</div> </div>
@@ -360,7 +363,9 @@ function NewRiskForm({
required={true} required={true}
/> />
{ownerError && ( {ownerError && (
<p className="text-sm text-destructive mt-1">Please select a risk owner</p> <p className="text-sm text-destructive mt-1">
Please select a risk owner
</p>
)} )}
</div> </div>

View File

@@ -216,7 +216,9 @@ const calculateRiskLevel = (score: number): string => {
}; };
// Helper function to get color for risk level that matches the matrix colors // Helper function to get color for risk level that matches the matrix colors
const getRiskLevelColor = (level: string): {backgroundColor: string, color: string} => { const getRiskLevelColor = (
level: string,
): { backgroundColor: string; color: string } => {
switch (level) { switch (level) {
case "High": case "High":
return { backgroundColor: "#ef4444", color: "#ffffff" }; // red-500 return { backgroundColor: "#ef4444", color: "#ffffff" }; // red-500
@@ -245,26 +247,46 @@ function getLikelihoodLabel(likelihood: number): {
if (likelihood === 5) if (likelihood === 5)
return { return {
label: "Frequent (5)", label: "Frequent (5)",
style: { backgroundColor: "#fff", color: "black", border: "1px solid #e2e8f0" }, style: {
backgroundColor: "#fff",
color: "black",
border: "1px solid #e2e8f0",
},
}; };
if (likelihood === 4) if (likelihood === 4)
return { return {
label: "Probable (4)", label: "Probable (4)",
style: { backgroundColor: "#fff", color: "black", border: "1px solid #e2e8f0" }, style: {
backgroundColor: "#fff",
color: "black",
border: "1px solid #e2e8f0",
},
}; };
if (likelihood === 3) if (likelihood === 3)
return { return {
label: "Occasional (3)", label: "Occasional (3)",
style: { backgroundColor: "#fff", color: "black", border: "1px solid #e2e8f0" }, style: {
backgroundColor: "#fff",
color: "black",
border: "1px solid #e2e8f0",
},
}; };
if (likelihood === 2) if (likelihood === 2)
return { return {
label: "Remote (2)", label: "Remote (2)",
style: { backgroundColor: "#fff", color: "black", border: "1px solid #e2e8f0" }, style: {
backgroundColor: "#fff",
color: "black",
border: "1px solid #e2e8f0",
},
}; };
return { return {
label: "Improbable (1)", label: "Improbable (1)",
style: { backgroundColor: "#fff", color: "black", border: "1px solid #e2e8f0" }, style: {
backgroundColor: "#fff",
color: "black",
border: "1px solid #e2e8f0",
},
}; };
} }
@@ -275,26 +297,46 @@ function getImpactLabel(impact: number): {
if (impact === 5) if (impact === 5)
return { return {
label: "Catastrophic (5)", label: "Catastrophic (5)",
style: { backgroundColor: "#fff", color: "black", border: "1px solid #e2e8f0" }, style: {
backgroundColor: "#fff",
color: "black",
border: "1px solid #e2e8f0",
},
}; };
if (impact === 4) if (impact === 4)
return { return {
label: "Significant (4)", label: "Significant (4)",
style: { backgroundColor: "#fff", color: "black", border: "1px solid #e2e8f0" }, style: {
backgroundColor: "#fff",
color: "black",
border: "1px solid #e2e8f0",
},
}; };
if (impact === 3) if (impact === 3)
return { return {
label: "Moderate (3)", label: "Moderate (3)",
style: { backgroundColor: "#fff", color: "black", border: "1px solid #e2e8f0" }, style: {
backgroundColor: "#fff",
color: "black",
border: "1px solid #e2e8f0",
},
}; };
if (impact === 2) if (impact === 2)
return { return {
label: "Low (2)", label: "Low (2)",
style: { backgroundColor: "#fff", color: "black", border: "1px solid #e2e8f0" }, style: {
backgroundColor: "#fff",
color: "black",
border: "1px solid #e2e8f0",
},
}; };
return { return {
label: "Negligible (1)", label: "Negligible (1)",
style: { backgroundColor: "#fff", color: "black", border: "1px solid #e2e8f0" }, style: {
backgroundColor: "#fff",
color: "black",
border: "1px solid #e2e8f0",
},
}; };
} }
@@ -307,7 +349,7 @@ function ShowRiskViewContent({
}) { }) {
const data = usePreloadedQuery<ShowRiskViewQuery>( const data = usePreloadedQuery<ShowRiskViewQuery>(
showRiskViewQuery, showRiskViewQuery,
queryRef queryRef,
); );
const { toast } = useToast(); const { toast } = useToast();
const environment = useRelayEnvironment(); const environment = useRelayEnvironment();
@@ -317,14 +359,14 @@ function ShowRiskViewContent({
const risk = data.node; const risk = data.node;
const severity = getRiskSeverity( const severity = getRiskSeverity(
risk.inherentLikelihood!, risk.inherentLikelihood!,
risk.inherentImpact! risk.inherentImpact!,
); );
// Fix typing for measures // Fix typing for measures
const measures = risk.measures?.edges?.map((edge) => edge.node) || []; const measures = risk.measures?.edges?.map((edge) => edge.node) || [];
const residualSeverity = getRiskSeverity( const residualSeverity = getRiskSeverity(
risk.residualLikelihood!, risk.residualLikelihood!,
risk.residualImpact! risk.residualImpact!,
); );
// Fix typing for policies // Fix typing for policies
@@ -340,9 +382,9 @@ function ShowRiskViewContent({
const [measureSearchQuery, setMeasureSearchQuery] = useState(""); const [measureSearchQuery, setMeasureSearchQuery] = useState("");
const [categoryFilter, setCategoryFilter] = useState<string | null>(null); const [categoryFilter, setCategoryFilter] = useState<string | null>(null);
const [isLoadingMeasures, setIsLoadingMeasures] = useState(false); const [isLoadingMeasures, setIsLoadingMeasures] = useState(false);
const [linkingMeasures, setLinkingMeasures] = useState<Record<string, boolean>>( const [linkingMeasures, setLinkingMeasures] = useState<
{} Record<string, boolean>
); >({});
const [unlinkingMeasures, setUnlinkingMeasures] = useState< const [unlinkingMeasures, setUnlinkingMeasures] = useState<
Record<string, boolean> Record<string, boolean>
>({}); >({});
@@ -363,21 +405,21 @@ function ShowRiskViewContent({
// Setup mutation hooks // Setup mutation hooks
const [createRiskMeasureMapping] = const [createRiskMeasureMapping] =
useMutation<ShowRiskViewCreateRiskMeasureMappingMutation>( useMutation<ShowRiskViewCreateRiskMeasureMappingMutation>(
createRiskMeasureMappingMutation createRiskMeasureMappingMutation,
); );
const [deleteRiskMeasureMapping] = const [deleteRiskMeasureMapping] =
useMutation<ShowRiskViewDeleteRiskMeasureMappingMutation>( useMutation<ShowRiskViewDeleteRiskMeasureMappingMutation>(
deleteRiskMeasureMappingMutation deleteRiskMeasureMappingMutation,
); );
// Setup policy mutation hooks // Setup policy mutation hooks
const [createRiskPolicyMapping] = const [createRiskPolicyMapping] =
useMutation<ShowRiskViewCreateRiskPolicyMappingMutation>( useMutation<ShowRiskViewCreateRiskPolicyMappingMutation>(
createRiskPolicyMappingMutation createRiskPolicyMappingMutation,
); );
const [deleteRiskPolicyMapping] = const [deleteRiskPolicyMapping] =
useMutation<ShowRiskViewDeleteRiskPolicyMappingMutation>( useMutation<ShowRiskViewDeleteRiskPolicyMappingMutation>(
deleteRiskPolicyMappingMutation deleteRiskPolicyMappingMutation,
); );
// Clear filters when dialog closes // Clear filters when dialog closes
@@ -411,7 +453,7 @@ function ShowRiskViewContent({
organizationMeasuresQuery, organizationMeasuresQuery,
{ {
organizationId, organizationId,
} },
).subscribe({ ).subscribe({
next: (data) => { next: (data) => {
setOrganizationMeasuresData(data); setOrganizationMeasuresData(data);
@@ -441,7 +483,7 @@ function ShowRiskViewContent({
organizationPoliciesQuery, organizationPoliciesQuery,
{ {
organizationId, organizationId,
} },
).subscribe({ ).subscribe({
next: (data) => { next: (data) => {
setOrganizationPoliciesData(data); setOrganizationPoliciesData(data);
@@ -463,7 +505,7 @@ function ShowRiskViewContent({
const getMeasures = useCallback(() => { const getMeasures = useCallback(() => {
if (!organizationMeasuresData?.organization?.measures?.edges) return []; if (!organizationMeasuresData?.organization?.measures?.edges) return [];
return organizationMeasuresData.organization.measures.edges.map( return organizationMeasuresData.organization.measures.edges.map(
(edge) => edge.node (edge) => edge.node,
); );
}, [organizationMeasuresData]); }, [organizationMeasuresData]);
@@ -508,7 +550,7 @@ function ShowRiskViewContent({
const getPolicies = useCallback(() => { const getPolicies = useCallback(() => {
if (!organizationPoliciesData?.organization?.policies?.edges) return []; if (!organizationPoliciesData?.organization?.policies?.edges) return [];
return organizationPoliciesData.organization.policies.edges.map( return organizationPoliciesData.organization.policies.edges.map(
(edge) => edge.node (edge) => edge.node,
); );
}, [organizationPoliciesData]); }, [organizationPoliciesData]);
@@ -531,7 +573,7 @@ function ShowRiskViewContent({
NonNullable< NonNullable<
ShowRiskViewOrganizationMeasuresQuery$data["organization"] ShowRiskViewOrganizationMeasuresQuery$data["organization"]
>["measures"] >["measures"]
>["edges"][0]["node"] >["edges"][0]["node"],
) => { ) => {
if (!risk.id) return; if (!risk.id) return;
@@ -593,7 +635,7 @@ function ShowRiskViewContent({
}, },
}); });
}, },
[risk.id, createRiskMeasureMapping, toast, environment, loadQuery] [risk.id, createRiskMeasureMapping, toast, environment, loadQuery],
); );
// Handle unlinking a measure from this risk // Handle unlinking a measure from this risk
@@ -603,7 +645,7 @@ function ShowRiskViewContent({
NonNullable< NonNullable<
ShowRiskViewOrganizationMeasuresQuery$data["organization"] ShowRiskViewOrganizationMeasuresQuery$data["organization"]
>["measures"] >["measures"]
>["edges"][0]["node"] >["edges"][0]["node"],
) => { ) => {
if (!risk.id) return; if (!risk.id) return;
@@ -665,7 +707,7 @@ function ShowRiskViewContent({
}, },
}); });
}, },
[risk.id, deleteRiskMeasureMapping, toast, environment, loadQuery] [risk.id, deleteRiskMeasureMapping, toast, environment, loadQuery],
); );
// Handle linking a policy to this risk // Handle linking a policy to this risk
@@ -675,7 +717,7 @@ function ShowRiskViewContent({
NonNullable< NonNullable<
ShowRiskViewOrganizationPoliciesQuery$data["organization"] ShowRiskViewOrganizationPoliciesQuery$data["organization"]
>["policies"] >["policies"]
>["edges"][0]["node"] >["edges"][0]["node"],
) => { ) => {
if (!risk.id) return; if (!risk.id) return;
@@ -737,7 +779,7 @@ function ShowRiskViewContent({
}, },
}); });
}, },
[risk.id, createRiskPolicyMapping, toast, environment, loadQuery] [risk.id, createRiskPolicyMapping, toast, environment, loadQuery],
); );
// Handle unlinking a policy from this risk // Handle unlinking a policy from this risk
@@ -747,7 +789,7 @@ function ShowRiskViewContent({
NonNullable< NonNullable<
ShowRiskViewOrganizationPoliciesQuery$data["organization"] ShowRiskViewOrganizationPoliciesQuery$data["organization"]
>["policies"] >["policies"]
>["edges"][0]["node"] >["edges"][0]["node"],
) => { ) => {
if (!risk.id) return; if (!risk.id) return;
@@ -809,7 +851,7 @@ function ShowRiskViewContent({
}, },
}); });
}, },
[risk.id, deleteRiskPolicyMapping, toast, environment, loadQuery] [risk.id, deleteRiskPolicyMapping, toast, environment, loadQuery],
); );
return ( return (
@@ -916,7 +958,8 @@ function ShowRiskViewContent({
className="px-5 py-2 rounded-md font-medium text-sm flex justify-center items-center whitespace-nowrap" className="px-5 py-2 rounded-md font-medium text-sm flex justify-center items-center whitespace-nowrap"
style={severity.style} style={severity.style}
> >
{severity.level} ({risk.inherentLikelihood! * risk.inherentImpact!}) {severity.level} (
{risk.inherentLikelihood! * risk.inherentImpact!})
</div> </div>
</div> </div>
</div> </div>
@@ -953,7 +996,8 @@ function ShowRiskViewContent({
className="px-5 py-2 rounded-md font-medium text-sm flex justify-center items-center whitespace-nowrap" className="px-5 py-2 rounded-md font-medium text-sm flex justify-center items-center whitespace-nowrap"
style={residualSeverity.style} style={residualSeverity.style}
> >
{residualSeverity.level} ({risk.residualLikelihood! * risk.residualImpact!}) {residualSeverity.level} (
{risk.residualLikelihood! * risk.residualImpact!})
</div> </div>
</div> </div>
</div> </div>
@@ -964,7 +1008,16 @@ function ShowRiskViewContent({
risk.inherentImpact !== risk.residualImpact) && ( risk.inherentImpact !== risk.residualImpact) && (
<div className="mt-6 text-center pt-4 border-t"> <div className="mt-6 text-center pt-4 border-t">
<p className="text-sm text-slate-600"> <p className="text-sm text-slate-600">
Risk severity reduced from <span className="font-semibold">{severity.level} ({risk.inherentLikelihood! * risk.inherentImpact!})</span> to <span className="font-semibold">{residualSeverity.level} ({risk.residualLikelihood! * risk.residualImpact!})</span> Risk severity reduced from{" "}
<span className="font-semibold">
{severity.level} (
{risk.inherentLikelihood! * risk.inherentImpact!})
</span>{" "}
to{" "}
<span className="font-semibold">
{residualSeverity.level} (
{risk.residualLikelihood! * risk.residualImpact!})
</span>
{risk.treatment === "MITIGATED" && {risk.treatment === "MITIGATED" &&
risk.measures?.edges && risk.measures?.edges &&
risk.measures.edges.length > 0 && risk.measures.edges.length > 0 &&
@@ -987,9 +1040,7 @@ function ShowRiskViewContent({
<CardTitle>Note</CardTitle> <CardTitle>Note</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="prose prose-sm max-w-none"> <div className="prose prose-sm max-w-none">{risk.note}</div>
{risk.note}
</div>
</CardContent> </CardContent>
</Card> </Card>
)} )}
@@ -1153,7 +1204,10 @@ function ShowRiskViewContent({
</Tabs> </Tabs>
{/* Dialog for linking measures */} {/* Dialog for linking measures */}
<Dialog open={isMeasureDialogOpen} onOpenChange={setIsMeasureDialogOpen}> <Dialog
open={isMeasureDialogOpen}
onOpenChange={setIsMeasureDialogOpen}
>
<DialogContent className="max-w-3xl"> <DialogContent className="max-w-3xl">
<DialogHeader> <DialogHeader>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
@@ -1209,10 +1263,11 @@ function ShowRiskViewContent({
{filteredMeasures().map((measure) => { {filteredMeasures().map((measure) => {
// For each render, recalculate linked status directly against the current risk data // For each render, recalculate linked status directly against the current risk data
const isLinked = risk.measures?.edges?.some( const isLinked = risk.measures?.edges?.some(
(edge) => edge.node.id === measure.id (edge) => edge.node.id === measure.id,
); );
const isLinking = linkingMeasures[measure.id] || false; const isLinking = linkingMeasures[measure.id] || false;
const isUnlinking = unlinkingMeasures[measure.id] || false; const isUnlinking =
unlinkingMeasures[measure.id] || false;
return ( return (
<div <div
@@ -1310,7 +1365,7 @@ function ShowRiskViewContent({
{filteredPolicies().map((policy) => { {filteredPolicies().map((policy) => {
// For each render, recalculate linked status directly against the current risk data // For each render, recalculate linked status directly against the current risk data
const isLinked = policies.some( const isLinked = policies.some(
(riskPolicy) => riskPolicy.id === policy.id (riskPolicy) => riskPolicy.id === policy.id,
); );
const isLinking = linkingPolicies[policy.id] || false; const isLinking = linkingPolicies[policy.id] || false;
const isUnlinking = unlinkingPolicies[policy.id] || false; const isUnlinking = unlinkingPolicies[policy.id] || false;

View File

@@ -75,7 +75,10 @@ const vendorListFragment = graphql`
websiteUrl websiteUrl
createdAt createdAt
updatedAt updatedAt
riskAssessments(first: 1, orderBy: { direction: DESC, field: ASSESSED_AT }) { riskAssessments(
first: 1
orderBy: { direction: DESC, field: ASSESSED_AT }
) {
edges { edges {
node { node {
id id
@@ -191,7 +194,7 @@ function ListVendorContent({
const { toast } = useToast(); const { toast } = useToast();
const data = usePreloadedQuery<ListVendorViewQuery>( const data = usePreloadedQuery<ListVendorViewQuery>(
listVendorViewQuery, listVendorViewQuery,
queryRef queryRef,
); );
const [, setSearchParams] = useSearchParams(); const [, setSearchParams] = useSearchParams();
const [, startTransition] = useTransition(); const [, startTransition] = useTransition();
@@ -263,11 +266,11 @@ function ListVendorContent({
// Helper to format date (similar to "Mon, 8 Mar. 2025" in the design) // Helper to format date (similar to "Mon, 8 Mar. 2025" in the design)
const formatDate = (dateString: string) => { const formatDate = (dateString: string) => {
const date = new Date(dateString); const date = new Date(dateString);
return date.toLocaleDateString('en-US', { return date.toLocaleDateString("en-US", {
weekday: 'short', weekday: "short",
day: 'numeric', day: "numeric",
month: 'short', month: "short",
year: 'numeric' year: "numeric",
}); });
}; };
@@ -311,7 +314,7 @@ function ListVendorContent({
dataSensitivity: null, dataSensitivity: null,
businessImpact: null, businessImpact: null,
isExpired: false, isExpired: false,
assessedAt: null assessedAt: null,
}; };
} }
@@ -324,7 +327,7 @@ function ListVendorContent({
dataSensitivity: latestAssessment.dataSensitivity, dataSensitivity: latestAssessment.dataSensitivity,
businessImpact: latestAssessment.businessImpact, businessImpact: latestAssessment.businessImpact,
isExpired, isExpired,
assessedAt: latestAssessment.assessedAt assessedAt: latestAssessment.assessedAt,
}; };
}; };
@@ -411,11 +414,11 @@ function ListVendorContent({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{vendor.websiteUrl && ( {vendor.websiteUrl && (
<img <img
src={getFaviconUrl(vendor.websiteUrl) || ''} src={getFaviconUrl(vendor.websiteUrl) || ""}
alt="" alt=""
className="w-4 h-4" className="w-4 h-4"
onError={(e) => { onError={(e) => {
e.currentTarget.style.display = 'none'; e.currentTarget.style.display = "none";
}} }}
/> />
)} )}
@@ -450,7 +453,8 @@ function ListVendorContent({
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
<span className="font-medium">Create new vendor:</span> {searchTerm} <span className="font-medium">Create new vendor:</span>{" "}
{searchTerm}
</div> </div>
</button> </button>
</div> </div>
@@ -538,7 +542,9 @@ function ListVendorContent({
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Avatar className="h-8 w-8 bg-white"> <Avatar className="h-8 w-8 bg-white">
{(() => { {(() => {
const faviconUrl = vendor.websiteUrl ? getFaviconUrl(vendor.websiteUrl) : null; const faviconUrl = vendor.websiteUrl
? getFaviconUrl(vendor.websiteUrl)
: null;
return faviconUrl ? ( return faviconUrl ? (
<AvatarImage src={faviconUrl} alt={vendor.name} /> <AvatarImage src={faviconUrl} alt={vendor.name} />
) : ( ) : (
@@ -564,7 +570,9 @@ function ListVendorContent({
Assessment expired Assessment expired
</Badge> </Badge>
) : ( ) : (
<Badge className={`${getRiskBadgeStyle(riskStatus.dataSensitivity)} font-medium text-xs px-2 py-1 rounded-md`}> <Badge
className={`${getRiskBadgeStyle(riskStatus.dataSensitivity)} font-medium text-xs px-2 py-1 rounded-md`}
>
{riskStatus.dataSensitivity || "NONE"} {riskStatus.dataSensitivity || "NONE"}
</Badge> </Badge>
)} )}
@@ -579,13 +587,17 @@ function ListVendorContent({
Assessment expired Assessment expired
</Badge> </Badge>
) : ( ) : (
<Badge className={`${getRiskBadgeStyle(riskStatus.businessImpact)} font-medium text-xs px-2 py-1 rounded-md`}> <Badge
className={`${getRiskBadgeStyle(riskStatus.businessImpact)} font-medium text-xs px-2 py-1 rounded-md`}
>
{riskStatus.businessImpact || "LOW"} {riskStatus.businessImpact || "LOW"}
</Badge> </Badge>
)} )}
</td> </td>
<td className="py-4 px-4"> <td className="py-4 px-4">
{riskStatus.assessedAt ? formatDate(riskStatus.assessedAt) : "N/A"} {riskStatus.assessedAt
? formatDate(riskStatus.assessedAt)
: "N/A"}
</td> </td>
<td className="py-4 px-4 text-right"> <td className="py-4 px-4 text-right">
<DropdownMenu> <DropdownMenu>
@@ -605,7 +617,7 @@ function ListVendorContent({
e.preventDefault(); e.preventDefault();
if ( if (
window.confirm( window.confirm(
"Are you sure you want to delete this vendor?" "Are you sure you want to delete this vendor?",
) )
) { ) {
deleteVendor({ deleteVendor({

File diff suppressed because it is too large Load Diff