dms upload input preview prop on core component; use medium sized preview for pdf in iframe, image file in <img> tag; split entschuldigung von/bis from 2 datetime inputs into 2 date and 2 time input since this is apparently a usability issue; changed validation for this in order to verify both chronologically; also added validation for this to backend; since assistenz can edit these exact values, also backend validate on updateEntschuldigung in AdministrationApi.php;

This commit is contained in:
Johann Hoffmann
2026-10-19 17:04:46 +02:00
parent e009636e52
commit e067f346ea
3 changed files with 107 additions and 1 deletions
+19 -1
View File
@@ -1,8 +1,10 @@
import DmsItem from './Dms/Item.js';
import DmsPreview from './Dms/Preview.js';
export default {
components: {
DmsItem
DmsItem,
DmsPreview
},
emits: [
'update:modelValue'
@@ -24,6 +26,21 @@ export default {
disabled: {
type: Boolean,
default: false
},
// shows the chosen images and pdfs below the input before the upload
preview: Boolean,
// small, medium, large or a css length like '400px'
previewHeight: {
type: String,
default: 'medium'
}
},
computed: {
previewFiles() {
if (!this.preview || !this.modelValue)
return [];
// stored dms files are json placeholders, they have no content to show
return Array.from(this.modelValue).filter(file => file instanceof File && file.type != 'application/x.fhc-dms+json');
}
},
methods: {
@@ -103,5 +120,6 @@ export default {
>
</dms-item>
</ul>
<dms-preview v-for="(file, index) in previewFiles" :key="index" :file="file" :height="previewHeight"></dms-preview>
</div>`
}
@@ -0,0 +1,68 @@
// only these types get a preview. html or svg in the iframe would run their scripts in the page origin
const IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
const HEIGHTS = {
small: '30vh',
medium: '50vh',
large: '80vh'
};
/**
* Preview of one chosen file before the upload: an image as img, a pdf in the pdf viewer of the browser.
* height: small, medium, large or a css length like '400px'.
*/
export default {
props: {
file: {
type: File,
required: true
},
height: {
type: String,
default: 'medium'
}
},
data() {
return {
url: ''
};
},
computed: {
isImage() {
return IMAGE_TYPES.includes(this.file.type);
},
// android chrome has no inline pdf viewer, there a pdf gets no preview
isPdf() {
return this.file.type == 'application/pdf' && navigator.pdfViewerEnabled !== false;
},
cssHeight() {
return HEIGHTS[this.height] || this.height;
}
},
watch: {
file: {
handler() {
this.revokeUrl();
// an object url only references the file, no base64 copy in memory
if (this.isImage || this.isPdf)
this.url = URL.createObjectURL(this.file);
},
immediate: true
}
},
unmounted() {
this.revokeUrl();
},
methods: {
revokeUrl() {
if (this.url)
URL.revokeObjectURL(this.url);
this.url = '';
}
},
template: `
<div v-if="url" class="form-upload-dms-preview mt-2">
<img v-if="isImage" :src="url" :alt="file.name" class="img-fluid border rounded" :style="{ maxHeight: cssHeight }">
<iframe v-else :src="url" :title="file.name" class="d-block w-100 border rounded" :style="{ height: cssHeight }"></iframe>
</div>`
}
+20
View File
@@ -33136,6 +33136,26 @@ array(
)
)
),
array(
'app' => 'anwesenheiten',
'category' => 'global',
'phrase' => 'errorValidateUhrzeit',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Die Endzeit muss nach der Startzeit liegen.',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'The end time must be after the start time.',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'anwesenheiten',
'category' => 'global',