From ed25114c45734000cd4b7ee295499e22784cd8ab Mon Sep 17 00:00:00 2001 From: Maksym Date: Fri, 2 Feb 2024 13:58:37 +0200 Subject: [PATCH] Edit organization public info (#31) * Update org tabs * Update drawer & UI components * Add no data viewer * Update links block * Update edit links drawer * Add draggable context * Update link forms * Update form * Update org links * Add org owner restriction to edit links * Fix review issues * Update no data icon --- package.json | 3 + src/api/modules/orgs/helpers/orgs.ts | 13 +- src/common/NoDataViewer.tsx | 57 ++++++++ src/common/index.ts | 1 + src/contexts/vertical-draggable.tsx | 53 +++++++ src/enums/icons.ts | 66 +++++---- src/hooks/form.ts | 29 +++- .../pages/OrgsId/contexts/org-details.tsx | 2 +- .../pages/OrgGroupsId/components/List.tsx | 2 +- .../OrgGroups/pages/OrgGroupsId/index.tsx | 6 +- .../OrgRoot/components/EditLinksDrawer.tsx | 133 +++++++++++++++++ .../pages/OrgRoot/components/LinkForm.tsx | 135 ++++++++++-------- .../pages/OrgRoot/components/LinksBlock.tsx | 57 ++++++-- .../OrgRoot/components/ProofsLinkForm.tsx | 72 ++++++++++ .../OrgRoot/components/VerifyProofsBlock.tsx | 4 +- .../Orgs/pages/OrgsId/pages/OrgRoot/index.tsx | 28 ++-- src/pages/Orgs/pages/OrgsList/index.tsx | 8 +- src/pages/UiKit/UiKitOther.tsx | 2 +- src/theme/components.ts | 115 +++++++++++---- src/ui/UiDrawer.tsx | 62 +++++++- src/ui/UiIconButton.tsx | 2 +- src/ui/UiSearchField.tsx | 2 +- src/ui/UiSelect.tsx | 4 +- src/ui/index.ts | 2 +- yarn.lock | 67 ++++++++- 25 files changed, 758 insertions(+), 167 deletions(-) create mode 100644 src/common/NoDataViewer.tsx create mode 100644 src/contexts/vertical-draggable.tsx create mode 100644 src/pages/Orgs/pages/OrgsId/pages/OrgRoot/components/EditLinksDrawer.tsx create mode 100644 src/pages/Orgs/pages/OrgsId/pages/OrgRoot/components/ProofsLinkForm.tsx diff --git a/package.json b/package.json index 563a1b49..3af97477 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,9 @@ "@distributedlab/jac": "^1.0.0-rc.9", "@distributedlab/tools": "^1.0.0-rc.9", "@distributedlab/w3p": "^1.0.0-rc.9", + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/modifiers": "^7.0.0", + "@dnd-kit/sortable": "^8.0.0", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@hookform/resolvers": "^3.3.2", diff --git a/src/api/modules/orgs/helpers/orgs.ts b/src/api/modules/orgs/helpers/orgs.ts index 9bdc972d..ca7dfcf2 100644 --- a/src/api/modules/orgs/helpers/orgs.ts +++ b/src/api/modules/orgs/helpers/orgs.ts @@ -2,6 +2,7 @@ import { api } from '@/api/clients' import { type Organization, type OrganizationCreate, + type OrgMetadata, type OrgsRequestQueryParams, OrgsStatuses, type OrgUser, @@ -102,8 +103,8 @@ export const loadOrgs = async (query: OrgsRequestQueryParams) => { return data } -export const loadOrgsAmount = async () => { - const { data } = await api.get(`${ApiServicePaths.Orgs}/v1/orgs/amount`) +export const loadOrgsCount = async () => { + const { data } = await api.get(`${ApiServicePaths.Orgs}/v1/orgs/count`) return data } @@ -139,6 +140,14 @@ export const verifyOrg = async (id: string) => { return data } +export const updateOrgMetadata = async (id: string, metadata: Partial) => { + const { data } = await api.patch>(`${ApiServicePaths.Orgs}/v1/orgs/${id}`, { + body: { data: metadata }, + }) + + return data +} + export const loadOrgUsers = async (id: string, query: OrgsRequestQueryParams) => { const { data } = await api.get(`${ApiServicePaths.Orgs}/v1/orgs/${id}/users`, { query, diff --git a/src/common/NoDataViewer.tsx b/src/common/NoDataViewer.tsx new file mode 100644 index 00000000..e4cae009 --- /dev/null +++ b/src/common/NoDataViewer.tsx @@ -0,0 +1,57 @@ +import { Stack, StackProps, Typography, useTheme } from '@mui/material' +import { ReactNode } from 'react' + +import { UiIcon } from '@/ui' + +interface Props extends StackProps { + icon?: ReactNode + title?: string + description?: string + action?: ReactNode +} + +export default function NoDataViewer({ + icon = , + title = 'No data', + description, + action, + ...rest +}: Props) { + const { palette, spacing } = useTheme() + + return ( + + + {icon} + + + {title} + {description && ( + + {description} + + )} + + {action} + + ) +} diff --git a/src/common/index.ts b/src/common/index.ts index b99a7419..b4969ccc 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -1,5 +1,6 @@ export { default as AppNavbar } from './AppNavbar' export { default as FillRequestForm } from './FillRequestForm' +export { default as NoDataViewer } from './NoDataViewer' export { default as PageListFilters } from './PageListFilters' export { default as PageTitles } from './PageTitles' export { default as ProfileMenu } from './ProfileMenu' diff --git a/src/contexts/vertical-draggable.tsx b/src/contexts/vertical-draggable.tsx new file mode 100644 index 00000000..5f046942 --- /dev/null +++ b/src/contexts/vertical-draggable.tsx @@ -0,0 +1,53 @@ +import { + closestCenter, + DndContext, + PointerSensor, + TouchSensor, + UniqueIdentifier, + useSensor, + useSensors, +} from '@dnd-kit/core' +import { restrictToParentElement, restrictToVerticalAxis } from '@dnd-kit/modifiers' +import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable' +import { PropsWithChildren } from 'react' + +type SortableId = UniqueIdentifier | { id: UniqueIdentifier } + +interface Props extends PropsWithChildren { + items: T[] + onItemsChange?: (items: T[]) => void + onItemsMove?: (oldIndex: number, newIndex: number) => void +} + +export default function VerticalDraggableContext({ + items, + onItemsMove, + onItemsChange, + children, +}: Props) { + const sensors = useSensors(useSensor(PointerSensor), useSensor(TouchSensor)) + + return ( + { + if (!over?.id) return + + if (active.id !== over.id) { + const itemIds = items.map(item => (typeof item === 'object' ? item.id : item)) + const oldIndex = itemIds.indexOf(active.id) + const newIndex = itemIds.indexOf(over.id) + + onItemsMove?.(oldIndex, newIndex) + onItemsChange?.(arrayMove(items, oldIndex, newIndex)) + } + }} + > + + {children} + + + ) +} diff --git a/src/enums/icons.ts b/src/enums/icons.ts index a697d37a..00da4ce7 100644 --- a/src/enums/icons.ts +++ b/src/enums/icons.ts @@ -1,28 +1,32 @@ -import { default as AccountCircleIcon } from '@mui/icons-material/AccountCircle' -import { default as Add } from '@mui/icons-material/Add' -import { default as AddPhotoAlternateOutlined } from '@mui/icons-material/AddPhotoAlternateOutlined' -import { default as CheckIcon } from '@mui/icons-material/Check' -import { default as ChevronLeft } from '@mui/icons-material/ChevronLeft' -import { default as Close } from '@mui/icons-material/Close' -import { default as ContentCopy } from '@mui/icons-material/ContentCopy' -import { default as DarkModeOutlined } from '@mui/icons-material/DarkModeOutlined' -import { default as DeleteIcon } from '@mui/icons-material/Delete' -import { default as ErrorOutlineIcon } from '@mui/icons-material/ErrorOutline' -import { default as InfoIcon } from '@mui/icons-material/Info' -import { default as InfoOutlinedIcon } from '@mui/icons-material/InfoOutlined' -import { default as KeyboardArrowDownOutlinedIcon } from '@mui/icons-material/KeyboardArrowDownOutlined' -import { default as Layers } from '@mui/icons-material/Layers' -import { default as LightModeOutlined } from '@mui/icons-material/LightModeOutlined' -import { default as Link } from '@mui/icons-material/Link' -import { default as Logout } from '@mui/icons-material/Logout' -import { default as Notifications } from '@mui/icons-material/Notifications' -import { default as OpenInNew } from '@mui/icons-material/OpenInNew' -import { default as QrCode } from '@mui/icons-material/QrCode' -import { default as Search } from '@mui/icons-material/Search' -import { default as Tune } from '@mui/icons-material/Tune' -import { default as Verified } from '@mui/icons-material/Verified' -import { default as WarningAmberIcon } from '@mui/icons-material/WarningAmber' -import { default as Work } from '@mui/icons-material/Work' +import AccountCircleIcon from '@mui/icons-material/AccountCircle' +import Add from '@mui/icons-material/Add' +import AddPhotoAlternateOutlined from '@mui/icons-material/AddPhotoAlternateOutlined' +import CheckIcon from '@mui/icons-material/Check' +import ChevronLeft from '@mui/icons-material/ChevronLeft' +import Close from '@mui/icons-material/Close' +import ContentCopy from '@mui/icons-material/ContentCopy' +import DarkModeOutlined from '@mui/icons-material/DarkModeOutlined' +import DeleteIcon from '@mui/icons-material/Delete' +import DeleteOutlined from '@mui/icons-material/DeleteOutlined' +import DragIndicator from '@mui/icons-material/DragIndicator' +import DriveFileRenameOutlineOutlined from '@mui/icons-material/DriveFileRenameOutlineOutlined' +import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline' +import FolderOff from '@mui/icons-material/FolderOff' +import InfoIcon from '@mui/icons-material/Info' +import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined' +import KeyboardArrowDownOutlinedIcon from '@mui/icons-material/KeyboardArrowDownOutlined' +import Layers from '@mui/icons-material/Layers' +import LightModeOutlined from '@mui/icons-material/LightModeOutlined' +import Link from '@mui/icons-material/Link' +import Logout from '@mui/icons-material/Logout' +import Notifications from '@mui/icons-material/Notifications' +import OpenInNew from '@mui/icons-material/OpenInNew' +import QrCode from '@mui/icons-material/QrCode' +import Search from '@mui/icons-material/Search' +import Tune from '@mui/icons-material/Tune' +import Verified from '@mui/icons-material/Verified' +import WarningAmberIcon from '@mui/icons-material/WarningAmber' +import Work from '@mui/icons-material/Work' export enum Icons { Metamask = 'metamask', @@ -44,20 +48,24 @@ export const ICON_COMPONENTS = { close: Close, darkModeOutlined: DarkModeOutlined, delete: DeleteIcon, + deleteOutlined: DeleteOutlined, + dragIndicator: DragIndicator, + driveFileRenameOutlineOutlined: DriveFileRenameOutlineOutlined, errorOutline: ErrorOutlineIcon, - qrCode: QrCode, + folderOff: FolderOff, info: InfoIcon, infoOutlined: InfoOutlinedIcon, keyboardArrowDownOutlined: KeyboardArrowDownOutlinedIcon, - openInNew: OpenInNew, + layers: Layers, lightModeOutlined: LightModeOutlined, link: Link, logOut: Logout, + notifications: Notifications, + openInNew: OpenInNew, + qrCode: QrCode, search: Search, tune: Tune, verified: Verified, warningAmber: WarningAmberIcon, work: Work, - layers: Layers, - notifications: Notifications, } diff --git a/src/hooks/form.ts b/src/hooks/form.ts index 5a00dad0..6d7c3df9 100644 --- a/src/hooks/form.ts +++ b/src/hooks/form.ts @@ -1,12 +1,33 @@ import { yupResolver } from '@hookform/resolvers/yup' import { useState } from 'react' -import { DefaultValues, FieldErrorsImpl, Merge, useForm as useFormHook } from 'react-hook-form' +import { + Control, + DefaultValues, + FieldErrorsImpl, + FieldPath, + FieldValues, + useForm as useFormHook, + UseFormHandleSubmit, + UseFormRegister, +} from 'react-hook-form' import * as Yup from 'yup' -export const useForm = ( +export type Form = { + isFormDisabled: boolean + getErrorMessage: (fieldName: FieldPath) => string + enableForm: () => void + disableForm: () => void + formState: T + formErrors: FieldErrorsImpl + register: UseFormRegister + handleSubmit: UseFormHandleSubmit + control: Control +} + +export const useForm = ( defaultValues: R, schemaBuilder: (yup: typeof Yup) => T, -) => { +): Form => { const [isFormDisabled, setIsFormDisabled] = useState(false) const { @@ -24,7 +45,7 @@ export const useForm = ( resolver: yupResolver(schemaBuilder(Yup)), }) - const getErrorMessage = (fieldName: keyof Merge>): string => { + const getErrorMessage = (fieldName: FieldPath): string => { return errors[fieldName]?.message?.toString() ?? '' } diff --git a/src/pages/Orgs/pages/OrgsId/contexts/org-details.tsx b/src/pages/Orgs/pages/OrgsId/contexts/org-details.tsx index 14684ac7..229872e9 100644 --- a/src/pages/Orgs/pages/OrgsId/contexts/org-details.tsx +++ b/src/pages/Orgs/pages/OrgsId/contexts/org-details.tsx @@ -43,7 +43,7 @@ export const OrgDetailsContextProvider = ({ children }: { children: ReactNode }) isExact: true, }, { - label: 'Private', + label: 'Internal', route: generatePath(RoutePaths.OrgsIdGroups, { id: org.id }), }, ]} diff --git a/src/pages/Orgs/pages/OrgsId/pages/OrgGroups/pages/OrgGroupsId/components/List.tsx b/src/pages/Orgs/pages/OrgsId/pages/OrgGroups/pages/OrgGroupsId/components/List.tsx index abd2121d..ba9060fd 100644 --- a/src/pages/Orgs/pages/OrgsId/pages/OrgGroups/pages/OrgGroupsId/components/List.tsx +++ b/src/pages/Orgs/pages/OrgsId/pages/OrgGroups/pages/OrgGroupsId/components/List.tsx @@ -103,7 +103,7 @@ export default function List({ filter, ...rest }: Props) { ))} {drawerContent && ( - setIsDrawerShown(false)} anchor='right'> + setIsDrawerShown(false)}> Member Details diff --git a/src/pages/Orgs/pages/OrgsId/pages/OrgGroups/pages/OrgGroupsId/index.tsx b/src/pages/Orgs/pages/OrgsId/pages/OrgGroups/pages/OrgGroupsId/index.tsx index d679d322..750dad0c 100644 --- a/src/pages/Orgs/pages/OrgsId/pages/OrgGroups/pages/OrgGroupsId/index.tsx +++ b/src/pages/Orgs/pages/OrgsId/pages/OrgGroups/pages/OrgGroupsId/index.tsx @@ -166,11 +166,7 @@ export default function OrgGroupsId() { /> {routes} - setIsInviteDrawerShown(false)} - > + setIsInviteDrawerShown(false)}> theme.spacing(100)} diff --git a/src/pages/Orgs/pages/OrgsId/pages/OrgRoot/components/EditLinksDrawer.tsx b/src/pages/Orgs/pages/OrgsId/pages/OrgRoot/components/EditLinksDrawer.tsx new file mode 100644 index 00000000..c370f573 --- /dev/null +++ b/src/pages/Orgs/pages/OrgsId/pages/OrgRoot/components/EditLinksDrawer.tsx @@ -0,0 +1,133 @@ +import { CircularProgress, DrawerProps, Stack } from '@mui/material' +import { FormEvent, useCallback } from 'react' +import { useFieldArray } from 'react-hook-form' + +import { OrgMetadataLink, updateOrgMetadata } from '@/api/modules/orgs' +import VerticalDraggableContext from '@/contexts/vertical-draggable' +import { BusEvents } from '@/enums' +import { bus, ErrorHandler } from '@/helpers' +import { useForm } from '@/hooks' +import { useOrgDetails } from '@/pages/Orgs/pages/OrgsId/hooks' +import { UiButton, UiDrawer, UiDrawerActions, UiDrawerContent, UiDrawerTitle, UiIcon } from '@/ui' + +import LinkForm from './LinkForm' + +interface Props extends DrawerProps { + links: OrgMetadataLink[] + onLinksUpdate?: (links: OrgMetadataLink[]) => void +} + +const DEFAULT_LINK_VALUES: OrgMetadataLink = { + title: '', + url: '', +} + +const MAX_LINKS_COUNT = 10 + +export default function EditLinksDrawer({ links, onLinksUpdate, ...rest }: Props) { + const { org } = useOrgDetails() + + const form = useForm( + { + links: links.length ? links : [DEFAULT_LINK_VALUES], + }, + yup => + yup.object().shape({ + links: yup.array().of( + yup.object().shape({ + title: yup.string().required(), + url: yup.string().url().required(), + }), + ), + }), + ) + + const { fields, append, remove, move } = useFieldArray({ + name: 'links', + control: form.control, + }) + + const submit = useCallback( + async ({ links }: { links: OrgMetadataLink[] }) => { + form.disableForm() + + try { + await updateOrgMetadata(org.id, { links }) + onLinksUpdate?.(links) + bus.emit(BusEvents.success, { + message: 'Links updated', + }) + } catch (error) { + ErrorHandler.process(error) + } + + form.enableForm() + }, + [form, org.id, onLinksUpdate], + ) + + return ( + ) => { + e.preventDefault() + form.handleSubmit(submit)() + }, + }} + {...rest} + > + + {links.length ? 'Edit links' : 'Add links'} + + + + + {fields.map((field, index) => ( + remove(fields.indexOf(field))} + /> + ))} + + + + {fields.length < MAX_LINKS_COUNT && ( + } + onClick={() => append({ title: '', url: '' })} + sx={{ mt: fields.length ? 4 : 0 }} + > + Add link + + )} + + + + Save + + + + {form.isFormDisabled && ( + theme.palette.background.light} + > + + + )} + + ) +} diff --git a/src/pages/Orgs/pages/OrgsId/pages/OrgRoot/components/LinkForm.tsx b/src/pages/Orgs/pages/OrgsId/pages/OrgRoot/components/LinkForm.tsx index f602b2ad..394d2b38 100644 --- a/src/pages/Orgs/pages/OrgsId/pages/OrgRoot/components/LinkForm.tsx +++ b/src/pages/Orgs/pages/OrgsId/pages/OrgRoot/components/LinkForm.tsx @@ -1,72 +1,91 @@ -import { InputAdornment } from '@mui/material' -import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react' -import { useSearchParams } from 'react-router-dom' +import { useSortable } from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' +import { FormControl, Stack, Typography } from '@mui/material' +import { useMemo } from 'react' +import { Controller, FieldArrayWithId, FieldPath } from 'react-hook-form' -import { UiButton, UiSearchField } from '@/ui' +import { OrgMetadataLink } from '@/api/modules/orgs' +import { Form } from '@/hooks' +import { UiIcon, UiIconButton, UiTextField } from '@/ui' interface Props { - isLoading: boolean - onLinkIdChange: (linkId: string) => void + field: FieldArrayWithId + form: Form<{ links: OrgMetadataLink[] }> + index: number + onRemove?: () => void } -export default function LinkForm({ isLoading, onLinkIdChange }: Props) { - const [params] = useSearchParams() - const [linkOrLinkId, setLinkOrLinkId] = useState(params.get('linkId') ?? '') - const [isVerifying, setIsVerifying] = useState(false) +export default function LinkForm({ field, index, form, onRemove }: Props) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ + id: field.id, + }) - const linkId = useMemo(() => { - if (!isVerifying) return '' - - try { - const url = new URL(linkOrLinkId) - return url.pathname.split('/').pop() || '' - } catch { - return linkOrLinkId - } - }, [linkOrLinkId, isVerifying]) - - const handleLinkChange = useCallback( - (e: ChangeEvent) => { - setLinkOrLinkId(e.target.value) - setIsVerifying(false) - }, - [setLinkOrLinkId, setIsVerifying], + const formFields = useMemo< + { + name: FieldPath<{ links: OrgMetadataLink[] }> + placeholder: string + }[] + >( + () => [ + { + name: `links.${index}.title`, + placeholder: 'Title', + }, + { + name: `links.${index}.url`, + placeholder: 'URL', + }, + ], + [index], ) - useEffect(() => { - onLinkIdChange(linkId) - }, [linkId, onLinkIdChange]) + const hasManyLinks = useMemo(() => form.formState.links.length > 1, [form]) return ( -
{ - e.preventDefault() - setIsVerifying(true) + theme.palette.background.paper, + zIndex: isDragging ? 1000 : 0, }} > - - - {isLoading ? 'Verifying...' : 'Verify'} - - - ), - }} - onChange={handleLinkChange} - /> - + + Link {index + 1} + {hasManyLinks && ( + + + + + + + + + )} + + + {formFields.map(({ name, placeholder }) => ( + ( + + + + )} + /> + ))} +
) } diff --git a/src/pages/Orgs/pages/OrgsId/pages/OrgRoot/components/LinksBlock.tsx b/src/pages/Orgs/pages/OrgsId/pages/OrgRoot/components/LinksBlock.tsx index a8af8d59..6f843ccf 100644 --- a/src/pages/Orgs/pages/OrgsId/pages/OrgRoot/components/LinksBlock.tsx +++ b/src/pages/Orgs/pages/OrgsId/pages/OrgRoot/components/LinksBlock.tsx @@ -1,12 +1,18 @@ import { Stack, Typography, useTheme } from '@mui/material' +import { useState } from 'react' +import { NoDataViewer } from '@/common' import { useOrgDetails } from '@/pages/Orgs/pages/OrgsId/hooks' +import { UiButton, UiIcon } from '@/ui' +import EditLinksDrawer from './EditLinksDrawer' import LinkItem from './LinkItem' export default function LinksBlock() { - const { org } = useOrgDetails() + const { org, isOrgOwner } = useOrgDetails() const { palette } = useTheme() + const [isLinkDrawerShown, setIsLinkDrawerShown] = useState(false) + const [links, setLinks] = useState(org.metadata.links ?? []) return ( - Links - - {org.metadata.links?.length ? ( - org.metadata.links.map((link, index) => ) - ) : ( - + Links + {!!links.length && isOrgOwner && ( + } + onClick={() => setIsLinkDrawerShown(true)} > - No links yet - + Edit + )} + + {links.length ? ( + links.map((link, index) => ) + ) : ( + setIsLinkDrawerShown(true)}> + Add + + ) + } + /> + )} + + + setIsLinkDrawerShown(false)} + onLinksUpdate={links => { + setIsLinkDrawerShown(false) + setLinks(links) + }} + /> ) } diff --git a/src/pages/Orgs/pages/OrgsId/pages/OrgRoot/components/ProofsLinkForm.tsx b/src/pages/Orgs/pages/OrgsId/pages/OrgRoot/components/ProofsLinkForm.tsx new file mode 100644 index 00000000..1bebd97a --- /dev/null +++ b/src/pages/Orgs/pages/OrgsId/pages/OrgRoot/components/ProofsLinkForm.tsx @@ -0,0 +1,72 @@ +import { InputAdornment } from '@mui/material' +import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react' +import { useSearchParams } from 'react-router-dom' + +import { UiButton, UiSearchField } from '@/ui' + +interface Props { + isLoading: boolean + onLinkIdChange: (linkId: string) => void +} + +export default function ProofsLinkForm({ isLoading, onLinkIdChange }: Props) { + const [params] = useSearchParams() + const [linkOrLinkId, setLinkOrLinkId] = useState(params.get('linkId') ?? '') + const [isVerifying, setIsVerifying] = useState(false) + + const linkId = useMemo(() => { + if (!isVerifying) return '' + + try { + const url = new URL(linkOrLinkId) + return url.pathname.split('/').pop() || '' + } catch { + return linkOrLinkId + } + }, [linkOrLinkId, isVerifying]) + + const handleLinkChange = useCallback( + (e: ChangeEvent) => { + setLinkOrLinkId(e.target.value) + setIsVerifying(false) + }, + [setLinkOrLinkId, setIsVerifying], + ) + + useEffect(() => { + onLinkIdChange(linkId) + }, [linkId, onLinkIdChange]) + + return ( +
{ + e.preventDefault() + setIsVerifying(true) + }} + > + + + {isLoading ? 'Verifying...' : 'Verify'} + + + ), + }} + onChange={handleLinkChange} + /> + + ) +} diff --git a/src/pages/Orgs/pages/OrgsId/pages/OrgRoot/components/VerifyProofsBlock.tsx b/src/pages/Orgs/pages/OrgsId/pages/OrgRoot/components/VerifyProofsBlock.tsx index b5e2c0df..628a604a 100644 --- a/src/pages/Orgs/pages/OrgsId/pages/OrgRoot/components/VerifyProofsBlock.tsx +++ b/src/pages/Orgs/pages/OrgsId/pages/OrgRoot/components/VerifyProofsBlock.tsx @@ -4,8 +4,8 @@ import { useState } from 'react' import { useLinkProofs } from '@/api/modules/link' import { UiIcon, UiTooltip } from '@/ui' -import LinkForm from './LinkForm' import ProofFieldForm from './ProofFieldForm' +import ProofsLinkForm from './ProofsLinkForm' export default function VerifyProofsBlock() { const { palette } = useTheme() @@ -44,7 +44,7 @@ export default function VerifyProofsBlock() {
- + {linkId && ( <> diff --git a/src/pages/Orgs/pages/OrgsId/pages/OrgRoot/index.tsx b/src/pages/Orgs/pages/OrgsId/pages/OrgRoot/index.tsx index eda4298d..730c5ca9 100644 --- a/src/pages/Orgs/pages/OrgsId/pages/OrgRoot/index.tsx +++ b/src/pages/Orgs/pages/OrgsId/pages/OrgRoot/index.tsx @@ -1,4 +1,4 @@ -import { Stack, Typography, useTheme } from '@mui/material' +import { Box, Stack, Typography, useTheme } from '@mui/material' import { NavLink } from 'react-router-dom' import { RoutePaths } from '@/enums' @@ -12,23 +12,27 @@ export default function OrgRoot() { const { palette } = useTheme() return ( - - {isOrgOwner && orgTabs} - - - + + + View all organizations - + {isOrgOwner && orgTabs} + - - - - - + + + ) } diff --git a/src/pages/Orgs/pages/OrgsList/index.tsx b/src/pages/Orgs/pages/OrgsList/index.tsx index ea431c12..e89f5db1 100644 --- a/src/pages/Orgs/pages/OrgsList/index.tsx +++ b/src/pages/Orgs/pages/OrgsList/index.tsx @@ -3,7 +3,7 @@ import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { Navigate, NavLink } from 'react-router-dom' -import { loadOrgsAmount, OrgsRequestFilters, OrgsRequestFiltersMap } from '@/api/modules/orgs' +import { loadOrgsCount, OrgsRequestFilters, OrgsRequestFiltersMap } from '@/api/modules/orgs' import { PageListFilters, PageTitles } from '@/common' import { RoutePaths } from '@/enums' import { useLoading, useMetamaskZkpSnapContext, useNestedRoutes } from '@/hooks' @@ -47,10 +47,10 @@ export default function OrgsList() { ]) const init = useCallback(async () => { - return loadOrgsAmount() + return loadOrgsCount() }, []) - const { data: orgsAmount } = useLoading(undefined, init, { + const { data: orgsCount } = useLoading(undefined, init, { loadOnMount: true, }) @@ -60,7 +60,7 @@ export default function OrgsList() { setIsDrawerShown(prev => !prev)}>{`Toggle Drawer`} - setIsDrawerShown(false)}> + setIsDrawerShown(false)}> > = { root: ({ theme }) => ({ borderRadius: theme.spacing(250), transition: Transitions.Default, - '&.MuiButton-sizeLarge': { - ...typography.buttonLarge, - px: theme.spacing(4), - height: theme.spacing(12), - }, - '&.MuiButton-sizeMedium': { - ...typography.buttonMedium, - px: theme.spacing(4), - height: theme.spacing(8), + }), + containedSizeLarge: ({ theme }) => ({ + ...typography.buttonLarge, + padding: theme.spacing(0, 4), + height: theme.spacing(12), + }), + containedSizeMedium: ({ theme }) => ({ + ...typography.buttonMedium, + padding: theme.spacing(0, 4), + height: theme.spacing(8), + }), + containedSizeSmall: ({ theme }) => ({ + ...typography.buttonSmall, + padding: theme.spacing(0, 2), + height: theme.spacing(6), + }), + fullWidth: { + width: '100%', + }, + text: { + padding: 0, + minWidth: 'unset', + '&:hover': { + backgroundColor: 'transparent', }, - '&.MuiButton-sizeSmall': { - ...typography.buttonSmall, - px: theme.spacing(2), - height: theme.spacing(6), + }, + textPrimary: ({ theme }) => ({ + color: theme.palette.text.primary, + }), + textSecondary: ({ theme }) => ({ + color: theme.palette.text.secondary, + '&:hover': { + color: theme.palette.secondary.main, }, }), }, @@ -115,7 +134,7 @@ export const components: Components> = { }, }, '& .MuiInputBase-sizeSmall': { - height: theme.spacing(8), + height: theme.spacing(10), }, '& .MuiOutlinedInput-notchedOutline': { transition: Transitions.Default, @@ -177,23 +196,42 @@ export const components: Components> = { }, }, MuiIconButton: { + defaultProps: { + color: 'primary', + }, styleOverrides: { - root: ({ theme }) => ({ + root: { padding: 0, - color: theme.palette.text.primary, transition: Transitions.Default, - - '&[disabled]': { - color: theme.palette.text.disabled, - }, - - '&:not([disabled]):hover': { + '&:hover': { backgroundColor: 'transparent', + }, + }, + colorPrimary: ({ theme }) => ({ + color: theme.palette.text.primary, + }), + colorSecondary: ({ theme }) => ({ + color: theme.palette.text.secondary, + '&:hover': { color: theme.palette.text.primary, }, - - '&:not([disabled]).active, &:not([disabled]):active': { - color: theme.palette.primary.main, + }), + colorSuccess: ({ theme }) => ({ + color: theme.palette.success.main, + '&:hover': { + color: theme.palette.success.dark, + }, + }), + colorError: ({ theme }) => ({ + color: theme.palette.error.main, + '&:hover': { + color: theme.palette.error.dark, + }, + }), + colorWarning: ({ theme }) => ({ + color: theme.palette.warning.main, + '&:hover': { + color: theme.palette.warning.dark, }, }), }, @@ -288,4 +326,31 @@ export const components: Components> = { }), }, }, + MuiDrawer: { + defaultProps: { + anchor: 'right', + }, + styleOverrides: { + root: { + '& > .MuiBackdrop-root': { + backgroundColor: 'rgba(32, 32, 32, 0.50)', + }, + }, + paper: ({ theme }) => ({ + width: '100%', + maxWidth: theme.spacing(108), + backgroundColor: theme.palette.background.paper, + boxShadow: 'none', + border: 'none', + borderRadius: theme.spacing(3), + }), + paperAnchorRight: ({ theme }) => ({ + height: 'unset', + top: theme.spacing(3), + bottom: theme.spacing(3), + left: 'unset', + right: theme.spacing(3), + }), + }, + }, } diff --git a/src/ui/UiDrawer.tsx b/src/ui/UiDrawer.tsx index 81a8083f..2aa63244 100644 --- a/src/ui/UiDrawer.tsx +++ b/src/ui/UiDrawer.tsx @@ -1,7 +1,61 @@ -import { Drawer, type DrawerProps } from '@mui/material' +import { + Box, + type BoxProps, + DialogTitle, + Drawer, + type DrawerProps, + ModalProps, + Stack, + type StackProps, + Typography, + useTheme, +} from '@mui/material' -interface Props extends DrawerProps {} +import UiIcon from './UiIcon' +import UiIconButton from './UiIconButton' -export default function UiDrawer({ children, ...rest }: Props) { - return {children} +interface UiDrawerTitleProps extends StackProps { + onClose?: ModalProps['onClose'] +} + +export function UiDrawerTitle({ children, onClose, ...rest }: UiDrawerTitleProps) { + const { palette } = useTheme() + + return ( + + + {children} + + onClose?.(e, 'backdropClick')} + sx={{ color: palette.text.secondary }} + > + + + + ) +} + +export function UiDrawerContent(props: BoxProps) { + return +} + +export function UiDrawerActions(props: BoxProps) { + return ( + theme.palette.divider} {...props}> + ) +} + +export default function UiDrawer(props: DrawerProps) { + return } diff --git a/src/ui/UiIconButton.tsx b/src/ui/UiIconButton.tsx index 223d810a..6361acd0 100644 --- a/src/ui/UiIconButton.tsx +++ b/src/ui/UiIconButton.tsx @@ -2,7 +2,7 @@ import { IconButton, type IconButtonProps } from '@mui/material' interface Props extends IconButtonProps {} -export default function UiButton({ ...rest }: Props) { +export default function UiIconButton({ ...rest }: Props) { return ( , ref autoComplete='off' sx={{ ...props.sx, - height: spacing(10), + height: props.size === 'small' ? spacing(8) : spacing(10), }} InputProps={{ ...props.InputProps, diff --git a/src/ui/UiSelect.tsx b/src/ui/UiSelect.tsx index 03b1a936..079dcdaf 100644 --- a/src/ui/UiSelect.tsx +++ b/src/ui/UiSelect.tsx @@ -53,11 +53,11 @@ const UiSelect = forwardRef( {options.map(({ value, label, adornmentLeft, adornmentRight }, idx) => ( theme.spacing(1)} > {adornmentLeft} {label} diff --git a/src/ui/index.ts b/src/ui/index.ts index b33abe97..9c0419d8 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -2,7 +2,7 @@ export * from './modals' export { default as UiButton } from './UiButton' export { default as UiCheckbox } from './UiCheckbox' export { default as UiDatePicker } from './UiDatePicker' -export { default as UiDrawer } from './UiDrawer' +export { default as UiDrawer, UiDrawerActions, UiDrawerContent, UiDrawerTitle } from './UiDrawer' export { default as UiIcon } from './UiIcon' export { default as UiIconButton } from './UiIconButton' export { default as UiImageUploader } from './UiImageUploader' diff --git a/yarn.lock b/yarn.lock index 046a58af..3b3bdd62 100644 --- a/yarn.lock +++ b/yarn.lock @@ -126,6 +126,68 @@ __metadata: languageName: node linkType: hard +"@dnd-kit/accessibility@npm:^3.1.0": + version: 3.1.0 + resolution: "@dnd-kit/accessibility@npm:3.1.0" + dependencies: + tslib: "npm:^2.0.0" + peerDependencies: + react: ">=16.8.0" + checksum: 4f9d24e801d66d4fbb551ec389ed90424dd4c5bbdf527000a618e9abb9833cbd84d9a79e362f470ccbccfbd6d00217a9212c92f3cef66e01c951c7f79625b9d7 + languageName: node + linkType: hard + +"@dnd-kit/core@npm:^6.1.0": + version: 6.1.0 + resolution: "@dnd-kit/core@npm:6.1.0" + dependencies: + "@dnd-kit/accessibility": "npm:^3.1.0" + "@dnd-kit/utilities": "npm:^3.2.2" + tslib: "npm:^2.0.0" + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: c793eb97cb59285ca8937ebcdfcd27cff09d750ae06722e36ca5ed07925e41abc36a38cff98f9f6056f7a07810878d76909826142a2968330e7e22060e6be584 + languageName: node + linkType: hard + +"@dnd-kit/modifiers@npm:^7.0.0": + version: 7.0.0 + resolution: "@dnd-kit/modifiers@npm:7.0.0" + dependencies: + "@dnd-kit/utilities": "npm:^3.2.2" + tslib: "npm:^2.0.0" + peerDependencies: + "@dnd-kit/core": ^6.1.0 + react: ">=16.8.0" + checksum: 542e1d2b6102a5c826118c36158aab23c5437d24008cab4848b0866d3d850b4410c4f465690767dd1f31fde33a1fa9d238675be70f174c179485ce376f0c8aa6 + languageName: node + linkType: hard + +"@dnd-kit/sortable@npm:^8.0.0": + version: 8.0.0 + resolution: "@dnd-kit/sortable@npm:8.0.0" + dependencies: + "@dnd-kit/utilities": "npm:^3.2.2" + tslib: "npm:^2.0.0" + peerDependencies: + "@dnd-kit/core": ^6.1.0 + react: ">=16.8.0" + checksum: a6066c652b892c6a11320c7d8f5c18fdf723e721e8eea37f4ab657dee1ac5e7ca710ac32ce0712a57fe968bc07c13bcea5d5599d90dfdd95619e162befd4d2fb + languageName: node + linkType: hard + +"@dnd-kit/utilities@npm:^3.2.2": + version: 3.2.2 + resolution: "@dnd-kit/utilities@npm:3.2.2" + dependencies: + tslib: "npm:^2.0.0" + peerDependencies: + react: ">=16.8.0" + checksum: 9aa90526f3e3fd567b5acc1b625a63177b9e8d00e7e50b2bd0e08fa2bf4dba7e19529777e001fdb8f89a7ce69f30b190c8364d390212634e0afdfa8c395e85a0 + languageName: node + linkType: hard + "@emotion/babel-plugin@npm:^11.11.0": version: 11.11.0 resolution: "@emotion/babel-plugin@npm:11.11.0" @@ -8813,6 +8875,9 @@ __metadata: "@distributedlab/jac": "npm:^1.0.0-rc.9" "@distributedlab/tools": "npm:^1.0.0-rc.9" "@distributedlab/w3p": "npm:^1.0.0-rc.9" + "@dnd-kit/core": "npm:^6.1.0" + "@dnd-kit/modifiers": "npm:^7.0.0" + "@dnd-kit/sortable": "npm:^8.0.0" "@emotion/react": "npm:^11.11.1" "@emotion/styled": "npm:^11.11.0" "@esbuild-plugins/node-globals-polyfill": "npm:^0.2.3" @@ -10365,7 +10430,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.1, tslib@npm:^2.5.0": +"tslib@npm:^2.0.0, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.1, tslib@npm:^2.5.0": version: 2.6.2 resolution: "tslib@npm:2.6.2" checksum: e03a8a4271152c8b26604ed45535954c0a45296e32445b4b87f8a5abdb2421f40b59b4ca437c4346af0f28179780d604094eb64546bee2019d903d01c6c19bdb