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('descriptions', (query) => query.orderBy('id', 'asc'))
.preload('coverage') .preload('coverage')
.preload('licenses') .preload('licenses')
.preload('authors') .preload('authors', (query) => query.orderBy('pivot_sort_order', 'asc'))
.preload('contributors') .preload('contributors', (query) => query.orderBy('pivot_sort_order', 'asc'))
// .preload('subjects') // .preload('subjects')
.preload('subjects', (builder) => { .preload('subjects', (builder) => {
builder.orderBy('id', 'asc').withCount('datasets'); builder.orderBy('id', 'asc').withCount('datasets');

View file

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

View file

@ -500,4 +500,5 @@ let messagesProvider = new SimpleMessagesProvider({
createDatasetValidator.messagesProvider = messagesProvider; createDatasetValidator.messagesProvider = messagesProvider;
updateDatasetValidator.messagesProvider = messagesProvider; updateDatasetValidator.messagesProvider = messagesProvider;
updateEditorDatasetValidator.messagesProvider = messagesProvider;
// export default createDatasetValidator; // 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" autocomplete="off"
@keydown.down="onArrowDown" @keydown.down="onArrowDown"
@keydown.up="onArrowUp" @keydown.up="onArrowUp"
@keydown.enter="onEnter" @keydown.enter.prevent="onEnter"
/> />
<svg <svg
class="w-4 h-4 absolute left-2.5 top-3.5" class="w-4 h-4 absolute left-2.5 top-3.5"

View file

@ -232,7 +232,8 @@
</CardBox> </CardBox>
<!-- (7) authors --> <!-- (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'" <SearchAutocomplete source="/api/persons" :response-property="'first_name'"
placeholder="search in person table...." v-on:person="onAddAuthor"></SearchAutocomplete> placeholder="search in person table...." v-on:person="onAddAuthor"></SearchAutocomplete>
@ -245,7 +246,8 @@
<!-- (8) contributors --> <!-- (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'" <SearchAutocomplete source="/api/persons" :response-property="'first_name'"
placeholder="search in person table...." v-on:person="onAddContributor"> placeholder="search in person table...." v-on:person="onAddContributor">
</SearchAutocomplete> </SearchAutocomplete>
@ -734,6 +736,11 @@ const removeDescription = (key: any) => {
form.descriptions.splice(key, 1); 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) => { const onAddAuthor = (person: Person) => {
if (form.authors.filter((e) => e.id === person.id).length > 0) { if (form.authors.filter((e) => e.id === person.id).length > 0) {
notify({ type: 'warning', title: 'Warning', text: 'person is already defined as author' }, 4000); 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) => { const onAddContributor = (person: Person) => {
if (form.contributors.filter((e) => e.id === person.id).length > 0) { if (form.contributors.filter((e) => e.id === person.id).length > 0) {
notify({ type: 'warning', title: 'Warning', text: 'person is already defined as contributor' }, 4000); notify({ type: 'warning', title: 'Warning', text: 'person is already defined as contributor' }, 4000);

View file

@ -196,7 +196,7 @@ const formatServerState = (state: string) => {
<BaseButton <BaseButton
v-if="can.approve && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')" v-if="can.approve && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
:route-name="stardust.route('editor.dataset.reject', [dataset.id])" :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>
<BaseButton <BaseButton

View file

@ -122,12 +122,9 @@ const formatServerState = (state: string) => {
<tbody> <tbody>
<tr v-for="dataset in props.datasets.data" :key="dataset.id" :class="[getRowClass(dataset)]"> <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"> <td data-label="Login"
<!-- <Link v-bind:href="stardust.route('user.show', [user.id])" class="py-4 whitespace-nowrap text-gray-700 dark:text-white">
class="no-underline hover:underline text-cyan-600 dark:text-cyan-400"> <div class="text-sm table-title">{{ dataset.main_title }}</div>
{{ user.login }}
</Link> -->
<div class="text-sm font-medium">{{ dataset.main_title }}</div>
</td> </td>
<td class="py-4 whitespace-nowrap text-gray-700 dark:text-white"> <td class="py-4 whitespace-nowrap text-gray-700 dark:text-white">
<div class="text-sm">{{ dataset.id }}</div> <div class="text-sm">{{ dataset.id }}</div>
@ -185,3 +182,16 @@ const formatServerState = (state: string) => {
</LayoutAuthenticated> </LayoutAuthenticated>
</template> </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' }); 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) => { const onAddContributor = (person: Person) => {
if (form.contributors.filter((e) => e.id === person.id).length > 0) { if (form.contributors.filter((e) => e.id === person.id).length > 0) {
notify({ type: 'warning', title: 'Warning', text: 'person is already defined as contributor' }, 4000); 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" /> <input class="form-checkbox" name="rights" id="rights" type="checkbox" v-model="dataset.rights" />
terms and conditions terms and conditions
</label> --> </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 <FormField label="Rights"
:class="{ 'text-red-400': form.errors.rights }" class="mt-8 w-full mx-2 flex-1 flex-col"> 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"> <label for="rights" class="checkbox mr-6 mb-3 last:mr-0">
<input type="checkbox" id="rights" required v-model="form.rights" /> <input type="checkbox" id="rights" required v-model="form.rights" />
<span class="check" /> <span class="check" />
@ -797,8 +805,8 @@ Removes a selected keyword
<TablePersons :errors="form.errors" :persons="form.authors" :relation="'authors'" <TablePersons :errors="form.errors" :persons="form.authors" :relation="'authors'"
v-if="form.authors.length > 0" /> v-if="form.authors.length > 0" />
<div class="text-red-400 text-sm" v-if="errors.authors && Array.isArray(errors.authors)"> <div class="text-red-400 text-sm" v-if="form.errors.authors && Array.isArray(form.errors.authors)">
{{ errors.authors.join(', ') }} {{ form.errors.authors.join(', ') }}
</div> </div>
<div class="w-full md:w-1/2"> <div class="w-full md:w-1/2">
<label class="block" for="additionalCreators">Add additional creator(s) if creator is <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)"> v-if="form.errors.contributors && Array.isArray(form.errors.contributors)">
{{ form.errors.contributors.join(', ') }} {{ form.errors.contributors.join(', ') }}
</div> </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> </CardBox>
</div> </div>

View file

@ -241,11 +241,13 @@
</CardBox> </CardBox>
<!-- (7) authors --> <!-- (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'" <SearchAutocomplete source="/api/persons" :response-property="'first_name'"
placeholder="search in person table...." v-on:person="onAddAuthor"></SearchAutocomplete> 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" <div class="text-red-400 text-sm"
v-if="form.errors.authors && Array.isArray(form.errors.authors)"> v-if="form.errors.authors && Array.isArray(form.errors.authors)">
{{ form.errors.authors.join(', ') }} {{ form.errors.authors.join(', ') }}
@ -254,7 +256,8 @@
<!-- (8) contributors --> <!-- (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'" <SearchAutocomplete source="/api/persons" :response-property="'first_name'"
placeholder="search in person table...." v-on:person="onAddContributor"> placeholder="search in person table...." v-on:person="onAddContributor">
</SearchAutocomplete> </SearchAutocomplete>
@ -422,7 +425,8 @@
class="bg-red-100 group w-full h-full rounded-md cursor-pointer relative shadow-sm overflow-hidden"> class="bg-red-100 group w-full h-full rounded-md cursor-pointer relative shadow-sm overflow-hidden">
<section <section
class="flex flex-col rounded-md text-xs break-words w-full h-full z-20 absolute top-0 py-2 px-3"> 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 }} {{ element.value }}
</h1> </h1>
<div class="flex flex-col mt-auto"> <div class="flex flex-col mt-auto">
@ -431,7 +435,7 @@
</p> </p>
<p class="p-1 size text-xs text-gray-700"> <p class="p-1 size text-xs text-gray-700">
<span class="font-semibold">Relation:</span> {{ element.relation }} <span class="font-semibold">Relation:</span> {{ element.relation }}
</p> </p>
<div class="flex justify-end mt-1"> <div class="flex justify-end mt-1">
<button <button
class="restore ml-auto focus:outline-none hover:bg-gray-300 p-1 rounded-md text-gray-800" 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"; // import EditComponent from "./../EditComponent";
// export default 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 LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
import { useForm, Head, usePage } from '@inertiajs/vue3'; import { useForm, Head, usePage } from '@inertiajs/vue3';
import { computed, ComputedRef } from 'vue'; import { computed, ComputedRef } from 'vue';
@ -634,12 +636,6 @@ const flash: ComputedRef<any> = computed(() => {
const errors: ComputedRef<any> = computed(() => { const errors: ComputedRef<any> = computed(() => {
return usePage().props.errors; return usePage().props.errors;
}); });
// const errors: ComputedRef<any> = computed(() => {
// return usePage().props.errors;
// });
// const projects = reactive([]);
// const licenses = reactive([]);
const mapOptions: MapOptions = { const mapOptions: MapOptions = {
center: [48.208174, 16.373819], center: [48.208174, 16.373819],
@ -679,44 +675,8 @@ 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);
// 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> => { const submit = async (): Promise<void> => {
let route = stardust.route('dataset.update', [props.dataset.id]); 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) => { let licenses = form.licenses.map((obj) => {
if (hasIdAttribute(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( const [fileUploads, fileInputs] = form.files?.reduce(
([fileUploads, fileInputs], obj) => { ([fileUploads, fileInputs], obj) => {
if (!obj.id) { if (!obj.id) {
@ -815,6 +769,11 @@ const removeDescription = (key: any) => {
form.descriptions.splice(key, 1); 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) => { const onAddAuthor = (person: Person) => {
if (form.authors.filter((e) => e.id === person.id).length > 0) { if (form.authors.filter((e) => e.id === person.id).length > 0) {
notify({ type: 'warning', title: 'Warning', text: 'person is already defined as author' }, 4000); 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) => { const onAddContributor = (person: Person) => {
if (form.contributors.filter((e) => e.id === person.id).length > 0) { if (form.contributors.filter((e) => e.id === person.id).length > 0) {
notify({ type: 'warning', title: 'Warning', text: 'person is already defined as contributor' }, 4000); notify({ type: 'warning', title: 'Warning', text: 'person is already defined as contributor' }, 4000);