💄 front: user rights search and add dialog (#50)
This commit is contained in:
committed by
ericlinagora
parent
a524df0168
commit
62dcb71c8a
@@ -1,135 +0,0 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import UserBlock from '@molecules/grouped-rows/user';
|
||||
import { Input } from '@atoms/input/input-text';
|
||||
import { useSearchUsers } from '@features/users/hooks/use-search-user-list';
|
||||
import { Button } from '@atoms/button/button';
|
||||
import { UserType } from '@features/users/types/user';
|
||||
import { InputDecorationIcon } from '@atoms/input/input-decoration-icon';
|
||||
import { SearchIcon } from '@heroicons/react/solid';
|
||||
import { Info } from '@atoms/text';
|
||||
import Languages from '@features/global/services/languages-service';
|
||||
import _ from 'lodash';
|
||||
import { DriveFileAccessLevel } from 'app/features/drive/types';
|
||||
|
||||
|
||||
export default (props: {
|
||||
className?: string;
|
||||
onChange: (users: UserType[]) => void;
|
||||
initialUsers: UserType[];
|
||||
level: DriveFileAccessLevel;
|
||||
}) => {
|
||||
const [users, setUsers] = useState<UserType[]>(props.initialUsers);
|
||||
const { search, result, query } = useSearchUsers({ scope: 'company' });
|
||||
const [isFocus, setFocus] = useState(false);
|
||||
const inputElement = useRef<HTMLInputElement>(null);
|
||||
const maxUserResultsShown = 5;
|
||||
const [selectedUserKey, setSelectedUserKey] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (users.length) props.onChange(users);
|
||||
}, [users]);
|
||||
|
||||
const shownResults = _.reverse(result.slice(0, maxUserResultsShown));
|
||||
const selectedKeyIndex = shownResults.findIndex(({id}) => id === selectedUserKey);
|
||||
|
||||
const moveSelection = (offset: number) => {
|
||||
if (selectedKeyIndex === -1) {
|
||||
if (offset < 0)
|
||||
setSelectedUserKey(shownResults[shownResults.length - 1].id || '');
|
||||
else if (offset > 0)
|
||||
setSelectedUserKey(shownResults[0].id || '');
|
||||
} else {
|
||||
const nextIndex = (selectedKeyIndex + shownResults.length + offset) % shownResults.length;
|
||||
setSelectedUserKey(shownResults[nextIndex].id || '');
|
||||
}
|
||||
};
|
||||
const doEnterKey = () => {
|
||||
const user = shownResults[selectedKeyIndex];
|
||||
if (user) {
|
||||
setUsers([user]);
|
||||
search('');
|
||||
}
|
||||
setSelectedUserKey('');
|
||||
};
|
||||
|
||||
const keyEvents : { [key: string]: () => void } = {
|
||||
"ArrowUp": () => moveSelection(-1),
|
||||
"ArrowDown": () => moveSelection(1),
|
||||
"Enter": () => doEnterKey(),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full relative">
|
||||
<InputDecorationIcon
|
||||
prefix={SearchIcon}
|
||||
input={({ className }) => (
|
||||
<Input
|
||||
onFocus={() => setFocus(true)}
|
||||
onBlur={() => {
|
||||
setTimeout(() => {
|
||||
if (inputElement.current !== document.activeElement) {
|
||||
setFocus(false);
|
||||
}
|
||||
}, 200);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.code in keyEvents) {
|
||||
keyEvents[e.code]();
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
placeholder={Languages.t('components.select-users_search_users')}
|
||||
className={props.className + ' ' + className + ' w-full'}
|
||||
theme="plain"
|
||||
onChange={e => {
|
||||
search(e.target.value);
|
||||
setSelectedUserKey('');
|
||||
}}
|
||||
inputRef={inputElement}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{isFocus && query?.trim() && (
|
||||
<div className="absolute w-full end-1 z-10 bg-white dark:bg-zinc-800 dark:text-white rounded-md border shadow-md p-2">
|
||||
<div>
|
||||
{result.length === 0 && (
|
||||
<div className="text-center">
|
||||
<Info>{Languages.t('components.user_picker.modal_no_result')}</Info>
|
||||
</div>
|
||||
)}
|
||||
{shownResults.map((user, i) => {
|
||||
return (
|
||||
<UserBlock
|
||||
key={user.id}
|
||||
user={user}
|
||||
className={
|
||||
"rounded m-1 p-3 new-direct-channel-proposed-user flex flex-row items-center justify-center align-baseline"
|
||||
+ (i === selectedKeyIndex ? ' ring' : (i > 0 && i !== (selectedKeyIndex + 1) ? ' border-t' : ''))}
|
||||
suffix={
|
||||
<Button
|
||||
onClick={() => {
|
||||
setUsers([user]);
|
||||
search('');
|
||||
setSelectedUserKey('');
|
||||
}}
|
||||
size="sm"
|
||||
>
|
||||
{Languages.t('components.user_picker.modal.result_add.' + props.level)}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{result.length > maxUserResultsShown &&
|
||||
<div className='grow text-center italic'>
|
||||
{Languages.t('components.user_picker.modal_results_trucated',
|
||||
[maxUserResultsShown, result.length],
|
||||
'First {{$1}} results of {{$2}} shown...')}
|
||||
</div>}
|
||||
<div className='-mb-px' />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -29,6 +29,7 @@ export const AccessModal = () => {
|
||||
return (
|
||||
<Modal
|
||||
open={state.open}
|
||||
className='!overflow-visible'
|
||||
onClose={() => setState({ ...state, open: false })}
|
||||
>
|
||||
{!!state.id && <AccessModalContent id={state.id} />}
|
||||
|
||||
+175
-59
@@ -1,90 +1,206 @@
|
||||
import { Subtitle } from '@atoms/text';
|
||||
import UserBlock from '@molecules/grouped-rows/user';
|
||||
import { useState, useRef } from 'react';
|
||||
|
||||
import type { DriveFileAccessLevel } from '@features/drive/types';
|
||||
|
||||
import Languages from 'features/global/services/languages-service';
|
||||
import { useDriveItem } from '@features/drive/hooks/use-drive-item';
|
||||
import { DriveFileAccessLevel } from '@features/drive/types';
|
||||
import { useCurrentUser } from '@features/users/hooks/use-current-user';
|
||||
import { useUser } from '@features/users/hooks/use-user';
|
||||
import { UserType } from '@features/users/types/user';
|
||||
import { useState } from 'react';
|
||||
import SelectUsers from '../../components/select-users';
|
||||
import { AccessLevelDropdown } from '../../components/access-level-dropdown';
|
||||
import Languages from 'features/global/services/languages-service';
|
||||
import { useSearchUsers } from '@features/users/hooks/use-search-user-list';
|
||||
import { changeUserAccess, getUserAccessLevel, getAllUserAccesses } from '@features/files/utils/access-info-helpers';
|
||||
|
||||
import { Subtitle } from '@atoms/text';
|
||||
import { Input } from '@atoms/input/input-text';
|
||||
import { Button } from '@atoms/button/button';
|
||||
import { SearchIcon } from '@heroicons/react/solid';
|
||||
import { InputDecorationIcon } from '@atoms/input/input-decoration-icon';
|
||||
import { AccessLevelDropdown } from '../../components/access-level-dropdown';
|
||||
import UserBlock from '@molecules/grouped-rows/user';
|
||||
|
||||
export const InternalAccessManager = ({ id, disabled }: { id: string; disabled: boolean }) => {
|
||||
const { item } = useDriveItem(id);
|
||||
const { item, loading, update } = useDriveItem(id);
|
||||
const [level, setLevel] = useState<DriveFileAccessLevel>('manage');
|
||||
const usersWithAccess = item && getAllUserAccesses(item);
|
||||
const { search, result, query } = useSearchUsers({ scope: 'company' });
|
||||
const [searchString, setSearchString] = useState("");
|
||||
const [isFocus, setFocus] = useState(false);
|
||||
const inputElement = useRef<HTMLInputElement>(null);
|
||||
const maxUserResultsShown = 5;
|
||||
const minUserHeight = 3;
|
||||
const showResults = isFocus && query?.trim();
|
||||
|
||||
const knownUsers = item && new Set(getAllUserAccesses(item).map(({id}) => id));
|
||||
const absentUserResults = item && result && result.filter(({id}) => !knownUsers?.has(id!));
|
||||
const shownResults = absentUserResults && absentUserResults.slice(0, maxUserResultsShown);
|
||||
const resultFooterText = knownUsers && shownResults &&
|
||||
(shownResults.length == 0
|
||||
? Languages.t('components.user_picker.modal_results_count_none', [searchString])
|
||||
: (absentUserResults.length > maxUserResultsShown
|
||||
? Languages.t('components.user_picker.modal_results_truncated', [maxUserResultsShown, absentUserResults.length])
|
||||
: false
|
||||
));
|
||||
|
||||
const [selectedUserKey, setSelectedUserKey] = useState('');
|
||||
const selectedKeyIndex = shownResults ? shownResults.findIndex(({id}) => id === selectedUserKey) : -1;
|
||||
|
||||
const moveSelection = (offset: number) => {
|
||||
if (!shownResults) return;
|
||||
if (selectedKeyIndex === -1) {
|
||||
if (offset < 0)
|
||||
setSelectedUserKey(shownResults[shownResults.length - 1].id || '');
|
||||
else if (offset > 0)
|
||||
setSelectedUserKey(shownResults[0].id || '');
|
||||
} else if (selectedKeyIndex != null) {
|
||||
const nextIndex = (selectedKeyIndex + shownResults.length + offset) % shownResults.length;
|
||||
setSelectedUserKey(shownResults[nextIndex].id || '');
|
||||
}
|
||||
};
|
||||
|
||||
const doEnterKey = () => {
|
||||
const user = shownResults && selectedKeyIndex > -1 && shownResults[selectedKeyIndex];
|
||||
if (user) {
|
||||
item && update(changeUserAccess(item, user.id!, level));
|
||||
search('');
|
||||
}
|
||||
setSelectedUserKey('');
|
||||
};
|
||||
|
||||
const keyEvents : { [key: string]: () => void } = {
|
||||
"ArrowUp": () => moveSelection(-1),
|
||||
"ArrowDown": () => moveSelection(1),
|
||||
"Enter": () => doEnterKey(),
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Subtitle className="block mt-2 mb-1">
|
||||
{Languages.t('components.internal-access_specific_rules')}
|
||||
{Languages.t('components.internal-access_manage_subtitle')}
|
||||
</Subtitle>
|
||||
<div className="rounded-md border mt-2 dark:border-zinc-700">
|
||||
<UserAccessSelector id={id} disabled={disabled} />
|
||||
|
||||
{item && getAllUserAccesses(item)?.map(user =>
|
||||
<UserAccessLevel key={user.id} id={id} userId={user?.id} disabled={disabled} />
|
||||
<div className="rounded-md border-t mt-2 dark:border-zinc-700">
|
||||
<div className="p-4 flex flex-row items-center justify-center rounded-t-md border-x dark:border-zinc-700">
|
||||
<div className="grow">
|
||||
<InputDecorationIcon
|
||||
prefix={SearchIcon}
|
||||
input={({ className }) => (
|
||||
<Input
|
||||
onFocus={() => setFocus(true)}
|
||||
onBlur={() => {
|
||||
setTimeout(() => {
|
||||
if (inputElement.current !== document.activeElement) {
|
||||
setFocus(false);
|
||||
}
|
||||
}, 200);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.code in keyEvents) {
|
||||
keyEvents[e.code]();
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
placeholder={Languages.t('components.select-users_search_users')}
|
||||
className={className + ' w-full rounded-r-none'}
|
||||
theme="plain"
|
||||
onChange={e => {
|
||||
search(e.target.value);
|
||||
setSearchString(e.target.value);
|
||||
}}
|
||||
value={searchString}
|
||||
inputRef={inputElement}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<AccessLevelDropdown
|
||||
className="rounded-l-none !p-0 leading-tight text-end !pr-8 !pl-2 border-none bg-zinc-100 dark:bg-zinc-800"
|
||||
disabled={loading || disabled}
|
||||
hiddenLevels={['remove']}
|
||||
level={level}
|
||||
onChange={level => setLevel(level)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{showResults &&
|
||||
<>
|
||||
<div className="flex flex-row border-t !border-x-0 dark:border-zinc-700 h-1" />
|
||||
<div className="relative w-full h-0"> {/* Necessary so the relative results don't take height */}
|
||||
<div className={(showResults ? "rounded-md " : "rounded-md ") + "absolute z-10 w-full shadow-md border bg-white dark:bg-zinc-900 mt-2 dark:border-zinc-700"}>
|
||||
{ shownResults && shownResults.map((user, index) =>
|
||||
<UserAccessLevel
|
||||
key={user.id}
|
||||
id={id}
|
||||
userId={user.id!}
|
||||
disabled={disabled}
|
||||
className={'!border-x-0' + (index === selectedKeyIndex ? ' ring' : (index > 0 && index !== ((selectedKeyIndex ?? 0) + 1) ? ' border-t' : ''))}
|
||||
isSearchResultAdd={level}
|
||||
onAddSearchResult={(userId) => {
|
||||
item && update(changeUserAccess(item, userId, level));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!loading && resultFooterText && <>
|
||||
<div className={(result.length == 0 ? 'rounded-md' : 'rounded-b-md') + ' grow text-center italic dark:text-zinc-400 py-2 dark:border-zinc-700 border-t bg-zinc-100 dark:bg-zinc-800'}>
|
||||
{resultFooterText}
|
||||
</div>
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row !border-x-0 dark:border-zinc-700 h-2" />
|
||||
</>}
|
||||
{usersWithAccess?.map((user, index) =>
|
||||
<UserAccessLevel
|
||||
key={user.id}
|
||||
id={id}
|
||||
userId={user?.id}
|
||||
disabled={disabled}
|
||||
className={(showResults ? '!rounded-none !rounded-t-md' : '') + (index === usersWithAccess.length - 1 ? ' rounded-b-md border-y' : ' border-t')}
|
||||
/>
|
||||
)}
|
||||
<div className="-mb-px" />
|
||||
{(usersWithAccess?.length ?? 0) < minUserHeight &&
|
||||
[...Array(minUserHeight - (usersWithAccess?.length ?? 0))].map((_, i) =>
|
||||
<div key={i} className="p-4 flex flex-row">
|
||||
<div className='w-9 h-9 border border-transparent' /> {/* make same size as userblock */}
|
||||
</div>
|
||||
)}
|
||||
{(!showResults) &&
|
||||
<div className="mt-2 flex flex-row items-center justify-center border-none h-1"> </div>
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const UserAccessSelector = ({ id, disabled }: { id: string; disabled: boolean }) => {
|
||||
const { item, loading, update } = useDriveItem(id);
|
||||
const [level, setLevel] = useState<DriveFileAccessLevel>('manage');
|
||||
|
||||
return (
|
||||
<div className="p-4 flex flex-row items-center justify-center">
|
||||
<div className="grow">
|
||||
<SelectUsers
|
||||
className="rounded-r-none"
|
||||
level={level}
|
||||
onChange={(users: UserType[]) => {
|
||||
const id = users[0]?.id; //TODO: all others ignored
|
||||
item && id && update(changeUserAccess(item, id, level));
|
||||
}}
|
||||
initialUsers={[]}
|
||||
/>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<AccessLevelDropdown
|
||||
className="rounded-l-none"
|
||||
disabled={loading || disabled}
|
||||
hiddenLevels={['remove']}
|
||||
level={level}
|
||||
onChange={level => setLevel(level)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const UserAccessLevel = ({
|
||||
id,
|
||||
userId,
|
||||
disabled,
|
||||
}: {
|
||||
const UserAccessLevel = (props: {
|
||||
id: string;
|
||||
userId: string;
|
||||
disabled: boolean;
|
||||
className?: string;
|
||||
isSearchResultAdd?: DriveFileAccessLevel;
|
||||
onAddSearchResult?: (userId: string) => void,
|
||||
}) => {
|
||||
const { item, loading, update } = useDriveItem(id);
|
||||
const user = useUser(userId);
|
||||
const { item, loading, update } = useDriveItem(props.id);
|
||||
const user = useUser(props.userId);
|
||||
const { user: currentUser } = useCurrentUser();
|
||||
return (
|
||||
<UserBlock
|
||||
className="p-4 border-t dark:border-zinc-700"
|
||||
className={"p-4 border-x dark:border-zinc-700" + ' ' + (props.className ?? '')}
|
||||
user={user}
|
||||
isSelf={!!currentUser?.id && user?.id === currentUser?.id}
|
||||
suffix={
|
||||
<AccessLevelDropdown
|
||||
disabled={loading || disabled || user?.id === currentUser?.id}
|
||||
level={(item && getUserAccessLevel(item, userId)) || "none"}
|
||||
onChange={level => item && update(changeUserAccess(item, userId, level === 'remove' ? false : level))}
|
||||
/>
|
||||
props.isSearchResultAdd
|
||||
? <Button
|
||||
disabled={loading || props.disabled || user?.id === currentUser?.id}
|
||||
onClick={() => user && props.onAddSearchResult!(props.userId)}
|
||||
size="sm"
|
||||
>
|
||||
{Languages.t('components.user_picker.modal.result_add.' + props.isSearchResultAdd)}
|
||||
</Button>
|
||||
|
||||
: <AccessLevelDropdown
|
||||
disabled={loading || props.disabled || user?.id === currentUser?.id}
|
||||
level={(item && getUserAccessLevel(item, props.userId)) || "none"}
|
||||
onChange={level => item && update(changeUserAccess(item, props.userId, level === 'remove' ? false : level))}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user