JFIFxxC      C  " }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr{ gilour

File "workspace-members-dialog.tsx"

Full Path: /home/markqprx/iniasli.pro/client/workspace/workspace-members-dialog.tsx
File size: 13.09 KB
MIME-type: text/plain
Charset: utf-8

import {useEffect, useState} from 'react';
import {AnimatePresence, m} from 'framer-motion';
import {useWorkspaceWithMembers} from './requests/workspace-with-members';
import {ProgressCircle} from '../ui/progress/progress-circle';
import {Workspace} from './types/workspace';
import {GroupIcon} from '../icons/material/Group';
import {WorkspaceMember} from './types/workspace-member';
import {useAuth} from '../auth/use-auth';
import {
  ChipField,
  ChipValue,
} from '../ui/forms/input-field/chip-field/chip-field';
import {useValueLists} from '../http/value-lists';
import {Button} from '../ui/buttons/button';
import {ArrowDropDownIcon} from '../icons/material/ArrowDropDown';
import {useInviteMembers} from './requests/invite-members';
import {WorkspaceInvite} from './types/workspace-invite';
import {ConfirmationDialog} from '../ui/overlays/dialog/confirmation-dialog';
import {useResendInvite} from './requests/resend-invite';
import {isEmail} from '../utils/string/is-email';
import {ButtonSize} from '../ui/buttons/button-size';
import {useChangeRole} from './requests/change-role';
import {IconButton} from '../ui/buttons/icon-button';
import {useRemoveMember} from './requests/remove-member';
import {CloseIcon} from '../icons/material/Close';
import {ExitToAppIcon} from '../icons/material/ExitToApp';
import {toast} from '../ui/toast/toast';
import {DialogTrigger} from '../ui/overlays/dialog/dialog-trigger';
import {Menu, MenuItem, MenuTrigger} from '../ui/navigation/menu/menu-trigger';
import {useDialogContext} from '../ui/overlays/dialog/dialog-context';
import {Dialog} from '../ui/overlays/dialog/dialog';
import {DialogHeader} from '../ui/overlays/dialog/dialog-header';
import {DialogBody} from '../ui/overlays/dialog/dialog-body';
import {Trans} from '../i18n/trans';
import {useTrans} from '../i18n/use-trans';
import {message} from '../i18n/message';
import {LeaveWorkspaceConfirmation} from './leave-workspace-confirmation';

interface WorkspaceMembersDialogProps {
  workspace: Workspace;
}
export function WorkspaceMembersDialog({
  workspace,
}: WorkspaceMembersDialogProps) {
  const {data, isLoading} = useWorkspaceWithMembers(workspace.id);
  return (
    <Dialog size="lg">
      <DialogHeader>
        <Trans message="Manage workspace members" />
      </DialogHeader>
      <DialogBody>
        {isLoading ? (
          <div className="flex items-center justify-center min-h-[238px]">
            <ProgressCircle isIndeterminate aria-label="Loading workspace..." />
          </div>
        ) : (
          <Manager workspace={data!.workspace} />
        )}
      </DialogBody>
    </Dialog>
  );
}

interface ManagerProps {
  workspace: Workspace;
}
function Manager({workspace}: ManagerProps) {
  const can = usePermissions(workspace);
  const members: (WorkspaceMember | WorkspaceInvite)[] = [
    ...(workspace.members || []),
    ...(workspace.invites || []),
  ];

  return (
    <div>
      {can.invite && <InviteChipField workspace={workspace} />}
      <div className="flex items-center gap-10 mb-14 text-base">
        <GroupIcon className="icon-sm" />
        <Trans
          message="Members of `:workspace`"
          values={{workspace: workspace.name}}
        />
      </div>
      <AnimatePresence initial={false}>
        {members.map(member => (
          <MemberListItem
            key={`${member.model_type}.${member.id}`}
            workspace={workspace}
            member={member}
          />
        ))}
      </AnimatePresence>
    </div>
  );
}

