Fix all lazy suspense callbacks
Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
@@ -1,87 +1,9 @@
|
|||||||
import { useState } from "react";
|
import { PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
import { useNavigate } from "react-router";
|
import { lazy, Suspense } from "react";
|
||||||
import {
|
import { useLocation } from "react-router";
|
||||||
graphql,
|
import { ErrorBoundaryWithLocation } from "./ErrorBoundary";
|
||||||
ConnectionHandler,
|
|
||||||
useMutation,
|
|
||||||
useLazyLoadQuery,
|
|
||||||
} from "react-relay";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
CardDescription,
|
|
||||||
CardContent,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { useToast } from "@/hooks/use-toast";
|
|
||||||
import { HelpCircle } from "lucide-react";
|
|
||||||
import { CreateOrganizationPageCreateOrganizationMutation } from "./__generated__/CreateOrganizationPageCreateOrganizationMutation.graphql";
|
|
||||||
import { CreateOrganizationPageViewerQuery } from "./__generated__/CreateOrganizationPageViewerQuery.graphql";
|
|
||||||
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
|
||||||
|
|
||||||
const createOrganizationMutation = graphql`
|
export function CreateOrganizationViewSkeleton() {
|
||||||
mutation CreateOrganizationPageCreateOrganizationMutation(
|
|
||||||
$input: CreateOrganizationInput!
|
|
||||||
$connections: [ID!]!
|
|
||||||
) {
|
|
||||||
createOrganization(input: $input) {
|
|
||||||
organizationEdge @appendEdge(connections: $connections) {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
logoUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const viewerQuery = graphql`
|
|
||||||
query CreateOrganizationPageViewerQuery {
|
|
||||||
viewer {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
function EditableField({
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
type = "text",
|
|
||||||
helpText,
|
|
||||||
required,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
type?: string;
|
|
||||||
helpText?: string;
|
|
||||||
required?: boolean;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<HelpCircle className="h-4 w-4 text-gray-400" />
|
|
||||||
<Label className="text-sm">{label}</Label>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Input
|
|
||||||
type={type}
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => onChange(e.target.value)}
|
|
||||||
required={required}
|
|
||||||
/>
|
|
||||||
{helpText && <p className="text-sm text-gray-500">{helpText}</p>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CreateOrganizationPageSkeleton() {
|
|
||||||
return (
|
return (
|
||||||
<PageTemplateSkeleton
|
<PageTemplateSkeleton
|
||||||
title="Create Organization"
|
title="Create Organization"
|
||||||
@@ -92,90 +14,19 @@ export function CreateOrganizationPageSkeleton() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CreateOrganizationPage() {
|
const CreateOrganizationView = lazy(() => import("./CreateOrganizationView"));
|
||||||
const navigate = useNavigate();
|
|
||||||
const { toast } = useToast();
|
|
||||||
const data = useLazyLoadQuery<CreateOrganizationPageViewerQuery>(
|
|
||||||
viewerQuery,
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
const [createOrganization] =
|
|
||||||
useMutation<CreateOrganizationPageCreateOrganizationMutation>(
|
|
||||||
createOrganizationMutation
|
|
||||||
);
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
name: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleFieldChange = (field: keyof typeof formData, value: string) => {
|
export function CreateOrganizationPage() {
|
||||||
setFormData((prev) => ({
|
const location = useLocation();
|
||||||
...prev,
|
|
||||||
[field]: value,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
createOrganization({
|
|
||||||
variables: {
|
|
||||||
input: {
|
|
||||||
name: formData.name,
|
|
||||||
},
|
|
||||||
connections: [
|
|
||||||
ConnectionHandler.getConnectionID(
|
|
||||||
data.viewer.id,
|
|
||||||
"OrganizationSwitcher_organizations"
|
|
||||||
),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
onCompleted: (response) => {
|
|
||||||
const newOrg = response.createOrganization.organizationEdge.node;
|
|
||||||
toast({
|
|
||||||
title: "Success",
|
|
||||||
description: "Organization created successfully",
|
|
||||||
variant: "default",
|
|
||||||
});
|
|
||||||
navigate(`/organizations/${newOrg.id}`);
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: error.message || "Failed to create organization",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageTemplate
|
<Suspense
|
||||||
title="Create Organization"
|
key={location.pathname}
|
||||||
description="Create a new organization to manage your compliance and security needs."
|
fallback={<CreateOrganizationViewSkeleton />}
|
||||||
>
|
>
|
||||||
<form onSubmit={handleSubmit}>
|
<ErrorBoundaryWithLocation>
|
||||||
<Card className="max-w-2xl">
|
<CreateOrganizationView />
|
||||||
<CardHeader>
|
</ErrorBoundaryWithLocation>
|
||||||
<CardTitle>Organization Details</CardTitle>
|
</Suspense>
|
||||||
<CardDescription>
|
|
||||||
Enter the basic information about your organization.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-6">
|
|
||||||
<EditableField
|
|
||||||
label="Organization Name"
|
|
||||||
value={formData.name}
|
|
||||||
onChange={(value) => handleFieldChange("name", value)}
|
|
||||||
helpText="The name of your organization as it will appear throughout the platform."
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button type="submit" className="w-full">
|
|
||||||
Create Organization
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</form>
|
|
||||||
</PageTemplate>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
170
apps/console/src/pages/organizations/CreateOrganizationView.tsx
Normal file
170
apps/console/src/pages/organizations/CreateOrganizationView.tsx
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
import {
|
||||||
|
graphql,
|
||||||
|
ConnectionHandler,
|
||||||
|
useMutation,
|
||||||
|
useLazyLoadQuery,
|
||||||
|
} from "react-relay";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { HelpCircle } from "lucide-react";
|
||||||
|
import { CreateOrganizationViewCreateOrganizationMutation } from "./__generated__/CreateOrganizationViewCreateOrganizationMutation.graphql";
|
||||||
|
import { CreateOrganizationViewViewerQuery } from "./__generated__/CreateOrganizationViewViewerQuery.graphql";
|
||||||
|
import { PageTemplate } from "@/components/PageTemplate";
|
||||||
|
|
||||||
|
const createOrganizationMutation = graphql`
|
||||||
|
mutation CreateOrganizationViewCreateOrganizationMutation(
|
||||||
|
$input: CreateOrganizationInput!
|
||||||
|
$connections: [ID!]!
|
||||||
|
) {
|
||||||
|
createOrganization(input: $input) {
|
||||||
|
organizationEdge @appendEdge(connections: $connections) {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
logoUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const viewerQuery = graphql`
|
||||||
|
query CreateOrganizationViewViewerQuery {
|
||||||
|
viewer {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function EditableField({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
type = "text",
|
||||||
|
helpText,
|
||||||
|
required,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
type?: string;
|
||||||
|
helpText?: string;
|
||||||
|
required?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<HelpCircle className="h-4 w-4 text-gray-400" />
|
||||||
|
<Label className="text-sm">{label}</Label>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Input
|
||||||
|
type={type}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
required={required}
|
||||||
|
/>
|
||||||
|
{helpText && <p className="text-sm text-gray-500">{helpText}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CreateOrganizationView() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const data = useLazyLoadQuery<CreateOrganizationViewViewerQuery>(
|
||||||
|
viewerQuery,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
const [createOrganization] =
|
||||||
|
useMutation<CreateOrganizationViewCreateOrganizationMutation>(
|
||||||
|
createOrganizationMutation
|
||||||
|
);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFieldChange = (field: keyof typeof formData, value: string) => {
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[field]: value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
createOrganization({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
name: formData.name,
|
||||||
|
},
|
||||||
|
connections: [
|
||||||
|
ConnectionHandler.getConnectionID(
|
||||||
|
data.viewer.id,
|
||||||
|
"OrganizationSwitcher_organizations"
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
onCompleted: (response) => {
|
||||||
|
const newOrg = response.createOrganization.organizationEdge.node;
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: "Organization created successfully",
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
|
navigate(`/organizations/${newOrg.id}`);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: error.message || "Failed to create organization",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageTemplate
|
||||||
|
title="Create Organization"
|
||||||
|
description="Create a new organization to manage your compliance and security needs."
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<Card className="max-w-2xl">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Organization Details</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Enter the basic information about your organization.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<EditableField
|
||||||
|
label="Organization Name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(value) => handleFieldChange("name", value)}
|
||||||
|
helpText="The name of your organization as it will appear throughout the platform."
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit" className="w-full">
|
||||||
|
Create Organization
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</form>
|
||||||
|
</PageTemplate>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,227 +1,67 @@
|
|||||||
import { lazy, Suspense } from "react";
|
|
||||||
import { Route, Routes } from "react-router";
|
import { Route, Routes } from "react-router";
|
||||||
import { CreateOrganizationPageSkeleton } from "./CreateOrganizationPage";
|
|
||||||
import { ControlOverviewPageSkeleton } from "./frameworks/controls/ControlOverviewPage";
|
|
||||||
import { CreateControlPageSkeleton } from "./frameworks/controls/CreateControlPage";
|
|
||||||
import { UpdateControlPageSkeleton } from "./frameworks/controls/UpdateControlPage";
|
|
||||||
import { CreateFrameworkPageSkeleton } from "./frameworks/CreateFrameworkPage";
|
|
||||||
import { UpdateFrameworkPageSkeleton } from "./frameworks/UpdateFrameworkPage";
|
|
||||||
import "./HomePage";
|
|
||||||
import { CreatePeoplePageSkeleton } from "./people/CreatePeoplePage";
|
|
||||||
import { PeopleListPageSkeleton } from "./people/PeopleListPage";
|
|
||||||
import { PeopleOverviewPageSkeleton } from "./people/PeopleOverviewPage";
|
|
||||||
import { CreatePolicyPageSkeleton } from "./policies/CreatePolicyPage";
|
|
||||||
import { PolicyListPageSkeleton } from "./policies/PolicyListPage";
|
|
||||||
import { PolicyOverviewPageSkeleton } from "./policies/PolicyOverviewPage";
|
|
||||||
import { UpdatePolicyPageSkeleton } from "./policies/UpdatePolicyPage";
|
|
||||||
import { SettingsPageSkeleton } from "./SettingsPage";
|
|
||||||
import { VendorListPage } from "./vendors/VendorListPage";
|
import { VendorListPage } from "./vendors/VendorListPage";
|
||||||
import { VendorOverviewPageSkeleton } from "./vendors/VendorOverviewPage";
|
|
||||||
import { ErrorBoundaryWithLocation } from "./ErrorBoundary";
|
|
||||||
import OrganizationLayout from "./OrganizationLayout";
|
import OrganizationLayout from "./OrganizationLayout";
|
||||||
import NoOrganizationLayout from "./NoOrganizationLayout";
|
import NoOrganizationLayout from "./NoOrganizationLayout";
|
||||||
import { FrameworkListPage } from "./frameworks/FrameworkListPage";
|
import { FrameworkListPage } from "./frameworks/FrameworkListPage";
|
||||||
import { FrameworkPage } from "./frameworks/FrameworkPage";
|
import { FrameworkPage } from "./frameworks/FrameworkPage";
|
||||||
|
import { PeopleListPage } from "./people/PeopleListPage";
|
||||||
const CreateOrganizationPage = lazy(() => import("./CreateOrganizationPage"));
|
import { CreatePeoplePage } from "./people/CreatePeoplePage";
|
||||||
const ControlOverviewPage = lazy(
|
import { PeoplePage } from "./people/PeoplePage";
|
||||||
() => import("./frameworks/controls/ControlOverviewPage")
|
import { CreateFrameworkPage } from "./frameworks/CreateFrameworkPage";
|
||||||
);
|
import { UpdateFrameworkPage } from "./frameworks/UpdateFrameworkPage";
|
||||||
const CreateControlPage = lazy(
|
import { CreateControlPage } from "./frameworks/controls/CreateControlPage";
|
||||||
() => import("./frameworks/controls/CreateControlPage")
|
import { ControlPage } from "./frameworks/controls/ControlPage";
|
||||||
);
|
import { UpdateControlPage } from "./frameworks/controls/UpdateControlPage";
|
||||||
const UpdateControlPage = lazy(
|
import { VendorPage } from "./vendors/VendorPage";
|
||||||
() => import("./frameworks/controls/UpdateControlPage")
|
import { PolicyListPage } from "./policies/PolicyListPage";
|
||||||
);
|
import { CreatePolicyPage } from "./policies/CreatePolicyPage";
|
||||||
const CreateFrameworkPage = lazy(
|
import { PolicyPage } from "./policies/PolicyPage";
|
||||||
() => import("./frameworks/CreateFrameworkPage")
|
import { UpdatePolicyPage } from "./policies/UpdatePolicyPage";
|
||||||
);
|
import { SettingsPage } from "./SettingsPage";
|
||||||
const UpdateFrameworkPage = lazy(
|
import { CreateOrganizationPage } from "./CreateOrganizationPage";
|
||||||
() => import("./frameworks/UpdateFrameworkPage")
|
import HomePage from "./HomePage";
|
||||||
);
|
|
||||||
const HomePage = lazy(() => import("./HomePage"));
|
|
||||||
const CreatePeoplePage = lazy(() => import("./people/CreatePeoplePage"));
|
|
||||||
const PeopleListPage = lazy(() => import("./people/PeopleListPage"));
|
|
||||||
const PeopleOverviewPage = lazy(() => import("./people/PeopleOverviewPage"));
|
|
||||||
const CreatePolicyPage = lazy(() => import("./policies/CreatePolicyPage"));
|
|
||||||
const PolicyListPage = lazy(() => import("./policies/PolicyListPage"));
|
|
||||||
const PolicyOverviewPage = lazy(() => import("./policies/PolicyOverviewPage"));
|
|
||||||
const UpdatePolicyPage = lazy(() => import("./policies/UpdatePolicyPage"));
|
|
||||||
const SettingsPage = lazy(() => import("./SettingsPage"));
|
|
||||||
|
|
||||||
const VendorOverviewPage = lazy(() => import("./vendors/VendorOverviewPage"));
|
|
||||||
|
|
||||||
export function OrganizationsRoutes() {
|
export function OrganizationsRoutes() {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path=":organizationId/*" element={<OrganizationLayout />}>
|
<Route path=":organizationId/*" element={<OrganizationLayout />}>
|
||||||
<Route
|
<Route index element={<HomePage />} />
|
||||||
index
|
<Route path="people" element={<PeopleListPage />} />
|
||||||
element={
|
<Route path="people/create" element={<CreatePeoplePage />} />
|
||||||
<Suspense>
|
<Route path="people/:peopleId" element={<PeoplePage />} />
|
||||||
<ErrorBoundaryWithLocation>
|
|
||||||
<HomePage />
|
|
||||||
</ErrorBoundaryWithLocation>
|
|
||||||
</Suspense>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="people"
|
|
||||||
element={
|
|
||||||
<Suspense fallback={<PeopleListPageSkeleton />}>
|
|
||||||
<ErrorBoundaryWithLocation>
|
|
||||||
<PeopleListPage />
|
|
||||||
</ErrorBoundaryWithLocation>
|
|
||||||
</Suspense>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="people/create"
|
|
||||||
element={
|
|
||||||
<Suspense fallback={<CreatePeoplePageSkeleton />}>
|
|
||||||
<ErrorBoundaryWithLocation>
|
|
||||||
<CreatePeoplePage />
|
|
||||||
</ErrorBoundaryWithLocation>
|
|
||||||
</Suspense>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="people/:peopleId"
|
|
||||||
element={
|
|
||||||
<Suspense fallback={<PeopleOverviewPageSkeleton />}>
|
|
||||||
<ErrorBoundaryWithLocation>
|
|
||||||
<PeopleOverviewPage />
|
|
||||||
</ErrorBoundaryWithLocation>
|
|
||||||
</Suspense>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route path="vendors" element={<VendorListPage />} />
|
<Route path="vendors" element={<VendorListPage />} />
|
||||||
<Route path="frameworks" element={<FrameworkListPage />} />
|
<Route path="frameworks" element={<FrameworkListPage />} />
|
||||||
<Route
|
<Route path="frameworks/create" element={<CreateFrameworkPage />} />
|
||||||
path="frameworks/create"
|
|
||||||
element={
|
|
||||||
<Suspense fallback={<CreateFrameworkPageSkeleton />}>
|
|
||||||
<ErrorBoundaryWithLocation>
|
|
||||||
<CreateFrameworkPage />
|
|
||||||
</ErrorBoundaryWithLocation>
|
|
||||||
</Suspense>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route path="frameworks/:frameworkId" element={<FrameworkPage />} />
|
<Route path="frameworks/:frameworkId" element={<FrameworkPage />} />
|
||||||
<Route
|
<Route
|
||||||
path="frameworks/:frameworkId/update"
|
path="frameworks/:frameworkId/update"
|
||||||
element={
|
element={<UpdateFrameworkPage />}
|
||||||
<Suspense fallback={<UpdateFrameworkPageSkeleton />}>
|
|
||||||
<ErrorBoundaryWithLocation>
|
|
||||||
<UpdateFrameworkPage />
|
|
||||||
</ErrorBoundaryWithLocation>
|
|
||||||
</Suspense>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="frameworks/:frameworkId/controls/create"
|
path="frameworks/:frameworkId/controls/create"
|
||||||
element={
|
element={<CreateControlPage />}
|
||||||
<Suspense fallback={<CreateControlPageSkeleton />}>
|
|
||||||
<ErrorBoundaryWithLocation>
|
|
||||||
<CreateControlPage />
|
|
||||||
</ErrorBoundaryWithLocation>
|
|
||||||
</Suspense>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="frameworks/:frameworkId/controls/:controlId"
|
path="frameworks/:frameworkId/controls/:controlId"
|
||||||
element={
|
element={<ControlPage />}
|
||||||
<Suspense fallback={<ControlOverviewPageSkeleton />}>
|
|
||||||
<ErrorBoundaryWithLocation>
|
|
||||||
<ControlOverviewPage />
|
|
||||||
</ErrorBoundaryWithLocation>
|
|
||||||
</Suspense>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="frameworks/:frameworkId/controls/:controlId/update"
|
path="frameworks/:frameworkId/controls/:controlId/update"
|
||||||
element={
|
element={<UpdateControlPage />}
|
||||||
<Suspense fallback={<UpdateControlPageSkeleton />}>
|
|
||||||
<ErrorBoundaryWithLocation>
|
|
||||||
<UpdateControlPage />
|
|
||||||
</ErrorBoundaryWithLocation>
|
|
||||||
</Suspense>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="vendors/:vendorId"
|
|
||||||
element={
|
|
||||||
<Suspense fallback={<VendorOverviewPageSkeleton />}>
|
|
||||||
<ErrorBoundaryWithLocation>
|
|
||||||
<VendorOverviewPage />
|
|
||||||
</ErrorBoundaryWithLocation>
|
|
||||||
</Suspense>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{/* Policy Routes */}
|
|
||||||
<Route
|
|
||||||
path="policies"
|
|
||||||
element={
|
|
||||||
<Suspense fallback={<PolicyListPageSkeleton />}>
|
|
||||||
<ErrorBoundaryWithLocation>
|
|
||||||
<PolicyListPage />
|
|
||||||
</ErrorBoundaryWithLocation>
|
|
||||||
</Suspense>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="policies/create"
|
|
||||||
element={
|
|
||||||
<Suspense fallback={<CreatePolicyPageSkeleton />}>
|
|
||||||
<ErrorBoundaryWithLocation>
|
|
||||||
<CreatePolicyPage />
|
|
||||||
</ErrorBoundaryWithLocation>
|
|
||||||
</Suspense>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="policies/:policyId"
|
|
||||||
element={
|
|
||||||
<Suspense fallback={<PolicyOverviewPageSkeleton />}>
|
|
||||||
<ErrorBoundaryWithLocation>
|
|
||||||
<PolicyOverviewPage />
|
|
||||||
</ErrorBoundaryWithLocation>
|
|
||||||
</Suspense>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
<Route path="vendors/:vendorId" element={<VendorPage />} />
|
||||||
|
<Route path="policies" element={<PolicyListPage />} />
|
||||||
|
<Route path="policies/create" element={<CreatePolicyPage />} />
|
||||||
|
<Route path="policies/:policyId" element={<PolicyPage />} />
|
||||||
<Route
|
<Route
|
||||||
path="policies/:policyId/update"
|
path="policies/:policyId/update"
|
||||||
element={
|
element={<UpdatePolicyPage />}
|
||||||
<Suspense fallback={<UpdatePolicyPageSkeleton />}>
|
|
||||||
<ErrorBoundaryWithLocation>
|
|
||||||
<UpdatePolicyPage />
|
|
||||||
</ErrorBoundaryWithLocation>
|
|
||||||
</Suspense>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="settings"
|
|
||||||
element={
|
|
||||||
<Suspense fallback={<SettingsPageSkeleton />}>
|
|
||||||
<ErrorBoundaryWithLocation>
|
|
||||||
<SettingsPage />
|
|
||||||
</ErrorBoundaryWithLocation>
|
|
||||||
</Suspense>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
<Route path="settings" element={<SettingsPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="*" element={<NoOrganizationLayout />}>
|
<Route path="*" element={<NoOrganizationLayout />}>
|
||||||
<Route
|
<Route path="create" element={<CreateOrganizationPage />} />
|
||||||
path="create"
|
|
||||||
element={
|
|
||||||
<Suspense fallback={<CreateOrganizationPageSkeleton />}>
|
|
||||||
<ErrorBoundaryWithLocation>
|
|
||||||
<CreateOrganizationPage />
|
|
||||||
</ErrorBoundaryWithLocation>
|
|
||||||
</Suspense>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,525 +1,11 @@
|
|||||||
import { Building2, Upload, MoreVertical } from "lucide-react";
|
import { PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
import { Button } from "@/components/ui/button";
|
import { lazy, Suspense } from "react";
|
||||||
import {
|
import { useLocation } from "react-router";
|
||||||
Card,
|
import { ErrorBoundaryWithLocation } from "./ErrorBoundary";
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
CardDescription,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
import { Suspense, useEffect, useState, useRef } from "react";
|
|
||||||
import {
|
|
||||||
graphql,
|
|
||||||
PreloadedQuery,
|
|
||||||
usePreloadedQuery,
|
|
||||||
useQueryLoader,
|
|
||||||
useMutation,
|
|
||||||
} from "react-relay";
|
|
||||||
import { useParams } from "react-router";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { useToast } from "@/hooks/use-toast";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import type { SettingsPageQuery as SettingsPageQueryType } from "./__generated__/SettingsPageQuery.graphql";
|
|
||||||
import type { SettingsPageUpdateOrganizationMutation as SettingsPageUpdateOrganizationMutationType } from "./__generated__/SettingsPageUpdateOrganizationMutation.graphql";
|
|
||||||
import type { SettingsPageInviteUserMutation as SettingsPageInviteUserMutationType } from "./__generated__/SettingsPageInviteUserMutation.graphql";
|
|
||||||
import type { SettingsPageRemoveUserMutation as SettingsPageRemoveUserMutationType } from "./__generated__/SettingsPageRemoveUserMutation.graphql";
|
|
||||||
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
|
||||||
|
|
||||||
const settingsPageQuery = graphql`
|
const SettingsView = lazy(() => import("./SettingsView"));
|
||||||
query SettingsPageQuery($organizationID: ID!) {
|
|
||||||
organization: node(id: $organizationID) {
|
|
||||||
id
|
|
||||||
... on Organization {
|
|
||||||
name
|
|
||||||
logoUrl
|
|
||||||
users(first: 100) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
fullName
|
|
||||||
email
|
|
||||||
createdAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const updateOrganizationMutation = graphql`
|
export function SettingsViewSkeleton() {
|
||||||
mutation SettingsPageUpdateOrganizationMutation(
|
|
||||||
$input: UpdateOrganizationInput!
|
|
||||||
) {
|
|
||||||
updateOrganization(input: $input) {
|
|
||||||
organization {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
logoUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const inviteUserMutation = graphql`
|
|
||||||
mutation SettingsPageInviteUserMutation($input: InviteUserInput!) {
|
|
||||||
inviteUser(input: $input) {
|
|
||||||
success
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const removeUserMutation = graphql`
|
|
||||||
mutation SettingsPageRemoveUserMutation($input: RemoveUserInput!) {
|
|
||||||
removeUser(input: $input) {
|
|
||||||
success
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
function SettingsPageContent({
|
|
||||||
queryRef,
|
|
||||||
}: {
|
|
||||||
queryRef: PreloadedQuery<SettingsPageQueryType>;
|
|
||||||
}) {
|
|
||||||
const data = usePreloadedQuery(settingsPageQuery, queryRef);
|
|
||||||
const organization = data.organization;
|
|
||||||
const users = organization.users?.edges.map((edge) => edge.node) || [];
|
|
||||||
const { toast } = useToast();
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const [isEditNameOpen, setIsEditNameOpen] = useState(false);
|
|
||||||
const [isInviteOpen, setIsInviteOpen] = useState(false);
|
|
||||||
const [inviteEmail, setInviteEmail] = useState("");
|
|
||||||
const [inviteFullName, setInviteFullName] = useState("");
|
|
||||||
const [isInviting, setIsInviting] = useState(false);
|
|
||||||
const [organizationName, setOrganizationName] = useState(
|
|
||||||
organization.name || ""
|
|
||||||
);
|
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
|
||||||
const [isRemoving, setIsRemoving] = useState(false);
|
|
||||||
|
|
||||||
const [updateOrganization] =
|
|
||||||
useMutation<SettingsPageUpdateOrganizationMutationType>(
|
|
||||||
updateOrganizationMutation
|
|
||||||
);
|
|
||||||
|
|
||||||
const [inviteUser] =
|
|
||||||
useMutation<SettingsPageInviteUserMutationType>(inviteUserMutation);
|
|
||||||
|
|
||||||
const [removeUser] =
|
|
||||||
useMutation<SettingsPageRemoveUserMutationType>(removeUserMutation);
|
|
||||||
|
|
||||||
const { organizationId } = useParams();
|
|
||||||
const [, loadQuery] =
|
|
||||||
useQueryLoader<SettingsPageQueryType>(settingsPageQuery);
|
|
||||||
|
|
||||||
const handleUpdateName = () => {
|
|
||||||
updateOrganization({
|
|
||||||
variables: {
|
|
||||||
input: {
|
|
||||||
organizationId: organization.id,
|
|
||||||
name: organizationName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
onCompleted: () => {
|
|
||||||
toast({
|
|
||||||
title: "Organization updated",
|
|
||||||
description: "Organization name has been updated successfully.",
|
|
||||||
variant: "default",
|
|
||||||
});
|
|
||||||
setIsEditNameOpen(false);
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast({
|
|
||||||
title: "Error updating organization",
|
|
||||||
description: error.message,
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLogoUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const file = e.target.files?.[0];
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
// Create a FileReader to read the file as a data URL
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onloadend = () => {
|
|
||||||
setIsUploading(true);
|
|
||||||
|
|
||||||
updateOrganization({
|
|
||||||
variables: {
|
|
||||||
input: {
|
|
||||||
organizationId: organization.id,
|
|
||||||
logo: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
uploadables: {
|
|
||||||
"input.logo": file,
|
|
||||||
},
|
|
||||||
onCompleted: () => {
|
|
||||||
setIsUploading(false);
|
|
||||||
toast({
|
|
||||||
title: "Logo updated",
|
|
||||||
description: "Organization logo has been updated successfully.",
|
|
||||||
variant: "default",
|
|
||||||
});
|
|
||||||
if (fileInputRef.current) {
|
|
||||||
fileInputRef.current.value = "";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
setIsUploading(false);
|
|
||||||
toast({
|
|
||||||
title: "Error updating logo",
|
|
||||||
description: error.message,
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInviteMember = () => {
|
|
||||||
if (!inviteEmail || !inviteFullName) {
|
|
||||||
toast({
|
|
||||||
title: "Missing information",
|
|
||||||
description:
|
|
||||||
"Please provide both email and full name for the invitation",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsInviting(true);
|
|
||||||
|
|
||||||
inviteUser({
|
|
||||||
variables: {
|
|
||||||
input: {
|
|
||||||
organizationId: organization.id,
|
|
||||||
email: inviteEmail,
|
|
||||||
fullName: inviteFullName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
onCompleted: (response) => {
|
|
||||||
setIsInviting(false);
|
|
||||||
if (response.inviteUser?.success) {
|
|
||||||
toast({
|
|
||||||
title: "Invitation sent",
|
|
||||||
description: `An invitation has been sent to ${inviteEmail}`,
|
|
||||||
variant: "default",
|
|
||||||
});
|
|
||||||
setIsInviteOpen(false);
|
|
||||||
setInviteEmail("");
|
|
||||||
setInviteFullName("");
|
|
||||||
} else {
|
|
||||||
toast({
|
|
||||||
title: "Error sending invitation",
|
|
||||||
description: "The invitation could not be sent. Please try again.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
setIsInviting(false);
|
|
||||||
toast({
|
|
||||||
title: "Error sending invitation",
|
|
||||||
description: error.message,
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveUser = (userId: string) => {
|
|
||||||
setIsRemoving(true);
|
|
||||||
|
|
||||||
removeUser({
|
|
||||||
variables: {
|
|
||||||
input: {
|
|
||||||
organizationId: organization.id,
|
|
||||||
userId: userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
onCompleted: (response) => {
|
|
||||||
setIsRemoving(false);
|
|
||||||
if (response.removeUser?.success) {
|
|
||||||
toast({
|
|
||||||
title: "User removed",
|
|
||||||
description: "The user has been removed from the organization.",
|
|
||||||
variant: "default",
|
|
||||||
});
|
|
||||||
// Refresh the query to update the UI
|
|
||||||
loadQuery({ organizationID: organizationId! });
|
|
||||||
} else {
|
|
||||||
toast({
|
|
||||||
title: "Error removing user",
|
|
||||||
description: "The user could not be removed. Please try again.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
setIsRemoving(false);
|
|
||||||
toast({
|
|
||||||
title: "Error removing user",
|
|
||||||
description: error.message,
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageTemplate
|
|
||||||
title="Settings"
|
|
||||||
description="Manage your details and personal preferences here"
|
|
||||||
>
|
|
||||||
<div className="space-y-6">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Organization information</CardTitle>
|
|
||||||
<CardDescription>Manage your organization details</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-6">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium">Organization logo</label>
|
|
||||||
<div className="flex items-center justify-between rounded-lg border p-3 shadow-xs">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{organization.logoUrl ? (
|
|
||||||
<img
|
|
||||||
src={organization.logoUrl}
|
|
||||||
alt="Logo"
|
|
||||||
className="h-10 w-10 rounded-lg object-cover"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg border">
|
|
||||||
<Upload className="h-5 w-5 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
Upload a logo to be displayed at the top of your trust page
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
ref={fileInputRef}
|
|
||||||
onChange={handleLogoUpload}
|
|
||||||
accept="image/*"
|
|
||||||
className="hidden"
|
|
||||||
id="logo-upload"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => fileInputRef.current?.click()}
|
|
||||||
disabled={isUploading}
|
|
||||||
>
|
|
||||||
{isUploading ? "Uploading..." : "Change image"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium">Organization name</label>
|
|
||||||
<div className="flex items-center justify-between rounded-lg border p-3 shadow-xs">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Building2 className="h-5 w-5 text-muted-foreground" />
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
Set the name of the organization
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-sm">{organization.name}</span>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
setOrganizationName(organization.name || "");
|
|
||||||
setIsEditNameOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<CardTitle>Workspace members</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Manage who has privileged access to your workspace and their
|
|
||||||
permissions.
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setIsInviteOpen(true)}
|
|
||||||
>
|
|
||||||
<Upload className="mr-2 h-4 w-4" />
|
|
||||||
Invite member
|
|
||||||
</Button>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{users.length === 0 ? (
|
|
||||||
<div className="flex flex-col items-center justify-center p-8 text-center">
|
|
||||||
<div className="rounded-full bg-muted p-3">
|
|
||||||
<Building2 className="h-6 w-6 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
<h3 className="mt-4 text-lg font-medium">No members found</h3>
|
|
||||||
<p className="mt-2 text-sm text-muted-foreground">
|
|
||||||
You haven't added any members to your workspace yet.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
users.map((user) => (
|
|
||||||
<div
|
|
||||||
key={user.id}
|
|
||||||
className="flex items-center justify-between rounded-lg border p-3 shadow-xs"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Avatar>
|
|
||||||
<AvatarFallback>
|
|
||||||
{user.fullName.charAt(0).toUpperCase()}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
{user.fullName}
|
|
||||||
</span>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{user.email}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
Owner
|
|
||||||
</span>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="ghost" size="icon">
|
|
||||||
<MoreVertical className="h-4 w-4" />
|
|
||||||
<span className="sr-only">Open menu</span>
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="text-red-600"
|
|
||||||
onClick={() => handleRemoveUser(user.id)}
|
|
||||||
disabled={isRemoving}
|
|
||||||
>
|
|
||||||
{isRemoving ? "Removing..." : "Remove member"}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Dialog open={isEditNameOpen} onOpenChange={setIsEditNameOpen}>
|
|
||||||
<DialogContent className="sm:max-w-md">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Edit Organization Name</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Update the name of your organization.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="space-y-4 py-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="organization-name">Organization Name</Label>
|
|
||||||
<Input
|
|
||||||
id="organization-name"
|
|
||||||
value={organizationName}
|
|
||||||
onChange={(e) => setOrganizationName(e.target.value)}
|
|
||||||
placeholder="Enter organization name"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="outline" onClick={() => setIsEditNameOpen(false)}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleUpdateName}>Save Changes</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<Dialog open={isInviteOpen} onOpenChange={setIsInviteOpen}>
|
|
||||||
<DialogContent className="sm:max-w-md">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Invite Team Member</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Send an invitation to join your workspace.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="space-y-4 py-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="email">Email Address</Label>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
value={inviteEmail}
|
|
||||||
onChange={(e) => setInviteEmail(e.target.value)}
|
|
||||||
placeholder="Enter email address"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="fullName">Full Name</Label>
|
|
||||||
<Input
|
|
||||||
id="fullName"
|
|
||||||
type="text"
|
|
||||||
value={inviteFullName}
|
|
||||||
onChange={(e) => setInviteFullName(e.target.value)}
|
|
||||||
placeholder="Enter full name"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="outline" onClick={() => setIsInviteOpen(false)}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleInviteMember} disabled={isInviting}>
|
|
||||||
{isInviting ? "Sending..." : "Send Invitation"}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</PageTemplate>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SettingsPageSkeleton() {
|
|
||||||
return (
|
return (
|
||||||
<PageTemplateSkeleton
|
<PageTemplateSkeleton
|
||||||
title="Settings"
|
title="Settings"
|
||||||
@@ -540,23 +26,14 @@ export function SettingsPageSkeleton() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export function SettingsPage() {
|
||||||
const [queryRef, loadQuery] =
|
const location = useLocation();
|
||||||
useQueryLoader<SettingsPageQueryType>(settingsPageQuery);
|
|
||||||
|
|
||||||
const { organizationId } = useParams();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadQuery({ organizationID: organizationId! });
|
|
||||||
}, [loadQuery, organizationId]);
|
|
||||||
|
|
||||||
if (!queryRef) {
|
|
||||||
return <SettingsPageSkeleton />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<SettingsPageSkeleton />}>
|
<Suspense key={location.pathname} fallback={<SettingsViewSkeleton />}>
|
||||||
<SettingsPageContent queryRef={queryRef} />
|
<ErrorBoundaryWithLocation>
|
||||||
|
<SettingsView />
|
||||||
|
</ErrorBoundaryWithLocation>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
542
apps/console/src/pages/organizations/SettingsView.tsx
Normal file
542
apps/console/src/pages/organizations/SettingsView.tsx
Normal file
@@ -0,0 +1,542 @@
|
|||||||
|
import { Building2, Upload, MoreVertical } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
CardDescription,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Suspense, useEffect, useState, useRef } from "react";
|
||||||
|
import {
|
||||||
|
graphql,
|
||||||
|
PreloadedQuery,
|
||||||
|
usePreloadedQuery,
|
||||||
|
useQueryLoader,
|
||||||
|
useMutation,
|
||||||
|
} from "react-relay";
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import type { SettingsViewQuery as SettingsViewQueryType } from "./__generated__/SettingsViewQuery.graphql";
|
||||||
|
import type { SettingsViewUpdateOrganizationMutation as SettingsViewUpdateOrganizationMutationType } from "./__generated__/SettingsViewUpdateOrganizationMutation.graphql";
|
||||||
|
import type { SettingsViewInviteUserMutation as SettingsViewInviteUserMutationType } from "./__generated__/SettingsViewInviteUserMutation.graphql";
|
||||||
|
import type { SettingsViewRemoveUserMutation as SettingsViewRemoveUserMutationType } from "./__generated__/SettingsViewRemoveUserMutation.graphql";
|
||||||
|
import { PageTemplate } from "@/components/PageTemplate";
|
||||||
|
import { SettingsViewSkeleton } from "./SettingsPage";
|
||||||
|
|
||||||
|
const settingsViewQuery = graphql`
|
||||||
|
query SettingsViewQuery($organizationID: ID!) {
|
||||||
|
organization: node(id: $organizationID) {
|
||||||
|
id
|
||||||
|
... on Organization {
|
||||||
|
name
|
||||||
|
logoUrl
|
||||||
|
users(first: 100) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
fullName
|
||||||
|
email
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const updateOrganizationMutation = graphql`
|
||||||
|
mutation SettingsViewUpdateOrganizationMutation(
|
||||||
|
$input: UpdateOrganizationInput!
|
||||||
|
) {
|
||||||
|
updateOrganization(input: $input) {
|
||||||
|
organization {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
logoUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const inviteUserMutation = graphql`
|
||||||
|
mutation SettingsViewInviteUserMutation($input: InviteUserInput!) {
|
||||||
|
inviteUser(input: $input) {
|
||||||
|
success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const removeUserMutation = graphql`
|
||||||
|
mutation SettingsViewRemoveUserMutation($input: RemoveUserInput!) {
|
||||||
|
removeUser(input: $input) {
|
||||||
|
success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function SettingsViewContent({
|
||||||
|
queryRef,
|
||||||
|
}: {
|
||||||
|
queryRef: PreloadedQuery<SettingsViewQueryType>;
|
||||||
|
}) {
|
||||||
|
const data = usePreloadedQuery(settingsViewQuery, queryRef);
|
||||||
|
const organization = data.organization;
|
||||||
|
const users = organization.users?.edges.map((edge) => edge.node) || [];
|
||||||
|
const { toast } = useToast();
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const [isEditNameOpen, setIsEditNameOpen] = useState(false);
|
||||||
|
const [isInviteOpen, setIsInviteOpen] = useState(false);
|
||||||
|
const [inviteEmail, setInviteEmail] = useState("");
|
||||||
|
const [inviteFullName, setInviteFullName] = useState("");
|
||||||
|
const [isInviting, setIsInviting] = useState(false);
|
||||||
|
const [organizationName, setOrganizationName] = useState(
|
||||||
|
organization.name || ""
|
||||||
|
);
|
||||||
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
const [isRemoving, setIsRemoving] = useState(false);
|
||||||
|
|
||||||
|
const [updateOrganization] =
|
||||||
|
useMutation<SettingsViewUpdateOrganizationMutationType>(
|
||||||
|
updateOrganizationMutation
|
||||||
|
);
|
||||||
|
|
||||||
|
const [inviteUser] =
|
||||||
|
useMutation<SettingsViewInviteUserMutationType>(inviteUserMutation);
|
||||||
|
|
||||||
|
const [removeUser] =
|
||||||
|
useMutation<SettingsViewRemoveUserMutationType>(removeUserMutation);
|
||||||
|
|
||||||
|
const { organizationId } = useParams();
|
||||||
|
const [, loadQuery] =
|
||||||
|
useQueryLoader<SettingsViewQueryType>(settingsViewQuery);
|
||||||
|
|
||||||
|
const handleUpdateName = () => {
|
||||||
|
updateOrganization({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
organizationId: organization.id,
|
||||||
|
name: organizationName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onCompleted: () => {
|
||||||
|
toast({
|
||||||
|
title: "Organization updated",
|
||||||
|
description: "Organization name has been updated successfully.",
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
|
setIsEditNameOpen(false);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast({
|
||||||
|
title: "Error updating organization",
|
||||||
|
description: error.message,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogoUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
// Create a FileReader to read the file as a data URL
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = () => {
|
||||||
|
setIsUploading(true);
|
||||||
|
|
||||||
|
updateOrganization({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
organizationId: organization.id,
|
||||||
|
logo: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
uploadables: {
|
||||||
|
"input.logo": file,
|
||||||
|
},
|
||||||
|
onCompleted: () => {
|
||||||
|
setIsUploading(false);
|
||||||
|
toast({
|
||||||
|
title: "Logo updated",
|
||||||
|
description: "Organization logo has been updated successfully.",
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
setIsUploading(false);
|
||||||
|
toast({
|
||||||
|
title: "Error updating logo",
|
||||||
|
description: error.message,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInviteMember = () => {
|
||||||
|
if (!inviteEmail || !inviteFullName) {
|
||||||
|
toast({
|
||||||
|
title: "Missing information",
|
||||||
|
description:
|
||||||
|
"Please provide both email and full name for the invitation",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsInviting(true);
|
||||||
|
|
||||||
|
inviteUser({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
organizationId: organization.id,
|
||||||
|
email: inviteEmail,
|
||||||
|
fullName: inviteFullName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onCompleted: (response) => {
|
||||||
|
setIsInviting(false);
|
||||||
|
if (response.inviteUser?.success) {
|
||||||
|
toast({
|
||||||
|
title: "Invitation sent",
|
||||||
|
description: `An invitation has been sent to ${inviteEmail}`,
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
|
setIsInviteOpen(false);
|
||||||
|
setInviteEmail("");
|
||||||
|
setInviteFullName("");
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: "Error sending invitation",
|
||||||
|
description: "The invitation could not be sent. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
setIsInviting(false);
|
||||||
|
toast({
|
||||||
|
title: "Error sending invitation",
|
||||||
|
description: error.message,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveUser = (userId: string) => {
|
||||||
|
setIsRemoving(true);
|
||||||
|
|
||||||
|
removeUser({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
organizationId: organization.id,
|
||||||
|
userId: userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onCompleted: (response) => {
|
||||||
|
setIsRemoving(false);
|
||||||
|
if (response.removeUser?.success) {
|
||||||
|
toast({
|
||||||
|
title: "User removed",
|
||||||
|
description: "The user has been removed from the organization.",
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
|
// Refresh the query to update the UI
|
||||||
|
loadQuery({ organizationID: organizationId! });
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: "Error removing user",
|
||||||
|
description: "The user could not be removed. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
setIsRemoving(false);
|
||||||
|
toast({
|
||||||
|
title: "Error removing user",
|
||||||
|
description: error.message,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageTemplate
|
||||||
|
title="Settings"
|
||||||
|
description="Manage your details and personal preferences here"
|
||||||
|
>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Organization information</CardTitle>
|
||||||
|
<CardDescription>Manage your organization details</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Organization logo</label>
|
||||||
|
<div className="flex items-center justify-between rounded-lg border p-3 shadow-xs">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{organization.logoUrl ? (
|
||||||
|
<img
|
||||||
|
src={organization.logoUrl}
|
||||||
|
alt="Logo"
|
||||||
|
className="h-10 w-10 rounded-lg object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg border">
|
||||||
|
<Upload className="h-5 w-5 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
Upload a logo to be displayed at the top of your trust page
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={handleLogoUpload}
|
||||||
|
accept="image/*"
|
||||||
|
className="hidden"
|
||||||
|
id="logo-upload"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
disabled={isUploading}
|
||||||
|
>
|
||||||
|
{isUploading ? "Uploading..." : "Change image"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Organization name</label>
|
||||||
|
<div className="flex items-center justify-between rounded-lg border p-3 shadow-xs">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Building2 className="h-5 w-5 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
Set the name of the organization
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm">{organization.name}</span>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setOrganizationName(organization.name || "");
|
||||||
|
setIsEditNameOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle>Workspace members</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Manage who has privileged access to your workspace and their
|
||||||
|
permissions.
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setIsInviteOpen(true)}
|
||||||
|
>
|
||||||
|
<Upload className="mr-2 h-4 w-4" />
|
||||||
|
Invite member
|
||||||
|
</Button>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{users.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center p-8 text-center">
|
||||||
|
<div className="rounded-full bg-muted p-3">
|
||||||
|
<Building2 className="h-6 w-6 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<h3 className="mt-4 text-lg font-medium">No members found</h3>
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
|
You haven't added any members to your workspace yet.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
users.map((user) => (
|
||||||
|
<div
|
||||||
|
key={user.id}
|
||||||
|
className="flex items-center justify-between rounded-lg border p-3 shadow-xs"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Avatar>
|
||||||
|
<AvatarFallback>
|
||||||
|
{user.fullName.charAt(0).toUpperCase()}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{user.fullName}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{user.email}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Owner
|
||||||
|
</span>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<MoreVertical className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Open menu</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="text-red-600"
|
||||||
|
onClick={() => handleRemoveUser(user.id)}
|
||||||
|
disabled={isRemoving}
|
||||||
|
>
|
||||||
|
{isRemoving ? "Removing..." : "Remove member"}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog open={isEditNameOpen} onOpenChange={setIsEditNameOpen}>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Edit Organization Name</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Update the name of your organization.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="organization-name">Organization Name</Label>
|
||||||
|
<Input
|
||||||
|
id="organization-name"
|
||||||
|
value={organizationName}
|
||||||
|
onChange={(e) => setOrganizationName(e.target.value)}
|
||||||
|
placeholder="Enter organization name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setIsEditNameOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleUpdateName}>Save Changes</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={isInviteOpen} onOpenChange={setIsInviteOpen}>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Invite Team Member</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Send an invitation to join your workspace.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="email">Email Address</Label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
value={inviteEmail}
|
||||||
|
onChange={(e) => setInviteEmail(e.target.value)}
|
||||||
|
placeholder="Enter email address"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="fullName">Full Name</Label>
|
||||||
|
<Input
|
||||||
|
id="fullName"
|
||||||
|
type="text"
|
||||||
|
value={inviteFullName}
|
||||||
|
onChange={(e) => setInviteFullName(e.target.value)}
|
||||||
|
placeholder="Enter full name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setIsInviteOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleInviteMember} disabled={isInviting}>
|
||||||
|
{isInviting ? "Sending..." : "Send Invitation"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</PageTemplate>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SettingsView() {
|
||||||
|
const [queryRef, loadQuery] =
|
||||||
|
useQueryLoader<SettingsViewQueryType>(settingsViewQuery);
|
||||||
|
|
||||||
|
const { organizationId } = useParams();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadQuery({ organizationID: organizationId! });
|
||||||
|
}, [loadQuery, organizationId]);
|
||||||
|
|
||||||
|
if (!queryRef) {
|
||||||
|
return <SettingsViewSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<SettingsViewSkeleton />}>
|
||||||
|
<SettingsViewContent queryRef={queryRef} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<a147a01f27db2e92570cd66f6b9c1df9>>
|
* @generated SignedSource<<d5934a25e82e73935b121d9f456b41d5>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -12,11 +12,11 @@ import { ConcreteRequest } from 'relay-runtime';
|
|||||||
export type CreateOrganizationInput = {
|
export type CreateOrganizationInput = {
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
export type CreateOrganizationPageCreateOrganizationMutation$variables = {
|
export type CreateOrganizationViewCreateOrganizationMutation$variables = {
|
||||||
connections: ReadonlyArray<string>;
|
connections: ReadonlyArray<string>;
|
||||||
input: CreateOrganizationInput;
|
input: CreateOrganizationInput;
|
||||||
};
|
};
|
||||||
export type CreateOrganizationPageCreateOrganizationMutation$data = {
|
export type CreateOrganizationViewCreateOrganizationMutation$data = {
|
||||||
readonly createOrganization: {
|
readonly createOrganization: {
|
||||||
readonly organizationEdge: {
|
readonly organizationEdge: {
|
||||||
readonly node: {
|
readonly node: {
|
||||||
@@ -27,9 +27,9 @@ export type CreateOrganizationPageCreateOrganizationMutation$data = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type CreateOrganizationPageCreateOrganizationMutation = {
|
export type CreateOrganizationViewCreateOrganizationMutation = {
|
||||||
response: CreateOrganizationPageCreateOrganizationMutation$data;
|
response: CreateOrganizationViewCreateOrganizationMutation$data;
|
||||||
variables: CreateOrganizationPageCreateOrganizationMutation$variables;
|
variables: CreateOrganizationViewCreateOrganizationMutation$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -101,7 +101,7 @@ return {
|
|||||||
],
|
],
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "CreateOrganizationPageCreateOrganizationMutation",
|
"name": "CreateOrganizationViewCreateOrganizationMutation",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -126,7 +126,7 @@ return {
|
|||||||
(v0/*: any*/)
|
(v0/*: any*/)
|
||||||
],
|
],
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "CreateOrganizationPageCreateOrganizationMutation",
|
"name": "CreateOrganizationViewCreateOrganizationMutation",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -159,16 +159,16 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "aa445272c59c25a9a2edc7592c1fd7cc",
|
"cacheID": "867788ffb7d7d5f42d86fc0b42e91d8f",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "CreateOrganizationPageCreateOrganizationMutation",
|
"name": "CreateOrganizationViewCreateOrganizationMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation CreateOrganizationPageCreateOrganizationMutation(\n $input: CreateOrganizationInput!\n) {\n createOrganization(input: $input) {\n organizationEdge {\n node {\n id\n name\n logoUrl\n }\n }\n }\n}\n"
|
"text": "mutation CreateOrganizationViewCreateOrganizationMutation(\n $input: CreateOrganizationInput!\n) {\n createOrganization(input: $input) {\n organizationEdge {\n node {\n id\n name\n logoUrl\n }\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "ec0da70244afe2faa02b05ee418b829e";
|
(node as any).hash = "999f210db4fc65cbad47472294d9cf46";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<371bd1a7433b281f20dd8925915a7ff2>>
|
* @generated SignedSource<<4f30528c9f1b2def73d7058623843697>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -9,15 +9,15 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
|
||||||
import { ConcreteRequest } from 'relay-runtime';
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
export type CreateOrganizationPageViewerQuery$variables = Record<PropertyKey, never>;
|
export type CreateOrganizationViewViewerQuery$variables = Record<PropertyKey, never>;
|
||||||
export type CreateOrganizationPageViewerQuery$data = {
|
export type CreateOrganizationViewViewerQuery$data = {
|
||||||
readonly viewer: {
|
readonly viewer: {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type CreateOrganizationPageViewerQuery = {
|
export type CreateOrganizationViewViewerQuery = {
|
||||||
response: CreateOrganizationPageViewerQuery$data;
|
response: CreateOrganizationViewViewerQuery$data;
|
||||||
variables: CreateOrganizationPageViewerQuery$variables;
|
variables: CreateOrganizationViewViewerQuery$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -46,7 +46,7 @@ return {
|
|||||||
"argumentDefinitions": [],
|
"argumentDefinitions": [],
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "CreateOrganizationPageViewerQuery",
|
"name": "CreateOrganizationViewViewerQuery",
|
||||||
"selections": (v0/*: any*/),
|
"selections": (v0/*: any*/),
|
||||||
"type": "Query",
|
"type": "Query",
|
||||||
"abstractKey": null
|
"abstractKey": null
|
||||||
@@ -55,20 +55,20 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": [],
|
"argumentDefinitions": [],
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "CreateOrganizationPageViewerQuery",
|
"name": "CreateOrganizationViewViewerQuery",
|
||||||
"selections": (v0/*: any*/)
|
"selections": (v0/*: any*/)
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "8bb495e88bada2ffda332ffbc11f3340",
|
"cacheID": "70c2f23f09fbeac5b8e8436e6c68b5f6",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "CreateOrganizationPageViewerQuery",
|
"name": "CreateOrganizationViewViewerQuery",
|
||||||
"operationKind": "query",
|
"operationKind": "query",
|
||||||
"text": "query CreateOrganizationPageViewerQuery {\n viewer {\n id\n }\n}\n"
|
"text": "query CreateOrganizationViewViewerQuery {\n viewer {\n id\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "2341c7947749857243e9f492235dfd61";
|
(node as any).hash = "200816c6c6173d8c7a4e5bcdab45dc45";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<7eb47f6f0589a4b7c7e65fd4c313422c>>
|
* @generated SignedSource<<c68c659132315eb3f36849f37a95fef8>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -14,17 +14,17 @@ export type InviteUserInput = {
|
|||||||
fullName: string;
|
fullName: string;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
};
|
};
|
||||||
export type SettingsPageInviteUserMutation$variables = {
|
export type SettingsViewInviteUserMutation$variables = {
|
||||||
input: InviteUserInput;
|
input: InviteUserInput;
|
||||||
};
|
};
|
||||||
export type SettingsPageInviteUserMutation$data = {
|
export type SettingsViewInviteUserMutation$data = {
|
||||||
readonly inviteUser: {
|
readonly inviteUser: {
|
||||||
readonly success: boolean;
|
readonly success: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type SettingsPageInviteUserMutation = {
|
export type SettingsViewInviteUserMutation = {
|
||||||
response: SettingsPageInviteUserMutation$data;
|
response: SettingsViewInviteUserMutation$data;
|
||||||
variables: SettingsPageInviteUserMutation$variables;
|
variables: SettingsViewInviteUserMutation$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -66,7 +66,7 @@ return {
|
|||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "SettingsPageInviteUserMutation",
|
"name": "SettingsViewInviteUserMutation",
|
||||||
"selections": (v1/*: any*/),
|
"selections": (v1/*: any*/),
|
||||||
"type": "Mutation",
|
"type": "Mutation",
|
||||||
"abstractKey": null
|
"abstractKey": null
|
||||||
@@ -75,20 +75,20 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "SettingsPageInviteUserMutation",
|
"name": "SettingsViewInviteUserMutation",
|
||||||
"selections": (v1/*: any*/)
|
"selections": (v1/*: any*/)
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "732d61a66a202dc879bb81c72fb2fb24",
|
"cacheID": "08abe9c261dc68b1c76991ad5e6f4d95",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "SettingsPageInviteUserMutation",
|
"name": "SettingsViewInviteUserMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation SettingsPageInviteUserMutation(\n $input: InviteUserInput!\n) {\n inviteUser(input: $input) {\n success\n }\n}\n"
|
"text": "mutation SettingsViewInviteUserMutation(\n $input: InviteUserInput!\n) {\n inviteUser(input: $input) {\n success\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "d09b74116ff70680029667104b5b34db";
|
(node as any).hash = "5e95d05ee3c0d58f9413790a4a9516bf";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<7fcee268b2974653bc3734b49b4082a4>>
|
* @generated SignedSource<<d22ffd34989ddeb523e19aab24261e58>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -9,10 +9,10 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
|
||||||
import { ConcreteRequest } from 'relay-runtime';
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
export type SettingsPageQuery$variables = {
|
export type SettingsViewQuery$variables = {
|
||||||
organizationID: string;
|
organizationID: string;
|
||||||
};
|
};
|
||||||
export type SettingsPageQuery$data = {
|
export type SettingsViewQuery$data = {
|
||||||
readonly organization: {
|
readonly organization: {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly logoUrl?: string | null | undefined;
|
readonly logoUrl?: string | null | undefined;
|
||||||
@@ -29,9 +29,9 @@ export type SettingsPageQuery$data = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type SettingsPageQuery = {
|
export type SettingsViewQuery = {
|
||||||
response: SettingsPageQuery$data;
|
response: SettingsViewQuery$data;
|
||||||
variables: SettingsPageQuery$variables;
|
variables: SettingsViewQuery$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -143,7 +143,7 @@ return {
|
|||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "SettingsPageQuery",
|
"name": "SettingsViewQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": "organization",
|
"alias": "organization",
|
||||||
@@ -166,7 +166,7 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "SettingsPageQuery",
|
"name": "SettingsViewQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": "organization",
|
"alias": "organization",
|
||||||
@@ -191,16 +191,16 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "de417aa8583ad2d1cc7ef18223a4dc2d",
|
"cacheID": "1749c3e17b6efd13be678a0e968b7ac4",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "SettingsPageQuery",
|
"name": "SettingsViewQuery",
|
||||||
"operationKind": "query",
|
"operationKind": "query",
|
||||||
"text": "query SettingsPageQuery(\n $organizationID: ID!\n) {\n organization: node(id: $organizationID) {\n __typename\n id\n ... on Organization {\n name\n logoUrl\n users(first: 100) {\n edges {\n node {\n id\n fullName\n email\n createdAt\n }\n }\n }\n }\n }\n}\n"
|
"text": "query SettingsViewQuery(\n $organizationID: ID!\n) {\n organization: node(id: $organizationID) {\n __typename\n id\n ... on Organization {\n name\n logoUrl\n users(first: 100) {\n edges {\n node {\n id\n fullName\n email\n createdAt\n }\n }\n }\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "1d9482689dd9f305ca7a45fdf4ab088c";
|
(node as any).hash = "147757d21500b3eb42848ebe8a43bd9f";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<9d909d5fc7076593317b01aab165513d>>
|
* @generated SignedSource<<83fe53df5d49d9e2441a2e02dfaa63ee>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -13,17 +13,17 @@ export type RemoveUserInput = {
|
|||||||
organizationId: string;
|
organizationId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
};
|
};
|
||||||
export type SettingsPageRemoveUserMutation$variables = {
|
export type SettingsViewRemoveUserMutation$variables = {
|
||||||
input: RemoveUserInput;
|
input: RemoveUserInput;
|
||||||
};
|
};
|
||||||
export type SettingsPageRemoveUserMutation$data = {
|
export type SettingsViewRemoveUserMutation$data = {
|
||||||
readonly removeUser: {
|
readonly removeUser: {
|
||||||
readonly success: boolean;
|
readonly success: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type SettingsPageRemoveUserMutation = {
|
export type SettingsViewRemoveUserMutation = {
|
||||||
response: SettingsPageRemoveUserMutation$data;
|
response: SettingsViewRemoveUserMutation$data;
|
||||||
variables: SettingsPageRemoveUserMutation$variables;
|
variables: SettingsViewRemoveUserMutation$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -65,7 +65,7 @@ return {
|
|||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "SettingsPageRemoveUserMutation",
|
"name": "SettingsViewRemoveUserMutation",
|
||||||
"selections": (v1/*: any*/),
|
"selections": (v1/*: any*/),
|
||||||
"type": "Mutation",
|
"type": "Mutation",
|
||||||
"abstractKey": null
|
"abstractKey": null
|
||||||
@@ -74,20 +74,20 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "SettingsPageRemoveUserMutation",
|
"name": "SettingsViewRemoveUserMutation",
|
||||||
"selections": (v1/*: any*/)
|
"selections": (v1/*: any*/)
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "62588cd355b82b5a48d0b539a009b3bd",
|
"cacheID": "4a8d30e01815dbee5f216bbacf9f39e6",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "SettingsPageRemoveUserMutation",
|
"name": "SettingsViewRemoveUserMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation SettingsPageRemoveUserMutation(\n $input: RemoveUserInput!\n) {\n removeUser(input: $input) {\n success\n }\n}\n"
|
"text": "mutation SettingsViewRemoveUserMutation(\n $input: RemoveUserInput!\n) {\n removeUser(input: $input) {\n success\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "8d375bb2dfdab0f9d9520eee9f732b10";
|
(node as any).hash = "a6446703d214b8c6c436aabed2d78dd7";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<76018198697d82d6f630558618c6aaff>>
|
* @generated SignedSource<<f4b385701b9d41fb1d7617694a08c117>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -14,10 +14,10 @@ export type UpdateOrganizationInput = {
|
|||||||
name?: string | null | undefined;
|
name?: string | null | undefined;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
};
|
};
|
||||||
export type SettingsPageUpdateOrganizationMutation$variables = {
|
export type SettingsViewUpdateOrganizationMutation$variables = {
|
||||||
input: UpdateOrganizationInput;
|
input: UpdateOrganizationInput;
|
||||||
};
|
};
|
||||||
export type SettingsPageUpdateOrganizationMutation$data = {
|
export type SettingsViewUpdateOrganizationMutation$data = {
|
||||||
readonly updateOrganization: {
|
readonly updateOrganization: {
|
||||||
readonly organization: {
|
readonly organization: {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
@@ -26,9 +26,9 @@ export type SettingsPageUpdateOrganizationMutation$data = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type SettingsPageUpdateOrganizationMutation = {
|
export type SettingsViewUpdateOrganizationMutation = {
|
||||||
response: SettingsPageUpdateOrganizationMutation$data;
|
response: SettingsViewUpdateOrganizationMutation$data;
|
||||||
variables: SettingsPageUpdateOrganizationMutation$variables;
|
variables: SettingsViewUpdateOrganizationMutation$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -95,7 +95,7 @@ return {
|
|||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "SettingsPageUpdateOrganizationMutation",
|
"name": "SettingsViewUpdateOrganizationMutation",
|
||||||
"selections": (v1/*: any*/),
|
"selections": (v1/*: any*/),
|
||||||
"type": "Mutation",
|
"type": "Mutation",
|
||||||
"abstractKey": null
|
"abstractKey": null
|
||||||
@@ -104,20 +104,20 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "SettingsPageUpdateOrganizationMutation",
|
"name": "SettingsViewUpdateOrganizationMutation",
|
||||||
"selections": (v1/*: any*/)
|
"selections": (v1/*: any*/)
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "2bf2a6e054aed9bd337372682342a30f",
|
"cacheID": "f2a2984a2d4e764019f4355c13dab46e",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "SettingsPageUpdateOrganizationMutation",
|
"name": "SettingsViewUpdateOrganizationMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation SettingsPageUpdateOrganizationMutation(\n $input: UpdateOrganizationInput!\n) {\n updateOrganization(input: $input) {\n organization {\n id\n name\n logoUrl\n }\n }\n}\n"
|
"text": "mutation SettingsViewUpdateOrganizationMutation(\n $input: UpdateOrganizationInput!\n) {\n updateOrganization(input: $input) {\n organization {\n id\n name\n logoUrl\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "737ec44a08ac05e9f78827811cc0ca0e";
|
(node as any).hash = "9570926c4f654570cbfe7328cd9893d6";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,215 +1,11 @@
|
|||||||
import { Suspense, useState } from "react";
|
import { PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
import { useNavigate, useParams } from "react-router";
|
import { lazy, Suspense } from "react";
|
||||||
import { graphql, useMutation, ConnectionHandler } from "react-relay";
|
import { useLocation } from "react-router";
|
||||||
import { Button } from "@/components/ui/button";
|
import { ErrorBoundaryWithLocation } from "../ErrorBoundary";
|
||||||
import { Card } from "@/components/ui/card";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { useToast } from "@/hooks/use-toast";
|
|
||||||
import { HelpCircle } from "lucide-react";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { CreateFrameworkPageCreateFrameworkMutation } from "./__generated__/CreateFrameworkPageCreateFrameworkMutation.graphql";
|
|
||||||
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
|
||||||
|
|
||||||
const createFrameworkMutation = graphql`
|
const CreateFrameworkView = lazy(() => import("./CreateFrameworkView"));
|
||||||
mutation CreateFrameworkPageCreateFrameworkMutation(
|
|
||||||
$input: CreateFrameworkInput!
|
|
||||||
$connections: [ID!]!
|
|
||||||
) {
|
|
||||||
createFramework(input: $input) {
|
|
||||||
frameworkEdge @prependEdge(connections: $connections) {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
description
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
function EditableField({
|
export function CreateFrameworkViewSkeleton() {
|
||||||
label,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
type = "text",
|
|
||||||
helpText,
|
|
||||||
required,
|
|
||||||
multiline = false,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
type?: string;
|
|
||||||
helpText?: string;
|
|
||||||
required?: boolean;
|
|
||||||
multiline?: boolean;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Label htmlFor={label} className="text-sm font-medium">
|
|
||||||
{label}
|
|
||||||
{required && <span className="text-red-500">*</span>}
|
|
||||||
</Label>
|
|
||||||
{helpText && (
|
|
||||||
<div className="relative flex items-center">
|
|
||||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<span className="sr-only">{helpText}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{multiline ? (
|
|
||||||
<Textarea
|
|
||||||
id={label}
|
|
||||||
value={value}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
|
||||||
onChange(e.target.value)
|
|
||||||
}
|
|
||||||
className={cn(
|
|
||||||
"w-full resize-none",
|
|
||||||
required && !value && "border-red-500"
|
|
||||||
)}
|
|
||||||
placeholder={`Enter ${label.toLowerCase()}`}
|
|
||||||
rows={4}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Input
|
|
||||||
id={label}
|
|
||||||
type={type}
|
|
||||||
value={value}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
||||||
onChange(e.target.value)
|
|
||||||
}
|
|
||||||
className={cn("w-full", required && !value && "border-red-500")}
|
|
||||||
placeholder={`Enter ${label.toLowerCase()}`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CreateFrameworkPageContent() {
|
|
||||||
const { organizationId } = useParams();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { toast } = useToast();
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const [commit, isInFlight] =
|
|
||||||
useMutation<CreateFrameworkPageCreateFrameworkMutation>(
|
|
||||||
createFrameworkMutation
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFieldChange = (field: keyof typeof formData, value: unknown) => {
|
|
||||||
setFormData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[field]: value,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!formData.name || !formData.description) {
|
|
||||||
toast({
|
|
||||||
title: "Validation Error",
|
|
||||||
description: "Please fill in all required fields.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const connectionId = ConnectionHandler.getConnectionID(
|
|
||||||
organizationId!,
|
|
||||||
"FrameworkListPage_frameworks"
|
|
||||||
);
|
|
||||||
|
|
||||||
commit({
|
|
||||||
variables: {
|
|
||||||
input: {
|
|
||||||
organizationId: organizationId!,
|
|
||||||
name: formData.name,
|
|
||||||
description: formData.description,
|
|
||||||
},
|
|
||||||
connections: [connectionId],
|
|
||||||
},
|
|
||||||
onCompleted(data, errors) {
|
|
||||||
if (errors) {
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: errors[0]?.message || "Failed to create framework",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: "Success",
|
|
||||||
description: "Framework created successfully",
|
|
||||||
});
|
|
||||||
|
|
||||||
navigate(
|
|
||||||
`/organizations/${organizationId}/frameworks/${data.createFramework.frameworkEdge.node.id}`
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onError(error) {
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: error.message || "Failed to create framework",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageTemplate
|
|
||||||
title="Create Framework"
|
|
||||||
description="Create a new framework to organize your controls"
|
|
||||||
>
|
|
||||||
<Card className="max-w-2xl">
|
|
||||||
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
|
||||||
<EditableField
|
|
||||||
label="Name"
|
|
||||||
value={formData.name}
|
|
||||||
onChange={(value) => handleFieldChange("name", value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditableField
|
|
||||||
label="Description"
|
|
||||||
value={formData.description}
|
|
||||||
onChange={(value) => handleFieldChange("description", value)}
|
|
||||||
required
|
|
||||||
multiline
|
|
||||||
helpText="Provide a detailed description of the framework"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-3">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() =>
|
|
||||||
navigate(`/organizations/${organizationId}/frameworks`)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={isInFlight}>
|
|
||||||
{isInFlight ? "Creating..." : "Create Framework"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Card>
|
|
||||||
</PageTemplate>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CreateFrameworkPageSkeleton() {
|
|
||||||
return (
|
return (
|
||||||
<PageTemplateSkeleton
|
<PageTemplateSkeleton
|
||||||
title="Create Framework"
|
title="Create Framework"
|
||||||
@@ -220,10 +16,17 @@ export function CreateFrameworkPageSkeleton() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CreateFrameworkPage() {
|
export function CreateFrameworkPage() {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<CreateFrameworkPageSkeleton />}>
|
<Suspense
|
||||||
<CreateFrameworkPageContent />
|
key={location.pathname}
|
||||||
|
fallback={<CreateFrameworkViewSkeleton />}
|
||||||
|
>
|
||||||
|
<ErrorBoundaryWithLocation>
|
||||||
|
<CreateFrameworkView />
|
||||||
|
</ErrorBoundaryWithLocation>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
import { Suspense, useState } from "react";
|
||||||
|
import { useNavigate, useParams } from "react-router";
|
||||||
|
import { graphql, useMutation, ConnectionHandler } from "react-relay";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { HelpCircle } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { CreateFrameworkViewCreateFrameworkMutation } from "./__generated__/CreateFrameworkViewCreateFrameworkMutation.graphql";
|
||||||
|
import { PageTemplate } from "@/components/PageTemplate";
|
||||||
|
import { CreateFrameworkViewSkeleton } from "./CreateFrameworkPage";
|
||||||
|
|
||||||
|
const createFrameworkMutation = graphql`
|
||||||
|
mutation CreateFrameworkViewCreateFrameworkMutation(
|
||||||
|
$input: CreateFrameworkInput!
|
||||||
|
$connections: [ID!]!
|
||||||
|
) {
|
||||||
|
createFramework(input: $input) {
|
||||||
|
frameworkEdge @prependEdge(connections: $connections) {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function EditableField({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
type = "text",
|
||||||
|
helpText,
|
||||||
|
required,
|
||||||
|
multiline = false,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
type?: string;
|
||||||
|
helpText?: string;
|
||||||
|
required?: boolean;
|
||||||
|
multiline?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Label htmlFor={label} className="text-sm font-medium">
|
||||||
|
{label}
|
||||||
|
{required && <span className="text-red-500">*</span>}
|
||||||
|
</Label>
|
||||||
|
{helpText && (
|
||||||
|
<div className="relative flex items-center">
|
||||||
|
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="sr-only">{helpText}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{multiline ? (
|
||||||
|
<Textarea
|
||||||
|
id={label}
|
||||||
|
value={value}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||||
|
onChange(e.target.value)
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
"w-full resize-none",
|
||||||
|
required && !value && "border-red-500"
|
||||||
|
)}
|
||||||
|
placeholder={`Enter ${label.toLowerCase()}`}
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
id={label}
|
||||||
|
type={type}
|
||||||
|
value={value}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onChange(e.target.value)
|
||||||
|
}
|
||||||
|
className={cn("w-full", required && !value && "border-red-500")}
|
||||||
|
placeholder={`Enter ${label.toLowerCase()}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreateFrameworkViewContent() {
|
||||||
|
const { organizationId } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [commit, isInFlight] =
|
||||||
|
useMutation<CreateFrameworkViewCreateFrameworkMutation>(
|
||||||
|
createFrameworkMutation
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFieldChange = (field: keyof typeof formData, value: unknown) => {
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[field]: value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!formData.name || !formData.description) {
|
||||||
|
toast({
|
||||||
|
title: "Validation Error",
|
||||||
|
description: "Please fill in all required fields.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectionId = ConnectionHandler.getConnectionID(
|
||||||
|
organizationId!,
|
||||||
|
"FrameworkListPage_frameworks"
|
||||||
|
);
|
||||||
|
|
||||||
|
commit({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
organizationId: organizationId!,
|
||||||
|
name: formData.name,
|
||||||
|
description: formData.description,
|
||||||
|
},
|
||||||
|
connections: [connectionId],
|
||||||
|
},
|
||||||
|
onCompleted(data, errors) {
|
||||||
|
if (errors) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: errors[0]?.message || "Failed to create framework",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: "Framework created successfully",
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate(
|
||||||
|
`/organizations/${organizationId}/frameworks/${data.createFramework.frameworkEdge.node.id}`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: error.message || "Failed to create framework",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageTemplate
|
||||||
|
title="Create Framework"
|
||||||
|
description="Create a new framework to organize your controls"
|
||||||
|
>
|
||||||
|
<Card className="max-w-2xl">
|
||||||
|
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
||||||
|
<EditableField
|
||||||
|
label="Name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(value) => handleFieldChange("name", value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditableField
|
||||||
|
label="Description"
|
||||||
|
value={formData.description}
|
||||||
|
onChange={(value) => handleFieldChange("description", value)}
|
||||||
|
required
|
||||||
|
multiline
|
||||||
|
helpText="Provide a detailed description of the framework"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() =>
|
||||||
|
navigate(`/organizations/${organizationId}/frameworks`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={isInFlight}>
|
||||||
|
{isInFlight ? "Creating..." : "Create Framework"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
</PageTemplate>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CreateFrameworkView() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<CreateFrameworkViewSkeleton />}>
|
||||||
|
<CreateFrameworkViewContent />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import { Card, CardContent } from "@/components/ui/card";
|
|||||||
|
|
||||||
const FrameworkListView = lazy(() => import("./FrameworkListView"));
|
const FrameworkListView = lazy(() => import("./FrameworkListView"));
|
||||||
|
|
||||||
export function FrameworkListSkeleton() {
|
export function FrameworkListViewSkeleton() {
|
||||||
return (
|
return (
|
||||||
<PageTemplateSkeleton
|
<PageTemplateSkeleton
|
||||||
title="Frameworks"
|
title="Frameworks"
|
||||||
@@ -40,7 +40,7 @@ export function FrameworkListPage() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense key={location.pathname} fallback={<FrameworkListSkeleton />}>
|
<Suspense key={location.pathname} fallback={<FrameworkListViewSkeleton />}>
|
||||||
<ErrorBoundaryWithLocation>
|
<ErrorBoundaryWithLocation>
|
||||||
<FrameworkListView />
|
<FrameworkListView />
|
||||||
</ErrorBoundaryWithLocation>
|
</ErrorBoundaryWithLocation>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { FrameworkListViewImportFrameworkMutation as FrameworkListViewImportFrameworkMutationType } from "./__generated__/FrameworkListViewImportFrameworkMutation.graphql";
|
import { FrameworkListViewImportFrameworkMutation as FrameworkListViewImportFrameworkMutationType } from "./__generated__/FrameworkListViewImportFrameworkMutation.graphql";
|
||||||
import { PageTemplate } from "@/components/PageTemplate";
|
import { PageTemplate } from "@/components/PageTemplate";
|
||||||
import { FrameworkListSkeleton } from "./FrameworkListPage";
|
import { FrameworkListViewSkeleton } from "./FrameworkListPage";
|
||||||
|
|
||||||
const FrameworkListViewQuery = graphql`
|
const FrameworkListViewQuery = graphql`
|
||||||
query FrameworkListViewQuery($organizationId: ID!) {
|
query FrameworkListViewQuery($organizationId: ID!) {
|
||||||
@@ -292,11 +292,11 @@ export default function FrameworkListView() {
|
|||||||
}, [loadQuery, organizationId]);
|
}, [loadQuery, organizationId]);
|
||||||
|
|
||||||
if (!queryRef) {
|
if (!queryRef) {
|
||||||
return <FrameworkListSkeleton />;
|
return <FrameworkListViewSkeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<FrameworkListSkeleton />}>
|
<Suspense fallback={<FrameworkListViewSkeleton />}>
|
||||||
{queryRef && <FrameworkListViewContent queryRef={queryRef} />}
|
{queryRef && <FrameworkListViewContent queryRef={queryRef} />}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,240 +1,11 @@
|
|||||||
import { Suspense, useState, useEffect } from "react";
|
import { PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
import { useNavigate, useParams } from "react-router";
|
import { lazy, Suspense } from "react";
|
||||||
import {
|
import { useLocation } from "react-router";
|
||||||
graphql,
|
import { ErrorBoundaryWithLocation } from "../ErrorBoundary";
|
||||||
useMutation,
|
|
||||||
usePreloadedQuery,
|
|
||||||
PreloadedQuery,
|
|
||||||
useQueryLoader,
|
|
||||||
} from "react-relay";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card } from "@/components/ui/card";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { useToast } from "@/hooks/use-toast";
|
|
||||||
import { HelpCircle } from "lucide-react";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { UpdateFrameworkPageUpdateFrameworkMutation } from "./__generated__/UpdateFrameworkPageUpdateFrameworkMutation.graphql";
|
|
||||||
import { UpdateFrameworkPageQuery as UpdateFrameworkPageQueryType } from "./__generated__/UpdateFrameworkPageQuery.graphql";
|
|
||||||
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
|
||||||
|
|
||||||
const updateFrameworkMutation = graphql`
|
const UpdateFrameworkView = lazy(() => import("./UpdateFrameworkView"));
|
||||||
mutation UpdateFrameworkPageUpdateFrameworkMutation(
|
|
||||||
$input: UpdateFrameworkInput!
|
|
||||||
) {
|
|
||||||
updateFramework(input: $input) {
|
|
||||||
framework {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
description
|
|
||||||
version
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const updateFrameworkQuery = graphql`
|
export function UpdateFrameworkViewSkeleton() {
|
||||||
query UpdateFrameworkPageQuery($frameworkId: ID!) {
|
|
||||||
node(id: $frameworkId) {
|
|
||||||
... on Framework {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
description
|
|
||||||
version
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
function EditableField({
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
type = "text",
|
|
||||||
helpText,
|
|
||||||
required,
|
|
||||||
multiline = false,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
type?: string;
|
|
||||||
helpText?: string;
|
|
||||||
required?: boolean;
|
|
||||||
multiline?: boolean;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Label htmlFor={label} className="text-sm font-medium">
|
|
||||||
{label}
|
|
||||||
{required && <span className="text-red-500">*</span>}
|
|
||||||
</Label>
|
|
||||||
{helpText && (
|
|
||||||
<div className="relative flex items-center">
|
|
||||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<span className="sr-only">{helpText}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{multiline ? (
|
|
||||||
<Textarea
|
|
||||||
id={label}
|
|
||||||
value={value}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
|
||||||
onChange(e.target.value)
|
|
||||||
}
|
|
||||||
className={cn(
|
|
||||||
"w-full resize-none",
|
|
||||||
required && !value && "border-red-500"
|
|
||||||
)}
|
|
||||||
placeholder={`Enter ${label.toLowerCase()}`}
|
|
||||||
rows={4}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Input
|
|
||||||
id={label}
|
|
||||||
type={type}
|
|
||||||
value={value}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
||||||
onChange(e.target.value)
|
|
||||||
}
|
|
||||||
className={cn("w-full", required && !value && "border-red-500")}
|
|
||||||
placeholder={`Enter ${label.toLowerCase()}`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpdateFrameworkPageContent({
|
|
||||||
queryRef,
|
|
||||||
}: {
|
|
||||||
queryRef: PreloadedQuery<UpdateFrameworkPageQueryType>;
|
|
||||||
}) {
|
|
||||||
const { organizationId, frameworkId } = useParams();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { toast } = useToast();
|
|
||||||
const data = usePreloadedQuery(updateFrameworkQuery, queryRef);
|
|
||||||
const [, setEditedFields] = useState<Set<string>>(new Set());
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data.node) {
|
|
||||||
setFormData({
|
|
||||||
name: data.node.name || "",
|
|
||||||
description: data.node.description || "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [data.node]);
|
|
||||||
|
|
||||||
const [commit, isInFlight] =
|
|
||||||
useMutation<UpdateFrameworkPageUpdateFrameworkMutation>(
|
|
||||||
updateFrameworkMutation
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFieldChange = (field: keyof typeof formData, value: unknown) => {
|
|
||||||
setFormData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[field]: value,
|
|
||||||
}));
|
|
||||||
setEditedFields((prev) => new Set(prev).add(field));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
navigate(`/organizations/${organizationId}/frameworks/${frameworkId}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!formData.name || !formData.description) {
|
|
||||||
toast({
|
|
||||||
title: "Validation Error",
|
|
||||||
description: "Please fill in all required fields.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
commit({
|
|
||||||
variables: {
|
|
||||||
input: {
|
|
||||||
id: frameworkId!,
|
|
||||||
expectedVersion: data.node.version!,
|
|
||||||
name: formData.name,
|
|
||||||
description: formData.description,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
onCompleted(data, errors) {
|
|
||||||
if (errors) {
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: errors[0]?.message || "Failed to update framework",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: "Success",
|
|
||||||
description: "Framework updated successfully",
|
|
||||||
});
|
|
||||||
|
|
||||||
navigate(`/organizations/${organizationId}/frameworks/${frameworkId}`);
|
|
||||||
},
|
|
||||||
onError(error) {
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: error.message || "Failed to update framework",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageTemplate
|
|
||||||
title="Update Framework"
|
|
||||||
description="Update the framework details"
|
|
||||||
>
|
|
||||||
<Card className="max-w-2xl">
|
|
||||||
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
|
||||||
<EditableField
|
|
||||||
label="Name"
|
|
||||||
value={formData.name}
|
|
||||||
onChange={(value) => handleFieldChange("name", value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditableField
|
|
||||||
label="Description"
|
|
||||||
value={formData.description}
|
|
||||||
onChange={(value) => handleFieldChange("description", value)}
|
|
||||||
required
|
|
||||||
multiline
|
|
||||||
helpText="Provide a detailed description of the framework"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-3">
|
|
||||||
<Button type="button" variant="outline" onClick={handleCancel}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={isInFlight}>
|
|
||||||
{isInFlight ? "Updating..." : "Update Framework"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Card>
|
|
||||||
</PageTemplate>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function UpdateFrameworkPageSkeleton() {
|
|
||||||
return (
|
return (
|
||||||
<PageTemplateSkeleton
|
<PageTemplateSkeleton
|
||||||
title="Update Framework"
|
title="Update Framework"
|
||||||
@@ -245,24 +16,17 @@ export function UpdateFrameworkPageSkeleton() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UpdateFrameworkPage() {
|
export function UpdateFrameworkPage() {
|
||||||
const { frameworkId } = useParams();
|
const location = useLocation();
|
||||||
const [queryRef, loadQuery] =
|
|
||||||
useQueryLoader<UpdateFrameworkPageQueryType>(updateFrameworkQuery);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (frameworkId) {
|
|
||||||
loadQuery({ frameworkId });
|
|
||||||
}
|
|
||||||
}, [frameworkId, loadQuery]);
|
|
||||||
|
|
||||||
if (!queryRef) {
|
|
||||||
return <UpdateFrameworkPageSkeleton />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<UpdateFrameworkPageSkeleton />}>
|
<Suspense
|
||||||
<UpdateFrameworkPageContent queryRef={queryRef} />
|
key={location.pathname}
|
||||||
|
fallback={<UpdateFrameworkViewSkeleton />}
|
||||||
|
>
|
||||||
|
<ErrorBoundaryWithLocation>
|
||||||
|
<UpdateFrameworkView />
|
||||||
|
</ErrorBoundaryWithLocation>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,258 @@
|
|||||||
|
import { Suspense, useState, useEffect } from "react";
|
||||||
|
import { useNavigate, useParams } from "react-router";
|
||||||
|
import {
|
||||||
|
graphql,
|
||||||
|
useMutation,
|
||||||
|
usePreloadedQuery,
|
||||||
|
PreloadedQuery,
|
||||||
|
useQueryLoader,
|
||||||
|
} from "react-relay";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { HelpCircle } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { UpdateFrameworkViewUpdateFrameworkMutation } from "./__generated__/UpdateFrameworkViewUpdateFrameworkMutation.graphql";
|
||||||
|
import { UpdateFrameworkViewQuery as UpdateFrameworkViewQueryType } from "./__generated__/UpdateFrameworkViewQuery.graphql";
|
||||||
|
import { PageTemplate } from "@/components/PageTemplate";
|
||||||
|
import { UpdateFrameworkViewSkeleton } from "./UpdateFrameworkPage";
|
||||||
|
|
||||||
|
const updateFrameworkMutation = graphql`
|
||||||
|
mutation UpdateFrameworkViewUpdateFrameworkMutation(
|
||||||
|
$input: UpdateFrameworkInput!
|
||||||
|
) {
|
||||||
|
updateFramework(input: $input) {
|
||||||
|
framework {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const updateFrameworkQuery = graphql`
|
||||||
|
query UpdateFrameworkViewQuery($frameworkId: ID!) {
|
||||||
|
node(id: $frameworkId) {
|
||||||
|
... on Framework {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function EditableField({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
type = "text",
|
||||||
|
helpText,
|
||||||
|
required,
|
||||||
|
multiline = false,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
type?: string;
|
||||||
|
helpText?: string;
|
||||||
|
required?: boolean;
|
||||||
|
multiline?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Label htmlFor={label} className="text-sm font-medium">
|
||||||
|
{label}
|
||||||
|
{required && <span className="text-red-500">*</span>}
|
||||||
|
</Label>
|
||||||
|
{helpText && (
|
||||||
|
<div className="relative flex items-center">
|
||||||
|
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="sr-only">{helpText}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{multiline ? (
|
||||||
|
<Textarea
|
||||||
|
id={label}
|
||||||
|
value={value}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||||
|
onChange(e.target.value)
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
"w-full resize-none",
|
||||||
|
required && !value && "border-red-500"
|
||||||
|
)}
|
||||||
|
placeholder={`Enter ${label.toLowerCase()}`}
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
id={label}
|
||||||
|
type={type}
|
||||||
|
value={value}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onChange(e.target.value)
|
||||||
|
}
|
||||||
|
className={cn("w-full", required && !value && "border-red-500")}
|
||||||
|
placeholder={`Enter ${label.toLowerCase()}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UpdateFrameworkViewContent({
|
||||||
|
queryRef,
|
||||||
|
}: {
|
||||||
|
queryRef: PreloadedQuery<UpdateFrameworkViewQueryType>;
|
||||||
|
}) {
|
||||||
|
const { organizationId, frameworkId } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const data = usePreloadedQuery(updateFrameworkQuery, queryRef);
|
||||||
|
const [, setEditedFields] = useState<Set<string>>(new Set());
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data.node) {
|
||||||
|
setFormData({
|
||||||
|
name: data.node.name || "",
|
||||||
|
description: data.node.description || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data.node]);
|
||||||
|
|
||||||
|
const [commit, isInFlight] =
|
||||||
|
useMutation<UpdateFrameworkViewUpdateFrameworkMutation>(
|
||||||
|
updateFrameworkMutation
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFieldChange = (field: keyof typeof formData, value: unknown) => {
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[field]: value,
|
||||||
|
}));
|
||||||
|
setEditedFields((prev) => new Set(prev).add(field));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
navigate(`/organizations/${organizationId}/frameworks/${frameworkId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!formData.name || !formData.description) {
|
||||||
|
toast({
|
||||||
|
title: "Validation Error",
|
||||||
|
description: "Please fill in all required fields.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
commit({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
id: frameworkId!,
|
||||||
|
expectedVersion: data.node.version!,
|
||||||
|
name: formData.name,
|
||||||
|
description: formData.description,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onCompleted(data, errors) {
|
||||||
|
if (errors) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: errors[0]?.message || "Failed to update framework",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: "Framework updated successfully",
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate(`/organizations/${organizationId}/frameworks/${frameworkId}`);
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: error.message || "Failed to update framework",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageTemplate
|
||||||
|
title="Update Framework"
|
||||||
|
description="Update the framework details"
|
||||||
|
>
|
||||||
|
<Card className="max-w-2xl">
|
||||||
|
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
||||||
|
<EditableField
|
||||||
|
label="Name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(value) => handleFieldChange("name", value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditableField
|
||||||
|
label="Description"
|
||||||
|
value={formData.description}
|
||||||
|
onChange={(value) => handleFieldChange("description", value)}
|
||||||
|
required
|
||||||
|
multiline
|
||||||
|
helpText="Provide a detailed description of the framework"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3">
|
||||||
|
<Button type="button" variant="outline" onClick={handleCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={isInFlight}>
|
||||||
|
{isInFlight ? "Updating..." : "Update Framework"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
</PageTemplate>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UpdateFrameworkView() {
|
||||||
|
const { frameworkId } = useParams();
|
||||||
|
const [queryRef, loadQuery] =
|
||||||
|
useQueryLoader<UpdateFrameworkViewQueryType>(updateFrameworkQuery);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (frameworkId) {
|
||||||
|
loadQuery({ frameworkId });
|
||||||
|
}
|
||||||
|
}, [frameworkId, loadQuery]);
|
||||||
|
|
||||||
|
if (!queryRef) {
|
||||||
|
return <UpdateFrameworkViewSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<UpdateFrameworkViewSkeleton />}>
|
||||||
|
<UpdateFrameworkViewContent queryRef={queryRef} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<9c88cb0bf2c3491784bdea7e46c918a3>>
|
* @generated SignedSource<<dca7347e7f5c3d8271623553ee52094d>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -14,11 +14,11 @@ export type CreateFrameworkInput = {
|
|||||||
name: string;
|
name: string;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
};
|
};
|
||||||
export type CreateFrameworkPageCreateFrameworkMutation$variables = {
|
export type CreateFrameworkViewCreateFrameworkMutation$variables = {
|
||||||
connections: ReadonlyArray<string>;
|
connections: ReadonlyArray<string>;
|
||||||
input: CreateFrameworkInput;
|
input: CreateFrameworkInput;
|
||||||
};
|
};
|
||||||
export type CreateFrameworkPageCreateFrameworkMutation$data = {
|
export type CreateFrameworkViewCreateFrameworkMutation$data = {
|
||||||
readonly createFramework: {
|
readonly createFramework: {
|
||||||
readonly frameworkEdge: {
|
readonly frameworkEdge: {
|
||||||
readonly node: {
|
readonly node: {
|
||||||
@@ -29,9 +29,9 @@ export type CreateFrameworkPageCreateFrameworkMutation$data = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type CreateFrameworkPageCreateFrameworkMutation = {
|
export type CreateFrameworkViewCreateFrameworkMutation = {
|
||||||
response: CreateFrameworkPageCreateFrameworkMutation$data;
|
response: CreateFrameworkViewCreateFrameworkMutation$data;
|
||||||
variables: CreateFrameworkPageCreateFrameworkMutation$variables;
|
variables: CreateFrameworkViewCreateFrameworkMutation$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -103,7 +103,7 @@ return {
|
|||||||
],
|
],
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "CreateFrameworkPageCreateFrameworkMutation",
|
"name": "CreateFrameworkViewCreateFrameworkMutation",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -128,7 +128,7 @@ return {
|
|||||||
(v0/*: any*/)
|
(v0/*: any*/)
|
||||||
],
|
],
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "CreateFrameworkPageCreateFrameworkMutation",
|
"name": "CreateFrameworkViewCreateFrameworkMutation",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -161,16 +161,16 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "142ffd990da914bddaa2752dfff0faaf",
|
"cacheID": "7502bf40a1731a4b7fd1a019ac438b39",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "CreateFrameworkPageCreateFrameworkMutation",
|
"name": "CreateFrameworkViewCreateFrameworkMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation CreateFrameworkPageCreateFrameworkMutation(\n $input: CreateFrameworkInput!\n) {\n createFramework(input: $input) {\n frameworkEdge {\n node {\n id\n name\n description\n }\n }\n }\n}\n"
|
"text": "mutation CreateFrameworkViewCreateFrameworkMutation(\n $input: CreateFrameworkInput!\n) {\n createFramework(input: $input) {\n frameworkEdge {\n node {\n id\n name\n description\n }\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "60b83b4b5302f15a8e6f6b711c905963";
|
(node as any).hash = "cb800b9880232714d34698beb7bf8d84";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<c4c46870b65e75baab882f1e9689c119>>
|
* @generated SignedSource<<08d71ea16ac38e11d5b1ce3fb11bcaf3>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -9,10 +9,10 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
|
||||||
import { ConcreteRequest } from 'relay-runtime';
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
export type UpdateFrameworkPageQuery$variables = {
|
export type UpdateFrameworkViewQuery$variables = {
|
||||||
frameworkId: string;
|
frameworkId: string;
|
||||||
};
|
};
|
||||||
export type UpdateFrameworkPageQuery$data = {
|
export type UpdateFrameworkViewQuery$data = {
|
||||||
readonly node: {
|
readonly node: {
|
||||||
readonly description?: string;
|
readonly description?: string;
|
||||||
readonly id?: string;
|
readonly id?: string;
|
||||||
@@ -20,9 +20,9 @@ export type UpdateFrameworkPageQuery$data = {
|
|||||||
readonly version?: number;
|
readonly version?: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type UpdateFrameworkPageQuery = {
|
export type UpdateFrameworkViewQuery = {
|
||||||
response: UpdateFrameworkPageQuery$data;
|
response: UpdateFrameworkViewQuery$data;
|
||||||
variables: UpdateFrameworkPageQuery$variables;
|
variables: UpdateFrameworkViewQuery$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -73,7 +73,7 @@ return {
|
|||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "UpdateFrameworkPageQuery",
|
"name": "UpdateFrameworkViewQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -105,7 +105,7 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "UpdateFrameworkPageQuery",
|
"name": "UpdateFrameworkViewQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -139,16 +139,16 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "cae85e293ba2bd3d1d0f13acd1e777f5",
|
"cacheID": "180cbef2756c525af017ca6996a7c2d6",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "UpdateFrameworkPageQuery",
|
"name": "UpdateFrameworkViewQuery",
|
||||||
"operationKind": "query",
|
"operationKind": "query",
|
||||||
"text": "query UpdateFrameworkPageQuery(\n $frameworkId: ID!\n) {\n node(id: $frameworkId) {\n __typename\n ... on Framework {\n id\n name\n description\n version\n }\n id\n }\n}\n"
|
"text": "query UpdateFrameworkViewQuery(\n $frameworkId: ID!\n) {\n node(id: $frameworkId) {\n __typename\n ... on Framework {\n id\n name\n description\n version\n }\n id\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "56744b0fcc4892c88f70c71b0ca49b90";
|
(node as any).hash = "d644d01f1d8a31b6c87c7f6360996796";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<822ef9b3867da7de4bca37e5be0e077e>>
|
* @generated SignedSource<<115402b83869511df5e91ab206ae99e3>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -15,10 +15,10 @@ export type UpdateFrameworkInput = {
|
|||||||
id: string;
|
id: string;
|
||||||
name?: string | null | undefined;
|
name?: string | null | undefined;
|
||||||
};
|
};
|
||||||
export type UpdateFrameworkPageUpdateFrameworkMutation$variables = {
|
export type UpdateFrameworkViewUpdateFrameworkMutation$variables = {
|
||||||
input: UpdateFrameworkInput;
|
input: UpdateFrameworkInput;
|
||||||
};
|
};
|
||||||
export type UpdateFrameworkPageUpdateFrameworkMutation$data = {
|
export type UpdateFrameworkViewUpdateFrameworkMutation$data = {
|
||||||
readonly updateFramework: {
|
readonly updateFramework: {
|
||||||
readonly framework: {
|
readonly framework: {
|
||||||
readonly description: string;
|
readonly description: string;
|
||||||
@@ -28,9 +28,9 @@ export type UpdateFrameworkPageUpdateFrameworkMutation$data = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type UpdateFrameworkPageUpdateFrameworkMutation = {
|
export type UpdateFrameworkViewUpdateFrameworkMutation = {
|
||||||
response: UpdateFrameworkPageUpdateFrameworkMutation$data;
|
response: UpdateFrameworkViewUpdateFrameworkMutation$data;
|
||||||
variables: UpdateFrameworkPageUpdateFrameworkMutation$variables;
|
variables: UpdateFrameworkViewUpdateFrameworkMutation$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -104,7 +104,7 @@ return {
|
|||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "UpdateFrameworkPageUpdateFrameworkMutation",
|
"name": "UpdateFrameworkViewUpdateFrameworkMutation",
|
||||||
"selections": (v1/*: any*/),
|
"selections": (v1/*: any*/),
|
||||||
"type": "Mutation",
|
"type": "Mutation",
|
||||||
"abstractKey": null
|
"abstractKey": null
|
||||||
@@ -113,20 +113,20 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "UpdateFrameworkPageUpdateFrameworkMutation",
|
"name": "UpdateFrameworkViewUpdateFrameworkMutation",
|
||||||
"selections": (v1/*: any*/)
|
"selections": (v1/*: any*/)
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "d85d38851d3d94709a5b6c4090678ea4",
|
"cacheID": "08fc83afdc1227aa23cac0778cdd2cc0",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "UpdateFrameworkPageUpdateFrameworkMutation",
|
"name": "UpdateFrameworkViewUpdateFrameworkMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation UpdateFrameworkPageUpdateFrameworkMutation(\n $input: UpdateFrameworkInput!\n) {\n updateFramework(input: $input) {\n framework {\n id\n name\n description\n version\n }\n }\n}\n"
|
"text": "mutation UpdateFrameworkViewUpdateFrameworkMutation(\n $input: UpdateFrameworkInput!\n) {\n updateFramework(input: $input) {\n framework {\n id\n name\n description\n version\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "c363b35a095da9142d61907ca7ba3c62";
|
(node as any).hash = "6dfde24d73c2cbc0c5ca147d076fbd26";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { lazy, Suspense } from "react";
|
||||||
|
import { useLocation } from "react-router";
|
||||||
|
import { ErrorBoundaryWithLocation } from "../../ErrorBoundary";
|
||||||
|
|
||||||
|
const ControlView = lazy(() => import("./ControlView"));
|
||||||
|
|
||||||
|
export function ControlViewSkeleton() {
|
||||||
|
return (
|
||||||
|
<PageTemplateSkeleton
|
||||||
|
actions={
|
||||||
|
<div className="flex items-center gap-2 w-1/3">
|
||||||
|
<div className="bg-muted animate-pulse h-8 w-1/3 rounded-lg" />
|
||||||
|
<div className="bg-muted animate-pulse h-8 w-1/3 rounded-lg" />
|
||||||
|
<div className="bg-muted animate-pulse h-8 w-1/3 rounded-full" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{[1, 2, 3].map((i) => (
|
||||||
|
<Card key={i}>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="h-6 w-48 bg-muted animate-pulse rounded mb-2" />
|
||||||
|
<div className="h-4 w-full bg-muted animate-pulse rounded" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</PageTemplateSkeleton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ControlPage() {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense key={location.pathname} fallback={<ControlViewSkeleton />}>
|
||||||
|
<ErrorBoundaryWithLocation>
|
||||||
|
<ControlView />
|
||||||
|
</ErrorBoundaryWithLocation>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -64,17 +64,18 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
|
||||||
import type { ControlOverviewPageQuery as ControlOverviewPageQueryType } from "./__generated__/ControlOverviewPageQuery.graphql";
|
import type { ControlViewQuery as ControlViewQueryType } from "./__generated__/ControlViewQuery.graphql";
|
||||||
import type { ControlOverviewPageUpdateTaskStateMutation as ControlOverviewPageUpdateTaskStateMutationType } from "./__generated__/ControlOverviewPageUpdateTaskStateMutation.graphql";
|
import type { ControlViewUpdateTaskStateMutation as ControlViewUpdateTaskStateMutationType } from "./__generated__/ControlViewUpdateTaskStateMutation.graphql";
|
||||||
import type { ControlOverviewPageCreateTaskMutation as ControlOverviewPageCreateTaskMutationType } from "./__generated__/ControlOverviewPageCreateTaskMutation.graphql";
|
import type { ControlViewCreateTaskMutation as ControlViewCreateTaskMutationType } from "./__generated__/ControlViewCreateTaskMutation.graphql";
|
||||||
import type { ControlOverviewPageDeleteTaskMutation as ControlOverviewPageDeleteTaskMutationType } from "./__generated__/ControlOverviewPageDeleteTaskMutation.graphql";
|
import type { ControlViewDeleteTaskMutation as ControlViewDeleteTaskMutationType } from "./__generated__/ControlViewDeleteTaskMutation.graphql";
|
||||||
import type { ControlOverviewPageUploadEvidenceMutation as ControlOverviewPageUploadEvidenceMutationType } from "./__generated__/ControlOverviewPageUploadEvidenceMutation.graphql";
|
import type { ControlViewUploadEvidenceMutation as ControlViewUploadEvidenceMutationType } from "./__generated__/ControlViewUploadEvidenceMutation.graphql";
|
||||||
import type { ControlOverviewPageDeleteEvidenceMutation as ControlOverviewPageDeleteEvidenceMutationType } from "./__generated__/ControlOverviewPageDeleteEvidenceMutation.graphql";
|
import type { ControlViewDeleteEvidenceMutation as ControlViewDeleteEvidenceMutationType } from "./__generated__/ControlViewDeleteEvidenceMutation.graphql";
|
||||||
import type { ControlOverviewPageAssignTaskMutation as ControlOverviewPageAssignTaskMutationType } from "./__generated__/ControlOverviewPageAssignTaskMutation.graphql";
|
import type { ControlViewAssignTaskMutation as ControlViewAssignTaskMutationType } from "./__generated__/ControlViewAssignTaskMutation.graphql";
|
||||||
import type { ControlOverviewPageUnassignTaskMutation as ControlOverviewPageUnassignTaskMutationType } from "./__generated__/ControlOverviewPageUnassignTaskMutation.graphql";
|
import type { ControlViewUnassignTaskMutation as ControlViewUnassignTaskMutationType } from "./__generated__/ControlViewUnassignTaskMutation.graphql";
|
||||||
import type { ControlOverviewPageOrganizationQuery$data } from "./__generated__/ControlOverviewPageOrganizationQuery.graphql";
|
import type { ControlViewOrganizationQuery$data } from "./__generated__/ControlViewOrganizationQuery.graphql";
|
||||||
import type { ControlOverviewPageUpdateControlStateMutation as ControlOverviewPageUpdateControlStateMutationType } from "./__generated__/ControlOverviewPageUpdateControlStateMutation.graphql";
|
import type { ControlViewUpdateControlStateMutation as ControlViewUpdateControlStateMutationType } from "./__generated__/ControlViewUpdateControlStateMutation.graphql";
|
||||||
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
import { PageTemplate } from "@/components/PageTemplate";
|
||||||
|
import { ControlViewSkeleton } from "./ControlPage";
|
||||||
|
|
||||||
// Function to format ISO8601 duration to human-readable format
|
// Function to format ISO8601 duration to human-readable format
|
||||||
const formatDuration = (isoDuration: string): string => {
|
const formatDuration = (isoDuration: string): string => {
|
||||||
@@ -113,8 +114,8 @@ const formatDuration = (isoDuration: string): string => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const controlOverviewPageQuery = graphql`
|
const controlViewQuery = graphql`
|
||||||
query ControlOverviewPageQuery($controlId: ID!) {
|
query ControlViewQuery($controlId: ID!) {
|
||||||
control: node(id: $controlId) {
|
control: node(id: $controlId) {
|
||||||
id
|
id
|
||||||
... on Control {
|
... on Control {
|
||||||
@@ -124,7 +125,7 @@ const controlOverviewPageQuery = graphql`
|
|||||||
importance
|
importance
|
||||||
category
|
category
|
||||||
version
|
version
|
||||||
tasks(first: 100) @connection(key: "ControlOverviewPage_tasks") {
|
tasks(first: 100) @connection(key: "ControlView_tasks") {
|
||||||
__id
|
__id
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
@@ -139,8 +140,7 @@ const controlOverviewPageQuery = graphql`
|
|||||||
fullName
|
fullName
|
||||||
primaryEmailAddress
|
primaryEmailAddress
|
||||||
}
|
}
|
||||||
evidences(first: 50)
|
evidences(first: 50) @connection(key: "ControlView_evidences") {
|
||||||
@connection(key: "ControlOverviewPage_evidences") {
|
|
||||||
__id
|
__id
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
@@ -164,9 +164,7 @@ const controlOverviewPageQuery = graphql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const updateTaskStateMutation = graphql`
|
const updateTaskStateMutation = graphql`
|
||||||
mutation ControlOverviewPageUpdateTaskStateMutation(
|
mutation ControlViewUpdateTaskStateMutation($input: UpdateTaskInput!) {
|
||||||
$input: UpdateTaskInput!
|
|
||||||
) {
|
|
||||||
updateTask(input: $input) {
|
updateTask(input: $input) {
|
||||||
task {
|
task {
|
||||||
id
|
id
|
||||||
@@ -178,7 +176,7 @@ const updateTaskStateMutation = graphql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const createTaskMutation = graphql`
|
const createTaskMutation = graphql`
|
||||||
mutation ControlOverviewPageCreateTaskMutation(
|
mutation ControlViewCreateTaskMutation(
|
||||||
$input: CreateTaskInput!
|
$input: CreateTaskInput!
|
||||||
$connections: [ID!]!
|
$connections: [ID!]!
|
||||||
) {
|
) {
|
||||||
@@ -203,7 +201,7 @@ const createTaskMutation = graphql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const deleteTaskMutation = graphql`
|
const deleteTaskMutation = graphql`
|
||||||
mutation ControlOverviewPageDeleteTaskMutation(
|
mutation ControlViewDeleteTaskMutation(
|
||||||
$input: DeleteTaskInput!
|
$input: DeleteTaskInput!
|
||||||
$connections: [ID!]!
|
$connections: [ID!]!
|
||||||
) {
|
) {
|
||||||
@@ -214,7 +212,7 @@ const deleteTaskMutation = graphql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const uploadEvidenceMutation = graphql`
|
const uploadEvidenceMutation = graphql`
|
||||||
mutation ControlOverviewPageUploadEvidenceMutation(
|
mutation ControlViewUploadEvidenceMutation(
|
||||||
$input: UploadEvidenceInput!
|
$input: UploadEvidenceInput!
|
||||||
$connections: [ID!]!
|
$connections: [ID!]!
|
||||||
) {
|
) {
|
||||||
@@ -237,7 +235,7 @@ const uploadEvidenceMutation = graphql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const deleteEvidenceMutation = graphql`
|
const deleteEvidenceMutation = graphql`
|
||||||
mutation ControlOverviewPageDeleteEvidenceMutation(
|
mutation ControlViewDeleteEvidenceMutation(
|
||||||
$input: DeleteEvidenceInput!
|
$input: DeleteEvidenceInput!
|
||||||
$connections: [ID!]!
|
$connections: [ID!]!
|
||||||
) {
|
) {
|
||||||
@@ -249,7 +247,7 @@ const deleteEvidenceMutation = graphql`
|
|||||||
|
|
||||||
// Add a GraphQL query to fetch the fileUrl for an evidence item
|
// Add a GraphQL query to fetch the fileUrl for an evidence item
|
||||||
const getEvidenceFileUrlQuery = graphql`
|
const getEvidenceFileUrlQuery = graphql`
|
||||||
query ControlOverviewPageGetEvidenceFileUrlQuery($evidenceId: ID!) {
|
query ControlViewGetEvidenceFileUrlQuery($evidenceId: ID!) {
|
||||||
node(id: $evidenceId) {
|
node(id: $evidenceId) {
|
||||||
... on Evidence {
|
... on Evidence {
|
||||||
id
|
id
|
||||||
@@ -260,7 +258,7 @@ const getEvidenceFileUrlQuery = graphql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const assignTaskMutation = graphql`
|
const assignTaskMutation = graphql`
|
||||||
mutation ControlOverviewPageAssignTaskMutation($input: AssignTaskInput!) {
|
mutation ControlViewAssignTaskMutation($input: AssignTaskInput!) {
|
||||||
assignTask(input: $input) {
|
assignTask(input: $input) {
|
||||||
task {
|
task {
|
||||||
id
|
id
|
||||||
@@ -276,7 +274,7 @@ const assignTaskMutation = graphql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const unassignTaskMutation = graphql`
|
const unassignTaskMutation = graphql`
|
||||||
mutation ControlOverviewPageUnassignTaskMutation($input: UnassignTaskInput!) {
|
mutation ControlViewUnassignTaskMutation($input: UnassignTaskInput!) {
|
||||||
unassignTask(input: $input) {
|
unassignTask(input: $input) {
|
||||||
task {
|
task {
|
||||||
id
|
id
|
||||||
@@ -292,9 +290,7 @@ const unassignTaskMutation = graphql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const updateControlStateMutation = graphql`
|
const updateControlStateMutation = graphql`
|
||||||
mutation ControlOverviewPageUpdateControlStateMutation(
|
mutation ControlViewUpdateControlStateMutation($input: UpdateControlInput!) {
|
||||||
$input: UpdateControlInput!
|
|
||||||
) {
|
|
||||||
updateControl(input: $input) {
|
updateControl(input: $input) {
|
||||||
control {
|
control {
|
||||||
id
|
id
|
||||||
@@ -306,12 +302,12 @@ const updateControlStateMutation = graphql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const organizationQuery = graphql`
|
const organizationQuery = graphql`
|
||||||
query ControlOverviewPageOrganizationQuery($organizationId: ID!) {
|
query ControlViewOrganizationQuery($organizationId: ID!) {
|
||||||
organization: node(id: $organizationId) {
|
organization: node(id: $organizationId) {
|
||||||
id
|
id
|
||||||
... on Organization {
|
... on Organization {
|
||||||
peoples(first: 100, orderBy: { direction: ASC, field: FULL_NAME })
|
peoples(first: 100, orderBy: { direction: ASC, field: FULL_NAME })
|
||||||
@connection(key: "ControlOverviewPage_peoples") {
|
@connection(key: "ControlView_peoples") {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
@@ -325,13 +321,13 @@ const organizationQuery = graphql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function ControlOverviewPageContent({
|
function ControlViewContent({
|
||||||
queryRef,
|
queryRef,
|
||||||
}: {
|
}: {
|
||||||
queryRef: PreloadedQuery<ControlOverviewPageQueryType>;
|
queryRef: PreloadedQuery<ControlViewQueryType>;
|
||||||
}) {
|
}) {
|
||||||
const data = usePreloadedQuery<ControlOverviewPageQueryType>(
|
const data = usePreloadedQuery<ControlViewQueryType>(
|
||||||
controlOverviewPageQuery,
|
controlViewQuery,
|
||||||
queryRef
|
queryRef
|
||||||
);
|
);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@@ -341,7 +337,7 @@ function ControlOverviewPageContent({
|
|||||||
|
|
||||||
// Load organization data for people selector
|
// Load organization data for people selector
|
||||||
const [organizationData, setOrganizationData] =
|
const [organizationData, setOrganizationData] =
|
||||||
useState<ControlOverviewPageOrganizationQuery$data | null>(null);
|
useState<ControlViewOrganizationQuery$data | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (organizationId) {
|
if (organizationId) {
|
||||||
@@ -350,9 +346,7 @@ function ControlOverviewPageContent({
|
|||||||
})
|
})
|
||||||
.toPromise()
|
.toPromise()
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
setOrganizationData(
|
setOrganizationData(response as ControlViewOrganizationQuery$data);
|
||||||
response as ControlOverviewPageOrganizationQuery$data
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Error fetching organization data:", error);
|
console.error("Error fetching organization data:", error);
|
||||||
@@ -417,31 +411,26 @@ function ControlOverviewPageContent({
|
|||||||
return "bg-gray-100 text-gray-800";
|
return "bg-gray-100 text-gray-800";
|
||||||
};
|
};
|
||||||
|
|
||||||
const [updateTask] =
|
const [updateTask] = useMutation<ControlViewUpdateTaskStateMutationType>(
|
||||||
useMutation<ControlOverviewPageUpdateTaskStateMutationType>(
|
updateTaskStateMutation
|
||||||
updateTaskStateMutation
|
);
|
||||||
);
|
|
||||||
const [createTask] =
|
const [createTask] =
|
||||||
useMutation<ControlOverviewPageCreateTaskMutationType>(createTaskMutation);
|
useMutation<ControlViewCreateTaskMutationType>(createTaskMutation);
|
||||||
const [deleteTask] =
|
const [deleteTask] =
|
||||||
useMutation<ControlOverviewPageDeleteTaskMutationType>(deleteTaskMutation);
|
useMutation<ControlViewDeleteTaskMutationType>(deleteTaskMutation);
|
||||||
const [uploadEvidence] =
|
const [uploadEvidence] = useMutation<ControlViewUploadEvidenceMutationType>(
|
||||||
useMutation<ControlOverviewPageUploadEvidenceMutationType>(
|
uploadEvidenceMutation
|
||||||
uploadEvidenceMutation
|
);
|
||||||
);
|
const [deleteEvidence] = useMutation<ControlViewDeleteEvidenceMutationType>(
|
||||||
const [deleteEvidence] =
|
deleteEvidenceMutation
|
||||||
useMutation<ControlOverviewPageDeleteEvidenceMutationType>(
|
);
|
||||||
deleteEvidenceMutation
|
|
||||||
);
|
|
||||||
const [assignTask] =
|
const [assignTask] =
|
||||||
useMutation<ControlOverviewPageAssignTaskMutationType>(assignTaskMutation);
|
useMutation<ControlViewAssignTaskMutationType>(assignTaskMutation);
|
||||||
const [unassignTask] =
|
const [unassignTask] =
|
||||||
useMutation<ControlOverviewPageUnassignTaskMutationType>(
|
useMutation<ControlViewUnassignTaskMutationType>(unassignTaskMutation);
|
||||||
unassignTaskMutation
|
|
||||||
);
|
|
||||||
|
|
||||||
const [updateControlState] =
|
const [updateControlState] =
|
||||||
useMutation<ControlOverviewPageUpdateControlStateMutationType>(
|
useMutation<ControlViewUpdateControlStateMutationType>(
|
||||||
updateControlStateMutation
|
updateControlStateMutation
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1971,36 +1960,10 @@ function ControlOverviewPageContent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ControlOverviewPageSkeleton() {
|
export default function ControlView() {
|
||||||
return (
|
|
||||||
<PageTemplateSkeleton
|
|
||||||
actions={
|
|
||||||
<div className="flex items-center gap-2 w-1/3">
|
|
||||||
<div className="bg-muted animate-pulse h-8 w-1/3 rounded-lg" />
|
|
||||||
<div className="bg-muted animate-pulse h-8 w-1/3 rounded-lg" />
|
|
||||||
<div className="bg-muted animate-pulse h-8 w-1/3 rounded-full" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{[1, 2, 3].map((i) => (
|
|
||||||
<Card key={i}>
|
|
||||||
<CardContent className="p-6">
|
|
||||||
<div className="h-6 w-48 bg-muted animate-pulse rounded mb-2" />
|
|
||||||
<div className="h-4 w-full bg-muted animate-pulse rounded" />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</PageTemplateSkeleton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ControlOverviewPage() {
|
|
||||||
const { controlId } = useParams();
|
const { controlId } = useParams();
|
||||||
const [queryRef, loadQuery] = useQueryLoader<ControlOverviewPageQueryType>(
|
const [queryRef, loadQuery] =
|
||||||
controlOverviewPageQuery
|
useQueryLoader<ControlViewQueryType>(controlViewQuery);
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (controlId) {
|
if (controlId) {
|
||||||
@@ -2009,12 +1972,12 @@ export default function ControlOverviewPage() {
|
|||||||
}, [controlId, loadQuery]);
|
}, [controlId, loadQuery]);
|
||||||
|
|
||||||
if (!queryRef) {
|
if (!queryRef) {
|
||||||
return <ControlOverviewPageSkeleton />;
|
return <ControlViewSkeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<ControlOverviewPageSkeleton />}>
|
<Suspense fallback={<ControlViewSkeleton />}>
|
||||||
<ControlOverviewPageContent queryRef={queryRef} />
|
<ControlViewContent queryRef={queryRef} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,260 +1,11 @@
|
|||||||
import { Suspense, useState } from "react";
|
import { PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
import { useNavigate, useParams } from "react-router";
|
import { ErrorBoundaryWithLocation } from "../../ErrorBoundary";
|
||||||
import { graphql, useMutation, ConnectionHandler } from "react-relay";
|
import { lazy, Suspense } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { useLocation } from "react-router";
|
||||||
import { Card } from "@/components/ui/card";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { useToast } from "@/hooks/use-toast";
|
|
||||||
import { HelpCircle } from "lucide-react";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import {
|
|
||||||
CreateControlPageCreateControlMutation,
|
|
||||||
ControlImportance,
|
|
||||||
} from "./__generated__/CreateControlPageCreateControlMutation.graphql";
|
|
||||||
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
|
||||||
|
|
||||||
const createControlMutation = graphql`
|
const CreateControlView = lazy(() => import("./CreateControlView"));
|
||||||
mutation CreateControlPageCreateControlMutation(
|
|
||||||
$input: CreateControlInput!
|
|
||||||
$connections: [ID!]!
|
|
||||||
) {
|
|
||||||
createControl(input: $input) {
|
|
||||||
controlEdge @prependEdge(connections: $connections) {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
description
|
|
||||||
category
|
|
||||||
state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
function EditableField({
|
export function CreateControlViewSkeleton() {
|
||||||
label,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
type = "text",
|
|
||||||
helpText,
|
|
||||||
required,
|
|
||||||
multiline = false,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
type?: string;
|
|
||||||
helpText?: string;
|
|
||||||
required?: boolean;
|
|
||||||
multiline?: boolean;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Label htmlFor={label} className="text-sm font-medium">
|
|
||||||
{label}
|
|
||||||
{required && <span className="text-red-500">*</span>}
|
|
||||||
</Label>
|
|
||||||
{helpText && (
|
|
||||||
<div className="relative flex items-center">
|
|
||||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<span className="sr-only">{helpText}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{multiline ? (
|
|
||||||
<Textarea
|
|
||||||
id={label}
|
|
||||||
value={value}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
|
||||||
onChange(e.target.value)
|
|
||||||
}
|
|
||||||
className={cn(
|
|
||||||
"w-full resize-none",
|
|
||||||
required && !value && "border-red-500"
|
|
||||||
)}
|
|
||||||
placeholder={`Enter ${label.toLowerCase()}`}
|
|
||||||
rows={4}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Input
|
|
||||||
id={label}
|
|
||||||
type={type}
|
|
||||||
value={value}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
||||||
onChange(e.target.value)
|
|
||||||
}
|
|
||||||
className={cn("w-full", required && !value && "border-red-500")}
|
|
||||||
placeholder={`Enter ${label.toLowerCase()}`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CreateControlPageContent() {
|
|
||||||
const { organizationId, frameworkId } = useParams();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { toast } = useToast();
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
category: "",
|
|
||||||
importance: "MANDATORY" as ControlImportance,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [commit, isInFlight] =
|
|
||||||
useMutation<CreateControlPageCreateControlMutation>(createControlMutation);
|
|
||||||
|
|
||||||
const handleFieldChange = (field: keyof typeof formData, value: unknown) => {
|
|
||||||
setFormData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[field]: value,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!formData.name || !formData.description || !formData.category) {
|
|
||||||
toast({
|
|
||||||
title: "Validation Error",
|
|
||||||
description: "Please fill in all required fields.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const connectionId = ConnectionHandler.getConnectionID(
|
|
||||||
frameworkId!,
|
|
||||||
"FrameworkOverviewPage_controls"
|
|
||||||
);
|
|
||||||
|
|
||||||
commit({
|
|
||||||
variables: {
|
|
||||||
input: {
|
|
||||||
frameworkId: frameworkId!,
|
|
||||||
name: formData.name,
|
|
||||||
description: formData.description,
|
|
||||||
category: formData.category,
|
|
||||||
importance: formData.importance,
|
|
||||||
},
|
|
||||||
connections: [connectionId],
|
|
||||||
},
|
|
||||||
onCompleted(data, errors) {
|
|
||||||
if (errors) {
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: errors[0]?.message || "Failed to create control",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: "Success",
|
|
||||||
description: "Control created successfully",
|
|
||||||
});
|
|
||||||
|
|
||||||
navigate(
|
|
||||||
`/organizations/${organizationId}/frameworks/${frameworkId}/controls/${data.createControl.controlEdge.node.id}`
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onError(error) {
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: error.message || "Failed to create control",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageTemplate
|
|
||||||
title="Create Control"
|
|
||||||
description="Create a new control for your framework"
|
|
||||||
>
|
|
||||||
<Card className="max-w-2xl">
|
|
||||||
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
|
||||||
<EditableField
|
|
||||||
label="Name"
|
|
||||||
value={formData.name}
|
|
||||||
onChange={(value) => handleFieldChange("name", value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditableField
|
|
||||||
label="Category"
|
|
||||||
value={formData.category}
|
|
||||||
onChange={(value) => handleFieldChange("category", value)}
|
|
||||||
required
|
|
||||||
helpText="The category this control belongs to"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditableField
|
|
||||||
label="Description"
|
|
||||||
value={formData.description}
|
|
||||||
onChange={(value) => handleFieldChange("description", value)}
|
|
||||||
required
|
|
||||||
multiline
|
|
||||||
helpText="Provide a detailed description of the control"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="importance" className="text-sm font-medium">
|
|
||||||
Importance
|
|
||||||
</Label>
|
|
||||||
<Select
|
|
||||||
value={formData.importance}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
handleFieldChange("importance", value as ControlImportance)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select importance" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="MANDATORY">Mandatory</SelectItem>
|
|
||||||
<SelectItem value="PREFERRED">Preferred</SelectItem>
|
|
||||||
<SelectItem value="ADVANCED">Advanced</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-3">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() =>
|
|
||||||
navigate(
|
|
||||||
`/organizations/${organizationId}/frameworks/${frameworkId}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={isInFlight}>
|
|
||||||
{isInFlight ? "Creating..." : "Create Control"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Card>
|
|
||||||
</PageTemplate>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CreateControlPageSkeleton() {
|
|
||||||
return (
|
return (
|
||||||
<PageTemplateSkeleton
|
<PageTemplateSkeleton
|
||||||
title="Create Control"
|
title="Create Control"
|
||||||
@@ -265,10 +16,14 @@ export function CreateControlPageSkeleton() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CreateControlPage() {
|
export function CreateControlPage() {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<CreateControlPageSkeleton />}>
|
<Suspense key={location.pathname} fallback={<CreateControlViewSkeleton />}>
|
||||||
<CreateControlPageContent />
|
<ErrorBoundaryWithLocation>
|
||||||
|
<CreateControlView />
|
||||||
|
</ErrorBoundaryWithLocation>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,264 @@
|
|||||||
|
import { Suspense, useState } from "react";
|
||||||
|
import { useNavigate, useParams } from "react-router";
|
||||||
|
import { graphql, useMutation, ConnectionHandler } from "react-relay";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { HelpCircle } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
CreateControlViewCreateControlMutation,
|
||||||
|
ControlImportance,
|
||||||
|
} from "./__generated__/CreateControlViewCreateControlMutation.graphql";
|
||||||
|
import { PageTemplate } from "@/components/PageTemplate";
|
||||||
|
import { CreateControlViewSkeleton } from "./CreateControlPage";
|
||||||
|
|
||||||
|
const createControlMutation = graphql`
|
||||||
|
mutation CreateControlViewCreateControlMutation(
|
||||||
|
$input: CreateControlInput!
|
||||||
|
$connections: [ID!]!
|
||||||
|
) {
|
||||||
|
createControl(input: $input) {
|
||||||
|
controlEdge @prependEdge(connections: $connections) {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
category
|
||||||
|
state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function EditableField({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
type = "text",
|
||||||
|
helpText,
|
||||||
|
required,
|
||||||
|
multiline = false,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
type?: string;
|
||||||
|
helpText?: string;
|
||||||
|
required?: boolean;
|
||||||
|
multiline?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Label htmlFor={label} className="text-sm font-medium">
|
||||||
|
{label}
|
||||||
|
{required && <span className="text-red-500">*</span>}
|
||||||
|
</Label>
|
||||||
|
{helpText && (
|
||||||
|
<div className="relative flex items-center">
|
||||||
|
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="sr-only">{helpText}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{multiline ? (
|
||||||
|
<Textarea
|
||||||
|
id={label}
|
||||||
|
value={value}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||||
|
onChange(e.target.value)
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
"w-full resize-none",
|
||||||
|
required && !value && "border-red-500"
|
||||||
|
)}
|
||||||
|
placeholder={`Enter ${label.toLowerCase()}`}
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
id={label}
|
||||||
|
type={type}
|
||||||
|
value={value}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onChange(e.target.value)
|
||||||
|
}
|
||||||
|
className={cn("w-full", required && !value && "border-red-500")}
|
||||||
|
placeholder={`Enter ${label.toLowerCase()}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreateControlViewContent() {
|
||||||
|
const { organizationId, frameworkId } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
category: "",
|
||||||
|
importance: "MANDATORY" as ControlImportance,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [commit, isInFlight] =
|
||||||
|
useMutation<CreateControlViewCreateControlMutation>(createControlMutation);
|
||||||
|
|
||||||
|
const handleFieldChange = (field: keyof typeof formData, value: unknown) => {
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[field]: value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!formData.name || !formData.description || !formData.category) {
|
||||||
|
toast({
|
||||||
|
title: "Validation Error",
|
||||||
|
description: "Please fill in all required fields.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectionId = ConnectionHandler.getConnectionID(
|
||||||
|
frameworkId!,
|
||||||
|
"FrameworkOverviewPage_controls"
|
||||||
|
);
|
||||||
|
|
||||||
|
commit({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
frameworkId: frameworkId!,
|
||||||
|
name: formData.name,
|
||||||
|
description: formData.description,
|
||||||
|
category: formData.category,
|
||||||
|
importance: formData.importance,
|
||||||
|
},
|
||||||
|
connections: [connectionId],
|
||||||
|
},
|
||||||
|
onCompleted(data, errors) {
|
||||||
|
if (errors) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: errors[0]?.message || "Failed to create control",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: "Control created successfully",
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate(
|
||||||
|
`/organizations/${organizationId}/frameworks/${frameworkId}/controls/${data.createControl.controlEdge.node.id}`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: error.message || "Failed to create control",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageTemplate
|
||||||
|
title="Create Control"
|
||||||
|
description="Create a new control for your framework"
|
||||||
|
>
|
||||||
|
<Card className="max-w-2xl">
|
||||||
|
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
||||||
|
<EditableField
|
||||||
|
label="Name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(value) => handleFieldChange("name", value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditableField
|
||||||
|
label="Category"
|
||||||
|
value={formData.category}
|
||||||
|
onChange={(value) => handleFieldChange("category", value)}
|
||||||
|
required
|
||||||
|
helpText="The category this control belongs to"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditableField
|
||||||
|
label="Description"
|
||||||
|
value={formData.description}
|
||||||
|
onChange={(value) => handleFieldChange("description", value)}
|
||||||
|
required
|
||||||
|
multiline
|
||||||
|
helpText="Provide a detailed description of the control"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="importance" className="text-sm font-medium">
|
||||||
|
Importance
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={formData.importance}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
handleFieldChange("importance", value as ControlImportance)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select importance" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="MANDATORY">Mandatory</SelectItem>
|
||||||
|
<SelectItem value="PREFERRED">Preferred</SelectItem>
|
||||||
|
<SelectItem value="ADVANCED">Advanced</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() =>
|
||||||
|
navigate(
|
||||||
|
`/organizations/${organizationId}/frameworks/${frameworkId}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={isInFlight}>
|
||||||
|
{isInFlight ? "Creating..." : "Create Control"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
</PageTemplate>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CreateControlView() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<CreateControlViewSkeleton />}>
|
||||||
|
<CreateControlViewContent />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,337 +1,11 @@
|
|||||||
import { Suspense, useState, useEffect } from "react";
|
import { PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
import { useNavigate, useParams } from "react-router";
|
import { lazy, Suspense } from "react";
|
||||||
import {
|
import { useLocation } from "react-router";
|
||||||
graphql,
|
import { ErrorBoundaryWithLocation } from "../../ErrorBoundary";
|
||||||
useMutation,
|
|
||||||
usePreloadedQuery,
|
|
||||||
PreloadedQuery,
|
|
||||||
useQueryLoader,
|
|
||||||
} from "react-relay";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card } from "@/components/ui/card";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { useToast } from "@/hooks/use-toast";
|
|
||||||
import { HelpCircle } from "lucide-react";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import type { UpdateControlPageUpdateControlMutation as UpdateControlPageUpdateControlMutationType } from "./__generated__/UpdateControlPageUpdateControlMutation.graphql";
|
|
||||||
import type {
|
|
||||||
ControlState,
|
|
||||||
ControlImportance,
|
|
||||||
} from "./__generated__/UpdateControlPageUpdateControlMutation.graphql";
|
|
||||||
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
|
||||||
|
|
||||||
const updateControlMutation = graphql`
|
const UpdateControlView = lazy(() => import("./UpdateControlView"));
|
||||||
mutation UpdateControlPageUpdateControlMutation($input: UpdateControlInput!) {
|
|
||||||
updateControl(input: $input) {
|
|
||||||
control {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
description
|
|
||||||
category
|
|
||||||
importance
|
|
||||||
state
|
|
||||||
version
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const updateControlQuery = graphql`
|
export function UpdateControlViewSkeleton() {
|
||||||
query UpdateControlPageQuery($controlId: ID!) {
|
|
||||||
node(id: $controlId) {
|
|
||||||
... on Control {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
description
|
|
||||||
category
|
|
||||||
importance
|
|
||||||
state
|
|
||||||
version
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
function EditableField({
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
type = "text",
|
|
||||||
helpText,
|
|
||||||
required,
|
|
||||||
multiline = false,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
type?: string;
|
|
||||||
helpText?: string;
|
|
||||||
required?: boolean;
|
|
||||||
multiline?: boolean;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Label htmlFor={label} className="text-sm font-medium">
|
|
||||||
{label}
|
|
||||||
{required && <span className="text-red-500">*</span>}
|
|
||||||
</Label>
|
|
||||||
{helpText && (
|
|
||||||
<div className="relative flex items-center">
|
|
||||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<span className="sr-only">{helpText}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{multiline ? (
|
|
||||||
<Textarea
|
|
||||||
id={label}
|
|
||||||
value={value}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
|
||||||
onChange(e.target.value)
|
|
||||||
}
|
|
||||||
className={cn(
|
|
||||||
"w-full resize-none",
|
|
||||||
required && !value && "border-red-500"
|
|
||||||
)}
|
|
||||||
placeholder={`Enter ${label.toLowerCase()}`}
|
|
||||||
rows={4}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Input
|
|
||||||
id={label}
|
|
||||||
type={type}
|
|
||||||
value={value}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
||||||
onChange(e.target.value)
|
|
||||||
}
|
|
||||||
className={cn("w-full", required && !value && "border-red-500")}
|
|
||||||
placeholder={`Enter ${label.toLowerCase()}`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpdateControlPageContent({
|
|
||||||
queryRef,
|
|
||||||
}: {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
queryRef: PreloadedQuery<any>;
|
|
||||||
}) {
|
|
||||||
const { organizationId, frameworkId, controlId } = useParams();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { toast } = useToast();
|
|
||||||
const data = usePreloadedQuery(updateControlQuery, queryRef);
|
|
||||||
const [editedFields, setEditedFields] = useState<Set<string>>(new Set());
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
category: "",
|
|
||||||
state: "",
|
|
||||||
importance: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data.node) {
|
|
||||||
setFormData({
|
|
||||||
name: data.node.name || "",
|
|
||||||
description: data.node.description || "",
|
|
||||||
category: data.node.category || "",
|
|
||||||
state: data.node.state || "",
|
|
||||||
importance: data.node.importance || "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [data.node]);
|
|
||||||
|
|
||||||
const [commit, isInFlight] =
|
|
||||||
useMutation<UpdateControlPageUpdateControlMutationType>(
|
|
||||||
updateControlMutation
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFieldChange = (field: keyof typeof formData, value: string) => {
|
|
||||||
setFormData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[field]: value,
|
|
||||||
}));
|
|
||||||
setEditedFields((prev) => new Set(prev).add(field));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
navigate(
|
|
||||||
`/organizations/${organizationId}/frameworks/${frameworkId}/controls/${controlId}`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasChanges = editedFields.size > 0;
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!formData.name || !formData.description || !formData.category) {
|
|
||||||
toast({
|
|
||||||
title: "Validation Error",
|
|
||||||
description: "Please fill in all required fields.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const input: {
|
|
||||||
id: string;
|
|
||||||
expectedVersion: number;
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
category?: string;
|
|
||||||
state?: ControlState;
|
|
||||||
importance?: ControlImportance;
|
|
||||||
} = {
|
|
||||||
id: controlId!,
|
|
||||||
expectedVersion: data.node.version,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (editedFields.has("name")) {
|
|
||||||
input.name = formData.name;
|
|
||||||
}
|
|
||||||
if (editedFields.has("description")) {
|
|
||||||
input.description = formData.description;
|
|
||||||
}
|
|
||||||
if (editedFields.has("category")) {
|
|
||||||
input.category = formData.category;
|
|
||||||
}
|
|
||||||
if (editedFields.has("state")) {
|
|
||||||
input.state = formData.state as ControlState;
|
|
||||||
}
|
|
||||||
if (editedFields.has("importance")) {
|
|
||||||
input.importance = formData.importance as ControlImportance;
|
|
||||||
}
|
|
||||||
|
|
||||||
commit({
|
|
||||||
variables: {
|
|
||||||
input,
|
|
||||||
},
|
|
||||||
onCompleted(data, errors) {
|
|
||||||
if (errors) {
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: errors[0]?.message || "Failed to update control",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: "Success",
|
|
||||||
description: "Control updated successfully",
|
|
||||||
});
|
|
||||||
|
|
||||||
navigate(
|
|
||||||
`/organizations/${organizationId}/frameworks/${frameworkId}/controls/${controlId}`
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onError(error) {
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: error.message || "Failed to update control",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageTemplate
|
|
||||||
title="Update Control"
|
|
||||||
description="Update the control details"
|
|
||||||
>
|
|
||||||
<Card className="max-w-2xl">
|
|
||||||
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
|
||||||
<EditableField
|
|
||||||
label="Name"
|
|
||||||
value={formData.name}
|
|
||||||
onChange={(value) => handleFieldChange("name", value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditableField
|
|
||||||
label="Description"
|
|
||||||
value={formData.description}
|
|
||||||
onChange={(value) => handleFieldChange("description", value)}
|
|
||||||
required
|
|
||||||
multiline
|
|
||||||
helpText="Provide a detailed description of the control"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditableField
|
|
||||||
label="Category"
|
|
||||||
value={formData.category}
|
|
||||||
onChange={(value) => handleFieldChange("category", value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="importance" className="text-sm font-medium">
|
|
||||||
Importance
|
|
||||||
</Label>
|
|
||||||
<Select
|
|
||||||
value={formData.importance}
|
|
||||||
onValueChange={(value) => handleFieldChange("importance", value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select importance" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="MANDATORY">Mandatory</SelectItem>
|
|
||||||
<SelectItem value="PREFERRED">Preferred</SelectItem>
|
|
||||||
<SelectItem value="ADVANCED">Advanced</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="state" className="text-sm font-medium">
|
|
||||||
State
|
|
||||||
</Label>
|
|
||||||
<Select
|
|
||||||
value={formData.state}
|
|
||||||
onValueChange={(value) => handleFieldChange("state", value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select state" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="NOT_STARTED">Not Started</SelectItem>
|
|
||||||
<SelectItem value="IN_PROGRESS">In Progress</SelectItem>
|
|
||||||
<SelectItem value="NOT_APPLICABLE">Not Applicable</SelectItem>
|
|
||||||
<SelectItem value="IMPLEMENTED">Implemented</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-3">
|
|
||||||
<Button type="button" variant="outline" onClick={handleCancel}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={isInFlight || !hasChanges}>
|
|
||||||
{isInFlight ? "Updating..." : "Update Control"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Card>
|
|
||||||
</PageTemplate>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function UpdateControlPageSkeleton() {
|
|
||||||
return (
|
return (
|
||||||
<PageTemplateSkeleton
|
<PageTemplateSkeleton
|
||||||
title="Update Control"
|
title="Update Control"
|
||||||
@@ -342,24 +16,14 @@ export function UpdateControlPageSkeleton() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UpdateControlPage() {
|
export function UpdateControlPage() {
|
||||||
const { controlId } = useParams();
|
const location = useLocation();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const [queryRef, loadQuery] = useQueryLoader<any>(updateControlQuery);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (controlId) {
|
|
||||||
loadQuery({ controlId });
|
|
||||||
}
|
|
||||||
}, [controlId, loadQuery]);
|
|
||||||
|
|
||||||
if (!queryRef) {
|
|
||||||
return <UpdateControlPageSkeleton />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<UpdateControlPageSkeleton />}>
|
<Suspense key={location.pathname} fallback={<UpdateControlViewSkeleton />}>
|
||||||
<UpdateControlPageContent queryRef={queryRef} />
|
<ErrorBoundaryWithLocation>
|
||||||
|
<UpdateControlView />
|
||||||
|
</ErrorBoundaryWithLocation>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,355 @@
|
|||||||
|
import { Suspense, useState, useEffect } from "react";
|
||||||
|
import { useNavigate, useParams } from "react-router";
|
||||||
|
import {
|
||||||
|
graphql,
|
||||||
|
useMutation,
|
||||||
|
usePreloadedQuery,
|
||||||
|
PreloadedQuery,
|
||||||
|
useQueryLoader,
|
||||||
|
} from "react-relay";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { HelpCircle } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import type { UpdateControlViewUpdateControlMutation as UpdateControlViewUpdateControlMutationType } from "./__generated__/UpdateControlViewUpdateControlMutation.graphql";
|
||||||
|
import type {
|
||||||
|
ControlState,
|
||||||
|
ControlImportance,
|
||||||
|
} from "./__generated__/UpdateControlViewUpdateControlMutation.graphql";
|
||||||
|
import { PageTemplate } from "@/components/PageTemplate";
|
||||||
|
import { UpdateControlViewSkeleton } from "./UpdateControlPage";
|
||||||
|
|
||||||
|
const updateControlMutation = graphql`
|
||||||
|
mutation UpdateControlViewUpdateControlMutation($input: UpdateControlInput!) {
|
||||||
|
updateControl(input: $input) {
|
||||||
|
control {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
category
|
||||||
|
importance
|
||||||
|
state
|
||||||
|
version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const updateControlQuery = graphql`
|
||||||
|
query UpdateControlViewQuery($controlId: ID!) {
|
||||||
|
node(id: $controlId) {
|
||||||
|
... on Control {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
category
|
||||||
|
importance
|
||||||
|
state
|
||||||
|
version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function EditableField({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
type = "text",
|
||||||
|
helpText,
|
||||||
|
required,
|
||||||
|
multiline = false,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
type?: string;
|
||||||
|
helpText?: string;
|
||||||
|
required?: boolean;
|
||||||
|
multiline?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Label htmlFor={label} className="text-sm font-medium">
|
||||||
|
{label}
|
||||||
|
{required && <span className="text-red-500">*</span>}
|
||||||
|
</Label>
|
||||||
|
{helpText && (
|
||||||
|
<div className="relative flex items-center">
|
||||||
|
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="sr-only">{helpText}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{multiline ? (
|
||||||
|
<Textarea
|
||||||
|
id={label}
|
||||||
|
value={value}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||||
|
onChange(e.target.value)
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
"w-full resize-none",
|
||||||
|
required && !value && "border-red-500"
|
||||||
|
)}
|
||||||
|
placeholder={`Enter ${label.toLowerCase()}`}
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
id={label}
|
||||||
|
type={type}
|
||||||
|
value={value}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onChange(e.target.value)
|
||||||
|
}
|
||||||
|
className={cn("w-full", required && !value && "border-red-500")}
|
||||||
|
placeholder={`Enter ${label.toLowerCase()}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UpdateControlViewContent({
|
||||||
|
queryRef,
|
||||||
|
}: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
queryRef: PreloadedQuery<any>;
|
||||||
|
}) {
|
||||||
|
const { organizationId, frameworkId, controlId } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const data = usePreloadedQuery(updateControlQuery, queryRef);
|
||||||
|
const [editedFields, setEditedFields] = useState<Set<string>>(new Set());
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
category: "",
|
||||||
|
state: "",
|
||||||
|
importance: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data.node) {
|
||||||
|
setFormData({
|
||||||
|
name: data.node.name || "",
|
||||||
|
description: data.node.description || "",
|
||||||
|
category: data.node.category || "",
|
||||||
|
state: data.node.state || "",
|
||||||
|
importance: data.node.importance || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data.node]);
|
||||||
|
|
||||||
|
const [commit, isInFlight] =
|
||||||
|
useMutation<UpdateControlViewUpdateControlMutationType>(
|
||||||
|
updateControlMutation
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFieldChange = (field: keyof typeof formData, value: string) => {
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[field]: value,
|
||||||
|
}));
|
||||||
|
setEditedFields((prev) => new Set(prev).add(field));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
navigate(
|
||||||
|
`/organizations/${organizationId}/frameworks/${frameworkId}/controls/${controlId}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasChanges = editedFields.size > 0;
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!formData.name || !formData.description || !formData.category) {
|
||||||
|
toast({
|
||||||
|
title: "Validation Error",
|
||||||
|
description: "Please fill in all required fields.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const input: {
|
||||||
|
id: string;
|
||||||
|
expectedVersion: number;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
category?: string;
|
||||||
|
state?: ControlState;
|
||||||
|
importance?: ControlImportance;
|
||||||
|
} = {
|
||||||
|
id: controlId!,
|
||||||
|
expectedVersion: data.node.version,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (editedFields.has("name")) {
|
||||||
|
input.name = formData.name;
|
||||||
|
}
|
||||||
|
if (editedFields.has("description")) {
|
||||||
|
input.description = formData.description;
|
||||||
|
}
|
||||||
|
if (editedFields.has("category")) {
|
||||||
|
input.category = formData.category;
|
||||||
|
}
|
||||||
|
if (editedFields.has("state")) {
|
||||||
|
input.state = formData.state as ControlState;
|
||||||
|
}
|
||||||
|
if (editedFields.has("importance")) {
|
||||||
|
input.importance = formData.importance as ControlImportance;
|
||||||
|
}
|
||||||
|
|
||||||
|
commit({
|
||||||
|
variables: {
|
||||||
|
input,
|
||||||
|
},
|
||||||
|
onCompleted(data, errors) {
|
||||||
|
if (errors) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: errors[0]?.message || "Failed to update control",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: "Control updated successfully",
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate(
|
||||||
|
`/organizations/${organizationId}/frameworks/${frameworkId}/controls/${controlId}`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: error.message || "Failed to update control",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageTemplate
|
||||||
|
title="Update Control"
|
||||||
|
description="Update the control details"
|
||||||
|
>
|
||||||
|
<Card className="max-w-2xl">
|
||||||
|
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
||||||
|
<EditableField
|
||||||
|
label="Name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(value) => handleFieldChange("name", value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditableField
|
||||||
|
label="Description"
|
||||||
|
value={formData.description}
|
||||||
|
onChange={(value) => handleFieldChange("description", value)}
|
||||||
|
required
|
||||||
|
multiline
|
||||||
|
helpText="Provide a detailed description of the control"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditableField
|
||||||
|
label="Category"
|
||||||
|
value={formData.category}
|
||||||
|
onChange={(value) => handleFieldChange("category", value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="importance" className="text-sm font-medium">
|
||||||
|
Importance
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={formData.importance}
|
||||||
|
onValueChange={(value) => handleFieldChange("importance", value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select importance" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="MANDATORY">Mandatory</SelectItem>
|
||||||
|
<SelectItem value="PREFERRED">Preferred</SelectItem>
|
||||||
|
<SelectItem value="ADVANCED">Advanced</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="state" className="text-sm font-medium">
|
||||||
|
State
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={formData.state}
|
||||||
|
onValueChange={(value) => handleFieldChange("state", value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select state" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="NOT_STARTED">Not Started</SelectItem>
|
||||||
|
<SelectItem value="IN_PROGRESS">In Progress</SelectItem>
|
||||||
|
<SelectItem value="NOT_APPLICABLE">Not Applicable</SelectItem>
|
||||||
|
<SelectItem value="IMPLEMENTED">Implemented</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3">
|
||||||
|
<Button type="button" variant="outline" onClick={handleCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={isInFlight || !hasChanges}>
|
||||||
|
{isInFlight ? "Updating..." : "Update Control"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
</PageTemplate>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UpdateControlView() {
|
||||||
|
const { controlId } = useParams();
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const [queryRef, loadQuery] = useQueryLoader<any>(updateControlQuery);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (controlId) {
|
||||||
|
loadQuery({ controlId });
|
||||||
|
}
|
||||||
|
}, [controlId, loadQuery]);
|
||||||
|
|
||||||
|
if (!queryRef) {
|
||||||
|
return <UpdateControlViewSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<UpdateControlViewSkeleton />}>
|
||||||
|
<UpdateControlViewContent queryRef={queryRef} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<bd1f2132960c1cdd84906d7de58765e9>>
|
* @generated SignedSource<<18bc7a503d34d2bcb4cdfb6b4cfc1912>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -13,10 +13,10 @@ export type AssignTaskInput = {
|
|||||||
assignedToId: string;
|
assignedToId: string;
|
||||||
taskId: string;
|
taskId: string;
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageAssignTaskMutation$variables = {
|
export type ControlViewAssignTaskMutation$variables = {
|
||||||
input: AssignTaskInput;
|
input: AssignTaskInput;
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageAssignTaskMutation$data = {
|
export type ControlViewAssignTaskMutation$data = {
|
||||||
readonly assignTask: {
|
readonly assignTask: {
|
||||||
readonly task: {
|
readonly task: {
|
||||||
readonly assignedTo: {
|
readonly assignedTo: {
|
||||||
@@ -29,9 +29,9 @@ export type ControlOverviewPageAssignTaskMutation$data = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageAssignTaskMutation = {
|
export type ControlViewAssignTaskMutation = {
|
||||||
response: ControlOverviewPageAssignTaskMutation$data;
|
response: ControlViewAssignTaskMutation$data;
|
||||||
variables: ControlOverviewPageAssignTaskMutation$variables;
|
variables: ControlViewAssignTaskMutation$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -118,7 +118,7 @@ return {
|
|||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "ControlOverviewPageAssignTaskMutation",
|
"name": "ControlViewAssignTaskMutation",
|
||||||
"selections": (v2/*: any*/),
|
"selections": (v2/*: any*/),
|
||||||
"type": "Mutation",
|
"type": "Mutation",
|
||||||
"abstractKey": null
|
"abstractKey": null
|
||||||
@@ -127,20 +127,20 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "ControlOverviewPageAssignTaskMutation",
|
"name": "ControlViewAssignTaskMutation",
|
||||||
"selections": (v2/*: any*/)
|
"selections": (v2/*: any*/)
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "2a75b40af9d9853b851d153595dc1317",
|
"cacheID": "0a406c0e166607d86896adc0dfb5618e",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "ControlOverviewPageAssignTaskMutation",
|
"name": "ControlViewAssignTaskMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation ControlOverviewPageAssignTaskMutation(\n $input: AssignTaskInput!\n) {\n assignTask(input: $input) {\n task {\n id\n version\n assignedTo {\n id\n fullName\n primaryEmailAddress\n }\n }\n }\n}\n"
|
"text": "mutation ControlViewAssignTaskMutation(\n $input: AssignTaskInput!\n) {\n assignTask(input: $input) {\n task {\n id\n version\n assignedTo {\n id\n fullName\n primaryEmailAddress\n }\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "02f3b1fb21bc39cea7fde9cc3945adc6";
|
(node as any).hash = "1a9a18038c2f6ad39d3efe61d2d0e33d";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<318d80720efe2f6e610353e3a640d786>>
|
* @generated SignedSource<<93ac47858fbd7c339faac83f193cb21b>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -17,11 +17,11 @@ export type CreateTaskInput = {
|
|||||||
name: string;
|
name: string;
|
||||||
timeEstimate?: any | null | undefined;
|
timeEstimate?: any | null | undefined;
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageCreateTaskMutation$variables = {
|
export type ControlViewCreateTaskMutation$variables = {
|
||||||
connections: ReadonlyArray<string>;
|
connections: ReadonlyArray<string>;
|
||||||
input: CreateTaskInput;
|
input: CreateTaskInput;
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageCreateTaskMutation$data = {
|
export type ControlViewCreateTaskMutation$data = {
|
||||||
readonly createTask: {
|
readonly createTask: {
|
||||||
readonly taskEdge: {
|
readonly taskEdge: {
|
||||||
readonly node: {
|
readonly node: {
|
||||||
@@ -40,9 +40,9 @@ export type ControlOverviewPageCreateTaskMutation$data = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageCreateTaskMutation = {
|
export type ControlViewCreateTaskMutation = {
|
||||||
response: ControlOverviewPageCreateTaskMutation$data;
|
response: ControlViewCreateTaskMutation$data;
|
||||||
variables: ControlOverviewPageCreateTaskMutation$variables;
|
variables: ControlViewCreateTaskMutation$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -162,7 +162,7 @@ return {
|
|||||||
],
|
],
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "ControlOverviewPageCreateTaskMutation",
|
"name": "ControlViewCreateTaskMutation",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -187,7 +187,7 @@ return {
|
|||||||
(v0/*: any*/)
|
(v0/*: any*/)
|
||||||
],
|
],
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "ControlOverviewPageCreateTaskMutation",
|
"name": "ControlViewCreateTaskMutation",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -220,16 +220,16 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "ea82092f026318c66f62c910449be445",
|
"cacheID": "f823a58c96ee652fd522df58de3710a1",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "ControlOverviewPageCreateTaskMutation",
|
"name": "ControlViewCreateTaskMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation ControlOverviewPageCreateTaskMutation(\n $input: CreateTaskInput!\n) {\n createTask(input: $input) {\n taskEdge {\n node {\n id\n name\n description\n timeEstimate\n state\n version\n assignedTo {\n id\n fullName\n primaryEmailAddress\n }\n }\n }\n }\n}\n"
|
"text": "mutation ControlViewCreateTaskMutation(\n $input: CreateTaskInput!\n) {\n createTask(input: $input) {\n taskEdge {\n node {\n id\n name\n description\n timeEstimate\n state\n version\n assignedTo {\n id\n fullName\n primaryEmailAddress\n }\n }\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "1587bb98852e8a5ff4e4d6cb408f4d1e";
|
(node as any).hash = "5d50a685dad17446a0cab1891dc00374";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<c09b83c20157fc20bf74a2c9c031b716>>
|
* @generated SignedSource<<5cc5a8fcb02623e685586d2f3510fb4a>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -12,18 +12,18 @@ import { ConcreteRequest } from 'relay-runtime';
|
|||||||
export type DeleteEvidenceInput = {
|
export type DeleteEvidenceInput = {
|
||||||
evidenceId: string;
|
evidenceId: string;
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageDeleteEvidenceMutation$variables = {
|
export type ControlViewDeleteEvidenceMutation$variables = {
|
||||||
connections: ReadonlyArray<string>;
|
connections: ReadonlyArray<string>;
|
||||||
input: DeleteEvidenceInput;
|
input: DeleteEvidenceInput;
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageDeleteEvidenceMutation$data = {
|
export type ControlViewDeleteEvidenceMutation$data = {
|
||||||
readonly deleteEvidence: {
|
readonly deleteEvidence: {
|
||||||
readonly deletedEvidenceId: string;
|
readonly deletedEvidenceId: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageDeleteEvidenceMutation = {
|
export type ControlViewDeleteEvidenceMutation = {
|
||||||
response: ControlOverviewPageDeleteEvidenceMutation$data;
|
response: ControlViewDeleteEvidenceMutation$data;
|
||||||
variables: ControlOverviewPageDeleteEvidenceMutation$variables;
|
variables: ControlViewDeleteEvidenceMutation$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -59,7 +59,7 @@ return {
|
|||||||
],
|
],
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "ControlOverviewPageDeleteEvidenceMutation",
|
"name": "ControlViewDeleteEvidenceMutation",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -84,7 +84,7 @@ return {
|
|||||||
(v0/*: any*/)
|
(v0/*: any*/)
|
||||||
],
|
],
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "ControlOverviewPageDeleteEvidenceMutation",
|
"name": "ControlViewDeleteEvidenceMutation",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -117,16 +117,16 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "51d62fb682189fa0aaa5cc5b7a5b7862",
|
"cacheID": "594da4829274c817424f9b84ba99a5a6",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "ControlOverviewPageDeleteEvidenceMutation",
|
"name": "ControlViewDeleteEvidenceMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation ControlOverviewPageDeleteEvidenceMutation(\n $input: DeleteEvidenceInput!\n) {\n deleteEvidence(input: $input) {\n deletedEvidenceId\n }\n}\n"
|
"text": "mutation ControlViewDeleteEvidenceMutation(\n $input: DeleteEvidenceInput!\n) {\n deleteEvidence(input: $input) {\n deletedEvidenceId\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "f55cf1c279c95dcabc11e1b5aafcdc46";
|
(node as any).hash = "a6b5e90e931adf04f45be9403b291ad9";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<cd5744c9739535825e9922b9cb95f034>>
|
* @generated SignedSource<<e6c50692038abaca7921b9f0810ddfaa>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -12,18 +12,18 @@ import { ConcreteRequest } from 'relay-runtime';
|
|||||||
export type DeleteTaskInput = {
|
export type DeleteTaskInput = {
|
||||||
taskId: string;
|
taskId: string;
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageDeleteTaskMutation$variables = {
|
export type ControlViewDeleteTaskMutation$variables = {
|
||||||
connections: ReadonlyArray<string>;
|
connections: ReadonlyArray<string>;
|
||||||
input: DeleteTaskInput;
|
input: DeleteTaskInput;
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageDeleteTaskMutation$data = {
|
export type ControlViewDeleteTaskMutation$data = {
|
||||||
readonly deleteTask: {
|
readonly deleteTask: {
|
||||||
readonly deletedTaskId: string;
|
readonly deletedTaskId: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageDeleteTaskMutation = {
|
export type ControlViewDeleteTaskMutation = {
|
||||||
response: ControlOverviewPageDeleteTaskMutation$data;
|
response: ControlViewDeleteTaskMutation$data;
|
||||||
variables: ControlOverviewPageDeleteTaskMutation$variables;
|
variables: ControlViewDeleteTaskMutation$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -59,7 +59,7 @@ return {
|
|||||||
],
|
],
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "ControlOverviewPageDeleteTaskMutation",
|
"name": "ControlViewDeleteTaskMutation",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -84,7 +84,7 @@ return {
|
|||||||
(v0/*: any*/)
|
(v0/*: any*/)
|
||||||
],
|
],
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "ControlOverviewPageDeleteTaskMutation",
|
"name": "ControlViewDeleteTaskMutation",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -117,16 +117,16 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "06db9599a3e83354c0e865d08cdb29dd",
|
"cacheID": "36b910121f02343b2e920adda58d7032",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "ControlOverviewPageDeleteTaskMutation",
|
"name": "ControlViewDeleteTaskMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation ControlOverviewPageDeleteTaskMutation(\n $input: DeleteTaskInput!\n) {\n deleteTask(input: $input) {\n deletedTaskId\n }\n}\n"
|
"text": "mutation ControlViewDeleteTaskMutation(\n $input: DeleteTaskInput!\n) {\n deleteTask(input: $input) {\n deletedTaskId\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "885a98007e48ab3175d0480c5a750e19";
|
(node as any).hash = "3aa9ea0eca32df8e6fe0258ee1140e56";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<5fff8009247229346c3cda94359f2129>>
|
* @generated SignedSource<<6097cb76bff61c342c2b6389a8e7f618>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -9,18 +9,18 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
|
||||||
import { ConcreteRequest } from 'relay-runtime';
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
export type ControlOverviewPageGetEvidenceFileUrlQuery$variables = {
|
export type ControlViewGetEvidenceFileUrlQuery$variables = {
|
||||||
evidenceId: string;
|
evidenceId: string;
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageGetEvidenceFileUrlQuery$data = {
|
export type ControlViewGetEvidenceFileUrlQuery$data = {
|
||||||
readonly node: {
|
readonly node: {
|
||||||
readonly fileUrl?: string | null | undefined;
|
readonly fileUrl?: string | null | undefined;
|
||||||
readonly id?: string;
|
readonly id?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageGetEvidenceFileUrlQuery = {
|
export type ControlViewGetEvidenceFileUrlQuery = {
|
||||||
response: ControlOverviewPageGetEvidenceFileUrlQuery$data;
|
response: ControlViewGetEvidenceFileUrlQuery$data;
|
||||||
variables: ControlOverviewPageGetEvidenceFileUrlQuery$variables;
|
variables: ControlViewGetEvidenceFileUrlQuery$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -57,7 +57,7 @@ return {
|
|||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "ControlOverviewPageGetEvidenceFileUrlQuery",
|
"name": "ControlViewGetEvidenceFileUrlQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -87,7 +87,7 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "ControlOverviewPageGetEvidenceFileUrlQuery",
|
"name": "ControlViewGetEvidenceFileUrlQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -119,16 +119,16 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "debb5fda7cec88257a50998c78ee1e33",
|
"cacheID": "b656f027db1774647bcd0eefad36f66f",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "ControlOverviewPageGetEvidenceFileUrlQuery",
|
"name": "ControlViewGetEvidenceFileUrlQuery",
|
||||||
"operationKind": "query",
|
"operationKind": "query",
|
||||||
"text": "query ControlOverviewPageGetEvidenceFileUrlQuery(\n $evidenceId: ID!\n) {\n node(id: $evidenceId) {\n __typename\n ... on Evidence {\n id\n fileUrl\n }\n id\n }\n}\n"
|
"text": "query ControlViewGetEvidenceFileUrlQuery(\n $evidenceId: ID!\n) {\n node(id: $evidenceId) {\n __typename\n ... on Evidence {\n id\n fileUrl\n }\n id\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "a97d9390a7415ff87916e268458f5ead";
|
(node as any).hash = "a06f8279b87274b3cec4a11f44a53f49";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<b005acefd80f6091d29b4e095f4d271c>>
|
* @generated SignedSource<<be19ac6afdae6f9be1b869afbd0a0838>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -9,10 +9,10 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
|
||||||
import { ConcreteRequest } from 'relay-runtime';
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
export type ControlOverviewPageOrganizationQuery$variables = {
|
export type ControlViewOrganizationQuery$variables = {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageOrganizationQuery$data = {
|
export type ControlViewOrganizationQuery$data = {
|
||||||
readonly organization: {
|
readonly organization: {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly peoples?: {
|
readonly peoples?: {
|
||||||
@@ -26,9 +26,9 @@ export type ControlOverviewPageOrganizationQuery$data = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageOrganizationQuery = {
|
export type ControlViewOrganizationQuery = {
|
||||||
response: ControlOverviewPageOrganizationQuery$data;
|
response: ControlViewOrganizationQuery$data;
|
||||||
variables: ControlOverviewPageOrganizationQuery$variables;
|
variables: ControlViewOrganizationQuery$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -153,7 +153,7 @@ return {
|
|||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "ControlOverviewPageOrganizationQuery",
|
"name": "ControlViewOrganizationQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": "organization",
|
"alias": "organization",
|
||||||
@@ -174,10 +174,10 @@ return {
|
|||||||
],
|
],
|
||||||
"concreteType": "PeopleConnection",
|
"concreteType": "PeopleConnection",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "__ControlOverviewPage_peoples_connection",
|
"name": "__ControlView_peoples_connection",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": (v5/*: any*/),
|
"selections": (v5/*: any*/),
|
||||||
"storageKey": "__ControlOverviewPage_peoples_connection(orderBy:{\"direction\":\"ASC\",\"field\":\"FULL_NAME\"})"
|
"storageKey": "__ControlView_peoples_connection(orderBy:{\"direction\":\"ASC\",\"field\":\"FULL_NAME\"})"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"type": "Organization",
|
"type": "Organization",
|
||||||
@@ -194,7 +194,7 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "ControlOverviewPageOrganizationQuery",
|
"name": "ControlViewOrganizationQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": "organization",
|
"alias": "organization",
|
||||||
@@ -226,7 +226,7 @@ return {
|
|||||||
"orderBy"
|
"orderBy"
|
||||||
],
|
],
|
||||||
"handle": "connection",
|
"handle": "connection",
|
||||||
"key": "ControlOverviewPage_peoples",
|
"key": "ControlView_peoples",
|
||||||
"kind": "LinkedHandle",
|
"kind": "LinkedHandle",
|
||||||
"name": "peoples"
|
"name": "peoples"
|
||||||
}
|
}
|
||||||
@@ -240,7 +240,7 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "b92e9c3fbb4dcdb62652d4f9dd9f3712",
|
"cacheID": "9a339515a618cf48b77e74890b016272",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"connection": [
|
"connection": [
|
||||||
@@ -255,13 +255,13 @@ return {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"name": "ControlOverviewPageOrganizationQuery",
|
"name": "ControlViewOrganizationQuery",
|
||||||
"operationKind": "query",
|
"operationKind": "query",
|
||||||
"text": "query ControlOverviewPageOrganizationQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n peoples(first: 100, orderBy: {direction: ASC, field: FULL_NAME}) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
|
"text": "query ControlViewOrganizationQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n peoples(first: 100, orderBy: {direction: ASC, field: FULL_NAME}) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "215186695662d7c8344e35b1f053ee5f";
|
(node as any).hash = "22507c997964e19a663c819b750e50c9";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<e273ee54aacd3db274341d21b33809b2>>
|
* @generated SignedSource<<d90309f0abd5f91c5f38c3070e27758b>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -14,10 +14,10 @@ export type ControlState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "N
|
|||||||
export type EvidenceState = "EXPIRED" | "INVALID" | "VALID";
|
export type EvidenceState = "EXPIRED" | "INVALID" | "VALID";
|
||||||
export type EvidenceType = "FILE" | "LINK";
|
export type EvidenceType = "FILE" | "LINK";
|
||||||
export type TaskState = "DONE" | "TODO";
|
export type TaskState = "DONE" | "TODO";
|
||||||
export type ControlOverviewPageQuery$variables = {
|
export type ControlViewQuery$variables = {
|
||||||
controlId: string;
|
controlId: string;
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageQuery$data = {
|
export type ControlViewQuery$data = {
|
||||||
readonly control: {
|
readonly control: {
|
||||||
readonly category?: string;
|
readonly category?: string;
|
||||||
readonly description?: string;
|
readonly description?: string;
|
||||||
@@ -61,9 +61,9 @@ export type ControlOverviewPageQuery$data = {
|
|||||||
readonly version?: number;
|
readonly version?: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageQuery = {
|
export type ControlViewQuery = {
|
||||||
response: ControlOverviewPageQuery$data;
|
response: ControlViewQuery$data;
|
||||||
variables: ControlOverviewPageQuery$variables;
|
variables: ControlViewQuery$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -305,7 +305,7 @@ return {
|
|||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "ControlOverviewPageQuery",
|
"name": "ControlViewQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": "control",
|
"alias": "control",
|
||||||
@@ -330,7 +330,7 @@ return {
|
|||||||
"args": null,
|
"args": null,
|
||||||
"concreteType": "TaskConnection",
|
"concreteType": "TaskConnection",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "__ControlOverviewPage_tasks_connection",
|
"name": "__ControlView_tasks_connection",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
@@ -361,7 +361,7 @@ return {
|
|||||||
"args": null,
|
"args": null,
|
||||||
"concreteType": "EvidenceConnection",
|
"concreteType": "EvidenceConnection",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "__ControlOverviewPage_evidences_connection",
|
"name": "__ControlView_evidences_connection",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": (v15/*: any*/),
|
"selections": (v15/*: any*/),
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
@@ -394,7 +394,7 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "ControlOverviewPageQuery",
|
"name": "ControlViewQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": "control",
|
"alias": "control",
|
||||||
@@ -461,7 +461,7 @@ return {
|
|||||||
"args": (v17/*: any*/),
|
"args": (v17/*: any*/),
|
||||||
"filters": null,
|
"filters": null,
|
||||||
"handle": "connection",
|
"handle": "connection",
|
||||||
"key": "ControlOverviewPage_evidences",
|
"key": "ControlView_evidences",
|
||||||
"kind": "LinkedHandle",
|
"kind": "LinkedHandle",
|
||||||
"name": "evidences"
|
"name": "evidences"
|
||||||
},
|
},
|
||||||
@@ -483,7 +483,7 @@ return {
|
|||||||
"args": (v16/*: any*/),
|
"args": (v16/*: any*/),
|
||||||
"filters": null,
|
"filters": null,
|
||||||
"handle": "connection",
|
"handle": "connection",
|
||||||
"key": "ControlOverviewPage_tasks",
|
"key": "ControlView_tasks",
|
||||||
"kind": "LinkedHandle",
|
"kind": "LinkedHandle",
|
||||||
"name": "tasks"
|
"name": "tasks"
|
||||||
}
|
}
|
||||||
@@ -497,7 +497,7 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "6f419912ad357b36c51113ef7672ad51",
|
"cacheID": "35556c446c544e8b281b572272daae6b",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"connection": [
|
"connection": [
|
||||||
@@ -518,13 +518,13 @@ return {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"name": "ControlOverviewPageQuery",
|
"name": "ControlViewQuery",
|
||||||
"operationKind": "query",
|
"operationKind": "query",
|
||||||
"text": "query ControlOverviewPageQuery(\n $controlId: ID!\n) {\n control: node(id: $controlId) {\n __typename\n id\n ... on Control {\n name\n description\n state\n importance\n category\n version\n tasks(first: 100) {\n edges {\n node {\n id\n name\n description\n state\n timeEstimate\n version\n assignedTo {\n id\n fullName\n primaryEmailAddress\n }\n evidences(first: 50) {\n edges {\n node {\n id\n mimeType\n filename\n size\n state\n type\n url\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
|
"text": "query ControlViewQuery(\n $controlId: ID!\n) {\n control: node(id: $controlId) {\n __typename\n id\n ... on Control {\n name\n description\n state\n importance\n category\n version\n tasks(first: 100) {\n edges {\n node {\n id\n name\n description\n state\n timeEstimate\n version\n assignedTo {\n id\n fullName\n primaryEmailAddress\n }\n evidences(first: 50) {\n edges {\n node {\n id\n mimeType\n filename\n size\n state\n type\n url\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "59a0c6e5a414410be244c7a62bba7696";
|
(node as any).hash = "cf9ee1fdc5e0e09c6b883299af7ffa37";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<ec9f52c0ab13cd30148d7b2a97f59b63>>
|
* @generated SignedSource<<c9adac41c9c5084befb16e9c01a12900>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -12,10 +12,10 @@ import { ConcreteRequest } from 'relay-runtime';
|
|||||||
export type UnassignTaskInput = {
|
export type UnassignTaskInput = {
|
||||||
taskId: string;
|
taskId: string;
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageUnassignTaskMutation$variables = {
|
export type ControlViewUnassignTaskMutation$variables = {
|
||||||
input: UnassignTaskInput;
|
input: UnassignTaskInput;
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageUnassignTaskMutation$data = {
|
export type ControlViewUnassignTaskMutation$data = {
|
||||||
readonly unassignTask: {
|
readonly unassignTask: {
|
||||||
readonly task: {
|
readonly task: {
|
||||||
readonly assignedTo: {
|
readonly assignedTo: {
|
||||||
@@ -28,9 +28,9 @@ export type ControlOverviewPageUnassignTaskMutation$data = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageUnassignTaskMutation = {
|
export type ControlViewUnassignTaskMutation = {
|
||||||
response: ControlOverviewPageUnassignTaskMutation$data;
|
response: ControlViewUnassignTaskMutation$data;
|
||||||
variables: ControlOverviewPageUnassignTaskMutation$variables;
|
variables: ControlViewUnassignTaskMutation$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -117,7 +117,7 @@ return {
|
|||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "ControlOverviewPageUnassignTaskMutation",
|
"name": "ControlViewUnassignTaskMutation",
|
||||||
"selections": (v2/*: any*/),
|
"selections": (v2/*: any*/),
|
||||||
"type": "Mutation",
|
"type": "Mutation",
|
||||||
"abstractKey": null
|
"abstractKey": null
|
||||||
@@ -126,20 +126,20 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "ControlOverviewPageUnassignTaskMutation",
|
"name": "ControlViewUnassignTaskMutation",
|
||||||
"selections": (v2/*: any*/)
|
"selections": (v2/*: any*/)
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "dc570710d28f3208dbaff23593819f4d",
|
"cacheID": "15ebb533fa8c9ef25aba306106a198eb",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "ControlOverviewPageUnassignTaskMutation",
|
"name": "ControlViewUnassignTaskMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation ControlOverviewPageUnassignTaskMutation(\n $input: UnassignTaskInput!\n) {\n unassignTask(input: $input) {\n task {\n id\n version\n assignedTo {\n id\n fullName\n primaryEmailAddress\n }\n }\n }\n}\n"
|
"text": "mutation ControlViewUnassignTaskMutation(\n $input: UnassignTaskInput!\n) {\n unassignTask(input: $input) {\n task {\n id\n version\n assignedTo {\n id\n fullName\n primaryEmailAddress\n }\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "4c809cd810a4178ab4f7a6d020338187";
|
(node as any).hash = "b0956d42397dc015e2ece4ff37a83857";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<f8dd4ed041b15ddf484ff9b1735c69ed>>
|
* @generated SignedSource<<42dcc8ca7d9ad288321e73fa5175db44>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -20,10 +20,10 @@ export type UpdateControlInput = {
|
|||||||
name?: string | null | undefined;
|
name?: string | null | undefined;
|
||||||
state?: ControlState | null | undefined;
|
state?: ControlState | null | undefined;
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageUpdateControlStateMutation$variables = {
|
export type ControlViewUpdateControlStateMutation$variables = {
|
||||||
input: UpdateControlInput;
|
input: UpdateControlInput;
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageUpdateControlStateMutation$data = {
|
export type ControlViewUpdateControlStateMutation$data = {
|
||||||
readonly updateControl: {
|
readonly updateControl: {
|
||||||
readonly control: {
|
readonly control: {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
@@ -32,9 +32,9 @@ export type ControlOverviewPageUpdateControlStateMutation$data = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageUpdateControlStateMutation = {
|
export type ControlViewUpdateControlStateMutation = {
|
||||||
response: ControlOverviewPageUpdateControlStateMutation$data;
|
response: ControlViewUpdateControlStateMutation$data;
|
||||||
variables: ControlOverviewPageUpdateControlStateMutation$variables;
|
variables: ControlViewUpdateControlStateMutation$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -101,7 +101,7 @@ return {
|
|||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "ControlOverviewPageUpdateControlStateMutation",
|
"name": "ControlViewUpdateControlStateMutation",
|
||||||
"selections": (v1/*: any*/),
|
"selections": (v1/*: any*/),
|
||||||
"type": "Mutation",
|
"type": "Mutation",
|
||||||
"abstractKey": null
|
"abstractKey": null
|
||||||
@@ -110,20 +110,20 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "ControlOverviewPageUpdateControlStateMutation",
|
"name": "ControlViewUpdateControlStateMutation",
|
||||||
"selections": (v1/*: any*/)
|
"selections": (v1/*: any*/)
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "f119c567d66788b2e3bed77950602ab5",
|
"cacheID": "b34b9c0ddec55d16efcc2f38e8334773",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "ControlOverviewPageUpdateControlStateMutation",
|
"name": "ControlViewUpdateControlStateMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation ControlOverviewPageUpdateControlStateMutation(\n $input: UpdateControlInput!\n) {\n updateControl(input: $input) {\n control {\n id\n state\n version\n }\n }\n}\n"
|
"text": "mutation ControlViewUpdateControlStateMutation(\n $input: UpdateControlInput!\n) {\n updateControl(input: $input) {\n control {\n id\n state\n version\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "a7792a068992b6e9b0c560426b8a99a4";
|
(node as any).hash = "3eaaab55403bfaf343d4a740ad0128e1";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<2e946185d02bd4e3d510d81c68cb9863>>
|
* @generated SignedSource<<70ae17b69b366175da4d00bfae78ae7f>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -18,10 +18,10 @@ export type UpdateTaskInput = {
|
|||||||
taskId: string;
|
taskId: string;
|
||||||
timeEstimate?: any | null | undefined;
|
timeEstimate?: any | null | undefined;
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageUpdateTaskStateMutation$variables = {
|
export type ControlViewUpdateTaskStateMutation$variables = {
|
||||||
input: UpdateTaskInput;
|
input: UpdateTaskInput;
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageUpdateTaskStateMutation$data = {
|
export type ControlViewUpdateTaskStateMutation$data = {
|
||||||
readonly updateTask: {
|
readonly updateTask: {
|
||||||
readonly task: {
|
readonly task: {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
@@ -30,9 +30,9 @@ export type ControlOverviewPageUpdateTaskStateMutation$data = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageUpdateTaskStateMutation = {
|
export type ControlViewUpdateTaskStateMutation = {
|
||||||
response: ControlOverviewPageUpdateTaskStateMutation$data;
|
response: ControlViewUpdateTaskStateMutation$data;
|
||||||
variables: ControlOverviewPageUpdateTaskStateMutation$variables;
|
variables: ControlViewUpdateTaskStateMutation$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -99,7 +99,7 @@ return {
|
|||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "ControlOverviewPageUpdateTaskStateMutation",
|
"name": "ControlViewUpdateTaskStateMutation",
|
||||||
"selections": (v1/*: any*/),
|
"selections": (v1/*: any*/),
|
||||||
"type": "Mutation",
|
"type": "Mutation",
|
||||||
"abstractKey": null
|
"abstractKey": null
|
||||||
@@ -108,20 +108,20 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "ControlOverviewPageUpdateTaskStateMutation",
|
"name": "ControlViewUpdateTaskStateMutation",
|
||||||
"selections": (v1/*: any*/)
|
"selections": (v1/*: any*/)
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "4ebc7b830cc1c2129ec26429ab681c44",
|
"cacheID": "1b9a74365007e7c46668815c9e37cd0a",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "ControlOverviewPageUpdateTaskStateMutation",
|
"name": "ControlViewUpdateTaskStateMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation ControlOverviewPageUpdateTaskStateMutation(\n $input: UpdateTaskInput!\n) {\n updateTask(input: $input) {\n task {\n id\n state\n version\n }\n }\n}\n"
|
"text": "mutation ControlViewUpdateTaskStateMutation(\n $input: UpdateTaskInput!\n) {\n updateTask(input: $input) {\n task {\n id\n state\n version\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "9a076b4306fee5793009ea3082e5cde2";
|
(node as any).hash = "7cb2f42aadcc5f709377d2bf9dfea2da";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<6726ce2446381674b1992ece913fd680>>
|
* @generated SignedSource<<f6ace4744710269a7c6502726617efcb>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -19,11 +19,11 @@ export type UploadEvidenceInput = {
|
|||||||
type: EvidenceType;
|
type: EvidenceType;
|
||||||
url?: string | null | undefined;
|
url?: string | null | undefined;
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageUploadEvidenceMutation$variables = {
|
export type ControlViewUploadEvidenceMutation$variables = {
|
||||||
connections: ReadonlyArray<string>;
|
connections: ReadonlyArray<string>;
|
||||||
input: UploadEvidenceInput;
|
input: UploadEvidenceInput;
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageUploadEvidenceMutation$data = {
|
export type ControlViewUploadEvidenceMutation$data = {
|
||||||
readonly uploadEvidence: {
|
readonly uploadEvidence: {
|
||||||
readonly evidenceEdge: {
|
readonly evidenceEdge: {
|
||||||
readonly node: {
|
readonly node: {
|
||||||
@@ -40,9 +40,9 @@ export type ControlOverviewPageUploadEvidenceMutation$data = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type ControlOverviewPageUploadEvidenceMutation = {
|
export type ControlViewUploadEvidenceMutation = {
|
||||||
response: ControlOverviewPageUploadEvidenceMutation$data;
|
response: ControlViewUploadEvidenceMutation$data;
|
||||||
variables: ControlOverviewPageUploadEvidenceMutation$variables;
|
variables: ControlViewUploadEvidenceMutation$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -156,7 +156,7 @@ return {
|
|||||||
],
|
],
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "ControlOverviewPageUploadEvidenceMutation",
|
"name": "ControlViewUploadEvidenceMutation",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -181,7 +181,7 @@ return {
|
|||||||
(v0/*: any*/)
|
(v0/*: any*/)
|
||||||
],
|
],
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "ControlOverviewPageUploadEvidenceMutation",
|
"name": "ControlViewUploadEvidenceMutation",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -214,16 +214,16 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "de3681706266cd453ddccb5fe2900ea8",
|
"cacheID": "62fbef44afc988cdedc94c7a0292a432",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "ControlOverviewPageUploadEvidenceMutation",
|
"name": "ControlViewUploadEvidenceMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation ControlOverviewPageUploadEvidenceMutation(\n $input: UploadEvidenceInput!\n) {\n uploadEvidence(input: $input) {\n evidenceEdge {\n node {\n id\n filename\n fileUrl\n mimeType\n type\n url\n size\n state\n createdAt\n }\n }\n }\n}\n"
|
"text": "mutation ControlViewUploadEvidenceMutation(\n $input: UploadEvidenceInput!\n) {\n uploadEvidence(input: $input) {\n evidenceEdge {\n node {\n id\n filename\n fileUrl\n mimeType\n type\n url\n size\n state\n createdAt\n }\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "396146017c07e06dc709199a0b5ad012";
|
(node as any).hash = "1e2b47702f9a03efdca7fdfd44ebbccf";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<6a4f7190379c0f273e97cedf6aebe517>>
|
* @generated SignedSource<<a38544ace1fb4fc6da394e8ea2c35787>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -18,11 +18,11 @@ export type CreateControlInput = {
|
|||||||
importance: ControlImportance;
|
importance: ControlImportance;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
export type CreateControlPageCreateControlMutation$variables = {
|
export type CreateControlViewCreateControlMutation$variables = {
|
||||||
connections: ReadonlyArray<string>;
|
connections: ReadonlyArray<string>;
|
||||||
input: CreateControlInput;
|
input: CreateControlInput;
|
||||||
};
|
};
|
||||||
export type CreateControlPageCreateControlMutation$data = {
|
export type CreateControlViewCreateControlMutation$data = {
|
||||||
readonly createControl: {
|
readonly createControl: {
|
||||||
readonly controlEdge: {
|
readonly controlEdge: {
|
||||||
readonly node: {
|
readonly node: {
|
||||||
@@ -35,9 +35,9 @@ export type CreateControlPageCreateControlMutation$data = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type CreateControlPageCreateControlMutation = {
|
export type CreateControlViewCreateControlMutation = {
|
||||||
response: CreateControlPageCreateControlMutation$data;
|
response: CreateControlViewCreateControlMutation$data;
|
||||||
variables: CreateControlPageCreateControlMutation$variables;
|
variables: CreateControlViewCreateControlMutation$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -123,7 +123,7 @@ return {
|
|||||||
],
|
],
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "CreateControlPageCreateControlMutation",
|
"name": "CreateControlViewCreateControlMutation",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -148,7 +148,7 @@ return {
|
|||||||
(v0/*: any*/)
|
(v0/*: any*/)
|
||||||
],
|
],
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "CreateControlPageCreateControlMutation",
|
"name": "CreateControlViewCreateControlMutation",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -181,16 +181,16 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "c6b6a979bccf19dd57123c49d5ce738b",
|
"cacheID": "f0f7112f1eb67ed8e7f0fe38b427b0c5",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "CreateControlPageCreateControlMutation",
|
"name": "CreateControlViewCreateControlMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation CreateControlPageCreateControlMutation(\n $input: CreateControlInput!\n) {\n createControl(input: $input) {\n controlEdge {\n node {\n id\n name\n description\n category\n state\n }\n }\n }\n}\n"
|
"text": "mutation CreateControlViewCreateControlMutation(\n $input: CreateControlInput!\n) {\n createControl(input: $input) {\n controlEdge {\n node {\n id\n name\n description\n category\n state\n }\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "4f8f339056a2f83c78ce3a642fa6ecc6";
|
(node as any).hash = "b9b19ec5600f99a2a8e75e0bd9f86355";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<86f7062f21c125e478c5a7a58b495d6c>>
|
* @generated SignedSource<<839284c6c29a5727d0ca521b1d804c5f>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -11,10 +11,10 @@
|
|||||||
import { ConcreteRequest } from 'relay-runtime';
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
export type ControlImportance = "ADVANCED" | "MANDATORY" | "PREFERRED";
|
export type ControlImportance = "ADVANCED" | "MANDATORY" | "PREFERRED";
|
||||||
export type ControlState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
|
export type ControlState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
|
||||||
export type UpdateControlPageQuery$variables = {
|
export type UpdateControlViewQuery$variables = {
|
||||||
controlId: string;
|
controlId: string;
|
||||||
};
|
};
|
||||||
export type UpdateControlPageQuery$data = {
|
export type UpdateControlViewQuery$data = {
|
||||||
readonly node: {
|
readonly node: {
|
||||||
readonly category?: string;
|
readonly category?: string;
|
||||||
readonly description?: string;
|
readonly description?: string;
|
||||||
@@ -25,9 +25,9 @@ export type UpdateControlPageQuery$data = {
|
|||||||
readonly version?: number;
|
readonly version?: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type UpdateControlPageQuery = {
|
export type UpdateControlViewQuery = {
|
||||||
response: UpdateControlPageQuery$data;
|
response: UpdateControlViewQuery$data;
|
||||||
variables: UpdateControlPageQuery$variables;
|
variables: UpdateControlViewQuery$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -99,7 +99,7 @@ return {
|
|||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "UpdateControlPageQuery",
|
"name": "UpdateControlViewQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -134,7 +134,7 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "UpdateControlPageQuery",
|
"name": "UpdateControlViewQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -171,16 +171,16 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "8b9ee499b6fc5726d6ce679205b59501",
|
"cacheID": "d578024453f1901119a772154524070d",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "UpdateControlPageQuery",
|
"name": "UpdateControlViewQuery",
|
||||||
"operationKind": "query",
|
"operationKind": "query",
|
||||||
"text": "query UpdateControlPageQuery(\n $controlId: ID!\n) {\n node(id: $controlId) {\n __typename\n ... on Control {\n id\n name\n description\n category\n importance\n state\n version\n }\n id\n }\n}\n"
|
"text": "query UpdateControlViewQuery(\n $controlId: ID!\n) {\n node(id: $controlId) {\n __typename\n ... on Control {\n id\n name\n description\n category\n importance\n state\n version\n }\n id\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "f3336cff42059e52e983e4cd9d6c2f1e";
|
(node as any).hash = "e6a997a27a8e45c01ad415e73b794cae";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<8eaa34f32a83e626da3331f2e0c6a7cf>>
|
* @generated SignedSource<<5223ef96f75ea5f5d4b124d190dfd455>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -20,10 +20,10 @@ export type UpdateControlInput = {
|
|||||||
name?: string | null | undefined;
|
name?: string | null | undefined;
|
||||||
state?: ControlState | null | undefined;
|
state?: ControlState | null | undefined;
|
||||||
};
|
};
|
||||||
export type UpdateControlPageUpdateControlMutation$variables = {
|
export type UpdateControlViewUpdateControlMutation$variables = {
|
||||||
input: UpdateControlInput;
|
input: UpdateControlInput;
|
||||||
};
|
};
|
||||||
export type UpdateControlPageUpdateControlMutation$data = {
|
export type UpdateControlViewUpdateControlMutation$data = {
|
||||||
readonly updateControl: {
|
readonly updateControl: {
|
||||||
readonly control: {
|
readonly control: {
|
||||||
readonly category: string;
|
readonly category: string;
|
||||||
@@ -36,9 +36,9 @@ export type UpdateControlPageUpdateControlMutation$data = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type UpdateControlPageUpdateControlMutation = {
|
export type UpdateControlViewUpdateControlMutation = {
|
||||||
response: UpdateControlPageUpdateControlMutation$data;
|
response: UpdateControlViewUpdateControlMutation$data;
|
||||||
variables: UpdateControlPageUpdateControlMutation$variables;
|
variables: UpdateControlViewUpdateControlMutation$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -133,7 +133,7 @@ return {
|
|||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "UpdateControlPageUpdateControlMutation",
|
"name": "UpdateControlViewUpdateControlMutation",
|
||||||
"selections": (v1/*: any*/),
|
"selections": (v1/*: any*/),
|
||||||
"type": "Mutation",
|
"type": "Mutation",
|
||||||
"abstractKey": null
|
"abstractKey": null
|
||||||
@@ -142,20 +142,20 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "UpdateControlPageUpdateControlMutation",
|
"name": "UpdateControlViewUpdateControlMutation",
|
||||||
"selections": (v1/*: any*/)
|
"selections": (v1/*: any*/)
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "3d05aa40f4dba0b24241235c54b85c4f",
|
"cacheID": "1702f16b6cc6452422c0f72acee25359",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "UpdateControlPageUpdateControlMutation",
|
"name": "UpdateControlViewUpdateControlMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation UpdateControlPageUpdateControlMutation(\n $input: UpdateControlInput!\n) {\n updateControl(input: $input) {\n control {\n id\n name\n description\n category\n importance\n state\n version\n }\n }\n}\n"
|
"text": "mutation UpdateControlViewUpdateControlMutation(\n $input: UpdateControlInput!\n) {\n updateControl(input: $input) {\n control {\n id\n name\n description\n category\n importance\n state\n version\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "0de0b5b9560213e95af60cc1732dee4c";
|
(node as any).hash = "e0f0940fe564406ef95473420b336128";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,301 +1,11 @@
|
|||||||
import { Suspense, useState } from "react";
|
import { PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
import { useNavigate, useParams } from "react-router";
|
import { lazy, Suspense } from "react";
|
||||||
import { graphql, useMutation, ConnectionHandler } from "react-relay";
|
import { useLocation } from "react-router";
|
||||||
import { Button } from "@/components/ui/button";
|
import { ErrorBoundaryWithLocation } from "../ErrorBoundary";
|
||||||
import { Card } from "@/components/ui/card";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { useToast } from "@/hooks/use-toast";
|
|
||||||
import { HelpCircle } from "lucide-react";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { CreatePeoplePageCreatePeopleMutation } from "./__generated__/CreatePeoplePageCreatePeopleMutation.graphql";
|
|
||||||
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
|
||||||
|
|
||||||
const createPeopleMutation = graphql`
|
const CreatePeopleView = lazy(() => import("./CreatePeopleView"));
|
||||||
mutation CreatePeoplePageCreatePeopleMutation(
|
|
||||||
$input: CreatePeopleInput!
|
|
||||||
$connections: [ID!]!
|
|
||||||
) {
|
|
||||||
createPeople(input: $input) {
|
|
||||||
peopleEdge @prependEdge(connections: $connections) {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
fullName
|
|
||||||
primaryEmailAddress
|
|
||||||
additionalEmailAddresses
|
|
||||||
kind
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
function EditableField({
|
export function CreatePeopleViewSkeleton() {
|
||||||
label,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
type = "text",
|
|
||||||
helpText,
|
|
||||||
required,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
type?: string;
|
|
||||||
helpText?: string;
|
|
||||||
required?: boolean;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<HelpCircle className="h-4 w-4 text-gray-400" />
|
|
||||||
<Label className="text-sm">{label}</Label>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Input
|
|
||||||
type={type}
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => onChange(e.target.value)}
|
|
||||||
required={required}
|
|
||||||
/>
|
|
||||||
{helpText && <p className="text-sm text-gray-500">{helpText}</p>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CreatePeoplePageContent() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { organizationId } = useParams();
|
|
||||||
|
|
||||||
const [createPeople] =
|
|
||||||
useMutation<CreatePeoplePageCreatePeopleMutation>(createPeopleMutation);
|
|
||||||
const { toast } = useToast();
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
fullName: "",
|
|
||||||
primaryEmailAddress: "",
|
|
||||||
additionalEmailAddresses: [] as string[],
|
|
||||||
kind: "EMPLOYEE" as "EMPLOYEE" | "CONTRACTOR" | "SERVICE_ACCOUNT",
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleFieldChange = (field: keyof typeof formData, value: unknown) => {
|
|
||||||
setFormData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[field]: value,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
createPeople({
|
|
||||||
variables: {
|
|
||||||
connections: [
|
|
||||||
ConnectionHandler.getConnectionID(
|
|
||||||
organizationId!,
|
|
||||||
"PeopleListPage_peoples",
|
|
||||||
{
|
|
||||||
orderBy: {
|
|
||||||
direction: "ASC",
|
|
||||||
field: "FULL_NAME",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
),
|
|
||||||
ConnectionHandler.getConnectionID(
|
|
||||||
organizationId!,
|
|
||||||
"PeopleSelector_organization_peoples",
|
|
||||||
{
|
|
||||||
orderBy: {
|
|
||||||
direction: "ASC",
|
|
||||||
field: "FULL_NAME",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
),
|
|
||||||
ConnectionHandler.getConnectionID(
|
|
||||||
organizationId!,
|
|
||||||
"ControlOverviewPage_peoples",
|
|
||||||
{
|
|
||||||
orderBy: {
|
|
||||||
direction: "ASC",
|
|
||||||
field: "FULL_NAME",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
),
|
|
||||||
],
|
|
||||||
input: {
|
|
||||||
organizationId: organizationId!,
|
|
||||||
fullName: formData.fullName,
|
|
||||||
primaryEmailAddress: formData.primaryEmailAddress,
|
|
||||||
additionalEmailAddresses: formData.additionalEmailAddresses,
|
|
||||||
kind: formData.kind,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
onCompleted: () => {
|
|
||||||
toast({
|
|
||||||
title: "Success",
|
|
||||||
description: "Person created successfully",
|
|
||||||
variant: "default",
|
|
||||||
});
|
|
||||||
navigate(`/organizations/${organizationId}/people`);
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: error.message || "Failed to create person",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageTemplate
|
|
||||||
title="Create Person"
|
|
||||||
description="Add a new person interacting with organization"
|
|
||||||
>
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<div className="max-w-2xl space-y-6">
|
|
||||||
<EditableField
|
|
||||||
label="Full Name"
|
|
||||||
value={formData.fullName}
|
|
||||||
onChange={(value) => handleFieldChange("fullName", value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditableField
|
|
||||||
label="Primary Email"
|
|
||||||
value={formData.primaryEmailAddress}
|
|
||||||
type="email"
|
|
||||||
onChange={(value) =>
|
|
||||||
handleFieldChange("primaryEmailAddress", value)
|
|
||||||
}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<HelpCircle className="h-4 w-4 text-gray-400" />
|
|
||||||
<Label className="text-sm">Additional Email Addresses</Label>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{formData.additionalEmailAddresses.map((email, index) => (
|
|
||||||
<div key={index} className="flex gap-2">
|
|
||||||
<Input
|
|
||||||
type="email"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => {
|
|
||||||
const newEmails = [...formData.additionalEmailAddresses];
|
|
||||||
newEmails[index] = e.target.value;
|
|
||||||
handleFieldChange("additionalEmailAddresses", newEmails);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
const newEmails =
|
|
||||||
formData.additionalEmailAddresses.filter(
|
|
||||||
(_, i) => i !== index
|
|
||||||
);
|
|
||||||
handleFieldChange("additionalEmailAddresses", newEmails);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
handleFieldChange("additionalEmailAddresses", [
|
|
||||||
...formData.additionalEmailAddresses,
|
|
||||||
"",
|
|
||||||
]);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Add Email
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card className="p-6">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h2 className="text-lg font-medium">Additional Information</h2>
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
Additional details about the person
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<HelpCircle className="h-4 w-4 text-gray-400" />
|
|
||||||
<Label className="text-sm">Kind</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleFieldChange("kind", "EMPLOYEE")}
|
|
||||||
className={cn(
|
|
||||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
|
||||||
formData.kind === "EMPLOYEE"
|
|
||||||
? "bg-blue-100 text-blue-900 ring-2 ring-blue-600 ring-offset-2"
|
|
||||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Employee
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleFieldChange("kind", "CONTRACTOR")}
|
|
||||||
className={cn(
|
|
||||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
|
||||||
formData.kind === "CONTRACTOR"
|
|
||||||
? "bg-purple-100 text-purple-900 ring-2 ring-purple-600 ring-offset-2"
|
|
||||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Contractor
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() =>
|
|
||||||
handleFieldChange("kind", "SERVICE_ACCOUNT")
|
|
||||||
}
|
|
||||||
className={cn(
|
|
||||||
"rounded-full px-4 py-1 text-sm transition-colors",
|
|
||||||
formData.kind === "SERVICE_ACCOUNT"
|
|
||||||
? "bg-green-100 text-green-900 ring-2 ring-green-600 ring-offset-2"
|
|
||||||
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Service Account
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
<div className="fixed bottom-6 right-6 flex gap-2">
|
|
||||||
<Button type="button" variant="outline" onClick={() => navigate(-1)}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
|
||||||
>
|
|
||||||
Create Person
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</PageTemplate>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CreatePeoplePageSkeleton() {
|
|
||||||
return (
|
return (
|
||||||
<PageTemplateSkeleton
|
<PageTemplateSkeleton
|
||||||
title="Create Person"
|
title="Create Person"
|
||||||
@@ -306,10 +16,14 @@ export function CreatePeoplePageSkeleton() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CreatePeoplePage() {
|
export function CreatePeoplePage() {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<CreatePeoplePageSkeleton />}>
|
<Suspense key={location.pathname} fallback={<CreatePeopleViewSkeleton />}>
|
||||||
<CreatePeoplePageContent />
|
<ErrorBoundaryWithLocation>
|
||||||
|
<CreatePeopleView />
|
||||||
|
</ErrorBoundaryWithLocation>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
305
apps/console/src/pages/organizations/people/CreatePeopleView.tsx
Normal file
305
apps/console/src/pages/organizations/people/CreatePeopleView.tsx
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
import { Suspense, useState } from "react";
|
||||||
|
import { useNavigate, useParams } from "react-router";
|
||||||
|
import { graphql, useMutation, ConnectionHandler } from "react-relay";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { HelpCircle } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { CreatePeopleViewCreatePeopleMutation } from "./__generated__/CreatePeopleViewCreatePeopleMutation.graphql";
|
||||||
|
import { PageTemplate } from "@/components/PageTemplate";
|
||||||
|
import { CreatePeopleViewSkeleton } from "./CreatePeoplePage";
|
||||||
|
|
||||||
|
const createPeopleMutation = graphql`
|
||||||
|
mutation CreatePeopleViewCreatePeopleMutation(
|
||||||
|
$input: CreatePeopleInput!
|
||||||
|
$connections: [ID!]!
|
||||||
|
) {
|
||||||
|
createPeople(input: $input) {
|
||||||
|
peopleEdge @prependEdge(connections: $connections) {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
fullName
|
||||||
|
primaryEmailAddress
|
||||||
|
additionalEmailAddresses
|
||||||
|
kind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function EditableField({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
type = "text",
|
||||||
|
helpText,
|
||||||
|
required,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
type?: string;
|
||||||
|
helpText?: string;
|
||||||
|
required?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<HelpCircle className="h-4 w-4 text-gray-400" />
|
||||||
|
<Label className="text-sm">{label}</Label>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Input
|
||||||
|
type={type}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
required={required}
|
||||||
|
/>
|
||||||
|
{helpText && <p className="text-sm text-gray-500">{helpText}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreatePeopleViewContent() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { organizationId } = useParams();
|
||||||
|
|
||||||
|
const [createPeople] =
|
||||||
|
useMutation<CreatePeopleViewCreatePeopleMutation>(createPeopleMutation);
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
fullName: "",
|
||||||
|
primaryEmailAddress: "",
|
||||||
|
additionalEmailAddresses: [] as string[],
|
||||||
|
kind: "EMPLOYEE" as "EMPLOYEE" | "CONTRACTOR" | "SERVICE_ACCOUNT",
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFieldChange = (field: keyof typeof formData, value: unknown) => {
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[field]: value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
createPeople({
|
||||||
|
variables: {
|
||||||
|
connections: [
|
||||||
|
ConnectionHandler.getConnectionID(
|
||||||
|
organizationId!,
|
||||||
|
"PeopleListPage_peoples",
|
||||||
|
{
|
||||||
|
orderBy: {
|
||||||
|
direction: "ASC",
|
||||||
|
field: "FULL_NAME",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
ConnectionHandler.getConnectionID(
|
||||||
|
organizationId!,
|
||||||
|
"PeopleSelector_organization_peoples",
|
||||||
|
{
|
||||||
|
orderBy: {
|
||||||
|
direction: "ASC",
|
||||||
|
field: "FULL_NAME",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
ConnectionHandler.getConnectionID(
|
||||||
|
organizationId!,
|
||||||
|
"ControlOverviewPage_peoples",
|
||||||
|
{
|
||||||
|
orderBy: {
|
||||||
|
direction: "ASC",
|
||||||
|
field: "FULL_NAME",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
],
|
||||||
|
input: {
|
||||||
|
organizationId: organizationId!,
|
||||||
|
fullName: formData.fullName,
|
||||||
|
primaryEmailAddress: formData.primaryEmailAddress,
|
||||||
|
additionalEmailAddresses: formData.additionalEmailAddresses,
|
||||||
|
kind: formData.kind,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onCompleted: () => {
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: "Person created successfully",
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
|
navigate(`/organizations/${organizationId}/people`);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: error.message || "Failed to create person",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageTemplate
|
||||||
|
title="Create Person"
|
||||||
|
description="Add a new person interacting with organization"
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="max-w-2xl space-y-6">
|
||||||
|
<EditableField
|
||||||
|
label="Full Name"
|
||||||
|
value={formData.fullName}
|
||||||
|
onChange={(value) => handleFieldChange("fullName", value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditableField
|
||||||
|
label="Primary Email"
|
||||||
|
value={formData.primaryEmailAddress}
|
||||||
|
type="email"
|
||||||
|
onChange={(value) =>
|
||||||
|
handleFieldChange("primaryEmailAddress", value)
|
||||||
|
}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<HelpCircle className="h-4 w-4 text-gray-400" />
|
||||||
|
<Label className="text-sm">Additional Email Addresses</Label>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{formData.additionalEmailAddresses.map((email, index) => (
|
||||||
|
<div key={index} className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newEmails = [...formData.additionalEmailAddresses];
|
||||||
|
newEmails[index] = e.target.value;
|
||||||
|
handleFieldChange("additionalEmailAddresses", newEmails);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
const newEmails =
|
||||||
|
formData.additionalEmailAddresses.filter(
|
||||||
|
(_, i) => i !== index
|
||||||
|
);
|
||||||
|
handleFieldChange("additionalEmailAddresses", newEmails);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
handleFieldChange("additionalEmailAddresses", [
|
||||||
|
...formData.additionalEmailAddresses,
|
||||||
|
"",
|
||||||
|
]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add Email
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h2 className="text-lg font-medium">Additional Information</h2>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Additional details about the person
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<HelpCircle className="h-4 w-4 text-gray-400" />
|
||||||
|
<Label className="text-sm">Kind</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleFieldChange("kind", "EMPLOYEE")}
|
||||||
|
className={cn(
|
||||||
|
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||||
|
formData.kind === "EMPLOYEE"
|
||||||
|
? "bg-blue-100 text-blue-900 ring-2 ring-blue-600 ring-offset-2"
|
||||||
|
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Employee
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleFieldChange("kind", "CONTRACTOR")}
|
||||||
|
className={cn(
|
||||||
|
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||||
|
formData.kind === "CONTRACTOR"
|
||||||
|
? "bg-purple-100 text-purple-900 ring-2 ring-purple-600 ring-offset-2"
|
||||||
|
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Contractor
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
handleFieldChange("kind", "SERVICE_ACCOUNT")
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
"rounded-full px-4 py-1 text-sm transition-colors",
|
||||||
|
formData.kind === "SERVICE_ACCOUNT"
|
||||||
|
? "bg-green-100 text-green-900 ring-2 ring-green-600 ring-offset-2"
|
||||||
|
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Service Account
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<div className="fixed bottom-6 right-6 flex gap-2">
|
||||||
|
<Button type="button" variant="outline" onClick={() => navigate(-1)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
||||||
|
>
|
||||||
|
Create Person
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</PageTemplate>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CreatePeopleView() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<CreatePeopleViewSkeleton />}>
|
||||||
|
<CreatePeopleViewContent />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,299 +1,11 @@
|
|||||||
import { Suspense, useEffect, useTransition } from "react";
|
import { PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
import {
|
import { lazy, Suspense } from "react";
|
||||||
graphql,
|
import { useLocation } from "react-router";
|
||||||
PreloadedQuery,
|
import { ErrorBoundaryWithLocation } from "../ErrorBoundary";
|
||||||
usePreloadedQuery,
|
|
||||||
useQueryLoader,
|
|
||||||
useMutation,
|
|
||||||
usePaginationFragment,
|
|
||||||
} from "react-relay";
|
|
||||||
import { useSearchParams, useParams } from "react-router";
|
|
||||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { UserPlus, Trash2, ChevronRight } from "lucide-react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Link } from "react-router";
|
|
||||||
import type { PeopleListPageQuery as PeopleListPageQueryType } from "./__generated__/PeopleListPageQuery.graphql";
|
|
||||||
import type { PeopleListPageDeletePeopleMutation } from "./__generated__/PeopleListPageDeletePeopleMutation.graphql";
|
|
||||||
import { PeopleListPagePaginationQuery } from "./__generated__/PeopleListPagePaginationQuery.graphql";
|
|
||||||
import { PeopleListPage_peoples$key } from "./__generated__/PeopleListPage_peoples.graphql";
|
|
||||||
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 25;
|
const PeopleListView = lazy(() => import("./PeopleListView"));
|
||||||
|
|
||||||
const peopleListPageQuery = graphql`
|
export function PeopleListViewSkeleton() {
|
||||||
query PeopleListPageQuery(
|
|
||||||
$organizationId: ID!
|
|
||||||
$first: Int
|
|
||||||
$after: CursorKey
|
|
||||||
$last: Int
|
|
||||||
$before: CursorKey
|
|
||||||
) {
|
|
||||||
organization: node(id: $organizationId) {
|
|
||||||
...PeopleListPage_peoples
|
|
||||||
@arguments(first: $first, after: $after, last: $last, before: $before)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const peopleListFragment = graphql`
|
|
||||||
fragment PeopleListPage_peoples on Organization
|
|
||||||
@refetchable(queryName: "PeopleListPagePaginationQuery")
|
|
||||||
@argumentDefinitions(
|
|
||||||
first: { type: "Int" }
|
|
||||||
after: { type: "CursorKey" }
|
|
||||||
last: { type: "Int" }
|
|
||||||
before: { type: "CursorKey" }
|
|
||||||
) {
|
|
||||||
id
|
|
||||||
peoples(
|
|
||||||
first: $first
|
|
||||||
after: $after
|
|
||||||
last: $last
|
|
||||||
before: $before
|
|
||||||
orderBy: { direction: ASC, field: FULL_NAME }
|
|
||||||
) @connection(key: "PeopleListPage_peoples") {
|
|
||||||
__id
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
fullName
|
|
||||||
primaryEmailAddress
|
|
||||||
additionalEmailAddresses
|
|
||||||
kind
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pageInfo {
|
|
||||||
hasNextPage
|
|
||||||
hasPreviousPage
|
|
||||||
startCursor
|
|
||||||
endCursor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const deletePeopleMutation = graphql`
|
|
||||||
mutation PeopleListPageDeletePeopleMutation(
|
|
||||||
$input: DeletePeopleInput!
|
|
||||||
$connections: [ID!]!
|
|
||||||
) {
|
|
||||||
deletePeople(input: $input) {
|
|
||||||
deletedPeopleId @deleteEdge(connections: $connections)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
function LoadAboveButton({
|
|
||||||
isLoading,
|
|
||||||
hasMore,
|
|
||||||
onLoadMore,
|
|
||||||
}: {
|
|
||||||
isLoading: boolean;
|
|
||||||
hasMore: boolean;
|
|
||||||
onLoadMore: () => void;
|
|
||||||
}) {
|
|
||||||
if (!hasMore) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={onLoadMore}
|
|
||||||
disabled={isLoading}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
{isLoading ? "Loading..." : "Load above"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function LoadBelowButton({
|
|
||||||
isLoading,
|
|
||||||
hasMore,
|
|
||||||
onLoadMore,
|
|
||||||
}: {
|
|
||||||
isLoading: boolean;
|
|
||||||
hasMore: boolean;
|
|
||||||
onLoadMore: () => void;
|
|
||||||
}) {
|
|
||||||
if (!hasMore) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={onLoadMore}
|
|
||||||
disabled={isLoading || !hasMore}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
{isLoading ? "Loading..." : "Load below"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function PeopleListContent({
|
|
||||||
queryRef,
|
|
||||||
}: {
|
|
||||||
queryRef: PreloadedQuery<PeopleListPageQueryType>;
|
|
||||||
}) {
|
|
||||||
const data = usePreloadedQuery<PeopleListPageQueryType>(
|
|
||||||
peopleListPageQuery,
|
|
||||||
queryRef
|
|
||||||
);
|
|
||||||
const [, setSearchParams] = useSearchParams();
|
|
||||||
const [, startTransition] = useTransition();
|
|
||||||
const [deletePeople] =
|
|
||||||
useMutation<PeopleListPageDeletePeopleMutation>(deletePeopleMutation);
|
|
||||||
const { organizationId } = useParams();
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: peoplesConnection,
|
|
||||||
loadNext,
|
|
||||||
loadPrevious,
|
|
||||||
hasNext,
|
|
||||||
hasPrevious,
|
|
||||||
isLoadingNext,
|
|
||||||
isLoadingPrevious,
|
|
||||||
} = usePaginationFragment<
|
|
||||||
PeopleListPagePaginationQuery,
|
|
||||||
PeopleListPage_peoples$key
|
|
||||||
>(peopleListFragment, data.organization);
|
|
||||||
|
|
||||||
const peoples =
|
|
||||||
peoplesConnection.peoples.edges.map((edge) => edge.node) ?? [];
|
|
||||||
const pageInfo = peoplesConnection.peoples.pageInfo;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageTemplate
|
|
||||||
title="People"
|
|
||||||
description="Keep track of your company's workforce and their progress
|
|
||||||
towards completing tasks assigned to them."
|
|
||||||
actions={
|
|
||||||
<Button
|
|
||||||
asChild
|
|
||||||
variant="outline"
|
|
||||||
style={{ borderRadius: "0.5rem" }}
|
|
||||||
className="gap-2"
|
|
||||||
>
|
|
||||||
<Link to={`/organizations/${organizationId}/people/create`}>
|
|
||||||
<UserPlus className="h-4 w-4" />
|
|
||||||
Add a person
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="space-y-2">
|
|
||||||
{peoples.map((person) => (
|
|
||||||
<Link
|
|
||||||
key={person?.id}
|
|
||||||
to={`/organizations/${organizationId}/people/${person?.id}`}
|
|
||||||
className="block"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between p-4 rounded-xl border bg-card hover:bg-accent/5 transition-colors">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Avatar className="h-8 w-8">
|
|
||||||
<AvatarFallback>{person?.fullName?.[0]}</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<p className="font-medium">{person?.fullName}</p>
|
|
||||||
{person?.primaryEmailAddress && (
|
|
||||||
<>
|
|
||||||
<span className="text-muted-foreground">•</span>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{person.primaryEmailAddress}
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Badge
|
|
||||||
variant="secondary"
|
|
||||||
className="bg-lime-9 text-white rounded-full px-3 py-0.5 text-xs font-medium"
|
|
||||||
>
|
|
||||||
{person?.kind === "EMPLOYEE"
|
|
||||||
? "Employee"
|
|
||||||
: person?.kind === "CONTRACTOR"
|
|
||||||
? "Contractor"
|
|
||||||
: person?.kind === "SERVICE_ACCOUNT"
|
|
||||||
? "Service Account"
|
|
||||||
: "Vendor"}
|
|
||||||
</Badge>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8 text-muted-foreground hover:bg-transparent hover:[&>svg]:text-destructive"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (
|
|
||||||
window.confirm(
|
|
||||||
"Are you sure you want to delete this person?"
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
deletePeople({
|
|
||||||
variables: {
|
|
||||||
connections: [peoplesConnection.peoples.__id],
|
|
||||||
input: {
|
|
||||||
peopleId: person.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4 transition-colors" />
|
|
||||||
</Button>
|
|
||||||
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<LoadAboveButton
|
|
||||||
isLoading={isLoadingPrevious}
|
|
||||||
hasMore={hasPrevious}
|
|
||||||
onLoadMore={() => {
|
|
||||||
startTransition(() => {
|
|
||||||
setSearchParams((prev) => {
|
|
||||||
prev.set("before", pageInfo?.startCursor || "");
|
|
||||||
prev.delete("after");
|
|
||||||
return prev;
|
|
||||||
});
|
|
||||||
loadPrevious(ITEMS_PER_PAGE);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<LoadBelowButton
|
|
||||||
isLoading={isLoadingNext}
|
|
||||||
hasMore={hasNext}
|
|
||||||
onLoadMore={() => {
|
|
||||||
startTransition(() => {
|
|
||||||
setSearchParams((prev) => {
|
|
||||||
prev.set("after", pageInfo?.endCursor || "");
|
|
||||||
prev.delete("before");
|
|
||||||
return prev;
|
|
||||||
});
|
|
||||||
loadNext(ITEMS_PER_PAGE);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</PageTemplate>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PeopleListPageSkeleton() {
|
|
||||||
return (
|
return (
|
||||||
<PageTemplateSkeleton
|
<PageTemplateSkeleton
|
||||||
title="People"
|
title="People"
|
||||||
@@ -318,33 +30,14 @@ export function PeopleListPageSkeleton() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PeopleListPage() {
|
export function PeopleListPage() {
|
||||||
const [searchParams] = useSearchParams();
|
const location = useLocation();
|
||||||
const [queryRef, loadQuery] =
|
|
||||||
useQueryLoader<PeopleListPageQueryType>(peopleListPageQuery);
|
|
||||||
|
|
||||||
const { organizationId } = useParams();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const after = searchParams.get("after");
|
|
||||||
const before = searchParams.get("before");
|
|
||||||
|
|
||||||
loadQuery({
|
|
||||||
organizationId: organizationId!,
|
|
||||||
first: before ? undefined : ITEMS_PER_PAGE,
|
|
||||||
after: after || undefined,
|
|
||||||
last: before ? ITEMS_PER_PAGE : undefined,
|
|
||||||
before: before || undefined,
|
|
||||||
});
|
|
||||||
}, [loadQuery, organizationId, searchParams]);
|
|
||||||
|
|
||||||
if (!queryRef) {
|
|
||||||
return <PeopleListPageSkeleton />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<PeopleListPageSkeleton />}>
|
<Suspense key={location.pathname} fallback={<PeopleListViewSkeleton />}>
|
||||||
<PeopleListContent queryRef={queryRef} />
|
<ErrorBoundaryWithLocation>
|
||||||
|
<PeopleListView />
|
||||||
|
</ErrorBoundaryWithLocation>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
326
apps/console/src/pages/organizations/people/PeopleListView.tsx
Normal file
326
apps/console/src/pages/organizations/people/PeopleListView.tsx
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
import { Suspense, useEffect, useTransition } from "react";
|
||||||
|
import {
|
||||||
|
graphql,
|
||||||
|
PreloadedQuery,
|
||||||
|
usePreloadedQuery,
|
||||||
|
useQueryLoader,
|
||||||
|
useMutation,
|
||||||
|
usePaginationFragment,
|
||||||
|
} from "react-relay";
|
||||||
|
import { useSearchParams, useParams } from "react-router";
|
||||||
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { UserPlus, Trash2, ChevronRight } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Link } from "react-router";
|
||||||
|
import type { PeopleListViewQuery as PeopleListViewQueryType } from "./__generated__/PeopleListViewQuery.graphql";
|
||||||
|
import type { PeopleListViewDeletePeopleMutation } from "./__generated__/PeopleListViewDeletePeopleMutation.graphql";
|
||||||
|
import { PeopleListViewPaginationQuery } from "./__generated__/PeopleListViewPaginationQuery.graphql";
|
||||||
|
import { PeopleListView_peoples$key } from "./__generated__/PeopleListView_peoples.graphql";
|
||||||
|
import { PageTemplate } from "@/components/PageTemplate";
|
||||||
|
import { PeopleListViewSkeleton } from "./PeopleListPage";
|
||||||
|
|
||||||
|
const ITEMS_PER_PAGE = 25;
|
||||||
|
|
||||||
|
const peopleListViewQuery = graphql`
|
||||||
|
query PeopleListViewQuery(
|
||||||
|
$organizationId: ID!
|
||||||
|
$first: Int
|
||||||
|
$after: CursorKey
|
||||||
|
$last: Int
|
||||||
|
$before: CursorKey
|
||||||
|
) {
|
||||||
|
organization: node(id: $organizationId) {
|
||||||
|
...PeopleListView_peoples
|
||||||
|
@arguments(first: $first, after: $after, last: $last, before: $before)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const peopleListFragment = graphql`
|
||||||
|
fragment PeopleListView_peoples on Organization
|
||||||
|
@refetchable(queryName: "PeopleListViewPaginationQuery")
|
||||||
|
@argumentDefinitions(
|
||||||
|
first: { type: "Int" }
|
||||||
|
after: { type: "CursorKey" }
|
||||||
|
last: { type: "Int" }
|
||||||
|
before: { type: "CursorKey" }
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
peoples(
|
||||||
|
first: $first
|
||||||
|
after: $after
|
||||||
|
last: $last
|
||||||
|
before: $before
|
||||||
|
orderBy: { direction: ASC, field: FULL_NAME }
|
||||||
|
) @connection(key: "PeopleListView_peoples") {
|
||||||
|
__id
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
fullName
|
||||||
|
primaryEmailAddress
|
||||||
|
additionalEmailAddresses
|
||||||
|
kind
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pageInfo {
|
||||||
|
hasNextPage
|
||||||
|
hasPreviousPage
|
||||||
|
startCursor
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const deletePeopleMutation = graphql`
|
||||||
|
mutation PeopleListViewDeletePeopleMutation(
|
||||||
|
$input: DeletePeopleInput!
|
||||||
|
$connections: [ID!]!
|
||||||
|
) {
|
||||||
|
deletePeople(input: $input) {
|
||||||
|
deletedPeopleId @deleteEdge(connections: $connections)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function LoadAboveButton({
|
||||||
|
isLoading,
|
||||||
|
hasMore,
|
||||||
|
onLoadMore,
|
||||||
|
}: {
|
||||||
|
isLoading: boolean;
|
||||||
|
hasMore: boolean;
|
||||||
|
onLoadMore: () => void;
|
||||||
|
}) {
|
||||||
|
if (!hasMore) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={onLoadMore}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{isLoading ? "Loading..." : "Load above"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LoadBelowButton({
|
||||||
|
isLoading,
|
||||||
|
hasMore,
|
||||||
|
onLoadMore,
|
||||||
|
}: {
|
||||||
|
isLoading: boolean;
|
||||||
|
hasMore: boolean;
|
||||||
|
onLoadMore: () => void;
|
||||||
|
}) {
|
||||||
|
if (!hasMore) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={onLoadMore}
|
||||||
|
disabled={isLoading || !hasMore}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{isLoading ? "Loading..." : "Load below"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PeopleListContent({
|
||||||
|
queryRef,
|
||||||
|
}: {
|
||||||
|
queryRef: PreloadedQuery<PeopleListViewQueryType>;
|
||||||
|
}) {
|
||||||
|
const data = usePreloadedQuery<PeopleListViewQueryType>(
|
||||||
|
peopleListViewQuery,
|
||||||
|
queryRef
|
||||||
|
);
|
||||||
|
const [, setSearchParams] = useSearchParams();
|
||||||
|
const [, startTransition] = useTransition();
|
||||||
|
const [deletePeople] =
|
||||||
|
useMutation<PeopleListViewDeletePeopleMutation>(deletePeopleMutation);
|
||||||
|
const { organizationId } = useParams();
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: peoplesConnection,
|
||||||
|
loadNext,
|
||||||
|
loadPrevious,
|
||||||
|
hasNext,
|
||||||
|
hasPrevious,
|
||||||
|
isLoadingNext,
|
||||||
|
isLoadingPrevious,
|
||||||
|
} = usePaginationFragment<
|
||||||
|
PeopleListViewPaginationQuery,
|
||||||
|
PeopleListView_peoples$key
|
||||||
|
>(peopleListFragment, data.organization);
|
||||||
|
|
||||||
|
const peoples =
|
||||||
|
peoplesConnection.peoples.edges.map((edge) => edge.node) ?? [];
|
||||||
|
const pageInfo = peoplesConnection.peoples.pageInfo;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageTemplate
|
||||||
|
title="People"
|
||||||
|
description="Keep track of your company's workforce and their progress
|
||||||
|
towards completing tasks assigned to them."
|
||||||
|
actions={
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
variant="outline"
|
||||||
|
style={{ borderRadius: "0.5rem" }}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<Link to={`/organizations/${organizationId}/people/create`}>
|
||||||
|
<UserPlus className="h-4 w-4" />
|
||||||
|
Add a person
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
{peoples.map((person) => (
|
||||||
|
<Link
|
||||||
|
key={person?.id}
|
||||||
|
to={`/organizations/${organizationId}/people/${person?.id}`}
|
||||||
|
className="block"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between p-4 rounded-xl border bg-card hover:bg-accent/5 transition-colors">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Avatar className="h-8 w-8">
|
||||||
|
<AvatarFallback>{person?.fullName?.[0]}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<p className="font-medium">{person?.fullName}</p>
|
||||||
|
{person?.primaryEmailAddress && (
|
||||||
|
<>
|
||||||
|
<span className="text-muted-foreground">•</span>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{person.primaryEmailAddress}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="bg-lime-9 text-white rounded-full px-3 py-0.5 text-xs font-medium"
|
||||||
|
>
|
||||||
|
{person?.kind === "EMPLOYEE"
|
||||||
|
? "Employee"
|
||||||
|
: person?.kind === "CONTRACTOR"
|
||||||
|
? "Contractor"
|
||||||
|
: person?.kind === "SERVICE_ACCOUNT"
|
||||||
|
? "Service Account"
|
||||||
|
: "Vendor"}
|
||||||
|
</Badge>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 text-muted-foreground hover:bg-transparent hover:[&>svg]:text-destructive"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (
|
||||||
|
window.confirm(
|
||||||
|
"Are you sure you want to delete this person?"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
deletePeople({
|
||||||
|
variables: {
|
||||||
|
connections: [peoplesConnection.peoples.__id],
|
||||||
|
input: {
|
||||||
|
peopleId: person.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 transition-colors" />
|
||||||
|
</Button>
|
||||||
|
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<LoadAboveButton
|
||||||
|
isLoading={isLoadingPrevious}
|
||||||
|
hasMore={hasPrevious}
|
||||||
|
onLoadMore={() => {
|
||||||
|
startTransition(() => {
|
||||||
|
setSearchParams((prev) => {
|
||||||
|
prev.set("before", pageInfo?.startCursor || "");
|
||||||
|
prev.delete("after");
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
loadPrevious(ITEMS_PER_PAGE);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<LoadBelowButton
|
||||||
|
isLoading={isLoadingNext}
|
||||||
|
hasMore={hasNext}
|
||||||
|
onLoadMore={() => {
|
||||||
|
startTransition(() => {
|
||||||
|
setSearchParams((prev) => {
|
||||||
|
prev.set("after", pageInfo?.endCursor || "");
|
||||||
|
prev.delete("before");
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
loadNext(ITEMS_PER_PAGE);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</PageTemplate>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PeopleListView() {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const [queryRef, loadQuery] =
|
||||||
|
useQueryLoader<PeopleListViewQueryType>(peopleListViewQuery);
|
||||||
|
|
||||||
|
const { organizationId } = useParams();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const after = searchParams.get("after");
|
||||||
|
const before = searchParams.get("before");
|
||||||
|
|
||||||
|
loadQuery({
|
||||||
|
organizationId: organizationId!,
|
||||||
|
first: before ? undefined : ITEMS_PER_PAGE,
|
||||||
|
after: after || undefined,
|
||||||
|
last: before ? ITEMS_PER_PAGE : undefined,
|
||||||
|
before: before || undefined,
|
||||||
|
});
|
||||||
|
}, [loadQuery, organizationId, searchParams]);
|
||||||
|
|
||||||
|
if (!queryRef) {
|
||||||
|
return <PeopleListViewSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<PeopleListViewSkeleton />}>
|
||||||
|
<PeopleListContent queryRef={queryRef} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
apps/console/src/pages/organizations/people/PeoplePage.tsx
Normal file
36
apps/console/src/pages/organizations/people/PeoplePage.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
|
import { lazy, Suspense } from "react";
|
||||||
|
import { useLocation } from "react-router";
|
||||||
|
import { ErrorBoundaryWithLocation } from "../ErrorBoundary";
|
||||||
|
|
||||||
|
const PeopleView = lazy(() => import("./PeopleView"));
|
||||||
|
|
||||||
|
export function PeopleViewSkeleton() {
|
||||||
|
return (
|
||||||
|
<PageTemplateSkeleton>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="h-8 w-48 bg-muted animate-pulse rounded" />
|
||||||
|
<div className="h-4 w-96 bg-muted animate-pulse rounded" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{[1, 2].map((i) => (
|
||||||
|
<div key={i} className="h-20 bg-muted animate-pulse rounded-lg" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageTemplateSkeleton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PeoplePage() {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense key={location.pathname} fallback={<PeopleViewSkeleton />}>
|
||||||
|
<ErrorBoundaryWithLocation>
|
||||||
|
<PeopleView />
|
||||||
|
</ErrorBoundaryWithLocation>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,13 +14,14 @@ import {
|
|||||||
useMutation,
|
useMutation,
|
||||||
} from "react-relay";
|
} from "react-relay";
|
||||||
import { Suspense, useEffect, useState, useCallback } from "react";
|
import { Suspense, useEffect, useState, useCallback } from "react";
|
||||||
import type { PeopleOverviewPageQuery as PeopleOverviewPageQueryType } from "./__generated__/PeopleOverviewPageQuery.graphql";
|
import type { PeopleViewQuery as PeopleViewQueryType } from "./__generated__/PeopleViewQuery.graphql";
|
||||||
import { useParams } from "react-router";
|
import { useParams } from "react-router";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
import { PageTemplate } from "@/components/PageTemplate";
|
||||||
|
import { PeopleViewSkeleton } from "./PeoplePage";
|
||||||
|
|
||||||
const peopleOverviewPageQuery = graphql`
|
const peopleViewQuery = graphql`
|
||||||
query PeopleOverviewPageQuery($peopleId: ID!) {
|
query PeopleViewQuery($peopleId: ID!) {
|
||||||
node(id: $peopleId) {
|
node(id: $peopleId) {
|
||||||
... on People {
|
... on People {
|
||||||
id
|
id
|
||||||
@@ -37,7 +38,7 @@ const peopleOverviewPageQuery = graphql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const updatePeopleMutation = graphql`
|
const updatePeopleMutation = graphql`
|
||||||
mutation PeopleOverviewPageUpdatePeopleMutation($input: UpdatePeopleInput!) {
|
mutation PeopleViewUpdatePeopleMutation($input: UpdatePeopleInput!) {
|
||||||
updatePeople(input: $input) {
|
updatePeople(input: $input) {
|
||||||
people {
|
people {
|
||||||
id
|
id
|
||||||
@@ -83,12 +84,12 @@ function EditableField({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PeopleOverviewPageContent({
|
function PeopleViewContent({
|
||||||
queryRef,
|
queryRef,
|
||||||
}: {
|
}: {
|
||||||
queryRef: PreloadedQuery<PeopleOverviewPageQueryType>;
|
queryRef: PreloadedQuery<PeopleViewQueryType>;
|
||||||
}) {
|
}) {
|
||||||
const data = usePreloadedQuery(peopleOverviewPageQuery, queryRef);
|
const data = usePreloadedQuery(peopleViewQuery, queryRef);
|
||||||
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 || "",
|
||||||
@@ -97,9 +98,7 @@ function PeopleOverviewPageContent({
|
|||||||
kind: data.node.kind,
|
kind: data.node.kind,
|
||||||
});
|
});
|
||||||
const [commit] = useMutation(updatePeopleMutation);
|
const [commit] = useMutation(updatePeopleMutation);
|
||||||
const [, loadQuery] = useQueryLoader<PeopleOverviewPageQueryType>(
|
const [, loadQuery] = useQueryLoader<PeopleViewQueryType>(peopleViewQuery);
|
||||||
peopleOverviewPageQuery
|
|
||||||
);
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const hasChanges = editedFields.size > 0;
|
const hasChanges = editedFields.size > 0;
|
||||||
@@ -299,41 +298,22 @@ function PeopleOverviewPageContent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PeopleOverviewPageSkeleton() {
|
export default function PeopleView() {
|
||||||
return (
|
|
||||||
<PageTemplateSkeleton>
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="h-8 w-48 bg-muted animate-pulse rounded" />
|
|
||||||
<div className="h-4 w-96 bg-muted animate-pulse rounded" />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{[1, 2].map((i) => (
|
|
||||||
<div key={i} className="h-20 bg-muted animate-pulse rounded-lg" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PageTemplateSkeleton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PeopleOverviewPage() {
|
|
||||||
const { peopleId } = useParams();
|
const { peopleId } = useParams();
|
||||||
const [queryRef, loadQuery] = useQueryLoader<PeopleOverviewPageQueryType>(
|
const [queryRef, loadQuery] =
|
||||||
peopleOverviewPageQuery
|
useQueryLoader<PeopleViewQueryType>(peopleViewQuery);
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadQuery({ peopleId: peopleId! });
|
loadQuery({ peopleId: peopleId! });
|
||||||
}, [loadQuery, peopleId]);
|
}, [loadQuery, peopleId]);
|
||||||
|
|
||||||
if (!queryRef) {
|
if (!queryRef) {
|
||||||
return <PeopleOverviewPageSkeleton />;
|
return <PeopleViewSkeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<PeopleOverviewPageSkeleton />}>
|
<Suspense fallback={<PeopleViewSkeleton />}>
|
||||||
<PeopleOverviewPageContent queryRef={queryRef} />
|
<PeopleViewContent queryRef={queryRef} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<6c8122e298c594879d1cd54bf297c11b>>
|
* @generated SignedSource<<eb07431152c49241a6e636984bbd5bee>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -17,11 +17,11 @@ export type CreatePeopleInput = {
|
|||||||
organizationId: string;
|
organizationId: string;
|
||||||
primaryEmailAddress: string;
|
primaryEmailAddress: string;
|
||||||
};
|
};
|
||||||
export type CreatePeoplePageCreatePeopleMutation$variables = {
|
export type CreatePeopleViewCreatePeopleMutation$variables = {
|
||||||
connections: ReadonlyArray<string>;
|
connections: ReadonlyArray<string>;
|
||||||
input: CreatePeopleInput;
|
input: CreatePeopleInput;
|
||||||
};
|
};
|
||||||
export type CreatePeoplePageCreatePeopleMutation$data = {
|
export type CreatePeopleViewCreatePeopleMutation$data = {
|
||||||
readonly createPeople: {
|
readonly createPeople: {
|
||||||
readonly peopleEdge: {
|
readonly peopleEdge: {
|
||||||
readonly node: {
|
readonly node: {
|
||||||
@@ -34,9 +34,9 @@ export type CreatePeoplePageCreatePeopleMutation$data = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type CreatePeoplePageCreatePeopleMutation = {
|
export type CreatePeopleViewCreatePeopleMutation = {
|
||||||
response: CreatePeoplePageCreatePeopleMutation$data;
|
response: CreatePeopleViewCreatePeopleMutation$data;
|
||||||
variables: CreatePeoplePageCreatePeopleMutation$variables;
|
variables: CreatePeopleViewCreatePeopleMutation$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -122,7 +122,7 @@ return {
|
|||||||
],
|
],
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "CreatePeoplePageCreatePeopleMutation",
|
"name": "CreatePeopleViewCreatePeopleMutation",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -147,7 +147,7 @@ return {
|
|||||||
(v0/*: any*/)
|
(v0/*: any*/)
|
||||||
],
|
],
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "CreatePeoplePageCreatePeopleMutation",
|
"name": "CreatePeopleViewCreatePeopleMutation",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -180,16 +180,16 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "3dc15f5a577685faa1c20d26a7f1d716",
|
"cacheID": "f09d8f7be5234f21e7bfabecf8d8b0af",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "CreatePeoplePageCreatePeopleMutation",
|
"name": "CreatePeopleViewCreatePeopleMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation CreatePeoplePageCreatePeopleMutation(\n $input: CreatePeopleInput!\n) {\n createPeople(input: $input) {\n peopleEdge {\n node {\n id\n fullName\n primaryEmailAddress\n additionalEmailAddresses\n kind\n }\n }\n }\n}\n"
|
"text": "mutation CreatePeopleViewCreatePeopleMutation(\n $input: CreatePeopleInput!\n) {\n createPeople(input: $input) {\n peopleEdge {\n node {\n id\n fullName\n primaryEmailAddress\n additionalEmailAddresses\n kind\n }\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "5ac3b2fbe80bf8a35d5595c6658a39cc";
|
(node as any).hash = "ac8a85af6d2902de6f475588679ba038";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<c4cdffc060744bb3853074ed3b123f61>>
|
* @generated SignedSource<<0602531d8b8a402e73b4823e6393da31>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -12,18 +12,18 @@ import { ConcreteRequest } from 'relay-runtime';
|
|||||||
export type DeletePeopleInput = {
|
export type DeletePeopleInput = {
|
||||||
peopleId: string;
|
peopleId: string;
|
||||||
};
|
};
|
||||||
export type PeopleListPageDeletePeopleMutation$variables = {
|
export type PeopleListViewDeletePeopleMutation$variables = {
|
||||||
connections: ReadonlyArray<string>;
|
connections: ReadonlyArray<string>;
|
||||||
input: DeletePeopleInput;
|
input: DeletePeopleInput;
|
||||||
};
|
};
|
||||||
export type PeopleListPageDeletePeopleMutation$data = {
|
export type PeopleListViewDeletePeopleMutation$data = {
|
||||||
readonly deletePeople: {
|
readonly deletePeople: {
|
||||||
readonly deletedPeopleId: string;
|
readonly deletedPeopleId: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type PeopleListPageDeletePeopleMutation = {
|
export type PeopleListViewDeletePeopleMutation = {
|
||||||
response: PeopleListPageDeletePeopleMutation$data;
|
response: PeopleListViewDeletePeopleMutation$data;
|
||||||
variables: PeopleListPageDeletePeopleMutation$variables;
|
variables: PeopleListViewDeletePeopleMutation$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -59,7 +59,7 @@ return {
|
|||||||
],
|
],
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "PeopleListPageDeletePeopleMutation",
|
"name": "PeopleListViewDeletePeopleMutation",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -84,7 +84,7 @@ return {
|
|||||||
(v0/*: any*/)
|
(v0/*: any*/)
|
||||||
],
|
],
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "PeopleListPageDeletePeopleMutation",
|
"name": "PeopleListViewDeletePeopleMutation",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -117,16 +117,16 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "977639987276708b1e4758508eaaf439",
|
"cacheID": "2b9ad65c15101fb4e421c78ca4fd871c",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "PeopleListPageDeletePeopleMutation",
|
"name": "PeopleListViewDeletePeopleMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation PeopleListPageDeletePeopleMutation(\n $input: DeletePeopleInput!\n) {\n deletePeople(input: $input) {\n deletedPeopleId\n }\n}\n"
|
"text": "mutation PeopleListViewDeletePeopleMutation(\n $input: DeletePeopleInput!\n) {\n deletePeople(input: $input) {\n deletedPeopleId\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "6552abc6c02cdc2c8e84ebbce1eb51f1";
|
(node as any).hash = "cc33cd4710c915cec66c217c010d728b";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<7976ca2ca06bb48d6dd6a30b1182ed02>>
|
* @generated SignedSource<<6aff6203f26803074cd1ea539096cbed>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -10,21 +10,21 @@
|
|||||||
|
|
||||||
import { ConcreteRequest } from 'relay-runtime';
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
import { FragmentRefs } from "relay-runtime";
|
import { FragmentRefs } from "relay-runtime";
|
||||||
export type PeopleListPagePaginationQuery$variables = {
|
export type PeopleListViewPaginationQuery$variables = {
|
||||||
after?: string | null | undefined;
|
after?: string | null | undefined;
|
||||||
before?: string | null | undefined;
|
before?: string | null | undefined;
|
||||||
first?: number | null | undefined;
|
first?: number | null | undefined;
|
||||||
id: string;
|
id: string;
|
||||||
last?: number | null | undefined;
|
last?: number | null | undefined;
|
||||||
};
|
};
|
||||||
export type PeopleListPagePaginationQuery$data = {
|
export type PeopleListViewPaginationQuery$data = {
|
||||||
readonly node: {
|
readonly node: {
|
||||||
readonly " $fragmentSpreads": FragmentRefs<"PeopleListPage_peoples">;
|
readonly " $fragmentSpreads": FragmentRefs<"PeopleListView_peoples">;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type PeopleListPagePaginationQuery = {
|
export type PeopleListViewPaginationQuery = {
|
||||||
response: PeopleListPagePaginationQuery$data;
|
response: PeopleListViewPaginationQuery$data;
|
||||||
variables: PeopleListPagePaginationQuery$variables;
|
variables: PeopleListViewPaginationQuery$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -119,7 +119,7 @@ return {
|
|||||||
],
|
],
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "PeopleListPagePaginationQuery",
|
"name": "PeopleListViewPaginationQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -137,7 +137,7 @@ return {
|
|||||||
(v9/*: any*/)
|
(v9/*: any*/)
|
||||||
],
|
],
|
||||||
"kind": "FragmentSpread",
|
"kind": "FragmentSpread",
|
||||||
"name": "PeopleListPage_peoples"
|
"name": "PeopleListView_peoples"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
@@ -156,7 +156,7 @@ return {
|
|||||||
(v3/*: any*/)
|
(v3/*: any*/)
|
||||||
],
|
],
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "PeopleListPagePaginationQuery",
|
"name": "PeopleListViewPaginationQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -313,7 +313,7 @@ return {
|
|||||||
"orderBy"
|
"orderBy"
|
||||||
],
|
],
|
||||||
"handle": "connection",
|
"handle": "connection",
|
||||||
"key": "PeopleListPage_peoples",
|
"key": "PeopleListView_peoples",
|
||||||
"kind": "LinkedHandle",
|
"kind": "LinkedHandle",
|
||||||
"name": "peoples"
|
"name": "peoples"
|
||||||
}
|
}
|
||||||
@@ -327,16 +327,16 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "8a0df2b5a4b762e4b549f018a28add57",
|
"cacheID": "0bd9bfd41ba132557259776c016df0b9",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "PeopleListPagePaginationQuery",
|
"name": "PeopleListViewPaginationQuery",
|
||||||
"operationKind": "query",
|
"operationKind": "query",
|
||||||
"text": "query PeopleListPagePaginationQuery(\n $after: CursorKey\n $before: CursorKey\n $first: Int\n $last: Int\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...PeopleListPage_peoples_pbnwq\n id\n }\n}\n\nfragment PeopleListPage_peoples_pbnwq on Organization {\n id\n peoples(first: $first, after: $after, last: $last, before: $before, orderBy: {direction: ASC, field: FULL_NAME}) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n additionalEmailAddresses\n kind\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n}\n"
|
"text": "query PeopleListViewPaginationQuery(\n $after: CursorKey\n $before: CursorKey\n $first: Int\n $last: Int\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...PeopleListView_peoples_pbnwq\n id\n }\n}\n\nfragment PeopleListView_peoples_pbnwq on Organization {\n id\n peoples(first: $first, after: $after, last: $last, before: $before, orderBy: {direction: ASC, field: FULL_NAME}) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n additionalEmailAddresses\n kind\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "8346075f50aa8845dded3a6bfa292c2b";
|
(node as any).hash = "d30fc5e157c1696a257cd337b6f08e1c";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<0e769620c683a087fc071d47649f3ed3>>
|
* @generated SignedSource<<7d6cb5fe1c907da0abcadd390ca2b492>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -10,21 +10,21 @@
|
|||||||
|
|
||||||
import { ConcreteRequest } from 'relay-runtime';
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
import { FragmentRefs } from "relay-runtime";
|
import { FragmentRefs } from "relay-runtime";
|
||||||
export type PeopleListPageQuery$variables = {
|
export type PeopleListViewQuery$variables = {
|
||||||
after?: string | null | undefined;
|
after?: string | null | undefined;
|
||||||
before?: string | null | undefined;
|
before?: string | null | undefined;
|
||||||
first?: number | null | undefined;
|
first?: number | null | undefined;
|
||||||
last?: number | null | undefined;
|
last?: number | null | undefined;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
};
|
};
|
||||||
export type PeopleListPageQuery$data = {
|
export type PeopleListViewQuery$data = {
|
||||||
readonly organization: {
|
readonly organization: {
|
||||||
readonly " $fragmentSpreads": FragmentRefs<"PeopleListPage_peoples">;
|
readonly " $fragmentSpreads": FragmentRefs<"PeopleListView_peoples">;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type PeopleListPageQuery = {
|
export type PeopleListViewQuery = {
|
||||||
response: PeopleListPageQuery$data;
|
response: PeopleListViewQuery$data;
|
||||||
variables: PeopleListPageQuery$variables;
|
variables: PeopleListViewQuery$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -119,7 +119,7 @@ return {
|
|||||||
],
|
],
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "PeopleListPageQuery",
|
"name": "PeopleListViewQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": "organization",
|
"alias": "organization",
|
||||||
@@ -137,7 +137,7 @@ return {
|
|||||||
(v9/*: any*/)
|
(v9/*: any*/)
|
||||||
],
|
],
|
||||||
"kind": "FragmentSpread",
|
"kind": "FragmentSpread",
|
||||||
"name": "PeopleListPage_peoples"
|
"name": "PeopleListView_peoples"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
@@ -156,7 +156,7 @@ return {
|
|||||||
(v1/*: any*/)
|
(v1/*: any*/)
|
||||||
],
|
],
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "PeopleListPageQuery",
|
"name": "PeopleListViewQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": "organization",
|
"alias": "organization",
|
||||||
@@ -313,7 +313,7 @@ return {
|
|||||||
"orderBy"
|
"orderBy"
|
||||||
],
|
],
|
||||||
"handle": "connection",
|
"handle": "connection",
|
||||||
"key": "PeopleListPage_peoples",
|
"key": "PeopleListView_peoples",
|
||||||
"kind": "LinkedHandle",
|
"kind": "LinkedHandle",
|
||||||
"name": "peoples"
|
"name": "peoples"
|
||||||
}
|
}
|
||||||
@@ -327,16 +327,16 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "b289901944c533975a18564d5af6cd02",
|
"cacheID": "004c9018c6fc0e77855a01138ccc00d9",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "PeopleListPageQuery",
|
"name": "PeopleListViewQuery",
|
||||||
"operationKind": "query",
|
"operationKind": "query",
|
||||||
"text": "query PeopleListPageQuery(\n $organizationId: ID!\n $first: Int\n $after: CursorKey\n $last: Int\n $before: CursorKey\n) {\n organization: node(id: $organizationId) {\n __typename\n ...PeopleListPage_peoples_pbnwq\n id\n }\n}\n\nfragment PeopleListPage_peoples_pbnwq on Organization {\n id\n peoples(first: $first, after: $after, last: $last, before: $before, orderBy: {direction: ASC, field: FULL_NAME}) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n additionalEmailAddresses\n kind\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n}\n"
|
"text": "query PeopleListViewQuery(\n $organizationId: ID!\n $first: Int\n $after: CursorKey\n $last: Int\n $before: CursorKey\n) {\n organization: node(id: $organizationId) {\n __typename\n ...PeopleListView_peoples_pbnwq\n id\n }\n}\n\nfragment PeopleListView_peoples_pbnwq on Organization {\n id\n peoples(first: $first, after: $after, last: $last, before: $before, orderBy: {direction: ASC, field: FULL_NAME}) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n additionalEmailAddresses\n kind\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "0a1918698d276906f5e17baa40f756e3";
|
(node as any).hash = "560fc78e36c19c5b05a3e67a5e542e3b";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<c7666035d738d537e2695a319df441ee>>
|
* @generated SignedSource<<da4ebef4106cce1470c644c4b8b703a6>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
import { ReaderFragment } from 'relay-runtime';
|
import { ReaderFragment } from 'relay-runtime';
|
||||||
export type PeopleKind = "CONTRACTOR" | "EMPLOYEE" | "SERVICE_ACCOUNT";
|
export type PeopleKind = "CONTRACTOR" | "EMPLOYEE" | "SERVICE_ACCOUNT";
|
||||||
import { FragmentRefs } from "relay-runtime";
|
import { FragmentRefs } from "relay-runtime";
|
||||||
export type PeopleListPage_peoples$data = {
|
export type PeopleListView_peoples$data = {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly peoples: {
|
readonly peoples: {
|
||||||
readonly __id: string;
|
readonly __id: string;
|
||||||
@@ -33,11 +33,11 @@ export type PeopleListPage_peoples$data = {
|
|||||||
readonly startCursor: string | null | undefined;
|
readonly startCursor: string | null | undefined;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
readonly " $fragmentType": "PeopleListPage_peoples";
|
readonly " $fragmentType": "PeopleListView_peoples";
|
||||||
};
|
};
|
||||||
export type PeopleListPage_peoples$key = {
|
export type PeopleListView_peoples$key = {
|
||||||
readonly " $data"?: PeopleListPage_peoples$data;
|
readonly " $data"?: PeopleListView_peoples$data;
|
||||||
readonly " $fragmentSpreads": FragmentRefs<"PeopleListPage_peoples">;
|
readonly " $fragmentSpreads": FragmentRefs<"PeopleListView_peoples">;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ReaderFragment = (function(){
|
const node: ReaderFragment = (function(){
|
||||||
@@ -99,14 +99,14 @@ return {
|
|||||||
"fragmentPathInResult": [
|
"fragmentPathInResult": [
|
||||||
"node"
|
"node"
|
||||||
],
|
],
|
||||||
"operation": require('./PeopleListPagePaginationQuery.graphql'),
|
"operation": require('./PeopleListViewPaginationQuery.graphql'),
|
||||||
"identifierInfo": {
|
"identifierInfo": {
|
||||||
"identifierField": "id",
|
"identifierField": "id",
|
||||||
"identifierQueryVariableName": "id"
|
"identifierQueryVariableName": "id"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "PeopleListPage_peoples",
|
"name": "PeopleListView_peoples",
|
||||||
"selections": [
|
"selections": [
|
||||||
(v1/*: any*/),
|
(v1/*: any*/),
|
||||||
{
|
{
|
||||||
@@ -123,7 +123,7 @@ return {
|
|||||||
],
|
],
|
||||||
"concreteType": "PeopleConnection",
|
"concreteType": "PeopleConnection",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "__PeopleListPage_peoples_connection",
|
"name": "__PeopleListView_peoples_connection",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
@@ -257,7 +257,7 @@ return {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"storageKey": "__PeopleListPage_peoples_connection(orderBy:{\"direction\":\"ASC\",\"field\":\"FULL_NAME\"})"
|
"storageKey": "__PeopleListView_peoples_connection(orderBy:{\"direction\":\"ASC\",\"field\":\"FULL_NAME\"})"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"type": "Organization",
|
"type": "Organization",
|
||||||
@@ -265,6 +265,6 @@ return {
|
|||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "8346075f50aa8845dded3a6bfa292c2b";
|
(node as any).hash = "d30fc5e157c1696a257cd337b6f08e1c";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<4ff94d3e8f2e4c6c0d8c3ad43abee2d6>>
|
* @generated SignedSource<<207a779cf8cbf3e42191be9825d4b653>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -10,10 +10,10 @@
|
|||||||
|
|
||||||
import { ConcreteRequest } from 'relay-runtime';
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
export type PeopleKind = "CONTRACTOR" | "EMPLOYEE" | "SERVICE_ACCOUNT";
|
export type PeopleKind = "CONTRACTOR" | "EMPLOYEE" | "SERVICE_ACCOUNT";
|
||||||
export type PeopleOverviewPageQuery$variables = {
|
export type PeopleViewQuery$variables = {
|
||||||
peopleId: string;
|
peopleId: string;
|
||||||
};
|
};
|
||||||
export type PeopleOverviewPageQuery$data = {
|
export type PeopleViewQuery$data = {
|
||||||
readonly node: {
|
readonly node: {
|
||||||
readonly additionalEmailAddresses?: ReadonlyArray<string>;
|
readonly additionalEmailAddresses?: ReadonlyArray<string>;
|
||||||
readonly createdAt?: string;
|
readonly createdAt?: string;
|
||||||
@@ -25,9 +25,9 @@ export type PeopleOverviewPageQuery$data = {
|
|||||||
readonly version?: number;
|
readonly version?: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type PeopleOverviewPageQuery = {
|
export type PeopleViewQuery = {
|
||||||
response: PeopleOverviewPageQuery$data;
|
response: PeopleViewQuery$data;
|
||||||
variables: PeopleOverviewPageQuery$variables;
|
variables: PeopleViewQuery$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -106,7 +106,7 @@ return {
|
|||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "PeopleOverviewPageQuery",
|
"name": "PeopleViewQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -142,7 +142,7 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "PeopleOverviewPageQuery",
|
"name": "PeopleViewQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -180,16 +180,16 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "1383ca6e8c8b205082cc68223bec1833",
|
"cacheID": "c392876240212ba16428ae5edb843d47",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "PeopleOverviewPageQuery",
|
"name": "PeopleViewQuery",
|
||||||
"operationKind": "query",
|
"operationKind": "query",
|
||||||
"text": "query PeopleOverviewPageQuery(\n $peopleId: ID!\n) {\n node(id: $peopleId) {\n __typename\n ... on People {\n id\n fullName\n primaryEmailAddress\n additionalEmailAddresses\n kind\n createdAt\n updatedAt\n version\n }\n id\n }\n}\n"
|
"text": "query PeopleViewQuery(\n $peopleId: ID!\n) {\n node(id: $peopleId) {\n __typename\n ... on People {\n id\n fullName\n primaryEmailAddress\n additionalEmailAddresses\n kind\n createdAt\n updatedAt\n version\n }\n id\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "fa229186ad9e51f3a60b23fa88a882aa";
|
(node as any).hash = "4fc97d6cd7fc4590b7be7b5a79ae7ab0";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<4da12585c00c981fc6f2a6f74d5d3ef6>>
|
* @generated SignedSource<<c1c3f4fa54eb6e43c52b088d2b872fee>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -18,10 +18,10 @@ export type UpdatePeopleInput = {
|
|||||||
kind?: PeopleKind | null | undefined;
|
kind?: PeopleKind | null | undefined;
|
||||||
primaryEmailAddress?: string | null | undefined;
|
primaryEmailAddress?: string | null | undefined;
|
||||||
};
|
};
|
||||||
export type PeopleOverviewPageUpdatePeopleMutation$variables = {
|
export type PeopleViewUpdatePeopleMutation$variables = {
|
||||||
input: UpdatePeopleInput;
|
input: UpdatePeopleInput;
|
||||||
};
|
};
|
||||||
export type PeopleOverviewPageUpdatePeopleMutation$data = {
|
export type PeopleViewUpdatePeopleMutation$data = {
|
||||||
readonly updatePeople: {
|
readonly updatePeople: {
|
||||||
readonly people: {
|
readonly people: {
|
||||||
readonly additionalEmailAddresses: ReadonlyArray<string>;
|
readonly additionalEmailAddresses: ReadonlyArray<string>;
|
||||||
@@ -34,9 +34,9 @@ export type PeopleOverviewPageUpdatePeopleMutation$data = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type PeopleOverviewPageUpdatePeopleMutation = {
|
export type PeopleViewUpdatePeopleMutation = {
|
||||||
response: PeopleOverviewPageUpdatePeopleMutation$data;
|
response: PeopleViewUpdatePeopleMutation$data;
|
||||||
variables: PeopleOverviewPageUpdatePeopleMutation$variables;
|
variables: PeopleViewUpdatePeopleMutation$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -131,7 +131,7 @@ return {
|
|||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "PeopleOverviewPageUpdatePeopleMutation",
|
"name": "PeopleViewUpdatePeopleMutation",
|
||||||
"selections": (v1/*: any*/),
|
"selections": (v1/*: any*/),
|
||||||
"type": "Mutation",
|
"type": "Mutation",
|
||||||
"abstractKey": null
|
"abstractKey": null
|
||||||
@@ -140,20 +140,20 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "PeopleOverviewPageUpdatePeopleMutation",
|
"name": "PeopleViewUpdatePeopleMutation",
|
||||||
"selections": (v1/*: any*/)
|
"selections": (v1/*: any*/)
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "6961fac292b72111b2d0a46f078e3efc",
|
"cacheID": "96bba526a231dd76ef58d32ec01aac3a",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "PeopleOverviewPageUpdatePeopleMutation",
|
"name": "PeopleViewUpdatePeopleMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation PeopleOverviewPageUpdatePeopleMutation(\n $input: UpdatePeopleInput!\n) {\n updatePeople(input: $input) {\n people {\n id\n fullName\n primaryEmailAddress\n additionalEmailAddresses\n kind\n updatedAt\n version\n }\n }\n}\n"
|
"text": "mutation PeopleViewUpdatePeopleMutation(\n $input: UpdatePeopleInput!\n) {\n updatePeople(input: $input) {\n people {\n id\n fullName\n primaryEmailAddress\n additionalEmailAddresses\n kind\n updatedAt\n version\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "01a8fefe43cdc344f9a4dd62a60edc6c";
|
(node as any).hash = "957952927fbe2337a180599f34ce961c";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,266 +1,11 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
import { useNavigate, useParams } from "react-router";
|
import { lazy, Suspense } from "react";
|
||||||
import {
|
import { useLocation } from "react-router";
|
||||||
ConnectionHandler,
|
import { ErrorBoundaryWithLocation } from "../ErrorBoundary";
|
||||||
graphql,
|
|
||||||
useMutation,
|
|
||||||
useQueryLoader,
|
|
||||||
usePreloadedQuery,
|
|
||||||
PreloadedQuery,
|
|
||||||
} from "react-relay";
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { useToast } from "@/hooks/use-toast";
|
|
||||||
import { Calendar, User } from "lucide-react";
|
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|
||||||
import PolicyEditor from "@/components/PolicyEditor";
|
|
||||||
import PeopleSelector from "@/components/PeopleSelector";
|
|
||||||
import { Suspense } from "react";
|
|
||||||
import type { CreatePolicyPageMutation } from "./__generated__/CreatePolicyPageMutation.graphql";
|
|
||||||
import type { CreatePolicyPageQuery as CreatePolicyPageQueryType } from "./__generated__/CreatePolicyPageQuery.graphql";
|
|
||||||
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
|
||||||
|
|
||||||
const CreatePolicyQuery = graphql`
|
const CreatePolicyView = lazy(() => import("./CreatePolicyView"));
|
||||||
query CreatePolicyPageQuery($organizationId: ID!) {
|
|
||||||
organization: node(id: $organizationId) {
|
|
||||||
...PeopleSelector_organization
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const CreatePolicyMutation = graphql`
|
export function CreatePolicyViewSkeleton() {
|
||||||
mutation CreatePolicyPageMutation(
|
|
||||||
$input: CreatePolicyInput!
|
|
||||||
$connections: [ID!]!
|
|
||||||
) {
|
|
||||||
createPolicy(input: $input) {
|
|
||||||
policyEdge @prependEdge(connections: $connections) {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
content
|
|
||||||
status
|
|
||||||
reviewDate
|
|
||||||
owner {
|
|
||||||
id
|
|
||||||
fullName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
function CreatePolicyForm({
|
|
||||||
queryRef,
|
|
||||||
}: {
|
|
||||||
queryRef: PreloadedQuery<CreatePolicyPageQueryType>;
|
|
||||||
}) {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { organizationId } = useParams();
|
|
||||||
const data = usePreloadedQuery<CreatePolicyPageQueryType>(
|
|
||||||
CreatePolicyQuery,
|
|
||||||
queryRef
|
|
||||||
);
|
|
||||||
const [name, setName] = useState("");
|
|
||||||
const [content, setContent] = useState("");
|
|
||||||
const [status, setStatus] = useState<"DRAFT" | "ACTIVE">("DRAFT");
|
|
||||||
const [reviewDate, setReviewDate] = useState("");
|
|
||||||
const [ownerId, setOwnerId] = useState<string | null>(null);
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
"CreatePolicyPage state - content:",
|
|
||||||
content
|
|
||||||
? content.substring(0, 50) + (content.length > 50 ? "..." : "")
|
|
||||||
: "empty"
|
|
||||||
);
|
|
||||||
|
|
||||||
const [commitMutation] =
|
|
||||||
useMutation<CreatePolicyPageMutation>(CreatePolicyMutation);
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!ownerId) {
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: "Please select an owner for the policy.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSubmitting(true);
|
|
||||||
|
|
||||||
// Convert reviewDate string to ISO format for the API
|
|
||||||
let reviewDateValue = null;
|
|
||||||
if (reviewDate) {
|
|
||||||
reviewDateValue = new Date(reviewDate).toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
const input = {
|
|
||||||
organizationId: organizationId!,
|
|
||||||
name,
|
|
||||||
content,
|
|
||||||
status,
|
|
||||||
reviewDate: reviewDateValue,
|
|
||||||
ownerId,
|
|
||||||
};
|
|
||||||
|
|
||||||
commitMutation({
|
|
||||||
variables: {
|
|
||||||
input,
|
|
||||||
connections: [
|
|
||||||
ConnectionHandler.getConnectionID(
|
|
||||||
organizationId!,
|
|
||||||
"PolicyListPage_policies"
|
|
||||||
),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
onCompleted: (response, errors) => {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
if (errors) {
|
|
||||||
console.error("Error creating policy:", errors);
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: "Failed to create policy. Please try again.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: "Success",
|
|
||||||
description: "Policy created successfully!",
|
|
||||||
});
|
|
||||||
|
|
||||||
navigate(
|
|
||||||
`/organizations/${organizationId}/policies/${response.createPolicy.policyEdge.node.id}`
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
console.error("Error creating policy:", error);
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: "Failed to create policy. Please try again.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageTemplate
|
|
||||||
title="Create Policy"
|
|
||||||
description="Create a new policy for your organization"
|
|
||||||
>
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<div className="grid gap-6">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Policy Information</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="name">Policy Name</Label>
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
placeholder="Enter policy name"
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="content">Policy Content</Label>
|
|
||||||
<div className="min-h-[300px]">
|
|
||||||
<PolicyEditor
|
|
||||||
initialContent={content}
|
|
||||||
onChange={(html) => setContent(html)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Status</Label>
|
|
||||||
<RadioGroup
|
|
||||||
value={status}
|
|
||||||
onValueChange={(value: "DRAFT" | "ACTIVE") =>
|
|
||||||
setStatus(value)
|
|
||||||
}
|
|
||||||
className="flex space-x-4"
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<RadioGroupItem value="DRAFT" id="draft" />
|
|
||||||
<Label htmlFor="draft" className="cursor-pointer">
|
|
||||||
Draft
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<RadioGroupItem value="ACTIVE" id="active" />
|
|
||||||
<Label htmlFor="active" className="cursor-pointer">
|
|
||||||
Active
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</RadioGroup>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="owner" className="flex items-center gap-2">
|
|
||||||
<User className="h-4 w-4" />
|
|
||||||
Policy Owner
|
|
||||||
</Label>
|
|
||||||
<PeopleSelector
|
|
||||||
organizationRef={data.organization}
|
|
||||||
selectedPersonId={ownerId}
|
|
||||||
onSelect={setOwnerId}
|
|
||||||
placeholder="Select policy owner"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="reviewDate" className="flex items-center gap-2">
|
|
||||||
<Calendar className="h-4 w-4" />
|
|
||||||
Review Date
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="reviewDate"
|
|
||||||
type="date"
|
|
||||||
value={reviewDate}
|
|
||||||
onChange={(e) => setReviewDate(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-4">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() =>
|
|
||||||
navigate(`/organizations/${organizationId}/policies`)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={isSubmitting}>
|
|
||||||
{isSubmitting ? "Creating..." : "Create Policy"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</PageTemplate>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CreatePolicyPageSkeleton() {
|
|
||||||
return (
|
return (
|
||||||
<PageTemplateSkeleton
|
<PageTemplateSkeleton
|
||||||
title="Create Policy"
|
title="Create Policy"
|
||||||
@@ -271,24 +16,14 @@ export function CreatePolicyPageSkeleton() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CreatePolicyPage() {
|
export function CreatePolicyPage() {
|
||||||
const { organizationId } = useParams();
|
const location = useLocation();
|
||||||
const [queryRef, loadQuery] =
|
|
||||||
useQueryLoader<CreatePolicyPageQueryType>(CreatePolicyQuery);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (organizationId) {
|
|
||||||
loadQuery({ organizationId });
|
|
||||||
}
|
|
||||||
}, [organizationId, loadQuery]);
|
|
||||||
|
|
||||||
if (!queryRef) {
|
|
||||||
return <CreatePolicyPageSkeleton />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<CreatePolicyPageSkeleton />}>
|
<Suspense key={location.pathname} fallback={<CreatePolicyViewSkeleton />}>
|
||||||
{<CreatePolicyForm queryRef={queryRef} />}
|
<ErrorBoundaryWithLocation>
|
||||||
|
<CreatePolicyView />
|
||||||
|
</ErrorBoundaryWithLocation>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,284 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useNavigate, useParams } from "react-router";
|
||||||
|
import {
|
||||||
|
ConnectionHandler,
|
||||||
|
graphql,
|
||||||
|
useMutation,
|
||||||
|
useQueryLoader,
|
||||||
|
usePreloadedQuery,
|
||||||
|
PreloadedQuery,
|
||||||
|
} from "react-relay";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { Calendar, User } from "lucide-react";
|
||||||
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
|
import PolicyEditor from "@/components/PolicyEditor";
|
||||||
|
import PeopleSelector from "@/components/PeopleSelector";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
import type { CreatePolicyViewMutation } from "./__generated__/CreatePolicyViewMutation.graphql";
|
||||||
|
import type { CreatePolicyViewQuery as CreatePolicyViewQueryType } from "./__generated__/CreatePolicyViewQuery.graphql";
|
||||||
|
import { PageTemplate } from "@/components/PageTemplate";
|
||||||
|
import { CreatePolicyViewSkeleton } from "./CreatePolicyPage";
|
||||||
|
|
||||||
|
const CreatePolicyQuery = graphql`
|
||||||
|
query CreatePolicyViewQuery($organizationId: ID!) {
|
||||||
|
organization: node(id: $organizationId) {
|
||||||
|
...PeopleSelector_organization
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CreatePolicyMutation = graphql`
|
||||||
|
mutation CreatePolicyViewMutation(
|
||||||
|
$input: CreatePolicyInput!
|
||||||
|
$connections: [ID!]!
|
||||||
|
) {
|
||||||
|
createPolicy(input: $input) {
|
||||||
|
policyEdge @prependEdge(connections: $connections) {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
content
|
||||||
|
status
|
||||||
|
reviewDate
|
||||||
|
owner {
|
||||||
|
id
|
||||||
|
fullName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function CreatePolicyForm({
|
||||||
|
queryRef,
|
||||||
|
}: {
|
||||||
|
queryRef: PreloadedQuery<CreatePolicyViewQueryType>;
|
||||||
|
}) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { organizationId } = useParams();
|
||||||
|
const data = usePreloadedQuery<CreatePolicyViewQueryType>(
|
||||||
|
CreatePolicyQuery,
|
||||||
|
queryRef
|
||||||
|
);
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [content, setContent] = useState("");
|
||||||
|
const [status, setStatus] = useState<"DRAFT" | "ACTIVE">("DRAFT");
|
||||||
|
const [reviewDate, setReviewDate] = useState("");
|
||||||
|
const [ownerId, setOwnerId] = useState<string | null>(null);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"CreatePolicyView state - content:",
|
||||||
|
content
|
||||||
|
? content.substring(0, 50) + (content.length > 50 ? "..." : "")
|
||||||
|
: "empty"
|
||||||
|
);
|
||||||
|
|
||||||
|
const [commitMutation] =
|
||||||
|
useMutation<CreatePolicyViewMutation>(CreatePolicyMutation);
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!ownerId) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Please select an owner for the policy.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
// Convert reviewDate string to ISO format for the API
|
||||||
|
let reviewDateValue = null;
|
||||||
|
if (reviewDate) {
|
||||||
|
reviewDateValue = new Date(reviewDate).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = {
|
||||||
|
organizationId: organizationId!,
|
||||||
|
name,
|
||||||
|
content,
|
||||||
|
status,
|
||||||
|
reviewDate: reviewDateValue,
|
||||||
|
ownerId,
|
||||||
|
};
|
||||||
|
|
||||||
|
commitMutation({
|
||||||
|
variables: {
|
||||||
|
input,
|
||||||
|
connections: [
|
||||||
|
ConnectionHandler.getConnectionID(
|
||||||
|
organizationId!,
|
||||||
|
"PolicyListPage_policies"
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
onCompleted: (response, errors) => {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
if (errors) {
|
||||||
|
console.error("Error creating policy:", errors);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to create policy. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: "Policy created successfully!",
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate(
|
||||||
|
`/organizations/${organizationId}/policies/${response.createPolicy.policyEdge.node.id}`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
console.error("Error creating policy:", error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to create policy. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageTemplate
|
||||||
|
title="Create Policy"
|
||||||
|
description="Create a new policy for your organization"
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="grid gap-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Policy Information</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="name">Policy Name</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
placeholder="Enter policy name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="content">Policy Content</Label>
|
||||||
|
<div className="min-h-[300px]">
|
||||||
|
<PolicyEditor
|
||||||
|
initialContent={content}
|
||||||
|
onChange={(html) => setContent(html)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Status</Label>
|
||||||
|
<RadioGroup
|
||||||
|
value={status}
|
||||||
|
onValueChange={(value: "DRAFT" | "ACTIVE") =>
|
||||||
|
setStatus(value)
|
||||||
|
}
|
||||||
|
className="flex space-x-4"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="DRAFT" id="draft" />
|
||||||
|
<Label htmlFor="draft" className="cursor-pointer">
|
||||||
|
Draft
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="ACTIVE" id="active" />
|
||||||
|
<Label htmlFor="active" className="cursor-pointer">
|
||||||
|
Active
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="owner" className="flex items-center gap-2">
|
||||||
|
<User className="h-4 w-4" />
|
||||||
|
Policy Owner
|
||||||
|
</Label>
|
||||||
|
<PeopleSelector
|
||||||
|
organizationRef={data.organization}
|
||||||
|
selectedPersonId={ownerId}
|
||||||
|
onSelect={setOwnerId}
|
||||||
|
placeholder="Select policy owner"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="reviewDate" className="flex items-center gap-2">
|
||||||
|
<Calendar className="h-4 w-4" />
|
||||||
|
Review Date
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="reviewDate"
|
||||||
|
type="date"
|
||||||
|
value={reviewDate}
|
||||||
|
onChange={(e) => setReviewDate(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() =>
|
||||||
|
navigate(`/organizations/${organizationId}/policies`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? "Creating..." : "Create Policy"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</PageTemplate>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CreatePolicyView() {
|
||||||
|
const { organizationId } = useParams();
|
||||||
|
const [queryRef, loadQuery] =
|
||||||
|
useQueryLoader<CreatePolicyViewQueryType>(CreatePolicyQuery);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (organizationId) {
|
||||||
|
loadQuery({ organizationId });
|
||||||
|
}
|
||||||
|
}, [organizationId, loadQuery]);
|
||||||
|
|
||||||
|
if (!queryRef) {
|
||||||
|
return <CreatePolicyViewSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<CreatePolicyViewSkeleton />}>
|
||||||
|
{<CreatePolicyForm queryRef={queryRef} />}
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,353 +1,12 @@
|
|||||||
import { Suspense, useEffect, useState } from "react";
|
import { PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
import {
|
|
||||||
graphql,
|
|
||||||
PreloadedQuery,
|
|
||||||
usePreloadedQuery,
|
|
||||||
useQueryLoader,
|
|
||||||
} from "react-relay";
|
|
||||||
import { Card, CardContent, CardFooter } from "@/components/ui/card";
|
import { Card, CardContent, CardFooter } from "@/components/ui/card";
|
||||||
import { Link, useParams } from "react-router";
|
import { lazy, Suspense } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { useLocation } from "react-router";
|
||||||
import { Input } from "@/components/ui/input";
|
import { ErrorBoundaryWithLocation } from "../ErrorBoundary";
|
||||||
import {
|
|
||||||
Plus,
|
|
||||||
FileText,
|
|
||||||
Search,
|
|
||||||
Clock,
|
|
||||||
Filter,
|
|
||||||
ArrowUpDown,
|
|
||||||
} from "lucide-react";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { format } from "date-fns";
|
|
||||||
import type { PolicyListPageQuery as PolicyListPageQueryType } from "./__generated__/PolicyListPageQuery.graphql";
|
|
||||||
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
|
||||||
|
|
||||||
const PolicyListPageQuery = graphql`
|
const PolicyListView = lazy(() => import("./PolicyListView"));
|
||||||
query PolicyListPageQuery($organizationId: ID!) {
|
|
||||||
organization: node(id: $organizationId) {
|
|
||||||
... on Organization {
|
|
||||||
policies(first: 25) @connection(key: "PolicyListPage_policies") {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
content
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
status
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
function PolicyCard({
|
export function PolicyListViewSkeleton() {
|
||||||
title,
|
|
||||||
content,
|
|
||||||
status,
|
|
||||||
updatedAt,
|
|
||||||
}: {
|
|
||||||
title: string;
|
|
||||||
content?: string;
|
|
||||||
status?: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}) {
|
|
||||||
const formattedUpdatedAt = new Date(updatedAt);
|
|
||||||
|
|
||||||
// Extract a short description from the content and strip HTML tags
|
|
||||||
const stripHtmlTags = (html: string) => {
|
|
||||||
// First remove HTML tags
|
|
||||||
const withoutTags = html.replace(/<[^>]*>/g, "");
|
|
||||||
// Then decode HTML entities
|
|
||||||
const decoded = withoutTags
|
|
||||||
.replace(/&/g, "&")
|
|
||||||
.replace(/</g, "<")
|
|
||||||
.replace(/>/g, ">")
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, "'")
|
|
||||||
.replace(/ /g, " ");
|
|
||||||
// Remove markdown headers
|
|
||||||
return decoded.replace(/#.*?\n/, "").trim();
|
|
||||||
};
|
|
||||||
|
|
||||||
const description = content
|
|
||||||
? stripHtmlTags(content).substring(0, 120) +
|
|
||||||
(content.length > 120 ? "..." : "")
|
|
||||||
: "No description available";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="relative overflow-hidden border bg-card transition-all hover:shadow-md h-full flex flex-col">
|
|
||||||
<CardContent className="p-6 flex-grow">
|
|
||||||
<div className="flex flex-col h-full">
|
|
||||||
<div className="flex justify-between items-start mb-3">
|
|
||||||
<h3 className="font-semibold text-xl">{title}</h3>
|
|
||||||
{status && (
|
|
||||||
<Badge
|
|
||||||
className={`${
|
|
||||||
status === "ACTIVE"
|
|
||||||
? "bg-green-100 text-green-700 hover:bg-green-200"
|
|
||||||
: status === "DRAFT"
|
|
||||||
? "bg-yellow-100 text-yellow-700 hover:bg-yellow-200"
|
|
||||||
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{status === "ACTIVE"
|
|
||||||
? "Security"
|
|
||||||
: status === "DRAFT"
|
|
||||||
? "Draft"
|
|
||||||
: status}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground text-sm line-clamp-3 mb-4">
|
|
||||||
{description}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground mt-auto">
|
|
||||||
<Clock className="h-4 w-4" />
|
|
||||||
<span>
|
|
||||||
Last updated: {format(formattedUpdatedAt, "yyyy-MM-dd")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="p-0 border-t">
|
|
||||||
<div className="w-full grid grid-cols-2">
|
|
||||||
<Button variant="ghost" className="rounded-none h-12 border-r">
|
|
||||||
<FileText className="h-5 w-5 mr-2" />
|
|
||||||
View Policy
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" className="rounded-none h-12">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
className="mr-2"
|
|
||||||
>
|
|
||||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
||||||
<polyline points="7 10 12 15 17 10" />
|
|
||||||
<line x1="12" y1="15" x2="12" y2="3" />
|
|
||||||
</svg>
|
|
||||||
Download
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function PolicyListPageContent({
|
|
||||||
queryRef,
|
|
||||||
}: {
|
|
||||||
queryRef: PreloadedQuery<PolicyListPageQueryType>;
|
|
||||||
}) {
|
|
||||||
const data = usePreloadedQuery<PolicyListPageQueryType>(
|
|
||||||
PolicyListPageQuery,
|
|
||||||
queryRef
|
|
||||||
);
|
|
||||||
const { organizationId } = useParams();
|
|
||||||
const policies =
|
|
||||||
data.organization.policies?.edges.map((edge) => edge?.node) ?? [];
|
|
||||||
|
|
||||||
// State for search, filtering and sorting
|
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
|
||||||
const [statusFilter, setStatusFilter] = useState("ALL");
|
|
||||||
const [sortBy, setSortBy] = useState("name-asc");
|
|
||||||
|
|
||||||
// Filter and sort policies
|
|
||||||
const filteredPolicies = policies
|
|
||||||
.filter((policy) => {
|
|
||||||
// Filter by search query
|
|
||||||
const matchesSearch = policy.name
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(searchQuery.toLowerCase());
|
|
||||||
|
|
||||||
// Filter by status
|
|
||||||
const matchesStatus =
|
|
||||||
statusFilter === "ALL" || policy.status === statusFilter;
|
|
||||||
|
|
||||||
return matchesSearch && matchesStatus;
|
|
||||||
})
|
|
||||||
.sort((a, b) => {
|
|
||||||
// Sort policies
|
|
||||||
switch (sortBy) {
|
|
||||||
case "name-asc":
|
|
||||||
return a.name.localeCompare(b.name);
|
|
||||||
case "name-desc":
|
|
||||||
return b.name.localeCompare(a.name);
|
|
||||||
case "updated-desc":
|
|
||||||
return (
|
|
||||||
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
|
||||||
);
|
|
||||||
case "updated-asc":
|
|
||||||
return (
|
|
||||||
new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime()
|
|
||||||
);
|
|
||||||
case "created-desc":
|
|
||||||
return (
|
|
||||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
||||||
);
|
|
||||||
case "created-asc":
|
|
||||||
return (
|
|
||||||
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageTemplate
|
|
||||||
title="Policies"
|
|
||||||
description="Manage your organization's policies"
|
|
||||||
actions={
|
|
||||||
<Button asChild>
|
|
||||||
<Link to={`/organizations/${organizationId}/policies/create`}>
|
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
|
||||||
Create Policy
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{/* Search and filter controls */}
|
|
||||||
<div className="flex flex-col md:flex-row gap-4 mb-6">
|
|
||||||
<div className="relative flex-1">
|
|
||||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
placeholder="Search policies..."
|
|
||||||
className="pl-10"
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
|
||||||
<SelectTrigger className="w-[180px]">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Filter className="h-4 w-4" />
|
|
||||||
<SelectValue placeholder="Filter by status" />
|
|
||||||
</div>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="ALL">All Statuses</SelectItem>
|
|
||||||
<SelectItem value="ACTIVE">Active</SelectItem>
|
|
||||||
<SelectItem value="DRAFT">Draft</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="outline" className="flex items-center gap-2">
|
|
||||||
<ArrowUpDown className="h-4 w-4" />
|
|
||||||
Sort
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuItem onClick={() => setSortBy("name-asc")}>
|
|
||||||
Name (A-Z)
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => setSortBy("name-desc")}>
|
|
||||||
Name (Z-A)
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => setSortBy("updated-desc")}>
|
|
||||||
Recently Updated
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => setSortBy("updated-asc")}>
|
|
||||||
Oldest Updated
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => setSortBy("created-desc")}>
|
|
||||||
Recently Created
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => setSortBy("created-asc")}>
|
|
||||||
Oldest Created
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Results summary */}
|
|
||||||
<div className="mb-4 text-sm text-muted-foreground">
|
|
||||||
Showing {filteredPolicies.length} of {policies.length} policies
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Policy grid */}
|
|
||||||
<div className="space-y-6">
|
|
||||||
{filteredPolicies.length > 0 ? (
|
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
||||||
{filteredPolicies.map((policy) => (
|
|
||||||
<Link
|
|
||||||
key={policy.id}
|
|
||||||
to={`/organizations/${organizationId}/policies/${policy.id}`}
|
|
||||||
className="group"
|
|
||||||
>
|
|
||||||
<PolicyCard
|
|
||||||
title={policy.name}
|
|
||||||
content={policy.content}
|
|
||||||
status={policy.status}
|
|
||||||
updatedAt={policy.updatedAt}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-12 border rounded-lg bg-muted/20">
|
|
||||||
<FileText className="mx-auto h-8 w-8 text-muted-foreground mb-3" />
|
|
||||||
<h3 className="text-lg font-medium">No policies found</h3>
|
|
||||||
<p className="text-muted-foreground mb-4">
|
|
||||||
{searchQuery || statusFilter !== "ALL"
|
|
||||||
? "Try adjusting your search or filters"
|
|
||||||
: "Create your first policy to get started"}
|
|
||||||
</p>
|
|
||||||
{searchQuery || statusFilter !== "ALL" ? (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
setSearchQuery("");
|
|
||||||
setStatusFilter("ALL");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Clear filters
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button asChild>
|
|
||||||
<Link to={`/organizations/${organizationId}/policies/create`}>
|
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
|
||||||
Create Policy
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</PageTemplate>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PolicyListPageSkeleton() {
|
|
||||||
return (
|
return (
|
||||||
<PageTemplateSkeleton
|
<PageTemplateSkeleton
|
||||||
title="Policies"
|
title="Policies"
|
||||||
@@ -398,23 +57,14 @@ export function PolicyListPageSkeleton() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PolicyListPage() {
|
export function PolicyListPage() {
|
||||||
const [queryRef, loadQuery] =
|
const location = useLocation();
|
||||||
useQueryLoader<PolicyListPageQueryType>(PolicyListPageQuery);
|
|
||||||
|
|
||||||
const { organizationId } = useParams();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadQuery({ organizationId: organizationId! });
|
|
||||||
}, [loadQuery, organizationId]);
|
|
||||||
|
|
||||||
if (!queryRef) {
|
|
||||||
return <PolicyListPageSkeleton />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<PolicyListPageSkeleton />}>
|
<Suspense key={location.pathname} fallback={<PolicyListViewSkeleton />}>
|
||||||
{<PolicyListPageContent queryRef={queryRef} />}
|
<ErrorBoundaryWithLocation>
|
||||||
|
<PolicyListView />
|
||||||
|
</ErrorBoundaryWithLocation>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
370
apps/console/src/pages/organizations/policies/PolicyListView.tsx
Normal file
370
apps/console/src/pages/organizations/policies/PolicyListView.tsx
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
import { Suspense, useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
graphql,
|
||||||
|
PreloadedQuery,
|
||||||
|
usePreloadedQuery,
|
||||||
|
useQueryLoader,
|
||||||
|
} from "react-relay";
|
||||||
|
import { Card, CardContent, CardFooter } from "@/components/ui/card";
|
||||||
|
import { Link, useParams } from "react-router";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Plus,
|
||||||
|
FileText,
|
||||||
|
Search,
|
||||||
|
Clock,
|
||||||
|
Filter,
|
||||||
|
ArrowUpDown,
|
||||||
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import type { PolicyListViewQuery as PolicyListViewQueryType } from "./__generated__/PolicyListViewQuery.graphql";
|
||||||
|
import { PageTemplate } from "@/components/PageTemplate";
|
||||||
|
import { PolicyListViewSkeleton } from "./PolicyListPage";
|
||||||
|
|
||||||
|
const PolicyListViewQuery = graphql`
|
||||||
|
query PolicyListViewQuery($organizationId: ID!) {
|
||||||
|
organization: node(id: $organizationId) {
|
||||||
|
... on Organization {
|
||||||
|
policies(first: 25) @connection(key: "PolicyListView_policies") {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
content
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function PolicyCard({
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
status,
|
||||||
|
updatedAt,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
content?: string;
|
||||||
|
status?: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}) {
|
||||||
|
const formattedUpdatedAt = new Date(updatedAt);
|
||||||
|
|
||||||
|
// Extract a short description from the content and strip HTML tags
|
||||||
|
const stripHtmlTags = (html: string) => {
|
||||||
|
// First remove HTML tags
|
||||||
|
const withoutTags = html.replace(/<[^>]*>/g, "");
|
||||||
|
// Then decode HTML entities
|
||||||
|
const decoded = withoutTags
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
.replace(/ /g, " ");
|
||||||
|
// Remove markdown headers
|
||||||
|
return decoded.replace(/#.*?\n/, "").trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
const description = content
|
||||||
|
? stripHtmlTags(content).substring(0, 120) +
|
||||||
|
(content.length > 120 ? "..." : "")
|
||||||
|
: "No description available";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="relative overflow-hidden border bg-card transition-all hover:shadow-md h-full flex flex-col">
|
||||||
|
<CardContent className="p-6 flex-grow">
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<div className="flex justify-between items-start mb-3">
|
||||||
|
<h3 className="font-semibold text-xl">{title}</h3>
|
||||||
|
{status && (
|
||||||
|
<Badge
|
||||||
|
className={`${
|
||||||
|
status === "ACTIVE"
|
||||||
|
? "bg-green-100 text-green-700 hover:bg-green-200"
|
||||||
|
: status === "DRAFT"
|
||||||
|
? "bg-yellow-100 text-yellow-700 hover:bg-yellow-200"
|
||||||
|
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{status === "ACTIVE"
|
||||||
|
? "Security"
|
||||||
|
: status === "DRAFT"
|
||||||
|
? "Draft"
|
||||||
|
: status}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-muted-foreground text-sm line-clamp-3 mb-4">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground mt-auto">
|
||||||
|
<Clock className="h-4 w-4" />
|
||||||
|
<span>
|
||||||
|
Last updated: {format(formattedUpdatedAt, "yyyy-MM-dd")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className="p-0 border-t">
|
||||||
|
<div className="w-full grid grid-cols-2">
|
||||||
|
<Button variant="ghost" className="rounded-none h-12 border-r">
|
||||||
|
<FileText className="h-5 w-5 mr-2" />
|
||||||
|
View Policy
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" className="rounded-none h-12">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="mr-2"
|
||||||
|
>
|
||||||
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||||
|
<polyline points="7 10 12 15 17 10" />
|
||||||
|
<line x1="12" y1="15" x2="12" y2="3" />
|
||||||
|
</svg>
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PolicyListViewContent({
|
||||||
|
queryRef,
|
||||||
|
}: {
|
||||||
|
queryRef: PreloadedQuery<PolicyListViewQueryType>;
|
||||||
|
}) {
|
||||||
|
const data = usePreloadedQuery<PolicyListViewQueryType>(
|
||||||
|
PolicyListViewQuery,
|
||||||
|
queryRef
|
||||||
|
);
|
||||||
|
const { organizationId } = useParams();
|
||||||
|
const policies =
|
||||||
|
data.organization.policies?.edges.map((edge) => edge?.node) ?? [];
|
||||||
|
|
||||||
|
// State for search, filtering and sorting
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [statusFilter, setStatusFilter] = useState("ALL");
|
||||||
|
const [sortBy, setSortBy] = useState("name-asc");
|
||||||
|
|
||||||
|
// Filter and sort policies
|
||||||
|
const filteredPolicies = policies
|
||||||
|
.filter((policy) => {
|
||||||
|
// Filter by search query
|
||||||
|
const matchesSearch = policy.name
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchQuery.toLowerCase());
|
||||||
|
|
||||||
|
// Filter by status
|
||||||
|
const matchesStatus =
|
||||||
|
statusFilter === "ALL" || policy.status === statusFilter;
|
||||||
|
|
||||||
|
return matchesSearch && matchesStatus;
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
// Sort policies
|
||||||
|
switch (sortBy) {
|
||||||
|
case "name-asc":
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
case "name-desc":
|
||||||
|
return b.name.localeCompare(a.name);
|
||||||
|
case "updated-desc":
|
||||||
|
return (
|
||||||
|
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
||||||
|
);
|
||||||
|
case "updated-asc":
|
||||||
|
return (
|
||||||
|
new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime()
|
||||||
|
);
|
||||||
|
case "created-desc":
|
||||||
|
return (
|
||||||
|
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||||
|
);
|
||||||
|
case "created-asc":
|
||||||
|
return (
|
||||||
|
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageTemplate
|
||||||
|
title="Policies"
|
||||||
|
description="Manage your organization's policies"
|
||||||
|
actions={
|
||||||
|
<Button asChild>
|
||||||
|
<Link to={`/organizations/${organizationId}/policies/create`}>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Create Policy
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* Search and filter controls */}
|
||||||
|
<div className="flex flex-col md:flex-row gap-4 mb-6">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search policies..."
|
||||||
|
className="pl-10"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||||
|
<SelectTrigger className="w-[180px]">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Filter className="h-4 w-4" />
|
||||||
|
<SelectValue placeholder="Filter by status" />
|
||||||
|
</div>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="ALL">All Statuses</SelectItem>
|
||||||
|
<SelectItem value="ACTIVE">Active</SelectItem>
|
||||||
|
<SelectItem value="DRAFT">Draft</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="outline" className="flex items-center gap-2">
|
||||||
|
<ArrowUpDown className="h-4 w-4" />
|
||||||
|
Sort
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem onClick={() => setSortBy("name-asc")}>
|
||||||
|
Name (A-Z)
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setSortBy("name-desc")}>
|
||||||
|
Name (Z-A)
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setSortBy("updated-desc")}>
|
||||||
|
Recently Updated
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setSortBy("updated-asc")}>
|
||||||
|
Oldest Updated
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setSortBy("created-desc")}>
|
||||||
|
Recently Created
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setSortBy("created-asc")}>
|
||||||
|
Oldest Created
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Results summary */}
|
||||||
|
<div className="mb-4 text-sm text-muted-foreground">
|
||||||
|
Showing {filteredPolicies.length} of {policies.length} policies
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Policy grid */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
{filteredPolicies.length > 0 ? (
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{filteredPolicies.map((policy) => (
|
||||||
|
<Link
|
||||||
|
key={policy.id}
|
||||||
|
to={`/organizations/${organizationId}/policies/${policy.id}`}
|
||||||
|
className="group"
|
||||||
|
>
|
||||||
|
<PolicyCard
|
||||||
|
title={policy.name}
|
||||||
|
content={policy.content}
|
||||||
|
status={policy.status}
|
||||||
|
updatedAt={policy.updatedAt}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-12 border rounded-lg bg-muted/20">
|
||||||
|
<FileText className="mx-auto h-8 w-8 text-muted-foreground mb-3" />
|
||||||
|
<h3 className="text-lg font-medium">No policies found</h3>
|
||||||
|
<p className="text-muted-foreground mb-4">
|
||||||
|
{searchQuery || statusFilter !== "ALL"
|
||||||
|
? "Try adjusting your search or filters"
|
||||||
|
: "Create your first policy to get started"}
|
||||||
|
</p>
|
||||||
|
{searchQuery || statusFilter !== "ALL" ? (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setSearchQuery("");
|
||||||
|
setStatusFilter("ALL");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear filters
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button asChild>
|
||||||
|
<Link to={`/organizations/${organizationId}/policies/create`}>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Create Policy
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</PageTemplate>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PolicyListView() {
|
||||||
|
const [queryRef, loadQuery] =
|
||||||
|
useQueryLoader<PolicyListViewQueryType>(PolicyListViewQuery);
|
||||||
|
|
||||||
|
const { organizationId } = useParams();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadQuery({ organizationId: organizationId! });
|
||||||
|
}, [loadQuery, organizationId]);
|
||||||
|
|
||||||
|
if (!queryRef) {
|
||||||
|
return <PolicyListViewSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<PolicyListViewSkeleton />}>
|
||||||
|
{<PolicyListViewContent queryRef={queryRef} />}
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
120
apps/console/src/pages/organizations/policies/PolicyPage.tsx
Normal file
120
apps/console/src/pages/organizations/policies/PolicyPage.tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { lazy, Suspense } from "react";
|
||||||
|
import { useLocation } from "react-router";
|
||||||
|
import { ErrorBoundaryWithLocation } from "../ErrorBoundary";
|
||||||
|
|
||||||
|
const PolicyView = lazy(() => import("./PolicyView"));
|
||||||
|
|
||||||
|
export function PolicyViewSkeleton() {
|
||||||
|
return (
|
||||||
|
<PageTemplateSkeleton
|
||||||
|
withDescription
|
||||||
|
actions={
|
||||||
|
<div className="flex gap-2 w-1/3">
|
||||||
|
<div className="bg-muted animate-pulse h-8 w-1/2 rounded-lg" />
|
||||||
|
<div className="bg-muted animate-pulse h-8 w-1/2 rounded-lg" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
<div className="lg:col-span-2">
|
||||||
|
<Card className="border shadow-sm">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="p-2 rounded-md bg-slate-100/50 h-10 w-10 animate-pulse" />
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="h-6 w-16 bg-muted animate-pulse rounded-md" />
|
||||||
|
<div className="h-6 w-20 bg-muted animate-pulse rounded-md" />
|
||||||
|
<div className="h-6 w-16 bg-muted animate-pulse rounded-md" />
|
||||||
|
</div>
|
||||||
|
<div className="ml-auto">
|
||||||
|
<div className="h-9 w-9 bg-muted animate-pulse rounded-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-8 w-3/4 bg-muted animate-pulse rounded mb-3" />
|
||||||
|
|
||||||
|
<div className="space-y-2 mb-6">
|
||||||
|
<div className="h-4 w-full bg-muted animate-pulse rounded" />
|
||||||
|
<div className="h-4 w-full bg-muted animate-pulse rounded" />
|
||||||
|
<div className="h-4 w-2/3 bg-muted animate-pulse rounded" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex border-b mb-6">
|
||||||
|
<div className="px-4 py-2 h-8 w-28 bg-muted animate-pulse rounded" />
|
||||||
|
<div className="px-4 py-2 h-8 w-28 bg-muted animate-pulse rounded opacity-50" />
|
||||||
|
<div className="px-4 py-2 h-8 w-28 bg-muted animate-pulse rounded opacity-50" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="h-4 w-full bg-muted animate-pulse rounded" />
|
||||||
|
<div className="h-4 w-full bg-muted animate-pulse rounded" />
|
||||||
|
<div className="h-4 w-full bg-muted animate-pulse rounded" />
|
||||||
|
<div className="h-4 w-full bg-muted animate-pulse rounded" />
|
||||||
|
<div className="h-4 w-3/4 bg-muted animate-pulse rounded" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="lg:col-span-1">
|
||||||
|
<Card className="border shadow-sm mb-6">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="h-6 w-32 bg-muted animate-pulse rounded mb-6" />
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<div className="h-4 w-28 bg-muted animate-pulse rounded mb-1" />
|
||||||
|
<div className="h-5 w-24 bg-muted animate-pulse rounded" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="h-4 w-28 bg-muted animate-pulse rounded mb-1" />
|
||||||
|
<div className="h-5 w-24 bg-muted animate-pulse rounded" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="h-4 w-28 bg-muted animate-pulse rounded mb-1" />
|
||||||
|
<div className="h-5 w-36 bg-muted animate-pulse rounded" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className="my-4" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="h-5 w-32 bg-muted animate-pulse rounded mb-3" />
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="h-4 w-48 bg-muted animate-pulse rounded" />
|
||||||
|
<div className="h-4 w-48 bg-muted animate-pulse rounded" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="h-10 w-full bg-muted animate-pulse rounded mb-6" />
|
||||||
|
|
||||||
|
<Card className="border shadow-sm bg-red-50/30">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="h-5 w-28 bg-muted animate-pulse rounded mb-3" />
|
||||||
|
<div className="h-16 w-full bg-muted animate-pulse rounded mb-4" />
|
||||||
|
<div className="h-10 w-full bg-muted animate-pulse rounded" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageTemplateSkeleton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PolicyPage() {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense key={location.pathname} fallback={<PolicyViewSkeleton />}>
|
||||||
|
<ErrorBoundaryWithLocation>
|
||||||
|
<PolicyView />
|
||||||
|
</ErrorBoundaryWithLocation>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -13,13 +13,14 @@ import { Card, CardContent } from "@/components/ui/card";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import type { PolicyOverviewPageQuery as PolicyOverviewPageQueryType } from "./__generated__/PolicyOverviewPageQuery.graphql";
|
import type { PolicyViewQuery as PolicyViewQueryType } from "./__generated__/PolicyViewQuery.graphql";
|
||||||
import type { PolicyOverviewPageDeleteMutation } from "./__generated__/PolicyOverviewPageDeleteMutation.graphql";
|
import type { PolicyViewDeleteMutation } from "./__generated__/PolicyViewDeleteMutation.graphql";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
import { PageTemplate } from "@/components/PageTemplate";
|
||||||
|
import { PolicyViewSkeleton } from "./PolicyPage";
|
||||||
|
|
||||||
const PolicyOverviewPageQuery = graphql`
|
const PolicyViewQuery = graphql`
|
||||||
query PolicyOverviewPageQuery($policyId: ID!) {
|
query PolicyViewQuery($policyId: ID!) {
|
||||||
node(id: $policyId) {
|
node(id: $policyId) {
|
||||||
id
|
id
|
||||||
... on Policy {
|
... on Policy {
|
||||||
@@ -40,7 +41,7 @@ const PolicyOverviewPageQuery = graphql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const DeletePolicyMutation = graphql`
|
const DeletePolicyMutation = graphql`
|
||||||
mutation PolicyOverviewPageDeleteMutation(
|
mutation PolicyViewDeleteMutation(
|
||||||
$input: DeletePolicyInput!
|
$input: DeletePolicyInput!
|
||||||
$connections: [ID!]!
|
$connections: [ID!]!
|
||||||
) {
|
) {
|
||||||
@@ -50,12 +51,12 @@ const DeletePolicyMutation = graphql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function PolicyOverviewPageContent({
|
function PolicyViewContent({
|
||||||
queryRef,
|
queryRef,
|
||||||
}: {
|
}: {
|
||||||
queryRef: PreloadedQuery<PolicyOverviewPageQueryType>;
|
queryRef: PreloadedQuery<PolicyViewQueryType>;
|
||||||
}) {
|
}) {
|
||||||
const data = usePreloadedQuery(PolicyOverviewPageQuery, queryRef);
|
const data = usePreloadedQuery(PolicyViewQuery, queryRef);
|
||||||
const policy = data.node;
|
const policy = data.node;
|
||||||
const { organizationId } = useParams();
|
const { organizationId } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -64,7 +65,7 @@ function PolicyOverviewPageContent({
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const [commitDeleteMutation] =
|
const [commitDeleteMutation] =
|
||||||
useMutation<PolicyOverviewPageDeleteMutation>(DeletePolicyMutation);
|
useMutation<PolicyViewDeleteMutation>(DeletePolicyMutation);
|
||||||
|
|
||||||
const handleDeletePolicy = useCallback(() => {
|
const handleDeletePolicy = useCallback(() => {
|
||||||
if (
|
if (
|
||||||
@@ -402,111 +403,9 @@ function PolicyOverviewPageContent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PolicyOverviewPageSkeleton() {
|
export default function PolicyView() {
|
||||||
return (
|
const [queryRef, loadQuery] =
|
||||||
<PageTemplateSkeleton
|
useQueryLoader<PolicyViewQueryType>(PolicyViewQuery);
|
||||||
withDescription
|
|
||||||
actions={
|
|
||||||
<div className="flex gap-2 w-1/3">
|
|
||||||
<div className="bg-muted animate-pulse h-8 w-1/2 rounded-lg" />
|
|
||||||
<div className="bg-muted animate-pulse h-8 w-1/2 rounded-lg" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
||||||
<div className="lg:col-span-2">
|
|
||||||
<Card className="border shadow-sm">
|
|
||||||
<CardContent className="p-6">
|
|
||||||
<div className="flex items-center gap-3 mb-4">
|
|
||||||
<div className="p-2 rounded-md bg-slate-100/50 h-10 w-10 animate-pulse" />
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<div className="h-6 w-16 bg-muted animate-pulse rounded-md" />
|
|
||||||
<div className="h-6 w-20 bg-muted animate-pulse rounded-md" />
|
|
||||||
<div className="h-6 w-16 bg-muted animate-pulse rounded-md" />
|
|
||||||
</div>
|
|
||||||
<div className="ml-auto">
|
|
||||||
<div className="h-9 w-9 bg-muted animate-pulse rounded-full" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="h-8 w-3/4 bg-muted animate-pulse rounded mb-3" />
|
|
||||||
|
|
||||||
<div className="space-y-2 mb-6">
|
|
||||||
<div className="h-4 w-full bg-muted animate-pulse rounded" />
|
|
||||||
<div className="h-4 w-full bg-muted animate-pulse rounded" />
|
|
||||||
<div className="h-4 w-2/3 bg-muted animate-pulse rounded" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex border-b mb-6">
|
|
||||||
<div className="px-4 py-2 h-8 w-28 bg-muted animate-pulse rounded" />
|
|
||||||
<div className="px-4 py-2 h-8 w-28 bg-muted animate-pulse rounded opacity-50" />
|
|
||||||
<div className="px-4 py-2 h-8 w-28 bg-muted animate-pulse rounded opacity-50" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="h-4 w-full bg-muted animate-pulse rounded" />
|
|
||||||
<div className="h-4 w-full bg-muted animate-pulse rounded" />
|
|
||||||
<div className="h-4 w-full bg-muted animate-pulse rounded" />
|
|
||||||
<div className="h-4 w-full bg-muted animate-pulse rounded" />
|
|
||||||
<div className="h-4 w-3/4 bg-muted animate-pulse rounded" />
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="lg:col-span-1">
|
|
||||||
<Card className="border shadow-sm mb-6">
|
|
||||||
<CardContent className="p-6">
|
|
||||||
<div className="h-6 w-32 bg-muted animate-pulse rounded mb-6" />
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<div className="h-4 w-28 bg-muted animate-pulse rounded mb-1" />
|
|
||||||
<div className="h-5 w-24 bg-muted animate-pulse rounded" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="h-4 w-28 bg-muted animate-pulse rounded mb-1" />
|
|
||||||
<div className="h-5 w-24 bg-muted animate-pulse rounded" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="h-4 w-28 bg-muted animate-pulse rounded mb-1" />
|
|
||||||
<div className="h-5 w-36 bg-muted animate-pulse rounded" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr className="my-4" />
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="h-5 w-32 bg-muted animate-pulse rounded mb-3" />
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="h-4 w-48 bg-muted animate-pulse rounded" />
|
|
||||||
<div className="h-4 w-48 bg-muted animate-pulse rounded" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<div className="h-10 w-full bg-muted animate-pulse rounded mb-6" />
|
|
||||||
|
|
||||||
<Card className="border shadow-sm bg-red-50/30">
|
|
||||||
<CardContent className="p-6">
|
|
||||||
<div className="h-5 w-28 bg-muted animate-pulse rounded mb-3" />
|
|
||||||
<div className="h-16 w-full bg-muted animate-pulse rounded mb-4" />
|
|
||||||
<div className="h-10 w-full bg-muted animate-pulse rounded" />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PageTemplateSkeleton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PolicyOverviewPage() {
|
|
||||||
const [queryRef, loadQuery] = useQueryLoader<PolicyOverviewPageQueryType>(
|
|
||||||
PolicyOverviewPageQuery
|
|
||||||
);
|
|
||||||
|
|
||||||
const { policyId } = useParams();
|
const { policyId } = useParams();
|
||||||
|
|
||||||
@@ -515,12 +414,12 @@ export default function PolicyOverviewPage() {
|
|||||||
}, [loadQuery, policyId]);
|
}, [loadQuery, policyId]);
|
||||||
|
|
||||||
if (!queryRef) {
|
if (!queryRef) {
|
||||||
return <PolicyOverviewPageSkeleton />;
|
return <PolicyViewSkeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<PolicyOverviewPageSkeleton />}>
|
<Suspense fallback={<PolicyViewSkeleton />}>
|
||||||
{queryRef && <PolicyOverviewPageContent queryRef={queryRef} />}
|
{queryRef && <PolicyViewContent queryRef={queryRef} />}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,257 +1,11 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
import { useNavigate, useParams } from "react-router";
|
import { lazy, Suspense } from "react";
|
||||||
import {
|
import { useLocation } from "react-router";
|
||||||
graphql,
|
import { ErrorBoundaryWithLocation } from "../ErrorBoundary";
|
||||||
useMutation,
|
|
||||||
usePreloadedQuery,
|
|
||||||
useQueryLoader,
|
|
||||||
PreloadedQuery,
|
|
||||||
} from "react-relay";
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { toast } from "@/hooks/use-toast";
|
|
||||||
import { Calendar, User } from "lucide-react";
|
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|
||||||
import { Suspense } from "react";
|
|
||||||
import PolicyEditor from "@/components/PolicyEditor";
|
|
||||||
import PeopleSelector from "@/components/PeopleSelector";
|
|
||||||
import type { UpdatePolicyPageQuery as UpdatePolicyPageQueryType } from "./__generated__/UpdatePolicyPageQuery.graphql";
|
|
||||||
import type { UpdatePolicyPageMutation as UpdatePolicyPageMutationType } from "./__generated__/UpdatePolicyPageMutation.graphql";
|
|
||||||
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
|
||||||
|
|
||||||
const UpdatePolicyPageQuery = graphql`
|
const UpdatePolicyView = lazy(() => import("./UpdatePolicyView"));
|
||||||
query UpdatePolicyPageQuery($policyId: ID!, $organizationId: ID!) {
|
|
||||||
policy: node(id: $policyId) {
|
|
||||||
id
|
|
||||||
... on Policy {
|
|
||||||
name
|
|
||||||
content
|
|
||||||
status
|
|
||||||
version
|
|
||||||
reviewDate
|
|
||||||
owner {
|
|
||||||
id
|
|
||||||
fullName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
organization: node(id: $organizationId) {
|
|
||||||
...PeopleSelector_organization
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const UpdatePolicyMutation = graphql`
|
export function UpdatePolicyViewSkeleton() {
|
||||||
mutation UpdatePolicyPageMutation($input: UpdatePolicyInput!) {
|
|
||||||
updatePolicy(input: $input) {
|
|
||||||
policy {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
content
|
|
||||||
status
|
|
||||||
version
|
|
||||||
reviewDate
|
|
||||||
owner {
|
|
||||||
id
|
|
||||||
fullName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
function UpdatePolicyPageContent({
|
|
||||||
queryRef,
|
|
||||||
}: {
|
|
||||||
queryRef: PreloadedQuery<UpdatePolicyPageQueryType>;
|
|
||||||
}) {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { organizationId, policyId } = useParams();
|
|
||||||
const data = usePreloadedQuery<UpdatePolicyPageQueryType>(
|
|
||||||
UpdatePolicyPageQuery,
|
|
||||||
queryRef
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("UpdatePolicyPage data:", data.policy);
|
|
||||||
|
|
||||||
const [name, setName] = useState(data.policy.name);
|
|
||||||
const [content, setContent] = useState(data.policy.content || "");
|
|
||||||
const [status, setStatus] = useState(data.policy.status);
|
|
||||||
const [reviewDate, setReviewDate] = useState(data.policy.reviewDate || "");
|
|
||||||
const [ownerId, setOwnerId] = useState<string | null>(
|
|
||||||
data.policy.owner?.id || null
|
|
||||||
);
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
"UpdatePolicyPage state - content:",
|
|
||||||
content
|
|
||||||
? content.substring(0, 50) + (content.length > 50 ? "..." : "")
|
|
||||||
: "empty"
|
|
||||||
);
|
|
||||||
|
|
||||||
const [commitMutation] =
|
|
||||||
useMutation<UpdatePolicyPageMutationType>(UpdatePolicyMutation);
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setIsSubmitting(true);
|
|
||||||
|
|
||||||
// Convert reviewDate string to ISO format for the API
|
|
||||||
let reviewDateValue = null;
|
|
||||||
if (reviewDate) {
|
|
||||||
reviewDateValue = new Date(reviewDate).toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
commitMutation({
|
|
||||||
variables: {
|
|
||||||
input: {
|
|
||||||
id: data.policy.id,
|
|
||||||
name,
|
|
||||||
content,
|
|
||||||
status,
|
|
||||||
reviewDate: reviewDateValue,
|
|
||||||
ownerId,
|
|
||||||
expectedVersion: data.policy.version!,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
onCompleted: (response, errors) => {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
if (errors) {
|
|
||||||
console.error("Error updating policy:", errors);
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: "Failed to update policy. Please try again.",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: "Success",
|
|
||||||
description: "Policy updated successfully.",
|
|
||||||
});
|
|
||||||
|
|
||||||
navigate(`/organizations/${organizationId}/policies/${policyId}`);
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
console.error("Error updating policy:", error);
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: "Failed to update policy. Please try again.",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageTemplate title="Update Policy" description="Update an existing policy">
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<div className="grid gap-6">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Policy Information</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="name">Policy Name</Label>
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
placeholder="Enter policy name"
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="content">Policy Content</Label>
|
|
||||||
<div className="min-h-[300px]">
|
|
||||||
<PolicyEditor
|
|
||||||
initialContent={content}
|
|
||||||
onChange={(html) => setContent(html)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Status</Label>
|
|
||||||
<RadioGroup
|
|
||||||
value={status}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
setStatus(value as "DRAFT" | "ACTIVE")
|
|
||||||
}
|
|
||||||
className="flex space-x-4"
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<RadioGroupItem value="DRAFT" id="draft" />
|
|
||||||
<Label htmlFor="draft" className="cursor-pointer">
|
|
||||||
Draft
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<RadioGroupItem value="ACTIVE" id="active" />
|
|
||||||
<Label htmlFor="active" className="cursor-pointer">
|
|
||||||
Active
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</RadioGroup>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="owner" className="flex items-center gap-2">
|
|
||||||
<User className="h-4 w-4" />
|
|
||||||
Policy Owner
|
|
||||||
</Label>
|
|
||||||
<PeopleSelector
|
|
||||||
organizationRef={data.organization}
|
|
||||||
selectedPersonId={ownerId}
|
|
||||||
onSelect={setOwnerId}
|
|
||||||
placeholder="Select policy owner"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="reviewDate" className="flex items-center gap-2">
|
|
||||||
<Calendar className="h-4 w-4" />
|
|
||||||
Review Date
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="reviewDate"
|
|
||||||
type="date"
|
|
||||||
value={reviewDate}
|
|
||||||
onChange={(e) => setReviewDate(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-4">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() =>
|
|
||||||
navigate(
|
|
||||||
`/organizations/${organizationId}/policies/${policyId}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={isSubmitting}>
|
|
||||||
{isSubmitting ? "Updating..." : "Update Policy"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</PageTemplate>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function UpdatePolicyPageSkeleton() {
|
|
||||||
return (
|
return (
|
||||||
<PageTemplateSkeleton
|
<PageTemplateSkeleton
|
||||||
title="Update Policy"
|
title="Update Policy"
|
||||||
@@ -271,25 +25,14 @@ export function UpdatePolicyPageSkeleton() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UpdatePolicyPage() {
|
export function UpdatePolicyPage() {
|
||||||
const { organizationId, policyId } = useParams();
|
const location = useLocation();
|
||||||
const [queryRef, loadQuery] = useQueryLoader<UpdatePolicyPageQueryType>(
|
|
||||||
UpdatePolicyPageQuery
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (organizationId && policyId) {
|
|
||||||
loadQuery({ organizationId, policyId });
|
|
||||||
}
|
|
||||||
}, [organizationId, policyId, loadQuery]);
|
|
||||||
|
|
||||||
if (!queryRef) {
|
|
||||||
return <UpdatePolicyPageSkeleton />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<UpdatePolicyPageSkeleton />}>
|
<Suspense key={location.pathname} fallback={<UpdatePolicyViewSkeleton />}>
|
||||||
{queryRef && <UpdatePolicyPageContent queryRef={queryRef} />}
|
<ErrorBoundaryWithLocation>
|
||||||
|
<UpdatePolicyView />
|
||||||
|
</ErrorBoundaryWithLocation>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,276 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useNavigate, useParams } from "react-router";
|
||||||
|
import {
|
||||||
|
graphql,
|
||||||
|
useMutation,
|
||||||
|
usePreloadedQuery,
|
||||||
|
useQueryLoader,
|
||||||
|
PreloadedQuery,
|
||||||
|
} from "react-relay";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { toast } from "@/hooks/use-toast";
|
||||||
|
import { Calendar, User } from "lucide-react";
|
||||||
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
import PolicyEditor from "@/components/PolicyEditor";
|
||||||
|
import PeopleSelector from "@/components/PeopleSelector";
|
||||||
|
import type { UpdatePolicyViewQuery as UpdatePolicyViewQueryType } from "./__generated__/UpdatePolicyViewQuery.graphql";
|
||||||
|
import type { UpdatePolicyViewMutation as UpdatePolicyViewMutationType } from "./__generated__/UpdatePolicyViewMutation.graphql";
|
||||||
|
import { PageTemplate } from "@/components/PageTemplate";
|
||||||
|
import { UpdatePolicyViewSkeleton } from "./UpdatePolicyPage";
|
||||||
|
|
||||||
|
const UpdatePolicyViewQuery = graphql`
|
||||||
|
query UpdatePolicyViewQuery($policyId: ID!, $organizationId: ID!) {
|
||||||
|
policy: node(id: $policyId) {
|
||||||
|
id
|
||||||
|
... on Policy {
|
||||||
|
name
|
||||||
|
content
|
||||||
|
status
|
||||||
|
version
|
||||||
|
reviewDate
|
||||||
|
owner {
|
||||||
|
id
|
||||||
|
fullName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
organization: node(id: $organizationId) {
|
||||||
|
...PeopleSelector_organization
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const UpdatePolicyMutation = graphql`
|
||||||
|
mutation UpdatePolicyViewMutation($input: UpdatePolicyInput!) {
|
||||||
|
updatePolicy(input: $input) {
|
||||||
|
policy {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
content
|
||||||
|
status
|
||||||
|
version
|
||||||
|
reviewDate
|
||||||
|
owner {
|
||||||
|
id
|
||||||
|
fullName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function UpdatePolicyViewContent({
|
||||||
|
queryRef,
|
||||||
|
}: {
|
||||||
|
queryRef: PreloadedQuery<UpdatePolicyViewQueryType>;
|
||||||
|
}) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { organizationId, policyId } = useParams();
|
||||||
|
const data = usePreloadedQuery<UpdatePolicyViewQueryType>(
|
||||||
|
UpdatePolicyViewQuery,
|
||||||
|
queryRef
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("UpdatePolicyView data:", data.policy);
|
||||||
|
|
||||||
|
const [name, setName] = useState(data.policy.name);
|
||||||
|
const [content, setContent] = useState(data.policy.content || "");
|
||||||
|
const [status, setStatus] = useState(data.policy.status);
|
||||||
|
const [reviewDate, setReviewDate] = useState(data.policy.reviewDate || "");
|
||||||
|
const [ownerId, setOwnerId] = useState<string | null>(
|
||||||
|
data.policy.owner?.id || null
|
||||||
|
);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"UpdatePolicyView state - content:",
|
||||||
|
content
|
||||||
|
? content.substring(0, 50) + (content.length > 50 ? "..." : "")
|
||||||
|
: "empty"
|
||||||
|
);
|
||||||
|
|
||||||
|
const [commitMutation] =
|
||||||
|
useMutation<UpdatePolicyViewMutationType>(UpdatePolicyMutation);
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
// Convert reviewDate string to ISO format for the API
|
||||||
|
let reviewDateValue = null;
|
||||||
|
if (reviewDate) {
|
||||||
|
reviewDateValue = new Date(reviewDate).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
commitMutation({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
id: data.policy.id,
|
||||||
|
name,
|
||||||
|
content,
|
||||||
|
status,
|
||||||
|
reviewDate: reviewDateValue,
|
||||||
|
ownerId,
|
||||||
|
expectedVersion: data.policy.version!,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onCompleted: (response, errors) => {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
if (errors) {
|
||||||
|
console.error("Error updating policy:", errors);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to update policy. Please try again.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: "Policy updated successfully.",
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate(`/organizations/${organizationId}/policies/${policyId}`);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
console.error("Error updating policy:", error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to update policy. Please try again.",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageTemplate title="Update Policy" description="Update an existing policy">
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="grid gap-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Policy Information</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="name">Policy Name</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
placeholder="Enter policy name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="content">Policy Content</Label>
|
||||||
|
<div className="min-h-[300px]">
|
||||||
|
<PolicyEditor
|
||||||
|
initialContent={content}
|
||||||
|
onChange={(html) => setContent(html)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Status</Label>
|
||||||
|
<RadioGroup
|
||||||
|
value={status}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setStatus(value as "DRAFT" | "ACTIVE")
|
||||||
|
}
|
||||||
|
className="flex space-x-4"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="DRAFT" id="draft" />
|
||||||
|
<Label htmlFor="draft" className="cursor-pointer">
|
||||||
|
Draft
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="ACTIVE" id="active" />
|
||||||
|
<Label htmlFor="active" className="cursor-pointer">
|
||||||
|
Active
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="owner" className="flex items-center gap-2">
|
||||||
|
<User className="h-4 w-4" />
|
||||||
|
Policy Owner
|
||||||
|
</Label>
|
||||||
|
<PeopleSelector
|
||||||
|
organizationRef={data.organization}
|
||||||
|
selectedPersonId={ownerId}
|
||||||
|
onSelect={setOwnerId}
|
||||||
|
placeholder="Select policy owner"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="reviewDate" className="flex items-center gap-2">
|
||||||
|
<Calendar className="h-4 w-4" />
|
||||||
|
Review Date
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="reviewDate"
|
||||||
|
type="date"
|
||||||
|
value={reviewDate}
|
||||||
|
onChange={(e) => setReviewDate(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() =>
|
||||||
|
navigate(
|
||||||
|
`/organizations/${organizationId}/policies/${policyId}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? "Updating..." : "Update Policy"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</PageTemplate>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UpdatePolicyView() {
|
||||||
|
const { organizationId, policyId } = useParams();
|
||||||
|
const [queryRef, loadQuery] = useQueryLoader<UpdatePolicyViewQueryType>(
|
||||||
|
UpdatePolicyViewQuery
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (organizationId && policyId) {
|
||||||
|
loadQuery({ organizationId, policyId });
|
||||||
|
}
|
||||||
|
}, [organizationId, policyId, loadQuery]);
|
||||||
|
|
||||||
|
if (!queryRef) {
|
||||||
|
return <UpdatePolicyViewSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<UpdatePolicyViewSkeleton />}>
|
||||||
|
{queryRef && <UpdatePolicyViewContent queryRef={queryRef} />}
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<29caed5a50be912ff48f9c131ce4045b>>
|
* @generated SignedSource<<6100eacd24cc3b7e7fe5dc4539323e75>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -18,11 +18,11 @@ export type CreatePolicyInput = {
|
|||||||
reviewDate?: string | null | undefined;
|
reviewDate?: string | null | undefined;
|
||||||
status: PolicyStatus;
|
status: PolicyStatus;
|
||||||
};
|
};
|
||||||
export type CreatePolicyPageMutation$variables = {
|
export type CreatePolicyViewMutation$variables = {
|
||||||
connections: ReadonlyArray<string>;
|
connections: ReadonlyArray<string>;
|
||||||
input: CreatePolicyInput;
|
input: CreatePolicyInput;
|
||||||
};
|
};
|
||||||
export type CreatePolicyPageMutation$data = {
|
export type CreatePolicyViewMutation$data = {
|
||||||
readonly createPolicy: {
|
readonly createPolicy: {
|
||||||
readonly policyEdge: {
|
readonly policyEdge: {
|
||||||
readonly node: {
|
readonly node: {
|
||||||
@@ -39,9 +39,9 @@ export type CreatePolicyPageMutation$data = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type CreatePolicyPageMutation = {
|
export type CreatePolicyViewMutation = {
|
||||||
response: CreatePolicyPageMutation$data;
|
response: CreatePolicyViewMutation$data;
|
||||||
variables: CreatePolicyPageMutation$variables;
|
variables: CreatePolicyViewMutation$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -147,7 +147,7 @@ return {
|
|||||||
],
|
],
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "CreatePolicyPageMutation",
|
"name": "CreatePolicyViewMutation",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -172,7 +172,7 @@ return {
|
|||||||
(v0/*: any*/)
|
(v0/*: any*/)
|
||||||
],
|
],
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "CreatePolicyPageMutation",
|
"name": "CreatePolicyViewMutation",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -205,16 +205,16 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "cf17928879d0f2b2e68367ed2c132d0a",
|
"cacheID": "732425c047d258a0c592d60073c4fcbd",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "CreatePolicyPageMutation",
|
"name": "CreatePolicyViewMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation CreatePolicyPageMutation(\n $input: CreatePolicyInput!\n) {\n createPolicy(input: $input) {\n policyEdge {\n node {\n id\n name\n content\n status\n reviewDate\n owner {\n id\n fullName\n }\n }\n }\n }\n}\n"
|
"text": "mutation CreatePolicyViewMutation(\n $input: CreatePolicyInput!\n) {\n createPolicy(input: $input) {\n policyEdge {\n node {\n id\n name\n content\n status\n reviewDate\n owner {\n id\n fullName\n }\n }\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "1ddb9b7010ecf1fe55e2b56aaa034295";
|
(node as any).hash = "af0c4091836cf3c833749da8495e378f";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<21831aed35a902922b54a96fcb5c8272>>
|
* @generated SignedSource<<0d6c35628ee18fe62e21ea579a02d118>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -10,17 +10,17 @@
|
|||||||
|
|
||||||
import { ConcreteRequest } from 'relay-runtime';
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
import { FragmentRefs } from "relay-runtime";
|
import { FragmentRefs } from "relay-runtime";
|
||||||
export type CreatePolicyPageQuery$variables = {
|
export type CreatePolicyViewQuery$variables = {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
};
|
};
|
||||||
export type CreatePolicyPageQuery$data = {
|
export type CreatePolicyViewQuery$data = {
|
||||||
readonly organization: {
|
readonly organization: {
|
||||||
readonly " $fragmentSpreads": FragmentRefs<"PeopleSelector_organization">;
|
readonly " $fragmentSpreads": FragmentRefs<"PeopleSelector_organization">;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type CreatePolicyPageQuery = {
|
export type CreatePolicyViewQuery = {
|
||||||
response: CreatePolicyPageQuery$data;
|
response: CreatePolicyViewQuery$data;
|
||||||
variables: CreatePolicyPageQuery$variables;
|
variables: CreatePolicyViewQuery$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -72,7 +72,7 @@ return {
|
|||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "CreatePolicyPageQuery",
|
"name": "CreatePolicyViewQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": "organization",
|
"alias": "organization",
|
||||||
@@ -98,7 +98,7 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "CreatePolicyPageQuery",
|
"name": "CreatePolicyViewQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": "organization",
|
"alias": "organization",
|
||||||
@@ -215,16 +215,16 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "e9d10cdbee082ff5e5f311b547bbc73d",
|
"cacheID": "85c335b0eeb8dc59c5255083f3c3c7e8",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "CreatePolicyPageQuery",
|
"name": "CreatePolicyViewQuery",
|
||||||
"operationKind": "query",
|
"operationKind": "query",
|
||||||
"text": "query CreatePolicyPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ...PeopleSelector_organization\n id\n }\n}\n\nfragment PeopleSelector_organization on Organization {\n id\n peoples(first: 100, orderBy: {direction: ASC, field: FULL_NAME}) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n"
|
"text": "query CreatePolicyViewQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ...PeopleSelector_organization\n id\n }\n}\n\nfragment PeopleSelector_organization on Organization {\n id\n peoples(first: 100, orderBy: {direction: ASC, field: FULL_NAME}) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "9ab69c4bfcba5c896760f297ab38ff51";
|
(node as any).hash = "9184af33ef358fb3c201d41469d49cc8";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<a49c4b1ca92e109b1091a1431fe0c871>>
|
* @generated SignedSource<<605e2b8b9bc6fdd85a121a1f1904cc00>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -10,10 +10,10 @@
|
|||||||
|
|
||||||
import { ConcreteRequest } from 'relay-runtime';
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
export type PolicyStatus = "ACTIVE" | "DRAFT";
|
export type PolicyStatus = "ACTIVE" | "DRAFT";
|
||||||
export type PolicyListPageQuery$variables = {
|
export type PolicyListViewQuery$variables = {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
};
|
};
|
||||||
export type PolicyListPageQuery$data = {
|
export type PolicyListViewQuery$data = {
|
||||||
readonly organization: {
|
readonly organization: {
|
||||||
readonly policies?: {
|
readonly policies?: {
|
||||||
readonly edges: ReadonlyArray<{
|
readonly edges: ReadonlyArray<{
|
||||||
@@ -29,9 +29,9 @@ export type PolicyListPageQuery$data = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type PolicyListPageQuery = {
|
export type PolicyListViewQuery = {
|
||||||
response: PolicyListPageQuery$data;
|
response: PolicyListViewQuery$data;
|
||||||
variables: PolicyListPageQuery$variables;
|
variables: PolicyListViewQuery$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -168,7 +168,7 @@ return {
|
|||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "PolicyListPageQuery",
|
"name": "PolicyListViewQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": "organization",
|
"alias": "organization",
|
||||||
@@ -186,7 +186,7 @@ return {
|
|||||||
"args": null,
|
"args": null,
|
||||||
"concreteType": "PolicyConnection",
|
"concreteType": "PolicyConnection",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "__PolicyListPage_policies_connection",
|
"name": "__PolicyListView_policies_connection",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": (v4/*: any*/),
|
"selections": (v4/*: any*/),
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
@@ -206,7 +206,7 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "PolicyListPageQuery",
|
"name": "PolicyListViewQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": "organization",
|
"alias": "organization",
|
||||||
@@ -235,7 +235,7 @@ return {
|
|||||||
"args": (v5/*: any*/),
|
"args": (v5/*: any*/),
|
||||||
"filters": null,
|
"filters": null,
|
||||||
"handle": "connection",
|
"handle": "connection",
|
||||||
"key": "PolicyListPage_policies",
|
"key": "PolicyListView_policies",
|
||||||
"kind": "LinkedHandle",
|
"kind": "LinkedHandle",
|
||||||
"name": "policies"
|
"name": "policies"
|
||||||
}
|
}
|
||||||
@@ -250,7 +250,7 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "5aa6e466b6daa957aa9c582530e385ec",
|
"cacheID": "361b81169a803b656d673b7b8d1c5583",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"connection": [
|
"connection": [
|
||||||
@@ -265,13 +265,13 @@ return {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"name": "PolicyListPageQuery",
|
"name": "PolicyListViewQuery",
|
||||||
"operationKind": "query",
|
"operationKind": "query",
|
||||||
"text": "query PolicyListPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n policies(first: 25) {\n edges {\n node {\n id\n name\n content\n createdAt\n updatedAt\n status\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n id\n }\n}\n"
|
"text": "query PolicyListViewQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n policies(first: 25) {\n edges {\n node {\n id\n name\n content\n createdAt\n updatedAt\n status\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n id\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "853e3413fd1b3f8781b11a1b7679c08f";
|
(node as any).hash = "c0ed3d1334a535f752062c6479ed3a7e";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<3f819f2e923960fed57f95a4535782be>>
|
* @generated SignedSource<<4e559f39bd9ec2b3782113ef034cad03>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -12,18 +12,18 @@ import { ConcreteRequest } from 'relay-runtime';
|
|||||||
export type DeletePolicyInput = {
|
export type DeletePolicyInput = {
|
||||||
policyId: string;
|
policyId: string;
|
||||||
};
|
};
|
||||||
export type PolicyOverviewPageDeleteMutation$variables = {
|
export type PolicyViewDeleteMutation$variables = {
|
||||||
connections: ReadonlyArray<string>;
|
connections: ReadonlyArray<string>;
|
||||||
input: DeletePolicyInput;
|
input: DeletePolicyInput;
|
||||||
};
|
};
|
||||||
export type PolicyOverviewPageDeleteMutation$data = {
|
export type PolicyViewDeleteMutation$data = {
|
||||||
readonly deletePolicy: {
|
readonly deletePolicy: {
|
||||||
readonly deletedPolicyId: string;
|
readonly deletedPolicyId: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type PolicyOverviewPageDeleteMutation = {
|
export type PolicyViewDeleteMutation = {
|
||||||
response: PolicyOverviewPageDeleteMutation$data;
|
response: PolicyViewDeleteMutation$data;
|
||||||
variables: PolicyOverviewPageDeleteMutation$variables;
|
variables: PolicyViewDeleteMutation$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -59,7 +59,7 @@ return {
|
|||||||
],
|
],
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "PolicyOverviewPageDeleteMutation",
|
"name": "PolicyViewDeleteMutation",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -84,7 +84,7 @@ return {
|
|||||||
(v0/*: any*/)
|
(v0/*: any*/)
|
||||||
],
|
],
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "PolicyOverviewPageDeleteMutation",
|
"name": "PolicyViewDeleteMutation",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -117,16 +117,16 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "0a54108fe8b1d0e8c306ede62182d0b4",
|
"cacheID": "67e67147e0c7ec0489346b1ad86899c9",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "PolicyOverviewPageDeleteMutation",
|
"name": "PolicyViewDeleteMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation PolicyOverviewPageDeleteMutation(\n $input: DeletePolicyInput!\n) {\n deletePolicy(input: $input) {\n deletedPolicyId\n }\n}\n"
|
"text": "mutation PolicyViewDeleteMutation(\n $input: DeletePolicyInput!\n) {\n deletePolicy(input: $input) {\n deletedPolicyId\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "2ca6b448cda30ef9d66197344469c15b";
|
(node as any).hash = "7d421e9b068f3f676a4c3069769b1c18";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<67ee63bc3751efdd9ecdf2ad9ab5e197>>
|
* @generated SignedSource<<eaeef5694bdd615c03bea3b3f8957df3>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -10,10 +10,10 @@
|
|||||||
|
|
||||||
import { ConcreteRequest } from 'relay-runtime';
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
export type PolicyStatus = "ACTIVE" | "DRAFT";
|
export type PolicyStatus = "ACTIVE" | "DRAFT";
|
||||||
export type PolicyOverviewPageQuery$variables = {
|
export type PolicyViewQuery$variables = {
|
||||||
policyId: string;
|
policyId: string;
|
||||||
};
|
};
|
||||||
export type PolicyOverviewPageQuery$data = {
|
export type PolicyViewQuery$data = {
|
||||||
readonly node: {
|
readonly node: {
|
||||||
readonly content?: string;
|
readonly content?: string;
|
||||||
readonly createdAt?: string;
|
readonly createdAt?: string;
|
||||||
@@ -29,9 +29,9 @@ export type PolicyOverviewPageQuery$data = {
|
|||||||
readonly updatedAt?: string;
|
readonly updatedAt?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type PolicyOverviewPageQuery = {
|
export type PolicyViewQuery = {
|
||||||
response: PolicyOverviewPageQuery$data;
|
response: PolicyViewQuery$data;
|
||||||
variables: PolicyOverviewPageQuery$variables;
|
variables: PolicyViewQuery$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -136,7 +136,7 @@ return {
|
|||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "PolicyOverviewPageQuery",
|
"name": "PolicyViewQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -159,7 +159,7 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "PolicyOverviewPageQuery",
|
"name": "PolicyViewQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -184,16 +184,16 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "c57fd1a2ff5ffa776bebb9f15bd534bc",
|
"cacheID": "26bf57eeab9c600a3146e01085ecae8e",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "PolicyOverviewPageQuery",
|
"name": "PolicyViewQuery",
|
||||||
"operationKind": "query",
|
"operationKind": "query",
|
||||||
"text": "query PolicyOverviewPageQuery(\n $policyId: ID!\n) {\n node(id: $policyId) {\n __typename\n id\n ... on Policy {\n name\n content\n createdAt\n updatedAt\n reviewDate\n status\n owner {\n id\n fullName\n primaryEmailAddress\n }\n }\n }\n}\n"
|
"text": "query PolicyViewQuery(\n $policyId: ID!\n) {\n node(id: $policyId) {\n __typename\n id\n ... on Policy {\n name\n content\n createdAt\n updatedAt\n reviewDate\n status\n owner {\n id\n fullName\n primaryEmailAddress\n }\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "e6c4f286358df61d18ea6deb367d7182";
|
(node as any).hash = "0fcc28fc290df69d14d7d549bd09216f";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<f37225ac5a4b6774dadbd4f23c3bbfe9>>
|
* @generated SignedSource<<9a97197f1f0eec8c00d0a58e0b07e8bc>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -19,10 +19,10 @@ export type UpdatePolicyInput = {
|
|||||||
reviewDate?: string | null | undefined;
|
reviewDate?: string | null | undefined;
|
||||||
status?: PolicyStatus | null | undefined;
|
status?: PolicyStatus | null | undefined;
|
||||||
};
|
};
|
||||||
export type UpdatePolicyPageMutation$variables = {
|
export type UpdatePolicyViewMutation$variables = {
|
||||||
input: UpdatePolicyInput;
|
input: UpdatePolicyInput;
|
||||||
};
|
};
|
||||||
export type UpdatePolicyPageMutation$data = {
|
export type UpdatePolicyViewMutation$data = {
|
||||||
readonly updatePolicy: {
|
readonly updatePolicy: {
|
||||||
readonly policy: {
|
readonly policy: {
|
||||||
readonly content: string;
|
readonly content: string;
|
||||||
@@ -38,9 +38,9 @@ export type UpdatePolicyPageMutation$data = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type UpdatePolicyPageMutation = {
|
export type UpdatePolicyViewMutation = {
|
||||||
response: UpdatePolicyPageMutation$data;
|
response: UpdatePolicyViewMutation$data;
|
||||||
variables: UpdatePolicyPageMutation$variables;
|
variables: UpdatePolicyViewMutation$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -148,7 +148,7 @@ return {
|
|||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "UpdatePolicyPageMutation",
|
"name": "UpdatePolicyViewMutation",
|
||||||
"selections": (v2/*: any*/),
|
"selections": (v2/*: any*/),
|
||||||
"type": "Mutation",
|
"type": "Mutation",
|
||||||
"abstractKey": null
|
"abstractKey": null
|
||||||
@@ -157,20 +157,20 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "UpdatePolicyPageMutation",
|
"name": "UpdatePolicyViewMutation",
|
||||||
"selections": (v2/*: any*/)
|
"selections": (v2/*: any*/)
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "cab3ba1fb88944fade60b5d6a0e0b7b9",
|
"cacheID": "1ba586009eaf6e79dd2ab5862856cc63",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "UpdatePolicyPageMutation",
|
"name": "UpdatePolicyViewMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation UpdatePolicyPageMutation(\n $input: UpdatePolicyInput!\n) {\n updatePolicy(input: $input) {\n policy {\n id\n name\n content\n status\n version\n reviewDate\n owner {\n id\n fullName\n }\n }\n }\n}\n"
|
"text": "mutation UpdatePolicyViewMutation(\n $input: UpdatePolicyInput!\n) {\n updatePolicy(input: $input) {\n policy {\n id\n name\n content\n status\n version\n reviewDate\n owner {\n id\n fullName\n }\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "51e4a0e297540f4e83dcb6d809430ac7";
|
(node as any).hash = "47570dfcceba283c51a4ef2f88143d39";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<0ee0955e5b5136c90cd3230dd2f10163>>
|
* @generated SignedSource<<b3ede0dedb94e95e89f0d67715a536b3>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -11,11 +11,11 @@
|
|||||||
import { ConcreteRequest } from 'relay-runtime';
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
import { FragmentRefs } from "relay-runtime";
|
import { FragmentRefs } from "relay-runtime";
|
||||||
export type PolicyStatus = "ACTIVE" | "DRAFT";
|
export type PolicyStatus = "ACTIVE" | "DRAFT";
|
||||||
export type UpdatePolicyPageQuery$variables = {
|
export type UpdatePolicyViewQuery$variables = {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
policyId: string;
|
policyId: string;
|
||||||
};
|
};
|
||||||
export type UpdatePolicyPageQuery$data = {
|
export type UpdatePolicyViewQuery$data = {
|
||||||
readonly organization: {
|
readonly organization: {
|
||||||
readonly " $fragmentSpreads": FragmentRefs<"PeopleSelector_organization">;
|
readonly " $fragmentSpreads": FragmentRefs<"PeopleSelector_organization">;
|
||||||
};
|
};
|
||||||
@@ -32,9 +32,9 @@ export type UpdatePolicyPageQuery$data = {
|
|||||||
readonly version?: number;
|
readonly version?: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type UpdatePolicyPageQuery = {
|
export type UpdatePolicyViewQuery = {
|
||||||
response: UpdatePolicyPageQuery$data;
|
response: UpdatePolicyViewQuery$data;
|
||||||
variables: UpdatePolicyPageQuery$variables;
|
variables: UpdatePolicyViewQuery$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -161,7 +161,7 @@ return {
|
|||||||
],
|
],
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "UpdatePolicyPageQuery",
|
"name": "UpdatePolicyViewQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": "policy",
|
"alias": "policy",
|
||||||
@@ -203,7 +203,7 @@ return {
|
|||||||
(v0/*: any*/)
|
(v0/*: any*/)
|
||||||
],
|
],
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "UpdatePolicyPageQuery",
|
"name": "UpdatePolicyViewQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": "policy",
|
"alias": "policy",
|
||||||
@@ -328,16 +328,16 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "09418de2f0938515d2f5f67876a1cfaf",
|
"cacheID": "16bd0bbc57d75f5c4d40eccd560b3da5",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "UpdatePolicyPageQuery",
|
"name": "UpdatePolicyViewQuery",
|
||||||
"operationKind": "query",
|
"operationKind": "query",
|
||||||
"text": "query UpdatePolicyPageQuery(\n $policyId: ID!\n $organizationId: ID!\n) {\n policy: node(id: $policyId) {\n __typename\n id\n ... on Policy {\n name\n content\n status\n version\n reviewDate\n owner {\n id\n fullName\n }\n }\n }\n organization: node(id: $organizationId) {\n __typename\n ...PeopleSelector_organization\n id\n }\n}\n\nfragment PeopleSelector_organization on Organization {\n id\n peoples(first: 100, orderBy: {direction: ASC, field: FULL_NAME}) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n"
|
"text": "query UpdatePolicyViewQuery(\n $policyId: ID!\n $organizationId: ID!\n) {\n policy: node(id: $policyId) {\n __typename\n id\n ... on Policy {\n name\n content\n status\n version\n reviewDate\n owner {\n id\n fullName\n }\n }\n }\n organization: node(id: $organizationId) {\n __typename\n ...PeopleSelector_organization\n id\n }\n}\n\nfragment PeopleSelector_organization on Organization {\n id\n peoples(first: 100, orderBy: {direction: ASC, field: FULL_NAME}) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "abba76ee5208701e443130d650b5c8d1";
|
(node as any).hash = "1494ae295ead283c9784cc748c5536f7";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -5,7 +5,7 @@ import { ErrorBoundaryWithLocation } from "../ErrorBoundary";
|
|||||||
|
|
||||||
const VendorListView = lazy(() => import("./VendorListView"));
|
const VendorListView = lazy(() => import("./VendorListView"));
|
||||||
|
|
||||||
export function VendorListSkeleton() {
|
export function VendorListViewSkeleton() {
|
||||||
return (
|
return (
|
||||||
<PageTemplateSkeleton
|
<PageTemplateSkeleton
|
||||||
title="Vendors"
|
title="Vendors"
|
||||||
@@ -36,7 +36,7 @@ export function VendorListPage() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense key={location.pathname} fallback={<VendorListSkeleton />}>
|
<Suspense key={location.pathname} fallback={<VendorListViewSkeleton />}>
|
||||||
<ErrorBoundaryWithLocation>
|
<ErrorBoundaryWithLocation>
|
||||||
<VendorListView />
|
<VendorListView />
|
||||||
</ErrorBoundaryWithLocation>
|
</ErrorBoundaryWithLocation>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { Link } from "react-router";
|
|||||||
import Fuse from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { PageTemplate } from "@/components/PageTemplate";
|
import { PageTemplate } from "@/components/PageTemplate";
|
||||||
import { VendorListSkeleton } from "./VendorListPage";
|
import { VendorListViewSkeleton } from "./VendorListPage";
|
||||||
import { VendorListViewQuery as VendorListViewQueryType } from "./__generated__/VendorListViewQuery.graphql";
|
import { VendorListViewQuery as VendorListViewQueryType } from "./__generated__/VendorListViewQuery.graphql";
|
||||||
import { VendorListViewCreateVendorMutation } from "./__generated__/VendorListViewCreateVendorMutation.graphql";
|
import { VendorListViewCreateVendorMutation } from "./__generated__/VendorListViewCreateVendorMutation.graphql";
|
||||||
import { VendorListViewDeleteVendorMutation } from "./__generated__/VendorListViewDeleteVendorMutation.graphql";
|
import { VendorListViewDeleteVendorMutation } from "./__generated__/VendorListViewDeleteVendorMutation.graphql";
|
||||||
@@ -478,11 +478,11 @@ export default function VendorListView() {
|
|||||||
}, [loadQuery, organizationId]);
|
}, [loadQuery, organizationId]);
|
||||||
|
|
||||||
if (!queryRef) {
|
if (!queryRef) {
|
||||||
return <VendorListSkeleton />;
|
return <VendorListViewSkeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<VendorListSkeleton />}>
|
<Suspense fallback={<VendorListViewSkeleton />}>
|
||||||
<VendorListContent queryRef={queryRef} />
|
<VendorListContent queryRef={queryRef} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
34
apps/console/src/pages/organizations/vendors/VendorPage.tsx
vendored
Normal file
34
apps/console/src/pages/organizations/vendors/VendorPage.tsx
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { PageTemplateSkeleton } from "@/components/PageTemplate";
|
||||||
|
import { lazy, Suspense } from "react";
|
||||||
|
import { useLocation } from "react-router";
|
||||||
|
import { ErrorBoundaryWithLocation } from "../ErrorBoundary";
|
||||||
|
|
||||||
|
const VendorView = lazy(() => import("./VendorView"));
|
||||||
|
|
||||||
|
export function VendorViewSkeleton() {
|
||||||
|
return (
|
||||||
|
<PageTemplateSkeleton>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="h-8 w-48 bg-muted animate-pulse rounded" />
|
||||||
|
<div className="h-4 w-96 bg-muted animate-pulse rounded" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{[1, 2].map((i) => (
|
||||||
|
<div key={i} className="h-20 bg-muted animate-pulse rounded-lg" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</PageTemplateSkeleton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VendorPage() {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense key={location.pathname} fallback={<VendorViewSkeleton />}>
|
||||||
|
<ErrorBoundaryWithLocation>
|
||||||
|
<VendorView />
|
||||||
|
</ErrorBoundaryWithLocation>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,13 +14,14 @@ import {
|
|||||||
useMutation,
|
useMutation,
|
||||||
} from "react-relay";
|
} from "react-relay";
|
||||||
import { Suspense, useEffect, useState, useCallback } from "react";
|
import { Suspense, useEffect, useState, useCallback } from "react";
|
||||||
import type { VendorOverviewPageQuery as VendorOverviewPageQueryType } from "./__generated__/VendorOverviewPageQuery.graphql";
|
import type { VendorViewQuery as VendorViewQueryType } from "./__generated__/VendorViewQuery.graphql";
|
||||||
import { useParams } from "react-router";
|
import { useParams } from "react-router";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { PageTemplate, PageTemplateSkeleton } from "@/components/PageTemplate";
|
import { PageTemplate } from "@/components/PageTemplate";
|
||||||
|
import { VendorViewSkeleton } from "./VendorPage";
|
||||||
|
|
||||||
const vendorOverviewPageQuery = graphql`
|
const vendorViewQuery = graphql`
|
||||||
query VendorOverviewPageQuery($vendorId: ID!) {
|
query VendorViewQuery($vendorId: ID!) {
|
||||||
node(id: $vendorId) {
|
node(id: $vendorId) {
|
||||||
... on Vendor {
|
... on Vendor {
|
||||||
id
|
id
|
||||||
@@ -42,7 +43,7 @@ const vendorOverviewPageQuery = graphql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const updateVendorMutation = graphql`
|
const updateVendorMutation = graphql`
|
||||||
mutation VendorOverviewPageUpdateVendorMutation($input: UpdateVendorInput!) {
|
mutation VendorViewUpdateVendorMutation($input: UpdateVendorInput!) {
|
||||||
updateVendor(input: $input) {
|
updateVendor(input: $input) {
|
||||||
vendor {
|
vendor {
|
||||||
id
|
id
|
||||||
@@ -106,12 +107,12 @@ function formatDateForAPI(dateStr: string): string {
|
|||||||
return date.toISOString();
|
return date.toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function VendorOverviewPageContent({
|
function VendorViewContent({
|
||||||
queryRef,
|
queryRef,
|
||||||
}: {
|
}: {
|
||||||
queryRef: PreloadedQuery<VendorOverviewPageQueryType>;
|
queryRef: PreloadedQuery<VendorViewQueryType>;
|
||||||
}) {
|
}) {
|
||||||
const data = usePreloadedQuery(vendorOverviewPageQuery, queryRef);
|
const data = usePreloadedQuery(vendorViewQuery, queryRef);
|
||||||
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 || "",
|
||||||
@@ -126,9 +127,7 @@ function VendorOverviewPageContent({
|
|||||||
privacyPolicyUrl: data.node.privacyPolicyUrl || "",
|
privacyPolicyUrl: data.node.privacyPolicyUrl || "",
|
||||||
});
|
});
|
||||||
const [commit] = useMutation(updateVendorMutation);
|
const [commit] = useMutation(updateVendorMutation);
|
||||||
const [, loadQuery] = useQueryLoader<VendorOverviewPageQueryType>(
|
const [, loadQuery] = useQueryLoader<VendorViewQueryType>(vendorViewQuery);
|
||||||
vendorOverviewPageQuery
|
|
||||||
);
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const hasChanges = editedFields.size > 0;
|
const hasChanges = editedFields.size > 0;
|
||||||
@@ -393,39 +392,22 @@ function VendorOverviewPageContent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VendorOverviewPageSkeleton() {
|
export default function VendorView() {
|
||||||
return (
|
|
||||||
<PageTemplateSkeleton>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="h-8 w-48 bg-muted animate-pulse rounded" />
|
|
||||||
<div className="h-4 w-96 bg-muted animate-pulse rounded" />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{[1, 2].map((i) => (
|
|
||||||
<div key={i} className="h-20 bg-muted animate-pulse rounded-lg" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</PageTemplateSkeleton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function VendorOverviewPage() {
|
|
||||||
const { vendorId } = useParams();
|
const { vendorId } = useParams();
|
||||||
const [queryRef, loadQuery] = useQueryLoader<VendorOverviewPageQueryType>(
|
const [queryRef, loadQuery] =
|
||||||
vendorOverviewPageQuery
|
useQueryLoader<VendorViewQueryType>(vendorViewQuery);
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadQuery({ vendorId: vendorId! });
|
loadQuery({ vendorId: vendorId! });
|
||||||
}, [loadQuery, vendorId]);
|
}, [loadQuery, vendorId]);
|
||||||
|
|
||||||
if (!queryRef) {
|
if (!queryRef) {
|
||||||
return <VendorOverviewPageSkeleton />;
|
return <VendorViewSkeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<VendorOverviewPageSkeleton />}>
|
<Suspense fallback={<VendorViewSkeleton />}>
|
||||||
<VendorOverviewPageContent queryRef={queryRef} />
|
<VendorViewContent queryRef={queryRef} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<e6f91ab15045c3a5139af514ed4fe3c1>>
|
* @generated SignedSource<<dd3ffb0b93fc3b99db6d95ae1074922b>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -11,10 +11,10 @@
|
|||||||
import { ConcreteRequest } from 'relay-runtime';
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
export type RiskTier = "CRITICAL" | "GENERAL" | "SIGNIFICANT";
|
export type RiskTier = "CRITICAL" | "GENERAL" | "SIGNIFICANT";
|
||||||
export type ServiceCriticality = "HIGH" | "LOW" | "MEDIUM";
|
export type ServiceCriticality = "HIGH" | "LOW" | "MEDIUM";
|
||||||
export type VendorOverviewPageQuery$variables = {
|
export type VendorViewQuery$variables = {
|
||||||
vendorId: string;
|
vendorId: string;
|
||||||
};
|
};
|
||||||
export type VendorOverviewPageQuery$data = {
|
export type VendorViewQuery$data = {
|
||||||
readonly node: {
|
readonly node: {
|
||||||
readonly createdAt?: string;
|
readonly createdAt?: string;
|
||||||
readonly description?: string;
|
readonly description?: string;
|
||||||
@@ -31,9 +31,9 @@ export type VendorOverviewPageQuery$data = {
|
|||||||
readonly version?: number;
|
readonly version?: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type VendorOverviewPageQuery = {
|
export type VendorViewQuery = {
|
||||||
response: VendorOverviewPageQuery$data;
|
response: VendorViewQuery$data;
|
||||||
variables: VendorOverviewPageQuery$variables;
|
variables: VendorViewQuery$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -147,7 +147,7 @@ return {
|
|||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "VendorOverviewPageQuery",
|
"name": "VendorViewQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -188,7 +188,7 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "VendorOverviewPageQuery",
|
"name": "VendorViewQuery",
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -231,16 +231,16 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "9163462a149138110e8573e0b28350f9",
|
"cacheID": "3569222e84bb1fa070b4ce2145d677ac",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "VendorOverviewPageQuery",
|
"name": "VendorViewQuery",
|
||||||
"operationKind": "query",
|
"operationKind": "query",
|
||||||
"text": "query VendorOverviewPageQuery(\n $vendorId: ID!\n) {\n node(id: $vendorId) {\n __typename\n ... on Vendor {\n id\n name\n description\n serviceStartAt\n serviceTerminationAt\n serviceCriticality\n riskTier\n statusPageUrl\n termsOfServiceUrl\n privacyPolicyUrl\n createdAt\n updatedAt\n version\n }\n id\n }\n}\n"
|
"text": "query VendorViewQuery(\n $vendorId: ID!\n) {\n node(id: $vendorId) {\n __typename\n ... on Vendor {\n id\n name\n description\n serviceStartAt\n serviceTerminationAt\n serviceCriticality\n riskTier\n statusPageUrl\n termsOfServiceUrl\n privacyPolicyUrl\n createdAt\n updatedAt\n version\n }\n id\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "56567f61e1016e70d6993ac132ca8382";
|
(node as any).hash = "9cdc25f48997786054c22246fb417db8";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<918d7356c117ff6830c2c41b7db72ddd>>
|
* @generated SignedSource<<8799e334baf7ee70b0a03519f6da8f97>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -24,10 +24,10 @@ export type UpdateVendorInput = {
|
|||||||
statusPageUrl?: string | null | undefined;
|
statusPageUrl?: string | null | undefined;
|
||||||
termsOfServiceUrl?: string | null | undefined;
|
termsOfServiceUrl?: string | null | undefined;
|
||||||
};
|
};
|
||||||
export type VendorOverviewPageUpdateVendorMutation$variables = {
|
export type VendorViewUpdateVendorMutation$variables = {
|
||||||
input: UpdateVendorInput;
|
input: UpdateVendorInput;
|
||||||
};
|
};
|
||||||
export type VendorOverviewPageUpdateVendorMutation$data = {
|
export type VendorViewUpdateVendorMutation$data = {
|
||||||
readonly updateVendor: {
|
readonly updateVendor: {
|
||||||
readonly vendor: {
|
readonly vendor: {
|
||||||
readonly description: string;
|
readonly description: string;
|
||||||
@@ -45,9 +45,9 @@ export type VendorOverviewPageUpdateVendorMutation$data = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type VendorOverviewPageUpdateVendorMutation = {
|
export type VendorViewUpdateVendorMutation = {
|
||||||
response: VendorOverviewPageUpdateVendorMutation$data;
|
response: VendorViewUpdateVendorMutation$data;
|
||||||
variables: VendorOverviewPageUpdateVendorMutation$variables;
|
variables: VendorViewUpdateVendorMutation$variables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const node: ConcreteRequest = (function(){
|
const node: ConcreteRequest = (function(){
|
||||||
@@ -177,7 +177,7 @@ return {
|
|||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "VendorOverviewPageUpdateVendorMutation",
|
"name": "VendorViewUpdateVendorMutation",
|
||||||
"selections": (v1/*: any*/),
|
"selections": (v1/*: any*/),
|
||||||
"type": "Mutation",
|
"type": "Mutation",
|
||||||
"abstractKey": null
|
"abstractKey": null
|
||||||
@@ -186,20 +186,20 @@ return {
|
|||||||
"operation": {
|
"operation": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "VendorOverviewPageUpdateVendorMutation",
|
"name": "VendorViewUpdateVendorMutation",
|
||||||
"selections": (v1/*: any*/)
|
"selections": (v1/*: any*/)
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "30449b25e96907e852ae1638ab425111",
|
"cacheID": "d6de753f4ecc8f59921e246e178bf8a0",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "VendorOverviewPageUpdateVendorMutation",
|
"name": "VendorViewUpdateVendorMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation VendorOverviewPageUpdateVendorMutation(\n $input: UpdateVendorInput!\n) {\n updateVendor(input: $input) {\n vendor {\n id\n name\n description\n serviceStartAt\n serviceTerminationAt\n serviceCriticality\n riskTier\n statusPageUrl\n termsOfServiceUrl\n privacyPolicyUrl\n updatedAt\n version\n }\n }\n}\n"
|
"text": "mutation VendorViewUpdateVendorMutation(\n $input: UpdateVendorInput!\n) {\n updateVendor(input: $input) {\n vendor {\n id\n name\n description\n serviceStartAt\n serviceTerminationAt\n serviceCriticality\n riskTier\n statusPageUrl\n termsOfServiceUrl\n privacyPolicyUrl\n updatedAt\n version\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "e07426684611544b6f4c8d1668a19794";
|
(node as any).hash = "836cf8657449473503596456b8deb873";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
Reference in New Issue
Block a user