diff --git a/packages/screens/Organizations/components/RolesOrg/RolesDeployerSteps.tsx b/packages/screens/Organizations/components/RolesOrg/RolesDeployerSteps.tsx index bd6b04d75..7449216b1 100644 --- a/packages/screens/Organizations/components/RolesOrg/RolesDeployerSteps.tsx +++ b/packages/screens/Organizations/components/RolesOrg/RolesDeployerSteps.tsx @@ -1,5 +1,7 @@ +import { zodResolver } from "@hookform/resolvers/zod"; import { useQueryClient } from "@tanstack/react-query"; import { useState } from "react"; +import { useFieldArray, useForm } from "react-hook-form"; import { View } from "react-native"; import { RolesMembersSettingsSection } from "./RolesMembersSettingsSection"; @@ -25,8 +27,10 @@ import { CreateDaoFormType, LAUNCHING_PROCESS_STEPS, ROLES_BASED_ORGANIZATION_STEPS, - RolesMemberSettingFormType, - RolesSettingFormType, + RolesFormType, + RolesMembersFormType, + zodRolesMembersObject, + zodRolesObject, } from "@/utils/types/organizations"; export const RolesDeployerSteps: React.FC<{ @@ -49,12 +53,37 @@ export const RolesDeployerSteps: React.FC<{ const network = getNetwork(selectedWallet?.networkId); const queryClient = useQueryClient(); const { setToast } = useFeedbacks(); + const rolesMembersForm = useForm({ + resolver: zodResolver(zodRolesMembersObject), + defaultValues: { + members: selectedWallet?.address + ? [ + { + addr: selectedWallet?.address, + weight: "0", + roles: "", + }, + ] + : [], + }, + }); + const rolesForm = useForm({ + resolver: zodResolver(zodRolesObject), + defaultValues: { + roles: [], + }, + }); + const membersField = useFieldArray({ + control: rolesMembersForm.control, + name: "members", + }); + const rolesField = useFieldArray({ + control: rolesForm.control, + name: "roles", + }); + const [configureVotingFormData, setConfigureVotingFormData] = useState(); - const [rolesSettingsFormData, setRolesSettingsFormData] = - useState(); - const [memberSettingsFormData, setMemberSettingsFormData] = - useState(); const createDaoContract = async (): Promise => { try { @@ -62,20 +91,18 @@ export const RolesDeployerSteps: React.FC<{ case NetworkKind.Gno: { const name = organizationData?.associatedHandle!; const roles = - rolesSettingsFormData?.roles?.map((role) => ({ + rolesField?.fields?.map((role) => ({ name: role.name.trim(), color: role.color, - resources: role.resources, + resources: role.resources || [], })) || []; - const initialMembers = (memberSettingsFormData?.members || []).map( - (member) => ({ - address: member.addr, - weight: parseInt(member.weight, 10), - roles: member.roles - ? member.roles.split(",").map((role) => role.trim()) - : [], - }), - ); + const initialMembers = (membersField?.fields || []).map((member) => ({ + address: member.addr, + weight: parseInt(member.weight, 10), + roles: member.roles + ? member.roles.split(",").map((role) => role.trim()) + : [], + })); const pkgPath = await adenaDeployGnoDAO( network.id, selectedWallet?.address!, @@ -113,7 +140,7 @@ export const RolesDeployerSteps: React.FC<{ network.cwAdminFactoryContractAddress!; const walletAddress = selectedWallet.address; - if (!memberSettingsFormData) return false; + if (!membersField.fields) return false; const params: CreateDaoMemberBasedParams = { networkId, sender: walletAddress, @@ -127,7 +154,7 @@ export const RolesDeployerSteps: React.FC<{ description: organizationData.organizationDescription, tns: organizationData.associatedHandle, imageUrl: organizationData.imageUrl, - members: memberSettingsFormData.members.map((value) => ({ + members: membersField.fields.map((value) => ({ addr: value.addr, weight: parseInt(value.weight, 10), })), @@ -184,13 +211,11 @@ export const RolesDeployerSteps: React.FC<{ setCurrentStep(2); }; - const onSubmitRolesSettings = (data: RolesSettingFormType) => { - setRolesSettingsFormData(data); + const onSubmitRolesSettings = () => { setCurrentStep(3); }; - const onSubmitMemberSettings = (data: RolesMemberSettingFormType) => { - setMemberSettingsFormData(data); + const onSubmitMemberSettings = () => { setCurrentStep(4); }; @@ -215,8 +240,8 @@ export const RolesDeployerSteps: React.FC<{ ); setOrganizationData(undefined); setConfigureVotingFormData(undefined); - setRolesSettingsFormData(undefined); - setMemberSettingsFormData(undefined); + rolesMembersForm.reset(); + rolesForm.reset(); setCurrentStep(0); setDAOAddress(""); setLaunchingStep(0); @@ -239,14 +264,25 @@ export const RolesDeployerSteps: React.FC<{ currentStep === 2 ? { display: "flex", flex: 1 } : { display: "none" } } > - + - + diff --git a/packages/screens/Organizations/components/RolesOrg/RolesMembersSettingsSection.tsx b/packages/screens/Organizations/components/RolesOrg/RolesMembersSettingsSection.tsx index 7361b2cfe..ae96338b6 100644 --- a/packages/screens/Organizations/components/RolesOrg/RolesMembersSettingsSection.tsx +++ b/packages/screens/Organizations/components/RolesOrg/RolesMembersSettingsSection.tsx @@ -1,10 +1,10 @@ -import React, { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; +import React from "react"; +import { Control, UseFieldArrayAppend } from "react-hook-form"; import { Pressable, ScrollView, View, ViewStyle } from "react-native"; +import { z } from "zod"; import trashSVG from "../../../../../assets/icons/trash.svg"; import walletInputSVG from "../../../../../assets/icons/wallet-input.svg"; -import useSelectedWallet from "../../../../hooks/useSelectedWallet"; import { BrandText } from "@/components/BrandText"; import { SVG } from "@/components/SVG"; @@ -18,50 +18,31 @@ import { fontSemibold28 } from "@/utils/style/fonts"; import { layout } from "@/utils/style/layout"; import { ROLES_BASED_ORGANIZATION_STEPS, - RolesMemberSettingFormType, + RolesMembersFormType, + zodRolesMemberObject, } from "@/utils/types/organizations"; interface RolesMembersSettingsSectionProps { - onSubmit: (form: RolesMemberSettingFormType) => void; + members: z.infer[]; + append: UseFieldArrayAppend; + control: Control; + remove: (index?: number | number[] | undefined) => void; + handleSubmit: () => void; } export const RolesMembersSettingsSection: React.FC< RolesMembersSettingsSectionProps -> = ({ onSubmit }) => { - const { handleSubmit, control, resetField, unregister } = - useForm(); - - // this effect put the selected wallet address in the first field only on initial load - const selectedWallet = useSelectedWallet(); - const [initialReset, setInitialReset] = useState(false); - useEffect(() => { - if (initialReset) { - return; - } - if (!selectedWallet?.address) { - return; - } - resetField("members.0.addr", { - defaultValue: selectedWallet?.address, - }); - setInitialReset(true); - }, [selectedWallet?.address, resetField, initialReset]); - - const [addressIndexes, setAddressIndexes] = useState([0]); - - // functions - const removeAddressField = (id: number, index: number) => { - unregister(`members.${index}.addr`); - unregister(`members.${index}.weight`); - unregister(`members.${index}.roles`); - if (addressIndexes.length > 1) { - const copyIndex = [...addressIndexes].filter((i) => i !== id); - setAddressIndexes(copyIndex); - } +> = ({ handleSubmit, append, remove, members, control }) => { + const removeAddressField = (index: number) => { + remove(index); }; const addAddressField = () => { - setAddressIndexes([...addressIndexes, Math.floor(Math.random() * 200000)]); + append({ + addr: "", + weight: "", + roles: "", + }); }; return ( @@ -70,10 +51,10 @@ export const RolesMembersSettingsSection: React.FC< Members - {addressIndexes.map((id, index) => ( - + {members.map((member, index) => ( + - + name={`members.${index}.addr`} noBrokenCorners label="Member Address" @@ -85,7 +66,7 @@ export const RolesMembersSettingsSection: React.FC< > removeAddressField(id, index)} + onPress={() => removeAddressField(index)} > @@ -93,7 +74,7 @@ export const RolesMembersSettingsSection: React.FC< - + name={`members.${index}.weight`} noBrokenCorners label="Weight" @@ -106,7 +87,7 @@ export const RolesMembersSettingsSection: React.FC< - + name={`members.${index}.roles`} noBrokenCorners label="Roles - separate with a comma" @@ -125,7 +106,7 @@ export const RolesMembersSettingsSection: React.FC< diff --git a/packages/screens/Organizations/components/RolesOrg/RolesModalCreateRole.tsx b/packages/screens/Organizations/components/RolesOrg/RolesModalCreateRole.tsx index 1e5a57cfd..b2fa00fff 100644 --- a/packages/screens/Organizations/components/RolesOrg/RolesModalCreateRole.tsx +++ b/packages/screens/Organizations/components/RolesOrg/RolesModalCreateRole.tsx @@ -1,4 +1,6 @@ -import { Control } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; import { ScrollView, TouchableOpacity, View } from "react-native"; import { BrandText } from "@/components/BrandText"; @@ -11,50 +13,66 @@ import { SpacerColumn, SpacerRow } from "@/components/spacer"; import { neutral33 } from "@/utils/style/colors"; import { fontSemibold18 } from "@/utils/style/fonts"; import { layout } from "@/utils/style/layout"; -import { RolesSettingFormType } from "@/utils/types/organizations"; +import { RoleFormType, zodRoleObject } from "@/utils/types/organizations"; interface RolesModalCreateRoleProps { modalVisible: boolean; - rolesIndexes: number[]; - resources: { name: string; resources: string[]; value: boolean }[]; - control: Control; onCloseModal: () => void; - onCheckboxChange: (index: number) => void; - addRoleField: () => void; + addRoleField: (name: string, color: string, resources: string[]) => void; } export const RolesModalCreateRole: React.FC = ({ modalVisible, - rolesIndexes, - resources, - control, onCloseModal, - onCheckboxChange, addRoleField, }) => { + const { control, handleSubmit, reset } = useForm({ + resolver: zodResolver(zodRoleObject), + }); + + const getInitialState = () => { + return fakeResources.map((fakeResource) => ({ + ...fakeResource, + value: false, + })); + }; + const [resources, setResources] = + useState<{ name: string; resources: string[]; value: boolean }[]>( + getInitialState(), + ); + + const onCheckboxChange = (index: number) => { + const copyResources = [...resources]; + copyResources[index].value = !copyResources[index].value; + setResources(copyResources); + }; + return ( { + onCloseModal(); + setResources(getInitialState()); + }} width={480} label="Add a new Role" > - + control={control} + name="name" noBrokenCorners - name={`roles.${rolesIndexes.length}.name`} label="Role name" placeholder="Role name" rules={{ required: true }} placeHolder="Role name" /> - + control={control} noBrokenCorners - name={`roles.${rolesIndexes.length}.color`} + name="color" label="Role color" placeholder="Role color" placeHolder="Role color" @@ -90,9 +108,59 @@ export const RolesModalCreateRole: React.FC = ({ - + { + let finalResources: string[] | undefined = []; + resources.forEach((resource) => { + if (resource.value === true) { + finalResources = finalResources?.concat(resource.resources); + } + }); + setResources(getInitialState()); + addRoleField(data.name, data.color, finalResources); + reset(); + })} + /> ); }; + +// TODO: Create a hook to get all the resources +const fakeResources = [ + { + name: "Organizations", + resources: [], + }, + { + name: "Social Feed", + resources: ["gno.land/r/teritori/social_feeds.CreatePost"], + }, + { + name: "Marketplace", + resources: [], + }, + { + name: "Launchpad NFT", + resources: [], + }, + { + name: "Launchpad ERC20", + resources: [], + }, + { + name: "Name Service", + resources: [], + }, + { + name: "Multisig Wallet", + resources: [], + }, + { + name: "Projects", + resources: [], + }, +]; diff --git a/packages/screens/Organizations/components/RolesOrg/RolesReviewInformationSection.tsx b/packages/screens/Organizations/components/RolesOrg/RolesReviewInformationSection.tsx index 00a4a26e1..6514b32c5 100644 --- a/packages/screens/Organizations/components/RolesOrg/RolesReviewInformationSection.tsx +++ b/packages/screens/Organizations/components/RolesOrg/RolesReviewInformationSection.tsx @@ -7,6 +7,7 @@ import { View, ViewStyle, } from "react-native"; +import { z } from "zod"; import { ReviewCollapsable } from "./../ReviewCollapsable"; import { ReviewCollapsableItem } from "./../ReviewCollapsableItem"; @@ -24,17 +25,17 @@ import { tinyAddress } from "@/utils/text"; import { ConfigureVotingFormType, CreateDaoFormType, - RolesMemberSettingFormType, - RolesSettingFormType, TokenSettingFormType, + zodRoleObject, + zodRolesMemberObject, } from "@/utils/types/organizations"; interface RolesReviewInformationSectionProps { organizationData?: CreateDaoFormType; votingSettingData?: ConfigureVotingFormType; tokenSettingData?: TokenSettingFormType; - memberSettingData?: RolesMemberSettingFormType; - rolesSettingData?: RolesSettingFormType; + memberSettingData?: z.infer[]; + rolesSettingData?: z.infer[]; onSubmit: () => void; } @@ -164,7 +165,7 @@ export const RolesReviewInformationSection: React.FC< - {rolesSettingData?.roles?.map((role, index) => ( + {rolesSettingData?.map((role, index) => ( )} /> - {rolesSettingData?.roles.length !== index + 1 && ( + {rolesSettingData?.length !== index + 1 && ( )} @@ -183,7 +184,7 @@ export const RolesReviewInformationSection: React.FC< - {memberSettingData?.members.map((member, index) => ( + {memberSettingData?.map((member, index) => ( void; + roles: z.infer[]; + remove: (index?: number | number[] | undefined) => void; + append: UseFieldArrayAppend; + handleSubmit: () => void; } export const RolesSettingsSection: React.FC = ({ - onSubmit, + roles, + remove, + append, + handleSubmit, }) => { - const { - handleSubmit, - control, - unregister, - register, - setValue, - resetField, - getValues, - } = useForm(); const [modalVisible, setModalVisible] = useState(false); - const [rolesIndexes, setRolesIndexes] = useState([]); - const [resources, setResources] = - useState<{ name: string; resources: string[]; value: boolean }[]>( - fakeResources, - ); - const removeRoleField = (id: number, index: number) => { - unregister(`roles.${index}.name`); - unregister(`roles.${index}.color`); - unregister(`roles.${index}.resources`); - if (rolesIndexes.length > 0) { - const copyIndex = [...rolesIndexes].filter((i) => i !== id); - setRolesIndexes(copyIndex); - } - }; - - const resetModal = () => { - resetField(`roles.${rolesIndexes.length}.name`); - resetField(`roles.${rolesIndexes.length}.color`); - resetField(`roles.${rolesIndexes.length}.resources`); + const removeRoleField = (index: number) => { + remove(index); }; const onOpenModal = () => { - resetModal(); - setResources(fakeResources.map((r) => ({ ...r, value: false }))); setModalVisible(true); }; - const addRoleField = () => { - register(`roles.${rolesIndexes.length}.resources`); - const selectedResources = resources - .filter((r) => r.value) - .flatMap((r) => r.resources); - setValue(`roles.${rolesIndexes.length}.resources`, selectedResources); - console.log(`Selected resources: ${selectedResources}`); - setRolesIndexes([...rolesIndexes, Math.floor(Math.random() * 200000)]); + const addRoleField = (name: string, color: string, resources: string[]) => { + append({ + name, + color, + resources, + }); setModalVisible(false); }; @@ -85,21 +63,11 @@ export const RolesSettingsSection: React.FC = ({ setModalVisible(false); }; - const onCheckboxChange = (index: number) => { - const copyResources = [...resources]; - copyResources[index].value = !copyResources[index].value; - setResources(copyResources); - }; - return ( = ({ { - const role = getValues(`roles.${index}`); + const role = item; if (!role) { return null; } return ( - + = ({ resources: role.resources, }} removeRoleField={removeRoleField} - id={item} index={index} /> @@ -167,7 +134,7 @@ export const RolesSettingsSection: React.FC = ({ @@ -177,10 +144,9 @@ export const RolesSettingsSection: React.FC = ({ const RoleTableRow: React.FC<{ role: { name: string; color: string; resources: string[] | undefined }; - removeRoleField: (id: number, index: number) => void; - id: number; + removeRoleField: (index: number) => void; index: number; -}> = ({ role, removeRoleField, id, index }) => { +}> = ({ role, removeRoleField, index }) => { return ( { - removeRoleField(id, index); + removeRoleField(index); }} > @@ -249,47 +215,3 @@ const columns: TableColumns = { minWidth: 30, }, }; - -// TODO: Create a hook to get all the resources -const fakeResources = [ - { - name: "Organizations", - resources: [], - value: false, - }, - { - name: "Social Feed", - resources: ["gno.land/r/teritori/social_feeds.CreatePost"], - value: false, - }, - { - name: "Marketplace", - resources: [], - value: false, - }, - { - name: "Launchpad NFT", - resources: [], - value: false, - }, - { - name: "Launchpad ERC20", - resources: [], - value: false, - }, - { - name: "Name Service", - resources: [], - value: false, - }, - { - name: "Multisig Wallet", - resources: [], - value: false, - }, - { - name: "Projects", - resources: [], - value: false, - }, -]; diff --git a/packages/utils/types/organizations.ts b/packages/utils/types/organizations.ts index eb1cf9457..a8256e5cc 100644 --- a/packages/utils/types/organizations.ts +++ b/packages/utils/types/organizations.ts @@ -1,3 +1,5 @@ +import { z } from "zod"; + import { NSAvailability } from "./tns"; export enum DaoType { @@ -41,13 +43,26 @@ export type MembershipMemberSettingFormType = { // ROLES BASED ORGANIZATION FORM TYPES -export type RolesSettingFormType = { - roles: { name: string; color: string; resources: string[] | undefined }[]; -}; - -export type RolesMemberSettingFormType = { - members: { addr: string; weight: string; roles: string | undefined }[]; -}; +export const zodRoleObject = z.object({ + name: z.string().trim().min(1), + color: z.string().trim().min(1), + resources: z.array(z.string()).optional(), +}); +export const zodRolesObject = z.object({ + roles: z.array(zodRoleObject), +}); +export const zodRolesMemberObject = z.object({ + addr: z.string().trim().min(1), + weight: z.string().trim().min(1), + // TODO: change it to an array + roles: z.string(), +}); +export const zodRolesMembersObject = z.object({ + members: z.array(zodRolesMemberObject), +}); +export type RolesMembersFormType = z.infer; +export type RolesFormType = z.infer; +export type RoleFormType = z.infer; // TOKEN BASED ORGANIZATION FORM TYPES