feat(checkReferenceType): add check reference type feature
All checks were successful
CI / container-job (push) Successful in 39s
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
This commit is contained in:
parent
c350e9c373
commit
0d259b6464
6 changed files with 525 additions and 450 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
676
package-lock.json
generated
676
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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>
|
||||
|
|
16
resources/js/types/models.ts
Normal file
16
resources/js/types/models.ts
Normal 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[];
|
||||
}
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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'),
|
||||
],
|
||||
};
|
||||
};
|
Loading…
Add table
Reference in a new issue