[web] Deduce parent directory (#1663)

This commit is contained in:
Manav Rathi 2024-05-09 11:03:29 +05:30 committed by GitHub
commit 70878e8f54
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 272 additions and 198 deletions

View file

@ -37,7 +37,7 @@
"photoswipe": "file:./thirdparty/photoswipe",
"piexifjs": "^1.0.6",
"pure-react-carousel": "^1.30.1",
"react-dropzone": "^11.2.4",
"react-dropzone": "^14.2",
"react-otp-input": "^2.3.1",
"react-select": "^4.3.1",
"react-top-loading-bar": "^2.0.1",

View file

@ -1,6 +1,8 @@
import { basename } from "@/next/file";
import log from "@/next/log";
import type { CollectionMapping, Electron, ZipItem } from "@/next/types/ipc";
import { firstNonEmpty } from "@/utils/array";
import { ensure } from "@/utils/ensure";
import { CustomError } from "@ente/shared/error";
import { isPromise } from "@ente/shared/utils";
import DiscFullIcon from "@mui/icons-material/DiscFull";
@ -324,17 +326,17 @@ export default function Uploader({
// Trigger an upload when any of the dependencies change.
useEffect(() => {
// Re the paths:
// About the paths:
//
// - These are not necessarily the full paths. In particular, when
// running on the browser they'll be the relative paths (at best) or
// just the file-name otherwise.
//
// - All the paths use POSIX separators. See inline comments.
//
const allItemAndPaths = [
// See: [Note: webkitRelativePath]. In particular, they use POSIX
// separators.
webFiles.map((f) => [f, f.webkitRelativePath ?? f.name]),
// Relative path (using POSIX separators) or the file's name.
webFiles.map((f) => [f, pathLikeForWebFile(f)]),
// The paths we get from the desktop app all eventually come either
// from electron.selectDirectory or electron.pathForFile, both of
// which return POSIX paths.
@ -822,6 +824,37 @@ const desktopFilesAndZipItems = async (electron: Electron, files: File[]) => {
return { fileAndPaths, zipItems };
};
/**
* Return the relative path or name of a File object selected or
* drag-and-dropped on the web.
*
* There are three cases here:
*
* 1. If the user selects individual file(s), then the returned File objects
* will only have a `name`.
*
* 2. If the user selects directory(ies), then the returned File objects will
* have a `webkitRelativePath`. For more details, see [Note:
* webkitRelativePath]. In particular, these will POSIX separators.
*
* 3. If the user drags-and-drops, then the react-dropzone library that we use
* will internally convert `webkitRelativePath` to `path`, but otherwise it
* behaves same as case 2.
* https://github.com/react-dropzone/file-selector/blob/master/src/file.ts#L1214
*/
const pathLikeForWebFile = (file: File): string =>
ensure(
firstNonEmpty([
// We need to check first, since path is not a property of
// the standard File objects.
"path" in file && typeof file.path == "string"
? file.path
: undefined,
file.webkitRelativePath,
file.name,
]),
);
// This is used to prompt the user the make upload strategy choice
interface ImportSuggestion {
rootFolderName: string;

View file

@ -1,9 +1,24 @@
export default function UploadSelectorInputs({
type GetInputProps = () => React.HTMLAttributes<HTMLInputElement>;
interface UploadSelectorInputsProps {
getDragAndDropInputProps: GetInputProps;
getFileSelectorInputProps: GetInputProps;
getFolderSelectorInputProps: GetInputProps;
getZipFileSelectorInputProps?: GetInputProps;
}
/**
* Create a bunch of HTML inputs elements, one each for the given props.
*
* These hidden input element serve as the way for us to show various file /
* folder Selector dialogs and handle drag and drop inputs.
*/
export const UploadSelectorInputs: React.FC<UploadSelectorInputsProps> = ({
getDragAndDropInputProps,
getFileSelectorInputProps,
getFolderSelectorInputProps,
getZipFileSelectorInputProps,
}) {
}) => {
return (
<>
<input {...getDragAndDropInputProps()} />
@ -14,4 +29,4 @@ export default function UploadSelectorInputs({
)}
</>
);
}
};

View file

@ -1,82 +1,36 @@
import {
SESSION_KEYS,
clearKeys,
getKey,
} from "@ente/shared/storage/sessionStorage";
import { Typography, styled } from "@mui/material";
import { t } from "i18next";
import { useRouter } from "next/router";
import {
createContext,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
constructEmailList,
createAlbum,
getAllLatestCollections,
getAllLocalCollections,
getCollectionSummaries,
getFavItemIds,
getHiddenItemsSummary,
getSectionSummaries,
} from "services/collectionService";
import { getLocalFiles, syncFiles } from "services/fileService";
import { checkSubscriptionPurchase } from "utils/billing";
import log from "@/next/log";
import { APPS } from "@ente/shared/apps/constants";
import { CenteredFlex } from "@ente/shared/components/Container";
import EnteSpinner from "@ente/shared/components/EnteSpinner";
import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
import { CustomError } from "@ente/shared/error";
import { useFileInput } from "@ente/shared/hooks/useFileInput";
import useMemoSingleThreaded from "@ente/shared/hooks/useMemoSingleThreaded";
import InMemoryStore, { MS_KEYS } from "@ente/shared/storage/InMemoryStore";
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
import {
getToken,
isFirstLogin,
justSignedUp,
setIsFirstLogin,
setJustSignedUp,
} from "@ente/shared/storage/localStorage/helpers";
import CollectionSelector, {
CollectionSelectorAttributes,
} from "components/Collections/CollectionSelector";
import FullScreenDropZone from "components/FullScreenDropZone";
import { LoadingOverlay } from "components/LoadingOverlay";
import PhotoFrame from "components/PhotoFrame";
import Sidebar from "components/Sidebar";
import SelectedFileOptions from "components/pages/gallery/SelectedFileOptions";
import { useDropzone } from "react-dropzone";
import {
isTokenValid,
syncMapEnabled,
validateKey,
} from "services/userService";
import { preloadImage } from "utils/common";
import {
FILE_OPS_TYPE,
constructFileToCollectionMap,
getSelectedFiles,
getUniqueFiles,
handleFileOps,
mergeMetadata,
sortFiles,
} from "utils/file";
import log from "@/next/log";
import { APPS } from "@ente/shared/apps/constants";
import { CenteredFlex } from "@ente/shared/components/Container";
import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
import { CustomError } from "@ente/shared/error";
import useFileInput from "@ente/shared/hooks/useFileInput";
import useMemoSingleThreaded from "@ente/shared/hooks/useMemoSingleThreaded";
import InMemoryStore, { MS_KEYS } from "@ente/shared/storage/InMemoryStore";
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
import { getToken } from "@ente/shared/storage/localStorage/helpers";
SESSION_KEYS,
clearKeys,
getKey,
} from "@ente/shared/storage/sessionStorage";
import { User } from "@ente/shared/user/types";
import { isPromise } from "@ente/shared/utils";
import { Typography, styled } from "@mui/material";
import AuthenticateUserModal from "components/AuthenticateUserModal";
import Collections from "components/Collections";
import CollectionNamer, {
CollectionNamerAttributes,
} from "components/Collections/CollectionNamer";
import CollectionSelector, {
CollectionSelectorAttributes,
} from "components/Collections/CollectionSelector";
import ExportModal from "components/ExportModal";
import {
FilesDownloadProgress,
@ -85,13 +39,18 @@ import {
import FixCreationTime, {
FixCreationTimeAttributes,
} from "components/FixCreationTime";
import FullScreenDropZone from "components/FullScreenDropZone";
import GalleryEmptyState from "components/GalleryEmptyState";
import { LoadingOverlay } from "components/LoadingOverlay";
import PhotoFrame from "components/PhotoFrame";
import { ITEM_TYPE, TimeStampListItem } from "components/PhotoList";
import SearchResultInfo from "components/Search/SearchResultInfo";
import Sidebar from "components/Sidebar";
import Uploader from "components/Upload/Uploader";
import UploadInputs from "components/UploadSelectorInputs";
import { UploadSelectorInputs } from "components/UploadSelectorInputs";
import { GalleryNavbar } from "components/pages/gallery/Navbar";
import PlanSelector from "components/pages/gallery/PlanSelector";
import SelectedFileOptions from "components/pages/gallery/SelectedFileOptions";
import {
ALL_SECTION,
ARCHIVE_SECTION,
@ -100,15 +59,42 @@ import {
TRASH_SECTION,
} from "constants/collection";
import { SYNC_INTERVAL_IN_MICROSECONDS } from "constants/gallery";
import { t } from "i18next";
import { useRouter } from "next/router";
import { AppContext } from "pages/_app";
import {
createContext,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useDropzone } from "react-dropzone";
import { clipService } from "services/clip-service";
import { constructUserIDToEmailMap } from "services/collectionService";
import {
constructEmailList,
constructUserIDToEmailMap,
createAlbum,
getAllLatestCollections,
getAllLocalCollections,
getCollectionSummaries,
getFavItemIds,
getHiddenItemsSummary,
getSectionSummaries,
} from "services/collectionService";
import downloadManager from "services/download";
import { syncEmbeddings, syncFileEmbeddings } from "services/embeddingService";
import { syncEntities } from "services/entityService";
import { getLocalFiles, syncFiles } from "services/fileService";
import locationSearchService from "services/locationSearchService";
import { getLocalTrashedFiles, syncTrash } from "services/trashService";
import uploadManager from "services/upload/uploadManager";
import {
isTokenValid,
syncMapEnabled,
validateKey,
} from "services/userService";
import { Collection, CollectionSummaries } from "types/collection";
import { EnteFile } from "types/file";
import {
@ -120,6 +106,7 @@ import {
} from "types/gallery";
import { Search, SearchResultSummary, UpdateSearch } from "types/search";
import { FamilyData } from "types/user";
import { checkSubscriptionPurchase } from "utils/billing";
import {
COLLECTION_OPS_TYPE,
constructCollectionNameMap,
@ -131,6 +118,16 @@ import {
splitNormalAndHiddenCollections,
} from "utils/collection";
import ComlinkSearchWorker from "utils/comlink/ComlinkSearchWorker";
import { preloadImage } from "utils/common";
import {
FILE_OPS_TYPE,
constructFileToCollectionMap,
getSelectedFiles,
getUniqueFiles,
handleFileOps,
mergeMetadata,
sortFiles,
} from "utils/file";
import { isArchivedFile } from "utils/magicMetadata";
import { getSessionExpiredMessage } from "utils/ui";
import { getLocalFamilyData } from "utils/user/family";
@ -201,8 +198,11 @@ export default function Gallery() {
const [isPhotoSwipeOpen, setIsPhotoSwipeOpen] = useState(false);
const {
// A function to call to get the props we should apply to the container,
getRootProps: getDragAndDropRootProps,
// ... the props we should apply to the <input> element,
getInputProps: getDragAndDropInputProps,
// ... and the files that we got.
acceptedFiles: dragAndDropFiles,
} = useDropzone({
noClick: true,
@ -210,23 +210,23 @@ export default function Gallery() {
disabled: shouldDisableDropzone,
});
const {
selectedFiles: fileSelectorFiles,
open: openFileSelector,
getInputProps: getFileSelectorInputProps,
openSelector: openFileSelector,
selectedFiles: fileSelectorFiles,
} = useFileInput({
directory: false,
});
const {
selectedFiles: folderSelectorFiles,
open: openFolderSelector,
getInputProps: getFolderSelectorInputProps,
openSelector: openFolderSelector,
selectedFiles: folderSelectorFiles,
} = useFileInput({
directory: true,
});
const {
selectedFiles: fileSelectorZipFiles,
open: openZipFileSelector,
getInputProps: getZipFileSelectorInputProps,
openSelector: openZipFileSelector,
selectedFiles: fileSelectorZipFiles,
} = useFileInput({
directory: false,
accept: ".zip",
@ -1013,14 +1013,14 @@ export default function Gallery() {
setSelectedFiles: setSelected,
}}
>
<FullScreenDropZone
getDragAndDropRootProps={getDragAndDropRootProps}
>
<UploadInputs
getDragAndDropInputProps={getDragAndDropInputProps}
getFileSelectorInputProps={getFileSelectorInputProps}
getFolderSelectorInputProps={getFolderSelectorInputProps}
getZipFileSelectorInputProps={getZipFileSelectorInputProps}
<FullScreenDropZone {...{ getDragAndDropRootProps }}>
<UploadSelectorInputs
{...{
getDragAndDropInputProps,
getFileSelectorInputProps,
getFolderSelectorInputProps,
getZipFileSelectorInputProps,
}}
/>
{blockingLoad && (
<LoadingOverlay>

View file

@ -1,15 +1,51 @@
import log from "@/next/log";
import { logoutUser } from "@ente/accounts/services/user";
import { APPS } from "@ente/shared/apps/constants";
import {
CenteredFlex,
SpaceBetweenFlex,
VerticallyCentered,
} from "@ente/shared/components/Container";
import EnteSpinner from "@ente/shared/components/EnteSpinner";
import FormPaper from "@ente/shared/components/Form/FormPaper";
import FormPaperTitle from "@ente/shared/components/Form/FormPaper/Title";
import OverflowMenu from "@ente/shared/components/OverflowMenu/menu";
import { OverflowMenuOption } from "@ente/shared/components/OverflowMenu/option";
import SingleInputForm, {
SingleInputFormProps,
} from "@ente/shared/components/SingleInputForm";
import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
import { ENTE_WEBSITE_LINK } from "@ente/shared/constants/urls";
import ComlinkCryptoWorker from "@ente/shared/crypto";
import { CustomError, parseSharingErrorCodes } from "@ente/shared/error";
import { useFileInput } from "@ente/shared/hooks/useFileInput";
import AddPhotoAlternateOutlined from "@mui/icons-material/AddPhotoAlternateOutlined";
import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined";
import MoreHoriz from "@mui/icons-material/MoreHoriz";
import Typography from "@mui/material/Typography";
import bs58 from "bs58";
import { CollectionInfo } from "components/Collections/CollectionInfo";
import { CollectionInfoBarWrapper } from "components/Collections/styledComponents";
import {
FilesDownloadProgress,
FilesDownloadProgressAttributes,
} from "components/FilesDownloadProgress";
import FullScreenDropZone from "components/FullScreenDropZone";
import { LoadingOverlay } from "components/LoadingOverlay";
import PhotoFrame from "components/PhotoFrame";
import { ITEM_TYPE, TimeStampListItem } from "components/PhotoList";
import UploadButton from "components/Upload/UploadButton";
import Uploader from "components/Upload/Uploader";
import { UploadSelectorInputs } from "components/UploadSelectorInputs";
import SharedAlbumNavbar from "components/pages/sharedAlbum/Navbar";
import SelectedFileOptions from "components/pages/sharedAlbum/SelectedFileOptions";
import { ALL_SECTION } from "constants/collection";
import { t } from "i18next";
import { useRouter } from "next/router";
import { AppContext } from "pages/_app";
import { useContext, useEffect, useMemo, useRef, useState } from "react";
import { useDropzone } from "react-dropzone";
import downloadManager from "services/download";
import {
getLocalPublicCollection,
getLocalPublicCollectionPassword,
@ -25,50 +61,6 @@ import {
} from "services/publicCollectionService";
import { Collection } from "types/collection";
import { EnteFile } from "types/file";
import {
downloadSelectedFiles,
getSelectedFiles,
mergeMetadata,
sortFiles,
} from "utils/file";
import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery";
import { logoutUser } from "@ente/accounts/services/user";
import { APPS } from "@ente/shared/apps/constants";
import EnteSpinner from "@ente/shared/components/EnteSpinner";
import FormPaper from "@ente/shared/components/Form/FormPaper";
import FormPaperTitle from "@ente/shared/components/Form/FormPaper/Title";
import OverflowMenu from "@ente/shared/components/OverflowMenu/menu";
import { OverflowMenuOption } from "@ente/shared/components/OverflowMenu/option";
import SingleInputForm, {
SingleInputFormProps,
} from "@ente/shared/components/SingleInputForm";
import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
import { ENTE_WEBSITE_LINK } from "@ente/shared/constants/urls";
import ComlinkCryptoWorker from "@ente/shared/crypto";
import useFileInput from "@ente/shared/hooks/useFileInput";
import AddPhotoAlternateOutlined from "@mui/icons-material/AddPhotoAlternateOutlined";
import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined";
import MoreHoriz from "@mui/icons-material/MoreHoriz";
import Typography from "@mui/material/Typography";
import bs58 from "bs58";
import { CollectionInfo } from "components/Collections/CollectionInfo";
import { CollectionInfoBarWrapper } from "components/Collections/styledComponents";
import {
FilesDownloadProgress,
FilesDownloadProgressAttributes,
} from "components/FilesDownloadProgress";
import FullScreenDropZone from "components/FullScreenDropZone";
import { LoadingOverlay } from "components/LoadingOverlay";
import { ITEM_TYPE, TimeStampListItem } from "components/PhotoList";
import UploadButton from "components/Upload/UploadButton";
import Uploader from "components/Upload/Uploader";
import UploadSelectorInputs from "components/UploadSelectorInputs";
import SharedAlbumNavbar from "components/pages/sharedAlbum/Navbar";
import SelectedFileOptions from "components/pages/sharedAlbum/SelectedFileOptions";
import { useRouter } from "next/router";
import { useDropzone } from "react-dropzone";
import downloadManager from "services/download";
import {
SelectedState,
SetFilesDownloadProgressAttributes,
@ -76,6 +68,13 @@ import {
UploadTypeSelectorIntent,
} from "types/gallery";
import { downloadCollectionFiles, isHiddenCollection } from "utils/collection";
import {
downloadSelectedFiles,
getSelectedFiles,
mergeMetadata,
sortFiles,
} from "utils/file";
import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery";
export default function PublicCollectionGallery() {
const token = useRef<string>(null);
@ -118,16 +117,16 @@ export default function PublicCollectionGallery() {
disabled: shouldDisableDropzone,
});
const {
selectedFiles: fileSelectorFiles,
open: openFileSelector,
getInputProps: getFileSelectorInputProps,
openSelector: openFileSelector,
selectedFiles: fileSelectorFiles,
} = useFileInput({
directory: false,
});
const {
selectedFiles: folderSelectorFiles,
open: openFolderSelector,
getInputProps: getFolderSelectorInputProps,
openSelector: openFolderSelector,
selectedFiles: folderSelectorFiles,
} = useFileInput({
directory: true,
});
@ -543,14 +542,13 @@ export default function PublicCollectionGallery() {
photoListFooter,
}}
>
<FullScreenDropZone
getDragAndDropRootProps={getDragAndDropRootProps}
>
<FullScreenDropZone {...{ getDragAndDropRootProps }}>
<UploadSelectorInputs
getDragAndDropInputProps={getDragAndDropInputProps}
getFileSelectorInputProps={getFileSelectorInputProps}
getFolderSelectorInputProps={getFolderSelectorInputProps}
getZipFileSelectorInputProps={undefined}
{...{
getDragAndDropInputProps,
getFileSelectorInputProps,
getFolderSelectorInputProps,
}}
/>
<SharedAlbumNavbar
showUploadButton={

View file

@ -133,17 +133,19 @@ some cases.
## Media
- ["jszip"](https://github.com/Stuk/jszip) is used for reading zip files in
- [jszip](https://github.com/Stuk/jszip) is used for reading zip files in
JavaScript (Live photos are zip files under the hood).
- ["file-type"](https://github.com/sindresorhus/file-type) is used for MIME
type detection. We are at an old version 16.5.4 because v17 onwards the
package became ESM only - for our limited use case, the custom Webpack
configuration that entails is not worth the upgrade.
- [file-type](https://github.com/sindresorhus/file-type) is used for MIME type
detection. We are at an old version 16.5.4 because v17 onwards the package
became ESM only - for our limited use case, the custom Webpack configuration
that entails is not worth the upgrade.
## Photos app specific
### Misc
- [react-dropzone](https://github.com/react-dropzone/react-dropzone/) is a
React hook to create a drag-and-drop input zone.
- "sanitize-filename" is for converting arbitrary strings into strings that
are suitable for being used as filenames.
- [sanitize-filename](https://github.com/parshap/node-sanitize-filename) is
for converting arbitrary strings into strings that are suitable for being
used as filenames.

View file

@ -1,56 +1,71 @@
import { useCallback, useRef, useState } from "react";
interface UseFileInputParams {
/**
* If `true`, the file open dialog will ask the user to select directories.
* Otherwise it'll ask the user to select files (default).
*/
directory?: boolean;
/**
* If specified, it'll restrict the type of files that the user can select
* by setting the "accept" attribute of the underlying HTML input element we
* use to surface the file selector dialog. For value of accept can be an
* extension or a MIME type (See
* https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept).
*/
accept?: string;
}
interface UseFileInputResult {
/**
* A function to call to get the properties that should be passed to a dummy
* `input` element that needs to be created to anchor the select file
* dialog. This input HTML element is not going to be visible, but it needs
* to be part of the DOM for {@link openSelector} to have effect.
*/
getInputProps: () => React.HTMLAttributes<HTMLInputElement>;
/**
* A function that can be called to open the select file / directory dialog.
*/
openSelector: () => void;
/**
* The list of {@link File}s that the user selected.
*
* This will be a list even if the user selected directories - in that case,
* it will be the recursive list of files within this directory.
*/
selectedFiles: File[];
}
/**
* Return three things:
* Wrap a open file selector into an easy to use package.
*
* - A function that can be called to trigger the showing of the select file /
* directory dialog.
* Returns a {@link UseFileInputResult} which contains a function to get the
* props for an input element, a function to open the file selector, and the
* list of selected files.
*
* - The list of properties that should be passed to a dummy `input` element
* that needs to be created to anchor the select file dialog. This input HTML
* element is not going to be visible, but it needs to be part of the DOM fro
* the open trigger to have effect.
*
* - The list of files that the user selected. This will be a list even if the
* user selected directories - in that case, it will be the recursive list of
* files within this directory.
*
* @param param0
*
* - If {@link directory} is true, the file open dialog will ask the user to
* select directories. Otherwise it'll ask the user to select files.
*
* - If {@link accept} is specified, it'll restrict the type of files that the
* user can select by setting the "accept" attribute of the underlying HTML
* input element we use to surface the file selector dialog. For value of
* accept can be an extension or a MIME type (See
* https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept).
* See the documentation of {@link UseFileInputParams} and
* {@link UseFileInputResult} for more details.
*/
export default function useFileInput({
export const useFileInput = ({
directory,
accept,
}: UseFileInputParams) {
}: UseFileInputParams): UseFileInputResult => {
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const inputRef = useRef<HTMLInputElement>();
const openSelectorDialog = useCallback(() => {
const openSelector = useCallback(() => {
if (inputRef.current) {
inputRef.current.value = null;
inputRef.current.click();
}
}, []);
const handleChange: React.ChangeEventHandler<HTMLInputElement> = async (
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (
event,
) => {
if (!!event.target && !!event.target.files) {
setSelectedFiles([...event.target.files]);
}
const files = event.target?.files;
if (files) setSelectedFiles([...files]);
};
// [Note: webkitRelativePath]
@ -78,12 +93,8 @@ export default function useFileInput({
onChange: handleChange,
...(accept ? { accept } : {}),
}),
[],
[directoryOpts, accept, handleChange],
);
return {
getInputProps,
open: openSelectorDialog,
selectedFiles: selectedFiles,
};
}
return { getInputProps, openSelector, selectedFiles };
};

View file

@ -13,3 +13,18 @@ export const shuffled = <T>(xs: T[]) =>
.map((x) => [Math.random(), x])
.sort()
.map(([, x]) => x) as T[];
/**
* Return the first non-empty string from the given list of strings.
*
* This function is needed because the `a ?? b` idiom doesn't do what you'd
* expect when a is "". Perhaps the behaviour is wrong, perhaps the expecation
* is wrong; this function papers over the differences.
*
* If none of the strings are non-empty, or if there are no strings in the given
* array, return undefined.
*/
export const firstNonEmpty = (ss: (string | undefined)[]) => {
for (const s of ss) if (s && s.length > 0) return s;
return undefined;
};

View file

@ -2503,12 +2503,12 @@ file-entry-cache@^6.0.1:
dependencies:
flat-cache "^3.0.4"
file-selector@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.4.0.tgz#59ec4f27aa5baf0841e9c6385c8386bef4d18b17"
integrity sha512-iACCiXeMYOvZqlF1kTiYINzgepRBymz1wwjiuup9u9nayhb6g4fSwiyJ/6adli+EPwrWtpgQAh2PoS7HukEGEg==
file-selector@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.6.0.tgz#fa0a8d9007b829504db4d07dd4de0310b65287dc"
integrity sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==
dependencies:
tslib "^2.0.3"
tslib "^2.4.0"
file-type@16.5.4:
version "16.5.4"
@ -3887,13 +3887,13 @@ react-dom@^18:
loose-envify "^1.1.0"
scheduler "^0.23.0"
react-dropzone@^11.2.4:
version "11.7.1"
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-11.7.1.tgz#3851bb75b26af0bf1b17ce1449fd980e643b9356"
integrity sha512-zxCMwhfPy1olUEbw3FLNPLhAm/HnaYH5aELIEglRbqabizKAdHs0h+WuyOpmA+v1JXn0++fpQDdNfUagWt5hJQ==
react-dropzone@^14.2:
version "14.2.3"
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-14.2.3.tgz#0acab68308fda2d54d1273a1e626264e13d4e84b"
integrity sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==
dependencies:
attr-accept "^2.2.2"
file-selector "^0.4.0"
file-selector "^0.6.0"
prop-types "^15.8.1"
react-fast-compare@^2.0.1:
@ -4592,7 +4592,7 @@ tsconfig-paths@^3.15.0:
minimist "^1.2.6"
strip-bom "^3.0.0"
tslib@^2.0.0, tslib@^2.0.3, tslib@^2.4.0, tslib@^2.6.2:
tslib@^2.0.0, tslib@^2.4.0, tslib@^2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==