Skip to content

Commit

Permalink
feat(frontend): update user roles list during project creation (#2135)
Browse files Browse the repository at this point in the history
* feat(package): tanstack table install

* feat(routes): manage-users route add

* feat(assetModules): icon add

* feat(css): remove counter button from input

* fix(paginationType): place paginationType to common

* feat(table): table radix component add

* fix(user): update ts type

* fix(pagination): update pagination type

* fix(table): update styles

* feat(dataTable): DataTable components add

* feat(user): update user role func add

* fix(searchBar): fix style

* feat(assetModules): add icon

* feat(manageUsers): manage users table & role update functionality add

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix(manageUsers): update row column

* fix(lock): fix broken lock file

* fix(user): GetUserListForSelect api func add specifically for select

* feat(user): slice add for user list select

* fix(select2): handleApiSearch add for server side search

* fix(projectDetailsForm): refactor, search users list on backend

* fix(projectDetailsForm): add name prop to select

* fix(common): add previousSelecetdOptions state

* fix(projectDetailsForm): reorder form field

* fix(projectDetailsForm): update placeholder text

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
NSUWAL123 and pre-commit-ci[bot] authored Jan 31, 2025
1 parent 25f6ff6 commit 79c87f5
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 27 deletions.
23 changes: 23 additions & 0 deletions src/frontend/src/api/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,26 @@ export const UpdateUserRole = (url: string, payload: { role: 'READ_ONLY' | 'ADMI
await updateUserRole(url);
};
};

export const GetUserListForSelect = (
url: string,
params: { page: number; results_per_page: number; search: string },
) => {
return async (dispatch: AppDispatch) => {
dispatch(UserActions.SetUserListForSelectLoading(true));

const getUserList = async (url: string) => {
try {
const response: AxiosResponse<{ results: userType[]; pagination: paginationType }> = await axios.get(url, {
params,
});
dispatch(UserActions.SetUserListForSelect(response.data.results));
} catch (error) {
} finally {
dispatch(UserActions.SetUserListForSelectLoading(false));
}
};

await getUserList(url);
};
};
43 changes: 38 additions & 5 deletions src/frontend/src/components/common/Select2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { Command, CommandGroup, CommandItem } from '@/components/RadixComponents
import { Popover, PopoverContent, PopoverTrigger } from '@/components/RadixComponents/Popover';
import useDebouncedInput from '@/hooks/useDebouncedInput';
import AssetModules from '@/shared/AssetModules';
import { useAppSelector } from '@/types/reduxTypes';
import { useDispatch } from 'react-redux';
import { CommonActions } from '@/store/slices/CommonSlice';

export interface selectPropType
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'onFocus' | 'onAbort'> {
Expand All @@ -21,8 +24,10 @@ export interface selectPropType
onFocus?: (e?: any) => void;
onChange?: (e: any) => void;
enableSearchbar?: boolean;
handleApiSearch?: (e: string) => void;
name?: string;
}
type selectOptionsType = {
export type selectOptionsType = {
label: string;
value: string | boolean | number;
id?: string | number;
Expand All @@ -32,6 +37,7 @@ type selectOptionsType = {
};

function Select2({
name = 'select', // required if it's server-side search implemented
options = [],
multiple = false,
choose = 'id',
Expand All @@ -47,12 +53,17 @@ function Select2({
style,
checkBox = false,
enableSearchbar = true,
handleApiSearch, // if search is handled on backend
}: selectPropType) {
const dispatch = useDispatch();

const [open, setOpen] = React.useState(false);
const [searchText, setSearchText] = React.useState('');
const [filteredOptionsData, setFilteredOptionsData] = React.useState<any>([]);
const [filteredOptionsData, setFilteredOptionsData] = React.useState<selectOptionsType[]>([]);
const [dropDownWidth, setDropDownWidth] = React.useState<number | undefined>(0);

const previousSelectedOptions = useAppSelector((state) => state.common.previousSelectedOptions);

const handleSelect = (currentValue: any) => {
if (onFocus) onFocus();

Expand Down Expand Up @@ -85,7 +96,13 @@ function Select2({
const [searchTextData, handleChangeData] = useDebouncedInput({
ms: 400,
init: searchText,
onChange: (debouncedEvent) => setSearchText(debouncedEvent.target.value),
onChange: (debouncedEvent) => {
if (handleApiSearch) {
handleApiSearch(debouncedEvent.target.value);
} else {
setSearchText(debouncedEvent.target.value);
}
},
});

const triggerRef = useRef<HTMLButtonElement | null>(null);
Expand Down Expand Up @@ -190,6 +207,17 @@ function Select2({
key={option.value?.toString()}
onSelect={() => {
handleSelect(option[choose as keyof selectPropType]);
// if server-side search then store the selected option since options list is cleared
if (handleApiSearch && name) {
dispatch(
CommonActions.SetPreviousSelectedOptions({
key: name,
options: previousSelectedOptions[name]
? [...previousSelectedOptions[name], option]
: [option],
}),
);
}
}}
className="fmtm-flex fmtm-items-center fmtm-gap-[0.15rem] hover:fmtm-bg-red-50 fmtm-duration-150"
>
Expand Down Expand Up @@ -221,7 +249,7 @@ function Select2({
))
) : (
<div className="fmtm-body-sm fmtm-line-clamp-1 fmtm-flex fmtm-h-[4.25rem] fmtm-items-center fmtm-justify-center fmtm-text-start">
No Data Found.
No Data Found
</div>
)}
</CommandGroup>
Expand All @@ -236,7 +264,12 @@ function Select2({
key={val}
className="fmtm-bg-[#F5F5F5] fmtm-rounded-full fmtm-px-2 fmtm-py-1 fmtm-border-[1px] fmtm-border-[#D7D7D7] fmtm-text-[#484848] fmtm-flex fmtm-items-center fmtm-gap-1"
>
<p>{options.find((option) => option.value === val)?.label}</p>
<p>
{handleApiSearch && name
? [...previousSelectedOptions[name as string], ...options]?.find((option) => option.value === val)
?.label
: options.find((option) => option.value === val)?.label}
</p>
<AssetModules.CloseIcon
onClick={() => handleSelect(val)}
className="!fmtm-text-[1.125rem] fmtm-cursor-pointer hover:fmtm-text-red-600"
Expand Down
51 changes: 29 additions & 22 deletions src/frontend/src/components/createnewproject/ProjectDetailsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import RichTextEditor from '@/components/common/Editor/Editor';
import useDocumentTitle from '@/utilfunctions/useDocumentTitle';
import DescriptionSection from '@/components/createnewproject/Description';
import Select2 from '@/components/common/Select2';
import { GetUserListService } from '@/api/User';
import { GetUserListForSelect } from '@/api/User';
import { UserActions } from '@/store/slices/UserSlice';

const VITE_API_URL = import.meta.env.VITE_API_URL;

Expand All @@ -26,7 +27,7 @@ const ProjectDetailsForm = ({ flag }) => {
const projectDetails = useAppSelector((state) => state.createproject.projectDetails);
const organisationListData = useAppSelector((state) => state.createproject.organisationList);
const organisationListLoading = useAppSelector((state) => state.createproject.organisationListLoading);
const userList = useAppSelector((state) => state.user.userList)?.map((user) => ({
const userList = useAppSelector((state) => state.user.userListForSelect)?.map((user) => ({
id: user.id,
label: user.username,
value: user.id,
Expand Down Expand Up @@ -66,10 +67,6 @@ const ProjectDetailsForm = ({ flag }) => {
};
}, []);

useEffect(() => {
dispatch(GetUserListService(`${VITE_API_URL}/users`));
}, []);

const handleInputChanges = (e) => {
handleChange(e);
dispatch(CreateProjectActions.SetIsUnsavedChanges(true));
Expand Down Expand Up @@ -178,22 +175,6 @@ const ProjectDetailsForm = ({ flag }) => {
<p className="fmtm-form-error fmtm-text-red-600 fmtm-text-sm fmtm-py-1">{errors.organisation_id}</p>
)}
</div>
{/* Select project admin */}
<div>
<p className="fmtm-text-[1rem] fmtm-mb-2 fmtm-font-semibold !fmtm-bg-transparent">Assign Project Admin</p>
<Select2
options={userList || []}
value={values.project_admins}
onChange={(value: any) => {
handleCustomChange('project_admins', value);
}}
placeholder="Assign Project Admin"
className="naxatw-w-1/5 naxatw-min-w-[9rem]"
multiple
checkBox
isLoading={userListLoading}
/>
</div>
{/* Custom ODK creds toggle */}
<div
className="fmtm-flex fmtm-flex-col fmtm-gap-6"
Expand All @@ -218,6 +199,32 @@ const ProjectDetailsForm = ({ flag }) => {
<ODKCredentialsFields values={values} errors={errors} handleChange={handleChange} />
)}
</div>
{/* Select project admin */}
<div>
<p className="fmtm-text-[1rem] fmtm-mb-2 fmtm-font-semibold !fmtm-bg-transparent">Assign Project Admin</p>
<Select2
name="project_admins"
options={userList || []}
value={values.project_admins}
onChange={(value: any) => {
handleCustomChange('project_admins', value);
}}
placeholder="Search for FMTM users"
className="naxatw-w-1/5 naxatw-min-w-[9rem]"
multiple
checkBox
isLoading={userListLoading}
handleApiSearch={(value) => {
if (value) {
dispatch(
GetUserListForSelect(`${VITE_API_URL}/users`, { search: value, page: 1, results_per_page: 30 }),
);
} else {
dispatch(UserActions.SetUserListForSelect([]));
}
}}
/>
</div>
{/* Hashtags */}
<div>
<InputTextField
Expand Down
6 changes: 6 additions & 0 deletions src/frontend/src/store/slices/CommonSlice.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { CommonStateTypes } from '@/store/types/ICommon';
import { selectOptionsType } from '@/components/common/Select2';

const initialState: CommonStateTypes = {
snackbar: {
Expand All @@ -16,6 +17,7 @@ const initialState: CommonStateTypes = {
},
},
projectNotFound: false,
previousSelectedOptions: {},
};

const CommonSlice = createSlice({
Expand All @@ -37,6 +39,10 @@ const CommonSlice = createSlice({
SetProjectNotFound(state, action: PayloadAction<boolean>) {
state.projectNotFound = action.payload;
},
// set previous selected options of select component
SetPreviousSelectedOptions(state, action: PayloadAction<{ key: string; options: selectOptionsType[] }>) {
state.previousSelectedOptions[action.payload.key] = action.payload.options;
},
},
});

Expand Down
8 changes: 8 additions & 0 deletions src/frontend/src/store/slices/UserSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export const initialState: UserStateTypes = {
},
userListLoading: false,
updateUserRoleLoading: false,
userListForSelect: [],
userListForSelectLoading: false,
};

const UserSlice = createSlice({
Expand All @@ -38,6 +40,12 @@ const UserSlice = createSlice({
SetUpdateUserRoleLoading: (state, action: PayloadAction<boolean>) => {
state.updateUserRoleLoading = action.payload;
},
SetUserListForSelect: (state, action: PayloadAction<UserStateTypes['userListForSelect']>) => {
state.userListForSelect = action.payload;
},
SetUserListForSelectLoading: (state, action: PayloadAction<boolean>) => {
state.userListLoading = action.payload;
},
},
});

Expand Down
3 changes: 3 additions & 0 deletions src/frontend/src/store/types/ICommon.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { selectOptionsType } from '@/components/common/Select2';

export type CommonStateTypes = {
snackbar: snackbarTypes;
loading: boolean;
Expand All @@ -8,6 +10,7 @@ export type CommonStateTypes = {
};
};
projectNotFound: boolean;
previousSelectedOptions: Record<string, selectOptionsType[]>;
};

type snackbarTypes = {
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/src/store/types/IUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ export type UserStateTypes = {
userList: { results: userType[]; pagination: paginationType };
userListLoading: boolean;
updateUserRoleLoading: boolean;
userListForSelect: userType[];
userListForSelectLoading: boolean;
};

0 comments on commit 79c87f5

Please sign in to comment.