mirror of
https://github.com/strapi/strapi.git
synced 2025-11-02 10:55:37 +00:00
Merge branch 'main' into fix/issue-9308-File_upload_related_fields_returning_null
This commit is contained in:
commit
14ba46950b
@ -116,7 +116,7 @@ filter out the relation from the array of relations. This is handled inside the
|
||||
|
||||
:::note
|
||||
Connecting relations adds the item to the end of the list, whilst loading more relations prepends to
|
||||
the beginning of the list. This is the expected behaviour.
|
||||
the beginning of the list. This is the expected behaviour, to keep the order of the list in the UI in sync with the API response.
|
||||
:::
|
||||
|
||||
The `RelationInput` component takes the field in `modifiedData` as its source of truth. You could therefore consider this to
|
||||
@ -126,7 +126,7 @@ data for the api.
|
||||
|
||||
### Cleaning data to be posted to the API
|
||||
|
||||
The API to update the enttiy expects relations to be categorised into two groups, a `connect` array and `disconnect` array.
|
||||
The API to update the entity expects relations to be categorised into two groups, a `connect` array and `disconnect` array.
|
||||
You could do this as the user interacts with the input but we found this to be confusing and then involved us managing three
|
||||
different arrays which makes the code more complex. Instead, because the browser doesn't really care about whats new and removed
|
||||
and we have a copy of the slice of data we're mutating from the server we can run a small diff algorithm to determine which
|
||||
@ -142,3 +142,85 @@ relations have been connected and which have been disconnected. Returning an obj
|
||||
```
|
||||
|
||||
## Frontend component architecture
|
||||
|
||||
The input field for relation fields consist of two components:
|
||||
|
||||
### `RelationInputDataManager`
|
||||
|
||||
This container component handles data fetching and data normalization for the `RelationInput` component. This has been extracted from
|
||||
the `RelationInput` so that Strapi is able to move the underlying component into the design-system if the community would need it
|
||||
(most other input components can be consumed from there).
|
||||
|
||||
### `RelationInput`
|
||||
|
||||
This component is the presentational counterpart to the `RelationInputDataManager` component. It renders an input field based on the data passed from the data manager.
|
||||
|
||||
Under the hood it is using `react-window` to render a list of relations in a virtualized view. Some fields need to render thousands of relations, which
|
||||
would otherwise have a negative impact on the overall performance of the content-manager.
|
||||
|
||||
## useRelation() hook
|
||||
|
||||
This hook takes care of data-fetching and normalizes results relations aswell as search-results.
|
||||
|
||||
```ts
|
||||
const { relations: RelationResults, search: RelationResults, searchFor } = useRelation(reactQueryCacheKey: string, options: Options);
|
||||
```
|
||||
|
||||
### `Options`
|
||||
|
||||
`option`s is a mandatory configuration and should implement the following shape:
|
||||
|
||||
```ts
|
||||
type Options = {
|
||||
name: string; // name of the relation field
|
||||
relation: RelationConfiguration;
|
||||
search: SearchConfiguration;
|
||||
}
|
||||
|
||||
type RelationConfiguration = {
|
||||
endpoint: string; // URL from where existing relations should be fetched
|
||||
enabled: boolean; // defines whether relations should be fetched once the hook is called
|
||||
pageParams: object; // additional query params which will be appended to `endpoint`
|
||||
onLoad: (results: RelationResult[]) => void; // callback that will be fired after relations have been fetched (paginated)
|
||||
normalizeArguments = {
|
||||
mainFieldName: string; // name of the target model main field, determining which field to display (fallback: id)
|
||||
shouldAddLink: boolean; // if the user is allowed to read the target model, the returned relations should include a link to the target
|
||||
targetModel: object; // target content-type model
|
||||
};
|
||||
pageGoal: number; // the current page-count of the already loaded relations used to keep the redux store and query cache in sync.
|
||||
}
|
||||
|
||||
type SearchConfiguration = {
|
||||
endpoint: string; // URL from where new relations should be fetched
|
||||
pageParams: object; // additional query params which will be appended to `endpoint`
|
||||
}
|
||||
```
|
||||
|
||||
### Return values
|
||||
|
||||
`relations` and `search` both return a consistent relation format:
|
||||
|
||||
```ts
|
||||
type RelationResults = RelationResult[];
|
||||
|
||||
type RelationResult = {
|
||||
id: number;
|
||||
href?: string; // based on `shouldAddLink` and the `targetModel`
|
||||
publicationState: 'draft' | 'published';
|
||||
mainField: string; // will fallback to "id" if not set
|
||||
}
|
||||
```
|
||||
|
||||
#### `relations`
|
||||
|
||||
`relations` refers to a [inifinite-query return type](https://tanstack.com/query/v4/docs/react/guides/infinite-queries) from react-query. It exposes paginated relational data
|
||||
aswell as methods to check if there are more pages or fetch more paginated results. Relations for a given field are fetched as soon as the hook is called.
|
||||
|
||||
#### `search`
|
||||
|
||||
`search` refers to a [inifinite-query return type](https://tanstack.com/query/v4/docs/react/guides/infinite-queries) from react-query. It exposes paginated search results
|
||||
for a relational field. Search results are only fetched after `searchFor()` has been called.
|
||||
|
||||
#### `searchFor(string)`
|
||||
|
||||
`searchFor` is a method which can be used to search for entities which haven't been connected with the source entity yet. The method accepts a search-term: `searchFor("term")`.
|
||||
|
||||
@ -14,7 +14,7 @@ The following example shows a basic way to use the `useFetchClient` hook to make
|
||||
|
||||
```jsx
|
||||
import {useState} from "react"
|
||||
import useFetchClient from '@strapi/admin/admin/src/hooks/useFetchClient';
|
||||
import { useFetchClient } from '@strapi/helper-plugin';
|
||||
|
||||
const Component = () => {
|
||||
const [items, setItems] = useState([]);
|
||||
@ -57,7 +57,7 @@ The following information is the internal additions we've added to the axios ins
|
||||
|
||||
### Base URL
|
||||
|
||||
The default URL will be the one defined in the environment variable: `STRAPI_ADMIN_BACKEND_URL`.
|
||||
The default URL is the one defined in the _getFetchClient_ utility
|
||||
|
||||
### Interceptors
|
||||
|
||||
@ -68,8 +68,6 @@ The request interceptor adds the following parameters to the header:
|
||||
```js
|
||||
{
|
||||
Authorization: `Bearer <AUTH_TOKEN>`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ import { Link } from '@strapi/design-system/v2/Link';
|
||||
|
||||
const Notification = ({ dispatch, notification }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { message, link, type, id, onClose, timeout, blockTransition } = notification;
|
||||
const { message, link, type, id, onClose, timeout, blockTransition, title } = notification;
|
||||
|
||||
const formattedMessage = (msg) =>
|
||||
typeof msg === 'string' ? msg : formatMessage(msg, msg.values);
|
||||
@ -37,6 +37,7 @@ const Notification = ({ dispatch, notification }) => {
|
||||
let variant;
|
||||
let alertTitle;
|
||||
|
||||
// TODO break out this logic into separate file
|
||||
if (type === 'info') {
|
||||
variant = 'default';
|
||||
alertTitle = formatMessage({
|
||||
@ -44,17 +45,29 @@ const Notification = ({ dispatch, notification }) => {
|
||||
defaultMessage: 'Information:',
|
||||
});
|
||||
} else if (type === 'warning') {
|
||||
// type should be renamed to danger in the future, but it might introduce changes if done now
|
||||
variant = 'danger';
|
||||
alertTitle = formatMessage({
|
||||
id: 'notification.warning.title',
|
||||
defaultMessage: 'Warning:',
|
||||
});
|
||||
} else if (type === 'softWarning') {
|
||||
// type should be renamed to just warning in the future
|
||||
variant = 'warning';
|
||||
alertTitle = formatMessage({
|
||||
id: 'notification.warning.title',
|
||||
defaultMessage: 'Warning:',
|
||||
});
|
||||
variant = 'danger';
|
||||
} else {
|
||||
variant = 'success';
|
||||
alertTitle = formatMessage({
|
||||
id: 'notification.success.title',
|
||||
defaultMessage: 'Success:',
|
||||
});
|
||||
variant = 'success';
|
||||
}
|
||||
|
||||
if (title) {
|
||||
alertTitle = typeof title === 'string' ? title : formatMessage(title);
|
||||
}
|
||||
|
||||
return (
|
||||
@ -125,6 +138,14 @@ Notification.propTypes = {
|
||||
onClose: PropTypes.func,
|
||||
timeout: PropTypes.number,
|
||||
blockTransition: PropTypes.bool,
|
||||
title: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
defaultMessage: PropTypes.string,
|
||||
values: PropTypes.object,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
};
|
||||
|
||||
|
||||
@ -23,6 +23,7 @@ const notificationReducer = (state = initialState, action) =>
|
||||
timeout: get(action, ['config', 'timeout'], 2500),
|
||||
blockTransition: get(action, ['config', 'blockTransition'], false),
|
||||
onClose: get(action, ['config', 'onClose'], null),
|
||||
title: get(action, ['config', 'title'], null),
|
||||
});
|
||||
draftState.notifId = state.notifId + 1;
|
||||
break;
|
||||
|
||||
@ -36,6 +36,7 @@ describe('ADMIN | COMPONENTS | NOTIFICATIONS | reducer', () => {
|
||||
timeout: 2500,
|
||||
blockTransition: false,
|
||||
onClose: null,
|
||||
title: null,
|
||||
},
|
||||
],
|
||||
notifId: 1,
|
||||
|
||||
@ -10,4 +10,3 @@ export { default as usePermissionsDataManager } from './usePermissionsDataManage
|
||||
export { default as useReleaseNotification } from './useReleaseNotification';
|
||||
export { default as useThemeToggle } from './useThemeToggle';
|
||||
export { default as useRegenerate } from './useRegenerate';
|
||||
export { default as useFetchClient } from './useFetchClient';
|
||||
|
||||
@ -14,6 +14,7 @@ import {
|
||||
TrackingProvider,
|
||||
prefixFileUrlWithBackendUrl,
|
||||
useAppInfos,
|
||||
useFetchClient,
|
||||
} from '@strapi/helper-plugin';
|
||||
import axios from 'axios';
|
||||
import { SkipToContent } from '@strapi/design-system/Main';
|
||||
@ -25,7 +26,7 @@ import NotFoundPage from '../NotFoundPage';
|
||||
import UseCasePage from '../UseCasePage';
|
||||
import { getUID } from './utils';
|
||||
import routes from './utils/routes';
|
||||
import { useConfigurations, useFetchClient } from '../../hooks';
|
||||
import { useConfigurations } from '../../hooks';
|
||||
|
||||
const AuthenticatedApp = lazy(() =>
|
||||
import(/* webpackChunkName: "Admin-authenticatedApp" */ '../../components/AuthenticatedApp')
|
||||
|
||||
@ -3,3 +3,7 @@
|
||||
## Description
|
||||
|
||||
Helper to develop Strapi plugins.
|
||||
|
||||
## Contributing
|
||||
|
||||
Please read our [Contributing Guide](../../../CONTRIBUTING.md) before submitting a Pull Request to the project.
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
/**
|
||||
*
|
||||
* useFetchClient
|
||||
*
|
||||
*/
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { getFetchClient } from '../../utils/getFetchClient';
|
||||
import getFetchClient from '../../utils/getFetchClient';
|
||||
|
||||
const useFetchClient = () => {
|
||||
const controller = useRef(null);
|
||||
@ -7,6 +12,7 @@ const useFetchClient = () => {
|
||||
if (controller.current === null) {
|
||||
controller.current = new AbortController();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
controller.current.abort();
|
||||
@ -20,7 +20,7 @@ const HomePage = () => {
|
||||
const handleClick = () => {
|
||||
toggleNotification({
|
||||
// required
|
||||
type: 'info|success|warning',
|
||||
type: 'info|success|warning|softWarning',
|
||||
// required
|
||||
message: { id: 'notification.version.update.message', defaultMessage: 'A new version is available' },
|
||||
// optional
|
||||
@ -35,6 +35,8 @@ const HomePage = () => {
|
||||
blockTransition: true,
|
||||
// optional
|
||||
onClose: () => localStorage.setItem('STRAPI_UPDATE_NOTIF', true),
|
||||
// optional
|
||||
title: { id: 'notification.default.title, defaultMessage: 'Warning: '}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -29,6 +29,7 @@ export { default as useRBAC } from './hooks/useRBAC';
|
||||
export { default as usePersistentState } from './hooks/usePersistentState';
|
||||
export { default as useFocusWhenNavigate } from './hooks/useFocusWhenNavigate';
|
||||
export { default as useLockScroll } from './hooks/useLockScroll';
|
||||
export { default as useFetchClient } from './hooks/useFetchClient';
|
||||
|
||||
// Providers
|
||||
export { default as GuidedTourProvider } from './providers/GuidedTourProvider';
|
||||
@ -100,3 +101,4 @@ export { default as wrapAxiosInstance } from './utils/wrapAxiosInstance';
|
||||
export { default as request } from './utils/request';
|
||||
export { default as getAPIInnerErrors } from './utils/getAPIInnerErrors';
|
||||
export { default as getYupInnerErrors } from './utils/getYupInnerErrors';
|
||||
export { default as getFetchClient } from './utils/getFetchClient';
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
import axios from 'axios';
|
||||
import { auth } from '@strapi/helper-plugin';
|
||||
import auth from '../auth';
|
||||
|
||||
export const reqInterceptor = async (config) => {
|
||||
config.headers = {
|
||||
Authorization: `Bearer ${auth.getToken()}`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
return config;
|
||||
@ -33,13 +31,16 @@ export const addInterceptors = (instance) => {
|
||||
instance.interceptors.response.use(resInterceptor, resErrorInterceptor);
|
||||
};
|
||||
|
||||
export const fetchClient = ({ baseURL }) => {
|
||||
export const fetchClient = () => {
|
||||
const instance = axios.create({
|
||||
baseURL,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
addInterceptors(instance);
|
||||
|
||||
return instance;
|
||||
};
|
||||
|
||||
export default fetchClient({ baseURL: process.env.STRAPI_ADMIN_BACKEND_URL });
|
||||
export default fetchClient();
|
||||
@ -1,5 +1,5 @@
|
||||
import { AxiosError } from 'axios';
|
||||
import { auth } from '@strapi/helper-plugin';
|
||||
import auth from '../../auth';
|
||||
import {
|
||||
reqInterceptor,
|
||||
reqErrorInterceptor,
|
||||
@ -7,13 +7,13 @@ import {
|
||||
resErrorInterceptor,
|
||||
fetchClient,
|
||||
addInterceptors,
|
||||
} from '../fetchClient';
|
||||
} from '../index';
|
||||
|
||||
const token = 'coolToken';
|
||||
auth.getToken = jest.fn().mockReturnValue(token);
|
||||
auth.clearAppStorage = jest.fn().mockReturnValue(token);
|
||||
|
||||
describe('ADMIN | utils | fetchClient', () => {
|
||||
describe('HELPER-PLUGIN | utils | fetchClient', () => {
|
||||
describe('Test the interceptors', () => {
|
||||
it('API request should add authorization token to header', async () => {
|
||||
const apiInstance = fetchClient({
|
||||
@ -21,7 +21,6 @@ describe('ADMIN | utils | fetchClient', () => {
|
||||
});
|
||||
const result = await apiInstance.interceptors.request.handlers[0].fulfilled({ headers: {} });
|
||||
expect(result.headers.Authorization).toContain(`Bearer ${token}`);
|
||||
expect(result.headers.Accept).toBe('application/json');
|
||||
expect(apiInstance.interceptors.response.handlers[0].fulfilled('foo')).toBe('foo');
|
||||
});
|
||||
describe('Test the addInterceptor function', () => {
|
||||
@ -1,6 +1,7 @@
|
||||
import instance from './fetchClient';
|
||||
import instance from '../fetchClient';
|
||||
|
||||
export const getFetchClient = (defaultOptions = {}) => {
|
||||
const getFetchClient = (defaultOptions = {}) => {
|
||||
instance.defaults.baseURL = window.strapi.backendURL;
|
||||
return {
|
||||
get: (url, config) => instance.get(url, { ...defaultOptions, ...config }),
|
||||
put: (url, data, config) => instance.put(url, data, { ...defaultOptions, ...config }),
|
||||
@ -8,3 +9,5 @@ export const getFetchClient = (defaultOptions = {}) => {
|
||||
del: (url, config) => instance.delete(url, { ...defaultOptions, ...config }),
|
||||
};
|
||||
};
|
||||
|
||||
export default getFetchClient;
|
||||
@ -1,10 +1,10 @@
|
||||
import { auth } from '@strapi/helper-plugin';
|
||||
import { getFetchClient } from '../getFetchClient';
|
||||
import auth from '../../auth';
|
||||
import getFetchClient from '../index';
|
||||
|
||||
const token = 'coolToken';
|
||||
auth.getToken = jest.fn().mockReturnValue(token);
|
||||
|
||||
describe('ADMIN | utils | getFetchClient', () => {
|
||||
describe('HELPER-PLUGIN | utils | getFetchClient', () => {
|
||||
it('should return the 4 HTTP methods to call GET, POST, PUT and DELETE apis', () => {
|
||||
const response = getFetchClient();
|
||||
expect(response).toHaveProperty('get');
|
||||
@ -19,7 +19,6 @@ describe('ADMIN | utils | getFetchClient', () => {
|
||||
} catch (err) {
|
||||
const { headers } = err.config;
|
||||
expect(headers.Authorization).toContain(`Bearer ${token}`);
|
||||
expect(headers.Accept).toBe('application/json');
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -5,7 +5,7 @@ function wrapAxiosInstance(instance) {
|
||||
(methodName) => {
|
||||
wrapper[methodName] = (...args) => {
|
||||
console.log(
|
||||
'Deprecation warning: Usage of "axiosInstance" utility is deprecated and will be removed in the next major release. Instead, use the useFetchClient() hook, which is exported from the admin: { useFetchClient } from "@strapi/helper-plugin"'
|
||||
'Deprecation warning: Usage of "axiosInstance" utility is deprecated and will be removed in the next major release. Instead, use the useFetchClient() hook, which is exported from the helper-plugin: { useFetchClient } from "@strapi/helper-plugin"'
|
||||
);
|
||||
|
||||
return instance[methodName](...args);
|
||||
|
||||
@ -8,35 +8,18 @@ import { Typography } from '@strapi/design-system/Typography';
|
||||
import { PreviewCell } from './PreviewCell';
|
||||
import { formatBytes } from '../../utils';
|
||||
|
||||
export const CellContent = ({
|
||||
alternativeText,
|
||||
content,
|
||||
cellType,
|
||||
elementType,
|
||||
mime,
|
||||
fileExtension,
|
||||
thumbnailURL,
|
||||
url,
|
||||
}) => {
|
||||
export const CellContent = ({ cellType, contentType, content, name }) => {
|
||||
const { formatDate, formatMessage } = useIntl();
|
||||
|
||||
switch (cellType) {
|
||||
case 'image':
|
||||
return (
|
||||
<PreviewCell
|
||||
alternativeText={alternativeText}
|
||||
fileExtension={fileExtension}
|
||||
mime={mime}
|
||||
type={elementType}
|
||||
thumbnailURL={thumbnailURL}
|
||||
url={url}
|
||||
/>
|
||||
);
|
||||
return <PreviewCell type={contentType} content={content} />;
|
||||
|
||||
case 'date':
|
||||
return <Typography>{formatDate(parseISO(content), { dateStyle: 'full' })}</Typography>;
|
||||
return <Typography>{formatDate(parseISO(content[name]), { dateStyle: 'full' })}</Typography>;
|
||||
|
||||
case 'size':
|
||||
if (elementType === 'folder')
|
||||
if (contentType === 'folder')
|
||||
return (
|
||||
<Typography
|
||||
aria-label={formatMessage({
|
||||
@ -48,10 +31,10 @@ export const CellContent = ({
|
||||
</Typography>
|
||||
);
|
||||
|
||||
return <Typography>{formatBytes(content)}</Typography>;
|
||||
return <Typography>{formatBytes(content[name])}</Typography>;
|
||||
|
||||
case 'ext':
|
||||
if (elementType === 'folder')
|
||||
if (contentType === 'folder')
|
||||
return (
|
||||
<Typography
|
||||
aria-label={formatMessage({
|
||||
@ -63,10 +46,10 @@ export const CellContent = ({
|
||||
</Typography>
|
||||
);
|
||||
|
||||
return <Typography>{getFileExtension(content).toUpperCase()}</Typography>;
|
||||
return <Typography>{getFileExtension(content[name]).toUpperCase()}</Typography>;
|
||||
|
||||
case 'text':
|
||||
return <Typography>{content}</Typography>;
|
||||
return <Typography>{content[name]}</Typography>;
|
||||
|
||||
default:
|
||||
return (
|
||||
@ -82,22 +65,19 @@ export const CellContent = ({
|
||||
}
|
||||
};
|
||||
|
||||
CellContent.defaultProps = {
|
||||
alternativeText: null,
|
||||
content: '',
|
||||
fileExtension: '',
|
||||
mime: '',
|
||||
thumbnailURL: null,
|
||||
url: null,
|
||||
};
|
||||
|
||||
CellContent.propTypes = {
|
||||
alternativeText: PropTypes.string,
|
||||
content: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
fileExtension: PropTypes.string,
|
||||
mime: PropTypes.string,
|
||||
thumbnailURL: PropTypes.string,
|
||||
cellType: PropTypes.string.isRequired,
|
||||
elementType: PropTypes.string.isRequired,
|
||||
url: PropTypes.string,
|
||||
contentType: PropTypes.string.isRequired,
|
||||
content: PropTypes.shape({
|
||||
alternativeText: PropTypes.string,
|
||||
ext: PropTypes.string,
|
||||
formats: PropTypes.shape({
|
||||
thumbnail: PropTypes.shape({
|
||||
url: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
mime: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
}).isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
@ -1,69 +1,80 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import { prefixFileUrlWithBackendUrl, pxToRem } from '@strapi/helper-plugin';
|
||||
import { Avatar } from '@strapi/design-system/Avatar';
|
||||
import { Flex } from '@strapi/design-system/Flex';
|
||||
import { Typography } from '@strapi/design-system/Typography';
|
||||
import { getFileExtension, prefixFileUrlWithBackendUrl, pxToRem } from '@strapi/helper-plugin';
|
||||
import { Avatar, Initials } from '@strapi/design-system/Avatar';
|
||||
import { Box } from '@strapi/design-system/Box';
|
||||
import { Icon } from '@strapi/design-system/Icon';
|
||||
import Folder from '@strapi/icons/Folder';
|
||||
|
||||
const GenericAssetWrapper = styled(Flex)`
|
||||
span {
|
||||
/* The smallest fontSize in the DS is not small enough in this case */
|
||||
font-size: ${pxToRem(10)};
|
||||
import { AssetType } from '../../constants';
|
||||
import { createAssetUrl } from '../../utils';
|
||||
import { VideoPreview } from '../AssetCard/VideoPreview';
|
||||
|
||||
const VideoPreviewWrapper = styled(Box)`
|
||||
figure {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
canvas,
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
}
|
||||
`;
|
||||
|
||||
export const PreviewCell = ({ alternativeText, fileExtension, mime, thumbnailURL, type, url }) => {
|
||||
export const PreviewCell = ({ type, content }) => {
|
||||
if (type === 'folder') {
|
||||
return (
|
||||
<Flex
|
||||
background="secondary100"
|
||||
height={pxToRem(26)}
|
||||
justifyContent="center"
|
||||
width={pxToRem(26)}
|
||||
borderRadius="50%"
|
||||
>
|
||||
<Icon color="secondary500" as={Folder} />
|
||||
</Flex>
|
||||
<Initials background="secondary100" textColor="secondary600">
|
||||
<Icon color="secondary500" width={pxToRem(16)} height={pxToRem(16)} as={Folder} />
|
||||
</Initials>
|
||||
);
|
||||
}
|
||||
|
||||
if (mime.includes('image')) {
|
||||
const mediaURL = prefixFileUrlWithBackendUrl(thumbnailURL) ?? prefixFileUrlWithBackendUrl(url);
|
||||
const { alternativeText, ext, formats, mime, name, url } = content;
|
||||
|
||||
if (mime.includes(AssetType.Image)) {
|
||||
const mediaURL =
|
||||
prefixFileUrlWithBackendUrl(formats?.thumbnail?.url) ?? prefixFileUrlWithBackendUrl(url);
|
||||
|
||||
return <Avatar src={mediaURL} alt={alternativeText} preview />;
|
||||
}
|
||||
|
||||
if (mime.includes(AssetType.Video)) {
|
||||
return (
|
||||
<VideoPreviewWrapper>
|
||||
<VideoPreview
|
||||
url={createAssetUrl(content, true)}
|
||||
mime={mime}
|
||||
alt={alternativeText ?? name}
|
||||
/>
|
||||
</VideoPreviewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<GenericAssetWrapper
|
||||
background="secondary100"
|
||||
height={pxToRem(26)}
|
||||
justifyContent="center"
|
||||
width={pxToRem(26)}
|
||||
borderRadius="50%"
|
||||
>
|
||||
<Typography variant="sigma" textColor="secondary600">
|
||||
{fileExtension}
|
||||
</Typography>
|
||||
</GenericAssetWrapper>
|
||||
<Initials background="secondary100" textColor="secondary600">
|
||||
{getFileExtension(ext)}
|
||||
</Initials>
|
||||
);
|
||||
};
|
||||
|
||||
PreviewCell.defaultProps = {
|
||||
alternativeText: null,
|
||||
fileExtension: '',
|
||||
mime: '',
|
||||
thumbnailURL: null,
|
||||
url: null,
|
||||
};
|
||||
|
||||
PreviewCell.propTypes = {
|
||||
alternativeText: PropTypes.string,
|
||||
fileExtension: PropTypes.string,
|
||||
mime: PropTypes.string,
|
||||
thumbnailURL: PropTypes.string,
|
||||
content: PropTypes.shape({
|
||||
alternativeText: PropTypes.string,
|
||||
ext: PropTypes.string,
|
||||
formats: PropTypes.shape({
|
||||
thumbnail: PropTypes.shape({
|
||||
url: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
mime: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
}).isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
url: PropTypes.string,
|
||||
};
|
||||
|
||||
@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { getFileExtension, onRowClick, stopPropagation } from '@strapi/helper-plugin';
|
||||
import { onRowClick, stopPropagation } from '@strapi/helper-plugin';
|
||||
import { BaseCheckbox } from '@strapi/design-system/BaseCheckbox';
|
||||
import { Flex } from '@strapi/design-system/Flex';
|
||||
import { IconButton } from '@strapi/design-system/IconButton';
|
||||
@ -35,18 +35,7 @@ export const TableRows = ({
|
||||
return (
|
||||
<Tbody>
|
||||
{rows.map((element) => {
|
||||
const {
|
||||
alternativeText,
|
||||
id,
|
||||
isSelectable,
|
||||
name,
|
||||
ext,
|
||||
url,
|
||||
mime,
|
||||
folderURL,
|
||||
formats,
|
||||
type: elementType,
|
||||
} = element;
|
||||
const { id, isSelectable, name, folderURL, type: contentType } = element;
|
||||
|
||||
const isSelected = !!selected.find((currentRow) => currentRow.id === id);
|
||||
|
||||
@ -54,16 +43,16 @@ export const TableRows = ({
|
||||
<Tr
|
||||
key={id}
|
||||
{...onRowClick({
|
||||
fn: () => handleRowClickFn(element, elementType, id),
|
||||
fn: () => handleRowClickFn(element, contentType, id),
|
||||
})}
|
||||
>
|
||||
<Td {...stopPropagation}>
|
||||
<BaseCheckbox
|
||||
aria-label={formatMessage(
|
||||
{
|
||||
id: elementType === 'asset' ? 'list-assets-select' : 'list.folder.select',
|
||||
id: contentType === 'asset' ? 'list-assets-select' : 'list.folder.select',
|
||||
defaultMessage:
|
||||
elementType === 'asset' ? 'Select {name} asset' : 'Select {name} folder',
|
||||
contentType === 'asset' ? 'Select {name} asset' : 'Select {name} folder',
|
||||
},
|
||||
{ name }
|
||||
)}
|
||||
@ -76,14 +65,10 @@ export const TableRows = ({
|
||||
return (
|
||||
<Td key={name}>
|
||||
<CellContent
|
||||
alternativeText={alternativeText}
|
||||
content={element[name]}
|
||||
fileExtension={getFileExtension(ext)}
|
||||
mime={mime}
|
||||
content={element}
|
||||
cellType={cellType}
|
||||
elementType={elementType}
|
||||
thumbnailURL={formats?.thumbnail?.url}
|
||||
url={url}
|
||||
contentType={contentType}
|
||||
name={name}
|
||||
/>
|
||||
</Td>
|
||||
);
|
||||
@ -91,7 +76,7 @@ export const TableRows = ({
|
||||
|
||||
<Td {...stopPropagation}>
|
||||
<Flex justifyContent="flex-end">
|
||||
{elementType === 'folder' && (
|
||||
{contentType === 'folder' && (
|
||||
<IconButton
|
||||
forwardedAs={folderURL ? Link : undefined}
|
||||
label={formatMessage({
|
||||
@ -111,7 +96,7 @@ export const TableRows = ({
|
||||
defaultMessage: 'Edit',
|
||||
})}
|
||||
onClick={() =>
|
||||
elementType === 'asset' ? onEditAsset(element) : onEditFolder(element)
|
||||
contentType === 'asset' ? onEditAsset(element) : onEditFolder(element)
|
||||
}
|
||||
noBorder
|
||||
>
|
||||
|
||||
@ -6,14 +6,20 @@ import { ThemeProvider, lightTheme } from '@strapi/design-system';
|
||||
import { CellContent } from '../CellContent';
|
||||
|
||||
const PROPS_FIXTURE = {
|
||||
alternativeText: 'alternative alt',
|
||||
cellType: 'image',
|
||||
elementType: 'asset',
|
||||
content: 'michka-picture-url-default.jpeg',
|
||||
fileExtension: '.jpeg',
|
||||
mime: 'image/jpeg',
|
||||
thumbnailURL: 'michka-picture-url-thumbnail.jpeg',
|
||||
url: 'michka-picture-url-default.jpeg',
|
||||
contentType: 'asset',
|
||||
content: {
|
||||
alternativeText: 'alternative alt',
|
||||
ext: 'jpeg',
|
||||
formats: {
|
||||
thumbnail: {
|
||||
url: 'michka-picture-url-thumbnail.jpeg',
|
||||
},
|
||||
},
|
||||
mime: 'image/jpeg',
|
||||
url: 'michka-picture-url-default.jpeg',
|
||||
},
|
||||
name: 'preview',
|
||||
};
|
||||
|
||||
const ComponentFixture = (props) => {
|
||||
@ -42,49 +48,63 @@ describe('TableList | CellContent', () => {
|
||||
});
|
||||
|
||||
it('should render image cell type when element type is asset and mime does not include image', () => {
|
||||
const { container, getByText } = setup({ mime: 'application/pdf', fileExtension: 'pdf' });
|
||||
const { container, getByText } = setup({
|
||||
content: { ...PROPS_FIXTURE.element, mime: 'application/pdf', ext: 'pdf' },
|
||||
});
|
||||
|
||||
expect(getByText('pdf')).toBeInTheDocument();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render image cell type when element type is folder', () => {
|
||||
const { container } = setup({ elementType: 'folder' });
|
||||
const { container } = setup({ contentType: 'folder' });
|
||||
|
||||
expect(container.querySelector('path')).toBeInTheDocument();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render text cell type', () => {
|
||||
const { container, getByText } = setup({ cellType: 'text', content: 'some text' });
|
||||
const { container, getByText } = setup({
|
||||
cellType: 'text',
|
||||
content: { ...PROPS_FIXTURE.content, name: 'some text' },
|
||||
name: 'name',
|
||||
});
|
||||
|
||||
expect(getByText('some text')).toBeInTheDocument();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render extension cell type when element type is asset', () => {
|
||||
const { container, getByText } = setup({ cellType: 'ext', content: '.pdf' });
|
||||
const { container, getByText } = setup({
|
||||
cellType: 'ext',
|
||||
content: { ...PROPS_FIXTURE.content, ext: '.pdf' },
|
||||
name: 'ext',
|
||||
});
|
||||
|
||||
expect(getByText('PDF')).toBeInTheDocument();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render extension cell type with "-" when element type is folder', () => {
|
||||
const { container, getByText } = setup({ cellType: 'ext', elementType: 'folder' });
|
||||
const { container, getByText } = setup({ cellType: 'ext', contentType: 'folder' });
|
||||
|
||||
expect(getByText('-')).toBeInTheDocument();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render size cell type when element type is asset', () => {
|
||||
const { container, getByText } = setup({ cellType: 'size', content: '20.5435' });
|
||||
const { container, getByText } = setup({
|
||||
cellType: 'size',
|
||||
content: { ...PROPS_FIXTURE.content, size: '20.5435' },
|
||||
name: 'size',
|
||||
});
|
||||
|
||||
expect(getByText('21KB')).toBeInTheDocument();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render size cell type with "-" when element type is folder', () => {
|
||||
const { container, getByText } = setup({ cellType: 'size', elementType: 'folder' });
|
||||
const { container, getByText } = setup({ cellType: 'size', contentType: 'folder' });
|
||||
|
||||
expect(getByText('-')).toBeInTheDocument();
|
||||
expect(container).toMatchSnapshot();
|
||||
@ -93,7 +113,8 @@ describe('TableList | CellContent', () => {
|
||||
it('should render date cell type', () => {
|
||||
const { container, getByText } = setup({
|
||||
cellType: 'date',
|
||||
content: '2022-11-18T12:08:02.202Z',
|
||||
content: { ...PROPS_FIXTURE.content, updatedAt: '2022-11-18T12:08:02.202Z' },
|
||||
name: 'updatedAt',
|
||||
});
|
||||
|
||||
expect(getByText('Friday, November 18, 2022')).toBeInTheDocument();
|
||||
|
||||
@ -5,12 +5,17 @@ import { ThemeProvider, lightTheme } from '@strapi/design-system';
|
||||
import { PreviewCell } from '../PreviewCell';
|
||||
|
||||
const PROPS_FIXTURE = {
|
||||
alternativeText: 'alternative alt',
|
||||
fileExtension: 'jpeg',
|
||||
mime: 'image/jpeg',
|
||||
name: 'michka',
|
||||
thumbnailURL: 'michka-picture-url-thumbnail.jpeg',
|
||||
url: 'michka-picture-url-default.jpeg',
|
||||
content: {
|
||||
alternativeText: 'alternative alt',
|
||||
ext: 'jpeg',
|
||||
formats: {
|
||||
thumbnail: {
|
||||
url: 'michka-picture-url-thumbnail.jpeg',
|
||||
},
|
||||
},
|
||||
mime: 'image/jpeg',
|
||||
url: 'michka-picture-url-default.jpeg',
|
||||
},
|
||||
type: 'asset',
|
||||
};
|
||||
|
||||
@ -41,7 +46,7 @@ describe('TableList | PreviewCell', () => {
|
||||
});
|
||||
|
||||
it('should render an image with default url if thumbnail is not available', () => {
|
||||
const { getByRole } = setup({ thumbnailURL: undefined });
|
||||
const { getByRole } = setup({ content: { ...PROPS_FIXTURE.content, formats: undefined } });
|
||||
|
||||
expect(getByRole('img', { name: 'alternative alt' })).toHaveAttribute(
|
||||
'src',
|
||||
@ -59,7 +64,9 @@ describe('TableList | PreviewCell', () => {
|
||||
|
||||
describe('rendering files', () => {
|
||||
it('should render a file with fileExtension', () => {
|
||||
const { getByText } = setup({ mime: 'application/pdf', fileExtension: 'pdf' });
|
||||
const { getByText } = setup({
|
||||
content: { ...PROPS_FIXTURE.content, mime: 'application/pdf', ext: 'pdf' },
|
||||
});
|
||||
|
||||
expect(getByText('pdf')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@ -218,8 +218,8 @@ exports[`TableList | CellContent should render image cell type when element type
|
||||
.c0 {
|
||||
background: #eaf5ff;
|
||||
border-radius: 50%;
|
||||
width: 1.625rem;
|
||||
height: 1.625rem;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
@ -241,22 +241,23 @@ exports[`TableList | CellContent should render image cell type when element type
|
||||
}
|
||||
|
||||
.c3 {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.43;
|
||||
font-weight: 600;
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.45;
|
||||
text-transform: uppercase;
|
||||
color: #0c75af;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.c2 span {
|
||||
font-size: 0.625rem;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="c0 c1 c2"
|
||||
height="1.625rem"
|
||||
width="1.625rem"
|
||||
height="26px"
|
||||
width="26px"
|
||||
>
|
||||
<span
|
||||
class="c3"
|
||||
@ -355,7 +356,7 @@ exports[`TableList | CellContent should render image cell type when element type
|
||||
`;
|
||||
|
||||
exports[`TableList | CellContent should render image cell type when element type is folder 1`] = `
|
||||
.c4 {
|
||||
.c6 {
|
||||
border: 0;
|
||||
-webkit-clip: rect(0 0 0 0);
|
||||
clip: rect(0 0 0 0);
|
||||
@ -370,12 +371,14 @@ exports[`TableList | CellContent should render image cell type when element type
|
||||
.c0 {
|
||||
background: #eaf5ff;
|
||||
border-radius: 50%;
|
||||
width: 1.625rem;
|
||||
height: 1.625rem;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
.c4 {
|
||||
color: #66b7f1;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
@ -396,32 +399,49 @@ exports[`TableList | CellContent should render image cell type when element type
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.c3 path {
|
||||
.c3 {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.43;
|
||||
font-weight: 600;
|
||||
font-size: 0.6875rem;
|
||||
color: #0c75af;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.c5 path {
|
||||
fill: #66b7f1;
|
||||
}
|
||||
|
||||
.c2 span {
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="c0 c1"
|
||||
height="1.625rem"
|
||||
width="1.625rem"
|
||||
class="c0 c1 c2"
|
||||
height="26px"
|
||||
width="26px"
|
||||
>
|
||||
<svg
|
||||
class="c2 c3"
|
||||
fill="none"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
<span
|
||||
class="c3"
|
||||
>
|
||||
<path
|
||||
d="M12.414 5H21a1 1 0 011 1v14a1 1 0 01-1 1H3a1 1 0 01-1-1V4a1 1 0 011-1h7.414l2 2z"
|
||||
fill="#212134"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
class="c4 c5"
|
||||
fill="none"
|
||||
height="1rem"
|
||||
viewBox="0 0 24 24"
|
||||
width="1rem"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12.414 5H21a1 1 0 011 1v14a1 1 0 01-1 1H3a1 1 0 01-1-1V4a1 1 0 011-1h7.414l2 2z"
|
||||
fill="#212134"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="c4"
|
||||
class="c6"
|
||||
>
|
||||
<p
|
||||
aria-live="polite"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user