diff --git a/plane-src/apps/web/ce/components/projects/settings/useProjectColumns.tsx b/plane-src/apps/web/ce/components/projects/settings/useProjectColumns.tsx index 75e6156..0f390b8 100644 --- a/plane-src/apps/web/ce/components/projects/settings/useProjectColumns.tsx +++ b/plane-src/apps/web/ce/components/projects/settings/useProjectColumns.tsx @@ -11,7 +11,7 @@ import type { IWorkspaceMember, TProjectMembership } from "@plane/types"; import { renderFormattedDate } from "@plane/utils"; // components import { MemberHeaderColumn } from "@/components/project/member-header-column"; -import { AccountTypeColumn, NameColumn } from "@/components/project/settings/member-columns"; +import { AccountTypeColumn, NameColumn, ProjectMemberActionsColumn } from "@/components/project/settings/member-columns"; // hooks import { useMember } from "@/hooks/store/use-member"; import { useUser, useUserPermissions } from "@/hooks/store/user"; @@ -21,6 +21,9 @@ export interface RowData extends Pick { member: IWorkspaceMember; } +const stickyNameHeaderClassName = "nodedc-settings-table-sticky left-0 z-20 min-w-max"; +const stickyNameCellClassName = "nodedc-settings-table-sticky left-0 z-10 min-w-max"; + type TUseProjectColumnsProps = { projectId: string; workspaceSlug: string; @@ -60,13 +63,16 @@ export const useProjectColumns = (props: TUseProjectColumnsProps) => { { key: "Full Name", content: "Full name", - thClassName: "text-left", + thClassName: stickyNameHeaderClassName, + tdClassName: stickyNameCellClassName, thRender: () => ( - +
+ +
), tdRender: (rowData: RowData) => ( { handleDisplayFilterUpdate={handleDisplayFilterUpdate} /> ), - tdRender: (rowData: RowData) =>
{rowData.member.display_name}
, + tdRender: (rowData: RowData) =>
{rowData.member.display_name}
, }, { key: "Email", @@ -100,7 +106,7 @@ export const useProjectColumns = (props: TUseProjectColumnsProps) => { handleDisplayFilterUpdate={handleDisplayFilterUpdate} /> ), - tdRender: (rowData: RowData) =>
{rowData.member.email}
, + tdRender: (rowData: RowData) =>
{rowData.member.email}
, }, { key: "Account Type", @@ -131,7 +137,22 @@ export const useProjectColumns = (props: TUseProjectColumnsProps) => { handleDisplayFilterUpdate={handleDisplayFilterUpdate} /> ), - tdRender: (rowData: RowData) =>
{renderFormattedDate(rowData?.member?.joining_date)}
, + tdRender: (rowData: RowData) => ( +
{renderFormattedDate(rowData?.member?.joining_date)}
+ ), + }, + { + key: "Actions", + content: Действия, + tdRender: (rowData: RowData) => ( + + ), }, ]; return { diff --git a/plane-src/apps/web/core/components/project/member-list-item.tsx b/plane-src/apps/web/core/components/project/member-list-item.tsx index 8c0b388..18c3653 100644 --- a/plane-src/apps/web/core/components/project/member-list-item.tsx +++ b/plane-src/apps/web/core/components/project/member-list-item.tsx @@ -77,16 +77,19 @@ export const ProjectMemberListItem = observer(function ProjectMemberListItem(pro onSubmit={() => handleRemove(removeMemberModal.member.id)} /> )} - member !== null) ?? []) as any} - keyExtractor={(rowData) => rowData?.member.id ?? ""} - tHeadClassName="border-b border-subtle" - thClassName="text-left font-medium divide-x-0 text-placeholder" - tBodyClassName="divide-y-0" - tBodyTrClassName="divide-x-0 p-4 h-[40px] text-secondary" - tHeadTrClassName="divide-x-0" - /> +
+
member !== null) ?? []) as any} + keyExtractor={(rowData) => rowData?.member.id ?? ""} + tableClassName="nodedc-settings-table-surface w-max table-auto border-separate border-spacing-0 overflow-visible" + tHeadClassName="border-b border-white/6" + thClassName="text-left font-medium divide-x-0 text-placeholder" + tBodyClassName="divide-y-0" + tBodyTrClassName="divide-x-0 h-11 px-4 text-secondary" + tHeadTrClassName="divide-x-0" + /> + ); }); diff --git a/plane-src/apps/web/core/components/project/member-list.tsx b/plane-src/apps/web/core/components/project/member-list.tsx index 187452e..61ad139 100644 --- a/plane-src/apps/web/core/components/project/member-list.tsx +++ b/plane-src/apps/web/core/components/project/member-list.tsx @@ -116,7 +116,7 @@ export const ProjectMemberList = observer(function ProjectMemberList(props: TPro {!projectMemberIds ? ( ) : ( -
+
{searchedProjectMembers.length !== 0 && ( { + if (!isOpen || !workspaceSlug || !projectId) return; + + void Promise.all([ + fetchWorkspaceMembers(workspaceSlug.toString()), + fetchProjectMembers(workspaceSlug.toString(), projectId.toString(), true), + ]).catch(console.error); + }, [fetchProjectMembers, fetchWorkspaceMembers, isOpen, projectId, workspaceSlug]); + const onSubmit = async (formData: FormValues) => { if (!workspaceSlug || !projectId || isSubmitting) return; @@ -133,19 +142,22 @@ export const SendProjectInvitationModal = observer(function SendProjectInvitatio const memberDetails = getWorkspaceMemberDetails(userId); if (!memberDetails?.member) return; + + const displayName = memberDetails.member.display_name || memberDetails.member.email || ""; + const fullName = [memberDetails.member.first_name, memberDetails.member.last_name].filter(Boolean).join(" "); + const secondaryLabel = fullName && fullName !== displayName ? ` (${fullName})` : ""; + return { - value: `${memberDetails?.member.id}`, - query: `${memberDetails?.member.first_name} ${ - memberDetails?.member.last_name - } ${memberDetails?.member.display_name.toLowerCase()}`, + value: `${memberDetails.member.id}`, + query: `${displayName} ${fullName} ${memberDetails.member.email ?? ""}`.toLowerCase(), content: (
- +
- {memberDetails?.member.display_name} ( - {memberDetails?.member.first_name + " " + memberDetails?.member.last_name}) + {displayName} + {secondaryLabel}
), @@ -226,8 +238,9 @@ export const SendProjectInvitationModal = observer(function SendProjectInvitatio EUserPermissions[newValue as keyof typeof EUserPermissions] ); }} + noResultsMessage="Нет доступных участников workspace" options={options} - optionsClassName="w-48" + optionsClassName="min-w-[24rem]" /> ); }} @@ -249,25 +262,29 @@ export const SendProjectInvitationModal = observer(function SendProjectInvitatio parseInt(key) <= (currentProjectRole ?? EUserPermissions.GUEST)) - .map(([key, label]) => ({ - key, - title: label, - isChecked: String(field.value) === key, - onClick: () => - setValue( - `members.${index}.role`, - EUserPermissions[ROLE[parseInt(key)].toUpperCase() as keyof typeof EUserPermissions] - ), - }))} + .map(([key, label]) => { + const roleKey = parseInt(key) as keyof typeof ROLE; + + return { + key, + title: label, + isChecked: String(field.value) === key, + onClick: () => + setValue( + `members.${index}.role`, + EUserPermissions[ROLE[roleKey].toUpperCase() as keyof typeof EUserPermissions] + ), + }; + })} menuButton={ -
+
{field.value ? ROLE[field.value] : t("project_invitation_modal.select_role")}
} - menuButtonWrapperClassName="w-24" + menuButtonWrapperClassName="min-w-[7.5rem]" /> )} /> @@ -302,10 +319,10 @@ export const SendProjectInvitationModal = observer(function SendProjectInvitatio {t("common.add_more")}
- - + )} +
); } @@ -112,7 +118,7 @@ export const AccountTypeColumn = observer(function AccountTypeColumn(props: Acco formState: { errors }, } = useForm(); // derived values - const roleLabel = ROLE[rowData.original_role ?? EUserPermissions.GUEST]; + const roleLabel = getProjectRoleLabel(rowData.original_role ?? EUserPermissions.GUEST); const isCurrentUser = currentUser?.id === rowData.member.id; const isRowDataWorkspaceAdmin = [EUserPermissions.ADMIN].includes( Number(getWorkspaceMemberDetails(rowData.member.id)?.role) ?? EUserPermissions.GUEST @@ -154,39 +160,55 @@ export const AccountTypeColumn = observer(function AccountTypeColumn(props: Acco rules={{ required: "Role is required." }} render={() => ( ({ - key, - title: label, - isChecked: String(rowData.original_role) === key, - onClick: async () => { - if (!workspaceSlug) return; - await updateMemberRole(workspaceSlug.toString(), projectId.toString(), rowData.member.id, key).catch( - (err) => { + dropdownClassName="!p-2" + dropdownContentClassName="!w-44" + options={Object.entries(checkCurrentOptionWorkspaceRole(rowData.member.id)).map(([key]) => { + const role = Number(key) as EUserPermissions; + + return { + key, + title: getProjectRoleLabel(role), + isChecked: String(rowData.original_role) === key, + onClick: async () => { + if (!workspaceSlug) return; + await updateMemberRole( + workspaceSlug.toString(), + projectId.toString(), + rowData.member.id, + Number(key) as EUserProjectRoles + ).catch((err) => { console.log(err, "err"); const error = err.error; const errorString = Array.isArray(error) ? error[0] : error; setToast({ type: TOAST_TYPE.ERROR, - title: "You can’t change this role yet.", - message: errorString ?? "An error occurred while updating member role. Please try again.", + title: "Ошибка", + message: errorString ?? "Не удалось обновить роль участника. Попробуйте ещё раз.", }); - } - ); - }, - }))} - menuButton={ -
- {roleLabel} + }); + }, + }; + })} + menuButton={({ open }) => ( +
+ {roleLabel} +
- } - menuButtonWrapperClassName={`w-32 rounded-md p-0 !justify-start !px-0 hover:bg-surface-1 ${errors.role ? "border-danger-strong" : "border-none"}`} + )} + menuButtonWrapperClassName="flex rounded-[1.25rem] border-0 outline-none" + placement="bottom-end" /> )} /> ) : ( -
- {roleLabel} +
+ {roleLabel}
)} diff --git a/plane-src/packages/ui/src/dropdowns/custom-search-select.tsx b/plane-src/packages/ui/src/dropdowns/custom-search-select.tsx index f62377c..4cee396 100644 --- a/plane-src/packages/ui/src/dropdowns/custom-search-select.tsx +++ b/plane-src/packages/ui/src/dropdowns/custom-search-select.tsx @@ -146,7 +146,7 @@ export function CustomSearchSelect(props: ICustomSearchSelectProps) {