diff --git a/src/assets/theme/components/button/button.variants.ts b/src/assets/theme/components/button/button.variants.ts index 6be8be51d..4eb788d3f 100644 --- a/src/assets/theme/components/button/button.variants.ts +++ b/src/assets/theme/components/button/button.variants.ts @@ -74,6 +74,20 @@ const tertiary = defineStyle({ _focus: {}, }); +const danger = defineStyle({ + border: '1px solid', + borderColor: 'red-1', + color: 'red-1', + _hover: { + borderColor: 'red-0', + color: 'red-0', + }, + _active: { + borderColor: 'red-0', + color: 'red-0', + }, +}); + const stepper = defineStyle({ border: '1px solid', borderColor: 'neutral-3', @@ -98,6 +112,7 @@ const buttonVariants = { secondary, tertiary, stepper, + danger, }; export default buttonVariants; diff --git a/src/components/Roles/forms/RoleFormInfo.tsx b/src/components/Roles/forms/RoleFormInfo.tsx index 1e6c51b52..0e1417420 100644 --- a/src/components/Roles/forms/RoleFormInfo.tsx +++ b/src/components/Roles/forms/RoleFormInfo.tsx @@ -20,8 +20,7 @@ export default function RoleFormInfo() { return ( <> {() => ( {t('canCreateProposals')} @@ -318,8 +319,7 @@ export function RoleFormMember() { return ( (); + + if (values.roleEditing?.roleEditingPaymentIndex !== undefined) { + if (values.roleEditing?.isTermed) { + return ( + + ); + } else { + return ; + } + } + + return null; +} export function RoleFormPaymentStreams() { const { t } = useTranslation(['roles']); @@ -61,6 +79,7 @@ export function RoleFormPaymentStreams() { {sortedPayments.length === 0 && ( {t('addPayment')} - + + {!!sortedPayments.length && } {sortedPayments.map(payment => { // @note don't render if form isn't valid diff --git a/src/components/Roles/forms/RoleFormTabs.tsx b/src/components/Roles/forms/RoleFormTabs.tsx index b5b733be1..07235e9e7 100644 --- a/src/components/Roles/forms/RoleFormTabs.tsx +++ b/src/components/Roles/forms/RoleFormTabs.tsx @@ -1,6 +1,6 @@ import { Button, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react'; import { useFormikContext } from 'formik'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { Blocker, useNavigate } from 'react-router-dom'; import { Hex } from 'viem'; @@ -11,8 +11,6 @@ import { useRolesStore } from '../../../store/roles/useRolesStore'; import { EditBadgeStatus, RoleFormValues, RoleHatFormValue } from '../../../types/roles'; import RoleFormInfo from './RoleFormInfo'; import { RoleFormMember } from './RoleFormMember'; -import { RoleFormPaymentStream } from './RoleFormPaymentStream'; -import RoleFormPaymentStreamTermed from './RoleFormPaymentStreamTermed'; import { RoleFormPaymentStreams } from './RoleFormPaymentStreams'; import { useRoleFormEditedRole } from './useRoleFormEditedRole'; @@ -55,28 +53,12 @@ export function RoleFormTabs({ } }, [setFieldValue, values.hats, values.roleEditing, hatId]); - const [tabIndex, setTabIndex] = useState(0); - const safeAddress = safe?.address; if (!safeAddress) return null; - if (values.roleEditing?.roleEditingPaymentIndex !== undefined) { - if (values.roleEditing?.isTermed) { - return ( - - ); - } else { - return ; - } - } - return ( <> - + {t('roleInfo')} {t('member')} @@ -112,7 +94,6 @@ export function RoleFormTabs({ } else if (existingRoleHat !== undefined) { setFieldValue(`hats.${hatIndex}`, { ...existingRoleHat, - resolvedWearer: existingRoleHat.wearerAddress, roleTerms: existingRoleHat.roleTerms.allTerms, }); } diff --git a/src/components/Roles/forms/RoleFormTerms.tsx b/src/components/Roles/forms/RoleFormTerms.tsx index 4198a12e3..5aaf182f0 100644 --- a/src/components/Roles/forms/RoleFormTerms.tsx +++ b/src/components/Roles/forms/RoleFormTerms.tsx @@ -225,6 +225,8 @@ function RoleTermExpiredTerms({ { + amplitude.track(analyticsEvents.RolesEditPageOpened); + }, []); + + const { safe } = useDaoInfoStore(); + + const { rolesSchema } = useRolesSchema(); + const { hatsTree } = useRolesStore(); + + const { createEditRolesProposal } = useCreateRoles(); + const initialValues: RoleFormValues = useMemo(() => { + const hats = hatsTree?.roleHats || []; + return { + proposalMetadata: { + title: '', + description: '', + }, + hats: hats.map(hat => ({ + ...hat, + resolvedWearer: hat.wearerAddress, + wearer: hat.wearerAddress, + roleTerms: hat.roleTerms.allTerms, + })), + customNonce: safe?.nextNonce || 0, + actions: [], + }; + }, [hatsTree?.roleHats, safe?.nextNonce]); + return ( + + initialValues={initialValues} + enableReinitialize + validationSchema={rolesSchema} + validateOnMount + onSubmit={createEditRolesProposal} + > + {({ handleSubmit }) => ( +
+ + + )} + + ); +} diff --git a/src/pages/dao/roles/edit/SafeRolesEditPage.tsx b/src/pages/dao/roles/edit/SafeRolesEditPage.tsx index 4dcf586e8..9793477b8 100644 --- a/src/pages/dao/roles/edit/SafeRolesEditPage.tsx +++ b/src/pages/dao/roles/edit/SafeRolesEditPage.tsx @@ -1,23 +1,18 @@ import * as amplitude from '@amplitude/analytics-browser'; -import { Box, Button, Flex, Hide, Show } from '@chakra-ui/react'; +import { Box, Button, Flex, Show } from '@chakra-ui/react'; import { Plus } from '@phosphor-icons/react'; -import { Formik } from 'formik'; -import { useEffect, useMemo, useState } from 'react'; +import { useFormikContext } from 'formik'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Outlet, useNavigate } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { Hex, toHex } from 'viem'; import { RoleCardEdit } from '../../../../components/Roles/RoleCard'; import { RoleCardLoading } from '../../../../components/Roles/RolePageCard'; import { RolesEditTable } from '../../../../components/Roles/RolesTable'; -import DraggableDrawer from '../../../../components/ui/containers/DraggableDrawer'; import NoDataCard from '../../../../components/ui/containers/NoDataCard'; -import { ModalBase } from '../../../../components/ui/modals/ModalBase'; -import { UnsavedChangesWarningContent } from '../../../../components/ui/modals/UnsavedChangesWarningContent'; import PageHeader from '../../../../components/ui/page/Header/PageHeader'; import { DAO_ROUTES } from '../../../../constants/routes'; import { getRandomBytes } from '../../../../helpers'; -import { useRolesSchema } from '../../../../hooks/schemas/roles/useRolesSchema'; -import useCreateRoles from '../../../../hooks/utils/useCreateRoles'; import { useNavigationBlocker } from '../../../../hooks/utils/useNavigationBlocker'; import { analyticsEvents } from '../../../../insights/analyticsEvents'; import { useNetworkConfig } from '../../../../providers/NetworkConfig/NetworkConfigProvider'; @@ -34,29 +29,11 @@ export function SafeRolesEditPage() { const { safe } = useDaoInfoStore(); const { addressPrefix } = useNetworkConfig(); - const { rolesSchema } = useRolesSchema(); + const { values, setFieldValue } = useFormikContext(); + const { hatsTree } = useRolesStore(); const navigate = useNavigate(); - const { createEditRolesProposal } = useCreateRoles(); - - const initialValues: RoleFormValues = useMemo(() => { - const hats = hatsTree?.roleHats || []; - return { - proposalMetadata: { - title: '', - description: '', - }, - hats: hats.map(hat => ({ - ...hat, - resolvedWearer: hat.wearerAddress, - wearer: hat.wearerAddress, - roleTerms: hat.roleTerms.allTerms, - })), - customNonce: safe?.nextNonce || 0, - actions: [], - }; - }, [hatsTree?.roleHats, safe?.nextNonce]); const [hasEditedRoles, setHasEditedRoles] = useState(false); @@ -73,141 +50,100 @@ export function SafeRolesEditPage() { const showNoRolesCard = !hatsTreeLoading && (hatsTree === null || hatsTree.roleHats.length === 0); return ( - - initialValues={initialValues} - enableReinitialize - validationSchema={rolesSchema} - validateOnMount - onSubmit={createEditRolesProposal} - > - {({ handleSubmit, values, setFieldValue }) => ( -
- {blocker.state === 'blocked' && ( - <> - - {}} - onOpen={() => {}} - headerContent={null} - initialHeight="23rem" - closeOnOverlayClick={false} - > - - - - - {}} - > - - - - + <> + + + + + ), + onClick: async () => { + const newId = toHex(getRandomBytes(), { size: 32 }); + setFieldValue('roleEditing', { id: newId, canCreateProposals: false }); + showRoleEditDetails(newId); + }, + }} + /> + + + + + + {hatsTree === undefined && } + {showNoRolesCard && values.hats.length === 0 && ( + )} - - - - - ), - onClick: async () => { - const newId = toHex(getRandomBytes(), { size: 32 }); - setFieldValue('roleEditing', { id: newId, canCreateProposals: false }); - showRoleEditDetails(newId); - }, + {values.hats.map(hat => ( + { + setFieldValue('roleEditing', hat); + showRoleEditDetails(hat.id); }} + payments={hat.payments} + isTermed={!!hat.isTermed} /> - - - - - - {hatsTree === undefined && } - {showNoRolesCard && values.hats.length === 0 && ( - - )} - {values.hats.map(hat => ( - { - setFieldValue('roleEditing', hat); - showRoleEditDetails(hat.id); - }} - payments={hat.payments} - isTermed={!!hat.isTermed} - /> - ))} - - - - - - - - - )} - + ))} +
+
+ + + + + ); } diff --git a/src/pages/dao/roles/edit/details/SafeRoleEditDetailsPage.tsx b/src/pages/dao/roles/edit/details/SafeRoleEditDetailsPage.tsx index b6f330200..e13e44f3b 100644 --- a/src/pages/dao/roles/edit/details/SafeRoleEditDetailsPage.tsx +++ b/src/pages/dao/roles/edit/details/SafeRoleEditDetailsPage.tsx @@ -1,39 +1,16 @@ -import { - Box, - Button, - Drawer, - DrawerBody, - DrawerContent, - DrawerOverlay, - Flex, - Hide, - Icon, - IconButton, - Portal, - Show, - Text, -} from '@chakra-ui/react'; -import { ArrowLeft, DotsThree, Trash } from '@phosphor-icons/react'; +import { Box } from '@chakra-ui/react'; +import { Trash } from '@phosphor-icons/react'; import { FieldArray, useFormikContext } from 'formik'; -import { useEffect, useRef, useState } from 'react'; +import { useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate, useSearchParams } from 'react-router-dom'; -import { Hex, isHex } from 'viem'; +import { isHex } from 'viem'; import { RoleFormTabs } from '../../../../../components/Roles/forms/RoleFormTabs'; -import DraggableDrawer from '../../../../../components/ui/containers/DraggableDrawer'; -import { ModalBase } from '../../../../../components/ui/modals/ModalBase'; -import { UnsavedChangesWarningContent } from '../../../../../components/ui/modals/UnsavedChangesWarningContent'; -import { - BACKGROUND_SEMI_TRANSPARENT, - CARD_SHADOW, - NEUTRAL_2_82_TRANSPARENT, - useHeaderHeight, -} from '../../../../../constants/common'; +import PageHeader from '../../../../../components/ui/page/Header/PageHeader'; import { DAO_ROUTES } from '../../../../../constants/routes'; import { useNavigationBlocker } from '../../../../../hooks/utils/useNavigationBlocker'; import { useNetworkConfig } from '../../../../../providers/NetworkConfig/NetworkConfigProvider'; import { useDaoInfoStore } from '../../../../../store/daoInfo/useDaoInfoStore'; -import { useRolesStore } from '../../../../../store/roles/useRolesStore'; import { EditBadgeStatus, EditedRole, @@ -41,98 +18,9 @@ import { RoleHatFormValue, } from '../../../../../types/roles'; -function EditRoleMenu({ onRemove, hatId }: { hatId: Hex; onRemove: () => void }) { - const { t } = useTranslation(['roles']); - const { values, setFieldValue } = useFormikContext(); - const [showMenu, setShowMenu] = useState(false); - const menuRef = useRef(null); - const hatIndex = values.hats.findIndex(h => h.id === hatId); - const role = values.hats[hatIndex]; - - const handleRemoveRole = () => { - const editedRole: EditedRole = { - fieldNames: [], - status: EditBadgeStatus.Removed, - }; - - if (hatIndex !== -1) { - if (role?.editedRole?.status === EditBadgeStatus.New) { - setFieldValue( - 'hats', - values.hats.filter(h => h.id !== hatId), - ); - } else { - setFieldValue(`hats.${hatIndex}`, { ...values.hats[hatIndex], editedRole }); - } - } - - setFieldValue('roleEditing', undefined); - setFieldValue('newRoleTerm', undefined); - setTimeout(() => onRemove(), 50); - }; - - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (menuRef.current && !menuRef.current.contains(event.target as Node)) { - setShowMenu(false); - } - }; - document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, []); - - return ( - <> - setShowMenu(show => !show)} - /> - {showMenu && ( - - - - )} - - ); -} - export function SafeRoleEditDetailsPage() { - const headerHeight = useHeaderHeight(); const { t } = useTranslation(['roles']); const { safe } = useDaoInfoStore(); - const { getPayment } = useRolesStore(); const { addressPrefix } = useNetworkConfig(); const navigate = useNavigate(); const { values, setFieldValue, touched, setTouched } = useFormikContext(); @@ -154,6 +42,12 @@ export function SafeRoleEditDetailsPage() { if (!safe?.address) return null; if (hatEditingId === undefined) return null; + const hatIndex = values.hats.findIndex(h => h.id === hatEditingId); + if (hatIndex === undefined) return null; + + const role = values.hats[hatIndex]; + if (!role) return null; + const goBackToRolesEdit = () => { backupRoleEditing.current = values.roleEditing; backupTouched.current = touched.roleEditing; @@ -169,184 +63,67 @@ export function SafeRoleEditDetailsPage() { }, 50); }; - const paymentIndex = values.roleEditing?.roleEditingPaymentIndex; - const streamId = - paymentIndex !== undefined ? values.roleEditing?.payments?.[paymentIndex]?.streamId : undefined; + const handleRemoveRole = () => { + const editedRole: EditedRole = { + fieldNames: [], + status: EditBadgeStatus.Removed, + }; - const goBackToRoleEdit = () => { - if (!values.roleEditing?.payments || paymentIndex === undefined || !hatEditingId) return; - const isExistingPayment = !!streamId ? getPayment(hatEditingId, streamId) : undefined; - // if payment is new, and unedited, remove it - if ( - paymentIndex === values.roleEditing.payments.length - 1 && - !values.roleEditing.editedRole && - !isExistingPayment - ) { - setFieldValue( - 'roleEditing.payments', - values.roleEditing.payments.filter((_, index) => index !== paymentIndex), - ); + if (hatIndex !== -1) { + if (role?.editedRole?.status === EditBadgeStatus.New) { + setFieldValue( + 'hats', + values.hats.filter(h => h.id !== hatEditingId), + ); + } else { + setFieldValue(`hats.${hatIndex}`, { ...values.hats[hatIndex], editedRole }); + } } + setFieldValue('roleEditing', undefined); setFieldValue('newRoleTerm', undefined); - setTimeout(() => { - setFieldValue('roleEditing.roleEditingPaymentIndex', undefined); - }, 200); + setTimeout(() => goBackToRolesEdit(), 50); }; - const goBackText = t( - paymentIndex === undefined ? 'editRole' : streamId ? 'payments' : 'addPayment', - ); - return ( <> - {blocker.state === 'blocked' && ( - <> - - {}} - onOpen={() => {}} - headerContent={null} - initialHeight="23rem" - closeOnOverlayClick={false} - > - { - setFieldValue('roleEditing', backupRoleEditing.current); - setFieldValue('newRoleTerm', backupNewRoleTerm.current); - setTouched({ roleEditing: backupTouched.current }); - blocker.reset(); - }} - /> - - - - {}} - > - { - setFieldValue('roleEditing', backupRoleEditing.current); - setFieldValue('newRoleTerm', backupNewRoleTerm.current); - setTouched({ roleEditing: backupTouched.current }); - blocker.reset(); - }} - /> - - - - )} {({ push }: { push: (roleHatFormValue: RoleHatFormValue) => void }) => ( <> - - - - - - {paymentIndex === undefined && ( - - - - )} - - - + + - - - - - - - - - - - } - onClick={ - paymentIndex === undefined ? goBackToRolesEdit : goBackToRoleEdit - } - /> - {goBackText} - - {paymentIndex === undefined && ( - - - - )} - - - - - - + ), + onClick: handleRemoveRole, + }} + /> + + )} diff --git a/src/router.tsx b/src/router.tsx index e15ff64bb..f17ec215c 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -18,6 +18,7 @@ import { SafeProposalCreatePage } from './pages/dao/proposals/new/SafeProposalCr import { SafeSablierProposalCreatePage } from './pages/dao/proposals/new/sablier/SafeSablierProposalCreatePage'; import { SafeRolesPage } from './pages/dao/roles/SafeRolesPage'; import { SafeRoleDetailsPage } from './pages/dao/roles/details/SafeRoleDetailsPage'; +import SafeRolesEditFormikPageWrapper from './pages/dao/roles/edit/SafeRolesEditFormikPageWrapper'; import { SafeRolesEditPage } from './pages/dao/roles/edit/SafeRolesEditPage'; import { SafeRoleEditDetailsPage } from './pages/dao/roles/edit/details/SafeRoleEditDetailsPage'; import { SafeRolesEditProposalSummaryPage } from './pages/dao/roles/edit/summary/SafeRolesEditProposalSummaryPage'; @@ -92,8 +93,12 @@ export const router = (addressPrefix: string, daoAddress: string | undefined) => }, { path: DAO_ROUTES.rolesEdit.path, - element: , + element: , children: [ + { + index: true, + element: , + }, { path: 'details', element: ,