interface MemberListItemProps {
  member: WorkspaceMember | WorkspaceInvite;
  workspace: Workspace;
}
function MemberListItem({workspace, member}: MemberListItemProps) {
  return (
    <m.div
      initial={{x: '-100%', opacity: 0}}
      animate={{x: 0, opacity: 1}}
      exit={{x: '100%', opacity: 0}}
      transition={{type: 'tween', duration: 0.125}}
      className="flex items-start text-sm gap-14 mb-20"
      key={`${member.model_type}.${member.id}`}
    >
      <img
        className="w-36 h-36 rounded flex-shrink-0"
        src={member.avatar}
        alt=""
      />
      <div className="md:flex flex-auto items-center justify-between gap-14 min-w-0">
        <div className="overflow-hidden mb-10 md:mb-0 md:mr-10">
          <div className="flex items-center justify-start gap-6">
            <div className="overflow-hidden text-ellipsis whitespace-nowrap">
              {member.display_name}
            </div>
            <MemberDisplayNameAppend workspace={workspace} member={member} />
          </div>
          <div className="text-muted">{member.email}</div>
        </div>
        <MemberActions workspace={workspace} member={member} />
      </div>
    </m.div>
  );
}

function usePermissions(workspace: Workspace) {
  const {user: authUser} = useAuth();
  const response = {update: false, invite: false, delete: false};
  const permissions = ['update', 'invite', 'delete'] as const;
  const authMember = workspace.members?.find(mb => mb.id === authUser?.id);

  if (authMember) {
    permissions.forEach(permission => {
      response[permission] =
        authMember.is_owner ||
        !!authMember.permissions?.find(
          p => p.name === `workspace_members.${permission}`,
        );
    });
  }

  return response;
}

interface MemberActionsProps {
  workspace: Workspace;
  member: WorkspaceMember | WorkspaceInvite;
}
function MemberActions({workspace, member}: MemberActionsProps) {
  const [selectedRole, setSelectedRole] = useState<number>(member.role_id);
  const changeRole = useChangeRole();
  const {user} = useAuth();

  const can = usePermissions(workspace);
  const isOwner = member.model_type === 'member' && member.is_owner;
  const isCurrentUser =
    member.model_type === 'member' && user?.id === member.id;

  const roleSelector =
    !can.update || isOwner || isCurrentUser ? (
      <div className="text-muted ml-auto first:capitalize">
        <Trans message={member.role_name} />
      </div>
    ) : (
      <RoleMenuTrigger
        className="ml-auto flex-shrink-0"
        size="xs"
        value={selectedRole}
        isDisabled={changeRole.isPending}
        onChange={roleId => {
          setSelectedRole(roleId);
          changeRole.mutate({
            roleId,
            workspaceId: workspace.id,
            member,
          });
        }}
      />
    );

  return (
    <>
      {roleSelector}
      {!isOwner && (isCurrentUser || can.delete) && (
        <RemoveMemberButton
          type={isCurrentUser ? 'leave' : 'remove'}
          member={member}
          workspace={workspace}
        />
      )}
    </>
  );
}

interface InviteChipFieldProps {
  workspace: Workspace;
}
function InviteChipField({workspace}: InviteChipFieldProps) {
  const {trans} = useTrans();
  const [chips, setChips] = useState<ChipValue[]>([]);
  const allEmailsValid = chips.every(chip => !chip.invalid);
  const displayWith = (chip: ChipValue) => chip.description || chip.name;
  const [selectedRole, setSelectedRole] = useState<number>();
  const inviteMembers = useInviteMembers();
  const {data} = useValueLists(['workspaceRoles']);

  useEffect(() => {
    if (!selectedRole && data?.workspaceRoles?.length) {
      setSelectedRole(data.workspaceRoles[0].id);
    }
  }, [data, selectedRole]);

  return (
    <div className="mb-30">
      <ChipField
        value={chips}
        onChange={setChips}
        displayWith={displayWith}
        validateWith={chip => {
          const invalid = !isEmail(chip.description);
          return {
            ...chip,
            invalid,
            errorMessage: invalid
              ? trans({message: 'Not a valid email'})
              : undefined,
          };
        }}
        placeholder={trans({message: 'Enter email addresses'})}
        label={<Trans message="Invite people" />}
      />
      <div className="flex items-center gap-14 justify-between mt-14">
        <RoleMenuTrigger onChange={setSelectedRole} value={selectedRole} />
        {chips.length && selectedRole ? (
          <Button
            variant="flat"
            color="primary"
            size="sm"
            disabled={inviteMembers.isPending || !allEmailsValid}
            onClick={() => {
              inviteMembers.mutate(
                {
                  emails: chips.map(c => displayWith(c)),
                  roleId: selectedRole,
                  workspaceId: workspace.id,
                },
                {
                  onSuccess: () => {
                    setChips([]);
                  },
                },
              );
            }}
          >
            <Trans message="Invite" />
          </Button>
        ) : null}
      </div>
    </div>
  );
}

