@@ -1,11 +1,11 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as ToastPrimitives from "@radix-ui/react-toast"
|
import * as ToastPrimitives from "@radix-ui/react-toast";
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import { X } from "lucide-react"
|
import { X } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const ToastProvider = ToastPrimitives.Provider
|
const ToastProvider = ToastPrimitives.Provider;
|
||||||
|
|
||||||
const ToastViewport = React.forwardRef<
|
const ToastViewport = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||||
@@ -15,12 +15,12 @@ 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}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
||||||
|
|
||||||
const toastVariants = cva(
|
const toastVariants = cva(
|
||||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||||
@@ -35,8 +35,8 @@ const toastVariants = cva(
|
|||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
const Toast = React.forwardRef<
|
const Toast = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||||
@@ -49,9 +49,9 @@ const Toast = React.forwardRef<
|
|||||||
className={cn(toastVariants({ variant }), className)}
|
className={cn(toastVariants({ variant }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
Toast.displayName = ToastPrimitives.Root.displayName
|
Toast.displayName = ToastPrimitives.Root.displayName;
|
||||||
|
|
||||||
const ToastAction = React.forwardRef<
|
const ToastAction = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||||
@@ -61,12 +61,12 @@ 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-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
"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-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
ToastAction.displayName = ToastPrimitives.Action.displayName;
|
||||||
|
|
||||||
const ToastClose = React.forwardRef<
|
const ToastClose = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||||
@@ -76,15 +76,15 @@ const ToastClose = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
toast-close=""
|
toast-close=""
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</ToastPrimitives.Close>
|
</ToastPrimitives.Close>
|
||||||
))
|
));
|
||||||
ToastClose.displayName = ToastPrimitives.Close.displayName
|
ToastClose.displayName = ToastPrimitives.Close.displayName;
|
||||||
|
|
||||||
const ToastTitle = React.forwardRef<
|
const ToastTitle = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||||
@@ -95,8 +95,8 @@ const ToastTitle = React.forwardRef<
|
|||||||
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
|
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
ToastTitle.displayName = ToastPrimitives.Title.displayName;
|
||||||
|
|
||||||
const ToastDescription = React.forwardRef<
|
const ToastDescription = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||||
@@ -107,12 +107,12 @@ const ToastDescription = React.forwardRef<
|
|||||||
className={cn("text-sm opacity-90", className)}
|
className={cn("text-sm opacity-90", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
ToastDescription.displayName = ToastPrimitives.Description.displayName;
|
||||||
|
|
||||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
|
||||||
|
|
||||||
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
type ToastActionElement = React.ReactElement<typeof ToastAction>;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
type ToastProps,
|
type ToastProps,
|
||||||
@@ -124,4 +124,4 @@ export {
|
|||||||
ToastDescription,
|
ToastDescription,
|
||||||
ToastClose,
|
ToastClose,
|
||||||
ToastAction,
|
ToastAction,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useToast } from "@/hooks/use-toast"
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import {
|
import {
|
||||||
Toast,
|
Toast,
|
||||||
ToastClose,
|
ToastClose,
|
||||||
@@ -6,10 +6,10 @@ import {
|
|||||||
ToastProvider,
|
ToastProvider,
|
||||||
ToastTitle,
|
ToastTitle,
|
||||||
ToastViewport,
|
ToastViewport,
|
||||||
} from "@/components/ui/toast"
|
} from "@/components/ui/toast";
|
||||||
|
|
||||||
export function Toaster() {
|
export function Toaster() {
|
||||||
const { toasts } = useToast()
|
const { toasts } = useToast();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
@@ -25,9 +25,9 @@ export function Toaster() {
|
|||||||
{action}
|
{action}
|
||||||
<ToastClose />
|
<ToastClose />
|
||||||
</Toast>
|
</Toast>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
<ToastViewport />
|
<ToastViewport />
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,78 +1,75 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
// Inspired by react-hot-toast library
|
// Inspired by react-hot-toast library
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
|
|
||||||
import type {
|
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
|
||||||
ToastActionElement,
|
|
||||||
ToastProps,
|
|
||||||
} from "@/components/ui/toast"
|
|
||||||
|
|
||||||
const TOAST_LIMIT = 1
|
const TOAST_LIMIT = 1;
|
||||||
const TOAST_REMOVE_DELAY = 1000000
|
const TOAST_REMOVE_DELAY = 1000000;
|
||||||
|
|
||||||
type ToasterToast = ToastProps & {
|
type ToasterToast = ToastProps & {
|
||||||
id: string
|
id: string;
|
||||||
title?: React.ReactNode
|
title?: React.ReactNode;
|
||||||
description?: React.ReactNode
|
description?: React.ReactNode;
|
||||||
action?: ToastActionElement
|
action?: ToastActionElement;
|
||||||
}
|
};
|
||||||
|
|
||||||
const actionTypes = {
|
const actionTypes = {
|
||||||
ADD_TOAST: "ADD_TOAST",
|
ADD_TOAST: "ADD_TOAST",
|
||||||
UPDATE_TOAST: "UPDATE_TOAST",
|
UPDATE_TOAST: "UPDATE_TOAST",
|
||||||
DISMISS_TOAST: "DISMISS_TOAST",
|
DISMISS_TOAST: "DISMISS_TOAST",
|
||||||
REMOVE_TOAST: "REMOVE_TOAST",
|
REMOVE_TOAST: "REMOVE_TOAST",
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
let count = 0
|
let count = 0;
|
||||||
|
|
||||||
function genId() {
|
function genId() {
|
||||||
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
count = (count + 1) % Number.MAX_SAFE_INTEGER;
|
||||||
return count.toString()
|
return count.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
type ActionType = typeof actionTypes
|
type ActionType = typeof actionTypes;
|
||||||
|
|
||||||
type Action =
|
type Action =
|
||||||
| {
|
| {
|
||||||
type: ActionType["ADD_TOAST"]
|
type: ActionType["ADD_TOAST"];
|
||||||
toast: ToasterToast
|
toast: ToasterToast;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: ActionType["UPDATE_TOAST"]
|
type: ActionType["UPDATE_TOAST"];
|
||||||
toast: Partial<ToasterToast>
|
toast: Partial<ToasterToast>;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: ActionType["DISMISS_TOAST"]
|
type: ActionType["DISMISS_TOAST"];
|
||||||
toastId?: ToasterToast["id"]
|
toastId?: ToasterToast["id"];
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: ActionType["REMOVE_TOAST"]
|
type: ActionType["REMOVE_TOAST"];
|
||||||
toastId?: ToasterToast["id"]
|
toastId?: ToasterToast["id"];
|
||||||
}
|
};
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
toasts: ToasterToast[]
|
toasts: ToasterToast[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
const addToRemoveQueue = (toastId: string) => {
|
const addToRemoveQueue = (toastId: string) => {
|
||||||
if (toastTimeouts.has(toastId)) {
|
if (toastTimeouts.has(toastId)) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
toastTimeouts.delete(toastId)
|
toastTimeouts.delete(toastId);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "REMOVE_TOAST",
|
type: "REMOVE_TOAST",
|
||||||
toastId: toastId,
|
toastId: toastId,
|
||||||
})
|
});
|
||||||
}, TOAST_REMOVE_DELAY)
|
}, TOAST_REMOVE_DELAY);
|
||||||
|
|
||||||
toastTimeouts.set(toastId, timeout)
|
toastTimeouts.set(toastId, timeout);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const reducer = (state: State, action: Action): State => {
|
export const reducer = (state: State, action: Action): State => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
@@ -80,27 +77,27 @@ export const reducer = (state: State, action: Action): State => {
|
|||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||||
}
|
};
|
||||||
|
|
||||||
case "UPDATE_TOAST":
|
case "UPDATE_TOAST":
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
toasts: state.toasts.map((t) =>
|
toasts: state.toasts.map((t) =>
|
||||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
t.id === action.toast.id ? { ...t, ...action.toast } : t,
|
||||||
),
|
),
|
||||||
}
|
};
|
||||||
|
|
||||||
case "DISMISS_TOAST": {
|
case "DISMISS_TOAST": {
|
||||||
const { toastId } = action
|
const { toastId } = action;
|
||||||
|
|
||||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||||
// but I'll keep it here for simplicity
|
// but I'll keep it here for simplicity
|
||||||
if (toastId) {
|
if (toastId) {
|
||||||
addToRemoveQueue(toastId)
|
addToRemoveQueue(toastId);
|
||||||
} else {
|
} else {
|
||||||
state.toasts.forEach((toast) => {
|
state.toasts.forEach((toast) => {
|
||||||
addToRemoveQueue(toast.id)
|
addToRemoveQueue(toast.id);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -111,46 +108,46 @@ export const reducer = (state: State, action: Action): State => {
|
|||||||
...t,
|
...t,
|
||||||
open: false,
|
open: false,
|
||||||
}
|
}
|
||||||
: t
|
: t,
|
||||||
),
|
),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
case "REMOVE_TOAST":
|
case "REMOVE_TOAST":
|
||||||
if (action.toastId === undefined) {
|
if (action.toastId === undefined) {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
toasts: [],
|
toasts: [],
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const listeners: Array<(state: State) => void> = []
|
const listeners: Array<(state: State) => void> = [];
|
||||||
|
|
||||||
let memoryState: State = { toasts: [] }
|
let memoryState: State = { toasts: [] };
|
||||||
|
|
||||||
function dispatch(action: Action) {
|
function dispatch(action: Action) {
|
||||||
memoryState = reducer(memoryState, action)
|
memoryState = reducer(memoryState, action);
|
||||||
listeners.forEach((listener) => {
|
listeners.forEach((listener) => {
|
||||||
listener(memoryState)
|
listener(memoryState);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
type Toast = Omit<ToasterToast, "id">
|
type Toast = Omit<ToasterToast, "id">;
|
||||||
|
|
||||||
function toast({ ...props }: Toast) {
|
function toast({ ...props }: Toast) {
|
||||||
const id = genId()
|
const id = genId();
|
||||||
|
|
||||||
const update = (props: ToasterToast) =>
|
const update = (props: ToasterToast) =>
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "UPDATE_TOAST",
|
type: "UPDATE_TOAST",
|
||||||
toast: { ...props, id },
|
toast: { ...props, id },
|
||||||
})
|
});
|
||||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "ADD_TOAST",
|
type: "ADD_TOAST",
|
||||||
@@ -159,36 +156,36 @@ function toast({ ...props }: Toast) {
|
|||||||
id,
|
id,
|
||||||
open: true,
|
open: true,
|
||||||
onOpenChange: (open) => {
|
onOpenChange: (open) => {
|
||||||
if (!open) dismiss()
|
if (!open) dismiss();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: id,
|
id: id,
|
||||||
dismiss,
|
dismiss,
|
||||||
update,
|
update,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function useToast() {
|
function useToast() {
|
||||||
const [state, setState] = React.useState<State>(memoryState)
|
const [state, setState] = React.useState<State>(memoryState);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
listeners.push(setState)
|
listeners.push(setState);
|
||||||
return () => {
|
return () => {
|
||||||
const index = listeners.indexOf(setState)
|
const index = listeners.indexOf(setState);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
listeners.splice(index, 1)
|
listeners.splice(index, 1);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}, [state])
|
}, [state]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
toast,
|
toast,
|
||||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export { useToast, toast }
|
export { useToast, toast };
|
||||||
|
|||||||
@@ -32,7 +32,10 @@ const createPeoplePageQuery = graphql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const createPeopleMutation = graphql`
|
const createPeopleMutation = graphql`
|
||||||
mutation CreatePeoplePageCreatePeopleMutation($input: CreatePeopleInput!, $connections: [ID!]!) {
|
mutation CreatePeoplePageCreatePeopleMutation(
|
||||||
|
$input: CreatePeopleInput!
|
||||||
|
$connections: [ID!]!
|
||||||
|
) {
|
||||||
createPeople(input: $input) {
|
createPeople(input: $input) {
|
||||||
peopleEdge @prependEdge(connections: $connections) {
|
peopleEdge @prependEdge(connections: $connections) {
|
||||||
node {
|
node {
|
||||||
@@ -89,17 +92,18 @@ function CreatePeoplePageContent({
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const environment = useRelayEnvironment();
|
const environment = useRelayEnvironment();
|
||||||
const data = usePreloadedQuery(createPeoplePageQuery, queryRef);
|
const data = usePreloadedQuery(createPeoplePageQuery, queryRef);
|
||||||
const [createPeople] = useMutation<CreatePeoplePageCreatePeopleMutation>(createPeopleMutation);
|
const [createPeople] =
|
||||||
|
useMutation<CreatePeoplePageCreatePeopleMutation>(createPeopleMutation);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
fullName: '',
|
fullName: "",
|
||||||
primaryEmailAddress: '',
|
primaryEmailAddress: "",
|
||||||
additionalEmailAddresses: [] as string[],
|
additionalEmailAddresses: [] as string[],
|
||||||
kind: 'EMPLOYEE' as 'EMPLOYEE' | 'CONTRACTOR',
|
kind: "EMPLOYEE" as "EMPLOYEE" | "CONTRACTOR",
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleFieldChange = (field: keyof typeof formData, value: any) => {
|
const handleFieldChange = (field: keyof typeof formData, value: any) => {
|
||||||
setFormData(prev => ({
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[field]: value,
|
[field]: value,
|
||||||
}));
|
}));
|
||||||
@@ -107,7 +111,10 @@ function CreatePeoplePageContent({
|
|||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const peopleConnectionId = ConnectionHandler.getConnectionID(data.currentOrganization.id, "PeopleListPage_peoples");
|
const peopleConnectionId = ConnectionHandler.getConnectionID(
|
||||||
|
data.currentOrganization.id,
|
||||||
|
"PeopleListPage_peoples",
|
||||||
|
);
|
||||||
|
|
||||||
createPeople({
|
createPeople({
|
||||||
variables: {
|
variables: {
|
||||||
@@ -150,7 +157,7 @@ function CreatePeoplePageContent({
|
|||||||
<EditableField
|
<EditableField
|
||||||
label="Full Name"
|
label="Full Name"
|
||||||
value={formData.fullName}
|
value={formData.fullName}
|
||||||
onChange={(value) => handleFieldChange('fullName', value)}
|
onChange={(value) => handleFieldChange("fullName", value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -158,7 +165,9 @@ function CreatePeoplePageContent({
|
|||||||
label="Primary Email"
|
label="Primary Email"
|
||||||
value={formData.primaryEmailAddress}
|
value={formData.primaryEmailAddress}
|
||||||
type="email"
|
type="email"
|
||||||
onChange={(value) => handleFieldChange('primaryEmailAddress', value)}
|
onChange={(value) =>
|
||||||
|
handleFieldChange("primaryEmailAddress", value)
|
||||||
|
}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -174,17 +183,28 @@ function CreatePeoplePageContent({
|
|||||||
type="email"
|
type="email"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newEmails = [...formData.additionalEmailAddresses];
|
const newEmails = [
|
||||||
|
...formData.additionalEmailAddresses,
|
||||||
|
];
|
||||||
newEmails[index] = e.target.value;
|
newEmails[index] = e.target.value;
|
||||||
handleFieldChange('additionalEmailAddresses', newEmails);
|
handleFieldChange(
|
||||||
|
"additionalEmailAddresses",
|
||||||
|
newEmails,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newEmails = formData.additionalEmailAddresses.filter((_, i) => i !== index);
|
const newEmails =
|
||||||
handleFieldChange('additionalEmailAddresses', newEmails);
|
formData.additionalEmailAddresses.filter(
|
||||||
|
(_, i) => i !== index,
|
||||||
|
);
|
||||||
|
handleFieldChange(
|
||||||
|
"additionalEmailAddresses",
|
||||||
|
newEmails,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
@@ -195,7 +215,10 @@ function CreatePeoplePageContent({
|
|||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleFieldChange('additionalEmailAddresses', [...formData.additionalEmailAddresses, '']);
|
handleFieldChange("additionalEmailAddresses", [
|
||||||
|
...formData.additionalEmailAddresses,
|
||||||
|
"",
|
||||||
|
]);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add Email
|
Add Email
|
||||||
@@ -206,7 +229,9 @@ function CreatePeoplePageContent({
|
|||||||
<Card className="p-6">
|
<Card className="p-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h2 className="text-lg font-medium">Additional Information</h2>
|
<h2 className="text-lg font-medium">
|
||||||
|
Additional Information
|
||||||
|
</h2>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
Additional details about the person
|
Additional details about the person
|
||||||
</p>
|
</p>
|
||||||
@@ -221,24 +246,24 @@ function CreatePeoplePageContent({
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleFieldChange('kind', 'EMPLOYEE')}
|
onClick={() => handleFieldChange("kind", "EMPLOYEE")}
|
||||||
className={cn(
|
className={cn(
|
||||||
"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-gray-100 text-gray-900 hover:bg-gray-200"
|
: "bg-gray-100 text-gray-900 hover:bg-gray-200",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Employee
|
Employee
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleFieldChange('kind', 'CONTRACTOR')}
|
onClick={() => handleFieldChange("kind", "CONTRACTOR")}
|
||||||
className={cn(
|
className={cn(
|
||||||
"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-gray-100 text-gray-900 hover:bg-gray-200"
|
: "bg-gray-100 text-gray-900 hover:bg-gray-200",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Contractor
|
Contractor
|
||||||
@@ -251,11 +276,7 @@ function CreatePeoplePageContent({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="fixed bottom-6 right-6 flex gap-2">
|
<div className="fixed bottom-6 right-6 flex gap-2">
|
||||||
<Button
|
<Button type="button" variant="outline" onClick={() => navigate(-1)}>
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => navigate(-1)}
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -55,9 +55,7 @@ function FrameworkCard({
|
|||||||
return (
|
return (
|
||||||
<Card className="relative overflow-hidden border bg-card p-6">
|
<Card className="relative overflow-hidden border bg-card p-6">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="size-16">
|
<div className="size-16">{icon}</div>
|
||||||
{icon}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -100,7 +98,7 @@ function FrameworkListPageContent({
|
|||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{frameworks.map((framework) => {
|
{frameworks.map((framework) => {
|
||||||
const validatedControls = framework.controls.edges.filter(
|
const validatedControls = framework.controls.edges.filter(
|
||||||
edge => edge?.node?.state === "IMPLEMENTED"
|
(edge) => edge?.node?.state === "IMPLEMENTED",
|
||||||
).length;
|
).length;
|
||||||
const totalControls = framework.controls.edges.length;
|
const totalControls = framework.controls.edges.length;
|
||||||
|
|
||||||
@@ -112,11 +110,13 @@ function FrameworkListPageContent({
|
|||||||
icon={
|
icon={
|
||||||
<div className="flex size-full items-center justify-center rounded-full bg-blue-100">
|
<div className="flex size-full items-center justify-center rounded-full bg-blue-100">
|
||||||
<span className="text-lg font-semibold text-blue-900">
|
<span className="text-lg font-semibold text-blue-900">
|
||||||
{framework.name.split(' ')[0]}
|
{framework.name.split(" ")[0]}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
status={validatedControls === totalControls ? "Compliant" : undefined}
|
status={
|
||||||
|
validatedControls === totalControls ? "Compliant" : undefined
|
||||||
|
}
|
||||||
progress={
|
progress={
|
||||||
validatedControls === totalControls
|
validatedControls === totalControls
|
||||||
? "All controls validated"
|
? "All controls validated"
|
||||||
|
|||||||
@@ -147,7 +147,8 @@ function PeopleListContent({
|
|||||||
PeopleListPage_peoples$key
|
PeopleListPage_peoples$key
|
||||||
>(peopleListFragment, data.currentOrganization);
|
>(peopleListFragment, data.currentOrganization);
|
||||||
|
|
||||||
const peoples = peoplesConnection.peoples.edges.map((edge) => edge.node) ?? [];
|
const peoples =
|
||||||
|
peoplesConnection.peoples.edges.map((edge) => edge.node) ?? [];
|
||||||
const pageInfo = peoplesConnection.peoples.pageInfo;
|
const pageInfo = peoplesConnection.peoples.pageInfo;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -91,13 +91,15 @@ function PeopleOverviewPageContent({
|
|||||||
const { setBreadcrumbSegment } = useBreadcrumb();
|
const { setBreadcrumbSegment } = useBreadcrumb();
|
||||||
const [editedFields, setEditedFields] = useState<Set<string>>(new Set());
|
const [editedFields, setEditedFields] = useState<Set<string>>(new Set());
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
fullName: data.node.fullName || '',
|
fullName: data.node.fullName || "",
|
||||||
primaryEmailAddress: data.node.primaryEmailAddress || '',
|
primaryEmailAddress: data.node.primaryEmailAddress || "",
|
||||||
additionalEmailAddresses: data.node.additionalEmailAddresses || [],
|
additionalEmailAddresses: data.node.additionalEmailAddresses || [],
|
||||||
kind: data.node.kind,
|
kind: data.node.kind,
|
||||||
});
|
});
|
||||||
const [commit] = useMutation(updatePeopleMutation);
|
const [commit] = useMutation(updatePeopleMutation);
|
||||||
const [_, loadQuery] = useQueryLoader<PeopleOverviewPageQueryType>(peopleOverviewPageQuery);
|
const [_, loadQuery] = useQueryLoader<PeopleOverviewPageQueryType>(
|
||||||
|
peopleOverviewPageQuery,
|
||||||
|
);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const hasChanges = editedFields.size > 0;
|
const hasChanges = editedFields.size > 0;
|
||||||
@@ -120,10 +122,11 @@ function PeopleOverviewPageContent({
|
|||||||
setEditedFields(new Set());
|
setEditedFields(new Set());
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
if (error.message?.includes('concurrent modification')) {
|
if (error.message?.includes("concurrent modification")) {
|
||||||
toast({
|
toast({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
description: "Someone else modified this person. Reloading latest data.",
|
description:
|
||||||
|
"Someone else modified this person. Reloading latest data.",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
loadQuery({ peopleId: data.node.id! });
|
loadQuery({ peopleId: data.node.id! });
|
||||||
@@ -139,17 +142,17 @@ function PeopleOverviewPageContent({
|
|||||||
}, [commit, data.node.id, data.node.version, formData, loadQuery, toast]);
|
}, [commit, data.node.id, data.node.version, formData, loadQuery, toast]);
|
||||||
|
|
||||||
const handleFieldChange = (field: keyof typeof formData, value: any) => {
|
const handleFieldChange = (field: keyof typeof formData, value: any) => {
|
||||||
setFormData(prev => ({
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[field]: value,
|
[field]: value,
|
||||||
}));
|
}));
|
||||||
setEditedFields(prev => new Set(prev).add(field));
|
setEditedFields((prev) => new Set(prev).add(field));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
setFormData({
|
setFormData({
|
||||||
fullName: data.node.fullName || '',
|
fullName: data.node.fullName || "",
|
||||||
primaryEmailAddress: data.node.primaryEmailAddress || '',
|
primaryEmailAddress: data.node.primaryEmailAddress || "",
|
||||||
additionalEmailAddresses: data.node.additionalEmailAddresses || [],
|
additionalEmailAddresses: data.node.additionalEmailAddresses || [],
|
||||||
kind: data.node.kind,
|
kind: data.node.kind,
|
||||||
});
|
});
|
||||||
@@ -169,14 +172,16 @@ function PeopleOverviewPageContent({
|
|||||||
<EditableField
|
<EditableField
|
||||||
label="Full Name"
|
label="Full Name"
|
||||||
value={formData.fullName}
|
value={formData.fullName}
|
||||||
onChange={(value) => handleFieldChange('fullName', value)}
|
onChange={(value) => handleFieldChange("fullName", value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<EditableField
|
<EditableField
|
||||||
label="Primary Email"
|
label="Primary Email"
|
||||||
value={formData.primaryEmailAddress}
|
value={formData.primaryEmailAddress}
|
||||||
type="email"
|
type="email"
|
||||||
onChange={(value) => handleFieldChange('primaryEmailAddress', value)}
|
onChange={(value) =>
|
||||||
|
handleFieldChange("primaryEmailAddress", value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -193,14 +198,17 @@ function PeopleOverviewPageContent({
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newEmails = [...formData.additionalEmailAddresses];
|
const newEmails = [...formData.additionalEmailAddresses];
|
||||||
newEmails[index] = e.target.value;
|
newEmails[index] = e.target.value;
|
||||||
handleFieldChange('additionalEmailAddresses', newEmails);
|
handleFieldChange("additionalEmailAddresses", newEmails);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newEmails = formData.additionalEmailAddresses.filter((_, i) => i !== index);
|
const newEmails =
|
||||||
handleFieldChange('additionalEmailAddresses', newEmails);
|
formData.additionalEmailAddresses.filter(
|
||||||
|
(_, i) => i !== index,
|
||||||
|
);
|
||||||
|
handleFieldChange("additionalEmailAddresses", newEmails);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
@@ -210,7 +218,10 @@ function PeopleOverviewPageContent({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleFieldChange('additionalEmailAddresses', [...formData.additionalEmailAddresses, '']);
|
handleFieldChange("additionalEmailAddresses", [
|
||||||
|
...formData.additionalEmailAddresses,
|
||||||
|
"",
|
||||||
|
]);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add Email
|
Add Email
|
||||||
@@ -235,23 +246,23 @@ function PeopleOverviewPageContent({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleFieldChange('kind', 'EMPLOYEE')}
|
onClick={() => handleFieldChange("kind", "EMPLOYEE")}
|
||||||
className={cn(
|
className={cn(
|
||||||
"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-gray-100 text-gray-900 hover:bg-gray-200"
|
: "bg-gray-100 text-gray-900 hover:bg-gray-200",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Employee
|
Employee
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleFieldChange('kind', 'CONTRACTOR')}
|
onClick={() => handleFieldChange("kind", "CONTRACTOR")}
|
||||||
className={cn(
|
className={cn(
|
||||||
"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-gray-100 text-gray-900 hover:bg-gray-200"
|
: "bg-gray-100 text-gray-900 hover:bg-gray-200",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Contractor
|
Contractor
|
||||||
@@ -266,10 +277,7 @@ function PeopleOverviewPageContent({
|
|||||||
|
|
||||||
{hasChanges && (
|
{hasChanges && (
|
||||||
<div className="fixed bottom-6 right-6 flex gap-2">
|
<div className="fixed bottom-6 right-6 flex gap-2">
|
||||||
<Button
|
<Button variant="outline" onClick={handleCancel}>
|
||||||
variant="outline"
|
|
||||||
onClick={handleCancel}
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -46,18 +46,18 @@ const vendorOverviewPageQuery = graphql`
|
|||||||
const updateVendorMutation = graphql`
|
const updateVendorMutation = graphql`
|
||||||
mutation VendorOverviewPageUpdateVendorMutation($input: UpdateVendorInput!) {
|
mutation VendorOverviewPageUpdateVendorMutation($input: UpdateVendorInput!) {
|
||||||
updateVendor(input: $input) {
|
updateVendor(input: $input) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
description
|
description
|
||||||
serviceStartAt
|
serviceStartAt
|
||||||
serviceTerminationAt
|
serviceTerminationAt
|
||||||
serviceCriticality
|
serviceCriticality
|
||||||
riskTier
|
riskTier
|
||||||
statusPageUrl
|
statusPageUrl
|
||||||
termsOfServiceUrl
|
termsOfServiceUrl
|
||||||
privacyPolicyUrl
|
privacyPolicyUrl
|
||||||
updatedAt
|
updatedAt
|
||||||
version
|
version
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -95,13 +95,13 @@ function EditableField({
|
|||||||
|
|
||||||
// Format date for input field (YYYY-MM-DDTHH:mm)
|
// Format date for input field (YYYY-MM-DDTHH:mm)
|
||||||
function formatDateForInput(date: string | null | undefined): string {
|
function formatDateForInput(date: string | null | undefined): string {
|
||||||
if (!date) return '';
|
if (!date) return "";
|
||||||
return new Date(date).toISOString().slice(0, 16);
|
return new Date(date).toISOString().slice(0, 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format date for API (2006-01-02T15:04:05.999999999Z07:00)
|
// Format date for API (2006-01-02T15:04:05.999999999Z07:00)
|
||||||
function formatDateForAPI(dateStr: string): string {
|
function formatDateForAPI(dateStr: string): string {
|
||||||
if (!dateStr) return '';
|
if (!dateStr) return "";
|
||||||
const date = new Date(dateStr);
|
const date = new Date(dateStr);
|
||||||
return date.toISOString();
|
return date.toISOString();
|
||||||
}
|
}
|
||||||
@@ -115,19 +115,21 @@ function VendorOverviewPageContent({
|
|||||||
const { setBreadcrumbSegment } = useBreadcrumb();
|
const { setBreadcrumbSegment } = useBreadcrumb();
|
||||||
const [editedFields, setEditedFields] = useState<Set<string>>(new Set());
|
const [editedFields, setEditedFields] = useState<Set<string>>(new Set());
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: data.node.name || '',
|
name: data.node.name || "",
|
||||||
description: data.node.description || '',
|
description: data.node.description || "",
|
||||||
// Format dates properly for datetime-local input
|
// Format dates properly for datetime-local input
|
||||||
serviceStartAt: formatDateForInput(data.node.serviceStartAt),
|
serviceStartAt: formatDateForInput(data.node.serviceStartAt),
|
||||||
serviceTerminationAt: formatDateForInput(data.node.serviceTerminationAt),
|
serviceTerminationAt: formatDateForInput(data.node.serviceTerminationAt),
|
||||||
serviceCriticality: data.node.serviceCriticality,
|
serviceCriticality: data.node.serviceCriticality,
|
||||||
riskTier: data.node.riskTier,
|
riskTier: data.node.riskTier,
|
||||||
statusPageUrl: data.node.statusPageUrl || '',
|
statusPageUrl: data.node.statusPageUrl || "",
|
||||||
termsOfServiceUrl: data.node.termsOfServiceUrl || '',
|
termsOfServiceUrl: data.node.termsOfServiceUrl || "",
|
||||||
privacyPolicyUrl: data.node.privacyPolicyUrl || '',
|
privacyPolicyUrl: data.node.privacyPolicyUrl || "",
|
||||||
});
|
});
|
||||||
const [commit] = useMutation(updateVendorMutation);
|
const [commit] = useMutation(updateVendorMutation);
|
||||||
const [_, loadQuery] = useQueryLoader<VendorOverviewPageQueryType>(vendorOverviewPageQuery);
|
const [_, loadQuery] = useQueryLoader<VendorOverviewPageQueryType>(
|
||||||
|
vendorOverviewPageQuery,
|
||||||
|
);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const hasChanges = editedFields.size > 0;
|
const hasChanges = editedFields.size > 0;
|
||||||
@@ -136,7 +138,9 @@ function VendorOverviewPageContent({
|
|||||||
const formattedData = {
|
const formattedData = {
|
||||||
...formData,
|
...formData,
|
||||||
serviceStartAt: formatDateForAPI(formData.serviceStartAt),
|
serviceStartAt: formatDateForAPI(formData.serviceStartAt),
|
||||||
serviceTerminationAt: formData.serviceTerminationAt ? formatDateForAPI(formData.serviceTerminationAt) : null,
|
serviceTerminationAt: formData.serviceTerminationAt
|
||||||
|
? formatDateForAPI(formData.serviceTerminationAt)
|
||||||
|
: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
commit({
|
commit({
|
||||||
@@ -156,15 +160,15 @@ function VendorOverviewPageContent({
|
|||||||
setEditedFields(new Set());
|
setEditedFields(new Set());
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
if (error.message?.includes('concurrent modification')) {
|
if (error.message?.includes("concurrent modification")) {
|
||||||
toast({
|
toast({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
description: "Someone else modified this vendor. Reloading latest data.",
|
description:
|
||||||
|
"Someone else modified this vendor. Reloading latest data.",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
|
|
||||||
loadQuery({ vendorId: data.node.id! });
|
loadQuery({ vendorId: data.node.id! });
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
toast({
|
toast({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
@@ -180,25 +184,25 @@ function VendorOverviewPageContent({
|
|||||||
}, [commit, data.node.id, data.node.version, formData, loadQuery, toast]);
|
}, [commit, data.node.id, data.node.version, formData, loadQuery, toast]);
|
||||||
|
|
||||||
const handleFieldChange = (field: keyof typeof formData, value: any) => {
|
const handleFieldChange = (field: keyof typeof formData, value: any) => {
|
||||||
setFormData(prev => ({
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[field]: value,
|
[field]: value,
|
||||||
}));
|
}));
|
||||||
setEditedFields(prev => new Set(prev).add(field));
|
setEditedFields((prev) => new Set(prev).add(field));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update the cancel handler to also format dates
|
// Update the cancel handler to also format dates
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: data.node.name || '',
|
name: data.node.name || "",
|
||||||
description: data.node.description || '',
|
description: data.node.description || "",
|
||||||
serviceStartAt: formatDateForInput(data.node.serviceStartAt),
|
serviceStartAt: formatDateForInput(data.node.serviceStartAt),
|
||||||
serviceTerminationAt: formatDateForInput(data.node.serviceTerminationAt),
|
serviceTerminationAt: formatDateForInput(data.node.serviceTerminationAt),
|
||||||
serviceCriticality: data.node.serviceCriticality,
|
serviceCriticality: data.node.serviceCriticality,
|
||||||
riskTier: data.node.riskTier,
|
riskTier: data.node.riskTier,
|
||||||
statusPageUrl: data.node.statusPageUrl || '',
|
statusPageUrl: data.node.statusPageUrl || "",
|
||||||
termsOfServiceUrl: data.node.termsOfServiceUrl || '',
|
termsOfServiceUrl: data.node.termsOfServiceUrl || "",
|
||||||
privacyPolicyUrl: data.node.privacyPolicyUrl || '',
|
privacyPolicyUrl: data.node.privacyPolicyUrl || "",
|
||||||
});
|
});
|
||||||
setEditedFields(new Set());
|
setEditedFields(new Set());
|
||||||
};
|
};
|
||||||
@@ -216,13 +220,13 @@ function VendorOverviewPageContent({
|
|||||||
<EditableField
|
<EditableField
|
||||||
label="Name"
|
label="Name"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(value) => handleFieldChange('name', value)}
|
onChange={(value) => handleFieldChange("name", value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<EditableField
|
<EditableField
|
||||||
label="Description"
|
label="Description"
|
||||||
value={formData.description}
|
value={formData.description}
|
||||||
onChange={(value) => handleFieldChange('description', value)}
|
onChange={(value) => handleFieldChange("description", value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Card className="p-6">
|
<Card className="p-6">
|
||||||
@@ -239,14 +243,18 @@ function VendorOverviewPageContent({
|
|||||||
label="Service Start At"
|
label="Service Start At"
|
||||||
value={formData.serviceStartAt}
|
value={formData.serviceStartAt}
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
onChange={(value) => handleFieldChange('serviceStartAt', value)}
|
onChange={(value) =>
|
||||||
|
handleFieldChange("serviceStartAt", value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<EditableField
|
<EditableField
|
||||||
label="Service Termination At"
|
label="Service Termination At"
|
||||||
value={formData.serviceTerminationAt}
|
value={formData.serviceTerminationAt}
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
onChange={(value) => handleFieldChange('serviceTerminationAt', value)}
|
onChange={(value) =>
|
||||||
|
handleFieldChange("serviceTerminationAt", value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -256,45 +264,51 @@ function VendorOverviewPageContent({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleFieldChange('serviceCriticality', 'LOW')}
|
onClick={() =>
|
||||||
|
handleFieldChange("serviceCriticality", "LOW")
|
||||||
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||||
formData.serviceCriticality === 'LOW'
|
formData.serviceCriticality === "LOW"
|
||||||
? "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-gray-100 text-gray-900 hover:bg-gray-200"
|
: "bg-gray-100 text-gray-900 hover:bg-gray-200",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Low
|
Low
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleFieldChange('serviceCriticality', 'MEDIUM')}
|
onClick={() =>
|
||||||
|
handleFieldChange("serviceCriticality", "MEDIUM")
|
||||||
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||||
formData.serviceCriticality === 'MEDIUM'
|
formData.serviceCriticality === "MEDIUM"
|
||||||
? "bg-yellow-100 text-yellow-900 ring-2 ring-yellow-600 ring-offset-2"
|
? "bg-yellow-100 text-yellow-900 ring-2 ring-yellow-600 ring-offset-2"
|
||||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
: "bg-gray-100 text-gray-900 hover:bg-gray-200",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Medium
|
Medium
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleFieldChange('serviceCriticality', 'HIGH')}
|
onClick={() =>
|
||||||
|
handleFieldChange("serviceCriticality", "HIGH")
|
||||||
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||||
formData.serviceCriticality === 'HIGH'
|
formData.serviceCriticality === "HIGH"
|
||||||
? "bg-red-100 text-red-900 ring-2 ring-red-600 ring-offset-2"
|
? "bg-red-100 text-red-900 ring-2 ring-red-600 ring-offset-2"
|
||||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
: "bg-gray-100 text-gray-900 hover:bg-gray-200",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
High
|
High
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
{formData.serviceCriticality === 'HIGH' &&
|
{formData.serviceCriticality === "HIGH" &&
|
||||||
"Critical service - downtime severely impacts end-users"}
|
"Critical service - downtime severely impacts end-users"}
|
||||||
{formData.serviceCriticality === 'MEDIUM' &&
|
{formData.serviceCriticality === "MEDIUM" &&
|
||||||
"Important service - downtime moderately affects end-users"}
|
"Important service - downtime moderately affects end-users"}
|
||||||
{formData.serviceCriticality === 'LOW' &&
|
{formData.serviceCriticality === "LOW" &&
|
||||||
"Non-critical service - minimal end-user impact if down"}
|
"Non-critical service - minimal end-user impact if down"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -306,45 +320,47 @@ function VendorOverviewPageContent({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleFieldChange('riskTier', 'CRITICAL')}
|
onClick={() => handleFieldChange("riskTier", "CRITICAL")}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||||
formData.riskTier === 'CRITICAL'
|
formData.riskTier === "CRITICAL"
|
||||||
? "bg-red-100 text-red-900 ring-2 ring-red-600 ring-offset-2"
|
? "bg-red-100 text-red-900 ring-2 ring-red-600 ring-offset-2"
|
||||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
: "bg-gray-100 text-gray-900 hover:bg-gray-200",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Critical
|
Critical
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleFieldChange('riskTier', 'SIGNIFICANT')}
|
onClick={() =>
|
||||||
|
handleFieldChange("riskTier", "SIGNIFICANT")
|
||||||
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||||
formData.riskTier === 'SIGNIFICANT'
|
formData.riskTier === "SIGNIFICANT"
|
||||||
? "bg-yellow-100 text-yellow-900 ring-2 ring-yellow-600 ring-offset-2"
|
? "bg-yellow-100 text-yellow-900 ring-2 ring-yellow-600 ring-offset-2"
|
||||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
: "bg-gray-100 text-gray-900 hover:bg-gray-200",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Significant
|
Significant
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleFieldChange('riskTier', 'GENERAL')}
|
onClick={() => handleFieldChange("riskTier", "GENERAL")}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||||
formData.riskTier === 'GENERAL'
|
formData.riskTier === "GENERAL"
|
||||||
? "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-gray-100 text-gray-900 hover:bg-gray-200"
|
: "bg-gray-100 text-gray-900 hover:bg-gray-200",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
General
|
General
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
{formData.riskTier === 'CRITICAL' &&
|
{formData.riskTier === "CRITICAL" &&
|
||||||
"Handles sensitive data, critical for platform operation"}
|
"Handles sensitive data, critical for platform operation"}
|
||||||
{formData.riskTier === 'SIGNIFICANT' &&
|
{formData.riskTier === "SIGNIFICANT" &&
|
||||||
"No user data access, but important for platform management"}
|
"No user data access, but important for platform management"}
|
||||||
{formData.riskTier === 'GENERAL' &&
|
{formData.riskTier === "GENERAL" &&
|
||||||
"General vendor with minimal risk"}
|
"General vendor with minimal risk"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -352,19 +368,25 @@ function VendorOverviewPageContent({
|
|||||||
<EditableField
|
<EditableField
|
||||||
label="Status Page URL"
|
label="Status Page URL"
|
||||||
value={formData.statusPageUrl || ""}
|
value={formData.statusPageUrl || ""}
|
||||||
onChange={(value) => handleFieldChange('statusPageUrl', value)}
|
onChange={(value) =>
|
||||||
|
handleFieldChange("statusPageUrl", value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<EditableField
|
<EditableField
|
||||||
label="Terms of Service URL"
|
label="Terms of Service URL"
|
||||||
value={formData.termsOfServiceUrl || ""}
|
value={formData.termsOfServiceUrl || ""}
|
||||||
onChange={(value) => handleFieldChange('termsOfServiceUrl', value)}
|
onChange={(value) =>
|
||||||
|
handleFieldChange("termsOfServiceUrl", value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<EditableField
|
<EditableField
|
||||||
label="Privacy Policy URL"
|
label="Privacy Policy URL"
|
||||||
value={formData.privacyPolicyUrl || ""}
|
value={formData.privacyPolicyUrl || ""}
|
||||||
onChange={(value) => handleFieldChange('privacyPolicyUrl', value)}
|
onChange={(value) =>
|
||||||
|
handleFieldChange("privacyPolicyUrl", value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -374,10 +396,7 @@ function VendorOverviewPageContent({
|
|||||||
|
|
||||||
{hasChanges && (
|
{hasChanges && (
|
||||||
<div className="fixed bottom-6 right-6 flex gap-2">
|
<div className="fixed bottom-6 right-6 flex gap-2">
|
||||||
<Button
|
<Button variant="outline" onClick={handleCancel}>
|
||||||
variant="outline"
|
|
||||||
onClick={handleCancel}
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
Reference in New Issue
Block a user