feat: update API controllers, validations, and Vue components
All checks were successful
CI / container-job (push) Successful in 49s
All checks were successful
CI / container-job (push) Successful in 49s
- Modified Api/Authors.Controller.ts to use only personal types and sort by dataset_count. - Completely rewritten AvatarController.ts. - Added new Api/CollectionsController.ts for querying collections and collection_roles. - Modified Api/DatasetController.ts to preload titles, identifier and order by server_date_published. - Modified FileController.ts to serve files from /storage/app/data/ instead of /storage/app/public. - Added new Api/UserController for requesting submitters (getSubmitters). - Improved OaiController.ts with performant DB queries for better ResumptionToken handling. - Modified Submitter/DatasetController.ts by adding a categorize method for library classification. - Rewritten ResumptionToken.ts. - Improved TokenWorkerService.ts to utilize browser fingerprint. - Edited dataset.ts by adding the doiIdentifier property. - Enhanced person.ts to improve the fullName property. - Completely rewritten AsideMenuItem.vue component. - Updated CarBoxClient.vue to use TypeScript. - Added new CardBoxDataset.vue for displaying recent datasets on the dashboard. - Completely rewritten TableSampleClients.vue for the dashboard. - Completely rewritten UserAvatar.vue. - Made small layout changes in Dashboard.vue. - Added new Category.vue for browsing scientific collections. - Adapted the pinia store in main.ts. - Added additional routes in start/routes.ts and start/api/routes.ts. - Improved referenceValidation.ts for better ISBN existence checking. - NPM dependency updates.
This commit is contained in:
parent
36cd7a757b
commit
b540547e4c
34 changed files with 1757 additions and 1018 deletions
|
@ -1,91 +1,343 @@
|
|||
<template>
|
||||
<div class="flex flex-col h-screen p-4 bg-gray-100">
|
||||
<header class="flex justify-between items-center mb-4">
|
||||
<h1 class="text-xl font-bold">SKOS Browser</h1>
|
||||
<div class="flex space-x-2">
|
||||
<button @click="updateApp" title="Update the application">
|
||||
<!-- <img src="/Resources/Images/refresh.png" alt="Update" class="w-4 h-4" /> -->
|
||||
</button>
|
||||
<button @click="showInfo" title="Info">
|
||||
<!-- <img src="/Resources/Images/info.png" alt="Info" class="w-4 h-4" /> -->
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<LayoutAuthenticated>
|
||||
|
||||
<div class="bg-white shadow-md rounded-lg p-4 mb-6">
|
||||
<h2 class="text-lg font-semibold">GBA-Thesaurus</h2>
|
||||
<label class="block text-sm font-medium">Aktueller Endpoint:</label>
|
||||
<!-- <TreeView :items="endpoints" @select="onEndpointSelected" /> -->
|
||||
</div>
|
||||
<Head title="Profile"></Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton :icon="mdiLibraryShelves" title="Library Classification" main>
|
||||
<div class="bg-lime-100 shadow rounded-lg p-6 mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<label for="role-select" class="block text-lg font-medium text-gray-700 mb-1">
|
||||
Select Classification Role <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select id="role-select" v-model="selectedCollectionRole"
|
||||
class="w-full border border-gray-300 rounded-md p-2 text-gray-700 focus:ring-2 focus:ring-indigo-500"
|
||||
required>
|
||||
<!-- <option value="" disabled selected>Please select a role</option> -->
|
||||
<option v-for="collRole in collectionRoles" :key="collRole.id" :value="collRole">
|
||||
{{ collRole.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ml-4 hidden md:block">
|
||||
<span class="text-sm text-gray-600 italic">* required</span>
|
||||
</div>
|
||||
</div>
|
||||
</SectionTitleLineWithButton>
|
||||
|
||||
|
||||
<!-- Available TopLevel Collections -->
|
||||
<CardBox class="mb-4 rounded-lg p-4">
|
||||
<h2 class="text-lg font-bold text-gray-800 dark:text-slate-400 mb-2">Available Toplevel-Collections
|
||||
<span v-if="selectedCollectionRole && !selectedToplevelCollection"
|
||||
class="text-sm text-red-500 italic">(click to
|
||||
select)</span>
|
||||
</h2>
|
||||
<ul class="flex flex-wrap gap-2">
|
||||
<li v-for="col in collections" :key="col.id" :class="{
|
||||
'cursor-pointer p-2 border border-gray-200 rounded hover:bg-sky-50 text-sky-700 text-sm': true,
|
||||
'bg-sky-100 border-sky-500': selectedToplevelCollection && selectedToplevelCollection.id === col.id
|
||||
}" @click="onToplevelCollectionSelected(col)">
|
||||
{{ `${col.name} (${col.number})` }}
|
||||
</li>
|
||||
<li v-if="collections.length === 0" class="text-gray-800 dark:text-slate-400">
|
||||
No collections available.
|
||||
</li>
|
||||
</ul>
|
||||
</CardBox>
|
||||
|
||||
<!-- Collections Listing -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||
|
||||
<!-- Broader Collection (Parent) -->
|
||||
<CardBox v-if="selectedCollection" class="rounded-lg p-4" has-form-data>
|
||||
<h2 class="text-lg font-bold text-gray-800 dark:text-slate-400 mb-2">Broader Collection</h2>
|
||||
<ul class="flex flex-wrap gap-2 max-h-60 overflow-y-auto">
|
||||
<li v-for="parent in broaderCollections" :key="parent.id"
|
||||
class="cursor-pointer p-2 border border-gray-200 rounded bg-green-50 text-green-700 text-sm hover:bg-green-100 hover:underline"
|
||||
@click="onCollectionSelected(parent)" title="Click to select this collection">
|
||||
{{ `${parent.name} (${parent.number})` }}
|
||||
</li>
|
||||
<li v-if="broaderCollections.length === 0" class="text-gray-500 text-sm">
|
||||
No broader collections available.
|
||||
</li>
|
||||
</ul>
|
||||
</CardBox>
|
||||
|
||||
<!-- Selected Collection Details -->
|
||||
<CardBox v-if="selectedCollection" class="rounded-lg p-4" has-form-data>
|
||||
<h3 class="text-xl font-bold text-gray-800 dark:text-slate-400 mb-2">Selected Collection</h3>
|
||||
<p
|
||||
class="cursor-pointer p-2 border border-gray-200 rounded bg-green-50 text-green-700 text-sm hover:bg-green-100">
|
||||
{{ `${selectedCollection.name} (${selectedCollection.number})` }}
|
||||
</p>
|
||||
</CardBox>
|
||||
|
||||
<!-- Narrower Collections (Children) -->
|
||||
<CardBox v-if="selectedCollection" class="rounded-lg p-4" has-form-data>
|
||||
<h2 class="text-lg font-bold text-gray-800 dark:text-slate-400 mb-2">Narrower Collections</h2>
|
||||
<!-- <ul class="flex flex-wrap gap-2 max-h-60 overflow-y-auto">
|
||||
<li v-for="child in narrowerCollections" :key="child.id"
|
||||
class="cursor-pointer p-2 border border-gray-200 rounded bg-green-50 text-green-700 text-sm hover:bg-green-100 hover:underline"
|
||||
@click="onCollectionSelected(child)">
|
||||
{{ `${child.name} (${child.number})` }}
|
||||
</li>
|
||||
<li v-if="narrowerCollections.length === 0" class="text-gray-500 text-sm">
|
||||
No sub-collections available.
|
||||
</li>
|
||||
</ul> -->
|
||||
<draggable v-if="narrowerCollections.length > 0" v-model="narrowerCollections"
|
||||
:group="{ name: 'collections' }" tag="ul" class="flex flex-wrap gap-2 max-h-60 overflow-y-auto">
|
||||
<template #item="{ element: child }">
|
||||
<li :key="child.id"
|
||||
class="cursor-pointer p-2 border border-gray-200 rounded bg-green-50 text-green-700 text-sm hover:bg-green-100 hover:underline"
|
||||
@click="onCollectionSelected(child)">
|
||||
{{ `${child.name} (${child.number})` }}
|
||||
</li>
|
||||
</template>
|
||||
</draggable>
|
||||
<ul v-else class="flex flex-wrap gap-2 max-h-60 overflow-y-auto">
|
||||
<li class="text-gray-500 text-sm">
|
||||
No sub-collections available.
|
||||
</li>
|
||||
</ul>
|
||||
</CardBox>
|
||||
|
||||
<div class="bg-white shadow-md rounded-lg p-4">
|
||||
<h2 class="text-lg font-semibold">Konzept-Suche</h2>
|
||||
<!-- <Autocomplete v-model="selectedConcept" :items="concepts" placeholder="Search for a concept" @change="onConceptSelected" /> -->
|
||||
<div class="mt-4">
|
||||
<h3 class="text-md font-medium">Ausgewähltes Konzept</h3>
|
||||
<p>{{ selectedConcept.title }}</p>
|
||||
<a :href="selectedConcept.uri" target="_blank" class="text-blue-500">URI</a>
|
||||
<textarea
|
||||
v-model="selectedConcept.description"
|
||||
class="mt-2 w-full h-24 border rounded"
|
||||
placeholder="Description"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<h3 class="text-md font-medium">Untergeordnete Konzepte</h3>
|
||||
<!-- <LinkLabelList :items="narrowerConcepts" /> -->
|
||||
|
||||
<div class="mb-4 rounded-lg">
|
||||
<div v-if="selectedCollection" class="bg-gray-100 shadow rounded-lg p-6 mb-6">
|
||||
<p class="mb-4 text-gray-700">Please drag your collections here to classify your previously created
|
||||
dataset
|
||||
according to library classification standards.</p>
|
||||
<draggable v-model="dropCollections" :group="{ name: 'collections' }"
|
||||
class="min-h-36 border-dashed border-2 border-gray-400 p-4 text-sm flex flex-wrap gap-2 max-h-60 overflow-y-auto"
|
||||
tag="ul">
|
||||
<template #item="{ element }">
|
||||
<div :key="element.id"
|
||||
class="p-2 m-1 bg-sky-200 text-sky-800 rounded flex items-center gap-2 h-7">
|
||||
<span>{{ element.name }} ({{ element.number }})</span>
|
||||
<button
|
||||
@click="dropCollections = dropCollections.filter(item => item.id !== element.id)"
|
||||
class="hover:text-sky-600 flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20"
|
||||
fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<h3 class="text-md font-medium">Übergeordnete Konzepte</h3>
|
||||
<!-- <LinkLabelList :items="broaderConcepts" /> -->
|
||||
|
||||
<div class="p-6 border-t border-gray-100 dark:border-slate-800">
|
||||
<BaseButtons>
|
||||
<BaseButton @click.stop="syncDatasetCollections" label="Save" color="info" small
|
||||
:disabled="isSaveDisabled" :style="{ opacity: isSaveDisabled ? 0.5 : 1 }">
|
||||
</BaseButton>
|
||||
</BaseButtons>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<h3 class="text-md font-medium">Verwandte Konzepte</h3>
|
||||
<!-- <LinkLabelList :items="relatedConcepts" /> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</SectionMain>
|
||||
</LayoutAuthenticated>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// import TreeView from './TreeView.vue'; // Assuming you have a TreeView component
|
||||
// import Autocomplete from './Autocomplete.vue'; // Assuming you have an Autocomplete component
|
||||
// import LinkLabelList from './LinkLabelList.vue'; // Assuming you have a LinkLabelList component
|
||||
<script lang="ts" setup>
|
||||
import { ref, Ref, watch, computed } from 'vue';
|
||||
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
||||
import SectionMain from '@/Components/SectionMain.vue';
|
||||
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
|
||||
import axios from 'axios';
|
||||
import { mdiLibraryShelves } from '@mdi/js';
|
||||
import draggable from 'vuedraggable';
|
||||
import BaseButton from '@/Components/BaseButton.vue';
|
||||
import BaseButtons from '@/Components/BaseButtons.vue';
|
||||
import CardBox from '@/Components/CardBox.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
// TreeView,
|
||||
// Autocomplete,
|
||||
// LinkLabelList,
|
||||
interface CollectionRole {
|
||||
id: number;
|
||||
name: string;
|
||||
collections?: any[];
|
||||
}
|
||||
|
||||
interface Collection {
|
||||
id: number;
|
||||
name: string;
|
||||
number: string;
|
||||
parent_id?: number | null;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
collectionRoles: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
endpoints: [], // This should be populated with your data
|
||||
concepts: [], // This should be populated with your data
|
||||
selectedConcept: {},
|
||||
narrowerConcepts: [], // Populate with data
|
||||
broaderConcepts: [], // Populate with data
|
||||
relatedConcepts: [], // Populate with data
|
||||
};
|
||||
dataset: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
methods: {
|
||||
updateApp() {
|
||||
// Handle app update logic
|
||||
},
|
||||
showInfo() {
|
||||
// Handle showing information
|
||||
},
|
||||
onEndpointSelected(endpoint) {
|
||||
// Handle endpoint selection
|
||||
},
|
||||
onConceptSelected(concept) {
|
||||
this.selectedConcept = concept;
|
||||
// Handle concept selection logic, e.g., fetching related concepts
|
||||
},
|
||||
relatedCollections: Array<Collection>
|
||||
});
|
||||
|
||||
const collectionRoles: Ref<CollectionRole[]> = ref(props.collectionRoles as CollectionRole[]);
|
||||
const collections: Ref<Collection[]> = ref<Collection[]>([]);
|
||||
const selectedCollectionRole = ref<CollectionRole | null>(null);
|
||||
const selectedToplevelCollection = ref<Collection | null>(null);
|
||||
const selectedCollection = ref<Collection | null>(null);
|
||||
const narrowerCollections = ref<Collection[]>([]);
|
||||
const broaderCollections = ref<Collection[]>([]);
|
||||
|
||||
|
||||
// const onCollectionRoleSelected = (event: Event) => {
|
||||
// const target = event.target as HTMLSelectElement;
|
||||
// const roleId = Number(target.value);
|
||||
// selectedCollectionRole.value =
|
||||
// collectionRoles.value.find((role: CollectionRole) => role.id === roleId) || null;
|
||||
|
||||
// // Clear any previously selected collection or related data
|
||||
// selectedCollection.value = null;
|
||||
// narrowerCollections.value = [];
|
||||
// broaderCollections.value = [];
|
||||
|
||||
// // fetchTopLevelCollections(roleId);
|
||||
// collections.value = selectedCollectionRole.value?.collections || []
|
||||
// };
|
||||
|
||||
// New reactive array to hold dropped collections for the dataset
|
||||
const dropCollections: Ref<Collection[]> = ref([]);
|
||||
|
||||
// If there are related collections passed in, fill dropCollections with these.
|
||||
if (props.relatedCollections && props.relatedCollections.length > 0) {
|
||||
dropCollections.value = props.relatedCollections;
|
||||
}
|
||||
|
||||
// Add a computed property for the disabled state based on dropCollections length
|
||||
const isSaveDisabled = computed(() => dropCollections.value.length === 0);
|
||||
|
||||
// If the collectionRoles prop might load asynchronously (or change), you can watch for it:
|
||||
watch(
|
||||
() => props.collectionRoles as CollectionRole[],
|
||||
(newCollectionRoles: CollectionRole[]) => {
|
||||
collectionRoles.value = newCollectionRoles;
|
||||
// Preselect the role with name "ccs" if it exists
|
||||
const found: CollectionRole | undefined = collectionRoles.value.find(
|
||||
role => role.name.toLowerCase() === 'ccs'
|
||||
);
|
||||
if (found?.name === 'ccs') {
|
||||
selectedCollectionRole.value = found;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
// Watch for changes in selectedCollectionRole and update related collections state
|
||||
watch(
|
||||
() => selectedCollectionRole.value as CollectionRole,
|
||||
(newSelectedCollectionRole: CollectionRole | null) => {
|
||||
if (newSelectedCollectionRole != null) {
|
||||
collections.value = newSelectedCollectionRole.collections || []
|
||||
} else {
|
||||
selectedToplevelCollection.value = null;
|
||||
selectedCollection.value = null;
|
||||
collections.value = []
|
||||
}
|
||||
// Reset dependent variables when the role changes
|
||||
selectedCollection.value = null
|
||||
narrowerCollections.value = []
|
||||
broaderCollections.value = []
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Watch for changes in dropCollections
|
||||
watch(
|
||||
() => dropCollections.value,
|
||||
() => {
|
||||
if (selectedCollection.value) {
|
||||
fetchCollections(selectedCollection.value.id);
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
const onToplevelCollectionSelected = (collection: Collection) => {
|
||||
selectedToplevelCollection.value = collection;
|
||||
selectedCollection.value = collection;
|
||||
// call the API endpoint to get both.
|
||||
fetchCollections(collection.id)
|
||||
};
|
||||
|
||||
const onCollectionSelected = (collection: Collection) => {
|
||||
selectedCollection.value = collection;
|
||||
// call the API endpoint to get both.
|
||||
fetchCollections(collection.id)
|
||||
};
|
||||
|
||||
// New function to load both narrower and broader concepts using the real API route.
|
||||
const fetchCollections = async (collectionId: number) => {
|
||||
try {
|
||||
const response = await axios.get(`/api/collections/${collectionId}`);
|
||||
const data = response.data;
|
||||
|
||||
// Set narrower concepts with filtered collections
|
||||
narrowerCollections.value = data.narrowerCollections.filter(
|
||||
collection => !dropCollections.value.some(dc => dc.id === collection.id)
|
||||
);
|
||||
// For broader concepts, if present, wrap it in an array (or change your template accordingly)
|
||||
broaderCollections.value = data.broaderCollection;
|
||||
} catch (error) {
|
||||
console.error('Error in fetchConcepts:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const syncDatasetCollections = async () => {
|
||||
try {
|
||||
// Extract the ids from the dropCollections list
|
||||
const collectionIds = dropCollections.value.map(item => item.id);
|
||||
await axios.post('/api/dataset/collections/sync', { collections: collectionIds });
|
||||
// Optionally show a success message or refresh dataset info
|
||||
} catch (error) {
|
||||
console.error('Error syncing dataset collections:', error);
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Add your styles here */
|
||||
.btn-primary {
|
||||
background-color: #4f46e5;
|
||||
color: white;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #4338ca;
|
||||
}
|
||||
|
||||
.btn-primary:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px #fff, 0 0 0 4px #4f46e5;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: white;
|
||||
color: #374151;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
.btn-secondary:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px #fff, 0 0 0 4px #6366f1;
|
||||
}
|
||||
</style>
|
||||
|
|
Loading…
Add table
editor.link_modal.header
Reference in a new issue