hotfix: enhance radio button and file upload components
- Improved the styling and functionality of the radio button component, including a new radio button style. - Added a loading spinner to the file upload component to indicate when large files are being processed. - Added the ability to sort files in the file upload component. - Fixed an issue where the radio button component was not correctly updating the model value. - Updated the dataset creation and edit forms to use the new radio button component. - Added a global declaration for the `sort_order` property on the `File` interface. - Updated the API to filter authors by first and last name. - Removed the import of `_checkbox-radio-switch.css` as the radio button styling is now handled within the component.
This commit is contained in:
parent
b93e46207f
commit
9823364670
11 changed files with 272 additions and 181 deletions
|
@ -17,6 +17,15 @@
|
|||
<p class="text-lg text-blue-700">Drop files to upload</p>
|
||||
</div>
|
||||
|
||||
<!-- Loading Spinner when processing big files -->
|
||||
<div v-if="isLoading" class="absolute inset-0 z-60 flex items-center justify-center bg-gray-500 bg-opacity-50">
|
||||
<svg class="animate-spin h-12 w-12 text-white" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M12 2a10 10 0 0110 10h-4a6 6 0 00-6-6V2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- scroll area -->
|
||||
<div class="h-full p-8 w-full h-full flex flex-col">
|
||||
<header class="flex items-center justify-center w-full">
|
||||
|
@ -32,9 +41,8 @@
|
|||
<p class="mb-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
<span class="font-semibold">Click to upload</span> or drag and drop
|
||||
</p>
|
||||
<!-- <p class="text-xs text-gray-500 dark:text-gray-400">SVG, PNG, JPG or GIF (MAX. 800x400px)</p> -->
|
||||
</div>
|
||||
<input id="dropzone-file" type="file" class="hidden" @change="onChangeFile" multiple="true" />
|
||||
<input id="dropzone-file" type="file" class="hidden" @click="showSpinner" @change="onChangeFile" @cancel="cancelSpinner" multiple="true" />
|
||||
</label>
|
||||
</header>
|
||||
|
||||
|
@ -241,6 +249,8 @@ class FileUploadComponent extends Vue {
|
|||
|
||||
@Ref('overlay') overlay: HTMLDivElement;
|
||||
|
||||
|
||||
public isLoading: boolean = false;
|
||||
private counter: number = 0;
|
||||
// @Prop() files: Array<TestFile>;
|
||||
|
||||
|
@ -264,7 +274,7 @@ class FileUploadComponent extends Vue {
|
|||
set deletetFiles(values: Array<TethysFile>) {
|
||||
// this.modelValue = value;
|
||||
this.filesToDelete.length = 0;
|
||||
this.filesToDelete.push(...values);
|
||||
this.filesToDelete.push(...values);
|
||||
}
|
||||
|
||||
get items(): Array<TethysFile | File> {
|
||||
|
@ -342,10 +352,10 @@ class FileUploadComponent extends Vue {
|
|||
}
|
||||
|
||||
// reset counter and append file to gallery when file is dropped
|
||||
|
||||
public dropHandler(event: DragEvent): void {
|
||||
event.preventDefault();
|
||||
const dataTransfer = event.dataTransfer;
|
||||
// let bigFileFound = false;
|
||||
if (dataTransfer) {
|
||||
for (const file of event.dataTransfer?.files) {
|
||||
// let fileName = String(file.name.replace(/\.[^/.]+$/, ''));
|
||||
|
@ -353,28 +363,73 @@ class FileUploadComponent extends Vue {
|
|||
// if (file.type.match('image.*')) {
|
||||
// this.generateURL(file);
|
||||
// }
|
||||
// if (file.size > 62914560) { // 60 MB in bytes
|
||||
// bigFileFound = true;
|
||||
// }
|
||||
this._addFile(file);
|
||||
}
|
||||
this.overlay.classList.remove('draggedover');
|
||||
this.counter = 0;
|
||||
}
|
||||
// if (bigFileFound) {
|
||||
// this.isLoading = true;
|
||||
// // Assume file processing delay; adjust timeout as needed or rely on async processing completion.
|
||||
// setTimeout(() => {
|
||||
// this.isLoading = false;
|
||||
// }, 1500);
|
||||
// }
|
||||
}
|
||||
|
||||
public showSpinner() {
|
||||
// event.preventDefault();
|
||||
this.isLoading = true;
|
||||
}
|
||||
|
||||
public cancelSpinner() {
|
||||
// const target = event.target as HTMLInputElement;
|
||||
// // If no files were selected, remove spinner
|
||||
// if (!target.files || target.files.length === 0) {
|
||||
// this.isLoading = false;
|
||||
// }
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
public onChangeFile(event: Event) {
|
||||
event.preventDefault();
|
||||
let target = event.target as HTMLInputElement;
|
||||
// let uploadedFile = event.target.files[0];
|
||||
// let fileName = String(event.target.files[0].name.replace(/\.[^/.]+$/, ''));
|
||||
|
||||
for (const file of event.target.files) {
|
||||
// let fileName = String(event.target.files[0].name.replace(/\.[^/.]+$/, ''));
|
||||
// file.label = fileName;
|
||||
// if (file.type.match('image.*')) {
|
||||
// this.generateURL(file);
|
||||
// }
|
||||
this._addFile(file);
|
||||
if (target && target.files) {
|
||||
for (const file of event.target.files) {
|
||||
// let fileName = String(event.target.files[0].name.replace(/\.[^/.]+$/, ''));
|
||||
// file.label = fileName;
|
||||
// if (file.type.match('image.*')) {
|
||||
// this.generateURL(file);
|
||||
// }
|
||||
// Immediately set spinner if any file is large (over 100 MB)
|
||||
// for (const file of target.files) {
|
||||
// if (file.size > 62914560) { // 100 MB
|
||||
// bigFileFound = true;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// if (bigFileFound) {
|
||||
// this.isLoading = true;
|
||||
// }
|
||||
this._addFile(file);
|
||||
|
||||
}
|
||||
}
|
||||
// if (bigFileFound) {
|
||||
// this.isLoading = true;
|
||||
// setTimeout(() => {
|
||||
// this.isLoading = false;
|
||||
// }, 1500);
|
||||
// }
|
||||
// this.overlay.classList.remove('draggedover');
|
||||
this.counter = 0;
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
get errors(): IDictionary {
|
||||
|
@ -445,7 +500,7 @@ class FileUploadComponent extends Vue {
|
|||
let localUrl: string = '';
|
||||
if (file instanceof File) {
|
||||
localUrl = URL.createObjectURL(file as Blob);
|
||||
}
|
||||
}
|
||||
// else if (file.fileData) {
|
||||
// // const blob = new Blob([file.fileData]);
|
||||
// // localUrl = URL.createObjectURL(blob);
|
||||
|
@ -465,17 +520,6 @@ class FileUploadComponent extends Vue {
|
|||
return localUrl;
|
||||
}
|
||||
|
||||
// private async downloadFile(id: number): Promise<string> {
|
||||
// const response = await axios.get<Blob>(`/api/download/${id}`, {
|
||||
// responseType: 'blob',
|
||||
// });
|
||||
// const url = URL.createObjectURL(response.data);
|
||||
// setTimeout(() => {
|
||||
// URL.revokeObjectURL(url);
|
||||
// }, 1000);
|
||||
// return url;
|
||||
// }
|
||||
|
||||
public getFileSize(file: File) {
|
||||
if (file.size > 1024) {
|
||||
if (file.size > 1048576) {
|
||||
|
@ -488,17 +532,6 @@ class FileUploadComponent extends Vue {
|
|||
}
|
||||
}
|
||||
|
||||
// private _addFile(file) {
|
||||
// // const isImage = file.type.match('image.*');
|
||||
// // const objectURL = URL.createObjectURL(file);
|
||||
|
||||
// // this.files[objectURL] = file;
|
||||
// // let test: TethysFile = { upload: file, label: "dfdsfs", sorting: 0 };
|
||||
// // file.sorting = this.files.length;
|
||||
// file.sort_order = (this.items.length + 1),
|
||||
// this.files.push(file);
|
||||
// }
|
||||
|
||||
private _addFile(file: File) {
|
||||
// const reader = new FileReader();
|
||||
// reader.onload = (event) => {
|
||||
|
@ -530,14 +563,11 @@ class FileUploadComponent extends Vue {
|
|||
// this.items.push(test);
|
||||
this.items[this.items.length] = test;
|
||||
} else {
|
||||
file.sort_order = this.items.length + 1;
|
||||
this.items.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
// use to check if a file is being dragged
|
||||
// private _hasFiles({ types = [] as Array<string> }) {
|
||||
// return types.indexOf('Files') > -1;
|
||||
// }
|
||||
private _hasFiles(dataTransfer: DataTransfer | null): boolean {
|
||||
return dataTransfer ? dataTransfer.items.length > 0 : false;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
type?: 'checkbox' | 'radio' | 'switch';
|
||||
label?: string | null;
|
||||
modelValue: Array<any> | string | number | boolean | null;
|
||||
inputValue: string | number | boolean;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
|
@ -9,7 +18,7 @@ const props = defineProps({
|
|||
type: {
|
||||
type: String,
|
||||
default: 'checkbox',
|
||||
validator: (value) => ['checkbox', 'radio', 'switch'].includes(value),
|
||||
validator: (value: string) => ['checkbox', 'radio', 'switch'].includes(value),
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
|
@ -23,19 +32,65 @@ const props = defineProps({
|
|||
type: [String, Number, Boolean],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
});// const props = defineProps<Props>();
|
||||
|
||||
// const emit = defineEmits(['update:modelValue']);
|
||||
const emit = defineEmits<{ (e: 'update:modelValue', value: Props['modelValue']): void }>();
|
||||
|
||||
const computedValue = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emit('update:modelValue', value);
|
||||
// If type is radio, wrap the new value inside an array.
|
||||
if (props.type === 'radio') {
|
||||
emit('update:modelValue', [value]);
|
||||
} else {
|
||||
emit('update:modelValue', value);
|
||||
}
|
||||
},
|
||||
});
|
||||
const inputType = computed(() => (props.type === 'radio' ? 'radio' : 'checkbox'));
|
||||
|
||||
// Define isChecked for radio inputs: it's true when the current modelValue equals the inputValue
|
||||
const isChecked = computed(() => {
|
||||
if (props.type === 'radio') {
|
||||
return Array.isArray(computedValue.value) &&
|
||||
computedValue.value.length > 0 &&
|
||||
computedValue.value[0] === props.inputValue;
|
||||
}
|
||||
return computedValue.value === props.inputValue;
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<!-- <template>
|
||||
<label v-if="type == 'radio'" :class="[type, 'mr-6 mb-3 last:mr-0 inline-flex items-center cursor-pointer relative']">
|
||||
<input v-model="computedValue" :type="inputType" :name="name" :value="inputValue" class="absolute left-0 opacity-0 -z-1 focus:outline-none focus:ring-0" />
|
||||
<span
|
||||
class="check border-gray-700 border transition-colors duration-200 dark:bg-slate-800 block w-5 h-5 rounded-full"
|
||||
:class="{ 'bg-no-repeat bg-center bg-blue-600 border-blue-600 border-4': isChecked }"/>
|
||||
<span class="pl-2">{{ label }}</span>
|
||||
</label>
|
||||
<label v-else :class="[type, 'mr-6 mb-3 last:mr-0']">
|
||||
<input v-model="computedValue" :type="inputType" :name="name" :value="inputValue" />
|
||||
<span class="check" />
|
||||
<span class="pl-2">{{ label }}</span>
|
||||
</label>
|
||||
</template> -->
|
||||
|
||||
<template>
|
||||
<label :class="type" class="mr-6 mb-3 last:mr-0">
|
||||
<label v-if="type === 'radio'" :class="[type]"
|
||||
class="mr-6 mb-3 last:mr-0 inline-flex items-center cursor-pointer relative">
|
||||
<input v-model="computedValue" :type="inputType" :name="name" :value="inputValue"
|
||||
class="absolute left-0 opacity-0 -z-1 focus:outline-none focus:ring-0"
|
||||
:checked="isChecked" />
|
||||
<span class="check border transition-colors duration-200 dark:bg-slate-800 block w-5 h-5 rounded-full" :class="{
|
||||
'border-gray-700': !isChecked,
|
||||
'bg-radio-checked bg-no-repeat bg-center bg-blue-600 border-blue-600 border-4': isChecked
|
||||
}" />
|
||||
<span class="pl-2">{{ label }}</span>
|
||||
</label>
|
||||
|
||||
<label v-else class="mr-6 mb-3 last:mr-0" :class="[type]">
|
||||
<input v-model="computedValue" :type="inputType" :name="name" :value="inputValue" />
|
||||
<span class="check" />
|
||||
<span class="pl-2">{{ label }}</span>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, ref, PropType } from 'vue';
|
||||
import FormCheckRadio from '@/Components/FormCheckRadio.vue';
|
||||
import BaseButton from '@/Components/BaseButton.vue';
|
||||
import FormControl from '@/Components/FormControl.vue';
|
||||
import { mdiPlusCircle } from '@mdi/js';
|
||||
// import BaseButton from '@/Components/BaseButton.vue';
|
||||
// import FormControl from '@/Components/FormControl.vue';
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Object,
|
||||
|
@ -23,7 +23,7 @@ const props = defineProps({
|
|||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
type: String as PropType<'checkbox' | 'radio' | 'switch'>,
|
||||
default: 'checkbox',
|
||||
validator: (value: string) => ['checkbox', 'radio', 'switch'].includes(value),
|
||||
},
|
||||
|
@ -78,11 +78,11 @@ const addOption = () => {
|
|||
|
||||
const inputElClass = computed(() => {
|
||||
const base = [
|
||||
'px-3 py-2 max-w-full focus:ring focus:outline-none border-gray-700 rounded w-full',
|
||||
'px-3 py-2 max-w-full border-gray-700 rounded w-full',
|
||||
'dark:placeholder-gray-400',
|
||||
'h-12',
|
||||
'border',
|
||||
'bg-transparent'
|
||||
'bg-transparent'
|
||||
// props.isReadOnly ? 'bg-gray-50 dark:bg-slate-600' : 'bg-white dark:bg-slate-800',
|
||||
];
|
||||
// if (props.icon) {
|
||||
|
@ -108,7 +108,7 @@ const inputElClass = computed(() => {
|
|||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-6H5v-2h6V5h2v6h6v2h-6v6z" />
|
||||
</svg>
|
||||
</div>
|
||||
<FormCheckRadio v-for="(value, key) in options" :key="key" v-model="computedValue" :type="type" :name="name"
|
||||
:input-value="key" :label="value" :class="componentClass" />
|
||||
<FormCheckRadio v-for="(value, key) in options" :key="key" v-model="computedValue" :type="type"
|
||||
:name="name" :input-value="key" :label="value" :class="componentClass" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -320,12 +320,17 @@ const nextStep = async () => {
|
|||
} else if (formStep.value == 3) {
|
||||
route = stardust.route('dataset.third.step');
|
||||
}
|
||||
// formStep.value++;
|
||||
// When posting in steps 1-3, remove any file uploads from the data.
|
||||
await form
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
rights: form.rights && form.rights == true ? 'true' : 'false',
|
||||
}))
|
||||
.transform((data: Dataset) => {
|
||||
// Create payload and set rights (transforming to a string if needed)
|
||||
const payload: any = { ...data, rights: data.rights ? 'true' : 'false' };
|
||||
// Remove the files property so that the partial update is done without files
|
||||
if (payload.files) {
|
||||
delete payload.files;
|
||||
}
|
||||
return payload;
|
||||
})
|
||||
.post(route, {
|
||||
onSuccess: () => {
|
||||
// console.log(form.data());
|
||||
|
@ -334,7 +339,6 @@ const nextStep = async () => {
|
|||
},
|
||||
});
|
||||
};
|
||||
|
||||
const prevStep = () => {
|
||||
formStep.value--;
|
||||
};
|
||||
|
@ -343,7 +347,7 @@ const submit = async () => {
|
|||
let route = stardust.route('dataset.submit');
|
||||
|
||||
const files = form.files.map((obj) => {
|
||||
return new File([obj.blob], obj.label, { type: obj.type, lastModified: obj.lastModified });
|
||||
return new File([obj.blob], obj.label, { type: obj.type, lastModified: obj.lastModified, sort_order: obj.sort_order });
|
||||
});
|
||||
|
||||
// formStep.value++;
|
||||
|
@ -576,7 +580,7 @@ Removes a selected keyword
|
|||
|
||||
<FormField label="Licenses" wrap-body :class="{ 'text-red-400': form.errors.licenses }"
|
||||
class="mt-8 w-full mx-2 flex-1">
|
||||
<FormCheckRadioGroup v-model="form.licenses" name="roles" is-column
|
||||
<FormCheckRadioGroup type="radio" v-model="form.licenses" name="licenses" is-column
|
||||
:options="props.licenses" />
|
||||
</FormField>
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
<!-- (2) licenses -->
|
||||
<FormField label="Licenses" wrap-body :class="{ 'text-red-400': form.errors.licenses }"
|
||||
class="mt-8 w-full mx-2 flex-1">
|
||||
<FormCheckRadioGroup v-model="form.licenses" name="licenses" is-column :options="licenses" />
|
||||
<FormCheckRadioGroup type="radio" v-model="form.licenses" name="licenses" is-column :options="licenses" />
|
||||
</FormField>
|
||||
|
||||
<div class="flex flex-col md:flex-row">
|
||||
|
|
Loading…
Add table
editor.link_modal.header
Reference in a new issue