💄 front: WIP Implementation of the design for the share link portion (#50)
This commit is contained in:
committed by
ericlinagora
parent
e0fa40f77b
commit
3a0e0a9085
@@ -204,12 +204,24 @@
|
||||
"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-update-subtitle" : "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-title": "Share with link",
|
||||
"components.public-link-security": "Link security settings",
|
||||
"components.public-link-security-change": "Change",
|
||||
"components.public-link-security-change-subtitle": "Set password, expiration date, etc.",
|
||||
"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_removal_title": "About to remove password",
|
||||
"components.public-link-security_password_removal_body": "Warning: about to remove the password on the public link making available to anyone knowing it",
|
||||
"components.public-link-security_password_removal_confirm": "Remove password",
|
||||
"components.public-link-security_expired": "Expired",
|
||||
"components.public-link-security_field_confirm_edit": "Done",
|
||||
"components.public-link-security_expiration_title": "Link expiration",
|
||||
"components.public-link-security_expiration_removal_title": "About to remove expiry date of an expired link",
|
||||
"components.public-link-security_expiration_removal_no_password": "Warning: about to remove the expiry on the public link making available to anyone knowing it",
|
||||
"components.public-link-security_expiration_removal_but_password": "Warning: about to remove the expiry on the public link making available to anyone with the password",
|
||||
"components.public-link-security_expiration_removal_confirm": "Remove 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.",
|
||||
|
||||
@@ -182,7 +182,20 @@
|
||||
"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",
|
||||
"components.public-link-security_expired": "Expiration",
|
||||
"components.public-link-security_expired": "Expiré",
|
||||
"components.public-link-security_expiration_title": "Expiration du lien",
|
||||
"components.public-link-security_expiration_removal_title": "À propos de la suppression de la date d'expiration d'un lien expiré",
|
||||
"components.public-link-security_expiration_removal_no_password": "Attention : sur le point de supprimer l'expiration du lien public, rendant celui-ci accessible à quiconque le connaît",
|
||||
"components.public-link-security_expiration_removal_but_password": "Attention : sur le point de supprimer l'expiration du lien public, rendant celui-ci accessible à quiconque possède le mot de passe",
|
||||
"components.public-link-security_expiration_removal_confirm": "Supprimer l'expiration",
|
||||
"components.public-link-security_field_confirm_edit": "Terminé",
|
||||
"components.public-link-security_password_removal_title": "À propos de la suppression du mot de passe",
|
||||
"components.public-link-security_password_removal_body": "Attention : sur le point de supprimer le mot de passe du lien public, le rendant accessible à quiconque le connaît",
|
||||
"components.public-link-security_password_removal_confirm": "Supprimer le mot de passe",
|
||||
"components.public-link-access-level-update-subtitle": "Toute personne disposant du lien y a accès à",
|
||||
"components.public-link-security-change": "Modifier",
|
||||
"components.public-link-security-change-subtitle": "Définir le mot de passe, la date d'expiration, etc.",
|
||||
"components.public-link-security-title": "Partager avec un lien",
|
||||
"components.internal-access_access_manage": "Gestion générale des accès",
|
||||
"components.internal-access_inherit_parent": "Hériter du dossier parent",
|
||||
"components.internal-access_inherit_parent_perm": "Hériter des permissions du dossier parent",
|
||||
|
||||
@@ -179,10 +179,23 @@
|
||||
"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_expiration_title": "Истечение срока действия ссылки",
|
||||
"components.public-link-security_expiration_removal_title": "О удалении срока действия истекшей ссылки",
|
||||
"components.public-link-security_expiration_removal_no_password": "Предупреждение: о удалении срока действия общедоступной ссылки, что делает её доступной для любого, кто знает её",
|
||||
"components.public-link-security_expiration_removal_but_password": "Предупреждение: о удалении срока действия общедоступной ссылки, что делает её доступной для любого, кто знает пароль",
|
||||
"components.public-link-security_expiration_removal_confirm": "Удалить срок действия",
|
||||
"components.public-link-security_field_confirm_edit": "Готово",
|
||||
"components.public-link-security_password_removal_title": "О удалении пароля",
|
||||
"components.public-link-security_password_removal_body": "Предупреждение: о удалении пароля с общедоступной ссылки, что делает её доступной для любого, кто знает её",
|
||||
"components.public-link-security_password_removal_confirm": "Удалить пароль",
|
||||
"components.public-link-access-level-update-subtitle": "У каждого, кто имеет ссылку, есть доступ к",
|
||||
"components.public-link-security": "Настройки безопасности ссылки",
|
||||
"components.public-link-security-change": "Изменить",
|
||||
"components.public-link-security-change-subtitle": "Установить пароль, срок действия и т.д.",
|
||||
"components.public-link-security-title": "Поделиться ссылкой",
|
||||
"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.",
|
||||
|
||||
@@ -195,10 +195,23 @@
|
||||
"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",
|
||||
"components.public-link-security_expired": "Hết hạn",
|
||||
"components.public-link-security_expiration_title": "Hết hạn liên kết",
|
||||
"components.public-link-security_expiration_removal_title": "Về việc xóa ngày hết hạn của một liên kết đã hết hạn",
|
||||
"components.public-link-security_expiration_removal_no_password": "Cảnh báo: về việc xóa ngày hết hạn của liên kết công cộng, khiến nó có sẵn cho bất kỳ ai biết nó",
|
||||
"components.public-link-security_expiration_removal_but_password": "Cảnh báo: về việc xóa ngày hết hạn của liên kết công cộng, khiến nó có sẵn cho bất kỳ ai có mật khẩu",
|
||||
"components.public-link-security_expiration_removal_confirm": "Xóa hết hạn",
|
||||
"components.public-link-security_field_confirm_edit": "Hoàn tất",
|
||||
"components.public-link-security_password_removal_title": "Về việc xóa mật khẩu",
|
||||
"components.public-link-security_password_removal_body": "Cảnh báo: về việc xóa mật khẩu của liên kết công cộng, khiến nó có sẵn cho bất kỳ ai biết nó",
|
||||
"components.public-link-security_password_removal_confirm": "Xóa mật khẩu",
|
||||
"components.public-link-access-level-update-subtitle": "Bất kỳ ai có liên kết đều có quyền truy cập vào",
|
||||
"components.public-link-security": "Cài đặt bảo mật liên kết",
|
||||
"components.public-link-security-change": "Thay đổi",
|
||||
"components.public-link-security-change-subtitle": "Thiết lập mật khẩu, ngày hết hạn, vv.",
|
||||
"components.public-link-security-title": "Chia sẻ với liên kết",
|
||||
"components.internal-access_access_manage": "Quản lý quyền truy cập chung",
|
||||
"components.internal-access_inherit_parent": "Thừa hưởng từ thư mục cha",
|
||||
"components.internal-access_inherit_parent_perm": "Chọn có thừa hưởng quyền của thư mục cha hay không.",
|
||||
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
import moment from 'moment';
|
||||
import 'moment/min/locales';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { uniqueId } from 'lodash';
|
||||
|
||||
import Languages from 'features/global/services/languages-service';
|
||||
|
||||
import BaseBlock from '@molecules/grouped-rows/base';
|
||||
import { Base, Info } from '@atoms/text';
|
||||
import { Input } from 'app/atoms/input/input-text';
|
||||
import { CheckboxSlider } from 'app/atoms/input/input-checkbox-slider';
|
||||
import { Button } from 'app/atoms/button/button';
|
||||
import { ShieldExclamationIcon, CalendarIcon, PencilAltIcon } from '@heroicons/react/outline';
|
||||
import { Modal, ModalContent } from 'app/atoms/modal';
|
||||
|
||||
import Styles from './styles';
|
||||
|
||||
export const ExpiryEditorRow = (props: {
|
||||
disabled?: boolean;
|
||||
value: number;
|
||||
isLinkPasswordProtected: boolean;
|
||||
onChange: (value: number) => Promise<void>;
|
||||
}) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [currentEditedValue, setCurrentEditedValue] = useState(0);
|
||||
const [isWaitingForSave, setIsWaitingForSave] = useState(false);
|
||||
const [isConfirmingExpiryRemoval, setIsConfirmingExpiryRemoval] = useState(false);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [chkExpiryId] = useState(uniqueId("chk"));
|
||||
|
||||
const disabled = props.disabled || isWaitingForSave;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditing || !inputRef.current) return;
|
||||
let timeout: NodeJS.Timeout | null = setTimeout(() => {
|
||||
timeout = null;
|
||||
inputRef.current?.focus();
|
||||
}, 10);
|
||||
return () => {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
}, [ isEditing ])
|
||||
|
||||
function saveValue(value: number) {
|
||||
setIsWaitingForSave(true);
|
||||
props.
|
||||
onChange(value).
|
||||
then(
|
||||
() => {
|
||||
setIsWaitingForSave(false);
|
||||
setIsEditing(false);
|
||||
},
|
||||
(e: unknown) => {
|
||||
console.error("Error while saving expiry date:", e);
|
||||
setIsWaitingForSave(false);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function confirmSaveExpiry(expiry: number) {
|
||||
if (expiry === props.value) {
|
||||
setIsEditing(false);
|
||||
return;
|
||||
}
|
||||
if (expiry == 0 && (props.value > 0 && props.value < Date.now()))
|
||||
setIsConfirmingExpiryRemoval(true);
|
||||
else
|
||||
saveValue(expiry);
|
||||
}
|
||||
|
||||
function expirationDate(exp: moment.MomentInput) {
|
||||
moment.locale(Languages.getLanguage());
|
||||
return moment(exp).fromNow(true).toLocaleString();
|
||||
}
|
||||
|
||||
return <>
|
||||
<BaseBlock
|
||||
className={"m-4" + (disabled ? Styles.Disabled.Yes : "")}
|
||||
disabled={disabled}
|
||||
avatar={<CalendarIcon className={Styles.SmallIconClasses} />}
|
||||
title={
|
||||
<Base>
|
||||
<label htmlFor={chkExpiryId}>{Languages.t('components.public-link-security_expiration_title')}</label>
|
||||
{!!props.value && !isEditing &&
|
||||
<>
|
||||
<a
|
||||
className={disabled ? Styles.Disabled.Yes : ""}
|
||||
onClick={() => {
|
||||
if (!disabled) {
|
||||
setCurrentEditedValue(props.value);
|
||||
setIsEditing(true);
|
||||
}
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
<PencilAltIcon className={Styles.SmallIconClasses + "ml-1 inline align-bottom"} />
|
||||
</a>
|
||||
|
||||
{(currentEditedValue || props.value) <= Date.now()
|
||||
? <Info className="ml-2 !text-red-500">
|
||||
({Languages.t('components.public-link-security_expired')})
|
||||
</Info>
|
||||
: <Info className="ml-2">({expirationDate(currentEditedValue || props.value)})</Info>
|
||||
}{' '}
|
||||
</>
|
||||
}
|
||||
</Base>
|
||||
}
|
||||
subtitle={''}
|
||||
suffix={isEditing
|
||||
? <>
|
||||
<Input
|
||||
inputRef={inputRef}
|
||||
disabled={disabled}
|
||||
className="max-w-xs mr-4 mt-1"
|
||||
type="date"
|
||||
size='sm'
|
||||
min={moment().add(1, 'days').toISOString().split('T')[0]}
|
||||
value={new Date(currentEditedValue).toISOString().split('T')[0]}
|
||||
onChange={e => e.target.value && setCurrentEditedValue(new Date(e.target.value).getTime())}
|
||||
onKeyUp={e => {
|
||||
if (e.key == 'Escape')
|
||||
setIsEditing(false);
|
||||
else if (e.key == 'Enter')
|
||||
confirmSaveExpiry(currentEditedValue);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
theme="default"
|
||||
size="sm"
|
||||
className="max-w-xs mr-2 mt-1"
|
||||
onClick={() => confirmSaveExpiry(currentEditedValue)}
|
||||
>
|
||||
{Languages.t('components.public-link-security_field_confirm_edit')}
|
||||
</Button>
|
||||
</>
|
||||
: <CheckboxSlider
|
||||
id={chkExpiryId}
|
||||
checked={!!props.value}
|
||||
disabled={disabled}
|
||||
onChange={({target: { checked }}) => {
|
||||
if (checked) {
|
||||
setIsEditing(checked);
|
||||
if (checked && !currentEditedValue)
|
||||
setCurrentEditedValue(props.value || moment().add(7, 'days').unix() * 1000);
|
||||
} else
|
||||
confirmSaveExpiry(0);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<Modal open={isConfirmingExpiryRemoval} onClose={() => setIsConfirmingExpiryRemoval(false)}>
|
||||
<ModalContent
|
||||
title={Languages.t("components.public-link-security_expiration_removal_title")}
|
||||
text={props.isLinkPasswordProtected
|
||||
? Languages.t("components.public-link-security_expiration_removal_but_password")
|
||||
: Languages.t("components.public-link-security_expiration_removal_no_password")
|
||||
}
|
||||
theme="danger"
|
||||
icon={ShieldExclamationIcon}
|
||||
buttons={
|
||||
<>
|
||||
<Button
|
||||
className="ml-2"
|
||||
theme='default'
|
||||
onClick={() => setIsConfirmingExpiryRemoval(false)}
|
||||
>
|
||||
{Languages.t("general.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
theme="danger"
|
||||
onClick={() => {
|
||||
setIsConfirmingExpiryRemoval(false)
|
||||
saveValue(0);
|
||||
}}
|
||||
>
|
||||
{Languages.t("components.public-link-security_expiration_removal_confirm")}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Modal>
|
||||
</>;
|
||||
}
|
||||
@@ -1,13 +1,21 @@
|
||||
import { atom, useRecoilState } from 'recoil';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
import { useDriveItem } from '@features/drive/hooks/use-drive-item';
|
||||
import { useCurrentCompany } from '@features/companies/hooks/use-companies';
|
||||
import Languages from 'features/global/services/languages-service';
|
||||
import { useDriveItem, getPublicLink } from '@features/drive/hooks/use-drive-item';
|
||||
import { useCurrentCompany } from '@features/companies/hooks/use-companies';
|
||||
import { changePublicLink, hasAnyPublicLinkAccess } from '@features/files/utils/access-info-helpers';
|
||||
import type { DriveFileAccessLevel } from '@features/drive/types';
|
||||
|
||||
import A from '@atoms/link';
|
||||
import { Subtitle } from '@atoms/text';
|
||||
import { LockClosedIcon, EyeIcon, PencilIcon, ScaleIcon, EyeOffIcon } from '@heroicons/react/outline';
|
||||
import { Modal, ModalContent } from '@atoms/modal';
|
||||
import { PublicLinkAccessOptions } from '../public-link/public-link-access-options';
|
||||
import BaseBlock from '@molecules/grouped-rows/base';
|
||||
import { AccessLevelDropdown, translateAccessLevel } from '../../components/access-level-dropdown';
|
||||
import { CopyLinkButton } from './copy-link-button';
|
||||
import Styles from './styles';
|
||||
|
||||
export type PublicLinkModalType = {
|
||||
open: boolean;
|
||||
@@ -30,6 +38,7 @@ export const PublicLinkModal = () => {
|
||||
<Modal
|
||||
open={state.open}
|
||||
onClose={() => {
|
||||
setIsOnAdvancedScreen(false);
|
||||
setState({ ...state, open: false });
|
||||
}}
|
||||
>
|
||||
@@ -43,6 +52,61 @@ export const PublicLinkModal = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const ChangePublicLinkAccessLevelRow = (props: {
|
||||
disabled?: boolean;
|
||||
level: DriveFileAccessLevel | null;
|
||||
onChange: (level: DriveFileAccessLevel & 'remove') => void;
|
||||
}) => {
|
||||
const IconForLevel = {
|
||||
'manage': ScaleIcon,
|
||||
'write': PencilIcon,
|
||||
'read': EyeIcon,
|
||||
'none': EyeOffIcon,
|
||||
}[props.level || 'none'];
|
||||
return (
|
||||
<BaseBlock
|
||||
className='p-4 flex flex-row items-center justify-center text-zinc-800 dark:text-white'
|
||||
avatar={<IconForLevel className="w-5 mr-2" />}
|
||||
title={translateAccessLevel(props.level || 'none')}
|
||||
subtitle={Languages.t("components.public-link-access-level-update-subtitle")}
|
||||
suffix={
|
||||
<AccessLevelDropdown
|
||||
className='!p-0 leading-tight text-end !pr-8 border-none bg-transparent dark:bg-transparent'
|
||||
disabled={props.disabled}
|
||||
size={'sm'}
|
||||
level={props.level}
|
||||
hiddenLevels={['remove']}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const SwitchToAdvancedSettingsRow = (props: {
|
||||
disabled: boolean,
|
||||
onShowAdvancedScreen: (active: boolean) => void,
|
||||
}) =>
|
||||
<BaseBlock
|
||||
className='p-4 flex flex-row items-center justify-center text-zinc-800 dark:text-white'
|
||||
avatar={<LockClosedIcon className="w-5 mr-2" />}
|
||||
title={Languages.t('components.public-link-security')}
|
||||
suffix={
|
||||
<A
|
||||
className={"pr-4 inline-block" + (props.disabled ? ' !text-zinc-500' : '')}
|
||||
disabled={props.disabled}
|
||||
noColor={props.disabled}
|
||||
onClick={() => {
|
||||
if (!props.disabled)
|
||||
props.onShowAdvancedScreen(true);
|
||||
}}
|
||||
>
|
||||
{Languages.t("components.public-link-security-change")}
|
||||
</A>
|
||||
}
|
||||
subtitle={Languages.t("components.public-link-security-change-subtitle")}
|
||||
/>;
|
||||
|
||||
const PublicLinkModalContent = (props: {
|
||||
id: string,
|
||||
isOnAdvancedScreen: boolean,
|
||||
@@ -57,19 +121,63 @@ const PublicLinkModalContent = (props: {
|
||||
refreshCompany();
|
||||
}, []);
|
||||
const havePublicLink = hasAnyPublicLinkAccess(item);
|
||||
const publicLink = getPublicLink(item);
|
||||
|
||||
return (
|
||||
<ModalContent
|
||||
title={
|
||||
<>
|
||||
{Languages.t(props.isOnAdvancedScreen
|
||||
? 'components.item_context_menu.manage_access_advanced_to'
|
||||
: 'components.item_context_menu.manage_access_to') + ' '}
|
||||
{Languages.t('components.public-link-security-title') + ' '}
|
||||
<strong>{item?.name}</strong>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Subtitle>Im content ! {havePublicLink ? "Got a" : "No"} public link.</Subtitle>
|
||||
<div className="rounded-md border dark:border-zinc-700 my-5 mb-8">
|
||||
<ChangePublicLinkAccessLevelRow
|
||||
disabled={loading}
|
||||
level={item?.access_info?.public?.level || 'none'}
|
||||
onChange={level => {
|
||||
item && update(changePublicLink(item, { level }));
|
||||
}}
|
||||
/>
|
||||
<SwitchToAdvancedSettingsRow
|
||||
disabled={!havePublicLink}
|
||||
onShowAdvancedScreen={props.onShowAdvancedScreen}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row place-content-end my-4">
|
||||
<CopyLinkButton textToCopy={havePublicLink && publicLink} />
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
open={props.isOnAdvancedScreen}
|
||||
onClose={() => { props.onShowAdvancedScreen(false); }}
|
||||
>
|
||||
<ModalContent
|
||||
title={
|
||||
<>
|
||||
{Languages.t('components.public-link-security-title') + ' '}
|
||||
<strong>{item?.name}</strong>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className='my-4'><Subtitle>{Languages.t('components.public-link-security')}</Subtitle></div>
|
||||
<div className={Styles.RoundedBorderSection}>
|
||||
<PublicLinkAccessOptions
|
||||
disabled={loading || access !== 'manage'}
|
||||
password={item?.access_info?.public?.password}
|
||||
expiration={item?.access_info?.public?.expiration}
|
||||
onChangePassword={async (password: string) => {
|
||||
item && await update(changePublicLink(item, { password: password || '' }));
|
||||
}}
|
||||
onChangeExpiration={async (expiration: number) => {
|
||||
item && await update(changePublicLink(item, { expiration }));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { uniqueId } from 'lodash';
|
||||
|
||||
import Languages from 'features/global/services/languages-service';
|
||||
|
||||
import BaseBlock from '@molecules/grouped-rows/base';
|
||||
import { Base } from '@atoms/text';
|
||||
import { Input } from 'app/atoms/input/input-text';
|
||||
import { CheckboxSlider } from 'app/atoms/input/input-checkbox-slider';
|
||||
import { Button } from 'app/atoms/button/button';
|
||||
import { ShieldCheckIcon, PencilAltIcon, ShieldExclamationIcon } from '@heroicons/react/outline';
|
||||
import { Modal, ModalContent } from 'app/atoms/modal';
|
||||
|
||||
import Styles from './styles';
|
||||
|
||||
export const PasswordEditorRow = (props: {
|
||||
disabled?: boolean;
|
||||
password?: string;
|
||||
isLinkExpired: boolean;
|
||||
onChangePassword: (password: string) => Promise<void>;
|
||||
}) => {
|
||||
const [isEditingPassword, setIsEditingPassword] = useState(false);
|
||||
const [currentEditedPassword, setCurrentEditedPassword] = useState("");
|
||||
const [isWaitingForPasswordSave, setIsWaitingForPasswordSave] = useState(false);
|
||||
const [isConfirmingPasswordRemoval, setIsConfirmingPasswordRemoval] = useState(false);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [chkPasswordId] = useState(uniqueId("chk"));
|
||||
|
||||
const disabled = props.disabled || isWaitingForPasswordSave;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditingPassword || !inputRef.current) return;
|
||||
let timeout: NodeJS.Timeout | null = setTimeout(() => {
|
||||
timeout = null;
|
||||
inputRef.current?.focus();
|
||||
}, 10);
|
||||
return () => {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
}, [ isEditingPassword ])
|
||||
|
||||
function savePassword(password: string) {
|
||||
setIsWaitingForPasswordSave(true);
|
||||
props.
|
||||
onChangePassword(password).
|
||||
then(
|
||||
() => {
|
||||
setIsWaitingForPasswordSave(false);
|
||||
setIsEditingPassword(false);
|
||||
},
|
||||
(e: unknown) => {
|
||||
console.error("Error while saving password:", e);
|
||||
setIsWaitingForPasswordSave(false);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function confirmSavePassword(password: string) {
|
||||
if (password === (props.password ?? "")) {
|
||||
setIsEditingPassword(false);
|
||||
return;
|
||||
}
|
||||
// If link is also expired, removing the password won't expose it,
|
||||
// no need to stress the user with a confirmation
|
||||
if (!password.length && !props.isLinkExpired)
|
||||
setIsConfirmingPasswordRemoval(true);
|
||||
else
|
||||
savePassword(password);
|
||||
}
|
||||
|
||||
return <>
|
||||
<BaseBlock
|
||||
className={"m-4" + (disabled ? Styles.Disabled.Yes : "")}
|
||||
disabled={disabled}
|
||||
avatar={<ShieldCheckIcon className={Styles.SmallIconClasses} />}
|
||||
title={
|
||||
<Base>
|
||||
<label htmlFor={chkPasswordId}>{Languages.t('components.public-link-security_password')}</label>
|
||||
{!!props.password?.length && !isEditingPassword &&
|
||||
<a
|
||||
className={disabled ? Styles.Disabled.Yes : ""}
|
||||
onClick={() => {
|
||||
if (!disabled) {
|
||||
setCurrentEditedPassword(props.password!);
|
||||
setIsEditingPassword(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PencilAltIcon className={Styles.SmallIconClasses + "ml-1 inline align-bottom"} />
|
||||
</a>
|
||||
}
|
||||
</Base>
|
||||
}
|
||||
subtitle={''}
|
||||
suffix={isEditingPassword
|
||||
? <>
|
||||
<Input
|
||||
inputRef={inputRef}
|
||||
disabled={disabled}
|
||||
className="max-w-xs mr-4 mt-1"
|
||||
size='sm'
|
||||
value={currentEditedPassword}
|
||||
onChange={e => setCurrentEditedPassword(e.target.value)}
|
||||
onKeyUp={e => {
|
||||
if (e.key == 'Escape')
|
||||
setIsEditingPassword(false);
|
||||
else if (e.key == 'Enter')
|
||||
confirmSavePassword(currentEditedPassword);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
theme="default"
|
||||
size="sm"
|
||||
className="max-w-xs mr-2 mt-1"
|
||||
onClick={() => confirmSavePassword(currentEditedPassword)}
|
||||
>
|
||||
{Languages.t('components.public-link-security_field_confirm_edit')}
|
||||
</Button>
|
||||
</>
|
||||
: <CheckboxSlider
|
||||
id={chkPasswordId}
|
||||
checked={!!props.password?.length}
|
||||
disabled={disabled}
|
||||
onChange={({target: { checked }}) => {
|
||||
if (checked) {
|
||||
setIsEditingPassword(checked);
|
||||
if (checked && !currentEditedPassword.length)
|
||||
setCurrentEditedPassword(!props.password?.length ? Math.random().toString(36).slice(-8) : props.password);
|
||||
} else
|
||||
setIsConfirmingPasswordRemoval(true);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<Modal open={isConfirmingPasswordRemoval} onClose={() => setIsConfirmingPasswordRemoval(false)}>
|
||||
<ModalContent
|
||||
title={Languages.t("components.public-link-security_password_removal_title")}
|
||||
text={Languages.t("components.public-link-security_password_removal_body")}
|
||||
theme="danger"
|
||||
icon={ShieldExclamationIcon}
|
||||
buttons={
|
||||
<>
|
||||
<Button
|
||||
className="ml-2"
|
||||
theme='default'
|
||||
onClick={() => setIsConfirmingPasswordRemoval(false)}
|
||||
>
|
||||
{Languages.t("general.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
theme="danger"
|
||||
onClick={() => {
|
||||
setIsConfirmingPasswordRemoval(false)
|
||||
savePassword("");
|
||||
}}
|
||||
>
|
||||
{Languages.t("components.public-link-security_password_removal_confirm")}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Modal>
|
||||
</>;
|
||||
}
|
||||
+20
-114
@@ -1,120 +1,26 @@
|
||||
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';
|
||||
import { PasswordEditorRow } from './password-editor-row';
|
||||
import { ExpiryEditorRow } from './expiry-editor-row';
|
||||
|
||||
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);
|
||||
};
|
||||
onChangePassword: (password: string) => Promise<void>;
|
||||
onChangeExpiration: (expiration: number) => Promise<void>;
|
||||
}) => (
|
||||
<>
|
||||
<PasswordEditorRow
|
||||
disabled={props.disabled}
|
||||
password={props.password}
|
||||
onChangePassword={props.onChangePassword}
|
||||
isLinkExpired={!!props.expiration && props.expiration < Date.now()}
|
||||
/>
|
||||
<ExpiryEditorRow
|
||||
disabled={props.disabled}
|
||||
value={props.expiration ?? 0}
|
||||
onChange={props.onChangeExpiration}
|
||||
isLinkPasswordProtected={!!props.password?.length}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+2
-5
@@ -1,12 +1,10 @@
|
||||
import { Subtitle } from '@atoms/text';
|
||||
import { useDriveItem, getPublicLink } from '@features/drive/hooks/use-drive-item';
|
||||
import { copyToClipboard } from '@features/global/utils/CopyClipboard';
|
||||
import { Input } from 'app/atoms/input/input-text';
|
||||
import { useState } from 'react';
|
||||
import { AccessLevelDropdown } from '../../components/access-level-dropdown';
|
||||
import Languages from 'features/global/services/languages-service';
|
||||
import { Button } from '@atoms/button/button';
|
||||
import { LinkIcon, UserGroupIcon, CheckCircleIcon } from '@heroicons/react/outline';
|
||||
import { UserGroupIcon } from '@heroicons/react/outline';
|
||||
import type { DriveFileAccessLevelForPublicLink } from 'app/features/drive/types';
|
||||
import { changePublicLink } from '@features/files/utils/access-info-helpers';
|
||||
import { CopyLinkButton } from './copy-link-button';
|
||||
@@ -24,7 +22,6 @@ export const PublicLinkManager = ({ id, disabled }: { id: string; disabled?: boo
|
||||
if (level === 'none')
|
||||
setPublicLinkCreationLevel(defaultPublicLinkLevel);
|
||||
};
|
||||
const [didJustCompleteACopy, setDidJustCompleteACopy] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -57,7 +54,7 @@ export const PublicLinkManager = ({ id, disabled }: { id: string; disabled?: boo
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<AccessLevelDropdown
|
||||
hiddenLevels={['manage'].concat(havePublicLink ? [] : ['none'])}
|
||||
hiddenLevels={['manage', 'remove'].concat(havePublicLink ? [] : ['none'])}
|
||||
disabled={loading || disabled}
|
||||
level={publicLinkCreationLevelSafe}
|
||||
labelOverrides={{'none': Languages.t('components.public-link-access-level-delete')}}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export default {
|
||||
RoundedBorderSection: "rounded-md border dark:border-zinc-700 my-5 mb-8",
|
||||
SmallIconClasses: " w-5 mr-2 dark:text-white ",
|
||||
Disabled: {
|
||||
Yes: " opacity-50 cursor-default ",
|
||||
},
|
||||
}
|
||||
@@ -109,43 +109,11 @@ const AccessModalContent = (props: {
|
||||
</>
|
||||
}
|
||||
>
|
||||
{!props.isOnAdvancedScreen ?
|
||||
<>
|
||||
<PublicLinkManager id={id} disabled={access !== 'manage'} />
|
||||
<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) => {
|
||||
item && changePublicLink(item, { password: password || '' });
|
||||
}}
|
||||
onChangeExpiration={(expiration: number) => {
|
||||
item && changePublicLink(item, { expiration: expiration || 0 });
|
||||
}}
|
||||
/>}
|
||||
{ parentItem?.parent_id !== null && <>
|
||||
<InheritAccessOptions
|
||||
item={item}
|
||||
disabled={loading}
|
||||
onUpdate={update}
|
||||
/>
|
||||
<CuteDepictionOfFolderHierarchy
|
||||
file={item}
|
||||
parent={parentItem}
|
||||
/>
|
||||
</>}
|
||||
</>}
|
||||
{FeatureTogglesService.isActiveFeatureName(FeatureNames.COMPANY_SEARCH_USERS) && (
|
||||
<InternalAccessManager id={id} disabled={access !== 'manage'} />
|
||||
)}
|
||||
</ModalContent>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user