mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(console): image uploader (#3323)
This commit is contained in:
parent
07fab67c24
commit
0971f99e98
27 changed files with 506 additions and 33 deletions
|
@ -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",
|
||||
|
|
3
packages/console/src/assets/images/upload.svg
Normal file
3
packages/console/src/assets/images/upload.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.25842 6.42508L9.16675 4.50841V12.5001C9.16675 12.7211 9.25455 12.9331 9.41083 13.0893C9.56711 13.2456 9.77907 13.3334 10.0001 13.3334C10.2211 13.3334 10.4331 13.2456 10.5893 13.0893C10.7456 12.9331 10.8334 12.7211 10.8334 12.5001V4.50841L12.7417 6.42508C12.8192 6.50318 12.9114 6.56518 13.0129 6.60749C13.1145 6.64979 13.2234 6.67158 13.3334 6.67158C13.4434 6.67158 13.5523 6.64979 13.6539 6.60749C13.7554 6.56518 13.8476 6.50318 13.9251 6.42508C14.0032 6.34761 14.0652 6.25544 14.1075 6.15389C14.1498 6.05234 14.1716 5.94342 14.1716 5.83341C14.1716 5.7234 14.1498 5.61448 14.1075 5.51293C14.0652 5.41138 14.0032 5.31921 13.9251 5.24174L10.5917 1.90841C10.5125 1.83254 10.419 1.77307 10.3167 1.73341C10.1139 1.65006 9.8863 1.65006 9.68342 1.73341C9.58112 1.77307 9.48767 1.83254 9.40841 1.90841L6.07508 5.24174C5.99738 5.31944 5.93575 5.41168 5.8937 5.5132C5.85165 5.61472 5.83001 5.72353 5.83001 5.83341C5.83001 5.94329 5.85165 6.0521 5.8937 6.15362C5.93575 6.25514 5.99738 6.34738 6.07508 6.42508C6.15278 6.50278 6.24502 6.56441 6.34654 6.60646C6.44806 6.64851 6.55687 6.67015 6.66675 6.67015C6.77663 6.67015 6.88544 6.64851 6.98696 6.60646C7.08847 6.56441 7.18072 6.50278 7.25842 6.42508ZM17.5001 11.6667C17.2791 11.6667 17.0671 11.7545 16.9108 11.9108C16.7545 12.0671 16.6667 12.2791 16.6667 12.5001V15.8334C16.6667 16.0544 16.579 16.2664 16.4227 16.4227C16.2664 16.5789 16.0544 16.6667 15.8334 16.6667H4.16675C3.94573 16.6667 3.73377 16.5789 3.57749 16.4227C3.42121 16.2664 3.33341 16.0544 3.33341 15.8334V12.5001C3.33341 12.2791 3.24562 12.0671 3.08934 11.9108C2.93306 11.7545 2.7211 11.6667 2.50008 11.6667C2.27907 11.6667 2.06711 11.7545 1.91083 11.9108C1.75455 12.0671 1.66675 12.2791 1.66675 12.5001V15.8334C1.66675 16.4965 1.93014 17.1323 2.39898 17.6012C2.86782 18.07 3.50371 18.3334 4.16675 18.3334H15.8334C16.4965 18.3334 17.1323 18.07 17.6012 17.6012C18.07 17.1323 18.3334 16.4965 18.3334 15.8334V12.5001C18.3334 12.2791 18.2456 12.0671 18.0893 11.9108C17.9331 11.7545 17.7211 11.6667 17.5001 11.6667Z" fill="currentColor"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
|
@ -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);
|
||||
}
|
122
packages/console/src/components/FileUploader/index.tsx
Normal file
122
packages/console/src/components/FileUploader/index.tsx
Normal file
|
@ -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<string>();
|
||||
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<UserAssetsResponse>();
|
||||
|
||||
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 (
|
||||
<div>
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={classNames(
|
||||
styles.uploader,
|
||||
uploaderError && styles.uploaderError,
|
||||
isDragActive && styles.dragActive
|
||||
)}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<div className={styles.placeholder}>
|
||||
{isUploading ? (
|
||||
<>
|
||||
<Ring className={styles.uploadingIcon} />
|
||||
<div className={styles.actionDescription}>{t('components.uploader.uploading')}</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<UploaderIcon className={styles.icon} />
|
||||
<div className={styles.actionDescription}>
|
||||
{t('components.uploader.action_description')}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={classNames(styles.description, hasError && styles.error)}>
|
||||
{hasError ? uploaderError : limitDescription}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileUploader;
|
|
@ -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);
|
||||
}
|
50
packages/console/src/components/ImageUploader/index.tsx
Normal file
50
packages/console/src/components/ImageUploader/index.tsx
Normal file
|
@ -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 (
|
||||
<FileUploader
|
||||
allowedMimeTypes={['image/jpeg', 'image/png', 'image/gif', 'image/webp']}
|
||||
maxSize={maxImageSize}
|
||||
limitDescription={limitDescription}
|
||||
onCompleted={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.imageUploader}>
|
||||
<img alt={name} src={value} />
|
||||
<IconButton
|
||||
className={styles.delete}
|
||||
onClick={() => {
|
||||
onChange('');
|
||||
}}
|
||||
>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className={styles.limit}>{limitDescription}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageUploader;
|
|
@ -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,21 +15,21 @@ 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<T extends AuthedRouter>(...[router]: RouterInitArgs<T>) {
|
||||
router.get('/user-assets/service-status', async (ctx, next) => {
|
||||
router.get(
|
||||
'/user-assets/service-status',
|
||||
koaGuard({
|
||||
response: userAssetsServiceStatusGuard,
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { storageProviderConfig } = SystemContext.shared;
|
||||
|
||||
ctx.body = storageProviderConfig
|
||||
const status = storageProviderConfig
|
||||
? {
|
||||
status: 'ready',
|
||||
allowUploadMimeTypes,
|
||||
|
@ -32,8 +39,11 @@ export default function userAssetsRoutes<T extends AuthedRouter>(...[router]: Ro
|
|||
status: 'not_configured',
|
||||
};
|
||||
|
||||
ctx.body = status;
|
||||
|
||||
return next();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/user-assets',
|
||||
|
@ -41,12 +51,16 @@ export default function userAssetsRoutes<T extends AuthedRouter>(...[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<T extends AuthedRouter>(...[router]: Ro
|
|||
publicUrl: storageProviderConfig.publicUrl,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
const result: UserAssetsResponse = {
|
||||
url,
|
||||
};
|
||||
|
||||
ctx.body = result;
|
||||
} catch {
|
||||
throw new RequestError('storage.upload_error');
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
33
packages/schemas/src/types/user-assets.ts
Normal file
33
packages/schemas/src/types/user-assets.ts
Normal file
|
@ -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<typeof allowUploadMimeTypeGuard>;
|
||||
|
||||
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<typeof userAssetsServiceStatusGuard>;
|
||||
|
||||
export const userAssetsGuard = z.object({
|
||||
url: z.string(),
|
||||
});
|
||||
|
||||
export type UserAssetsResponse = z.infer<typeof userAssetsGuard>;
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue