[WEB-4896]feat: filters to project and workspace members list (#7786)
This commit is contained in:
parent
85bffaa231
commit
586a7a48ba
20 changed files with 914 additions and 80 deletions
|
|
@ -20,6 +20,7 @@ import { cn } from "@plane/utils";
|
||||||
import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view";
|
import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view";
|
||||||
import { CountChip } from "@/components/common/count-chip";
|
import { CountChip } from "@/components/common/count-chip";
|
||||||
import { PageHead } from "@/components/core/page-title";
|
import { PageHead } from "@/components/core/page-title";
|
||||||
|
import { MemberListFiltersDropdown } from "@/components/project/dropdowns/filters/member-list";
|
||||||
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
|
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
|
||||||
import { WorkspaceMembersList } from "@/components/workspace/settings/members-list";
|
import { WorkspaceMembersList } from "@/components/workspace/settings/members-list";
|
||||||
// helpers
|
// helpers
|
||||||
|
|
@ -41,7 +42,7 @@ const WorkspaceMembersSettingsPage = observer(() => {
|
||||||
// store hooks
|
// store hooks
|
||||||
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
|
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
|
||||||
const {
|
const {
|
||||||
workspace: { workspaceMemberIds, inviteMembersToWorkspace },
|
workspace: { workspaceMemberIds, inviteMembersToWorkspace, filtersStore },
|
||||||
} = useMember();
|
} = useMember();
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
@ -88,8 +89,20 @@ const WorkspaceMembersSettingsPage = observer(() => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handler for role filter updates
|
||||||
|
const handleRoleFilterUpdate = (role: string) => {
|
||||||
|
const currentFilters = filtersStore.filters;
|
||||||
|
const currentRoles = currentFilters?.roles || [];
|
||||||
|
const updatedRoles = currentRoles.includes(role) ? currentRoles.filter((r) => r !== role) : [...currentRoles, role];
|
||||||
|
|
||||||
|
filtersStore.updateFilters({
|
||||||
|
roles: updatedRoles.length > 0 ? updatedRoles : undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// derived values
|
// derived values
|
||||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Members` : undefined;
|
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Members` : undefined;
|
||||||
|
const appliedRoleFilters = filtersStore.filters?.roles || [];
|
||||||
|
|
||||||
// if user is not authorized to view this page
|
// if user is not authorized to view this page
|
||||||
if (workspaceUserInfo && !canPerformWorkspaceMemberActions) {
|
if (workspaceUserInfo && !canPerformWorkspaceMemberActions) {
|
||||||
|
|
@ -116,7 +129,8 @@ const WorkspaceMembersSettingsPage = observer(() => {
|
||||||
<CountChip count={workspaceMemberIds.length} className="h-5 m-auto" />
|
<CountChip count={workspaceMemberIds.length} className="h-5 m-auto" />
|
||||||
)}
|
)}
|
||||||
</h4>
|
</h4>
|
||||||
<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">
|
<div className="flex items-center gap-2">
|
||||||
|
<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">
|
||||||
<Search className="h-3.5 w-3.5 text-custom-text-400" />
|
<Search className="h-3.5 w-3.5 text-custom-text-400" />
|
||||||
<input
|
<input
|
||||||
className="w-full max-w-[234px] border-none bg-transparent text-sm outline-none placeholder:text-custom-text-400"
|
className="w-full max-w-[234px] border-none bg-transparent text-sm outline-none placeholder:text-custom-text-400"
|
||||||
|
|
@ -126,6 +140,11 @@ const WorkspaceMembersSettingsPage = observer(() => {
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<MemberListFiltersDropdown
|
||||||
|
appliedFilters={appliedRoleFilters}
|
||||||
|
handleUpdate={handleRoleFilterUpdate}
|
||||||
|
memberType="workspace"
|
||||||
|
/>
|
||||||
{canPerformWorkspaceAdminActions && (
|
{canPerformWorkspaceAdminActions && (
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
|
|
@ -138,6 +157,7 @@ const WorkspaceMembersSettingsPage = observer(() => {
|
||||||
)}
|
)}
|
||||||
<BillingActionsButton canPerformWorkspaceAdminActions={canPerformWorkspaceAdminActions} />
|
<BillingActionsButton canPerformWorkspaceAdminActions={canPerformWorkspaceAdminActions} />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<WorkspaceMembersList searchQuery={searchQuery} isAdmin={canPerformWorkspaceAdminActions} />
|
<WorkspaceMembersList searchQuery={searchQuery} isAdmin={canPerformWorkspaceAdminActions} />
|
||||||
</section>
|
</section>
|
||||||
</SettingsContentWrapper>
|
</SettingsContentWrapper>
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,14 @@ import { useState } from "react";
|
||||||
// plane imports
|
// plane imports
|
||||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||||
import { IWorkspaceMember, TProjectMembership } from "@plane/types";
|
import { IWorkspaceMember, TProjectMembership } from "@plane/types";
|
||||||
|
import { renderFormattedDate } from "@plane/utils";
|
||||||
// components
|
// components
|
||||||
|
import { MemberHeaderColumn } from "@/components/project/member-header-column";
|
||||||
import { AccountTypeColumn, NameColumn } from "@/components/project/settings/member-columns";
|
import { AccountTypeColumn, NameColumn } from "@/components/project/settings/member-columns";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { useMember } from "@/hooks/store/use-member";
|
||||||
import { useUser, useUserPermissions } from "@/hooks/store/user";
|
import { useUser, useUserPermissions } from "@/hooks/store/user";
|
||||||
|
import { IMemberFilters } from "@/store/member/utils";
|
||||||
|
|
||||||
export interface RowData extends Pick<TProjectMembership, "original_role"> {
|
export interface RowData extends Pick<TProjectMembership, "original_role"> {
|
||||||
member: IWorkspaceMember;
|
member: IWorkspaceMember;
|
||||||
|
|
@ -20,9 +24,15 @@ export const useProjectColumns = (props: TUseProjectColumnsProps) => {
|
||||||
const { projectId, workspaceSlug } = props;
|
const { projectId, workspaceSlug } = props;
|
||||||
// states
|
// states
|
||||||
const [removeMemberModal, setRemoveMemberModal] = useState<RowData | null>(null);
|
const [removeMemberModal, setRemoveMemberModal] = useState<RowData | null>(null);
|
||||||
|
|
||||||
// store hooks
|
// store hooks
|
||||||
const { data: currentUser } = useUser();
|
const { data: currentUser } = useUser();
|
||||||
const { allowPermissions, getProjectRoleByWorkspaceSlugAndProjectId } = useUserPermissions();
|
const { allowPermissions, getProjectRoleByWorkspaceSlugAndProjectId } = useUserPermissions();
|
||||||
|
const {
|
||||||
|
project: {
|
||||||
|
filters: { getFilters, updateFilters },
|
||||||
|
},
|
||||||
|
} = useMember();
|
||||||
// derived values
|
// derived values
|
||||||
const isAdmin = allowPermissions(
|
const isAdmin = allowPermissions(
|
||||||
[EUserPermissions.ADMIN],
|
[EUserPermissions.ADMIN],
|
||||||
|
|
@ -33,11 +43,11 @@ export const useProjectColumns = (props: TUseProjectColumnsProps) => {
|
||||||
const currentProjectRole =
|
const currentProjectRole =
|
||||||
getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug.toString(), projectId.toString()) ?? EUserPermissions.GUEST;
|
getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug.toString(), projectId.toString()) ?? EUserPermissions.GUEST;
|
||||||
|
|
||||||
const getFormattedDate = (dateStr: string) => {
|
const displayFilters = getFilters(projectId);
|
||||||
const date = new Date(dateStr);
|
|
||||||
|
|
||||||
const options: Intl.DateTimeFormatOptions = { year: "numeric", month: "long", day: "numeric" };
|
// handlers
|
||||||
return date.toLocaleDateString("en-US", options);
|
const handleDisplayFilterUpdate = (filters: Partial<IMemberFilters>) => {
|
||||||
|
updateFilters(projectId, filters);
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
|
|
@ -45,6 +55,13 @@ export const useProjectColumns = (props: TUseProjectColumnsProps) => {
|
||||||
key: "Full Name",
|
key: "Full Name",
|
||||||
content: "Full name",
|
content: "Full name",
|
||||||
thClassName: "text-left",
|
thClassName: "text-left",
|
||||||
|
thRender: () => (
|
||||||
|
<MemberHeaderColumn
|
||||||
|
property="full_name"
|
||||||
|
displayFilters={displayFilters}
|
||||||
|
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||||
|
/>
|
||||||
|
),
|
||||||
tdRender: (rowData: RowData) => (
|
tdRender: (rowData: RowData) => (
|
||||||
<NameColumn
|
<NameColumn
|
||||||
rowData={rowData}
|
rowData={rowData}
|
||||||
|
|
@ -58,12 +75,37 @@ export const useProjectColumns = (props: TUseProjectColumnsProps) => {
|
||||||
{
|
{
|
||||||
key: "Display Name",
|
key: "Display Name",
|
||||||
content: "Display name",
|
content: "Display name",
|
||||||
|
thRender: () => (
|
||||||
|
<MemberHeaderColumn
|
||||||
|
property="display_name"
|
||||||
|
displayFilters={displayFilters}
|
||||||
|
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||||
|
/>
|
||||||
|
),
|
||||||
tdRender: (rowData: RowData) => <div className="w-32">{rowData.member.display_name}</div>,
|
tdRender: (rowData: RowData) => <div className="w-32">{rowData.member.display_name}</div>,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "Email",
|
||||||
|
content: "Email",
|
||||||
|
thRender: () => (
|
||||||
|
<MemberHeaderColumn
|
||||||
|
property="email"
|
||||||
|
displayFilters={displayFilters}
|
||||||
|
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
tdRender: (rowData: RowData) => <div className="w-48 text-custom-text-200">{rowData.member.email}</div>,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "Account Type",
|
key: "Account Type",
|
||||||
content: "Account type",
|
content: "Account type",
|
||||||
|
thRender: () => (
|
||||||
|
<MemberHeaderColumn
|
||||||
|
property="role"
|
||||||
|
displayFilters={displayFilters}
|
||||||
|
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||||
|
/>
|
||||||
|
),
|
||||||
tdRender: (rowData: RowData) => (
|
tdRender: (rowData: RowData) => (
|
||||||
<AccountTypeColumn
|
<AccountTypeColumn
|
||||||
rowData={rowData}
|
rowData={rowData}
|
||||||
|
|
@ -76,8 +118,21 @@ export const useProjectColumns = (props: TUseProjectColumnsProps) => {
|
||||||
{
|
{
|
||||||
key: "Joining Date",
|
key: "Joining Date",
|
||||||
content: "Joining date",
|
content: "Joining date",
|
||||||
tdRender: (rowData: RowData) => <div>{getFormattedDate(rowData?.member?.joining_date || "")}</div>,
|
thRender: () => (
|
||||||
|
<MemberHeaderColumn
|
||||||
|
property="joining_date"
|
||||||
|
displayFilters={displayFilters}
|
||||||
|
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
tdRender: (rowData: RowData) => <div>{renderFormattedDate(rowData?.member?.joining_date)}</div>,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
return { columns, removeMemberModal, setRemoveMemberModal };
|
return {
|
||||||
|
columns,
|
||||||
|
removeMemberModal,
|
||||||
|
setRemoveMemberModal,
|
||||||
|
displayFilters,
|
||||||
|
handleDisplayFilterUpdate,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,12 @@ import { useState } from "react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
|
import { renderFormattedDate } from "@plane/utils";
|
||||||
|
import { MemberHeaderColumn } from "@/components/project/member-header-column";
|
||||||
import { AccountTypeColumn, NameColumn, RowData } from "@/components/workspace/settings/member-columns";
|
import { AccountTypeColumn, NameColumn, RowData } from "@/components/workspace/settings/member-columns";
|
||||||
|
import { useMember } from "@/hooks/store/use-member";
|
||||||
import { useUser, useUserPermissions } from "@/hooks/store/user";
|
import { useUser, useUserPermissions } from "@/hooks/store/user";
|
||||||
|
import { IMemberFilters } from "@/store/member/utils";
|
||||||
|
|
||||||
export const useMemberColumns = () => {
|
export const useMemberColumns = () => {
|
||||||
// states
|
// states
|
||||||
|
|
@ -13,23 +17,33 @@ export const useMemberColumns = () => {
|
||||||
|
|
||||||
const { data: currentUser } = useUser();
|
const { data: currentUser } = useUser();
|
||||||
const { allowPermissions } = useUserPermissions();
|
const { allowPermissions } = useUserPermissions();
|
||||||
|
const {
|
||||||
|
workspace: {
|
||||||
|
filtersStore: { filters, updateFilters },
|
||||||
|
},
|
||||||
|
} = useMember();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const getFormattedDate = (dateStr: string) => {
|
|
||||||
const date = new Date(dateStr);
|
|
||||||
|
|
||||||
const options: Intl.DateTimeFormatOptions = { year: "numeric", month: "long", day: "numeric" };
|
|
||||||
return date.toLocaleDateString("en-US", options);
|
|
||||||
};
|
|
||||||
|
|
||||||
// derived values
|
// derived values
|
||||||
const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
|
const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
|
||||||
|
|
||||||
|
// handlers
|
||||||
|
const handleDisplayFilterUpdate = (filterUpdates: Partial<IMemberFilters>) => {
|
||||||
|
updateFilters(filterUpdates);
|
||||||
|
};
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
key: "Full name",
|
key: "Full name",
|
||||||
content: t("workspace_settings.settings.members.details.full_name"),
|
content: t("workspace_settings.settings.members.details.full_name"),
|
||||||
thClassName: "text-left",
|
thClassName: "text-left",
|
||||||
|
thRender: () => (
|
||||||
|
<MemberHeaderColumn
|
||||||
|
property="full_name"
|
||||||
|
displayFilters={filters}
|
||||||
|
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||||
|
/>
|
||||||
|
),
|
||||||
tdRender: (rowData: RowData) => (
|
tdRender: (rowData: RowData) => (
|
||||||
<NameColumn
|
<NameColumn
|
||||||
rowData={rowData}
|
rowData={rowData}
|
||||||
|
|
@ -44,18 +58,39 @@ export const useMemberColumns = () => {
|
||||||
{
|
{
|
||||||
key: "Display name",
|
key: "Display name",
|
||||||
content: t("workspace_settings.settings.members.details.display_name"),
|
content: t("workspace_settings.settings.members.details.display_name"),
|
||||||
|
thRender: () => (
|
||||||
|
<MemberHeaderColumn
|
||||||
|
property="display_name"
|
||||||
|
displayFilters={filters}
|
||||||
|
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||||
|
/>
|
||||||
|
),
|
||||||
tdRender: (rowData: RowData) => <div className="w-32">{rowData.member.display_name}</div>,
|
tdRender: (rowData: RowData) => <div className="w-32">{rowData.member.display_name}</div>,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
key: "Email address",
|
key: "Email address",
|
||||||
content: t("workspace_settings.settings.members.details.email_address"),
|
content: t("workspace_settings.settings.members.details.email_address"),
|
||||||
|
thRender: () => (
|
||||||
|
<MemberHeaderColumn
|
||||||
|
property="email"
|
||||||
|
displayFilters={filters}
|
||||||
|
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||||
|
/>
|
||||||
|
),
|
||||||
tdRender: (rowData: RowData) => <div className="w-48 truncate">{rowData.member.email}</div>,
|
tdRender: (rowData: RowData) => <div className="w-48 truncate">{rowData.member.email}</div>,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
key: "Account type",
|
key: "Account type",
|
||||||
content: t("workspace_settings.settings.members.details.account_type"),
|
content: t("workspace_settings.settings.members.details.account_type"),
|
||||||
|
thRender: () => (
|
||||||
|
<MemberHeaderColumn
|
||||||
|
property="role"
|
||||||
|
displayFilters={filters}
|
||||||
|
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||||
|
/>
|
||||||
|
),
|
||||||
tdRender: (rowData: RowData) => <AccountTypeColumn rowData={rowData} workspaceSlug={workspaceSlug as string} />,
|
tdRender: (rowData: RowData) => <AccountTypeColumn rowData={rowData} workspaceSlug={workspaceSlug as string} />,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -70,7 +105,14 @@ export const useMemberColumns = () => {
|
||||||
{
|
{
|
||||||
key: "Joining date",
|
key: "Joining date",
|
||||||
content: t("workspace_settings.settings.members.details.joining_date"),
|
content: t("workspace_settings.settings.members.details.joining_date"),
|
||||||
tdRender: (rowData: RowData) => <div>{getFormattedDate(rowData?.member?.joining_date || "")}</div>,
|
thRender: () => (
|
||||||
|
<MemberHeaderColumn
|
||||||
|
property="joining_date"
|
||||||
|
displayFilters={filters}
|
||||||
|
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
tdRender: (rowData: RowData) => <div>{renderFormattedDate(rowData?.member?.joining_date)}</div>,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
return { columns, workspaceSlug, removeMemberModal, setRemoveMemberModal };
|
return { columns, workspaceSlug, removeMemberModal, setRemoveMemberModal };
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { EUserProjectRoles } from "@plane/types";
|
||||||
import type { RootStore } from "@/plane-web/store/root.store";
|
import type { RootStore } from "@/plane-web/store/root.store";
|
||||||
// store
|
// store
|
||||||
import type { IMemberRootStore } from "@/store/member";
|
import type { IMemberRootStore } from "@/store/member";
|
||||||
import { BaseProjectMemberStore, IBaseProjectMemberStore } from "@/store/member/base-project-member.store";
|
import { BaseProjectMemberStore, IBaseProjectMemberStore } from "@/store/member/project/base-project-member.store";
|
||||||
|
|
||||||
export type IProjectMemberStore = IBaseProjectMemberStore;
|
export type IProjectMemberStore = IBaseProjectMemberStore;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ import { useParams } from "next/navigation";
|
||||||
// plane imports
|
// plane imports
|
||||||
import type { TDisplayConfig } from "@plane/editor";
|
import type { TDisplayConfig } from "@plane/editor";
|
||||||
import type { JSONContent, TPageVersion } from "@plane/types";
|
import type { JSONContent, TPageVersion } from "@plane/types";
|
||||||
import { isJSONContentEmpty } from "@plane/utils";
|
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
|
import { isJSONContentEmpty } from "@plane/utils";
|
||||||
// components
|
// components
|
||||||
import { DocumentEditor } from "@/components/editor/document/editor";
|
import { DocumentEditor } from "@/components/editor/document/editor";
|
||||||
// hooks
|
// hooks
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { ChevronDown } from "lucide-react";
|
||||||
|
// plane imports
|
||||||
|
import { EUserProjectRoles, EUserWorkspaceRoles } from "@plane/types";
|
||||||
|
// plane ui
|
||||||
|
import { Button, CustomMenu } from "@plane/ui";
|
||||||
|
// components
|
||||||
|
import { FilterHeader, FilterOption } from "@/components/issues/issue-layouts/filters";
|
||||||
|
|
||||||
|
interface IRoleOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
appliedFilters: string[] | null;
|
||||||
|
handleUpdate: (role: string) => void;
|
||||||
|
memberType: "project" | "workspace";
|
||||||
|
};
|
||||||
|
|
||||||
|
const PROJECT_ROLE_OPTIONS: IRoleOption[] = [
|
||||||
|
{ value: String(EUserProjectRoles.ADMIN), label: "Admin" },
|
||||||
|
{ value: String(EUserProjectRoles.MEMBER), label: "Member" },
|
||||||
|
{ value: String(EUserProjectRoles.GUEST), label: "Guest" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const WORKSPACE_ROLE_OPTIONS: IRoleOption[] = [
|
||||||
|
{ value: String(EUserWorkspaceRoles.ADMIN), label: "Admin" },
|
||||||
|
{ value: String(EUserWorkspaceRoles.MEMBER), label: "Member" },
|
||||||
|
{ value: String(EUserWorkspaceRoles.GUEST), label: "Guest" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Role filter group component
|
||||||
|
const RoleFilterGroup: React.FC<{
|
||||||
|
appliedFilters: string[] | null;
|
||||||
|
handleUpdate: (role: string) => void;
|
||||||
|
memberType: "project" | "workspace";
|
||||||
|
}> = observer(({ appliedFilters, handleUpdate, memberType }) => {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(true);
|
||||||
|
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||||
|
const roleOptions = memberType === "project" ? PROJECT_ROLE_OPTIONS : WORKSPACE_ROLE_OPTIONS;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<FilterHeader
|
||||||
|
title={`Roles${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
|
||||||
|
isPreviewEnabled={isExpanded}
|
||||||
|
handleIsPreviewEnabled={() => setIsExpanded(!isExpanded)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{roleOptions.map((role) => {
|
||||||
|
const isSelected = appliedFilters?.includes(role.value) ?? false;
|
||||||
|
return (
|
||||||
|
<FilterOption
|
||||||
|
key={`role-${role.value}`}
|
||||||
|
isChecked={isSelected}
|
||||||
|
title={role.label}
|
||||||
|
onClick={() => handleUpdate(role.value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const MemberListFilters: React.FC<Props> = observer((props) => {
|
||||||
|
const { appliedFilters, handleUpdate, memberType } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Role Filter Group */}
|
||||||
|
<RoleFilterGroup appliedFilters={appliedFilters} handleUpdate={handleUpdate} memberType={memberType} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dropdown component for member list filters
|
||||||
|
export const MemberListFiltersDropdown: React.FC<Props> = observer((props) => {
|
||||||
|
const { appliedFilters, handleUpdate, memberType } = props;
|
||||||
|
|
||||||
|
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CustomMenu
|
||||||
|
customButton={
|
||||||
|
<div className="relative">
|
||||||
|
<Button variant="neutral-primary" size="sm" className="flex items-center gap-2">
|
||||||
|
<span>Filters</span>
|
||||||
|
<ChevronDown className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
{appliedFiltersCount > 0 && (
|
||||||
|
<div className="absolute -top-1 -right-1 h-2 w-2 rounded-full bg-custom-primary-100" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
placement="bottom-start"
|
||||||
|
>
|
||||||
|
<MemberListFilters appliedFilters={appliedFilters} handleUpdate={handleUpdate} memberType={memberType} />
|
||||||
|
</CustomMenu>
|
||||||
|
);
|
||||||
|
});
|
||||||
114
apps/web/core/components/project/member-header-column.tsx
Normal file
114
apps/web/core/components/project/member-header-column.tsx
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
// ui
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { ArrowDownWideNarrow, ArrowUpNarrowWide, CheckIcon, ChevronDownIcon, Eraser, MoveRight } from "lucide-react";
|
||||||
|
// constants
|
||||||
|
import { MEMBER_PROPERTY_DETAILS, IProjectMemberDisplayProperties, TMemberOrderByOptions } from "@plane/constants";
|
||||||
|
// i18n
|
||||||
|
import { useTranslation } from "@plane/i18n";
|
||||||
|
// types
|
||||||
|
import { CustomMenu } from "@plane/ui";
|
||||||
|
import { IMemberFilters } from "@/store/member/utils";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
property: keyof IProjectMemberDisplayProperties;
|
||||||
|
displayFilters?: IMemberFilters;
|
||||||
|
handleDisplayFilterUpdate: (data: Partial<IMemberFilters>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MemberHeaderColumn = observer((props: Props) => {
|
||||||
|
const { displayFilters, handleDisplayFilterUpdate, property } = props;
|
||||||
|
// i18n
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const propertyDetails = MEMBER_PROPERTY_DETAILS[property];
|
||||||
|
|
||||||
|
const activeSortingProperty = displayFilters?.order_by;
|
||||||
|
|
||||||
|
const handleOrderBy = (order: TMemberOrderByOptions, _itemKey: keyof IProjectMemberDisplayProperties) => {
|
||||||
|
handleDisplayFilterUpdate({ order_by: order });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearSorting = () => {
|
||||||
|
handleDisplayFilterUpdate({ order_by: undefined });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!propertyDetails) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CustomMenu
|
||||||
|
customButtonClassName="clickable !w-full"
|
||||||
|
customButtonTabIndex={-1}
|
||||||
|
className="!w-full"
|
||||||
|
customButton={
|
||||||
|
<div className="flex w-full cursor-pointer items-center justify-between gap-1.5 py-2 text-sm text-custom-text-200 hover:text-custom-text-100">
|
||||||
|
<span>{t(propertyDetails.i18n_title)}</span>
|
||||||
|
<div className="ml-3 flex">
|
||||||
|
{(activeSortingProperty === propertyDetails.ascendingOrderKey ||
|
||||||
|
activeSortingProperty === propertyDetails.descendingOrderKey) && (
|
||||||
|
<div className="flex h-3.5 w-3.5 items-center justify-center rounded-full">
|
||||||
|
{propertyDetails.ascendingOrderKey === activeSortingProperty ? (
|
||||||
|
<ArrowDownWideNarrow className="h-3 w-3" />
|
||||||
|
) : (
|
||||||
|
<ArrowUpNarrowWide className="h-3 w-3" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
placement="bottom-end"
|
||||||
|
closeOnSelect
|
||||||
|
>
|
||||||
|
{propertyDetails.isSortingAllowed && (
|
||||||
|
<>
|
||||||
|
<CustomMenu.MenuItem onClick={() => handleOrderBy(propertyDetails.ascendingOrderKey, property)}>
|
||||||
|
<div
|
||||||
|
className={`flex items-center justify-between gap-1.5 px-1 ${
|
||||||
|
activeSortingProperty === propertyDetails.ascendingOrderKey
|
||||||
|
? "text-custom-text-100"
|
||||||
|
: "text-custom-text-200 hover:text-custom-text-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ArrowDownWideNarrow className="h-3 w-3 stroke-[1.5]" />
|
||||||
|
<span>{propertyDetails.ascendingOrderTitle}</span>
|
||||||
|
<MoveRight className="h-3 w-3" />
|
||||||
|
<span>{propertyDetails.descendingOrderTitle}</span>
|
||||||
|
</div>
|
||||||
|
{activeSortingProperty === propertyDetails.ascendingOrderKey && <CheckIcon className="h-3 w-3" />}
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
|
||||||
|
<CustomMenu.MenuItem onClick={() => handleOrderBy(propertyDetails.descendingOrderKey, property)}>
|
||||||
|
<div
|
||||||
|
className={`flex items-center justify-between gap-1.5 px-1 ${
|
||||||
|
activeSortingProperty === propertyDetails.descendingOrderKey
|
||||||
|
? "text-custom-text-100"
|
||||||
|
: "text-custom-text-200 hover:text-custom-text-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ArrowUpNarrowWide className="h-3 w-3 stroke-[1.5]" />
|
||||||
|
<span>{propertyDetails.descendingOrderTitle}</span>
|
||||||
|
<MoveRight className="h-3 w-3" />
|
||||||
|
<span>{propertyDetails.ascendingOrderTitle}</span>
|
||||||
|
</div>
|
||||||
|
{activeSortingProperty === propertyDetails.descendingOrderKey && <CheckIcon className="h-3 w-3" />}
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
|
||||||
|
{(activeSortingProperty === propertyDetails.ascendingOrderKey ||
|
||||||
|
activeSortingProperty === propertyDetails.descendingOrderKey) && (
|
||||||
|
<CustomMenu.MenuItem className="mt-0.5" key={property} onClick={handleClearSorting}>
|
||||||
|
<div className="flex items-center gap-2 px-1">
|
||||||
|
<Eraser className="h-3 w-3" />
|
||||||
|
<span>{t("common.actions.clear_sorting")}</span>
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CustomMenu>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -13,7 +13,7 @@ import { useAppRouter } from "@/hooks/use-app-router";
|
||||||
// plane web imports
|
// plane web imports
|
||||||
import { useProjectColumns } from "@/plane-web/components/projects/settings/useProjectColumns";
|
import { useProjectColumns } from "@/plane-web/components/projects/settings/useProjectColumns";
|
||||||
// store
|
// store
|
||||||
import { IProjectMemberDetails } from "@/store/member/base-project-member.store";
|
import { IProjectMemberDetails } from "@/store/member/project/base-project-member.store";
|
||||||
// local imports
|
// local imports
|
||||||
import { ConfirmProjectMemberRemove } from "./confirm-project-member-remove";
|
import { ConfirmProjectMemberRemove } from "./confirm-project-member-remove";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import { MembersSettingsLoader } from "@/components/ui/loader/settings/members";
|
||||||
import { useMember } from "@/hooks/store/use-member";
|
import { useMember } from "@/hooks/store/use-member";
|
||||||
import { useUserPermissions } from "@/hooks/store/user";
|
import { useUserPermissions } from "@/hooks/store/user";
|
||||||
// local imports
|
// local imports
|
||||||
|
import { MemberListFiltersDropdown } from "./dropdowns/filters/member-list";
|
||||||
import { ProjectMemberListItem } from "./member-list-item";
|
import { ProjectMemberListItem } from "./member-list-item";
|
||||||
import { SendProjectInvitationModal } from "./send-project-invitation-modal";
|
import { SendProjectInvitationModal } from "./send-project-invitation-modal";
|
||||||
|
|
||||||
|
|
@ -27,14 +28,14 @@ export const ProjectMemberList: React.FC<TProjectMemberListProps> = observer((pr
|
||||||
const [inviteModal, setInviteModal] = useState(false);
|
const [inviteModal, setInviteModal] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const {
|
const {
|
||||||
project: { projectMemberIds, getProjectMemberDetails },
|
project: { projectMemberIds, getFilteredProjectMemberDetails, filters },
|
||||||
} = useMember();
|
} = useMember();
|
||||||
const { allowPermissions } = useUserPermissions();
|
const { allowPermissions } = useUserPermissions();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const searchedProjectMembers = (projectMemberIds ?? []).filter((userId) => {
|
const searchedProjectMembers = (projectMemberIds ?? []).filter((userId) => {
|
||||||
const memberDetails = projectId ? getProjectMemberDetails(userId, projectId.toString()) : null;
|
const memberDetails = projectId ? getFilteredProjectMemberDetails(userId, projectId.toString()) : null;
|
||||||
|
|
||||||
if (!memberDetails?.member || !memberDetails.original_role) return false;
|
if (!memberDetails?.member || !memberDetails.original_role) return false;
|
||||||
|
|
||||||
|
|
@ -43,12 +44,31 @@ export const ProjectMemberList: React.FC<TProjectMemberListProps> = observer((pr
|
||||||
|
|
||||||
return displayName?.includes(searchQuery.toLowerCase()) || fullName.includes(searchQuery.toLowerCase());
|
return displayName?.includes(searchQuery.toLowerCase()) || fullName.includes(searchQuery.toLowerCase());
|
||||||
});
|
});
|
||||||
|
|
||||||
const memberDetails = searchedProjectMembers?.map((memberId) =>
|
const memberDetails = searchedProjectMembers?.map((memberId) =>
|
||||||
projectId ? getProjectMemberDetails(memberId, projectId.toString()) : null
|
projectId ? getFilteredProjectMemberDetails(memberId, projectId.toString()) : null
|
||||||
);
|
);
|
||||||
|
|
||||||
const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
|
const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
|
||||||
|
|
||||||
|
// Handler for role filter updates
|
||||||
|
const handleRoleFilterUpdate = (role: string) => {
|
||||||
|
if (projectId) {
|
||||||
|
const currentFilters = filters.getFilters(projectId);
|
||||||
|
const currentRoles = currentFilters?.roles || [];
|
||||||
|
const updatedRoles = currentRoles.includes(role)
|
||||||
|
? currentRoles.filter((r) => r !== role)
|
||||||
|
: [...currentRoles, role];
|
||||||
|
|
||||||
|
filters.updateFilters(projectId, {
|
||||||
|
roles: updatedRoles.length > 0 ? updatedRoles : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get current role filters
|
||||||
|
const appliedRoleFilters = projectId ? filters.getFilters(projectId)?.roles || [] : [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SendProjectInvitationModal
|
<SendProjectInvitationModal
|
||||||
|
|
@ -59,7 +79,8 @@ export const ProjectMemberList: React.FC<TProjectMemberListProps> = observer((pr
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center justify-between gap-4 py-2 overflow-x-hidden border-b border-custom-border-100">
|
<div className="flex items-center justify-between gap-4 py-2 overflow-x-hidden border-b border-custom-border-100">
|
||||||
<div className="text-base font-semibold">{t("common.members")}</div>
|
<div className="text-base font-semibold">{t("common.members")}</div>
|
||||||
<div className="ml-auto flex items-center justify-start gap-1.5 rounded-md border border-custom-border-200 bg-custom-background-100 px-2 py-1">
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex items-center justify-start gap-1.5 rounded-md border border-custom-border-200 bg-custom-background-100 px-2 py-1">
|
||||||
<Search className="h-3.5 w-3.5" />
|
<Search className="h-3.5 w-3.5" />
|
||||||
<input
|
<input
|
||||||
className="w-full max-w-[234px] border-none bg-transparent text-sm focus:outline-none placeholder:text-custom-text-400"
|
className="w-full max-w-[234px] border-none bg-transparent text-sm focus:outline-none placeholder:text-custom-text-400"
|
||||||
|
|
@ -69,6 +90,11 @@ export const ProjectMemberList: React.FC<TProjectMemberListProps> = observer((pr
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<MemberListFiltersDropdown
|
||||||
|
appliedFilters={appliedRoleFilters}
|
||||||
|
handleUpdate={handleRoleFilterUpdate}
|
||||||
|
memberType="project"
|
||||||
|
/>
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
|
|
@ -82,6 +108,7 @@ export const ProjectMemberList: React.FC<TProjectMemberListProps> = observer((pr
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{!projectMemberIds ? (
|
{!projectMemberIds ? (
|
||||||
<MembersSettingsLoader />
|
<MembersSettingsLoader />
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ export const WorkspaceMembersList: FC<{ searchQuery: string; isAdmin: boolean }>
|
||||||
fetchWorkspaceMembers,
|
fetchWorkspaceMembers,
|
||||||
fetchWorkspaceMemberInvitations,
|
fetchWorkspaceMemberInvitations,
|
||||||
workspaceMemberIds,
|
workspaceMemberIds,
|
||||||
|
getFilteredWorkspaceMemberIds,
|
||||||
getSearchedWorkspaceMemberIds,
|
getSearchedWorkspaceMemberIds,
|
||||||
workspaceMemberInvitationIds,
|
workspaceMemberInvitationIds,
|
||||||
getSearchedWorkspaceInvitationIds,
|
getSearchedWorkspaceInvitationIds,
|
||||||
|
|
@ -49,7 +50,8 @@ export const WorkspaceMembersList: FC<{ searchQuery: string; isAdmin: boolean }>
|
||||||
if (!workspaceMemberIds && !workspaceMemberInvitationIds) return <MembersSettingsLoader />;
|
if (!workspaceMemberIds && !workspaceMemberInvitationIds) return <MembersSettingsLoader />;
|
||||||
|
|
||||||
// derived values
|
// derived values
|
||||||
const searchedMemberIds = getSearchedWorkspaceMemberIds(searchQuery);
|
const filteredMemberIds = workspaceSlug ? getFilteredWorkspaceMemberIds(workspaceSlug.toString()) : [];
|
||||||
|
const searchedMemberIds = searchQuery ? getSearchedWorkspaceMemberIds(searchQuery) : filteredMemberIds;
|
||||||
const searchedInvitationsIds = getSearchedWorkspaceInvitationIds(searchQuery);
|
const searchedInvitationsIds = getSearchedWorkspaceInvitationIds(searchQuery);
|
||||||
const memberDetails = searchedMemberIds?.map((memberId) => getWorkspaceMemberDetails(memberId));
|
const memberDetails = searchedMemberIds?.map((memberId) => getWorkspaceMemberDetails(memberId));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ import {
|
||||||
// root store
|
// root store
|
||||||
import { IWorkspaceIssues, WorkspaceIssues } from "@/plane-web/store/issue/workspace/issue.store";
|
import { IWorkspaceIssues, WorkspaceIssues } from "@/plane-web/store/issue/workspace/issue.store";
|
||||||
import type { RootStore } from "@/plane-web/store/root.store";
|
import type { RootStore } from "@/plane-web/store/root.store";
|
||||||
import { IWorkspaceMembership } from "@/store/member/workspace-member.store";
|
import { IWorkspaceMembership } from "@/store/member/workspace/workspace-member.store";
|
||||||
// issues data store
|
// issues data store
|
||||||
import { IArchivedIssuesFilter, ArchivedIssuesFilter, IArchivedIssues, ArchivedIssues } from "./archived";
|
import { IArchivedIssuesFilter, ArchivedIssuesFilter, IArchivedIssues, ArchivedIssues } from "./archived";
|
||||||
import { ICycleIssuesFilter, CycleIssuesFilter, ICycleIssues, CycleIssues } from "./cycle";
|
import { ICycleIssuesFilter, CycleIssuesFilter, ICycleIssues, CycleIssues } from "./cycle";
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { IUserLite } from "@plane/types";
|
||||||
import { IProjectMemberStore, ProjectMemberStore } from "@/plane-web/store/member/project-member.store";
|
import { IProjectMemberStore, ProjectMemberStore } from "@/plane-web/store/member/project-member.store";
|
||||||
import type { RootStore } from "@/plane-web/store/root.store";
|
import type { RootStore } from "@/plane-web/store/root.store";
|
||||||
// local imports
|
// local imports
|
||||||
import { IWorkspaceMemberStore, WorkspaceMemberStore } from "./workspace-member.store";
|
import { IWorkspaceMemberStore, WorkspaceMemberStore } from "./workspace/workspace-member.store";
|
||||||
|
|
||||||
export interface IMemberRootStore {
|
export interface IMemberRootStore {
|
||||||
// observables
|
// observables
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,13 @@ import type { RootStore } from "@/plane-web/store/root.store";
|
||||||
// services
|
// services
|
||||||
import { ProjectMemberService } from "@/services/project";
|
import { ProjectMemberService } from "@/services/project";
|
||||||
// store
|
// store
|
||||||
|
import { IProjectStore } from "@/store/project/project.store";
|
||||||
import { IRouterStore } from "@/store/router.store";
|
import { IRouterStore } from "@/store/router.store";
|
||||||
import { IUserStore } from "@/store/user";
|
import { IUserStore } from "@/store/user";
|
||||||
// local imports
|
// local imports
|
||||||
import { IProjectStore } from "../project/project.store";
|
import { IMemberRootStore } from "../index";
|
||||||
import { IMemberRootStore } from ".";
|
import { sortProjectMembers } from "../utils";
|
||||||
|
import { ProjectMemberFiltersStore, IProjectMemberFiltersStore } from "./project-member-filters.store";
|
||||||
|
|
||||||
export interface IProjectMemberDetails extends Omit<TProjectMembership, "member"> {
|
export interface IProjectMemberDetails extends Omit<TProjectMembership, "member"> {
|
||||||
member: IUserLite;
|
member: IUserLite;
|
||||||
|
|
@ -31,12 +33,15 @@ export interface IBaseProjectMemberStore {
|
||||||
projectMemberMap: {
|
projectMemberMap: {
|
||||||
[projectId: string]: Record<string, TProjectMembership>;
|
[projectId: string]: Record<string, TProjectMembership>;
|
||||||
};
|
};
|
||||||
|
// filters store
|
||||||
|
filters: IProjectMemberFiltersStore;
|
||||||
// computed
|
// computed
|
||||||
projectMemberIds: string[] | null;
|
projectMemberIds: string[] | null;
|
||||||
// computed actions
|
// computed actions
|
||||||
getProjectMemberFetchStatus: (projectId: string) => boolean;
|
getProjectMemberFetchStatus: (projectId: string) => boolean;
|
||||||
getProjectMemberDetails: (userId: string, projectId: string) => IProjectMemberDetails | null;
|
getProjectMemberDetails: (userId: string, projectId: string) => IProjectMemberDetails | null;
|
||||||
getProjectMemberIds: (projectId: string, includeGuestUsers: boolean) => string[] | null;
|
getProjectMemberIds: (projectId: string, includeGuestUsers: boolean) => string[] | null;
|
||||||
|
getFilteredProjectMemberDetails: (userId: string, projectId: string) => IProjectMemberDetails | null;
|
||||||
// fetch actions
|
// fetch actions
|
||||||
fetchProjectMembers: (
|
fetchProjectMembers: (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
|
|
@ -67,6 +72,8 @@ export abstract class BaseProjectMemberStore implements IBaseProjectMemberStore
|
||||||
projectMemberMap: {
|
projectMemberMap: {
|
||||||
[projectId: string]: Record<string, TProjectMembership>;
|
[projectId: string]: Record<string, TProjectMembership>;
|
||||||
} = {};
|
} = {};
|
||||||
|
// filters store
|
||||||
|
filters: IProjectMemberFiltersStore;
|
||||||
// stores
|
// stores
|
||||||
routerStore: IRouterStore;
|
routerStore: IRouterStore;
|
||||||
userStore: IUserStore;
|
userStore: IUserStore;
|
||||||
|
|
@ -88,31 +95,40 @@ export abstract class BaseProjectMemberStore implements IBaseProjectMemberStore
|
||||||
updateMemberRole: action,
|
updateMemberRole: action,
|
||||||
removeMemberFromProject: action,
|
removeMemberFromProject: action,
|
||||||
});
|
});
|
||||||
|
|
||||||
// root store
|
// root store
|
||||||
this.rootStore = _rootStore;
|
this.rootStore = _rootStore;
|
||||||
this.routerStore = _rootStore.router;
|
this.routerStore = _rootStore.router;
|
||||||
this.userStore = _rootStore.user;
|
this.userStore = _rootStore.user;
|
||||||
this.memberRoot = _memberRoot;
|
this.memberRoot = _memberRoot;
|
||||||
this.projectRoot = _rootStore.projectRoot.project;
|
this.projectRoot = _rootStore.projectRoot.project;
|
||||||
|
this.filters = new ProjectMemberFiltersStore();
|
||||||
// services
|
// services
|
||||||
this.projectMemberService = new ProjectMemberService();
|
this.projectMemberService = new ProjectMemberService();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description get the list of all the user ids of all the members of the current project
|
* @description get the list of all the user ids of all the members of the current project
|
||||||
|
* Returns filtered and sorted member IDs based on current filters
|
||||||
*/
|
*/
|
||||||
get projectMemberIds() {
|
get projectMemberIds() {
|
||||||
const projectId = this.routerStore.projectId;
|
const projectId = this.routerStore.projectId;
|
||||||
if (!projectId) return null;
|
if (!projectId) return null;
|
||||||
let members = Object.values(this.projectMemberMap?.[projectId] ?? {});
|
|
||||||
|
const members = Object.values(this.projectMemberMap?.[projectId] ?? {});
|
||||||
if (members.length === 0) return null;
|
if (members.length === 0) return null;
|
||||||
members = sortBy(members, [
|
|
||||||
(m) => m.member !== this.userStore.data?.id,
|
// Access the filters directly to ensure MobX tracking
|
||||||
(m) => this.memberRoot.memberMap?.[m.member]?.display_name.toLowerCase(),
|
const currentFilters = this.filters.filtersMap[projectId];
|
||||||
]);
|
|
||||||
const memberIds = members.map((m) => m.member);
|
// Apply filters and sorting directly here to ensure MobX tracking
|
||||||
return memberIds;
|
const sortedMembers = sortProjectMembers(
|
||||||
|
members,
|
||||||
|
this.memberRoot?.memberMap || {},
|
||||||
|
(member) => member.member,
|
||||||
|
currentFilters
|
||||||
|
);
|
||||||
|
|
||||||
|
return sortedMembers.map((member) => member.member);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -202,6 +218,41 @@ export abstract class BaseProjectMemberStore implements IBaseProjectMemberStore
|
||||||
return memberIds;
|
return memberIds;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description get the filtered project member details for a specific user
|
||||||
|
* @param userId
|
||||||
|
* @param projectId
|
||||||
|
*/
|
||||||
|
getFilteredProjectMemberDetails = computedFn((userId: string, projectId: string) => {
|
||||||
|
const projectMember = this.getProjectMembershipByUserId(userId, projectId);
|
||||||
|
const userDetails = this.memberRoot?.memberMap?.[projectMember?.member];
|
||||||
|
if (!projectMember || !userDetails) return null;
|
||||||
|
|
||||||
|
// Check if this member passes the current filters
|
||||||
|
const allMembers = this.getProjectMemberships(projectId);
|
||||||
|
const filteredMemberIds = this.filters.getFilteredMemberIds(
|
||||||
|
allMembers,
|
||||||
|
this.memberRoot?.memberMap || {},
|
||||||
|
(member) => member.member,
|
||||||
|
projectId
|
||||||
|
);
|
||||||
|
|
||||||
|
// Return null if this user doesn't pass the filters
|
||||||
|
if (!filteredMemberIds.includes(userId)) return null;
|
||||||
|
|
||||||
|
const memberDetails: IProjectMemberDetails = {
|
||||||
|
id: projectMember.id,
|
||||||
|
role: projectMember.role,
|
||||||
|
original_role: projectMember.original_role,
|
||||||
|
member: {
|
||||||
|
...userDetails,
|
||||||
|
joining_date: projectMember.created_at ?? undefined,
|
||||||
|
},
|
||||||
|
created_at: projectMember.created_at,
|
||||||
|
};
|
||||||
|
return memberDetails;
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description fetch the list of all the members of a project
|
* @description fetch the list of all the members of a project
|
||||||
* @param workspaceSlug
|
* @param workspaceSlug
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { action, makeObservable, observable } from "mobx";
|
||||||
|
import { computedFn } from "mobx-utils";
|
||||||
|
// types
|
||||||
|
import type { IUserLite, TProjectMembership } from "@plane/types";
|
||||||
|
// local imports
|
||||||
|
import { IMemberFilters, sortProjectMembers } from "../utils";
|
||||||
|
|
||||||
|
export interface IProjectMemberFiltersStore {
|
||||||
|
// observables
|
||||||
|
filtersMap: Record<string, IMemberFilters>;
|
||||||
|
// computed actions
|
||||||
|
getFilteredMemberIds: (
|
||||||
|
members: TProjectMembership[],
|
||||||
|
memberDetailsMap: Record<string, IUserLite>,
|
||||||
|
getMemberKey: (member: TProjectMembership) => string,
|
||||||
|
projectId: string
|
||||||
|
) => string[];
|
||||||
|
// actions
|
||||||
|
updateFilters: (projectId: string, filters: Partial<IMemberFilters>) => void;
|
||||||
|
getFilters: (projectId: string) => IMemberFilters | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ProjectMemberFiltersStore implements IProjectMemberFiltersStore {
|
||||||
|
// observables
|
||||||
|
filtersMap: Record<string, IMemberFilters> = {};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
makeObservable(this, {
|
||||||
|
// observables
|
||||||
|
filtersMap: observable,
|
||||||
|
// actions
|
||||||
|
updateFilters: action,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description get filtered and sorted member ids
|
||||||
|
* @param members - array of project membership objects
|
||||||
|
* @param memberDetailsMap - map of member details by user id
|
||||||
|
* @param getMemberKey - function to get member key from membership object
|
||||||
|
* @param projectId - project id to get filters for
|
||||||
|
*/
|
||||||
|
getFilteredMemberIds = computedFn(
|
||||||
|
(
|
||||||
|
members: TProjectMembership[],
|
||||||
|
memberDetailsMap: Record<string, IUserLite>,
|
||||||
|
getMemberKey: (member: TProjectMembership) => string,
|
||||||
|
projectId: string
|
||||||
|
): string[] => {
|
||||||
|
if (!members || members.length === 0) return [];
|
||||||
|
|
||||||
|
// Apply filters and sorting
|
||||||
|
const sortedMembers = sortProjectMembers(members, memberDetailsMap, getMemberKey, this.filtersMap[projectId]);
|
||||||
|
|
||||||
|
return sortedMembers.map(getMemberKey);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
getFilters = (projectId: string) => this.filtersMap[projectId];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description update filters
|
||||||
|
* @param projectId - project id
|
||||||
|
* @param filters - partial filters to update
|
||||||
|
*/
|
||||||
|
updateFilters = (projectId: string, filters: Partial<IMemberFilters>) => {
|
||||||
|
const current = this.filtersMap[projectId] ?? {};
|
||||||
|
this.filtersMap[projectId] = { ...current, ...filters };
|
||||||
|
};
|
||||||
|
}
|
||||||
163
apps/web/core/store/member/utils.ts
Normal file
163
apps/web/core/store/member/utils.ts
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
// Types and utilities for member filtering
|
||||||
|
import type { EUserPermissions, TMemberOrderByOptions } from "@plane/constants";
|
||||||
|
import type { IUserLite, TProjectMembership } from "@plane/types";
|
||||||
|
|
||||||
|
export interface IMemberFilters {
|
||||||
|
order_by?: TMemberOrderByOptions;
|
||||||
|
roles?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to parse order key and direction
|
||||||
|
export const parseOrderKey = (orderKey?: TMemberOrderByOptions): { field: string; direction: "asc" | "desc" } => {
|
||||||
|
// Default to sorting by display_name in ascending order when no order key is provided
|
||||||
|
if (!orderKey) {
|
||||||
|
return {
|
||||||
|
field: "display_name",
|
||||||
|
direction: "asc",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDescending = orderKey.startsWith("-");
|
||||||
|
const field = isDescending ? orderKey.slice(1) : orderKey;
|
||||||
|
return {
|
||||||
|
field,
|
||||||
|
direction: isDescending ? "desc" : "asc",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Unified function to get sort key for any member type
|
||||||
|
export const getMemberSortKey = (memberDetails: IUserLite, field: string, memberRole?: string): string | Date => {
|
||||||
|
switch (field) {
|
||||||
|
case "display_name":
|
||||||
|
return memberDetails.display_name?.toLowerCase() || "";
|
||||||
|
case "full_name": {
|
||||||
|
const firstName = memberDetails.first_name || "";
|
||||||
|
const lastName = memberDetails.last_name || "";
|
||||||
|
return `${firstName} ${lastName}`.toLowerCase().trim();
|
||||||
|
}
|
||||||
|
case "email":
|
||||||
|
return memberDetails.email?.toLowerCase() || "";
|
||||||
|
case "joining_date":
|
||||||
|
return memberDetails.joining_date ? new Date(memberDetails.joining_date) : new Date(NaN);
|
||||||
|
case "role":
|
||||||
|
return (memberRole ?? "").toString().toLowerCase();
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter functions
|
||||||
|
export const filterProjectMembersByRole = (
|
||||||
|
members: TProjectMembership[],
|
||||||
|
roleFilters: string[]
|
||||||
|
): TProjectMembership[] => {
|
||||||
|
if (roleFilters.length === 0) return members;
|
||||||
|
|
||||||
|
return members.filter((member) => {
|
||||||
|
const memberRole = String(member.role ?? member.original_role ?? "");
|
||||||
|
return roleFilters.includes(memberRole);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const filterWorkspaceMembersByRole = <T extends { role: string | EUserPermissions }>(
|
||||||
|
members: T[],
|
||||||
|
roleFilters: string[]
|
||||||
|
): T[] => {
|
||||||
|
if (roleFilters.length === 0) return members;
|
||||||
|
|
||||||
|
return members.filter((member) => {
|
||||||
|
const memberRole = String(member.role ?? "");
|
||||||
|
return roleFilters.includes(memberRole);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Unified sorting function
|
||||||
|
export const sortMembers = <T>(
|
||||||
|
members: T[],
|
||||||
|
memberDetailsMap: Record<string, IUserLite>,
|
||||||
|
getMemberKey: (member: T) => string,
|
||||||
|
getMemberRole: (member: T) => string,
|
||||||
|
orderBy?: TMemberOrderByOptions
|
||||||
|
): T[] => {
|
||||||
|
if (!orderBy) return members;
|
||||||
|
|
||||||
|
const { field, direction } = parseOrderKey(orderBy);
|
||||||
|
|
||||||
|
return [...members].sort((a, b) => {
|
||||||
|
const aKey = getMemberKey(a);
|
||||||
|
const bKey = getMemberKey(b);
|
||||||
|
const aMemberDetails = memberDetailsMap[aKey];
|
||||||
|
const bMemberDetails = memberDetailsMap[bKey];
|
||||||
|
|
||||||
|
if (!aMemberDetails || !bMemberDetails) return 0;
|
||||||
|
|
||||||
|
const aRole = getMemberRole(a);
|
||||||
|
const bRole = getMemberRole(b);
|
||||||
|
|
||||||
|
const aValue = getMemberSortKey(aMemberDetails, field, aRole);
|
||||||
|
const bValue = getMemberSortKey(bMemberDetails, field, bRole);
|
||||||
|
|
||||||
|
let comparison = 0;
|
||||||
|
|
||||||
|
if (field === "joining_date") {
|
||||||
|
// For dates, we need to handle Date objects
|
||||||
|
const aDate = aValue instanceof Date ? aValue : new Date(aValue);
|
||||||
|
const bDate = bValue instanceof Date ? bValue : new Date(bValue);
|
||||||
|
comparison = aDate.getTime() - bDate.getTime();
|
||||||
|
} else {
|
||||||
|
// For strings, use localeCompare for proper alphabetical sorting
|
||||||
|
const aStr = String(aValue);
|
||||||
|
const bStr = String(bValue);
|
||||||
|
comparison = aStr.localeCompare(bStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
return direction === "desc" ? -comparison : comparison;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Specific implementations using the unified functions
|
||||||
|
export const sortProjectMembers = (
|
||||||
|
members: TProjectMembership[],
|
||||||
|
memberDetailsMap: Record<string, IUserLite>,
|
||||||
|
getMemberKey: (member: TProjectMembership) => string,
|
||||||
|
filters?: IMemberFilters
|
||||||
|
): TProjectMembership[] => {
|
||||||
|
// Apply role filtering first
|
||||||
|
const filteredMembers =
|
||||||
|
filters?.roles && filters.roles.length > 0 ? filterProjectMembersByRole(members, filters.roles) : members;
|
||||||
|
|
||||||
|
// If no order_by filter, return filtered members
|
||||||
|
if (!filters?.order_by) return filteredMembers;
|
||||||
|
|
||||||
|
// Apply sorting
|
||||||
|
return sortMembers(
|
||||||
|
filteredMembers,
|
||||||
|
memberDetailsMap,
|
||||||
|
getMemberKey,
|
||||||
|
(member) => String(member.role ?? member.original_role ?? ""),
|
||||||
|
filters.order_by
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sortWorkspaceMembers = <T extends { role: string | EUserPermissions }>(
|
||||||
|
members: T[],
|
||||||
|
memberDetailsMap: Record<string, IUserLite>,
|
||||||
|
getMemberKey: (member: T) => string,
|
||||||
|
filters?: IMemberFilters
|
||||||
|
): T[] => {
|
||||||
|
// Apply role filtering first
|
||||||
|
const filteredMembers =
|
||||||
|
filters?.roles && filters.roles.length > 0 ? filterWorkspaceMembersByRole(members, filters.roles) : members;
|
||||||
|
|
||||||
|
// If no order_by filter, return filtered members
|
||||||
|
if (!filters?.order_by) return filteredMembers;
|
||||||
|
|
||||||
|
// Apply sorting
|
||||||
|
return sortMembers(
|
||||||
|
filteredMembers,
|
||||||
|
memberDetailsMap,
|
||||||
|
getMemberKey,
|
||||||
|
(member) => String(member.role ?? ""),
|
||||||
|
filters.order_by
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { action, makeObservable, observable } from "mobx";
|
||||||
|
import { computedFn } from "mobx-utils";
|
||||||
|
// types
|
||||||
|
import type { EUserPermissions } from "@plane/constants";
|
||||||
|
import type { IUserLite } from "@plane/types";
|
||||||
|
// local imports
|
||||||
|
import { IMemberFilters, sortWorkspaceMembers } from "../utils";
|
||||||
|
|
||||||
|
// Workspace membership interface matching the store structure
|
||||||
|
interface IWorkspaceMembership {
|
||||||
|
id: string;
|
||||||
|
member: string;
|
||||||
|
role: EUserPermissions;
|
||||||
|
is_active?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWorkspaceMemberFiltersStore {
|
||||||
|
// observables
|
||||||
|
filters: IMemberFilters;
|
||||||
|
// computed actions
|
||||||
|
getFilteredMemberIds: (
|
||||||
|
members: IWorkspaceMembership[],
|
||||||
|
memberDetailsMap: Record<string, IUserLite>,
|
||||||
|
getMemberKey: (member: IWorkspaceMembership) => string
|
||||||
|
) => string[];
|
||||||
|
// actions
|
||||||
|
updateFilters: (filters: Partial<IMemberFilters>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WorkspaceMemberFiltersStore implements IWorkspaceMemberFiltersStore {
|
||||||
|
// observables
|
||||||
|
filters: IMemberFilters = {};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
makeObservable(this, {
|
||||||
|
// observables
|
||||||
|
filters: observable,
|
||||||
|
// actions
|
||||||
|
updateFilters: action,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description get filtered and sorted member ids
|
||||||
|
* @param members - array of workspace membership objects
|
||||||
|
* @param memberDetailsMap - map of member details by user id
|
||||||
|
* @param getMemberKey - function to get member key from membership object
|
||||||
|
*/
|
||||||
|
getFilteredMemberIds = computedFn(
|
||||||
|
(
|
||||||
|
members: IWorkspaceMembership[],
|
||||||
|
memberDetailsMap: Record<string, IUserLite>,
|
||||||
|
getMemberKey: (member: IWorkspaceMembership) => string
|
||||||
|
): string[] => {
|
||||||
|
if (!members || members.length === 0) return [];
|
||||||
|
|
||||||
|
// Apply filters and sorting
|
||||||
|
const sortedMembers = sortWorkspaceMembers(members, memberDetailsMap, getMemberKey, this.filters);
|
||||||
|
|
||||||
|
return sortedMembers.map(getMemberKey);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description update filters
|
||||||
|
* @param filters - partial filters to update
|
||||||
|
*/
|
||||||
|
updateFilters = (filters: Partial<IMemberFilters>) => {
|
||||||
|
this.filters = { ...this.filters, ...filters };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -12,8 +12,9 @@ import { WorkspaceService } from "@/plane-web/services";
|
||||||
import type { IRouterStore } from "@/store/router.store";
|
import type { IRouterStore } from "@/store/router.store";
|
||||||
import type { IUserStore } from "@/store/user";
|
import type { IUserStore } from "@/store/user";
|
||||||
// store
|
// store
|
||||||
import type { CoreRootStore } from "../root.store";
|
import type { CoreRootStore } from "../../root.store";
|
||||||
import type { IMemberRootStore } from ".";
|
import type { IMemberRootStore } from "../index.ts";
|
||||||
|
import { WorkspaceMemberFiltersStore, IWorkspaceMemberFiltersStore } from "./workspace-member-filters.store";
|
||||||
|
|
||||||
export interface IWorkspaceMembership {
|
export interface IWorkspaceMembership {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -26,12 +27,15 @@ export interface IWorkspaceMemberStore {
|
||||||
// observables
|
// observables
|
||||||
workspaceMemberMap: Record<string, Record<string, IWorkspaceMembership>>;
|
workspaceMemberMap: Record<string, Record<string, IWorkspaceMembership>>;
|
||||||
workspaceMemberInvitations: Record<string, IWorkspaceMemberInvitation[]>;
|
workspaceMemberInvitations: Record<string, IWorkspaceMemberInvitation[]>;
|
||||||
|
// filters store
|
||||||
|
filtersStore: IWorkspaceMemberFiltersStore;
|
||||||
// computed
|
// computed
|
||||||
workspaceMemberIds: string[] | null;
|
workspaceMemberIds: string[] | null;
|
||||||
workspaceMemberInvitationIds: string[] | null;
|
workspaceMemberInvitationIds: string[] | null;
|
||||||
memberMap: Record<string, IWorkspaceMembership> | null;
|
memberMap: Record<string, IWorkspaceMembership> | null;
|
||||||
// computed actions
|
// computed actions
|
||||||
getWorkspaceMemberIds: (workspaceSlug: string) => string[];
|
getWorkspaceMemberIds: (workspaceSlug: string) => string[];
|
||||||
|
getFilteredWorkspaceMemberIds: (workspaceSlug: string) => string[];
|
||||||
getSearchedWorkspaceMemberIds: (searchQuery: string) => string[] | null;
|
getSearchedWorkspaceMemberIds: (searchQuery: string) => string[] | null;
|
||||||
getSearchedWorkspaceInvitationIds: (searchQuery: string) => string[] | null;
|
getSearchedWorkspaceInvitationIds: (searchQuery: string) => string[] | null;
|
||||||
getWorkspaceMemberDetails: (workspaceMemberId: string) => IWorkspaceMember | null;
|
getWorkspaceMemberDetails: (workspaceMemberId: string) => IWorkspaceMember | null;
|
||||||
|
|
@ -58,6 +62,8 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||||
[workspaceSlug: string]: Record<string, IWorkspaceMembership>;
|
[workspaceSlug: string]: Record<string, IWorkspaceMembership>;
|
||||||
} = {}; // { workspaceSlug: { userId: userDetails } }
|
} = {}; // { workspaceSlug: { userId: userDetails } }
|
||||||
workspaceMemberInvitations: Record<string, IWorkspaceMemberInvitation[]> = {}; // { workspaceSlug: [invitations] }
|
workspaceMemberInvitations: Record<string, IWorkspaceMemberInvitation[]> = {}; // { workspaceSlug: [invitations] }
|
||||||
|
// filters store
|
||||||
|
filtersStore: IWorkspaceMemberFiltersStore;
|
||||||
// stores
|
// stores
|
||||||
routerStore: IRouterStore;
|
routerStore: IRouterStore;
|
||||||
userStore: IUserStore;
|
userStore: IUserStore;
|
||||||
|
|
@ -82,7 +88,8 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||||
updateMemberInvitation: action,
|
updateMemberInvitation: action,
|
||||||
deleteMemberInvitation: action,
|
deleteMemberInvitation: action,
|
||||||
});
|
});
|
||||||
|
// initialize filters store
|
||||||
|
this.filtersStore = new WorkspaceMemberFiltersStore();
|
||||||
// root store
|
// root store
|
||||||
this.routerStore = _rootStore.router;
|
this.routerStore = _rootStore.router;
|
||||||
this.userStore = _rootStore.user;
|
this.userStore = _rootStore.user;
|
||||||
|
|
@ -126,6 +133,25 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||||
return memberIds;
|
return memberIds;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description get the filtered and sorted list of all the user ids of all the members of the workspace
|
||||||
|
* @param workspaceSlug
|
||||||
|
*/
|
||||||
|
getFilteredWorkspaceMemberIds = computedFn((workspaceSlug: string) => {
|
||||||
|
let members = Object.values(this.workspaceMemberMap?.[workspaceSlug] ?? {});
|
||||||
|
//filter out bots and inactive members
|
||||||
|
members = members.filter((m) => m.is_active && !this.memberRoot?.memberMap?.[m.member]?.is_bot);
|
||||||
|
|
||||||
|
// Use filters store to get filtered member ids
|
||||||
|
const memberIds = this.filtersStore.getFilteredMemberIds(
|
||||||
|
members,
|
||||||
|
this.memberRoot?.memberMap || {},
|
||||||
|
(member) => member.member
|
||||||
|
);
|
||||||
|
|
||||||
|
return memberIds;
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description get the list of all the user ids that match the search query of all the members of the current workspace
|
* @description get the list of all the user ids that match the search query of all the members of the current workspace
|
||||||
* @param searchQuery
|
* @param searchQuery
|
||||||
|
|
@ -133,9 +159,9 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||||
getSearchedWorkspaceMemberIds = computedFn((searchQuery: string) => {
|
getSearchedWorkspaceMemberIds = computedFn((searchQuery: string) => {
|
||||||
const workspaceSlug = this.routerStore.workspaceSlug;
|
const workspaceSlug = this.routerStore.workspaceSlug;
|
||||||
if (!workspaceSlug) return null;
|
if (!workspaceSlug) return null;
|
||||||
const workspaceMemberIds = this.workspaceMemberIds;
|
const filteredMemberIds = this.getFilteredWorkspaceMemberIds(workspaceSlug);
|
||||||
if (!workspaceMemberIds) return null;
|
if (!filteredMemberIds) return null;
|
||||||
const searchedWorkspaceMemberIds = workspaceMemberIds?.filter((userId) => {
|
const searchedWorkspaceMemberIds = filteredMemberIds.filter((userId) => {
|
||||||
const memberDetails = this.getWorkspaceMemberDetails(userId);
|
const memberDetails = this.getWorkspaceMemberDetails(userId);
|
||||||
if (!memberDetails) return false;
|
if (!memberDetails) return false;
|
||||||
const memberSearchQuery = `${memberDetails.member.first_name} ${memberDetails.member.last_name} ${
|
const memberSearchQuery = `${memberDetails.member.first_name} ${memberDetails.member.last_name} ${
|
||||||
|
|
@ -15,6 +15,7 @@ export * from "./icon";
|
||||||
export * from "./instance";
|
export * from "./instance";
|
||||||
export * from "./intake";
|
export * from "./intake";
|
||||||
export * from "./issue";
|
export * from "./issue";
|
||||||
|
export * from "./members";
|
||||||
export * from "./label";
|
export * from "./label";
|
||||||
export * from "./metadata";
|
export * from "./metadata";
|
||||||
export * from "./module";
|
export * from "./module";
|
||||||
|
|
|
||||||
79
packages/constants/src/members.ts
Normal file
79
packages/constants/src/members.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
// Member property constants - Single source of truth for member spreadsheet properties
|
||||||
|
|
||||||
|
export type TMemberOrderByOptions =
|
||||||
|
| "display_name"
|
||||||
|
| "-display_name"
|
||||||
|
| "full_name"
|
||||||
|
| "-full_name"
|
||||||
|
| "email"
|
||||||
|
| "-email"
|
||||||
|
| "joining_date"
|
||||||
|
| "-joining_date"
|
||||||
|
| "role"
|
||||||
|
| "-role";
|
||||||
|
|
||||||
|
export interface IProjectMemberDisplayProperties {
|
||||||
|
full_name: boolean;
|
||||||
|
display_name: boolean;
|
||||||
|
email: boolean;
|
||||||
|
joining_date: boolean;
|
||||||
|
role: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MEMBER_PROPERTY_DETAILS: {
|
||||||
|
[key in keyof IProjectMemberDisplayProperties]: {
|
||||||
|
i18n_title: string;
|
||||||
|
ascendingOrderKey: TMemberOrderByOptions;
|
||||||
|
ascendingOrderTitle: string;
|
||||||
|
descendingOrderKey: TMemberOrderByOptions;
|
||||||
|
descendingOrderTitle: string;
|
||||||
|
iconName: string;
|
||||||
|
isSortingAllowed: boolean;
|
||||||
|
};
|
||||||
|
} = {
|
||||||
|
full_name: {
|
||||||
|
i18n_title: "project_members.full_name",
|
||||||
|
ascendingOrderKey: "full_name",
|
||||||
|
ascendingOrderTitle: "A",
|
||||||
|
descendingOrderKey: "-full_name",
|
||||||
|
descendingOrderTitle: "Z",
|
||||||
|
iconName: "User",
|
||||||
|
isSortingAllowed: true,
|
||||||
|
},
|
||||||
|
display_name: {
|
||||||
|
i18n_title: "project_members.display_name",
|
||||||
|
ascendingOrderKey: "display_name",
|
||||||
|
ascendingOrderTitle: "A",
|
||||||
|
descendingOrderKey: "-display_name",
|
||||||
|
descendingOrderTitle: "Z",
|
||||||
|
iconName: "User",
|
||||||
|
isSortingAllowed: true,
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
i18n_title: "project_members.email",
|
||||||
|
ascendingOrderKey: "email",
|
||||||
|
ascendingOrderTitle: "A",
|
||||||
|
descendingOrderKey: "-email",
|
||||||
|
descendingOrderTitle: "Z",
|
||||||
|
iconName: "Mail",
|
||||||
|
isSortingAllowed: true,
|
||||||
|
},
|
||||||
|
joining_date: {
|
||||||
|
i18n_title: "project_members.joining_date",
|
||||||
|
ascendingOrderKey: "joining_date",
|
||||||
|
ascendingOrderTitle: "Old",
|
||||||
|
descendingOrderKey: "-joining_date",
|
||||||
|
descendingOrderTitle: "New",
|
||||||
|
iconName: "Calendar",
|
||||||
|
isSortingAllowed: true,
|
||||||
|
},
|
||||||
|
role: {
|
||||||
|
i18n_title: "project_members.role",
|
||||||
|
ascendingOrderKey: "role",
|
||||||
|
ascendingOrderTitle: "Guest",
|
||||||
|
descendingOrderKey: "-role",
|
||||||
|
descendingOrderTitle: "Admin",
|
||||||
|
iconName: "Shield",
|
||||||
|
isSortingAllowed: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -2405,5 +2405,12 @@
|
||||||
"open_button": "Open navigation pane",
|
"open_button": "Open navigation pane",
|
||||||
"close_button": "Close navigation pane",
|
"close_button": "Close navigation pane",
|
||||||
"outline_floating_button": "Open outline"
|
"outline_floating_button": "Open outline"
|
||||||
|
},
|
||||||
|
"project_members": {
|
||||||
|
"full_name": "Full name",
|
||||||
|
"display_name": "Display name",
|
||||||
|
"email": "Email",
|
||||||
|
"joining_date": "Joining date",
|
||||||
|
"role": "Role"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue