Compare commits

..

3 commits

Author SHA1 Message Date
0d259b6464 feat(checkReferenceType): add check reference type feature
All checks were successful
CI / container-job (push) Successful in 39s
Update npm packages and related dependencies
Adapt tailwind.config.js with new utilities and configuration adjustments
Implement categorizeUpdate() method in Submitter/DatasetController.ts for synchronizing dataset collections
Apply style updates in Category.vue for improved drag-and-drop experience and visual cues
Add new route in start/routes.ts for dataset categorization flow
2025-03-17 17:26:29 +01:00
c350e9c373 hotfix: update edit mode of dataset ('ver fogotten to adapt edit mode)
Update DatasetController.ts: use correct moveToDisk method in update method
2025-03-17 12:54:26 +01:00
51a5673a3d hotfix: update @types/leaflet and adjust map styling
Update package.json to bump @types/leaflet
Define leaflet map z-index directly in the main CSS via apps.cc for consistent component use
Scope all SearchMap.vue styles locally
2025-03-17 12:17:47 +01:00
9 changed files with 533 additions and 455 deletions

View file

@ -1047,10 +1047,10 @@ export default class DatasetController {
// name: fileName, // name: fileName,
// overwrite: true, // overwrite in case of conflict // overwrite: true, // overwrite in case of conflict
// }); // });
await fileData.moveToDisk(datasetFullPath, { await fileData.moveToDisk(datasetFullPath, 'local', {
name: fileName, name: fileName,
overwrite: true, // overwrite in case of conflict overwrite: true, // overwrite in case of conflict
driver: 'local', disk: 'local',
}); });
//save to db: //save to db:
@ -1248,4 +1248,51 @@ export default class DatasetController {
relatedCollections: dataset.collections, relatedCollections: dataset.collections,
}); });
} }
public async categorizeUpdate({ request, response, session }: HttpContext) {
// Get the dataset id from the route parameter
const id = request.param('id');
const dataset = await Dataset.query().preload('files').where('id', id).firstOrFail();
const validStates = ['inprogress', 'rejected_editor'];
if (!validStates.includes(dataset.server_state)) {
return response
.flash(
'warning',
`Invalid server state. Dataset with id ${id} cannot be categorized. Dataset has server state ${dataset.server_state}.`,
)
.redirect()
.toRoute('dataset.list');
}
let trx: TransactionClientContract | null = null;
try {
trx = await db.transaction();
// const user = (await User.find(auth.user?.id)) as User;
// await this.createDatasetAndAssociations(user, request, trx);
// Retrieve the selected collections from the request.
// This should be an array of collection ids.
const collections: number[] = request.input('collections', []);
// Synchronize the dataset collections using the transaction.
await dataset.useTransaction(trx).related('collections').sync(collections);
// Commit the transaction.await trx.commit()
await trx.commit();
// Redirect with a success flash message.
// return response.flash('success', 'Dataset collections updated successfully!').redirect().toRoute('dataset.list');
session.flash('message', 'Dataset collections updated successfully!');
return response.redirect().toRoute('dataset.list');
} catch (error) {
if (trx !== null) {
await trx.rollback();
}
console.error('Failed tocatgorize dataset collections:', error);
// throw new ValidationException(true, { 'upload error': `failed to create dataset and related models. ${error}` });
throw error;
}
}
} }

678
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -39,7 +39,7 @@
"@types/clamscan": "^2.0.4", "@types/clamscan": "^2.0.4",
"@types/escape-html": "^1.0.4", "@types/escape-html": "^1.0.4",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"@types/leaflet": "^1.9.3", "@types/leaflet": "^1.9.16",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/node": "^22.10.2", "@types/node": "^22.10.2",
"@types/proxy-addr": "^2.0.0", "@types/proxy-addr": "^2.0.0",

View file

