tycho-components 0.37.2 → 0.38.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,7 @@ type Props = {
7
7
  accept: Record<string, string[]>;
8
8
  messages?: Partial<Record<'label.dropzone' | 'label.uploaded.file', string>>;
9
9
  onDrop?: () => void;
10
+ /** Initial value for the keep-original-name checkbox (default false). */
10
11
  keepName?: boolean;
11
12
  title?: string;
12
13
  alternativeButton?: {
@@ -7,6 +7,7 @@ export type AppDropzoneBodyProps = {
7
7
  accept: Record<string, string[]>;
8
8
  messages?: Partial<Record<'label.dropzone' | 'label.uploaded.file', string>>;
9
9
  onDrop?: () => void;
10
+ /** Initial value for the keep-original-name checkbox (default false). */
10
11
  keepName?: boolean;
11
12
  /** When false, the confirm button is hidden (e.g. when used inside modal with its own confirm) */
12
13
  showConfirmButton?: boolean;
@@ -1,4 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import Checkbox from '@mui/material/Checkbox';
3
+ import FormControlLabel from '@mui/material/FormControlLabel';
2
4
  import { forwardRef, useContext, useEffect, useImperativeHandle, useRef, useState, } from 'react';
3
5
  import Dropzone from 'react-dropzone';
4
6
  import { useTranslation } from 'react-i18next';
@@ -6,16 +8,25 @@ import { Button } from 'tycho-storybook';
6
8
  import CommonContext from '../../configs/CommonContext';
7
9
  import { toastLoading } from '../../configs/store/actions';
8
10
  import { useMessageUtils } from '../../configs/useMessageUtils';
11
+ import AppModalConfirm from '../AppModal/AppModalConfirm';
9
12
  import UploadService from './UploadService';
10
13
  import './style.scss';
11
- function AppDropzoneBodyInner({ folder, onSuccess, onError, accept, messages, onDrop, keepName, showConfirmButton = true, disablePreview = false, disableConfirm = false, }, ref) {
14
+ function isFileAlreadyExistsError(err) {
15
+ return (err?.response?.status === 409 &&
16
+ err?.response?.data?.description === 'upload.file.already.exists');
17
+ }
18
+ function AppDropzoneBodyInner({ folder, onSuccess, onError, accept, messages, onDrop, keepName = false, showConfirmButton = true, disablePreview = false, disableConfirm = false, }, ref) {
12
19
  const { t } = useTranslation('upload');
13
20
  const { state, dispatch } = useContext(CommonContext);
14
21
  const { dispatchError } = useMessageUtils();
15
22
  const [file, setFile] = useState();
16
23
  const [preview, setPreview] = useState();
24
+ const [keepOriginalName, setKeepOriginalName] = useState(keepName);
25
+ const [replaceConfirmOpen, setReplaceConfirmOpen] = useState(false);
17
26
  const fileRef = useRef();
27
+ const keepOriginalNameRef = useRef(keepOriginalName);
18
28
  fileRef.current = file;
29
+ keepOriginalNameRef.current = keepOriginalName;
19
30
  const isImageFile = (f) => {
20
31
  return f.type?.startsWith('image/') ?? false;
21
32
  };
@@ -31,29 +42,63 @@ function AppDropzoneBodyInner({ folder, onSuccess, onError, accept, messages, on
31
42
  }
32
43
  setPreview(undefined);
33
44
  }, [file, disablePreview]);
34
- const upload = async () => {
45
+ const buildUploadFormData = (currentFile, replace) => {
46
+ const data = new FormData();
47
+ data.append('file', currentFile);
48
+ data.append('folder', folder);
49
+ if (keepOriginalNameRef.current) {
50
+ data.append('keepOriginalName', 'true');
51
+ }
52
+ if (replace) {
53
+ data.append('replace', 'true');
54
+ }
55
+ return data;
56
+ };
57
+ const executeUpload = async (currentFile, replace) => {
58
+ const response = await UploadService.execute(buildUploadFormData(currentFile, replace));
59
+ onSuccess(response.data);
60
+ };
61
+ const upload = async (replace = false) => {
35
62
  const currentFile = fileRef.current;
36
63
  if (!currentFile || state.toastLoading)
37
64
  return;
38
65
  dispatch(toastLoading(true));
39
- const data = new FormData();
40
- data.append('file', currentFile);
41
- data.append('folder', folder);
42
- keepName && data.append('keepOriginalName', 'true');
43
- UploadService.execute(data)
44
- .then((r) => {
45
- onSuccess(r.data);
46
- })
47
- .catch((err) => {
48
- dispatchError({ err: 'error.uploading.image', t, key: 'upload' });
66
+ try {
67
+ if (keepOriginalNameRef.current && !replace) {
68
+ const validateData = new FormData();
69
+ validateData.append('folder', folder);
70
+ validateData.append('filename', currentFile.name);
71
+ validateData.append('keepOriginalName', 'true');
72
+ try {
73
+ await UploadService.validate(validateData);
74
+ }
75
+ catch (err) {
76
+ if (isFileAlreadyExistsError(err)) {
77
+ dispatch(toastLoading(false));
78
+ setReplaceConfirmOpen(true);
79
+ return;
80
+ }
81
+ dispatchError({ err, t, key: 'upload' });
82
+ onError?.(err);
83
+ return;
84
+ }
85
+ }
86
+ await executeUpload(currentFile, replace);
87
+ }
88
+ catch (err) {
89
+ if (!replace && isFileAlreadyExistsError(err)) {
90
+ setReplaceConfirmOpen(true);
91
+ return;
92
+ }
93
+ dispatchError({ err, t, key: 'upload' });
49
94
  onError?.(err);
50
- })
51
- .finally(() => {
95
+ }
96
+ finally {
52
97
  dispatch(toastLoading(false));
53
- });
98
+ }
54
99
  };
55
100
  useImperativeHandle(ref, () => ({
56
- upload,
101
+ upload: () => upload(false),
57
102
  }));
58
103
  const handleDrop = (files) => {
59
104
  if (files.length === 0)
@@ -64,11 +109,17 @@ function AppDropzoneBodyInner({ folder, onSuccess, onError, accept, messages, on
64
109
  const handleCancel = () => {
65
110
  setFile(undefined);
66
111
  };
112
+ const handleReplaceConfirm = () => {
113
+ setReplaceConfirmOpen(false);
114
+ upload(true);
115
+ };
67
116
  const resolveMessage = (key) => {
68
117
  const customMessage = messages?.[key]?.trim();
69
118
  return customMessage || t(key);
70
119
  };
71
- return (_jsxs("div", { className: "dropzone-container", children: [!file && (_jsx(Dropzone, { onDrop: (acceptedFiles) => handleDrop(acceptedFiles), accept: accept, maxFiles: 1, children: ({ getRootProps, getInputProps }) => (_jsxs("div", { ...getRootProps(), className: "dropzone", children: [_jsx("input", { ...getInputProps() }), _jsx("span", { children: resolveMessage('label.dropzone') })] })) })), file && (_jsxs("div", { className: "uploaded-file", children: [preview && _jsx("img", { className: "preview", src: preview, alt: "" }), !preview && (_jsxs(_Fragment, { children: [_jsx("b", { children: resolveMessage('label.uploaded.file') }), _jsx("span", { children: file.name })] })), showConfirmButton && (_jsxs("div", { className: "buttons", children: [_jsx(Button, { onClick: handleCancel, text: t('common:button.cancel'), size: "small", mode: "outlined", color: "danger" }), _jsx(Button, { onClick: upload, text: t('common:button.confirm'), size: "small", mode: "outlined", disabled: disableConfirm })] }))] }))] }));
120
+ return (_jsxs("div", { className: "dropzone-container", children: [!file && (_jsx(Dropzone, { onDrop: (acceptedFiles) => handleDrop(acceptedFiles), accept: accept, maxFiles: 1, children: ({ getRootProps, getInputProps }) => (_jsxs("div", { ...getRootProps(), className: "dropzone", children: [_jsx("input", { ...getInputProps() }), _jsx("span", { children: resolveMessage('label.dropzone') })] })) })), file && (_jsxs("div", { className: "uploaded-file", children: [preview && _jsx("img", { className: "preview", src: preview, alt: "" }), !preview && (_jsxs(_Fragment, { children: [_jsx("b", { children: resolveMessage('label.uploaded.file') }), _jsx("span", { children: file.name })] })), showConfirmButton && (_jsxs("div", { className: "buttons", children: [_jsx(Button, { onClick: handleCancel, text: t('common:button.cancel'), size: "small", mode: "outlined", color: "danger" }), _jsx(Button, { onClick: () => upload(false), text: t('common:button.confirm'), size: "small", mode: "outlined", disabled: disableConfirm })] }))] })), _jsx(FormControlLabel, { className: "keep-name-checkbox", control: _jsx(Checkbox, { checked: keepOriginalName, onChange: (_, checked) => setKeepOriginalName(checked) }), label: t('label.keep.original.name') }), replaceConfirmOpen && (_jsx(AppModalConfirm, { title: t('replace.confirm.title'), subtitle: t('replace.confirm.subtitle', {
121
+ 0: fileRef.current?.name ?? '',
122
+ }), onClose: () => setReplaceConfirmOpen(false), onConfirm: handleReplaceConfirm }))] }));
72
123
  }
73
124
  const AppDropzoneBody = forwardRef(AppDropzoneBodyInner);
74
125
  export default AppDropzoneBody;
@@ -1,6 +1,8 @@
1
1
  import { UploadedFile } from './UploadedFile';
2
2
  declare function execute(data: FormData): Promise<import("axios").AxiosResponse<UploadedFile, any, {}>>;
3
+ declare function validate(data: FormData): Promise<import("axios").AxiosResponse<any, any, {}>>;
3
4
  declare const UploadService: {
4
5
  execute: typeof execute;
6
+ validate: typeof validate;
5
7
  };
6
8
  export default UploadService;
@@ -5,14 +5,18 @@ const getJwtToken = () => {
5
5
  const cookie = Cookies.get(JWT_TOKEN);
6
6
  return cookie === 'undefined' ? '' : cookie;
7
7
  };
8
+ const authHeaders = () => ({
9
+ Authorization: `Bearer ${getJwtToken() || ''}`,
10
+ });
8
11
  function execute(data) {
9
- const token = getJwtToken() || '';
10
- const header = {
11
- Authorization: `Bearer ${token}`,
12
- };
13
12
  return axios.post(import.meta.env.VITE_APP_UPLOAD_URL, data, {
14
- headers: header,
13
+ headers: authHeaders(),
15
14
  });
16
15
  }
17
- const UploadService = { execute };
16
+ function validate(data) {
17
+ return axios.post(`${import.meta.env.VITE_APP_UPLOAD_URL}/validate`, data, {
18
+ headers: authHeaders(),
19
+ });
20
+ }
21
+ const UploadService = { execute, validate };
18
22
  export default UploadService;
@@ -1,14 +1,22 @@
1
1
  .dropzone-container {
2
2
  display: flex;
3
+ flex-direction: column;
3
4
  align-items: center;
4
5
  justify-content: center;
6
+ gap: var(--spacing-200);
7
+ box-sizing: border-box;
8
+ width: 100%;
9
+ max-width: 100%;
10
+ min-width: 0;
5
11
 
6
12
  .dropzone {
7
13
  display: flex;
8
14
  align-items: center;
9
15
  justify-content: center;
10
16
  text-align: center;
17
+ box-sizing: border-box;
11
18
  width: 100%;
19
+ max-width: 100%;
12
20
  padding: var(--spacing-700);
13
21
  border-radius: var(--radius-100);
14
22
  border: 2px dashed var(--border-subtle-3);
@@ -26,17 +34,27 @@
26
34
  display: flex;
27
35
  flex-direction: column;
28
36
  align-items: center;
37
+ box-sizing: border-box;
29
38
  width: 100%;
39
+ max-width: 100%;
30
40
  text-align: center;
31
41
  border: 1px solid var(--border-subtle-3);
32
42
  border-radius: var(--radius-100);
33
- padding: 16px;
34
- gap: 16px;
43
+ padding: var(--spacing-200);
44
+ gap: var(--spacing-200);
35
45
 
36
46
  .buttons {
37
47
  display: flex;
38
- gap: 16px;
39
- margin-top: 16px;
48
+ gap: var(--spacing-200);
49
+ margin-top: var(--spacing-200);
50
+ }
51
+ }
52
+
53
+ .keep-name-checkbox {
54
+ align-self: flex-start;
55
+
56
+ > .MuiCheckbox-root {
57
+ padding: 0px 8px;
40
58
  }
41
59
  }
42
60
  }
@@ -3,21 +3,51 @@ export declare const UploadTexts: {
3
3
  'label.dropzone': string;
4
4
  'label.uploaded.file': string;
5
5
  'label.confirm': string;
6
+ 'label.keep.original.name': string;
6
7
  'modal.title': string;
7
8
  'error.uploading.image': string;
9
+ 'upload.file.already.exists': string;
10
+ 'replace.confirm.title': string;
11
+ 'replace.confirm.subtitle': string;
12
+ 'upload.file.read.error': string;
13
+ 'upload.file.type.detect.error': string;
14
+ 'upload.file.type.unknown': string;
15
+ 'upload.file.threat.detected': string;
16
+ 'upload.file.write.error': string;
17
+ 'upload.file.name.required': string;
8
18
  };
9
19
  'pt-BR': {
10
20
  'label.dropzone': string;
11
21
  'label.uploaded.file': string;
12
22
  'label.confirm': string;
23
+ 'label.keep.original.name': string;
13
24
  'modal.title': string;
14
25
  'error.uploading.image': string;
26
+ 'upload.file.already.exists': string;
27
+ 'replace.confirm.title': string;
28
+ 'replace.confirm.subtitle': string;
29
+ 'upload.file.read.error': string;
30
+ 'upload.file.type.detect.error': string;
31
+ 'upload.file.type.unknown': string;
32
+ 'upload.file.threat.detected': string;
33
+ 'upload.file.write.error': string;
34
+ 'upload.file.name.required': string;
15
35
  };
16
36
  it: {
17
37
  'label.dropzone': string;
18
38
  'label.uploaded.file': string;
19
39
  'label.confirm': string;
40
+ 'label.keep.original.name': string;
20
41
  'modal.title': string;
21
42
  'error.uploading.image': string;
43
+ 'upload.file.already.exists': string;
44
+ 'replace.confirm.title': string;
45
+ 'replace.confirm.subtitle': string;
46
+ 'upload.file.read.error': string;
47
+ 'upload.file.type.detect.error': string;
48
+ 'upload.file.type.unknown': string;
49
+ 'upload.file.threat.detected': string;
50
+ 'upload.file.write.error': string;
51
+ 'upload.file.name.required': string;
22
52
  };
23
53
  };
@@ -3,21 +3,51 @@ export const UploadTexts = {
3
3
  'label.dropzone': 'Drag and drop or click to upload a file',
4
4
  'label.uploaded.file': 'You are uploading the following file:',
5
5
  'label.confirm': 'Press confirm to continue',
6
+ 'label.keep.original.name': 'Keep original file name',
6
7
  'modal.title': 'Upload a file',
7
8
  'error.uploading.image': 'An error occurred while uploading a file. Contact the administrator.',
9
+ 'upload.file.already.exists': 'A file named "{{0}}" already exists in this folder.',
10
+ 'replace.confirm.title': 'Replace existing file?',
11
+ 'replace.confirm.subtitle': 'A file named "{{0}}" already exists. Do you want to replace it?',
12
+ 'upload.file.read.error': 'Could not read the uploaded file.',
13
+ 'upload.file.type.detect.error': 'Could not detect the file type.',
14
+ 'upload.file.type.unknown': 'Unknown file type; upload is not allowed.',
15
+ 'upload.file.threat.detected': 'This file type is not allowed.',
16
+ 'upload.file.write.error': 'Could not save the uploaded file.',
17
+ 'upload.file.name.required': 'A file name is required for validation.',
8
18
  },
9
19
  'pt-BR': {
10
20
  'label.dropzone': 'Arraste e solte ou clique para enviar um arquivo.',
11
21
  'label.uploaded.file': 'Você está enviando o seguinte arquivo:',
12
22
  'label.confirm': 'Pressione confirmar para continuar',
23
+ 'label.keep.original.name': 'Manter o nome original do arquivo',
13
24
  'modal.title': 'Upload de arquivo',
14
25
  'error.uploading.image': 'Ocorreu um erro ao enviar o arquivo. Entre em contato com o administrador.',
26
+ 'upload.file.already.exists': 'Já existe um arquivo chamado "{{0}}" nesta pasta.',
27
+ 'replace.confirm.title': 'Substituir arquivo existente?',
28
+ 'replace.confirm.subtitle': 'Já existe um arquivo chamado "{{0}}". Deseja substituí-lo?',
29
+ 'upload.file.read.error': 'Não foi possível ler o arquivo enviado.',
30
+ 'upload.file.type.detect.error': 'Não foi possível detectar o tipo do arquivo.',
31
+ 'upload.file.type.unknown': 'Tipo de arquivo desconhecido; o envio não é permitido.',
32
+ 'upload.file.threat.detected': 'Este tipo de arquivo não é permitido.',
33
+ 'upload.file.write.error': 'Não foi possível salvar o arquivo enviado.',
34
+ 'upload.file.name.required': 'É necessário informar o nome do arquivo para validação.',
15
35
  },
16
36
  it: {
17
37
  'label.dropzone': 'Trascina e rilascia o clicca per caricare un file',
18
38
  'label.uploaded.file': 'Stai caricando il seguente file:',
19
39
  'label.confirm': 'Premi conferma per continuare',
40
+ 'label.keep.original.name': 'Mantieni il nome originale del file',
20
41
  'modal.title': 'Carica un file',
21
42
  'error.uploading.image': "Si è verificato un errore durante il caricamento del file. Contatta l'amministratore.",
43
+ 'upload.file.already.exists': 'Esiste già un file chiamato "{{0}}" in questa cartella.',
44
+ 'replace.confirm.title': 'Sostituire il file esistente?',
45
+ 'replace.confirm.subtitle': 'Esiste già un file chiamato "{{0}}". Vuoi sostituirlo?',
46
+ 'upload.file.read.error': 'Impossibile leggere il file caricato.',
47
+ 'upload.file.type.detect.error': 'Impossibile rilevare il tipo di file.',
48
+ 'upload.file.type.unknown': 'Tipo di file sconosciuto; il caricamento non è consentito.',
49
+ 'upload.file.threat.detected': 'Questo tipo di file non è consentito.',
50
+ 'upload.file.write.error': 'Impossibile salvare il file caricato.',
51
+ 'upload.file.name.required': 'Il nome del file è obbligatorio per la convalida.',
22
52
  },
23
53
  };
@@ -1,5 +1,7 @@
1
1
  declare function updateLiveTour(liveTour: boolean): Promise<import("axios").AxiosResponse<string, any, {}>>;
2
+ declare function updateSettings(settings: Record<string, unknown>): Promise<import("axios").AxiosResponse<string, any, {}>>;
2
3
  declare const ProfileService: {
3
4
  updateLiveTour: typeof updateLiveTour;
5
+ updateSettings: typeof updateSettings;
4
6
  };
5
7
  export default ProfileService;
@@ -2,7 +2,11 @@ import api from '../api';
2
2
  function updateLiveTour(liveTour) {
3
3
  return api.patch(`${import.meta.env.VITE_APP_AUTH_API}/profile/live-tour`, { liveTour });
4
4
  }
5
+ function updateSettings(settings) {
6
+ return api.patch(`${import.meta.env.VITE_APP_AUTH_API}/profile/settings`, { settings });
7
+ }
5
8
  const ProfileService = {
6
9
  updateLiveTour,
10
+ updateSettings,
7
11
  };
8
12
  export default ProfileService;
@@ -20,4 +20,5 @@ export type User = {
20
20
  permissions: string[];
21
21
  login?: string;
22
22
  liveTour?: boolean;
23
+ settings?: Record<string, any>;
23
24
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tycho-components",
3
3
  "private": false,
4
- "version": "0.37.2",
4
+ "version": "0.38.1",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "exports": {
@@ -82,7 +82,7 @@
82
82
  "react-i18next": "^13.0.2",
83
83
  "react-router-dom": "^6.14.2",
84
84
  "react-toastify": "^9.1.3",
85
- "tycho-storybook": "0.10.15",
85
+ "tycho-storybook": "0.10.16",
86
86
  "wavesurfer-react": "^2.2.2",
87
87
  "wavesurfer.js": "^6.6.3"
88
88
  },
@@ -111,7 +111,7 @@
111
111
  "react-toastify": "^9.1.3",
112
112
  "sass-embedded": "^1.97.2",
113
113
  "storybook": "^10.1.11",
114
- "tycho-storybook": "^0.10.15",
114
+ "tycho-storybook": "^0.10.16",
115
115
  "typescript": "^5.7.3",
116
116
  "vite": "^7.0.0",
117
117
  "vitest": "^3.2.6",