@@ -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() {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const badgeVariants = cva(
|
|||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export interface BadgeProps
|
export interface BadgeProps
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const BreadCrumbNavLink = ({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"text-quaternary",
|
"text-quaternary",
|
||||||
isActive && "text-primary",
|
isActive && "text-primary",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 />} />
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ function FrameworkViewContent({
|
|||||||
}) {
|
}) {
|
||||||
const data = usePreloadedQuery<FrameworkViewQuery>(
|
const data = usePreloadedQuery<FrameworkViewQuery>(
|
||||||
frameworkViewQuery,
|
frameworkViewQuery,
|
||||||
queryRef
|
queryRef,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!data?.node) {
|
if (!data?.node) {
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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} />;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user