chore: migrate media-library utils to Typescript (#21532)

* chore: migrate to TS typeFromMime

* chore: migrate to TS toSingularTypes

* chore: migrate to TS prefixPluginTranslations

* chore: migrate to TS prefixFileUrlWithBackendUrl

* chore: migrate to TS moveElement

* chore: migrate to TS getTrad

* chore: migrate to TS getFileExtension

* chore: migrate to TS containsAssetFilter

* chore: migrate to TS displayedFilters

* chore: migrate to TS downloadFile

* chore: remove findRecursiveFolderMetadatas because is unused

* chore: migrate to TS appendSearchParamsToUrl

* chore: migrate to TS formatBytes

* chore: migrate to TS formatDuration

* chore: migrate to TS urlYupSchema

* chore: migrate to TS urlsToAssets

* chore: migrate to TS rawFileToAsset

* chore: migrate to TS getFolderURL

* chore: migrate to TS getFolderParents

* chore: migrate to TS createAssetUrl

* chore: migrate to TS findRecursiveFolderByValue

* chore: migrate to TS getAllowedFiles

* chore: migrate to TS getBreadcrumbDataCM

* chore: migrate to TS normalizeAPIError

* chore: migrate to TS getAPIInnerErrors

* chore: migrate to TS getBreadcrumbDataML and change the utils imports

* chore: fix export from index

* chore: reduce the errors type definition

* chore: change Query type

* chore: change the way utils are exported in the index

* chore: reduce the code in the custom declaration type file
This commit is contained in:
Simone 2024-10-04 16:57:43 +02:00 committed by GitHub
parent 43e15a3ba8
commit 38dcf9a2f0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
101 changed files with 711 additions and 621 deletions

View File

@ -0,0 +1,9 @@
export {};
declare global {
interface Window {
strapi: {
backendURL: string;
};
}
}

View File

@ -5,7 +5,7 @@ import { Filter } from '@strapi/icons';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import displayedFilters from '../../../utils/displayedFilters';
import { displayedFilters } from '../../../utils';
import FilterList from '../../FilterList';
import FilterPopover from '../../FilterPopover';

View File

@ -6,7 +6,7 @@ import { Search } from '@strapi/icons';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import getTrad from '../../../../utils/getTrad';
import { getTrad } from '../../../../utils';
const SearchAsset = ({ onChangeSearch, queryValue }) => {
const { formatMessage } = useIntl();

View File

@ -24,9 +24,7 @@ import {
} from '../../../constants';
import { useFolder } from '../../../hooks/useFolder';
import { usePersistentState } from '../../../hooks/usePersistentState';
import { getBreadcrumbDataCM, toSingularTypes } from '../../../utils';
import getAllowedFiles from '../../../utils/getAllowedFiles';
import getTrad from '../../../utils/getTrad';
import { getBreadcrumbDataCM, toSingularTypes, getTrad, getAllowedFiles } from '../../../utils';
import { AssetGridList } from '../../AssetGridList';
import { Breadcrumbs } from '../../Breadcrumbs';
import { EmptyAssets } from '../../EmptyAssets';

View File

@ -4,7 +4,7 @@ import { Flex, Typography } from '@strapi/design-system';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import getTrad from '../../../utils/getTrad';
import { getTrad } from '../../../utils';
import { AssetGridList } from '../../AssetGridList';
export const SelectedStep = ({ selectedAssets, onSelectAsset, onReorderAsset }) => {

View File

@ -12,9 +12,7 @@ import { useFolders } from '../../hooks/useFolders';
import { useMediaLibraryPermissions } from '../../hooks/useMediaLibraryPermissions';
import useModalQueryParams from '../../hooks/useModalQueryParams';
import { useSelectionState } from '../../hooks/useSelectionState';
import { containsAssetFilter, getTrad } from '../../utils';
import getAllowedFiles from '../../utils/getAllowedFiles';
import { moveElement } from '../../utils/moveElement';
import { containsAssetFilter, getTrad, getAllowedFiles, moveElement } from '../../utils';
import { EditAssetContent } from '../EditAssetDialog';
import { EditFolderContent } from '../EditFolderDialog';

View File

@ -9,8 +9,7 @@ import { useIntl } from 'react-intl';
import { AssetDefinition, FolderDefinition } from '../../constants';
import { useBulkMove } from '../../hooks/useBulkMove';
import { useFolderStructure } from '../../hooks/useFolderStructure';
import { getTrad } from '../../utils';
import { normalizeAPIError } from '../../utils/normalizeAPIError';
import { getTrad, normalizeAPIError } from '../../utils';
import SelectTree from '../SelectTree';
export const BulkMoveDialog = ({ onClose, selected, currentFolder }) => {

View File

@ -7,7 +7,7 @@ import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import { useClipboard } from '../../hooks/useClipboard';
import getTrad from '../../utils/getTrad';
import { getTrad } from '../../utils';
export const CopyLinkButton = ({ url }) => {
const { toggleNotification } = useNotification();

View File

@ -6,7 +6,7 @@ import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import { styled, useTheme } from 'styled-components';
import getTrad from '../../../utils/getTrad';
import { getTrad } from '../../../utils';
import { CroppingActionRow } from './components';

View File

@ -10,9 +10,7 @@ import { AssetDefinition, AssetType } from '../../../constants';
import { useCropImg } from '../../../hooks/useCropImg';
import { useEditAsset } from '../../../hooks/useEditAsset';
import { useUpload } from '../../../hooks/useUpload';
import { createAssetUrl } from '../../../utils';
import { downloadFile } from '../../../utils/downloadFile';
import getTrad from '../../../utils/getTrad';
import { createAssetUrl, getTrad, downloadFile } from '../../../utils';
import { CopyLinkButton } from '../../CopyLinkButton';
import { UploadProgress } from '../../UploadProgress';
import { RemoveAssetDialog } from '../RemoveAssetDialog';

View File

@ -27,8 +27,7 @@ import * as yup from 'yup';
import { AssetDefinition } from '../../constants';
import { useEditAsset } from '../../hooks/useEditAsset';
import { useFolderStructure } from '../../hooks/useFolderStructure';
import { findRecursiveFolderByValue, getTrad, getFileExtension } from '../../utils';
import formatBytes from '../../utils/formatBytes';
import { findRecursiveFolderByValue, getTrad, getFileExtension, formatBytes } from '../../utils';
import { ContextInfo } from '../ContextInfo';
import SelectTree from '../SelectTree';

View File

@ -7,7 +7,7 @@ import { IntlProvider } from 'react-intl';
import { QueryClient, QueryClientProvider } from 'react-query';
import en from '../../../translations/en.json';
import { downloadFile } from '../../../utils/downloadFile';
import { downloadFile } from '../../../utils';
import { EditAssetDialog } from '../index';
jest.mock('../../../hooks/useFolderStructure');

View File

@ -13,7 +13,7 @@ import { IntlProvider } from 'react-intl';
import { QueryClient, QueryClientProvider } from 'react-query';
import en from '../../../translations/en.json';
import { downloadFile } from '../../../utils/downloadFile';
import { downloadFile } from '../../../utils';
import { EditAssetDialog } from '../index';
jest.mock('../../../utils/downloadFile');

View File

@ -6,8 +6,7 @@ import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import { AssetDefinition } from '../../../constants';
import { prefixFileUrlWithBackendUrl } from '../../../utils';
import getTrad from '../../../utils/getTrad';
import { getTrad, prefixFileUrlWithBackendUrl } from '../../../utils';
import { CopyLinkButton } from '../../CopyLinkButton';
export const CarouselAssetActions = ({ asset, onDeleteAsset, onAddAsset, onEditAsset }) => {

View File

@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import { AssetDefinition } from '../../../constants';
import getTrad from '../../../utils/getTrad';
import { getTrad } from '../../../utils/getTrad';
import { EditAssetDialog } from '../../EditAssetDialog';
import { CarouselAsset } from './CarouselAsset';

View File

@ -7,8 +7,7 @@ import { useIntl } from 'react-intl';
import { styled } from 'styled-components';
import { AssetSource } from '../../../constants';
import getTrad from '../../../utils/getTrad';
import { rawFileToAsset } from '../../../utils/rawFileToAsset';
import { getTrad, rawFileToAsset } from '../../../utils';
const TextAlignTypography = styled(Typography)`
align-items: center;

View File

@ -4,8 +4,7 @@ import { useField, useNotification } from '@strapi/admin/strapi-admin';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import getAllowedFiles from '../../utils/getAllowedFiles';
import getTrad from '../../utils/getTrad';
import { getTrad, getAllowedFiles } from '../../utils';
import { AssetDialog } from '../AssetDialog';
import { EditFolderDialog } from '../EditFolderDialog';
import { UploadAssetDialog } from '../UploadAssetDialog/UploadAssetDialog';

View File

@ -1,7 +0,0 @@
export default function flattenTree(tree, parent, depth = 0) {
return tree.flatMap((item) =>
item.children
? [{ ...item, parent: parent?.value, depth }, ...flattenTree(item.children, item, depth + 1)]
: { ...item, depth, parent: parent?.value }
);
}

View File

@ -0,0 +1,26 @@
type TreeNode<T> = {
value: T;
children?: TreeNode<T>[];
label?: string;
};
export type FlattenedNode<T> = {
value: T;
parent?: T;
depth: number;
// we need the label in places where flattenTree is used
label?: string;
children?: TreeNode<T>[];
};
export default function flattenTree<T>(
tree: TreeNode<T>[],
parent: TreeNode<T> | null = null,
depth: number = 0
): FlattenedNode<T>[] {
return tree.flatMap((item) =>
item.children
? [{ ...item, parent: parent?.value, depth }, ...flattenTree(item.children, item, depth + 1)]
: { ...item, depth, parent: parent?.value }
);
}

View File

@ -4,7 +4,7 @@ import { Box, Divider, Modal, Tabs } from '@strapi/design-system';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import getTrad from '../../../utils/getTrad';
import { getTrad } from '../../../utils';
import { FromComputerForm } from './FromComputerForm';
import { FromUrlForm } from './FromUrlForm';

View File

@ -9,8 +9,7 @@ import { useIntl } from 'react-intl';
import { styled } from 'styled-components';
import { AssetSource } from '../../../constants';
import getTrad from '../../../utils/getTrad';
import { rawFileToAsset } from '../../../utils/rawFileToAsset';
import { getTrad, rawFileToAsset } from '../../../utils';
const Wrapper = styled(Flex)`
flex-direction: column;

View File

@ -6,9 +6,7 @@ import { Form, Formik } from 'formik';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import getTrad from '../../../utils/getTrad';
import { urlsToAssets } from '../../../utils/urlsToAssets';
import { urlSchema } from '../../../utils/urlYupSchema';
import { getTrad, urlsToAssets, urlSchema } from '../../../utils';
export const FromUrlForm = ({ onClose, onAddAsset, trackedLocation }) => {
const [loading, setLoading] = useState(false);

View File

@ -6,7 +6,7 @@ import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import { AssetDefinition } from '../../../constants';
import getTrad from '../../../utils/getTrad';
import { getTrad } from '../../../utils';
import { AssetCard } from '../../AssetCard/AssetCard';
import { UploadingAssetCard } from '../../AssetCard/UploadingAssetCard';

View File

@ -7,7 +7,10 @@ import { QueryClient, QueryClientProvider } from 'react-query';
import { PendingAssetStep } from '../PendingAssetStep';
jest.mock('../../../../utils/getTrad', () => (x) => x);
jest.mock('../../../../utils', () => ({
...jest.requireActual('../../../../utils'),
getTrad: (x) => x,
}));
const queryClient = new QueryClient({
defaultOptions: {

View File

@ -6,8 +6,7 @@ import { MediaLibraryDialog } from './components/MediaLibraryDialog';
import { MediaLibraryInput } from './components/MediaLibraryInput';
import { PERMISSIONS } from './constants';
import pluginId from './pluginId';
import getTrad from './utils/getTrad';
import { prefixPluginTranslations } from './utils/prefixPluginTranslations';
import { getTrad, prefixPluginTranslations } from './utils';
const name = pluginPkg.strapi.name;

View File

@ -0,0 +1,12 @@
// TODO: replace this file with the constants file when it will be migrated to TS
export enum AssetType {
Video = 'video',
Image = 'image',
Document = 'doc',
Audio = 'audio',
}
export enum AssetSource {
Url = 'url',
Computer = 'computer',
}

View File

@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import { pageSizes, sortOptions } from '../../../../constants';
import getTrad from '../../../../utils/getTrad';
import { getTrad } from '../../../../utils';
const Settings = ({ sort = '', pageSize = 10, onChange }) => {
const { formatMessage } = useIntl();

View File

@ -16,7 +16,7 @@ import { NavLink } from 'react-router-dom';
import { useConfig } from '../../../hooks/useConfig';
import pluginID from '../../../pluginId';
import getTrad from '../../../utils/getTrad';
import { getTrad } from '../../../utils';
import { Settings } from './components/Settings';
import { onChange, setLoaded } from './state/actions';

View File

@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import { AssetDefinition, FolderDefinition } from '../../../../constants';
import getTrad from '../../../../utils/getTrad';
import { getTrad } from '../../../../utils';
import { BulkDeleteButton } from './BulkDeleteButton';
import { BulkMoveButton } from './BulkMoveButton';

View File

@ -7,7 +7,7 @@ import { useIntl } from 'react-intl';
import FilterList from '../../../../components/FilterList';
import FilterPopover from '../../../../components/FilterPopover';
import displayedFilters from '../../../../utils/displayedFilters';
import { displayedFilters } from '../../../../utils';
export const Filters = () => {
const [open, setOpen] = React.useState(false);

View File

@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import { AssetDefinition, FolderDefinition } from '../../../constants';
import getTrad from '../../../utils/getTrad';
import { getTrad } from '../../../utils';
import { BulkDeleteButton } from './BulkDeleteButton';
import { BulkMoveButton } from './BulkMoveButton';

View File

@ -7,7 +7,7 @@ import { useIntl } from 'react-intl';
import FilterList from '../../../components/FilterList';
import FilterPopover from '../../../components/FilterPopover';
import displayedFilters from '../../../utils/displayedFilters';
import { displayedFilters } from '../../../utils';
export const Filters = () => {
const [open, setOpen] = React.useState(false);

View File

@ -1,24 +0,0 @@
/**
* Append the given search params to the given URL.
*
* @param {String} url The URL string to append the search params to
* @param {Object} params The object of search params to append to the URL
* @returns {String} A string representing the URL with the search params appended
*/
const appendSearchParamsToUrl = ({ url, params }) => {
if (url === undefined || typeof params !== 'object') {
return url;
}
const urlObj = new URL(url, window.strapi.backendURL);
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
urlObj.searchParams.append(key, value);
}
});
return urlObj.toString();
};
export { appendSearchParamsToUrl };

View File

@ -0,0 +1,22 @@
interface AppendSearchParamsToUrlProps {
url?: string;
params?: Record<string, string | null | undefined> | string;
}
const appendSearchParamsToUrl = ({ url, params }: AppendSearchParamsToUrlProps) => {
if (url === undefined || typeof params !== 'object') {
return url;
}
const urlObj = new URL(url, window.strapi.backendURL);
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
urlObj.searchParams.append(key, value);
}
});
return urlObj.toString();
};
export { appendSearchParamsToUrl };

View File

@ -1,4 +1,6 @@
const containsMimeTypeFilter = (query) => {
import type { Query } from '../../../shared/contracts/files';
const containsMimeTypeFilter = (query: Query | null) => {
const filters = query?.filters?.$and;
if (!filters) {
@ -12,8 +14,6 @@ const containsMimeTypeFilter = (query) => {
return !!result;
};
const containsAssetFilter = (query) => {
export const containsAssetFilter = (query: Query | null) => {
return containsMimeTypeFilter(query);
};
export default containsAssetFilter;

View File

@ -1,20 +0,0 @@
import prefixFileUrlWithBackendUrl from './prefixFileUrlWithBackendUrl';
/**
* Create image URL for asset
* @param {Object} asset
* @param {Boolean} forThumbnail - if true, return URL for thumbnail
* if there's no thumbnail, return the URL of the original image.
* @return {String} URL
*/
const createAssetUrl = (asset, forThumbnail = true) => {
if (asset.isLocal) {
return asset.url;
}
const assetUrl = forThumbnail ? asset?.formats?.thumbnail?.url || asset.url : asset.url;
return prefixFileUrlWithBackendUrl(assetUrl);
};
export default createAssetUrl;

View File

@ -0,0 +1,13 @@
import { prefixFileUrlWithBackendUrl } from './prefixFileUrlWithBackendUrl';
import type { File } from '../../../shared/contracts/files';
export const createAssetUrl = (asset: File, forThumbnail = true) => {
if (asset.isLocal) {
return asset.url;
}
const assetUrl = forThumbnail ? asset?.formats?.thumbnail?.url || asset.url : asset.url;
return prefixFileUrlWithBackendUrl(assetUrl);
};

View File

@ -1,4 +1,4 @@
const displayedFilters = [
export const displayedFilters = [
{
name: 'createdAt',
fieldSchema: {
@ -27,5 +27,3 @@ const displayedFilters = [
metadatas: { label: 'type' },
},
];
export default displayedFilters;

View File

@ -1,4 +1,4 @@
export const downloadFile = async (url, fileName) => {
export const downloadFile = async (url: string, fileName: string) => {
const fileBlob = await fetch(url).then((res) => res.blob());
const urlDownload = window.URL.createObjectURL(fileBlob);
const link = document.createElement('a');

View File

@ -1,17 +0,0 @@
export default function findRecursiveFolderByValue(data, value) {
let result;
function iter(a) {
if (a.value === value) {
result = a;
return true;
}
return Array.isArray(a.children) && a.children.some(iter);
}
data.some(iter);
return result;
}

View File

@ -0,0 +1,26 @@
import type { FolderNode } from '../../../shared/contracts/folders';
interface FolderStructureValue extends Omit<FolderNode, 'children'> {
value: number | null;
children?: FolderStructureValue[];
}
type Value = number | null | { value: number | null };
export function findRecursiveFolderByValue(data: FolderStructureValue[], value: Value) {
let result;
function iter(a: FolderStructureValue) {
if (a.value === value) {
result = a;
return true;
}
return Array.isArray(a.children) && a.children.some(iter);
}
data.some(iter);
return result;
}

View File

@ -1,15 +0,0 @@
const findRecursiveFolderMetadatas = (node, searchedId, parentId = null) => {
let result = null;
if (node.value === parseInt(searchedId, 10)) {
result = { parentId, currentFolderLabel: node.label };
} else {
for (let i = 0; i < node.children.length && !result; i++) {
result = findRecursiveFolderMetadatas(node.children[i], searchedId, node.value);
}
}
return result;
};
export default findRecursiveFolderMetadatas;

View File

@ -1,13 +0,0 @@
import byteSize from 'byte-size';
function formatBytes(receivedBytes, decimals = 0) {
const { value, unit } = byteSize(receivedBytes * 1000, { precision: decimals });
if (!unit) {
return '0B';
}
return `${value}${unit.toUpperCase()}`;
}
export default formatBytes;

View File

@ -0,0 +1,12 @@
import byteSize from 'byte-size';
export function formatBytes(receivedBytes: number | string, decimals = 0) {
const realBytes = typeof receivedBytes === 'string' ? Number(receivedBytes) : receivedBytes;
const { value, unit } = byteSize(realBytes * 1000, { precision: decimals });
if (!unit) {
return '0B';
}
return `${value}${unit.toUpperCase()}`;
}

View File

@ -1,8 +1,8 @@
import { intervalToDuration } from 'date-fns';
const zeroPad = (num) => String(num).padStart(2, '0');
const zeroPad = (num?: number) => String(num).padStart(2, '0');
export const formatDuration = (durationInSecond) => {
export const formatDuration = (durationInSecond: number) => {
const duration = intervalToDuration({ start: 0, end: durationInSecond * 1000 });
return `${zeroPad(duration.hours)}:${zeroPad(duration.minutes)}:${zeroPad(duration.seconds)}`;

View File

@ -1,15 +1,24 @@
import { normalizeAPIError } from './normalizeAPIError';
import type { FetchError } from '@strapi/admin/strapi-admin';
import type { MessageDescriptor } from 'react-intl';
type GetAPIInnerErrorsReturn = {
[key: string]: MessageDescriptor;
};
/**
*
* Returns a normalized error message
*
*/
function getAPIInnerErrors(error, { getTrad }) {
export function getAPIInnerErrors(
error: FetchError,
{ getTrad }: { getTrad: (key: string) => string }
) {
const normalizedError = normalizeAPIError(error, getTrad);
if (normalizedError && 'errors' in normalizedError) {
return normalizedError.errors.reduce((acc, error) => {
return normalizedError.errors.reduce<GetAPIInnerErrorsReturn>((acc, error) => {
if ('path' in error.values) {
acc[error.values.path] = {
id: error.id,
@ -23,5 +32,3 @@ function getAPIInnerErrors(error, { getTrad }) {
return normalizedError?.defaultMessage;
}
export default getAPIInnerErrors;

View File

@ -1,16 +1,28 @@
import toSingularTypes from './toSingularTypes';
import { toSingularTypes } from './toSingularTypes';
import type { File } from '../../../shared/contracts/files';
/**
* Returns the files that can be added to the media field
* @param {Object[]} pluralTypes Array of string (allowedTypes)
* @param {Object[]} files Array of files
* @returns Object[]
*/
const getAllowedFiles = (pluralTypes, files) => {
interface AllowedFiles extends File {
documentId: string;
isSelectable: boolean;
locale: string | null;
type: string;
}
export const getAllowedFiles = (pluralTypes: string[], files: AllowedFiles[]) => {
const singularTypes = toSingularTypes(pluralTypes);
const allowedFiles = files.filter((file) => {
const fileType = file.mime.split('/')[0];
const fileType = file?.mime?.split('/')[0];
if (!fileType) {
return false;
}
if (singularTypes.includes('file') && !['video', 'image', 'audio'].includes(fileType)) {
return true;
@ -21,5 +33,3 @@ const getAllowedFiles = (pluralTypes, files) => {
return allowedFiles;
};
export default getAllowedFiles;

View File

@ -1,34 +0,0 @@
import getTrad from './getTrad';
const getBreadcrumbDataML = (folder) => {
let data = [
{
id: null,
label: { id: getTrad('plugin.name'), defaultMessage: 'Media Library' },
},
];
if (folder?.parent?.parent) {
data.push([]);
}
if (folder?.parent) {
data.push({
id: folder.parent.id,
label: folder.parent.name,
path: folder.parent.path,
});
}
if (folder) {
data.push({
id: folder.id,
label: folder.name,
path: folder.path,
});
}
return data;
};
export default getBreadcrumbDataML;

View File

@ -0,0 +1,53 @@
import { getTrad } from './getTrad';
import type { Folder } from '../../../shared/contracts/folders';
import type { MessageDescriptor } from 'react-intl';
interface BreadcrumbDataFolder extends Omit<Folder, 'children' | 'files' | 'parent'> {
parent?: BreadcrumbDataFolder;
children?: {
count: number;
};
files?: {
count: number;
};
}
interface BreadcrumbItem {
id?: number | null;
label?: MessageDescriptor | string;
path?: string;
}
type BreadcrumbData = BreadcrumbItem | [];
export const getBreadcrumbDataCM = (folder: BreadcrumbDataFolder | null) => {
let data: BreadcrumbData[] = [
{
id: null,
label: { id: getTrad('plugin.name'), defaultMessage: 'Media Library' },
},
];
if (folder?.parent?.parent) {
data.push([]);
}
if (folder?.parent) {
data.push({
id: folder.parent.id,
label: folder.parent.name,
path: folder.parent.path,
});
}
if (folder) {
data.push({
id: folder.id,
label: folder.name,
path: folder.path,
});
}
return data;
};

View File

@ -1,38 +0,0 @@
import getFolderURL from './getFolderURL';
import getTrad from './getTrad';
const getBreadcrumbDataML = (folder, { pathname, query }) => {
let data = [
{
id: null,
label: { id: getTrad('plugin.name'), defaultMessage: 'Media Library' },
href: folder ? getFolderURL(pathname, query) : undefined,
},
];
if (folder?.parent?.parent) {
data.push([]);
}
if (folder?.parent) {
data.push({
id: folder.parent.id,
label: folder.parent.name,
href: getFolderURL(pathname, query, {
folder: folder.parent.id,
folderPath: folder.parent.path,
}),
});
}
if (folder) {
data.push({
id: folder.id,
label: folder.name,
});
}
return data;
};
export default getBreadcrumbDataML;

View File

@ -0,0 +1,60 @@
import { getFolderURL } from './getFolderURL';
import { getTrad } from './getTrad';
import type { Query } from '../../../shared/contracts/files';
import type { Folder } from '../../../shared/contracts/folders';
import type { MessageDescriptor } from 'react-intl';
interface GetBreadcrumbDataMLProps {
folder: Folder;
options: {
pathname: string;
query?: Query;
};
}
interface GetBreadcrumbDataMLReturn {
id: number | null;
label: string | MessageDescriptor;
href?: string;
}
type BreadcrumbData = GetBreadcrumbDataMLReturn | [];
export const getBreadcrumbDataML = (
folder: GetBreadcrumbDataMLProps['folder'] | null,
{ pathname, query }: GetBreadcrumbDataMLProps['options']
) => {
let data: BreadcrumbData[] = [
{
id: null,
label: { id: getTrad('plugin.name'), defaultMessage: 'Media Library' },
href: folder ? getFolderURL(pathname, query || {}) : undefined,
},
];
if (folder?.parent && typeof folder?.parent !== 'number' && folder?.parent?.parent) {
data.push([]);
}
if (folder?.parent && typeof folder.parent !== 'number') {
data.push({
id: folder.parent.id,
label: folder.parent.name,
href: getFolderURL(pathname, query || {}, {
folder: folder.parent.id?.toString(),
folderPath: folder.parent.path,
}),
});
}
if (folder) {
data.push({
id: folder.id,
label: folder.name,
});
}
return data;
};
export default getBreadcrumbDataML;

View File

@ -1,3 +0,0 @@
const getFileExtension = (ext) => (ext && ext[0] === '.' ? ext.substring(1) : ext);
export default getFileExtension;

View File

@ -0,0 +1,2 @@
export const getFileExtension = (ext?: string | null) =>
ext && ext[0] === '.' ? ext.substring(1) : ext;

View File

@ -1,6 +1,13 @@
import flattenTree from '../components/SelectTree/utils/flattenTree';
const getFolderParents = (folders, currentFolderId) => {
import type { FolderNode } from '../../../shared/contracts/folders';
interface FolderStructureValue extends Omit<FolderNode, 'children'> {
value: number | null;
children?: FolderStructureValue[];
}
export const getFolderParents = (folders: FolderStructureValue[], currentFolderId: number) => {
const parents = [];
const flatFolders = flattenTree(folders);
const currentFolder = flatFolders.find((folder) => folder.value === currentFolderId);
@ -14,11 +21,9 @@ const getFolderParents = (folders, currentFolderId) => {
while (parent !== undefined) {
// eslint-disable-next-line no-loop-func
let parentToStore = flatFolders.find(({ value }) => value === parent);
parents.push({ id: parentToStore.value, label: parentToStore.label });
parent = parentToStore.parent;
parents.push({ id: parentToStore?.value, label: parentToStore?.label });
parent = parentToStore?.parent;
}
return parents.reverse();
};
export default getFolderParents;

View File

@ -1,16 +1,12 @@
/**
* @param {string} pathname
* @param {object} currentQuery
* @param {string} query._q Search value of the query
* @param {object} newQuery
* @param {string} newQuery.folder
* @param {string} newQuery.folderPath
* @returns {string}
*/
import type { Query } from '../../../shared/contracts/files';
import { stringify } from 'qs';
const getFolderURL = (pathname, currentQuery, { folder, folderPath } = {}) => {
export const getFolderURL = (
pathname: string,
currentQuery: Query,
{ folder, folderPath }: { folder?: string; folderPath?: string } = {}
) => {
const { _q, ...queryParamsWithoutQ } = currentQuery;
const queryParamsString = stringify(
{
@ -25,5 +21,3 @@ const getFolderURL = (pathname, currentQuery, { folder, folderPath } = {}) => {
// we remove it here to allow navigating in a folder and see the result of this navigation
return `${pathname}${queryParamsString ? `?${queryParamsString}` : ''}`;
};
export default getFolderURL;

View File

@ -1,5 +0,0 @@
import pluginId from '../pluginId';
const getTrad = (id) => `${pluginId}.${id}`;
export default getTrad;

View File

@ -0,0 +1,3 @@
import pluginId from '../pluginId';
export const getTrad = (id: string) => `${pluginId}.${id}`;

View File

@ -1,16 +0,0 @@
export { appendSearchParamsToUrl } from './appendSearchParamsToUrl';
export { default as containsAssetFilter } from './containsAssetFilter';
export { default as createAssetUrl } from './createAssetUrl';
export { default as findRecursiveFolderByValue } from './findRecursiveFolderByValue';
export { default as findRecursiveFolderMetadatas } from './findRecursiveFolderMetadatas';
export { default as formatBytes } from './formatBytes';
export * from './formatDuration';
export { default as getBreadcrumbDataCM } from './getBreadcrumbDataCM';
export { default as getBreadcrumbDataML } from './getBreadcrumbDataML';
export { default as getFolderParents } from './getFolderParents';
export { default as getFolderURL } from './getFolderURL';
export { default as getTrad } from './getTrad';
export { default as toSingularTypes } from './toSingularTypes';
export { default as getFileExtension } from './getFileExtension';
export { default as prefixFileUrlWithBackendUrl } from './prefixFileUrlWithBackendUrl';
export { default as getAPIInnerErrors } from './getAPIInnerErrors';

View File

@ -0,0 +1,25 @@
export * from './appendSearchParamsToUrl';
export * from './containsAssetFilter';
export * from './createAssetUrl';
export * from './displayedFilters';
export * from './downloadFile';
export * from './findRecursiveFolderByValue';
export * from './formatBytes';
export * from './formatDuration';
export * from './getAllowedFiles';
export * from './getAPIInnerErrors';
export * from './getBreadcrumbDataCM';
export * from './getBreadcrumbDataML';
export * from './getFileExtension';
export * from './getFolderParents';
export * from './getFolderURL';
export * from './getTrad';
export * from './moveElement';
export * from './normalizeAPIError';
export * from './prefixFileUrlWithBackendUrl';
export * from './prefixPluginTranslations';
export * from './rawFileToAsset';
export * from './toSingularTypes';
export * from './typeFromMime';
export * from './urlsToAssets';
export * from './urlYupSchema';

View File

@ -1,4 +1,4 @@
const move = (array, oldIndex, newIndex) => {
const move = (array: number[], oldIndex: number, newIndex: number) => {
if (newIndex >= array.length) {
newIndex = array.length - 1;
}
@ -7,7 +7,7 @@ const move = (array, oldIndex, newIndex) => {
return array;
};
export const moveElement = (array, index, offset) => {
export const moveElement = (array: number[], index: number, offset: number) => {
const newIndex = index + offset;
return move(array, index, newIndex);

View File

@ -1,53 +0,0 @@
function getPrefixedId(message, callback) {
const prefixedMessage = `apiError.${message}`;
// if a prefix function has been passed in it is used to
// prefix the id, e.g. to allow an error message to be
// set only for a localization namespace
if (typeof callback === 'function') {
return callback(prefixedMessage);
}
return prefixedMessage;
}
function normalizeError(error, { name, intlMessagePrefixCallback }) {
const { message } = error;
const normalizedError = {
id: getPrefixedId(message, intlMessagePrefixCallback),
defaultMessage: message,
name: error.name ?? name,
values: {},
};
if ('path' in error) {
normalizedError.values = { path: error.path.join('.') };
}
return normalizedError;
}
const validateErrorIsYupValidationError = (err) =>
typeof err.details === 'object' && err.details !== null && 'errors' in err.details;
export function normalizeAPIError(apiError, intlMessagePrefixCallback) {
const error = apiError.response?.data.error;
if (error) {
// some errors carry multiple errors (such as ValidationError)
if (validateErrorIsYupValidationError(error)) {
return {
name: error.name,
message: error?.message || null,
errors: error.details.errors.map((err) =>
normalizeError(err, { name: error.name, intlMessagePrefixCallback })
),
};
}
return normalizeError(error, { intlMessagePrefixCallback });
}
return null;
}

View File

@ -0,0 +1,92 @@
import type { errors } from '@strapi/utils';
import type { FetchError } from '@strapi/admin/strapi-admin';
type ApiError = InstanceType<(typeof errors)[keyof typeof errors]>;
interface NormalizeErrorOptions {
name?: string;
intlMessagePrefixCallback?: (id: string) => string;
}
interface NormalizeErrorReturn {
id: string;
defaultMessage: string;
name?: string;
values: Record<'path', string> | Record<string, never>;
}
interface YupFormattedError {
path: string[];
message: string;
name: string;
}
function getPrefixedId(message: string, callback?: (prefixedMessage: string) => string) {
const prefixedMessage = `apiError.${message}`;
// if a prefix function has been passed in it is used to
// prefix the id, e.g. to allow an error message to be
// set only for a localization namespace
if (typeof callback === 'function') {
return callback(prefixedMessage);
}
return prefixedMessage;
}
function normalizeError(
error: ApiError | YupFormattedError,
{ name, intlMessagePrefixCallback }: NormalizeErrorOptions
): NormalizeErrorReturn {
const { message } = error;
const normalizedError = {
id: getPrefixedId(message, intlMessagePrefixCallback),
defaultMessage: message,
name: error.name ?? name,
values: {},
};
if ('path' in error) {
normalizedError.values = { path: error.path.join('.') };
}
return normalizedError;
}
const validateErrorIsYupValidationError = (
err: ApiError
): err is errors.YupValidationError & { details: { errors: YupFormattedError[] } } =>
typeof err.details === 'object' && err.details !== null && 'errors' in err.details;
/**
* Normalize the format of `ResponseError`
* in places where the hook `useAPIErrorHandler` can not called
* (e.g. outside of a React component).
*/
export function normalizeAPIError(
apiError: FetchError,
intlMessagePrefixCallback?: NormalizeErrorOptions['intlMessagePrefixCallback']
):
| NormalizeErrorReturn
| { name: string; message: string | null; errors: NormalizeErrorReturn[] }
| null {
const error = apiError.response?.data.error;
if (error) {
// some errors carry multiple errors (such as ValidationError)
if (validateErrorIsYupValidationError(error)) {
return {
name: error.name,
message: error?.message || null,
errors: error.details.errors.map((err) =>
normalizeError(err, { name: error.name, intlMessagePrefixCallback })
),
};
}
return normalizeError(error, { intlMessagePrefixCallback });
}
return null;
}

View File

@ -1,12 +0,0 @@
/**
* Create the file URL with the backend URL
* @param {Object} asset
* @param {String} fileURL - if true, return the file URL with the Backend url
* if there's no file url or it doesn't start with a slash return the original file url.
* @return {String} file Url
*/
const prefixFileUrlWithBackendUrl = (fileURL) => {
return !!fileURL && fileURL.startsWith('/') ? `${window.strapi.backendURL}${fileURL}` : fileURL;
};
export default prefixFileUrlWithBackendUrl;

View File

@ -0,0 +1,3 @@
export const prefixFileUrlWithBackendUrl = (fileURL?: string) => {
return !!fileURL && fileURL.startsWith('/') ? `${window.strapi.backendURL}${fileURL}` : fileURL;
};

View File

@ -1,13 +0,0 @@
const prefixPluginTranslations = (trad, pluginId) => {
if (!pluginId) {
throw new TypeError("pluginId can't be empty");
}
return Object.keys(trad).reduce((acc, current) => {
acc[`${pluginId}.${current}`] = trad[current];
return acc;
}, {});
};
export { prefixPluginTranslations };

View File

@ -0,0 +1,15 @@
type Translations = {
[key: string]: string;
};
export const prefixPluginTranslations = (trad: Translations, pluginId?: string) => {
if (!pluginId) {
throw new TypeError("pluginId can't be empty");
}
return Object.keys(trad).reduce((acc: Translations, current: string) => {
acc[`${pluginId}.${current}`] = trad[current];
return acc;
}, {});
};

View File

@ -1,6 +1,9 @@
// TODO: replace this import with the import from constants file when it will be migrated to TS
import { AssetSource } from '../newConstants';
import { typeFromMime } from './typeFromMime';
import type { RawFile } from '../../../shared/contracts/files';
export const rawFileToAsset = (rawFile, assetSource) => {
export const rawFileToAsset = (rawFile: RawFile, assetSource: AssetSource) => {
return {
size: rawFile.size / 1000,
createdAt: new Date(rawFile.lastModified).toISOString(),

View File

@ -1,4 +1,4 @@
import { appendSearchParamsToUrl } from '..';
import { appendSearchParamsToUrl } from '../appendSearchParamsToUrl';
describe('appendSearchParamsToUrl', () => {
const updateTime = '2023-07-19T03:00:00.000Z';
@ -47,7 +47,7 @@ describe('appendSearchParamsToUrl', () => {
});
describe('relativeURL', () => {
let originalBackendURL;
let originalBackendURL: string;
beforeAll(() => {
/**

View File

@ -1,4 +1,4 @@
import { containsAssetFilter } from '..';
import { containsAssetFilter } from '../containsAssetFilter';
describe('containsAssetFilter', () => {
test('does not fail on empty query objects', () => {

View File

@ -10,11 +10,11 @@ describe('downloadFile', () => {
documentSpy.mockReturnValue({
click: clickSpy,
set href(val) {
set href(val: string) {
hrefSpy(val);
},
setAttribute: setAttributeSpy,
});
} as unknown as HTMLAnchorElement);
await downloadFile('/some/file', 'my-filename');

View File

@ -1,4 +1,4 @@
import findRecursiveFolderByValue from '../findRecursiveFolderByValue';
import { findRecursiveFolderByValue } from '../findRecursiveFolderByValue';
const FIXTURE = [
{

View File

@ -1,51 +0,0 @@
import findRecursiveFolderMetadatas from '../findRecursiveFolderMetadatas';
const FIXTURE_STRUCTURE = {
value: null,
label: 'Media Library',
children: [
{
value: 1,
label: 'Cats',
children: [
{
value: 2,
label: 'Michka',
children: [],
},
],
},
],
};
describe('ML || utils || findRecursiveFolderMetadatas', () => {
test('should return parent folder id and label', () => {
const result = findRecursiveFolderMetadatas(FIXTURE_STRUCTURE, 2);
expect(result).toEqual({
parentId: 1,
currentFolderLabel: 'Michka',
});
});
test('should return parent id null if parent is root ML', () => {
const result = findRecursiveFolderMetadatas(FIXTURE_STRUCTURE, 1);
expect(result).toEqual({
currentFolderLabel: 'Cats',
parentId: null,
});
});
test('should return null if searched id does not exist', () => {
const result = findRecursiveFolderMetadatas(FIXTURE_STRUCTURE, 10);
expect(result).toEqual(null);
});
test('should return null if searched id does not exist (nullish)', () => {
const result = findRecursiveFolderMetadatas(FIXTURE_STRUCTURE, null);
expect(result).toEqual(null);
});
});

View File

@ -1,4 +1,4 @@
import formatBytes from '../formatBytes';
import { formatBytes } from '../formatBytes';
describe('UPLOAD | components | EditForm | utils', () => {
describe('formatBytes', () => {

View File

@ -1,6 +1,6 @@
import { FetchError } from '@strapi/admin/strapi-admin';
import getAPIInnerErrors from '../getAPIInnerErrors';
import { getAPIInnerErrors } from '../getAPIInnerErrors';
const API_VALIDATION_ERROR_FIXTURE = new FetchError('ValidationError', {
data: {
@ -24,7 +24,6 @@ const API_VALIDATION_ERROR_FIXTURE = new FetchError('ValidationError', {
},
},
},
status: 422,
});
const API_APPLICATION_ERROR_FIXTURE = new FetchError('ApplicationError', {
@ -35,7 +34,6 @@ const API_APPLICATION_ERROR_FIXTURE = new FetchError('ApplicationError', {
details: {},
},
},
status: 400,
});
describe('getAPIInnerError', () => {

View File

@ -1,153 +0,0 @@
import getAllowedFiles from '../getAllowedFiles';
const files = [
{
id: 1,
mime: 'application',
},
{
id: 2,
mime: 'application',
},
{
id: 3,
mime: 'image/png',
},
{
id: 4,
mime: 'video/mov',
},
{
id: 5,
mime: 'image/jpg',
},
{
id: 6,
mime: 'image/test',
},
{
id: 7,
mime: 'audio/mpeg',
},
{
id: 8,
mime: 'audio/x-wav',
},
{
id: 9,
mime: 'audio/ogg',
},
];
describe('UPLOAD | components | MediaLibraryInput | utils | getAllowedFiles', () => {
it('returns an empty array of when the allowed files is empty', () => {
const results = getAllowedFiles([], files);
expect(results).toEqual([]);
});
it('returns an array with elements that are not video or image when the allowedTypes is files', () => {
const results = getAllowedFiles(['files'], files);
expect(results).toEqual([
{
id: 1,
mime: 'application',
},
{
id: 2,
mime: 'application',
},
]);
});
it('returns an array with elements that are only video when the allowedTypes is videos', () => {
const results = getAllowedFiles(['videos'], files);
expect(results).toEqual([
{
id: 4,
mime: 'video/mov',
},
]);
});
it('returns an array with elements that are only video when the allowedTypes is videos', () => {
const results = getAllowedFiles(['audios'], files);
expect(results).toEqual([
{
id: 7,
mime: 'audio/mpeg',
},
{
id: 8,
mime: 'audio/x-wav',
},
{
id: 9,
mime: 'audio/ogg',
},
]);
});
it('returns an array with elements that are only image when the allowedTypes is images', () => {
const results = getAllowedFiles(['images'], files);
expect(results).toEqual([
{
id: 3,
mime: 'image/png',
},
{
id: 5,
mime: 'image/jpg',
},
{
id: 6,
mime: 'image/test',
},
]);
});
it('returns an array with elements that are image and video when the allowedTypes are videos and images', () => {
const results = getAllowedFiles(['videos', 'images', 'audios'], files);
expect(results).toEqual([
{
id: 3,
mime: 'image/png',
},
{
id: 4,
mime: 'video/mov',
},
{
id: 5,
mime: 'image/jpg',
},
{
id: 6,
mime: 'image/test',
},
{
id: 7,
mime: 'audio/mpeg',
},
{
id: 8,
mime: 'audio/x-wav',
},
{
id: 9,
mime: 'audio/ogg',
},
]);
});
it('returns an array with all the elements', () => {
const results = getAllowedFiles(['videos', 'images', 'files', 'audios'], files);
expect(results).toEqual(files);
});
});

View File

@ -0,0 +1,133 @@
import { getAllowedFiles } from '../getAllowedFiles';
const COMMON_PROPERTIES = {
size: 100,
createdAt: '2021-09-01T00:00:00.000Z',
updatedAt: '2021-09-01T00:00:00.000Z',
folder: null,
folderPath: '/',
documentId: 'documentId',
hash: 'hash',
locale: null,
provider: 'local',
isSelectable: true,
type: 'asset',
};
const FILE_1 = {
id: 1,
mime: 'application',
name: 'file.application',
url: '/uploads/file.application',
...COMMON_PROPERTIES,
};
const FILE_2 = {
id: 2,
mime: 'application',
name: 'file2.application',
url: '/uploads/file2.application',
...COMMON_PROPERTIES,
};
const FILE_3 = {
id: 3,
mime: 'image/png',
name: 'image.png',
url: '/uploads/image.png',
...COMMON_PROPERTIES,
};
const FILE_4 = {
id: 4,
mime: 'video/mov',
name: 'video.mov',
url: '/uploads/video.mov',
...COMMON_PROPERTIES,
};
const FILE_5 = {
id: 5,
mime: 'image/jpg',
name: 'image2.jpg',
url: '/uploads/image2.jpg',
...COMMON_PROPERTIES,
};
const FILE_6 = {
id: 6,
mime: 'image/test',
name: 'image.test',
url: '/uploads/image.test',
...COMMON_PROPERTIES,
};
const FILE_7 = {
id: 7,
mime: 'audio/mpeg',
name: 'audio.mpeg',
url: '/uploads/audio.mpeg',
...COMMON_PROPERTIES,
};
const FILE_8 = {
id: 8,
mime: 'audio/x-wav',
name: 'audio.x-wav',
url: '/uploads/audio.x-wav',
...COMMON_PROPERTIES,
};
const FILE_9 = {
id: 9,
mime: 'audio/ogg',
name: 'audio.ogg',
url: '/uploads/audio.ogg',
...COMMON_PROPERTIES,
};
const files = [FILE_1, FILE_2, FILE_3, FILE_4, FILE_5, FILE_6, FILE_7, FILE_8, FILE_9];
describe('UPLOAD | components | MediaLibraryInput | utils | getAllowedFiles', () => {
it('returns an empty array of when the allowed files is empty', () => {
const results = getAllowedFiles([], files);
expect(results).toEqual([]);
});
it('returns an array with elements that are not video or image when the allowedTypes is files', () => {
const results = getAllowedFiles(['files'], files);
expect(results).toEqual([FILE_1, FILE_2]);
});
it('returns an array with elements that are only video when the allowedTypes is videos', () => {
const results = getAllowedFiles(['videos'], files);
expect(results).toEqual([FILE_4]);
});
it('returns an array with elements that are only video when the allowedTypes is videos', () => {
const results = getAllowedFiles(['audios'], files);
expect(results).toEqual([FILE_7, FILE_8, FILE_9]);
});
it('returns an array with elements that are only image when the allowedTypes is images', () => {
const results = getAllowedFiles(['images'], files);
expect(results).toEqual([FILE_3, FILE_5, FILE_6]);
});
it('returns an array with elements that are image and video when the allowedTypes are videos and images', () => {
const results = getAllowedFiles(['videos', 'images', 'audios'], files);
expect(results).toEqual([FILE_3, FILE_4, FILE_5, FILE_6, FILE_7, FILE_8, FILE_9]);
});
it('returns an array with all the elements', () => {
const results = getAllowedFiles(['videos', 'images', 'files', 'audios'], files);
expect(results).toEqual(files);
});
});

View File

@ -1,9 +1,10 @@
import { getBreadcrumbDataCM } from '..';
import { getBreadcrumbDataCM } from '../getBreadcrumbDataCM';
const FIXTURE_FOLDER = {
id: 1,
name: 'first-level',
path: '/1 ',
pathId: 1,
};
describe('getBreadcrumbDataCM', () => {

View File

@ -1,4 +1,4 @@
import { getBreadcrumbDataML } from '..';
import { getBreadcrumbDataML } from '../getBreadcrumbDataML';
const FIXTURE_PATHNAME = '/media-library';
const FIXTURE_QUERY = {

View File

@ -1,11 +1,10 @@
import getFileExtension from '../getFileExtension';
import { getFileExtension } from '../getFileExtension';
describe('getFileExtension', () => {
it('should return undefined if ext does not exits', () => {
const ext = null;
const expected = null;
// @ts-expect-error ext should be a string so will throw error that ext is null.
expect(getFileExtension(ext)).toEqual(expected);
});

View File

@ -1,4 +1,4 @@
import getFolderParents from '../getFolderParents';
import { getFolderParents } from '../getFolderParents';
const FIXTURE_FOLDER_STRUCTURE = [
{

View File

@ -1,8 +1,8 @@
import { getFolderURL } from '..';
import { getFolderURL } from '../getFolderURL';
const FIXTURE_PATHNAME = '/media-library';
const FIXTURE_QUERY = {};
const FIXTURE_FOLDER = 1;
const FIXTURE_FOLDER = '1';
const FIXTURE_FOLDER_PATH = '/1/2/3';
describe('getFolderURL', () => {
@ -24,12 +24,8 @@ describe('getFolderURL', () => {
test('keeps and stringifies query parameter', () => {
expect(
getFolderURL(
FIXTURE_PATHNAME,
{ ...FIXTURE_QUERY, some: 'thing' },
{ folder: FIXTURE_FOLDER }
)
).toMatchInlineSnapshot(`"/media-library?some=thing&folder=1"`);
getFolderURL(FIXTURE_PATHNAME, { ...FIXTURE_QUERY }, { folder: FIXTURE_FOLDER })
).toMatchInlineSnapshot(`"/media-library?folder=1"`);
});
test('includes folderPath if provided', () => {

View File

@ -24,7 +24,6 @@ const API_VALIDATION_ERROR_FIXTURE = new FetchError('ValidationError', {
},
},
},
status: 422,
});
const API_APPLICATION_ERROR_FIXTURE = new FetchError('ApplicationError', {
@ -35,7 +34,6 @@ const API_APPLICATION_ERROR_FIXTURE = new FetchError('ApplicationError', {
details: {},
},
},
status: 400,
});
describe('normalizeAPIError', () => {
@ -66,7 +64,7 @@ describe('normalizeAPIError', () => {
});
test('Handle ValidationError with custom prefix function', () => {
const prefixFunction = (id) => `custom.${id}`;
const prefixFunction = (id: string) => `custom.${id}`;
expect(normalizeAPIError(API_VALIDATION_ERROR_FIXTURE, prefixFunction)).toStrictEqual({
name: 'ValidationError',
@ -103,7 +101,7 @@ describe('normalizeAPIError', () => {
});
test('Handle ApplicationError with custom prefix function', () => {
const prefixFunction = (id) => `custom.${id}`;
const prefixFunction = (id: string) => `custom.${id}`;
expect(normalizeAPIError(API_APPLICATION_ERROR_FIXTURE, prefixFunction)).toStrictEqual({
name: 'ApplicationError',

View File

@ -1,4 +1,4 @@
import prefixFileUrlWithBackendUrl from '../prefixFileUrlWithBackendUrl';
import { prefixFileUrlWithBackendUrl } from '../prefixFileUrlWithBackendUrl';
describe('prefixFileUrlWithBackendUrl', () => {
it("should add the strapi back-end url if the file's url startsWith '/'", () => {

View File

@ -1,4 +1,4 @@
import toSingularTypes from '../toSingularTypes';
import { toSingularTypes } from '../toSingularTypes';
describe('UPLOAD | utils | toSingularTypes', () => {
it('returns an array', () => {

View File

@ -1,4 +1,5 @@
import { AssetType } from '../../constants';
// TODO: replace this import with the import from constants file when it will be migrated to TS
import { AssetType } from '../../newConstants';
import { typeFromMime } from '../typeFromMime';
describe('typeFromMime', () => {

View File

@ -1,14 +0,0 @@
/**
* Transforms an arrays of plural type to singular one
* @param {Object[]} types
* @returns Object[]
*/
const toSingularTypes = (types) => {
if (!types) {
return [];
}
return types.map((type) => type.substring(0, type.length - 1));
};
export default toSingularTypes;

View File

@ -0,0 +1,9 @@
export const toSingularTypes = (types?: string[]) => {
if (!types) {
return [];
}
return types.map((type) => type.substring(0, type.length - 1));
};
export default toSingularTypes;

View File

@ -1,6 +1,7 @@
import { AssetType } from '../constants';
// TODO: replace this import with the import from constants file when it will be migrated to TS
import { AssetType } from '../newConstants';
export const typeFromMime = (mime) => {
export const typeFromMime = (mime: string) => {
if (mime.includes(AssetType.Image)) {
return AssetType.Image;
}

View File

@ -1,7 +1,7 @@
import { translatedErrors as errorsTrads } from '@strapi/admin/strapi-admin';
import * as yup from 'yup';
import getTrad from './getTrad';
import { getTrad } from './getTrad';
export const urlSchema = yup.object().shape({
urls: yup.string().test({

View File

@ -1,18 +1,19 @@
import { AssetSource } from '../constants';
// TODO: replace this import with the import from constants file when it will be migrated to TS
import { AssetSource } from '../newConstants';
import { typeFromMime } from './typeFromMime';
function getFilenameFromURL(url) {
function getFilenameFromURL(url: string) {
return new URL(url).pathname.split('/').pop();
}
export const urlsToAssets = async (urls) => {
export const urlsToAssets = async (urls: string[]) => {
const assetPromises = urls.map((url) =>
fetch(url).then(async (res) => {
const blob = await res.blob();
const loadedFile = new File([blob], getFilenameFromURL(res.url), {
type: res.headers.get('content-type'),
const loadedFile = new File([blob], getFilenameFromURL(res.url)!, {
type: res.headers.get('content-type') || undefined,
});
return {
@ -29,7 +30,7 @@ export const urlsToAssets = async (urls) => {
const assets = assetsResults.map((fullFilledAsset) => ({
source: AssetSource.Url,
name: fullFilledAsset.name,
type: typeFromMime(fullFilledAsset.mime),
type: typeFromMime(fullFilledAsset.mime!),
url: fullFilledAsset.url,
ext: fullFilledAsset.url.split('.').pop(),
mime: fullFilledAsset.mime,

View File

@ -5,6 +5,6 @@
"baseUrl": ".",
"outDir": "./dist"
},
"include": ["./src", "../shared", "../package.json"],
"include": ["./src", "./custom.d.ts", "../shared", "../package.json"],
"exclude": ["**/__mocks__", "./src/**/tests", "**/*.test.*"]
}

View File

@ -7,5 +7,5 @@
"@tests/*": ["./tests/*"]
}
},
"include": ["../package.json", "./src", "../shared", "./tests"]
"include": ["../package.json", "./src", "../shared", "./tests", "./custom.d.ts"]
}

View File

@ -87,6 +87,7 @@
"@testing-library/dom": "10.1.0",
"@testing-library/react": "15.0.7",
"@testing-library/user-event": "14.5.2",
"@types/byte-size": "8.1.2",
"@types/fs-extra": "11.0.4",
"@types/koa": "2.13.4",
"@types/koa-range": "0.3.5",

View File

@ -4,6 +4,58 @@ type SortOrder = 'ASC' | 'DESC';
type SortKey = 'createdAt' | 'name';
// Abstract type for comparison operators where the keys are generic strings
type ComparisonOperators<T> = {
[operator: string]: T | T[] | boolean; // Any string can be used as an operator key
};
// Abstract type for filter conditions with dynamic field names
export type FilterCondition<T> = {
[field: string]: T | ComparisonOperators<T> | FilterCondition<T>; // Field names are dynamic and values are comparison operators
};
// Abstract type for filters where the logical operator (like $and) is a generic string
type Filters<T> = {
[logicOperator: string]: FilterCondition<T>[]; // Logical operator key is a generic string
};
export type Query = {
_q?: string;
folderPath?: string;
folder?:
| null
| number
| {
id: number;
};
page?:
| string
| number
| {
id: string | number;
};
pageSize?: string | number;
pagination?: {
pageSize: number;
};
sort?: `${SortKey}:${SortOrder}`;
filters?: Filters<string | number | boolean>;
state?: boolean;
};
type FileFormat = {
name: string;
hash: string;
ext: string;
mime: string;
path: null | string;
width: number;
height: number;
size: number;
sizeInBytes: number;
url: string;
};
export interface File {
id: number;
name: string;
@ -11,7 +63,7 @@ export interface File {
caption?: string | null;
width?: number;
height?: number;
formats?: Record<string, unknown>;
formats?: Record<string, FileFormat>;
hash: string;
ext?: string;
mime?: string;
@ -23,7 +75,7 @@ export interface File {
provider?: string;
provider_metadata?: Record<string, unknown>;
isUrlSigned?: boolean;
folder?: number;
folder?: number | null;
folderPath?: string;
related?: {
id: string | number;
@ -33,12 +85,11 @@ export interface File {
createdAt?: string;
updatedAt?: string;
createdBy?: number;
publishedAt?: string;
updatedBy?: number;
isLocal?: boolean;
}
/**
* GET /upload/files - Get files
*/
export interface RawFile extends Blob {
size: number;
lastModified: number;
@ -53,6 +104,9 @@ export interface Pagination {
total: number;
}
/**
* GET /upload/files - Get files
*/
export declare namespace GetFiles {
export interface Request {
body: {};

View File

@ -9,20 +9,20 @@ import type { File } from './files';
export interface Folder {
id: number;
name: string;
pathId: number;
pathId?: number;
/**
* parent id
*/
parent?: number;
parent?: number | null | Folder;
/**
* children ids
*/
children?: number[];
path: string;
path?: string;
files?: File[];
}
type FolderNode = Partial<Folder> & {
export type FolderNode = Partial<Omit<Folder, 'children'>> & {
children: FolderNode[];
};

Some files were not shown because too many files have changed in this diff Show More