feat(ratecard): crud rename and ratecard-assignee-selector create
This commit is contained in:
@@ -9,7 +9,7 @@ export default class ProjectRateCardController extends WorklenzControllerBase {
|
|||||||
|
|
||||||
// Insert a single role for a project
|
// Insert a single role for a project
|
||||||
@HandleExceptions()
|
@HandleExceptions()
|
||||||
public static async insertOne(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
public static async createOne(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||||
const { project_id, job_title_id, rate } = req.body;
|
const { project_id, job_title_id, rate } = req.body;
|
||||||
if (!project_id || !job_title_id || typeof rate !== "number") {
|
if (!project_id || !job_title_id || typeof rate !== "number") {
|
||||||
return res.status(400).send(new ServerResponse(false, null, "Invalid input"));
|
return res.status(400).send(new ServerResponse(false, null, "Invalid input"));
|
||||||
@@ -26,7 +26,7 @@ public static async insertOne(req: IWorkLenzRequest, res: IWorkLenzResponse): Pr
|
|||||||
}
|
}
|
||||||
// Insert multiple roles for a project
|
// Insert multiple roles for a project
|
||||||
@HandleExceptions()
|
@HandleExceptions()
|
||||||
public static async insertMany(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
public static async createMany(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||||
const { project_id, roles } = req.body;
|
const { project_id, roles } = req.body;
|
||||||
if (!Array.isArray(roles) || !project_id) {
|
if (!Array.isArray(roles) || !project_id) {
|
||||||
return res.status(400).send(new ServerResponse(false, null, "Invalid input"));
|
return res.status(400).send(new ServerResponse(false, null, "Invalid input"));
|
||||||
@@ -50,10 +50,17 @@ public static async insertOne(req: IWorkLenzRequest, res: IWorkLenzResponse): Pr
|
|||||||
|
|
||||||
// Get all roles for a project
|
// Get all roles for a project
|
||||||
@HandleExceptions()
|
@HandleExceptions()
|
||||||
public static async getFromProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
public static async getByProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||||
const { project_id } = req.params;
|
const { project_id } = req.params;
|
||||||
const q = `
|
const q = `
|
||||||
SELECT fprr.*, jt.name as jobtitle
|
SELECT
|
||||||
|
fprr.*,
|
||||||
|
jt.name as jobtitle,
|
||||||
|
(
|
||||||
|
SELECT COALESCE(json_agg(pm.id), '[]'::json)
|
||||||
|
FROM project_members pm
|
||||||
|
WHERE pm.project_rate_card_role_id = fprr.id
|
||||||
|
) AS members
|
||||||
FROM finance_project_rate_card_roles fprr
|
FROM finance_project_rate_card_roles fprr
|
||||||
LEFT JOIN job_titles jt ON fprr.job_title_id = jt.id
|
LEFT JOIN job_titles jt ON fprr.job_title_id = jt.id
|
||||||
WHERE fprr.project_id = $1
|
WHERE fprr.project_id = $1
|
||||||
@@ -65,10 +72,17 @@ public static async insertOne(req: IWorkLenzRequest, res: IWorkLenzResponse): Pr
|
|||||||
|
|
||||||
// Get a single role by id
|
// Get a single role by id
|
||||||
@HandleExceptions()
|
@HandleExceptions()
|
||||||
public static async getFromId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const q = `
|
const q = `
|
||||||
SELECT fprr.*, jt.name as jobtitle
|
SELECT
|
||||||
|
fprr.*,
|
||||||
|
jt.name as jobtitle,
|
||||||
|
(
|
||||||
|
SELECT COALESCE(json_agg(pm.id), '[]'::json)
|
||||||
|
FROM project_members pm
|
||||||
|
WHERE pm.project_rate_card_role_id = fprr.id
|
||||||
|
) AS members
|
||||||
FROM finance_project_rate_card_roles fprr
|
FROM finance_project_rate_card_roles fprr
|
||||||
LEFT JOIN job_titles jt ON fprr.job_title_id = jt.id
|
LEFT JOIN job_titles jt ON fprr.job_title_id = jt.id
|
||||||
WHERE fprr.id = $1;
|
WHERE fprr.id = $1;
|
||||||
@@ -79,7 +93,7 @@ public static async insertOne(req: IWorkLenzRequest, res: IWorkLenzResponse): Pr
|
|||||||
|
|
||||||
// Update a single role by id
|
// Update a single role by id
|
||||||
@HandleExceptions()
|
@HandleExceptions()
|
||||||
public static async updateFromId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
public static async updateById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { job_title_id, rate } = req.body;
|
const { job_title_id, rate } = req.body;
|
||||||
const q = `
|
const q = `
|
||||||
@@ -94,7 +108,7 @@ public static async insertOne(req: IWorkLenzRequest, res: IWorkLenzResponse): Pr
|
|||||||
|
|
||||||
// Update all roles for a project (delete then insert)
|
// Update all roles for a project (delete then insert)
|
||||||
@HandleExceptions()
|
@HandleExceptions()
|
||||||
public static async updateFromProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
public static async updateByProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||||
const { project_id, roles } = req.body;
|
const { project_id, roles } = req.body;
|
||||||
if (!Array.isArray(roles) || !project_id) {
|
if (!Array.isArray(roles) || !project_id) {
|
||||||
return res.status(400).send(new ServerResponse(false, null, "Invalid input"));
|
return res.status(400).send(new ServerResponse(false, null, "Invalid input"));
|
||||||
@@ -123,7 +137,7 @@ public static async insertOne(req: IWorkLenzRequest, res: IWorkLenzResponse): Pr
|
|||||||
|
|
||||||
// Delete a single role by id
|
// Delete a single role by id
|
||||||
@HandleExceptions()
|
@HandleExceptions()
|
||||||
public static async deleteFromId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const q = `DELETE FROM finance_project_rate_card_roles WHERE id = $1 RETURNING *;`;
|
const q = `DELETE FROM finance_project_rate_card_roles WHERE id = $1 RETURNING *;`;
|
||||||
const result = await db.query(q, [id]);
|
const result = await db.query(q, [id]);
|
||||||
@@ -132,7 +146,7 @@ public static async insertOne(req: IWorkLenzRequest, res: IWorkLenzResponse): Pr
|
|||||||
|
|
||||||
// Delete all roles for a project
|
// Delete all roles for a project
|
||||||
@HandleExceptions()
|
@HandleExceptions()
|
||||||
public static async deleteFromProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
public static async deleteByProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||||
const { project_id } = req.params;
|
const { project_id } = req.params;
|
||||||
const q = `DELETE FROM finance_project_rate_card_roles WHERE project_id = $1 RETURNING *;`;
|
const q = `DELETE FROM finance_project_rate_card_roles WHERE project_id = $1 RETURNING *;`;
|
||||||
const result = await db.query(q, [project_id]);
|
const result = await db.query(q, [project_id]);
|
||||||
|
|||||||
@@ -10,52 +10,52 @@ const projectRatecardApiRouter = express.Router();
|
|||||||
projectRatecardApiRouter.post(
|
projectRatecardApiRouter.post(
|
||||||
"/",
|
"/",
|
||||||
projectManagerValidator,
|
projectManagerValidator,
|
||||||
safeControllerFunction(ProjectRateCardController.insertMany)
|
safeControllerFunction(ProjectRateCardController.createMany)
|
||||||
);
|
);
|
||||||
// Insert a single role for a project
|
// Insert a single role for a project
|
||||||
projectRatecardApiRouter.post(
|
projectRatecardApiRouter.post(
|
||||||
"/create-project-rate-card-role",
|
"/create-project-rate-card-role",
|
||||||
projectManagerValidator,
|
projectManagerValidator,
|
||||||
safeControllerFunction(ProjectRateCardController.insertOne)
|
safeControllerFunction(ProjectRateCardController.createOne)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get all roles for a project
|
// Get all roles for a project
|
||||||
projectRatecardApiRouter.get(
|
projectRatecardApiRouter.get(
|
||||||
"/project/:project_id",
|
"/project/:project_id",
|
||||||
safeControllerFunction(ProjectRateCardController.getFromProjectId)
|
safeControllerFunction(ProjectRateCardController.getByProjectId)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get a single role by id
|
// Get a single role by id
|
||||||
projectRatecardApiRouter.get(
|
projectRatecardApiRouter.get(
|
||||||
"/:id",
|
"/:id",
|
||||||
idParamValidator,
|
idParamValidator,
|
||||||
safeControllerFunction(ProjectRateCardController.getFromId)
|
safeControllerFunction(ProjectRateCardController.getById)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update a single role by id
|
// Update a single role by id
|
||||||
projectRatecardApiRouter.put(
|
projectRatecardApiRouter.put(
|
||||||
"/:id",
|
"/:id",
|
||||||
idParamValidator,
|
idParamValidator,
|
||||||
safeControllerFunction(ProjectRateCardController.updateFromId)
|
safeControllerFunction(ProjectRateCardController.updateById)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update all roles for a project (delete then insert)
|
// Update all roles for a project (delete then insert)
|
||||||
projectRatecardApiRouter.put(
|
projectRatecardApiRouter.put(
|
||||||
"/project/:project_id",
|
"/project/:project_id",
|
||||||
safeControllerFunction(ProjectRateCardController.updateFromProjectId)
|
safeControllerFunction(ProjectRateCardController.updateByProjectId)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Delete a single role by id
|
// Delete a single role by id
|
||||||
projectRatecardApiRouter.delete(
|
projectRatecardApiRouter.delete(
|
||||||
"/:id",
|
"/:id",
|
||||||
idParamValidator,
|
idParamValidator,
|
||||||
safeControllerFunction(ProjectRateCardController.deleteFromId)
|
safeControllerFunction(ProjectRateCardController.deleteById)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Delete all roles for a project
|
// Delete all roles for a project
|
||||||
projectRatecardApiRouter.delete(
|
projectRatecardApiRouter.delete(
|
||||||
"/project/:project_id",
|
"/project/:project_id",
|
||||||
safeControllerFunction(ProjectRateCardController.deleteFromProjectId)
|
safeControllerFunction(ProjectRateCardController.deleteByProjectId)
|
||||||
);
|
);
|
||||||
|
|
||||||
export default projectRatecardApiRouter;
|
export default projectRatecardApiRouter;
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { InputRef } from 'antd/es/input';
|
||||||
|
import Dropdown from 'antd/es/dropdown';
|
||||||
|
import Card from 'antd/es/card';
|
||||||
|
import List from 'antd/es/list';
|
||||||
|
import Input from 'antd/es/input';
|
||||||
|
import Checkbox from 'antd/es/checkbox';
|
||||||
|
import Button from 'antd/es/button';
|
||||||
|
import Empty from 'antd/es/empty';
|
||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import SingleAvatar from '../common/single-avatar/single-avatar';
|
||||||
|
import { JobRoleType } from '@/types/project/ratecard.types';
|
||||||
|
import { IProjectMembersViewModel, IProjectMemberViewModel } from '@/types/projectMember.types';
|
||||||
|
|
||||||
|
interface RateCardAssigneeSelectorProps {
|
||||||
|
projectId: string;
|
||||||
|
onChange?: (memberId: string) => void;
|
||||||
|
selectedMemberIds?: string[];
|
||||||
|
memberlist?: IProjectMemberViewModel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const RateCardAssigneeSelector = ({
|
||||||
|
projectId,
|
||||||
|
onChange,
|
||||||
|
selectedMemberIds = [],
|
||||||
|
memberlist,
|
||||||
|
}: RateCardAssigneeSelectorProps) => {
|
||||||
|
const membersInputRef = useRef<InputRef>(null);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [members, setMembers] = useState<IProjectMemberViewModel[]>(memberlist || []);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const filteredMembers = members.filter(member =>
|
||||||
|
member.name?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
const dropdownContent = (
|
||||||
|
<Card styles={{ body: { padding: 8 } }}>
|
||||||
|
<Input
|
||||||
|
ref={membersInputRef}
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={e => setSearchQuery(e.currentTarget.value)}
|
||||||
|
placeholder="Search members"
|
||||||
|
/>
|
||||||
|
<List style={{ padding: 0, overflow: 'auto' }}>
|
||||||
|
{filteredMembers.length ? (
|
||||||
|
filteredMembers.map(member => (
|
||||||
|
<List.Item
|
||||||
|
key={member.id}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 8,
|
||||||
|
alignItems: 'flex-start', // changed from 'l' to 'flex-start' for left alignment
|
||||||
|
padding: '4px 8px',
|
||||||
|
border: 'none',
|
||||||
|
cursor: member.pending_invitation ? 'not-allowed' : 'pointer',
|
||||||
|
opacity: member.pending_invitation ? 0.5 : 1,
|
||||||
|
justifyContent: 'flex-start', // ensure content is aligned left
|
||||||
|
textAlign: 'left', // ensure text is aligned left
|
||||||
|
}}
|
||||||
|
onClick={() => !member.pending_invitation && onChange?.(member.id || '')}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedMemberIds.includes(member.id || '')}
|
||||||
|
disabled={member.pending_invitation}
|
||||||
|
onChange={() => onChange?.(member.id || '')}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<SingleAvatar
|
||||||
|
avatarUrl={member.avatar_url}
|
||||||
|
name={member.name}
|
||||||
|
email={member.email}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span>{member.name}</span>
|
||||||
|
{/* <span style={{ fontSize: 12, color: '#888' }}>{member.email}</span> */}
|
||||||
|
</List.Item>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Empty />
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
overlayClassName="custom-dropdown"
|
||||||
|
trigger={['click']}
|
||||||
|
dropdownRender={() => dropdownContent}
|
||||||
|
onOpenChange={open => {
|
||||||
|
if (open) setTimeout(() => membersInputRef.current?.focus(), 0);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
shape="circle"
|
||||||
|
size="small"
|
||||||
|
icon={<PlusOutlined style={{ fontSize: 12 }} />}
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RateCardAssigneeSelector;
|
||||||
@@ -18,7 +18,7 @@ const initialState: financeState = {
|
|||||||
isRatecardDrawerOpen: false,
|
isRatecardDrawerOpen: false,
|
||||||
isFinanceDrawerOpen: false,
|
isFinanceDrawerOpen: false,
|
||||||
isImportRatecardsDrawerOpen: false,
|
isImportRatecardsDrawerOpen: false,
|
||||||
currency: 'LKR',
|
currency: 'USD',
|
||||||
isRatecardsLoading: false,
|
isRatecardsLoading: false,
|
||||||
isFinanceDrawerloading: false,
|
isFinanceDrawerloading: false,
|
||||||
drawerRatecard: null,
|
drawerRatecard: null,
|
||||||
|
|||||||
@@ -14,8 +14,13 @@ import {
|
|||||||
} from '@/features/finance/project-finance-slice';
|
} from '@/features/finance/project-finance-slice';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { jobTitlesApiService } from '@/api/settings/job-titles/job-titles.api.service';
|
import { jobTitlesApiService } from '@/api/settings/job-titles/job-titles.api.service';
|
||||||
|
import RateCardAssigneeSelector from '@/components/project-ratecard/ratecard-assignee-selector';
|
||||||
|
import { projectsApiService } from '@/api/projects/projects.api.service';
|
||||||
|
import { IProjectMemberViewModel } from '@/types/projectMember.types';
|
||||||
|
|
||||||
|
|
||||||
const RatecardTable: React.FC = () => {
|
const RatecardTable: React.FC = () => {
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { t } = useTranslation('project-view-finance');
|
const { t } = useTranslation('project-view-finance');
|
||||||
const { projectId } = useParams();
|
const { projectId } = useParams();
|
||||||
@@ -30,6 +35,40 @@ const RatecardTable: React.FC = () => {
|
|||||||
const [addingRow, setAddingRow] = useState<boolean>(false);
|
const [addingRow, setAddingRow] = useState<boolean>(false);
|
||||||
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
||||||
const [jobTitles, setJobTitles] = useState<RatecardType[]>([]);
|
const [jobTitles, setJobTitles] = useState<RatecardType[]>([]);
|
||||||
|
const [members, setMembers] = useState<IProjectMemberViewModel[]>([]);
|
||||||
|
const [isLoadingMembers, setIsLoading] = useState(false);
|
||||||
|
const pagination = {
|
||||||
|
current: 1,
|
||||||
|
pageSize: 100,
|
||||||
|
field: 'name',
|
||||||
|
order: 'asc',
|
||||||
|
};
|
||||||
|
const getProjectMembers = async () => {
|
||||||
|
if (!projectId) return;
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await projectsApiService.getMembers(
|
||||||
|
projectId,
|
||||||
|
pagination.current,
|
||||||
|
pagination.pageSize,
|
||||||
|
pagination.field,
|
||||||
|
pagination.order,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
if (res.done) {
|
||||||
|
setMembers(res.body?.data || []);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching members:', error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getProjectMembers();
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Fetch job titles for selection
|
// Fetch job titles for selection
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -72,7 +111,7 @@ const RatecardTable: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Handle job title select for new row
|
// Handle job title select for new row
|
||||||
const handleSelectJobTitle = async(jobTitleId: string) => {
|
const handleSelectJobTitle = async (jobTitleId: string) => {
|
||||||
const jobTitle = jobTitles.find((jt) => jt.id === jobTitleId);
|
const jobTitle = jobTitles.find((jt) => jt.id === jobTitleId);
|
||||||
if (!jobTitle || !projectId) return;
|
if (!jobTitle || !projectId) return;
|
||||||
// Prevent duplicates
|
// Prevent duplicates
|
||||||
@@ -180,29 +219,23 @@ const RatecardTable: React.FC = () => {
|
|||||||
{
|
{
|
||||||
title: t('membersColumn'),
|
title: t('membersColumn'),
|
||||||
dataIndex: 'members',
|
dataIndex: 'members',
|
||||||
render: (members: string[] | null | undefined) =>
|
render: (memberscol: string[] | null | undefined, record: JobRoleType, index: number) => (
|
||||||
members && members.length > 0 ? (
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4, position: 'relative' }}>
|
||||||
<Avatar.Group>
|
<Avatar.Group>
|
||||||
{members.map((member, i) => (
|
{memberscol && memberscol.length > 0 &&
|
||||||
|
memberscol.map((member, i) => (
|
||||||
<CustomAvatar key={i} avatarName={member} size={26} />
|
<CustomAvatar key={i} avatarName={member} size={26} />
|
||||||
))}
|
))}
|
||||||
</Avatar.Group>
|
</Avatar.Group>
|
||||||
) : (
|
<div>
|
||||||
<Button
|
<RateCardAssigneeSelector
|
||||||
shape="circle"
|
projectId={projectId as string}
|
||||||
icon={
|
selectedMemberIds={memberscol || []}
|
||||||
<PlusOutlined
|
onChange={memberId => handleMemberChange(memberId, index)}
|
||||||
style={{
|
memberlist={members}
|
||||||
fontSize: 12,
|
|
||||||
width: 22,
|
|
||||||
height: 22,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -225,6 +258,22 @@ const RatecardTable: React.FC = () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const handleMemberChange = (memberId: string, rowIndex: number) => {
|
||||||
|
setRoles(prev =>
|
||||||
|
prev.map((role, idx) => {
|
||||||
|
if (idx !== rowIndex) return role;
|
||||||
|
const members = Array.isArray(role.members) ? [...role.members] : [];
|
||||||
|
const memberIdx = members.indexOf(memberId);
|
||||||
|
if (memberIdx > -1) {
|
||||||
|
members.splice(memberIdx, 1); // remove if exists
|
||||||
|
} else {
|
||||||
|
members.push(memberId); // add if not exists
|
||||||
|
}
|
||||||
|
return { ...role, members };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table
|
<Table
|
||||||
dataSource={
|
dataSource={
|
||||||
|
|||||||
Reference in New Issue
Block a user