From 0971f99e98407fcf8cbd41f1ab416ae869557f04 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Fri, 10 Mar 2023 16:35:25 +0800 Subject: [PATCH] feat(console): image uploader (#3323) --- packages/console/package.json | 1 + packages/console/src/assets/images/upload.svg | 3 + .../components/FileUploader/index.module.scss | 70 ++++++++++ .../src/components/FileUploader/index.tsx | 122 ++++++++++++++++++ .../ImageUploader/index.module.scss | 28 ++++ .../src/components/ImageUploader/index.tsx | 50 +++++++ packages/core/src/routes/user-assets.ts | 56 +++++--- packages/core/src/utils/storage/consts.ts | 13 -- .../translation/admin-console/components.ts | 15 +++ .../de/translation/admin-console/index.ts | 2 + .../translation/admin-console/components.ts | 15 +++ .../en/translation/admin-console/index.ts | 2 + .../translation/admin-console/components.ts | 15 +++ .../fr/translation/admin-console/index.ts | 2 + .../translation/admin-console/components.ts | 15 +++ .../ko/translation/admin-console/index.ts | 2 + .../translation/admin-console/components.ts | 15 +++ .../pt-br/translation/admin-console/index.ts | 2 + .../translation/admin-console/components.ts | 15 +++ .../pt-pt/translation/admin-console/index.ts | 2 + .../translation/admin-console/components.ts | 15 +++ .../tr-tr/translation/admin-console/index.ts | 2 + .../translation/admin-console/components.ts | 15 +++ .../zh-cn/translation/admin-console/index.ts | 2 + packages/schemas/src/types/index.ts | 1 + packages/schemas/src/types/user-assets.ts | 33 +++++ pnpm-lock.yaml | 26 ++++ 27 files changed, 506 insertions(+), 33 deletions(-) create mode 100644 packages/console/src/assets/images/upload.svg create mode 100644 packages/console/src/components/FileUploader/index.module.scss create mode 100644 packages/console/src/components/FileUploader/index.tsx create mode 100644 packages/console/src/components/ImageUploader/index.module.scss create mode 100644 packages/console/src/components/ImageUploader/index.tsx create mode 100644 packages/phrases/src/locales/de/translation/admin-console/components.ts create mode 100644 packages/phrases/src/locales/en/translation/admin-console/components.ts create mode 100644 packages/phrases/src/locales/fr/translation/admin-console/components.ts create mode 100644 packages/phrases/src/locales/ko/translation/admin-console/components.ts create mode 100644 packages/phrases/src/locales/pt-br/translation/admin-console/components.ts create mode 100644 packages/phrases/src/locales/pt-pt/translation/admin-console/components.ts create mode 100644 packages/phrases/src/locales/tr-tr/translation/admin-console/components.ts create mode 100644 packages/phrases/src/locales/zh-cn/translation/admin-console/components.ts create mode 100644 packages/schemas/src/types/user-assets.ts diff --git a/packages/console/package.json b/packages/console/package.json index 096357ee6..4b2fa9a7c 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -75,6 +75,7 @@ "react-dnd": "^16.0.0", "react-dnd-html5-backend": "^16.0.0", "react-dom": "^18.0.0", + "react-dropzone": "^14.2.3", "react-hook-form": "^7.34.0", "react-hot-toast": "^2.2.0", "react-i18next": "^11.18.3", diff --git a/packages/console/src/assets/images/upload.svg b/packages/console/src/assets/images/upload.svg new file mode 100644 index 000000000..945d61bcb --- /dev/null +++ b/packages/console/src/assets/images/upload.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/console/src/components/FileUploader/index.module.scss b/packages/console/src/components/FileUploader/index.module.scss new file mode 100644 index 000000000..8afbaaf11 --- /dev/null +++ b/packages/console/src/components/FileUploader/index.module.scss @@ -0,0 +1,70 @@ +@use '@/scss/underscore' as _; + +.uploader { + border: 1px dashed var(--color-border); + border-radius: 8px; + padding: _.unit(5.5); + + > input { + display: none; + } + + .placeholder { + display: flex; + flex-direction: column; + align-items: center; + + .icon { + color: var(--color-text-secondary); + } + + .uploadingIcon { + width: 20px; + height: 20px; + color: var(--color-primary); + } + + .actionDescription { + margin-top: _.unit(1); + font: var(--font-body-2); + user-select: none; + } + } + + &:hover { + cursor: pointer; + border-color: var(--color-primary); + + .placeholder { + .icon { + color: var(--color-primary); + } + } + } + + &.dragActive { + cursor: copy; + background-color: var(--color-hover-variant); + border-color: var(--color-primary); + + .placeholder { + .icon { + color: var(--color-primary); + } + } + } + + &.uploaderError { + border-color: var(--color-error); + } +} + +.description { + font: var(--font-body-2); + color: var(--color-text-secondary); + margin-top: _.unit(2); +} + +.error { + color: var(--color-error); +} diff --git a/packages/console/src/components/FileUploader/index.tsx b/packages/console/src/components/FileUploader/index.tsx new file mode 100644 index 000000000..d1903f42f --- /dev/null +++ b/packages/console/src/components/FileUploader/index.tsx @@ -0,0 +1,122 @@ +import type { UserAssetsResponse, AllowedUploadMimeType } from '@logto/schemas'; +import { maxUploadFileSize } from '@logto/schemas'; +import classNames from 'classnames'; +import { useCallback, useState } from 'react'; +import { useDropzone } from 'react-dropzone'; +import { useTranslation } from 'react-i18next'; + +import UploaderIcon from '@/assets/images/upload.svg'; +import useApi from '@/hooks/use-api'; + +import { Ring } from '../Spinner'; +import * as styles from './index.module.scss'; + +const allowedFileCount = 1; + +type Props = { + maxSize: number; // In bytes + allowedMimeTypes: AllowedUploadMimeType[]; + limitDescription: string; + onCompleted: (fileUrl: string) => void; +}; + +const FileUploader = ({ maxSize, allowedMimeTypes, limitDescription, onCompleted }: Props) => { + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + const [isUploading, setIsUploading] = useState(false); + const [uploaderError, setUploaderError] = useState(); + const hasError = Boolean(uploaderError); + + const api = useApi(); + + const onDrop = useCallback( + async (acceptedFiles: File[]) => { + setUploaderError(undefined); + + if (acceptedFiles.length > allowedFileCount) { + setUploaderError(t('components.uploader.error_file_count', { count: allowedFileCount })); + + return; + } + + const selectedFile = acceptedFiles[0]; + + if (!selectedFile) { + return; + } + + if (!allowedMimeTypes.map(String).includes(selectedFile.type)) { + const supportedFileTypes = allowedMimeTypes.map((type) => + type.split('/')[1]?.toUpperCase() + ); + setUploaderError(t('components.uploader.error_file_type', { types: supportedFileTypes })); + + return; + } + + const fileSizeLimit = Math.min(maxSize, maxUploadFileSize); + + if (selectedFile.size > fileSizeLimit) { + setUploaderError(t('components.uploader.error_file_size', { count: fileSizeLimit / 1024 })); + + return; + } + + const formData = new FormData(); + formData.append('file', selectedFile); + + try { + setIsUploading(true); + const { url } = await api + .post('api/user-assets', { body: formData }) + .json(); + + onCompleted(url); + } catch { + setUploaderError(t('components.uploader.error_upload')); + } finally { + setIsUploading(false); + } + }, + [allowedMimeTypes, api, maxSize, onCompleted, t] + ); + + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + onDrop, + disabled: isUploading, + }); + + return ( +
+
+ +
+ {isUploading ? ( + <> + +
{t('components.uploader.uploading')}
+ + ) : ( + <> + +
+ {t('components.uploader.action_description')} +
+ + )} +
+
+
+ {hasError ? uploaderError : limitDescription} +
+
+ ); +}; + +export default FileUploader; diff --git a/packages/console/src/components/ImageUploader/index.module.scss b/packages/console/src/components/ImageUploader/index.module.scss new file mode 100644 index 000000000..e26e1dcdd --- /dev/null +++ b/packages/console/src/components/ImageUploader/index.module.scss @@ -0,0 +1,28 @@ +@use '@/scss/underscore' as _; + +.imageUploader { + border: 1px dashed var(--color-border); + border-radius: 8px; + padding: _.unit(6) 0; + display: flex; + flex-direction: column; + align-items: center; + position: relative; + + .delete { + position: absolute; + right: _.unit(2); + bottom: _.unit(2); + } + + > img { + height: 40px; + object-fit: cover; + } +} + +.limit { + margin-top: _.unit(2); + font: var(--font-body-2); + color: var(--color-text-secondary); +} diff --git a/packages/console/src/components/ImageUploader/index.tsx b/packages/console/src/components/ImageUploader/index.tsx new file mode 100644 index 000000000..72652cc15 --- /dev/null +++ b/packages/console/src/components/ImageUploader/index.tsx @@ -0,0 +1,50 @@ +import { useTranslation } from 'react-i18next'; + +import Delete from '@/assets/images/delete.svg'; + +import FileUploader from '../FileUploader'; +import IconButton from '../IconButton'; +import * as styles from './index.module.scss'; + +type Props = { + name: string; + value: string; + onChange: (value: string) => void; +}; + +const maxImageSize = 500 * 1024; // 500 KB + +const ImageUploader = ({ name, value, onChange }: Props) => { + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + const limitDescription = t('components.uploader.image_limit'); + + if (!value) { + return ( + + ); + } + + return ( +
+
+ {name} + { + onChange(''); + }} + > + + +
+
{limitDescription}
+
+ ); +}; + +export default ImageUploader; diff --git a/packages/core/src/routes/user-assets.ts b/packages/core/src/routes/user-assets.ts index 775d23f17..440283dd6 100644 --- a/packages/core/src/routes/user-assets.ts +++ b/packages/core/src/routes/user-assets.ts @@ -1,6 +1,13 @@ import { readFile } from 'fs/promises'; import { generateStandardId } from '@logto/core-kit'; +import type { UserAssetsResponse } from '@logto/schemas'; +import { + userAssetsGuard, + userAssetsServiceStatusGuard, + allowUploadMimeTypes, + maxUploadFileSize, +} from '@logto/schemas'; import { format } from 'date-fns'; import { object } from 'zod'; @@ -8,32 +15,35 @@ import RequestError from '#src/errors/RequestError/index.js'; import koaGuard from '#src/middleware/koa-guard.js'; import SystemContext from '#src/tenants/SystemContext.js'; import assertThat from '#src/utils/assert-that.js'; -import { - allowUploadMimeTypes, - maxUploadFileSize, - uploadFileGuard, -} from '#src/utils/storage/consts.js'; +import { uploadFileGuard } from '#src/utils/storage/consts.js'; import { buildUploadFile } from '#src/utils/storage/index.js'; import { getTenantId } from '#src/utils/tenant.js'; import type { AuthedRouter, RouterInitArgs } from './types.js'; export default function userAssetsRoutes(...[router]: RouterInitArgs) { - router.get('/user-assets/service-status', async (ctx, next) => { - const { storageProviderConfig } = SystemContext.shared; + router.get( + '/user-assets/service-status', + koaGuard({ + response: userAssetsServiceStatusGuard, + }), + async (ctx, next) => { + const { storageProviderConfig } = SystemContext.shared; + const status = storageProviderConfig + ? { + status: 'ready', + allowUploadMimeTypes, + maxUploadFileSize, + } + : { + status: 'not_configured', + }; - ctx.body = storageProviderConfig - ? { - status: 'ready', - allowUploadMimeTypes, - maxUploadFileSize, - } - : { - status: 'not_configured', - }; + ctx.body = status; - return next(); - }); + return next(); + } + ); router.post( '/user-assets', @@ -41,12 +51,16 @@ export default function userAssetsRoutes(...[router]: Ro files: object({ file: uploadFileGuard, }), + response: userAssetsGuard, }), async (ctx, next) => { const { file } = ctx.guard.files; assertThat(file.size <= maxUploadFileSize, 'guard.file_size_exceeded'); - assertThat(allowUploadMimeTypes.includes(file.mimetype), 'guard.mime_type_not_allowed'); + assertThat( + allowUploadMimeTypes.map(String).includes(file.mimetype), + 'guard.mime_type_not_allowed' + ); const tenantId = getTenantId(ctx.URL); assertThat(tenantId, 'guard.can_not_get_tenant_id'); @@ -67,9 +81,11 @@ export default function userAssetsRoutes(...[router]: Ro publicUrl: storageProviderConfig.publicUrl, }); - ctx.body = { + const result: UserAssetsResponse = { url, }; + + ctx.body = result; } catch { throw new RequestError('storage.upload_error'); } diff --git a/packages/core/src/utils/storage/consts.ts b/packages/core/src/utils/storage/consts.ts index 967200f89..8e2b44fff 100644 --- a/packages/core/src/utils/storage/consts.ts +++ b/packages/core/src/utils/storage/consts.ts @@ -1,18 +1,5 @@ import { number, object, string } from 'zod'; -export const maxUploadFileSize = 8 * 1024 * 1024; // 8MB -// Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types -export const allowUploadMimeTypes = [ - 'image/jpeg', - 'image/png', - 'image/gif', - 'image/vnd.microsoft.icon', - 'image/svg+xml', - 'image/tiff', - 'image/webp', - 'image/bmp', -]; - export const uploadFileGuard = object({ filepath: string(), mimetype: string(), diff --git a/packages/phrases/src/locales/de/translation/admin-console/components.ts b/packages/phrases/src/locales/de/translation/admin-console/components.ts new file mode 100644 index 000000000..55423ca0f --- /dev/null +++ b/packages/phrases/src/locales/de/translation/admin-console/components.ts @@ -0,0 +1,15 @@ +const components = { + uploader: { + action_description: 'Drag and drop or browse', // UNTRANSLATED + uploading: 'Uploading...', // UNTRANSLATED + image_limit: 'Upload image under 500KB, JPG, PNG, GIF, WEBP only.', // UNTRANSLATED + error_upload: 'Something went wrong. File upload failed.', // UNTRANSLATED + error_file_size: 'File size is too large. Please upload a file under {{count, number}}KB.', // UNTRANSLATED + error_file_type: + 'File type is not supported. {{types, list(style: narrow; type: conjunction;)}} only.', // UNTRANSLATED + error_file_count_one: 'You can only upload {{count, number}} file.', // UNTRANSLATED + error_file_count_other: 'You can only upload {{count, number}} files.', // UNTRANSLATED + }, +}; + +export default components; diff --git a/packages/phrases/src/locales/de/translation/admin-console/index.ts b/packages/phrases/src/locales/de/translation/admin-console/index.ts index ae413c20b..bc1b949ef 100644 --- a/packages/phrases/src/locales/de/translation/admin-console/index.ts +++ b/packages/phrases/src/locales/de/translation/admin-console/index.ts @@ -3,6 +3,7 @@ import api_resources from './api-resources.js'; import application_details from './application-details.js'; import applications from './applications.js'; import cloud from './cloud.js'; +import components from './components.js'; import connector_details from './connector-details.js'; import connectors from './connectors.js'; import contact from './contact.js'; @@ -55,6 +56,7 @@ const admin_console = { permissions, cloud, profile, + components, }; export default admin_console; diff --git a/packages/phrases/src/locales/en/translation/admin-console/components.ts b/packages/phrases/src/locales/en/translation/admin-console/components.ts new file mode 100644 index 000000000..55423ca0f --- /dev/null +++ b/packages/phrases/src/locales/en/translation/admin-console/components.ts @@ -0,0 +1,15 @@ +const components = { + uploader: { + action_description: 'Drag and drop or browse', // UNTRANSLATED + uploading: 'Uploading...', // UNTRANSLATED + image_limit: 'Upload image under 500KB, JPG, PNG, GIF, WEBP only.', // UNTRANSLATED + error_upload: 'Something went wrong. File upload failed.', // UNTRANSLATED + error_file_size: 'File size is too large. Please upload a file under {{count, number}}KB.', // UNTRANSLATED + error_file_type: + 'File type is not supported. {{types, list(style: narrow; type: conjunction;)}} only.', // UNTRANSLATED + error_file_count_one: 'You can only upload {{count, number}} file.', // UNTRANSLATED + error_file_count_other: 'You can only upload {{count, number}} files.', // UNTRANSLATED + }, +}; + +export default components; diff --git a/packages/phrases/src/locales/en/translation/admin-console/index.ts b/packages/phrases/src/locales/en/translation/admin-console/index.ts index 14e2ce9b3..d0c1a8151 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/index.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/index.ts @@ -3,6 +3,7 @@ import api_resources from './api-resources.js'; import application_details from './application-details.js'; import applications from './applications.js'; import cloud from './cloud.js'; +import components from './components.js'; import connector_details from './connector-details.js'; import connectors from './connectors.js'; import contact from './contact.js'; @@ -55,6 +56,7 @@ const admin_console = { permissions, cloud, profile, + components, }; export default admin_console; diff --git a/packages/phrases/src/locales/fr/translation/admin-console/components.ts b/packages/phrases/src/locales/fr/translation/admin-console/components.ts new file mode 100644 index 000000000..55423ca0f --- /dev/null +++ b/packages/phrases/src/locales/fr/translation/admin-console/components.ts @@ -0,0 +1,15 @@ +const components = { + uploader: { + action_description: 'Drag and drop or browse', // UNTRANSLATED + uploading: 'Uploading...', // UNTRANSLATED + image_limit: 'Upload image under 500KB, JPG, PNG, GIF, WEBP only.', // UNTRANSLATED + error_upload: 'Something went wrong. File upload failed.', // UNTRANSLATED + error_file_size: 'File size is too large. Please upload a file under {{count, number}}KB.', // UNTRANSLATED + error_file_type: + 'File type is not supported. {{types, list(style: narrow; type: conjunction;)}} only.', // UNTRANSLATED + error_file_count_one: 'You can only upload {{count, number}} file.', // UNTRANSLATED + error_file_count_other: 'You can only upload {{count, number}} files.', // UNTRANSLATED + }, +}; + +export default components; diff --git a/packages/phrases/src/locales/fr/translation/admin-console/index.ts b/packages/phrases/src/locales/fr/translation/admin-console/index.ts index 14e2ce9b3..d0c1a8151 100644 --- a/packages/phrases/src/locales/fr/translation/admin-console/index.ts +++ b/packages/phrases/src/locales/fr/translation/admin-console/index.ts @@ -3,6 +3,7 @@ import api_resources from './api-resources.js'; import application_details from './application-details.js'; import applications from './applications.js'; import cloud from './cloud.js'; +import components from './components.js'; import connector_details from './connector-details.js'; import connectors from './connectors.js'; import contact from './contact.js'; @@ -55,6 +56,7 @@ const admin_console = { permissions, cloud, profile, + components, }; export default admin_console; diff --git a/packages/phrases/src/locales/ko/translation/admin-console/components.ts b/packages/phrases/src/locales/ko/translation/admin-console/components.ts new file mode 100644 index 000000000..55423ca0f --- /dev/null +++ b/packages/phrases/src/locales/ko/translation/admin-console/components.ts @@ -0,0 +1,15 @@ +const components = { + uploader: { + action_description: 'Drag and drop or browse', // UNTRANSLATED + uploading: 'Uploading...', // UNTRANSLATED + image_limit: 'Upload image under 500KB, JPG, PNG, GIF, WEBP only.', // UNTRANSLATED + error_upload: 'Something went wrong. File upload failed.', // UNTRANSLATED + error_file_size: 'File size is too large. Please upload a file under {{count, number}}KB.', // UNTRANSLATED + error_file_type: + 'File type is not supported. {{types, list(style: narrow; type: conjunction;)}} only.', // UNTRANSLATED + error_file_count_one: 'You can only upload {{count, number}} file.', // UNTRANSLATED + error_file_count_other: 'You can only upload {{count, number}} files.', // UNTRANSLATED + }, +}; + +export default components; diff --git a/packages/phrases/src/locales/ko/translation/admin-console/index.ts b/packages/phrases/src/locales/ko/translation/admin-console/index.ts index 34d208a95..441a90548 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/index.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/index.ts @@ -3,6 +3,7 @@ import api_resources from './api-resources.js'; import application_details from './application-details.js'; import applications from './applications.js'; import cloud from './cloud.js'; +import components from './components.js'; import connector_details from './connector-details.js'; import connectors from './connectors.js'; import contact from './contact.js'; @@ -55,6 +56,7 @@ const admin_console = { permissions, cloud, profile, + components, }; export default admin_console; diff --git a/packages/phrases/src/locales/pt-br/translation/admin-console/components.ts b/packages/phrases/src/locales/pt-br/translation/admin-console/components.ts new file mode 100644 index 000000000..55423ca0f --- /dev/null +++ b/packages/phrases/src/locales/pt-br/translation/admin-console/components.ts @@ -0,0 +1,15 @@ +const components = { + uploader: { + action_description: 'Drag and drop or browse', // UNTRANSLATED + uploading: 'Uploading...', // UNTRANSLATED + image_limit: 'Upload image under 500KB, JPG, PNG, GIF, WEBP only.', // UNTRANSLATED + error_upload: 'Something went wrong. File upload failed.', // UNTRANSLATED + error_file_size: 'File size is too large. Please upload a file under {{count, number}}KB.', // UNTRANSLATED + error_file_type: + 'File type is not supported. {{types, list(style: narrow; type: conjunction;)}} only.', // UNTRANSLATED + error_file_count_one: 'You can only upload {{count, number}} file.', // UNTRANSLATED + error_file_count_other: 'You can only upload {{count, number}} files.', // UNTRANSLATED + }, +}; + +export default components; diff --git a/packages/phrases/src/locales/pt-br/translation/admin-console/index.ts b/packages/phrases/src/locales/pt-br/translation/admin-console/index.ts index c188bb9c7..11ab86ac0 100644 --- a/packages/phrases/src/locales/pt-br/translation/admin-console/index.ts +++ b/packages/phrases/src/locales/pt-br/translation/admin-console/index.ts @@ -3,6 +3,7 @@ import api_resources from './api-resources.js'; import application_details from './application-details.js'; import applications from './applications.js'; import cloud from './cloud.js'; +import components from './components.js'; import connector_details from './connector-details.js'; import connectors from './connectors.js'; import contact from './contact.js'; @@ -55,6 +56,7 @@ const admin_console = { permissions, cloud, profile, + components, }; export default admin_console; diff --git a/packages/phrases/src/locales/pt-pt/translation/admin-console/components.ts b/packages/phrases/src/locales/pt-pt/translation/admin-console/components.ts new file mode 100644 index 000000000..55423ca0f --- /dev/null +++ b/packages/phrases/src/locales/pt-pt/translation/admin-console/components.ts @@ -0,0 +1,15 @@ +const components = { + uploader: { + action_description: 'Drag and drop or browse', // UNTRANSLATED + uploading: 'Uploading...', // UNTRANSLATED + image_limit: 'Upload image under 500KB, JPG, PNG, GIF, WEBP only.', // UNTRANSLATED + error_upload: 'Something went wrong. File upload failed.', // UNTRANSLATED + error_file_size: 'File size is too large. Please upload a file under {{count, number}}KB.', // UNTRANSLATED + error_file_type: + 'File type is not supported. {{types, list(style: narrow; type: conjunction;)}} only.', // UNTRANSLATED + error_file_count_one: 'You can only upload {{count, number}} file.', // UNTRANSLATED + error_file_count_other: 'You can only upload {{count, number}} files.', // UNTRANSLATED + }, +}; + +export default components; diff --git a/packages/phrases/src/locales/pt-pt/translation/admin-console/index.ts b/packages/phrases/src/locales/pt-pt/translation/admin-console/index.ts index 2cd9985b5..81cc4d752 100644 --- a/packages/phrases/src/locales/pt-pt/translation/admin-console/index.ts +++ b/packages/phrases/src/locales/pt-pt/translation/admin-console/index.ts @@ -3,6 +3,7 @@ import api_resources from './api-resources.js'; import application_details from './application-details.js'; import applications from './applications.js'; import cloud from './cloud.js'; +import components from './components.js'; import connector_details from './connector-details.js'; import connectors from './connectors.js'; import contact from './contact.js'; @@ -55,6 +56,7 @@ const admin_console = { permissions, cloud, profile, + components, }; export default admin_console; diff --git a/packages/phrases/src/locales/tr-tr/translation/admin-console/components.ts b/packages/phrases/src/locales/tr-tr/translation/admin-console/components.ts new file mode 100644 index 000000000..55423ca0f --- /dev/null +++ b/packages/phrases/src/locales/tr-tr/translation/admin-console/components.ts @@ -0,0 +1,15 @@ +const components = { + uploader: { + action_description: 'Drag and drop or browse', // UNTRANSLATED + uploading: 'Uploading...', // UNTRANSLATED + image_limit: 'Upload image under 500KB, JPG, PNG, GIF, WEBP only.', // UNTRANSLATED + error_upload: 'Something went wrong. File upload failed.', // UNTRANSLATED + error_file_size: 'File size is too large. Please upload a file under {{count, number}}KB.', // UNTRANSLATED + error_file_type: + 'File type is not supported. {{types, list(style: narrow; type: conjunction;)}} only.', // UNTRANSLATED + error_file_count_one: 'You can only upload {{count, number}} file.', // UNTRANSLATED + error_file_count_other: 'You can only upload {{count, number}} files.', // UNTRANSLATED + }, +}; + +export default components; diff --git a/packages/phrases/src/locales/tr-tr/translation/admin-console/index.ts b/packages/phrases/src/locales/tr-tr/translation/admin-console/index.ts index 860a0279c..997359803 100644 --- a/packages/phrases/src/locales/tr-tr/translation/admin-console/index.ts +++ b/packages/phrases/src/locales/tr-tr/translation/admin-console/index.ts @@ -3,6 +3,7 @@ import api_resources from './api-resources.js'; import application_details from './application-details.js'; import applications from './applications.js'; import cloud from './cloud.js'; +import components from './components.js'; import connector_details from './connector-details.js'; import connectors from './connectors.js'; import contact from './contact.js'; @@ -55,6 +56,7 @@ const admin_console = { permissions, cloud, profile, + components, }; export default admin_console; diff --git a/packages/phrases/src/locales/zh-cn/translation/admin-console/components.ts b/packages/phrases/src/locales/zh-cn/translation/admin-console/components.ts new file mode 100644 index 000000000..55423ca0f --- /dev/null +++ b/packages/phrases/src/locales/zh-cn/translation/admin-console/components.ts @@ -0,0 +1,15 @@ +const components = { + uploader: { + action_description: 'Drag and drop or browse', // UNTRANSLATED + uploading: 'Uploading...', // UNTRANSLATED + image_limit: 'Upload image under 500KB, JPG, PNG, GIF, WEBP only.', // UNTRANSLATED + error_upload: 'Something went wrong. File upload failed.', // UNTRANSLATED + error_file_size: 'File size is too large. Please upload a file under {{count, number}}KB.', // UNTRANSLATED + error_file_type: + 'File type is not supported. {{types, list(style: narrow; type: conjunction;)}} only.', // UNTRANSLATED + error_file_count_one: 'You can only upload {{count, number}} file.', // UNTRANSLATED + error_file_count_other: 'You can only upload {{count, number}} files.', // UNTRANSLATED + }, +}; + +export default components; diff --git a/packages/phrases/src/locales/zh-cn/translation/admin-console/index.ts b/packages/phrases/src/locales/zh-cn/translation/admin-console/index.ts index 760375026..13e6bd61c 100644 --- a/packages/phrases/src/locales/zh-cn/translation/admin-console/index.ts +++ b/packages/phrases/src/locales/zh-cn/translation/admin-console/index.ts @@ -3,6 +3,7 @@ import api_resources from './api-resources.js'; import application_details from './application-details.js'; import applications from './applications.js'; import cloud from './cloud.js'; +import components from './components.js'; import connector_details from './connector-details.js'; import connectors from './connectors.js'; import contact from './contact.js'; @@ -55,6 +56,7 @@ const admin_console = { permissions, cloud, profile, + components, }; export default admin_console; diff --git a/packages/schemas/src/types/index.ts b/packages/schemas/src/types/index.ts index 63e617e14..195456ac0 100644 --- a/packages/schemas/src/types/index.ts +++ b/packages/schemas/src/types/index.ts @@ -12,3 +12,4 @@ export * from './verification-code.js'; export * from './application.js'; export * from './system.js'; export * from './tenant.js'; +export * from './user-assets.js'; diff --git a/packages/schemas/src/types/user-assets.ts b/packages/schemas/src/types/user-assets.ts new file mode 100644 index 000000000..6a2f1ffc7 --- /dev/null +++ b/packages/schemas/src/types/user-assets.ts @@ -0,0 +1,33 @@ +import { z } from 'zod'; + +export const maxUploadFileSize = 8 * 1024 * 1024; // 8MB + +// Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types +export const allowUploadMimeTypes = [ + 'image/jpeg', + 'image/png', + 'image/gif', + 'image/vnd.microsoft.icon', + 'image/svg+xml', + 'image/tiff', + 'image/webp', + 'image/bmp', +] as const; + +const allowUploadMimeTypeGuard = z.enum(allowUploadMimeTypes); + +export type AllowedUploadMimeType = z.infer; + +export const userAssetsServiceStatusGuard = z.object({ + status: z.union([z.literal('ready'), z.literal('not_configured')]), + allowUploadMimeTypes: z.array(allowUploadMimeTypeGuard).optional(), + maxUploadFileSize: z.number().optional(), +}); + +export type UserAssetsServiceStatusResponse = z.infer; + +export const userAssetsGuard = z.object({ + url: z.string(), +}); + +export type UserAssetsResponse = z.infer; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6792b726e..afc7439fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -228,6 +228,7 @@ importers: react-dnd: ^16.0.0 react-dnd-html5-backend: ^16.0.0 react-dom: ^18.0.0 + react-dropzone: ^14.2.3 react-hook-form: ^7.34.0 react-hot-toast: ^2.2.0 react-i18next: ^11.18.3 @@ -302,6 +303,7 @@ importers: react-dnd: 16.0.0_3hx2ussxxho4jajbwrd6gq34qe react-dnd-html5-backend: 16.0.0 react-dom: 18.2.0_react@18.2.0 + react-dropzone: 14.2.3_react@18.2.0 react-hook-form: 7.34.0_react@18.2.0 react-hot-toast: 2.2.0_npw22p3c4ehm7n7vxn2gqac44u react-i18next: 11.18.3_shxxmfhtk2bc4pbx5cyq3uoph4 @@ -4882,6 +4884,11 @@ packages: /asynckit/0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + /attr-accept/2.2.2: + resolution: {integrity: sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==} + engines: {node: '>=4'} + dev: true + /available-typed-arrays/1.0.5: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} @@ -7128,6 +7135,13 @@ packages: flat-cache: 3.0.4 dev: true + /file-selector/0.6.0: + resolution: {integrity: sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==} + engines: {node: '>= 12'} + dependencies: + tslib: 2.4.1 + dev: true + /filelist/1.0.2: resolution: {integrity: sha512-z7O0IS8Plc39rTCq6i6iHxk43duYOn8uFJiWSewIq0Bww1RNybVHSCjahmcC87ZqAm4OTvFzlzeGu3XAzG1ctQ==} dependencies: @@ -12074,6 +12088,18 @@ packages: scheduler: 0.23.0 dev: true + /react-dropzone/14.2.3_react@18.2.0: + resolution: {integrity: sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==} + engines: {node: '>= 10.13'} + peerDependencies: + react: '>= 16.8 || 18.0.0 || ^18.0.0' + dependencies: + attr-accept: 2.2.2 + file-selector: 0.6.0 + prop-types: 15.8.1 + react: 18.2.0 + dev: true + /react-error-overlay/6.0.9: resolution: {integrity: sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==} dev: true