Skip to content

Commit

Permalink
refactor(ui): error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
bouassaba committed Nov 30, 2024
1 parent 2c32725 commit 4fd6b41
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 126 deletions.
16 changes: 8 additions & 8 deletions ui/src/client/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,15 @@ async function handleResponse(
if (response.status === 401 && redirect) {
window.location.href = '/sign-in'
}
let error
try {
error = await response.json()
} catch {
error = 'Oops! something went wrong.'
}
if (showError) {
let message
try {
message = errorToString(await response.json())
} catch {
message = 'Oops! something went wrong.'
}
store.dispatch(errorOccurred(message))
throw new Error(message)
store.dispatch(errorOccurred(errorToString(error)))
}
throw error
}
}
19 changes: 9 additions & 10 deletions ui/src/components/sharing/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,23 @@ import {
} from '@chakra-ui/react'
import cx from 'classnames'
import FileAPI from '@/client/api/file'
import { geOwnerPermission } from '@/client/api/permission'
import { swrConfig } from '@/client/options'
import { useAppDispatch, useAppSelector } from '@/store/hook'
import { sharingModalDidClose } from '@/store/ui/files'
import SharingGroupOverview from './sharing-group-overview'
import SharingUserOverview from './sharing-user-overview'
import SharingGroupForm from './sharing-group-form'
import SharingUserForm from './sharing-user-form'

