hotfix (dataset): enhance dataset editing and validation

- Modified the TableKeywords component to remove the external_key reset when the type is updated, only resetting the value.
- Updated the DatasetController to pass authorization checks (`can.edit`, `can.delete`) to the edit view.
- Updated the arrayContainsTypes validation rule to improve the error messages for titles and descriptions, clarifying the requirements for main and translated entries.
- Updated the Dataset Edit view to:
  - Remove unused code and comments.
  - Add authorization checks to the save button.
  - Add a release button.
  - Add icons to the save and release buttons.
  - Add a computed property `hasUnsavedChanges` to determine if there are unsaved changes in the form.
This commit is contained in:
Kaimbacher 2025-04-18 11:39:19 +02:00
parent 2cb33a779c
commit c3ae4327b7
4 changed files with 83 additions and 55 deletions

View file

@ -926,7 +926,7 @@ export default class DatasetController {
// throw new GeneralException(trans('exceptions.publish.release.update_error')); // throw new GeneralException(trans('exceptions.publish.release.update_error'));
} }
public async edit({ request, inertia, response }: HttpContext) { public async edit({ request, inertia, response, auth }: HttpContext) {
const id = request.param('id'); const id = request.param('id');
const datasetQuery = Dataset.query().where('id', id); const datasetQuery = Dataset.query().where('id', id);
datasetQuery datasetQuery
@ -1015,6 +1015,10 @@ export default class DatasetController {
referenceIdentifierTypes: Object.entries(ReferenceIdentifierTypes).map(([key, value]) => ({ value: key, label: value })), referenceIdentifierTypes: Object.entries(ReferenceIdentifierTypes).map(([key, value]) => ({ value: key, label: value })),
relationTypes: Object.entries(RelationTypes).map(([key, value]) => ({ value: key, label: value })), relationTypes: Object.entries(RelationTypes).map(([key, value]) => ({ value: key, label: value })),
doctypes: DatasetTypes, doctypes: DatasetTypes,
can: {
edit: await auth.user?.can(['dataset-edit']),
delete: await auth.user?.can(['dataset-delete']),
},
}); });
} }

View file

