diff --git a/garage/src/components/CreateMissionModal.tsx b/garage/src/components/CreateMissionModal.tsx index 6c43b6bd..f3dec95f 100644 --- a/garage/src/components/CreateMissionModal.tsx +++ b/garage/src/components/CreateMissionModal.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { Result } from "@tableland/sdk"; import { Button, FormControl, @@ -25,12 +26,13 @@ import { useDisclosure, useToast, } from "@chakra-ui/react"; +import isEqual from "lodash/isEqual"; import { DeleteIcon } from "@chakra-ui/icons"; import { ChainAwareButton } from "./ChainAwareButton"; import { Database } from "@tableland/sdk"; import { useSigner } from "../hooks/useSigner"; import { isPresent } from "../utils/types"; -import { MissionReward, MissionDeliverable } from "../types"; +import { Mission, MissionReward, MissionDeliverable } from "../types"; import { secondaryChain, deployment } from "../env"; const { missionsTable } = deployment; @@ -176,16 +178,24 @@ const StateImportModal = ({ ); }; -export const CreateMissionModal = ({ isOpen, onClose }: ModalProps) => { - const toast = useToast(); - const signer = useSigner({ chainId: secondaryChain.id }); - - const db = useMemo(() => { - if (signer) return new Database({ signer }); - }, [signer]); - - const [formState, setFormState] = useState(initialFormState); +type BaseModalProps = ModalProps & { + title: string; + isFormValid: boolean; + formState: FormState; + setFormState: React.Dispatch>; + onMutate?: (formState: FormState) => Promise>; +}; +const BaseMissionModal = ({ + isOpen, + onClose, + title, + formState, + setFormState, + isFormValid, + onMutate, +}: BaseModalProps) => { + const toast = useToast(); const { name, description, @@ -198,34 +208,17 @@ export const CreateMissionModal = ({ isOpen, onClose }: ModalProps) => { maxNumberOfContributions, } = formState; - const isFormValid = useMemo(() => isValid(formState), [formState]); const [txnState, setTxnState] = useState< "idle" | "querying" | "success" | "fail" >("idle"); const onSubmit = useCallback(async () => { - if (!db) return; + if (!onMutate) return; setTxnState("querying"); try { - const { meta: insert } = await db - .prepare( - `INSERT INTO ${missionsTable} (name, description, tags, requirements, deliverables, rewards, contributions_start_block, contributions_end_block, max_number_of_contributions, contributions_disabled) VALUES (?, ?, JSON(?), JSON(?), JSON(?), JSON(?), ?, ?, ?, 0)` - ) - .bind( - name, - description, - JSON.stringify(tags), - JSON.stringify(requirements), - JSON.stringify(deliverables), - JSON.stringify(rewards), - contributionsStartBlock, - contributionsEndBlock, - maxNumberOfContributions - ) - .run(); - + const { meta: insert } = await onMutate(formState); await insert.txn?.wait(); setTxnState("success"); toast({ title: "Success", status: "success", duration: 7_500 }); @@ -244,7 +237,7 @@ export const CreateMissionModal = ({ isOpen, onClose }: ModalProps) => { } } } - }, [db, formState, setTxnState, toast]); + }, [onMutate, formState, setTxnState, toast]); const onNameInputChanged = useCallback( (e: React.ChangeEvent) => { @@ -448,10 +441,9 @@ export const CreateMissionModal = ({ isOpen, onClose }: ModalProps) => { useEffect(() => { if (isOpen) { - setFormState(initialFormState); setTxnState("idle"); } - }, [isOpen, setFormState, setTxnState]); + }, [isOpen, setTxnState]); const { isOpen: exportIsOpen, @@ -481,7 +473,7 @@ export const CreateMissionModal = ({ isOpen, onClose }: ModalProps) => { - Create Mission + {title} @@ -758,3 +750,150 @@ export const CreateMissionModal = ({ isOpen, onClose }: ModalProps) => { ); }; + +export const CreateMissionModal = ({ isOpen, onClose }: ModalProps) => { + const signer = useSigner({ chainId: secondaryChain.id }); + + const db = useMemo(() => { + if (signer) return new Database({ signer }); + }, [signer]); + + const [formState, setFormState] = useState(initialFormState); + + const isFormValid = useMemo(() => isValid(formState), [formState]); + + const mutate = db + ? (state: FormState) => { + const { + name, + description, + tags, + requirements, + deliverables, + rewards, + contributionsStartBlock, + contributionsEndBlock, + maxNumberOfContributions, + } = state; + + return db + .prepare( + `INSERT INTO ${missionsTable} (name, description, tags, requirements, deliverables, rewards, contributions_start_block, contributions_end_block, max_number_of_contributions, contributions_disabled) VALUES (?, ?, JSON(?), JSON(?), JSON(?), JSON(?), ?, ?, ?, 0)` + ) + .bind( + name, + description, + JSON.stringify(tags), + JSON.stringify(requirements), + JSON.stringify(deliverables), + JSON.stringify(rewards), + contributionsStartBlock, + contributionsEndBlock, + maxNumberOfContributions + ) + .run(); + } + : undefined; + + useEffect(() => { + if (isOpen) { + setFormState(initialFormState); + } + }, [isOpen, setFormState]); + + return ( + + ); +}; + +type EditModalProps = { mission: Mission } & ModalProps; + +export const EditMissionModal = ({ + isOpen, + onClose, + mission, +}: EditModalProps) => { + const { id, contributionsDisabled, ...baseMission } = mission; + const initialState = useMemo( + () => ({ + contributionsStartBlock: 0, + contributionsEndBlock: 0, + maxNumberOfContributions: 0, + ...baseMission, + }), + [baseMission] + ); + + const signer = useSigner({ chainId: secondaryChain.id }); + + const db = useMemo(() => { + if (signer) return new Database({ signer }); + }, [signer]); + + const mutate = db + ? (state: FormState) => { + const { + name, + description, + tags, + requirements, + deliverables, + rewards, + contributionsStartBlock, + contributionsEndBlock, + maxNumberOfContributions, + } = state; + + return db + .prepare( + `UPDATE ${missionsTable} SET name = ?, description = ?, tags = ?, requirements = ?, deliverables = ?, rewards = ?, contributions_start_block = ?, contributions_end_block = ?, max_number_of_contributions = ? WHERE id = ?` + ) + .bind( + name, + description, + JSON.stringify(tags), + JSON.stringify(requirements), + JSON.stringify(deliverables), + JSON.stringify(rewards), + contributionsStartBlock, + contributionsEndBlock, + maxNumberOfContributions, + id + ) + .run(); + } + : undefined; + + const [formState, setFormState] = useState(initialState); + + const isFormValid = useMemo( + () => isValid(formState) && !isEqual(initialState, formState), + [formState] + ); + + useEffect(() => { + if (isOpen) { + setFormState(initialState); + } + }, [isOpen, setFormState]); + + return ( + + ); +}; diff --git a/garage/src/pages/Admin/MissionAdmin.tsx b/garage/src/pages/Admin/MissionAdmin.tsx index d9da52ed..efa4ac9a 100644 --- a/garage/src/pages/Admin/MissionAdmin.tsx +++ b/garage/src/pages/Admin/MissionAdmin.tsx @@ -29,6 +29,7 @@ import { Tbody, Textarea, useToast, + useDisclosure, } from "@chakra-ui/react"; import { useContractRead, @@ -46,6 +47,7 @@ import { truncateWalletAddress } from "../../utils/fmt"; import { as0xString } from "../../utils/types"; import { useMission, useContributions } from "../../hooks/useMissions"; import { secondaryChain, deployment } from "../../env"; +import { EditMissionModal } from "../../components/CreateMissionModal"; import { abi } from "../../abis/MissionsManager"; const { missionContributionsTable, missionContractAddress } = deployment; @@ -320,6 +322,8 @@ export const MissionAdmin = () => { if (isSuccess && !isTxLoading) refreshContributionsStatus(); }, [isSuccess, isTxLoading, refreshContributionsStatus]); + const { isOpen, onClose, onOpen } = useDisclosure(); + return ( <> { contribution={reviewContribution} onTransactionCompleted={refreshContributions} /> + {mission && ( + + )} { # {mission.id} – {mission.description} - Contributions + + Contributions Contributions are:{" "} {contributionsDisabled ? "disabled" : "enabled"} @@ -384,14 +394,14 @@ export const MissionAdmin = () => { )} {contributions && ( <> - Contributions pending review + Contributions pending review status === "pending_review" )} onReviewContribution={setReviewContribution} /> - Reviewed + Reviewed status !== "pending_review"