Eric Doughty-Papassideris
2024-04-06 23:21:18 +02:00
committed by ericlinagora
parent a2e4334361
commit 579b64972e
18 changed files with 564 additions and 370 deletions
+14 -10
View File
@@ -196,16 +196,19 @@
"components.locked_features.locked_only_office_popup.text": "Whoops!! Seems like you want to edit the file from Tdrive. You might want to consider upgrading your plan if you want to collaborate around office files. Explore our pricing options and select the best option for your team collaboration needs.",
"components.locked_features.locked_drive_popup.title": "You have just reached the drive limit",
"components.locked_features.locked_drive_popup.subtitle": "In the free version, you can store only 6GB.",
"components.public-link-acess.public_link_acess": "Public link acess",
"components.public-link-acess.info_acess_true": "Anyone with this link will have access to this item.",
"components.public-link-acess.info_acess_false": "This item is not available by public link.",
"components.public-link-acess.public-link-placeholder": "https://tdrive.com/your-simple-link",
"components.public-link-acess.public_link_access": "Share via link",
"components.public-link-acess.info_acess_expired": "Expired",
"components.public-link-copied-info" : "Public link copied to clipboard",
"components.public-link-copy" : "Copy public link to clip board",
"components.public-link-copied-info" : "Copied",
"components.public-link-get" : "Create link",
"components.public-link-copy" : "Copy link",
"components.public-link-access-level-create" : "Anyone with the link will have access to",
"components.public-link-access-level-update" : "Anyone with the link has access to",
"components.public-link-access-level-delete" : "Delete link",
"components.public-link-security": "Public link security",
"components.public-link-security_password": "Password",
"components.public-link-security_password_copied": "Password copied to clipboard",
"components.public-link-security_expired": "Expiration",
"components.public-link-security_password": "Password",
"components.public-link-security_password_copied": "Password copied to clipboard",
"components.public-link-security_expired": "Expiration",
"components.internal-access_access_manage":"General access management",
"components.internal-access_inherit_parent":"Inherit parent folder",
"components.internal-access_inherit_parent_perm":"Choose to inherit or not the parent folder permissions.",
@@ -304,7 +307,8 @@
"components.item_context_menu.download": "Download",
"components.item_context_menu.rename": "Rename",
"components.item_context_menu.manage_access": "Manage access",
"components.item_context_menu.manage_access_to": "Manage access to",
"components.item_context_menu.manage_access_to": "Share",
"components.item_context_menu.manage_access_advanced_to": "Manage access to",
"components.item_context_menu.share": "Share",
"components.item_context_menu.copy_link": "Copy link",
"components.item_context_menu.copy_link.success": "Link copied to clipboard",
@@ -342,5 +346,5 @@
"scenes.app.shared_with_me.edit": "Edit",
"ONLYOFFICE Word Document": "Create a Document",
"ONLYOFFICE Excel Document": "Create a Spreadsheet",
"ONLYOFFICE PowerPoint Document": "Create a Presentation"
"ONLYOFFICE PowerPoint Document": "Create a Presentation"
}
+3 -5
View File
@@ -123,15 +123,13 @@
"components.locked_features.locked_only_office_popup.text": "¡Ups! Parece que desea editar el archivo de Tdrive. Es posible que desee considerar la posibilidad de actualizar su plan si desea colaborar en torno a los archivos de Office. Explore nuestras opciones de precios y seleccione la mejor opción para las necesidades de colaboración de su equipo.",
"components.locked_features.locked_drive_popup.title": "No puedes subir mas archivos",
"components.locked_features.locked_drive_popup.subtitle": "En la versión gratuita, puede almacenar solo 6 GB.",
"components.public-link-acess.public_link_acess": "Accesibilidad del enlace público",
"components.public-link-acess.info_acess_true": "Cualquier persona con a este enlace tiene acceso al archivo.",
"components.public-link-acess.info_acess_false": "Este archivo no está disponible por enlace público.",
"components.public-link-acess.public_link_access": "Accesibilidad del enlace público",
"components.public-link-acess.info_acess_expired": "Expirado",
"components.public-link-copied-info" : "Enlace público copiado en el portapapeles",
"components.public-link-copy" : "Copiar enlace en el portapapeles",
"components.public-link-security": "Seguridad del enlace público",
"components.public-link-security_password": "Contraseña",
"components.public-link-security_password_copied": "Contraseña copiada en el portapapeles",
"components.public-link-security_password": "Contraseña",
"components.public-link-security_password_copied": "Contraseña copiada en el portapapeles",
"components.public-link-security_expired": "Expiración",
"scenes.app.popup.adduser.adresses_message": "Por favor, separe las direcciones con una coma o un espacio. Recuerda que Tdrive puede limitar el número de invitaciones en función de tu suscripción.",
"scenes.app.channelsbar.currentuser.logout": "Cerrar sesión",
+6 -6
View File
@@ -173,12 +173,11 @@
"components.locked_features.locked_only_office_popup.text": "Oups!! Vous souhaitez modifier le fichier à partir de Twake Drive. Vous voudrez peut-être envisager de mettre à niveau votre plan si vous souhaitez collaborer autour de fichiers bureautiques. Explorez nos options de tarification et sélectionnez la meilleure option pour les besoins de collaboration de votre équipe.",
"components.locked_features.locked_drive_popup.title": "Vous venez d'atteindre la limite d'importation",
"components.locked_features.locked_drive_popup.subtitle": "Dans la version gratuite, vous ne pouvez pas stocker plus de 6 Go.",
"components.public-link-acess.public_link_acess": "Lien d'accès public",
"components.public-link-acess.info_acess_true": "N'importe qui ayant ce lien aura accès à ce fichier",
"components.public-link-acess.info_acess_false": "Ce fichier n'est pas accessible via un lien public",
"components.public-link-acess.public_link_access": "Partager avec un lien",
"components.public-link-acess.info_acess_expired": "Expiré",
"components.public-link-copied-info": "Le lien public a été copié dans le presse-papiers",
"components.public-link-copy": "Copier le lien public dans le presse-papiers",
"components.public-link-get" : "Créer un lien",
"components.public-link-copied-info": "Copié",
"components.public-link-copy": "Copier le lien",
"components.public-link-security": "Sécurité du lien public",
"components.public-link-security_password": "Mot de passe",
"components.public-link-security_password_copied": "Mot de passe copié dans le presse-papiers",
@@ -297,7 +296,8 @@
"components.item_context_menu.download": "Télécharger",
"components.item_context_menu.rename": "Renommer",
"components.item_context_menu.manage_access": "Gérer l'accès",
"components.item_context_menu.manage_access_to": "Gérer l'accès à",
"components.item_context_menu.manage_access_to": "Partage de",
"components.item_context_menu.manage_access_advanced_to": "Gérer l'accès de",
"components.item_context_menu.share": "Partager",
"components.item_context_menu.copy_link": "Copier le lien",
"components.item_context_menu.copy_link.success": "Lien copié dans le presse-papiers",
+7 -8
View File
@@ -173,16 +173,15 @@
"components.locked_features.locked_guests_popup.learn_more_button": "Узнать больше",
"components.locked_features.locked_guests_popup.or": "Или",
"scenes.client.main_view.main_header.search_input": "Поиск",
"components.public-link-acess.public_link_acess": "Public link acess",
"components.public-link-acess.info_acess_true": "Anyone with this link will have access to this item.",
"components.public-link-acess.info_acess_false": "This item is not available by public link.",
"components.public-link-acess.public_link_access": "Поделитесь ссылкой",
"components.public-link-acess.info_acess_expired": "Expired",
"components.public-link-copied-info" : "Public link copied to clipboard",
"components.public-link-copy" : "Copy public link to clip board",
"components.public-link-copied-info" : "Ссылка скопирована",
"components.public-link-get" : "Создать ссылку",
"components.public-link-copy" : "Копировать ссылку",
"components.public-link-security": "Public link security",
"components.public-link-security_password": "Password",
"components.public-link-security_password_copied": "Password copied to clipboard",
"components.public-link-security_expired": "Expiration",
"components.public-link-security_password": "Password",
"components.public-link-security_password_copied": "Password copied to clipboard",
"components.public-link-security_expired": "Expiration",
"components.internal-access_access_manage":"General access management",
"components.internal-access_inherit_parent":"Inherit parent folder",
"components.internal-access_inherit_parent_perm":"Choose to inherit or not the parent folder permissions.",
+6 -7
View File
@@ -67,7 +67,7 @@
"scenes.apps.account.account.main_email": "Email chính",
"scenes.apps.account.account.email_add": "Thêm e-mail mới",
"scenes.apps.account.account.email_add_modal.invalid_code": "Mã này không hợp lệ",
"scenes.apps.account.account.password": "Mật khẩu",
"scenes.apps.account.account.password": "Mật khẩu",
"scenes.apps.account.account.password_modal.old_password": "Mật khẩu hiện tại",
"scenes.apps.account.account.password_modal.password": "Mật khẩu mới",
"scenes.apps.account.account.password_modal.bad_old_password": "Mật khẩu cũ không chính xác.",
@@ -189,12 +189,11 @@
"components.locked_features.locked_only_office_popup.text": "Ồ, không! Có vẻ như bạn muốn chỉnh sửa tệp từ Tdrive. Bạn nên cân nhắc nâng cấp gói đăng ký nếu muốn cộng tác xung quanh các tệp văn phòng. Hãy khám phá các tùy chọn giá của chúng tôi và chọn tùy chọn phù hợp nhất với nhu cầu cộng tác nhóm của bạn.",
"components.locked_features.locked_drive_popup.title": "Bạn vừa đạt đến giới hạn ổ đĩa",
"components.locked_features.locked_drive_popup.subtitle": "Trong phiên bản miễn phí, bạn chỉ có thể lưu trữ 6GB.",
"components.public-link-acess.public_link_acess": "Quyền truy cập liên kết công khai",
"components.public-link-acess.info_acess_true": "Bất kỳ ai có liên kết này đều có thể truy cập vào mục này.",
"components.public-link-acess.info_acess_false": "Mục này không khả dụng theo liên kết công khai.",
"components.public-link-acess.public_link_access": "Chia sẻ bằng một liên kết",
"components.public-link-acess.info_acess_expired": "Hết hạn",
"components.public-link-copied-info": "Liên kết công khai đã được sao chép vào khay nhớ tạm",
"components.public-link-copy": "Sao chép liên kết công khai vào khay nhớ tạm",
"components.public-link-copied-info": "Đã sao chép liên kết",
"components.public-link-get" : "Tạo đường link",
"components.public-link-copy": "Sao chép đường dẫn",
"components.public-link-security": "Bảo mật liên kết công khai",
"components.public-link-security_password": "Mật khẩu",
"components.public-link-security_password_copied": "Mật khẩu đã được sao chép vào khay nhớ tạm",
@@ -270,7 +269,7 @@
"components.side_menu.buttons.empty_trash": "Làm trống thùng rác",
"components.header_path.shared_trash": "Thùng rác được chia sẻ",
"components.header_path.my_trash": "Thùng rác của tôi",
"components.header_path.shared_with_me": "Chia sẻ với tôi",
"components.header_path.shared_with_me": "Chia sẻ với tôi",
"scenes.app.drive.nothing": "Không có gì ở đây.",
"scenes.app.drive.drag_and_drop": "Kéo và thả tệp để tải lên hoặc nhấp vào nút 'Thêm tài liệu'.",
"scenes.app.drive.add_doc": "Thêm tài liệu hoặc thư mục",
@@ -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>
);
@@ -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>
);
};
@@ -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,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} />
@@ -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>
</>
);
};
@@ -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>
</>
);