mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-04 17:26:19 +00:00
feat: adds group projects table
This commit is contained in:
@@ -5,4 +5,4 @@ export {
|
||||
useRemoveUserFromGroup,
|
||||
useUpdateGroup
|
||||
} from "./mutations";
|
||||
export { useGetGroupById, useListGroupUsers } from "./queries";
|
||||
export { useGetGroupById, useListGroupProjects, useListGroupUsers } from "./queries";
|
||||
|
||||
@@ -2,7 +2,13 @@ import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { EFilterReturnedUsers, TGroup, TGroupUser } from "./types";
|
||||
import {
|
||||
EFilterReturnedProjects,
|
||||
EFilterReturnedUsers,
|
||||
TGroup,
|
||||
TGroupProject,
|
||||
TGroupUser
|
||||
} from "./types";
|
||||
|
||||
export const groupKeys = {
|
||||
getGroupById: (groupId: string) => [{ groupId }, "group"] as const,
|
||||
@@ -41,7 +47,22 @@ export const groupKeys = {
|
||||
...groupKeys.forGroupUserMemberships(slug),
|
||||
projectId,
|
||||
{ offset, limit, search, filter }
|
||||
] as const
|
||||
] as const,
|
||||
allGroupProjects: () => ["group-projects"] as const,
|
||||
forGroupProjects: (groupId: string) => [...groupKeys.allGroupProjects(), groupId] as const,
|
||||
specificGroupProjects: ({
|
||||
groupId,
|
||||
offset,
|
||||
limit,
|
||||
search,
|
||||
filter
|
||||
}: {
|
||||
groupId: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
search: string;
|
||||
filter?: EFilterReturnedProjects;
|
||||
}) => [...groupKeys.forGroupProjects(groupId), { offset, limit, search, filter }] as const
|
||||
};
|
||||
|
||||
export const useGetGroupById = (groupId: string) => {
|
||||
@@ -148,3 +169,46 @@ export const useListProjectGroupUsers = ({
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useListGroupProjects = ({
|
||||
id,
|
||||
offset = 0,
|
||||
limit = 10,
|
||||
search,
|
||||
filter
|
||||
}: {
|
||||
id: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
search: string;
|
||||
filter?: EFilterReturnedProjects;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: groupKeys.specificGroupProjects({
|
||||
groupId: id,
|
||||
offset,
|
||||
limit,
|
||||
search,
|
||||
filter
|
||||
}),
|
||||
enabled: Boolean(id),
|
||||
placeholderData: (previousData) => previousData,
|
||||
queryFn: async () => {
|
||||
const params = new URLSearchParams({
|
||||
offset: String(offset),
|
||||
limit: String(limit),
|
||||
search,
|
||||
...(filter && { filter })
|
||||
});
|
||||
|
||||
const { data } = await apiRequest.get<{ projects: TGroupProject[]; totalCount: number }>(
|
||||
`/api/v1/groups/${id}/projects`,
|
||||
{
|
||||
params
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -52,7 +52,21 @@ export type TGroupUser = {
|
||||
joinedGroupAt: Date;
|
||||
};
|
||||
|
||||
export type TGroupProject = {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
type: string;
|
||||
joinedGroupAt: Date;
|
||||
};
|
||||
|
||||
export enum EFilterReturnedUsers {
|
||||
EXISTING_MEMBERS = "existingMembers",
|
||||
NON_MEMBERS = "nonMembers"
|
||||
}
|
||||
|
||||
export enum EFilterReturnedProjects {
|
||||
ASSIGNED_PROJECTS = "assignedProjects",
|
||||
UNASSIGNED_PROJECTS = "unassignedProjects"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { groupKeys } from "../groups/queries";
|
||||
import { userKeys } from "../users/query-keys";
|
||||
import { projectKeys } from "./query-keys";
|
||||
import {
|
||||
@@ -30,10 +31,11 @@ export const useAddGroupToWorkspace = () => {
|
||||
|
||||
return groupMembership;
|
||||
},
|
||||
onSuccess: (_, { projectId }) => {
|
||||
onSuccess: (_, { projectId, groupId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: projectKeys.getProjectGroupMemberships(projectId)
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: groupKeys.forGroupProjects(groupId) });
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -77,11 +79,13 @@ export const useDeleteGroupFromWorkspace = () => {
|
||||
} = await apiRequest.delete(`/api/v1/projects/${projectId}/groups/${groupId}`);
|
||||
return groupMembership;
|
||||
},
|
||||
onSuccess: (_, { projectId, username }) => {
|
||||
onSuccess: (_, { projectId, username, groupId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: projectKeys.getProjectGroupMemberships(projectId)
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: groupKeys.forGroupProjects(groupId) });
|
||||
|
||||
if (username) {
|
||||
queryClient.invalidateQueries({ queryKey: userKeys.listUserGroupMemberships(username) });
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import { usePopUp } from "@app/hooks/usePopUp";
|
||||
import { GroupCreateUpdateModal } from "./components/GroupCreateUpdateModal";
|
||||
import { GroupDetailsSection } from "./components/GroupDetailsSection";
|
||||
import { GroupMembersSection } from "./components/GroupMembersSection";
|
||||
import { GroupProjectsSection } from "./components/GroupProjectsSection";
|
||||
|
||||
export enum TabSections {
|
||||
Member = "members",
|
||||
@@ -154,7 +155,10 @@ const Page = () => {
|
||||
<div className="w-full md:w-96">
|
||||
<GroupDetailsSection groupId={groupId} handlePopUpOpen={handlePopUpOpen} />
|
||||
</div>
|
||||
<GroupMembersSection groupId={groupId} groupSlug={data.group.slug} />
|
||||
<div className="flex grow flex-col gap-4">
|
||||
<GroupMembersSection groupId={groupId} groupSlug={data.group.slug} />
|
||||
<GroupProjectsSection groupId={groupId} groupSlug={data.group.slug} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
import { useState } from "react";
|
||||
import { faFolder, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
EmptyState,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Pagination,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionGroupActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { useDebounce, useResetPageHelper } from "@app/hooks";
|
||||
import {
|
||||
useAddGroupToWorkspace as useAddProjectToGroup,
|
||||
useListGroupProjects
|
||||
} from "@app/hooks/api";
|
||||
import { EFilterReturnedProjects } from "@app/hooks/api/groups/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["addGroupProjects"]>;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["addGroupProjects"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const AddGroupProjectModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const [page, setPage] = useState(1);
|
||||
const [perPage, setPerPage] = useState(10);
|
||||
const [searchProjectFilter, setSearchProjectFilter] = useState("");
|
||||
const [debouncedSearch] = useDebounce(searchProjectFilter);
|
||||
|
||||
const popUpData = popUp?.addGroupProjects?.data as {
|
||||
groupId: string;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
const offset = (page - 1) * perPage;
|
||||
|
||||
const { data, isPending } = useListGroupProjects({
|
||||
id: popUpData?.groupId,
|
||||
offset,
|
||||
limit: perPage,
|
||||
search: debouncedSearch,
|
||||
filter: EFilterReturnedProjects.UNASSIGNED_PROJECTS
|
||||
});
|
||||
|
||||
const { totalCount = 0 } = data ?? {};
|
||||
|
||||
useResetPageHelper({
|
||||
totalCount,
|
||||
offset,
|
||||
setPage
|
||||
});
|
||||
|
||||
const { mutateAsync: addProjectToGroupMutateAsync, isPending: isAdding } = useAddProjectToGroup();
|
||||
|
||||
const handleAddProject = async (projectId: string, projectName: string) => {
|
||||
if (!popUpData?.groupId) {
|
||||
createNotification({
|
||||
text: "Some data is missing, please refresh the page and try again",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await addProjectToGroupMutateAsync({
|
||||
groupId: popUpData.groupId,
|
||||
projectId
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: `Successfully assigned project ${projectName} to the group`,
|
||||
type: "success"
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.addGroupProjects?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("addGroupProjects", isOpen);
|
||||
}}
|
||||
>
|
||||
<ModalContent title="Add Group Projects">
|
||||
<Input
|
||||
value={searchProjectFilter}
|
||||
onChange={(e) => setSearchProjectFilter(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search projects..."
|
||||
/>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Project</Th>
|
||||
<Th>Type</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPending && <TableSkeleton columns={3} innerKey="group-projects" />}
|
||||
{!isPending &&
|
||||
data?.projects?.map((project) => {
|
||||
return (
|
||||
<Tr className="items-center" key={`group-project-${project.id}`}>
|
||||
<Td>
|
||||
<p>{project.name}</p>
|
||||
{project.description && (
|
||||
<p className="text-sm text-mineshaft-400">{project.description}</p>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<p className="capitalize">{project.type?.replace("-", " ") || "-"}</p>
|
||||
</Td>
|
||||
<Td className="flex justify-end">
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionGroupActions.Edit}
|
||||
a={OrgPermissionSubjects.Groups}
|
||||
>
|
||||
{(isAllowed) => {
|
||||
return (
|
||||
<Button
|
||||
isLoading={isAdding}
|
||||
isDisabled={!isAllowed}
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
onClick={() => handleAddProject(project.id, project.name)}
|
||||
>
|
||||
Assign
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
</OrgPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isPending && totalCount > 0 && (
|
||||
<Pagination
|
||||
count={totalCount}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={(newPage) => setPage(newPage)}
|
||||
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
||||
/>
|
||||
)}
|
||||
{!isPending && !data?.projects?.length && (
|
||||
<EmptyState
|
||||
title={
|
||||
debouncedSearch
|
||||
? "No projects match search"
|
||||
: "All projects are already assigned to the group"
|
||||
}
|
||||
icon={faFolder}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import { faEllipsisV, faFolderMinus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
IconButton,
|
||||
Td,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionGroupActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { TGroupProject } from "@app/hooks/api/groups/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
project: TGroupProject;
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["removeProjectFromGroup"]>,
|
||||
data?: object
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const GroupProjectRow = ({ project, handlePopUpOpen }: Props) => {
|
||||
return (
|
||||
<Tr className="items-center" key={`group-project-${project.id}`}>
|
||||
<Td>
|
||||
<p>{project.name}</p>
|
||||
</Td>
|
||||
<Td>
|
||||
<p className="capitalize">{project.type?.replace("-", " ") || "-"}</p>
|
||||
</Td>
|
||||
<Td>
|
||||
<Tooltip content={new Date(project.joinedGroupAt).toLocaleString()}>
|
||||
<p>{new Date(project.joinedGroupAt).toLocaleDateString()}</p>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
<Td>
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<OrgPermissionCan I={OrgPermissionGroupActions.Edit} a={OrgPermissionSubjects.Groups}>
|
||||
{(isAllowed) => {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faFolderMinus} />}
|
||||
onClick={() =>
|
||||
handlePopUpOpen("removeProjectFromGroup", {
|
||||
projectId: project.id,
|
||||
projectName: project.name
|
||||
})
|
||||
}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Remove Project From Group
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
}}
|
||||
</OrgPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { DeleteActionModal, IconButton } from "@app/components/v2";
|
||||
import { OrgPermissionGroupActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { useDeleteGroupFromWorkspace as useRemoveProjectFromGroup } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { AddGroupProjectModal } from "../AddGroupProjectModal";
|
||||
import { GroupProjectsTable } from "./GroupProjectsTable";
|
||||
|
||||
type Props = {
|
||||
groupId: string;
|
||||
groupSlug: string;
|
||||
};
|
||||
|
||||
export const GroupProjectsSection = ({ groupId, groupSlug }: Props) => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||
"addGroupProjects",
|
||||
"removeProjectFromGroup"
|
||||
] as const);
|
||||
|
||||
const { mutateAsync: removeProjectFromGroupMutateAsync } = useRemoveProjectFromGroup();
|
||||
|
||||
const handleRemoveProjectFromGroup = async (projectId: string, projectName: string) => {
|
||||
await removeProjectFromGroupMutateAsync({
|
||||
groupId,
|
||||
projectId
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: `Successfully removed project ${projectName} from the group`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpToggle("removeProjectFromGroup", false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">Group Projects</h3>
|
||||
<OrgPermissionCan I={OrgPermissionGroupActions.Edit} a={OrgPermissionSubjects.Groups}>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
isDisabled={!isAllowed}
|
||||
ariaLabel="add project"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("addGroupProjects", {
|
||||
groupId,
|
||||
slug: groupSlug
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</IconButton>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<div className="py-4">
|
||||
<GroupProjectsTable
|
||||
groupId={groupId}
|
||||
groupSlug={groupSlug}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
/>
|
||||
</div>
|
||||
<AddGroupProjectModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeProjectFromGroup.isOpen}
|
||||
title={`Are you sure you want to remove ${
|
||||
(popUp?.removeProjectFromGroup?.data as { projectName: string })?.projectName || ""
|
||||
} from the group?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("removeProjectFromGroup", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() => {
|
||||
const projectData = popUp?.removeProjectFromGroup?.data as {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
};
|
||||
|
||||
return handleRemoveProjectFromGroup(projectData.projectId, projectData.projectName);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faFolder,
|
||||
faMagnifyingGlass,
|
||||
faSearch
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Pagination,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionGroupActions, OrgPermissionSubjects } from "@app/context";
|
||||
import {
|
||||
getUserTablePreference,
|
||||
PreferenceKey,
|
||||
setUserTablePreference
|
||||
} from "@app/helpers/userTablePreferences";
|
||||
import { usePagination, useResetPageHelper } from "@app/hooks";
|
||||
import { useListGroupProjects } from "@app/hooks/api";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { EFilterReturnedProjects } from "@app/hooks/api/groups/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { GroupProjectRow } from "./GroupProjectRow";
|
||||
|
||||
type Props = {
|
||||
groupId: string;
|
||||
groupSlug: string;
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["removeProjectFromGroup", "addGroupProjects"]>,
|
||||
data?: object
|
||||
) => void;
|
||||
};
|
||||
|
||||
enum GroupProjectsOrderBy {
|
||||
Name = "name"
|
||||
}
|
||||
|
||||
export const GroupProjectsTable = ({ groupId, groupSlug, handlePopUpOpen }: Props) => {
|
||||
const {
|
||||
search,
|
||||
debouncedSearch,
|
||||
setSearch,
|
||||
setPage,
|
||||
page,
|
||||
perPage,
|
||||
setPerPage,
|
||||
offset,
|
||||
orderDirection,
|
||||
toggleOrderDirection
|
||||
} = usePagination(GroupProjectsOrderBy.Name, {
|
||||
initPerPage: getUserTablePreference("groupProjectsTable", PreferenceKey.PerPage, 20)
|
||||
});
|
||||
|
||||
const handlePerPageChange = (newPerPage: number) => {
|
||||
setPerPage(newPerPage);
|
||||
setUserTablePreference("groupProjectsTable", PreferenceKey.PerPage, newPerPage);
|
||||
};
|
||||
|
||||
const { data: groupMemberships, isPending } = useListGroupProjects({
|
||||
id: groupId,
|
||||
offset,
|
||||
limit: perPage,
|
||||
search: debouncedSearch,
|
||||
filter: EFilterReturnedProjects.ASSIGNED_PROJECTS
|
||||
});
|
||||
|
||||
const filteredGroupProjects = useMemo(() => {
|
||||
return groupMemberships && groupMemberships?.projects
|
||||
? groupMemberships?.projects
|
||||
?.filter((project) => {
|
||||
const projectSearchString = `${project.name || ""} ${project.slug || ""} ${
|
||||
project.description || ""
|
||||
}`;
|
||||
return projectSearchString.toLowerCase().includes(search.trim().toLowerCase());
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const [projectOne, projectTwo] =
|
||||
orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
|
||||
|
||||
const projectOneComparisonString = projectOne.name || "";
|
||||
|
||||
const projectTwoComparisonString = projectTwo.name || "";
|
||||
|
||||
const comparison = projectOneComparisonString
|
||||
.toLowerCase()
|
||||
.localeCompare(projectTwoComparisonString.toLowerCase());
|
||||
|
||||
return comparison;
|
||||
})
|
||||
: [];
|
||||
}, [groupMemberships, orderDirection, search]);
|
||||
|
||||
useResetPageHelper({
|
||||
totalCount: filteredGroupProjects?.length,
|
||||
offset,
|
||||
setPage
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search projects..."
|
||||
/>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-1/3">
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className="ml-2"
|
||||
ariaLabel="sort"
|
||||
onClick={toggleOrderDirection}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={orderDirection === OrderByDirection.DESC ? faArrowUp : faArrowDown}
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>Type</Th>
|
||||
<Th>Added On</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPending && <TableSkeleton columns={4} innerKey="group-project-memberships" />}
|
||||
{!isPending &&
|
||||
filteredGroupProjects.slice(offset, perPage * page).map((project) => {
|
||||
return (
|
||||
<GroupProjectRow
|
||||
key={`group-project-${project.id}`}
|
||||
project={project}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{Boolean(filteredGroupProjects.length) && (
|
||||
<Pagination
|
||||
count={filteredGroupProjects.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
{!isPending && !filteredGroupProjects?.length && (
|
||||
<EmptyState
|
||||
title={
|
||||
groupMemberships?.projects.length
|
||||
? "No projects match this search..."
|
||||
: "This group does not have any projects assigned yet"
|
||||
}
|
||||
icon={groupMemberships?.projects.length ? faSearch : faFolder}
|
||||
/>
|
||||
)}
|
||||
{!groupMemberships?.projects.length && (
|
||||
<OrgPermissionCan I={OrgPermissionGroupActions.Edit} a={OrgPermissionSubjects.Groups}>
|
||||
{(isAllowed) => (
|
||||
<div className="mb-4 flex items-center justify-center">
|
||||
<Button
|
||||
variant="solid"
|
||||
colorSchema="secondary"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("addGroupProjects", {
|
||||
groupId,
|
||||
slug: groupSlug
|
||||
});
|
||||
}}
|
||||
>
|
||||
Add projects
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { GroupProjectsSection } from "./GroupProjectsSection";
|
||||
@@ -1 +1,2 @@
|
||||
export { GroupDetailsSection } from "./GroupDetailsSection";
|
||||
export { GroupProjectsSection } from "./GroupProjectsSection";
|
||||
|
||||
Reference in New Issue
Block a user