Fix upload files / folders

This commit is contained in:
Romaric Mourgues
2023-03-28 10:58:48 +02:00
parent 682b847ad1
commit bdfa3cb851
15 changed files with 189 additions and 94 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ export const Alert = (props: {
let color = 'blue'; let color = 'blue';
let textColor = 'white'; let textColor = 'white';
if (props.theme === 'success') color = 'green-500'; if (props.theme === 'success') color = 'green-500';
if (props.theme === 'danger') color = 'red-500'; if (props.theme === 'danger') color = 'rose-500';
if (props.theme === 'warning') color = 'orange-500'; if (props.theme === 'warning') color = 'orange-500';
if (props.theme === 'gray') { if (props.theme === 'gray') {
color = 'zinc-50'; color = 'zinc-50';
@@ -0,0 +1,49 @@
import _ from "lodash";
import {
InputHTMLAttributes,
memo,
ReactNode,
useCallback,
useEffect,
useRef,
} from "react";
let interval: any = null;
export const AnimatedHeight = memo(
(props: { children: ReactNode } & InputHTMLAttributes<HTMLDivElement>) => {
const el = useRef<HTMLDivElement>(null);
const updateSize = useCallback(() => {
if (el.current) {
const contentHeight = el.current.scrollHeight;
const parent = el.current.parentNode as HTMLDivElement;
parent.style.height = `${contentHeight}px`;
parent.style.overflow = `hidden`;
}
}, [el]);
useEffect(() => {
interval = setInterval(() => {
updateSize();
}, 200);
return () => {
clearInterval(interval);
};
}, []);
return (
<div className="transition-all px-1 -mx-1">
<div
{..._.omit(props, "children")}
ref={el}
style={{
boxSizing: "border-box",
}}
>
{props.children}
</div>
</div>
);
}
);
@@ -16,7 +16,7 @@ export const InputLabel = (props: InputLabelProps) => {
<label className="block text-sm text-zinc-400">{props.label}</label> <label className="block text-sm text-zinc-400">{props.label}</label>
<div className="mt-1">{props.input}</div> <div className="mt-1">{props.input}</div>
{props.feedback && ( {props.feedback && (
<Info noColor className={props.hasError ? 'text-red-400' : 'text-blue-500'}> <Info noColor className={props.hasError ? 'text-rose-400' : 'text-blue-500'}>
{props.feedback} {props.feedback}
</Info> </Info>
)} )}
@@ -35,8 +35,8 @@ export const errorInputClassName = (theme: 'plain' | 'outline' = 'plain') => {
return ( return (
baseInputClassName + baseInputClassName +
(theme === 'plain' (theme === 'plain'
? 'bg-red-200 border-red-200 dark:bg-red-800 dark:border-red-800' ? 'bg-rose-200 border-rose-200 dark:bg-rose-800 dark:border-rose-800'
: 'bg-red-50 border-red-300 dark:bg-red-900 dark:border-red-800') : 'bg-rose-50 border-rose-300 dark:bg-rose-900 dark:border-rose-800')
); );
}; };
+1 -1
View File
@@ -155,7 +155,7 @@ export const ModalContent = (props: {
}) => { }) => {
let color = 'blue'; let color = 'blue';
if (props.theme === 'success') color = 'green'; if (props.theme === 'success') color = 'green';
if (props.theme === 'danger') color = 'red'; if (props.theme === 'danger') color = 'rose';
if (props.theme === 'warning') color = 'orange'; if (props.theme === 'warning') color = 'orange';
if (props.theme === 'gray') color = 'gray'; if (props.theme === 'gray') color = 'gray';
return ( return (
@@ -143,7 +143,7 @@ export const ChannelInformationForm = (props: {
<div className="mt-2"> <div className="mt-2">
{!!icon && ( {!!icon && (
<A <A
className="!text-red-500" className="!text-rose-500"
onClick={() => { onClick={() => {
setIcon(''); setIcon('');
}} }}
@@ -210,9 +210,9 @@ const RemoveBlock = (props: { channel?: ChannelType; onLeave?: Function }) => {
props.onLeave && props.onLeave(); props.onLeave && props.onLeave();
}); });
}} }}
className="-mx-2 my-1 p-2 rounded-md cursor-pointer hover:bg-red-50 dark:hover:bg-red-800" className="-mx-2 my-1 p-2 rounded-md cursor-pointer hover:bg-rose-50 dark:hover:bg-rose-800"
title={ title={
<Base noColor className="font-semibold text-red-500"> <Base noColor className="font-semibold text-rose-500">
{Languages.t('scenes.app.channelsbar.channel_removing')} {Languages.t('scenes.app.channelsbar.channel_removing')}
</Base> </Base>
} }
@@ -221,7 +221,7 @@ const RemoveBlock = (props: { channel?: ChannelType; onLeave?: Function }) => {
loading ? ( loading ? (
<Loader className="w-4 h-4 m-1" /> <Loader className="w-4 h-4 m-1" />
) : ( ) : (
<TrashIcon className="text-red-500 w-6 h-6" /> <TrashIcon className="text-rose-500 w-6 h-6" />
) )
} }
/> />
@@ -280,9 +280,9 @@ const LeaveBlock = (props: { channel?: ChannelType; onLeave?: Function }) => {
return leaveChannel(); return leaveChannel();
}} }}
className="-mx-2 my-1 p-2 rounded-md cursor-pointer hover:bg-red-50 dark:hover:bg-red-800" className="-mx-2 my-1 p-2 rounded-md cursor-pointer hover:bg-rose-50 dark:hover:bg-rose-800"
title={ title={
<Base noColor className="font-semibold text-red-500"> <Base noColor className="font-semibold text-rose-500">
{Languages.t('scenes.app.channelsbar.channel_leaving')} {Languages.t('scenes.app.channelsbar.channel_leaving')}
</Base> </Base>
} }
@@ -291,7 +291,7 @@ const LeaveBlock = (props: { channel?: ChannelType; onLeave?: Function }) => {
loading ? ( loading ? (
<Loader className="w-4 h-4 m-1" /> <Loader className="w-4 h-4 m-1" />
) : ( ) : (
<LogoutIcon className="text-red-500 w-6 h-6" /> <LogoutIcon className="text-rose-500 w-6 h-6" />
) )
} }
/> />
@@ -4,7 +4,7 @@ import React from 'react';
export default (): React.ReactElement => { export default (): React.ReactElement => {
return ( return (
<Text type="base" className="text-red-500" noColor={true}> <Text type="base" className="text-rose-500" noColor={true}>
{Languages.t( {Languages.t(
'components.invitation.reached_limit.text', 'components.invitation.reached_limit.text',
[], [],
@@ -21,6 +21,8 @@ type FileInputType = any;
type FileObjectType = { [key: string]: any }; type FileObjectType = { [key: string]: any };
let sharedFileInput: any = null; let sharedFileInput: any = null;
let sharedFolderInput: any = null;
export default class UploadZone extends React.Component<PropsType, StateType> { export default class UploadZone extends React.Component<PropsType, StateType> {
file_input: FileInputType = {}; file_input: FileInputType = {};
stopHoverTimeout: ReturnType<typeof setTimeout> | undefined; stopHoverTimeout: ReturnType<typeof setTimeout> | undefined;
@@ -41,7 +43,10 @@ export default class UploadZone extends React.Component<PropsType, StateType> {
componentDidMount() { componentDidMount() {
this.node && this.watch(this.node, document.body); this.node && this.watch(this.node, document.body);
if (!sharedFileInput) { if (
(this.props.directory && !sharedFolderInput) ||
(!this.props.directory && !sharedFileInput)
) {
this.file_input = document.createElement('input'); this.file_input = document.createElement('input');
this.file_input.type = 'file'; this.file_input.type = 'file';
this.file_input.style.position = 'absolute'; this.file_input.style.position = 'absolute';
@@ -49,14 +54,18 @@ export default class UploadZone extends React.Component<PropsType, StateType> {
this.file_input.style.left = '-10000px'; this.file_input.style.left = '-10000px';
this.file_input.style.width = '100px'; this.file_input.style.width = '100px';
this.file_input.multiple = this.props.multiple ? true : false; this.file_input.multiple = this.props.multiple ? true : false;
this.file_input.directory = this.props.multiple ? true : false; this.file_input.directory = this.props.directory ? true : false;
this.file_input.webkitdirectory = this.props.multiple ? true : false; this.file_input.webkitdirectory = this.props.directory ? true : false;
this.setCallback(); this.setCallback();
document.body.appendChild(this.file_input); document.body.appendChild(this.file_input);
sharedFileInput = this.file_input; if (this.props.directory) {
sharedFolderInput = this.file_input;
} else {
sharedFileInput = this.file_input;
}
} else { } else {
this.file_input = sharedFileInput; this.file_input = sharedFileInput;
} }
@@ -21,7 +21,7 @@ import { FolderRow } from './documents/folder-row';
import HeaderPath from './header-path'; import HeaderPath from './header-path';
import { ConfirmDeleteModal } from './modals/confirm-delete'; import { ConfirmDeleteModal } from './modals/confirm-delete';
import { ConfirmTrashModal } from './modals/confirm-trash'; import { ConfirmTrashModal } from './modals/confirm-trash';
import { CreateModal, CreateModalAtom } from './modals/create'; import { CreateModalAtom } from './modals/create';
import { PropertiesModal } from './modals/properties'; import { PropertiesModal } from './modals/properties';
import { AccessModal } from './modals/update-access'; import { AccessModal } from './modals/update-access';
import { VersionsModal } from './modals/versions'; import { VersionsModal } from './modals/versions';
@@ -1,11 +1,16 @@
import { Transition } from '@headlessui/react'; import { Transition } from '@headlessui/react';
import { ChevronLeftIcon, DesktopComputerIcon } from '@heroicons/react/outline'; import {
import { FolderIcon } from '@heroicons/react/solid'; ChevronLeftIcon,
DesktopComputerIcon,
DocumentDownloadIcon,
FolderAddIcon,
FolderDownloadIcon,
FolderIcon,
} from '@heroicons/react/outline';
import Avatar from 'app/atoms/avatar'; import Avatar from 'app/atoms/avatar';
import A from 'app/atoms/link'; import A from 'app/atoms/link';
import { Modal, ModalContent } from 'app/atoms/modal'; import { Modal, ModalContent } from 'app/atoms/modal';
import { Base } from 'app/atoms/text'; import { Base } from 'app/atoms/text';
import { useApplications } from 'app/features/applications/hooks/use-applications';
import { useCompanyApplications } from 'app/features/applications/hooks/use-company-applications'; import { useCompanyApplications } from 'app/features/applications/hooks/use-company-applications';
import { Application } from 'app/features/applications/types/application'; import { Application } from 'app/features/applications/types/application';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
@@ -29,9 +34,11 @@ export const CreateModalAtom = atom<CreateModalAtomType>({
export const CreateModal = ({ export const CreateModal = ({
selectFromDevice, selectFromDevice,
selectFolderFromDevice,
addFromUrl, addFromUrl,
}: { }: {
selectFromDevice: () => void; selectFromDevice: () => void;
selectFolderFromDevice: () => void;
addFromUrl: (url: string, name: string) => void; addFromUrl: (url: string, name: string) => void;
}) => { }) => {
const [state, setState] = useRecoilState(CreateModalAtom); const [state, setState] = useRecoilState(CreateModalAtom);
@@ -72,15 +79,20 @@ export const CreateModal = ({
> >
<div className="-m-2"> <div className="-m-2">
<CreateModalOption <CreateModalOption
icon={<FolderIcon className="w-5 h-5" />} icon={<FolderAddIcon className="w-5 h-5" />}
text="Create a folder" text="Create a folder"
onClick={() => setState({ ...state, type: 'folder' })} onClick={() => setState({ ...state, type: 'folder' })}
/> />
<CreateModalOption <CreateModalOption
icon={<DesktopComputerIcon className="w-5 h-5" />} icon={<DocumentDownloadIcon className="w-5 h-5" />}
text="Upload document from device" text="Upload files from device"
onClick={() => selectFromDevice()} onClick={() => selectFromDevice()}
/> />
<CreateModalOption
icon={<FolderDownloadIcon className="w-5 h-5" />}
text="Upload folders from device"
onClick={() => selectFolderFromDevice()}
/>
{(applications || []) {(applications || [])
.filter(app => app.display?.twake?.files?.editor?.empty_files?.length) .filter(app => app.display?.twake?.files?.editor?.empty_files?.length)
@@ -22,7 +22,7 @@ export const AccessLevel = ({
className={ className={
className + className +
' w-auto ' + ' w-auto ' +
(level === 'none' ? '!text-red-500 !bg-red-100 dark-bg-red-800' : '') (level === 'none' ? '!text-rose-500 !bg-rose-100 dark-bg-rose-800' : '')
} }
value={level || 'none'} value={level || 'none'}
onChange={e => onChange(e.target.value as DriveFileAccessLevel & 'remove')} onChange={e => onChange(e.target.value as DriveFileAccessLevel & 'remove')}
@@ -2,6 +2,7 @@ import { Button } from '@atoms/button/button';
import { PlusIcon, TruckIcon, UploadIcon, XIcon } from '@heroicons/react/outline'; import { PlusIcon, TruckIcon, UploadIcon, XIcon } from '@heroicons/react/outline';
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
import { useRecoilState, useSetRecoilState } from 'recoil'; import { useRecoilState, useSetRecoilState } from 'recoil';
import { AnimatedHeight } from '../../../atoms/animated-height';
import { getFilesTree } from '../../../components/uploads/file-tree-utils'; import { getFilesTree } from '../../../components/uploads/file-tree-utils';
import UploadZone from '../../../components/uploads/upload-zone'; import UploadZone from '../../../components/uploads/upload-zone';
import { useDriveItem } from '../../../features/drive/hooks/use-drive-item'; import { useDriveItem } from '../../../features/drive/hooks/use-drive-item';
@@ -17,6 +18,7 @@ export default () => {
const { access, item, inTrash } = useDriveItem(parentId); const { access, item, inTrash } = useDriveItem(parentId);
const { children: trashChildren } = useDriveItem('trash'); const { children: trashChildren } = useDriveItem('trash');
const uploadZoneRef = useRef<UploadZone | null>(null); const uploadZoneRef = useRef<UploadZone | null>(null);
const uploadFolderZoneRef = useRef<UploadZone | null>(null);
const setConfirmDeleteModalState = useSetRecoilState(ConfirmDeleteModalAtom); const setConfirmDeleteModalState = useSetRecoilState(ConfirmDeleteModalAtom);
const setCreationModalState = useSetRecoilState(CreateModalAtom); const setCreationModalState = useSetRecoilState(CreateModalAtom);
@@ -27,73 +29,96 @@ export default () => {
}, [item?.id, setCreationModalState]); }, [item?.id, setCreationModalState]);
return ( return (
<> <div className="-m-4">
<UploadZone <AnimatedHeight>
overClassName={'!hidden'} <div className="p-4">
className="hidden" <UploadZone
disableClick overClassName={'!hidden'}
parent={''} className="hidden"
multiple={true} disableClick
ref={uploadZoneRef} parent={''}
driveCollectionKey={'side-menu'} multiple={true}
onAddFiles={async (_, event) => { ref={uploadZoneRef}
const tree = await getFilesTree(event); driveCollectionKey={'side-menu'}
setCreationModalState({ parent_id: '', open: false }); onAddFiles={async (_, event) => {
uploadTree(tree, { const tree = await getFilesTree(event);
companyId, setCreationModalState({ parent_id: '', open: false });
parentId, uploadTree(tree, {
}); companyId,
}} parentId,
/> });
<CreateModal }}
selectFromDevice={() => uploadZoneRef.current?.open()} />
addFromUrl={(url, name) => { <UploadZone
setCreationModalState({ parent_id: '', open: false }); overClassName={'!hidden'}
uploadFromUrl(url, name, { className="hidden"
companyId, disableClick
parentId, parent={''}
}); multiple={true}
}} ref={uploadFolderZoneRef}
/> directory={true}
driveCollectionKey={'side-menu'}
onAddFiles={async (_, event) => {
const tree = await getFilesTree(event);
setCreationModalState({ parent_id: '', open: false });
uploadTree(tree, {
companyId,
parentId,
});
}}
/>
<CreateModal
selectFolderFromDevice={() => uploadFolderZoneRef.current?.open()}
selectFromDevice={() => uploadZoneRef.current?.open()}
addFromUrl={(url, name) => {
setCreationModalState({ parent_id: '', open: false });
uploadFromUrl(url, name, {
companyId,
parentId,
});
}}
/>
{inTrash && access === 'manage' && ( {inTrash && access === 'manage' && (
<> <>
<Button <Button
onClick={() => onClick={() =>
setConfirmDeleteModalState({ setConfirmDeleteModalState({
open: true, open: true,
items: trashChildren, items: trashChildren,
}) })
} }
size="lg" size="lg"
theme="danger" theme="danger"
className="w-full mb-2 justify-center" className="w-full mb-2 justify-center"
> >
<TruckIcon className="w-5 h-5 mr-2" /> Empty trash <TruckIcon className="w-5 h-5 mr-2" /> Empty trash
</Button> </Button>
</> </>
)} )}
{!(inTrash || access === 'read') && ( {!(inTrash || access === 'read') && (
<> <>
<Button <Button
onClick={() => uploadZoneRef.current?.open()} onClick={() => uploadZoneRef.current?.open()}
size="lg" size="lg"
theme="primary" theme="primary"
className="w-full mb-2 justify-center" className="w-full mb-2 justify-center"
style={{ boxShadow: '0 0 10px 0 rgba(0, 122, 255, 0.5)' }} style={{ boxShadow: '0 0 10px 0 rgba(0, 122, 255, 0.5)' }}
> >
<UploadIcon className="w-5 h-5 mr-2" /> Upload <UploadIcon className="w-5 h-5 mr-2" /> Upload
</Button> </Button>
<Button <Button
onClick={() => openItemModal()} onClick={() => openItemModal()}
size="lg" size="lg"
theme="secondary" theme="secondary"
className="w-full mb-2 justify-center" className="w-full mb-2 justify-center"
> >
<PlusIcon className="w-5 h-5 mr-2" /> Create <PlusIcon className="w-5 h-5 mr-2" /> Create
</Button> </Button>
</> </>
)} )}
</> </div>
</AnimatedHeight>
</div>
); );
}; };
@@ -76,7 +76,7 @@ export default () => {
theme="white" theme="white"
className={'w-full mb-1 ' + (inTrash ? activeClass : '')} className={'w-full mb-1 ' + (inTrash ? activeClass : '')}
> >
<TrashIcon className="w-5 h-5 mr-4 text-red-500" /> Trash <TrashIcon className="w-5 h-5 mr-4 text-rose-500" /> Trash
</Button> </Button>
)} )}
+1 -1
View File
@@ -1,7 +1,7 @@
const defaultTheme = require('tailwindcss/defaultTheme'); const defaultTheme = require('tailwindcss/defaultTheme');
let shades = []; let shades = [];
['zinc', 'red', 'orange', 'green', 'blue'].map(color => { ['zinc', 'rose', 'orange', 'green', 'blue'].map(color => {
[50, 100, 200, 300, 400, 500, 600, 700, 800, 900].map(shade => { [50, 100, 200, 300, 400, 500, 600, 700, 800, 900].map(shade => {
shades.push(`bg-${color}-${shade}`); shades.push(`bg-${color}-${shade}`);
shades.push(`border-${color}-${shade}`); shades.push(`border-${color}-${shade}`);