diff --git a/ui/src/client/fetcher.ts b/ui/src/client/fetcher.ts index 66c0579c3..12d893e0f 100644 --- a/ui/src/client/fetcher.ts +++ b/ui/src/client/fetcher.ts @@ -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 } } diff --git a/ui/src/components/sharing/index.tsx b/ui/src/components/sharing/index.tsx index 39b084874..6823259be 100644 --- a/ui/src/components/sharing/index.tsx +++ b/ui/src/components/sharing/index.tsx @@ -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(), ) @@ -69,7 +68,7 @@ const Sharing = () => { className={cx('flex', 'flex-row', 'items-center', 'gap-0.5')} > Users - {file && userPermissions && userPermissions.length > 0 ? ( + {userPermissions && userPermissions.length > 0 ? ( {userPermissions.length} @@ -81,7 +80,7 @@ const Sharing = () => { className={cx('flex', 'flex-row', 'items-center', 'gap-0.5')} > Groups - {file && groupPermissions && groupPermissions.length > 0 ? ( + {groupPermissions && groupPermissions.length > 0 ? ( {groupPermissions.length} @@ -91,10 +90,10 @@ const Sharing = () => { - + - + diff --git a/ui/src/components/sharing/sharing-form-skeleton.tsx b/ui/src/components/sharing/sharing-form-skeleton.tsx index b6b6e40a6..bb747a11e 100644 --- a/ui/src/components/sharing/sharing-form-skeleton.tsx +++ b/ui/src/components/sharing/sharing-form-skeleton.tsx @@ -12,9 +12,9 @@ import cx from 'classnames' const SharingFormSkeleton = () => (
- - - + + +
) diff --git a/ui/src/components/sharing/sharing-group-overview.tsx b/ui/src/components/sharing/sharing-group-form.tsx similarity index 68% rename from ui/src/components/sharing/sharing-group-overview.tsx rename to ui/src/components/sharing/sharing-group-form.tsx index d5384f8ca..2d06d2274 100644 --- a/ui/src/components/sharing/sharing-group-overview.tsx +++ b/ui/src/components/sharing/sharing-group-form.tsx @@ -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' @@ -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() const [permission, setPermission] = useState() - 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, @@ -89,8 +104,34 @@ const SharingGroupOverview = () => { return (
- {!groups ? : null} - {groups && groups.totalElements > 0 ? ( + {isWorkspaceLoading || isGroupsLoading ? : null} + {isWorkspaceError || isGroupsError ? ( + + ) : null} + {isWorkspaceReady && isGroupsEmpty ? ( + + {workspace && + geEditorPermission(workspace.organization.permission) ? ( + + ) : null} + + } + height="auto" + /> + ) : null} + {isWorkspaceReady && isGroupsReady ? (
{
) : null} - {groups && groups.totalElements === 0 ? ( - - {workspace && - geEditorPermission(workspace.organization.permission) ? ( - - ) : null} - - } - height="auto" - /> - ) : null} {isSingleSelection ? ( <>
@@ -152,4 +173,4 @@ const SharingGroupOverview = () => { ) } -export default SharingGroupOverview +export default SharingGroupForm diff --git a/ui/src/components/sharing/sharing-group-permissions.tsx b/ui/src/components/sharing/sharing-group-permissions.tsx index 0129ef568..78365dcd0 100644 --- a/ui/src/components/sharing/sharing-group-permissions.tsx +++ b/ui/src/components/sharing/sharing-group-permissions.tsx @@ -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, @@ -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() - 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) => { @@ -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 ? : null} - {permissions && permissions.length === 0 ? ( + {isPermissionsLoading ? : null} + {isPermissionsError ? ( + + ) : null} + {isPermissionsEmpty ? ( ) : null} - {permissions && permissions.length > 0 ? ( + {isPermissionsReady ? ( diff --git a/ui/src/components/sharing/sharing-user-overview.tsx b/ui/src/components/sharing/sharing-user-form.tsx similarity index 77% rename from ui/src/components/sharing/sharing-user-overview.tsx rename to ui/src/components/sharing/sharing-user-form.tsx index 7f83fef1d..21149141b 100644 --- a/ui/src/components/sharing/sharing-user-overview.tsx +++ b/ui/src/components/sharing/sharing-user-form.tsx @@ -10,17 +10,20 @@ import { useCallback, useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' import { Button } from '@chakra-ui/react' -import { IconCheck, IconPersonAdd, SectionPlaceholder, Select } from '@koupr/ui' +import { + IconCheck, + IconPersonAdd, + SectionError, + SectionPlaceholder, + Select, +} from '@koupr/ui' import { OptionBase } from 'chakra-react-select' import cx from 'classnames' import FileAPI from '@/client/api/file' -import { - geEditorPermission, - geOwnerPermission, - PermissionType, -} from '@/client/api/permission' +import { geEditorPermission, PermissionType } from '@/client/api/permission' import UserAPI, { User } from '@/client/api/user' import WorkspaceAPI from '@/client/api/workspace' +import { errorToString } from '@/client/error' import { swrConfig } from '@/client/options' import UserSelector from '@/components/common/user-selector' import { useAppDispatch, useAppSelector } from '@/store/hook' @@ -34,8 +37,8 @@ interface PermissionTypeOption extends OptionBase { label: string } -const SharingUserOverview = () => { - const { id: workspaceId, fileId } = useParams() +const SharingUserForm = () => { + const { id: workspaceId } = useParams() const navigate = useNavigate() const dispatch = useAppDispatch() const selection = useAppSelector((state) => state.ui.files.selection) @@ -43,19 +46,31 @@ const SharingUserOverview = () => { const [isGranting, setIsGranting] = useState(false) const [user, setUser] = useState() const [permission, setPermission] = useState() - const { data: workspace } = WorkspaceAPI.useGet(workspaceId) - const { data: file } = FileAPI.useGet(selection[0], swrConfig()) - const { data: users } = UserAPI.useList( + const { + data: workspace, + error: workspaceError, + isLoading: isWorkspaceLoading, + } = WorkspaceAPI.useGet(workspaceId, swrConfig()) + const { + data: users, + error: usersError, + isLoading: isUsersLoading, + } = UserAPI.useList( { organizationId: workspace?.organization.id, excludeMe: true, }, swrConfig(), ) - const { mutate: mutatePermissions } = FileAPI.useGetUserPermissions( - file && geOwnerPermission(file.permission) ? file.id : undefined, - ) const isSingleSelection = selection.length === 1 + const { mutate: mutatePermissions } = FileAPI.useGetGroupPermissions( + isSingleSelection ? selection[0] : undefined, + ) + const isWorkspaceError = !workspace && workspaceError + const isWorkspaceReady = workspace && !workspaceError + const isUsersError = !users && usersError + const isUsersEmpty = users && !usersError && users.totalElements === 0 + const isUsersReady = users && !usersError && users.totalElements > 0 const handleGrantPermission = useCallback(async () => { if (!user || !permission) { @@ -81,7 +96,6 @@ const SharingUserOverview = () => { setIsGranting(false) } }, [ - fileId, selection, user, permission, @@ -101,8 +115,14 @@ const SharingUserOverview = () => { return (
- {!users ? : null} - {users && users.totalElements === 0 ? ( + {isWorkspaceLoading || isUsersLoading ? : null} + {isWorkspaceError || isUsersError ? ( + + ) : null} + {isWorkspaceReady && isUsersEmpty ? ( { height="auto" /> ) : null} - {users && users.totalElements > 0 ? ( + {isWorkspaceReady && isUsersReady ? (
{ ) } -export default SharingUserOverview +export default SharingUserForm diff --git a/ui/src/components/sharing/sharing-user-permissions.tsx b/ui/src/components/sharing/sharing-user-permissions.tsx index 4e53ba7ab..7a6c94ebb 100644 --- a/ui/src/components/sharing/sharing-user-permissions.tsx +++ b/ui/src/components/sharing/sharing-user-permissions.tsx @@ -20,11 +20,17 @@ 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, { UserPermission } from '@/client/api/file' -import { geOwnerPermission } from '@/client/api/permission' import WorkspaceAPI from '@/client/api/workspace' +import { errorToString } from '@/client/error' import IdPUserAPI from '@/client/idp/user' import { swrConfig } from '@/client/options' import { getPictureUrlById } from '@/lib/helpers/picture' @@ -35,14 +41,19 @@ const SharingUserPermissions = () => { const selection = useAppSelector((state) => state.ui.files.selection) const mutateFiles = useAppSelector((state) => state.ui.files.mutate) const [revokedPermission, setRevokedPermission] = useState() - const { data: workspace } = WorkspaceAPI.useGet(workspaceId) - const { data: file } = FileAPI.useGet(selection[0], swrConfig()) + const { data: workspace } = WorkspaceAPI.useGet(workspaceId, swrConfig()) const { data: me } = IdPUserAPI.useGet() - const { data: permissions, mutate: mutatePermissions } = - FileAPI.useGetUserPermissions( - file && geOwnerPermission(file.permission) ? file.id : undefined, - swrConfig(), - ) + const { + data: permissions, + error: permissionsError, + isLoading: isPermissionsLoading, + mutate: mutatePermissions, + } = FileAPI.useGetUserPermissions(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: UserPermission) => { @@ -63,11 +74,14 @@ const SharingUserPermissions = () => { return ( <> - {!permissions ? : null} - {permissions && permissions.length === 0 ? ( + {isPermissionsLoading ? : null} + {isPermissionsError ? ( + + ) : null} + {isPermissionsEmpty ? ( ) : null} - {permissions && permissions.length > 0 ? ( + {isPermissionsReady ? (