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,
// overwrite: true, // overwrite in case of conflict
// });
await fileData.moveToDisk(datasetFullPath, {
await fileData.moveToDisk(datasetFullPath, 'local', {
name: fileName,
overwrite: true, // overwrite in case of conflict
driver: 'local',
disk: 'local',
});
//save to db:
@ -1248,4 +1248,51 @@ export default class DatasetController {
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/escape-html": "^1.0.4",
"@types/fs-extra": "^11.0.4",
"@types/leaflet": "^1.9.3",
"@types/leaflet": "^1.9.16",
"@types/luxon": "^3.4.2",
"@types/node": "^22.10.2",
"@types/proxy-addr": "^2.0.0",

View file

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

View file

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

View file

@ -1,7 +1,6 @@
<template>
<LayoutAuthenticated>
<Head title="Profile"></Head>
<Head title="Collections"></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">
@ -50,14 +49,18 @@
<!-- 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">
<h2 class="text-lg font-bold text-gray-800 dark:text-slate-400 mb-2">Broader Collection</h2>
<draggable v-if="broaderCollections.length > 0" v-model="broaderCollections"
:group="{ name: 'collections' }" tag="ul" class="flex flex-wrap gap-2 max-h-60 overflow-y-auto">
<template #item="{ element: parent }">
<li :key="parent.id" :draggable="!parent.inUse" :class="getChildClasses(parent)"
@click="onCollectionSelected(parent)">
{{ `${parent.name} (${parent.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 broader collections available.
</li>
</ul>
@ -66,30 +69,21 @@
<!-- 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">
<p :class="[
'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})` }}
</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> -->
<h2 class="text-lg font-bold text-gray-800 dark:text-slate-400 mb-2">Narrower Collections</h2>
<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"
<li :key="child.id" :draggable="!child.inUse" :class="getChildClasses(child)"
@click="onCollectionSelected(child)">
{{ `${child.name} (${child.number})` }}
</li>
@ -105,19 +99,21 @@
</div>
<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
dataset
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"
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 }">
<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)"
@click="selectedCollectionList = selectedCollectionList.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">
@ -128,10 +124,9 @@
</button>
</div>
</template>
</draggable>
</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
@ -139,17 +134,13 @@
</BaseButton>
</BaseButtons>
</div>
</SectionMain>
</LayoutAuthenticated>
</template>
<script lang="ts" setup>
<script setup lang="ts">
import { ref, Ref, watch, computed } from 'vue';
import { useForm } from '@inertiajs/vue3';
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
import SectionMain from '@/Components/SectionMain.vue';
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
@ -159,20 +150,19 @@ import draggable from 'vuedraggable';
import BaseButton from '@/Components/BaseButton.vue';
import BaseButtons from '@/Components/BaseButtons.vue';
import CardBox from '@/Components/CardBox.vue';
import { stardust } from '@eidellev/adonis-stardust/client';
import { CollectionRole, Collection } from '@/types/models';
interface CollectionRole {
id: number;
name: string;
collections?: any[];
}
// import CollectionRoleSelector from '@/Components/Collection/CollectionRoleSelector.vue';
// import ToplevelCollections from '@/Components/Collection/ToplevelCollections.vue';
// import CollectionHierarchy from '@/Components/Collection/CollectionHierarchy.vue';
// 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({
collectionRoles: {
type: Array,
@ -183,7 +173,10 @@ const props = defineProps({
type: Object,
default: () => ({}),
},
relatedCollections: Array<Collection>
relatedCollections: {
type: Array as () => Collection[],
default: () => [] as const
}
});
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 narrowerCollections = 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;
// 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);
/* --------------------------------------------------------------------------
Watchers and Initial Setup
-------------------------------------------------------------------------- */
// If the collectionRoles prop might load asynchronously (or change), you can watch for it:
watch(
() => props.collectionRoles as CollectionRole[],
@ -236,7 +223,8 @@ watch(
},
{ immediate: true }
);
// Watch for changes in selectedCollectionRole and update related collections state
// When collection role changes, update available collections and clear dependent state.
watch(
() => selectedCollectionRole.value as CollectionRole,
(newSelectedCollectionRole: CollectionRole | null) => {
@ -253,60 +241,77 @@ watch(
broaderCollections.value = []
},
{ immediate: true }
)
// Watch for changes in dropCollections
watch(
() => dropCollections.value,
() => {
if (selectedCollection.value) {
fetchCollections(selectedCollection.value.id);
}
},
{ deep: true }
);
/* --------------------------------------------------------------------------
Methods
-------------------------------------------------------------------------- */
const onToplevelCollectionSelected = (collection: Collection) => {
selectedToplevelCollection.value = collection;
selectedCollection.value = collection;
// call the API endpoint to get both.
fetchCollections(collection.id)
fetchCollections(collection.id);
};
const onCollectionSelected = (collection: Collection) => {
selectedCollection.value = collection;
// 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) => {
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;
// Map each returned narrower collection
narrowerCollections.value = data.narrowerCollections.map((collection: Collection) => {
// If found, mark it as inUse.
const alreadyDropped = selectedCollectionList.value.find(dc => dc.id === collection.id);
return alreadyDropped ? { ...collection, inUse: true } : { ...collection, inUse: false };
});
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) {
console.error('Error in fetchConcepts:', error);
console.error('Error in fetchCollections:', 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);
}
// Extract the ids from the dropCollections list
form.collections = selectedCollectionList.value.map((item: Collection) => item.id);
form.put(stardust.route('dataset.categorizeUpdate', [props.dataset.id]), {
preserveState: true,
onSuccess: () => {
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>
<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')
.where('id', router.matchers.number())
.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');

View file

@ -102,7 +102,19 @@ module.exports = {
{ 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
// require('@tailwindcss/line-clamp'),
],
};
};