Skip to content

Commit 586a7a4

Browse files
[WEB-4896]feat: filters to project and workspace members list (#7786)
1 parent 85bffaa commit 586a7a4

20 files changed

Lines changed: 914 additions & 80 deletions

File tree

apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { cn } from "@plane/utils";
2020
import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view";
2121
import { CountChip } from "@/components/common/count-chip";
2222
import { PageHead } from "@/components/core/page-title";
23+
import { MemberListFiltersDropdown } from "@/components/project/dropdowns/filters/member-list";
2324
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
2425
import { WorkspaceMembersList } from "@/components/workspace/settings/members-list";
2526
// helpers
@@ -41,7 +42,7 @@ const WorkspaceMembersSettingsPage = observer(() => {
4142
// store hooks
4243
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
4344
const {
44-
workspace: { workspaceMemberIds, inviteMembersToWorkspace },
45+
workspace: { workspaceMemberIds, inviteMembersToWorkspace, filtersStore },
4546
} = useMember();
4647
const { currentWorkspace } = useWorkspace();
4748
const { t } = useTranslation();
@@ -88,8 +89,20 @@ const WorkspaceMembersSettingsPage = observer(() => {
8889
});
8990
};
9091

92+
// Handler for role filter updates
93+
const handleRoleFilterUpdate = (role: string) => {
94+
const currentFilters = filtersStore.filters;
95+
const currentRoles = currentFilters?.roles || [];
96+
const updatedRoles = currentRoles.includes(role) ? currentRoles.filter((r) => r !== role) : [...currentRoles, role];
97+
98+
filtersStore.updateFilters({
99+
roles: updatedRoles.length > 0 ? updatedRoles : undefined,
100+
});
101+
};
102+
91103
// derived values
92104
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Members` : undefined;
105+
const appliedRoleFilters = filtersStore.filters?.roles || [];
93106

94107
// if user is not authorized to view this page
95108
if (workspaceUserInfo && !canPerformWorkspaceMemberActions) {
@@ -116,27 +129,34 @@ const WorkspaceMembersSettingsPage = observer(() => {
116129
<CountChip count={workspaceMemberIds.length} className="h-5 m-auto" />
117130
)}
118131
</h4>
119-
<div className="ml-auto flex items-center gap-1.5 rounded-md border border-custom-border-200 bg-custom-background-100 px-2.5 py-1.5">
120-
<Search className="h-3.5 w-3.5 text-custom-text-400" />
121-
<input
122-
className="w-full max-w-[234px] border-none bg-transparent text-sm outline-none placeholder:text-custom-text-400"
123-
placeholder={`${t("search")}...`}
124-
value={searchQuery}
125-
autoFocus
126-
onChange={(e) => setSearchQuery(e.target.value)}
132+
<div className="flex items-center gap-2">
133+
<div className="flex items-center gap-1.5 rounded-md border border-custom-border-200 bg-custom-background-100 px-2.5 py-1.5">
134+
<Search className="h-3.5 w-3.5 text-custom-text-400" />
135+
<input
136+
className="w-full max-w-[234px] border-none bg-transparent text-sm outline-none placeholder:text-custom-text-400"
137+
placeholder={`${t("search")}...`}
138+
value={searchQuery}
139+
autoFocus
140+
onChange={(e) => setSearchQuery(e.target.value)}
141+
/>
142+
</div>
143+
<MemberListFiltersDropdown
144+
appliedFilters={appliedRoleFilters}
145+
handleUpdate={handleRoleFilterUpdate}
146+
memberType="workspace"
127147
/>
148+
{canPerformWorkspaceAdminActions && (
149+
<Button
150+
variant="primary"
151+
size="sm"
152+
onClick={() => setInviteModal(true)}
153+
data-ph-element={MEMBER_TRACKER_ELEMENTS.HEADER_ADD_BUTTON}
154+
>
155+
{t("workspace_settings.settings.members.add_member")}
156+
</Button>
157+
)}
158+
<BillingActionsButton canPerformWorkspaceAdminActions={canPerformWorkspaceAdminActions} />
128159
</div>
129-
{canPerformWorkspaceAdminActions && (
130-
<Button
131-
variant="primary"
132-
size="sm"
133-
onClick={() => setInviteModal(true)}
134-
data-ph-element={MEMBER_TRACKER_ELEMENTS.HEADER_ADD_BUTTON}
135-
>
136-
{t("workspace_settings.settings.members.add_member")}
137-
</Button>
138-
)}
139-
<BillingActionsButton canPerformWorkspaceAdminActions={canPerformWorkspaceAdminActions} />
140160
</div>
141161
<WorkspaceMembersList searchQuery={searchQuery} isAdmin={canPerformWorkspaceAdminActions} />
142162
</section>

apps/web/ce/components/projects/settings/useProjectColumns.tsx

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ import { useState } from "react";
22
// plane imports
33
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
44
import { IWorkspaceMember, TProjectMembership } from "@plane/types";
5+
import { renderFormattedDate } from "@plane/utils";
56
// components
7+
import { MemberHeaderColumn } from "@/components/project/member-header-column";
68
import { AccountTypeColumn, NameColumn } from "@/components/project/settings/member-columns";
79
// hooks
10+
import { useMember } from "@/hooks/store/use-member";
811
import { useUser, useUserPermissions } from "@/hooks/store/user";
12+
import { IMemberFilters } from "@/store/member/utils";
913

1014
export interface RowData extends Pick<TProjectMembership, "original_role"> {
1115
member: IWorkspaceMember;
@@ -20,9 +24,15 @@ export const useProjectColumns = (props: TUseProjectColumnsProps) => {
2024
const { projectId, workspaceSlug } = props;
2125
// states
2226
const [removeMemberModal, setRemoveMemberModal] = useState<RowData | null>(null);
27+
2328
// store hooks
2429
const { data: currentUser } = useUser();
2530
const { allowPermissions, getProjectRoleByWorkspaceSlugAndProjectId } = useUserPermissions();
31+
const {
32+
project: {
33+
filters: { getFilters, updateFilters },
34+
},
35+
} = useMember();
2636
// derived values
2737
const isAdmin = allowPermissions(
2838
[EUserPermissions.ADMIN],
@@ -33,18 +43,25 @@ export const useProjectColumns = (props: TUseProjectColumnsProps) => {
3343
const currentProjectRole =
3444
getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug.toString(), projectId.toString()) ?? EUserPermissions.GUEST;
3545

36-
const getFormattedDate = (dateStr: string) => {
37-
const date = new Date(dateStr);
46+
const displayFilters = getFilters(projectId);
3847

39-
const options: Intl.DateTimeFormatOptions = { year: "numeric", month: "long", day: "numeric" };
40-
return date.toLocaleDateString("en-US", options);
48+
// handlers
49+
const handleDisplayFilterUpdate = (filters: Partial<IMemberFilters>) => {
50+
updateFilters(projectId, filters);
4151
};
4252

4353
const columns = [
4454
{
4555
key: "Full Name",
4656
content: "Full name",
4757
thClassName: "text-left",
58+
thRender: () => (
59+
<MemberHeaderColumn
60+
property="full_name"
61+
displayFilters={displayFilters}
62+
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
63+
/>
64+
),
4865
tdRender: (rowData: RowData) => (
4966
<NameColumn
5067
rowData={rowData}
@@ -58,12 +75,37 @@ export const useProjectColumns = (props: TUseProjectColumnsProps) => {
5875
{
5976
key: "Display Name",
6077
content: "Display name",
78+
thRender: () => (
79+
<MemberHeaderColumn
80+
property="display_name"
81+
displayFilters={displayFilters}
82+
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
83+
/>
84+
),
6185
tdRender: (rowData: RowData) => <div className="w-32">{rowData.member.display_name}</div>,
6286
},
63-
87+
{
88+
key: "Email",
89+
content: "Email",
90+
thRender: () => (
91+
<MemberHeaderColumn
92+
property="email"
93+
displayFilters={displayFilters}
94+
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
95+
/>
96+
),
97+
tdRender: (rowData: RowData) => <div className="w-48 text-custom-text-200">{rowData.member.email}</div>,
98+
},
6499
{
65100
key: "Account Type",
66101
content: "Account type",
102+
thRender: () => (
103+
<MemberHeaderColumn
104+
property="role"
105+
displayFilters={displayFilters}
106+
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
107+
/>
108+
),
67109
tdRender: (rowData: RowData) => (
68110
<AccountTypeColumn
69111
rowData={rowData}
@@ -76,8 +118,21 @@ export const useProjectColumns = (props: TUseProjectColumnsProps) => {
76118
{
77119
key: "Joining Date",
78120
content: "Joining date",
79-
tdRender: (rowData: RowData) => <div>{getFormattedDate(rowData?.member?.joining_date || "")}</div>,
121+
thRender: () => (
122+
<MemberHeaderColumn
123+
property="joining_date"
124+
displayFilters={displayFilters}
125+
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
126+
/>
127+
),
128+
tdRender: (rowData: RowData) => <div>{renderFormattedDate(rowData?.member?.joining_date)}</div>,
80129
},
81130
];
82-
return { columns, removeMemberModal, setRemoveMemberModal };
131+
return {
132+
columns,
133+
removeMemberModal,
134+
setRemoveMemberModal,
135+
displayFilters,
136+
handleDisplayFilterUpdate,
137+
};
83138
};

apps/web/ce/components/workspace/settings/useMemberColumns.tsx

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@ import { useState } from "react";
22
import { useParams } from "next/navigation";
33
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
44
import { useTranslation } from "@plane/i18n";
5+
import { renderFormattedDate } from "@plane/utils";
6+
import { MemberHeaderColumn } from "@/components/project/member-header-column";
57
import { AccountTypeColumn, NameColumn, RowData } from "@/components/workspace/settings/member-columns";
8+
import { useMember } from "@/hooks/store/use-member";
69
import { useUser, useUserPermissions } from "@/hooks/store/user";
10+
import { IMemberFilters } from "@/store/member/utils";
711

812
export const useMemberColumns = () => {
913
// states
@@ -13,23 +17,33 @@ export const useMemberColumns = () => {
1317

1418
const { data: currentUser } = useUser();
1519
const { allowPermissions } = useUserPermissions();
20+
const {
21+
workspace: {
22+
filtersStore: { filters, updateFilters },
23+
},
24+
} = useMember();
1625
const { t } = useTranslation();
1726

18-
const getFormattedDate = (dateStr: string) => {
19-
const date = new Date(dateStr);
20-
21-
const options: Intl.DateTimeFormatOptions = { year: "numeric", month: "long", day: "numeric" };
22-
return date.toLocaleDateString("en-US", options);
23-
};
24-
2527
// derived values
2628
const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
2729

30+
// handlers
31+
const handleDisplayFilterUpdate = (filterUpdates: Partial<IMemberFilters>) => {
32+
updateFilters(filterUpdates);
33+
};
34+
2835
const columns = [
2936
{
3037
key: "Full name",
3138
content: t("workspace_settings.settings.members.details.full_name"),
3239
thClassName: "text-left",
40+
thRender: () => (
41+
<MemberHeaderColumn
42+
property="full_name"
43+
displayFilters={filters}
44+
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
45+
/>
46+
),
3347
tdRender: (rowData: RowData) => (
3448
<NameColumn
3549
rowData={rowData}
@@ -44,18 +58,39 @@ export const useMemberColumns = () => {
4458
{
4559
key: "Display name",
4660
content: t("workspace_settings.settings.members.details.display_name"),
61+
thRender: () => (
62+
<MemberHeaderColumn
63+
property="display_name"
64+
displayFilters={filters}
65+
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
66+
/>
67+
),
4768
tdRender: (rowData: RowData) => <div className="w-32">{rowData.member.display_name}</div>,
4869
},
4970

5071
{
5172
key: "Email address",
5273
content: t("workspace_settings.settings.members.details.email_address"),
74+
thRender: () => (
75+
<MemberHeaderColumn
76+
property="email"
77+
displayFilters={filters}
78+
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
79+
/>
80+
),
5381
tdRender: (rowData: RowData) => <div className="w-48 truncate">{rowData.member.email}</div>,
5482
},
5583

5684
{
5785
key: "Account type",
5886
content: t("workspace_settings.settings.members.details.account_type"),
87+
thRender: () => (
88+
<MemberHeaderColumn
89+
property="role"
90+
displayFilters={filters}
91+
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
92+
/>
93+
),
5994
tdRender: (rowData: RowData) => <AccountTypeColumn rowData={rowData} workspaceSlug={workspaceSlug as string} />,
6095
},
6196

@@ -70,7 +105,14 @@ export const useMemberColumns = () => {
70105
{
71106
key: "Joining date",
72107
content: t("workspace_settings.settings.members.details.joining_date"),
73-
tdRender: (rowData: RowData) => <div>{getFormattedDate(rowData?.member?.joining_date || "")}</div>,
108+
thRender: () => (
109+
<MemberHeaderColumn
110+
property="joining_date"
111+
displayFilters={filters}
112+
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
113+
/>
114+
),
115+
tdRender: (rowData: RowData) => <div>{renderFormattedDate(rowData?.member?.joining_date)}</div>,
74116
},
75117
];
76118
return { columns, workspaceSlug, removeMemberModal, setRemoveMemberModal };

apps/web/ce/store/member/project-member.store.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { EUserProjectRoles } from "@plane/types";
55
import type { RootStore } from "@/plane-web/store/root.store";
66
// store
77
import type { IMemberRootStore } from "@/store/member";
8-
import { BaseProjectMemberStore, IBaseProjectMemberStore } from "@/store/member/base-project-member.store";
8+
import { BaseProjectMemberStore, IBaseProjectMemberStore } from "@/store/member/project/base-project-member.store";
99

1010
export type IProjectMemberStore = IBaseProjectMemberStore;
1111

apps/web/core/components/pages/version/editor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { useParams } from "next/navigation";
33
// plane imports
44
import type { TDisplayConfig } from "@plane/editor";
55
import type { JSONContent, TPageVersion } from "@plane/types";
6-
import { isJSONContentEmpty } from "@plane/utils";
76
import { Loader } from "@plane/ui";
7+
import { isJSONContentEmpty } from "@plane/utils";
88
// components
99
import { DocumentEditor } from "@/components/editor/document/editor";
1010
// hooks

0 commit comments

Comments
 (0)