diff --git a/packages/core/upload/admin/src/pages/App/components/DocAssetCard.js b/packages/core/upload/admin/src/pages/App/components/DocAssetCard.js
new file mode 100644
index 0000000000..81a2d702f0
--- /dev/null
+++ b/packages/core/upload/admin/src/pages/App/components/DocAssetCard.js
@@ -0,0 +1,69 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import styled from 'styled-components';
+import {
+ Card,
+ CardAction,
+ CardAsset,
+ CardBadge,
+ CardBody,
+ CardCheckbox,
+ CardContent,
+ CardHeader,
+ CardTitle,
+ CardSubtitle,
+} from '@strapi/parts/Card';
+import { IconButton } from '@strapi/parts/IconButton';
+import EditIcon from '@strapi/icons/EditIcon';
+import IconDocumentation from '@strapi/icons/IconDocumentation';
+import { useIntl } from 'react-intl';
+import { getTrad } from '../../../utils';
+
+const Extension = styled.span`
+ text-transform: uppercase;
+`;
+
+const IconWrapper = styled.span`
+ svg {
+ font-size: 3rem;
+ }
+`;
+
+export const DocAssetCard = ({ name, extension }) => {
+ const { formatMessage } = useIntl();
+
+ return (
+
+
+
+
+ }
+ />
+
+
+
+
+
+
+
+
+
+ {name}
+
+ {extension.replace('.', '')}
+
+
+
+ {formatMessage({ id: getTrad('settings.section.doc.label'), defaultMessage: 'Doc' })}
+
+
+
+ );
+};
+
+DocAssetCard.propTypes = {
+ extension: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+};
diff --git a/packages/core/upload/admin/src/pages/App/components/ImageAssetCard.js b/packages/core/upload/admin/src/pages/App/components/ImageAssetCard.js
new file mode 100644
index 0000000000..c61c592a0b
--- /dev/null
+++ b/packages/core/upload/admin/src/pages/App/components/ImageAssetCard.js
@@ -0,0 +1,61 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import styled from 'styled-components';
+import {
+ Card,
+ CardAction,
+ CardAsset,
+ CardBadge,
+ CardBody,
+ CardCheckbox,
+ CardContent,
+ CardHeader,
+ CardTitle,
+ CardSubtitle,
+} from '@strapi/parts/Card';
+import { IconButton } from '@strapi/parts/IconButton';
+import EditIcon from '@strapi/icons/EditIcon';
+import { useIntl } from 'react-intl';
+import { getTrad } from '../../../utils';
+
+const Extension = styled.span`
+ text-transform: uppercase;
+`;
+
+export const ImageAssetCard = ({ name, extension, height, width, thumbnail }) => {
+ const { formatMessage } = useIntl();
+
+ return (
+
+
+
+
+ }
+ />
+
+
+
+
+
+ {name}
+
+ {extension.replace('.', '')} - {height}✕{width}
+
+
+
+ {formatMessage({ id: getTrad('settings.section.image.label'), defaultMessage: 'Image' })}
+
+
+
+ );
+};
+
+ImageAssetCard.propTypes = {
+ extension: PropTypes.string.isRequired,
+ height: PropTypes.number.isRequired,
+ name: PropTypes.string.isRequired,
+ width: PropTypes.number.isRequired,
+ thumbnail: PropTypes.string.isRequired,
+};
diff --git a/packages/core/upload/admin/src/pages/App/components/ListView.js b/packages/core/upload/admin/src/pages/App/components/ListView.js
index f48a6468b2..1550c21f06 100644
--- a/packages/core/upload/admin/src/pages/App/components/ListView.js
+++ b/packages/core/upload/admin/src/pages/App/components/ListView.js
@@ -1,9 +1,50 @@
import React from 'react';
import PropTypes from 'prop-types';
+import { GridLayout } from '@strapi/parts/Layout';
+import { KeyboardNavigable } from '@strapi/parts/KeyboardNavigable';
+import { ImageAssetCard } from './ImageAssetCard';
+import { VideoAssetCard } from './VideoAssetCard';
+import { DocAssetCard } from './DocAssetCard';
-// TODO: implement the view
export const ListView = ({ assets }) => {
- return
Number of assets: {assets.length}
;
+ return (
+
+
+ {assets.map(asset => {
+ if (asset.mime.includes('video')) {
+ return (
+
+ );
+ }
+
+ if (asset.mime.includes('image')) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+ })}
+
+
+ );
};
ListView.propTypes = {
diff --git a/packages/core/upload/admin/src/pages/App/components/VideoAssetCard.js b/packages/core/upload/admin/src/pages/App/components/VideoAssetCard.js
new file mode 100644
index 0000000000..fb41e24b67
--- /dev/null
+++ b/packages/core/upload/admin/src/pages/App/components/VideoAssetCard.js
@@ -0,0 +1,67 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+import styled from 'styled-components';
+import {
+ Card,
+ CardAction,
+ CardAsset,
+ CardBadge,
+ CardBody,
+ CardCheckbox,
+ CardContent,
+ CardHeader,
+ CardTitle,
+ CardSubtitle,
+ CardTimer,
+} from '@strapi/parts/Card';
+import { IconButton } from '@strapi/parts/IconButton';
+import EditIcon from '@strapi/icons/EditIcon';
+import { useIntl } from 'react-intl';
+import { VideoPreview } from './VideoPreview';
+import { getTrad, formatDuration } from '../../../utils';
+
+const Extension = styled.span`
+ text-transform: uppercase;
+`;
+
+export const VideoAssetCard = ({ name, extension, url, mime }) => {
+ const { formatMessage } = useIntl();
+ const [duration, setDuration] = useState();
+ const formattedDuration = duration ? formatDuration(duration) : undefined;
+
+ return (
+
+
+
+
+ }
+ />
+
+
+
+
+ {formattedDuration || '...'}
+
+
+
+ {name}
+
+ {extension.replace('.', '')}
+
+
+
+ {formatMessage({ id: getTrad('settings.section.video.label'), defaultMessage: 'Doc' })}
+
+
+
+ );
+};
+
+VideoAssetCard.propTypes = {
+ extension: PropTypes.string.isRequired,
+ mime: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ url: PropTypes.string.isRequired,
+};
diff --git a/packages/core/upload/admin/src/pages/App/components/VideoPreview.js b/packages/core/upload/admin/src/pages/App/components/VideoPreview.js
new file mode 100644
index 0000000000..f5474c8bda
--- /dev/null
+++ b/packages/core/upload/admin/src/pages/App/components/VideoPreview.js
@@ -0,0 +1,52 @@
+/* eslint-disable jsx-a11y/media-has-caption */
+import React, { useRef, useState } from 'react';
+import PropTypes from 'prop-types';
+import styled from 'styled-components';
+
+const VideoPreviewWrapper = styled.div`
+ canvas {
+ display: block;
+ max-width: 100%;
+ max-height: 100%;
+ }
+`;
+
+export const VideoPreview = ({ url, mime, onLoadDuration }) => {
+ const [loaded, setLoaded] = useState(false);
+ const videoRef = useRef(null);
+ const canvasRef = useRef(null);
+
+ const handleThumbnailVisibility = () => {
+ const video = videoRef.current;
+ const canvas = canvasRef.current;
+
+ canvas.height = video.videoHeight;
+ canvas.width = video.videoWidth;
+ canvas.getContext('2d').drawImage(video, 0, 0);
+
+ onLoadDuration(video.duration);
+ setLoaded(true);
+ };
+
+ return (
+
+ {!loaded && (
+
+ )}
+
+
+
+ );
+};
+
+VideoPreview.propTypes = {
+ url: PropTypes.string.isRequired,
+ mime: PropTypes.string.isRequired,
+ onLoadDuration: PropTypes.func.isRequired,
+};
diff --git a/packages/core/upload/admin/src/pages/App/components/tests/ListView.test.js b/packages/core/upload/admin/src/pages/App/components/tests/ListView.test.js
new file mode 100644
index 0000000000..96ea02eabc
--- /dev/null
+++ b/packages/core/upload/admin/src/pages/App/components/tests/ListView.test.js
@@ -0,0 +1,792 @@
+import React from 'react';
+import { ThemeProvider, lightTheme } from '@strapi/parts';
+import { render as renderTL } from '@testing-library/react';
+import { ListView } from '../ListView';
+import en from '../../../../translations/en.json';
+
+jest.mock('../../../../utils', () => ({
+ ...jest.requireActual('../../../../utils'),
+ getTrad: x => x,
+}));
+
+jest.mock('react-intl', () => ({
+ FormattedMessage: ({ id }) => id,
+ useIntl: () => ({ formatMessage: jest.fn(({ id }) => en[id]) }),
+}));
+
+const data = [
+ {
+ id: 1,
+ name: 'strapi-cover_1fabc982ce.png',
+ alternativeText: '',
+ caption: '',
+ width: 1066,
+ height: 551,
+ formats: {
+ thumbnail: {
+ name: 'thumbnail_strapi-cover_1fabc982ce.png',
+ hash: 'thumbnail_strapi_cover_1fabc982ce_5b43615ed5',
+ ext: '.png',
+ mime: 'image/png',
+ width: 245,
+ height: 127,
+ size: 3.37,
+ path: null,
+ url: '/uploads/thumbnail_strapi_cover_1fabc982ce_5b43615ed5.png',
+ },
+ large: {
+ name: 'large_strapi-cover_1fabc982ce.png',
+ hash: 'large_strapi_cover_1fabc982ce_5b43615ed5',
+ ext: '.png',
+ mime: 'image/png',
+ width: 1000,
+ height: 517,
+ size: 22.43,
+ path: null,
+ url: '/uploads/large_strapi_cover_1fabc982ce_5b43615ed5.png',
+ },
+ medium: {
+ name: 'medium_strapi-cover_1fabc982ce.png',
+ hash: 'medium_strapi_cover_1fabc982ce_5b43615ed5',
+ ext: '.png',
+ mime: 'image/png',
+ width: 750,
+ height: 388,
+ size: 14.62,
+ path: null,
+ url: '/uploads/medium_strapi_cover_1fabc982ce_5b43615ed5.png',
+ },
+ small: {
+ name: 'small_strapi-cover_1fabc982ce.png',
+ hash: 'small_strapi_cover_1fabc982ce_5b43615ed5',
+ ext: '.png',
+ mime: 'image/png',
+ width: 500,
+ height: 258,
+ size: 8.38,
+ path: null,
+ url: '/uploads/small_strapi_cover_1fabc982ce_5b43615ed5.png',
+ },
+ },
+ hash: 'strapi_cover_1fabc982ce_5b43615ed5',
+ ext: '.png',
+ mime: 'image/png',
+ size: 6.85,
+ url: '/uploads/strapi_cover_1fabc982ce_5b43615ed5.png',
+ previewUrl: null,
+ provider: 'local',
+ provider_metadata: null,
+ created_at: '2021-09-14T07:32:50.816Z',
+ updated_at: '2021-09-14T07:32:50.816Z',
+ },
+ {
+ id: 5,
+ name: 'mov_bbb.mp4',
+ alternativeText: '',
+ caption: '',
+ width: null,
+ height: null,
+ formats: null,
+ hash: 'mov_bbb_2f3907f7aa',
+ ext: '.mp4',
+ mime: 'video/mp4',
+ size: 788.49,
+ url: '/uploads/mov_bbb_2f3907f7aa.mp4',
+ previewUrl: null,
+ provider: 'local',
+ provider_metadata: null,
+ created_at: '2021-09-14T07:48:30.882Z',
+ updated_at: '2021-09-14T07:48:30.882Z',
+ },
+ {
+ id: 6,
+ name: 'CARTE MARIAGE AVS - Printemps.pdf',
+ alternativeText: '',
+ caption: '',
+ width: null,
+ height: null,
+ formats: null,
+ hash: 'CARTE_MARIAGE_AVS_Printemps_1f87b19e18',
+ ext: '.pdf',
+ mime: 'application/pdf',
+ size: 422.37,
+ url: '/uploads/CARTE_MARIAGE_AVS_Printemps_1f87b19e18.pdf',
+ previewUrl: null,
+ provider: 'local',
+ provider_metadata: null,
+ created_at: '2021-09-14T07:51:59.845Z',
+ updated_at: '2021-09-14T07:51:59.845Z',
+ },
+];
+
+describe('MediaLibrary / ListView', () => {
+ it('snapshots the listview', () => {
+ const { container } = renderTL(
+
+
+
+ );
+
+ expect(container).toMatchInlineSnapshot(`
+ .c29 {
+ border: 0;
+ -webkit-clip: rect(0 0 0 0);
+ clip: rect(0 0 0 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ width: 1px;
+ }
+
+ .c0 {
+ display: grid;
+ grid-template-columns: repeat(auto-fit,minmax(250px,1fr));
+ grid-gap: 16px;
+ }
+
+ .c1 {
+ background: #ffffff;
+ border-radius: 4px;
+ box-shadow: 0px 1px 4px rgba(33,33,52,0.1);
+ }
+
+ .c11 {
+ padding-top: 8px;
+ padding-right: 12px;
+ padding-bottom: 8px;
+ padding-left: 12px;
+ }
+
+ .c16 {
+ background: #f6f6f9;
+ color: #666687;
+ padding: 4px;
+ border-radius: 4px;
+ }
+
+ .c23 {
+ background: #32324d;
+ color: #ffffff;
+ padding: 4px;
+ border-radius: 4px;
+ }
+
+ .c4 {
+ position: absolute;
+ top: 12px;
+ left: 12px;
+ }
+
+ .c6 {
+ position: absolute;
+ top: 12px;
+ right: 12px;
+ }
+
+ .c2 {
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-direction: row;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ -webkit-box-pack: center;
+ -webkit-justify-content: center;
+ -ms-flex-pack: center;
+ justify-content: center;
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ }
+
+ .c12 {
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-direction: row;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ }
+
+ .c10 {
+ margin: 0;
+ padding: 0;
+ max-height: 100%;
+ max-width: 100%;
+ }
+
+ .c9 {
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-pack: center;
+ -webkit-justify-content: center;
+ -ms-flex-pack: center;
+ justify-content: center;
+ height: 10.25rem;
+ width: 100%;
+ background: repeating-conic-gradient(#f6f6f9 0% 25%,transparent 0% 50%) 50% / 20px 20px;
+ }
+
+ .c13 {
+ font-weight: 500;
+ font-size: 0.75rem;
+ line-height: 1.33;
+ color: #32324d;
+ }
+
+ .c14 {
+ font-weight: 400;
+ font-size: 0.75rem;
+ line-height: 1.33;
+ color: #666687;
+ }
+
+ .c19 {
+ font-weight: 400;
+ font-size: 0.875rem;
+ line-height: 1.43;
+ color: #32324d;
+ }
+
+ .c25 {
+ font-weight: 400;
+ font-size: 0.75rem;
+ line-height: 1.33;
+ color: #ffffff;
+ }
+
+ .c20 {
+ font-weight: 600;
+ line-height: 1.14;
+ }
+
+ .c21 {
+ font-weight: 600;
+ font-size: 0.6875rem;
+ line-height: 1.45;
+ text-transform: uppercase;
+ }
+
+ .c17 {
+ display: inline-block;
+ }
+
+ .c18 {
+ margin-left: auto;
+ }
+
+ .c5 {
+ margin: 0;
+ height: 18px;
+ min-width: 18px;
+ border-radius: 4px;
+ border: 1px solid #c0c0cf;
+ -webkit-appearance: none;
+ background-color: #ffffff;
+ }
+
+ .c5:checked {
+ background-color: #4945ff;
+ border: 1px solid #4945ff;
+ }
+
+ .c5:checked:after {
+ content: '';
+ display: block;
+ position: relative;
+ background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iOCIgdmlld0JveD0iMCAwIDEwIDgiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGgKICAgIGQ9Ik04LjU1MzIzIDAuMzk2OTczQzguNjMxMzUgMC4zMTYzNTUgOC43NjA1MSAwLjMxNTgxMSA4LjgzOTMxIDAuMzk1NzY4TDkuODYyNTYgMS40MzQwN0M5LjkzODkzIDEuNTExNTcgOS45MzkzNSAxLjYzNTkgOS44NjM0OSAxLjcxMzlMNC4wNjQwMSA3LjY3NzI0QzMuOTg1OSA3Ljc1NzU1IDMuODU3MDcgNy43NTgwNSAzLjc3ODM0IDcuNjc4MzRMMC4xMzg2NiAzLjk5MzMzQzAuMDYxNzc5OCAzLjkxNTQ5IDAuMDYxNzEwMiAzLjc5MDMyIDAuMTM4NTA0IDMuNzEyNEwxLjE2MjEzIDIuNjczNzJDMS4yNDAzOCAyLjU5NDMyIDEuMzY4NDMgMi41OTQyMiAxLjQ0NjggMi42NzM0OEwzLjkyMTc0IDUuMTc2NDdMOC41NTMyMyAwLjM5Njk3M1oiCiAgICBmaWxsPSJ3aGl0ZSIKICAvPgo8L3N2Zz4=) no-repeat no-repeat center center;
+ width: 10px;
+ height: 10px;
+ left: 50%;
+ top: 50%;
+ -webkit-transform: translateX(-50%) translateY(-50%);
+ -ms-transform: translateX(-50%) translateY(-50%);
+ transform: translateX(-50%) translateY(-50%);
+ }
+
+ .c5:checked:disabled:after {
+ background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iOCIgdmlld0JveD0iMCAwIDEwIDgiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGgKICAgIGQ9Ik04LjU1MzIzIDAuMzk2OTczQzguNjMxMzUgMC4zMTYzNTUgOC43NjA1MSAwLjMxNTgxMSA4LjgzOTMxIDAuMzk1NzY4TDkuODYyNTYgMS40MzQwN0M5LjkzODkzIDEuNTExNTcgOS45MzkzNSAxLjYzNTkgOS44NjM0OSAxLjcxMzlMNC4wNjQwMSA3LjY3NzI0QzMuOTg1OSA3Ljc1NzU1IDMuODU3MDcgNy43NTgwNSAzLjc3ODM0IDcuNjc4MzRMMC4xMzg2NiAzLjk5MzMzQzAuMDYxNzc5OCAzLjkxNTQ5IDAuMDYxNzEwMiAzLjc5MDMyIDAuMTM4NTA0IDMuNzEyNEwxLjE2MjEzIDIuNjczNzJDMS4yNDAzOCAyLjU5NDMyIDEuMzY4NDMgMi41OTQyMiAxLjQ0NjggMi42NzM0OEwzLjkyMTc0IDUuMTc2NDdMOC41NTMyMyAwLjM5Njk3M1oiCiAgICBmaWxsPSIjOEU4RUE5IgogIC8+Cjwvc3ZnPg==) no-repeat no-repeat center center;
+ }
+
+ .c5:disabled {
+ background-color: #dcdce4;
+ border: 1px solid #c0c0cf;
+ }
+
+ .c5:indeterminate {
+ background-color: #4945ff;
+ border: 1px solid #4945ff;
+ }
+
+ .c5:indeterminate:after {
+ content: '';
+ display: block;
+ position: relative;
+ color: white;
+ height: 2px;
+ width: 10px;
+ background-color: #ffffff;
+ left: 50%;
+ top: 50%;
+ -webkit-transform: translateX(-50%) translateY(-50%);
+ -ms-transform: translateX(-50%) translateY(-50%);
+ transform: translateX(-50%) translateY(-50%);
+ }
+
+ .c5:indeterminate:disabled {
+ background-color: #dcdce4;
+ border: 1px solid #c0c0cf;
+ }
+
+ .c5:indeterminate:disabled:after {
+ background-color: #8e8ea9;
+ }
+
+ .c3 {
+ position: relative;
+ border-bottom: 1px solid #eaeaef;
+ }
+
+ .c24 {
+ position: absolute;
+ bottom: 4px;
+ right: 4px;
+ }
+
+ .c7 {
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ cursor: pointer;
+ padding: 8px;
+ border-radius: 4px;
+ background: #ffffff;
+ border: 1px solid #dcdce4;
+ }
+
+ .c7 svg {
+ height: 12px;
+ width: 12px;
+ }
+
+ .c7 svg > g,
+ .c7 svg path {
+ fill: #ffffff;
+ }
+
+ .c7[aria-disabled='true'] {
+ pointer-events: none;
+ }
+
+ .c8 svg > g,
+ .c8 svg path {
+ fill: #8e8ea9;
+ }
+
+ .c8:hover svg > g,
+ .c8:hover svg path {
+ fill: #666687;
+ }
+
+ .c8:active svg > g,
+ .c8:active svg path {
+ fill: #a5a5ba;
+ }
+
+ .c8[aria-disabled='true'] {
+ background-color: #eaeaef;
+ }
+
+ .c8[aria-disabled='true'] svg path {
+ fill: #666687;
+ }
+
+ .c15 {
+ text-transform: uppercase;
+ }
+
+ .c22 canvas {
+ display: block;
+ max-width: 100%;
+ max-height: 100%;
+ }
+
+ .c26 {
+ text-transform: uppercase;
+ }
+
+ .c28 {
+ text-transform: uppercase;
+ }
+
+ .c27 svg {
+ font-size: 3rem;
+ }
+
+
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+
+
+
+ strapi-cover_1fabc982ce.png
+
+
+
+ png
+
+ -
+ 551
+ ✕
+ 1066
+
+
+
+
+ Image
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mov_bbb.mp4
+
+
+
+ mp4
+
+
+
+
+
+ Video
+
+
+
+
+
+
+
+
+
+
+
+ CARTE MARIAGE AVS - Printemps.pdf
+
+
+
+ pdf
+
+
+
+
+
+ Doc
+
+
+
+
+
+
+
+
+
+ `);
+ });
+});
diff --git a/packages/core/upload/admin/src/translations/en.json b/packages/core/upload/admin/src/translations/en.json
index 6235a99665..63352e9ef0 100644
--- a/packages/core/upload/admin/src/translations/en.json
+++ b/packages/core/upload/admin/src/translations/en.json
@@ -75,8 +75,9 @@
"settings.form.videoPreview.description": "It will generate a six-second preview of the video (GIF)",
"settings.form.videoPreview.label": "Preview",
"settings.header.label": "Media Library - Settings",
- "settings.section.image.label": "IMAGE",
- "settings.section.video.label": "VIDEO",
+ "settings.section.image.label": "Image",
+ "settings.section.video.label": "Video",
+ "settings.section.doc.label": "Doc",
"settings.sub-header.label": "Configure the settings for the media library",
"sort.created_at_asc": "Oldest uploads",
"sort.created_at_desc": "Most recent uploads",
diff --git a/packages/core/upload/admin/src/utils/formatDuration.js b/packages/core/upload/admin/src/utils/formatDuration.js
new file mode 100644
index 0000000000..98a84b2bb7
--- /dev/null
+++ b/packages/core/upload/admin/src/utils/formatDuration.js
@@ -0,0 +1,12 @@
+export const formatDuration = durationInSecond => {
+ const formatter = new Intl.DateTimeFormat('default', {
+ hour: 'numeric',
+ minute: 'numeric',
+ second: 'numeric',
+ });
+
+ const date = new Date(1970, 0, 1);
+ date.setSeconds(durationInSecond);
+
+ return formatter.format(date);
+};
diff --git a/packages/core/upload/admin/src/utils/index.js b/packages/core/upload/admin/src/utils/index.js
index f1f63f28bd..381fdc35da 100644
--- a/packages/core/upload/admin/src/utils/index.js
+++ b/packages/core/upload/admin/src/utils/index.js
@@ -18,3 +18,4 @@ export { default as getYupError } from './getYupError';
export { default as ItemTypes } from './ItemTypes';
export { default as unformatBytes } from './unformatBytes';
export { default as urlSchema } from './urlYupSchema';
+export * from './formatDuration';