@ -154,7 +154,7 @@ const isKeywordReadOnly = (item: Subject) => {
<td data-label="Type" scope="row"> <td data-label="Type" scope="row">
<FormControl required v-model="item.type" <FormControl required v-model="item.type"
@update:modelValue="() => { item.external_key = undefined; item.value = ''; }" :type="'select'" @update:modelValue="() => { item.value = ''; }" :type="'select'"
placeholder="[Enter Language]" :options="props.subjectTypes"> placeholder="[Enter Language]" :options="props.subjectTypes">
<div class="text-red-400 text-sm" v-if="errors[`subjects.${index}.type`]"> <div class="text-red-400 text-sm" v-if="errors[`subjects.${index}.type`]">
{{ errors[`subjects.${index}.type`].join(', ') }} {{ errors[`subjects.${index}.type`].join(', ') }}

View file

@ -468,15 +468,6 @@
</div> </div>
<!-- <div class="mb-4">
<label for="description" class="block text-gray-700 font-bold mb-2">Description:</label>
<textarea id="description"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
v-model="form.type"></textarea>
</div> -->
<div class="mb-4"> <div class="mb-4">
<!-- <label for="project" class="block text-gray-700 font-bold mb-2">Project:</label> <!-- <label for="project" class="block text-gray-700 font-bold mb-2">Project:</label>
<select <select
@ -498,25 +489,15 @@
{{ form.errors['files'].join(', ') }} {{ form.errors['files'].join(', ') }}
</div> </div>
<!-- Add more input fields for the other properties of the dataset -->
<!-- <button
type="submit"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
>
Save
</button> -->
<template #footer> <template #footer>
<BaseButtons> <BaseButtons>
<BaseButton @click.stop="submit" :disabled="form.processing" label="Save" color="info" <BaseButton v-if="can.edit" @click.stop="submit" :disabled="form.processing" label="Save"
:class="{ 'opacity-25': form.processing }" small> color="info" :icon="mdiDisc" :class="{ 'opacity-25': form.processing }" small>
</BaseButton> </BaseButton>
<!-- <button :disabled="form.processing" :class="{ 'opacity-25': form.processing }" <BaseButton v-if="can.edit" :route-name="stardust.route('dataset.release', [dataset.id])"
class="text-base hover:scale-110 focus:outline-none flex justify-center px-4 py-2 rounded font-bold cursor-pointer hover:bg-teal-200 bg-teal-100 text-teal-700 border duration-200 ease-in-out border-teal-600 transition" color="info" :icon="mdiLockOpen" :label="'Release'" small
@click.stop="submit"> :disabled="form.processing"
Save :class="{ 'opacity-25': form.processing }" />
</button> -->
</BaseButtons> </BaseButtons>
</template> </template>
</CardBox> </CardBox>
@ -570,7 +551,9 @@ import {
mdiBookOpenPageVariant, mdiBookOpenPageVariant,
mdiEarthPlus, mdiEarthPlus,
mdiAlertBoxOutline, mdiAlertBoxOutline,
mdiRestore mdiRestore,
mdiLockOpen,
mdiDisc
} from '@mdi/js'; } from '@mdi/js';
import { notify } from '@/notiwind'; import { notify } from '@/notiwind';
import NotificationBar from '@/Components/NotificationBar.vue'; import NotificationBar from '@/Components/NotificationBar.vue';
@ -624,8 +607,10 @@ const props = defineProps({
type: Object, type: Object,
default: () => ({}), default: () => ({}),
}, },
can: {
type: Object,
default: () => ({}),
},
}); });
const flash: ComputedRef<any> = computed(() => { const flash: ComputedRef<any> = computed(() => {
@ -650,31 +635,70 @@ const fitBounds: LatLngBoundsExpression = [
]; ];
const mapId = 'test'; const mapId = 'test';
// const downloadFile = async (id: string): 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;
// };
// for (const file of props.dataset.files) {
// // console.log(`${file.name} path is ${file.filePath} here.`);
// file.fileSrc = ref("");
// // downloadFile(file.id).then((value: string) => {
// // file.fileSrc = ref(value);
// // form = useForm<Dataset>(props.dataset as Dataset);
// // });
// }
props.dataset.filesToDelete = []; props.dataset.filesToDelete = [];
props.dataset.subjectsToDelete = []; props.dataset.subjectsToDelete = [];
props.dataset.referencesToDelete = []; props.dataset.referencesToDelete = [];
let form = useForm<Dataset>(props.dataset as Dataset); let form = useForm<Dataset>(props.dataset as Dataset);
// Add this computed property to the script section
const hasUnsavedChanges = computed(() => {
// Check if form is processing
if (form.processing) return true;
// Compare current form state with original dataset
// Check basic properties
if (form.language !== props.dataset.language) return true;
if (form.type !== props.dataset.type) return true;
if (form.project_id !== props.dataset.project_id) return true;
if (form.embargo_date !== props.dataset.embargo_date) return true;
// Check if licenses have changed
const originalLicenses = Array.isArray(props.dataset.licenses)
? props.dataset.licenses.map(l => typeof l === 'object' ? l.id.toString() : l)
: [];
const currentLicenses = Array.isArray(form.licenses)
? form.licenses.map(l => typeof l === 'object' ? l.id.toString() : l)
: [];
if (JSON.stringify(currentLicenses) !== JSON.stringify(originalLicenses)) return true;
// Check if titles have changed
if (JSON.stringify(form.titles) !== JSON.stringify(props.dataset.titles)) return true;
// Check if descriptions have changed
if (JSON.stringify(form.descriptions) !== JSON.stringify(props.dataset.descriptions)) return true;
// Check if authors have changed
if (JSON.stringify(form.authors) !== JSON.stringify(props.dataset.authors)) return true;
// Check if contributors have changed
if (JSON.stringify(form.contributors) !== JSON.stringify(props.dataset.contributors)) return true;
// Check if subjects have changed
// if (JSON.stringify(form.subjects) !== JSON.stringify(props.dataset.subjects)) return true;
let test = JSON.stringify(form.subjects);
let test2 = JSON.stringify(props.dataset.subjects);
if (test !== test2) {
return true;
}
// Check if references have changed
if (JSON.stringify(form.references) !== JSON.stringify(props.dataset.references)) return true;
// Check if coverage has changed
if (JSON.stringify(form.coverage) !== JSON.stringify(props.dataset.coverage)) return true;
// Check if files have changed
if (form.files?.length !== props.dataset.files?.length) return true;
if (form.filesToDelete?.length > 0) return true;
// Check if there are new files to upload
if (form.files?.some(file => !file.id)) return true;
// No changes detected
return false;
});
const submit = async (): Promise<void> => { const submit = async (): Promise<void> => {
let route = stardust.route('dataset.update', [props.dataset.id]); let route = stardust.route('dataset.update', [props.dataset.id]);

View file

@ -44,20 +44,20 @@ async function arrayContainsTypes(value: unknown, options: Options, field: Field
if (field.getFieldPath() === 'titles') { if (field.getFieldPath() === 'titles') {
// For titles we expect one main and minimum one translated title. // For titles we expect one main and minimum one translated title.
if (!hasTypeA && !hasTypeB) { if (!hasTypeA && !hasTypeB) {
errorMessage = 'For titles, define one main title and minimum one translated title.'; errorMessage = 'For titles, define at least one main title and at least one Translated title as MAIN TITLE.';
} else if (!hasTypeA) { } else if (!hasTypeA) {
errorMessage = 'For titles, define one main title.'; errorMessage = 'For titles, define at least one main title.';
} else if (!hasTypeB) { } else if (!hasTypeB) {
errorMessage = 'For titles, define minimum one translated title.'; errorMessage = 'For Titles, define at least one Translated title as MAIN TITLE.';
} }
} else if (field.getFieldPath() === 'descriptions') { } else if (field.getFieldPath() === 'descriptions') {
// For descriptions we expect one abstracts description and minimum one translated description. // For descriptions we expect one abstracts description and minimum one translated description.
if (!hasTypeA && !hasTypeB) { if (!hasTypeA && !hasTypeB) {
errorMessage = 'For descriptions, define one abstract description and minimum one translated description.'; errorMessage = 'For descriptions, define at least one abstract and at least one Translated description as MAIN ABSTRACT.';
} else if (!hasTypeA) { } else if (!hasTypeA) {
errorMessage = 'For descriptions, define one abstract description.'; errorMessage = 'For descriptions, define at least one abstract.';
} else if (!hasTypeB) { } else if (!hasTypeB) {
errorMessage = 'For descriptions, define minimum one translated description.'; errorMessage = 'For Descriptions, define at least one Translated description as MAIN ABSTRACT.';
} }
} }