From 3266728a64bca2b12e2789d96d3cdf0802030bc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sza=C5=82owski?= Date: Fri, 12 Apr 2024 17:35:32 +0200 Subject: [PATCH] [#710] fix displaying modals to not block signing transactions --- CHANGELOG.md | 1 + .../StorageInformation.tsx | 19 +-- .../forms/useCreateGovernanceActionForm.ts | 137 +++++++-------- .../src/hooks/forms/useEditDRepInfoForm.ts | 157 +++++++++-------- .../src/hooks/forms/useRegisterAsdRepForm.tsx | 159 +++++++++--------- .../src/hooks/forms/useVoteContextForm.tsx | 23 ++- govtool/frontend/src/i18n/locales/en.ts | 10 +- govtool/frontend/src/utils/canonizeJSON.ts | 4 +- .../src/utils/generateMetadataBody.ts | 44 +++++ govtool/frontend/src/utils/index.ts | 1 + .../utils/tests/generateMetadataBody.test.ts | 84 +++++++++ 11 files changed, 387 insertions(+), 252 deletions(-) create mode 100644 govtool/frontend/src/utils/generateMetadataBody.ts create mode 100644 govtool/frontend/src/utils/tests/generateMetadataBody.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ad513ec20..6bcb3ba71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ changes. - Integrate frontend with metadata validation service [Issue 617](https://github.com/IntersectMBO/govtool/issues/617) - Implement a loading modal for the validation of the metadata [Issue 646](https://github.com/IntersectMBO/govtool/issues/646) +- Fix displaying modals to not block signing transactions [Issue 710](https://github.com/IntersectMBO/govtool/issues/710) - Change style of url button to trim the file name [Issue 655](https://github.com/IntersectMBO/govtool/issues/655) - Change regex for parsing urls to match urls without protocol [Issue 655](https://github.com/IntersectMBO/govtool/issues/655) diff --git a/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/StorageInformation.tsx b/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/StorageInformation.tsx index 2a9469196..83ff98967 100644 --- a/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/StorageInformation.tsx +++ b/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/StorageInformation.tsx @@ -10,9 +10,8 @@ import { useScreenDimension, } from "@hooks"; import { Step } from "@molecules"; -import { BgCard, ControlledField, LoadingModalState } from "@organisms"; +import { BgCard, ControlledField } from "@organisms"; import { URL_REGEX, openInNewTab } from "@utils"; -import { useModal } from "@context"; type StorageInformationProps = { setStep: Dispatch>; @@ -30,7 +29,6 @@ export const StorageInformation = ({ setStep }: StorageInformationProps) => { onClickDownloadJson, isLoading, } = useCreateGovernanceActionForm(setStep); - const { openModal, closeModal } = useModal(); const { screenWidth } = useScreenDimension(); const fileName = getValues("governance_action_type"); @@ -48,21 +46,6 @@ export const StorageInformation = ({ setStep }: StorageInformationProps) => { generateMetadata(); }, []); - useEffect(() => { - if (isLoading) { - openModal({ - type: "loadingModal", - state: { - title: t("createGovernanceAction.modals.loading.title"), - message: t("createGovernanceAction.modals.loading.message"), - dataTestId: "storing-information-loading", - }, - }); - } else { - closeModal(); - } - }, [isLoading]); - return ( >, ) => { + // Local state + const [isLoading, setIsLoading] = useState(false); + const [hash, setHash] = useState(null); + const [json, setJson] = useState(null); + + // DApp Connector const { buildNewInfoGovernanceAction, buildTreasuryGovernanceAction, buildSignSubmitConwayCertTx, } = useCardano(); - const { validateMetadata } = useValidateMutation(); + + // App Management const { t } = useTranslation(); - const [isLoading, setIsLoading] = useState(false); - const [hash, setHash] = useState(null); - const [json, setJson] = useState(null); const navigate = useNavigate(); const { openModal, closeModal } = useModal(); + + // Queries + const { validateMetadata } = useValidateMutation(); + + // Form const { control, formState: { errors, isValid }, @@ -64,6 +77,7 @@ export const useCreateGovernanceActionForm = ( } = useFormContext(); const govActionType = watch("governance_action_type"); + // Navigation const backToForm = useCallback(() => { setStep?.(3); closeModal(); @@ -74,31 +88,17 @@ export const useCreateGovernanceActionForm = ( closeModal(); }, []); + // Business Logic const generateMetadata = useCallback(async () => { - const data = getValues(); - if (!govActionType) { throw new Error("Governance action type is not defined"); } - const acceptedKeys = ["title", "motivation", "abstract", "rationale"]; - - const filteredData = Object.entries(data) - .filter(([key]) => acceptedKeys.includes(key)) - .map(([key, value]) => [CIP_108 + key, value]); - - const references = (data as CreateGovernanceActionValues).links - ?.filter((link) => link.link) - .map((link) => ({ - "@type": "Other", - [`${CIP_100}reference-label`]: "Label", - [`${CIP_100}reference-uri`]: link.link, - })); - - const body = { - ...Object.fromEntries(filteredData), - [`${CIP_108}references`]: references, - }; + const body = generateMetadataBody({ + data: getValues(), + acceptedKeys: ["title", "motivation", "abstract", "rationale"], + standardReference: CIP_108, + }); const jsonld = await generateJsonld(body, GOVERNANCE_ACTION_CONTEXT); @@ -117,42 +117,6 @@ export const useCreateGovernanceActionForm = ( downloadJson(json, govActionType); }, [govActionType, json]); - const validateHash = useCallback( - async (url: string, localHash: string | null) => { - try { - if (!localHash) { - throw new Error(MetadataValidationStatus.INVALID_HASH); - } - const result = await validateMetadata({ url, hash: localHash }); - - if (result.status) { - throw result.status; - } - } catch (error) { - if ( - Object.values(MetadataValidationStatus).includes( - error as MetadataValidationStatus, - ) - ) { - openModal({ - type: "statusModal", - state: { - ...storageInformationErrorModals[ - error as MetadataValidationStatus - ], - onSubmit: backToForm, - onCancel: backToDashboard, - // TODO: Open usersnap feedback - onFeedback: backToDashboard, - }, - }); - } - throw error; - } - }, - [backToForm], - ); - const buildTransaction = useCallback( async (data: CreateGovernanceActionValues) => { if (!hash) return; @@ -184,10 +148,8 @@ export const useCreateGovernanceActionForm = ( default: throw new Error(t("errors.invalidGovernanceActionType")); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (error: any) { - console.error(error); - throw error; + } catch (error) { + captureException(error); } }, [hash], @@ -211,12 +173,33 @@ export const useCreateGovernanceActionForm = ( }); }, []); + const showLoadingModal = useCallback(() => { + openModal({ + type: "loadingModal", + state: { + title: t("modals.pendingValidation.title"), + message: t("modals.pendingValidation.message"), + dataTestId: "storing-information-loading", + }, + }); + }, []); + const onSubmit = useCallback( async (data: CreateGovernanceActionValues) => { try { setIsLoading(true); + showLoadingModal(); + if (!hash) { + throw new Error(MetadataValidationStatus.INVALID_HASH); + } + const { status } = await validateMetadata({ + url: data.storingURL, + hash, + }); - await validateHash(data.storingURL, hash); + if (status) { + throw status; + } const govActionBuilder = await buildTransaction(data); await buildSignSubmitConwayCertTx({ @@ -227,7 +210,25 @@ export const useCreateGovernanceActionForm = ( showSuccessModal(); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { - captureException(error); + if ( + Object.values(MetadataValidationStatus).includes( + error as MetadataValidationStatus, + ) + ) { + openModal({ + type: "statusModal", + state: { + ...storageInformationErrorModals[ + error as MetadataValidationStatus + ], + onSubmit: backToForm, + onCancel: backToDashboard, + onFeedback: backToDashboard, + }, + }); + } else { + captureException(error); + } } finally { setIsLoading(false); } diff --git a/govtool/frontend/src/hooks/forms/useEditDRepInfoForm.ts b/govtool/frontend/src/hooks/forms/useEditDRepInfoForm.ts index 60a3e6bd0..0551f26a3 100644 --- a/govtool/frontend/src/hooks/forms/useEditDRepInfoForm.ts +++ b/govtool/frontend/src/hooks/forms/useEditDRepInfoForm.ts @@ -7,15 +7,18 @@ import { captureException } from "@sentry/react"; import { NodeObject } from "jsonld"; import { - CIP_100, CIP_QQQ, DREP_CONTEXT, - MetadataHashValidationErrors, PATHS, storageInformationErrorModals, } from "@consts"; import { useCardano, useModal } from "@context"; -import { canonizeJSON, downloadJson, generateJsonld } from "@utils"; +import { + canonizeJSON, + downloadJson, + generateJsonld, + generateMetadataBody, +} from "@utils"; import { MetadataValidationStatus } from "@models"; import { useValidateMutation } from "../mutations"; @@ -41,25 +44,23 @@ export const defaultEditDRepInfoValues: EditDRepInfoValues = { export const useEditDRepInfoForm = ( setStep?: Dispatch>, ) => { - const { validateMetadata } = useValidateMutation(); - const { t } = useTranslation(); - const navigate = useNavigate(); + // Local state const [isLoading, setIsLoading] = useState(false); const [hash, setHash] = useState(null); const [json, setJson] = useState(null); - const { closeModal, openModal } = useModal(); + + // DApp Connector const { buildDRepUpdateCert, buildSignSubmitConwayCertTx } = useCardano(); - const backToForm = useCallback(() => { - setStep?.(1); - closeModal(); - }, [setStep]); + // App Management + const { closeModal, openModal } = useModal(); + const { t } = useTranslation(); + const navigate = useNavigate(); - const backToDashboard = useCallback(() => { - navigate(PATHS.dashboard); - closeModal(); - }, []); + // Queries + const { validateMetadata } = useValidateMutation(); + // Form const { control, getValues, @@ -69,30 +70,27 @@ export const useEditDRepInfoForm = ( resetField, watch, } = useFormContext(); - const dRepName = watch("dRepName"); const isError = Object.keys(errors).length > 0; + // Navigation + const backToForm = useCallback(() => { + setStep?.(1); + closeModal(); + }, [setStep]); + + const backToDashboard = useCallback(() => { + navigate(PATHS.dashboard); + closeModal(); + }, []); + + // Business Logic const generateMetadata = useCallback(async () => { - const data = getValues(); - const acceptedKeys = ["dRepName", "bio", "email"]; - - const filteredData = Object.entries(data) - .filter(([key]) => acceptedKeys.includes(key)) - .map(([key, value]) => [CIP_QQQ + key, value]); - - const references = (data as EditDRepInfoValues).links - ?.filter((link) => link.link) - .map((link) => ({ - "@type": "Other", - [`${CIP_100}reference-label`]: "Label", - [`${CIP_100}reference-uri`]: link.link, - })); - - const body = { - ...Object.fromEntries(filteredData), - [`${CIP_QQQ}references`]: references, - }; + const body = generateMetadataBody({ + data: getValues(), + acceptedKeys: ["dRepName", "bio", "email"], + standardReference: CIP_QQQ, + }); const jsonld = await generateJsonld(body, DREP_CONTEXT, CIP_QQQ); @@ -107,41 +105,19 @@ export const useEditDRepInfoForm = ( const onClickDownloadJson = async () => { if (!json) return; - downloadJson(json, dRepName); }; - const validateHash = useCallback( - async (url: string) => { - try { - if (!hash) throw new Error(MetadataHashValidationErrors.INVALID_HASH); - - const result = await validateMetadata({ url, hash }); - - if (result.status) { - throw result.status; - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (error: any) { - if (Object.values(MetadataValidationStatus).includes(error)) { - openModal({ - type: "statusModal", - state: { - ...storageInformationErrorModals[ - error as MetadataValidationStatus - ], - onSubmit: backToForm, - onCancel: backToDashboard, - // TODO: Open usersnap feedback - onFeedback: backToDashboard, - }, - }); - } - throw error; - } - }, - [backToForm, hash], - ); + const showLoadingModal = useCallback(() => { + openModal({ + type: "loadingModal", + state: { + title: t("modals.pendingValidation.title"), + message: t("modals.pendingValidation.message"), + dataTestId: "storing-information-loading", + }, + }); + }, []); const showSuccessModal = useCallback(() => { openModal({ @@ -161,12 +137,21 @@ export const useEditDRepInfoForm = ( async (data: EditDRepInfoValues) => { const url = data.storingURL; - if (!hash) return; - try { + if (!hash) throw MetadataValidationStatus.INVALID_HASH; + setIsLoading(true); + showLoadingModal(); + + const { status } = await validateMetadata({ + url, + hash, + }); + + if (status) { + throw status; + } - await validateHash(url); const updateDRepMetadataCert = await buildDRepUpdateCert(url, hash); await buildSignSubmitConwayCertTx({ certBuilder: updateDRepMetadataCert, @@ -176,12 +161,40 @@ export const useEditDRepInfoForm = ( showSuccessModal(); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { - captureException(error); + if (Object.values(MetadataValidationStatus).includes(error)) { + openModal({ + type: "statusModal", + state: { + ...storageInformationErrorModals[ + error as MetadataValidationStatus + ], + onSubmit: backToForm, + onCancel: backToDashboard, + // TODO: Open usersnap feedback + onFeedback: backToDashboard, + }, + }); + } else { + captureException(error); + openModal({ + type: "statusModal", + state: { + status: "warning", + message: error?.replace("Error: ", ""), + onSubmit: () => { + closeModal(); + backToDashboard(); + }, + title: t("modals.common.oops"), + dataTestId: "wallet-connection-error-modal", + }, + }); + } } finally { setIsLoading(false); } }, - [buildDRepUpdateCert, buildSignSubmitConwayCertTx, hash, validateHash], + [buildDRepUpdateCert, buildSignSubmitConwayCertTx, hash], ); return { diff --git a/govtool/frontend/src/hooks/forms/useRegisterAsdRepForm.tsx b/govtool/frontend/src/hooks/forms/useRegisterAsdRepForm.tsx index 12f0b73b9..abe47967a 100644 --- a/govtool/frontend/src/hooks/forms/useRegisterAsdRepForm.tsx +++ b/govtool/frontend/src/hooks/forms/useRegisterAsdRepForm.tsx @@ -7,7 +7,6 @@ import { captureException } from "@sentry/react"; import { NodeObject } from "jsonld"; import { - CIP_100, CIP_QQQ, DREP_CONTEXT, PATHS, @@ -20,6 +19,7 @@ import { downloadJson, ellipsizeText, generateJsonld, + generateMetadataBody, } from "@utils"; import { useGetVoterInfo } from ".."; @@ -46,27 +46,25 @@ export const defaultRegisterAsDRepValues: RegisterAsDRepValues = { export const useRegisterAsdRepForm = ( setStep?: Dispatch>, ) => { - const { validateMetadata } = useValidateMutation(); - const { t } = useTranslation(); - const navigate = useNavigate(); + // Local state const [isLoading, setIsLoading] = useState(false); const [hash, setHash] = useState(null); const [json, setJson] = useState(null); - const { closeModal, openModal } = useModal(); + + // DApp Connector const { buildDRepRegCert, buildDRepUpdateCert, buildSignSubmitConwayCertTx } = useCardano(); - const { voter } = useGetVoterInfo(); - const backToForm = useCallback(() => { - setStep?.(2); - closeModal(); - }, [setStep]); + // App Management + const { t } = useTranslation(); + const navigate = useNavigate(); + const { closeModal, openModal } = useModal(); - const backToDashboard = useCallback(() => { - navigate(PATHS.dashboard); - closeModal(); - }, []); + // Queries + const { validateMetadata } = useValidateMutation(); + const { voter } = useGetVoterInfo(); + // Form const { control, getValues, @@ -80,26 +78,24 @@ export const useRegisterAsdRepForm = ( const dRepName = watch("dRepName"); const isError = Object.keys(errors).length > 0; + // Navigation + const backToForm = useCallback(() => { + setStep?.(2); + closeModal(); + }, [setStep]); + + const backToDashboard = useCallback(() => { + navigate(PATHS.dashboard); + closeModal(); + }, []); + + // Business Logic const generateMetadata = useCallback(async () => { - const data = getValues(); - const acceptedKeys = ["dRepName", "bio", "email"]; - - const filteredData = Object.entries(data) - .filter(([key]) => acceptedKeys.includes(key)) - .map(([key, value]) => [CIP_QQQ + key, value]); - - const references = (data as RegisterAsDRepValues).links - ?.filter((link) => link.link) - .map((link) => ({ - "@type": "Other", - [`${CIP_100}reference-label`]: "Label", - [`${CIP_100}reference-uri`]: link.link, - })); - - const body = { - ...Object.fromEntries(filteredData), - [`${CIP_QQQ}references`]: references, - }; + const body = generateMetadataBody({ + data: getValues(), + acceptedKeys: ["dRepName", "bio", "email"], + standardReference: CIP_QQQ, + }); const jsonld = await generateJsonld(body, DREP_CONTEXT, CIP_QQQ); @@ -118,42 +114,6 @@ export const useRegisterAsdRepForm = ( downloadJson(json, ellipsizeText(dRepName, 16, "")); }; - const validateHash = useCallback( - async (url: string) => { - try { - if (!hash) throw new Error(MetadataValidationStatus.INVALID_HASH); - - const result = await validateMetadata({ url, hash }); - - if (result.status) { - throw result.status; - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (error: any) { - if ( - Object.values(MetadataValidationStatus).includes( - error as MetadataValidationStatus, - ) - ) { - openModal({ - type: "statusModal", - state: { - ...storageInformationErrorModals[ - error as MetadataValidationStatus - ], - onSubmit: backToForm, - onCancel: backToDashboard, - // TODO: Open usersnap feedback - onFeedback: backToDashboard, - }, - }); - } - throw error; - } - }, - [backToForm, hash], - ); - const createRegistrationCert = useCallback( async (data: RegisterAsDRepValues) => { if (!hash) return; @@ -165,10 +125,8 @@ export const useRegisterAsdRepForm = ( } return await buildDRepRegCert(url, hash); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (error: any) { - console.error(error); - throw error; + } catch (error) { + captureException(error); } }, [ @@ -179,6 +137,17 @@ export const useRegisterAsdRepForm = ( ], ); + const showLoadingModal = useCallback(() => { + openModal({ + type: "loadingModal", + state: { + title: t("modals.pendingValidation.title"), + message: t("modals.pendingValidation.message"), + dataTestId: "storing-information-loading", + }, + }); + }, []); + const showSuccessModal = useCallback(() => { openModal({ type: "statusModal", @@ -196,9 +165,19 @@ export const useRegisterAsdRepForm = ( const onSubmit = useCallback( async (data: RegisterAsDRepValues) => { try { + if (!hash) throw MetadataValidationStatus.INVALID_HASH; + setIsLoading(true); + showLoadingModal(); + + const { status } = await validateMetadata({ + url: data.storingURL, + hash, + }); - await validateHash(data.storingURL); + if (status) { + throw status; + } const registerAsDRepCert = await createRegistrationCert(data); await buildSignSubmitConwayCertTx({ certBuilder: registerAsDRepCert, @@ -208,12 +187,40 @@ export const useRegisterAsdRepForm = ( showSuccessModal(); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { - captureException(error); + if (Object.values(MetadataValidationStatus).includes(error)) { + openModal({ + type: "statusModal", + state: { + ...storageInformationErrorModals[ + error as MetadataValidationStatus + ], + onSubmit: backToForm, + onCancel: backToDashboard, + // TODO: Open usersnap feedback + onFeedback: backToDashboard, + }, + }); + } else { + captureException(error); + openModal({ + type: "statusModal", + state: { + status: "warning", + message: error?.replace("Error: ", ""), + onSubmit: () => { + closeModal(); + backToDashboard(); + }, + title: t("modals.common.oops"), + dataTestId: "wallet-connection-error-modal", + }, + }); + } } finally { setIsLoading(false); } }, - [buildSignSubmitConwayCertTx, createRegistrationCert, hash, validateHash], + [buildSignSubmitConwayCertTx, createRegistrationCert, hash], ); return { diff --git a/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx b/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx index b14f1722f..c8f78b8b0 100644 --- a/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx +++ b/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx @@ -5,7 +5,12 @@ import { blake2bHex } from "blakejs"; import { captureException } from "@sentry/react"; import { CIP_108, VOTE_TEST_CONTEXT } from "@consts"; -import { canonizeJSON, downloadJson, generateJsonld } from "@utils"; +import { + canonizeJSON, + downloadJson, + generateJsonld, + generateMetadataBody, +} from "@utils"; import { MetadataValidationStatus } from "@models"; import { useValidateMutation } from "../mutations"; @@ -37,17 +42,11 @@ export const useVoteContextForm = ( } = useFormContext(); const generateMetadata = useCallback(async () => { - const data = getValues(); - - const acceptedKeys = ["voteContextText"]; - - const filteredData = Object.entries(data) - .filter(([key]) => acceptedKeys.includes(key)) - .map(([key, value]) => [CIP_108 + key, value]); - - const body = { - ...Object.fromEntries(filteredData), - }; + const body = generateMetadataBody({ + data: getValues(), + acceptedKeys: ["voteContextText"], + standardReference: CIP_108, + }); const jsonld = await generateJsonld(body, VOTE_TEST_CONTEXT); const canonizedJson = await canonizeJSON(jsonld); diff --git a/govtool/frontend/src/i18n/locales/en.ts b/govtool/frontend/src/i18n/locales/en.ts index faabf9afb..78ef05249 100644 --- a/govtool/frontend/src/i18n/locales/en.ts +++ b/govtool/frontend/src/i18n/locales/en.ts @@ -214,11 +214,6 @@ export const en = { "Your Governance Action may take a little time to submit to the chain.", title: "Governance Action submitted!", }, - loading: { - title: "GovTool Is Checking Your Data", - message: - "GovTool will read the URL that you supplied and make a check to see if it’s identical with the information that you entered on the form.", - }, }, }, delegation: { @@ -542,6 +537,11 @@ export const en = { message: "Before performing a new action please wait for the previous action transaction to be completed.", }, + pendingValidation: { + title: "GovTool Is Checking Your Data", + message: + "GovTool will read the URL that you supplied and make a check to see if it’s identical with the information that you entered on the form.", + }, }, editMetadata: { pageTitle: "Edit DRep Info", diff --git a/govtool/frontend/src/utils/canonizeJSON.ts b/govtool/frontend/src/utils/canonizeJSON.ts index 72f884708..3ecac3ab3 100644 --- a/govtool/frontend/src/utils/canonizeJSON.ts +++ b/govtool/frontend/src/utils/canonizeJSON.ts @@ -6,7 +6,9 @@ import jsonld from "jsonld"; * @param json - The JSON object to be canonized. * @returns A Promise that resolves to the canonized JSON object. */ -export const canonizeJSON = async (json: Record) => { +export const canonizeJSON = async ( + json: Record, +): Promise => { const canonized = await jsonld.canonize(json); return canonized; }; diff --git a/govtool/frontend/src/utils/generateMetadataBody.ts b/govtool/frontend/src/utils/generateMetadataBody.ts new file mode 100644 index 000000000..264034065 --- /dev/null +++ b/govtool/frontend/src/utils/generateMetadataBody.ts @@ -0,0 +1,44 @@ +import { CIP_100, CIP_108, CIP_QQQ } from "@/consts"; + +type StandardReference = typeof CIP_100 | typeof CIP_108 | typeof CIP_QQQ; + +type MetadataConfig = { + data: Record; + acceptedKeys: string[]; + standardReference: StandardReference; +}; + +/** + * Generates the metadata body based on the provided configuration. + * + * @param {MetadataConfig} config - The configuration object containing + * the data, accepted keys, and standard reference. + * @returns {Object} - The generated metadata body. + */ +export const generateMetadataBody = ({ + data, + acceptedKeys, + standardReference, +}: MetadataConfig) => { + const filteredData = Object.entries(data) + .filter(([key]) => acceptedKeys.includes(key)) + .map(([key, value]) => [standardReference + key, value]); + + const references = data?.links + ? (data.links as Array<{ link: string }>) + .filter((link) => link.link) + .map((link) => ({ + "@type": "Other", + [`${standardReference}reference-label`]: "Label", + [`${standardReference}reference-uri`]: link.link, + })) + : undefined; + + const body = Object.fromEntries(filteredData); + + if (references) { + body[`${standardReference}references`] = references; + } + + return body; +}; diff --git a/govtool/frontend/src/utils/index.ts b/govtool/frontend/src/utils/index.ts index 58333c14d..e1069f5c2 100644 --- a/govtool/frontend/src/utils/index.ts +++ b/govtool/frontend/src/utils/index.ts @@ -23,3 +23,4 @@ export * from "./openInNewTab"; export * from "./removeDuplicatedProposals"; export * from "./validateMetadataHash"; export * from "./wait"; +export * from "./generateMetadataBody"; diff --git a/govtool/frontend/src/utils/tests/generateMetadataBody.test.ts b/govtool/frontend/src/utils/tests/generateMetadataBody.test.ts new file mode 100644 index 000000000..a8cb40131 --- /dev/null +++ b/govtool/frontend/src/utils/tests/generateMetadataBody.test.ts @@ -0,0 +1,84 @@ +import { CIP_108 } from "@consts"; + +import { generateMetadataBody } from "../generateMetadataBody"; + +describe("generateMetadataBody", () => { + it("generates metadata body with filtered data", () => { + const data = { + name: "John Doe", + age: 30, + email: "johndoe@example.com", + }; + const acceptedKeys = ["name", "age"]; + const standardReference = CIP_108; + + const result = generateMetadataBody({ + data, + acceptedKeys, + standardReference, + }); + + expect(result).toEqual({ + "https://github.com/cardano-foundation/CIPs/blob/master/CIP-0108/README.md#name": + "John Doe", + "https://github.com/cardano-foundation/CIPs/blob/master/CIP-0108/README.md#age": 30, + }); + }); + + it("generates metadata body with filtered data and references", () => { + const data = { + name: "John Doe", + age: 30, + email: "johndoe@example.com", + links: [ + { link: "https://example.com/link1" }, + { link: "https://example.com/link2" }, + ], + }; + const acceptedKeys = ["name", "age"]; + const standardReference = CIP_108; + + const result = generateMetadataBody({ + data, + acceptedKeys, + standardReference, + }); + + expect(result).toEqual({ + "https://github.com/cardano-foundation/CIPs/blob/master/CIP-0108/README.md#name": + "John Doe", + "https://github.com/cardano-foundation/CIPs/blob/master/CIP-0108/README.md#age": 30, + "https://github.com/cardano-foundation/CIPs/blob/master/CIP-0108/README.md#references": + [ + { + "@type": "Other", + "https://github.com/cardano-foundation/CIPs/blob/master/CIP-0108/README.md#reference-label": + "Label", + "https://github.com/cardano-foundation/CIPs/blob/master/CIP-0108/README.md#reference-uri": + "https://example.com/link1", + }, + { + "@type": "Other", + "https://github.com/cardano-foundation/CIPs/blob/master/CIP-0108/README.md#reference-label": + "Label", + "https://github.com/cardano-foundation/CIPs/blob/master/CIP-0108/README.md#reference-uri": + "https://example.com/link2", + }, + ], + }); + }); + + it("generates metadata body with empty data", () => { + const data = {}; + const acceptedKeys = ["name", "age"]; + const standardReference = CIP_108; + + const result = generateMetadataBody({ + data, + acceptedKeys, + standardReference, + }); + + expect(result).toEqual({}); + }); +});