💄 Applied sharing dialog changes from figme (#50)
https://www.figma.com/proto/CWz4DatnJ7Qhl3JycJhbFY/TDrive---MUI3?type=design&node-id=1355-9893&t=7v0ocJCf6knnPDan-0&scaling=min-zoom&page-id=1189%3A23493&starting-point-node-id=1347%3A8530
This commit is contained in:
committed by
ericlinagora
parent
a2e4334361
commit
579b64972e
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
theme?: 'primary' | 'secondary' | 'danger' | 'default' | 'outline' | 'dark' | 'white';
|
||||
theme?: 'primary' | 'secondary' | 'danger' | 'default' | 'outline' | 'dark' | 'white' | 'green';
|
||||
size?: 'md' | 'lg' | 'sm';
|
||||
icon?: (props: any) => JSX.Element;
|
||||
iconSize?: 'md' | 'lg';
|
||||
@@ -40,6 +40,10 @@ export const Button = (props: ButtonProps) => {
|
||||
className =
|
||||
'text-zinc-300 border-0 bg-zinc-900 hover:bg-zinc-800 hover:text-white active:bg-zinc-900';
|
||||
|
||||
if (props.theme === 'green')
|
||||
className =
|
||||
'text-zinc-300 border-0 bg-green-700';
|
||||
|
||||
if (disabled) className += ' opacity-50 pointer-events-none';
|
||||
|
||||
if (props.size === 'lg') className = className + ' text-lg h-10';
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { CheckOutlineIcon } from '../icons-agnostic';
|
||||
import { BaseSmall } from '../text';
|
||||
import { Base, BaseSmall } from '../text';
|
||||
|
||||
export const Checkbox = (props: {
|
||||
label?: string;
|
||||
labelNormalSize?: boolean;
|
||||
onChange?: (status: boolean) => void;
|
||||
value?: boolean;
|
||||
className?: string;
|
||||
@@ -43,9 +44,14 @@ export const Checkbox = (props: {
|
||||
}}
|
||||
>
|
||||
{renderSwitch()}
|
||||
<BaseSmall className={'ml-2 ' + (props.disabled ? 'opacity-50' : 'cursor-pointer')}>
|
||||
{props.label}
|
||||
</BaseSmall>
|
||||
{props.labelNormalSize
|
||||
? <Base className={'ml-2 ' + (props.disabled ? 'opacity-50' : 'cursor-pointer')}>
|
||||
{props.label}
|
||||
</Base>
|
||||
: <BaseSmall className={'ml-2 ' + (props.disabled ? 'opacity-50' : 'cursor-pointer')}>
|
||||
{props.label}
|
||||
</BaseSmall>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import _ from 'lodash';
|
||||
import { defaultInputClassName, errorInputClassName } from './input-text';
|
||||
import { defaultInputClassName, errorInputClassName, ThemeName } from './input-text';
|
||||
|
||||
interface InputProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'size'> {
|
||||
theme?: 'plain' | 'outline';
|
||||
theme?: ThemeName;
|
||||
hasError?: boolean;
|
||||
size?: 'md' | 'lg' | 'sm';
|
||||
className?: string;
|
||||
|
||||
@@ -7,7 +7,7 @@ interface InputProps
|
||||
React.InputHTMLAttributes<HTMLInputElement> & React.TextareaHTMLAttributes<HTMLTextAreaElement>,
|
||||
'size'
|
||||
> {
|
||||
theme?: 'plain' | 'outline';
|
||||
theme?: ThemeName;
|
||||
label?: string;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
feedback?: string;
|
||||
@@ -19,21 +19,28 @@ interface InputProps
|
||||
inputRef?: React.Ref<HTMLInputElement | HTMLTextAreaElement>;
|
||||
}
|
||||
|
||||
const baseInputClassName =
|
||||
'tw-input block w-full rounded-md focus:ring-1 focus:ring-blue-500 z-0 focus:z-10 dark:text-white text-black text-base ';
|
||||
export type ThemeName = 'plain' | 'outline' | 'blue' | 'rose';
|
||||
|
||||
export const defaultInputClassName = (theme: 'plain' | 'outline' = 'plain') => {
|
||||
const baseInputClassName =
|
||||
'tw-input block w-full rounded-md focus:ring-1 focus:ring-blue-500 z-0 focus:z-10 text-base ';
|
||||
const baseTextClassName = ' dark:text-white text-black ';
|
||||
|
||||
export const defaultInputClassName = (theme: ThemeName = 'plain') => {
|
||||
const themeClasses = {
|
||||
'plain': 'bg-zinc-100 border-zinc-100 dark:bg-zinc-800 dark:border-zinc-800' + baseTextClassName,
|
||||
'blue': 'bg-zinc-100 border-zinc-100 dark:bg-zinc-800 dark:border-zinc-800 text-blue-700 dark:text-blue-500',
|
||||
'rose': 'text-rose-500 bg-rose-100 dark:text-rose-300 dark:bg-rose-900 border-rose-500',
|
||||
'outline': 'bg-zinc-50 border-zinc-300 dark:bg-zinc-800 dark:border-zinc-700' + baseTextClassName,
|
||||
};
|
||||
return (
|
||||
baseInputClassName +
|
||||
(theme === 'plain'
|
||||
? 'bg-zinc-100 border-zinc-100 dark:bg-zinc-800 dark:border-zinc-800'
|
||||
: 'bg-zinc-50 border-zinc-300 dark:bg-zinc-800 dark:border-zinc-700')
|
||||
(themeClasses[theme] || themeClasses.outline)
|
||||
);
|
||||
};
|
||||
|
||||
export const errorInputClassName = (theme: 'plain' | 'outline' = 'plain') => {
|
||||
export const errorInputClassName = (theme: ThemeName = 'plain') => {
|
||||
return (
|
||||
baseInputClassName +
|
||||
baseInputClassName + baseTextClassName +
|
||||
(theme === 'plain'
|
||||
? 'bg-rose-200 border-rose-200 dark:bg-rose-800 dark:border-rose-800'
|
||||
: 'bg-rose-50 border-rose-300 dark:bg-rose-900 dark:border-rose-800')
|
||||
|
||||
@@ -17,6 +17,7 @@ export const Modal = (props: {
|
||||
onClose?: () => void;
|
||||
children?: React.ReactNode;
|
||||
closable?: boolean;
|
||||
closeIcon?: ReactNode,
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
positioned?: boolean;
|
||||
@@ -130,7 +131,7 @@ export const Modal = (props: {
|
||||
className="hover:opacity-75 focus:outline-none "
|
||||
onClick={() => props.onClose && props.onClose()}
|
||||
>
|
||||
<DismissIcon />
|
||||
{props.closeIcon ? props.closeIcon : <DismissIcon />}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -90,10 +90,10 @@ export default (props: {
|
||||
)}
|
||||
/>
|
||||
{isFocus && query?.trim() && (
|
||||
<div className="absolute w-full top-0 -translate-y-full bg-white dark:bg-zinc-800 dark:text-white rounded-md border shadow-md p-2">
|
||||
<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 pt-8">
|
||||
<div className="text-center">
|
||||
<Info>{Languages.t('components.user_picker.modal_no_result')}</Info>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -8,6 +8,7 @@ export const AccessLevel = ({
|
||||
onChange,
|
||||
canRemove,
|
||||
hiddenLevels,
|
||||
labelOverrides,
|
||||
className,
|
||||
}: {
|
||||
disabled?: boolean;
|
||||
@@ -15,23 +16,26 @@ export const AccessLevel = ({
|
||||
onChange: (level: DriveFileAccessLevel & 'remove') => void;
|
||||
canRemove?: boolean;
|
||||
className?: string;
|
||||
labelOverrides?: { [key: string]: string };
|
||||
hiddenLevels?: string[];
|
||||
}) => {
|
||||
const createOption = (level: DriveFileAccessLevel, tKey: string) =>
|
||||
!hiddenLevels?.includes(level) && <option value={level}>{(labelOverrides || {})[level] || Languages.t(tKey)}</option>;
|
||||
return (
|
||||
<Select
|
||||
disabled={disabled}
|
||||
className={
|
||||
className +
|
||||
' w-auto ' +
|
||||
(level === 'none' ? '!text-rose-500 !bg-rose-100 dark-bg-rose-800' : '')
|
||||
' w-auto'
|
||||
}
|
||||
theme={level === 'none' ? 'rose' : 'outline'}
|
||||
value={level || 'none'}
|
||||
onChange={e => onChange(e.target.value as DriveFileAccessLevel & 'remove')}
|
||||
>
|
||||
{!hiddenLevels?.includes('manage') && <option value={'manage'}>{Languages.t('common.access-level_full_acess')}</option>}
|
||||
{!hiddenLevels?.includes('write') && <option value={'write'}>{Languages.t('common.access-level_write')}</option>}
|
||||
{!hiddenLevels?.includes('read') && <option value={'read'}>{Languages.t('common.access-level_read')}</option>}
|
||||
{!hiddenLevels?.includes('none') && <option value={'none'}>{Languages.t('common.access-level_no_access')}</option>}
|
||||
{createOption('manage', 'common.access-level_full_acess')}
|
||||
{createOption('write', 'common.access-level_write')}
|
||||
{createOption('read', 'common.access-level_read')}
|
||||
{createOption('none', 'common.access-level_no_access')}
|
||||
{canRemove && <option value={'remove'}>Remove</option>}
|
||||
</Select>
|
||||
);
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { Base } from '@atoms/text';
|
||||
import { FolderIcon } from '@heroicons/react/outline';
|
||||
import type { DriveItem } from 'app/features/drive/types';
|
||||
|
||||
export const CuteDepictionOfFolderHierarchy = (props: {
|
||||
file?: DriveItem,
|
||||
parent?: DriveItem,
|
||||
}) =>
|
||||
<>
|
||||
<Base className="p-4 pb-0 flex flex-row items-center justify-center">
|
||||
<div className="shrink">
|
||||
<FolderIcon className="w-7 mr-2" />
|
||||
</div>
|
||||
<div className="grow">
|
||||
{props.parent?.name}
|
||||
</div>
|
||||
</Base>
|
||||
<Base className="pl-5 flex flex-row items-center justify-center">
|
||||
<div className="shrink text-2xl text-zinc-500">
|
||||
└
|
||||
</div>
|
||||
<div className="shrink pl-2">
|
||||
<FolderIcon className="w-5 mr-2" />
|
||||
</div>
|
||||
<div className="grow">
|
||||
{props.file?.name}
|
||||
</div>
|
||||
</Base>
|
||||
</>;
|
||||
@@ -1,6 +1,8 @@
|
||||
import A from '@atoms/link';
|
||||
import { Info } from '@atoms/text';
|
||||
import { Modal, ModalContent } from '@atoms/modal';
|
||||
import { useDriveItem } from '@features/drive/hooks/use-drive-item';
|
||||
import { useEffect } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { atom, useRecoilState } from 'recoil';
|
||||
import { InternalAccessManager } from './internal-access';
|
||||
import { PublicLinkManager } from './public-link-access';
|
||||
@@ -9,6 +11,10 @@ import Languages from 'features/global/services/languages-service';
|
||||
import FeatureTogglesService, {
|
||||
FeatureNames,
|
||||
} from '@features/global/services/feature-toggles-service';
|
||||
import { ArrowLeftIcon, LockClosedIcon } from '@heroicons/react/outline';
|
||||
import { PublicLinkAccessOptions } from './public-link-access-options';
|
||||
import { CuteDepictionOfFolderHierarchy } from './cute-depiction-of-folder-hierarchy';
|
||||
import { InheritAccessOptions } from './inherit-access-options';
|
||||
|
||||
export type AccessModalType = {
|
||||
open: boolean;
|
||||
@@ -25,29 +31,127 @@ export const AccessModalAtom = atom<AccessModalType>({
|
||||
|
||||
export const AccessModal = () => {
|
||||
const [state, setState] = useRecoilState(AccessModalAtom);
|
||||
const [isOnAdvancedScreen, setIsOnAdvancedScreen] = useState(false);
|
||||
|
||||
return (
|
||||
<Modal open={state.open} onClose={() => setState({ ...state, open: false })}>
|
||||
{!!state.id && <AccessModalContent id={state.id} />}
|
||||
<Modal
|
||||
open={state.open}
|
||||
onClose={() => {
|
||||
if (isOnAdvancedScreen)
|
||||
setIsOnAdvancedScreen(false);
|
||||
else
|
||||
setState({ ...state, open: false });
|
||||
}}
|
||||
closeIcon={isOnAdvancedScreen && <ArrowLeftIcon className="w-5 dark:text-zinc-500" />}
|
||||
>
|
||||
{!!state.id &&
|
||||
<AccessModalContent
|
||||
id={state.id}
|
||||
isOnAdvancedScreen={isOnAdvancedScreen}
|
||||
onShowAdvancedScreen={(active) => setIsOnAdvancedScreen(active)}
|
||||
/>}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const AccessModalContent = ({ id }: { id: string }) => {
|
||||
const { item, access, refresh } = useDriveItem(id);
|
||||
const SwitchToAdvancedSettingsPanel = (props: {
|
||||
disabled: boolean,
|
||||
onShowAdvancedScreen: (active: boolean) => void,
|
||||
}) =>
|
||||
<div className="p-4 flex flex-row items-center justify-center text-zinc-800 dark:text-white">
|
||||
<div className="shrink-0">
|
||||
<LockClosedIcon className="w-5 mr-2" />
|
||||
</div>
|
||||
<div className="grow">
|
||||
<div>Advanced security settings</div>
|
||||
<div><Info>Set password, expiration date, etc.</Info></div>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<A
|
||||
className={"inline-block" + (props.disabled ? ' !text-zinc-500' : '')}
|
||||
disabled={props.disabled}
|
||||
noColor={props.disabled}
|
||||
onClick={() => {
|
||||
if (!props.disabled)
|
||||
props.onShowAdvancedScreen(true);
|
||||
}}
|
||||
>
|
||||
Change
|
||||
</A>
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
const AccessModalContent = (props: {
|
||||
id: string,
|
||||
isOnAdvancedScreen: boolean,
|
||||
onShowAdvancedScreen: (active: boolean) => void,
|
||||
}) => {
|
||||
const { id } = props;
|
||||
const { item, access, loading, update, refresh } = useDriveItem(id);
|
||||
const { item: parentItem } = useDriveItem(item?.parent_id || '');
|
||||
const { company, refresh: refreshCompany } = useCurrentCompany();
|
||||
useEffect(() => {
|
||||
refresh(id);
|
||||
refreshCompany();
|
||||
}, []);
|
||||
const havePublicLink = (item?.access_info?.public?.level || 'none') !== 'none';
|
||||
const haveAdvancedSettings = parentItem?.parent_id !== null || havePublicLink;
|
||||
|
||||
const updatePublicAccess = (key: string, value: string | number, skipLoading?: true) =>
|
||||
update({
|
||||
access_info: {
|
||||
entities: item?.access_info.entities || [],
|
||||
public: {
|
||||
...item!.access_info!.public!,
|
||||
[key]: value || '',
|
||||
},
|
||||
},
|
||||
}, skipLoading);
|
||||
|
||||
return (
|
||||
<ModalContent
|
||||
title={Languages.t('components.item_context_menu.manage_access_to') + ' ' + item?.name}
|
||||
title={Languages.t(
|
||||
props.isOnAdvancedScreen
|
||||
? 'components.item_context_menu.manage_access_advanced_to'
|
||||
: 'components.item_context_menu.manage_access_to') + ' ' + item?.name}
|
||||
>
|
||||
<PublicLinkManager id={id} disabled={access !== 'manage'} />
|
||||
{FeatureTogglesService.isActiveFeatureName(FeatureNames.COMPANY_SEARCH_USERS) && (
|
||||
<InternalAccessManager id={id} disabled={access !== 'manage'} />
|
||||
)}
|
||||
{!props.isOnAdvancedScreen ?
|
||||
<>
|
||||
<PublicLinkManager id={id} disabled={access !== 'manage'} />
|
||||
|
||||
{FeatureTogglesService.isActiveFeatureName(FeatureNames.COMPANY_SEARCH_USERS) && (
|
||||
<InternalAccessManager id={id} disabled={access !== 'manage'} />
|
||||
)}
|
||||
|
||||
{haveAdvancedSettings && <SwitchToAdvancedSettingsPanel
|
||||
disabled={!haveAdvancedSettings}
|
||||
onShowAdvancedScreen={props.onShowAdvancedScreen}
|
||||
/>}
|
||||
</> : <>
|
||||
{havePublicLink &&
|
||||
<PublicLinkAccessOptions
|
||||
disabled={loading || access !== 'manage'}
|
||||
password={item?.access_info?.public?.password}
|
||||
expiration={item?.access_info?.public?.expiration}
|
||||
onChangePassword={(password: string) => {
|
||||
updatePublicAccess('password', password || '', true);
|
||||
}}
|
||||
onChangeExpiration={(expiration: number) => {
|
||||
updatePublicAccess('expiration', expiration || 0);
|
||||
}}
|
||||
/>}
|
||||
{ parentItem?.parent_id !== null && <>
|
||||
<InheritAccessOptions
|
||||
item={item}
|
||||
disabled={loading}
|
||||
onUpdate={update}
|
||||
/>
|
||||
<CuteDepictionOfFolderHierarchy
|
||||
file={item}
|
||||
parent={parentItem}
|
||||
/>
|
||||
</>}
|
||||
</>}
|
||||
</ModalContent>
|
||||
);
|
||||
};
|
||||
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
import Languages from 'features/global/services/languages-service';
|
||||
|
||||
import type { DriveItem } from 'app/features/drive/types';
|
||||
|
||||
import { Base, Info, Subtitle } from '@atoms/text';
|
||||
import { Checkbox } from '@atoms/input/input-checkbox';
|
||||
import { AccessLevel } from './common';
|
||||
import AlertManager from '@features/global/services/alert-manager-service';
|
||||
|
||||
export const InheritAccessOptions = (props: {
|
||||
item?: DriveItem,
|
||||
disabled: boolean,
|
||||
onUpdate: (item: Partial<DriveItem>) => void,
|
||||
}) => {
|
||||
const folderEntity = props.item?.access_info.entities.filter(a => a.type === 'folder')?.[0] || {
|
||||
type: 'folder',
|
||||
id: 'parent',
|
||||
level: 'manage',
|
||||
};
|
||||
const companyEntity = props.item?.access_info.entities.filter(a => a.type === 'company')?.[0];
|
||||
const channelEntities = props.item?.access_info.entities.filter(a => a.type === 'channel') || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
{(companyEntity || folderEntity?.level === 'none' || channelEntities.length > 0) &&
|
||||
<Subtitle className="block mt-4 mb-1">{Languages.t('components.internal-access_access_manage')}</Subtitle>}
|
||||
|
||||
{folderEntity && (
|
||||
<div className="p-4 flex flex-row items-center justify-center">
|
||||
<div className="grow">
|
||||
<Base>{Languages.t('components.internal-access_inherit_parent')}</Base>
|
||||
<br />
|
||||
<Info>{Languages.t('components.internal-access_inherit_parent_perm')}</Info>
|
||||
</div>
|
||||
<div className="shrink-0 ml-2">
|
||||
<Checkbox
|
||||
disabled={props.disabled}
|
||||
onChange={status => {
|
||||
props.onUpdate({
|
||||
access_info: {
|
||||
entities: [
|
||||
...(props.item?.access_info.entities.filter(a => a.type !== 'folder') || []),
|
||||
{ ...folderEntity, level: status ? 'manage' : 'none' },
|
||||
],
|
||||
public: props.item?.access_info.public,
|
||||
},
|
||||
});
|
||||
}}
|
||||
value={folderEntity.level === 'manage'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{companyEntity && folderEntity.level === 'none' && (
|
||||
<div className="p-4 flex flex-row items-center justify-center">
|
||||
<div className="grow">
|
||||
<Base>{Languages.t('components.internal-access_company_member')}</Base>
|
||||
</div>
|
||||
<div className="shrink-0 ml-2">
|
||||
<AccessLevel
|
||||
disabled={props.disabled}
|
||||
onChange={level => {
|
||||
props.onUpdate({
|
||||
access_info: {
|
||||
entities: [
|
||||
...(props.item?.access_info.entities.filter(a => a.type !== 'company') || []),
|
||||
...(level !== 'remove' ? [{ ...companyEntity, level }] : []),
|
||||
],
|
||||
public: props.item?.access_info.public,
|
||||
},
|
||||
});
|
||||
}}
|
||||
level={companyEntity.level}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{channelEntities.length > 0 && (
|
||||
<div className="p-4 border-b flex flex-row items-center justify-center">
|
||||
<div className="grow">
|
||||
<Base>{Languages.t('components.internal-access_cannal')}</Base>
|
||||
<br />
|
||||
<Info>
|
||||
{channelEntities.length} {Languages.t('components.internal-access_cannal_info')}
|
||||
</Info>
|
||||
</div>
|
||||
<div className="shrink-0 ml-2">
|
||||
<AccessLevel
|
||||
disabled={props.disabled}
|
||||
hiddenLevels={['none']}
|
||||
canRemove
|
||||
onChange={level => {
|
||||
if (level === 'remove') {
|
||||
AlertManager.confirm(
|
||||
async () => {
|
||||
//Remove channel access
|
||||
props.onUpdate({
|
||||
access_info: {
|
||||
entities:
|
||||
props.item?.access_info?.entities.filter(e => e.type !== 'channel') || [],
|
||||
public: props.item?.access_info.public,
|
||||
},
|
||||
});
|
||||
},
|
||||
() => {
|
||||
//Do nothing
|
||||
},
|
||||
{
|
||||
text: Languages.t('components.internal-access_cannal_info_give_back'),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
props.onUpdate({
|
||||
access_info: {
|
||||
entities:
|
||||
props.item?.access_info?.entities.map(e => {
|
||||
if (e.type === 'channel') {
|
||||
return { ...e, level };
|
||||
}
|
||||
return e;
|
||||
}) || [],
|
||||
public: props.item?.access_info.public,
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
level={channelEntities[0].level}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</>);
|
||||
};
|
||||
|
||||
+1
-124
@@ -1,9 +1,7 @@
|
||||
import Avatar from '@atoms/avatar';
|
||||
import { Checkbox } from '@atoms/input/input-checkbox';
|
||||
import { Base, Info } from '@atoms/text';
|
||||
import { useDriveItem } from '@features/drive/hooks/use-drive-item';
|
||||
import { DriveFileAccessLevel } from '@features/drive/types';
|
||||
import AlertManager from '@features/global/services/alert-manager-service';
|
||||
import { useCurrentUser } from '@features/users/hooks/use-current-user';
|
||||
import { useUser } from '@features/users/hooks/use-user';
|
||||
import currentUserService from '@features/users/services/current-user-service';
|
||||
@@ -15,132 +13,11 @@ import Languages from 'features/global/services/languages-service';
|
||||
|
||||
|
||||
export const InternalAccessManager = ({ id, disabled }: { id: string; disabled: boolean }) => {
|
||||
const { item, loading, update } = useDriveItem(id);
|
||||
|
||||
const { item } = useDriveItem(id);
|
||||
const userEntities = item?.access_info.entities.filter(a => a.type === 'user') || [];
|
||||
const folderEntity = item?.access_info.entities.filter(a => a.type === 'folder')?.[0] || {
|
||||
type: 'folder',
|
||||
id: 'parent',
|
||||
level: 'manage',
|
||||
};
|
||||
const companyEntity = item?.access_info.entities.filter(a => a.type === 'company')?.[0];
|
||||
const channelEntities = item?.access_info.entities.filter(a => a.type === 'channel') || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Base className="block mt-4 mb-1">{Languages.t('components.internal-access_access_manage')}</Base>
|
||||
|
||||
<div className="rounded-md border overflow-hidden">
|
||||
{folderEntity && (
|
||||
<div className="p-4 border-b flex flex-row items-center justify-center">
|
||||
<div className="grow">
|
||||
<Base>{Languages.t('components.internal-access_inherit_parent')}</Base>
|
||||
<br />
|
||||
<Info>{Languages.t('components.internal-access_inherit_parent_perm')}</Info>
|
||||
</div>
|
||||
<div className="shrink-0 ml-2">
|
||||
<Checkbox
|
||||
disabled={loading || disabled}
|
||||
onChange={status => {
|
||||
update({
|
||||
access_info: {
|
||||
entities: [
|
||||
...(item?.access_info.entities.filter(a => a.type !== 'folder') || []),
|
||||
{ ...folderEntity, level: status ? 'manage' : 'none' },
|
||||
],
|
||||
public: item?.access_info.public,
|
||||
},
|
||||
});
|
||||
}}
|
||||
value={folderEntity.level === 'manage'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{companyEntity && folderEntity.level === 'none' && (
|
||||
<div className="p-4 border-b flex flex-row items-center justify-center">
|
||||
<div className="grow">
|
||||
<Base>{Languages.t('components.internal-access_company_member')}</Base>
|
||||
</div>
|
||||
<div className="shrink-0 ml-2">
|
||||
<AccessLevel
|
||||
disabled={loading || disabled}
|
||||
onChange={level => {
|
||||
update({
|
||||
access_info: {
|
||||
entities: [
|
||||
...(item?.access_info.entities.filter(a => a.type !== 'company') || []),
|
||||
...(level !== 'remove' ? [{ ...companyEntity, level }] : []),
|
||||
],
|
||||
public: item?.access_info.public,
|
||||
},
|
||||
});
|
||||
}}
|
||||
level={companyEntity.level}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{channelEntities.length > 0 && (
|
||||
<div className="p-4 border-b flex flex-row items-center justify-center">
|
||||
<div className="grow">
|
||||
<Base>{Languages.t('components.internal-access_cannal')}</Base>
|
||||
<br />
|
||||
<Info>
|
||||
{channelEntities.length} {Languages.t('components.internal-access_cannal_info')}
|
||||
</Info>
|
||||
</div>
|
||||
<div className="shrink-0 ml-2">
|
||||
<AccessLevel
|
||||
disabled={loading || disabled}
|
||||
hiddenLevels={['none']}
|
||||
canRemove
|
||||
onChange={level => {
|
||||
if (level === 'remove') {
|
||||
AlertManager.confirm(
|
||||
async () => {
|
||||
//Remove channel access
|
||||
update({
|
||||
access_info: {
|
||||
entities:
|
||||
item?.access_info?.entities.filter(e => e.type !== 'channel') || [],
|
||||
public: item?.access_info.public,
|
||||
},
|
||||
});
|
||||
},
|
||||
() => {
|
||||
//Do nothing
|
||||
},
|
||||
{
|
||||
text: Languages.t('components.internal-access_cannal_info_give_back'),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
update({
|
||||
access_info: {
|
||||
entities:
|
||||
item?.access_info?.entities.map(e => {
|
||||
if (e.type === 'channel') {
|
||||
return { ...e, level };
|
||||
}
|
||||
return e;
|
||||
}) || [],
|
||||
public: item?.access_info.public,
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
level={channelEntities[0].level}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="-mb-px" />
|
||||
</div>
|
||||
|
||||
<Base className="block mt-4 mb-1">{Languages.t('components.internal-access_specific_rules')}</Base>
|
||||
<div className="rounded-md border mt-2">
|
||||
<UserAccessSelector id={id} disabled={disabled} />
|
||||
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
import { Info, Subtitle } from '@atoms/text';
|
||||
import { ToasterService } from '@features/global/services/toaster-service';
|
||||
import { copyToClipboard } from '@features/global/utils/CopyClipboard';
|
||||
import { Checkbox } from 'app/atoms/input/input-checkbox';
|
||||
import { Input } from 'app/atoms/input/input-text';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import moment from 'moment';
|
||||
import 'moment/min/locales';
|
||||
import Languages from 'features/global/services/languages-service';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
export const PublicLinkAccessOptions = (props: {
|
||||
disabled?: boolean;
|
||||
password?: string;
|
||||
expiration?: number;
|
||||
onChangePassword: Function;
|
||||
onChangeExpiration: Function;
|
||||
}) => {
|
||||
const [usePassword, setUsePassword] = useState(!!props.password);
|
||||
const [password, setPassword] = useState(props.password);
|
||||
const [useExpiration, setUseExpiration] = useState((props.expiration || 0) > 0);
|
||||
const [expiration, setExpiration] = useState(props.expiration);
|
||||
const handlePasswordBlur = () => {
|
||||
props.onChangePassword(password);
|
||||
};
|
||||
|
||||
const debouncedOnChangePassword = debounce(passwordValue => {
|
||||
props.onChangePassword(passwordValue);
|
||||
}, 500); // 500ms delay
|
||||
|
||||
useEffect(() => {
|
||||
// Ensure the effect runs only if usePassword or password changes
|
||||
debouncedOnChangePassword(usePassword ? password : '');
|
||||
|
||||
// Cleanup function to cancel the debounced call if the component is unmounted or the dependencies change
|
||||
return () => {
|
||||
debouncedOnChangePassword.cancel();
|
||||
};
|
||||
}, [usePassword, password]);
|
||||
|
||||
useEffect(() => {
|
||||
props.onChangeExpiration(useExpiration ? expiration : 0);
|
||||
}, [useExpiration, expiration]);
|
||||
|
||||
function expirationDate(exp: moment.MomentInput) {
|
||||
moment.locale(Languages.getLanguage());
|
||||
return moment(exp).fromNow(true).toLocaleString();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Subtitle className="block mt-4 mb-1">
|
||||
{Languages.t('components.public-link-security')}
|
||||
</Subtitle>
|
||||
|
||||
<div className="p-4 rounded-md border">
|
||||
<div className="flex items-center justify-center w-full h-10">
|
||||
<Checkbox
|
||||
disabled={props.disabled}
|
||||
labelNormalSize={true}
|
||||
onChange={s => {
|
||||
setUsePassword(s);
|
||||
if (!password && s) setPassword(Math.random().toString(36).slice(-8));
|
||||
}}
|
||||
value={!!usePassword}
|
||||
label={Languages.t('components.public-link-security_password')}
|
||||
/>
|
||||
<div className="grow mr-2" />
|
||||
{!!usePassword && (
|
||||
<Input
|
||||
disabled={props.disabled}
|
||||
className="max-w-xs"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
// saves and copies password
|
||||
onClick={() => {
|
||||
if (password) copyToClipboard(password);
|
||||
ToasterService.success(
|
||||
Languages.t('components.public-link-security_password_copied'),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-center w-full h-10">
|
||||
<Checkbox
|
||||
disabled={props.disabled}
|
||||
labelNormalSize={true}
|
||||
onChange={s => {
|
||||
setUseExpiration(s);
|
||||
if (!expiration && s) setExpiration(Date.now() + 1000 * 60 * 60 * 24 * 7);
|
||||
}}
|
||||
value={!!useExpiration}
|
||||
label={Languages.t('components.public-link-security_expired')}
|
||||
/>
|
||||
{useExpiration && (expiration || 0) < Date.now() && (
|
||||
<Info className="ml-2 text-red-500">
|
||||
({Languages.t('components.public-link-security_expired')})
|
||||
</Info>
|
||||
)}
|
||||
{useExpiration && (expiration || 0) > Date.now() && (
|
||||
<Info className="ml-2">({expirationDate(expiration)})</Info>
|
||||
)}{' '}
|
||||
<div className="grow mr-2" />
|
||||
{!!useExpiration && (
|
||||
<Input
|
||||
disabled={props.disabled}
|
||||
type="date"
|
||||
className="max-w-xs"
|
||||
value={new Date(expiration || 0).toISOString().split('T')[0]}
|
||||
onChange={e => {
|
||||
e.target.value && setExpiration(new Date(e.target.value).getTime());
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+80
-175
@@ -1,200 +1,105 @@
|
||||
import A from '@atoms/link';
|
||||
import { Base, Info, Subtitle } from '@atoms/text';
|
||||
import { Subtitle } from '@atoms/text';
|
||||
import { useDriveItem, getPublicLink } from '@features/drive/hooks/use-drive-item';
|
||||
import { ToasterService } from '@features/global/services/toaster-service';
|
||||
import { copyToClipboard } from '@features/global/utils/CopyClipboard';
|
||||
import { Checkbox } from 'app/atoms/input/input-checkbox';
|
||||
import { Input } from 'app/atoms/input/input-text';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { AccessLevel } from './common';
|
||||
import moment from 'moment';
|
||||
import 'moment/min/locales';
|
||||
import Languages from 'features/global/services/languages-service';
|
||||
import { debounce } from 'lodash';
|
||||
import { Button } from '@atoms/button/button';
|
||||
import { LinkIcon, UserGroupIcon, CheckCircleIcon } from '@heroicons/react/outline';
|
||||
import type { DriveFileAccessLevel } from 'app/features/drive/types';
|
||||
|
||||
export const PublicLinkManager = ({ id, disabled }: { id: string; disabled?: boolean }) => {
|
||||
const { item, loading, update } = useDriveItem(id);
|
||||
const publicLink = getPublicLink(item);
|
||||
const defaultPublicLinkLevel = 'read';
|
||||
const publicLinkLevel = item?.access_info?.public?.level || 'none';
|
||||
const havePublicLink = publicLinkLevel !== 'none';
|
||||
const [publicLinkCreationLevel, setPublicLinkCreationLevel] = useState<DriveFileAccessLevel>(defaultPublicLinkLevel);
|
||||
const publicLinkCreationLevelSafe = havePublicLink ? publicLinkLevel || defaultPublicLinkLevel : publicLinkCreationLevel;
|
||||
const updatePublicLinkLevel = (level: DriveFileAccessLevel) => {
|
||||
update({
|
||||
access_info: {
|
||||
entities: item?.access_info.entities || [],
|
||||
public: { ...(item?.access_info?.public || { token: '' }), level },
|
||||
},
|
||||
});
|
||||
if (level === 'none')
|
||||
setPublicLinkCreationLevel(defaultPublicLinkLevel);
|
||||
};
|
||||
const [didJustCompleteACopy, setDidJustCompleteACopy] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Base className="block mt-2 mb-1">
|
||||
{Languages.t('components.public-link-acess.public_link_acess')}
|
||||
</Base>
|
||||
<div className="p-4 rounded-md border">
|
||||
<div className="flex flex-row overflow-hidden w-full">
|
||||
<hr />
|
||||
<Subtitle className="block mt-2 mb-1">
|
||||
{Languages.t('components.public-link-acess.public_link_access')}
|
||||
</Subtitle>
|
||||
<div className="rounded-md border">
|
||||
<div className="p-4 flex flex-row items-center justify-center">
|
||||
<div className="grow relative">
|
||||
<Input
|
||||
className={'w-full'}
|
||||
disabled={disabled || !havePublicLink}
|
||||
readOnly={true}
|
||||
onFocus={({target}) => target.select()}
|
||||
theme={havePublicLink ? "blue" : "outline"}
|
||||
value={havePublicLink ? publicLink : Languages.t('components.public-link-acess.public-link-placeholder')}
|
||||
/>
|
||||
<Button
|
||||
disabled={disabled || (!havePublicLink && publicLinkCreationLevel === 'none')}
|
||||
onClick={() => {
|
||||
if (havePublicLink) {
|
||||
copyToClipboard(publicLink);
|
||||
if (!didJustCompleteACopy) {
|
||||
// No point enqueuing further ones, the first timeout will undo immediately anyway
|
||||
// so not bothering with useEffect either
|
||||
setDidJustCompleteACopy(true);
|
||||
setTimeout(() => setDidJustCompleteACopy(false), 1500);
|
||||
}
|
||||
} else {
|
||||
updatePublicLinkLevel(publicLinkCreationLevel);
|
||||
}
|
||||
}}
|
||||
theme={didJustCompleteACopy ? "green" : "primary"}
|
||||
className="absolute top-0 right-0 justify-center"
|
||||
>
|
||||
{ didJustCompleteACopy
|
||||
? <CheckCircleIcon className="w-5 mr-2" />
|
||||
: <LinkIcon className="w-5 mr-2" />
|
||||
}
|
||||
{Languages.t(
|
||||
didJustCompleteACopy
|
||||
? 'components.public-link-copied-info'
|
||||
: (havePublicLink
|
||||
? "components.public-link-copy"
|
||||
: "components.public-link-get"))}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 flex flex-row items-center justify-center text-zinc-800 dark:text-white">
|
||||
<div className="shrink-0">
|
||||
<UserGroupIcon className="w-6 mr-2" />
|
||||
</div>
|
||||
<div className="grow">
|
||||
{item?.access_info?.public?.level !== 'none' && (
|
||||
<Info>{Languages.t('components.public-link-acess.info_acess_true')}</Info>
|
||||
)}
|
||||
{item?.access_info?.public?.level === 'none' && (
|
||||
<Info>{Languages.t('components.public-link-acess.info_acess_false')}</Info>
|
||||
)}
|
||||
{item?.access_info?.public?.level !== 'none' && (
|
||||
<>
|
||||
<br />
|
||||
<A
|
||||
className="inline-block"
|
||||
onClick={() => {
|
||||
copyToClipboard(publicLink);
|
||||
ToasterService.success(Languages.t('components.public-link-copied-info'));
|
||||
}}
|
||||
>
|
||||
{Languages.t('components.public-link-copy')}
|
||||
</A>
|
||||
</>
|
||||
)}
|
||||
{Languages.t('components.public-link-access-level-' + (havePublicLink ? 'update' : 'create'))}
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<AccessLevel
|
||||
hiddenLevels={['manage']}
|
||||
hiddenLevels={['manage'].concat(havePublicLink ? [] : ['none'])}
|
||||
disabled={loading || disabled}
|
||||
level={item?.access_info?.public?.level || null}
|
||||
level={publicLinkCreationLevelSafe}
|
||||
labelOverrides={{'none': Languages.t('components.public-link-access-level-delete')}}
|
||||
onChange={level => {
|
||||
update({
|
||||
access_info: {
|
||||
entities: item?.access_info.entities || [],
|
||||
public: { ...(item?.access_info?.public || { token: '' }), level },
|
||||
},
|
||||
});
|
||||
if (havePublicLink) {
|
||||
updatePublicLinkLevel(level);
|
||||
} else {
|
||||
setPublicLinkCreationLevel(level);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{item?.access_info?.public?.level !== 'none' && (
|
||||
<PublicLinkOptions
|
||||
disabled={loading || disabled}
|
||||
password={item?.access_info?.public?.password}
|
||||
expiration={item?.access_info?.public?.expiration}
|
||||
onChangePassword={(password: string) => {
|
||||
update({
|
||||
access_info: {
|
||||
entities: item?.access_info.entities || [],
|
||||
public: {
|
||||
...item!.access_info!.public!,
|
||||
password: password || '',
|
||||
},
|
||||
},
|
||||
}, true);
|
||||
}}
|
||||
onChangeExpiration={(expiration: number) => {
|
||||
update({
|
||||
access_info: {
|
||||
entities: item?.access_info.entities || [],
|
||||
public: {
|
||||
...item!.access_info!.public!,
|
||||
expiration: expiration || 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>{' '}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const PublicLinkOptions = (props: {
|
||||
disabled?: boolean;
|
||||
password?: string;
|
||||
expiration?: number;
|
||||
onChangePassword: Function;
|
||||
onChangeExpiration: Function;
|
||||
}) => {
|
||||
const [usePassword, setUsePassword] = useState(!!props.password);
|
||||
const [password, setPassword] = useState(props.password);
|
||||
const [useExpiration, setUseExpiration] = useState((props.expiration || 0) > 0);
|
||||
const [expiration, setExpiration] = useState(props.expiration);
|
||||
const handlePasswordBlur = () => {
|
||||
props.onChangePassword(password);
|
||||
};
|
||||
|
||||
const debouncedOnChangePassword = debounce(passwordValue => {
|
||||
props.onChangePassword(passwordValue);
|
||||
}, 500); // 500ms delay
|
||||
|
||||
useEffect(() => {
|
||||
// Ensure the effect runs only if usePassword or password changes
|
||||
debouncedOnChangePassword(usePassword ? password : '');
|
||||
|
||||
// Cleanup function to cancel the debounced call if the component is unmounted or the dependencies change
|
||||
return () => {
|
||||
debouncedOnChangePassword.cancel();
|
||||
};
|
||||
}, [usePassword, password]);
|
||||
|
||||
useEffect(() => {
|
||||
props.onChangeExpiration(useExpiration ? expiration : 0);
|
||||
}, [useExpiration, expiration]);
|
||||
|
||||
function expirationDate(exp: moment.MomentInput) {
|
||||
moment.locale(Languages.getLanguage());
|
||||
return moment(exp).fromNow(true).toLocaleString();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Subtitle className="block mt-4 mb-1">
|
||||
{Languages.t('components.public-link-security')}
|
||||
</Subtitle>
|
||||
<div className="flex items-center justify-center w-full h-10">
|
||||
<Checkbox
|
||||
disabled={props.disabled}
|
||||
onChange={s => {
|
||||
setUsePassword(s);
|
||||
if (!password && s) setPassword(Math.random().toString(36).slice(-8));
|
||||
}}
|
||||
value={!!usePassword}
|
||||
label={Languages.t('components.public-link-security_password')}
|
||||
/>
|
||||
<div className="grow mr-2" />
|
||||
{!!usePassword && (
|
||||
<Input
|
||||
disabled={props.disabled}
|
||||
className="max-w-xs"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
// saves and copies password
|
||||
onClick={() => {
|
||||
if (password) copyToClipboard(password);
|
||||
ToasterService.success(
|
||||
Languages.t('components.public-link-security_password_copied'),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-center w-full h-10">
|
||||
<Checkbox
|
||||
disabled={props.disabled}
|
||||
onChange={s => {
|
||||
setUseExpiration(s);
|
||||
if (!expiration && s) setExpiration(Date.now() + 1000 * 60 * 60 * 24 * 7);
|
||||
}}
|
||||
value={!!useExpiration}
|
||||
label={Languages.t('components.public-link-security_expired')}
|
||||
/>
|
||||
{useExpiration && (expiration || 0) < Date.now() && (
|
||||
<Info className="ml-2 text-red-500">
|
||||
({Languages.t('components.public-link-security_expired')})
|
||||
</Info>
|
||||
)}
|
||||
{useExpiration && (expiration || 0) > Date.now() && (
|
||||
<Info className="ml-2">({expirationDate(expiration)})</Info>
|
||||
)}{' '}
|
||||
<div className="grow mr-2" />
|
||||
{!!useExpiration && (
|
||||
<Input
|
||||
disabled={props.disabled}
|
||||
type="date"
|
||||
className="max-w-xs"
|
||||
value={new Date(expiration || 0).toISOString().split('T')[0]}
|
||||
onChange={e => {
|
||||
e.target.value && setExpiration(new Date(e.target.value).getTime());
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user