Use framework logo on console & trust

Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
Émile Ré
2025-12-16 19:05:34 +01:00
parent 427565c521
commit 8c772661bf
34 changed files with 375 additions and 337 deletions

View File

@@ -1,15 +0,0 @@
import { availableFrameworks } from "@probo/helpers";
import { Avatar } from "@probo/ui";
const availableLogos = new Map(
availableFrameworks.map((framework) => [framework.name, framework.logo])
);
export function FrameworkLogo({ name }: { name: string }) {
const logo = availableLogos.get(name);
return logo ? (
<img src={logo} alt="" className="h-12" />
) : (
<Avatar name={name} size="l" className="size-12" />
);
}

View File

@@ -36,6 +36,8 @@ export const auditNodeQuery = graphql`
framework {
id
name
lightLogoURL
darkLogoURL
}
organization {
id

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<fc4103e0e8b03c1b6b8ea07c4adcf095>>
* @generated SignedSource<<c802dc4c6e5b63e48b0050dfcf13b4b4>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -17,7 +17,9 @@ export type AuditGraphNodeQuery$data = {
readonly node: {
readonly createdAt?: any;
readonly framework?: {
readonly darkLogoURL: string | null | undefined;
readonly id: string;
readonly lightLogoURL: string | null | undefined;
readonly name: string;
};
readonly id?: string;
@@ -151,31 +153,47 @@ v9 = {
"name": "state",
"storageKey": null
},
v10 = [
(v2/*: any*/),
(v3/*: any*/)
],
v11 = {
v10 = {
"alias": null,
"args": null,
"concreteType": "Framework",
"kind": "LinkedField",
"name": "framework",
"plural": false,
"selections": (v10/*: any*/),
"selections": [
(v2/*: any*/),
(v3/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "lightLogoURL",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "darkLogoURL",
"storageKey": null
}
],
"storageKey": null
},
v12 = {
v11 = {
"alias": null,
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "organization",
"plural": false,
"selections": (v10/*: any*/),
"selections": [
(v2/*: any*/),
(v3/*: any*/)
],
"storageKey": null
},
v13 = {
v12 = {
"alias": null,
"args": null,
"kind": "ScalarField",
@@ -207,10 +225,10 @@ return {
(v7/*: any*/),
(v8/*: any*/),
(v9/*: any*/),
(v10/*: any*/),
(v11/*: any*/),
(v12/*: any*/),
(v6/*: any*/),
(v13/*: any*/)
(v12/*: any*/)
],
"type": "Audit",
"abstractKey": null
@@ -253,10 +271,10 @@ return {
(v7/*: any*/),
(v8/*: any*/),
(v9/*: any*/),
(v10/*: any*/),
(v11/*: any*/),
(v12/*: any*/),
(v6/*: any*/),
(v13/*: any*/)
(v12/*: any*/)
],
"type": "Audit",
"abstractKey": null
@@ -267,16 +285,16 @@ return {
]
},
"params": {
"cacheID": "eede00ab53ccd396e92b7fe6c1f1c2a2",
"cacheID": "af5fde4e1522c98b908630fdf1451ae9",
"id": null,
"metadata": {},
"name": "AuditGraphNodeQuery",
"operationKind": "query",
"text": "query AuditGraphNodeQuery(\n $auditId: ID!\n) {\n node(id: $auditId) {\n __typename\n ... on Audit {\n id\n name\n validFrom\n validUntil\n report {\n id\n filename\n mimeType\n size\n downloadUrl\n createdAt\n }\n reportUrl\n state\n framework {\n id\n name\n }\n organization {\n id\n name\n }\n createdAt\n updatedAt\n }\n id\n }\n}\n"
"text": "query AuditGraphNodeQuery(\n $auditId: ID!\n) {\n node(id: $auditId) {\n __typename\n ... on Audit {\n id\n name\n validFrom\n validUntil\n report {\n id\n filename\n mimeType\n size\n downloadUrl\n createdAt\n }\n reportUrl\n state\n framework {\n id\n name\n lightLogoURL\n darkLogoURL\n }\n organization {\n id\n name\n }\n createdAt\n updatedAt\n }\n id\n }\n}\n"
}
};
})();
(node as any).hash = "c07896ae39a7d16b42c7ee4216dc28fa";
(node as any).hash = "db92119c6d6f000963a0ff3d0fb74dfd";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<a67eb2a88e39d7eaf96910e0318dd1bc>>
* @generated SignedSource<<7aaf0b32e40fe8c723a28ab9590b62b5>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -243,6 +243,20 @@ return {
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "lightLogoURL",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "darkLogoURL",
"storageKey": null
},
(v3/*: any*/)
],
"storageKey": null
@@ -275,7 +289,7 @@ return {
]
},
"params": {
"cacheID": "b31afd68395ae800a18885bca3abfa1c",
"cacheID": "90e970007a500be89809ac0f2a77bccd",
"id": null,
"metadata": {
"connection": [
@@ -292,7 +306,7 @@ return {
},
"name": "FrameworkGraphListQuery",
"operationKind": "query",
"text": "query FrameworkGraphListQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n frameworks(first: 100) {\n edges {\n node {\n id\n ...FrameworksPageCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n id\n }\n}\n\nfragment FrameworksPageCardFragment on Framework {\n id\n name\n description\n}\n"
"text": "query FrameworkGraphListQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n frameworks(first: 100) {\n edges {\n node {\n id\n ...FrameworksPageCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n id\n }\n}\n\nfragment FrameworksPageCardFragment on Framework {\n id\n name\n description\n lightLogoURL\n darkLogoURL\n}\n"
}
};
})();

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<8901b5c750861080551542d26c2681d6>>
* @generated SignedSource<<6fcc115e1ab6df38ede8756968ae7a6a>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -123,6 +123,20 @@ return {
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "lightLogoURL",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "darkLogoURL",
"storageKey": null
},
{
"alias": null,
"args": null,
@@ -228,12 +242,12 @@ return {
]
},
"params": {
"cacheID": "29d6f638feb1bb633169abcfe083f5d2",
"cacheID": "68c09567aff1176075300c645f5d117d",
"id": null,
"metadata": {},
"name": "FrameworkGraphNodeQuery",
"operationKind": "query",
"text": "query FrameworkGraphNodeQuery(\n $frameworkId: ID!\n) {\n node(id: $frameworkId) {\n __typename\n ... on Framework {\n id\n name\n ...FrameworkDetailPageFragment\n }\n id\n }\n}\n\nfragment FrameworkDetailPageFragment on Framework {\n id\n name\n description\n organization {\n name\n id\n }\n controls(first: 250, orderBy: {field: SECTION_TITLE, direction: ASC}) {\n edges {\n node {\n id\n sectionTitle\n name\n status\n exclusionJustification\n }\n }\n }\n}\n"
"text": "query FrameworkGraphNodeQuery(\n $frameworkId: ID!\n) {\n node(id: $frameworkId) {\n __typename\n ... on Framework {\n id\n name\n ...FrameworkDetailPageFragment\n }\n id\n }\n}\n\nfragment FrameworkDetailPageFragment on Framework {\n id\n name\n description\n lightLogoURL\n darkLogoURL\n organization {\n name\n id\n }\n controls(first: 250, orderBy: {field: SECTION_TITLE, direction: ASC}) {\n edges {\n node {\n id\n sectionTitle\n name\n status\n exclusionJustification\n }\n }\n }\n}\n"
}
};
})();

View File

@@ -25,14 +25,24 @@ import {
IconArrowInbox,
useConfirm,
useToast,
FrameworkLogo,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { FrameworkLogo } from "/components/FrameworkLogo";
import { ControlledField } from "/components/form/ControlledField";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
import z from "zod";
import { getAuditStateLabel, getAuditStateVariant, auditStates, fileSize, sprintf, formatDatetime, formatError, formatDate, type GraphQLError } from "@probo/helpers";
import {
getAuditStateLabel,
getAuditStateVariant,
auditStates,
fileSize,
sprintf,
formatDatetime,
formatError,
formatDate,
type GraphQLError,
} from "@probo/helpers";
import type { AuditGraphNodeQuery } from "/hooks/graph/__generated__/AuditGraphNodeQuery.graphql";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
@@ -42,7 +52,13 @@ const updateAuditSchema = z.object({
name: z.string().nullable().optional(),
validFrom: z.string().optional(),
validUntil: z.string().optional(),
state: z.enum(["NOT_STARTED", "IN_PROGRESS", "COMPLETED", "REJECTED", "OUTDATED"]),
state: z.enum([
"NOT_STARTED",
"IN_PROGRESS",
"COMPLETED",
"REJECTED",
"OUTDATED",
]),
});
type Props = {
@@ -50,7 +66,10 @@ type Props = {
};
export default function AuditDetailsPage(props: Props) {
const audit = usePreloadedQuery<AuditGraphNodeQuery>(auditNodeQuery, props.queryRef);
const audit = usePreloadedQuery<AuditGraphNodeQuery>(
auditNodeQuery,
props.queryRef
);
const auditEntry = audit.node;
const { __ } = useTranslate();
const organizationId = useOrganizationId();
@@ -58,19 +77,20 @@ export default function AuditDetailsPage(props: Props) {
const navigate = useNavigate();
const deleteAudit = useDeleteAudit(
{ id: auditEntry.id!, framework: { name: auditEntry.framework!.name} },
{ id: auditEntry.id!, framework: { name: auditEntry.framework!.name } },
ConnectionHandler.getConnectionID(organizationId, "AuditsPage_audits"),
() => navigate(`/organizations/${organizationId}/audits`)
);
const { control, formState, handleSubmit, register, reset } = useFormWithSchema(updateAuditSchema, {
defaultValues: {
name: auditEntry.name || null,
validFrom: auditEntry.validFrom?.split('T')[0] || "",
validUntil: auditEntry.validUntil?.split('T')[0] || "",
state: auditEntry.state || "NOT_STARTED",
},
});
const { control, formState, handleSubmit, register, reset } =
useFormWithSchema(updateAuditSchema, {
defaultValues: {
name: auditEntry.name || null,
validFrom: auditEntry.validFrom?.split("T")[0] || "",
validUntil: auditEntry.validUntil?.split("T")[0] || "",
state: auditEntry.state || "NOT_STARTED",
},
});
const updateAudit = useUpdateAudit();
const [uploadAuditReport, isUploading] = useUploadAuditReport();
@@ -98,7 +118,10 @@ export default function AuditDetailsPage(props: Props) {
} catch (error) {
toast({
title: __("Error"),
description: formatError(__("Failed to update audit"), error as GraphQLError),
description: formatError(
__("Failed to update audit"),
error as GraphQLError
),
variant: "error",
});
}
@@ -131,7 +154,9 @@ export default function AuditDetailsPage(props: Props) {
to: `/organizations/${organizationId}/audits`,
},
{
label: (auditEntry.name || auditEntry.framework?.name) ?? __("Unknown Audit"),
label:
(auditEntry.name || auditEntry.framework?.name) ??
__("Unknown Audit"),
},
]}
/>
@@ -139,10 +164,16 @@ export default function AuditDetailsPage(props: Props) {
<div className="flex justify-between items-start">
<div className="flex items-center gap-4">
<div className="flex items-center gap-3">
<FrameworkLogo name={auditEntry.framework?.name || ""} />
<FrameworkLogo
name={auditEntry.framework?.name || ""}
lightLogoURL={auditEntry.framework?.lightLogoURL}
darkLogoURL={auditEntry.framework?.darkLogoURL}
/>
<div className="text-2xl">{auditEntry.framework?.name}</div>
</div>
<Badge variant={getAuditStateVariant(auditEntry.state || "NOT_STARTED")}>
<Badge
variant={getAuditStateVariant(auditEntry.state || "NOT_STARTED")}
>
{getAuditStateLabel(__, auditEntry.state || "NOT_STARTED")}
</Badge>
</div>
@@ -187,12 +218,10 @@ export default function AuditDetailsPage(props: Props) {
</Field>
<div className="flex justify-end">
{formState.isDirty && (
isAuthorized("Audit", "updateAudit") && (
<Button type="submit" disabled={formState.isSubmitting}>
{formState.isSubmitting ? __("Updating...") : __("Update")}
</Button>
)
{formState.isDirty && isAuthorized("Audit", "updateAudit") && (
<Button type="submit" disabled={formState.isSubmitting}>
{formState.isSubmitting ? __("Updating...") : __("Update")}
</Button>
)}
</div>
</form>
@@ -201,7 +230,7 @@ export default function AuditDetailsPage(props: Props) {
<div className="space-y-4">
<h3 className="text-lg font-medium">{__("Audit Report")}</h3>
{auditEntry.report ? (
{auditEntry.report ? (
<div className="space-y-4">
<div className="flex items-center justify-between p-4 bg-success-50 border border-success-200 rounded-lg">
<div className="flex items-center gap-3">
@@ -211,11 +240,10 @@ export default function AuditDetailsPage(props: Props) {
{auditEntry.report.filename}
</p>
<div className="flex items-center gap-4 text-sm text-success-700">
<span>{fileSize(__, auditEntry.report.size)}</span>
<span>
{fileSize(__, auditEntry.report.size)}
</span>
<span>
{__("Uploaded")} {formatDate(auditEntry.report.createdAt)}
{__("Uploaded")}{" "}
{formatDate(auditEntry.report.createdAt)}
</span>
</div>
</div>
@@ -224,7 +252,7 @@ export default function AuditDetailsPage(props: Props) {
<DropdownItem
onClick={() => {
if (auditEntry.report?.downloadUrl) {
window.open(auditEntry.report.downloadUrl, '_blank');
window.open(auditEntry.report.downloadUrl, "_blank");
}
}}
icon={IconArrowInbox}
@@ -244,10 +272,14 @@ export default function AuditDetailsPage(props: Props) {
) : (
<div className="space-y-4">
<p className="text-neutral-600">
{__("Upload the final audit report document (PDF recommended)")}
{__(
"Upload the final audit report document (PDF recommended)"
)}
</p>
<Dropzone
description={__("Only PDF, DOCX files up to 25MB are allowed")}
description={__(
"Only PDF, DOCX files up to 25MB are allowed"
)}
isUploading={isUploading}
onDrop={async (files) => {
if (files.length > 0 && auditEntry.id) {

View File

@@ -10,12 +10,12 @@ import {
Button,
ControlItem,
DropdownItem,
FrameworkLogo,
IconPencil,
IconPlusLarge,
IconTrashCan,
PageHeader,
} from "@probo/ui";
import { FrameworkLogo } from "/components/FrameworkLogo";
import {
connectionListKey,
frameworkNodeQuery,
@@ -39,6 +39,8 @@ const frameworkDetailFragment = graphql`
id
name
description
lightLogoURL
darkLogoURL
organization {
name
}
@@ -70,12 +72,8 @@ const generateFrameworkStateOfApplicabilityMutation = graphql`
`;
const exportFrameworkMutation = graphql`
mutation FrameworkDetailPageExportFrameworkMutation(
$frameworkId: ID!
) {
exportFramework(
input: { frameworkId: $frameworkId }
) {
mutation FrameworkDetailPageExportFrameworkMutation($frameworkId: ID!) {
exportFramework(input: { frameworkId: $frameworkId }) {
exportJobId
}
}
@@ -120,7 +118,8 @@ export default function FrameworkDetailPage(props: Props) {
exportFrameworkMutation,
{
errorMessage: "Failed to export framework",
successMessage: "Framework export started successfully. You will receive an email when the export is ready.",
successMessage:
"Framework export started successfully. You will receive an email when the export is ready.",
}
);
@@ -193,7 +192,11 @@ export default function FrameworkDetailPage(props: Props) {
{__("Export Framework")}
</DropdownItem>
{isAuthorized("Framework", "deleteFramework") && (
<DropdownItem icon={IconTrashCan} variant="danger" onClick={onDelete}>
<DropdownItem
icon={IconTrashCan}
variant="danger"
onClick={onDelete}
>
{__("Delete")}
</DropdownItem>
)}

View File

@@ -5,6 +5,7 @@ import {
Card,
DropdownItem,
FileButton,
FrameworkLogo,
FrameworkSelector,
IconFolderUpload,
IconPencil,
@@ -27,7 +28,6 @@ import { Link } from "react-router";
import type { FrameworksPageCardFragment$key } from "./__generated__/FrameworksPageCardFragment.graphql";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import { useState, type ChangeEventHandler, use } from "react";
import { FrameworkLogo } from "/components/FrameworkLogo";
import { FrameworkFormDialog } from "./dialogs/FrameworkFormDialog";
import { PermissionsContext } from "/providers/PermissionsContext";
@@ -120,7 +120,8 @@ export default function FrameworksPage(props: Props) {
const isLoading = isUploading || isImporting;
const hasAnyAction = isAuthorized("Framework", "updateFramework") ||
const hasAnyAction =
isAuthorized("Framework", "updateFramework") ||
isAuthorized("Framework", "deleteFramework");
return (
@@ -172,6 +173,8 @@ const frameworkCardFragment = graphql`
id
name
description
lightLogoURL
darkLogoURL
}
`;
@@ -200,7 +203,11 @@ function FrameworkCard(props: FrameworkCardProps) {
framework={framework}
/>
<div className="flex justify-between mb-3">
<FrameworkLogo {...framework} />
<FrameworkLogo
name={framework.name}
lightLogoURL={framework.lightLogoURL}
darkLogoURL={framework.darkLogoURL}
/>
{props.hasAnyAction && (
<ActionDropdown className="z-10 relative">
{isAuthorized("Framework", "updateFramework") && (

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<daf4012a65afffdedc10e4f5c7e2bc3c>>
* @generated SignedSource<<1da75e11f49e841800329ded9f67a1d4>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -24,8 +24,10 @@ export type FrameworkDetailPageFragment$data = {
};
}>;
};
readonly darkLogoURL: string | null | undefined;
readonly description: string | null | undefined;
readonly id: string;
readonly lightLogoURL: string | null | undefined;
readonly name: string;
readonly organization: {
readonly name: string;
@@ -67,6 +69,20 @@ return {
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "lightLogoURL",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "darkLogoURL",
"storageKey": null
},
{
"alias": null,
"args": null,
@@ -167,6 +183,6 @@ return {
};
})();
(node as any).hash = "c31fd5c0f675d2524f736d69c4ebebe3";
(node as any).hash = "34a82531b8812ee16e75eb035bc739e8";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<9fdc712e118402371c9951169aac320a>>
* @generated SignedSource<<412cc494e90709fca575c68f6deef112>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -11,8 +11,10 @@
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type FrameworksPageCardFragment$data = {
readonly darkLogoURL: string | null | undefined;
readonly description: string | null | undefined;
readonly id: string;
readonly lightLogoURL: string | null | undefined;
readonly name: string;
readonly " $fragmentType": "FrameworksPageCardFragment";
};
@@ -47,12 +49,26 @@ const node: ReaderFragment = {
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "lightLogoURL",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "darkLogoURL",
"storageKey": null
}
],
"type": "Framework",
"abstractKey": null
};
(node as any).hash = "4481f380673963e72f88071031e37d14";
(node as any).hash = "fe86a14971741a77f587ae015ebf0a22";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<9436ef052ea63a9785d82ec810e810f1>>
* @generated SignedSource<<2256362d89b6b5c2f5c707acb44d283e>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -158,6 +158,20 @@ return {
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "lightLogoURL",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "darkLogoURL",
"storageKey": null
}
],
"storageKey": null
@@ -187,12 +201,12 @@ return {
]
},
"params": {
"cacheID": "87a38788cb2e060657b552af69acb707",
"cacheID": "5584472f1e68fac3d766662c6d5dab90",
"id": null,
"metadata": {},
"name": "FrameworksPageImportMutation",
"operationKind": "mutation",
"text": "mutation FrameworksPageImportMutation(\n $input: ImportFrameworkInput!\n) {\n importFramework(input: $input) {\n frameworkEdge {\n node {\n id\n ...FrameworksPageCardFragment\n }\n }\n }\n}\n\nfragment FrameworksPageCardFragment on Framework {\n id\n name\n description\n}\n"
"text": "mutation FrameworksPageImportMutation(\n $input: ImportFrameworkInput!\n) {\n importFramework(input: $input) {\n frameworkEdge {\n node {\n id\n ...FrameworksPageCardFragment\n }\n }\n }\n}\n\nfragment FrameworksPageCardFragment on Framework {\n id\n name\n description\n lightLogoURL\n darkLogoURL\n}\n"
}
};
})();

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<b8d5c939f1b3a4337be9a4ecc2e8fd98>>
* @generated SignedSource<<af136d6db1a014d6241ad2c9c6dd5f55>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -159,6 +159,20 @@ return {
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "lightLogoURL",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "darkLogoURL",
"storageKey": null
}
],
"storageKey": null
@@ -188,12 +202,12 @@ return {
]
},
"params": {
"cacheID": "931895b4bbb7caacd8ee0ee69de76cd6",
"cacheID": "c66177d87b59d87fdf7cbc9268af431a",
"id": null,
"metadata": {},
"name": "FrameworkFormDialogMutation",
"operationKind": "mutation",
"text": "mutation FrameworkFormDialogMutation(\n $input: CreateFrameworkInput!\n) {\n createFramework(input: $input) {\n frameworkEdge {\n node {\n id\n ...FrameworksPageCardFragment\n }\n }\n }\n}\n\nfragment FrameworksPageCardFragment on Framework {\n id\n name\n description\n}\n"
"text": "mutation FrameworkFormDialogMutation(\n $input: CreateFrameworkInput!\n) {\n createFramework(input: $input) {\n frameworkEdge {\n node {\n id\n ...FrameworksPageCardFragment\n }\n }\n }\n}\n\nfragment FrameworksPageCardFragment on Framework {\n id\n name\n description\n lightLogoURL\n darkLogoURL\n}\n"
}
};
})();