@ -109,6 +109,9 @@
--radius: 15; --radius: 15;
--pi: 3.14159265358979; --pi: 3.14159265358979;
} }
.leaflet-container .leaflet-pane {
z-index: 30!important;
}
/* @layer base { /* @layer base {
html, html,

View file

@ -282,7 +282,7 @@ const handleDrawEventCreated = async (event) => {
</template> </template>
<style lang="css"> <style scoped lang="css">
/* .leaflet-container { /* .leaflet-container {
height: 600px; height: 600px;
width: 100%; width: 100%;

View file

@ -1,7 +1,6 @@
<template> <template>
<LayoutAuthenticated> <LayoutAuthenticated>
<Head title="Collections"></Head>
<Head title="Profile"></Head>
<SectionMain> <SectionMain>
<SectionTitleLineWithButton :icon="mdiLibraryShelves" title="Library Classification" main> <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 class="bg-lime-100 shadow rounded-lg p-6 mb-6 flex items-center justify-between">
@ -51,13 +50,17 @@
<!-- Broader Collection (Parent) --> <!-- Broader Collection (Parent) -->
<CardBox v-if="selectedCollection" class="rounded-lg p-4" has-form-data> <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> <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"> <draggable v-if="broaderCollections.length > 0" v-model="broaderCollections"
<li v-for="parent in broaderCollections" :key="parent.id" :group="{ name: 'collections' }" tag="ul" class="flex flex-wrap gap-2 max-h-60 overflow-y-auto">
class="cursor-pointer p-2 border border-gray-200 rounded bg-green-50 text-green-700 text-sm hover:bg-green-100 hover:underline" <template #item="{ element: parent }">
@click="onCollectionSelected(parent)" title="Click to select this collection"> <li :key="parent.id" :draggable="!parent.inUse" :class="getChildClasses(parent)"
@click="onCollectionSelected(parent)">
{{ `${parent.name} (${parent.number})` }} {{ `${parent.name} (${parent.number})` }}
</li> </li>
<li v-if="broaderCollections.length === 0" class="text-gray-500 text-sm"> </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 broader collections available. No broader collections available.
</li> </li>
</ul> </ul>
@ -66,8 +69,10 @@
<!-- Selected Collection Details --> <!-- Selected Collection Details -->
<CardBox v-if="selectedCollection" class="rounded-lg p-4" has-form-data> <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> <h3 class="text-xl font-bold text-gray-800 dark:text-slate-400 mb-2">Selected Collection</h3>
<p <p :class="[
class="cursor-pointer p-2 border border-gray-200 rounded bg-green-50 text-green-700 text-sm hover:bg-green-100"> 'cursor-pointer p-2 border border-gray-200 rounded text-sm',
selectedCollection.inUse ? 'bg-gray-200 text-gray-500 drag-none' : 'bg-green-50 text-green-700 hover:bg-green-100 hover:underline cursor-move'
]">
{{ `${selectedCollection.name} (${selectedCollection.number})` }} {{ `${selectedCollection.name} (${selectedCollection.number})` }}
</p> </p>
</CardBox> </CardBox>
@ -75,21 +80,10 @@
<!-- Narrower Collections (Children) --> <!-- Narrower Collections (Children) -->
<CardBox v-if="selectedCollection" class="rounded-lg p-4" has-form-data> <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> <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" <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"> :group="{ name: 'collections' }" tag="ul" class="flex flex-wrap gap-2 max-h-60 overflow-y-auto">
<template #item="{ element: child }"> <template #item="{ element: child }">
<li :key="child.id" <li :key="child.id" :draggable="!child.inUse" :class="getChildClasses(child)"
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)"> @click="onCollectionSelected(child)">
{{ `${child.name} (${child.number})` }} {{ `${child.name} (${child.number})` }}
</li> </li>
@ -105,19 +99,21 @@
</div> </div>
<div class="mb-4 rounded-lg"> <div class="mb-4 rounded-lg">
<div v-if="selectedCollection" class="bg-gray-100 shadow rounded-lg p-6 mb-6"> <div v-if="selectedCollection || selectedCollectionList.length > 0" class="bg-gray-100 shadow rounded-lg p-6 mb-6" :class="{ 'opacity-50': selectedCollection && selectedCollectionList.length === 0 }">
<p class="mb-4 text-gray-700">Please drag your collections here to classify your previously created <p class="mb-4 text-gray-700">Please drag your collections here to classify your previously created
dataset dataset
according to library classification standards.</p> according to library classification standards.</p>
<draggable v-model="dropCollections" :group="{ name: 'collections' }" <draggable v-model="selectedCollectionList" :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" 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"> tag="ul"
:disabled="selectedCollection === null && selectedCollectionList.length > 0"
:style="{ opacity: (selectedCollection === null && selectedCollectionList.length > 0) ? 0.5 : 1, pointerEvents: (selectedCollection === null && selectedCollectionList.length > 0) ? 'none' : 'auto' }">
<template #item="{ element }"> <template #item="{ element }">
<div :key="element.id" <div :key="element.id"
class="p-2 m-1 bg-sky-200 text-sky-800 rounded flex items-center gap-2 h-7"> 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> <span>{{ element.name }} ({{ element.number }})</span>
<button <button
@click="dropCollections = dropCollections.filter(item => item.id !== element.id)" @click="selectedCollectionList = selectedCollectionList.filter(item => item.id !== element.id)"
class="hover:text-sky-600 flex items-center"> 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" <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20"
fill="currentColor"> fill="currentColor">
@ -131,7 +127,6 @@
</draggable> </draggable>
</div> </div>
</div> </div>
<div class="p-6 border-t border-gray-100 dark:border-slate-800"> <div class="p-6 border-t border-gray-100 dark:border-slate-800">
<BaseButtons> <BaseButtons>
<BaseButton @click.stop="syncDatasetCollections" label="Save" color="info" small <BaseButton @click.stop="syncDatasetCollections" label="Save" color="info" small
@ -139,17 +134,13 @@
</BaseButton> </BaseButton>
</BaseButtons> </BaseButtons>
</div> </div>
</SectionMain> </SectionMain>
</LayoutAuthenticated> </LayoutAuthenticated>
</template> </template>
<script lang="ts" setup> <script setup lang="ts">
import { ref, Ref, watch, computed } from 'vue'; import { ref, Ref, watch, computed } from 'vue';
import { useForm } from '@inertiajs/vue3';
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue'; import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
import SectionMain from '@/Components/SectionMain.vue'; import SectionMain from '@/Components/SectionMain.vue';
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue'; import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
@ -159,20 +150,19 @@ import draggable from 'vuedraggable';
import BaseButton from '@/Components/BaseButton.vue'; import BaseButton from '@/Components/BaseButton.vue';
import BaseButtons from '@/Components/BaseButtons.vue'; import BaseButtons from '@/Components/BaseButtons.vue';
import CardBox from '@/Components/CardBox.vue'; import CardBox from '@/Components/CardBox.vue';
import { stardust } from '@eidellev/adonis-stardust/client';
import { CollectionRole, Collection } from '@/types/models';
interface CollectionRole { // import CollectionRoleSelector from '@/Components/Collection/CollectionRoleSelector.vue';
id: number; // import ToplevelCollections from '@/Components/Collection/ToplevelCollections.vue';
name: string; // import CollectionHierarchy from '@/Components/Collection/CollectionHierarchy.vue';
collections?: any[]; // import CollectionDropZone from '@/Components/Collection/CollectionDropZone.vue';
}
interface Collection {
id: number;
name: string;
number: string;
parent_id?: number | null;
}
/* --------------------------------------------------------------------------
Props & Reactive State
-------------------------------------------------------------------------- */
const props = defineProps({ const props = defineProps({
collectionRoles: { collectionRoles: {
type: Array, type: Array,
@ -183,7 +173,10 @@ const props = defineProps({
type: Object, type: Object,
default: () => ({}), default: () => ({}),
}, },
relatedCollections: Array<Collection> relatedCollections: {
type: Array as () => Collection[],
default: () => [] as const
}
}); });
const collectionRoles: Ref<CollectionRole[]> = ref(props.collectionRoles as CollectionRole[]); const collectionRoles: Ref<CollectionRole[]> = ref(props.collectionRoles as CollectionRole[]);
@ -193,34 +186,28 @@ const selectedToplevelCollection = ref<Collection | null>(null);
const selectedCollection = ref<Collection | null>(null); const selectedCollection = ref<Collection | null>(null);
const narrowerCollections = ref<Collection[]>([]); const narrowerCollections = ref<Collection[]>([]);
const broaderCollections = ref<Collection[]>([]); const broaderCollections = ref<Collection[]>([]);
// Reactive list that holds collections dropped by the user
const selectedCollectionList: Ref<Collection[]> = ref<Collection[]>([]);
const form = useForm({
collections: [] as number[],
});
// Watch for changes in dropCollections
watch(
() => selectedCollectionList.value,
() => {
if (selectedCollection.value) {
fetchCollections(selectedCollection.value.id);
}
},
{ deep: true }
);
// const onCollectionRoleSelected = (event: Event) => { /* --------------------------------------------------------------------------
// const target = event.target as HTMLSelectElement; Watchers and Initial Setup
// 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: // If the collectionRoles prop might load asynchronously (or change), you can watch for it:
watch( watch(
() => props.collectionRoles as CollectionRole[], () => props.collectionRoles as CollectionRole[],
@ -236,7 +223,8 @@ watch(
}, },
{ immediate: true } { immediate: true }
); );
// Watch for changes in selectedCollectionRole and update related collections state
// When collection role changes, update available collections and clear dependent state.
watch( watch(
() => selectedCollectionRole.value as CollectionRole, () => selectedCollectionRole.value as CollectionRole,
(newSelectedCollectionRole: CollectionRole | null) => { (newSelectedCollectionRole: CollectionRole | null) => {
@ -253,60 +241,77 @@ watch(
broaderCollections.value = [] broaderCollections.value = []
}, },
{ immediate: true } { immediate: true }
)
// Watch for changes in dropCollections
watch(
() => dropCollections.value,
() => {
if (selectedCollection.value) {
fetchCollections(selectedCollection.value.id);
}
},
{ deep: true }
); );
/* --------------------------------------------------------------------------
Methods
-------------------------------------------------------------------------- */
const onToplevelCollectionSelected = (collection: Collection) => { const onToplevelCollectionSelected = (collection: Collection) => {
selectedToplevelCollection.value = collection; selectedToplevelCollection.value = collection;
selectedCollection.value = collection; selectedCollection.value = collection;
// call the API endpoint to get both. // call the API endpoint to get both.
fetchCollections(collection.id) fetchCollections(collection.id);
}; };
const onCollectionSelected = (collection: Collection) => { const onCollectionSelected = (collection: Collection) => {
selectedCollection.value = collection; selectedCollection.value = collection;
// call the API endpoint to get both. // call the API endpoint to get both.
fetchCollections(collection.id) fetchCollections(collection.id);
}; };
// New function to load both narrower and broader concepts using the real API route. /**
* fetchCollections: Retrieves broader and narrower collections.
* Marks any narrower collection as inUse if it appears in selectedCollectionList.
*/
const fetchCollections = async (collectionId: number) => { const fetchCollections = async (collectionId: number) => {
try { try {
const response = await axios.get(`/api/collections/${collectionId}`); const response = await axios.get(`/api/collections/${collectionId}`);
const data = response.data; const data = response.data;
// Map each returned narrower collection
// Set narrower concepts with filtered collections narrowerCollections.value = data.narrowerCollections.map((collection: Collection) => {
narrowerCollections.value = data.narrowerCollections.filter( // If found, mark it as inUse.
collection => !dropCollections.value.some(dc => dc.id === collection.id) const alreadyDropped = selectedCollectionList.value.find(dc => dc.id === collection.id);
); return alreadyDropped ? { ...collection, inUse: true } : { ...collection, inUse: false };
// For broader concepts, if present, wrap it in an array (or change your template accordingly) });
broaderCollections.value = data.broaderCollection; broaderCollections.value = data.broaderCollection.map((collection: Collection) => {
const alreadyDropped = selectedCollectionList.value.find(dc => dc.id === collection.id);
return alreadyDropped ? { ...collection, inUse: true } : { ...collection, inUse: false };
});
} catch (error) { } catch (error) {
console.error('Error in fetchConcepts:', error); console.error('Error in fetchCollections:', error);
} }
}; };
const syncDatasetCollections = async () => { const syncDatasetCollections = async () => {
try {
// Extract the ids from the dropCollections list // Extract the ids from the dropCollections list
const collectionIds = dropCollections.value.map(item => item.id); form.collections = selectedCollectionList.value.map((item: Collection) => item.id);
await axios.post('/api/dataset/collections/sync', { collections: collectionIds }); form.put(stardust.route('dataset.categorizeUpdate', [props.dataset.id]), {
// Optionally show a success message or refresh dataset info preserveState: true,
} catch (error) { onSuccess: () => {
console.error('Error syncing dataset collections:', error); console.log('Dataset collections synced successfully');
} },
onError: (errors) => {
console.error('Error syncing dataset collections:', errors);
},
});
}; };
/**
* getChildClasses returns the Tailwind CSS classes to apply to each collection list item.
*/
const getChildClasses = (child: Collection) => {
return child.inUse
? 'p-2 border border-gray-200 rounded bg-gray-200 text-gray-500 cursor-pointer drag-none'
: 'p-2 border border-gray-200 rounded bg-green-50 text-green-700 cursor-move hover:bg-green-100 hover:underline'
}
// If there are related collections passed in, fill dropCollections with these.
if (props.relatedCollections && props.relatedCollections.length > 0) {
selectedCollectionList.value = props.relatedCollections;
}
// Add a computed property for the disabled state based on dropCollections length
const isSaveDisabled = computed(() => selectedCollectionList.value.length === 0);
</script> </script>
<style scoped> <style scoped>

View file

@ -0,0 +1,16 @@
/* --------------------------------------------------------------------------
Types and Interfaces
-------------------------------------------------------------------------- */
export interface Collection {
id: number;
name: string;
number: string;
parent_id?: number | null;
inUse?: boolean;
}
export interface CollectionRole {
id: number;
name: string;
collections?: Collection[];
}

View file

@ -319,6 +319,11 @@ router
.as('dataset.categorize') .as('dataset.categorize')
.where('id', router.matchers.number()) .where('id', router.matchers.number())
.use([middleware.auth(), middleware.can(['dataset-edit'])]); .use([middleware.auth(), middleware.can(['dataset-edit'])]);
router
.put('/dataset/:id/categorizeUpdate', [DatasetController, 'categorizeUpdate'])
.as('dataset.categorizeUpdate')
.where('id', router.matchers.number())
.use([middleware.auth(), middleware.can(['dataset-edit'])]);
}) })
.prefix('submitter'); .prefix('submitter');

View file

@ -102,6 +102,18 @@ module.exports = {
{ values: theme('asideScrollbars') }, { values: theme('asideScrollbars') },
); );
}), }),
plugin(function({ addUtilities }) {
const newUtilities = {
'.drag-none': {
'-webkit-user-drag': 'none',
'-khtml-user-drag': 'none',
'-moz-user-drag': 'none',
'-o-user-drag': 'none',
'user-drag': 'none',
},
}
addUtilities(newUtilities)
}),
// As of Tailwind CSS v3.3, the `@tailwindcss/line-clamp` plugin is now included by default // As of Tailwind CSS v3.3, the `@tailwindcss/line-clamp` plugin is now included by default
// require('@tailwindcss/line-clamp'), // require('@tailwindcss/line-clamp'),
], ],