- dark modus for geoera search (keywords)
All checks were successful
CI Pipeline / japa-tests (push) Successful in 50s

- enum ContributorTypes
- add contributor types for creating a dataset
- npm updates
- tailwind styling for _table.css
- adapting migration code for dataset_11_subject.ts
This commit is contained in:
Kaimbacher 2023-09-12 16:09:04 +02:00
parent 3ea2e8ca94
commit b1d587d9f5
9 changed files with 156 additions and 88 deletions

View File

@ -23,6 +23,7 @@ import {
ReferenceIdentifierTypes, ReferenceIdentifierTypes,
RelationTypes, RelationTypes,
DatasetTypes, DatasetTypes,
SubjectTypes,
} from 'Contracts/enums'; } from 'Contracts/enums';
import type { ModelQueryBuilderContract } from '@ioc:Adonis/Lucid/Orm'; import type { ModelQueryBuilderContract } from '@ioc:Adonis/Lucid/Orm';
import DatasetReference from 'App/Models/DatasetReference'; import DatasetReference from 'App/Models/DatasetReference';
@ -131,6 +132,8 @@ export default class DatasetController {
projects: projects, projects: projects,
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 })),
contributorTypes: ContributorTypes,
subjectTypes: SubjectTypes
}); });
} }
@ -190,7 +193,12 @@ export default class DatasetController {
}), }),
), ),
authors: schema.array([rules.minLength(1)]).members(schema.object().members({ email: schema.string({ trim: true }) })), authors: schema.array([rules.minLength(1)]).members(schema.object().members({ email: schema.string({ trim: true }) })),
contributors: schema.array.optional().members(
schema.object().members({
email: schema.string({ trim: true }),
pivot_contributor_type: schema.enum(Object.keys(ContributorTypes)),
}),
),
// project_id: schema.number(), // project_id: schema.number(),
}); });
@ -240,6 +248,12 @@ export default class DatasetController {
}), }),
), ),
authors: schema.array([rules.minLength(1)]).members(schema.object().members({ email: schema.string({ trim: true }) })), authors: schema.array([rules.minLength(1)]).members(schema.object().members({ email: schema.string({ trim: true }) })),
contributors: schema.array.optional().members(
schema.object().members({
email: schema.string({ trim: true }),
pivot_contributor_type: schema.enum(Object.keys(ContributorTypes)),
}),
),
// third step // third step
project_id: schema.number.optional(), project_id: schema.number.optional(),
embargo_date: schema.date.optional({ format: 'yyyy-MM-dd' }, [rules.after(10, 'days')]), embargo_date: schema.date.optional({ format: 'yyyy-MM-dd' }, [rules.after(10, 'days')]),
@ -481,31 +495,41 @@ export default class DatasetController {
private async savePersons(dataset: Dataset, persons: any[], role: string, trx: TransactionClientContract) { private async savePersons(dataset: Dataset, persons: any[], role: string, trx: TransactionClientContract) {
for (const [key, person] of persons.entries()) { for (const [key, person] of persons.entries()) {
const pivotData = { role: role, sort_order: key + 1 }; const pivotData = {
role: role,
sort_order: key + 1,
allow_email_contact: false,
...this.extractPivotAttributes(person), // Merge pivot attributes here
};
if (person.id !== undefined) { if (person.id !== undefined) {
await dataset await dataset
.useTransaction(trx) .useTransaction(trx)
.related('persons') .related('persons')
.attach({ .attach({
[person.id]: { [person.id]: pivotData,
role: pivotData.role,
sort_order: pivotData.sort_order,
allow_email_contact: false,
},
}); });
} else { } else {
const dataPerson = new Person(); const dataPerson = new Person();
dataPerson.fill(person); dataPerson.fill(person);
await dataset.useTransaction(trx).related('persons').save(dataPerson, false, { await dataset.useTransaction(trx).related('persons').save(dataPerson, false, pivotData);
role: pivotData.role,
sort_order: pivotData.sort_order,
allow_email_contact: false,
});
} }
} }
} }
// Helper function to extract pivot attributes from a person object
private extractPivotAttributes(person: any) {
const pivotAttributes = {};
for (const key in person) {
if (key.startsWith('pivot_')) {
// pivotAttributes[key] = person[key];
const cleanKey = key.replace('pivot_', ''); // Remove 'pivot_' prefix
pivotAttributes[cleanKey] = person[key];
}
}
return pivotAttributes;
}
public messages: CustomMessages = { public messages: CustomMessages = {
'minLength': '{{ field }} must be at least {{ options.minLength }} characters long', 'minLength': '{{ field }} must be at least {{ options.minLength }} characters long',
'maxLength': '{{ field }} must be less then {{ options.maxLength }} characters long', 'maxLength': '{{ field }} must be less then {{ options.maxLength }} characters long',
@ -532,6 +556,7 @@ export default class DatasetController {
'The language of the translated description must be different from the language of the dataset', 'The language of the translated description must be different from the language of the dataset',
'authors.minLength': 'at least {{ options.minLength }} author must be defined', 'authors.minLength': 'at least {{ options.minLength }} author must be defined',
'contributors.*.pivot_contributor_type.required': 'contributor type is required, if defined',
'after': `{{ field }} must be older than ${dayjs().add(10, 'day')}`, 'after': `{{ field }} must be older than ${dayjs().add(10, 'day')}`,

View File

@ -1,7 +1,7 @@
import { schema, CustomMessages, rules } from '@ioc:Adonis/Core/Validator'; import { schema, CustomMessages, rules } from '@ioc:Adonis/Core/Validator';
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { TitleTypes, DescriptionTypes, RelationTypes, ReferenceIdentifierTypes } from 'Contracts/enums'; import { TitleTypes, DescriptionTypes, RelationTypes, ReferenceIdentifierTypes, ContributorTypes } from 'Contracts/enums';
export default class CreateDatasetValidator { export default class CreateDatasetValidator {
constructor(protected ctx: HttpContextContract) {} constructor(protected ctx: HttpContextContract) {}
@ -58,6 +58,12 @@ export default class CreateDatasetValidator {
}), }),
), ),
authors: schema.array([rules.minLength(1)]).members(schema.object().members({ email: schema.string({ trim: true }) })), authors: schema.array([rules.minLength(1)]).members(schema.object().members({ email: schema.string({ trim: true }) })),
contributors: schema.array.optional().members(
schema.object().members({
email: schema.string({ trim: true }),
pivot_contributor_type: schema.enum(Object.keys(ContributorTypes)),
}),
),
// third step // third step
project_id: schema.number.optional(), project_id: schema.number.optional(),
embargo_date: schema.date.optional({ format: 'yyyy-MM-dd' }, [rules.after(10, 'days')]), embargo_date: schema.date.optional({ format: 'yyyy-MM-dd' }, [rules.after(10, 'days')]),
@ -149,6 +155,7 @@ export default class CreateDatasetValidator {
'The language of the translated description must be different from the language of the dataset', 'The language of the translated description must be different from the language of the dataset',
'authors.minLength': 'at least {{ options.minLength }} author must be defined', 'authors.minLength': 'at least {{ options.minLength }} author must be defined',
'contributors.*.pivot_contributor_type.required': 'contributor type is required, if defined',
'after': `{{ field }} must be older than ${dayjs().add(10, 'day')}`, 'after': `{{ field }} must be older than ${dayjs().add(10, 'day')}`,

View File

@ -54,31 +54,32 @@ export enum PersonRoles {
} }
export enum ContributorTypes { export enum ContributorTypes {
contact_person = 'ContactPerson', ContactPerson = 'ContactPerson',
data_collector = 'DataCollector', DataCollector = 'DataCollector',
data_curator = 'DataCurator', DataCurator = 'DataCurator',
data_manager = 'DataManager', DataManager = 'DataManager',
Distributor = 'Distributor', Distributor = 'Distributor',
editor = 'Editor', Editor = 'Editor',
hosting_institution = 'HostingInstitution', HostingInstitution = 'HostingInstitution',
producer = 'Producer', Producer = 'Producer',
poroject_leader = 'ProjectLeader', ProjectLeader = 'ProjectLeader',
project_manager = 'ProjectManager', ProjectManager = 'ProjectManager',
project_member = 'ProjectMember', ProjectMember = 'ProjectMember',
registration_agency = 'RegistrationAgency', RegistrationAgency = 'RegistrationAgency',
registration_authority = 'RegistrationAuthority', RegistrationAuthority = 'RegistrationAuthority',
related_person = 'RelatedPerson', RelatedPerson = 'RelatedPerson',
researcher = 'Researcher', Researcher = 'Researcher',
research_group = 'ResearchGroup', ResearchGroup = 'ResearchGroup',
rights_holder = 'RightsHolder', RightsHolder = 'RightsHolder',
sponsor = 'Sponsor', Sponsor = 'Sponsor',
supervisor = 'Supervisor', Supervisor = 'Supervisor',
work_package_leader = 'WorkPackageLeader', WorkPackageLeader = 'WorkPackageLeader',
other = 'Other', Other = 'Other',
} }
export enum SubjectTypes { export enum SubjectTypes {
uncontrolled = 'uncontrolled', uncontrolled = 'uncontrolled',
geoera = 'GeoEra',
} }
export enum ReferenceIdentifierTypes { export enum ReferenceIdentifierTypes {

View File

@ -9,7 +9,7 @@ export default class DatasetSubjects extends BaseSchema {
table.bigIncrements('id').defaultTo("nextval('dataset_subjects_id_seq')"); table.bigIncrements('id').defaultTo("nextval('dataset_subjects_id_seq')");
table.string('language', 3); table.string('language', 3);
// table.string('type', 255).notNullable().defaultTo('uncontrolled'); // table.string('type', 255).notNullable().defaultTo('uncontrolled');
table.enum('type', Object.values(SubjectTypes)).defaultTo('uncontrolled'); table.enum('type', Object.keys(SubjectTypes)).defaultTo('uncontrolled');
table.string('value', 255).notNullable(); table.string('value', 255).notNullable();
table.string('external_key', 255); table.string('external_key', 255);
table.timestamp('created_at', { useTz: false }).nullable(); table.timestamp('created_at', { useTz: false }).nullable();

View File

@ -40,9 +40,9 @@ table {
@apply lg:bg-gray-50 lg:dark:bg-slate-800; @apply lg:bg-gray-50 lg:dark:bg-slate-800;
} }
/* tbody tr:nth-child(even) { tbody tr:nth-child(even) {
@apply lg:bg-gray-50 lg:dark:bg-slate-800 lg:bg-opacity-25; @apply lg:bg-gray-50 lg:dark:bg-slate-600 lg:bg-opacity-25;
} */ }
td:before { td:before {
content: attr(data-label); content: attr(data-label);

View File

@ -235,7 +235,7 @@ const inputElClass = computed(() => {
// dark:placeholder-gray-400 dark:text-white dark:focus:border-blue-500" // dark:placeholder-gray-400 dark:text-white dark:focus:border-blue-500"
const base = [ const base = [
'block p-2.5 w-full z-20 text-sm text-gray-900 bg-gray-50 rounded-r-lg', 'block p-2.5 w-full z-20 text-sm text-gray-900 bg-gray-50 rounded-r-lg',
'dark:placeholder-gray-400', 'dark:placeholder-gray-400 dark:text-white dark:focus:border-blue-500',
'h-12', 'h-12',
props.borderless ? 'border-0' : 'border', props.borderless ? 'border-0' : 'border',
props.transparent ? 'bg-transparent' : 'bg-white dark:bg-slate-800', props.transparent ? 'bg-transparent' : 'bg-white dark:bg-slate-800',

View File

@ -20,6 +20,10 @@ const props = defineProps({
type: Array<Subject>, type: Array<Subject>,
default: () => [], default: () => [],
}, },
subjectTypes: {
type: Object,
default: () => ({}),
},
errors: { errors: {
type: Object, type: Object,
default: () => ({}), default: () => ({}),
@ -102,73 +106,59 @@ const removeItem = (key) => {
<tr> <tr>
<!-- <th v-if="checkable" /> --> <!-- <th v-if="checkable" /> -->
<!-- <th class="hidden lg:table-cell"></th> --> <!-- <th class="hidden lg:table-cell"></th> -->
<th>Type</th> <th scope="col">Type</th>
<th>Value</th> <th scope="col">Value</th>
<th>Language</th> <th scope="col">Language</th>
<th /> <th scope="col" />
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(item, index) in itemsPaginated" :key="index" class="bg-gray-50"> <tr v-for="(item, index) in itemsPaginated" :key="index">
<!-- <TableCheckboxCell v-if="checkable" @checked="checked($event, client)" /> --> <!-- <TableCheckboxCell v-if="checkable" @checked="checked($event, client)" /> -->
<!-- <td class="border-b-0 lg:w-6 before:hidden hidden lg:table-cell"> <!-- <td class="border-b-0 lg:w-6 before:hidden hidden lg:table-cell">
<UserAvatar :username="client.value" class="w-24 h-24 mx-auto lg:w-6 lg:h-6" /> <UserAvatar :username="client.value" class="w-24 h-24 mx-auto lg:w-6 lg:h-6" />
</td> --> </td> -->
<td data-label="Type"> <td data-label="Type" scope="row">
<FormControl <FormControl required v-model="item.type" :type="'select'" placeholder="[Enter Language]"
required :options="props.subjectTypes">
v-model="item.type"
:type="'select'"
placeholder="[Enter Language]"
:options="{ uncontrolled: 'uncontrolled', geoera: 'geoera' }"
>
<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(', ') }}
</div> </div>
</FormControl> </FormControl>
</td> </td>
<td data-label="Value"> <td data-label="Value" scope="row">
<SearchCategoryAutocomplete <SearchCategoryAutocomplete v-if="item.type !== 'uncontrolled'" v-model="item.value" @subject="(language) => {
v-if="item.type !== 'uncontrolled'"
v-model="item.value"
@subject="
(language) => {
item.language = language; item.language = language;
} }
" ">
>
<div class="text-red-400 text-sm" v-if="errors[`subjects.${index}.value`]"> <div class="text-red-400 text-sm" v-if="errors[`subjects.${index}.value`]">
{{ errors[`subjects.${index}.value`].join(', ') }} {{ errors[`subjects.${index}.value`].join(', ') }}
</div> </div>
</SearchCategoryAutocomplete> </SearchCategoryAutocomplete>
<FormControl v-else required v-model="item.value" type="text" placeholder="[enter keyword value]" :borderless="true"> <FormControl v-else required v-model="item.value" type="text" placeholder="[enter keyword value]"
:borderless="true">
<div class="text-red-400 text-sm" v-if="errors[`subjects.${index}.value`]"> <div class="text-red-400 text-sm" v-if="errors[`subjects.${index}.value`]">
{{ errors[`subjects.${index}.value`].join(', ') }} {{ errors[`subjects.${index}.value`].join(', ') }}
</div> </div>
</FormControl> </FormControl>
</td> </td>
<td data-label="Language"> <td data-label="Language" scope="row">
<FormControl <FormControl required v-model="item.language" :type="'select'" placeholder="[Enter Lang]"
required :options="{ de: 'de', en: 'en' }" :is-read-only="item.type != 'uncontrolled'">
v-model="item.language"
:type="'select'"
placeholder="[Enter Lang]"
:options="{ de: 'de', en: 'en' }"
:is-read-only="item.type != 'uncontrolled'"
>
<div class="text-red-400 text-sm" v-if="errors[`subjects.${index}.language`]"> <div class="text-red-400 text-sm" v-if="errors[`subjects.${index}.language`]">
{{ errors[`subjects.${index}.language`].join(', ') }} {{ errors[`subjects.${index}.language`].join(', ') }}
</div> </div>
</FormControl> </FormControl>
</td> </td>
<td class="before:hidden lg:w-1 whitespace-nowrap"> <td class="before:hidden lg:w-1 whitespace-nowrap" scope="row">
<BaseButtons type="justify-start lg:justify-end" no-wrap> <BaseButtons type="justify-start lg:justify-end" no-wrap>
<!-- <BaseButton color="info" :icon="mdiEye" small @click="isModalActive = true" /> --> <!-- <BaseButton color="info" :icon="mdiEye" small @click="isModalActive = true" /> -->
<BaseButton v-if="index > 2" color="danger" :icon="mdiTrashCan" small @click.prevent="removeItem(index)" /> <BaseButton v-if="index > 2" color="danger" :icon="mdiTrashCan" small
@click.prevent="removeItem(index)" />
</BaseButtons> </BaseButtons>
</td> </td>
</tr> </tr>
@ -179,15 +169,8 @@ const removeItem = (key) => {
<div class="p-3 lg:px-6 border-t border-gray-100 dark:border-slate-800"> <div class="p-3 lg:px-6 border-t border-gray-100 dark:border-slate-800">
<BaseLevel> <BaseLevel>
<BaseButtons> <BaseButtons>
<BaseButton <BaseButton v-for="page in pagesList" :key="page" :active="page === currentPage" :label="page + 1" small
v-for="page in pagesList" :outline="styleService.darkMode" @click="currentPage = page" />
:key="page"
:active="page === currentPage"
:label="page + 1"
small
:outline="styleService.darkMode"
@click="currentPage = page"
/>
</BaseButtons> </BaseButtons>
<small>Page {{ currentPageHuman }} of {{ numPages }}</small> <small>Page {{ currentPageHuman }} of {{ numPages }}</small>
</BaseLevel> </BaseLevel>
@ -197,3 +180,14 @@ const removeItem = (key) => {
{{ errors.subjects.join(', ') }} {{ errors.subjects.join(', ') }}
</div> </div>
</template> </template>
<style scoped>
/* tr:nth-child(even) {
background: gray;
}
tr:nth-child(od) {
background: white;
} */
</style>

View File

@ -7,13 +7,14 @@ import { mdiDragVariant } from '@mdi/js';
import BaseIcon from '@/Components/BaseIcon.vue'; import BaseIcon from '@/Components/BaseIcon.vue';
// import CardBoxModal from '@/Components/CardBoxModal.vue'; // import CardBoxModal from '@/Components/CardBoxModal.vue';
// import TableCheckboxCell from '@/Components/TableCheckboxCell.vue'; // import TableCheckboxCell from '@/Components/TableCheckboxCell.vue';
import BaseLevel from '@/Components/BaseLevel.vue'; // import BaseLevel from '@/Components/BaseLevel.vue';
import BaseButtons from '@/Components/BaseButtons.vue'; import BaseButtons from '@/Components/BaseButtons.vue';
import BaseButton from '@/Components/BaseButton.vue'; import BaseButton from '@/Components/BaseButton.vue';
import UserAvatar from '@/Components/UserAvatar.vue'; import UserAvatar from '@/Components/UserAvatar.vue';
// import Person from 'App/Models/Person'; // import Person from 'App/Models/Person';
import { Person } from '@/Stores/main'; import { Person } from '@/Stores/main';
import Draggable from 'vuedraggable'; import Draggable from 'vuedraggable';
import FormControl from '@/Components/FormControl.vue';
const props = defineProps({ const props = defineProps({
checkable: Boolean, checkable: Boolean,
@ -21,6 +22,14 @@ const props = defineProps({
type: Array<Person>, type: Array<Person>,
default: () => [], default: () => [],
}, },
contributortypes: {
type: Object,
default: () => ({}),
},
errors: {
type: Object,
default: () => ({}),
},
}); });
const styleService = StyleService(); const styleService = StyleService();
@ -125,6 +134,10 @@ const removeAuthor = (key) => {
<th class="hidden lg:table-cell"></th> <th class="hidden lg:table-cell"></th>
<th>Name</th> <th>Name</th>
<th>Email</th> <th>Email</th>
<th scope="col" v-if="Object.keys(contributortypes).length">
<span>Type</span>
</th>
<!-- <th>Name Type</th> --> <!-- <th>Name Type</th> -->
<!-- <th>Progress</th> --> <!-- <th>Progress</th> -->
<!-- <th>Created</th> --> <!-- <th>Created</th> -->
@ -136,7 +149,9 @@ const removeAuthor = (key) => {
<draggable id="galliwasery" tag="tbody" v-model="items" item-key="id"> <draggable id="galliwasery" tag="tbody" v-model="items" item-key="id">
<template #item="{ index, element }"> <template #item="{ index, element }">
<tr> <tr>
<td class="drag-icon"><BaseIcon :path="mdiDragVariant"/></td> <td class="drag-icon">
<BaseIcon :path="mdiDragVariant" />
</td>
<td scope="row">{{ index + 1 }}</td> <td scope="row">{{ index + 1 }}</td>
<!-- <TableCheckboxCell v-if="checkable" @checked="checked($event, client)" /> --> <!-- <TableCheckboxCell v-if="checkable" @checked="checked($event, client)" /> -->
<td class="border-b-0 lg:w-6 before:hidden hidden lg:table-cell"> <td class="border-b-0 lg:w-6 before:hidden hidden lg:table-cell">
@ -148,6 +163,20 @@ const removeAuthor = (key) => {
<td data-label="Email"> <td data-label="Email">
{{ element.email }} {{ element.email }}
</td> </td>
<td v-if="Object.keys(contributortypes).length">
<!-- <select type="text" v-model="element.pivot.contributor_type">
<option v-for="(option, i) in contributortypes" :value="option" :key="i">
{{ option }}
</option>
</select> -->
<FormControl required v-model="element.pivot_contributor_type" type="select"
:options="contributortypes" placeholder="[relation type]">
<div class="text-red-400 text-sm"
v-if="errors && Array.isArray(errors[`contributors.${index}.pivot_contributor_type`])">
{{ errors[`contributors.${index}.pivot_contributor_type`].join(', ') }}
</div>
</FormControl>
</td>
<!-- <td data-label="Name Type"> <!-- <td data-label="Name Type">
{{ client.name_type }} {{ client.name_type }}
</td> --> </td> -->

View File

@ -76,6 +76,14 @@ const props = defineProps({
type: Object, type: Object,
default: () => ({}), default: () => ({}),
}, },
contributorTypes: {
type: Object,
default: () => ({}),
},
subjectTypes: {
type: Object,
default: () => ({}),
},
errors: { errors: {
type: Object, type: Object,
default: () => ({}), default: () => ({}),
@ -720,7 +728,11 @@ Removes a selected keyword
placeholder="search in person table...." v-on:person="onAddContributor"> placeholder="search in person table...." v-on:person="onAddContributor">
</SearchAutocomplete> </SearchAutocomplete>
<TablePersons :persons="form.contributors" v-if="form.contributors.length > 0" /> <TablePersons :persons="form.contributors" v-if="form.contributors.length > 0"
:contributortypes="contributorTypes" :errors="form.errors"/>
<div class="text-red-400 text-sm" v-if="form.errors.contributors && Array.isArray(form.errors.contributors)">
{{ form.errors.contributors.join(', ') }}
</div>
</CardBox> </CardBox>
</div> </div>
@ -983,7 +995,7 @@ Removes a selected keyword
{{ subject.value }} <BaseButton color="danger" :icon="mdiTrashCan" small @click.prevent="removeKeyword(index)" /> {{ subject.value }} <BaseButton color="danger" :icon="mdiTrashCan" small @click.prevent="removeKeyword(index)" />
</li> </li>
</ul> --> </ul> -->
<TableKeywords :keywords="form.subjects" :errors="form.errors" <TableKeywords :keywords="form.subjects" :errors="form.errors" :subjectTypes="subjectTypes"
v-if="form.subjects.length > 0" /> v-if="form.subjects.length > 0" />
</CardBox> </CardBox>
</div> </div>