feat: Implement project management functionality with CRUD operations and UI integration
Some checks failed
build.yaml / feat: Implement project management functionality with CRUD operations and UI integration (push) Failing after 0s

feat: Implement project management functionality with CRUD operations and UI integration
- added projects_controller.ts for crud operations-
added views Edit-vue , Index.vue and Create.vue
- small adaptions in menu.ts
additional routes is start/routes.ts for projects
This commit is contained in:
Kaimbacher 2025-10-16 15:37:55 +02:00
commit f39fe75340
6 changed files with 551 additions and 1 deletions

View file

@ -0,0 +1,50 @@
// app/controllers/projects_controller.ts
import Project from '#models/project';
import type { HttpContext } from '@adonisjs/core/http';
export default class ProjectsController {
// GET /settings/projects
public async index({ inertia, auth }: HttpContext) {
const projects = await Project.all();
// return inertia.render('Admin/Project/Index', { projects });
return inertia.render('Admin/Project/Index', {
projects: projects,
can: {
edit: await auth.user?.can(['settings']),
create: await auth.user?.can(['settings']),
},
});
}
// GET /settings/projects/create
public async create({ inertia }: HttpContext) {
return inertia.render('Admin/Project/Create');
}
// POST /settings/projects
public async store({ request, response, session }: HttpContext) {
const data = request.only(['label', 'name', 'description']);
await Project.create(data);
session.flash('success', 'Project created successfully');
return response.redirect().toRoute('settings.project.index');
}
// GET /settings/projects/:id/edit
public async edit({ params, inertia }: HttpContext) {
const project = await Project.findOrFail(params.id);
return inertia.render('Admin/Project/Edit', { project });
}
// PUT /settings/projects/:id
public async update({ params, request, response, session }: HttpContext) {
const project = await Project.findOrFail(params.id);
const data = request.only(['label', 'name', 'description']);
await project.merge(data).save();
session.flash('success', 'Project updated successfully');
return response.redirect().toRoute('settings.project.index');
}
}

View file

@ -0,0 +1,144 @@
<script lang="ts" setup>
import { Head, useForm } from '@inertiajs/vue3';
import { mdiFolderPlus, mdiArrowLeftBoldOutline, mdiFormTextarea, mdiContentSave } from '@mdi/js';
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
import SectionMain from '@/Components/SectionMain.vue';
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
import CardBox from '@/Components/CardBox.vue';
import FormField from '@/Components/FormField.vue';
import FormControl from '@/Components/FormControl.vue';
import BaseButton from '@/Components/BaseButton.vue';
import BaseButtons from '@/Components/BaseButtons.vue';
import { stardust } from '@eidellev/adonis-stardust/client';
const form = useForm({
label: '',
name: '',
description: '',
});
const submit = async () => {
await form.post(stardust.route('settings.project.store'));
};
</script>
<template>
<LayoutAuthenticated>
<Head title="Create New Project" />
<SectionMain>
<SectionTitleLineWithButton :icon="mdiFolderPlus" title="Create New Project" main>
<BaseButton
:route-name="stardust.route('settings.project.index')"
:icon="mdiArrowLeftBoldOutline"
label="Back"
color="white"
rounded-full
small
/>
</SectionTitleLineWithButton>
<CardBox form @submit.prevent="submit()" class="shadow-lg">
<div class="grid grid-cols-1 gap-6">
<FormField
label="Label"
help="Required. Displayed project label"
:class="{ 'text-red-400': form.errors.label }"
>
<FormControl
v-model="form.label"
type="text"
placeholder="Enter a descriptive label..."
required
:error="form.errors.label"
class="transition-all focus:ring-2 focus:ring-blue-500"
>
<div class="text-red-400 text-sm mt-1" v-if="form.errors.label">
{{ form.errors.label }}
</div>
</FormControl>
</FormField>
<FormField
label="Name"
help="Required. Project identifier (slug, lowercase, no spaces)"
:class="{ 'text-red-400': form.errors.name }"
>
<FormControl
v-model="form.name"
type="text"
placeholder="e.g., my-awesome-project"
required
:error="form.errors.name"
class="font-mono transition-all focus:ring-2 focus:ring-blue-500"
>
<div class="text-red-400 text-sm mt-1" v-if="form.errors.name">
{{ form.errors.name }}
</div>
</FormControl>
</FormField>
<FormField
label="Description"
help="Optional. Detailed description of the project"
:class="{ 'text-red-400': form.errors.description }"
>
<FormControl
v-model="form.description"
:icon="mdiFormTextarea"
name="description"
type="textarea"
placeholder="Describe what this project is about..."
:error="form.errors.description"
class="transition-all focus:ring-2 focus:ring-blue-500"
>
<div class="text-red-400 text-sm mt-1" v-if="form.errors.description">
{{ form.errors.description }}
</div>
</FormControl>
</FormField>
</div>
<template #footer>
<BaseButtons class="justify-between">
<BaseButton
:route-name="stardust.route('settings.project.index')"
label="Cancel"
color="white"
outline
/>
<BaseButton
type="submit"
color="info"
:icon="mdiContentSave"
label="Create Project"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
class="transition-all hover:shadow-lg"
/>
</BaseButtons>
</template>
</CardBox>
<!-- Helper Card -->
<CardBox class="mt-6 bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-slate-800 dark:to-slate-900 border-l-4 border-blue-500">
<div class="flex items-start gap-4">
<div class="flex-shrink-0">
<div class="w-10 h-10 rounded-full bg-blue-500 dark:bg-blue-600 flex items-center justify-center">
<span class="text-white text-lg">💡</span>
</div>
</div>
<div class="flex-1">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white mb-2">
Quick Tips
</h3>
<ul class="text-sm text-gray-700 dark:text-gray-300 space-y-1">
<li> <strong>Label</strong> is what users will see in the interface</li>
<li> <strong>Name</strong> is a technical identifier (use lowercase and hyphens)</li>
<li> <strong>Description</strong> helps team members understand the project's purpose</li>
</ul>
</div>
</div>
</CardBox>
</SectionMain>
</LayoutAuthenticated>
</template>