const Sharing = () => {
const dispatch = useAppDispatch()
const selection = useAppSelector((state) => state.ui.files.selection)
const isModalOpen = useAppSelector((state) => state.ui.files.isShareModalOpen)
const { data: file } = FileAPI.useGet(selection[0], swrConfig())
const isSingleSelection = selection.length === 1
const { data: userPermissions } = FileAPI.useGetUserPermissions(
file && geOwnerPermission(file.permission) ? file.id : undefined,
isSingleSelection ? selection[0] : undefined,
swrConfig(),
)
const { data: groupPermissions } = FileAPI.useGetGroupPermissions(
file && geOwnerPermission(file.permission) ? file.id : undefined,
isSingleSelection ? selection[0] : undefined,
swrConfig(),
)

Expand Down Expand Up @@ -69,7 +68,7 @@ const Sharing = () => {
className={cx('flex', 'flex-row', 'items-center', 'gap-0.5')}
>
<span>Users</span>
{file && userPermissions && userPermissions.length > 0 ? (
{userPermissions && userPermissions.length > 0 ? (
<Tag className={cx('rounded-full')}>
{userPermissions.length}
</Tag>
Expand All @@ -81,7 +80,7 @@ const Sharing = () => {
className={cx('flex', 'flex-row', 'items-center', 'gap-0.5')}
>
<span>Groups</span>
{file && groupPermissions && groupPermissions.length > 0 ? (
{groupPermissions && groupPermissions.length > 0 ? (
<Tag className={cx('rounded-full')}>
{groupPermissions.length}
</Tag>
Expand All @@ -91,10 +90,10 @@ const Sharing = () => {
</TabList>
<TabPanels>
<TabPanel>
<SharingUserOverview />
<SharingUserForm />
</TabPanel>
<TabPanel>
<SharingGroupOverview />
<SharingGroupForm />
</TabPanel>
</TabPanels>
</Tabs>
Expand Down
6 changes: 3 additions & 3 deletions ui/src/components/sharing/sharing-form-skeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import cx from 'classnames'

const SharingFormSkeleton = () => (
<div className={cx('flex', 'flex-col', 'gap-1.5')}>
<Skeleton className={cx('rounded-xl', 'w-[40px]')} />
<Skeleton className={cx('rounded-xl', 'w-[40px]')} />
<Skeleton className={cx('rounded-xl', 'w-[40px]')} />
<Skeleton className={cx('rounded-xl', 'h-[40px]')} />
<Skeleton className={cx('rounded-xl', 'h-[40px]')} />
<Skeleton className={cx('rounded-xl', 'h-[40px]')} />
</div>
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,20 @@
import { useCallback, useState } from 'react'
import { Link, useParams } from 'react-router-dom'
import { Button } from '@chakra-ui/react'
import { IconAdd, IconCheck, SectionPlaceholder, Select } from '@koupr/ui'
import {
IconAdd,
IconCheck,
SectionError,
SectionPlaceholder,
Select,
} from '@koupr/ui'
import { OptionBase } from 'chakra-react-select'
import cx from 'classnames'
import FileAPI from '@/client/api/file'
import GroupAPI, { Group } from '@/client/api/group'
import {
geEditorPermission,
geOwnerPermission,
PermissionType,
} from '@/client/api/permission'
import { geEditorPermission, PermissionType } from '@/client/api/permission'
import WorkspaceAPI from '@/client/api/workspace'
import { errorToString } from '@/client/error'
import { swrConfig } from '@/client/options'
import GroupSelector from '@/components/common/group-selector'
import { useAppDispatch, useAppSelector } from '@/store/hook'
Expand All @@ -33,51 +36,63 @@ interface PermissionTypeOption extends OptionBase {
label: string
}

const SharingGroupOverview = () => {
const { id: workspaceId, fileId } = useParams()
const SharingGroupForm = () => {
const { id: workspaceId } = useParams()
const dispatch = useAppDispatch()
const selection = useAppSelector((state) => state.ui.files.selection)
const mutateFiles = useAppSelector((state) => state.ui.files.mutate)
const [isGranting, setIsGranting] = useState(false)
const [group, setGroup] = useState<Group>()
const [permission, setPermission] = useState<string>()
const { data: workspace } = WorkspaceAPI.useGet(workspaceId)
const { data: file } = FileAPI.useGet(selection[0], swrConfig())
const { data: groups } = GroupAPI.useList(
const {
data: workspace,
error: workspaceError,
isLoading: isWorkspaceLoading,
} = WorkspaceAPI.useGet(workspaceId, swrConfig())
const {
data: groups,
error: groupsError,
isLoading: isGroupsLoading,
} = GroupAPI.useList(
{
organizationId: workspace?.organization.id,
},
swrConfig(),
)
const isSingleSelection = selection.length === 1
const { mutate: mutatePermissions } = FileAPI.useGetGroupPermissions(
file && geOwnerPermission(file.permission) ? file.id : undefined,
isSingleSelection ? selection[0] : undefined,
)
const isSingleSelection = selection.length === 1
const isWorkspaceError = !workspace && workspaceError
const isWorkspaceReady = workspace && !workspaceError
const isGroupsError = !groups && groupsError
const isGroupsEmpty = groups && !groupsError && groups.totalElements === 0
const isGroupsReady = groups && !groupsError && groups.totalElements > 0

const handleGrantPermission = useCallback(async () => {
if (group && permission) {
try {
setIsGranting(true)
await FileAPI.grantGroupPermission({
ids: selection,
groupId: group.id,
permission,
})
await mutateFiles?.()
if (isSingleSelection) {
await mutatePermissions()
}
setGroup(undefined)
setIsGranting(false)
if (!isSingleSelection) {
dispatch(sharingModalDidClose())
}
} catch {
setIsGranting(false)
if (!group || !permission) {
return
}
try {
setIsGranting(true)
await FileAPI.grantGroupPermission({
ids: selection,
groupId: group.id,
permission,
})
await mutateFiles?.()
if (isSingleSelection) {
await mutatePermissions()
}
setGroup(undefined)
setIsGranting(false)
if (!isSingleSelection) {
dispatch(sharingModalDidClose())
}
} catch {
setIsGranting(false)
}
}, [
fileId,
selection,
group,
permission,
Expand All @@ -89,8 +104,34 @@ const SharingGroupOverview = () => {

return (
<div className={cx('flex', 'flex-col', 'gap-1.5')}>
{!groups ? <SharingFormSkeleton /> : null}
{groups && groups.totalElements > 0 ? (
{isWorkspaceLoading || isGroupsLoading ? <SharingFormSkeleton /> : null}
{isWorkspaceError || isGroupsError ? (
<SectionError
text={errorToString(workspaceError || groupsError)}
height="auto"
/>
) : null}
{isWorkspaceReady && isGroupsEmpty ? (
<SectionPlaceholder
text="This organization has no groups."
content={
<>
{workspace &&
geEditorPermission(workspace.organization.permission) ? (
<Button
as={Link}
leftIcon={<IconAdd />}
to={`/new/group?org=${workspace.organization.id}`}
>
New Group
</Button>
) : null}
</>
}
height="auto"
/>
) : null}
{isWorkspaceReady && isGroupsReady ? (
<div className={cx('flex', 'flex-col', 'gap-1.5')}>
<GroupSelector
value={group}
Expand Down Expand Up @@ -122,26 +163,6 @@ const SharingGroupOverview = () => {
</Button>
</div>
) : null}
{groups && groups.totalElements === 0 ? (
<SectionPlaceholder
text="This organization has no groups."
content={
<>
{workspace &&
geEditorPermission(workspace.organization.permission) ? (
<Button
as={Link}
leftIcon={<IconAdd />}
to={`/new/group?org=${workspace.organization.id}`}
>
New Group
</Button>
) : null}
</>
}
height="auto"
/>
) : null}
{isSingleSelection ? (
<>
<hr />
Expand All @@ -152,4 +173,4 @@ const SharingGroupOverview = () => {
)
}

export default SharingGroupOverview
export default SharingGroupForm
45 changes: 27 additions & 18 deletions ui/src/components/sharing/sharing-group-permissions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
// by the GNU Affero General Public License v3.0 only, included in the file
// AGPL-3.0-only in the root of this repository.
import { useCallback, useState } from 'react'
import { useParams } from 'react-router-dom'
import {
Table,
Thead,
Expand All @@ -20,25 +19,34 @@ import {
Badge,
Avatar,
} from '@chakra-ui/react'
import { IconDelete, SectionPlaceholder, SectionSpinner, Text } from '@koupr/ui'
import {
IconDelete,
SectionError,
SectionPlaceholder,
SectionSpinner,
Text,
} from '@koupr/ui'
import cx from 'classnames'
import FileAPI, { GroupPermission } from '@/client/api/file'
import { geOwnerPermission } from '@/client/api/permission'
import { errorToString } from '@/client/error'
import { swrConfig } from '@/client/options'
import { useAppSelector } from '@/store/hook'

const SharingGroupPermissions = () => {
const { fileId } = useParams()
const selection = useAppSelector((state) => state.ui.files.selection)
const mutateFiles = useAppSelector((state) => state.ui.files.mutate)
const [revokedPermission, setRevokedPermission] = useState<string>()
const { data: file } = FileAPI.useGet(selection[0], swrConfig())
const { data: permissions, mutate: mutatePermissions } =
FileAPI.useGetGroupPermissions(
file && geOwnerPermission(file.permission) ? file.id : undefined,
swrConfig(),
)
const isSingleSelection = selection.length === 1
const {
data: permissions,
error: permissionsError,
isLoading: isPermissionsLoading,
mutate: mutatePermissions,
} = FileAPI.useGetGroupPermissions(selection[0], swrConfig())
const isPermissionsError = !permissions && permissionsError
const isPermissionsEmpty =
permissions && !permissionsError && permissions.length === 0
const isPermissionsReady =
permissions && !permissionsError && permissions.length > 0

const handleRevokePermission = useCallback(
async (permission: GroupPermission) => {
Expand All @@ -49,23 +57,24 @@ const SharingGroupPermissions = () => {
groupId: permission.group.id,
})
await mutateFiles?.()
if (isSingleSelection) {
await mutatePermissions()
}
await mutatePermissions()
} finally {
setRevokedPermission(undefined)
}
},
[fileId, selection, isSingleSelection, mutateFiles, mutatePermissions],
[selection, mutateFiles, mutatePermissions],
)

return (
<>
{!permissions ? <SectionSpinner /> : null}
{permissions && permissions.length === 0 ? (
{isPermissionsLoading ? <SectionSpinner height="auto" /> : null}
{isPermissionsError ? (
<SectionError text={errorToString(permissionsError)} height="auto" />
) : null}
{isPermissionsEmpty ? (
<SectionPlaceholder text="Not shared with any groups." height="auto" />
) : null}
{permissions && permissions.length > 0 ? (
{isPermissionsReady ? (
<Table>
<Thead>
<Tr>
Expand Down
Loading

0 comments on commit 4fd6b41

Please sign in to comment.