From 95533278566da8867d4906e1a2a6b1758260e6d7 Mon Sep 17 00:00:00 2001 From: SangLv Date: Fri, 28 Jul 2023 15:27:16 +0700 Subject: [PATCH 01/21] 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 a12f040f5943f723b8a9c6bbed834a1b644b4c78 Mon Sep 17 00:00:00 2001 From: SangLv Date: Fri, 28 Jul 2023 16:38:43 +0700 Subject: [PATCH 02/21] 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 b526f73516a4ba2e3277b3f10c707cfb9b55349f Mon Sep 17 00:00:00 2001 From: SangLv Date: Fri, 28 Jul 2023 19:58:21 +0700 Subject: [PATCH 04/21] 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 81bc59a2bcfb0fd9a7a219574f11ec29fcdb1c5f Mon Sep 17 00:00:00 2001 From: SangLv Date: Fri, 28 Jul 2023 20:02:00 +0700 Subject: [PATCH 05/21] 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 06/21] 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 077dc97d56a222f94ec13e9e19c8b2f9848fa3ca Mon Sep 17 00:00:00 2001 From: SangLv Date: Fri, 28 Jul 2023 20:06:07 +0700 Subject: [PATCH 07/21] 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 e7737bde59e3d3260517b8e0cbe42e45e268ad74 Mon Sep 17 00:00:00 2001 From: SangLv Date: Fri, 28 Jul 2023 20:13:13 +0700 Subject: [PATCH 08/21] 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 0d51d50d83a44573c375de7e59a86c5fc28aa2e3 Mon Sep 17 00:00:00 2001 From: SangLv Date: Mon, 31 Jul 2023 10:53:39 +0700 Subject: [PATCH 09/21] 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 10/21] 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 08fc1942ccbe2379889b4bc10f2cd53618a9c586 Mon Sep 17 00:00:00 2001 From: SangLv Date: Mon, 31 Jul 2023 11:10:17 +0700 Subject: [PATCH 11/21] 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 736b1ab392a325961f4ee216add7d4043063d57a Mon Sep 17 00:00:00 2001 From: quanpython Date: Tue, 1 Aug 2023 16:56:07 +0700 Subject: [PATCH 12/21] 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 1f84768d04b3fa985fb15cd77f0260c1b8ad7866 Mon Sep 17 00:00:00 2001 From: quanpython Date: Tue, 1 Aug 2023 17:00:35 +0700 Subject: [PATCH 13/21] 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 7cbefded673be1507227ab62ad8c104bc6e0a1a7 Mon Sep 17 00:00:00 2001 From: quanpython Date: Tue, 1 Aug 2023 17:17:50 +0700 Subject: [PATCH 14/21] 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 373d9748640fc37693b59852d03ef6fbff245b93 Mon Sep 17 00:00:00 2001 From: quanpython Date: Mon, 31 Jul 2023 23:54:26 +0700 Subject: [PATCH 15/21] 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 8b6de9259582c534beed988b40badef3f33408f2 Mon Sep 17 00:00:00 2001 From: SangLv Date: Tue, 1 Aug 2023 19:11:49 +0700 Subject: [PATCH 16/21] 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 6e38132eb0473709fe4472f96a3595771bf5ab21 Mon Sep 17 00:00:00 2001 From: SangLv Date: Wed, 2 Aug 2023 09:19:49 +0700 Subject: [PATCH 17/21] 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 cc4834b4cffcb47949704670bf528a2630e2e848 Mon Sep 17 00:00:00 2001 From: SangLv Date: Wed, 2 Aug 2023 11:49:22 +0700 Subject: [PATCH 18/21] 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 3fa6c15e1bca152ed4d65fae071c9830fc296f9a Mon Sep 17 00:00:00 2001 From: SangLv Date: Wed, 2 Aug 2023 14:15:51 +0700 Subject: [PATCH 19/21] 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 e77818c32db927ecacc29baf60abca22af3aa47b Mon Sep 17 00:00:00 2001 From: SangLv Date: Wed, 2 Aug 2023 15:49:08 +0700 Subject: [PATCH 20/21] 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 dd9617948470a033d137120704d60a221eb93ec2 Mon Sep 17 00:00:00 2001 From: tienday <> Date: Thu, 3 Aug 2023 13:03:59 +0900 Subject: [PATCH 21/21] Apply style adjustments --- frontend/src/components/PopupShare.tsx | 42 ++++++++++++++------------ 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/frontend/src/components/PopupShare.tsx b/frontend/src/components/PopupShare.tsx index 0505bebd9..cee7a8d0d 100644 --- a/frontend/src/components/PopupShare.tsx +++ b/frontend/src/components/PopupShare.tsx @@ -1,6 +1,6 @@ import { Box, Button, - Dialog, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, Input, Radio, RadioGroup, styled + Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, FormControl, FormControlLabel, Input, Radio, RadioGroup, styled } from "@mui/material"; import { DataGrid, GridRenderCellParams } from "@mui/x-data-grid"; import { SHARE } from "../@types"; @@ -208,25 +208,27 @@ const PopupShare = ({open, handleClose, data, usersShare, id, isWorkspace, title sx={{margin: 0}} onKeyDown={handleClosePopup} > - {title || "Share Database record"} - {isWorkspace ? null : Experiment ID: {data.expId}} + {title || "Share Database Record"} {isWorkspace ? null : ( - - - - } label={"Share for Organization"}/> - } label={"Share for Users"}/> - - - - )} - + +
  • Experiment ID: {data.expId}
+ + + + } label={"Share for Organization"}/> + } label={"Share for Users"}/> + + + +
+ )} + { (shareType !== SHARE.ORGANIZATION || isWorkspace) ? <> @@ -252,7 +254,7 @@ const PopupShare = ({open, handleClose, data, usersShare, id, isWorkspace, title { stateUserShare && ({...user, share: true}))} columns={columnsShare(handleShareFalse)}