change(ui) - user limit check and other fixes

This commit is contained in:
Shekar Siri 2022-05-05 13:11:20 +02:00
parent 918f7e9d86
commit dfce25709a
9 changed files with 121 additions and 26 deletions

View file

@ -168,8 +168,11 @@ class Sites extends React.PureComponent {
</div>
<div className={ stl.list }>
<div key={ _site.key } className="grid grid-cols-12 gap-2 w-full group hover:bg-active-blue items-center border-b px-2 py-3">
<div>Name</div>
<div className="grid grid-cols-12 gap-2 w-full items-center border-b px-2 py-3 font-medium">
<div className="col-span-4">Name</div>
<div className="col-span-4">Key</div>
<div className="col-span-4"></div>
</div>
{
sites.map(_site => (
@ -191,7 +194,7 @@ class Sites extends React.PureComponent {
</div>
</div>
<div className="col-span-4">
<span className="px-2 py-1 bg-gray-lightest rounded border color-teal text-sm">{_site.projectKey}</span>
<span className="px-2 py-1 bg-gray-lightest rounded border text-sm">{_site.projectKey}</span>
</div>
{/* <div className="ml-3 flex items-center">
<div>{ _site.host }</div>

View file

@ -8,23 +8,26 @@ import { useModal } from 'App/components/Modal';
import UserForm from './components/UserForm';
import { connect } from 'react-redux';
const PERMISSION_WARNING = 'You dont have the permissions to perform this action.';
const LIMIT_WARNING = 'You have reached users limit.';
interface Props {
account: any;
isEnterprise: boolean;
limits: any;
}
function UsersView(props: Props) {
const { account, isEnterprise } = props;
const { account, limits, isEnterprise } = props;
const { userStore, roleStore } = useStore();
const userCount = useObserver(() => userStore.list.length);
const roles = useObserver(() => roleStore.list);
const { showModal } = useModal();
const reachedLimit = (limits.remaining + userStore.modifiedCount) <= 0;
const isAdmin = account.admin || account.superAdmin;
// const canAddUsers = isAdmin && userCount !== 0; // TODO fetch limits and disable button if limit reached
const editHandler = (user = null) => {
userStore.initUser(user).then(() => {
showModal(<UserForm />, { right: true });
showModal(<UserForm />, {});
});
}
@ -34,6 +37,8 @@ function UsersView(props: Props) {
}
}, []);
console.log('remaining', limits, reachedLimit)
return (
<div>
<div className="flex items-center justify-between">
@ -45,7 +50,7 @@ function UsersView(props: Props) {
<div>
<IconButton
id="add-button"
// disabled={ !canAddUsers }
disabled={ reachedLimit || !isAdmin }
circle
icon="plus"
outline
@ -54,8 +59,7 @@ function UsersView(props: Props) {
/>
</div>
}
// disabled={ canAddUsers }
// content={ `${ !canAddUsers ? (!isAdmin ? PERMISSION_WARNING : LIMIT_WARNING) : 'Add team member' }` }
content={ `${ !isAdmin ? PERMISSION_WARNING : (reachedLimit ? LIMIT_WARNING : 'Add team member') }` }
size="tiny"
inverted
position="top left"
@ -74,4 +78,6 @@ function UsersView(props: Props) {
export default connect(state => ({
account: state.getIn([ 'user', 'account' ]),
isEnterprise: state.getIn([ 'user', 'client', 'edition' ]) === 'ee',
limits: state.getIn([ 'user', 'account', 'limits', 'teamMember' ]),
// remaining: this.props.account.limits.teamMember.remaining
}))(UsersView);

View file

@ -12,9 +12,9 @@ interface Props {
}
function UserForm(props: Props) {
const { isSmtp = false, isEnterprise = false } = props;
const isSaving = false;
const { hideModal } = useModal();
const { userStore, roleStore } = useStore();
const isSaving = useObserver(() => userStore.saving);
const user: any = useObserver(() => userStore.instance);
const roles = useObserver(() => roleStore.list.filter(r => r.isProtected ? user.isSuperAdmin : true).map(r => ({ label: r.name, value: r.roleId })));
@ -98,7 +98,7 @@ function UserForm(props: Props) {
<div className="form-group">
<label htmlFor="role">{ 'Role' }</label>
<Select
placeholder="Role"
placeholder="Selct Role"
selection
options={ roles }
name="roleId"
@ -115,12 +115,12 @@ function UserForm(props: Props) {
<div className="flex items-center mr-auto">
<Button
onClick={ onSave }
disabled={ !user.valid() }
disabled={ !user.valid() || isSaving }
loading={ isSaving }
primary
marginRight
>
{ user.exists() ? 'Update' : 'Invite' }
{ user.exists() ? 'Update' : 'Invite' }
</Button>
<Button
data-hidden={ !user.exists() }
@ -143,7 +143,7 @@ function UserForm(props: Props) {
{ !user.isJoined && user.invitationLink &&
<CopyButton
content={user.invitationLink}
className="link"
className="link mt-4"
btnText="Copy invite link"
/>
}

View file

@ -31,7 +31,7 @@ function UserList(props) {
const editHandler = (user) => {
userStore.initUser(user).then(() => {
showModal(<UserForm />, { right: true });
showModal(<UserForm />, { });
});
}
@ -42,12 +42,18 @@ function UserList(props) {
<div className="grid grid-cols-12 p-3 border-b font-medium">
<div className="col-span-5">Name</div>
<div className="col-span-3">Role</div>
<div className="col-span-"></div>
<div className="col-span-2">Created On</div>
<div className="col-span-2"></div>
</div>
{sliceListPerPage(list, userStore.page - 1, userStore.pageSize).map((user: any) => (
<div key={user.id} className="">
<UserListItem user={user} editHandler={() => editHandler(user)} />
<UserListItem
user={user}
editHandler={() => editHandler(user)}
generateInvite={() => userStore.generateInviteCode(user.userId)}
copyInviteCode={() => userStore.copyInviteCode(user.userId)}
/>
</div>
))}
</div>

View file

@ -1,24 +1,52 @@
import React from 'react';
import { Icon } from 'UI';
import { Icon, CopyButton } from 'UI';
import { checkForRecent } from 'App/date';
interface Props {
user: any;
editHandler?: any;
generateInvite?: any;
copyInviteCode?: any;
}
function UserListItem(props: Props) {
const { user, editHandler = () => {} } = props;
const {
user,
editHandler = () => {},
generateInvite = () => {},
copyInviteCode = () => {},
} = props;
return (
<div className="grid grid-cols-12 p-3 py-4 border-b items-center select-none hover:bg-active-blue group">
<div className="col-span-5">{user.name}</div>
<div className="col-span-5">
{user.name}
{user.isAdmin && <span className="ml-2 px-2 py-1 bg-gray-lightest rounded border text-sm capitalize">Admin</span>}
{user.isSuperAdmin && <span className="ml-2 px-2 py-1 bg-gray-lightest rounded border text-sm capitalize">Owner</span>}
</div>
<div className="col-span-3">
<span className="px-2 py-1 bg-gray-lightest rounded border color-teal text-sm capitalize">
<span className="px-2 py-1 bg-gray-lightest rounded border text-sm capitalize">
{user.roleName}
</span>
</div>
<div className="col-span-4 justify-self-end invisible group-hover:visible">
<button className='' onClick={editHandler}>
<Icon name="pencil" color="teal" size="16" />
</button>
<div className="col-span-2">
<span>{user.createdAt && checkForRecent(user.createdAt, 'LLL dd, yyyy, hh:mm a')}</span>
</div>
{/* invisible */}
<div className="col-span-2 justify-self-end group-hover:visible">
<div className="grid grid-cols-2 gap-3 items-center justify-end">
{!user.isJoined && user.invitationLink ? (
<button className='' onClick={copyInviteCode}>
<Icon name="link-45deg" size="16" color="teal"/>
</button>
) : <div/>}
{!user.isJoined && user.isExpiredInvite && (
<button className='' onClick={generateInvite}>
<Icon name="link-45deg" size="16" color="red"/>
</button>
)}
<button className='' onClick={editHandler}>
<Icon name="pencil" color="teal" size="16" />
</button>
</div>
</div>
</div>
);

View file

@ -39,7 +39,7 @@ export default function({ plain = false, options, isSearchable = false, defaultV
return { ...provided, opacity, transition };
}
}
const defaultSelected = defaultValue ? options.find(x => x.value === defaultValue) : options[0];
const defaultSelected = defaultValue ? options.find(x => x.value === defaultValue) : null;
return (
<Select
options={options}

View file

@ -14,6 +14,8 @@ export interface IUser {
roleName: string
invitationLink: string
updateKey(key: string, value: any): void
fromJson(json: any): IUser
toJson(): any
toSave(): any
@ -79,6 +81,9 @@ export default class User implements IUser {
admin: this.isAdmin,
superAdmin: this.isSuperAdmin,
roleId: this.roleId,
joined: this.isJoined,
invitationLink: this.invitationLink,
expiredInvitation: this.isExpiredInvite,
}
}

View file

@ -2,6 +2,7 @@ import { makeAutoObservable, observable, action } from "mobx"
import User, { IUser } from "./types/user";
import { userService } from "App/services";
import { toast } from 'react-toastify';
import copy from 'copy-to-clipboard';
export default class UserStore {
list: IUser[] = [];
@ -9,6 +10,7 @@ export default class UserStore {
page: number = 1;
pageSize: number = 10;
searchQuery: string = "";
modifiedCount: number = 0;
loading: boolean = false;
saving: boolean = false;
@ -87,6 +89,7 @@ export default class UserStore {
userService.save(user).then(response => {
const newUser = new User().fromJson(response);
if (wasCreating) {
this.modifiedCount -= 1;
this.list.push(new User().fromJson(newUser));
toast.success('User created successfully');
} else {
@ -108,6 +111,7 @@ export default class UserStore {
return new Promise((resolve, reject) => {
userService.delete(userId)
.then(response => {
this.modifiedCount += 1;
this.list = this.list.filter(user => user.userId !== userId);
resolve(response);
}).catch(error => {
@ -118,4 +122,41 @@ export default class UserStore {
});
});
}
copyInviteCode(userId: string): void {
const content = this.list.find(u => u.userId === userId)?.invitationLink;
if (content) {
copy(content);
toast.success('Invite code copied successfully');
} else {
toast.error('Invite code not found');
}
}
generateInviteCode(userId: string): Promise<any> {
this.saving = true;
const promise = new Promise((resolve, reject) => {
userService.generateInviteCode(userId)
.then(response => {
const index = this.list.findIndex(u => u.userId === userId);
if (index > -1) {
this.list[index].updateKey('isExpiredInvite', false);
this.list[index].updateKey('invitationLink', response.invitationLink);
}
resolve(response);
}).catch(error => {
this.saving = false;
reject(error);
}).finally(() => {
this.saving = false;
});
});
toast.promise(promise, {
pending: 'Generating an invite code...',
success: 'Invite code generated successfully',
})
return promise;
}
}

View file

@ -37,6 +37,12 @@ export default class UserService {
}
}
generateInviteCode(userId: any): Promise<any> {
return this.client.get(`/client/members/${userId}/reset`)
.then(response => response.json())
.then(response => response.data || {});
}
delete(userId: string) {
return this.client.delete('/client/members/' + userId)
.then(response => response.json())