View file

@ -0,0 +1,154 @@
<script lang="ts" setup>
import { Head, useForm } from '@inertiajs/vue3';
import { mdiFolderEdit, mdiArrowLeftBoldOutline, mdiFormTextarea, mdiContentSave } from '@mdi/js';
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
import SectionMain from '@/Components/SectionMain.vue';
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
import CardBox from '@/Components/CardBox.vue';
import FormField from '@/Components/FormField.vue';
import FormControl from '@/Components/FormControl.vue';
import BaseButton from '@/Components/BaseButton.vue';
import BaseButtons from '@/Components/BaseButtons.vue';
import { stardust } from '@eidellev/adonis-stardust/client';
const props = defineProps({
project: {
type: Object,
required: true,
},
});
const form = useForm({
label: props.project.label,
name: props.project.name,
description: props.project.description,
});
const submit = async () => {
await form.put(stardust.route('settings.project.update', [props.project.id]));
};
</script>
<template>
<LayoutAuthenticated>
<Head :title="`Edit ${project.label}`" />
<SectionMain>
<SectionTitleLineWithButton :icon="mdiFolderEdit" :title="`Edit: ${project.label}`" main>
<BaseButton
:route-name="stardust.route('settings.project.index')"
:icon="mdiArrowLeftBoldOutline"
label="Back"
color="white"
rounded-full
small
/>
</SectionTitleLineWithButton>
<CardBox form @submit.prevent="submit()" class="shadow-lg">
<div class="grid grid-cols-1 gap-6">
<FormField
label="Label"
help="Project label (read-only)"
>
<FormControl
v-model="form.label"
type="text"
:is-read-only=true
class="bg-gray-100 dark:bg-slate-800 cursor-not-allowed opacity-75"
/>
</FormField>
<FormField
label="Name"
help="Required. Project identifier (slug)"
:class="{ 'text-red-400': form.errors.name }"
>
<FormControl
v-model="form.name"
type="text"
placeholder="Enter Name"
required
:error="form.errors.name"
class="font-mono transition-all focus:ring-2 focus:ring-blue-500"
>
<div class="text-red-400 text-sm mt-1" v-if="form.errors.name">
{{ form.errors.name }}
</div>
</FormControl>
</FormField>
<FormField
label="Description"
help="Optional. Detailed description of the project"
:class="{ 'text-red-400': form.errors.description }"
>
<FormControl
v-model="form.description"
:icon="mdiFormTextarea"
name="description"
type="textarea"
placeholder="Enter project description..."
:error="form.errors.description"
class="transition-all focus:ring-2 focus:ring-blue-500"
>
<div class="text-red-400 text-sm mt-1" v-if="form.errors.description">
{{ form.errors.description }}
</div>
</FormControl>
</FormField>
</div>
<template #footer>
<BaseButtons class="justify-between">
<BaseButton
:route-name="stardust.route('settings.project.index')"
label="Cancel"
color="white"
outline
/>
<BaseButton
type="submit"
color="info"
:icon="mdiContentSave"
label="Save Changes"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
class="transition-all hover:shadow-lg"
/>
</BaseButtons>
</template>
</CardBox>
<!-- Project Info Card -->
<CardBox class="mt-6 bg-gradient-to-br from-gray-50 to-gray-100 dark:from-slate-800 dark:to-slate-900">
<div class="flex items-start gap-4">
<div class="flex-shrink-0">
<div class="w-12 h-12 rounded-lg bg-blue-500 dark:bg-blue-600 flex items-center justify-center">
<span class="text-white text-xl font-bold">
{{ project.label.charAt(0).toUpperCase() }}
</span>
</div>
</div>
<div class="flex-1 min-w-0">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-1">
{{ project.label }}
</h3>
<p class="text-sm font-mono text-gray-600 dark:text-gray-400 mb-2">
{{ project.name }}
</p>
<div class="flex items-center gap-4 text-xs text-gray-500 dark:text-gray-500">
<span>
<span class="font-medium">Created:</span>
{{ new Date(project.created_at).toLocaleDateString() }}
</span>
<span>
<span class="font-medium">Updated:</span>
{{ new Date(project.updated_at).toLocaleDateString() }}
</span>
</div>
</div>
</div>
</CardBox>
</SectionMain>
</LayoutAuthenticated>
</template>

