From b9b86b170c1009b964646e351de8d83d965c3dd1 Mon Sep 17 00:00:00 2001 From: Dylan Decrulle Date: Thu, 14 Nov 2024 11:15:02 +0100 Subject: [PATCH 01/15] wip --- web/src/ui/i18n/resources/de.tsx | 2 +- web/src/ui/i18n/resources/en.tsx | 2 +- web/src/ui/i18n/resources/es.tsx | 2 +- web/src/ui/i18n/resources/fi.tsx | 2 +- web/src/ui/i18n/resources/fr.tsx | 20 ++- web/src/ui/i18n/resources/it.tsx | 2 +- web/src/ui/i18n/resources/nl.tsx | 2 +- web/src/ui/i18n/resources/no.tsx | 2 +- web/src/ui/i18n/resources/zh-CN.tsx | 2 +- web/src/ui/i18n/types.ts | 2 +- .../MyFilesShareDialog.stories.tsx | 18 --- .../NewComponents/MyFilesShareDialog.tsx | 95 ------------ .../MyFilesShareSelectTime.stories.tsx | 15 -- .../MyFilesCreateFolderDialog.stories.tsx | 0 .../MyFilesCreateFolderDialog.tsx | 0 .../myFiles/ShareFile/SelectTime.stories.tsx | 15 ++ .../SelectTime.tsx} | 5 +- .../myFiles/ShareFile/ShareDialog.stories.tsx | 46 ++++++ .../pages/myFiles/ShareFile/ShareDialog.tsx | 140 ++++++++++++++++++ .../DirectoryOrFileDetailed.stories.tsx | 2 +- .../DirectoryOrFileDetailed.tsx | 17 ++- 21 files changed, 246 insertions(+), 145 deletions(-) delete mode 100644 web/src/ui/pages/myFiles/NewComponents/MyFilesShareDialog.stories.tsx delete mode 100644 web/src/ui/pages/myFiles/NewComponents/MyFilesShareDialog.tsx delete mode 100644 web/src/ui/pages/myFiles/NewComponents/MyFilesShareSelectTime.stories.tsx rename web/src/ui/pages/myFiles/{NewComponents => ShareFile}/MyFilesCreateFolderDialog.stories.tsx (100%) rename web/src/ui/pages/myFiles/{NewComponents => ShareFile}/MyFilesCreateFolderDialog.tsx (100%) create mode 100644 web/src/ui/pages/myFiles/ShareFile/SelectTime.stories.tsx rename web/src/ui/pages/myFiles/{NewComponents/MyFilesShareSelectTime.tsx => ShareFile/SelectTime.tsx} (88%) create mode 100644 web/src/ui/pages/myFiles/ShareFile/ShareDialog.stories.tsx create mode 100644 web/src/ui/pages/myFiles/ShareFile/ShareDialog.tsx rename web/src/ui/pages/myFiles/{NewComponents => shared}/DirectoryOrFileDetailed.stories.tsx (89%) rename web/src/ui/pages/myFiles/{NewComponents => shared}/DirectoryOrFileDetailed.tsx (77%) diff --git a/web/src/ui/i18n/resources/de.tsx b/web/src/ui/i18n/resources/de.tsx index 22671b2e2..ff977ece8 100644 --- a/web/src/ui/i18n/resources/de.tsx +++ b/web/src/ui/i18n/resources/de.tsx @@ -314,7 +314,7 @@ export const translations: Translations<"de"> = { ) }, - MyFilesShareDialog: { + ShareDialog: { cancel: "Abbrechen", "create and copy link": "Erstellen und kopieren" }, diff --git a/web/src/ui/i18n/resources/en.tsx b/web/src/ui/i18n/resources/en.tsx index e6c28a033..503a9adc0 100644 --- a/web/src/ui/i18n/resources/en.tsx +++ b/web/src/ui/i18n/resources/en.tsx @@ -303,7 +303,7 @@ export const translations: Translations<"en"> = { ) }, - MyFilesShareDialog: { + ShareDialog: { cancel: "Cancel", "create and copy link": "Create and copy link" }, diff --git a/web/src/ui/i18n/resources/es.tsx b/web/src/ui/i18n/resources/es.tsx index 8a624c46f..9c1c6df99 100644 --- a/web/src/ui/i18n/resources/es.tsx +++ b/web/src/ui/i18n/resources/es.tsx @@ -315,7 +315,7 @@ export const translations: Translations<"en"> = { ) }, - MyFilesShareDialog: { + ShareDialog: { cancel: "Cancelar", "create and copy link": "Crear y copiar enlace" }, diff --git a/web/src/ui/i18n/resources/fi.tsx b/web/src/ui/i18n/resources/fi.tsx index ccf531483..ffd884a5b 100644 --- a/web/src/ui/i18n/resources/fi.tsx +++ b/web/src/ui/i18n/resources/fi.tsx @@ -311,7 +311,7 @@ export const translations: Translations<"fi"> = { ) }, - MyFilesShareDialog: { + ShareDialog: { cancel: "Peruuta", "create and copy link": "Luo ja kopioi linkki" }, diff --git a/web/src/ui/i18n/resources/fr.tsx b/web/src/ui/i18n/resources/fr.tsx index 469a7ae65..56ffe7590 100644 --- a/web/src/ui/i18n/resources/fr.tsx +++ b/web/src/ui/i18n/resources/fr.tsx @@ -317,9 +317,23 @@ export const translations: Translations<"fr"> = { ) }, - MyFilesShareDialog: { - cancel: "Annuler", - "create and copy link": "Créer et copier le lien" + ShareDialog: { + title: "Partager vos données", + close: "Fermer", + "create and copy link": "Créer et copier le lien", + "paragraph current policy": ({ + policy + }) => `Votre fichier est public, toute personne ayant le lien peut + télécharger votre fichier`, + "paragraph change policy": ({ + policy + }) => `Pour restreindre son accès, changez le statut de diffusion de + votre fichier.`, + "hint link access": ({ policy, expirationDate }) => + policy === "private" + ? `Votre lien ....` + : "Votre lien est disponible tant que le fichier est publique", + "label input link": "Lien d'accès" }, MySecrets: { "page title - my secrets": "My Secrets", diff --git a/web/src/ui/i18n/resources/it.tsx b/web/src/ui/i18n/resources/it.tsx index a441e61e8..09e09e627 100644 --- a/web/src/ui/i18n/resources/it.tsx +++ b/web/src/ui/i18n/resources/it.tsx @@ -312,7 +312,7 @@ export const translations: Translations<"it"> = { ) }, - MyFilesShareDialog: { + ShareDialog: { cancel: "Annulla", "create and copy link": "Creare e copiare il link" }, diff --git a/web/src/ui/i18n/resources/nl.tsx b/web/src/ui/i18n/resources/nl.tsx index 3ab4fcedf..beba0a47a 100644 --- a/web/src/ui/i18n/resources/nl.tsx +++ b/web/src/ui/i18n/resources/nl.tsx @@ -314,7 +314,7 @@ export const translations: Translations<"nl"> = { ) }, - MyFilesShareDialog: { + ShareDialog: { cancel: "Annuleren", "create and copy link": "Creare e copiare il link" }, diff --git a/web/src/ui/i18n/resources/no.tsx b/web/src/ui/i18n/resources/no.tsx index 9b358b107..437cd1272 100644 --- a/web/src/ui/i18n/resources/no.tsx +++ b/web/src/ui/i18n/resources/no.tsx @@ -310,7 +310,7 @@ export const translations: Translations<"no"> = { ) }, - MyFilesShareDialog: { + ShareDialog: { cancel: "Avbryt", "create and copy link": "Opprett og kopier lenke" }, diff --git a/web/src/ui/i18n/resources/zh-CN.tsx b/web/src/ui/i18n/resources/zh-CN.tsx index eb6ceb547..a189a8c81 100644 --- a/web/src/ui/i18n/resources/zh-CN.tsx +++ b/web/src/ui/i18n/resources/zh-CN.tsx @@ -281,7 +281,7 @@ export const translations: Translations<"zh-CN"> = { ) }, - MyFilesShareDialog: { + ShareDialog: { cancel: "取消", "create and copy link": "创建并复制链接" }, diff --git a/web/src/ui/i18n/types.ts b/web/src/ui/i18n/types.ts index c719bff0a..873c2aeb4 100644 --- a/web/src/ui/i18n/types.ts +++ b/web/src/ui/i18n/types.ts @@ -25,7 +25,7 @@ export type ComponentKey = | import("ui/pages/myFiles/Explorer/ExplorerUploadModal/ExplorerUploadProgress").I18n | import("ui/pages/myFiles/Explorer/ExplorerUploadModal/ExplorerUploadModal").I18n | import("ui/pages/myFiles/Explorer/ListExplorer/ListExplorerItems").I18n - | import("ui/pages/myFiles/NewComponents/MyFilesShareDialog").I18n + | import("ui/pages/myFiles/ShareFile/ShareDialog").I18n | import("ui/App/Header/Header").I18n | import("ui/App/LeftBar").I18n | import("ui/App/AutoLogoutCountdown").I18n diff --git a/web/src/ui/pages/myFiles/NewComponents/MyFilesShareDialog.stories.tsx b/web/src/ui/pages/myFiles/NewComponents/MyFilesShareDialog.stories.tsx deleted file mode 100644 index e7174c5e7..000000000 --- a/web/src/ui/pages/myFiles/NewComponents/MyFilesShareDialog.stories.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Meta, StoryObj } from "@storybook/react"; -import { MyFilesShareDialog } from "./MyFilesShareDialog"; - -const meta = { - title: "Pages/MyFiles/NewComponents/MyFilesShareDialog", - component: MyFilesShareDialog -} satisfies Meta; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = { - args: { - isPublic: false, - kind: "directory" - } -}; diff --git a/web/src/ui/pages/myFiles/NewComponents/MyFilesShareDialog.tsx b/web/src/ui/pages/myFiles/NewComponents/MyFilesShareDialog.tsx deleted file mode 100644 index f12edf72f..000000000 --- a/web/src/ui/pages/myFiles/NewComponents/MyFilesShareDialog.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { declareComponentKeys } from "i18nifty"; -import { Button } from "onyxia-ui/Button"; -import { Dialog } from "onyxia-ui/Dialog"; -import { memo, useState } from "react"; -import { useTranslation } from "ui/i18n"; -import { DirectoryOrFileDetailed } from "./DirectoryOrFileDetailed"; -import { Text } from "onyxia-ui/Text"; -import { tss } from "tss"; -import { getIconUrlByName } from "lazy-icons"; -import { MyFilesShareSelectTime } from "./MyFilesShareSelectTime"; - -export type Props = { isPublic: boolean; kind: "directory" | "file" }; -export const MyFilesShareDialog = memo((props: Props) => { - const { isPublic, kind } = props; - const { t } = useTranslation({ MyFilesShareDialog }); - - const { classes } = useStyles(); - const [isOpen, setIsOpen] = useState(true); - const onClose = () => setIsOpen(false); - - return ( - - -
- - - ou - - -
- - } - title={"Partager vos données"} - subtitle={ - - Créer un lien d’accès pour partager votre répertoire avec un - partenaire. - - } - buttons={ - <> - - - - } - /> - ); -}); - -const useStyles = tss.withName({ MyFilesShareDialog }).create(({ theme }) => ({ - dialogContent: { - display: "flex", - flexDirection: "column", - gap: theme.spacing(6), - marginBottom: theme.spacing(6) - }, - directoryDetails: { - margin: theme.muiTheme.spacing(4, 2, 4, 2) - }, - shareContainer: { - display: "flex", - justifyContent: "space-between" - }, - expirationText: { - display: "flex", - alignItems: "center", - gap: theme.spacing(1) - } -})); -const { i18n } = declareComponentKeys<"cancel" | "create and copy link">()({ - MyFilesShareDialog -}); -export type I18n = typeof i18n; diff --git a/web/src/ui/pages/myFiles/NewComponents/MyFilesShareSelectTime.stories.tsx b/web/src/ui/pages/myFiles/NewComponents/MyFilesShareSelectTime.stories.tsx deleted file mode 100644 index 74b901d06..000000000 --- a/web/src/ui/pages/myFiles/NewComponents/MyFilesShareSelectTime.stories.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Meta, StoryObj } from "@storybook/react"; -import { MyFilesShareSelectTime } from "./MyFilesShareSelectTime"; - -const meta: Meta = { - title: "Pages/MyFiles/NewComponents/MyFilesShareSelectTime", - component: MyFilesShareSelectTime -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = { - args: {} -}; diff --git a/web/src/ui/pages/myFiles/NewComponents/MyFilesCreateFolderDialog.stories.tsx b/web/src/ui/pages/myFiles/ShareFile/MyFilesCreateFolderDialog.stories.tsx similarity index 100% rename from web/src/ui/pages/myFiles/NewComponents/MyFilesCreateFolderDialog.stories.tsx rename to web/src/ui/pages/myFiles/ShareFile/MyFilesCreateFolderDialog.stories.tsx diff --git a/web/src/ui/pages/myFiles/NewComponents/MyFilesCreateFolderDialog.tsx b/web/src/ui/pages/myFiles/ShareFile/MyFilesCreateFolderDialog.tsx similarity index 100% rename from web/src/ui/pages/myFiles/NewComponents/MyFilesCreateFolderDialog.tsx rename to web/src/ui/pages/myFiles/ShareFile/MyFilesCreateFolderDialog.tsx diff --git a/web/src/ui/pages/myFiles/ShareFile/SelectTime.stories.tsx b/web/src/ui/pages/myFiles/ShareFile/SelectTime.stories.tsx new file mode 100644 index 000000000..1fd420bc0 --- /dev/null +++ b/web/src/ui/pages/myFiles/ShareFile/SelectTime.stories.tsx @@ -0,0 +1,15 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { SelectTime } from "./SelectTime"; + +const meta: Meta = { + title: "Pages/MyFiles/NewComponents/SelectTime", + component: SelectTime +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: {} +}; diff --git a/web/src/ui/pages/myFiles/NewComponents/MyFilesShareSelectTime.tsx b/web/src/ui/pages/myFiles/ShareFile/SelectTime.tsx similarity index 88% rename from web/src/ui/pages/myFiles/NewComponents/MyFilesShareSelectTime.tsx rename to web/src/ui/pages/myFiles/ShareFile/SelectTime.tsx index 566fad646..cf090e154 100644 --- a/web/src/ui/pages/myFiles/NewComponents/MyFilesShareSelectTime.tsx +++ b/web/src/ui/pages/myFiles/ShareFile/SelectTime.tsx @@ -10,13 +10,14 @@ type Props = { }; const expirationOptions = [ + { id: "1 heure", name: "1 heure" }, { id: "12 heures", name: "12 heures" }, { id: "24 heures", name: "24 heures" }, { id: "48 heures", name: "48 heures" }, { id: "7 jours", name: "7 jours" } ]; -export function MyFilesShareSelectTime(props: Props) { +export function SelectTime(props: Props) { const { className } = props; const labelId = useId(); @@ -41,7 +42,7 @@ export function MyFilesShareSelectTime(props: Props) { ); } -const useStyles = tss.withName({ MyFilesShareSelectTime }).create(() => ({ +const useStyles = tss.withName({ MyFilesShareSelectTime: SelectTime }).create(() => ({ timeSelectWrapper: { minWidth: 200 } diff --git a/web/src/ui/pages/myFiles/ShareFile/ShareDialog.stories.tsx b/web/src/ui/pages/myFiles/ShareFile/ShareDialog.stories.tsx new file mode 100644 index 000000000..4e624c2b9 --- /dev/null +++ b/web/src/ui/pages/myFiles/ShareFile/ShareDialog.stories.tsx @@ -0,0 +1,46 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { ShareDialog } from "./ShareDialog"; + +const meta = { + title: "Pages/MyFiles/ShareFile/ShareDialog", + component: ShareDialog +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Public: Story = { + args: { + file: { + kind: "file", + basename: "photo.png", + size: 2048000, // en bytes + lastModified: new Date("2023-09-15"), + policy: "public", + isBeingDeleted: false, + isPolicyChanging: false, + + isBeingCreated: true, + uploadPercent: 75 // Example upload percentage + }, + url: "https://minio.lab.sspcloud.fr/onyxia-datalab/public/photo.png" + } +}; + +export const Private: Story = { + args: { + file: { + kind: "file", + basename: "photo.png", + size: 2048000, // en bytes + lastModified: new Date("2023-09-15"), + policy: "private", + isBeingDeleted: false, + isPolicyChanging: false, + isBeingCreated: true, + uploadPercent: 75 // Example upload percentage + }, + url: "" + } +}; diff --git a/web/src/ui/pages/myFiles/ShareFile/ShareDialog.tsx b/web/src/ui/pages/myFiles/ShareFile/ShareDialog.tsx new file mode 100644 index 000000000..674cfd27a --- /dev/null +++ b/web/src/ui/pages/myFiles/ShareFile/ShareDialog.tsx @@ -0,0 +1,140 @@ +import { declareComponentKeys } from "i18nifty"; +import { Button } from "onyxia-ui/Button"; +import { Dialog } from "onyxia-ui/Dialog"; +import { memo, useState } from "react"; +import { useTranslation } from "ui/i18n"; +import { DirectoryOrFileDetailed } from "../shared/DirectoryOrFileDetailed"; +import { Text } from "onyxia-ui/Text"; +import { MuiIconComponentName } from "onyxia-ui/MuiIconComponentName"; +import { tss } from "tss"; +import { id } from "tsafe"; +import { SelectTime } from "./SelectTime"; +import { FileItem } from "../shared/types"; +import { TextField } from "@mui/material"; +import { CopyToClipboardIconButton } from "ui/shared/CopyToClipboardIconButton"; + +export type ShareDialogProps = { + file: FileItem; + url: string | undefined; //undefined if file.policy is public + isOpen: boolean; + onClose: () => void; + createSignedLink: (expirationTime: number) => Promise; +}; + +export const ShareDialog = memo((props: ShareDialogProps) => { + const { file, url } = props; + const { t } = useTranslation({ ShareDialog }); + + const { classes } = useStyles(); + const [isOpen, setIsOpen] = useState(true); + const onClose = () => setIsOpen(false); + + const isPublic = file.policy === "public"; + + const shareIconId = id( + isPublic ? "VisibilityOff" : "Visibility" + ); + + const [doCreateLink, setDoCreateLink] = useState(!isPublic); // If the file is private, we need to create a signed link + + return ( + + + {t("paragraph current policy", { policy: file.policy })} + + + + {t("paragraph change policy", { policy: file.policy })} + + + {doCreateLink ? ( +
+ + +
+ ) : ( + + ) + } + }} + helperText={t("hint link access", { + policy: file.policy, + expirationDate: undefined + })} + variant="standard" + value={url} + /> + )} + + } + title={t("title")} + subtitle={ + + } + buttons={ + <> + + + } + /> + ); +}); + +const useStyles = tss.withName({ ShareDialog }).create(({ theme }) => ({ + body: { + display: "flex", + flexDirection: "column", + gap: theme.spacing(6), + marginBottom: theme.spacing(6) + }, + directoryDetails: { + padding: theme.spacing(4) + }, + createLink: { + display: "flex", + flexDirection: "row", + justifyContent: "space-between" + } +})); +const { i18n } = declareComponentKeys< + | "title" + | "close" + | "create and copy link" + | { K: "paragraph current policy"; P: { policy: FileItem["policy"] } } + | { K: "paragraph change policy"; P: { policy: FileItem["policy"] } } + | { + K: "hint link access"; + P: { policy: FileItem["policy"]; expirationDate: Date | undefined }; + } + | "label input link" +>()({ + ShareDialog +}); +export type I18n = typeof i18n; diff --git a/web/src/ui/pages/myFiles/NewComponents/DirectoryOrFileDetailed.stories.tsx b/web/src/ui/pages/myFiles/shared/DirectoryOrFileDetailed.stories.tsx similarity index 89% rename from web/src/ui/pages/myFiles/NewComponents/DirectoryOrFileDetailed.stories.tsx rename to web/src/ui/pages/myFiles/shared/DirectoryOrFileDetailed.stories.tsx index 7d5a5fb27..001785221 100644 --- a/web/src/ui/pages/myFiles/NewComponents/DirectoryOrFileDetailed.stories.tsx +++ b/web/src/ui/pages/myFiles/shared/DirectoryOrFileDetailed.stories.tsx @@ -2,7 +2,7 @@ import { Meta, StoryObj } from "@storybook/react"; import { DirectoryOrFileDetailed } from "./DirectoryOrFileDetailed"; const meta = { - title: "Pages/MyFiles/NewComponents/DirectoryOrFileDetailed", + title: "Pages/MyFiles/shared/DirectoryOrFileDetailed", component: DirectoryOrFileDetailed } satisfies Meta; diff --git a/web/src/ui/pages/myFiles/NewComponents/DirectoryOrFileDetailed.tsx b/web/src/ui/pages/myFiles/shared/DirectoryOrFileDetailed.tsx similarity index 77% rename from web/src/ui/pages/myFiles/NewComponents/DirectoryOrFileDetailed.tsx rename to web/src/ui/pages/myFiles/shared/DirectoryOrFileDetailed.tsx index a179ae2c6..1f1c549f5 100644 --- a/web/src/ui/pages/myFiles/NewComponents/DirectoryOrFileDetailed.tsx +++ b/web/src/ui/pages/myFiles/shared/DirectoryOrFileDetailed.tsx @@ -3,6 +3,7 @@ import { ExplorerIcon } from "../Explorer/ExplorerIcon"; import { Text } from "onyxia-ui/Text"; import { Icon } from "onyxia-ui/Icon"; import { getIconUrlByName } from "lazy-icons"; +import { declareComponentKeys } from "i18nifty"; type Props = { className?: string; @@ -41,7 +42,14 @@ export function DirectoryOrFileDetailed(props: Props) { )} />   - {isPublic ? "Dossier public" : "Dossier privé"} + {(() => { + switch (kind) { + case "directory": + return isPublic ? "Dossier public" : "Dossier privé"; + case "file": + return isPublic ? "Fichier public" : "Fichier privé"; + } + })()} @@ -53,7 +61,6 @@ const useStyles = tss.withName({ DirectoryOrFileDetailed }).create(({ theme }) = root: { display: "flex", alignItems: "center" - //"margin": theme.spacing(2) }, iconWrapper: { paddingRight: theme.spacing(4) @@ -75,3 +82,9 @@ const useStyles = tss.withName({ DirectoryOrFileDetailed }).create(({ theme }) = marginRight: theme.spacing(1) } })); + +const { i18n } = declareComponentKeys<{ + K: "policy item"; + P: { isPublic: boolean; kind: Props["kind"] }; +}>()({ DirectoryOrFileDetailed }); +export type I18n = typeof i18n; From b9dd6d2e695b399db2553aee2ab7bc08582ba76a Mon Sep 17 00:00:00 2001 From: Dylan Decrulle Date: Thu, 14 Nov 2024 16:38:52 +0100 Subject: [PATCH 02/15] wip again --- .../core/usecases/fileExplorer/selectors.ts | 50 ++++++++++++-- web/src/core/usecases/fileExplorer/state.ts | 53 +++++++++++++- web/src/core/usecases/fileExplorer/thunks.ts | 36 ++++++++++ .../ui/pages/myFiles/Explorer/Explorer.tsx | 62 ++++++++++------- .../Explorer/ExplorerItems/ExplorerItems.tsx | 21 ++++-- .../pages/myFiles/ShareFile/ShareDialog.tsx | 69 +++++++++++++++++-- 6 files changed, 249 insertions(+), 42 deletions(-) diff --git a/web/src/core/usecases/fileExplorer/selectors.ts b/web/src/core/usecases/fileExplorer/selectors.ts index 00e2eff20..980d8f922 100644 --- a/web/src/core/usecases/fileExplorer/selectors.ts +++ b/web/src/core/usecases/fileExplorer/selectors.ts @@ -97,9 +97,9 @@ const currentWorkingDirectoryView = createSelector( objects, ongoingOperations, s3FilesBeingUploaded - ): CurrentWorkingDirectoryView | undefined => { + ): CurrentWorkingDirectoryView | null => { if (directoryPath === undefined) { - return undefined; + return null; } const items = objects .map((object): CurrentWorkingDirectoryView.Item => { @@ -187,6 +187,38 @@ const currentWorkingDirectoryView = createSelector( } ); +export type ShareView = ShareView.PublicFile | ShareView.PrivateFile; + +export namespace ShareView { + type Common = { + file: CurrentWorkingDirectoryView.Item.File; + }; + + export type PublicFile = Common & { + isPublic: true; + url: string; + }; + + export type PrivateFile = Common & { + isPublic: false; + signedUrl: string | undefined; + isSignedUrlBeingRequested: boolean; + }; +} + +const shareView = createSelector( + createSelector(state, state => state.directoryPath), + createSelector(state, state => state.objects), + + (directoryPath, objects): ShareView | undefined | null => { + if (directoryPath === undefined) { + return null; + } + + assert(false, "TODO"); + } +); + const isNavigationOngoing = createSelector(state, state => state.isNavigationOngoing); const workingDirectoryPath = createSelector( @@ -206,21 +238,25 @@ const pathMinDepth = createSelector(workingDirectoryPath, workingDirectoryPath = }); const main = createSelector( + createSelector(state, state => state.directoryPath), uploadProgress, commandLogsEntries, currentWorkingDirectoryView, isNavigationOngoing, pathMinDepth, createSelector(state, state => state.viewMode), + shareView, ( + directoryPath, uploadProgress, commandLogsEntries, currentWorkingDirectoryView, isNavigationOngoing, pathMinDepth, - viewMode + viewMode, + shareView ) => { - if (currentWorkingDirectoryView === undefined) { + if (directoryPath === null) { return { isCurrentWorkingDirectoryLoaded: false as const, isNavigationOngoing, @@ -231,6 +267,9 @@ const main = createSelector( }; } + assert(currentWorkingDirectoryView !== null); + assert(shareView !== null); + return { isCurrentWorkingDirectoryLoaded: true as const, isNavigationOngoing, @@ -238,7 +277,8 @@ const main = createSelector( commandLogsEntries, pathMinDepth, currentWorkingDirectoryView, - viewMode + viewMode, + shareView }; } ); diff --git a/web/src/core/usecases/fileExplorer/state.ts b/web/src/core/usecases/fileExplorer/state.ts index 76b276c3a..a4d4d3f3e 100644 --- a/web/src/core/usecases/fileExplorer/state.ts +++ b/web/src/core/usecases/fileExplorer/state.ts @@ -30,6 +30,13 @@ export type State = { resp: string | undefined; }[]; bucketPolicy: S3BucketPolicy; + share: + | { + fileBasename: string; + url: string | undefined; + isSignedUrlBeingRequested: boolean; + } + | undefined; }; export const name = "fileExplorer"; @@ -47,7 +54,8 @@ export const { reducer, actions } = createUsecaseActions({ bucketPolicy: { Version: "2012-10-17", Statement: [] - } + }, + share: undefined }), reducers: { fileUploadStarted: ( @@ -105,6 +113,7 @@ export const { reducer, actions } = createUsecaseActions({ state.s3FilesBeingUploaded = []; }, navigationStarted: state => { + assert(state.share === undefined); state.isNavigationOngoing = true; }, navigationCompleted: ( @@ -319,6 +328,48 @@ export const { reducer, actions } = createUsecaseActions({ : o ); state.bucketPolicy = payload.bucketPolicy; + }, + shareOpened: ( + state, + { + payload + }: { + payload: { + fileBasename: string; + url: string | undefined; + }; + } + ) => { + const { fileBasename, url } = payload; + + state.share = { + fileBasename, + url, + isSignedUrlBeingRequested: false + }; + }, + shareClosed: state => { + state.share = undefined; + }, + requestSignedUrlStarted: state => { + assert(state.share !== undefined); + state.share.isSignedUrlBeingRequested = true; + }, + requestSignedUrlCompleted: ( + state, + { + payload + }: { + payload: { + url: string; + }; + } + ) => { + const { url } = payload; + + assert(state.share !== undefined); + state.share.isSignedUrlBeingRequested = false; + state.share.url = url; } } }); diff --git a/web/src/core/usecases/fileExplorer/thunks.ts b/web/src/core/usecases/fileExplorer/thunks.ts index 9644e554a..104fedd66 100644 --- a/web/src/core/usecases/fileExplorer/thunks.ts +++ b/web/src/core/usecases/fileExplorer/thunks.ts @@ -739,5 +739,41 @@ export const thunks = { ); return downloadUrl; + }, + openShare: + (params: { fileBasename: string }) => + async (...args) => { + const { fileBasename } = params; + + const [dispatch, getState] = args; + + const { directoryPath, objects } = getState()[name]; + + assert(directoryPath !== undefined); + + const { s3Config } = await dispatch( + s3ConfigManagement.protectedThunks.getS3ConfigAndClientForExplorer() + ).then(r => { + assert(r !== undefined); + return r; + }); + + const url = (() => { + const currentObj = objects.find( + o => o.basename === fileBasename && o.kind === "file" + ); + + assert(currentObj !== undefined); + })(); + + dispatch( + actions.shareOpened({ + fileBasename, + url: + currentObj.policy === "private" + ? undefined + : `${s3Config.paramsOfCreateS3Client.url}/${pathJoin(directoryPath, fileBasename)}` + }) + ); } } satisfies Thunks; diff --git a/web/src/ui/pages/myFiles/Explorer/Explorer.tsx b/web/src/ui/pages/myFiles/Explorer/Explorer.tsx index 187b5d2d9..b0ebcdbd6 100644 --- a/web/src/ui/pages/myFiles/Explorer/Explorer.tsx +++ b/web/src/ui/pages/myFiles/Explorer/Explorer.tsx @@ -29,7 +29,7 @@ import { Dialog } from "onyxia-ui/Dialog"; import { useCallbackFactory } from "powerhooks/useCallbackFactory"; import { Deferred } from "evt/tools/Deferred"; import { useConst } from "powerhooks/useConst"; -import type { Param0 } from "tsafe"; +import type { Equals, Param0 } from "tsafe"; import { TextField } from "onyxia-ui/TextField"; import type { TextFieldProps } from "onyxia-ui/TextField"; import { useRerenderOnStateChange } from "evt/hooks/useRerenderOnStateChange"; @@ -44,6 +44,7 @@ import { import type { Item } from "../shared/types"; import { ViewMode } from "../shared/types"; import { isDirectory } from "../shared/tools"; +import { ShareDialog } from "../ShareFile/ShareDialog"; export type ExplorerProps = { /** @@ -169,21 +170,21 @@ export const Explorer = memo((props: ExplorerProps) => { onNavigate({ directoryPath: pathJoin(directoryPath, "..") }); }); - const { evtItemsAction } = useConst(() => ({ - evtItemsAction: Evt.create>() - })); + const evtExplorerItemsAction = useConst(() => + Evt.create>() + ); const buttonBarCallback = useConstCallback(buttonId => { switch (buttonId) { case "refresh": onRefresh(); - break; + return; case "delete": - evtItemsAction.post("DELETE SELECTED ITEM"); - break; + evtExplorerItemsAction.post("DELETE SELECTED ITEM"); + return; case "copy path": - evtItemsAction.post("COPY SELECTED ITEM PATH"); - break; + evtExplorerItemsAction.post("COPY SELECTED ITEM PATH"); + return; case "create directory": setCreateS3DirectoryDialogState({ directories: items @@ -191,12 +192,16 @@ export const Explorer = memo((props: ExplorerProps) => { .map(({ basename }) => basename), resolveBasename: basename => onCreateDirectory({ basename }) }); - break; + return; case "new": setIsUploadModalOpen(true); - break; + return; + case "share": + evtExplorerItemsAction.post("SHARE"); + return; } + assert>(); }); useEvt( @@ -276,6 +281,12 @@ export const Explorer = memo((props: ExplorerProps) => { } ); + const onShare = useConstCallback( + async ({ basename }: Param0) => { + assert(false, "TODO"); + } + ); + const itemsOnDeleteItems = useConstCallback( async ( { items }: Parameters[0], @@ -384,13 +395,11 @@ export const Explorer = memo((props: ExplorerProps) => {
{(() => { switch (viewMode) { @@ -407,10 +416,11 @@ export const Explorer = memo((props: ExplorerProps) => { onPolicyChange={onItemsPolicyChange} onCopyPath={itemsOnCopyPath} onDeleteItem={itemsOnDeleteItem} - evtAction={evtItemsAction} + onShare={onShare} + evtAction={evtExplorerItemsAction} /> ); - case "list": { + case "list": return ( { onPolicyChange={onItemsPolicyChange} onCopyPath={itemsOnCopyPath} onDeleteItems={itemsOnDeleteItems} - evtAction={evtItemsAction} + evtAction={evtExplorerItemsAction} /> ); - } - default: - return null; } + assert>(); })()}
@@ -475,6 +483,12 @@ export const Explorer = memo((props: ExplorerProps) => { } /> + + void; onDeleteItem: (params: { item: Item }) => void; onCopyPath: (params: { basename: string }) => void; - evtAction: NonPostableEvt<"DELETE SELECTED ITEM" | "COPY SELECTED ITEM PATH">; + onShare: (params: { basename: string }) => void; + evtAction: NonPostableEvt< + "DELETE SELECTED ITEM" | "COPY SELECTED ITEM PATH" | "SHARE" + >; }; export const ExplorerItems = memo((props: ExplorerItemsProps) => { @@ -46,7 +49,8 @@ export const ExplorerItems = memo((props: ExplorerItemsProps) => { evtAction, onPolicyChange, onCopyPath, - onDeleteItem + onDeleteItem, + onShare } = props; const isEmpty = items.length === 0; @@ -106,14 +110,21 @@ export const ExplorerItems = memo((props: ExplorerItemsProps) => { case "DELETE SELECTED ITEM": assert(selectedItem.kind !== "none"); onDeleteItem({ item: selectedItem }); - break; + return; case "COPY SELECTED ITEM PATH": assert(selectedItem.kind !== "none"); onCopyPath({ basename: selectedItem.basename }); - break; + return; + case "SHARE": + assert(selectedItem.kind === "file"); + onShare({ + basename: selectedItem.basename + }); + return; } + assert>(); }), [evtAction, onDeleteItem, onCopyPath, selectedItem] ); diff --git a/web/src/ui/pages/myFiles/ShareFile/ShareDialog.tsx b/web/src/ui/pages/myFiles/ShareFile/ShareDialog.tsx index 674cfd27a..c26c548dc 100644 --- a/web/src/ui/pages/myFiles/ShareFile/ShareDialog.tsx +++ b/web/src/ui/pages/myFiles/ShareFile/ShareDialog.tsx @@ -10,19 +10,72 @@ import { tss } from "tss"; import { id } from "tsafe"; import { SelectTime } from "./SelectTime"; import { FileItem } from "../shared/types"; -import { TextField } from "@mui/material"; +import TextField from "@mui/material/TextField"; import { CopyToClipboardIconButton } from "ui/shared/CopyToClipboardIconButton"; -export type ShareDialogProps = { - file: FileItem; - url: string | undefined; //undefined if file.policy is public +/* +{ + + import { useShareDialog } from "./ShareDialog"; + + const { ShareDialog, onOpenShareDialog } = useShareDialog(); + + return ( + onOpenShareDialog({ + file, url + })} > + + ); + +} + +{ + import { ShareDialog, useShareDialog } from "./ShareDialog"; + + + const { shareDialogState, onOpenShareDialog } = useShareDialog(); + + return ( + + onOpenShareDialog({ + file, url + })} > + + + ); + +} +*/ + +type ShareDialogState = { isOpen: boolean; onClose: () => void; - createSignedLink: (expirationTime: number) => Promise; + file: FileItem; + url: string | undefined; //undefined if file.policy is public + isRequestingUrl: boolean; + onRequestUrl: (params: { expirationTime: number }) => void; +}; + +export function useShareDialog(params: { + requestUrl: (params: { file: FileItem }) => Promise; +}): { + shareDialogSate: ShareDialogState; + onOpenShareDialog: (params: { + file: FileItem; + url: string | undefined; //undefined if file.policy is public + }) => void; +} { + return null as any; +} + +export type ShareDialogProps = { + state: ShareDialogState; }; export const ShareDialog = memo((props: ShareDialogProps) => { - const { file, url } = props; + const { + state: { file, url } + } = props; const { t } = useTranslation({ ShareDialog }); const { classes } = useStyles(); @@ -68,7 +121,9 @@ export const ShareDialog = memo((props: ShareDialogProps) => { slotProps={{ input: { endAdornment: ( - + ) } }} From b506115f90fa1883a1460a90633e3900ffc66ac1 Mon Sep 17 00:00:00 2001 From: Dylan Decrulle Date: Fri, 15 Nov 2024 13:10:52 +0100 Subject: [PATCH 03/15] checkpoint --- .../core/usecases/fileExplorer/selectors.ts | 60 +++++++++++++-- web/src/core/usecases/fileExplorer/state.ts | 33 ++++++-- web/src/core/usecases/fileExplorer/thunks.ts | 77 +++++++++++++++---- 3 files changed, 142 insertions(+), 28 deletions(-) diff --git a/web/src/core/usecases/fileExplorer/selectors.ts b/web/src/core/usecases/fileExplorer/selectors.ts index 980d8f922..2820af2a3 100644 --- a/web/src/core/usecases/fileExplorer/selectors.ts +++ b/web/src/core/usecases/fileExplorer/selectors.ts @@ -6,6 +6,7 @@ import * as s3ConfigManagement from "core/usecases/s3ConfigManagement"; import { assert } from "tsafe/assert"; import * as userAuthentication from "core/usecases/userAuthentication"; import { id } from "tsafe/id"; +import type { S3Object } from "core/ports/S3Client"; const state = (rootState: RootState): State => rootState[name]; @@ -190,8 +191,8 @@ const currentWorkingDirectoryView = createSelector( export type ShareView = ShareView.PublicFile | ShareView.PrivateFile; export namespace ShareView { - type Common = { - file: CurrentWorkingDirectoryView.Item.File; + export type Common = { + file: S3Object.File; }; export type PublicFile = Common & { @@ -201,6 +202,8 @@ export namespace ShareView { export type PrivateFile = Common & { isPublic: false; + validityDurationSecond: number; + validityDurationSecondOptions: number[]; signedUrl: string | undefined; isSignedUrlBeingRequested: boolean; }; @@ -209,13 +212,60 @@ export namespace ShareView { const shareView = createSelector( createSelector(state, state => state.directoryPath), createSelector(state, state => state.objects), - - (directoryPath, objects): ShareView | undefined | null => { + createSelector(state, state => state.share), + (directoryPath, objects, share): ShareView | undefined | null => { if (directoryPath === undefined) { return null; } - assert(false, "TODO"); + if (share === undefined) { + return undefined; + } + + const common: ShareView.Common = { + file: (() => { + const file = objects.find( + obj => obj.basename === share.fileBasename && obj.kind === "file" + ); + + assert(file !== undefined); + assert(file.kind === "file"); + + return file; + })() + }; + + const isPublic = share.isSignedUrlBeingRequested === undefined; + + if (isPublic) { + assert(share.url !== undefined); + + return id({ + ...common, + isPublic: true, + url: share.url + }); + } + + const { + url, + isSignedUrlBeingRequested, + validityDurationSecond, + validityDurationSecondOptions + } = share; + + assert(isSignedUrlBeingRequested !== undefined); + assert(validityDurationSecond !== undefined); + assert(validityDurationSecondOptions !== undefined); + + return id({ + ...common, + isPublic: false, + isSignedUrlBeingRequested, + signedUrl: url, + validityDurationSecond, + validityDurationSecondOptions + }); } ); diff --git a/web/src/core/usecases/fileExplorer/state.ts b/web/src/core/usecases/fileExplorer/state.ts index a4d4d3f3e..64cca4f71 100644 --- a/web/src/core/usecases/fileExplorer/state.ts +++ b/web/src/core/usecases/fileExplorer/state.ts @@ -34,7 +34,9 @@ export type State = { | { fileBasename: string; url: string | undefined; - isSignedUrlBeingRequested: boolean; + validityDurationSecond: number | undefined; + validityDurationSecondOptions: number[] | undefined; + isSignedUrlBeingRequested: boolean | undefined; } | undefined; }; @@ -337,16 +339,31 @@ export const { reducer, actions } = createUsecaseActions({ payload: { fileBasename: string; url: string | undefined; + validityDurationSecondOptions: number[] | undefined; }; } ) => { - const { fileBasename, url } = payload; - - state.share = { - fileBasename, - url, - isSignedUrlBeingRequested: false - }; + const { fileBasename, url, validityDurationSecondOptions } = payload; + + if (url !== undefined) { + state.share = { + fileBasename, + url, + isSignedUrlBeingRequested: undefined, + validityDurationSecondOptions: undefined, + validityDurationSecond: undefined + }; + } else { + assert(validityDurationSecondOptions !== undefined); + + state.share = { + fileBasename, + url, + isSignedUrlBeingRequested: false, + validityDurationSecondOptions, + validityDurationSecond: validityDurationSecondOptions[0] + }; + } }, shareClosed: state => { state.share = undefined; diff --git a/web/src/core/usecases/fileExplorer/thunks.ts b/web/src/core/usecases/fileExplorer/thunks.ts index 104fedd66..3cc8fcd50 100644 --- a/web/src/core/usecases/fileExplorer/thunks.ts +++ b/web/src/core/usecases/fileExplorer/thunks.ts @@ -692,9 +692,9 @@ export const thunks = { ); }, getFileDownloadUrl: - (params: { basename: string }) => + (params: { basename: string; validityDurationSecond: number }) => async (...args): Promise => { - const { basename } = params; + const { basename, validityDurationSecond } = params; const [dispatch, getState] = args; @@ -711,7 +711,7 @@ export const thunks = { dispatch( actions.commandLogIssued({ cmdId, - cmd: `mc share download --expire 1h ${pathJoin("s3", path)}` + cmd: `mc share download --expire ${validityDurationSecond}s ${pathJoin("s3", path)}` }) ); @@ -724,7 +724,7 @@ export const thunks = { const downloadUrl = await s3Client.getFileDownloadUrl({ path, - validityDurationSecond: 3600 + validityDurationSecond }); dispatch( @@ -732,7 +732,8 @@ export const thunks = { cmdId, resp: [ `URL: ${downloadUrl.split("?")[0]}`, - `Expire: 0 days 1 hours 0 minutes 0 seconds`, + // TODO: Pretty print + `Expire: ${validityDurationSecond} seconds`, `Share: ${downloadUrl}` ].join("\n") }) @@ -751,29 +752,75 @@ export const thunks = { assert(directoryPath !== undefined); - const { s3Config } = await dispatch( + const { s3Client, s3Config } = await dispatch( s3ConfigManagement.protectedThunks.getS3ConfigAndClientForExplorer() ).then(r => { assert(r !== undefined); return r; }); - const url = (() => { - const currentObj = objects.find( - o => o.basename === fileBasename && o.kind === "file" + const currentObj = objects.find( + o => o.basename === fileBasename && o.kind === "file" + ); + + assert(currentObj !== undefined); + + if (currentObj.policy === "public") { + dispatch( + actions.shareOpened({ + fileBasename, + url: `${s3Config.paramsOfCreateS3Client.url}/${pathJoin(directoryPath, fileBasename)}`, + validityDurationSecondOptions: undefined + }) ); + return; + } - assert(currentObj !== undefined); - })(); + const tokens = await s3Client.getToken({ doForceRenew: false }); + + assert(tokens !== undefined); + + const { expirationTime = Infinity } = tokens; + + const validityDurationSecondOptions = [ + 3_600, + 12 * 3_600, + 24 * 3_600, + 48 * 3_600, + 7 * 24 * 3_600 + ].filter(validityDuration => validityDuration < expirationTime - Date.now()); dispatch( actions.shareOpened({ fileBasename, - url: - currentObj.policy === "private" - ? undefined - : `${s3Config.paramsOfCreateS3Client.url}/${pathJoin(directoryPath, fileBasename)}` + url: undefined, + validityDurationSecondOptions }) ); + }, + closeShare: + () => + (...args) => { + const [dispatch, getState] = args; + + if (getState()[name].share === undefined) { + return; + } + + dispatch(actions.shareClosed()); + }, + requestShareSignedUrl: + (params: {}) => + async (...args) => { + const [dispatch, getState] = args; + + { + const state = getState()[name]; + + assert(state.share !== undefined); + assert(state.share.url === undefined); + } + + dispatch(actions.requestSignedUrlStarted()); } } satisfies Thunks; From 8803a6b3c58081f02b1d6d1a4b990596dd4a35fc Mon Sep 17 00:00:00 2001 From: Dylan Decrulle Date: Thu, 21 Nov 2024 13:50:45 +0100 Subject: [PATCH 04/15] wip --- web/src/core/usecases/fileExplorer/state.ts | 7 +- web/src/core/usecases/fileExplorer/thunks.ts | 21 +++- .../ui/pages/myFiles/Explorer/Explorer.tsx | 23 ++-- .../Explorer/ExplorerItems/ExplorerItems.tsx | 8 +- .../ListExplorer/ListExplorerItems.tsx | 14 ++- web/src/ui/pages/myFiles/MyFiles.tsx | 4 +- .../myFiles/ShareFile/SelectTime.stories.tsx | 24 +++- .../ui/pages/myFiles/ShareFile/SelectTime.tsx | 45 +++++-- .../myFiles/ShareFile/ShareDialog.stories.tsx | 20 ++- .../pages/myFiles/ShareFile/ShareDialog.tsx | 116 ++++++------------ 10 files changed, 162 insertions(+), 120 deletions(-) diff --git a/web/src/core/usecases/fileExplorer/state.ts b/web/src/core/usecases/fileExplorer/state.ts index 64cca4f71..9752ff267 100644 --- a/web/src/core/usecases/fileExplorer/state.ts +++ b/web/src/core/usecases/fileExplorer/state.ts @@ -368,9 +368,14 @@ export const { reducer, actions } = createUsecaseActions({ shareClosed: state => { state.share = undefined; }, - requestSignedUrlStarted: state => { + requestSignedUrlStarted: ( + state, + { payload }: { payload: { expirationTime: number } } + ) => { + const { expirationTime } = payload; assert(state.share !== undefined); state.share.isSignedUrlBeingRequested = true; + state.share.validityDurationSecond = expirationTime; }, requestSignedUrlCompleted: ( state, diff --git a/web/src/core/usecases/fileExplorer/thunks.ts b/web/src/core/usecases/fileExplorer/thunks.ts index 3cc8fcd50..d9c485e64 100644 --- a/web/src/core/usecases/fileExplorer/thunks.ts +++ b/web/src/core/usecases/fileExplorer/thunks.ts @@ -692,9 +692,9 @@ export const thunks = { ); }, getFileDownloadUrl: - (params: { basename: string; validityDurationSecond: number }) => + (params: { basename: string; validityDurationSecond?: number }) => async (...args): Promise => { - const { basename, validityDurationSecond } = params; + const { basename, validityDurationSecond = 3_600 } = params; const [dispatch, getState] = args; @@ -810,17 +810,26 @@ export const thunks = { dispatch(actions.shareClosed()); }, requestShareSignedUrl: - (params: {}) => + (params: { expirationTime: number }) => async (...args) => { + const { expirationTime } = params; const [dispatch, getState] = args; + const state = getState()[name]; { - const state = getState()[name]; - assert(state.share !== undefined); assert(state.share.url === undefined); } - dispatch(actions.requestSignedUrlStarted()); + dispatch(actions.requestSignedUrlStarted({ expirationTime })); + + const url = await dispatch( + thunks.getFileDownloadUrl({ + basename: state.share.fileBasename, + validityDurationSecond: expirationTime + }) + ); + + dispatch(actions.requestSignedUrlCompleted({ url })); } } satisfies Thunks; diff --git a/web/src/ui/pages/myFiles/Explorer/Explorer.tsx b/web/src/ui/pages/myFiles/Explorer/Explorer.tsx index b0ebcdbd6..544ee191a 100644 --- a/web/src/ui/pages/myFiles/Explorer/Explorer.tsx +++ b/web/src/ui/pages/myFiles/Explorer/Explorer.tsx @@ -45,6 +45,7 @@ import type { Item } from "../shared/types"; import { ViewMode } from "../shared/types"; import { isDirectory } from "../shared/tools"; import { ShareDialog } from "../ShareFile/ShareDialog"; +import { on } from "events"; export type ExplorerProps = { /** @@ -70,7 +71,7 @@ export type ExplorerProps = { onRefresh: () => void; onDeleteItem: (params: { item: Item }) => void; onDeleteItems: (params: { items: Item[] }) => void; - + onShareFile: (params: { fileBasename: string }) => void; onCreateDirectory: (params: { basename: string }) => void; onCopyPath: (params: { path: string }) => void; scrollableDivRef: RefObject; @@ -100,7 +101,8 @@ export const Explorer = memo((props: ExplorerProps) => { filesBeingUploaded, pathMinDepth, onViewModeChange, - viewMode + viewMode, + onShareFile } = props; const [items] = useMemo( @@ -198,7 +200,7 @@ export const Explorer = memo((props: ExplorerProps) => { setIsUploadModalOpen(true); return; case "share": - evtExplorerItemsAction.post("SHARE"); + evtExplorerItemsAction.post("SHARE SELECTED FILE"); return; } assert>(); @@ -281,9 +283,9 @@ export const Explorer = memo((props: ExplorerProps) => { } ); - const onShare = useConstCallback( - async ({ basename }: Param0) => { - assert(false, "TODO"); + const onShareDialogOpen = useConstCallback( + async ({ fileBasename }: Param0) => { + onShareFile({ fileBasename }); } ); @@ -416,7 +418,7 @@ export const Explorer = memo((props: ExplorerProps) => { onPolicyChange={onItemsPolicyChange} onCopyPath={itemsOnCopyPath} onDeleteItem={itemsOnDeleteItem} - onShare={onShare} + onShare={onShareDialogOpen} evtAction={evtExplorerItemsAction} /> ); @@ -433,6 +435,7 @@ export const Explorer = memo((props: ExplorerProps) => { onPolicyChange={onItemsPolicyChange} onCopyPath={itemsOnCopyPath} onDeleteItems={itemsOnDeleteItems} + onShare={onShareDialogOpen} evtAction={evtExplorerItemsAction} /> ); @@ -483,11 +486,7 @@ export const Explorer = memo((props: ExplorerProps) => { } /> - + void; onDeleteItem: (params: { item: Item }) => void; onCopyPath: (params: { basename: string }) => void; - onShare: (params: { basename: string }) => void; + onShare: (params: { fileBasename: string }) => void; evtAction: NonPostableEvt< - "DELETE SELECTED ITEM" | "COPY SELECTED ITEM PATH" | "SHARE" + "DELETE SELECTED ITEM" | "COPY SELECTED ITEM PATH" | "SHARE SELECTED FILE" >; }; @@ -117,10 +117,10 @@ export const ExplorerItems = memo((props: ExplorerItemsProps) => { basename: selectedItem.basename }); return; - case "SHARE": + case "SHARE SELECTED FILE": assert(selectedItem.kind === "file"); onShare({ - basename: selectedItem.basename + fileBasename: selectedItem.basename }); return; } diff --git a/web/src/ui/pages/myFiles/Explorer/ListExplorer/ListExplorerItems.tsx b/web/src/ui/pages/myFiles/Explorer/ListExplorer/ListExplorerItems.tsx index 4dba60d2d..675ab37c8 100644 --- a/web/src/ui/pages/myFiles/Explorer/ListExplorer/ListExplorerItems.tsx +++ b/web/src/ui/pages/myFiles/Explorer/ListExplorer/ListExplorerItems.tsx @@ -44,8 +44,9 @@ export type ListExplorerItemsProps = { onDeleteItems: (params: { items: Item[] }, onDeleteConfirmed?: () => void) => void; onCopyPath: (params: { basename: string }) => void; + onShare: (params: { fileBasename: string }) => void; evtAction: NonPostableEvt< - "DELETE SELECTED ITEM" | "COPY SELECTED ITEM PATH" //TODO: Delete, legacy from secret explorer + "DELETE SELECTED ITEM" | "COPY SELECTED ITEM PATH" | "SHARE SELECTED FILE" >; }; @@ -69,7 +70,8 @@ export const ListExplorerItems = memo((props: ListExplorerItemsProps) => { onDeleteItems, onOpenFile, onPolicyChange, - onSelectedItemKindValueChange + onSelectedItemKindValueChange, + onShare } = props; const apiRef = useGridApiRef(); @@ -257,6 +259,14 @@ export const ListExplorerItems = memo((props: ListExplorerItemsProps) => { basename: selectedItems[0].basename }); break; + case "SHARE SELECTED FILE": + assert( + selectedItems.length === 1 && selectedItems[0].kind === "file" + ); + onShare({ + fileBasename: selectedItems[0].basename + }); + return; } }), [evtAction, onDeleteItems, onCopyPath] diff --git a/web/src/ui/pages/myFiles/MyFiles.tsx b/web/src/ui/pages/myFiles/MyFiles.tsx index 0fa1223c4..e455abbc6 100644 --- a/web/src/ui/pages/myFiles/MyFiles.tsx +++ b/web/src/ui/pages/myFiles/MyFiles.tsx @@ -38,7 +38,8 @@ export default function MyFiles(props: Props) { uploadProgress, currentWorkingDirectoryView, pathMinDepth, - viewMode + viewMode, + shareView } = useCoreState("fileExplorer", "main"); const { fileExplorer } = useCore().functions; @@ -198,6 +199,7 @@ export default function MyFiles(props: Props) { onOpenFile={onOpenFile} viewMode={viewMode} onViewModeChange={fileExplorer.changeViewMode} + onShareFile={fileExplorer.openShare} /> ); diff --git a/web/src/ui/pages/myFiles/ShareFile/SelectTime.stories.tsx b/web/src/ui/pages/myFiles/ShareFile/SelectTime.stories.tsx index 1fd420bc0..fb073152a 100644 --- a/web/src/ui/pages/myFiles/ShareFile/SelectTime.stories.tsx +++ b/web/src/ui/pages/myFiles/ShareFile/SelectTime.stories.tsx @@ -1,5 +1,6 @@ import { Meta, StoryObj } from "@storybook/react"; import { SelectTime } from "./SelectTime"; +import { useState } from "react"; const meta: Meta = { title: "Pages/MyFiles/NewComponents/SelectTime", @@ -11,5 +12,26 @@ export default meta; type Story = StoryObj; export const Default: Story = { - args: {} + render: args => { + const [expirationValue, setExpirationValue] = useState( + args.validityDurationSecondOptions[0] + ); + + const handleExpirationValueChange = (newValue: number) => { + console.log("Expiration value changed to:", newValue); + setExpirationValue(newValue); + }; + + return ( + + ); + }, + args: { + className: "", + validityDurationSecondOptions: [3600, 7200, 10800] // Example options: 1h, 2h, 3h + } }; diff --git a/web/src/ui/pages/myFiles/ShareFile/SelectTime.tsx b/web/src/ui/pages/myFiles/ShareFile/SelectTime.tsx index cf090e154..3927dac9a 100644 --- a/web/src/ui/pages/myFiles/ShareFile/SelectTime.tsx +++ b/web/src/ui/pages/myFiles/ShareFile/SelectTime.tsx @@ -2,12 +2,9 @@ import { useId } from "react"; import InputLabel from "@mui/material/InputLabel"; import MenuItem from "@mui/material/MenuItem"; import FormControl from "@mui/material/FormControl"; -import Select from "@mui/material/Select"; +import Select, { SelectChangeEvent } from "@mui/material/Select"; import { tss } from "tss"; - -type Props = { - className?: string; -}; +import { assert } from "tsafe/assert"; const expirationOptions = [ { id: "1 heure", name: "1 heure" }, @@ -16,26 +13,52 @@ const expirationOptions = [ { id: "48 heures", name: "48 heures" }, { id: "7 jours", name: "7 jours" } ]; +type Props = { + className?: string; + validityDurationSecondOptions: number[]; + expirationValue: number; + onExpirationValueChange: ( + value: Props["validityDurationSecondOptions"][number] + ) => void; +}; export function SelectTime(props: Props) { - const { className } = props; + const { + className, + validityDurationSecondOptions, + expirationValue, + onExpirationValueChange + } = props; const labelId = useId(); const { classes, cx } = useStyles(); + + const handleChange = (event: SelectChangeEvent) => { + const newValue = event.target.value; + + assert( + typeof newValue === "number" && + validityDurationSecondOptions.includes(newValue) + ); + onExpirationValueChange(newValue); + }; + return ( Durée de validité - diff --git a/web/src/ui/pages/myFiles/ShareFile/ShareDialog.stories.tsx b/web/src/ui/pages/myFiles/ShareFile/ShareDialog.stories.tsx index 4e624c2b9..fb2694763 100644 --- a/web/src/ui/pages/myFiles/ShareFile/ShareDialog.stories.tsx +++ b/web/src/ui/pages/myFiles/ShareFile/ShareDialog.stories.tsx @@ -1,5 +1,6 @@ import { Meta, StoryObj } from "@storybook/react"; import { ShareDialog } from "./ShareDialog"; +import { action } from "@storybook/addon-actions"; const meta = { title: "Pages/MyFiles/ShareFile/ShareDialog", @@ -12,6 +13,11 @@ type Story = StoryObj; export const Public: Story = { args: { + isOpen: true, + onClose: action("onClose"), + isRequestingUrl: false, + onRequestUrl: (params: { expirationTime: number }) => + action(`onRequestUrl ${params.expirationTime}`), file: { kind: "file", basename: "photo.png", @@ -24,23 +30,31 @@ export const Public: Story = { isBeingCreated: true, uploadPercent: 75 // Example upload percentage }, - url: "https://minio.lab.sspcloud.fr/onyxia-datalab/public/photo.png" + url: "https://minio.lab.sspcloud.fr/onyxia-datalab/public/photo.png", + validityDurationSecondOptions: [3600, 7200, 10800] } }; export const Private: Story = { args: { + isOpen: true, + onClose: action("onClose"), + isRequestingUrl: false, + onRequestUrl: (params: { expirationTime: number }) => + action(`onRequestUrl ${params.expirationTime}`), file: { kind: "file", basename: "photo.png", size: 2048000, // en bytes lastModified: new Date("2023-09-15"), - policy: "private", + policy: "public", isBeingDeleted: false, isPolicyChanging: false, + isBeingCreated: true, uploadPercent: 75 // Example upload percentage }, - url: "" + url: "https://minio.lab.sspcloud.fr/onyxia-datalab/public/photo.png", + validityDurationSecondOptions: [3600, 7200, 10800] } }; diff --git a/web/src/ui/pages/myFiles/ShareFile/ShareDialog.tsx b/web/src/ui/pages/myFiles/ShareFile/ShareDialog.tsx index c26c548dc..dd41e79e8 100644 --- a/web/src/ui/pages/myFiles/ShareFile/ShareDialog.tsx +++ b/web/src/ui/pages/myFiles/ShareFile/ShareDialog.tsx @@ -5,90 +5,44 @@ import { memo, useState } from "react"; import { useTranslation } from "ui/i18n"; import { DirectoryOrFileDetailed } from "../shared/DirectoryOrFileDetailed"; import { Text } from "onyxia-ui/Text"; -import { MuiIconComponentName } from "onyxia-ui/MuiIconComponentName"; +import { getIconUrlByName } from "lazy-icons"; import { tss } from "tss"; -import { id } from "tsafe"; import { SelectTime } from "./SelectTime"; import { FileItem } from "../shared/types"; import TextField from "@mui/material/TextField"; import { CopyToClipboardIconButton } from "ui/shared/CopyToClipboardIconButton"; +import { CircularProgress } from "onyxia-ui/CircularProgress"; -/* -{ - - import { useShareDialog } from "./ShareDialog"; - - const { ShareDialog, onOpenShareDialog } = useShareDialog(); - - return ( - onOpenShareDialog({ - file, url - })} > - - ); - -} - -{ - import { ShareDialog, useShareDialog } from "./ShareDialog"; - - - const { shareDialogState, onOpenShareDialog } = useShareDialog(); - - return ( - - onOpenShareDialog({ - file, url - })} > - - - ); - -} -*/ - -type ShareDialogState = { +type ShareDialogProps = { isOpen: boolean; onClose: () => void; file: FileItem; url: string | undefined; //undefined if file.policy is public isRequestingUrl: boolean; + validityDurationSecondOptions: number[]; onRequestUrl: (params: { expirationTime: number }) => void; }; -export function useShareDialog(params: { - requestUrl: (params: { file: FileItem }) => Promise; -}): { - shareDialogSate: ShareDialogState; - onOpenShareDialog: (params: { - file: FileItem; - url: string | undefined; //undefined if file.policy is public - }) => void; -} { - return null as any; -} - -export type ShareDialogProps = { - state: ShareDialogState; -}; - export const ShareDialog = memo((props: ShareDialogProps) => { const { - state: { file, url } + file, + url, + isOpen, + onClose, + isRequestingUrl, + onRequestUrl, + validityDurationSecondOptions } = props; const { t } = useTranslation({ ShareDialog }); const { classes } = useStyles(); - const [isOpen, setIsOpen] = useState(true); - const onClose = () => setIsOpen(false); - - const isPublic = file.policy === "public"; - const shareIconId = id( - isPublic ? "VisibilityOff" : "Visibility" + const [valueExpirationTime, setValueExpirationTime] = useState( + validityDurationSecondOptions[0] ); + const isPublic = file.policy === "public"; - const [doCreateLink, setDoCreateLink] = useState(!isPublic); // If the file is private, we need to create a signed link + //const shareIconId = getIconUrlByName(isPublic ? "VisibilityOff" : "Visibility"); return ( { {t("paragraph change policy", { policy: file.policy })} - {doCreateLink ? ( + {url === undefined ? (
- +
) : ( @@ -121,15 +83,13 @@ export const ShareDialog = memo((props: ShareDialogProps) => { slotProps={{ input: { endAdornment: ( - + ) } }} helperText={t("hint link access", { policy: file.policy, - expirationDate: undefined + expirationDate: undefined //TODO })} variant="standard" value={url} @@ -147,16 +107,14 @@ export const ShareDialog = memo((props: ShareDialogProps) => { /> } buttons={ - <> - - + } /> ); From 8cd3945be65b0b7ebf516641b56d40c413a33cd4 Mon Sep 17 00:00:00 2001 From: Dylan Decrulle Date: Thu, 21 Nov 2024 16:44:27 +0100 Subject: [PATCH 05/15] i18n adapt components etc --- .../core/usecases/fileExplorer/selectors.ts | 4 +- web/src/ui/i18n/resources/de.tsx | 26 +++++++++- web/src/ui/i18n/resources/en.tsx | 26 +++++++++- web/src/ui/i18n/resources/es.tsx | 26 +++++++++- web/src/ui/i18n/resources/fi.tsx | 26 +++++++++- web/src/ui/i18n/resources/fr.tsx | 31 +++++++---- web/src/ui/i18n/resources/it.tsx | 26 +++++++++- web/src/ui/i18n/resources/nl.tsx | 26 +++++++++- web/src/ui/i18n/resources/no.tsx | 26 +++++++++- web/src/ui/i18n/resources/zh-CN.tsx | 26 +++++++++- .../ui/pages/myFiles/Explorer/Explorer.tsx | 4 +- .../ExplorerItems/ExplorerItems.stories.tsx | 10 +++- .../ListExplorerItems.stories.tsx | 5 +- web/src/ui/pages/myFiles/MyFiles.tsx | 3 +- .../ui/pages/myFiles/ShareFile/SelectTime.tsx | 7 --- .../myFiles/ShareFile/ShareDialog.stories.tsx | 51 ++++++++++++++----- .../pages/myFiles/ShareFile/ShareDialog.tsx | 43 +++++++++++----- 17 files changed, 296 insertions(+), 70 deletions(-) diff --git a/web/src/core/usecases/fileExplorer/selectors.ts b/web/src/core/usecases/fileExplorer/selectors.ts index 2820af2a3..c99a45d9f 100644 --- a/web/src/core/usecases/fileExplorer/selectors.ts +++ b/web/src/core/usecases/fileExplorer/selectors.ts @@ -204,7 +204,7 @@ export namespace ShareView { isPublic: false; validityDurationSecond: number; validityDurationSecondOptions: number[]; - signedUrl: string | undefined; + url: string | undefined; isSignedUrlBeingRequested: boolean; }; } @@ -262,7 +262,7 @@ const shareView = createSelector( ...common, isPublic: false, isSignedUrlBeingRequested, - signedUrl: url, + url, validityDurationSecond, validityDurationSecondOptions }); diff --git a/web/src/ui/i18n/resources/de.tsx b/web/src/ui/i18n/resources/de.tsx index ff977ece8..db59c5bc2 100644 --- a/web/src/ui/i18n/resources/de.tsx +++ b/web/src/ui/i18n/resources/de.tsx @@ -315,8 +315,30 @@ export const translations: Translations<"de"> = { ) }, ShareDialog: { - cancel: "Abbrechen", - "create and copy link": "Erstellen und kopieren" + title: "Ihre Daten teilen", + close: "Schließen", + "create and copy link": "Link erstellen und kopieren", + "paragraph current policy": ({ policy }) => { + switch (policy) { + case "public": + return "Ihre Datei ist öffentlich, jeder mit dem Link kann sie herunterladen."; + case "private": + return "Ihre Datei ist derzeit privat."; + } + }, + "paragraph change policy": ({ policy }) => { + switch (policy) { + case "public": + return "Um den Zugriff einzuschränken, ändern Sie den Freigabestatus Ihrer Datei."; + case "private": + return "Um Ihre Datei freizugeben und Zugriff zu gewähren, ändern Sie den Freigabestatus oder erstellen Sie einen temporären Zugriffslink."; + } + }, + "hint link access": params => + params.policy === "private" + ? `Dieser Link gewährt für ${params.expiration} Zugriff auf Ihre Daten.` + : "Ihr Link ist verfügbar, solange die Datei öffentlich ist", + "label input link": "Zugriffslink" }, MySecrets: { "page title - my secrets": "Meine Geheimnisse", diff --git a/web/src/ui/i18n/resources/en.tsx b/web/src/ui/i18n/resources/en.tsx index 503a9adc0..75da911f2 100644 --- a/web/src/ui/i18n/resources/en.tsx +++ b/web/src/ui/i18n/resources/en.tsx @@ -304,8 +304,30 @@ export const translations: Translations<"en"> = { ) }, ShareDialog: { - cancel: "Cancel", - "create and copy link": "Create and copy link" + title: "Share your data", + close: "Close", + "create and copy link": "Create and copy link", + "paragraph current policy": ({ policy }) => { + switch (policy) { + case "public": + return "Your file is public, anyone with the link can download it."; + case "private": + return "Your file is currently private."; + } + }, + "paragraph change policy": ({ policy }) => { + switch (policy) { + case "public": + return "To restrict its access, change your file's sharing status."; + case "private": + return "To share and provide access to your file, change the sharing status or create a temporary access link."; + } + }, + "hint link access": params => + params.policy === "private" + ? `This link will grant access to your data for ${params.expiration}.` + : "Your link is available as long as the file is public", + "label input link": "Access link" }, MySecrets: { "page title - my secrets": "My Secrets", diff --git a/web/src/ui/i18n/resources/es.tsx b/web/src/ui/i18n/resources/es.tsx index 9c1c6df99..8c7615f9b 100644 --- a/web/src/ui/i18n/resources/es.tsx +++ b/web/src/ui/i18n/resources/es.tsx @@ -316,8 +316,30 @@ export const translations: Translations<"en"> = { ) }, ShareDialog: { - cancel: "Cancelar", - "create and copy link": "Crear y copiar enlace" + title: "Compartir tus datos", + close: "Cerrar", + "create and copy link": "Crear y copiar enlace", + "paragraph current policy": ({ policy }) => { + switch (policy) { + case "public": + return "Tu archivo es público, cualquier persona con el enlace puede descargarlo."; + case "private": + return "Tu archivo está actualmente privado."; + } + }, + "paragraph change policy": ({ policy }) => { + switch (policy) { + case "public": + return "Para restringir su acceso, cambia el estado de difusión de tu archivo."; + case "private": + return "Para compartir y dar acceso a tu archivo, cambia el estado de difusión o crea un enlace de acceso temporal."; + } + }, + "hint link access": params => + params.policy === "private" + ? `Este enlace otorgará acceso a tus datos durante ${params.expiration}.` + : "Tu enlace está disponible mientras el archivo sea público", + "label input link": "Enlace de acceso" }, MySecrets: { "page title - my secrets": "Mis Secretos", diff --git a/web/src/ui/i18n/resources/fi.tsx b/web/src/ui/i18n/resources/fi.tsx index ffd884a5b..e51b21c1d 100644 --- a/web/src/ui/i18n/resources/fi.tsx +++ b/web/src/ui/i18n/resources/fi.tsx @@ -312,8 +312,30 @@ export const translations: Translations<"fi"> = { ) }, ShareDialog: { - cancel: "Peruuta", - "create and copy link": "Luo ja kopioi linkki" + title: "Jaa tietosi", + close: "Sulje", + "create and copy link": "Luo ja kopioi linkki", + "paragraph current policy": ({ policy }) => { + switch (policy) { + case "public": + return "Tiedostosi on julkinen, kuka tahansa linkin omistava voi ladata sen."; + case "private": + return "Tiedostosi on tällä hetkellä yksityinen."; + } + }, + "paragraph change policy": ({ policy }) => { + switch (policy) { + case "public": + return "Rajoittaaksesi pääsyä muuta tiedostosi jakamisen tilaa."; + case "private": + return "Jaa tiedosto ja anna pääsy muuttamalla jakamisen tilaa tai luomalla väliaikainen linkki."; + } + }, + "hint link access": params => + params.policy === "private" + ? `Tämä linkki antaa pääsyn tietoihisi ${params.expiration} ajaksi.` + : "Linkkisi on käytettävissä niin kauan kuin tiedosto on julkinen", + "label input link": "Pääsylinkki" }, MySecrets: { "page title - my secrets": "Omat salaisuudet", diff --git a/web/src/ui/i18n/resources/fr.tsx b/web/src/ui/i18n/resources/fr.tsx index 56ffe7590..0a53f5826 100644 --- a/web/src/ui/i18n/resources/fr.tsx +++ b/web/src/ui/i18n/resources/fr.tsx @@ -321,17 +321,26 @@ export const translations: Translations<"fr"> = { title: "Partager vos données", close: "Fermer", "create and copy link": "Créer et copier le lien", - "paragraph current policy": ({ - policy - }) => `Votre fichier est public, toute personne ayant le lien peut - télécharger votre fichier`, - "paragraph change policy": ({ - policy - }) => `Pour restreindre son accès, changez le statut de diffusion de - votre fichier.`, - "hint link access": ({ policy, expirationDate }) => - policy === "private" - ? `Votre lien ....` + "paragraph current policy": ({ policy }) => { + switch (policy) { + case "public": + return `Votre fichier est public, toute personne ayant le lien peut télécharger votre fichier.`; + case "private": + return "Votre fichier est actuellement privé."; + } + }, + "paragraph change policy": ({ policy }) => { + switch (policy) { + case "public": + return `Pour restreindre son accès, changez le statut de diffusion de + votre fichier.`; + case "private": + return `Pour partager et donner accès à votre fichier, changez le statut de diffusion ou créez un lien d’accès temporaire.`; + } + }, + "hint link access": params => + params.policy === "private" + ? `Ce lien donnera un accès à vos données pendant ${params.expiration}.` : "Votre lien est disponible tant que le fichier est publique", "label input link": "Lien d'accès" }, diff --git a/web/src/ui/i18n/resources/it.tsx b/web/src/ui/i18n/resources/it.tsx index 09e09e627..50aaf4b8d 100644 --- a/web/src/ui/i18n/resources/it.tsx +++ b/web/src/ui/i18n/resources/it.tsx @@ -313,8 +313,30 @@ export const translations: Translations<"it"> = { ) }, ShareDialog: { - cancel: "Annulla", - "create and copy link": "Creare e copiare il link" + title: "Condividi i tuoi dati", + close: "Chiudi", + "create and copy link": "Crea e copia il link", + "paragraph current policy": ({ policy }) => { + switch (policy) { + case "public": + return "Il tuo file è pubblico, chiunque abbia il link può scaricarlo."; + case "private": + return "Il tuo file è attualmente privato."; + } + }, + "paragraph change policy": ({ policy }) => { + switch (policy) { + case "public": + return "Per limitare l'accesso, modifica lo stato di condivisione del tuo file."; + case "private": + return "Per condividere e dare accesso al tuo file, modifica lo stato di condivisione o crea un link di accesso temporaneo."; + } + }, + "hint link access": params => + params.policy === "private" + ? `Questo link garantirà l'accesso ai tuoi dati per ${params.expiration}.` + : "Il tuo link è disponibile finché il file è pubblico", + "label input link": "Link di accesso" }, MySecrets: { "page title - my secrets": "I miei segreti", diff --git a/web/src/ui/i18n/resources/nl.tsx b/web/src/ui/i18n/resources/nl.tsx index beba0a47a..7b2a64036 100644 --- a/web/src/ui/i18n/resources/nl.tsx +++ b/web/src/ui/i18n/resources/nl.tsx @@ -315,8 +315,30 @@ export const translations: Translations<"nl"> = { ) }, ShareDialog: { - cancel: "Annuleren", - "create and copy link": "Creare e copiare il link" + title: "Deel je gegevens", + close: "Sluiten", + "create and copy link": "Link maken en kopiëren", + "paragraph current policy": ({ policy }) => { + switch (policy) { + case "public": + return "Je bestand is openbaar, iedereen met de link kan het downloaden."; + case "private": + return "Je bestand is momenteel privé."; + } + }, + "paragraph change policy": ({ policy }) => { + switch (policy) { + case "public": + return "Om toegang te beperken, verander de deelstatus van je bestand."; + case "private": + return "Om toegang te geven tot je bestand, verander de deelstatus of maak een tijdelijke toegangslink."; + } + }, + "hint link access": params => + params.policy === "private" + ? `Deze link geeft toegang tot je gegevens gedurende ${params.expiration}.` + : "Je link is beschikbaar zolang het bestand openbaar is", + "label input link": "Toegangslink" }, MySecrets: { "page title - my secrets": "My Secrets", diff --git a/web/src/ui/i18n/resources/no.tsx b/web/src/ui/i18n/resources/no.tsx index 437cd1272..9d201ea3e 100644 --- a/web/src/ui/i18n/resources/no.tsx +++ b/web/src/ui/i18n/resources/no.tsx @@ -311,8 +311,30 @@ export const translations: Translations<"no"> = { ) }, ShareDialog: { - cancel: "Avbryt", - "create and copy link": "Opprett og kopier lenke" + title: "Del dataene dine", + close: "Lukk", + "create and copy link": "Opprett og kopier lenke", + "paragraph current policy": ({ policy }) => { + switch (policy) { + case "public": + return "Filen din er offentlig, alle med lenken kan laste den ned."; + case "private": + return "Filen din er for øyeblikket privat."; + } + }, + "paragraph change policy": ({ policy }) => { + switch (policy) { + case "public": + return "For å begrense tilgangen, endre delingsstatusen til filen din."; + case "private": + return "For å dele og gi tilgang til filen din, endre delingsstatusen eller opprett en midlertidig tilgangslenke."; + } + }, + "hint link access": params => + params.policy === "private" + ? `Denne lenken gir tilgang til dataene dine i ${params.expiration}.` + : "Lenken din er tilgjengelig så lenge filen er offentlig", + "label input link": "Tilgangslenke" }, MySecrets: { "page title - my secrets": "Mine hemmeligheter", diff --git a/web/src/ui/i18n/resources/zh-CN.tsx b/web/src/ui/i18n/resources/zh-CN.tsx index a189a8c81..a0be9f202 100644 --- a/web/src/ui/i18n/resources/zh-CN.tsx +++ b/web/src/ui/i18n/resources/zh-CN.tsx @@ -282,8 +282,30 @@ export const translations: Translations<"zh-CN"> = { ) }, ShareDialog: { - cancel: "取消", - "create and copy link": "创建并复制链接" + title: "分享您的数据", + close: "关闭", + "create and copy link": "创建并复制链接", + "paragraph current policy": ({ policy }) => { + switch (policy) { + case "public": + return "您的文件是公开的,任何拥有链接的人都可以下载。"; + case "private": + return "您的文件当前是私密的。"; + } + }, + "paragraph change policy": ({ policy }) => { + switch (policy) { + case "public": + return "要限制访问,请更改文件的共享状态。"; + case "private": + return "要分享并提供对文件的访问,请更改共享状态或创建一个临时访问链接。"; + } + }, + "hint link access": params => + params.policy === "private" + ? `此链接将在 ${params.expiration} 内提供对您的数据的访问权限。` + : "只要文件是公开的,您的链接就可用", + "label input link": "访问链接" }, MySecrets: { "page title - my secrets": "我的密钥", diff --git a/web/src/ui/pages/myFiles/Explorer/Explorer.tsx b/web/src/ui/pages/myFiles/Explorer/Explorer.tsx index 544ee191a..50bf5a756 100644 --- a/web/src/ui/pages/myFiles/Explorer/Explorer.tsx +++ b/web/src/ui/pages/myFiles/Explorer/Explorer.tsx @@ -44,8 +44,6 @@ import { import type { Item } from "../shared/types"; import { ViewMode } from "../shared/types"; import { isDirectory } from "../shared/tools"; -import { ShareDialog } from "../ShareFile/ShareDialog"; -import { on } from "events"; export type ExplorerProps = { /** @@ -486,7 +484,7 @@ export const Explorer = memo((props: ExplorerProps) => { } /> - + {/* */} () + evtAction: Evt.create< + "DELETE SELECTED ITEM" | "COPY SELECTED ITEM PATH" | "SHARE SELECTED FILE" + >(), + onShare: action("Share file") } }; @@ -87,6 +90,9 @@ export const EmptyDirectory: Story = { onCopyPath: action("Copy path"), onPolicyChange: action("Policy change"), onSelectedItemKindValueChange: action("Selected item kind changed"), - evtAction: Evt.create<"DELETE SELECTED ITEM" | "COPY SELECTED ITEM PATH">() + evtAction: Evt.create< + "DELETE SELECTED ITEM" | "COPY SELECTED ITEM PATH" | "SHARE SELECTED FILE" + >(), + onShare: action("Share file") } }; diff --git a/web/src/ui/pages/myFiles/Explorer/ListExplorer/ListExplorerItems.stories.tsx b/web/src/ui/pages/myFiles/Explorer/ListExplorer/ListExplorerItems.stories.tsx index 11602e165..0d5a20e71 100644 --- a/web/src/ui/pages/myFiles/Explorer/ListExplorer/ListExplorerItems.stories.tsx +++ b/web/src/ui/pages/myFiles/Explorer/ListExplorer/ListExplorerItems.stories.tsx @@ -73,6 +73,9 @@ export const Default: Story = { onPolicyChange: action("Policy change"), onCopyPath: action("Copy path"), onSelectedItemKindValueChange: action("Selected item kind changed"), - evtAction: Evt.create<"DELETE SELECTED ITEM" | "COPY SELECTED ITEM PATH">() + evtAction: Evt.create< + "DELETE SELECTED ITEM" | "COPY SELECTED ITEM PATH" | "SHARE SELECTED FILE" + >(), + onShare: action("Share file") } }; diff --git a/web/src/ui/pages/myFiles/MyFiles.tsx b/web/src/ui/pages/myFiles/MyFiles.tsx index e455abbc6..a5b511d18 100644 --- a/web/src/ui/pages/myFiles/MyFiles.tsx +++ b/web/src/ui/pages/myFiles/MyFiles.tsx @@ -38,8 +38,7 @@ export default function MyFiles(props: Props) { uploadProgress, currentWorkingDirectoryView, pathMinDepth, - viewMode, - shareView + viewMode } = useCoreState("fileExplorer", "main"); const { fileExplorer } = useCore().functions; diff --git a/web/src/ui/pages/myFiles/ShareFile/SelectTime.tsx b/web/src/ui/pages/myFiles/ShareFile/SelectTime.tsx index 3927dac9a..57408778d 100644 --- a/web/src/ui/pages/myFiles/ShareFile/SelectTime.tsx +++ b/web/src/ui/pages/myFiles/ShareFile/SelectTime.tsx @@ -6,13 +6,6 @@ import Select, { SelectChangeEvent } from "@mui/material/Select"; import { tss } from "tss"; import { assert } from "tsafe/assert"; -const expirationOptions = [ - { id: "1 heure", name: "1 heure" }, - { id: "12 heures", name: "12 heures" }, - { id: "24 heures", name: "24 heures" }, - { id: "48 heures", name: "48 heures" }, - { id: "7 jours", name: "7 jours" } -]; type Props = { className?: string; validityDurationSecondOptions: number[]; diff --git a/web/src/ui/pages/myFiles/ShareFile/ShareDialog.stories.tsx b/web/src/ui/pages/myFiles/ShareFile/ShareDialog.stories.tsx index fb2694763..2d06f0b17 100644 --- a/web/src/ui/pages/myFiles/ShareFile/ShareDialog.stories.tsx +++ b/web/src/ui/pages/myFiles/ShareFile/ShareDialog.stories.tsx @@ -1,5 +1,6 @@ -import { Meta, StoryObj } from "@storybook/react"; import { ShareDialog } from "./ShareDialog"; +import { Meta, StoryObj } from "@storybook/react"; +import { useState } from "react"; import { action } from "@storybook/addon-actions"; const meta = { @@ -26,9 +27,7 @@ export const Public: Story = { policy: "public", isBeingDeleted: false, isPolicyChanging: false, - - isBeingCreated: true, - uploadPercent: 75 // Example upload percentage + isBeingCreated: false }, url: "https://minio.lab.sspcloud.fr/onyxia-datalab/public/photo.png", validityDurationSecondOptions: [3600, 7200, 10800] @@ -36,25 +35,51 @@ export const Public: Story = { }; export const Private: Story = { + render: args => { + const [url, setUrl] = useState(args.url); + const [isRequestingUrl, setIsRequestingUrl] = useState(false); + + const handleRequestUrl = (params: { expirationTime: number }) => { + action(`onRequestUrl ${params.expirationTime}`)(); + setIsRequestingUrl(true); // Simulate loading + + setTimeout(() => { + // Generate a dynamic URL after a 2-second delay + const generatedUrl = `https://example.com/file/photo.png?expires=${params.expirationTime}`; + setUrl(generatedUrl); + setIsRequestingUrl(false); // Stop loading + }, 2000); + }; + + return ( + + ); + }, args: { isOpen: true, onClose: action("onClose"), isRequestingUrl: false, - onRequestUrl: (params: { expirationTime: number }) => - action(`onRequestUrl ${params.expirationTime}`), file: { kind: "file", basename: "photo.png", - size: 2048000, // en bytes + size: 2048000, // in bytes lastModified: new Date("2023-09-15"), - policy: "public", + policy: "private", isBeingDeleted: false, isPolicyChanging: false, - - isBeingCreated: true, - uploadPercent: 75 // Example upload percentage + isBeingCreated: false }, - url: "https://minio.lab.sspcloud.fr/onyxia-datalab/public/photo.png", - validityDurationSecondOptions: [3600, 7200, 10800] + url: undefined, + validityDurationSecondOptions: [3600, 7200, 10800], + onRequestUrl: (params: { expirationTime: number }) => { + action( + `onRequestUrl triggered with expirationTime: ${params.expirationTime}` + )(); + } } }; diff --git a/web/src/ui/pages/myFiles/ShareFile/ShareDialog.tsx b/web/src/ui/pages/myFiles/ShareFile/ShareDialog.tsx index dd41e79e8..3ba4ce043 100644 --- a/web/src/ui/pages/myFiles/ShareFile/ShareDialog.tsx +++ b/web/src/ui/pages/myFiles/ShareFile/ShareDialog.tsx @@ -11,7 +11,7 @@ import { SelectTime } from "./SelectTime"; import { FileItem } from "../shared/types"; import TextField from "@mui/material/TextField"; import { CopyToClipboardIconButton } from "ui/shared/CopyToClipboardIconButton"; -import { CircularProgress } from "onyxia-ui/CircularProgress"; +import { CircularProgress } from "@mui/material"; type ShareDialogProps = { isOpen: boolean; @@ -42,8 +42,6 @@ export const ShareDialog = memo((props: ShareDialogProps) => { ); const isPublic = file.policy === "public"; - //const shareIconId = getIconUrlByName(isPublic ? "VisibilityOff" : "Visibility"); - return ( { } onExpirationValueChange={setValueExpirationTime} /> - + + {isRequestingUrl && ( + + )} + ) : ( { }} helperText={t("hint link access", { policy: file.policy, - expirationDate: undefined //TODO + expiration: valueExpirationTime.toLocaleString() //TODO })} variant="standard" value={url} @@ -134,6 +148,9 @@ const useStyles = tss.withName({ ShareDialog }).create(({ theme }) => ({ display: "flex", flexDirection: "row", justifyContent: "space-between" + }, + createLinkProgress: { + position: "absolute" } })); const { i18n } = declareComponentKeys< @@ -144,7 +161,7 @@ const { i18n } = declareComponentKeys< | { K: "paragraph change policy"; P: { policy: FileItem["policy"] } } | { K: "hint link access"; - P: { policy: FileItem["policy"]; expirationDate: Date | undefined }; + P: { policy: FileItem["policy"]; expiration: string | undefined }; } | "label input link" >()({ From 6907591b84b9577714bc30d5cf022018164ffff4 Mon Sep 17 00:00:00 2001 From: Dylan Decrulle Date: Fri, 22 Nov 2024 16:25:35 +0100 Subject: [PATCH 06/15] wip --- web/src/ui/i18n/resources/de.tsx | 31 +-- web/src/ui/i18n/resources/en.tsx | 34 ++- web/src/ui/i18n/resources/es.tsx | 34 ++- web/src/ui/i18n/resources/fi.tsx | 34 ++- web/src/ui/i18n/resources/fr.tsx | 35 ++- web/src/ui/i18n/resources/it.tsx | 34 ++- web/src/ui/i18n/resources/nl.tsx | 34 ++- web/src/ui/i18n/resources/no.tsx | 34 ++- web/src/ui/i18n/resources/zh-CN.tsx | 34 ++- .../ui/pages/myFiles/Explorer/Explorer.tsx | 33 ++- web/src/ui/pages/myFiles/MyFiles.tsx | 4 +- .../MyFilesCreateFolderDialog.stories.tsx | 2 +- .../myFiles/ShareFile/SelectTime.stories.tsx | 2 +- .../myFiles/ShareFile/ShareDialog.stories.tsx | 77 +++--- .../pages/myFiles/ShareFile/ShareDialog.tsx | 238 ++++++++++-------- 15 files changed, 335 insertions(+), 325 deletions(-) diff --git a/web/src/ui/i18n/resources/de.tsx b/web/src/ui/i18n/resources/de.tsx index db59c5bc2..afa9f98e2 100644 --- a/web/src/ui/i18n/resources/de.tsx +++ b/web/src/ui/i18n/resources/de.tsx @@ -318,26 +318,19 @@ export const translations: Translations<"de"> = { title: "Ihre Daten teilen", close: "Schließen", "create and copy link": "Link erstellen und kopieren", - "paragraph current policy": ({ policy }) => { - switch (policy) { - case "public": - return "Ihre Datei ist öffentlich, jeder mit dem Link kann sie herunterladen."; - case "private": - return "Ihre Datei ist derzeit privat."; - } - }, - "paragraph change policy": ({ policy }) => { - switch (policy) { - case "public": - return "Um den Zugriff einzuschränken, ändern Sie den Freigabestatus Ihrer Datei."; - case "private": - return "Um Ihre Datei freizugeben und Zugriff zu gewähren, ändern Sie den Freigabestatus oder erstellen Sie einen temporären Zugriffslink."; - } - }, + "paragraph current policy": ({ isPublic }) => + isPublic + ? "Ihre Datei ist öffentlich, jeder mit dem Link kann sie herunterladen." + : "Ihre Datei ist derzeit privat.", + "paragraph change policy": ({ isPublic }) => + isPublic + ? "Um den Zugriff einzuschränken, ändern Sie den Freigabestatus Ihrer Datei." + : "Um Ihre Datei freizugeben und Zugriff zu gewähren, ändern Sie den Freigabestatus oder erstellen Sie einen temporären Zugriffslink.", + "hint link access": params => - params.policy === "private" - ? `Dieser Link gewährt für ${params.expiration} Zugriff auf Ihre Daten.` - : "Ihr Link ist verfügbar, solange die Datei öffentlich ist", + params.isPublic + ? "Ihr Link ist verfügbar, solange die Datei öffentlich ist" + : `Dieser Link gewährt für ${params.expiration} Zugriff auf Ihre Daten.`, "label input link": "Zugriffslink" }, MySecrets: { diff --git a/web/src/ui/i18n/resources/en.tsx b/web/src/ui/i18n/resources/en.tsx index 75da911f2..65ead3816 100644 --- a/web/src/ui/i18n/resources/en.tsx +++ b/web/src/ui/i18n/resources/en.tsx @@ -307,26 +307,20 @@ export const translations: Translations<"en"> = { title: "Share your data", close: "Close", "create and copy link": "Create and copy link", - "paragraph current policy": ({ policy }) => { - switch (policy) { - case "public": - return "Your file is public, anyone with the link can download it."; - case "private": - return "Your file is currently private."; - } - }, - "paragraph change policy": ({ policy }) => { - switch (policy) { - case "public": - return "To restrict its access, change your file's sharing status."; - case "private": - return "To share and provide access to your file, change the sharing status or create a temporary access link."; - } - }, - "hint link access": params => - params.policy === "private" - ? `This link will grant access to your data for ${params.expiration}.` - : "Your link is available as long as the file is public", + "paragraph current policy": ({ isPublic }) => + isPublic + ? "Your file is public, anyone with the link can download it." + : "Your file is currently private.", + + "paragraph change policy": ({ isPublic }) => + isPublic + ? "To restrict its access, change your file's sharing status." + : "To share and provide access to your file, change the sharing status or create a temporary access link.", + + "hint link access": ({ isPublic, expiration }) => + isPublic + ? "Your link is available as long as the file is public." + : `This link will grant access to your data for ${expiration}.`, "label input link": "Access link" }, MySecrets: { diff --git a/web/src/ui/i18n/resources/es.tsx b/web/src/ui/i18n/resources/es.tsx index 8c7615f9b..c9374444f 100644 --- a/web/src/ui/i18n/resources/es.tsx +++ b/web/src/ui/i18n/resources/es.tsx @@ -319,26 +319,20 @@ export const translations: Translations<"en"> = { title: "Compartir tus datos", close: "Cerrar", "create and copy link": "Crear y copiar enlace", - "paragraph current policy": ({ policy }) => { - switch (policy) { - case "public": - return "Tu archivo es público, cualquier persona con el enlace puede descargarlo."; - case "private": - return "Tu archivo está actualmente privado."; - } - }, - "paragraph change policy": ({ policy }) => { - switch (policy) { - case "public": - return "Para restringir su acceso, cambia el estado de difusión de tu archivo."; - case "private": - return "Para compartir y dar acceso a tu archivo, cambia el estado de difusión o crea un enlace de acceso temporal."; - } - }, - "hint link access": params => - params.policy === "private" - ? `Este enlace otorgará acceso a tus datos durante ${params.expiration}.` - : "Tu enlace está disponible mientras el archivo sea público", + "paragraph current policy": ({ isPublic }) => + isPublic + ? "Tu archivo es público, cualquier persona con el enlace puede descargarlo." + : "Tu archivo está actualmente privado.", + + "paragraph change policy": ({ isPublic }) => + isPublic + ? "Para restringir su acceso, cambia el estado de difusión de tu archivo." + : "Para compartir y dar acceso a tu archivo, cambia el estado de difusión o crea un enlace de acceso temporal.", + + "hint link access": ({ isPublic, expiration }) => + isPublic + ? "Tu enlace está disponible mientras el archivo sea público." + : `Este enlace otorgará acceso a tus datos durante ${expiration}.`, "label input link": "Enlace de acceso" }, MySecrets: { diff --git a/web/src/ui/i18n/resources/fi.tsx b/web/src/ui/i18n/resources/fi.tsx index e51b21c1d..1215c58e2 100644 --- a/web/src/ui/i18n/resources/fi.tsx +++ b/web/src/ui/i18n/resources/fi.tsx @@ -315,26 +315,20 @@ export const translations: Translations<"fi"> = { title: "Jaa tietosi", close: "Sulje", "create and copy link": "Luo ja kopioi linkki", - "paragraph current policy": ({ policy }) => { - switch (policy) { - case "public": - return "Tiedostosi on julkinen, kuka tahansa linkin omistava voi ladata sen."; - case "private": - return "Tiedostosi on tällä hetkellä yksityinen."; - } - }, - "paragraph change policy": ({ policy }) => { - switch (policy) { - case "public": - return "Rajoittaaksesi pääsyä muuta tiedostosi jakamisen tilaa."; - case "private": - return "Jaa tiedosto ja anna pääsy muuttamalla jakamisen tilaa tai luomalla väliaikainen linkki."; - } - }, - "hint link access": params => - params.policy === "private" - ? `Tämä linkki antaa pääsyn tietoihisi ${params.expiration} ajaksi.` - : "Linkkisi on käytettävissä niin kauan kuin tiedosto on julkinen", + "paragraph current policy": ({ isPublic }) => + isPublic + ? "Tiedostosi on julkinen, kuka tahansa linkin omistava voi ladata sen." + : "Tiedostosi on tällä hetkellä yksityinen.", + + "paragraph change policy": ({ isPublic }) => + isPublic + ? "Rajoittaaksesi pääsyä muuta tiedostosi jakamisen tilaa." + : "Jaa tiedosto ja anna pääsy muuttamalla jakamisen tilaa tai luomalla väliaikainen linkki.", + + "hint link access": ({ isPublic, expiration }) => + isPublic + ? "Linkkisi on käytettävissä niin kauan kuin tiedosto on julkinen." + : `Tämä linkki antaa pääsyn tietoihisi ${expiration} ajaksi.`, "label input link": "Pääsylinkki" }, MySecrets: { diff --git a/web/src/ui/i18n/resources/fr.tsx b/web/src/ui/i18n/resources/fr.tsx index 0a53f5826..804205323 100644 --- a/web/src/ui/i18n/resources/fr.tsx +++ b/web/src/ui/i18n/resources/fr.tsx @@ -321,27 +321,20 @@ export const translations: Translations<"fr"> = { title: "Partager vos données", close: "Fermer", "create and copy link": "Créer et copier le lien", - "paragraph current policy": ({ policy }) => { - switch (policy) { - case "public": - return `Votre fichier est public, toute personne ayant le lien peut télécharger votre fichier.`; - case "private": - return "Votre fichier est actuellement privé."; - } - }, - "paragraph change policy": ({ policy }) => { - switch (policy) { - case "public": - return `Pour restreindre son accès, changez le statut de diffusion de - votre fichier.`; - case "private": - return `Pour partager et donner accès à votre fichier, changez le statut de diffusion ou créez un lien d’accès temporaire.`; - } - }, - "hint link access": params => - params.policy === "private" - ? `Ce lien donnera un accès à vos données pendant ${params.expiration}.` - : "Votre lien est disponible tant que le fichier est publique", + "paragraph current policy": ({ isPublic }) => + isPublic + ? "Votre fichier est public, toute personne ayant le lien peut télécharger votre fichier." + : "Votre fichier est actuellement privé.", + + "paragraph change policy": ({ isPublic }) => + isPublic + ? "Pour restreindre son accès, changez le statut de diffusion de votre fichier." + : "Pour partager et donner accès à votre fichier, changez le statut de diffusion ou créez un lien d’accès temporaire.", + + "hint link access": ({ isPublic, expiration }) => + isPublic + ? "Votre lien est disponible tant que le fichier est public." + : `Ce lien donnera un accès à vos données pendant ${expiration}.`, "label input link": "Lien d'accès" }, MySecrets: { diff --git a/web/src/ui/i18n/resources/it.tsx b/web/src/ui/i18n/resources/it.tsx index 50aaf4b8d..0123323c7 100644 --- a/web/src/ui/i18n/resources/it.tsx +++ b/web/src/ui/i18n/resources/it.tsx @@ -316,26 +316,20 @@ export const translations: Translations<"it"> = { title: "Condividi i tuoi dati", close: "Chiudi", "create and copy link": "Crea e copia il link", - "paragraph current policy": ({ policy }) => { - switch (policy) { - case "public": - return "Il tuo file è pubblico, chiunque abbia il link può scaricarlo."; - case "private": - return "Il tuo file è attualmente privato."; - } - }, - "paragraph change policy": ({ policy }) => { - switch (policy) { - case "public": - return "Per limitare l'accesso, modifica lo stato di condivisione del tuo file."; - case "private": - return "Per condividere e dare accesso al tuo file, modifica lo stato di condivisione o crea un link di accesso temporaneo."; - } - }, - "hint link access": params => - params.policy === "private" - ? `Questo link garantirà l'accesso ai tuoi dati per ${params.expiration}.` - : "Il tuo link è disponibile finché il file è pubblico", + "paragraph current policy": ({ isPublic }) => + isPublic + ? "Il tuo file è pubblico, chiunque abbia il link può scaricarlo." + : "Il tuo file è attualmente privato.", + + "paragraph change policy": ({ isPublic }) => + isPublic + ? "Per limitare l'accesso, modifica lo stato di condivisione del tuo file." + : "Per condividere e dare accesso al tuo file, modifica lo stato di condivisione o crea un link di accesso temporaneo.", + + "hint link access": ({ isPublic, expiration }) => + isPublic + ? "Il tuo link è disponibile finché il file è pubblico." + : `Questo link garantirà l'accesso ai tuoi dati per ${expiration}.`, "label input link": "Link di accesso" }, MySecrets: { diff --git a/web/src/ui/i18n/resources/nl.tsx b/web/src/ui/i18n/resources/nl.tsx index 7b2a64036..7db823a4c 100644 --- a/web/src/ui/i18n/resources/nl.tsx +++ b/web/src/ui/i18n/resources/nl.tsx @@ -318,26 +318,20 @@ export const translations: Translations<"nl"> = { title: "Deel je gegevens", close: "Sluiten", "create and copy link": "Link maken en kopiëren", - "paragraph current policy": ({ policy }) => { - switch (policy) { - case "public": - return "Je bestand is openbaar, iedereen met de link kan het downloaden."; - case "private": - return "Je bestand is momenteel privé."; - } - }, - "paragraph change policy": ({ policy }) => { - switch (policy) { - case "public": - return "Om toegang te beperken, verander de deelstatus van je bestand."; - case "private": - return "Om toegang te geven tot je bestand, verander de deelstatus of maak een tijdelijke toegangslink."; - } - }, - "hint link access": params => - params.policy === "private" - ? `Deze link geeft toegang tot je gegevens gedurende ${params.expiration}.` - : "Je link is beschikbaar zolang het bestand openbaar is", + "paragraph current policy": ({ isPublic }) => + isPublic + ? "Je bestand is openbaar, iedereen met de link kan het downloaden." + : "Je bestand is momenteel privé.", + + "paragraph change policy": ({ isPublic }) => + isPublic + ? "Om toegang te beperken, verander de deelstatus van je bestand." + : "Om toegang te geven tot je bestand, verander de deelstatus of maak een tijdelijke toegangslink.", + + "hint link access": ({ isPublic, expiration }) => + isPublic + ? "Je link is beschikbaar zolang het bestand openbaar is." + : `Deze link geeft toegang tot je gegevens gedurende ${expiration}.`, "label input link": "Toegangslink" }, MySecrets: { diff --git a/web/src/ui/i18n/resources/no.tsx b/web/src/ui/i18n/resources/no.tsx index 9d201ea3e..fe529ea69 100644 --- a/web/src/ui/i18n/resources/no.tsx +++ b/web/src/ui/i18n/resources/no.tsx @@ -314,26 +314,20 @@ export const translations: Translations<"no"> = { title: "Del dataene dine", close: "Lukk", "create and copy link": "Opprett og kopier lenke", - "paragraph current policy": ({ policy }) => { - switch (policy) { - case "public": - return "Filen din er offentlig, alle med lenken kan laste den ned."; - case "private": - return "Filen din er for øyeblikket privat."; - } - }, - "paragraph change policy": ({ policy }) => { - switch (policy) { - case "public": - return "For å begrense tilgangen, endre delingsstatusen til filen din."; - case "private": - return "For å dele og gi tilgang til filen din, endre delingsstatusen eller opprett en midlertidig tilgangslenke."; - } - }, - "hint link access": params => - params.policy === "private" - ? `Denne lenken gir tilgang til dataene dine i ${params.expiration}.` - : "Lenken din er tilgjengelig så lenge filen er offentlig", + "paragraph current policy": ({ isPublic }) => + isPublic + ? "Filen din er offentlig, alle med lenken kan laste den ned." + : "Filen din er for øyeblikket privat.", + + "paragraph change policy": ({ isPublic }) => + isPublic + ? "For å begrense tilgangen, endre delingsstatusen til filen din." + : "For å dele og gi tilgang til filen din, endre delingsstatusen eller opprett en midlertidig tilgangslenke.", + + "hint link access": ({ isPublic, expiration }) => + isPublic + ? "Lenken din er tilgjengelig så lenge filen er offentlig." + : `Denne lenken gir tilgang til dataene dine i ${expiration}.`, "label input link": "Tilgangslenke" }, MySecrets: { diff --git a/web/src/ui/i18n/resources/zh-CN.tsx b/web/src/ui/i18n/resources/zh-CN.tsx index a0be9f202..68df0d79b 100644 --- a/web/src/ui/i18n/resources/zh-CN.tsx +++ b/web/src/ui/i18n/resources/zh-CN.tsx @@ -285,26 +285,20 @@ export const translations: Translations<"zh-CN"> = { title: "分享您的数据", close: "关闭", "create and copy link": "创建并复制链接", - "paragraph current policy": ({ policy }) => { - switch (policy) { - case "public": - return "您的文件是公开的,任何拥有链接的人都可以下载。"; - case "private": - return "您的文件当前是私密的。"; - } - }, - "paragraph change policy": ({ policy }) => { - switch (policy) { - case "public": - return "要限制访问,请更改文件的共享状态。"; - case "private": - return "要分享并提供对文件的访问,请更改共享状态或创建一个临时访问链接。"; - } - }, - "hint link access": params => - params.policy === "private" - ? `此链接将在 ${params.expiration} 内提供对您的数据的访问权限。` - : "只要文件是公开的,您的链接就可用", + "paragraph current policy": ({ isPublic }) => + isPublic + ? "您的文件是公开的,任何拥有链接的人都可以下载。" + : "您的文件当前是私密的。", + + "paragraph change policy": ({ isPublic }) => + isPublic + ? "要限制访问,请更改文件的共享状态。" + : "要分享并提供对文件的访问,请更改共享状态或创建一个临时访问链接。", + + "hint link access": ({ isPublic, expiration }) => + isPublic + ? "只要文件是公开的,您的链接就可用。" + : `此链接将在 ${expiration} 内提供对您的数据的访问权限。`, "label input link": "访问链接" }, MySecrets: { diff --git a/web/src/ui/pages/myFiles/Explorer/Explorer.tsx b/web/src/ui/pages/myFiles/Explorer/Explorer.tsx index 50bf5a756..cef9b7e78 100644 --- a/web/src/ui/pages/myFiles/Explorer/Explorer.tsx +++ b/web/src/ui/pages/myFiles/Explorer/Explorer.tsx @@ -44,6 +44,8 @@ import { import type { Item } from "../shared/types"; import { ViewMode } from "../shared/types"; import { isDirectory } from "../shared/tools"; +import { ShareDialog } from "../ShareFile/ShareDialog"; +import { ShareView } from "core/usecases/fileExplorer"; export type ExplorerProps = { /** @@ -73,9 +75,9 @@ export type ExplorerProps = { onCreateDirectory: (params: { basename: string }) => void; onCopyPath: (params: { path: string }) => void; scrollableDivRef: RefObject; - pathMinDepth: number; onOpenFile: (params: { basename: string }) => void; + shareState: ShareView | undefined; //To modify } & Pick; //NOTE: TODO only defined when explorer type is s3 export const Explorer = memo((props: ExplorerProps) => { @@ -100,7 +102,8 @@ export const Explorer = memo((props: ExplorerProps) => { pathMinDepth, onViewModeChange, viewMode, - onShareFile + onShareFile, + shareState } = props; const [items] = useMemo( @@ -281,12 +284,6 @@ export const Explorer = memo((props: ExplorerProps) => { } ); - const onShareDialogOpen = useConstCallback( - async ({ fileBasename }: Param0) => { - onShareFile({ fileBasename }); - } - ); - const itemsOnDeleteItems = useConstCallback( async ( { items }: Parameters[0], @@ -314,10 +311,21 @@ export const Explorer = memo((props: ExplorerProps) => { } ); - const [isUploadModalOpen, setIsUploadModalOpen] = useState(false); + const [isUploadModalOpen, setIsUploadModalOpen] = useState(false); const onUploadModalClose = useConstCallback(() => setIsUploadModalOpen(false)); const onDragOver = useConstCallback(() => setIsUploadModalOpen(true)); + const [isShareModalOpen, setIsShareModalOpen] = useState(false); + + const onShareDialogOpen = useConstCallback( + async ({ fileBasename }: Param0) => { + setIsShareModalOpen(true); + onShareFile({ fileBasename }); + } + ); + + const onShareDialogClose = useConstCallback(() => setIsShareModalOpen(false)); + return ( <>
{ } /> - {/* */} + {/* */}
); diff --git a/web/src/ui/pages/myFiles/ShareFile/MyFilesCreateFolderDialog.stories.tsx b/web/src/ui/pages/myFiles/ShareFile/MyFilesCreateFolderDialog.stories.tsx index 980db64d0..a7298c1ff 100644 --- a/web/src/ui/pages/myFiles/ShareFile/MyFilesCreateFolderDialog.stories.tsx +++ b/web/src/ui/pages/myFiles/ShareFile/MyFilesCreateFolderDialog.stories.tsx @@ -2,7 +2,7 @@ import { Meta, StoryObj } from "@storybook/react"; import { MyFilesCreateFolderDialog } from "./MyFilesCreateFolderDialog"; const meta: Meta = { - title: "Pages/MyFiles/NewComponents/MyFilesCreateFolderDialog", + title: "Pages/MyFiles/ShareFile/MyFilesCreateFolderDialog", component: MyFilesCreateFolderDialog }; diff --git a/web/src/ui/pages/myFiles/ShareFile/SelectTime.stories.tsx b/web/src/ui/pages/myFiles/ShareFile/SelectTime.stories.tsx index fb073152a..0e167d5c3 100644 --- a/web/src/ui/pages/myFiles/ShareFile/SelectTime.stories.tsx +++ b/web/src/ui/pages/myFiles/ShareFile/SelectTime.stories.tsx @@ -3,7 +3,7 @@ import { SelectTime } from "./SelectTime"; import { useState } from "react"; const meta: Meta = { - title: "Pages/MyFiles/NewComponents/SelectTime", + title: "Pages/MyFiles/ShareFile/SelectTime", component: SelectTime }; diff --git a/web/src/ui/pages/myFiles/ShareFile/ShareDialog.stories.tsx b/web/src/ui/pages/myFiles/ShareFile/ShareDialog.stories.tsx index 2d06f0b17..0534b3162 100644 --- a/web/src/ui/pages/myFiles/ShareFile/ShareDialog.stories.tsx +++ b/web/src/ui/pages/myFiles/ShareFile/ShareDialog.stories.tsx @@ -16,27 +16,44 @@ export const Public: Story = { args: { isOpen: true, onClose: action("onClose"), - isRequestingUrl: false, - onRequestUrl: (params: { expirationTime: number }) => - action(`onRequestUrl ${params.expirationTime}`), + isPublic: true, file: { kind: "file", + policy: "public", basename: "photo.png", size: 2048000, // en bytes lastModified: new Date("2023-09-15"), - policy: "public", isBeingDeleted: false, isPolicyChanging: false, isBeingCreated: false }, - url: "https://minio.lab.sspcloud.fr/onyxia-datalab/public/photo.png", - validityDurationSecondOptions: [3600, 7200, 10800] + url: "https://minio.lab.sspcloud.fr/onyxia-datalab/public/photo.png" } }; -export const Private: Story = { - render: args => { - const [url, setUrl] = useState(args.url); +export const Private = { + args: { + isOpen: true, + isPublic: false, + onClose: action("onClose"), + file: { + policy: "private", + kind: "file", + basename: "photo.png", + size: 2048000, // in bytes + lastModified: new Date("2023-09-15"), + isBeingDeleted: false, + isPolicyChanging: false, + isBeingCreated: false + }, + url: undefined, + isRequestingUrl: false, + validityDurationSecondOptions: [3600, 7200, 10800], + validityDurationSecond: 3600, + onRequestUrl: action("onRequestUrl") + }, + render: () => { + const [url, setUrl] = useState(undefined); const [isRequestingUrl, setIsRequestingUrl] = useState(false); const handleRequestUrl = (params: { expirationTime: number }) => { @@ -53,33 +70,27 @@ export const Private: Story = { return ( { + action("onClose")(); + }} + file={{ + policy: "private", + kind: "file", + basename: "photo.png", + size: 2048000, // in bytes + lastModified: new Date("2023-09-15"), + isBeingDeleted: false, + isPolicyChanging: false, + isBeingCreated: false + }} url={url} isRequestingUrl={isRequestingUrl} onRequestUrl={handleRequestUrl} + validityDurationSecondOptions={[3600, 7200, 10800]} + validityDurationSecond={3600} /> ); - }, - args: { - isOpen: true, - onClose: action("onClose"), - isRequestingUrl: false, - file: { - kind: "file", - basename: "photo.png", - size: 2048000, // in bytes - lastModified: new Date("2023-09-15"), - policy: "private", - isBeingDeleted: false, - isPolicyChanging: false, - isBeingCreated: false - }, - url: undefined, - validityDurationSecondOptions: [3600, 7200, 10800], - onRequestUrl: (params: { expirationTime: number }) => { - action( - `onRequestUrl triggered with expirationTime: ${params.expirationTime}` - )(); - } } -}; +} satisfies Story; diff --git a/web/src/ui/pages/myFiles/ShareFile/ShareDialog.tsx b/web/src/ui/pages/myFiles/ShareFile/ShareDialog.tsx index 3ba4ce043..e8b42e8fc 100644 --- a/web/src/ui/pages/myFiles/ShareFile/ShareDialog.tsx +++ b/web/src/ui/pages/myFiles/ShareFile/ShareDialog.tsx @@ -11,114 +11,64 @@ import { SelectTime } from "./SelectTime"; import { FileItem } from "../shared/types"; import TextField from "@mui/material/TextField"; import { CopyToClipboardIconButton } from "ui/shared/CopyToClipboardIconButton"; -import { CircularProgress } from "@mui/material"; - -type ShareDialogProps = { - isOpen: boolean; - onClose: () => void; - file: FileItem; - url: string | undefined; //undefined if file.policy is public - isRequestingUrl: boolean; - validityDurationSecondOptions: number[]; - onRequestUrl: (params: { expirationTime: number }) => void; -}; +import { assert } from "tsafe/assert"; +import { CircularProgress } from "onyxia-ui/CircularProgress"; + +export type ShareDialogProps = ShareDialogProps.Close | ShareDialogProps.Open; +export namespace ShareDialogProps { + type Common = { + onClose: () => void; + }; + + export type Close = Common & { + isOpen: false; + }; + + export type Open = Common & { + isOpen: true; + file: FileItem; + } & BodyProps; + + export type BodyProps = BodyProps.PrivateFileProps | BodyProps.PublicFileProps; + + export namespace BodyProps { + export type PrivateFileProps = { + isPublic: false; // Clé discriminante + url: string | undefined; + isRequestingUrl: boolean; + validityDurationSecondOptions: number[]; + validityDurationSecond: number; + onRequestUrl: (params: { expirationTime: number }) => void; + }; + + export type PublicFileProps = { + isPublic: true; // Clé discriminante + url: string; + }; + } +} export const ShareDialog = memo((props: ShareDialogProps) => { - const { - file, - url, - isOpen, - onClose, - isRequestingUrl, - onRequestUrl, - validityDurationSecondOptions - } = props; + const { isOpen, onClose } = props; const { t } = useTranslation({ ShareDialog }); const { classes } = useStyles(); - const [valueExpirationTime, setValueExpirationTime] = useState( - validityDurationSecondOptions[0] - ); - const isPublic = file.policy === "public"; - return ( - - {t("paragraph current policy", { policy: file.policy })} - - - - {t("paragraph change policy", { policy: file.policy })} - - - {url === undefined ? ( -
- -
- - {isRequestingUrl && ( - - )} -
-
- ) : ( - - ) - } - }} - helperText={t("hint link access", { - policy: file.policy, - expiration: valueExpirationTime.toLocaleString() //TODO - })} - variant="standard" - value={url} - /> - )} - - } + body={isDialogOpen(props) ? : null} title={t("title")} subtitle={ - + isOpen && ( + + ) } buttons={ + {props.isRequestingUrl && ( + + )} + + + ); + })() + : (() => { + assert(url !== undefined); // Assure TypeScript que url est défini ici + return ( + + ) + } + }} + helperText={t("hint link access", { + isPublic, + expiration: undefined // TODO improve + })} + variant="standard" + value={url} + /> + ); + })()} + + ); +}); + +function isDialogOpen(props: ShareDialogProps): props is ShareDialogProps.Open { + return props.isOpen; +} + +function isPrivateFileProps( + props: ShareDialogProps.BodyProps +): props is ShareDialogProps.BodyProps.PrivateFileProps { + return !props.isPublic; +} + const useStyles = tss.withName({ ShareDialog }).create(({ theme }) => ({ body: { display: "flex", @@ -151,17 +186,22 @@ const useStyles = tss.withName({ ShareDialog }).create(({ theme }) => ({ }, createLinkProgress: { position: "absolute" + }, + createLinkButton: { + display: "inline-flex", + alignItems: "center", + justifyContent: "center" } })); const { i18n } = declareComponentKeys< | "title" | "close" | "create and copy link" - | { K: "paragraph current policy"; P: { policy: FileItem["policy"] } } - | { K: "paragraph change policy"; P: { policy: FileItem["policy"] } } + | { K: "paragraph current policy"; P: { isPublic: boolean } } + | { K: "paragraph change policy"; P: { isPublic: boolean } } | { K: "hint link access"; - P: { policy: FileItem["policy"]; expiration: string | undefined }; + P: { isPublic: boolean; expiration: string | undefined }; } | "label input link" >()({ From cb7fd838ffdca10d6c410ee090f20a41355f5a78 Mon Sep 17 00:00:00 2001 From: Dylan Decrulle Date: Fri, 22 Nov 2024 17:22:19 +0100 Subject: [PATCH 07/15] core connected --- .../core/usecases/fileExplorer/selectors.ts | 2 +- .../ui/pages/myFiles/Explorer/Explorer.tsx | 34 +++++++++++-------- .../myFiles/Explorer/ExplorerButtonBar.tsx | 3 +- web/src/ui/pages/myFiles/MyFiles.tsx | 4 ++- .../pages/myFiles/ShareFile/ShareDialog.tsx | 8 ++--- 5 files changed, 29 insertions(+), 22 deletions(-) diff --git a/web/src/core/usecases/fileExplorer/selectors.ts b/web/src/core/usecases/fileExplorer/selectors.ts index c99a45d9f..5fd2a65bb 100644 --- a/web/src/core/usecases/fileExplorer/selectors.ts +++ b/web/src/core/usecases/fileExplorer/selectors.ts @@ -306,7 +306,7 @@ const main = createSelector( viewMode, shareView ) => { - if (directoryPath === null) { + if (directoryPath === undefined) { return { isCurrentWorkingDirectoryLoaded: false as const, isNavigationOngoing, diff --git a/web/src/ui/pages/myFiles/Explorer/Explorer.tsx b/web/src/ui/pages/myFiles/Explorer/Explorer.tsx index cef9b7e78..794344678 100644 --- a/web/src/ui/pages/myFiles/Explorer/Explorer.tsx +++ b/web/src/ui/pages/myFiles/Explorer/Explorer.tsx @@ -44,7 +44,7 @@ import { import type { Item } from "../shared/types"; import { ViewMode } from "../shared/types"; import { isDirectory } from "../shared/tools"; -import { ShareDialog } from "../ShareFile/ShareDialog"; +import { ShareDialog, ShareDialogProps } from "../ShareFile/ShareDialog"; import { ShareView } from "core/usecases/fileExplorer"; export type ExplorerProps = { @@ -71,7 +71,9 @@ export type ExplorerProps = { onRefresh: () => void; onDeleteItem: (params: { item: Item }) => void; onDeleteItems: (params: { items: Item[] }) => void; - onShareFile: (params: { fileBasename: string }) => void; + onShareFileOpen: (params: { fileBasename: string }) => void; + onShareFileClose: () => void; + requestSignedUrl: (params: { expirationTime: number }) => void; onCreateDirectory: (params: { basename: string }) => void; onCopyPath: (params: { path: string }) => void; scrollableDivRef: RefObject; @@ -102,8 +104,10 @@ export const Explorer = memo((props: ExplorerProps) => { pathMinDepth, onViewModeChange, viewMode, - onShareFile, - shareState + onShareFileOpen, + onShareFileClose, + shareState, + requestSignedUrl } = props; const [items] = useMemo( @@ -315,17 +319,19 @@ export const Explorer = memo((props: ExplorerProps) => { const onUploadModalClose = useConstCallback(() => setIsUploadModalOpen(false)); const onDragOver = useConstCallback(() => setIsUploadModalOpen(true)); - const [isShareModalOpen, setIsShareModalOpen] = useState(false); - const onShareDialogOpen = useConstCallback( async ({ fileBasename }: Param0) => { - setIsShareModalOpen(true); - onShareFile({ fileBasename }); + onShareFileOpen({ fileBasename }); } ); - const onShareDialogClose = useConstCallback(() => setIsShareModalOpen(false)); + const onShareDialogClose = useConstCallback(() => onShareFileClose()); + const onRequestSignedUrl = useConstCallback( + async ({ expirationTime }: Param0) => { + requestSignedUrl({ expirationTime }); + } + ); return ( <>
{ } /> - {/* */} + isOpen={shareState !== undefined} + onRequestUrl={onRequestSignedUrl} + {...shareState} + /> { return false; case "new": case "create directory": - //return isFileOpen; return false; case "delete": case "share": - return selectedItemKind === "none"; + return selectedItemKind !== "file"; case "copy path": return selectedItemKind !== "file"; } diff --git a/web/src/ui/pages/myFiles/MyFiles.tsx b/web/src/ui/pages/myFiles/MyFiles.tsx index 3408bf76a..7e93d0065 100644 --- a/web/src/ui/pages/myFiles/MyFiles.tsx +++ b/web/src/ui/pages/myFiles/MyFiles.tsx @@ -199,7 +199,9 @@ export default function MyFiles(props: Props) { onOpenFile={onOpenFile} viewMode={viewMode} onViewModeChange={fileExplorer.changeViewMode} - onShareFile={fileExplorer.openShare} + onShareFileOpen={fileExplorer.openShare} + onShareFileClose={fileExplorer.closeShare} + requestSignedUrl={fileExplorer.requestShareSignedUrl} shareState={shareView} />
diff --git a/web/src/ui/pages/myFiles/ShareFile/ShareDialog.tsx b/web/src/ui/pages/myFiles/ShareFile/ShareDialog.tsx index e8b42e8fc..4630a2419 100644 --- a/web/src/ui/pages/myFiles/ShareFile/ShareDialog.tsx +++ b/web/src/ui/pages/myFiles/ShareFile/ShareDialog.tsx @@ -18,6 +18,7 @@ export type ShareDialogProps = ShareDialogProps.Close | ShareDialogProps.Open; export namespace ShareDialogProps { type Common = { onClose: () => void; + onRequestUrl: (params: { expirationTime: number }) => void; }; export type Close = Common & { @@ -38,7 +39,6 @@ export namespace ShareDialogProps { isRequestingUrl: boolean; validityDurationSecondOptions: number[]; validityDurationSecond: number; - onRequestUrl: (params: { expirationTime: number }) => void; }; export type PublicFileProps = { @@ -84,11 +84,11 @@ export const ShareDialog = memo((props: ShareDialogProps) => { ); }); -const ShareDialogBody = memo((props: ShareDialogProps.BodyProps) => { +const ShareDialogBody = memo((props: ShareDialogProps.Open) => { const { t } = useTranslation({ ShareDialog }); const { classes } = useStyles(); - const { isPublic, url } = props; + const { isPublic, url, onRequestUrl } = props; const [valueExpirationTime, setValueExpirationTime] = useState( isPrivateFileProps(props) ? props.validityDurationSecond : undefined @@ -117,7 +117,7 @@ const ShareDialogBody = memo((props: ShareDialogProps.BodyProps) => { startIcon={getIconUrlByName("Language")} variant="ternary" onClick={() => - props.onRequestUrl({ + onRequestUrl({ expirationTime: valueExpirationTime }) } From 87ac87eb7f301e12ae32c14c17c365959c36bdb4 Mon Sep 17 00:00:00 2001 From: Dylan Decrulle Date: Fri, 22 Nov 2024 17:28:30 +0100 Subject: [PATCH 08/15] ignore ts-error for now --- web/src/ui/pages/myFiles/Explorer/Explorer.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/ui/pages/myFiles/Explorer/Explorer.tsx b/web/src/ui/pages/myFiles/Explorer/Explorer.tsx index 794344678..b13dc8fae 100644 --- a/web/src/ui/pages/myFiles/Explorer/Explorer.tsx +++ b/web/src/ui/pages/myFiles/Explorer/Explorer.tsx @@ -498,6 +498,7 @@ export const Explorer = memo((props: ExplorerProps) => { } /> + {/* @ts-expect-error */} Date: Fri, 22 Nov 2024 17:29:53 +0100 Subject: [PATCH 09/15] fix story --- web/src/ui/pages/myFiles/ShareFile/ShareDialog.stories.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/src/ui/pages/myFiles/ShareFile/ShareDialog.stories.tsx b/web/src/ui/pages/myFiles/ShareFile/ShareDialog.stories.tsx index 0534b3162..5dc1489c3 100644 --- a/web/src/ui/pages/myFiles/ShareFile/ShareDialog.stories.tsx +++ b/web/src/ui/pages/myFiles/ShareFile/ShareDialog.stories.tsx @@ -14,6 +14,7 @@ type Story = StoryObj; export const Public: Story = { args: { + onRequestUrl: action("onRequestUrl"), isOpen: true, onClose: action("onClose"), isPublic: true, @@ -33,6 +34,7 @@ export const Public: Story = { export const Private = { args: { + onRequestUrl: action("onRequestUrl"), isOpen: true, isPublic: false, onClose: action("onClose"), @@ -49,8 +51,7 @@ export const Private = { url: undefined, isRequestingUrl: false, validityDurationSecondOptions: [3600, 7200, 10800], - validityDurationSecond: 3600, - onRequestUrl: action("onRequestUrl") + validityDurationSecond: 3600 }, render: () => { const [url, setUrl] = useState(undefined); From 6a14db58e5f1856e698b52d5a5c15cf9fc056e60 Mon Sep 17 00:00:00 2001 From: Dylan Decrulle Date: Mon, 25 Nov 2024 13:54:09 +0100 Subject: [PATCH 10/15] fix: explorer button bar --- web/src/ui/pages/myFiles/Explorer/ExplorerButtonBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/ui/pages/myFiles/Explorer/ExplorerButtonBar.tsx b/web/src/ui/pages/myFiles/Explorer/ExplorerButtonBar.tsx index 989eb154f..831c9c721 100644 --- a/web/src/ui/pages/myFiles/Explorer/ExplorerButtonBar.tsx +++ b/web/src/ui/pages/myFiles/Explorer/ExplorerButtonBar.tsx @@ -58,8 +58,8 @@ export const ExplorerButtonBar = memo((props: Props) => { case "create directory": return false; case "delete": + return selectedItemKind === "none"; case "share": - return selectedItemKind !== "file"; case "copy path": return selectedItemKind !== "file"; } From 52a9046303617e501c0d843d815e2fbe35b8edb7 Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Mon, 25 Nov 2024 17:19:43 +0100 Subject: [PATCH 11/15] Review --- .../core/usecases/fileExplorer/selectors.ts | 2 +- web/src/core/usecases/fileExplorer/state.ts | 22 +- web/src/core/usecases/fileExplorer/thunks.ts | 30 +- .../ui/pages/myFiles/Explorer/Explorer.tsx | 36 +- web/src/ui/pages/myFiles/MyFiles.tsx | 7 +- .../ui/pages/myFiles/ShareFile/SelectTime.tsx | 42 +-- .../pages/myFiles/ShareFile/ShareDialog.tsx | 354 ++++++++++-------- 7 files changed, 285 insertions(+), 208 deletions(-) diff --git a/web/src/core/usecases/fileExplorer/selectors.ts b/web/src/core/usecases/fileExplorer/selectors.ts index 5fd2a65bb..a7e1d038a 100644 --- a/web/src/core/usecases/fileExplorer/selectors.ts +++ b/web/src/core/usecases/fileExplorer/selectors.ts @@ -350,6 +350,6 @@ const isFileExplorerEnabled = (rootState: RootState) => { const directoryPath = createSelector(state, state => state.directoryPath); -export const protectedSelectors = { workingDirectoryPath, directoryPath }; +export const protectedSelectors = { workingDirectoryPath, directoryPath, shareView }; export const selectors = { main, isFileExplorerEnabled }; diff --git a/web/src/core/usecases/fileExplorer/state.ts b/web/src/core/usecases/fileExplorer/state.ts index 9752ff267..8cea8dc3e 100644 --- a/web/src/core/usecases/fileExplorer/state.ts +++ b/web/src/core/usecases/fileExplorer/state.ts @@ -368,14 +368,28 @@ export const { reducer, actions } = createUsecaseActions({ shareClosed: state => { state.share = undefined; }, - requestSignedUrlStarted: ( + shareSelectedValidityDurationChanged: ( state, - { payload }: { payload: { expirationTime: number } } + { + payload + }: { + payload: { + validityDurationSecond: number; + }; + } ) => { - const { expirationTime } = payload; + const { validityDurationSecond } = payload; + + assert(state.share !== undefined); + assert(state.share.validityDurationSecondOptions !== undefined); + assert( + state.share.validityDurationSecondOptions.includes(validityDurationSecond) + ); + state.share.validityDurationSecond = validityDurationSecond; + }, + requestSignedUrlStarted: state => { assert(state.share !== undefined); state.share.isSignedUrlBeingRequested = true; - state.share.validityDurationSecond = expirationTime; }, requestSignedUrlCompleted: ( state, diff --git a/web/src/core/usecases/fileExplorer/thunks.ts b/web/src/core/usecases/fileExplorer/thunks.ts index d9c485e64..b7c0ae267 100644 --- a/web/src/core/usecases/fileExplorer/thunks.ts +++ b/web/src/core/usecases/fileExplorer/thunks.ts @@ -809,24 +809,34 @@ export const thunks = { dispatch(actions.shareClosed()); }, + changeShareSelectedValidityDuration: + (params: { validityDurationSecond: number }) => + (...args) => { + const { validityDurationSecond } = params; + + const [dispatch] = args; + + dispatch( + actions.shareSelectedValidityDurationChanged({ validityDurationSecond }) + ); + }, requestShareSignedUrl: - (params: { expirationTime: number }) => + () => async (...args) => { - const { expirationTime } = params; const [dispatch, getState] = args; - const state = getState()[name]; - { - assert(state.share !== undefined); - assert(state.share.url === undefined); - } + const shareView = protectedSelectors.shareView(getState()); + + assert(shareView !== null); + assert(shareView !== undefined); + assert(!shareView.isPublic); - dispatch(actions.requestSignedUrlStarted({ expirationTime })); + dispatch(actions.requestSignedUrlStarted()); const url = await dispatch( thunks.getFileDownloadUrl({ - basename: state.share.fileBasename, - validityDurationSecond: expirationTime + basename: shareView.file.basename, + validityDurationSecond: shareView.validityDurationSecond }) ); diff --git a/web/src/ui/pages/myFiles/Explorer/Explorer.tsx b/web/src/ui/pages/myFiles/Explorer/Explorer.tsx index b13dc8fae..69635a72b 100644 --- a/web/src/ui/pages/myFiles/Explorer/Explorer.tsx +++ b/web/src/ui/pages/myFiles/Explorer/Explorer.tsx @@ -44,8 +44,8 @@ import { import type { Item } from "../shared/types"; import { ViewMode } from "../shared/types"; import { isDirectory } from "../shared/tools"; -import { ShareDialog, ShareDialogProps } from "../ShareFile/ShareDialog"; -import { ShareView } from "core/usecases/fileExplorer"; +import { ShareDialog } from "../ShareFile/ShareDialog"; +import type { ShareView } from "core/usecases/fileExplorer"; export type ExplorerProps = { /** @@ -71,15 +71,19 @@ export type ExplorerProps = { onRefresh: () => void; onDeleteItem: (params: { item: Item }) => void; onDeleteItems: (params: { items: Item[] }) => void; - onShareFileOpen: (params: { fileBasename: string }) => void; - onShareFileClose: () => void; - requestSignedUrl: (params: { expirationTime: number }) => void; onCreateDirectory: (params: { basename: string }) => void; onCopyPath: (params: { path: string }) => void; scrollableDivRef: RefObject; pathMinDepth: number; onOpenFile: (params: { basename: string }) => void; - shareState: ShareView | undefined; //To modify + + shareView: ShareView | undefined; + onShareFileOpen: (params: { fileBasename: string }) => void; + onShareFileClose: () => void; + onShareRequestSignedUrl: () => void; + onChangeShareSelectedValidityDuration: (params: { + validityDurationSecond: number; + }) => void; } & Pick; //NOTE: TODO only defined when explorer type is s3 export const Explorer = memo((props: ExplorerProps) => { @@ -104,10 +108,12 @@ export const Explorer = memo((props: ExplorerProps) => { pathMinDepth, onViewModeChange, viewMode, + + shareView, onShareFileOpen, onShareFileClose, - shareState, - requestSignedUrl + onShareRequestSignedUrl, + onChangeShareSelectedValidityDuration } = props; const [items] = useMemo( @@ -327,11 +333,6 @@ export const Explorer = memo((props: ExplorerProps) => { const onShareDialogClose = useConstCallback(() => onShareFileClose()); - const onRequestSignedUrl = useConstCallback( - async ({ expirationTime }: Param0) => { - requestSignedUrl({ expirationTime }); - } - ); return ( <>
{ } /> - {/* @ts-expect-error */}
); diff --git a/web/src/ui/pages/myFiles/ShareFile/SelectTime.tsx b/web/src/ui/pages/myFiles/ShareFile/SelectTime.tsx index 57408778d..2a6dae1f4 100644 --- a/web/src/ui/pages/myFiles/ShareFile/SelectTime.tsx +++ b/web/src/ui/pages/myFiles/ShareFile/SelectTime.tsx @@ -2,54 +2,52 @@ import { useId } from "react"; import InputLabel from "@mui/material/InputLabel"; import MenuItem from "@mui/material/MenuItem"; import FormControl from "@mui/material/FormControl"; -import Select, { SelectChangeEvent } from "@mui/material/Select"; +import Select from "@mui/material/Select"; import { tss } from "tss"; -import { assert } from "tsafe/assert"; type Props = { className?: string; validityDurationSecondOptions: number[]; - expirationValue: number; - onExpirationValueChange: ( - value: Props["validityDurationSecondOptions"][number] - ) => void; + validityDurationSecond: number; + onChangeShareSelectedValidityDuration: (props: { + validityDurationSecond: number; + }) => void; }; export function SelectTime(props: Props) { const { className, validityDurationSecondOptions, - expirationValue, - onExpirationValueChange + onChangeShareSelectedValidityDuration, + validityDurationSecond } = props; const labelId = useId(); const { classes, cx } = useStyles(); - const handleChange = (event: SelectChangeEvent) => { - const newValue = event.target.value; - - assert( - typeof newValue === "number" && - validityDurationSecondOptions.includes(newValue) - ); - onExpirationValueChange(newValue); - }; - return ( Durée de validité - + {validityDurationSecondOptions.map(validityDurationSecond => ( - {/* TODO : better format and translation */} - {validityDurationSecond} secondes + {formatDuration({ durationSeconds: validityDurationSecond })} ))} @@ -61,3 +63,8 @@ const useStyles = tss.withName({ MyFilesShareSelectTime: SelectTime }).create(() minWidth: 200 } })); + +const { i18n } = declareComponentKeys<"validity duration label">()({ + SelectTime +}); +export type I18n = typeof i18n; diff --git a/web/src/ui/pages/myFiles/ShareFile/ShareDialog.stories.tsx b/web/src/ui/pages/myFiles/ShareFile/ShareDialog.stories.tsx index 5dc1489c3..5b64aa96f 100644 --- a/web/src/ui/pages/myFiles/ShareFile/ShareDialog.stories.tsx +++ b/web/src/ui/pages/myFiles/ShareFile/ShareDialog.stories.tsx @@ -14,83 +14,87 @@ type Story = StoryObj; export const Public: Story = { args: { - onRequestUrl: action("onRequestUrl"), - isOpen: true, - onClose: action("onClose"), - isPublic: true, - file: { - kind: "file", - policy: "public", - basename: "photo.png", - size: 2048000, // en bytes - lastModified: new Date("2023-09-15"), - isBeingDeleted: false, - isPolicyChanging: false, - isBeingCreated: false + shareView: { + isPublic: true, + file: { + kind: "file", + policy: "public", + basename: "photo.png", + size: 2048000, // in bytes + lastModified: new Date("2023-09-15") + }, + url: "https://minio.lab.sspcloud.fr/onyxia-datalab/public/photo.png" }, - url: "https://minio.lab.sspcloud.fr/onyxia-datalab/public/photo.png" + onClose: action("onClose"), + onRequestUrl: action("onRequestUrl"), + onChangeShareSelectedValidityDuration: action( + "onChangeShareSelectedValidityDuration" + ) } }; -export const Private = { +export const Private: Story = { args: { - onRequestUrl: action("onRequestUrl"), - isOpen: true, - isPublic: false, - onClose: action("onClose"), - file: { - policy: "private", - kind: "file", - basename: "photo.png", - size: 2048000, // in bytes - lastModified: new Date("2023-09-15"), - isBeingDeleted: false, - isPolicyChanging: false, - isBeingCreated: false + shareView: { + isPublic: false, + file: { + kind: "file", + policy: "private", + basename: "photo.png", + size: 2048000, + lastModified: new Date("2023-09-15") + }, + url: undefined, + validityDurationSecondOptions: [3600, 7200, 10800], + validityDurationSecond: 3600, + isSignedUrlBeingRequested: false }, - url: undefined, - isRequestingUrl: false, - validityDurationSecondOptions: [3600, 7200, 10800], - validityDurationSecond: 3600 + onClose: action("onClose"), + onRequestUrl: action("onRequestUrl"), + onChangeShareSelectedValidityDuration: action( + "onChangeShareSelectedValidityDuration" + ) }, render: () => { const [url, setUrl] = useState(undefined); const [isRequestingUrl, setIsRequestingUrl] = useState(false); + const [validityDurationSecond, setValidityDurationSecond] = useState(3600); - const handleRequestUrl = (params: { expirationTime: number }) => { - action(`onRequestUrl ${params.expirationTime}`)(); - setIsRequestingUrl(true); // Simulate loading + const handleRequestUrl = () => { + action("onRequestUrl")(); + setIsRequestingUrl(true); setTimeout(() => { - // Generate a dynamic URL after a 2-second delay - const generatedUrl = `https://example.com/file/photo.png?expires=${params.expirationTime}`; + const generatedUrl = `https://example.com/file/photo.png?expires=${validityDurationSecond}`; setUrl(generatedUrl); - setIsRequestingUrl(false); // Stop loading + setIsRequestingUrl(false); }, 2000); }; return ( { - action("onClose")(); + shareView={{ + isPublic: false, + file: { + kind: "file", + policy: "private", + basename: "photo.png", + size: 2048000, + lastModified: new Date("2023-09-15") + }, + url: url, + validityDurationSecondOptions: [3600, 7200, 10800], + validityDurationSecond: validityDurationSecond, + isSignedUrlBeingRequested: isRequestingUrl }} - file={{ - policy: "private", - kind: "file", - basename: "photo.png", - size: 2048000, // in bytes - lastModified: new Date("2023-09-15"), - isBeingDeleted: false, - isPolicyChanging: false, - isBeingCreated: false - }} - url={url} - isRequestingUrl={isRequestingUrl} + onClose={action("onClose")} onRequestUrl={handleRequestUrl} - validityDurationSecondOptions={[3600, 7200, 10800]} - validityDurationSecond={3600} + onChangeShareSelectedValidityDuration={({ validityDurationSecond }) => { + action("onChangeShareSelectedValidityDuration")({ + validityDurationSecond + }); + setValidityDurationSecond(validityDurationSecond); + }} /> ); } diff --git a/web/src/ui/pages/myFiles/ShareFile/ShareDialog.tsx b/web/src/ui/pages/myFiles/ShareFile/ShareDialog.tsx index 891520947..aa37de681 100644 --- a/web/src/ui/pages/myFiles/ShareFile/ShareDialog.tsx +++ b/web/src/ui/pages/myFiles/ShareFile/ShareDialog.tsx @@ -12,6 +12,7 @@ import TextField from "@mui/material/TextField"; import { CopyToClipboardIconButton } from "ui/shared/CopyToClipboardIconButton"; import { CircularProgress } from "onyxia-ui/CircularProgress"; import type { ShareView } from "core/usecases/fileExplorer"; +import { formatDuration } from "ui/shared/formattedDate"; export type Props = { shareView: ShareView | undefined; @@ -118,7 +119,7 @@ const BodyPublic = memo( }} helperText={t("hint link access", { isPublic: true, - expiration: undefined // TODO improve + expiration: undefined })} variant="standard" value={shareView.url} @@ -213,7 +214,9 @@ const BodyPrivate = memo( }} helperText={t("hint link access", { isPublic: false, - expiration: undefined // TODO improve + expiration: formatDuration({ + durationSeconds: shareView.validityDurationSecond + }) })} variant="standard" value={shareView.url} diff --git a/web/src/ui/shared/formattedDate/dateTimeFormatter.ts b/web/src/ui/shared/formattedDate/dateTimeFormatter.ts new file mode 100644 index 000000000..20f3f01c3 --- /dev/null +++ b/web/src/ui/shared/formattedDate/dateTimeFormatter.ts @@ -0,0 +1,177 @@ +import { getTranslation } from "ui/i18n"; +import { durationDivisorKeys, fromNowDivisorKeys } from "./type"; +import { assert } from "tsafe/assert"; + +export const { fromNow } = (() => { + const { getFromNowUnits } = (() => { + type FromNowUnit = { + max: number; + divisor: number; + past1: string; + pastN: string; + future1: string; + futureN: string; + }; + + const SECOND = 1000; + const MINUTE = 60 * SECOND; + const HOUR = 60 * MINUTE; + const DAY = 24 * HOUR; + const WEEK = 7 * DAY; + const MONTH = 30 * DAY; + const YEAR = 365 * DAY; + + function getFromNowUnits(): FromNowUnit[] { + const { t } = getTranslation("formattedDate"); + + return fromNowDivisorKeys.map(divisorKey => ({ + divisor: (() => { + switch (divisorKey) { + case "now": + return 1; + case "second": + return SECOND; + case "minute": + return MINUTE; + case "hour": + return HOUR; + case "day": + return DAY; + case "week": + return WEEK; + case "month": + return MONTH; + case "year": + return YEAR; + } + })(), + max: (() => { + switch (divisorKey) { + case "now": + return 4 * SECOND; + case "second": + return MINUTE; + case "minute": + return HOUR; + case "hour": + return DAY; + case "day": + return WEEK; + case "week": + return MONTH; + case "month": + return YEAR; + case "year": + return Infinity; + } + })(), + past1: t("past1", { divisorKey }), + pastN: t("pastN", { divisorKey }), + future1: t("future1", { divisorKey }), + futureN: t("futureN", { divisorKey }) + })); + } + + return { getFromNowUnits }; + })(); + + function fromNow(params: { dateTime: number }): string { + const { dateTime } = params; + + const diff = Date.now() - dateTime; + const diffAbs = Math.abs(diff); + for (const unit of getFromNowUnits()) { + if (diffAbs < unit.max) { + const isFuture = diff < 0; + const x = Math.round(Math.abs(diff) / unit.divisor); + if (x <= 1) return isFuture ? unit.future1 : unit.past1; + return (isFuture ? unit.futureN : unit.pastN).replace("#", `${x}`); + } + } + assert(false); + } + return { fromNow }; +})(); + +export const { formatDuration } = (() => { + const { getDurationUnits } = (() => { + type DurationUnit = { + max: number; + divisor: number; + singular: string; + plural: string; + }; + const SECOND = 1000; + const MINUTE = 60 * SECOND; + const HOUR = 60 * MINUTE; + const DAY = 24 * HOUR; + const WEEK = 7 * DAY; + const MONTH = 30 * DAY; + const YEAR = 365 * DAY; + + function getDurationUnits(): DurationUnit[] { + const { t } = getTranslation("formattedDate"); + + return durationDivisorKeys.map(divisorKey => ({ + divisor: (() => { + switch (divisorKey) { + case "second": + return SECOND; + case "minute": + return MINUTE; + case "hour": + return HOUR; + case "day": + return DAY; + case "week": + return WEEK; + case "month": + return MONTH; + case "year": + return YEAR; + default: + throw new Error(`Unhandled divisorKey: ${divisorKey}`); + } + })(), + max: (() => { + switch (divisorKey) { + case "second": + return MINUTE; + case "minute": + return HOUR; + case "hour": + return 3 * DAY; + case "day": + return WEEK + DAY; + case "week": + return MONTH; + case "month": + return YEAR; + case "year": + return Infinity; + default: + throw new Error(`Unhandled divisorKey: ${divisorKey}`); + } + })(), + singular: t("singular", { divisorKey }), + plural: t("plural", { divisorKey }) + })); + } + + return { getDurationUnits }; + })(); + + function formatDuration(params: { durationSeconds: number }): string { + const { durationSeconds } = params; + + for (const unit of getDurationUnits()) { + if (durationSeconds * 1000 < unit.max) { + const x = Math.round(durationSeconds / (unit.divisor / 1000)); + return x === 1 ? unit.singular : unit.plural.replace("#", `${x}`); + } + } + assert(false); + } + + return { formatDuration }; +})(); diff --git a/web/src/ui/shared/formattedDate/index.ts b/web/src/ui/shared/formattedDate/index.ts index 5b162b1a3..b360bde05 100644 --- a/web/src/ui/shared/formattedDate/index.ts +++ b/web/src/ui/shared/formattedDate/index.ts @@ -1,2 +1,3 @@ -export { fromNow, useFormattedDate, useFromNow } from "./useFormattedDate"; +export { useFormattedDate, useFromNow } from "./useFormattedDate"; export { getFormattedDate } from "./getFormattedDate"; +export { fromNow, formatDuration } from "./dateTimeFormatter"; diff --git a/web/src/ui/shared/formattedDate/type.ts b/web/src/ui/shared/formattedDate/type.ts new file mode 100644 index 000000000..fe8f4154f --- /dev/null +++ b/web/src/ui/shared/formattedDate/type.ts @@ -0,0 +1,43 @@ +import { declareComponentKeys } from "i18nifty"; + +export const durationDivisorKeys = [ + "second", + "minute", + "hour", + "day", + "week", + "month", + "year" +] as const; +export type DurationDivisorKey = (typeof durationDivisorKeys)[number]; + +export const fromNowDivisorKeys = [...durationDivisorKeys, "now"] as const; +export type FromNowDivisorKey = (typeof fromNowDivisorKeys)[number]; + +const { i18n } = declareComponentKeys< + | { + K: "past1"; + P: { divisorKey: FromNowDivisorKey }; + } + | { + K: "pastN"; + P: { divisorKey: FromNowDivisorKey }; + } + | { + K: "future1"; + P: { divisorKey: FromNowDivisorKey }; + } + | { + K: "futureN"; + P: { divisorKey: FromNowDivisorKey }; + } + | { + K: "singular"; + P: { divisorKey: DurationDivisorKey }; + } + | { + K: "plural"; + P: { divisorKey: DurationDivisorKey }; + } +>()("formattedDate"); +export type I18n = typeof i18n; diff --git a/web/src/ui/shared/formattedDate/useFormattedDate.ts b/web/src/ui/shared/formattedDate/useFormattedDate.ts index b7b738868..4d376dd33 100644 --- a/web/src/ui/shared/formattedDate/useFormattedDate.ts +++ b/web/src/ui/shared/formattedDate/useFormattedDate.ts @@ -1,8 +1,7 @@ import { useMemo, useEffect, useReducer } from "react"; -import { useLang, getTranslation } from "ui/i18n"; -import { assert } from "tsafe/assert"; -import { declareComponentKeys } from "i18nifty"; +import { useLang } from "ui/i18n"; import { getFormattedDate } from "./getFormattedDate"; +import { fromNow } from "./dateTimeFormatter"; export function useFormattedDate(params: { time: number }): string { const { time } = params; @@ -13,98 +12,6 @@ export function useFormattedDate(params: { time: number }): string { return useMemo(() => getFormattedDate({ time, lang }), [time, lang]); } -export const { fromNow } = (() => { - const { getUnits } = (() => { - type Unit = { - max: number; - divisor: number; - past1: string; - pastN: string; - future1: string; - futureN: string; - }; - - const SECOND = 1000; - const MINUTE = 60 * SECOND; - const HOUR = 60 * MINUTE; - const DAY = 24 * HOUR; - const WEEK = 7 * DAY; - const MONTH = 30 * DAY; - const YEAR = 365 * DAY; - - function getUnits(): Unit[] { - const { t } = getTranslation("formattedDate"); - - return divisorKeys.map(divisorKey => ({ - divisor: (() => { - switch (divisorKey) { - case "now": - return 1; - case "second": - return SECOND; - case "minute": - return MINUTE; - case "hour": - return HOUR; - case "day": - return DAY; - case "week": - return WEEK; - case "month": - return MONTH; - case "year": - return YEAR; - } - })(), - max: (() => { - switch (divisorKey) { - case "now": - return 4 * SECOND; - case "second": - return MINUTE; - case "minute": - return HOUR; - case "hour": - return DAY; - case "day": - return WEEK; - case "week": - return MONTH; - case "month": - return YEAR; - case "year": - return Infinity; - } - })(), - past1: t("past1", { divisorKey }), - pastN: t("pastN", { divisorKey }), - future1: t("future1", { divisorKey }), - futureN: t("futureN", { divisorKey }) - })); - } - - return { getUnits }; - })(); - - function fromNow(params: { dateTime: number }): string { - const { dateTime } = params; - - const diff = Date.now() - dateTime; - const diffAbs = Math.abs(diff); - for (const unit of getUnits()) { - if (diffAbs < unit.max) { - const isFuture = diff < 0; - const x = Math.round(Math.abs(diff) / unit.divisor); - if (x <= 1) return isFuture ? unit.future1 : unit.past1; - return (isFuture ? unit.futureN : unit.pastN).replace("#", `${x}`); - } - } - assert(false); - } - - return { fromNow }; -})(); - export function useFromNow(params: { dateTime: number }) { const { dateTime } = params; @@ -128,35 +35,3 @@ export function useFromNow(params: { dateTime: number }) { return { fromNowText }; } - -const divisorKeys = [ - "now", - "second", - "minute", - "hour", - "day", - "week", - "month", - "year" -] as const; -type DivisorKey = (typeof divisorKeys)[number]; - -const { i18n } = declareComponentKeys< - | { - K: "past1"; - P: { divisorKey: DivisorKey }; - } - | { - K: "pastN"; - P: { divisorKey: DivisorKey }; - } - | { - K: "future1"; - P: { divisorKey: DivisorKey }; - } - | { - K: "futureN"; - P: { divisorKey: DivisorKey }; - } ->()("formattedDate"); -export type I18n = typeof i18n; From f27bde76e828655f2964a8198c446e89a5b3b36b Mon Sep 17 00:00:00 2001 From: Dylan Decrulle Date: Wed, 27 Nov 2024 12:34:17 +0100 Subject: [PATCH 14/15] remove default --- web/src/ui/shared/formattedDate/dateTimeFormatter.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/web/src/ui/shared/formattedDate/dateTimeFormatter.ts b/web/src/ui/shared/formattedDate/dateTimeFormatter.ts index 20f3f01c3..7ddc19ccd 100644 --- a/web/src/ui/shared/formattedDate/dateTimeFormatter.ts +++ b/web/src/ui/shared/formattedDate/dateTimeFormatter.ts @@ -129,8 +129,6 @@ export const { formatDuration } = (() => { return MONTH; case "year": return YEAR; - default: - throw new Error(`Unhandled divisorKey: ${divisorKey}`); } })(), max: (() => { @@ -149,8 +147,6 @@ export const { formatDuration } = (() => { return YEAR; case "year": return Infinity; - default: - throw new Error(`Unhandled divisorKey: ${divisorKey}`); } })(), singular: t("singular", { divisorKey }), From c9a48eb1abcad3d950718a8c9114639e00216a58 Mon Sep 17 00:00:00 2001 From: Dylan Decrulle Date: Thu, 28 Nov 2024 13:46:33 +0100 Subject: [PATCH 15/15] add pretty display in command bar --- web/src/core/tools/timeFormat/constants.ts | 21 +++ .../core/tools/timeFormat/formatDuration.ts | 100 ++++++++++++++ web/src/core/tools/timeFormat/type.ts | 8 ++ web/src/core/usecases/fileExplorer/thunks.ts | 13 +- .../shared/formattedDate/dateTimeFormatter.ts | 123 ++++-------------- web/src/ui/shared/formattedDate/type.ts | 5 + 6 files changed, 166 insertions(+), 104 deletions(-) create mode 100644 web/src/core/tools/timeFormat/constants.ts create mode 100644 web/src/core/tools/timeFormat/formatDuration.ts create mode 100644 web/src/core/tools/timeFormat/type.ts diff --git a/web/src/core/tools/timeFormat/constants.ts b/web/src/core/tools/timeFormat/constants.ts new file mode 100644 index 000000000..eddb27a20 --- /dev/null +++ b/web/src/core/tools/timeFormat/constants.ts @@ -0,0 +1,21 @@ +const SECOND = 1000; + +export const TIME_UNITS = { + SECOND, + MINUTE: 60 * SECOND, + HOUR: 60 * 60 * SECOND, + DAY: 24 * 60 * 60 * SECOND, + WEEK: 7 * 24 * 60 * 60 * SECOND, + MONTH: 30 * 24 * 60 * 60 * SECOND, + YEAR: 365 * 24 * 60 * 60 * SECOND +}; + +export const DURATION_DIVISOR_KEYS = [ + "second", + "minute", + "hour", + "day", + "week", + "month", + "year" +] as const; diff --git a/web/src/core/tools/timeFormat/formatDuration.ts b/web/src/core/tools/timeFormat/formatDuration.ts new file mode 100644 index 000000000..878d3fd44 --- /dev/null +++ b/web/src/core/tools/timeFormat/formatDuration.ts @@ -0,0 +1,100 @@ +import { assert } from "tsafe/assert"; +import { DurationTranslationFunction } from "./type"; +import { TIME_UNITS, DURATION_DIVISOR_KEYS } from "./constants"; + +export const { formatDuration } = (() => { + const { getDurationUnits } = (() => { + type DurationUnit = { + max: number; + divisor: number; + singular: string; + plural: string; + }; + + function getDurationUnits(t: DurationTranslationFunction): DurationUnit[] { + return DURATION_DIVISOR_KEYS.map(divisorKey => ({ + divisor: (() => { + switch (divisorKey) { + case "second": + return TIME_UNITS.SECOND; + case "minute": + return TIME_UNITS.MINUTE; + case "hour": + return TIME_UNITS.HOUR; + case "day": + return TIME_UNITS.DAY; + case "week": + return TIME_UNITS.WEEK; + case "month": + return TIME_UNITS.MONTH; + case "year": + return TIME_UNITS.YEAR; + } + })(), + max: (() => { + switch (divisorKey) { + case "second": + return TIME_UNITS.MINUTE; + case "minute": + return TIME_UNITS.HOUR; + case "hour": + return 3 * TIME_UNITS.DAY; + case "day": + return TIME_UNITS.WEEK + TIME_UNITS.DAY; + case "week": + return TIME_UNITS.MONTH; + case "month": + return TIME_UNITS.YEAR; + case "year": + return Infinity; + } + })(), + singular: t("singular", { divisorKey }), + plural: t("plural", { divisorKey }) + })); + } + + return { getDurationUnits }; + })(); + + function formatDuration(params: { + durationSeconds: number; + t: DurationTranslationFunction; + }): string { + const { durationSeconds, t } = params; + + for (const unit of getDurationUnits(t)) { + if (durationSeconds * 1000 < unit.max) { + const x = Math.round(durationSeconds / (unit.divisor / 1000)); + return x === 1 ? unit.singular : unit.plural.replace("#", `${x}`); + } + } + assert(false); + } + + return { formatDuration }; +})(); + +export const englishDurationFormatter: DurationTranslationFunction = (key, params) => { + const en = { + singular: { + second: "1 second", + minute: "1 minute", + hour: "1 hour", + day: "1 day", + week: "1 week", + month: "1 month", + year: "1 year" + }, + plural: { + second: "# seconds", + minute: "# minutes", + hour: "# hours", + day: "# days", + week: "# weeks", + month: "# months", + year: "# years" + } + }; + return en[key][params.divisorKey]; +}; diff --git a/web/src/core/tools/timeFormat/type.ts b/web/src/core/tools/timeFormat/type.ts new file mode 100644 index 000000000..63844441f --- /dev/null +++ b/web/src/core/tools/timeFormat/type.ts @@ -0,0 +1,8 @@ +import { DURATION_DIVISOR_KEYS } from "./constants"; + +export type DurationDivisorKey = (typeof DURATION_DIVISOR_KEYS)[number]; + +export type DurationTranslationFunction = ( + key: "singular" | "plural", + params: { divisorKey: DurationDivisorKey } +) => string; diff --git a/web/src/core/usecases/fileExplorer/thunks.ts b/web/src/core/usecases/fileExplorer/thunks.ts index b7c0ae267..7695b6fbc 100644 --- a/web/src/core/usecases/fileExplorer/thunks.ts +++ b/web/src/core/usecases/fileExplorer/thunks.ts @@ -7,6 +7,10 @@ import { join as pathJoin, basename as pathBasename } from "pathe"; import { crawlFactory } from "core/tools/crawl"; import * as s3ConfigManagement from "core/usecases/s3ConfigManagement"; import { S3Object } from "core/ports/S3Client"; +import { + formatDuration, + englishDurationFormatter +} from "core/tools/timeFormat/formatDuration"; export type ExplorersCreateParams = | ExplorersCreateParams.Directory @@ -708,10 +712,14 @@ export const thunks = { const cmdId = Date.now(); + const prettyDurationValue = formatDuration({ + durationSeconds: validityDurationSecond, + t: englishDurationFormatter + }); dispatch( actions.commandLogIssued({ cmdId, - cmd: `mc share download --expire ${validityDurationSecond}s ${pathJoin("s3", path)}` + cmd: `mc share download --expire ${prettyDurationValue} ${pathJoin("s3", path)}` }) ); @@ -732,8 +740,7 @@ export const thunks = { cmdId, resp: [ `URL: ${downloadUrl.split("?")[0]}`, - // TODO: Pretty print - `Expire: ${validityDurationSecond} seconds`, + `Expire: ${prettyDurationValue}`, `Share: ${downloadUrl}` ].join("\n") }) diff --git a/web/src/ui/shared/formattedDate/dateTimeFormatter.ts b/web/src/ui/shared/formattedDate/dateTimeFormatter.ts index 7ddc19ccd..1c433e9d9 100644 --- a/web/src/ui/shared/formattedDate/dateTimeFormatter.ts +++ b/web/src/ui/shared/formattedDate/dateTimeFormatter.ts @@ -1,5 +1,7 @@ import { getTranslation } from "ui/i18n"; -import { durationDivisorKeys, fromNowDivisorKeys } from "./type"; +import { fromNowDivisorKeys } from "./type"; +import { formatDuration as coreFormatDuration } from "core/tools/timeFormat/formatDuration"; +import { TIME_UNITS } from "core/tools/timeFormat/constants"; import { assert } from "tsafe/assert"; export const { fromNow } = (() => { @@ -13,14 +15,6 @@ export const { fromNow } = (() => { futureN: string; }; - const SECOND = 1000; - const MINUTE = 60 * SECOND; - const HOUR = 60 * MINUTE; - const DAY = 24 * HOUR; - const WEEK = 7 * DAY; - const MONTH = 30 * DAY; - const YEAR = 365 * DAY; - function getFromNowUnits(): FromNowUnit[] { const { t } = getTranslation("formattedDate"); @@ -30,37 +24,37 @@ export const { fromNow } = (() => { case "now": return 1; case "second": - return SECOND; + return TIME_UNITS.SECOND; case "minute": - return MINUTE; + return TIME_UNITS.MINUTE; case "hour": - return HOUR; + return TIME_UNITS.HOUR; case "day": - return DAY; + return TIME_UNITS.DAY; case "week": - return WEEK; + return TIME_UNITS.WEEK; case "month": - return MONTH; + return TIME_UNITS.MONTH; case "year": - return YEAR; + return TIME_UNITS.YEAR; } })(), max: (() => { switch (divisorKey) { case "now": - return 4 * SECOND; + return 4 * TIME_UNITS.SECOND; case "second": - return MINUTE; + return TIME_UNITS.MINUTE; case "minute": - return HOUR; + return TIME_UNITS.HOUR; case "hour": - return DAY; + return TIME_UNITS.DAY; case "day": - return WEEK; + return TIME_UNITS.WEEK; case "week": - return MONTH; + return TIME_UNITS.MONTH; case "month": - return YEAR; + return TIME_UNITS.YEAR; case "year": return Infinity; } @@ -93,81 +87,8 @@ export const { fromNow } = (() => { return { fromNow }; })(); -export const { formatDuration } = (() => { - const { getDurationUnits } = (() => { - type DurationUnit = { - max: number; - divisor: number; - singular: string; - plural: string; - }; - const SECOND = 1000; - const MINUTE = 60 * SECOND; - const HOUR = 60 * MINUTE; - const DAY = 24 * HOUR; - const WEEK = 7 * DAY; - const MONTH = 30 * DAY; - const YEAR = 365 * DAY; - - function getDurationUnits(): DurationUnit[] { - const { t } = getTranslation("formattedDate"); - - return durationDivisorKeys.map(divisorKey => ({ - divisor: (() => { - switch (divisorKey) { - case "second": - return SECOND; - case "minute": - return MINUTE; - case "hour": - return HOUR; - case "day": - return DAY; - case "week": - return WEEK; - case "month": - return MONTH; - case "year": - return YEAR; - } - })(), - max: (() => { - switch (divisorKey) { - case "second": - return MINUTE; - case "minute": - return HOUR; - case "hour": - return 3 * DAY; - case "day": - return WEEK + DAY; - case "week": - return MONTH; - case "month": - return YEAR; - case "year": - return Infinity; - } - })(), - singular: t("singular", { divisorKey }), - plural: t("plural", { divisorKey }) - })); - } - - return { getDurationUnits }; - })(); - - function formatDuration(params: { durationSeconds: number }): string { - const { durationSeconds } = params; - - for (const unit of getDurationUnits()) { - if (durationSeconds * 1000 < unit.max) { - const x = Math.round(durationSeconds / (unit.divisor / 1000)); - return x === 1 ? unit.singular : unit.plural.replace("#", `${x}`); - } - } - assert(false); - } - - return { formatDuration }; -})(); +export const formatDuration = (params: { durationSeconds: number }) => { + const { t } = getTranslation("formattedDate"); + const { durationSeconds } = params; + return coreFormatDuration({ durationSeconds, t }); +}; diff --git a/web/src/ui/shared/formattedDate/type.ts b/web/src/ui/shared/formattedDate/type.ts index fe8f4154f..3d42d22ac 100644 --- a/web/src/ui/shared/formattedDate/type.ts +++ b/web/src/ui/shared/formattedDate/type.ts @@ -11,6 +11,11 @@ export const durationDivisorKeys = [ ] as const; export type DurationDivisorKey = (typeof durationDivisorKeys)[number]; +export type DurationTranslationFunction = ( + key: "singular" | "plural", + params: { divisorKey: DurationDivisorKey } +) => string; + export const fromNowDivisorKeys = [...durationDivisorKeys, "now"] as const; export type FromNowDivisorKey = (typeof fromNowDivisorKeys)[number];