🔗 Implemented link-files

🔗 Implemented link-files
This commit is contained in:
Romaric Mourgues
2023-04-06 10:44:42 +02:00
committed by GitHub
10 changed files with 196 additions and 4 deletions
@@ -0,0 +1,11 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_618_59064)">
<path d="M0 8C0 3.58172 3.58172 0 8 0H32C36.4183 0 40 3.58172 40 8V32C40 36.4183 36.4183 40 32 40H8C3.58172 40 0 36.4183 0 32V8Z" fill="#C2E9FF"/>
<path d="M18.59 22.83C18.2 23.22 17.56 23.22 17.17 22.83C15.22 20.88 15.22 17.71 17.17 15.76L22.41 10.52C24.36 8.57 27.53 8.57 29.48 10.52C31.43 12.47 31.43 15.64 29.48 17.59L28.0441 19.0259C27.6545 19.4155 27.023 19.416 26.6328 19.0271C26.2426 18.6383 26.2414 18.0069 26.63 17.6166L28.06 16.18C29.24 15.01 29.24 13.11 28.06 11.94C26.89 10.76 24.99 10.76 23.82 11.94L18.59 17.17C17.41 18.34 17.41 20.24 18.59 21.41C19 21.8 19 22.44 18.59 22.83ZM21.41 17.17C21.8 16.78 22.44 16.78 22.83 17.17C24.78 19.12 24.78 22.29 22.83 24.24L17.59 29.48C15.64 31.43 12.47 31.43 10.52 29.48C8.57 27.53 8.57 24.36 10.52 22.41L11.9515 20.9785C12.3416 20.5876 12.9747 20.5876 13.3653 20.9781C13.3655 20.9776 13.3654 20.9782 13.3653 20.9781L13.3661 20.9797C13.7558 21.3707 13.7556 22.0036 13.3656 22.3943L11.94 23.82C10.76 24.99 10.76 26.89 11.94 28.06C13.11 29.24 15.01 29.24 16.18 28.06L21.41 22.83C22.59 21.66 22.59 19.76 21.41 18.59C21 18.2 21 17.56 21.41 17.17Z" fill="#00B2FF"/>
</g>
<defs>
<clipPath id="clip0_618_59064">
<rect width="40" height="40" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -9,6 +9,7 @@ import { ReactComponent as FileTypeSpreadsheetSvg } from './assets/file-type-spr
import { ReactComponent as FileTypeUnknownSvg } from './assets/file-type-unknown.svg'; import { ReactComponent as FileTypeUnknownSvg } from './assets/file-type-unknown.svg';
import { ReactComponent as FileTypeMediaSvg } from './assets/file-type-media.svg'; import { ReactComponent as FileTypeMediaSvg } from './assets/file-type-media.svg';
import { ReactComponent as FileTypeSlidesSvg } from './assets/file-type-slides.svg'; import { ReactComponent as FileTypeSlidesSvg } from './assets/file-type-slides.svg';
import { ReactComponent as FileTypeLinkSvg } from './assets/file-type-link.svg';
import { ReactComponent as RemoveSvg } from './assets/remove.svg'; import { ReactComponent as RemoveSvg } from './assets/remove.svg';
import { ReactComponent as SentSvg } from './assets/sent.svg'; import { ReactComponent as SentSvg } from './assets/sent.svg';
@@ -34,6 +35,8 @@ export const FileTypeSlidesIcon = (props: ComponentProps<'svg'>) => (
<FileTypeSlidesSvg {...props} /> <FileTypeSlidesSvg {...props} />
); );
export const FileTypeLinkIcon = (props: ComponentProps<'svg'>) => <FileTypeLinkSvg {...props} />;
export const RemoveIcon = (props: ComponentProps<'svg'>) => <RemoveSvg {...props} />; export const RemoveIcon = (props: ComponentProps<'svg'>) => <RemoveSvg {...props} />;
export const SentIcon = (props: ComponentProps<'svg'>) => <SentSvg {...props} />; export const SentIcon = (props: ComponentProps<'svg'>) => <SentSvg {...props} />;
@@ -79,5 +79,5 @@ export const useDrivePreviewDisplayData = () => {
fileId: status.details?.item.last_version_cache.file_metadata.external_id || '', fileId: status.details?.item.last_version_cache.file_metadata.external_id || '',
}); });
return { download, id, name, type, extension }; return { download, id, name, type, extension, size: status.details?.item.size };
}; };
@@ -105,7 +105,8 @@ class FileUploadAPIClient {
} }
public mimeToType(mime: string, extension?: string): FileTypes { public mimeToType(mime: string, extension?: string): FileTypes {
const { archives, images, pdf, slides, audio, spreadsheets, videos, documents } = fileTypeMimes; const { archives, images, pdf, slides, audio, spreadsheets, videos, documents, links } =
fileTypeMimes;
if (images.includes(mime)) return 'image'; if (images.includes(mime)) return 'image';
if (videos.includes(mime)) return 'video'; if (videos.includes(mime)) return 'video';
@@ -115,6 +116,7 @@ class FileUploadAPIClient {
if (archives.includes(mime)) return 'archive'; if (archives.includes(mime)) return 'archive';
if (spreadsheets.includes(mime)) return 'spreadsheet'; if (spreadsheets.includes(mime)) return 'spreadsheet';
if (documents.includes(mime)) return 'document'; if (documents.includes(mime)) return 'document';
if (links.includes(mime)) return 'link';
if (extension && AceModeList.getMode(extension) !== 'text') { if (extension && AceModeList.getMode(extension) !== 'text') {
return 'code'; return 'code';
@@ -151,6 +151,7 @@ export const fileTypeMimes: FileTypeMimes = {
'text/csv', 'text/csv',
'application/vnd.ms-excel.sheet.macroEnabled.12', 'application/vnd.ms-excel.sheet.macroEnabled.12',
], ],
links: ['text/uri-list'],
}; };
export type FileTypeMimes = { export type FileTypeMimes = {
@@ -162,4 +163,5 @@ export type FileTypeMimes = {
archives: string[]; archives: string[];
pdf: string[]; pdf: string[];
documents: string[]; documents: string[];
links: string[];
}; };
@@ -3,6 +3,7 @@ import { Button } from 'app/atoms/button/button';
import { import {
FileTypeArchiveIcon, FileTypeArchiveIcon,
FileTypeDocumentIcon, FileTypeDocumentIcon,
FileTypeLinkIcon,
FileTypeMediaIcon, FileTypeMediaIcon,
FileTypePdfIcon, FileTypePdfIcon,
FileTypeSlidesIcon, FileTypeSlidesIcon,
@@ -89,6 +90,8 @@ export const DocumentRow = ({
<FileTypeSpreadsheetIcon className={'h-5 w-5 shrink-0 text-gray-400'} /> <FileTypeSpreadsheetIcon className={'h-5 w-5 shrink-0 text-gray-400'} />
) : fileType === 'slides' ? ( ) : fileType === 'slides' ? (
<FileTypeSlidesIcon className={'h-5 w-5 shrink-0 text-gray-400'} /> <FileTypeSlidesIcon className={'h-5 w-5 shrink-0 text-gray-400'} />
) : fileType === 'link' ? (
<FileTypeLinkIcon className={'h-5 w-5 shrink-0 text-gray-400'} />
) : ( ) : (
<FileTypeUnknownIcon className={'h-5 w-5 shrink-0 text-gray-400'} /> <FileTypeUnknownIcon className={'h-5 w-5 shrink-0 text-gray-400'} />
)} )}
@@ -0,0 +1,80 @@
import { Button } from 'app/atoms/button/button';
import { Input } from 'app/atoms/input/input-text';
import { Info } from 'app/atoms/text';
import { useDriveActions } from 'app/features/drive/hooks/use-drive-actions';
import { useState } from 'react';
import { useRecoilState } from 'recoil';
import { CreateModalAtom } from '.';
import FileUploadService from 'features/files/services/file-upload-service';
export const CreateLink = () => {
const [name, setName] = useState<string>('');
const [link, setLink] = useState<string>('');
const [loading] = useState<boolean>(false);
const [state, setState] = useRecoilState(CreateModalAtom);
const { create } = useDriveActions();
const createLink = async () => {
let finalLink = link.trim();
if (!/^https?:\/\//i.test(finalLink)) finalLink = 'http://' + finalLink;
const file = new File(['[InternetShortcut]\nURL=' + finalLink], name?.trim() + '.url', {
type: 'text/uri-list',
});
await FileUploadService.upload([file], {
context: {
parentId: state.parent_id,
},
callback: (file, context) => {
if (file)
create(
{ name, parent_id: context.parentId, size: file.upload_data?.size },
{
provider: 'internal',
application_id: '',
file_metadata: {
name: file.metadata?.name,
size: file.upload_data?.size,
mime: file.metadata?.mime,
thumbnails: file?.thumbnails,
source: 'internal',
external_id: file.id,
},
},
);
},
});
};
return (
<>
<Info>Create a link</Info>
<Input
disabled={loading}
placeholder="Link name"
className="w-full mt-4"
onChange={e => setName(e.target.value)}
/>
<Input
disabled={loading}
placeholder="https://example.com"
className="w-full mt-4"
onChange={e => setLink(e.target.value)}
/>
<Button
disabled={!name || !link}
loading={loading}
className="mt-4 float-right"
onClick={async () => {
await createLink();
setState({ ...state, open: false });
}}
>
Create link
</Button>
</>
);
};
@@ -1,11 +1,10 @@
import { Transition } from '@headlessui/react'; import { Transition } from '@headlessui/react';
import { import {
ChevronLeftIcon, ChevronLeftIcon,
DesktopComputerIcon,
DocumentDownloadIcon, DocumentDownloadIcon,
FolderAddIcon, FolderAddIcon,
FolderDownloadIcon, FolderDownloadIcon,
FolderIcon, LinkIcon,
} from '@heroicons/react/outline'; } 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';
@@ -17,6 +16,7 @@ import { ReactNode } from 'react';
import { atom, useRecoilState } from 'recoil'; import { atom, useRecoilState } from 'recoil';
import { slideXTransition, slideXTransitionReverted } from 'src/utils/transitions'; import { slideXTransition, slideXTransitionReverted } from 'src/utils/transitions';
import { CreateFolder } from './create-folder'; import { CreateFolder } from './create-folder';
import { CreateLink } from './create-link';
export type CreateModalAtomType = { export type CreateModalAtomType = {
open: boolean; open: boolean;
@@ -93,6 +93,11 @@ export const CreateModal = ({
text="Upload folders from device" text="Upload folders from device"
onClick={() => selectFolderFromDevice()} onClick={() => selectFolderFromDevice()}
/> />
<CreateModalOption
icon={<LinkIcon className="w-5 h-5" />}
text="Create a link file"
onClick={() => setState({ ...state, type: 'link' })}
/>
{(applications || []) {(applications || [])
.filter(app => app.display?.twake?.files?.editor?.empty_files?.length) .filter(app => app.display?.twake?.files?.editor?.empty_files?.length)
@@ -148,6 +153,18 @@ export const CreateModal = ({
> >
<CreateFolder /> <CreateFolder />
</Transition> </Transition>
<Transition
style={{
gridColumn: '1 / 1',
gridRow: '1 / 1',
}}
show={state.type === 'link'}
as="div"
{...(!state.type ? slideXTransitionReverted : slideXTransition)}
>
<CreateLink />
</Transition>
</div> </div>
</ModalContent> </ModalContent>
</Modal> </Modal>
@@ -9,6 +9,7 @@ import PdfDisplay from './pdf/display';
import CodeDisplay from './code/display'; import CodeDisplay from './code/display';
import ArchiveDisplay from './archive/display'; import ArchiveDisplay from './archive/display';
import OtherDisplay from './other/display'; import OtherDisplay from './other/display';
import LinkDisplay from './link/display';
export default (): React.ReactElement => { export default (): React.ReactElement => {
const { download, type, name, id } = useDrivePreviewDisplayData(); const { download, type, name, id } = useDrivePreviewDisplayData();
@@ -39,6 +40,8 @@ export default (): React.ReactElement => {
return <ArchiveDisplay download={download} name={name} />; return <ArchiveDisplay download={download} name={name} />;
case 'pdf': case 'pdf':
return <PdfDisplay download={download} name={name} />; return <PdfDisplay download={download} name={name} />;
case 'link':
return <LinkDisplay download={download} name={name} />;
default: default:
return <OtherDisplay download={download} name={name} id={id} />; return <OtherDisplay download={download} name={name} id={id} />;
} }
@@ -0,0 +1,71 @@
import { Button } from 'app/atoms/button/button';
import { useDrivePreviewDisplayData } from 'app/features/drive/hooks/use-drive-preview';
import { useEffect, useState } from 'react';
export default (props: { download: string; name: string }) => {
const { size } = useDrivePreviewDisplayData();
const [error, setError] = useState(false);
const [loading, setLoading] = useState(true);
const openLink = async () => {
if ((size || 10000000) < 10000) {
setLoading(true);
//Download file content and extract link from url props.download
try {
const response = await fetch(props.download);
const blob = await response.blob();
const reader = new FileReader();
reader.readAsText(blob);
reader.onloadend = () => {
const result = reader.result as string;
const link = result.match(/URL=(.*)/);
if (link && link[1]) {
if (!link[1].match(/^(http|https):\/\//)) throw new Error('Invalid link');
window.open(link[1], '_blank');
} else {
setError(true);
}
setLoading(false);
};
} catch (e) {
setError(true);
setLoading(false);
}
return;
}
setError(true);
};
useEffect(() => {
openLink();
}, []);
if (loading) {
return (
<div className="text-white m-auto w-full text-center h-full flex items-center">
<span className="block w-full text-center">Opening link...</span>
</div>
);
}
if (error) {
return (
<div className="text-white m-auto w-full text-center h-full flex items-center">
<span className="block w-full text-center">
We can't open '{props.name}' as a link. You can download it instead.
</span>
</div>
);
}
return (
<div className="text-white m-auto w-full text-center h-full flex items-center">
<span className="block w-full text-center">
Link was open on another tab.
<br />
<br />
<Button onClick={() => openLink()}>Open again</Button>
</span>
</div>
);
};