Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#920: add reader support for file input #1520

Merged
merged 6 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions digiwf-apps/packages/apps/digiwf-tasklist/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,10 @@ declare module 'vue' {
KeyboardAccessibilityIcon: typeof import('./src/components/UI/icons/KeyboardAccessibilityIcon.vue')['default']
LeaveSiteDialog: typeof import('./src/components/common/LeaveSiteDialog.vue')['default']
LoadingFab: typeof import('./src/components/UI/LoadingFab.vue')['default']
Notifcation: typeof import('./src/components/common/Notifcation.vue')['default']
PdfOutput: typeof import('./src/components/form/PdfOutput.vue')['default']
ProcessDefinitionItem: typeof import('./src/components/process/ProcessDefinitionItem.vue')['default']
ProcessInstanceItem: typeof import('./src/components/process/ProcessInstanceItem.vue')['default']
SearchField: typeof import('./src/components/common/SearchField.vue')['default']
Snackbar: typeof import('./src/components/common/Snackbar.vue')['default']
SortBySelect: typeof import('./src/components/common/SortBySelect.vue')['default']
StatementIcon: typeof import('./src/components/UI/icons/StatementIcon.vue')['default']
TaskFollowUpDialog: typeof import('./src/components/task/TaskFollowUpDialog.vue')['default']
Expand Down Expand Up @@ -87,13 +85,13 @@ declare module 'vue' {
VListItemTitle: typeof import('vuetify/lib')['VListItemTitle']
VMain: typeof import('vuetify/lib')['VMain']
VMenu: typeof import('vuetify/lib')['VMenu']
VMessages: typeof import('vuetify/lib')['VMessages']
VMultiUserInput: typeof import('./src/components/schema/VMultiUserInput.vue')['default']
VNavigationDrawer: typeof import('vuetify/lib')['VNavigationDrawer']
VProgressCircular: typeof import('vuetify/lib')['VProgressCircular']
VRow: typeof import('vuetify/lib')['VRow']
VSelect: typeof import('vuetify/lib')['VSelect']
VSimpleTable: typeof import('vuetify/lib')['VSimpleTable']
VSnackbar: typeof import('vuetify/lib')['VSnackbar']
VSpacer: typeof import('vuetify/lib')['VSpacer']
VSpeedDial: typeof import('vuetify/lib')['VSpeedDial']
VStepper: typeof import('vuetify/lib')['VStepper']
Expand Down
Original file line number Diff line number Diff line change
@@ -1,57 +1,60 @@
<template>
<div>
<a target="_blank" @click="openInTab()">
<v-card class="doc-card mb-2" elevation="2" outlined max-width="350px">
<v-card-title class="text-subtitle-1 title">
<div class="d-flex align-start flex-row">
<v-icon left size="30" class="mr-2">
{{ icon }}
</v-icon>
{{ document.name }}
<v-card class="doc-card mb-2" elevation="2" tabindex="-1" outlined max-width="350px" @click="openInTab()">
<v-card-title class="text-subtitle-1 title">
<div class="d-flex align-start flex-row" tabindex="0">
<v-icon left size="30" class="mr-2" :aria-label="document.name">
{{ icon }}
</v-icon>
{{ document.name }}
</div>
</v-card-title>
<v-card-text>
<div class="preview">
<v-img
v-if="isImage"
class="preview-component"
:src="document.data"
max-width="200px"
:alt="'Bildvorschau von ' + document.name"
>
</v-img>

<vue2-pdf-embed
v-else-if="isPdf"
:source="document.data"
class="preview-component"
:aria-label="'PDF Vorschau von ' + document.name"

/>

<div v-else class="preview-text">Keine Vorschau verfügbar</div>
<div>
<div class="footer" tabindex="0">{{ documentSize }}</div>
<template v-if="!readonly">
<v-btn
class="remove-button ma-1"
elevation="1"
icon
@click.stop="removeDocument"
:aria-label="document.name + ' entfernen'"
>
<v-icon> mdi-delete</v-icon>
</v-btn>
</template>
</div>
</v-card-title>
<v-card-text>
<div class="preview">
<v-img
v-if="isImage"
class="preview-component"
:src="document.data"
max-width="200px"
alt="Image preview..."
>
</v-img>

<vue2-pdf-embed
v-else-if="document.type === 'application/pdf'"
:source="document.data"
class="preview-component"
/>

<div v-else class="preview-text">Keine Vorschau verfügbar</div>
<div>
<div class="footer">{{ formatBytes(0) }}</div>
<template v-if="!readonly">
<v-btn
class="remove-button ma-1"
elevation="1"
icon
@click.stop="removeDocument"
>
<v-icon> mdi-delete</v-icon>
</v-btn>
</template>
</div>
</div>
</v-card-text>
</v-card>
</a>
</div>
</v-card-text>
</v-card>
</div>
</template>

<script lang="ts">

import {computed, defineComponent} from "vue";
import {fileIcons} from "../util";
import {formatBytes} from "@/middleware/fileSize";
import {createBlobUrl} from "@/middleware/url";

export default defineComponent({
props: ['document', 'readonly'],
Expand All @@ -63,47 +66,11 @@ export default defineComponent({
const icon = computed(() => fileIcons[props.document.type] ?? "mdi-file");

const isImage = computed(() => props.document.type.toLowerCase() === "image/jpeg" || props.document.type.toLowerCase() === "image/png");

const blobUrl = (): string => {
const byteCharacters = calcByteCharacters.value;

const byteArrays: Uint8Array[] = [];

for (let offset = 0; offset < byteCharacters.length; offset += 1024) {
const slice = byteCharacters.slice(offset, offset + 1024);

const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
const blob = new Blob(byteArrays, {type: props.document.type});
return URL.createObjectURL(blob);
}
const isPdf = computed(() => props.document.type === 'application/pdf')

const openInTab = () => {
const calcVlobUrl = blobUrl();
const link = document.createElement("a");
link.href = calcVlobUrl;
link.setAttribute("download", props.document.name!);
document.body.appendChild(link);
link.click();
}

const formatBytes = (decimals = 2) => {
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];

const i = Math.floor(Math.log(props.document.size) / Math.log(k));

return (
parseFloat((props.document.size / Math.pow(k, i)).toFixed(dm)) +
" " +
sizes[i]
);
const url = createBlobUrl(calcByteCharacters.value, props.document.type);
window.open(url, "_blank")
}

const removeDocument = () => {
Expand All @@ -114,8 +81,9 @@ export default defineComponent({
calcByteCharacters,
icon,
isImage,
isPdf,
openInTab,
formatBytes,
documentSize: formatBytes(props.document.size),
removeDocument
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@
type="file"
v-bind="schema['x-props']"
@change="changeInput"
aria-label="Datei hochladen"
>
<template #label>
<span>{{ label }}</span>
<span v-if="isRequired()" aria-hidden="true" style="font-weight: bold; color: red"> *</span>
<span tabindex="0">{{ label }}</span>
<span v-if="isRequired()" aria-hidden="true" style="font-weight: bold; color: red" aria-label="Eingabe ist ein Pflichtfeld"> *</span>
</template>
<template #append-outer>
<v-tooltip v-if="schema.description" :open-on-hover="false" left>
<template v-slot:activator="{ on }">
<v-btn icon retain-focus-on-click @blur="on.blur" @click="on.click">
<v-btn icon retain-focus-on-click @blur="on.blur" @click="on.click" aria-label="Beschreibung anzeigen">
<v-icon> mdi-information</v-icon>
</v-btn>
</template>
Expand Down Expand Up @@ -62,8 +63,17 @@ import {
} from "@/middleware/presignedUrls";
import {checkRequired} from "@/validation/required";
import {getMimeType, validateFileType} from "@/validation/fileType";
import DwfFilePreview from "@/components/DwfFilePreview.vue";

/**
* existing bug!. Prepend icon cannot be overridden for set tabindex="-1". More information https://github.com/vuetifyjs/vuetify/issues/9580
*/
export default defineComponent({
computed: {
DwfFilePreview() {
return DwfFilePreview
}
},
props: [
'valid',
'readonly',
Expand All @@ -79,20 +89,19 @@ export default defineComponent({
'on'
],
setup(props) {
let model = "";
let fileValue = ref<File[] | null>(null);
let data: any = {};
let documents = ref<DocumentData[]>([]);
let errorMessage = ref<string>("");
let isLoading = ref<boolean>(false);
let uuid = "";
const fileValue = ref<File[]>([]);
const data: any = {};
const documents = ref<DocumentData[]>([]);
const errorMessage = ref<string>("");
const isLoading = ref<boolean>(false);
const uuid = ref("");
const maxFiles = props.schema.maxFiles || 10;
const maxFileSize = props.schema.maxFileSize || 10;
const maxTotalSize = props.schema.maxTotalSize;
const mbInByte = 1048576;
const hint = !!maxTotalSize ?
"Es dürfen maximal " + maxFiles + " Dateien mit einer Gesamtgröße von " + maxTotalSize + " MB hochgeladen werden" :
"Es dürfen maximal " + maxFiles + " Dateien hochgeladen werden";
`Es dürfen maximal ${maxFiles} Dateien mit einer Gesamtgröße von ${maxTotalSize} MB hochgeladen werden` :
`Es dürfen maximal ${maxFiles} Dateien hochgeladen werden`;
let rules = props.rules ? props.rules : true;

const apiEndpoint = inject<string>('apiEndpoint');
Expand Down Expand Up @@ -319,7 +328,7 @@ export default defineComponent({
try {
addDocument(event.target?.result, file);
} catch (e: any) {
errorMessage = e.message;
errorMessage.value = e.message;
}
};
reader.readAsArrayBuffer(file);
Expand Down Expand Up @@ -385,20 +394,19 @@ export default defineComponent({
//initialize uuid if enabled
if (props.schema.uuidEnabled) {
if (props.value && props.value.key) {
uuid = props.value.key;
uuid.value = props.value.key;
} else {
uuid = uuidv4();
uuid.value = uuidv4();
}
}
rules.push(() => documents.value.length <= maxFiles || 'Es dürfen maximal ' + maxFiles + ' Dateien übergeben werden');
rules.push(() => documents.value.length <= maxFiles || `Es dürfen maximal ${maxFiles} Dateien übergeben werden`);
if (!!maxTotalSize) {
rules.push(() => validateTotalSize() <= maxTotalSize || 'Die Gesamtgröße aller Dateien darf ' + maxTotalSize + ' MB nicht überschreiten');
rules.push(() => validateTotalSize() <= maxTotalSize || `Die Gesamtgröße aller Dateien darf ${maxTotalSize} MB nicht überschreiten`);
}
loadInitialValues();
})

return {
model,
fileValue,
data,
documents,
Expand All @@ -412,7 +420,6 @@ export default defineComponent({
removeDocument,
isRequired
}

}
});
</script>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const formatBytes = (size: number, decimals: number = 2) => {
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];

const i = Math.floor(Math.log(size) / Math.log(k));

return (
parseFloat((size / Math.pow(k, i)).toFixed(dm)) +
" " +
sizes[i]
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export const createBlobUrl = (byteCharacters: string, type: string): string => {

const byteArrays: Uint8Array[] = [];

for (let offset = 0; offset < byteCharacters.length; offset += 1024) {
const slice = byteCharacters.slice(offset, offset + 1024);

const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
const blob = new Blob(byteArrays, {type: type});
return URL.createObjectURL(blob);
}
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@
"requiredObject"
],
"fieldType": "file",
"title": "Files",
"title": "Dateien ohne Einschränkungen",
"x-display": "custom-multi-file-input",
"type": "object",
"filePath": "test",
Expand All @@ -249,7 +249,7 @@
"dense": true
},
"fieldType": "file",
"title": "Files with validation",
"title": "Dateien mit Validierung",
"x-display": "custom-multi-file-input",
"type": "object",
"filePath": "test",
Expand Down
Loading