interface RemoveMemberButtonProps {
  member: WorkspaceMember | WorkspaceInvite;
  workspace: Workspace;
  type: 'leave' | 'remove';
}
function RemoveMemberButton({
  member,
  workspace,
  type,
}: RemoveMemberButtonProps) {
  const removeMember = useRemoveMember();
  const {close} = useDialogContext();
  return (
    <DialogTrigger
      type="modal"
      onClose={isConfirmed => {
        if (isConfirmed) {
          removeMember.mutate({
            workspaceId: workspace.id,
            memberId: member.id,
            memberType: member.model_type,
          });
          if (type === 'leave') {
            close();
            toast(message('Left workspace'));
          }
        }
      }}
    >
      <IconButton
        size="md"
        className="text-muted flex-shrink-0"
        disabled={removeMember.isPending}
      >
        {type === 'leave' ? <ExitToAppIcon /> : <CloseIcon />}
      </IconButton>
      {type === 'leave' ? (
        <LeaveWorkspaceConfirmation />
      ) : (
        <RemoveMemberConfirmation member={member} />
      )}
    </DialogTrigger>
  );
}

interface RemoveMemberConfirmationProps {
  member: WorkspaceMember | WorkspaceInvite;
}
function RemoveMemberConfirmation({member}: RemoveMemberConfirmationProps) {
  return (
    <ConfirmationDialog
      isDanger
      title={<Trans message="Remove member" />}
      body={
        <div>
          <Trans
            message="Are you sure you want to remove `:name`?"
            values={{name: member.display_name}}
          />
          <div className="font-semibold mt-8">
            <Trans
              message="All workspace resources created by `:name` will be transferred to workspace owner."
              values={{
                name: member.display_name,
              }}
            />
          </div>
        </div>
      }
      confirm={<Trans message="Remove" />}
    />
  );
}

interface RoleMenuTriggerProps {
  onChange: (value: number) => void;
  value?: number;
  size?: ButtonSize;
  className?: string;
  isDisabled?: boolean;
}
function RoleMenuTrigger({
  value,
  onChange,
  size = 'xs',
  className,
  isDisabled,
}: RoleMenuTriggerProps) {
  const {data} = useValueLists(['workspaceRoles']);
  const role = data?.workspaceRoles?.find(r => r.id === value);
  if (!value || !role || !data?.workspaceRoles) return null;

  return (
    <MenuTrigger
      selectionMode="single"
      selectedValue={value}
      onSelectionChange={newValue => {
        onChange(newValue as number);
      }}
    >
      <Button
        className={className}
        size={size}
        variant="flat"
        color="chip"
        disabled={isDisabled}
        endIcon={<ArrowDropDownIcon />}
      >
        <Trans message={role.name} />
      </Button>
      <Menu>
        {data.workspaceRoles.map(r => (
          <MenuItem value={r.id} key={r.id} description={r.description}>
            <Trans message={r.name} />
          </MenuItem>
        ))}
      </Menu>
    </MenuTrigger>
  );
}

interface MemberDisplayNameAppendProps {
  member: WorkspaceMember | WorkspaceInvite;
  workspace: Workspace;
}
function MemberDisplayNameAppend({
  member,
  workspace,
}: MemberDisplayNameAppendProps) {
  const {user} = useAuth();
  const can = usePermissions(workspace);

  if (user?.id === member.id) {
    return (
      <div className="font-medium">
        (<Trans message="You" />)
      </div>
    );
  }
  if (member.model_type === 'invite') {
    return (
      <div className="flex items-center gap-4">
        <div>·</div>
        <div className="font-medium">
          <Trans message="Invited" />
        </div>
        {can.invite ? (
          <>
            <div>·</div>
            <ResendInviteDialogTrigger member={member} workspace={workspace} />
          </>
        ) : null}
      </div>
    );
  }
  return null;
}

function ResendInviteDialogTrigger({
  member,
  workspace,
}: MemberDisplayNameAppendProps) {
  const resendInvite = useResendInvite();
  return (
    <DialogTrigger
      type="modal"
      onClose={isConfirmed => {
        if (isConfirmed) {
          resendInvite.mutate({
            workspaceId: workspace.id,
            inviteId: member.id as string,
          });
        }
      }}
    >
      <Button
        variant="link"
        size="sm"
        color="primary"
        disabled={resendInvite.isPending}
      >
        <Trans message="Resend invite" />
      </Button>
      <ConfirmationDialog
        title={<Trans message="Resend invite" />}
        body={
          <Trans message="Are you sure you want to send this invite again?" />
        }
        confirm={<Trans message="Send" />}
      />
    </DialogTrigger>
  );
}