import { DoneIcon, EmailIcon, PlusIcon, TrashIcon, UserRemoveIcon, } from "outline-icons"; import { toast } from "sonner"; import { UserRole } from "@shared/types"; import { UserRoleHelper } from "@shared/utils/UserRoleHelper"; import stores from "~/stores"; import User from "~/models/User"; import Invite from "~/scenes/Invite"; import { UserChangeAvatarDialog, UserChangeEmailDialog, UserChangeNameDialog, UserChangeRoleDialog, UserDeleteDialog, UserSuspendDialog, } from "~/components/UserDialogs"; import { createAction, createActionWithChildren } from "~/actions"; import { dialogActionFactory, everyActiveModel, performBatchOnActiveModels, } from "~/actions/definitions/common"; import { UserSection } from "~/actions/sections"; import type { ActionContext } from "~/types"; export const inviteUser = dialogActionFactory({ analyticsName: "Invite people", section: UserSection, name: (t) => `${t("Invite people")}…`, title: (t) => t("Invite to workspace"), content: (onSubmit) => , icon: , keywords: "team member workspace user", width: "500px", visible: () => stores.policies.abilities(stores.auth.team?.id || "").inviteUser, }); /** * Creates an action that sets the role of the active users, marked as selected * when it is the role they already have. * * @param role - the role to assign. * @returns an action for use in menus. */ const updateUserRoleActionFactory = (role: UserRole) => createAction({ name: ({ t }) => UserRoleHelper.displayName(role, t), analyticsName: "Update user role", section: UserSection, selected: (context) => everyActiveModel(context, User, (user) => user.role === role), visible: (context) => everyActiveModel(context, User, (user) => { if (user.role === role) { return true; } const can = context.stores.policies.abilities(user.id); return UserRoleHelper.isRoleHigher(role, user.role) ? can.promote : can.demote; }), perform: ({ getActiveModels, stores: rootStore, t }) => { const users = getActiveModels(User).filter((user) => user.role !== role); if (!users.length) { return; } rootStore.dialogs.openModal({ title: t("Update role"), content: ( ), }); }, }); export const changeUserRole = createActionWithChildren({ name: ({ t }) => t("Change role"), analyticsName: "Change user role", section: UserSection, visible: (context) => everyActiveModel(context, User, (user) => { const can = context.stores.policies.abilities(user.id); return can.demote || can.promote; }), children: [UserRole.Admin, UserRole.Member, UserRole.Viewer].map((role) => updateUserRoleActionFactory(role) ), }); export const changeUserAvatar = dialogActionFactory({ analyticsName: "Change user avatar", section: UserSection, title: (t) => t("Change profile picture"), content: (onSubmit, context) => { const user = singleActiveUser(context); return user ? ( ) : null; }, visible: (context) => canUpdateSingleUser(context), }); export const changeUserName = dialogActionFactory({ analyticsName: "Change user name", section: UserSection, title: (t) => t("Change name"), content: (onSubmit, context) => { const user = singleActiveUser(context); return user ? ( ) : null; }, visible: (context) => canUpdateSingleUser(context), }); export const changeUserEmail = dialogActionFactory({ analyticsName: "Change user email", section: UserSection, title: (t) => t("Change email"), content: (onSubmit, context) => { const user = singleActiveUser(context); return user ? ( ) : null; }, visible: (context) => canUpdateSingleUser(context), }); export const suspendUser = dialogActionFactory({ analyticsName: "Suspend user", section: UserSection, name: (t) => `${t("Suspend user")}…`, title: (t, { getActiveModels }) => getActiveModels(User).length === 1 ? t("Suspend user") : t("Suspend {{ count }} user", { count: getActiveModels(User).length, }), content: (onSubmit, { getActiveModels }) => ( ), icon: , iconInContextMenu: false, dangerous: true, visible: (context) => everyActiveModel( context, User, (user) => !user.isInvited && !user.isSuspended && context.stores.policies.abilities(user.id).suspend ), }); export const activateUser = createAction({ name: ({ t }) => t("Activate user"), analyticsName: "Activate user", section: UserSection, icon: , iconInContextMenu: false, visible: (context) => everyActiveModel( context, User, (user) => !user.isInvited && user.isSuspended && context.stores.policies.abilities(user.id).activate ), perform: (context) => performBatchOnActiveModels( context, User, (user) => context.stores.users.activate(user), (users, succeeded, t) => users.length === 1 ? undefined : t("{{ count }} user activated", { count: succeeded }) ), }); export const resendInvite = createAction({ name: ({ t }) => t("Resend invite"), analyticsName: "Resend invite", section: UserSection, icon: , iconInContextMenu: false, visible: (context) => everyActiveModel( context, User, (user) => context.stores.policies.abilities(user.id).resendInvite ), perform: async (context) => { const users = context.getActiveModels(User); const succeeded = await performBatchOnActiveModels( context, User, (user) => context.stores.users.resendInvite(user), (models, count, t) => models.length === 1 ? t("Invite was resent to {{ userName }}", { userName: models[0].name, }) : t("{{ count }} invite resent", { count }) ); if (succeeded < users.length) { toast.error( context.t("Could not resend {{ count }} invite", { count: users.length - succeeded, }) ); } }, }); export const revokeInvite = createAction({ name: ({ t }) => `${t("Revoke invite")}…`, analyticsName: "Revoke invite", section: UserSection, icon: , iconInContextMenu: false, dangerous: true, visible: (context) => everyActiveModel(context, User, (user) => user.isInvited), perform: (context) => performBatchOnActiveModels( context, User, (user) => context.stores.users.delete(user), (users, succeeded, t) => users.length === 1 ? undefined : t("{{ count }} invite revoked", { count: succeeded }) ), }); export const deleteUser = dialogActionFactory({ analyticsName: "Delete user", section: UserSection, name: (t) => `${t("Delete user")}…`, title: (t, { getActiveModels }) => getActiveModels(User).length === 1 ? t("Delete user") : t("Delete {{ count }} user", { count: getActiveModels(User).length }), content: (onSubmit, { getActiveModels }) => ( ), icon: , iconInContextMenu: false, keywords: "leave", dangerous: true, visible: (context) => everyActiveModel( context, User, (user) => context.stores.policies.abilities(user.id).delete ), }); export const rootUserActions = [inviteUser]; /** The active user, when a single one is active. */ const singleActiveUser = ({ getActiveModels }: ActionContext) => { const users = getActiveModels(User); return users.length === 1 ? users[0] : undefined; }; const canUpdateSingleUser = (context: ActionContext) => { const user = singleActiveUser(context); return !!user && context.stores.policies.abilities(user.id).update; };