From a522f252c17a5fe788c84e48547993a15fde3e4f Mon Sep 17 00:00:00 2001 From: SangLv Date: Thu, 27 Jul 2023 11:41:37 +0700 Subject: [PATCH 01/30] add role check --- frontend/src/@types/index.ts | 6 ++++++ frontend/src/api/users/UsersApiDTO.ts | 4 ++++ .../src/components/Database/DatabaseExperiments.tsx | 6 ++++-- frontend/src/pages/Workspace/index.tsx | 10 +++++++--- 4 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 frontend/src/@types/index.ts diff --git a/frontend/src/@types/index.ts b/frontend/src/@types/index.ts new file mode 100644 index 000000000..d24dc3f0d --- /dev/null +++ b/frontend/src/@types/index.ts @@ -0,0 +1,6 @@ +export const enum ROLE { + ADMIN = 1, + MANAGER = 10, + OPERATOR = 20, + GUEST_OPERATOR = 30 +} \ No newline at end of file diff --git a/frontend/src/api/users/UsersApiDTO.ts b/frontend/src/api/users/UsersApiDTO.ts index 114847bec..eb7294a79 100644 --- a/frontend/src/api/users/UsersApiDTO.ts +++ b/frontend/src/api/users/UsersApiDTO.ts @@ -1,6 +1,10 @@ export type UserDTO = { uid: string email: string + id?: number + name?: string + organization_id?: number + role_id: number } export type AddUserDTO = { diff --git a/frontend/src/components/Database/DatabaseExperiments.tsx b/frontend/src/components/Database/DatabaseExperiments.tsx index ddb9b14c6..8317858bc 100644 --- a/frontend/src/components/Database/DatabaseExperiments.tsx +++ b/frontend/src/components/Database/DatabaseExperiments.tsx @@ -31,6 +31,8 @@ import { } from '../../store/slice/Database/DatabaseActions' import Loading from 'components/common/Loading' import { TypeData } from 'store/slice/Database/DatabaseSlice' +import { UserDTO } from 'api/users/UsersApiDTO' +import { ROLE } from '@types' type PopupAttributesProps = { data: string @@ -41,7 +43,7 @@ type PopupAttributesProps = { } type DatabaseProps = { - user?: Object + user?: UserDTO cellPath: string } @@ -397,7 +399,7 @@ const DatabaseExperiments = ({ user, cellPath }: DatabaseProps) => { void } -const columns = (handleOpenPopupShare: () => void, handleOpenPopupDel: () => void) => ( +const columns = (handleOpenPopupShare: () => void, handleOpenPopupDel: () => void, user?: UserDTO) => ( [ { field: 'id', @@ -108,7 +111,7 @@ const columns = (handleOpenPopupShare: () => void, handleOpenPopupDel: () => voi headerName: '', minWidth: 90, renderCell: (params: GridRenderCellParams) => ( - params.row.owner !== "User 2" ? + params.row.owner !== "User 2" && user && ([ROLE.ADMIN, ROLE.MANAGER].includes(user.role_id)) ? : "" @@ -288,6 +291,7 @@ const PopupDelete = ({open, handleClose}: PopupType) => { const Workspaces = () => { // const dispatch = useDispatch() // const workspaces = useSelector(selectWorkspaceList) + const user = useSelector(selectCurrentUser) const loading = useSelector(selectIsLoadingWorkspaceList) const [openShare, setOpenShare] = useState(false) const [openDel, setOpenDel] = useState(false) @@ -331,7 +335,7 @@ const Workspaces = () => { params.row.owner === "User 1"} /> {loading ? : null} From c74ee5a9b12924bb8a120070f8a9f1f3e003af9c Mon Sep 17 00:00:00 2001 From: SangLv Date: Fri, 28 Jul 2023 11:14:36 +0700 Subject: [PATCH 02/30] add role to dashboard --- frontend/src/pages/Dashboard/index.tsx | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/frontend/src/pages/Dashboard/index.tsx b/frontend/src/pages/Dashboard/index.tsx index fc421e091..1bc5e01e4 100644 --- a/frontend/src/pages/Dashboard/index.tsx +++ b/frontend/src/pages/Dashboard/index.tsx @@ -3,8 +3,12 @@ import { Box, styled, Typography } from '@mui/material' import StorageIcon from '@mui/icons-material/Storage' import AccountCircleIcon from '@mui/icons-material/AccountCircle' import AnalyticsIcon from '@mui/icons-material/Analytics' +import { useSelector } from 'react-redux' +import { selectCurrentUser } from 'store/slice/User/UserSelector' +import { ROLE } from '@types' const Dashboard = () => { + const user = useSelector(selectCurrentUser) return (

Dashboard

@@ -26,14 +30,17 @@ const Dashboard = () => { - - - - - Account - - - + { + user && user.role_id === ROLE.ADMIN ? + + + + + Account + + + : null + }
From 88906352a9a137d9b207749b57b552985c6d615e Mon Sep 17 00:00:00 2001 From: SangLv Date: Fri, 28 Jul 2023 13:37:21 +0700 Subject: [PATCH 03/30] fix UI workspaces --- frontend/src/pages/Workspace/index.tsx | 27 ++++++++------------------ 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/frontend/src/pages/Workspace/index.tsx b/frontend/src/pages/Workspace/index.tsx index 04dfcef3e..a38bb463b 100644 --- a/frontend/src/pages/Workspace/index.tsx +++ b/frontend/src/pages/Workspace/index.tsx @@ -106,15 +106,16 @@ const columns = (handleOpenPopupShare: () => void, handleOpenPopupDel: () => voi ), }, + user && [ROLE.ADMIN, ROLE.MANAGER].includes(user.role_id) && { field: 'share', headerName: '', minWidth: 90, renderCell: (params: GridRenderCellParams) => ( - params.row.owner !== "User 2" && user && ([ROLE.ADMIN, ROLE.MANAGER].includes(user.role_id)) ? - - - : "" + params.row.owner !== "User 2" && user && ([ROLE.ADMIN, ROLE.MANAGER].includes(user.role_id)) ? + + + : "" ), }, { @@ -141,14 +142,6 @@ const columnsShare = (handleShareFalse: (parmas: GridRenderCellParams) = {params.row.name} ), }, - { - field: "lab", - headerName: "Lab", - minWidth: 280, - renderCell: (params: GridRenderCellParams) => ( - {params.row.email} - ), - }, { field: "email", headerName: "Email", @@ -201,21 +194,18 @@ const dataShare = [ { id: 1, name: "User 1", - lab: "Labxxxx", email: "aaaaa@gmail.com", share: false }, { id: 2, name: "User 2", - lab: "Labxxxx", email: "aaaaa@gmail.com", share: true }, { id: 3, name: "User 3", - lab: "Labxxxx", email: "aaaaa@gmail.com", share: true } @@ -223,7 +213,7 @@ const dataShare = [ const PopupShare = ({open, handleClose}: PopupType) => { const [tableShare, setTableShare] = useState(dataShare) - if(!open) return <> + if(!open) return null const handleShareTrue = (params: GridRowParams) => { if(params.row.share) return const index = tableShare.findIndex(item => item.id === params.id) @@ -269,9 +259,8 @@ const PopupShare = ({open, handleClose}: PopupType) => { } const PopupDelete = ({open, handleClose}: PopupType) => { - if(!open) return <> + if(!open) return null return ( - { params.row.owner === "User 1"} /> {loading ? : null} From fbcee2df8565be0dfa2b1ae4ef18518f637c6c0f Mon Sep 17 00:00:00 2001 From: SangLv Date: Fri, 28 Jul 2023 14:32:06 +0700 Subject: [PATCH 04/30] add role del account --- frontend/src/pages/Account/index.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/Account/index.tsx b/frontend/src/pages/Account/index.tsx index 38f2eb457..6470b4c77 100644 --- a/frontend/src/pages/Account/index.tsx +++ b/frontend/src/pages/Account/index.tsx @@ -8,6 +8,7 @@ import { useNavigate } from "react-router-dom"; import { updateMePasswordApi } from 'api/users/UsersMe' import { deleteMe } from 'store/slice/User/UserActions' import { selectCurrentUser } from 'store/slice/User/UserSelector' +import { ROLE } from '@types' const Account = () => { const user = useSelector(selectCurrentUser) const dispatch = useDispatch() @@ -86,7 +87,10 @@ const Account = () => { Change Password - Delete Account + { + user && ROLE.ADMIN === user.role_id ? + Delete Account : null + } { isLoading && From 85f9a7ba9fa00934db5bd7535345bd621883b63a Mon Sep 17 00:00:00 2001 From: SangLv Date: Fri, 28 Jul 2023 15:27:16 +0700 Subject: [PATCH 05/30] add value to popup share --- frontend/src/@types/index.tsx | 5 ++ .../Database/DatabaseExperiments.tsx | 70 ++++++++++--------- 2 files changed, 43 insertions(+), 32 deletions(-) create mode 100644 frontend/src/@types/index.tsx diff --git a/frontend/src/@types/index.tsx b/frontend/src/@types/index.tsx new file mode 100644 index 000000000..a577b95a5 --- /dev/null +++ b/frontend/src/@types/index.tsx @@ -0,0 +1,5 @@ +export const enum SHARE { + NOSHARE = 0, + ORGANIZATION = 1, + USERS = 2, +} \ No newline at end of file diff --git a/frontend/src/components/Database/DatabaseExperiments.tsx b/frontend/src/components/Database/DatabaseExperiments.tsx index 686480d0c..f0f3acc10 100644 --- a/frontend/src/components/Database/DatabaseExperiments.tsx +++ b/frontend/src/components/Database/DatabaseExperiments.tsx @@ -34,6 +34,7 @@ import { import Loading from 'components/common/Loading' import { TypeData } from 'store/slice/Database/DatabaseSlice' import CancelIcon from '@mui/icons-material/Cancel' +import { SHARE } from '@types' export type Data = { id: number @@ -65,6 +66,10 @@ type PopupAttributesProps = { type PopupType = { open: boolean handleClose: () => void + data: { + expId: string + shareType: number + } } type DatabaseProps = { @@ -186,15 +191,7 @@ const columnsShare = (handleShareFalse: (parmas: GridRenderCellParams) = headerName: "Name", minWidth: 140, renderCell: (params: GridRenderCellParams) => ( - {params.row.name} - ), - }, - { - field: "lab", - headerName: "Lab", - minWidth: 280, - renderCell: (params: GridRenderCellParams) => ( - {params.row.email} + {params.row.name} ), }, { @@ -202,7 +199,7 @@ const columnsShare = (handleShareFalse: (parmas: GridRenderCellParams) = headerName: "Email", minWidth: 280, renderCell: (params: GridRenderCellParams) => ( - {params.row.email} + {params.row.email} ), }, { @@ -210,11 +207,11 @@ const columnsShare = (handleShareFalse: (parmas: GridRenderCellParams) = headerName: "", minWidth: 130, renderCell: (params: GridRenderCellParams) => { - if(!params.row.share) return "" + if(!params.row.share) return null return ( - + ) } }, @@ -226,7 +223,7 @@ const dataShare = [ name: "User 1", lab: "Labxxxx", email: "aaaaa@gmail.com", - share: false + share: true }, { id: 2, @@ -244,8 +241,8 @@ const dataShare = [ } ] -const PopupShare = ({open, handleClose}: PopupType) => { - const [value, setValue] = useState("Organization") +const PopupShare = ({open, handleClose, data}: PopupType) => { + const [value, setValue] = useState(data.shareType) const [tableShare, setTableShare] = useState(dataShare) const handleShareTrue = (params: GridRowParams) => { @@ -267,7 +264,7 @@ const PopupShare = ({open, handleClose}: PopupType) => { } const handleValue = (event: ChangeEvent) => { - setValue((event.target as HTMLInputElement).value); + setValue(Number((event.target as HTMLInputElement).value)); } if(!open) return null; @@ -290,21 +287,21 @@ const PopupShare = ({open, handleClose}: PopupType) => { name="row-radio-buttons-group" onChange={handleValue} > - } label={"Share for Organization"} /> - } label={"Share for Users"} /> + } label={"Share for Organization"} /> + } label={"Share for Users"} /> { - value !== "Organization" ? + value !== SHARE.ORGANIZATION ? <>

Permitted users

: null @@ -358,10 +355,11 @@ const DatabaseExperiments = ({ user, cellPath }: DatabaseProps) => { const [openShare, setOpenShare] = useState(false) const [dataDialog, setDataDialog] = useState<{ - type: string + type?: string data?: string | string[] expId?: string nameCol?: string + shareType?: number }>({ type: '', data: undefined, @@ -442,7 +440,8 @@ const DatabaseExperiments = ({ user, cellPath }: DatabaseProps) => { setDataDialog(pre => ({...pre, data: event.target.value})) } - const handleOpenShare = () => { + const handleOpenShare = (id?: string, value?: number) => { + setDataDialog({expId: id, shareType: value}) setOpenShare(true) } @@ -544,11 +543,17 @@ const DatabaseExperiments = ({ user, cellPath }: DatabaseProps) => { width: 160, sortable: false, filterable: false, - renderCell: (params: { row: DatabaseType }) => ( - - - - ), + renderCell: (params: { value: number, row: DatabaseType }) => { + const { value, row } = params + return ( + handleOpenShare(row.experiment_id, value)} + > + + + ) + } }, { field: 'publish_status', @@ -657,6 +662,7 @@ const DatabaseExperiments = ({ user, cellPath }: DatabaseProps) => { {loading ? : null} setOpenShare(false)} />
From 205522c89470331b7f1c547fab023ca6a9f773ad Mon Sep 17 00:00:00 2001 From: SangLv Date: Fri, 28 Jul 2023 16:38:43 +0700 Subject: [PATCH 06/30] add api get list user share --- frontend/src/api/database/index.ts | 5 +++ .../Database/DatabaseExperiments.tsx | 45 ++++++++++++------- .../store/slice/Database/DatabaseActions.ts | 15 +++++++ .../src/store/slice/Database/DatabaseSlice.ts | 6 ++- .../src/store/slice/Database/DatabaseType.ts | 8 ++++ 5 files changed, 62 insertions(+), 17 deletions(-) diff --git a/frontend/src/api/database/index.ts b/frontend/src/api/database/index.ts index a8ab22f65..3ede04684 100644 --- a/frontend/src/api/database/index.ts +++ b/frontend/src/api/database/index.ts @@ -30,3 +30,8 @@ export const postPublistApi = async (id: number, status: 'on' | 'off') => { const response = await axios.post(`/expdb/experiment/publish/${id}/${status}`) return response.data } + +export const getListUserShareApi = async (id: number) => { + const response = await axios.get(`/expdb/share/${id}/status`) + return response.data +} diff --git a/frontend/src/components/Database/DatabaseExperiments.tsx b/frontend/src/components/Database/DatabaseExperiments.tsx index f0f3acc10..10221c5f0 100644 --- a/frontend/src/components/Database/DatabaseExperiments.tsx +++ b/frontend/src/components/Database/DatabaseExperiments.tsx @@ -29,6 +29,7 @@ import { RootState } from '../../store/store' import { getExperimentsDatabase, getExperimentsPublicDatabase, + getListUserShare, postPublist, } from '../../store/slice/Database/DatabaseActions' import Loading from 'components/common/Loading' @@ -77,6 +78,14 @@ type DatabaseProps = { cellPath: string } +type UsersShare = { + id: number + name: string + email: string + created_at: string + updated_at: string +} + const columns = ( handleOpenAttributes: (value: string) => void, handleOpenDialog: (value: ImageUrls[], exp_id?: string) => void, @@ -207,7 +216,7 @@ const columnsShare = (handleShareFalse: (parmas: GridRenderCellParams) = headerName: "", minWidth: 130, renderCell: (params: GridRenderCellParams) => { - if(!params.row.share) return null + if(!params.row.share) return '' return ( - ) - } - }, -] - -const dataShare = [ - { - id: 1, - name: "User 1", - lab: "Labxxxx", - email: "aaaaa@gmail.com", - share: true - }, - { - id: 2, - name: "User 2", - lab: "Labxxxx", - email: "aaaaa@gmail.com", - share: true - }, - { - id: 3, - name: "User 3", - lab: "Labxxxx", - email: "aaaaa@gmail.com", - share: true - } -] - -const PopupShare = ({open, handleClose, data}: PopupType) => { - const [value, setValue] = useState(data.shareType) - const [tableShare, setTableShare] = useState(dataShare) - - const handleShareTrue = (params: GridRowParams) => { - if(params.row.share) return - const index = tableShare.findIndex(item => item.id === params.id) - setTableShare(pre => { - pre[index].share = true - return pre - }) - } - - const handleShareFalse = (params: GridRenderCellParams) => { - const indexSearch = tableShare.findIndex(item => item.id === params.id) - const newData = tableShare.map((item, index) => { - if(index === indexSearch) return {...item, share: false} - return item - }) - setTableShare(newData) - } - - const handleValue = (event: ChangeEvent) => { - setValue(Number((event.target as HTMLInputElement).value)); - } - - if(!data) return null; - - return ( - - - Share Database record - Experiment ID: {data.expId} - - - - } label={"Share for Organization"} /> - } label={"Share for Users"} /> - - - - - { - (value || data.shareType === SHARE.USERS) && value !== SHARE.ORGANIZATION ? - <> -

Permitted users

- - - : null - } -
- - - - -
-
- ) -} - const PopupAttributes = ({ data, open, @@ -384,7 +235,11 @@ const DatabaseExperiments = ({ user, cellPath }: DatabaseProps) => { }), ) - const dataListShare = useSelector + const { dataShare } = useSelector( + (state: RootState) => ({ + dataShare: state[DATABASE_SLICE_NAME].listShare, + }), + ) const pagiFilter = useCallback( (page?: number) => { @@ -430,8 +285,9 @@ const DatabaseExperiments = ({ user, cellPath }: DatabaseProps) => { }, [dataParams, user, dataParamsFilter]) useEffect(() => { - if(!openShare.id) return + if(!openShare.open || !openShare.id) return dispatch(getListUserShare({id: openShare.id})) + //eslint-disable-next-line }, [openShare]) const handleOpenDialog = (data: ImageUrls[] | ImageUrls, expId?: string, graphTitle?: string) => { @@ -671,11 +527,18 @@ const DatabaseExperiments = ({ user, cellPath }: DatabaseProps) => { role={!!user} /> {loading ? : null} - setOpenShare({...openShare, open: false})} - /> + {openShare.open && openShare.id ? + { + if(isSubmit) fetchApi(); + setOpenShare({...openShare, open: false})} + } + /> : null + } ) } @@ -690,13 +553,4 @@ const Content = styled('textarea')(() => ({ height: 'fit-content', })) -const DialogCustom = styled(Dialog)(({ theme }) => ({ - "& .MuiDialog-container": { - "& .MuiPaper-root": { - width: "70%", - maxWidth: "890px", - }, - }, -})) - export default DatabaseExperiments diff --git a/frontend/src/components/Database/PopupShare.tsx b/frontend/src/components/Database/PopupShare.tsx new file mode 100644 index 000000000..2f2d2f608 --- /dev/null +++ b/frontend/src/components/Database/PopupShare.tsx @@ -0,0 +1,156 @@ +import {Box, Button, + Dialog, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, Radio, RadioGroup, styled } from "@mui/material"; +import {DataGrid, GridRenderCellParams, GridRowParams } from "@mui/x-data-grid"; +import { SHARE } from "@types"; +import {ChangeEvent, useCallback, useEffect, useState} from "react"; +import { useDispatch } from "react-redux"; +import { postListUserShare } from "store/slice/Database/DatabaseActions"; +import CancelIcon from '@mui/icons-material/Cancel' +import { ListShare } from "store/slice/Database/DatabaseType"; + +type PopupType = { + open: boolean + id: number + handleClose: (v: boolean) => void + data: { + expId: string + shareType: number + } + dataListShare?: { + share_type: number + users: ListShare[] + } +} + +const PopupShare = ({open, handleClose, data, dataListShare, id}: PopupType) => { + const [shareType, setShareType] = useState(data.shareType) + const [userList, setUserList] = useState(dataListShare?.users.map(user => user.id) || []) + const dispatch = useDispatch(); + + + useEffect(() => { + if(dataListShare) { + setUserList(dataListShare.users.map(user => user.id)); + } + }, [dataListShare]) + + const handleShareTrue = (params: GridRowParams) => { + if(!params) return + const index = userList.findIndex(item => { + return item === params.id + }) + if(index < 0) { + userList.push(Number(params.id)) + } + } + + const handleShareFalse = (e: any, params: GridRenderCellParams) => { + e.preventDefault() + e.stopPropagation() + if(userList.includes(Number(params.id))) { + setUserList(userList.filter(id => id !== Number(params.id))) + } else setUserList([...userList, Number(params.id)]) + } + + const handleValue = (event: ChangeEvent) => { + setUserList(dataListShare?.users.map(user => user.id) || []) + setShareType(Number((event.target as HTMLInputElement).value)); + } + + const columnsShare = useCallback((handleShareFalse: (e: any, parmas: GridRenderCellParams) => void) => [ + { + field: "name", + headerName: "Name", + minWidth: 140, + renderCell: (params: GridRenderCellParams) => ( + {params.row.name} + ), + }, + { + field: "email", + headerName: "Email", + minWidth: 280, + renderCell: (params: GridRenderCellParams) => ( + {params.row.email} + ), + }, + { + field: "share", + headerName: "", + minWidth: 130, + renderCell: (params: GridRenderCellParams) => { + if(!params.row.share) return '' + return ( + + ) + } + }, + ], [userList]) + + const handleOke = async () => { + await dispatch(postListUserShare({id, data: {user_ids: userList, share_type: shareType }})) + handleClose(true); + } + + if(!data || !dataListShare) return null; + + return ( + + + Share Database record + Experiment ID: {data.expId} + + + + } label={"Share for Organization"} /> + } label={"Share for Users"} /> + + + + + { + shareType === SHARE.USERS ? + <> +

Permitted users

+ ({...user, share: true}))} + columns={columnsShare(handleShareFalse)} + hideFooterPagination + /> + + : null + } +
+ + + + +
+
+ ) +} + +const DialogCustom = styled(Dialog)(({ theme }) => ({ + "& .MuiDialog-container": { + "& .MuiPaper-root": { + width: "70%", + maxWidth: "890px", + }, + }, +})) + +export default PopupShare diff --git a/frontend/src/pages/Workspace/index.tsx b/frontend/src/pages/Workspace/index.tsx index 62c7760b6..7574f8bec 100644 --- a/frontend/src/pages/Workspace/index.tsx +++ b/frontend/src/pages/Workspace/index.tsx @@ -31,7 +31,8 @@ import SystemUpdateAltIcon from '@mui/icons-material/SystemUpdateAlt' import CancelIcon from '@mui/icons-material/Cancel' import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react' import EditIcon from '@mui/icons-material/Edit' -import PeopleOutlineIcon from '@mui/icons-material/PeopleOutline' +import GroupsIcon from '@mui/icons-material/Groups' + import { delWorkspace, exportWorkspace, @@ -118,7 +119,7 @@ const columns = ( ) => ( {params.value?.name} - {params.value.id !== user?.id ? : ''} + {params.value.id !== user?.id ? : ''} ), }, @@ -173,7 +174,7 @@ const columns = ( renderCell: (params: GridRenderCellParams) => params.row?.user?.id === user?.id && ( - + ), }, diff --git a/frontend/src/store/slice/Database/DatabaseActions.ts b/frontend/src/store/slice/Database/DatabaseActions.ts index 86736882f..8878887a5 100644 --- a/frontend/src/store/slice/Database/DatabaseActions.ts +++ b/frontend/src/store/slice/Database/DatabaseActions.ts @@ -3,6 +3,7 @@ import { DATABASE_SLICE_NAME, DatabaseDTO, DatabaseParams, + ListShareDTO, } from './DatabaseType' import { getCellsApi, @@ -10,6 +11,7 @@ import { getExperimentsApi, getExperimentsPublicApi, getListUserShareApi, + postListUserShareApi, postPublistApi, } from 'api/database' @@ -82,7 +84,7 @@ export const postPublist = createAsyncThunk< }) export const getListUserShare = createAsyncThunk< - DatabaseDTO, + ListShareDTO, {id: number} >(`${DATABASE_SLICE_NAME}/getListUserShare`, async (params, thunkAPI) => { const { rejectWithValue } = thunkAPI @@ -94,4 +96,19 @@ export const getListUserShare = createAsyncThunk< } }) +export const postListUserShare = createAsyncThunk< + ListShareDTO, + { + id: number + data: {share_type: number; user_ids: number[]} + } +>(`${DATABASE_SLICE_NAME}/postListUserShare`, async (params, thunkAPI) => { + const { rejectWithValue } = thunkAPI + try { + const response = await postListUserShareApi(params.id, params.data) + return response + } catch (e) { + return rejectWithValue(e) + } +}) diff --git a/frontend/src/store/slice/Database/DatabaseSlice.ts b/frontend/src/store/slice/Database/DatabaseSlice.ts index 349d9145b..e70eec3b4 100644 --- a/frontend/src/store/slice/Database/DatabaseSlice.ts +++ b/frontend/src/store/slice/Database/DatabaseSlice.ts @@ -4,6 +4,8 @@ import { getCellsDatabase, getExperimentsPublicDatabase, getCellsPublicDatabase, + getListUserShare, + postListUserShare, } from './DatabaseActions' import { DATABASE_SLICE_NAME, DatabaseDTO, ListShare } from './DatabaseType' @@ -20,21 +22,24 @@ const initData = { export type TypeData = { public: DatabaseDTO private: DatabaseDTO - listShare: ListShare | {} } export const initialState: { data: TypeData loading: boolean type: 'experiment' | 'cell' + listShare?: { + share_type: number + users: ListShare[] + } } = { data: { public: initData, private: initData, - listShare: {} }, loading: false, type: 'experiment', + listShare: undefined } export const databaseSlice = createSlice({ @@ -71,6 +76,13 @@ export const databaseSlice = createSlice({ } state.loading = true }) + .addCase(getListUserShare.pending, (state, action) => { + state.listShare = undefined + state.loading = true + }) + .addCase(postListUserShare.pending, (state) => { + state.loading = true + }) .addMatcher( isAnyOf( getCellsDatabase.fulfilled, @@ -91,12 +103,24 @@ export const databaseSlice = createSlice({ state.loading = false }, ) + .addMatcher( + isAnyOf( + getListUserShare.fulfilled, + ), + (state, action) => { + state.listShare = action.payload + state.loading = false + }, + ) .addMatcher( isAnyOf( getExperimentsDatabase.rejected, getCellsDatabase.rejected, getExperimentsPublicDatabase.rejected, getCellsPublicDatabase.rejected, + getListUserShare.rejected, + postListUserShare.rejected, + postListUserShare.fulfilled ), (state) => { state.loading = false diff --git a/frontend/src/store/slice/Database/DatabaseType.ts b/frontend/src/store/slice/Database/DatabaseType.ts index 263765391..9e999b82b 100644 --- a/frontend/src/store/slice/Database/DatabaseType.ts +++ b/frontend/src/store/slice/Database/DatabaseType.ts @@ -42,6 +42,11 @@ export type DatabaseDTO = { items: DatabaseType[] } +export type ListShareDTO = { + share_type: number + users: ListShare[] +} + export type ListShare = { id: number, name: string diff --git a/frontend/src/store/slice/Workspace/WorkspaceType.ts b/frontend/src/store/slice/Workspace/WorkspaceType.ts index ceb42ee03..195791851 100644 --- a/frontend/src/store/slice/Workspace/WorkspaceType.ts +++ b/frontend/src/store/slice/Workspace/WorkspaceType.ts @@ -30,4 +30,16 @@ export type Workspace = { loading: boolean } +export type ListShareWorkSpaces = { + id: number, + name: string + email: string + created_at: string + updated_at: string +} + +export type ListShareWorkspacesDTO = { + users: ListShareWorkSpaces[] +} + export type WorkspaceParams = { [key: string]: string | undefined | number | string[] | object } diff --git a/frontend/src/store/slice/Workspace/WorkspacesActions.ts b/frontend/src/store/slice/Workspace/WorkspacesActions.ts index 9e5632c7a..e0f7c8f4f 100644 --- a/frontend/src/store/slice/Workspace/WorkspacesActions.ts +++ b/frontend/src/store/slice/Workspace/WorkspacesActions.ts @@ -7,7 +7,10 @@ import { importWorkspaceApi, postWorkspaceApi, putWorkspaceApi, + getListUserShareWorkspaceApi, + postListUserShareWorkspaceApi, } from 'api/Workspace' +import { ListShareDTO } from '../Database/DatabaseType' import { ItemsWorkspace, WorkspaceDataDTO, @@ -94,3 +97,32 @@ export const exportWorkspace = createAsyncThunk( } }, ) + +export const getListUserShareWorkSpaces = createAsyncThunk< + ListShareDTO, + {id: number} +>(`${WORKSPACE_SLICE_NAME}/getListUserShareWorkspaces`, async (params, thunkAPI) => { + const { rejectWithValue } = thunkAPI + try { + const response = await getListUserShareWorkspaceApi(params.id) + return response + } catch (e) { + return rejectWithValue(e) + } +}) + +export const postListUserShare = createAsyncThunk< + ListShareDTO, + { + id: number + data: {share_type: number; user_ids: number[]} + } +>(`${WORKSPACE_SLICE_NAME}/postListUserShareWorkspaces`, async (params, thunkAPI) => { + const { rejectWithValue } = thunkAPI + try { + const response = await postListUserShareWorkspaceApi(params.id, params.data) + return response + } catch (e) { + return rejectWithValue(e) + } +}) From 58a79cd8456c3767c1153d0629881d18d0a6f6af Mon Sep 17 00:00:00 2001 From: SangLv Date: Fri, 28 Jul 2023 19:58:21 +0700 Subject: [PATCH 08/30] integrate api share workspace --- .../Database/DatabaseExperiments.tsx | 4 +- .../components/{Database => }/PopupShare.tsx | 96 +++++----- frontend/src/pages/Workspace/index.tsx | 174 +++++------------- .../slice/Workspace/WorkspaceSelector.ts | 1 + .../store/slice/Workspace/WorkspaceSlice.ts | 11 ++ .../store/slice/Workspace/WorkspaceType.ts | 7 +- .../slice/Workspace/WorkspacesActions.ts | 6 +- 7 files changed, 114 insertions(+), 185 deletions(-) rename frontend/src/components/{Database => }/PopupShare.tsx (53%) diff --git a/frontend/src/components/Database/DatabaseExperiments.tsx b/frontend/src/components/Database/DatabaseExperiments.tsx index 4aae2eb90..48be73246 100644 --- a/frontend/src/components/Database/DatabaseExperiments.tsx +++ b/frontend/src/components/Database/DatabaseExperiments.tsx @@ -33,7 +33,7 @@ import { import Loading from 'components/common/Loading' import { TypeData } from 'store/slice/Database/DatabaseSlice' import { SHARE } from '@types' -import PopupShare from './PopupShare' +import PopupShare from '../PopupShare' export type Data = { id: number @@ -532,7 +532,7 @@ const DatabaseExperiments = ({ user, cellPath }: DatabaseProps) => { id={openShare.id} open={openShare.open} data={dataDialog as { expId: string; shareType: number; }} - dataListShare={dataShare} + usersShare={dataShare} handleClose={(isSubmit) => { if(isSubmit) fetchApi(); setOpenShare({...openShare, open: false})} diff --git a/frontend/src/components/Database/PopupShare.tsx b/frontend/src/components/PopupShare.tsx similarity index 53% rename from frontend/src/components/Database/PopupShare.tsx rename to frontend/src/components/PopupShare.tsx index 2f2d2f608..27fbcf427 100644 --- a/frontend/src/components/Database/PopupShare.tsx +++ b/frontend/src/components/PopupShare.tsx @@ -1,38 +1,42 @@ import {Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, Radio, RadioGroup, styled } from "@mui/material"; import {DataGrid, GridRenderCellParams, GridRowParams } from "@mui/x-data-grid"; -import { SHARE } from "@types"; +import { SHARE } from "../@types"; import {ChangeEvent, useCallback, useEffect, useState} from "react"; import { useDispatch } from "react-redux"; -import { postListUserShare } from "store/slice/Database/DatabaseActions"; +import { postListUserShare } from "../store/slice/Database/DatabaseActions"; import CancelIcon from '@mui/icons-material/Cancel' -import { ListShare } from "store/slice/Database/DatabaseType"; +import { ListShare } from "../store/slice/Database/DatabaseType"; +import { ListUserShareWorkSpace } from "../store/slice/Workspace/WorkspaceType"; +import { postListUserShareWorkspaces } from "store/slice/Workspace/WorkspacesActions"; type PopupType = { open: boolean id: number handleClose: (v: boolean) => void - data: { + isWorkspace?: boolean + title?: string + data?: { expId: string shareType: number } - dataListShare?: { - share_type: number - users: ListShare[] + usersShare?: { + share_type?: number + users: (ListShare | ListUserShareWorkSpace)[] } } -const PopupShare = ({open, handleClose, data, dataListShare, id}: PopupType) => { - const [shareType, setShareType] = useState(data.shareType) - const [userList, setUserList] = useState(dataListShare?.users.map(user => user.id) || []) +const PopupShare = ({open, handleClose, data, usersShare, id, isWorkspace, title}: PopupType) => { + const [shareType, setShareType] = useState(data?.shareType || 0) + const [userList, setUserList] = useState(usersShare?.users.map(user => user.id) || []) const dispatch = useDispatch(); useEffect(() => { - if(dataListShare) { - setUserList(dataListShare.users.map(user => user.id)); + if(usersShare) { + setUserList(usersShare.users.map(user => user.id)); } - }, [dataListShare]) + }, [usersShare]) const handleShareTrue = (params: GridRowParams) => { if(!params) return @@ -53,7 +57,7 @@ const PopupShare = ({open, handleClose, data, dataListShare, id}: PopupType) => } const handleValue = (event: ChangeEvent) => { - setUserList(dataListShare?.users.map(user => user.id) || []) + setUserList(usersShare?.users.map(user => user.id) || []) setShareType(Number((event.target as HTMLInputElement).value)); } @@ -90,11 +94,15 @@ const PopupShare = ({open, handleClose, data, dataListShare, id}: PopupType) => ], [userList]) const handleOke = async () => { - await dispatch(postListUserShare({id, data: {user_ids: userList, share_type: shareType }})) + if(!isWorkspace) { + await dispatch(postListUserShare({id, data: {user_ids: userList, share_type: shareType }})) + } else { + await dispatch(postListUserShareWorkspaces({id, data: {user_ids: userList}})) + } handleClose(true); } - if(!data || !dataListShare) return null; + if(!data || !usersShare) return null; return ( @@ -103,36 +111,36 @@ const PopupShare = ({open, handleClose, data, dataListShare, id}: PopupType) => onClose={handleClose} sx={{margin: 0}} > - Share Database record - Experiment ID: {data.expId} - - - - } label={"Share for Organization"} /> - } label={"Share for Users"} /> - - - + {title || "Share Database record"} + {isWorkspace ? null : Experiment ID: {data.expId}} + {isWorkspace ? null : ( + + + + } label={"Share for Organization"}/> + } label={"Share for Users"}/> + + + + )} +

Permitted users

{ - shareType === SHARE.USERS ? - <> -

Permitted users

- ({...user, share: true}))} - columns={columnsShare(handleShareFalse)} - hideFooterPagination - /> - - : null + (shareType === SHARE.USERS || isWorkspace) ? + ({...user, share: true}))} + columns={columnsShare(handleShareFalse)} + hideFooterPagination + /> + : null }
diff --git a/frontend/src/pages/Workspace/index.tsx b/frontend/src/pages/Workspace/index.tsx index 7574f8bec..ef1437b7b 100644 --- a/frontend/src/pages/Workspace/index.tsx +++ b/frontend/src/pages/Workspace/index.tsx @@ -11,7 +11,7 @@ import { Pagination, IconButton, } from '@mui/material' -import { GridRenderCellParams, GridRowParams, DataGrid } from '@mui/x-data-grid' +import { GridRenderCellParams } from '@mui/x-data-grid' import { DataGridPro, GridRowEditStopReasons, @@ -26,6 +26,7 @@ import Loading from '../../components/common/Loading' import { selectIsLoadingWorkspaceList, selectWorkspaceData, + selectWorkspaceListUserShare, } from 'store/slice/Workspace/WorkspaceSelector' import SystemUpdateAltIcon from '@mui/icons-material/SystemUpdateAlt' import CancelIcon from '@mui/icons-material/Cancel' @@ -36,26 +37,17 @@ import GroupsIcon from '@mui/icons-material/Groups' import { delWorkspace, exportWorkspace, + getListUserShareWorkSpaces, getWorkspaceList, importWorkspace, postWorkspace, putWorkspace, } from 'store/slice/Workspace/WorkspacesActions' import moment from 'moment' - -type PopupType = { - open: boolean - handleClose: () => void - handleOkDel?: () => void - setNewWorkSpace?: (name: string) => void - value?: string - handleOkNew?: () => void - handleOkSave?: () => void - error?: string -} +import PopupShare from 'components/PopupShare' const columns = ( - handleOpenPopupShare: () => void, + handleOpenPopupShare: (id: number) => void, handleOpenPopupDel: (id: number) => void, handleDownload: (id: number) => void, user?: { id: number }, @@ -173,7 +165,7 @@ const columns = ( sortable: false, // todo enable when api complete renderCell: (params: GridRenderCellParams) => params.row?.user?.id === user?.id && ( - + handleOpenPopupShare(params.row.id)}> ), @@ -193,112 +185,15 @@ const columns = ( }, ] -const columnsShare = ( - handleShareFalse: (parmas: GridRenderCellParams) => void, -) => [ - { - field: 'name', - headerName: 'Name', - minWidth: 140, - renderCell: (params: GridRenderCellParams) => ( - {params.row.name} - ), - }, - { - field: 'lab', - headerName: 'Lab', - minWidth: 280, - renderCell: (params: GridRenderCellParams) => ( - {params.row.email} - ), - }, - { - field: 'email', - headerName: 'Email', - minWidth: 280, - renderCell: (params: GridRenderCellParams) => ( - {params.row.email} - ), - }, - { - field: 'share', - headerName: '', - minWidth: 130, - renderCell: (params: GridRenderCellParams) => { - if (!params.row.share) return '' - return ( - - ) - }, - }, -] - -const dataShare = [ - { - id: 1, - name: 'User 1', - lab: 'Labxxxx', - email: 'aaaaa@gmail.com', - share: false, - }, - { - id: 2, - name: 'User 2', - lab: 'Labxxxx', - email: 'aaaaa@gmail.com', - share: true, - }, - { - id: 3, - name: 'User 3', - lab: 'Labxxxx', - email: 'aaaaa@gmail.com', - share: true, - }, -] - -const PopupShare = ({ open, handleClose }: PopupType) => { - const [tableShare, setTableShare] = useState(dataShare) - const handleShareTrue = (params: GridRowParams) => { - if (params.row.share) return - const index = tableShare.findIndex((item) => item.id === params.id) - setTableShare((pre) => { - pre[index].share = true - return pre - }) - } - - const handleShareFalse = (params: GridRenderCellParams) => { - const indexSearch = tableShare.findIndex((item) => item.id === params.id) - const newData = tableShare.map((item, index) => { - if (index === indexSearch) return { ...item, share: false } - return item - }) - setTableShare(newData) - } - - return ( - - - Share Workspace - Permitted users - - - - - - - - - - ) +type PopupType = { + open: boolean + handleClose: () => void + handleOkDel?: () => void + setNewWorkSpace?: (name: string) => void + value?: string + handleOkNew?: () => void + handleOkSave?: () => void + error?: string } const PopupNew = ({ @@ -355,9 +250,10 @@ const PopupDelete = ({ open, handleClose, handleOkDel }: PopupType) => { const Workspaces = () => { const dispatch = useDispatch() const loading = useSelector(selectIsLoadingWorkspaceList) + const listUserShare = useSelector(selectWorkspaceListUserShare) const data = useSelector(selectWorkspaceData) const user = useSelector(selectCurrentUser) - const [open, setOpen] = useState({ share: false, del: false, new: false }) + const [open, setOpen] = useState({ share: false, del: false, new: false, shareId: 0 }) const [idDel, setIdDel] = useState() const [newWorkspace, setNewWorkSpace] = useState() const [error, setError] = useState('') @@ -380,10 +276,16 @@ const Workspaces = () => { //eslint-disable-next-line }, [dataParams]) - const handleOpenPopupShare = () => { - setOpen({ ...open, share: true }) + const handleOpenPopupShare = (shareId: number) => { + setOpen({ ...open, share: true, shareId}) } + useEffect(() => { + if(!open.share || !open.shareId) return + dispatch(getListUserShareWorkSpaces({id: open.shareId})) + //eslint-disable-next-line + }, [open.share, open.shareId]) + const handleClosePopupShare = () => { setOpen({ ...open, share: false }) } @@ -539,7 +441,22 @@ const Workspaces = () => { page={data.offset + 1} onChange={handlePage} /> - + {open.share ? + { + if(_isSubmit) { + dispatch(getWorkspaceList(dataParams)) + } + handleClosePopupShare() + }} + id={open.shareId} + data={{ expId: '', shareType: 0 }} + /> : null + } ({ }, })) -const DialogCustom = styled(Dialog)(({ theme }) => ({ - '& .MuiDialog-container': { - '& .MuiPaper-root': { - width: '70%', - maxWidth: '890px', - }, - }, -})) - export default Workspaces diff --git a/frontend/src/store/slice/Workspace/WorkspaceSelector.ts b/frontend/src/store/slice/Workspace/WorkspaceSelector.ts index 6f011300c..fd5d9020b 100644 --- a/frontend/src/store/slice/Workspace/WorkspaceSelector.ts +++ b/frontend/src/store/slice/Workspace/WorkspaceSelector.ts @@ -2,6 +2,7 @@ import { RootState } from 'store/store' export const selectWorkspace = (state: RootState) => state.workspace export const selectWorkspaceData = (state: RootState) => state.workspace.workspace +export const selectWorkspaceListUserShare = (state: RootState) => state.workspace.listUserShare export const selectActiveTab = (state: RootState) => state.workspace.currentWorkspace.selectedTab diff --git a/frontend/src/store/slice/Workspace/WorkspaceSlice.ts b/frontend/src/store/slice/Workspace/WorkspaceSlice.ts index 59c7c254d..40b2603d1 100644 --- a/frontend/src/store/slice/Workspace/WorkspaceSlice.ts +++ b/frontend/src/store/slice/Workspace/WorkspaceSlice.ts @@ -3,7 +3,9 @@ import { WORKSPACE_SLICE_NAME, Workspace } from './WorkspaceType' import { importExperimentByUid } from '../Experiments/ExperimentsActions' import { delWorkspace, + getListUserShareWorkSpaces, getWorkspaceList, + postListUserShareWorkspaces, postWorkspace, putWorkspace, } from './WorkspacesActions' @@ -19,6 +21,7 @@ const initialState: Workspace = { offset: 0, }, loading: false, + listUserShare: undefined } export const workspaceSlice = createSlice({ @@ -46,6 +49,10 @@ export const workspaceSlice = createSlice({ state.workspace = action.payload state.loading = false }) + .addCase(getListUserShareWorkSpaces.fulfilled, (state, action) => { + state.listUserShare = action.payload + state.loading = false + }) .addMatcher( isAnyOf( getWorkspaceList.rejected, @@ -55,6 +62,8 @@ export const workspaceSlice = createSlice({ putWorkspace.rejected, delWorkspace.fulfilled, delWorkspace.rejected, + getListUserShareWorkSpaces.rejected, + postListUserShareWorkspaces.rejected ), (state) => { state.loading = false @@ -66,6 +75,8 @@ export const workspaceSlice = createSlice({ postWorkspace.pending, putWorkspace.pending, delWorkspace.pending, + getListUserShareWorkSpaces.pending, + postListUserShareWorkspaces.pending ), (state) => { state.loading = true diff --git a/frontend/src/store/slice/Workspace/WorkspaceType.ts b/frontend/src/store/slice/Workspace/WorkspaceType.ts index 195791851..c57fa1417 100644 --- a/frontend/src/store/slice/Workspace/WorkspaceType.ts +++ b/frontend/src/store/slice/Workspace/WorkspaceType.ts @@ -28,9 +28,10 @@ export type Workspace = { selectedTab: number } loading: boolean + listUserShare?: ListUserShareWorkspaceDTO } -export type ListShareWorkSpaces = { +export type ListUserShareWorkSpace = { id: number, name: string email: string @@ -38,8 +39,8 @@ export type ListShareWorkSpaces = { updated_at: string } -export type ListShareWorkspacesDTO = { - users: ListShareWorkSpaces[] +export type ListUserShareWorkspaceDTO = { + users: ListUserShareWorkSpace[] } export type WorkspaceParams = { [key: string]: string | undefined | number | string[] | object } diff --git a/frontend/src/store/slice/Workspace/WorkspacesActions.ts b/frontend/src/store/slice/Workspace/WorkspacesActions.ts index e0f7c8f4f..25d38aaf5 100644 --- a/frontend/src/store/slice/Workspace/WorkspacesActions.ts +++ b/frontend/src/store/slice/Workspace/WorkspacesActions.ts @@ -101,7 +101,7 @@ export const exportWorkspace = createAsyncThunk( export const getListUserShareWorkSpaces = createAsyncThunk< ListShareDTO, {id: number} ->(`${WORKSPACE_SLICE_NAME}/getListUserShareWorkspaces`, async (params, thunkAPI) => { +>(`${WORKSPACE_SLICE_NAME}/getListUserShareWorkSpaces`, async (params, thunkAPI) => { const { rejectWithValue } = thunkAPI try { const response = await getListUserShareWorkspaceApi(params.id) @@ -111,11 +111,11 @@ export const getListUserShareWorkSpaces = createAsyncThunk< } }) -export const postListUserShare = createAsyncThunk< +export const postListUserShareWorkspaces = createAsyncThunk< ListShareDTO, { id: number - data: {share_type: number; user_ids: number[]} + data: {user_ids: number[]} } >(`${WORKSPACE_SLICE_NAME}/postListUserShareWorkspaces`, async (params, thunkAPI) => { const { rejectWithValue } = thunkAPI From a0e5d4c6465e9dcd3ac66ca949e99dae80f632e0 Mon Sep 17 00:00:00 2001 From: SangLv Date: Fri, 28 Jul 2023 20:02:00 +0700 Subject: [PATCH 09/30] remove import not use --- frontend/src/pages/Workspace/index.tsx | 46 ++++++++++++++------------ 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/frontend/src/pages/Workspace/index.tsx b/frontend/src/pages/Workspace/index.tsx index ef1437b7b..de9e62800 100644 --- a/frontend/src/pages/Workspace/index.tsx +++ b/frontend/src/pages/Workspace/index.tsx @@ -29,7 +29,6 @@ import { selectWorkspaceListUserShare, } from 'store/slice/Workspace/WorkspaceSelector' import SystemUpdateAltIcon from '@mui/icons-material/SystemUpdateAlt' -import CancelIcon from '@mui/icons-material/Cancel' import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react' import EditIcon from '@mui/icons-material/Edit' import GroupsIcon from '@mui/icons-material/Groups' @@ -253,7 +252,12 @@ const Workspaces = () => { const listUserShare = useSelector(selectWorkspaceListUserShare) const data = useSelector(selectWorkspaceData) const user = useSelector(selectCurrentUser) - const [open, setOpen] = useState({ share: false, del: false, new: false, shareId: 0 }) + const [open, setOpen] = useState({ + share: false, + del: false, + new: false, + shareId: 0, + }) const [idDel, setIdDel] = useState() const [newWorkspace, setNewWorkSpace] = useState() const [error, setError] = useState('') @@ -277,12 +281,12 @@ const Workspaces = () => { }, [dataParams]) const handleOpenPopupShare = (shareId: number) => { - setOpen({ ...open, share: true, shareId}) + setOpen({ ...open, share: true, shareId }) } useEffect(() => { - if(!open.share || !open.shareId) return - dispatch(getListUserShareWorkSpaces({id: open.shareId})) + if (!open.share || !open.shareId) return + dispatch(getListUserShareWorkSpaces({ id: open.shareId })) //eslint-disable-next-line }, [open.share, open.shareId]) @@ -441,22 +445,22 @@ const Workspaces = () => { page={data.offset + 1} onChange={handlePage} /> - {open.share ? - { - if(_isSubmit) { - dispatch(getWorkspaceList(dataParams)) - } - handleClosePopupShare() - }} - id={open.shareId} - data={{ expId: '', shareType: 0 }} - /> : null - } + {open.share ? ( + { + if (_isSubmit) { + dispatch(getWorkspaceList(dataParams)) + } + handleClosePopupShare() + }} + id={open.shareId} + data={{ expId: '', shareType: 0 }} + /> + ) : null} Date: Fri, 28 Jul 2023 20:04:01 +0700 Subject: [PATCH 10/30] fix eslint --- frontend/src/@types/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/@types/index.tsx b/frontend/src/@types/index.tsx index a577b95a5..af6f9aaef 100644 --- a/frontend/src/@types/index.tsx +++ b/frontend/src/@types/index.tsx @@ -2,4 +2,4 @@ export const enum SHARE { NOSHARE = 0, ORGANIZATION = 1, USERS = 2, -} \ No newline at end of file +} From c126c7e649c21bc049ace29e03d0c7d214ce7c65 Mon Sep 17 00:00:00 2001 From: SangLv Date: Fri, 28 Jul 2023 20:06:07 +0700 Subject: [PATCH 11/30] Add return type --- frontend/src/api/Workspace/index.ts | 5 ++- frontend/src/api/database/index.ts | 16 ++++---- .../store/slice/Database/DatabaseActions.ts | 39 +++++++++---------- .../slice/Workspace/WorkspacesActions.ts | 2 +- 4 files changed, 31 insertions(+), 31 deletions(-) diff --git a/frontend/src/api/Workspace/index.ts b/frontend/src/api/Workspace/index.ts index 782ebff96..a5315fc71 100644 --- a/frontend/src/api/Workspace/index.ts +++ b/frontend/src/api/Workspace/index.ts @@ -1,6 +1,7 @@ import axios from 'utils/axios' import qs from 'qs' import { ItemsWorkspace, WorkspaceDataDTO } from 'store/slice/Workspace/WorkspaceType' +import { ListShareDTO } from 'store/slice/Database/DatabaseType'; export type WorkspacePostDataDTO = { name: string; id?: number } @@ -41,12 +42,12 @@ export const exportWorkspaceApi = async (id: number): Promise => { return response.data } -export const getListUserShareWorkspaceApi = async (id: number) => { +export const getListUserShareWorkspaceApi = async (id: number): Promise => { const response = await axios.get(`/workspace/share/${id}/status`) return response.data } -export const postListUserShareWorkspaceApi = async (id: number, data: {user_ids: number[]}) => { +export const postListUserShareWorkspaceApi = async (id: number, data: {user_ids: number[]}): Promise => { const response = await axios.post(`/workspace/share/${id}/status`, data) return response.data } diff --git a/frontend/src/api/database/index.ts b/frontend/src/api/database/index.ts index aeb2a57ac..ab567fc8c 100644 --- a/frontend/src/api/database/index.ts +++ b/frontend/src/api/database/index.ts @@ -1,42 +1,42 @@ -import { DatabaseParams } from 'store/slice/Database/DatabaseType' +import { DatabaseDTO, DatabaseParams, ListShareDTO } from 'store/slice/Database/DatabaseType' import axios from 'utils/axios' import qs from 'qs' -export const getExperimentsPublicApi = async (params: DatabaseParams) => { +export const getExperimentsPublicApi = async (params: DatabaseParams): Promise => { const paramsNew = qs.stringify(params, { indices: false }) const response = await axios.get(`/public/experiments?${paramsNew}`) return response.data } -export const getCellsPublicApi = async (params: DatabaseParams) => { +export const getCellsPublicApi = async (params: DatabaseParams): Promise => { const paramsNew = qs.stringify(params, { indices: false }) const response = await axios.get(`/public/cells?${paramsNew}`) return response.data } -export const getExperimentsApi = async (params: DatabaseParams) => { +export const getExperimentsApi = async (params: DatabaseParams): Promise => { const paramsNew = qs.stringify(params, { indices: false }) const response = await axios.get(`/expdb/experiments?${paramsNew}`) return response.data } -export const getCellsApi = async (params: DatabaseParams) => { +export const getCellsApi = async (params: DatabaseParams): Promise => { const paramsNew = qs.stringify(params, { indices: false }) const response = await axios.get(`/expdb/cells?${paramsNew}`) return response.data } -export const postPublistApi = async (id: number, status: 'on' | 'off') => { +export const postPublistApi = async (id: number, status: 'on' | 'off'): Promise => { const response = await axios.post(`/expdb/experiment/publish/${id}/${status}`) return response.data } -export const getListUserShareApi = async (id: number) => { +export const getListUserShareApi = async (id: number): Promise => { const response = await axios.get(`/expdb/share/${id}/status`) return response.data } -export const postListUserShareApi = async (id: number, data: {share_type: number; user_ids: number[]}) => { +export const postListUserShareApi = async (id: number, data: {share_type: number; user_ids: number[]}): Promise => { const response = await axios.post(`/expdb/share/${id}/status`, data) return response.data } diff --git a/frontend/src/store/slice/Database/DatabaseActions.ts b/frontend/src/store/slice/Database/DatabaseActions.ts index 8878887a5..d986a0126 100644 --- a/frontend/src/store/slice/Database/DatabaseActions.ts +++ b/frontend/src/store/slice/Database/DatabaseActions.ts @@ -71,8 +71,8 @@ export const getCellsPublicDatabase = createAsyncThunk< }) export const postPublist = createAsyncThunk< - DatabaseDTO, - {id: number, status: "on" | "off"} + boolean, + { id: number; status: 'on' | 'off' } >(`${DATABASE_SLICE_NAME}/postPublist`, async (params, thunkAPI) => { const { rejectWithValue } = thunkAPI try { @@ -83,25 +83,25 @@ export const postPublist = createAsyncThunk< } }) -export const getListUserShare = createAsyncThunk< - ListShareDTO, - {id: number} ->(`${DATABASE_SLICE_NAME}/getListUserShare`, async (params, thunkAPI) => { - const { rejectWithValue } = thunkAPI - try { - const response = await getListUserShareApi(params.id) - return response - } catch (e) { - return rejectWithValue(e) - } -}) +export const getListUserShare = createAsyncThunk( + `${DATABASE_SLICE_NAME}/getListUserShare`, + async (params, thunkAPI) => { + const { rejectWithValue } = thunkAPI + try { + const response = await getListUserShareApi(params.id) + return response + } catch (e) { + return rejectWithValue(e) + } + }, +) export const postListUserShare = createAsyncThunk< - ListShareDTO, - { - id: number - data: {share_type: number; user_ids: number[]} - } + boolean, + { + id: number + data: { share_type: number; user_ids: number[] } + } >(`${DATABASE_SLICE_NAME}/postListUserShare`, async (params, thunkAPI) => { const { rejectWithValue } = thunkAPI try { @@ -111,4 +111,3 @@ export const postListUserShare = createAsyncThunk< return rejectWithValue(e) } }) - diff --git a/frontend/src/store/slice/Workspace/WorkspacesActions.ts b/frontend/src/store/slice/Workspace/WorkspacesActions.ts index 25d38aaf5..9664db43a 100644 --- a/frontend/src/store/slice/Workspace/WorkspacesActions.ts +++ b/frontend/src/store/slice/Workspace/WorkspacesActions.ts @@ -112,7 +112,7 @@ export const getListUserShareWorkSpaces = createAsyncThunk< }) export const postListUserShareWorkspaces = createAsyncThunk< - ListShareDTO, + boolean, { id: number data: {user_ids: number[]} From 019afad04c26c89816565b189cc410b66d4255d2 Mon Sep 17 00:00:00 2001 From: SangLv Date: Fri, 28 Jul 2023 20:13:13 +0700 Subject: [PATCH 12/30] remove type any --- frontend/src/components/PopupShare.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/PopupShare.tsx b/frontend/src/components/PopupShare.tsx index 27fbcf427..b7beeccb9 100644 --- a/frontend/src/components/PopupShare.tsx +++ b/frontend/src/components/PopupShare.tsx @@ -2,7 +2,7 @@ import {Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, Radio, RadioGroup, styled } from "@mui/material"; import {DataGrid, GridRenderCellParams, GridRowParams } from "@mui/x-data-grid"; import { SHARE } from "../@types"; -import {ChangeEvent, useCallback, useEffect, useState} from "react"; +import {ChangeEvent, MouseEvent, useCallback, useEffect, useState} from "react"; import { useDispatch } from "react-redux"; import { postListUserShare } from "../store/slice/Database/DatabaseActions"; import CancelIcon from '@mui/icons-material/Cancel' @@ -61,7 +61,7 @@ const PopupShare = ({open, handleClose, data, usersShare, id, isWorkspace, title setShareType(Number((event.target as HTMLInputElement).value)); } - const columnsShare = useCallback((handleShareFalse: (e: any, parmas: GridRenderCellParams) => void) => [ + const columnsShare = useCallback((handleShareFalse: (e: MouseEvent, parmas: GridRenderCellParams) => void) => [ { field: "name", headerName: "Name", From 0b78b94ec968117d37863ed4d1c07c744255b427 Mon Sep 17 00:00:00 2001 From: SangLv Date: Mon, 31 Jul 2023 10:53:39 +0700 Subject: [PATCH 13/30] disable sort and filter column share in PopupShar, fix type share --- frontend/src/@types/index.tsx | 4 ++-- frontend/src/components/PopupShare.tsx | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/frontend/src/@types/index.tsx b/frontend/src/@types/index.tsx index af6f9aaef..a08237fb0 100644 --- a/frontend/src/@types/index.tsx +++ b/frontend/src/@types/index.tsx @@ -1,5 +1,5 @@ export const enum SHARE { NOSHARE = 0, - ORGANIZATION = 1, - USERS = 2, + ORGANIZATION = 2, + USERS = 1, } diff --git a/frontend/src/components/PopupShare.tsx b/frontend/src/components/PopupShare.tsx index b7beeccb9..246dace44 100644 --- a/frontend/src/components/PopupShare.tsx +++ b/frontend/src/components/PopupShare.tsx @@ -81,6 +81,8 @@ const PopupShare = ({open, handleClose, data, usersShare, id, isWorkspace, title { field: "share", headerName: "", + filterable: false, + sortable: false, minWidth: 130, renderCell: (params: GridRenderCellParams) => { if(!params.row.share) return '' @@ -123,14 +125,16 @@ const PopupShare = ({open, handleClose, data, usersShare, id, isWorkspace, title name="row-radio-buttons-group" onChange={handleValue} > - } label={"Share for Organization"}/> - } label={"Share for Users"}/> + } label={"Share for Organization"}/> + } label={"Share for Users"}/> )} -

Permitted users

+ { + !isWorkspace && shareType !== SHARE.USERS ? null :

Permitted users

+ } { (shareType === SHARE.USERS || isWorkspace) ? Date: Mon, 31 Jul 2023 11:01:33 +0700 Subject: [PATCH 14/30] rename file --- frontend/src/@types/{index.tsx => index.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename frontend/src/@types/{index.tsx => index.ts} (97%) diff --git a/frontend/src/@types/index.tsx b/frontend/src/@types/index.ts similarity index 97% rename from frontend/src/@types/index.tsx rename to frontend/src/@types/index.ts index a08237fb0..3233248bb 100644 --- a/frontend/src/@types/index.tsx +++ b/frontend/src/@types/index.ts @@ -2,4 +2,4 @@ export const enum SHARE { NOSHARE = 0, ORGANIZATION = 2, USERS = 1, -} +} \ No newline at end of file From 2de73a0126155de331d4f69608e6e0332900bae7 Mon Sep 17 00:00:00 2001 From: SangLv Date: Mon, 31 Jul 2023 11:10:17 +0700 Subject: [PATCH 15/30] refactor code --- frontend/src/components/PopupShare.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/PopupShare.tsx b/frontend/src/components/PopupShare.tsx index 246dace44..6820a5a9a 100644 --- a/frontend/src/components/PopupShare.tsx +++ b/frontend/src/components/PopupShare.tsx @@ -132,18 +132,18 @@ const PopupShare = ({open, handleClose, data, usersShare, id, isWorkspace, title )} - { - !isWorkspace && shareType !== SHARE.USERS ? null :

Permitted users

- } { (shareType === SHARE.USERS || isWorkspace) ? + <> +

Permitted users

({...user, share: true}))} - columns={columnsShare(handleShareFalse)} - hideFooterPagination + sx={{minHeight: 500}} + onRowClick={handleShareTrue} + rows={usersShare.users.map(user => ({...user, share: true}))} + columns={columnsShare(handleShareFalse)} + hideFooterPagination /> + : null }
From 863964b434932a51993ed1cce43c5cefda7efb52 Mon Sep 17 00:00:00 2001 From: SangLv Date: Mon, 31 Jul 2023 17:49:16 +0700 Subject: [PATCH 16/30] fix comment --- frontend/src/pages/Account/index.tsx | 4 +- frontend/src/pages/Dashboard/index.tsx | 5 +- frontend/src/pages/Workspace/index.tsx | 96 +++++++++++++++++--------- frontend/src/utils/checkAdmin.ts | 11 +++ 4 files changed, 80 insertions(+), 36 deletions(-) create mode 100644 frontend/src/utils/checkAdmin.ts diff --git a/frontend/src/pages/Account/index.tsx b/frontend/src/pages/Account/index.tsx index 6470b4c77..63ad349b4 100644 --- a/frontend/src/pages/Account/index.tsx +++ b/frontend/src/pages/Account/index.tsx @@ -9,6 +9,8 @@ import { updateMePasswordApi } from 'api/users/UsersMe' import { deleteMe } from 'store/slice/User/UserActions' import { selectCurrentUser } from 'store/slice/User/UserSelector' import { ROLE } from '@types' +import { UserDTO } from 'api/users/UsersApiDTO' +import { isAdmin } from 'utils/checkAdmin' const Account = () => { const user = useSelector(selectCurrentUser) const dispatch = useDispatch() @@ -88,7 +90,7 @@ const Account = () => { Change Password { - user && ROLE.ADMIN === user.role_id ? + isAdmin(user) ? Delete Account : null } diff --git a/frontend/src/pages/Dashboard/index.tsx b/frontend/src/pages/Dashboard/index.tsx index 1bc5e01e4..8b89d8e7d 100644 --- a/frontend/src/pages/Dashboard/index.tsx +++ b/frontend/src/pages/Dashboard/index.tsx @@ -6,6 +6,7 @@ import AnalyticsIcon from '@mui/icons-material/Analytics' import { useSelector } from 'react-redux' import { selectCurrentUser } from 'store/slice/User/UserSelector' import { ROLE } from '@types' +import { isAdmin } from 'utils/checkAdmin' const Dashboard = () => { const user = useSelector(selectCurrentUser) @@ -31,8 +32,8 @@ const Dashboard = () => { { - user && user.role_id === ROLE.ADMIN ? - + isAdmin(user) ? + diff --git a/frontend/src/pages/Workspace/index.tsx b/frontend/src/pages/Workspace/index.tsx index a38bb463b..33e07fef5 100644 --- a/frontend/src/pages/Workspace/index.tsx +++ b/frontend/src/pages/Workspace/index.tsx @@ -21,13 +21,18 @@ import EditIcon from '@mui/icons-material/Edit'; import { ROLE } from '@types' import { selectCurrentUser } from 'store/slice/User/UserSelector' import { UserDTO } from 'api/users/UsersApiDTO' +import { isAdminOrManager } from 'utils/checkAdmin' type PopupType = { open: boolean handleClose: () => void } -const columns = (handleOpenPopupShare: () => void, handleOpenPopupDel: () => void, user?: UserDTO) => ( +const columns = ( + handleOpenPopupShare: () => void, + handleOpenPopupDel: () => void, + user: UserDTO, +) => ( [ { field: 'id', @@ -51,7 +56,7 @@ const columns = (handleOpenPopupShare: () => void, handleOpenPopupDel: () => voi }} > {params.value} - {params.row.owner !== "User 2" ? : ""} + {params.row.user_id === user.id ? : ""} ), }, @@ -63,8 +68,8 @@ const columns = (handleOpenPopupShare: () => void, handleOpenPopupDel: () => voi - {params.value} - {params.value === "User 2" ? : ""} + {params.row?.user.name} + {params.row.user_id !== user.id ? : ""} ), }, @@ -73,7 +78,7 @@ const columns = (handleOpenPopupShare: () => void, handleOpenPopupDel: () => voi headerName: 'Created', minWidth: 200, renderCell: (params: GridRenderCellParams) => ( - {params.value} + {params.row.created_at} ), }, { @@ -106,16 +111,16 @@ const columns = (handleOpenPopupShare: () => void, handleOpenPopupDel: () => voi
), }, - user && [ROLE.ADMIN, ROLE.MANAGER].includes(user.role_id) && + isAdminOrManager?.(user) && { field: 'share', headerName: '', minWidth: 90, renderCell: (params: GridRenderCellParams) => ( - params.row.owner !== "User 2" && user && ([ROLE.ADMIN, ROLE.MANAGER].includes(user.role_id)) ? - - - : "" + params.row?.user_id !== user.id ? + + + : "" ), }, { @@ -123,11 +128,11 @@ const columns = (handleOpenPopupShare: () => void, handleOpenPopupDel: () => voi headerName: '', minWidth: 130, renderCell: (params: GridRenderCellParams) => ( - params.row.owner !== "User 2" ? + params.row?.user_id === user.id ? Del : "" - ), + ) }, ] ) @@ -168,25 +173,46 @@ const columnsShare = (handleShareFalse: (parmas: GridRenderCellParams) = const data = [ { - id: 1, - owner: "User 1", - name: "Name 1", - created: "YYYY/MM/DD HH:MI", - share: false + id: 0, + name: "User1", + user_id: 5, + user: { + id: 5, + name: "admin", + email: "123@gmail.com", + created_at: "2023-07-31T10:31:18.088Z", + updated_at: "2023-07-31T10:31:18.088Z" + }, + created_at: "2023-07-31T10:31:18.088Z", + updated_at: "2023-07-31T10:31:18.088Z" }, { - id: 2, - owner: "User 2", - name: "Name 2", - created: "YYYY/MM/DD HH:MI", - share: true + id: 1, + name: "User2", + user_id: 2, + user: { + id: 2, + name: "manager", + email: "asfdsf@gmail.com", + created_at: "2023-07-31T10:31:18.088Z", + updated_at: "2023-07-31T10:31:18.088Z" + }, + created_at: "2023-07-31T10:31:18.088Z", + updated_at: "2023-07-31T10:31:18.088Z" }, { - id: 3, - owner: "User 1", - name: "Name 3", - created: "YYYY/MM/DD HH:MI", - share: true + id: 2, + name: "User1", + user_id: 5, + user: { + id: 5, + name: "admin", + email: "123@gmail.com", + created_at: "2023-07-31T10:31:18.088Z", + updated_at: "2023-07-31T10:31:18.088Z" + }, + created_at: "2023-07-31T10:31:18.088Z", + updated_at: "2023-07-31T10:31:18.088Z" } ] @@ -291,6 +317,7 @@ const Workspaces = () => { //eslint-disable-next-line }, []) */ + const handleOpenPopupShare = () => { setOpenShare(false) } @@ -321,12 +348,15 @@ const Workspaces = () => { Import New
- params.row.owner === "User 1"} - /> + { + user ? + params.row?.user_id === user.id} + /> : null + } {loading ? : null} diff --git a/frontend/src/utils/checkAdmin.ts b/frontend/src/utils/checkAdmin.ts new file mode 100644 index 000000000..cb1ab972a --- /dev/null +++ b/frontend/src/utils/checkAdmin.ts @@ -0,0 +1,11 @@ +import { ROLE } from "@types" +import { UserDTO } from "api/users/UsersApiDTO" + +export const isAdmin = (user: UserDTO | undefined) => { + return user && ROLE.ADMIN === user.role_id +} + +export const isAdminOrManager = (user?: UserDTO) => { + if(!user || ![ROLE.ADMIN, ROLE.MANAGER].includes(user.role_id)) return false + return true +} \ No newline at end of file From 0894a44e218fa4df122c478fba1801c6faa0d398 Mon Sep 17 00:00:00 2001 From: SangLv Date: Tue, 1 Aug 2023 10:51:57 +0700 Subject: [PATCH 17/30] add checkRole --- frontend/src/api/users/UsersApiDTO.ts | 1 - frontend/src/pages/Account/index.tsx | 4 +- frontend/src/pages/Dashboard/index.tsx | 3 +- frontend/src/pages/Workspace/index.tsx | 101 ++++++++---------- .../src/utils/{checkAdmin.ts => checkRole.ts} | 6 +- 5 files changed, 52 insertions(+), 63 deletions(-) rename frontend/src/utils/{checkAdmin.ts => checkRole.ts} (69%) diff --git a/frontend/src/api/users/UsersApiDTO.ts b/frontend/src/api/users/UsersApiDTO.ts index 588f49bc5..eb7294a79 100644 --- a/frontend/src/api/users/UsersApiDTO.ts +++ b/frontend/src/api/users/UsersApiDTO.ts @@ -1,5 +1,4 @@ export type UserDTO = { - id: number uid: string email: string id?: number diff --git a/frontend/src/pages/Account/index.tsx b/frontend/src/pages/Account/index.tsx index 63ad349b4..e419c7313 100644 --- a/frontend/src/pages/Account/index.tsx +++ b/frontend/src/pages/Account/index.tsx @@ -8,9 +8,7 @@ import { useNavigate } from "react-router-dom"; import { updateMePasswordApi } from 'api/users/UsersMe' import { deleteMe } from 'store/slice/User/UserActions' import { selectCurrentUser } from 'store/slice/User/UserSelector' -import { ROLE } from '@types' -import { UserDTO } from 'api/users/UsersApiDTO' -import { isAdmin } from 'utils/checkAdmin' +import { isAdmin } from 'utils/checkRole' const Account = () => { const user = useSelector(selectCurrentUser) const dispatch = useDispatch() diff --git a/frontend/src/pages/Dashboard/index.tsx b/frontend/src/pages/Dashboard/index.tsx index 8b89d8e7d..bfd73ff69 100644 --- a/frontend/src/pages/Dashboard/index.tsx +++ b/frontend/src/pages/Dashboard/index.tsx @@ -5,8 +5,7 @@ import AccountCircleIcon from '@mui/icons-material/AccountCircle' import AnalyticsIcon from '@mui/icons-material/Analytics' import { useSelector } from 'react-redux' import { selectCurrentUser } from 'store/slice/User/UserSelector' -import { ROLE } from '@types' -import { isAdmin } from 'utils/checkAdmin' +import { isAdmin } from 'utils/checkRole' const Dashboard = () => { const user = useSelector(selectCurrentUser) diff --git a/frontend/src/pages/Workspace/index.tsx b/frontend/src/pages/Workspace/index.tsx index 090897b8f..e465ee01a 100644 --- a/frontend/src/pages/Workspace/index.tsx +++ b/frontend/src/pages/Workspace/index.tsx @@ -25,7 +25,6 @@ import { selectWorkspaceData, } from 'store/slice/Workspace/WorkspaceSelector' import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react' -import PeopleOutlineIcon from '@mui/icons-material/PeopleOutline' import { delWorkspace, exportWorkspace, @@ -39,10 +38,9 @@ import SystemUpdateAltIcon from '@mui/icons-material/SystemUpdateAlt'; import CancelIcon from '@mui/icons-material/Cancel'; import GroupsIcon from '@mui/icons-material/Groups'; import EditIcon from '@mui/icons-material/Edit'; -import { ROLE } from '@types' import { selectCurrentUser } from 'store/slice/User/UserSelector' import { UserDTO } from 'api/users/UsersApiDTO' -import { isAdminOrManager } from 'utils/checkAdmin' +import { isCheckMe } from 'utils/checkRole' type PopupType = { open: boolean @@ -100,11 +98,11 @@ const columns = ( > {value} - {row.user?.id === user?.id && ( + {isCheckMe(user, row?.user?.id) ? ( onEdit?.(row.id)}> - )} + ) : null} ) }, @@ -120,7 +118,7 @@ const columns = ( ) => ( {params.value?.name} - {params.value.id !== user?.id ? : ''} + {params.value.id !== user?.id ? : ''} ), }, @@ -154,7 +152,6 @@ const columns = ( Result ), }, - isAdminOrManager?.(user) && { field: 'download', headerName: '', @@ -174,11 +171,11 @@ const columns = ( filterable: false, // todo enable when api complete sortable: false, // todo enable when api complete renderCell: (params: GridRenderCellParams) => - params.row?.user?.id === user?.id && ( + isCheckMe(user, params.row?.user?.id) ? ( - + - ), + ): null }, { field: 'delete', @@ -187,11 +184,11 @@ const columns = ( filterable: false, // todo enable when api complete sortable: false, // todo enable when api complete renderCell: (params: GridRenderCellParams) => - params.row?.user_id === user?.id && ( - handleOpenPopupDel(params.row.id)}> - Del - - ), + isCheckMe(user, params.row?.user?.id) ? ( + handleOpenPopupDel(params.row.id)}> + Del + + ) : null }, ] @@ -206,14 +203,6 @@ const columnsShare = ( {params.row.name} ), }, - { - field: 'lab', - headerName: 'Lab', - minWidth: 280, - renderCell: (params: GridRenderCellParams) => ( - {params.row.email} - ), - }, { field: 'email', headerName: 'Email', @@ -519,39 +508,39 @@ const Workspaces = () => { { user ? - - params.row.user?.id === user?.id} - onProcessRowUpdateError={onProcessRowUpdateError} - onRowEditStop={onRowEditStop} - processRowUpdate={processRowUpdate as any} - hideFooter={true} - /> - : null + + params.row.user?.id === user?.id} + onProcessRowUpdateError={onProcessRowUpdateError} + onRowEditStop={onRowEditStop} + processRowUpdate={processRowUpdate as any} + hideFooter={true} + /> + : null } { export const isAdminOrManager = (user?: UserDTO) => { if(!user || ![ROLE.ADMIN, ROLE.MANAGER].includes(user.role_id)) return false return true -} \ No newline at end of file +} + +export const isCheckMe = (user?: UserDTO, idUserWorkSpace?: number) => { + return user && idUserWorkSpace && user.id === idUserWorkSpace +} From c6dfb243992da1dd94e9271ed2808b369d95ffb5 Mon Sep 17 00:00:00 2001 From: SangLv Date: Tue, 1 Aug 2023 11:33:06 +0700 Subject: [PATCH 18/30] defind and use selector --- .../src/components/Database/DatabaseExperiments.tsx | 5 +++-- frontend/src/pages/Account/index.tsx | 6 +++--- frontend/src/pages/Dashboard/index.tsx | 8 +++----- frontend/src/pages/Login/index.tsx | 5 +++-- frontend/src/store/slice/User/UserSelector.ts | 9 +++++++++ frontend/src/utils/checkRole.ts | 10 ---------- 6 files changed, 21 insertions(+), 22 deletions(-) diff --git a/frontend/src/components/Database/DatabaseExperiments.tsx b/frontend/src/components/Database/DatabaseExperiments.tsx index 65faf8206..552394a45 100644 --- a/frontend/src/components/Database/DatabaseExperiments.tsx +++ b/frontend/src/components/Database/DatabaseExperiments.tsx @@ -31,7 +31,7 @@ import { import Loading from 'components/common/Loading' import { TypeData } from 'store/slice/Database/DatabaseSlice' import { UserDTO } from 'api/users/UsersApiDTO' -import { ROLE } from '@types' +import { isAdminOrManager } from 'store/slice/User/UserSelector' type PopupAttributesProps = { data: string @@ -190,6 +190,7 @@ const PopupAttributes = ({ const DatabaseExperiments = ({ user, cellPath }: DatabaseProps) => { const type: keyof TypeData = user ? 'private' : 'public' + const adminOrManager = useSelector(isAdminOrManager) const { data: dataExperiments, loading } = useSelector( (state: RootState) => ({ data: state[DATABASE_SLICE_NAME].data[type], @@ -395,7 +396,7 @@ const DatabaseExperiments = ({ user, cellPath }: DatabaseProps) => { { const user = useSelector(selectCurrentUser) + const admin = useSelector(isAdmin) const dispatch = useDispatch() const navigate = useNavigate() const [isDeleteConfirmModalOpen, setIsDeleteConfirmModalOpen] = useState(false) @@ -88,7 +88,7 @@ const Account = () => { Change Password { - isAdmin(user) ? + admin ? Delete Account : null } diff --git a/frontend/src/pages/Dashboard/index.tsx b/frontend/src/pages/Dashboard/index.tsx index bfd73ff69..64f493172 100644 --- a/frontend/src/pages/Dashboard/index.tsx +++ b/frontend/src/pages/Dashboard/index.tsx @@ -4,11 +4,9 @@ import StorageIcon from '@mui/icons-material/Storage' import AccountCircleIcon from '@mui/icons-material/AccountCircle' import AnalyticsIcon from '@mui/icons-material/Analytics' import { useSelector } from 'react-redux' -import { selectCurrentUser } from 'store/slice/User/UserSelector' -import { isAdmin } from 'utils/checkRole' - +import { isAdmin } from 'store/slice/User/UserSelector' const Dashboard = () => { - const user = useSelector(selectCurrentUser) + const admin = useSelector(isAdmin) return (

Dashboard

@@ -31,7 +29,7 @@ const Dashboard = () => { { - isAdmin(user) ? + admin ? diff --git a/frontend/src/pages/Login/index.tsx b/frontend/src/pages/Login/index.tsx index 9f87c6416..59a4bb1cc 100644 --- a/frontend/src/pages/Login/index.tsx +++ b/frontend/src/pages/Login/index.tsx @@ -1,6 +1,6 @@ import { Box, Stack, styled, Typography } from '@mui/material' import { useDispatch } from 'react-redux' -import { login } from 'store/slice/User/UserActions' +import {getMe, login} from 'store/slice/User/UserActions' import { AppDispatch } from 'store/store' import { ChangeEvent, FormEvent, useState } from 'react' import { Link, useNavigate } from 'react-router-dom' @@ -27,7 +27,8 @@ const Login = () => { setIsLoading(true) dispatch(login(values)) .unwrap() - .then((_) => { + .then(async (_) => { + await dispatch(getMe()) navigate('/console') }) .catch((_) => { diff --git a/frontend/src/store/slice/User/UserSelector.ts b/frontend/src/store/slice/User/UserSelector.ts index fa17bf27e..15ee84ccb 100644 --- a/frontend/src/store/slice/User/UserSelector.ts +++ b/frontend/src/store/slice/User/UserSelector.ts @@ -1,3 +1,4 @@ +import { ROLE } from '@types' import { RootState } from 'store/store' export const selectCurrentUser = (state: RootState) => state.user.currentUser @@ -5,3 +6,11 @@ export const selectCurrentUserUid = (state: RootState) => selectCurrentUser(state)?.uid export const selectCurrentUserEmail = (state: RootState) => selectCurrentUser(state)?.email + +export const isAdmin = (state: RootState) => { + return state.user && ROLE.ADMIN === state.user.currentUser?.role_id +} + +export const isAdminOrManager = (state: RootState) => { + return [ROLE.ADMIN, ROLE.MANAGER].includes(state.user.currentUser?.role_id as number) +} diff --git a/frontend/src/utils/checkRole.ts b/frontend/src/utils/checkRole.ts index 6c95c068a..29d8a6d06 100644 --- a/frontend/src/utils/checkRole.ts +++ b/frontend/src/utils/checkRole.ts @@ -1,15 +1,5 @@ -import { ROLE } from "@types" import { UserDTO } from "api/users/UsersApiDTO" -export const isAdmin = (user: UserDTO | undefined) => { - return user && ROLE.ADMIN === user.role_id -} - -export const isAdminOrManager = (user?: UserDTO) => { - if(!user || ![ROLE.ADMIN, ROLE.MANAGER].includes(user.role_id)) return false - return true -} - export const isCheckMe = (user?: UserDTO, idUserWorkSpace?: number) => { return user && idUserWorkSpace && user.id === idUserWorkSpace } From c4d4795914daead3a5f060581dc53e643483e2b8 Mon Sep 17 00:00:00 2001 From: SangLv Date: Tue, 1 Aug 2023 11:48:12 +0700 Subject: [PATCH 19/30] use isMe --- frontend/src/pages/Workspace/index.tsx | 12 ++++++------ frontend/src/utils/checkRole.ts | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/src/pages/Workspace/index.tsx b/frontend/src/pages/Workspace/index.tsx index e465ee01a..819e2b194 100644 --- a/frontend/src/pages/Workspace/index.tsx +++ b/frontend/src/pages/Workspace/index.tsx @@ -40,7 +40,7 @@ import GroupsIcon from '@mui/icons-material/Groups'; import EditIcon from '@mui/icons-material/Edit'; import { selectCurrentUser } from 'store/slice/User/UserSelector' import { UserDTO } from 'api/users/UsersApiDTO' -import { isCheckMe } from 'utils/checkRole' +import { isMe } from 'utils/checkRole' type PopupType = { open: boolean @@ -98,7 +98,7 @@ const columns = ( > {value} - {isCheckMe(user, row?.user?.id) ? ( + {isMe(user, row?.user?.id) ? ( onEdit?.(row.id)}> @@ -118,7 +118,7 @@ const columns = ( ) => ( {params.value?.name} - {params.value.id !== user?.id ? : ''} + {!isMe(user, params.value.id) ? : ''} ), }, @@ -171,7 +171,7 @@ const columns = ( filterable: false, // todo enable when api complete sortable: false, // todo enable when api complete renderCell: (params: GridRenderCellParams) => - isCheckMe(user, params.row?.user?.id) ? ( + isMe(user, params.row?.user?.id) ? ( @@ -184,7 +184,7 @@ const columns = ( filterable: false, // todo enable when api complete sortable: false, // todo enable when api complete renderCell: (params: GridRenderCellParams) => - isCheckMe(user, params.row?.user?.id) ? ( + isMe(user, params.row?.user?.id) ? ( handleOpenPopupDel(params.row.id)}> Del @@ -534,7 +534,7 @@ const Workspaces = () => { ).filter(Boolean) as any } onRowModesModelChange={handleRowModesModelChange} - isCellEditable={(params) => params.row.user?.id === user?.id} + isCellEditable={(params) => isMe(user, params.row.user?.id)} onProcessRowUpdateError={onProcessRowUpdateError} onRowEditStop={onRowEditStop} processRowUpdate={processRowUpdate as any} diff --git a/frontend/src/utils/checkRole.ts b/frontend/src/utils/checkRole.ts index 29d8a6d06..a98b4c85d 100644 --- a/frontend/src/utils/checkRole.ts +++ b/frontend/src/utils/checkRole.ts @@ -1,5 +1,5 @@ import { UserDTO } from "api/users/UsersApiDTO" -export const isCheckMe = (user?: UserDTO, idUserWorkSpace?: number) => { - return user && idUserWorkSpace && user.id === idUserWorkSpace +export const isMe = (user?: UserDTO, idUserWorkSpace?: number) => { + return !!(user && idUserWorkSpace && user.id === idUserWorkSpace) } From 7b568772806cdce54d1a67d278f1954a8d6c96b2 Mon Sep 17 00:00:00 2001 From: quanpython Date: Tue, 1 Aug 2023 16:56:07 +0700 Subject: [PATCH 20/30] create foreignkey for user_id workspaces --- ...b1594a40_create_foreign_key_for_user_id.py | 32 +++++++++++++++++++ studio/app/common/models/user.py | 16 ++++++++-- studio/app/common/models/workspace.py | 20 ++++++++++-- studio/app/common/routers/workspace.py | 12 ++----- studio/app/common/schemas/workspace.py | 1 - 5 files changed, 67 insertions(+), 14 deletions(-) create mode 100644 studio/alembic/versions/965cb1594a40_create_foreign_key_for_user_id.py diff --git a/studio/alembic/versions/965cb1594a40_create_foreign_key_for_user_id.py b/studio/alembic/versions/965cb1594a40_create_foreign_key_for_user_id.py new file mode 100644 index 000000000..e589015c5 --- /dev/null +++ b/studio/alembic/versions/965cb1594a40_create_foreign_key_for_user_id.py @@ -0,0 +1,32 @@ +"""create foreignkey for user_id + +Revision ID: 965cb1594a40 +Revises: 73d4d7abcd35 +Create Date: 2023-08-01 18:27:33.077883 + +""" +from alembic import op + +# revision identifiers, used by Alembic. +revision = "965cb1594a40" +down_revision = "73d4d7abcd35" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.execute( + "ALTER TABLE workspaces " "MODIFY COLUMN user_id BIGINT UNSIGNED NOT NULL;" + ) + op.drop_index("ix_workspaces_user_id", table_name="workspaces") + op.create_foreign_key("user", "workspaces", "users", ["user_id"], ["id"]) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint("user", "workspaces", type_="foreignkey") + op.create_index("ix_workspaces_user_id", "workspaces", ["user_id"], unique=False) + op.execute("ALTER TABLE workspaces " "MODIFY COLUMN user_id INTEGER NOT NULL;") + # ### end Alembic commands ### diff --git a/studio/app/common/models/user.py b/studio/app/common/models/user.py index 63c71c54b..6a96c72ef 100644 --- a/studio/app/common/models/user.py +++ b/studio/app/common/models/user.py @@ -1,8 +1,16 @@ from datetime import datetime -from typing import Dict, Optional +from typing import Dict, List, Optional from sqlalchemy.sql.functions import current_timestamp -from sqlmodel import JSON, Column, Field, Integer, String, UniqueConstraint +from sqlmodel import ( + JSON, + Column, + Field, + Integer, + Relationship, + String, + UniqueConstraint, +) from studio.app.common.models.base import Base, TimestampMixin @@ -18,6 +26,10 @@ class User(Base, TimestampMixin, table=True): attributes: Optional[Dict] = Field(default={}, sa_column=Column(JSON)) active: bool = Field(nullable=False) + workspace: List["Workspace"] = Relationship( # noqa: F821 + back_populates="user", sa_relationship_kwargs={"uselist": True} + ) + class Organization(Base, table=True): __tablename__ = "organization" diff --git a/studio/app/common/models/workspace.py b/studio/app/common/models/workspace.py index 39781eb0f..358bb999f 100644 --- a/studio/app/common/models/workspace.py +++ b/studio/app/common/models/workspace.py @@ -1,8 +1,17 @@ from datetime import datetime from typing import Optional +from sqlalchemy.dialects.mysql import BIGINT from sqlalchemy.sql.functions import current_timestamp -from sqlmodel import Column, Field, Integer, String, UniqueConstraint +from sqlmodel import ( + Column, + Field, + ForeignKey, + Integer, + Relationship, + String, + UniqueConstraint, +) from studio.app.common.models.base import Base, TimestampMixin @@ -11,8 +20,15 @@ class Workspace(Base, TimestampMixin, table=True): __tablename__ = "workspaces" name: str = Field(sa_column=Column(String(100), nullable=False)) - user_id: int = Field(nullable=False, index=True) + user_id: int = Field( + sa_column=Column( + BIGINT(unsigned=True), ForeignKey("users.id", name="user"), nullable=False + ), + ) deleted: bool = Field(nullable=False) + user: Optional["User"] = Relationship( # noqa: F821 + back_populates="workspace", + ) class WorkspacesShareUser(Base, table=True): diff --git a/studio/app/common/routers/workspace.py b/studio/app/common/routers/workspace.py index d96783ceb..7d3279e4f 100644 --- a/studio/app/common/routers/workspace.py +++ b/studio/app/common/routers/workspace.py @@ -15,7 +15,7 @@ WorkspacesSetting, WorkspaceUpdate, ) -from studio.app.optinist.schemas.base import SortDirection, SortOptions +from studio.app.optinist.schemas.base import SortOptions router = APIRouter(tags=["Workspace"]) @@ -32,7 +32,7 @@ def search_workspaces( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): - sort_column = getattr(common_model.Workspace, sortOptions.sort[0] or "id") + sa_sort_list = sortOptions.get_sa_sort_list(sa_table=common_model.Workspace) query = ( select(common_model.Workspace) .outerjoin( @@ -47,15 +47,9 @@ def search_workspaces( ), ) .group_by(common_model.Workspace.id) - .order_by( - sort_column.desc() - if sortOptions.sort[1] == SortDirection.desc - else sort_column.asc() - ) + .order_by(*sa_sort_list) ) data = paginate(db, query) - for ws in data.items: - ws.__dict__["user"] = db.query(common_model.User).get(ws.user_id) return data diff --git a/studio/app/common/schemas/workspace.py b/studio/app/common/schemas/workspace.py index 574cb5ba7..a3df2daba 100644 --- a/studio/app/common/schemas/workspace.py +++ b/studio/app/common/schemas/workspace.py @@ -9,7 +9,6 @@ class Workspace(BaseModel): id: Optional[int] name: str - user_id: Optional[int] user: Optional[UserInfo] created_at: Optional[datetime] updated_at: Optional[datetime] From f544085585520a14b3b37ebc3a286ed4a8c55141 Mon Sep 17 00:00:00 2001 From: quanpython Date: Tue, 1 Aug 2023 17:00:35 +0700 Subject: [PATCH 21/30] fix permission for database share --- studio/app/common/core/auth/auth_dependencies.py | 10 ++++++++++ studio/app/optinist/routers/expdb.py | 8 ++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/studio/app/common/core/auth/auth_dependencies.py b/studio/app/common/core/auth/auth_dependencies.py index 624af9cfa..a4c2b00c4 100644 --- a/studio/app/common/core/auth/auth_dependencies.py +++ b/studio/app/common/core/auth/auth_dependencies.py @@ -62,3 +62,13 @@ async def get_admin_user(current_user: User = Depends(get_current_user)): status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient privileges", ) + + +async def get_admin_data_user(current_user: User = Depends(get_current_user)): + if current_user.is_admin_data: + return current_user + else: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Insufficient privileges", + ) diff --git a/studio/app/optinist/routers/expdb.py b/studio/app/optinist/routers/expdb.py index f7336822a..f475924fb 100644 --- a/studio/app/optinist/routers/expdb.py +++ b/studio/app/optinist/routers/expdb.py @@ -6,7 +6,7 @@ from studio.app.common import models as common_model from studio.app.common.core.auth.auth_dependencies import ( - get_admin_user, + get_admin_data_user, get_current_user, ) from studio.app.common.db.database import get_db @@ -345,7 +345,7 @@ async def publish_db_experiment( id: int, flag: PublishFlags, db: Session = Depends(get_db), - current_admin_user: User = Depends(get_admin_user), + current_admin_user: User = Depends(get_admin_data_user), ): exp = ( db.query(optinist_model.Experiment) @@ -382,7 +382,7 @@ async def publish_db_experiment( def get_experiment_database_share_status( id: int, db: Session = Depends(get_db), - current_admin_user: User = Depends(get_admin_user), + current_admin_user: User = Depends(get_admin_data_user), ): exp = ( db.query(optinist_model.Experiment) @@ -428,7 +428,7 @@ def update_experiment_database_share_status( id: int, data: ExpDbExperimentSharePostStatus, db: Session = Depends(get_db), - current_admin_user: User = Depends(get_admin_user), + current_admin_user: User = Depends(get_admin_data_user), ): exp = ( db.query(optinist_model.Experiment) From 709b05c643c09c25fb7c4a02d4ca67a1a7a9dd55 Mon Sep 17 00:00:00 2001 From: quanpython Date: Tue, 1 Aug 2023 17:17:50 +0700 Subject: [PATCH 22/30] fix get expdb share --- studio/app/optinist/routers/expdb.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/studio/app/optinist/routers/expdb.py b/studio/app/optinist/routers/expdb.py index f475924fb..32072e07b 100644 --- a/studio/app/optinist/routers/expdb.py +++ b/studio/app/optinist/routers/expdb.py @@ -409,6 +409,9 @@ def get_experiment_database_share_status( db.query(common_model.User) .join( optinist_model.ExperimentShareUser, + optinist_model.ExperimentShareUser.user_id == common_model.User.id, + ) + .filter( optinist_model.ExperimentShareUser.experiment_uid == id, ) .all() From a06ea94a97127a2ce673726f89f53d1ffef30fdf Mon Sep 17 00:00:00 2001 From: quanpython Date: Mon, 31 Jul 2023 23:54:26 +0700 Subject: [PATCH 23/30] refactor list_user query --- studio/app/common/core/users/crud_users.py | 26 +++++++++------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/studio/app/common/core/users/crud_users.py b/studio/app/common/core/users/crud_users.py index 6d7545494..8ff15b812 100644 --- a/studio/app/common/core/users/crud_users.py +++ b/studio/app/common/core/users/crud_users.py @@ -54,18 +54,17 @@ async def get_user(db: Session, user_id: int, organization_id: int) -> User: async def list_user(db: Session, organization_id: int): try: - users = paginate( - db, - query=select(UserModel).filter( - UserModel.active.is_(True), - UserModel.organization_id == organization_id, - ), - ) - for user in users.items: - role = ( - db.query(UserRoleModel).filter(UserRoleModel.user_id == user.id).first() + query = ( + select( + UserRoleModel.role_id, + *UserModel.__table__.columns, ) - user.__dict__["role_id"] = role.role_id if role else None + .join(UserRoleModel, UserRoleModel.user_id == UserModel.id) + .filter( + UserModel.active.is_(True), UserModel.organization_id == organization_id + ) + ) + users = paginate(db, query=query, unique=False) return users except Exception as e: raise HTTPException(status_code=400, detail=str(e)) @@ -114,10 +113,7 @@ async def update_user( setattr(user_db, key, value) if role_id is not None: await set_role(db, user_id=user_db.id, role_id=role_id, auto_commit=False) - role = ( - db.query(UserRoleModel).filter(UserRoleModel.user_id == user_db.id).first() - ) - user_db.__dict__["role_id"] = role.role_id if role else None + user_db.__dict__["role_id"] = role_id firebase_auth.update_user(user_db.uid, email=data.email) db.commit() return User.from_orm(user_db) From 0075df32ec53a8d21035c052f4f241cf996e5f6a Mon Sep 17 00:00:00 2001 From: SangLv Date: Tue, 1 Aug 2023 19:11:49 +0700 Subject: [PATCH 24/30] integrate API search user share --- frontend/src/api/users/UsersAdmin.ts | 5 + frontend/src/api/users/UsersApiDTO.ts | 5 +- .../Database/DatabaseExperiments.tsx | 2 +- frontend/src/components/PopupShare.tsx | 269 ++++++++++++++---- frontend/src/store/slice/User/UserActions.ts | 18 +- frontend/src/store/slice/User/UserSelector.ts | 2 + frontend/src/store/slice/User/UserSlice.ts | 23 +- frontend/src/store/slice/User/UserType.ts | 2 + 8 files changed, 257 insertions(+), 69 deletions(-) diff --git a/frontend/src/api/users/UsersAdmin.ts b/frontend/src/api/users/UsersAdmin.ts index 2dd65f3ff..d30391da2 100644 --- a/frontend/src/api/users/UsersAdmin.ts +++ b/frontend/src/api/users/UsersAdmin.ts @@ -36,3 +36,8 @@ export const deleteUserApi = async (uid: string): Promise => { const response = await axios.delete(`/admin/users/${uid}`) return response.data } + +export const getListSearchApi = async (data: {keyword: string | null}): Promise => { + const response = await axios.get(`/users/search/share_users${data.keyword ? `?keyword=${data.keyword}` : ''}`) + return response.data +} diff --git a/frontend/src/api/users/UsersApiDTO.ts b/frontend/src/api/users/UsersApiDTO.ts index 416a90d83..a745892e8 100644 --- a/frontend/src/api/users/UsersApiDTO.ts +++ b/frontend/src/api/users/UsersApiDTO.ts @@ -1,7 +1,10 @@ export type UserDTO = { id: number - uid: string + uid?: string email: string + name?: string + create_at?: string + update_at?: string } export type AddUserDTO = { diff --git a/frontend/src/components/Database/DatabaseExperiments.tsx b/frontend/src/components/Database/DatabaseExperiments.tsx index 3af5cab8e..24b2c61c0 100644 --- a/frontend/src/components/Database/DatabaseExperiments.tsx +++ b/frontend/src/components/Database/DatabaseExperiments.tsx @@ -1,4 +1,4 @@ -import { Box, DialogTitle, FormControl, FormControlLabel, Input, Pagination, Radio, RadioGroup, styled } from '@mui/material' +import { Box, Input, Pagination, styled } from '@mui/material' import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react' import { useNavigate, useSearchParams } from 'react-router-dom' import ContentPasteSearchIcon from '@mui/icons-material/ContentPasteSearch'; diff --git a/frontend/src/components/PopupShare.tsx b/frontend/src/components/PopupShare.tsx index 6820a5a9a..bc31ea545 100644 --- a/frontend/src/components/PopupShare.tsx +++ b/frontend/src/components/PopupShare.tsx @@ -1,14 +1,22 @@ -import {Box, Button, - Dialog, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, Radio, RadioGroup, styled } from "@mui/material"; -import {DataGrid, GridRenderCellParams, GridRowParams } from "@mui/x-data-grid"; +import { + Box, Button, + Dialog, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, Input, Radio, RadioGroup, styled +} from "@mui/material"; +import {DataGrid, GridRenderCellParams, GridRowParams} from "@mui/x-data-grid"; import { SHARE } from "../@types"; -import {ChangeEvent, MouseEvent, useCallback, useEffect, useState} from "react"; -import { useDispatch } from "react-redux"; +import {ChangeEvent, MouseEvent as MouseEventReact, useCallback, useEffect, useRef, useState} from "react"; +import {useDispatch, useSelector} from "react-redux"; import { postListUserShare } from "../store/slice/Database/DatabaseActions"; import CancelIcon from '@mui/icons-material/Cancel' import { ListShare } from "../store/slice/Database/DatabaseType"; import { ListUserShareWorkSpace } from "../store/slice/Workspace/WorkspaceType"; import { postListUserShareWorkspaces } from "store/slice/Workspace/WorkspacesActions"; +import {selectListSearch, selectListSearchLoading} from "../store/slice/User/UserSelector"; +import {getListSearch} from "../store/slice/User/UserActions"; +import Loading from "./common/Loading"; +import {UserDTO} from "../api/users/UsersApiDTO"; +import CheckIcon from '@mui/icons-material/Check'; +import {resetUserSearch} from "../store/slice/User/UserSlice"; type PopupType = { open: boolean @@ -26,42 +34,106 @@ type PopupType = { } } +type TableSearch = { + usersSuggest: UserDTO[] + onClose: () => void + handleAddListUser: (user: UserDTO) => void + stateUserShare: UserDTO[] +} + +const TableListSearch = ({usersSuggest, onClose, handleAddListUser, stateUserShare}: TableSearch) => { + + const ref = useRef(null) + + useEffect(() => { + window.addEventListener('mousedown', onMouseDown) + return () => { + window.removeEventListener('mousedown', onMouseDown) + } + //eslint-disable-next-line + }, []) + + const onMouseDown = (event: MouseEvent) => { + if(ref.current?.contains((event as any).target) || (event as any).target.id === 'inputSearch') return; + onClose?.() + } + + return ( + console.log(123)}> + + {usersSuggest.map(item => { + const isSelected = stateUserShare.some(i => i.id === item.id) + return ( + handleAddListUser(item)} style={{ + cursor: isSelected ? 'not-allowed' : 'pointer' + }} + > + {`${item.name} (${item.email})`} + {isSelected ? : null} + + ) + })} + + + ) +} const PopupShare = ({open, handleClose, data, usersShare, id, isWorkspace, title}: PopupType) => { const [shareType, setShareType] = useState(data?.shareType || 0) - const [userList, setUserList] = useState(usersShare?.users.map(user => user.id) || []) - const dispatch = useDispatch(); - + const [userIdsSelected, setUserIdsSelected] = useState(usersShare?.users.map(user => user.id) || []) + const usersSuggest = useSelector(selectListSearch) + const loading = useSelector(selectListSearchLoading) + const [textSearch, setTextSearch] = useState('') + const [stateUserShare, setStateUserShare] = useState(usersShare || undefined) + const dispatch = useDispatch() + let timeout = useRef() useEffect(() => { if(usersShare) { - setUserList(usersShare.users.map(user => user.id)); + setUserIdsSelected(usersShare.users.map(user => user.id)); + setStateUserShare(usersShare) } }, [usersShare]) + useEffect(() => { + + }, [data]) + + useEffect(() => { + if(timeout.current) clearTimeout(timeout.current) + if(!textSearch) { + dispatch(resetUserSearch()) + return + } + timeout.current = setTimeout(() => { + dispatch(getListSearch({keyword: textSearch})) + }, 300) + //eslint-disable-next-line + }, [textSearch]) + const handleShareTrue = (params: GridRowParams) => { if(!params) return - const index = userList.findIndex(item => { + const index = userIdsSelected.findIndex(item => { return item === params.id }) if(index < 0) { - userList.push(Number(params.id)) + userIdsSelected.push(Number(params.id)) } } const handleShareFalse = (e: any, params: GridRenderCellParams) => { e.preventDefault() e.stopPropagation() - if(userList.includes(Number(params.id))) { - setUserList(userList.filter(id => id !== Number(params.id))) - } else setUserList([...userList, Number(params.id)]) + if(userIdsSelected.includes(Number(params.id))) { + setUserIdsSelected(userIdsSelected.filter(id => id !== Number(params.id))) + } else setUserIdsSelected([...userIdsSelected, Number(params.id)]) } const handleValue = (event: ChangeEvent) => { - setUserList(usersShare?.users.map(user => user.id) || []) + setUserIdsSelected(usersShare?.users.map(user => user.id) || []) setShareType(Number((event.target as HTMLInputElement).value)); } - const columnsShare = useCallback((handleShareFalse: (e: MouseEvent, parmas: GridRenderCellParams) => void) => [ + const columnsShare = useCallback((handleShareFalse: (e: MouseEventReact, parmas: GridRenderCellParams) => void) => [ { field: "name", headerName: "Name", @@ -75,7 +147,7 @@ const PopupShare = ({open, handleClose, data, usersShare, id, isWorkspace, title headerName: "Email", minWidth: 280, renderCell: (params: GridRenderCellParams) => ( - {params.row.email} + {params.row.email} ), }, { @@ -87,72 +159,113 @@ const PopupShare = ({open, handleClose, data, usersShare, id, isWorkspace, title renderCell: (params: GridRenderCellParams) => { if(!params.row.share) return '' return ( - + ) } }, - ], [userList]) + ], [userIdsSelected]) const handleOke = async () => { if(!isWorkspace) { - await dispatch(postListUserShare({id, data: {user_ids: userList, share_type: shareType }})) + await dispatch(postListUserShare({id, data: {user_ids: userIdsSelected, share_type: shareType }})) } else { - await dispatch(postListUserShareWorkspaces({id, data: {user_ids: userList}})) + await dispatch(postListUserShareWorkspaces({id, data: {user_ids: userIdsSelected}})) } handleClose(true); } + const handleSearch = (event: ChangeEvent) => { + setTextSearch(event.target.value) + } + + const handleCloseSearch = () => { + setTextSearch('') + dispatch(resetUserSearch()) + } + + const handleAddListUser = (user: any) => { + if(!usersSuggest || !stateUserShare) return + if(!stateUserShare.users.find((item: any) => item.id === user.id)) { + setStateUserShare({...stateUserShare, users: [...stateUserShare.users, user]}) + setUserIdsSelected([...userIdsSelected, user.id]) + } + } + if(!data || !usersShare) return null; return ( - - - {title || "Share Database record"} - {isWorkspace ? null : Experiment ID: {data.expId}} - {isWorkspace ? null : ( - - - - } label={"Share for Organization"}/> - } label={"Share for Users"}/> - - - - )} - - { - (shareType === SHARE.USERS || isWorkspace) ? - <> -

Permitted users

+ + + {title || "Share Database record"} + {isWorkspace ? null : Experiment ID: {data.expId}} + {isWorkspace ? null : ( + + + + } label={"Share for Organization"}/> + } label={"Share for Users"}/> + + + + )} + + { + (shareType === SHARE.USERS || isWorkspace) ? + <> + + + { + textSearch && usersSuggest ? + : null + } + +

Permitted users

+ { + stateUserShare && ({...user, share: true}))} + rows={stateUserShare?.users.map((user: any) => ({...user, share: true}))} columns={columnsShare(handleShareFalse)} hideFooterPagination /> - - : null - } -
- - - - -
-
+ } + + : null + } +
+ + + + +
+ { + loading ? : null + } +
) } @@ -165,4 +278,34 @@ const DialogCustom = styled(Dialog)(({ theme }) => ({ }, })) +const TableListSearchWrapper = styled(Box)(({ theme }) => ({ + position: 'absolute', + background: "#fff", + zIndex: 100, + width: "60%", + boxShadow: '0 6px 16px 0 rgba(0,0,0,.08), 0 3px 6px -4px rgba(0,0,0,.12), 0 9px 28px 8px rgba(0,0,0,.05)', + borderBottomLeftRadius: 8, + borderBottomRightRadius: 8, + maxHeight: 200, + overflow: 'auto' +})) + +const UlCustom = styled('ul')(({ theme }) => ({ + listStyle: 'none', + padding: 0, + margin: 0, +})) + +const LiCustom = styled('li')(({ theme }) => ({ + padding: theme.spacing(1, 2), + fontSize: 14, + cursor: "pointer", + display: 'flex', + justifyContent: "space-between", + alignItems: 'center', + "&:hover": { + backgroundColor: 'rgba(0,0,0,.04)' + } +})) + export default PopupShare diff --git a/frontend/src/store/slice/User/UserActions.ts b/frontend/src/store/slice/User/UserActions.ts index 1f1dd4077..787c5459c 100644 --- a/frontend/src/store/slice/User/UserActions.ts +++ b/frontend/src/store/slice/User/UserActions.ts @@ -1,8 +1,9 @@ import { createAsyncThunk } from '@reduxjs/toolkit' import { USER_SLICE_NAME } from './UserType' import { deleteMeApi, getMeApi, updateMeApi } from 'api/users/UsersMe' -import { UpdateUserDTO } from 'api/users/UsersApiDTO' +import {UpdateUserDTO, UserDTO} from 'api/users/UsersApiDTO' import { LoginDTO, loginApi } from 'api/auth/Auth' +import {getListSearchApi} from "../../../api/users/UsersAdmin"; export const login = createAsyncThunk( `${USER_SLICE_NAME}/login`, @@ -51,3 +52,18 @@ export const deleteMe = createAsyncThunk( } }, ) + +export const getListSearch = createAsyncThunk< + UserDTO[], + {keyword: string | null} +>( + `${USER_SLICE_NAME}/getListSearch`, + async (params, thunkAPI) => { + try { + const responseData = await getListSearchApi(params) + return responseData + } catch (e) { + return thunkAPI.rejectWithValue(e) + } + }, +) diff --git a/frontend/src/store/slice/User/UserSelector.ts b/frontend/src/store/slice/User/UserSelector.ts index fa17bf27e..e359e44c8 100644 --- a/frontend/src/store/slice/User/UserSelector.ts +++ b/frontend/src/store/slice/User/UserSelector.ts @@ -5,3 +5,5 @@ export const selectCurrentUserUid = (state: RootState) => selectCurrentUser(state)?.uid export const selectCurrentUserEmail = (state: RootState) => selectCurrentUser(state)?.email +export const selectListSearch = (state: RootState) => state.user.listUserSearch +export const selectListSearchLoading = (state: RootState) => state.user.loading diff --git a/frontend/src/store/slice/User/UserSlice.ts b/frontend/src/store/slice/User/UserSlice.ts index f0a210b14..ff01f7a7c 100644 --- a/frontend/src/store/slice/User/UserSlice.ts +++ b/frontend/src/store/slice/User/UserSlice.ts @@ -1,7 +1,7 @@ import { createSlice, isAnyOf } from '@reduxjs/toolkit' import { USER_SLICE_NAME } from './UserType' import { User } from './UserType' -import { deleteMe, getMe, login, updateMe } from './UserActions' +import {deleteMe, getListSearch, getMe, login, updateMe} from './UserActions' import { removeExToken, removeToken, @@ -10,7 +10,11 @@ import { saveToken, } from 'utils/auth/AuthUtils' -const initialState: User = { currentUser: undefined } +const initialState: User = { + currentUser: undefined, + listUserSearch: undefined, + loading: false +} export const userSlice = createSlice({ name: USER_SLICE_NAME, @@ -21,6 +25,9 @@ export const userSlice = createSlice({ removeExToken() state = initialState }, + resetUserSearch: (state) => { + state.listUserSearch = [] + } }, extraReducers: (builder) => { builder @@ -35,6 +42,16 @@ export const userSlice = createSlice({ .addCase(updateMe.fulfilled, (state, action) => { state.currentUser = action.payload }) + .addCase(getListSearch.pending, (state, action) => { + state.loading = true + }) + .addCase(getListSearch.fulfilled, (state, action) => { + state.loading = false + state.listUserSearch = action.payload + }) + .addCase(getListSearch.rejected, (state) => { + state.loading = false + }) .addMatcher( isAnyOf(login.rejected, getMe.rejected, deleteMe.fulfilled), (state) => { @@ -46,5 +63,5 @@ export const userSlice = createSlice({ }, }) -export const { logout } = userSlice.actions +export const { logout, resetUserSearch } = userSlice.actions export default userSlice.reducer diff --git a/frontend/src/store/slice/User/UserType.ts b/frontend/src/store/slice/User/UserType.ts index 9deed628e..9f3cc5d2f 100644 --- a/frontend/src/store/slice/User/UserType.ts +++ b/frontend/src/store/slice/User/UserType.ts @@ -4,4 +4,6 @@ export const USER_SLICE_NAME = 'user' export type User = { currentUser?: UserDTO + listUserSearch?: UserDTO[] + loading: boolean } From 64207ab78fa1ddbf2b75e29b4c197dd991c6eeac Mon Sep 17 00:00:00 2001 From: SangLv Date: Wed, 2 Aug 2023 09:19:49 +0700 Subject: [PATCH 25/30] add key to map --- frontend/src/components/PopupShare.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/PopupShare.tsx b/frontend/src/components/PopupShare.tsx index bc31ea545..60115c0b8 100644 --- a/frontend/src/components/PopupShare.tsx +++ b/frontend/src/components/PopupShare.tsx @@ -64,7 +64,7 @@ const TableListSearch = ({usersSuggest, onClose, handleAddListUser, stateUserSha {usersSuggest.map(item => { const isSelected = stateUserShare.some(i => i.id === item.id) return ( - handleAddListUser(item)} style={{ + handleAddListUser(item)} style={{ cursor: isSelected ? 'not-allowed' : 'pointer' }} > From 291b2cd044332c40125608a72f8fdc415d4b7ab5 Mon Sep 17 00:00:00 2001 From: SangLv Date: Wed, 2 Aug 2023 11:49:22 +0700 Subject: [PATCH 26/30] add dispatch getMe when submit login --- frontend/src/pages/Login/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/Login/index.tsx b/frontend/src/pages/Login/index.tsx index 9f87c6416..59a4bb1cc 100644 --- a/frontend/src/pages/Login/index.tsx +++ b/frontend/src/pages/Login/index.tsx @@ -1,6 +1,6 @@ import { Box, Stack, styled, Typography } from '@mui/material' import { useDispatch } from 'react-redux' -import { login } from 'store/slice/User/UserActions' +import {getMe, login} from 'store/slice/User/UserActions' import { AppDispatch } from 'store/store' import { ChangeEvent, FormEvent, useState } from 'react' import { Link, useNavigate } from 'react-router-dom' @@ -27,7 +27,8 @@ const Login = () => { setIsLoading(true) dispatch(login(values)) .unwrap() - .then((_) => { + .then(async (_) => { + await dispatch(getMe()) navigate('/console') }) .catch((_) => { From fc6fd59dd48ba76480cae523d367dc2cebb3be0a Mon Sep 17 00:00:00 2001 From: SangLv Date: Wed, 2 Aug 2023 14:15:51 +0700 Subject: [PATCH 27/30] add keydown esc cancel search, del user when click IconCancel, fix share_type --- .../Database/DatabaseExperiments.tsx | 2 +- frontend/src/components/PopupShare.tsx | 111 ++++++++++-------- 2 files changed, 60 insertions(+), 53 deletions(-) diff --git a/frontend/src/components/Database/DatabaseExperiments.tsx b/frontend/src/components/Database/DatabaseExperiments.tsx index 24b2c61c0..1f719ece1 100644 --- a/frontend/src/components/Database/DatabaseExperiments.tsx +++ b/frontend/src/components/Database/DatabaseExperiments.tsx @@ -430,7 +430,7 @@ const DatabaseExperiments = ({ user, cellPath }: DatabaseProps) => { sx={{ cursor: 'pointer' }} onClick={() => handleOpenShare(row.experiment_id, value, row.id)} > - +
) } diff --git a/frontend/src/components/PopupShare.tsx b/frontend/src/components/PopupShare.tsx index 60115c0b8..aad925730 100644 --- a/frontend/src/components/PopupShare.tsx +++ b/frontend/src/components/PopupShare.tsx @@ -2,21 +2,21 @@ import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, Input, Radio, RadioGroup, styled } from "@mui/material"; -import {DataGrid, GridRenderCellParams, GridRowParams} from "@mui/x-data-grid"; +import { DataGrid, GridRenderCellParams } from "@mui/x-data-grid"; import { SHARE } from "../@types"; -import {ChangeEvent, MouseEvent as MouseEventReact, useCallback, useEffect, useRef, useState} from "react"; -import {useDispatch, useSelector} from "react-redux"; +import { ChangeEvent, MouseEvent as MouseEventReact, useCallback, useEffect, useRef, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; import { postListUserShare } from "../store/slice/Database/DatabaseActions"; import CancelIcon from '@mui/icons-material/Cancel' import { ListShare } from "../store/slice/Database/DatabaseType"; import { ListUserShareWorkSpace } from "../store/slice/Workspace/WorkspaceType"; import { postListUserShareWorkspaces } from "store/slice/Workspace/WorkspacesActions"; -import {selectListSearch, selectListSearchLoading} from "../store/slice/User/UserSelector"; -import {getListSearch} from "../store/slice/User/UserActions"; +import { selectListSearch, selectListSearchLoading } from "../store/slice/User/UserSelector"; +import { getListSearch } from "../store/slice/User/UserActions"; import Loading from "./common/Loading"; -import {UserDTO} from "../api/users/UsersApiDTO"; +import { UserDTO } from "../api/users/UsersApiDTO"; import CheckIcon from '@mui/icons-material/Check'; -import {resetUserSearch} from "../store/slice/User/UserSlice"; +import { resetUserSearch } from "../store/slice/User/UserSlice"; type PopupType = { open: boolean @@ -64,13 +64,13 @@ const TableListSearch = ({usersSuggest, onClose, handleAddListUser, stateUserSha {usersSuggest.map(item => { const isSelected = stateUserShare.some(i => i.id === item.id) return ( - handleAddListUser(item)} style={{ - cursor: isSelected ? 'not-allowed' : 'pointer' - }} - > - {`${item.name} (${item.email})`} - {isSelected ? : null} - + handleAddListUser(item)} style={{ + cursor: isSelected ? 'not-allowed' : 'pointer' + }} + > + {`${item.name} (${item.email})`} + {isSelected ? : null} + ) })} @@ -79,7 +79,6 @@ const TableListSearch = ({usersSuggest, onClose, handleAddListUser, stateUserSha } const PopupShare = ({open, handleClose, data, usersShare, id, isWorkspace, title}: PopupType) => { const [shareType, setShareType] = useState(data?.shareType || 0) - const [userIdsSelected, setUserIdsSelected] = useState(usersShare?.users.map(user => user.id) || []) const usersSuggest = useSelector(selectListSearch) const loading = useSelector(selectListSearchLoading) const [textSearch, setTextSearch] = useState('') @@ -89,7 +88,7 @@ const PopupShare = ({open, handleClose, data, usersShare, id, isWorkspace, title useEffect(() => { if(usersShare) { - setUserIdsSelected(usersShare.users.map(user => user.id)); + // setUserIdsSelected(usersShare.users.map(user => user.id)); setStateUserShare(usersShare) } }, [usersShare]) @@ -110,26 +109,16 @@ const PopupShare = ({open, handleClose, data, usersShare, id, isWorkspace, title //eslint-disable-next-line }, [textSearch]) - const handleShareTrue = (params: GridRowParams) => { - if(!params) return - const index = userIdsSelected.findIndex(item => { - return item === params.id - }) - if(index < 0) { - userIdsSelected.push(Number(params.id)) - } - } - const handleShareFalse = (e: any, params: GridRenderCellParams) => { e.preventDefault() e.stopPropagation() - if(userIdsSelected.includes(Number(params.id))) { - setUserIdsSelected(userIdsSelected.filter(id => id !== Number(params.id))) - } else setUserIdsSelected([...userIdsSelected, Number(params.id)]) + if(!stateUserShare) return + const indexCheck = stateUserShare.users.findIndex(user => user.id === params.id) + const newStateUserShare = stateUserShare.users.filter((user, index) => index !== indexCheck) + setStateUserShare({...setStateUserShare, users: newStateUserShare}) } const handleValue = (event: ChangeEvent) => { - setUserIdsSelected(usersShare?.users.map(user => user.id) || []) setShareType(Number((event.target as HTMLInputElement).value)); } @@ -160,18 +149,29 @@ const PopupShare = ({open, handleClose, data, usersShare, id, isWorkspace, title if(!params.row.share) return '' return ( ) } }, - ], [userIdsSelected]) + //eslint-disable-next-line + ], [JSON.stringify(stateUserShare?.users)]) const handleOke = async () => { + if(!stateUserShare) return + let newUserIds = stateUserShare.users.map(user => user.id) + let newType = shareType if(!isWorkspace) { - await dispatch(postListUserShare({id, data: {user_ids: userIdsSelected, share_type: shareType }})) + if(shareType === SHARE.ORGANIZATION) { + newUserIds = [] + } + else if(shareType === SHARE.USERS && newUserIds.length < 1) { + newType = 0 + } + else if(newUserIds.length > 0) newType = SHARE.USERS + await dispatch(postListUserShare({id, data: {user_ids: newUserIds, share_type: newType }})) } else { - await dispatch(postListUserShareWorkspaces({id, data: {user_ids: userIdsSelected}})) + await dispatch(postListUserShareWorkspaces({id, data: {user_ids: newUserIds}})) } handleClose(true); } @@ -187,9 +187,15 @@ const PopupShare = ({open, handleClose, data, usersShare, id, isWorkspace, title const handleAddListUser = (user: any) => { if(!usersSuggest || !stateUserShare) return - if(!stateUserShare.users.find((item: any) => item.id === user.id)) { + if(!stateUserShare.users.find(item => item.id === user.id)) { setStateUserShare({...stateUserShare, users: [...stateUserShare.users, user]}) - setUserIdsSelected([...userIdsSelected, user.id]) + } + } + + const handleCancelSearch = (event: any) => { + if(event.key === 'Escape') { + setTextSearch('') + dispatch(resetUserSearch()) } } @@ -208,38 +214,39 @@ const PopupShare = ({open, handleClose, data, usersShare, id, isWorkspace, title - } label={"Share for Organization"}/> - } label={"Share for Users"}/> + } label={"Share for Organization"}/> + } label={"Share for Users"}/> )} { - (shareType === SHARE.USERS || isWorkspace) ? + (shareType !== SHARE.ORGANIZATION || isWorkspace) ? <> { textSearch && usersSuggest ? - : null + : null }

Permitted users

@@ -247,7 +254,7 @@ const PopupShare = ({open, handleClose, data, usersShare, id, isWorkspace, title stateUserShare && ({...user, share: true}))} columns={columnsShare(handleShareFalse)} hideFooterPagination From 67429abec86dcc95e5b8495c493497d1bb904c1e Mon Sep 17 00:00:00 2001 From: SangLv Date: Wed, 2 Aug 2023 14:44:53 +0700 Subject: [PATCH 28/30] marge share user --- frontend/src/api/users/UsersApiDTO.ts | 2 +- .../Database/DatabaseExperiments.tsx | 8 ----- frontend/src/components/PopupShare.tsx | 8 ++--- frontend/src/pages/Workspace/index.tsx | 35 ------------------- .../store/slice/Workspace/WorkspaceType.ts | 4 ++- 5 files changed, 7 insertions(+), 50 deletions(-) diff --git a/frontend/src/api/users/UsersApiDTO.ts b/frontend/src/api/users/UsersApiDTO.ts index 86dd7191c..13c0d24cd 100644 --- a/frontend/src/api/users/UsersApiDTO.ts +++ b/frontend/src/api/users/UsersApiDTO.ts @@ -4,7 +4,7 @@ export type UserDTO = { id?: number name?: string organization_id?: number - role_id: number + role_id?: number create_at?: string update_at?: string } diff --git a/frontend/src/components/Database/DatabaseExperiments.tsx b/frontend/src/components/Database/DatabaseExperiments.tsx index dc196cbfb..128915ea9 100644 --- a/frontend/src/components/Database/DatabaseExperiments.tsx +++ b/frontend/src/components/Database/DatabaseExperiments.tsx @@ -251,14 +251,6 @@ const DatabaseExperiments = ({ user, cellPath }: DatabaseProps) => { const dispatch = useDispatch() const navigate = useNavigate() - const type: keyof TypeData = user ? 'private' : 'public' - const { data: dataExperiments, loading } = useSelector( - (state: RootState) => ({ - data: state[DATABASE_SLICE_NAME].data[type], - loading: state[DATABASE_SLICE_NAME].loading, - }), - ) - const { dataShare } = useSelector( (state: RootState) => ({ dataShare: state[DATABASE_SLICE_NAME].listShare, diff --git a/frontend/src/components/PopupShare.tsx b/frontend/src/components/PopupShare.tsx index aad925730..b2bd79c4c 100644 --- a/frontend/src/components/PopupShare.tsx +++ b/frontend/src/components/PopupShare.tsx @@ -8,8 +8,6 @@ import { ChangeEvent, MouseEvent as MouseEventReact, useCallback, useEffect, use import { useDispatch, useSelector } from "react-redux"; import { postListUserShare } from "../store/slice/Database/DatabaseActions"; import CancelIcon from '@mui/icons-material/Cancel' -import { ListShare } from "../store/slice/Database/DatabaseType"; -import { ListUserShareWorkSpace } from "../store/slice/Workspace/WorkspaceType"; import { postListUserShareWorkspaces } from "store/slice/Workspace/WorkspacesActions"; import { selectListSearch, selectListSearchLoading } from "../store/slice/User/UserSelector"; import { getListSearch } from "../store/slice/User/UserActions"; @@ -30,7 +28,7 @@ type PopupType = { } usersShare?: { share_type?: number - users: (ListShare | ListUserShareWorkSpace)[] + users: UserDTO[] } } @@ -169,9 +167,9 @@ const PopupShare = ({open, handleClose, data, usersShare, id, isWorkspace, title newType = 0 } else if(newUserIds.length > 0) newType = SHARE.USERS - await dispatch(postListUserShare({id, data: {user_ids: newUserIds, share_type: newType }})) + await dispatch(postListUserShare({id, data: {user_ids: newUserIds as number[], share_type: newType }})) } else { - await dispatch(postListUserShareWorkspaces({id, data: {user_ids: newUserIds}})) + await dispatch(postListUserShareWorkspaces({id, data: {user_ids: newUserIds as number[]}})) } handleClose(true); } diff --git a/frontend/src/pages/Workspace/index.tsx b/frontend/src/pages/Workspace/index.tsx index 6f413277c..0ef3100c8 100644 --- a/frontend/src/pages/Workspace/index.tsx +++ b/frontend/src/pages/Workspace/index.tsx @@ -38,7 +38,6 @@ import { import PopupShare from 'components/PopupShare' import moment from 'moment' import SystemUpdateAltIcon from '@mui/icons-material/SystemUpdateAlt'; -import CancelIcon from '@mui/icons-material/Cancel'; import GroupsIcon from '@mui/icons-material/Groups'; import EditIcon from '@mui/icons-material/Edit'; import { selectCurrentUser } from 'store/slice/User/UserSelector' @@ -196,40 +195,6 @@ const columns = ( }, ] -const columnsShare = ( - handleShareFalse: (parmas: GridRenderCellParams) => void, -) => [ - { - field: 'name', - headerName: 'Name', - minWidth: 140, - renderCell: (params: GridRenderCellParams) => ( - {params.row.name} - ), - }, - { - field: 'email', - headerName: 'Email', - minWidth: 280, - renderCell: (params: GridRenderCellParams) => ( - {params.row.email} - ), - }, - { - field: 'share', - headerName: '', - minWidth: 130, - renderCell: (params: GridRenderCellParams) => { - if (!params.row.share) return '' - return ( - - ) - }, - }, -] - const PopupNew = ({ open, handleClose, diff --git a/frontend/src/store/slice/Workspace/WorkspaceType.ts b/frontend/src/store/slice/Workspace/WorkspaceType.ts index c57fa1417..5d6e1d74a 100644 --- a/frontend/src/store/slice/Workspace/WorkspaceType.ts +++ b/frontend/src/store/slice/Workspace/WorkspaceType.ts @@ -1,3 +1,5 @@ +import {UserDTO} from "../../../api/users/UsersApiDTO"; + export const WORKSPACE_SLICE_NAME = 'workspace' export type ItemsWorkspace = { @@ -40,7 +42,7 @@ export type ListUserShareWorkSpace = { } export type ListUserShareWorkspaceDTO = { - users: ListUserShareWorkSpace[] + users: UserDTO[] } export type WorkspaceParams = { [key: string]: string | undefined | number | string[] | object } From 87a45142575fd83a44e886ed756d00d9c434c444 Mon Sep 17 00:00:00 2001 From: SangLv Date: Wed, 2 Aug 2023 15:49:08 +0700 Subject: [PATCH 29/30] close popup when press esc --- frontend/src/components/PopupShare.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/PopupShare.tsx b/frontend/src/components/PopupShare.tsx index aad925730..0505bebd9 100644 --- a/frontend/src/components/PopupShare.tsx +++ b/frontend/src/components/PopupShare.tsx @@ -192,10 +192,9 @@ const PopupShare = ({open, handleClose, data, usersShare, id, isWorkspace, title } } - const handleCancelSearch = (event: any) => { + const handleClosePopup = (event: any) => { if(event.key === 'Escape') { - setTextSearch('') - dispatch(resetUserSearch()) + handleClose(false) } } @@ -207,6 +206,7 @@ const PopupShare = ({open, handleClose, data, usersShare, id, isWorkspace, title open={open} onClose={handleClose} sx={{margin: 0}} + onKeyDown={handleClosePopup} > {title || "Share Database record"} {isWorkspace ? null : Experiment ID: {data.expId}} @@ -237,7 +237,6 @@ const PopupShare = ({open, handleClose, data, usersShare, id, isWorkspace, title placeholder={"Search and add users"} value={textSearch} onChange={handleSearch} - onKeyDown={handleCancelSearch} /> { textSearch && usersSuggest ? From 916a91efd019101e6f0c64bccc445d8fb77f90c9 Mon Sep 17 00:00:00 2001 From: SangLv Date: Thu, 3 Aug 2023 17:17:11 +0700 Subject: [PATCH 30/30] add role to attribute --- .../Database/DatabaseExperiments.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/Database/DatabaseExperiments.tsx b/frontend/src/components/Database/DatabaseExperiments.tsx index 128915ea9..a4a6509c3 100644 --- a/frontend/src/components/Database/DatabaseExperiments.tsx +++ b/frontend/src/components/Database/DatabaseExperiments.tsx @@ -540,20 +540,20 @@ const DatabaseExperiments = ({ user, cellPath }: DatabaseProps) => { data={dataDialog.data} open={dataDialog.type === 'attribute'} handleClose={handleCloseDialog} - role={!!user} + role={!!adminOrManager} /> {loading ? : null} {openShare.open && openShare.id ? - { - if(isSubmit) fetchApi(); - setOpenShare({...openShare, open: false})} - } - /> : null + { + if(isSubmit) fetchApi(); + setOpenShare({...openShare, open: false})} + } + /> : null }
)