hotfix(dataset): enhance dataset creation and editing forms

- Added functionality to add new authors and contributors directly within the dataset creation and editing forms.
- Implemented `addNewAuthor` and `addNewContributor` methods to dynamically add new person objects to the authors and contributors arrays in the form data.
- Added header icons with click events to the `CardBox` component for authors and contributors sections to trigger the addition of new entries.
- Updated the dataset index views for reviewers and editors to improve the display of dataset titles, including adding a CSS class to truncate long titles.
- Ensured authors and contributors are ordered by `pivot_sort_order` when preloading in the Dataset and Editor controllers.
- Fixed an issue where pressing enter in the `SearchAutocomplete` component would submit the form.
- Updated validation messages to be available in the `updateEditorDatasetValidator`.
This commit is contained in:
Kaimbacher 2025-04-09 13:00:37 +02:00
parent f04c1f6327
commit 106f8d5f27
10 changed files with 379 additions and 382 deletions

View file

@ -504,8 +504,8 @@ export default class DatasetsController {
.preload('descriptions', (query) => query.orderBy('id', 'asc'))
.preload('coverage')
.preload('licenses')
.preload('authors')
.preload('contributors')
.preload('authors', (query) => query.orderBy('pivot_sort_order', 'asc'))
.preload('contributors', (query) => query.orderBy('pivot_sort_order', 'asc'))
// .preload('subjects')
.preload('subjects', (builder) => {
builder.orderBy('id', 'asc').withCount('datasets');

View file

@ -924,8 +924,8 @@ export default class DatasetController {
.preload('descriptions', (query) => query.orderBy('id', 'asc'))
.preload('coverage')
.preload('licenses')
.preload('authors')
.preload('contributors')
.preload('authors', (query) => query.orderBy('pivot_sort_order', 'asc'))
.preload('contributors', (query) => query.orderBy('pivot_sort_order', 'asc'))
// .preload('subjects')
.preload('subjects', (builder) => {
builder.orderBy('id', 'asc').withCount('datasets');

View file

@ -500,4 +500,5 @@ let messagesProvider = new SimpleMessagesProvider({
createDatasetValidator.messagesProvider = messagesProvider;
updateDatasetValidator.messagesProvider = messagesProvider;
updateEditorDatasetValidator.messagesProvider = messagesProvider;
// export default createDatasetValidator;

614
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -28,7 +28,7 @@
autocomplete="off"
@keydown.down="onArrowDown"
@keydown.up="onArrowUp"
@keydown.enter="onEnter"
@keydown.enter.prevent="onEnter"
/>
<svg
class="w-4 h-4 absolute left-2.5 top-3.5"

View file

@ -232,7 +232,8 @@
</CardBox>
<!-- (7) authors -->
<CardBox class="mb-6 shadow" has-table title="Creators" :icon="mdiBookOpenPageVariant">
<CardBox class="mb-6 shadow" has-table title="Creators" :icon="mdiBookOpenPageVariant"
:header-icon="mdiPlusCircle" v-on:header-icon-click="addNewAuthor()">
<SearchAutocomplete source="/api/persons" :response-property="'first_name'"
placeholder="search in person table...." v-on:person="onAddAuthor"></SearchAutocomplete>
@ -245,7 +246,8 @@
<!-- (8) contributors -->
<CardBox class="mb-6 shadow" has-table title="Contributors" :icon="mdiBookOpenPageVariant">
<CardBox class="mb-6 shadow" has-table title="Contributors" :icon="mdiBookOpenPageVariant"
:header-icon="mdiPlusCircle" v-on:header-icon-click="addNewContributor()">
<SearchAutocomplete source="/api/persons" :response-property="'first_name'"
placeholder="search in person table...." v-on:person="onAddContributor">
</SearchAutocomplete>
@ -734,6 +736,11 @@ const removeDescription = (key: any) => {
form.descriptions.splice(key, 1);
};
const addNewAuthor = () => {
let newAuthor = { status: false, first_name: '', last_name: '', email: '', academic_title: '', identifier_orcid: '', name_type: 'Personal' };
form.authors.push(newAuthor);
};
const onAddAuthor = (person: Person) => {
if (form.authors.filter((e) => e.id === person.id).length > 0) {
notify({ type: 'warning', title: 'Warning', text: 'person is already defined as author' }, 4000);
@ -745,6 +752,11 @@ const onAddAuthor = (person: Person) => {
}
};
const addNewContributor = () => {
let newContributor = { status: false, first_name: '', last_name: '', email: '', academic_title: '', identifier_orcid: '', name_type: 'Personal', pivot: { contributor_type: '' } };
form.contributors.push(newContributor);
};
const onAddContributor = (person: Person) => {
if (form.contributors.filter((e) => e.id === person.id).length > 0) {
notify({ type: 'warning', title: 'Warning', text: 'person is already defined as contributor' }, 4000);

View file

@ -196,7 +196,7 @@ const formatServerState = (state: string) => {
<BaseButton
v-if="can.approve && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
:route-name="stardust.route('editor.dataset.reject', [dataset.id])"
color="info" :icon="mdiUndo" label="'Reject'" small class="col-span-1">
color="info" :icon="mdiUndo" label="Reject" small class="col-span-1">
</BaseButton>
<BaseButton

View file

@ -122,12 +122,9 @@ const formatServerState = (state: string) => {
<tbody>
<tr v-for="dataset in props.datasets.data" :key="dataset.id" :class="[getRowClass(dataset)]">
<td data-label="Login" class="py-4 whitespace-nowrap text-gray-700 dark:text-white">
<!-- <Link v-bind:href="stardust.route('user.show', [user.id])"
class="no-underline hover:underline text-cyan-600 dark:text-cyan-400">
{{ user.login }}
</Link> -->
<div class="text-sm font-medium">{{ dataset.main_title }}</div>
<td data-label="Login"
class="py-4 whitespace-nowrap text-gray-700 dark:text-white">
<div class="text-sm table-title">{{ dataset.main_title }}</div>
</td>
<td class="py-4 whitespace-nowrap text-gray-700 dark:text-white">
<div class="text-sm">{{ dataset.id }}</div>
@ -185,3 +182,16 @@ const formatServerState = (state: string) => {
</LayoutAuthenticated>
</template>
<style scoped lang="css">
.table-title {
max-width: 200px;
/* set a maximum width */
overflow: hidden;
/* hide overflow */
text-overflow: ellipsis;
/* show ellipsis for overflowed text */
white-space: nowrap;
/* prevent wrapping */
}
</style>

View file

@ -444,6 +444,12 @@ const onAddAuthor = (person: Person) => {
notify({ type: 'info', text: 'person has been successfully added as author' });
}
};
const addNewContributor = () => {
let newContributor = { status: false, first_name: '', last_name: '', email: '', academic_title: '', identifier_orcid: '', name_type: 'Personal', pivot: { contributor_type: '' } };
form.contributors.push(newContributor);
};
const onAddContributor = (person: Person) => {
if (form.contributors.filter((e) => e.id === person.id).length > 0) {
notify({ type: 'warning', title: 'Warning', text: 'person is already defined as contributor' }, 4000);
@ -588,8 +594,10 @@ Removes a selected keyword
<input class="form-checkbox" name="rights" id="rights" type="checkbox" v-model="dataset.rights" />
terms and conditions
</label> -->
<FormField label="Rights" help="You must agree that you have read the Terms and Conditions. Please click on the 'i' icon to find a read the policy" wrap-body
:class="{ 'text-red-400': form.errors.rights }" class="mt-8 w-full mx-2 flex-1 flex-col">
<FormField label="Rights"
help="You must agree that you have read the Terms and Conditions. Please click on the 'i' icon to find a read the policy"
wrap-body :class="{ 'text-red-400': form.errors.rights }"
class="mt-8 w-full mx-2 flex-1 flex-col">
<label for="rights" class="checkbox mr-6 mb-3 last:mr-0">
<input type="checkbox" id="rights" required v-model="form.rights" />
<span class="check" />
@ -797,8 +805,8 @@ Removes a selected keyword
<TablePersons :errors="form.errors" :persons="form.authors" :relation="'authors'"
v-if="form.authors.length > 0" />
<div class="text-red-400 text-sm" v-if="errors.authors && Array.isArray(errors.authors)">
{{ errors.authors.join(', ') }}
<div class="text-red-400 text-sm" v-if="form.errors.authors && Array.isArray(form.errors.authors)">
{{ form.errors.authors.join(', ') }}
</div>
<div class="w-full md:w-1/2">
<label class="block" for="additionalCreators">Add additional creator(s) if creator is
@ -821,6 +829,12 @@ Removes a selected keyword
v-if="form.errors.contributors && Array.isArray(form.errors.contributors)">
{{ form.errors.contributors.join(', ') }}
</div>
<div class="w-full md:w-1/2">
<label class="block" for="additionalCreators">Add additional contributor(s) if
contributor is not in database</label>
<button class="bg-blue-500 text-white py-2 px-4 rounded-sm"
@click.prevent="addNewContributor()">+</button>
</div>
</CardBox>
</div>

View file

@ -241,11 +241,13 @@
</CardBox>
<!-- (7) authors -->
<CardBox class="mb-6 shadow" has-table title="Creators" :icon="mdiBookOpenPageVariant">
<CardBox class="mb-6 shadow" has-table title="Creators" :icon="mdiBookOpenPageVariant"
:header-icon="mdiPlusCircle" v-on:header-icon-click="addNewAuthor()">
<SearchAutocomplete source="/api/persons" :response-property="'first_name'"
placeholder="search in person table...." v-on:person="onAddAuthor"></SearchAutocomplete>
<TablePersons :persons="form.authors" v-if="form.authors.length > 0" :relation="'authors'" />
<TablePersons :persons="form.authors" v-if="form.authors.length > 0" :errors="form.errors"
:relation="'authors'" />
<div class="text-red-400 text-sm"
v-if="form.errors.authors && Array.isArray(form.errors.authors)">
{{ form.errors.authors.join(', ') }}
@ -254,7 +256,8 @@
<!-- (8) contributors -->
<CardBox class="mb-6 shadow" has-table title="Contributors" :icon="mdiBookOpenPageVariant">
<CardBox class="mb-6 shadow" has-table title="Contributors" :icon="mdiBookOpenPageVariant"
:header-icon="mdiPlusCircle" v-on:header-icon-click="addNewContributor()">
<SearchAutocomplete source="/api/persons" :response-property="'first_name'"
placeholder="search in person table...." v-on:person="onAddContributor">
</SearchAutocomplete>
@ -422,7 +425,8 @@
class="bg-red-100 group w-full h-full rounded-md cursor-pointer relative shadow-sm overflow-hidden">
<section
class="flex flex-col rounded-md text-xs break-words w-full h-full z-20 absolute top-0 py-2 px-3">
<h1 class="flex-1 text-gray-700 group-hover:text-blue-800 font-medium text-sm mb-1 truncate overflow-hidden whitespace-nowrap">
<h1
class="flex-1 text-gray-700 group-hover:text-blue-800 font-medium text-sm mb-1 truncate overflow-hidden whitespace-nowrap">
{{ element.value }}
</h1>
<div class="flex flex-col mt-auto">
@ -431,7 +435,7 @@
</p>
<p class="p-1 size text-xs text-gray-700">
<span class="font-semibold">Relation:</span> {{ element.relation }}
</p>
</p>
<div class="flex justify-end mt-1">
<button
class="restore ml-auto focus:outline-none hover:bg-gray-300 p-1 rounded-md text-gray-800"
@ -533,8 +537,6 @@
// import EditComponent from "./../EditComponent";
// export default EditComponent;
// import { Component, Vue, Prop, Setup, toNative } from 'vue-facing-decorator';
// import AuthLayout from '@/Layouts/Auth.vue';
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
import { useForm, Head, usePage } from '@inertiajs/vue3';
import { computed, ComputedRef } from 'vue';
@ -634,12 +636,6 @@ const flash: ComputedRef<any> = computed(() => {
const errors: ComputedRef<any> = computed(() => {
return usePage().props.errors;
});
// const errors: ComputedRef<any> = computed(() => {
// return usePage().props.errors;
// });
// const projects = reactive([]);
// const licenses = reactive([]);
const mapOptions: MapOptions = {
center: [48.208174, 16.373819],
@ -679,44 +675,8 @@ props.dataset.subjectsToDelete = [];
props.dataset.referencesToDelete = [];
let form = useForm<Dataset>(props.dataset as Dataset);
// const mainService = MainService();
// mainService.fetchfiles(props.dataset);
// const files = computed(() => props.dataset.file);
// let form = useForm<Dataset>(props.dataset as Dataset);
// const form = useForm({
// _method: 'put',
// login: props.user.login,
// email: props.user.email,
// password: '',
// password_confirmation: '',
// roles: props.userHasRoles, // fill actual user roles from db
// });
// async created() {
// // Fetch the list of projects and licenses from the server
// const response = await fetch('/api/datasets/edit/' + this.dataset.id);
// const data = await response.json();
// this.projects = data.projects;
// this.licenses = data.licenses;
// }
const submit = async (): Promise<void> => {
let route = stardust.route('dataset.update', [props.dataset.id]);
// await Inertia.post('/app/register', this.form);
// await router.post('/app/register', this.form);
let licenses = form.licenses.map((obj) => {
if (hasIdAttribute(obj)) {
@ -726,12 +686,6 @@ const submit = async (): Promise<void> => {
}
});
// const files = form.files.map((obj) => {
// return new File([obj.blob], obj.label, { type: obj.type, lastModified: obj.lastModified });
// });
const [fileUploads, fileInputs] = form.files?.reduce(
([fileUploads, fileInputs], obj) => {
if (!obj.id) {
@ -815,6 +769,11 @@ const removeDescription = (key: any) => {
form.descriptions.splice(key, 1);
};
const addNewAuthor = () => {
let newAuthor = { status: false, first_name: '', last_name: '', email: '', academic_title: '', identifier_orcid: '', name_type: 'Personal' };
form.authors.push(newAuthor);
};
const onAddAuthor = (person: Person) => {
if (form.authors.filter((e) => e.id === person.id).length > 0) {
notify({ type: 'warning', title: 'Warning', text: 'person is already defined as author' }, 4000);
@ -826,6 +785,11 @@ const onAddAuthor = (person: Person) => {
}
};
const addNewContributor = () => {
let newContributor = { status: false, first_name: '', last_name: '', email: '', academic_title: '', identifier_orcid: '', name_type: 'Personal', pivot: { contributor_type: '' } };
form.contributors.push(newContributor);
};
const onAddContributor = (person: Person) => {
if (form.contributors.filter((e) => e.id === person.id).length > 0) {
notify({ type: 'warning', title: 'Warning', text: 'person is already defined as contributor' }, 4000);