diff --git a/docs/docs/docs/01-core/utils/async.md b/docs/docs/docs/01-core/utils/async.md index 7779f58ad1..285df739f8 100644 --- a/docs/docs/docs/01-core/utils/async.md +++ b/docs/docs/docs/01-core/utils/async.md @@ -12,17 +12,64 @@ Async utils are grouping all function that interact with async stuff like Promis ## Detailed design -Available functions: +### mapAsync -- pipeAsync -- mapAsync -- reduceAsync +The `mapAsync` function is an asynchronous version of the `Array.prototype.map` method. -[See API reference](../../../api/api.mdx) (TODO) +Example usage: + +```js +const input = [1, 2, 3]; + +const output = await mapAsync(input, async (item) => { + return item * 2; +}); + +console.log(output); // [2, 4, 6] +``` + +### reduceAsync + +The `reduceAsync` function is an asynchronous version of the `Array.prototype.reduce` method. + +Example usage: + +```js +const input = [1, 2, 3]; + +const reducer = reduceAsync(input); +const output = await reducer(async (accumulator, item) => { + return accumulator + item; +}, 0); + +console.log(output); // 6 +``` + +### pipeAsync + +The `pipeAsync` function is a utility function for composing asynchronous functions. It takes a list of functions as input, and returns a new function that applies each function in turn to the input. + +Example usage: + +```js +async function addOne(input: number): Promise { + return input + 1; +} + +async function double(input: number): Promise { + return input * 2; +} + +const addOneAndDouble = pipeAsync(addOne, double); + +const output = await addOneAndDouble(3); + +console.log(output); // 8 +``` ### When to use -Everytime the code has to act with promises and iterate other them, an async utils function should be used. +Every time the code has to act with promises and iterate other them, an async utils function should be used. ### Should I add my function here ? @@ -32,6 +79,12 @@ Please consider the next point if a lots of functions are available in the async ## Potential improvements +Some ideas of functions that could be added: + +- Other `Array.prototype` methods: `filterAsync`, `someAsync`, `everyAsync`, `findAsync`, `findIndexAsync`, `flatMapAsync`. +- `retryAsync`: A function that retries an asynchronous operation a specified number of times if it fails. It takes an asynchronous operation and a number of retries as input, and returns the result of the operation if it succeeds within the specified number of retries, or throws an error if it fails after all retries. +- `timeoutAsync`: A function that adds a timeout to an asynchronous operation. It takes an asynchronous operation and a timeout duration as input, and returns the result of the operation if it completes within the specified timeout, or throws an error if it takes longer than the timeout. + If we begin to use lots of async utils function, we may consider to migrate to a specialized library like [asyncjs](http://caolan.github.io/async/v3/) ## Resources diff --git a/packages/core/admin/admin/src/components/AuthenticatedApp/index.js b/packages/core/admin/admin/src/components/AuthenticatedApp/index.js index a0bb170e27..8a83736507 100644 --- a/packages/core/admin/admin/src/components/AuthenticatedApp/index.js +++ b/packages/core/admin/admin/src/components/AuthenticatedApp/index.js @@ -38,7 +38,7 @@ const AuthenticatedApp = () => { const [ { data: appInfos, status }, { data: tagName, isLoading }, - { data: permissions, status: fetchPermissionsStatus, refetch, isFetched, isFetching }, + { data: permissions, status: fetchPermissionsStatus, refetch, isFetching }, { data: userRoles }, ] = useQueries([ { queryKey: 'app-infos', queryFn: fetchAppInfo }, @@ -86,7 +86,7 @@ const AuthenticatedApp = () => { // We don't need to wait for the release query to be fetched before rendering the plugins // however, we need the appInfos and the permissions const shouldShowNotDependentQueriesLoader = - (isFetching && isFetched) || status === 'loading' || fetchPermissionsStatus === 'loading'; + isFetching || status === 'loading' || fetchPermissionsStatus === 'loading'; const shouldShowLoader = isLoading || shouldShowNotDependentQueriesLoader; diff --git a/packages/core/admin/admin/src/content-manager/pages/ListView/index.js b/packages/core/admin/admin/src/content-manager/pages/ListView/index.js index a4d0c6f3ee..1eca5b378a 100644 --- a/packages/core/admin/admin/src/content-manager/pages/ListView/index.js +++ b/packages/core/admin/admin/src/content-manager/pages/ListView/index.js @@ -33,6 +33,7 @@ import { } from '@strapi/helper-plugin'; import { ArrowLeft, Cog, Plus } from '@strapi/icons'; import axios from 'axios'; +import getReviewWorkflowsColumn from 'ee_else_ce/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/getTableColumn'; import isEqual from 'lodash/isEqual'; import PropTypes from 'prop-types'; import { stringify } from 'qs'; @@ -391,6 +392,17 @@ function ListView({ return formattedHeaders; } + // this should not exist. Ideally we would use registerHook() similar to what has been done + // in the i18n plugin. In order to do that review-workflows should have been a plugin. In + // a future iteration we need to find a better pattern. + + // In CE this will return null - in EE a column definition including the custom formatting component. + const reviewWorkflowColumn = getReviewWorkflowsColumn(layout); + + if (reviewWorkflowColumn) { + formattedHeaders.push(reviewWorkflowColumn); + } + return [ ...formattedHeaders, { diff --git a/packages/core/admin/admin/src/pages/SettingsPage/pages/Users/EditPage/index.js b/packages/core/admin/admin/src/pages/SettingsPage/pages/Users/EditPage/index.js index dbfec9e2f5..c859aeb1be 100644 --- a/packages/core/admin/admin/src/pages/SettingsPage/pages/Users/EditPage/index.js +++ b/packages/core/admin/admin/src/pages/SettingsPage/pages/Users/EditPage/index.js @@ -183,13 +183,13 @@ const EditPage = ({ canUpdate }) => { validateOnChange={false} validationSchema={editValidation} > - {({ errors, values, handleChange, isSubmitting }) => { + {({ errors, values, handleChange, isSubmitting, dirty }) => { return (
} loading={isSubmitting} type="submit" diff --git a/packages/core/upload/admin/src/components/Breadcrumbs/CrumbSimpleMenuAsync.js b/packages/core/upload/admin/src/components/Breadcrumbs/CrumbSimpleMenuAsync.js index 5b93d1b9ff..1eaf42d9ce 100644 --- a/packages/core/upload/admin/src/components/Breadcrumbs/CrumbSimpleMenuAsync.js +++ b/packages/core/upload/admin/src/components/Breadcrumbs/CrumbSimpleMenuAsync.js @@ -59,7 +59,10 @@ export const CrumbSimpleMenuAsync = ({ parentsToOmit, currentFolderId, onChangeF ); } - const url = getFolderURL(pathname, query, ascendant?.id); + const url = getFolderURL(pathname, query, { + folder: ascendant?.id, + folderPath: ascendant?.path, + }); return ( diff --git a/packages/core/upload/admin/src/hooks/tests/useAssets.test.js b/packages/core/upload/admin/src/hooks/tests/useAssets.test.js index 0096235e2f..6102fdafdd 100644 --- a/packages/core/upload/admin/src/hooks/tests/useAssets.test.js +++ b/packages/core/upload/admin/src/hooks/tests/useAssets.test.js @@ -82,10 +82,8 @@ describe('useAssets', () => { filters: { $and: [ { - folder: { - id: { - $null: true, - }, + folderPath: { + $eq: '/', }, }, ], @@ -100,7 +98,7 @@ describe('useAssets', () => { }); test('fetches data from the right URL if a query was set', async () => { - const { result } = await setup({ query: { folder: 1 } }); + const { result } = await setup({ query: { folderPath: '/1/2' } }); await waitFor(() => result.current.isSuccess); const { get } = useFetchClient(); @@ -109,8 +107,8 @@ describe('useAssets', () => { filters: { $and: [ { - folder: { - id: 1, + folderPath: { + $eq: '/1/2', }, }, ], @@ -126,7 +124,7 @@ describe('useAssets', () => { test('allows to merge filter query params using filters.$and', async () => { const { result } = await setup({ - query: { folder: 5, filters: { $and: [{ something: 'true' }] } }, + query: { folderPath: '/1/2', filters: { $and: [{ something: 'true' }] } }, }); await waitFor(() => result.current.isSuccess); @@ -139,8 +137,8 @@ describe('useAssets', () => { something: true, }, { - folder: { - id: 5, + folderPath: { + $eq: '/1/2', }, }, ], @@ -154,9 +152,9 @@ describe('useAssets', () => { ); }); - test('does not use folder filter in params if _q', async () => { + test('does not use folderPath filter in params if _q', async () => { const { result } = await setup({ - query: { folder: 5, _q: 'something', filters: { $and: [{ something: 'true' }] } }, + query: { folderPath: '/1/2', _q: 'something', filters: { $and: [{ something: 'true' }] } }, }); await waitFor(() => result.current.isSuccess); @@ -183,7 +181,7 @@ describe('useAssets', () => { test('correctly encodes the search query _q', async () => { const _q = 'something&else'; const { result } = await setup({ - query: { folder: 5, _q, filters: { $and: [{ something: 'true' }] } }, + query: { folderPath: '/1/2', _q, filters: { $and: [{ something: 'true' }] } }, }); await waitFor(() => result.current.isSuccess); diff --git a/packages/core/upload/admin/src/hooks/useAssets.js b/packages/core/upload/admin/src/hooks/useAssets.js index dd42149b54..dc5fe70ee9 100644 --- a/packages/core/upload/admin/src/hooks/useAssets.js +++ b/packages/core/upload/admin/src/hooks/useAssets.js @@ -1,3 +1,5 @@ +import { useEffect } from 'react'; + import { useNotifyAT } from '@strapi/design-system'; import { useFetchClient, useNotification } from '@strapi/helper-plugin'; import { stringify } from 'qs'; @@ -13,7 +15,7 @@ export const useAssets = ({ skipWhen = false, query = {} } = {}) => { const { notifyStatus } = useNotifyAT(); const { get } = useFetchClient(); const dataRequestURL = getRequestUrl('files'); - const { folder, _q, ...paramsExceptFolderAndQ } = query; + const { folderPath, _q, ...paramsExceptFolderAndQ } = query; let params; @@ -29,19 +31,16 @@ export const useAssets = ({ skipWhen = false, query = {} } = {}) => { $and: [ ...(paramsExceptFolderAndQ?.filters?.$and ?? []), { - folder: { - id: folder ?? { - $null: true, - }, - }, + folderPath: { $eq: folderPath ?? '/' }, }, ], }, }; } - const getAssets = async () => { - try { + const { data, error, isLoading } = useQuery( + [pluginId, 'assets', stringify(params)], + async () => { const { data } = await get( `${dataRequestURL}${stringify(params, { encode: false, @@ -49,54 +48,59 @@ export const useAssets = ({ skipWhen = false, query = {} } = {}) => { })}` ); + return data; + }, + { + enabled: !skipWhen, + staleTime: 0, + cacheTime: 0, + select(data) { + if (data?.results && Array.isArray(data.results)) { + return { + ...data, + results: data.results + /** + * Filter out assets that don't have a name. + * So we don't try to render them as assets + * and get errors. + */ + .filter((asset) => asset.name) + .map((asset) => ({ + ...asset, + /** + * Mime and ext cannot be null in the front-end because + * we expect them to be strings and use the `includes` method. + */ + mime: asset.mime ?? '', + ext: asset.ext ?? '', + })), + }; + } + + return data; + }, + } + ); + + useEffect(() => { + if (data) { notifyStatus( formatMessage({ id: 'list.asset.at.finished', defaultMessage: 'The assets have finished loading.', }) ); + } + }, [data, formatMessage, notifyStatus]); - return data; - } catch (err) { + useEffect(() => { + if (error) { toggleNotification({ type: 'warning', message: { id: 'notification.error' }, }); - - throw err; } - }; - - const { data, error, isLoading } = useQuery([pluginId, 'assets', stringify(params)], getAssets, { - enabled: !skipWhen, - staleTime: 0, - cacheTime: 0, - select(data) { - if (data?.results && Array.isArray(data.results)) { - return { - ...data, - results: data.results - /** - * Filter out assets that don't have a name. - * So we don't try to render them as assets - * and get errors. - */ - .filter((asset) => asset.name) - .map((asset) => ({ - ...asset, - /** - * Mime and ext cannot be null in the front-end because - * we expect them to be strings and use the `includes` method. - */ - mime: asset.mime ?? '', - ext: asset.ext ?? '', - })), - }; - } - - return data; - }, - }); + }, [error, toggleNotification]); return { data, error, isLoading }; }; diff --git a/packages/core/upload/admin/src/pages/App/MediaLibrary/components/Header.js b/packages/core/upload/admin/src/pages/App/MediaLibrary/components/Header.js index e54b1f920d..893c50c692 100644 --- a/packages/core/upload/admin/src/pages/App/MediaLibrary/components/Header.js +++ b/packages/core/upload/admin/src/pages/App/MediaLibrary/components/Header.js @@ -25,6 +25,7 @@ export const Header = ({ const backQuery = { ...query, folder: folder?.parent?.id ?? undefined, + folderPath: folder?.parent?.path ?? undefined, }; return ( diff --git a/packages/core/upload/admin/src/pages/App/MediaLibrary/components/tests/__snapshots__/Header.test.js.snap b/packages/core/upload/admin/src/pages/App/MediaLibrary/components/tests/__snapshots__/Header.test.js.snap index 9701b63fa7..da36b0b80a 100644 --- a/packages/core/upload/admin/src/pages/App/MediaLibrary/components/tests/__snapshots__/Header.test.js.snap +++ b/packages/core/upload/admin/src/pages/App/MediaLibrary/components/tests/__snapshots__/Header.test.js.snap @@ -366,7 +366,7 @@ exports[`Header renders 1`] = `