Merge branch 'file' into 'main'
File See merge request cellule-financiere-pmo/design-system/visua-vue!10
This commit is contained in:
commit
2c91cfb38c
|
@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.0.10] - 2025-07-27
|
||||||
|
### Added
|
||||||
|
- File compoenent
|
||||||
|
- File upload component
|
||||||
|
- Scroll panel component
|
||||||
|
|
||||||
## [1.0.9] - 2025-07-24
|
## [1.0.9] - 2025-07-24
|
||||||
### Added
|
### Added
|
||||||
- Message component
|
- Message component
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
# visua-vue
|
# visua-vue
|
||||||
|
|
||||||
**Current version: v1.0.9**
|
**Current version: v1.0.10**
|
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@cellule-financiere-pmo/visua-vue",
|
"name": "@cellule-financiere-pmo/visua-vue",
|
||||||
"version": "1.0.9",
|
"version": "1.0.10",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@cellule-financiere-pmo/visua-vue",
|
"name": "@cellule-financiere-pmo/visua-vue",
|
||||||
"version": "1.0.9",
|
"version": "1.0.10",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cellule-financiere-pmo/visua": "1.1.3",
|
"@cellule-financiere-pmo/visua": "1.1.3",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@cellule-financiere-pmo/visua-vue",
|
"name": "@cellule-financiere-pmo/visua-vue",
|
||||||
"version": "1.0.9",
|
"version": "1.0.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|
14
src/App.vue
14
src/App.vue
|
@ -5,10 +5,11 @@
|
||||||
// import VAccordionView from '../template/VAccordionView.vue';
|
// import VAccordionView from '../template/VAccordionView.vue';
|
||||||
// import VInputView from '../template/VInputView.vue';
|
// import VInputView from '../template/VInputView.vue';
|
||||||
// import VCheckboxView from '../template/VCheckboxView.vue';
|
// import VCheckboxView from '../template/VCheckboxView.vue';
|
||||||
import VBadgeView from '../template/VBadgeView.vue';
|
// import VBadgeView from '../template/VBadgeView.vue';
|
||||||
import VSelectView from '../template/VSelectView.vue';
|
// import VSelectView from '../template/VSelectView.vue';
|
||||||
// import VProgressBarView from '../template/VProgressBarView.vue';
|
// import VProgressBarView from '../template/VProgressBarView.vue';
|
||||||
import VMessageView from '../template/VMessageView.vue';
|
// import VMessageView from '../template/VMessageView.vue';
|
||||||
|
import VFileUploadView from '../template/VFileUploadView.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,8 +20,9 @@ import VMessageView from '../template/VMessageView.vue';
|
||||||
<!-- <VAccordionView/> -->
|
<!-- <VAccordionView/> -->
|
||||||
<!-- <VInputView/> -->
|
<!-- <VInputView/> -->
|
||||||
<!-- <VCheckboxView/> -->
|
<!-- <VCheckboxView/> -->
|
||||||
<VBadgeView/>
|
<!-- <VBadgeView/>
|
||||||
<VSelectView/>
|
<VSelectView/> -->
|
||||||
<!-- <VProgressBarView/> -->
|
<!-- <VProgressBarView/> -->
|
||||||
<VMessageView/>
|
<!-- <VMessageView/> -->
|
||||||
|
<VFileUploadView/>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -13,3 +13,5 @@
|
||||||
@import './primevue-style/overlay.css';
|
@import './primevue-style/overlay.css';
|
||||||
@import './primevue-style/iconfield.css';
|
@import './primevue-style/iconfield.css';
|
||||||
@import './primevue-style/progressbar.css';
|
@import './primevue-style/progressbar.css';
|
||||||
|
@import './primevue-style/fileupload.css';
|
||||||
|
@import './primevue-style/scrollpanel.css';
|
||||||
|
|
24
src/assets/style/primevue-style/fileupload.css
Normal file
24
src/assets/style/primevue-style/fileupload.css
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
:root {
|
||||||
|
--p-fileupload-basic-gap: 0.5rem;
|
||||||
|
--p-fileupload-progressbar-height: 0.25rem;
|
||||||
|
--p-fileupload-file-list-gap: 0.5rem;
|
||||||
|
--p-fileupload-file-padding: 1rem;
|
||||||
|
--p-fileupload-file-gap: 1rem;
|
||||||
|
--p-fileupload-file-border-color: var(--border-default-grey);
|
||||||
|
--p-fileupload-file-info-gap: 0.5rem;
|
||||||
|
--p-fileupload-content-highlight-border-color: var(--p-primary-color);
|
||||||
|
--p-fileupload-content-padding: 0rem;
|
||||||
|
--p-fileupload-content-gap: 1rem;
|
||||||
|
--p-fileupload-header-background: transparent;
|
||||||
|
--p-fileupload-header-color: var(--text-default-grey);
|
||||||
|
--p-fileupload-header-padding: 0rem;
|
||||||
|
--p-fileupload-header-border-color: unset;
|
||||||
|
--p-fileupload-header-border-width: 0;
|
||||||
|
--p-fileupload-header-border-radius: 0;
|
||||||
|
--p-fileupload-header-gap: 0.5rem;
|
||||||
|
--p-fileupload-background: var(--background-transparent);
|
||||||
|
--p-fileupload-border-color: var(--border-default-grey);
|
||||||
|
--p-fileupload-color: var(--text-default-grey);
|
||||||
|
--p-fileupload-border-radius: 0px;
|
||||||
|
--p-fileupload-transition-duration: var(--transition-duration);
|
||||||
|
}
|
11
src/assets/style/primevue-style/scrollpanel.css
Normal file
11
src/assets/style/primevue-style/scrollpanel.css
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
:root {
|
||||||
|
--p-scrollpanel-bar-size: 0.5rem;
|
||||||
|
--p-scrollpanel-bar-border-radius: 0.25rem;
|
||||||
|
--p-scrollpanel-bar-focus-ring-width: var(--focus-width);
|
||||||
|
--p-scrollpanel-bar-focus-ring-style: var(--focus-style);
|
||||||
|
--p-scrollpanel-bar-focus-ring-color: var(--focus-color);
|
||||||
|
--p-scrollpanel-bar-focus-ring-offset: var(--focus-offset);
|
||||||
|
--p-scrollpanel-bar-focus-ring-shadow: none;
|
||||||
|
--p-scrollpanel-transition-duration: var(--transition-duration);
|
||||||
|
--p-scrollpanel-bar-background: var(--background-overlay-grey);
|
||||||
|
}
|
198
src/components/file/IVFileUpload.type.ts
Normal file
198
src/components/file/IVFileUpload.type.ts
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
import type { HintedString } from '@primevue/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface representing the properties of a FileUpload component.
|
||||||
|
*/
|
||||||
|
export interface IFile {
|
||||||
|
/**
|
||||||
|
* Optional unique identifier for the file input.
|
||||||
|
*/
|
||||||
|
id?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional label displayed above or beside the file input.
|
||||||
|
*/
|
||||||
|
label?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the types of files that the input accepts.
|
||||||
|
* Can be a single MIME type string or an array of MIME types.
|
||||||
|
* Example: "image/*" or ["image/png", "application/pdf"]
|
||||||
|
*/
|
||||||
|
accept?: string | string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional helper text displayed below the input to guide the user.
|
||||||
|
*/
|
||||||
|
hint?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional error message displayed when validation fails.
|
||||||
|
*/
|
||||||
|
error?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional message displayed when the input is valid.
|
||||||
|
*/
|
||||||
|
validMessage?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, disables the file input.
|
||||||
|
*/
|
||||||
|
disabled?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current value of the file input, typically a file path or name.
|
||||||
|
*/
|
||||||
|
modelValue?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileUploadProps {
|
||||||
|
/**
|
||||||
|
* Name of the request parameter to identify the files at backend.
|
||||||
|
*/
|
||||||
|
name?: string | undefined;
|
||||||
|
/**
|
||||||
|
* Remote url to upload the files.
|
||||||
|
*/
|
||||||
|
url?: string | undefined;
|
||||||
|
/**
|
||||||
|
* Defines the UI of the component, possible values are 'advanced' and 'basic'.
|
||||||
|
* @defaultValue advanced
|
||||||
|
*/
|
||||||
|
mode?: HintedString<'advanced' | 'basic'> | undefined;
|
||||||
|
/**
|
||||||
|
* Used to select multiple files at once from file dialog.
|
||||||
|
* @defaultValue false
|
||||||
|
*/
|
||||||
|
multiple?: boolean | undefined;
|
||||||
|
/**
|
||||||
|
* Pattern to restrict the allowed file types such as 'image/*'.
|
||||||
|
*/
|
||||||
|
accept?: string | undefined;
|
||||||
|
/**
|
||||||
|
* Disables the upload functionality.
|
||||||
|
* @defaultValue false
|
||||||
|
*/
|
||||||
|
disabled?: boolean | undefined;
|
||||||
|
/**
|
||||||
|
* When enabled, upload begins automatically after selection is completed.
|
||||||
|
* @defaultValue false
|
||||||
|
*/
|
||||||
|
auto?: boolean | undefined;
|
||||||
|
/**
|
||||||
|
* Maximum file size allowed in bytes.
|
||||||
|
*/
|
||||||
|
maxFileSize?: number | undefined;
|
||||||
|
/**
|
||||||
|
* Message of the invalid fize size.
|
||||||
|
* @defaultValue {0}: Invalid file size, file size should be smaller than {1.}
|
||||||
|
*/
|
||||||
|
invalidFileSizeMessage?: string | undefined;
|
||||||
|
/**
|
||||||
|
* Message to display when number of files to be uploaded exceeeds the limit.
|
||||||
|
* @defaultValue Maximum number of files to be uploaded is {0.}
|
||||||
|
*/
|
||||||
|
invalidFileLimitMessage?: string | undefined;
|
||||||
|
/**
|
||||||
|
* Message of the invalid fize type.
|
||||||
|
* @defaultValue '{0}: Invalid file type.'
|
||||||
|
*/
|
||||||
|
invalidFileTypeMessage?: string | undefined;
|
||||||
|
/**
|
||||||
|
* Maximum number of files that can be uploaded.
|
||||||
|
*/
|
||||||
|
fileLimit?: number | undefined;
|
||||||
|
/**
|
||||||
|
* Cross-site Access-Control requests should be made using credentials such as cookies, authorization headers or TLS client certificates.
|
||||||
|
* @defaultValue false
|
||||||
|
*/
|
||||||
|
withCredentials?: boolean | undefined;
|
||||||
|
/**
|
||||||
|
* Width of the image thumbnail in pixels.
|
||||||
|
* @defaultValue 50
|
||||||
|
*/
|
||||||
|
previewWidth?: number | undefined;
|
||||||
|
/**
|
||||||
|
* Label of the choose button. Defaults to PrimeVue Locale configuration.
|
||||||
|
*/
|
||||||
|
chooseLabel?: string | undefined;
|
||||||
|
/**
|
||||||
|
* Label of the upload button. Defaults to PrimeVue Locale configuration.
|
||||||
|
*/
|
||||||
|
uploadLabel?: string | undefined;
|
||||||
|
/**
|
||||||
|
* Label of the cancel button. Defaults to PrimeVue Locale configuration.
|
||||||
|
* @defaultValue Cancel
|
||||||
|
*/
|
||||||
|
cancelLabel?: string | undefined;
|
||||||
|
/**
|
||||||
|
* Whether to use the default upload or a manual implementation defined in uploadHandler callback. Defaults to PrimeVue Locale configuration.
|
||||||
|
*/
|
||||||
|
customUpload?: boolean | undefined;
|
||||||
|
/**
|
||||||
|
* Whether to show the upload button.
|
||||||
|
* @defaultValue true
|
||||||
|
*/
|
||||||
|
showUploadButton?: boolean | undefined;
|
||||||
|
/**
|
||||||
|
* Whether to show the cancel button.
|
||||||
|
* @defaultValue true
|
||||||
|
*/
|
||||||
|
showCancelButton?: boolean | undefined;
|
||||||
|
/**
|
||||||
|
* Icon of the choose button.
|
||||||
|
*/
|
||||||
|
chooseIcon?: string | undefined;
|
||||||
|
/**
|
||||||
|
* Icon of the upload button.
|
||||||
|
*/
|
||||||
|
uploadIcon?: string | undefined;
|
||||||
|
/**
|
||||||
|
* Icon of the cancel button.
|
||||||
|
*/
|
||||||
|
cancelIcon?: string | undefined;
|
||||||
|
/**
|
||||||
|
* Inline style of the component.
|
||||||
|
*/
|
||||||
|
style?: unknown;
|
||||||
|
/**
|
||||||
|
* Style class of the component.
|
||||||
|
*/
|
||||||
|
class?: unknown;
|
||||||
|
/**
|
||||||
|
* Used to pass all properties of the ButtonProps to the choose button inside the component.
|
||||||
|
* @type {ButtonProps}
|
||||||
|
* @defaultValue null
|
||||||
|
*/
|
||||||
|
chooseButtonProps?: object | undefined;
|
||||||
|
/**
|
||||||
|
* Used to pass all properties of the ButtonProps to the upload button inside the component.
|
||||||
|
* @type {ButtonProps}
|
||||||
|
* @defaultValue { severity: 'secondary' }
|
||||||
|
*/
|
||||||
|
uploadButtonProps?: object | undefined;
|
||||||
|
/**
|
||||||
|
* Used to pass all properties of the ButtonProps to the cancel button inside the component.
|
||||||
|
* @type {ButtonProps}
|
||||||
|
* @defaultValue { severity: 'secondary' }
|
||||||
|
*/
|
||||||
|
cancelButtonProps?: object | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extended interface for a customizable FileUpload component.
|
||||||
|
*
|
||||||
|
* Combines selected properties from IFile and FileUploadProps,
|
||||||
|
* while omitting and overriding specific ones for more control.
|
||||||
|
*/
|
||||||
|
export default interface IVFileUpload extends
|
||||||
|
Partial<Omit<IFile, 'accept' | 'error'>>,
|
||||||
|
Partial<Omit<FileUploadProps, 'auto' | 'mode' | 'multiple'>> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, enables the advanced mode of the file upload component,
|
||||||
|
* which may include features like drag-and-drop, file previews, etc.
|
||||||
|
*/
|
||||||
|
advanced?: boolean;
|
||||||
|
}
|
229
src/components/file/VFile.vue
Normal file
229
src/components/file/VFile.vue
Normal file
|
@ -0,0 +1,229 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import VBadge from '../badge/VBadge.vue';
|
||||||
|
import VButton from '../button/VButton.vue';
|
||||||
|
import VProgressBar from '../progressbar/VProgressBar.vue';
|
||||||
|
import { usePrimeVue } from 'primevue';
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import styles from '@visua/typography.module.css';
|
||||||
|
|
||||||
|
export interface IVFile {
|
||||||
|
file: File
|
||||||
|
advanced?: boolean
|
||||||
|
index?: number
|
||||||
|
removeFileCallback?: (index: number) => void
|
||||||
|
status?: 'pending' | 'completed' | 'error'
|
||||||
|
progress?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<IVFile>(), {
|
||||||
|
advanced: false,
|
||||||
|
index: 0,
|
||||||
|
removeFileCallback: () => {},
|
||||||
|
status: 'pending',
|
||||||
|
progress: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
const badgeType = computed(() => {
|
||||||
|
switch (props.status) {
|
||||||
|
case 'pending': return 'warning'
|
||||||
|
case 'completed': return 'success'
|
||||||
|
case 'error': return 'error'
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const badgeLabel = computed(() => {
|
||||||
|
switch (props.status) {
|
||||||
|
case 'pending': return 'En attente'
|
||||||
|
case 'completed': return 'Terminé'
|
||||||
|
case 'error': return 'Annulé'
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const $primevue = usePrimeVue();
|
||||||
|
const totalSize = ref(0);
|
||||||
|
const totalSizePercent = ref(0);
|
||||||
|
|
||||||
|
const formatSize = (bytes: number) => {
|
||||||
|
const k = 1024;
|
||||||
|
const dm = 3;
|
||||||
|
const sizes = $primevue.config.locale?.fileSizeTypes ?? ['B', 'KB', 'MB', 'GB', 'TB'];;
|
||||||
|
|
||||||
|
if (bytes === 0) {
|
||||||
|
return `0 ${ sizes[0]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
const formattedSize = parseFloat((bytes / k ** i).toFixed(dm));
|
||||||
|
|
||||||
|
return `${formattedSize} ${sizes[i]}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemoveTemplatingFile = (file: File, removeFileCallback: (index: number) => void, index: number) => {
|
||||||
|
removeFileCallback(index);
|
||||||
|
totalSize.value -= parseInt(formatSize(file.size));
|
||||||
|
totalSizePercent.value = totalSize.value / 10;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFileIconClass = (file: File) => {
|
||||||
|
const type = file?.name.toLowerCase();
|
||||||
|
if (type?.endsWith('.pdf')) return 'ri-file-pdf-2-line';
|
||||||
|
if (type?.endsWith('.doc') || type?.endsWith('.docx')) return 'ri-file-word-2-line';
|
||||||
|
if (type?.endsWith('.xls') || type?.endsWith('.xlsx')) return 'ri-file-excel-2-line';
|
||||||
|
if (type?.endsWith('.txt')) return 'ri-file-text-line';
|
||||||
|
if (type?.endsWith('.zip') || type?.endsWith('.rar')) return 'ri-file-zip-line';
|
||||||
|
return 'ri-file-line'; // icône générique
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFileIconColor = (file: File) => {
|
||||||
|
const type = file?.name.toLowerCase();
|
||||||
|
if (type?.endsWith('.pdf')) return 'var(--border-plain-error)'; // rouge
|
||||||
|
if (type?.endsWith('.doc') || type?.endsWith('.docx')) return 'var(--border-plain-blue-france)'; // bleu
|
||||||
|
if (type?.endsWith('.xls') || type?.endsWith('.xlsx')) return 'var(--border-plain-success)'; // vert
|
||||||
|
if (type?.endsWith('.txt')) return 'var(--border-plain-grey)'; // gris
|
||||||
|
if (type?.endsWith('.zip') || type?.endsWith('.rar')) return 'var(--illustration-color-850-tournesol-default)'; // jaune
|
||||||
|
return 'var(--border-plain-blue-france)'; // bleu marine
|
||||||
|
};
|
||||||
|
|
||||||
|
const borderColor = computed(() => {
|
||||||
|
switch (props.status) {
|
||||||
|
case 'pending': return '2px solid var(--system-color-950-warning-active)';
|
||||||
|
case 'completed': return '2px solid var(--system-color-950-success-active)';
|
||||||
|
case 'error': return '2px solid var(--system-color-950-success-active)';
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const getImagePreview = (file: File) => {
|
||||||
|
return URL.createObjectURL(file);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="props.advanced"
|
||||||
|
class="advanced-file"
|
||||||
|
:title="`Fichier : ${props.file.name}`"
|
||||||
|
:style="{border: borderColor}"
|
||||||
|
:class="[styles['text-body-XS-mention-text-Medium']]"
|
||||||
|
>
|
||||||
|
<VButton
|
||||||
|
tertiary
|
||||||
|
noOutline
|
||||||
|
size="sm"
|
||||||
|
icon="ri-close-line"
|
||||||
|
@click="onRemoveTemplatingFile(props.file, props.removeFileCallback, props.index)"
|
||||||
|
title='Spprimer le fichier'
|
||||||
|
ref="clearfile"
|
||||||
|
aria-controls="clear-file"
|
||||||
|
type="button"
|
||||||
|
class="button"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
v-if="file?.type.startsWith('image/')"
|
||||||
|
role="presentation"
|
||||||
|
:alt="props.file?.name"
|
||||||
|
:src="getImagePreview(props.file)"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
v-else
|
||||||
|
:class="getFileIconClass(props.file)"
|
||||||
|
:style="{color: getFileIconColor(props.file), fontSize: '4rem', fontWeight: 500}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span :title="props.file?.name" class="file-name">
|
||||||
|
{{ props.file?.name }}
|
||||||
|
</span>
|
||||||
|
<span>{{ formatSize(props.file?.size ?? 0) }}</span>
|
||||||
|
<VBadge
|
||||||
|
v-if="advanced"
|
||||||
|
:label="badgeLabel"
|
||||||
|
:type="badgeType"
|
||||||
|
small
|
||||||
|
no-icon
|
||||||
|
/>
|
||||||
|
<VProgressBar
|
||||||
|
v-if="advanced && status === 'pending'"
|
||||||
|
:value="props.progress"
|
||||||
|
:show-value="false"
|
||||||
|
style="width: 6rem; margin-top: 0.5rem;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- Simple file -->
|
||||||
|
<div v-else class="simple-file" >
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
v-if="file?.type.startsWith('image/')"
|
||||||
|
role="presentation"
|
||||||
|
:alt="props.file.name"
|
||||||
|
:src="getImagePreview(props.file)"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
v-else
|
||||||
|
:class="getFileIconClass(props.file)"
|
||||||
|
:style="{color: getFileIconColor(props.file), fontSize: '2.5rem', padding: '0px'}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="file-info" :class="[styles['text-body-SM-detail-text-Regular']]">
|
||||||
|
<span>{{ props.file?.name }}</span>
|
||||||
|
<span>{{ formatSize(props.file?.size ?? 0) }}</span>
|
||||||
|
</div>
|
||||||
|
<VBadge
|
||||||
|
:label="badgeLabel"
|
||||||
|
:type="badgeType"
|
||||||
|
small
|
||||||
|
no-icon
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
img{
|
||||||
|
width: 2.5rem;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button{
|
||||||
|
display: flex;
|
||||||
|
align-self: self-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-file{
|
||||||
|
border-radius: 3px;
|
||||||
|
width: fit-content;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.simple-file{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(var(--p-fileupload-content-gap) / 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name{
|
||||||
|
width: 6rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
margin: 0rem 0.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: start;
|
||||||
|
gap: 0.025rem;
|
||||||
|
}
|
||||||
|
</style>
|
362
src/components/file/VFileUpload.vue
Normal file
362
src/components/file/VFileUpload.vue
Normal file
|
@ -0,0 +1,362 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import FileUpload from 'primevue/fileupload';
|
||||||
|
import VHint from '../hint/VHint.vue';
|
||||||
|
import VLabel from '../label/VLabel.vue';
|
||||||
|
import VButton from '../button/VButton.vue';
|
||||||
|
import VButtonGroup from '../button/VButtonGroup.vue';
|
||||||
|
import VFile from './VFile.vue';
|
||||||
|
import VMessage from '../message/VMessage.vue';
|
||||||
|
import VScrollpanel from '../scrollpanel/VScrollpanel.vue';
|
||||||
|
import VLabelErrorProxy from './VLabelErrorProxy.vue';
|
||||||
|
import type IVFileUpload from './IVFileUpload.type';
|
||||||
|
import type { FileUploadErrorEvent, FileUploadProgressEvent, FileUploadRemoveEvent, FileUploadSelectEvent, FileUploadUploadEvent } from 'primevue/fileupload';
|
||||||
|
import { computed, useId, ref } from 'vue';
|
||||||
|
import styles from '@visua/typography.module.css';
|
||||||
|
|
||||||
|
const fileUploadRef = ref();
|
||||||
|
const fileProgressMap = ref<Record<string, number>>({});
|
||||||
|
const hasActiveError = ref(false);
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<IVFileUpload>(), {
|
||||||
|
id: () => useId(),
|
||||||
|
label: 'Ajouter un fichier',
|
||||||
|
validMessage: '',
|
||||||
|
hint: '',
|
||||||
|
disabled: false,
|
||||||
|
invalidFileSizeMessage: 'Taille de fichier {0} invalide, la taille du fichier doit être plus petite que {1}',
|
||||||
|
invalidFileLimitMessage: 'Nombre maximal des fichiers atteint, le nombre maximal est {0.}',
|
||||||
|
invalidFileTypeMessage: 'Type de fichier invalide, le type de fichier {0} n\'est pas pris en charge',
|
||||||
|
accept: undefined,
|
||||||
|
chooseLabel: 'Choisir',
|
||||||
|
name: undefined,
|
||||||
|
url: undefined,
|
||||||
|
maxFileSize: undefined,
|
||||||
|
fileLimit: undefined,
|
||||||
|
withCredentials: false,
|
||||||
|
previewWidth: 50,
|
||||||
|
uploadLabel: 'Téléverser',
|
||||||
|
cancelLabel: 'Annuler',
|
||||||
|
showCancelButton: true,
|
||||||
|
showUploadButton: true,
|
||||||
|
advanced: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits([
|
||||||
|
'select',
|
||||||
|
'before-upload',
|
||||||
|
'progress',
|
||||||
|
'upload',
|
||||||
|
'error',
|
||||||
|
'before-send',
|
||||||
|
'clear',
|
||||||
|
'remove',
|
||||||
|
'removeUploadFile',
|
||||||
|
'uploader',
|
||||||
|
]);
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
upload: () => fileUploadRef.value.upload()
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSelect = (event: FileUploadSelectEvent) => {
|
||||||
|
emit('select', event);
|
||||||
|
if (!props.advanced && event.files.length > 0 && fileUploadRef.value) {
|
||||||
|
fileUploadRef.value.files = [event.files.at(-1)];
|
||||||
|
fileUploadRef.value.uploadedFiles = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
emit('clear');
|
||||||
|
hasActiveError.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpload = (event: FileUploadUploadEvent) => {
|
||||||
|
emit('upload', event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = (event: FileUploadRemoveEvent) => {
|
||||||
|
emit('remove', event);
|
||||||
|
hasActiveError.value =false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProgress = (event: FileUploadProgressEvent) => {
|
||||||
|
emit('progress', event);
|
||||||
|
|
||||||
|
const files = fileUploadRef.value?.files || [];
|
||||||
|
files.forEach((file: File) => {
|
||||||
|
fileProgressMap.value[file.name] = event.progress;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleError = (event: FileUploadErrorEvent) => {
|
||||||
|
emit('error', event);
|
||||||
|
hasActiveError.value = true;
|
||||||
|
if (!props.advanced && fileUploadRef.value) {
|
||||||
|
fileUploadRef.value.uploadedFiles = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastSelectedFile = computed(() => {
|
||||||
|
const files = fileUploadRef.value?.files || [];
|
||||||
|
return files.at(-1) ?? null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const uploadEvent = (callback: () => void) => {
|
||||||
|
callback();
|
||||||
|
};
|
||||||
|
|
||||||
|
const padding = computed(() => props.advanced ? '1.125rem' : '0rem')
|
||||||
|
const borderColor = computed(() => props.advanced ? 'var(--border-default-grey)' : 'transparent');
|
||||||
|
|
||||||
|
const labelState = computed(() => {
|
||||||
|
if(!hasActiveError.value && !props.disabled) return 'default';
|
||||||
|
else if(hasActiveError.value && !props.disabled) return 'error';
|
||||||
|
else return undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
type MessageType = 'alert' | 'warning' | 'success' | 'info';
|
||||||
|
|
||||||
|
const globalStatusMessage = computed<{
|
||||||
|
type: MessageType;
|
||||||
|
title: string;
|
||||||
|
} | null>(() => {
|
||||||
|
const files = fileUploadRef.value?.files || [];
|
||||||
|
const uploaded = fileUploadRef.value?.uploadedFiles || [];
|
||||||
|
|
||||||
|
const hasError = files.some((f: { status: string; }) => f.status === 'error');
|
||||||
|
const hasPending = files.length > 0;
|
||||||
|
|
||||||
|
if (hasError) return { type: 'alert', title: 'Erreur lors du téléversement' };
|
||||||
|
if (hasPending) return { type: 'warning', title: 'En attente de téléversement' };
|
||||||
|
if (uploaded.length > 0) return { type: 'success', title: 'Tous les fichiers ont été téléversés' };
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="container" data-testid="file-label">
|
||||||
|
<VLabel
|
||||||
|
:for="`file-upload-${props.id}`"
|
||||||
|
:label="props.label"
|
||||||
|
:disabled="props.disabled"
|
||||||
|
:type="labelState"
|
||||||
|
:hint="props.hint"
|
||||||
|
/>
|
||||||
|
<FileUpload
|
||||||
|
ref="fileUploadRef"
|
||||||
|
:id="`file-upload-${props.id}`"
|
||||||
|
:multiple="props.advanced"
|
||||||
|
:invalid-file-limit-message="props.invalidFileLimitMessage"
|
||||||
|
:invalid-file-size-message="props.invalidFileSizeMessage"
|
||||||
|
:invalid-file-type-message="props.invalidFileTypeMessage"
|
||||||
|
mode="advanced"
|
||||||
|
:accept="props.accept"
|
||||||
|
:disabled="props.disabled"
|
||||||
|
v-bind="$attrs"
|
||||||
|
:auto="!props.advanced"
|
||||||
|
:choose-label="props.chooseLabel"
|
||||||
|
:upload-label="props.uploadLabel"
|
||||||
|
:cancel-label="props.cancelLabel"
|
||||||
|
:name="props.name"
|
||||||
|
:url="props.url"
|
||||||
|
:max-file-size="props.maxFileSize"
|
||||||
|
:file-limit="props.fileLimit"
|
||||||
|
:with-credentials="props.withCredentials"
|
||||||
|
:preview-width="props.previewWidth"
|
||||||
|
:show-cancel-button="props.showCancelButton"
|
||||||
|
:show-upload-button="props.showUploadButton"
|
||||||
|
@select="handleSelect($event)"
|
||||||
|
@before-send="emit('before-send', $event)"
|
||||||
|
@progress="handleProgress($event)"
|
||||||
|
@before-upload="emit('before-upload', $event)"
|
||||||
|
@uploader="emit('uploader', $event)"
|
||||||
|
@upload="handleUpload($event)"
|
||||||
|
@error="handleError($event)"
|
||||||
|
@clear="handleClear()"
|
||||||
|
@remove="handleRemove($event)"
|
||||||
|
@remove-uploaded-file="emit('removeUploadFile', $event)"
|
||||||
|
class="p-fileupload"
|
||||||
|
>
|
||||||
|
<template #header="slotProps">
|
||||||
|
<VButtonGroup
|
||||||
|
v-if="props.advanced"
|
||||||
|
:buttons="[
|
||||||
|
{
|
||||||
|
label: 'Parcourir...',
|
||||||
|
onClick: slotProps.chooseCallback,
|
||||||
|
title: 'parcourir les fichiers',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Téléverser',
|
||||||
|
icon: 'ri-upload-cloud-line',
|
||||||
|
onClick: () => uploadEvent(slotProps.uploadCallback),
|
||||||
|
secondary: true,
|
||||||
|
disabled: !slotProps.files || slotProps.files.length === 0,
|
||||||
|
title: 'televerser les fichiers'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Supprimer',
|
||||||
|
tertiary: true,
|
||||||
|
onClick: slotProps.clearCallback,
|
||||||
|
disabled: !slotProps.files || slotProps.files.length === 0,
|
||||||
|
title: 'suprimer les fichiers'
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
size="sm"
|
||||||
|
:disabled="props.disabled"
|
||||||
|
inlineLayoutWhen="always"
|
||||||
|
title="boutons de gestion des fichiers"
|
||||||
|
/>
|
||||||
|
<div v-else class="simple">
|
||||||
|
<VButton
|
||||||
|
label="Parcourir..."
|
||||||
|
:disabled="props.disabled"
|
||||||
|
size="sm"
|
||||||
|
@click="slotProps.chooseCallback"
|
||||||
|
title="parcourir les fichiers"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="(!slotProps.files || slotProps.files.length === 0) && (!slotProps.uploadedFiles || slotProps.uploadedFiles.length === 0) && !hasActiveError"
|
||||||
|
:class="[styles['text-body-SM-detail-text-Regular']]"
|
||||||
|
>
|
||||||
|
Aucun fichier sélectionné
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #empty v-if="props.advanced && !hasActiveError">
|
||||||
|
<div class="fileupload-empty" :class="[styles['text-body-SM-detail-text-Regular']]">
|
||||||
|
<i class="ri-upload-cloud-line upload-cloud-icon"></i>
|
||||||
|
<p>Glissez-déposez les fichiers ici pour les téléverser.</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #content="slotProps">
|
||||||
|
<div v-if="props.advanced && !hasActiveError" style="margin-top: 0.75rem;" class="file-content">
|
||||||
|
<VMessage
|
||||||
|
v-if="globalStatusMessage"
|
||||||
|
:type="globalStatusMessage.type"
|
||||||
|
:title="globalStatusMessage.title"
|
||||||
|
/>
|
||||||
|
<VScrollpanel height="20rem">
|
||||||
|
<div class="file-area">
|
||||||
|
<div v-if="slotProps.files.length > 0" class="file-non-uploaded-area">
|
||||||
|
<VFile
|
||||||
|
v-for="(file, index) in slotProps.files"
|
||||||
|
:key="file.name + file.type + file.size"
|
||||||
|
:file="file"
|
||||||
|
:index="index"
|
||||||
|
:remove-file-callback="slotProps.removeFileCallback"
|
||||||
|
:progress="fileProgressMap[file.name] || 0"
|
||||||
|
status="pending"
|
||||||
|
advanced
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="slotProps.uploadedFiles.length > 0" class="file-uploaded-area">
|
||||||
|
<VFile
|
||||||
|
v-for="(file, index) in slotProps.uploadedFiles"
|
||||||
|
:key="file.name + file.type + file.size"
|
||||||
|
:file="file"
|
||||||
|
:index="index"
|
||||||
|
:remove-file-callback="slotProps.removeFileCallback"
|
||||||
|
:progress="100"
|
||||||
|
status="completed"
|
||||||
|
advanced
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VScrollpanel>
|
||||||
|
</div>
|
||||||
|
<div v-if="!props.advanced && !hasActiveError">
|
||||||
|
<VFile
|
||||||
|
v-if="lastSelectedFile"
|
||||||
|
:file="lastSelectedFile"
|
||||||
|
:remove-file-callback="slotProps.removeFileCallback"
|
||||||
|
status="pending"
|
||||||
|
/>
|
||||||
|
<VFile
|
||||||
|
v-if="slotProps.uploadedFiles.length > 0 && slotProps.uploadedFiles.at(-1)"
|
||||||
|
:file="slotProps.uploadedFiles.at(-1)!"
|
||||||
|
:remove-file-callback="slotProps.removeFileCallback"
|
||||||
|
status="completed"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
role="alert"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
<VHint
|
||||||
|
v-for="message of slotProps.messages"
|
||||||
|
:key="message"
|
||||||
|
:title="message"
|
||||||
|
type="alert"
|
||||||
|
icon
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<VLabelErrorProxy :hasError="(slotProps.messages ?? []).length > 0" @update:error="hasActiveError = $event" />
|
||||||
|
</template>
|
||||||
|
</FileUpload>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
*{
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: start;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-fileupload{
|
||||||
|
width: 100%;
|
||||||
|
--p-fileupload-border-color: v-bind(borderColor);
|
||||||
|
padding: v-bind(padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
.simple{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--p-fileupload-file-gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileupload-empty{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-cloud-icon{
|
||||||
|
font-size: 5rem;
|
||||||
|
color: var(--border-contrast-grey);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-content,
|
||||||
|
.file-area{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-content{gap: 0.75rem;}
|
||||||
|
.file-area{gap: 0.5rem;}
|
||||||
|
|
||||||
|
.file-non-uploaded-area,
|
||||||
|
.file-uploaded-area {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
</style>
|
27
src/components/file/VLabelErrorProxy.vue
Normal file
27
src/components/file/VLabelErrorProxy.vue
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, onUpdated, watch } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
hasError: boolean
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:error', value: boolean): void
|
||||||
|
}>();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
emit('update:error', props.hasError);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUpdated(() => {
|
||||||
|
emit('update:error', props.hasError);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.hasError, (val) => {
|
||||||
|
emit('update:error', val);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<slot/>
|
||||||
|
</template>
|
35
src/components/scrollpanel/VScrollpanel.vue
Normal file
35
src/components/scrollpanel/VScrollpanel.vue
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ScrollPanel from 'primevue/scrollpanel';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
step?: number
|
||||||
|
height: string
|
||||||
|
width?: string
|
||||||
|
}>(), {
|
||||||
|
step: 5,
|
||||||
|
width: '100%'
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ScrollPanel
|
||||||
|
:step="props.step"
|
||||||
|
class="p-scrollpanel-content"
|
||||||
|
:style="{width: props.width, maxHeight: props.height}"
|
||||||
|
>
|
||||||
|
<slot/>
|
||||||
|
</ScrollPanel>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
*{
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-scrollpanel {
|
||||||
|
height: fit-content;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
</style>
|
45
test/VFileUpload.spec.ts
Normal file
45
test/VFileUpload.spec.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import VFileUpload from '../src/components/file/VFileUpload.vue'
|
||||||
|
|
||||||
|
describe('VFileUpload emits', () => {
|
||||||
|
it('doit émettre les événements personnalisés', async () => {
|
||||||
|
// Création des mocks
|
||||||
|
const onSelect = vi.fn();
|
||||||
|
const onUpload = vi.fn();
|
||||||
|
const onError = vi.fn();
|
||||||
|
const onRemove = vi.fn();
|
||||||
|
const onClear = vi.fn();
|
||||||
|
|
||||||
|
const wrapper = mount(VFileUpload, {
|
||||||
|
props: {
|
||||||
|
advanced: true,
|
||||||
|
onSelect,
|
||||||
|
onUpload,
|
||||||
|
onError,
|
||||||
|
onRemove,
|
||||||
|
onClear
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Simule une sélection de fichier
|
||||||
|
await wrapper.vm.$emit('select', { files: [{ name: 'test.png' }] });
|
||||||
|
expect(onSelect).toHaveBeenCalledWith({ files: [{ name: 'test.png' }] });
|
||||||
|
|
||||||
|
// Simule une erreur
|
||||||
|
await wrapper.vm.$emit('error', { message: 'Erreur test' });
|
||||||
|
expect(onError).toHaveBeenCalledWith({ message: 'Erreur test' });
|
||||||
|
|
||||||
|
// Simule un upload
|
||||||
|
await wrapper.vm.$emit('upload', { files: ['fichier1'] });
|
||||||
|
expect(onUpload).toHaveBeenCalledWith({ files: ['fichier1'] });
|
||||||
|
|
||||||
|
// Simule une suppression
|
||||||
|
await wrapper.vm.$emit('remove', { file: 'test.png' });
|
||||||
|
expect(onRemove).toHaveBeenCalledWith({ file: 'test.png' });
|
||||||
|
|
||||||
|
// Simule un clear
|
||||||
|
await wrapper.vm.$emit('clear');
|
||||||
|
expect(onClear).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user