View file

@ -0,0 +1,182 @@
<script setup>
import { Head, Link, useForm, usePage } from '@inertiajs/vue3';
import { mdiFolderMultiple, mdiPlus, mdiSquareEditOutline, mdiTrashCan, mdiAlertBoxOutline } from '@mdi/js';
import { computed, ref } from 'vue';
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
import SectionMain from '@/Components/SectionMain.vue';
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
import BaseButton from '@/Components/BaseButton.vue';
import CardBox from '@/Components/CardBox.vue';
import BaseButtons from '@/Components/BaseButtons.vue';
import NotificationBar from '@/Components/NotificationBar.vue';
import CardBoxModal from '@/Components/CardBoxModal.vue';
import { stardust } from '@eidellev/adonis-stardust/client';
const isModalDangerActive = ref(false);
const deleteId = ref();
const props = defineProps({
projects: {
type: Array,
default: () => [],
},
can: {
type: Object,
default: () => ({}),
},
});
const flash = computed(() => usePage().props.flash);
const projectCount = computed(() => props.projects.length);
const formDelete = useForm({});
const destroy = (id) => {
deleteId.value = id;
isModalDangerActive.value = true;
};
const onConfirm = async (id) => {
await formDelete.delete(stardust.route('settings.project.destroy', [id]));
deleteId.value = null;
};
const onCancel = () => {
deleteId.value = null;
};
const truncate = (text, length = 30) => {
if (!text) return '';
return text.length > length ? text.substring(0, length) + '...' : text;
};
const getProjectColor = (index) => {
const colors = [
'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300',
'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300',
'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300',
'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300',
'bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-300',
'bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-300',
];
return colors[index % colors.length];
};
</script>
<template>
<CardBoxModal
v-model="isModalDangerActive"
:delete-id="deleteId"
large-title="Delete Project"
button="danger"
button-label="Delete"
has-cancel
@confirm="onConfirm"
@cancel="onCancel"
>
<p>Are you sure you want to delete this project?</p>
<p>This action cannot be undone.</p>
</CardBoxModal>
<LayoutAuthenticated>
<Head title="Projects" />
<SectionMain>
<SectionTitleLineWithButton :icon="mdiFolderMultiple" title="Projects" main>
<div class="flex items-center gap-3">
<span class="text-sm text-gray-500 dark:text-gray-400 font-medium">
{{ projectCount }} {{ projectCount === 1 ? 'project' : 'projects' }}
</span>
<BaseButton
v-if="can.create"
:route-name="stardust.route('settings.project.create')"
:icon="mdiPlus"
label="New Project"
color="info"
rounded-full
small
class="shadow-md hover:shadow-lg transition-shadow"
/>
</div>
</SectionTitleLineWithButton>
<NotificationBar v-if="flash.message" color="success" :icon="mdiAlertBoxOutline">
{{ flash.message }}
</NotificationBar>
<CardBox class="mb-6" has-table>
<table>
<thead>
<tr>
<th>Label</th>
<th>Name</th>
<th v-if="can.edit || can.delete">Actions</th>
</tr>
</thead>
<tbody>
<tr v-if="projects.length === 0">
<td colspan="3" class="text-center py-12">
<div class="flex flex-col items-center justify-center text-gray-500 dark:text-gray-400">
<mdiFolderMultiple class="w-16 h-16 mb-4 opacity-50" />
<p class="text-lg font-medium mb-2">No projects yet</p>
<p class="text-sm mb-4">Get started by creating your first project</p>
<BaseButton
v-if="can.create"
:route-name="stardust.route('settings.project.create')"
:icon="mdiPlus"
label="Create Project"
color="info"
small
/>
</div>
</td>
</tr>
<tr v-for="(project, index) in projects" :key="project.id" class="hover:bg-gray-50 dark:hover:bg-slate-800 transition-colors">
<td data-label="Label">
<!-- <Link
:href="stardust.route('settings.project.show', [project.id])"
class="no-underline hover:underline"
> -->
<span
class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium transition-all hover:shadow-md"
:class="getProjectColor(index)"
:title="project.label"
>
{{ truncate(project.label, 25) }}
</span>
<!-- </Link> -->
</td>
<td data-label="Name">
<span
class="text-gray-700 dark:text-gray-300 font-mono text-sm"
:title="project.name"
>
{{ truncate(project.name, 30) }}
</span>
</td>
<td v-if="can.edit || can.delete" class="before:hidden lg:w-1 whitespace-nowrap">
<BaseButtons type="justify-start lg:justify-end" no-wrap>
<BaseButton
v-if="can.edit"
:route-name="stardust.route('settings.project.edit', [project.id])"
color="info"
:icon="mdiSquareEditOutline"
small
/>
<BaseButton
v-if="can.delete"
color="danger"
:icon="mdiTrashCan"
small
@click="destroy(project.id)"
/>
</BaseButtons>
</td>
</tr>
</tbody>
</table>
</CardBox>
</SectionMain>
</LayoutAuthenticated>
</template>

