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.
343 lines
15 KiB
Vue
343 lines
15 KiB
Vue
<template>
|
|
<LayoutAuthenticated>
|
|
|
|
<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>
|
|
|
|
<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="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>
|
|
|
|
|
|
|
|
|
|
|
|
</SectionMain>
|
|
</LayoutAuthenticated>
|
|
</template>
|
|
|
|
<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';
|
|
|
|
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: () => []
|
|
},
|
|
dataset: {
|
|
type: Object,
|
|
default: () => ({}),
|
|
},
|
|
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>
|
|
.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>
|