View file

@ -12,7 +12,7 @@ import {
mdiShieldCrownOutline,
mdiLicense,
mdiFileDocument,
mdiLibraryShelves
mdiFolderMultiple,
} from '@mdi/js';
export default [
@ -92,6 +92,12 @@ export default [
label: 'Licenses',
roles: ['administrator'],
},
{
route: 'settings.project.index',
icon: mdiFolderMultiple,
label: 'Projects',
roles: ['administrator'],
},
],
},

View file

@ -34,6 +34,7 @@ import DatasetController from '#app/Controllers/Http/Submitter/DatasetController
import PersonController from '#app/Controllers/Http/Submitter/PersonController';
import EditorDatasetController from '#app/Controllers/Http/Editor/DatasetController';
import ReviewerDatasetController from '#app/Controllers/Http/Reviewer/DatasetController';
import ProjectsController from '#app/controllers/projects_controller';
import './routes/api.js';
import { middleware } from './kernel.js';
import db from '@adonisjs/lucid/services/db'; // Import the DB service
@ -234,6 +235,19 @@ router
.where('id', router.matchers.number())
.use(middleware.can(['settings']));
// Project routes
// List all projects
router.get('/projects', [ProjectsController, 'index']).as('project.index');
// Show create form
router.get('/projects/create', [ProjectsController, 'create']).as('project.create').use(middleware.can(['settings']));;
// Store new project
router.post('/projects', [ProjectsController, 'store']).as('project.store').use(middleware.can(['settings']));;
// Show edit form
router.get('/projects/:id/edit',[ProjectsController, 'edit']).as('project.edit').use(middleware.can(['settings']));;
// Update project
router.put('/projects/:id',[ProjectsController, 'update']).as('project.update').use(middleware.can(['settings']));;
// Mimetype routes
router.get('/mimetype', [MimetypeController, 'index']).as('mimetype.index');
router