feat: ✨ add gitlab projects crud

This commit is contained in:
Alexis Gousseau 2026-05-19 12:44:17 +02:00
parent e77b8f405e
commit 833594afdf
No known key found for this signature in database
GPG key ID: 7F3C786EE12DBB72
20 changed files with 1391 additions and 3 deletions

View file

@ -0,0 +1,245 @@
<?php
namespace App\Http\Controllers\Back;
use App\Http\Requests\Back\GitlabProjects\StoreGitlabProjectRequest;
use App\Http\Requests\Back\GitlabProjects\UpdateGitlabProjectRequest;
use App\Models\GitlabProject;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Uwil\LaravelBackend\Controllers\BackendController;
use Uwil\LaravelBackend\Resources\CommonResource;
/**
* Handles CRUD operations for GitlabProject in the backend.
*/
class GitlabProjectController extends BackendController
{
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(GitlabProject::class);
$this->modelName = GitlabProject::modelName();
$this->modelPage = GitlabProject::modelPage();
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Inertia\Response
*/
public function index(Request $request): \Inertia\Response
{
/** @var \Illuminate\Database\Eloquent\Builder<\App\Models\GitlabProject> $query */
$query = GitlabProject::query();
if ($request->routeIs("back.$this->modelName.trash")) {
$query = $query->onlyTrashed();
}
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$query,
$search,
null,
'name',
);
}
$searchFields = \implode(', ', [
trans('validation.attributes.name'),
]);
/** Sort columns with a query */
$this->sortQuery($query);
/** Custom pagination */
$models = $this->paginate($query);
return $this->inertiaRender("back/Pages/$this->modelPage/Index", [
'models' => CommonResource::collection($models),
'search' => $search,
'searchFields' => $searchFields,
]);
}
/**
* Show the form for creating a new resource.
*
* @param \App\Models\GitlabProject $gitlab_project
* @return \Inertia\Response
*/
public function create(GitlabProject $gitlab_project): \Inertia\Response
{
return $this->inertiaRender("back/Pages/$this->modelPage/Create", [
'data' => CommonResource::make($gitlab_project),
]);
}
/**
* Show a specific resource.
*
* @param \App\Models\GitlabProject $gitlab_project
* @return \Inertia\Response
*/
public function show(GitlabProject $gitlab_project): \Inertia\Response
{
return $this->inertiaRender("back/Pages/$this->modelPage/Show", [
'data' => CommonResource::make($gitlab_project),
]);
}
/**
* Store a newly created resource.
*
* @param \App\Http\Requests\Back\GitlabProjects\StoreGitlabProjectRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreGitlabProjectRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$gitlab_project = new GitlabProject();
$gitlab_project->fill($request->validated());
if ($gitlab_project->saveOrFail()) {
return redirect()->route("back.$this->modelName.edit", $gitlab_project)
->with('success', trans('uwil-backend::crud.messages.has_been_created', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
return redirect()->back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_created', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\GitlabProject $gitlab_project
* @return \Inertia\Response
*/
public function edit(GitlabProject $gitlab_project): \Inertia\Response
{
return $this->inertiaRender("back/Pages/$this->modelPage/Edit", [
'data' => CommonResource::make($gitlab_project),
]);
}
/**
* Update the specified resource.
*
* @param \App\Http\Requests\Back\GitlabProjects\UpdateGitlabProjectRequest $request
* @param \App\Models\GitlabProject $gitlab_project
* @return \Illuminate\Http\RedirectResponse
*/
public function update(
UpdateGitlabProjectRequest $request,
GitlabProject $gitlab_project
): \Illuminate\Http\RedirectResponse {
return DB::transaction(function () use ($request, $gitlab_project) {
$gitlab_project->fill($request->validated());
if ($gitlab_project->saveOrFail()) {
return redirect()->route("back.$this->modelName.edit", $gitlab_project)
->with('success', trans('uwil-backend::crud.messages.has_been_updated', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
return redirect()->route("back.$this->modelName.edit", $gitlab_project)
->with('error', trans('uwil-backend::crud.messages.cannot_be_updated', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
});
}
/**
* Duplicate the specified resource.
*
* @param \App\Models\GitlabProject $gitlab_project
* @return \Inertia\Response
*/
public function duplicate(GitlabProject $gitlab_project): \Inertia\Response
{
return $this->create($gitlab_project->replicate());
}
/**
* Restore the specified resource.
*
* @param integer $gitlab_project
* @return \Illuminate\Http\RedirectResponse
*/
public function restore(int $gitlab_project): \Illuminate\Http\RedirectResponse
{
$gitlab_project = GitlabProject::withTrashed()->findOrFail($gitlab_project);
/** @var array<string,string> */
$previousQueries = [];
\parse_str(\parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries);
if ($gitlab_project->restore()) {
return redirect()->route("back.$this->modelName.trash", $previousQueries)
->with('success', trans('uwil-backend::crud.messages.has_been_restored', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
return redirect()->back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_restored', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
/**
* Destroy the specified resource.
*
* @param \App\Models\GitlabProject $gitlab_project
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(GitlabProject $gitlab_project): \Illuminate\Http\RedirectResponse
{
/** @var array<string,string> */
$previousQueries = [];
\parse_str(\parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries);
if ($gitlab_project->delete()) {
return redirect()->route("back.$this->modelName.index", $previousQueries)
->with('success', trans('uwil-backend::crud.messages.has_been_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
return redirect()->back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
/**
* Destroy the specified resource.
*
* @param \App\Models\GitlabProject|integer $gitlab_project
* @return \Illuminate\Http\RedirectResponse
*/
public function forceDestroy(GitlabProject|int $gitlab_project): \Illuminate\Http\RedirectResponse
{
if (is_int($gitlab_project)) {
$gitlab_project = GitlabProject::withTrashed()->findOrFail($gitlab_project);
}
/** @var array<string,string> */
$previousQueries = [];
\parse_str(\parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries);
if ($gitlab_project->forceDelete()) {
return redirect()->route("back.$this->modelName.trash", $previousQueries)
->with('success', trans('uwil-backend::crud.messages.has_been_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
return redirect()->back()
->with('error', trans('uwil-backend::crud.messages.cannot_be_deleted', [
'model' => Str::ucfirst(trans_choice("models.classes.$this->modelName", 1)),
]));
}
}

View file

@ -3,6 +3,7 @@
namespace App\Http\Middlewares;
use App\Enums\NoticeTypeEnum;
use App\Models\GitlabProject;
use App\Models\InvoiceCompany;
use App\Models\InvoicePackage;
use App\Models\Notice;
@ -26,12 +27,12 @@ class HandleInertiaRequests extends Middleware
...InertiaHelper::errorProps(parent::share($request)),
'auth' => [
'policies' => [
'notices' => [
'notices' => [
'viewAny' => Gate::allows('viewAny', Notice::class),
'viewTrash' => Gate::allows('viewTrash', Notice::class),
'create' => Gate::allows('create', Notice::class),
],
'invoicePackages' => [
'invoicePackages' => [
'viewAny' => Gate::allows('viewAny', InvoicePackage::class),
'viewTrash' => Gate::allows('viewTrash', InvoicePackage::class),
'create' => Gate::allows('create', InvoicePackage::class),
@ -41,6 +42,11 @@ class HandleInertiaRequests extends Middleware
'viewTrash' => Gate::allows('viewTrash', InvoiceCompany::class),
'create' => Gate::allows('create', InvoiceCompany::class),
],
'gitlabProjects' => [
'viewAny' => Gate::allows('viewAny', GitlabProject::class),
'viewTrash' => Gate::allows('viewTrash', GitlabProject::class),
'create' => Gate::allows('create', GitlabProject::class),
],
],
],
'noticeTypeEnum' => NoticeTypeEnum::toArray(),

View file

@ -0,0 +1,51 @@
<?php
namespace App\Http\Requests\Back\GitlabProjects;
use App\Models\GitlabProject;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
/**
* Handles authorization and validation for creating a new GitlabProject.
*/
class StoreGitlabProjectRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return Gate::check('create', GitlabProject::class);
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'name' => 'required|string|between:3,255',
'url' => 'required|string|url:http,https|unique:gitlab_projects,url',
'gitlab_id' => 'required|integer|unique:gitlab_projects,gitlab_id',
];
}
/**
* Get custom attributes for validator errors.
*
* @return array<string, string>
*/
public function attributes(): array
{
return [
'name' => trans('validation.custom.name'),
'url' => trans('validation.custom.url'),
'gitlab_id' => trans('validation.custom.gitlab_id'),
];
}
}

View file

@ -0,0 +1,48 @@
<?php
namespace App\Http\Requests\Back\GitlabProjects;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\Rule;
/**
* Handles authorization and validation for updating an existing GitlabProject.
*/
class UpdateGitlabProjectRequest extends StoreGitlabProjectRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
#[\Override]
public function authorize(): bool
{
return Gate::check('update', $this->route('gitlab_project'));
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
#[\Override]
public function rules(): array
{
/** @var \App\Models\GitlabProject $gitlab_project */
$gitlab_project = $this->route('gitlab_project');
return array_merge(parent::rules(), [
'url' => [
'required',
'string',
'url:http,https',
Rule::unique('gitlab_projects', 'url')->ignore($gitlab_project->getRouteKey()),
],
'gitlab_id' => [
'required',
'integer',
Rule::unique('gitlab_projects', 'gitlab_id')->ignore($gitlab_project->getRouteKey()),
],
]);
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Uwil\LaravelActivityLogs\Traits\HasActivityLog;
use Uwil\LaravelBackend\Traits\Models\HasCache;
use Uwil\LaravelBackend\Traits\Models\HasModelNaming;
use Uwil\LaravelI18n\Traits\Models\HasTranslations;
/**
* @property integer $id Primary key of the gitlab project relation.
* @property string $name Name of the gitlab project.
* @property string $url Url of the gitlab project.
* @property \App\Models\User $gitlab_id Gitlab id of the gitlab project.
* @property-read \Illuminate\Support\Carbon $created_at Date the gitlab project was created.
* @property-read \Illuminate\Support\Carbon $updated_at Date the gitlab project was updated.
* @property-read \Illuminate\Support\Carbon $deleted_at Date the gitlab project was deleted.
*/
class GitlabProject extends Model
{
use HasActivityLog;
use HasCache;
/** @use \Illuminate\Database\Eloquent\Factories\HasFactory<\Database\Factories\GitlabProjectFactory> */
use HasFactory;
use HasModelNaming;
use HasTranslations;
use SoftDeletes;
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'name',
'url',
'gitlab_id',
];
/**
* The translatable columns.
*
* @var list<string>
*/
public $translatable = [
'name',
];
}

View file

@ -0,0 +1,112 @@
<?php
namespace App\Policies;
use Illuminate\Database\Eloquent\Model;
use Uwil\LaravelBackend\Policies\Rules\UserStaticRules;
use Uwil\LaravelBackend\Support\UserRoleRegistry;
/**
* Handles authorization for GitlabProject model actions.
*/
class GitlabProjectPolicy
{
/**
* Determine whether the user can view any models.
*
* @param \Illuminate\Database\Eloquent\Model $authModel
* @return boolean
*/
public function viewAny(Model $authModel): bool
{
return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('conceptor')->value);
}
/**
* Determine whether the user can view trash models.
*
* @param \Illuminate\Database\Eloquent\Model $authModel
* @return boolean
*/
public function viewTrash(Model $authModel): bool
{
return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('conceptor')->value);
}
/**
* Determine whether the user can view the model.
*
* @param \Illuminate\Database\Eloquent\Model $authModel
* @return boolean
*/
public function view(Model $authModel): bool
{
return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('conceptor')->value);
}
/**
* Determine whether the user can create models.
*
* @param \Illuminate\Database\Eloquent\Model $authModel
* @return boolean
*/
public function create(Model $authModel): bool
{
return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('conceptor')->value);
}
/**
* Determine whether the user can update the model.
*
* @param \Illuminate\Database\Eloquent\Model $authModel
* @return boolean
*/
public function update(Model $authModel): bool
{
return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('conceptor')->value);
}
/**
* Determine whether the user can duplicate the model.
*
* @param \Illuminate\Database\Eloquent\Model $authModel
* @return boolean
*/
public function duplicate(Model $authModel): bool
{
return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('conceptor')->value);
}
/**
* Determine whether the user can restore the model.
*
* @param \Illuminate\Database\Eloquent\Model $authModel
* @return boolean
*/
public function restore(Model $authModel): bool
{
return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('conceptor')->value);
}
/**
* Determine whether the user can soft delete the model.
*
* @param \Illuminate\Database\Eloquent\Model $authModel
* @return boolean
*/
public function delete(Model $authModel): bool
{
return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('conceptor')->value);
}
/**
* Determine whether the user can permanently delete the model.
*
* @param \Illuminate\Database\Eloquent\Model $authModel
* @return boolean
*/
public function forceDelete(Model $authModel): bool
{
return UserStaticRules::atLeastRole($authModel, UserRoleRegistry::get('conceptor')->value);
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace Database\Factories;
use App\Models\GitlabProject;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* Factory for generating GitlabProject model instances with randomized data.
*/
class GitlabProjectFactory extends Factory
{
/**
* Associated model.
*
* @var class-string<\App\Models\GitlabProject>
*/
protected $model = GitlabProject::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
"name" => fake()->unique()->words(3, true),
"url" => fake()->unique()->url(),
"gitlab_id" => fake()->unique()->numberBetween(1, 1000),
];
}
}

View file

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::create('gitlab_projects', function (Blueprint $table) {
$table->id()->comment('Primary key of the gitlab project relation.');
$table->string('name')->comment('Name of the gitlab project.');
$table->string('url')->unique()->comment('Url of the gitlab project.');
$table->unsignedBigInteger('gitlab_id')->unique()->comment('Gitlab id of the gitlab project.');
$table->timestamp('created_at')->nullable()->comment('Date the gitlab project was created.');
$table->timestamp('updated_at')->nullable()->comment('Date the gitlab project was updated.');
$table->timestamp('deleted_at')->nullable()->comment('Date the gitlab project was deleted.');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::dropIfExists('gitlab_projects');
}
};

View file

@ -0,0 +1,25 @@
<?php
namespace Database\Seeders\Classic;
use App\Models\GitlabProject;
use Illuminate\Database\Seeder;
/**
* Seeds the gitlab_projects table.
*/
class GitlabProjectSeeder extends Seeder
{
/** @var string $table Associated table to this seeder. */
public static string $table = 'gitlab_projects';
/**
* Run the database seeds.
*
* @return void
*/
public function run(): void
{
GitlabProject::factory(10)->create();
}
}

View file

@ -7,5 +7,6 @@ return [
"notices" => "information|informations",
"invoice-packages" => "devis - forfait|devis - forfaits",
"invoice-companies" => "devis - société|devis - sociétés",
"gitlab-projects" => "Gitlab - projet|Gitlab - projets",
],
];

View file

@ -0,0 +1,75 @@
// * Packages
import { FolderGitIcon } from "lucide-react";
import { route } from "ziggy-js";
import { Link } from "@inertiajs/react";
// * Components shadcn
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
// * Other
import {
filterActions,
NavListLinksProps,
} from "@/back/Components/Layout/Sidebar/AppSidebar";
import { SidebarGroupLink } from "@/back/Components/Layout/Sidebar/SidebarRegistry";
import { AuthProp } from "@/back/types/global";
import str from "@/hooks/use-string";
import { GetTranslation } from "@/lib/utils";
import useTrans from "@/hooks/use-translations";
import Views from "@/back/lib/actions";
export default function SidebarLinkGitlabProjects(
auth: AuthProp,
): SidebarGroupLink {
const modelName = "gitlab-projects";
// Translations.
const transModel = GetTranslation(`models.classes.${modelName}`, {
choice: 2,
});
const transCreate = str(
useTrans(`uwil-backend::crud.actions.${Views.CREATION.label}`),
)
.ucFirst()
.value();
const transArchives = str(useTrans("uwil-backend::back.archives"))
.ucFirst()
.value();
const link: NavListLinksProps = {
title: str(transModel).ucFirst().value(),
model: modelName,
icon: FolderGitIcon,
show: auth.policies.gitlabProjects.viewAny,
actions: (model: string) =>
filterActions({
auth,
model,
items: [
auth.policies.gitlabProjects.create && (
<DropdownMenuItem key="create" className="p-0">
<Button variant="ghost" asChild>
<Link href={route(`back.${model}.create`)} className="w-full">
{/* eslint-disable-next-line react/jsx-pascal-case */}
<Views.CREATION.icon />
{transCreate}
</Link>
</Button>
</DropdownMenuItem>
),
auth.policies.gitlabProjects.viewTrash && (
<DropdownMenuItem className="p-0">
<Button variant="ghost" asChild>
<Link href={route(`back.${model}.trash`)} className="w-full">
{/* eslint-disable-next-line react/jsx-pascal-case */}
<Views.ARCHIVATION.icon />
{transArchives}
</Link>
</Button>
</DropdownMenuItem>
),
],
}),
};
return { link, group: "modules" };
}

View file

@ -0,0 +1,64 @@
// * Packages
import { usePage } from "@inertiajs/react";
import { route } from "ziggy-js";
import { ArrowLeftIcon } from "lucide-react";
// * Other
import AuthenticatedLayout from "@/back/Layouts/AuthenticatedLayout";
import Form from "@/back/Pages/GitlabProjects/Form";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import useTrans from "@/hooks/use-translations";
import { Actions, Methods } from "@/back/lib/enums";
import { AuthProp, DefaultModel, SingleResource } from "@/back/types/global";
export default function Create({
data,
modelName,
}: {
data: SingleResource;
modelName: string;
}) {
const { viewAny } = (usePage().props.auth as AuthProp).policies
.gitlabProjects;
const model = data.data as DefaultModel;
// Translations.
const transBackToList = useTrans("uwil-backend::back.crud_back_to_list");
const transTitle = useTrans(
route().current(`back.${modelName}.duplicate`)
? "uwil-backend::crud.helpers.duplication_model"
: "uwil-backend::crud.helpers.creation_model",
{
params: {
model: useTrans(`models.classes.${modelName}`, {
choice: 1,
}),
},
},
);
const beforeNode = () => {
return (
viewAny && (
<div className="flex justify-end items-center gap-x-1.5">
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.index`)}
content={<ArrowLeftIcon />}
tooltipContent={<p>{transBackToList}</p>}
/>
</div>
)
);
};
return (
<AuthenticatedLayout title={transTitle} beforeNode={beforeNode()}>
<Form
model={model}
modelName={modelName}
modelRoute={modelName}
action={Actions.STORE}
method={Methods.POST}
/>
</AuthenticatedLayout>
);
}

View file

@ -0,0 +1,72 @@
// * Packages
import { usePage } from "@inertiajs/react";
import { route } from "ziggy-js";
import { ArrowLeftIcon } from "lucide-react";
// * Other
import AuthenticatedLayout from "@/back/Layouts/AuthenticatedLayout";
import ModelActions from "@/back/Components/ModelActions";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import Form from "@/back/Pages/GitlabProjects/Form";
import useTrans from "@/hooks/use-translations";
import { Actions, Methods } from "@/back/lib/enums";
import { Views } from "@/back/lib/actions";
import { AuthProp, DefaultModel, SingleResource } from "@/back/types/global";
export default function Edit({
data,
modelName,
}: {
data: SingleResource;
modelName: string;
}) {
const { viewAny } = (usePage().props.auth as AuthProp).policies
.gitlabProjects;
const model = data.data as DefaultModel;
const actionModels = [
Views.VISUALIZATION,
Views.DUPLICATION,
Views.ARCHIVATION,
];
// Translations.
const transBackToList = useTrans("uwil-backend::back.crud_back_to_list");
const transTitle = useTrans("uwil-backend::crud.helpers.edition_model", {
params: {
model: useTrans(`models.classes.${modelName}`, {
choice: 1,
}),
},
});
const beforeNode = () => {
return (
viewAny && (
<div className="flex justify-end items-center gap-x-1.5">
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.index`)}
content={<ArrowLeftIcon />}
tooltipContent={<p>{transBackToList}</p>}
/>
</div>
)
);
};
return (
<AuthenticatedLayout title={transTitle} beforeNode={beforeNode()}>
<ModelActions
actions={actionModels}
modelRoute={modelName}
model={model}
modelUpdatedAt={model.updated_at.toString()}
/>
<Form
model={model}
modelName={modelName}
modelRoute={modelName}
action={Actions.UPDATE}
method={Methods.PATCH}
/>
</AuthenticatedLayout>
);
}

View file

@ -0,0 +1,137 @@
// * Packages
import { useForm, usePage } from "@inertiajs/react";
import { BookTextIcon, ExternalLinkIcon } from "lucide-react";
// * Components shadcn
//
// * Other
import FieldsetWrapper from "@/back/Components/FieldsetWrapper";
import useTrans from "@/hooks/use-translations";
import InputError from "@/back/Components/InputError";
import { GitlabProjectModel } from "@/back/types/project";
import SubmitButton from "@/back/Components/SubmitButton";
import useCrudSubmit from "@/back/hooks/use-crud-submit";
import { AppEnv } from "@/types/global";
import SimpleString from "@/back/Components/Inputs/SimpleString";
import str from "@/hooks/use-string";
import { GetTranslation, isAValidUrl } from "@/lib/utils";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import SimpleNumber from "@/back/Components/Inputs/SimpleNumber";
export default function Form({
model,
modelName,
modelRoute,
action,
method,
}: {
model?: GitlabProjectModel;
modelName: string;
modelRoute: string;
action: string;
method: number;
}) {
const { appEnv } = usePage().props as unknown as {
appEnv: AppEnv;
};
/** Form section */
const { post, setData, data } = useForm({
name: model?.name ? model?.name[appEnv.locale] : undefined,
url: model?.url ?? undefined,
gitlab_id: model?.gitlab_id ?? undefined,
});
const submit = useCrudSubmit({
method,
modelRoute,
modelName,
action,
model,
post,
data,
modelNameTrans: useTrans(`models.classes.${modelName}`, {
choice: 1,
}),
});
return (
<form id="config-form" onSubmit={submit}>
<InputError all />
<FieldsetWrapper
title={useTrans("uwil-backend::back.form_title_global")}
icon={<BookTextIcon className="mr-1" />}
description={useTrans("uwil-backend::back.form_subtitle_global")}
className="sm:grid-cols-2 gap-4"
>
<SimpleString
name="name"
label={str(
useTrans("uwil-backend::back.form_associate_to", {
params: {
attribute: useTrans("validation.custom.name"),
},
choice: 1,
}),
)
.ucFirst()
.value()}
placeholder="ex: John Doe"
value={data.name}
setData={setData}
/>
<SimpleNumber
name="gitlab_id"
label={str(
useTrans("uwil-backend::back.form_associate_to", {
params: {
attribute: useTrans("validation.custom.gitlab_id"),
},
choice: 1,
}),
)
.ucFirst()
.value()}
placeholder="ex: 100"
value={data.gitlab_id}
setData={setData}
/>
<div className="flex justify-center items-end gap-1.5">
<SimpleString
name="url"
label={str(
useTrans("uwil-backend::back.form_associate_to", {
params: {
attribute: useTrans("validation.custom.url"),
},
choice: 2,
}),
)
.ucFirst()
.value()}
type="url"
placeholder="ex: https://example.com/"
value={data.url}
setData={setData}
/>
{isAValidUrl(data.url) && (
<BtnOptionnalLinkWithTooltip
content={<ExternalLinkIcon />}
tooltipContent={GetTranslation("back.access_url")}
link={data.url}
externalLink
type="button"
size="icon"
/>
)}
</div>
</FieldsetWrapper>
<div className="text-center">
<SubmitButton
modelTrans={useTrans(`models.classes.${modelName}`, {
choice: 1,
})}
/>
</div>
</form>
);
}

View file

@ -0,0 +1,192 @@
// * Packages
import { route } from "ziggy-js";
import { usePage } from "@inertiajs/react";
import { Row } from "@tanstack/react-table";
import { ArchiveIcon, ArrowLeftIcon, PlusIcon } from "lucide-react";
// * Components shadcn
import { Badge } from "@/components/ui/badge";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
// * Other
import AuthenticatedLayout from "@/back/Layouts/AuthenticatedLayout";
import DataTable from "@/back/Components/Table/DataTable";
import DataTableSearch from "@/back/Components/Table/DataTableSearch";
import DataTableFilterButton from "@/back/Components/Table/DataTableFilterButton";
import SoftdeleteWarning from "@/back/Components/SoftdeleteWarning";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import { GetFormatedDate, GetTranslation } from "@/lib/utils";
import { Views } from "@/back/lib/actions";
import { AuthProp, Resource } from "@/back/types/global";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
import RenderTranslationCell from "@/back/Components/Table/RenderTranslationCell";
import { GitlabProjectModel } from "@/back/types/project";
const getColumns = () => {
return [
{
accessorKey: "name",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(GetTranslation(`validation.custom.${column.id}`))
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({ row }: { row: Row<GitlabProjectModel> }) => (
<RenderTranslationCell value={row.getValue("name")} />
),
},
{
accessorKey: "url",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(GetTranslation(`validation.custom.${column.id}`))
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({ row }: { row: Row<GitlabProjectModel> }) => (
<Tooltip>
<TooltipTrigger asChild>
<a
href={row.getValue("url")}
target="_blank"
rel="noreferrer"
className="text-primary underline underline-offset-4 hover:opacity-80 transition-opacity rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
{row.getValue("url")}
</a>
</TooltipTrigger>
<TooltipContent>{GetTranslation("back.access_url")}</TooltipContent>
</Tooltip>
),
},
{
accessorKey: "updated_at",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(
GetTranslation(`uwil-backend::validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({ row }: { row: Row<GitlabProjectModel> }) => (
<Badge variant="secondary">
{GetFormatedDate(row.getValue("updated_at"))}
</Badge>
),
},
];
};
export default function Index({
models,
searchFields,
modelName,
}: {
models: Resource;
searchFields: string;
modelName: string;
}) {
const { auth } = usePage().props as unknown as {
auth: AuthProp;
};
const { viewAny, viewTrash, create } = auth.policies.gitlabProjects;
const isArchiveRoute = route().current(`back.${modelName}.trash`);
const actionModels = isArchiveRoute
? [Views.RESTORATION, Views.FORCEDELETION]
: [
Views.VISUALIZATION,
Views.EDITION,
Views.DUPLICATION,
Views.ARCHIVATION,
];
const columns = getColumns();
// Translations.
const transModel = useTrans(`models.classes.${modelName}`, { choice: 2 });
const transArchiveTitle = useTrans(
"uwil-backend::crud.helpers.archives_model",
{
params: { model: transModel },
},
);
const transTitle = str(isArchiveRoute ? transArchiveTitle : transModel)
.ucFirst()
.value();
const transCrudBackList = useTrans("uwil-backend::back.crud_back_to_list");
const beforeNode = () => {
return (
isArchiveRoute &&
viewAny && (
<div className="flex justify-end items-center gap-x-1.5">
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.index`)}
content={<ArrowLeftIcon />}
tooltipContent={<p>{transCrudBackList}</p>}
/>
</div>
)
);
};
const transCrudAddEntry = useTrans("uwil-backend::back.crud_add_entry");
const transAccessTo = useTrans("uwil-backend::back.crud_access_to", {
params: {
target: useTrans("uwil-backend::back.archived_entries"),
},
});
const afterNode = () => {
return (
!isArchiveRoute && (
<div className="flex justify-end items-center gap-x-1.5">
{viewTrash && (
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.trash`)}
content={<ArchiveIcon />}
tooltipContent={<p>{transAccessTo}</p>}
variant="outline"
/>
)}
{create && (
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.create`)}
content={<PlusIcon />}
tooltipContent={<p>{transCrudAddEntry}</p>}
/>
)}
</div>
)
);
};
return (
<AuthenticatedLayout
title={transTitle}
beforeNode={beforeNode()}
afterNode={afterNode()}
>
{isArchiveRoute && <SoftdeleteWarning />}
<DataTableSearch
modelRoute={modelName}
searchFields={searchFields}
isArchiveRoute={isArchiveRoute}
/>
<DataTable
columns={columns}
modelRoute={modelName}
modelsPaginate={models}
actions={actionModels}
/>
</AuthenticatedLayout>
);
}

View file

@ -0,0 +1,150 @@
// * Packages
import { usePage } from "@inertiajs/react";
import { route } from "ziggy-js";
import { ArrowLeftIcon } from "lucide-react";
// * Components shadcn
import { Badge } from "@/components/ui/badge";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
// * Other
import AuthenticatedLayout from "@/back/Layouts/AuthenticatedLayout";
import DetailsTable from "@/back/Components/Table/DetailsTable";
import ModelActions from "@/back/Components/ModelActions";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import { GetFormatedDate, GetTranslation } from "@/lib/utils";
import { Views } from "@/back/lib/actions";
import { AuthProp, DefaultModel, SingleResource } from "@/back/types/global";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
import { TranslationField } from "@/back/types/i18n";
import RenderTranslationCell from "@/back/Components/Table/RenderTranslationCell";
const getColumns = () => {
return [
{
accessorKey: "name",
header: str(GetTranslation("validation.custom.name")).ucFirst().value(),
cell: ({ value }: { value: TranslationField }) => (
<RenderTranslationCell value={value} />
),
},
{
accessorKey: "url",
header: str(GetTranslation("validation.custom.url")).ucFirst().value(),
cell: ({ value }: { value: string }) => (
<Tooltip>
<TooltipTrigger asChild>
<a
href={value}
target="_blank"
rel="noreferrer"
className="text-primary underline underline-offset-4 hover:opacity-80 transition-opacity rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
{value}
</a>
</TooltipTrigger>
<TooltipContent>{GetTranslation("back.access_url")}</TooltipContent>
</Tooltip>
),
},
{
accessorKey: "gitlab_id",
header: str(GetTranslation("validation.custom.gitlab_id"))
.ucFirst()
.value(),
cell: ({ value }: { value: number }) => value,
},
{
accessorKey: "created_at",
header: str(
GetTranslation("uwil-backend::validation.attributes.created_at"),
)
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{GetFormatedDate(value)}</Badge>
),
},
{
accessorKey: "updated_at",
header: str(
GetTranslation("uwil-backend::validation.attributes.updated_at"),
)
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{GetFormatedDate(value)}</Badge>
),
},
{
accessorKey: "deleted_at",
header: str(
GetTranslation("uwil-backend::validation.attributes.deleted_at"),
)
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{GetFormatedDate(value)}</Badge>
),
},
];
};
export default function Show({
data,
modelName,
}: {
data: SingleResource;
modelName: string;
}) {
const { auth } = usePage().props as unknown as {
auth: AuthProp;
};
const { viewAny } = auth.policies.gitlabProjects;
const model = data.data as DefaultModel;
const actionModels = [Views.EDITION, Views.DUPLICATION, Views.ARCHIVATION];
const transTitle = useTrans(
"uwil-backend::crud.helpers.visualization_model",
{
params: {
model: useTrans(`models.classes.${modelName}`, {
choice: 1,
}),
},
},
);
const columns = getColumns();
// Translations.
const transCrudBackToList = useTrans("uwil-backend::back.crud_back_to_list");
const beforeNode = () => {
return (
viewAny && (
<div className="flex justify-end items-center gap-x-1.5">
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.index`)}
content={<ArrowLeftIcon />}
tooltipContent={<p>{transCrudBackToList}</p>}
/>
</div>
)
);
};
return (
<AuthenticatedLayout title={transTitle} beforeNode={beforeNode()}>
<ModelActions
actions={actionModels}
modelRoute={modelName}
model={model}
modelUpdatedAt={model.updated_at.toString()}
/>
{/* @ts-expect-error Weird error type on cell. */}
<DetailsTable columns={columns} model={model} />
</AuthenticatedLayout>
);
}

View file

@ -1,5 +1,5 @@
import { LaravelEnum } from "@/types/global";
import { DefaultModel } from "@/back/types/global";
import { DefaultModel, UserModel } from "@/back/types/global";
import { TranslationField } from "@/back/types/i18n";
type NoticeModel = DefaultModel & {
@ -17,3 +17,20 @@ type InvoicePackageModel = DefaultModel & {
type InvoiceCompanyModel = DefaultModel & {
name: TranslationField;
};
type GitlabProjectModel = DefaultModel & {
name: TranslationField;
url: string;
gitlab_id: number;
};
type SiteModel = DefaultModel & {
name: TranslationField;
url_re7: string;
url_prod: string;
project_manager_id: number;
project_manager: UserModel;
gitlab_project_id: number;
gitlab_project: GitlabProjectModel;
users: Array<UserModel>;
};

View file

@ -39,3 +39,17 @@ export const GetFormatedDate = (
) => {
return useFormatedDate(date, timezone, time);
};
/**
* Check if it's a valid url or not.
* @param {string} url
* @return {boolean}
*/
export const isAValidUrl = (url: string | undefined): boolean => {
if (!url) return false;
try {
return !!new URL(url).href;
} catch {
return false;
}
};

View file

@ -0,0 +1,14 @@
<?php
use App\Http\Controllers\Back\GitlabProjectController;
use Illuminate\Support\Facades\Route;
Route::get('/gitlab-projects/trash', [GitlabProjectController::class, 'index'])
->name('gitlab-projects.trash');
Route::patch('/gitlab-projects/{gitlab_project}/restore', [GitlabProjectController::class, 'restore'])
->name('gitlab-projects.restore');
Route::delete('/gitlab-projects/{gitlab_project}/force', [GitlabProjectController::class, 'forceDestroy'])
->name('gitlab-projects.forceDestroy');
Route::resource('gitlab-projects', GitlabProjectController::class);
Route::get('/gitlab-projects/{gitlab_project}/duplicate', [GitlabProjectController::class, 'duplicate'])
->name('gitlab-projects.duplicate');

View file

@ -0,0 +1,45 @@
<?php
use Diglactic\Breadcrumbs\Breadcrumbs;
use Diglactic\Breadcrumbs\Generator;
use Illuminate\Support\Str;
Breadcrumbs::for('back.gitlab-projects.index', function (Generator $trail) {
$trail->push(
Str::ucfirst(trans_choice('models.classes.gitlab-projects', \INF)),
route('back.gitlab-projects.index')
);
});
Breadcrumbs::for('back.gitlab-projects.create', function (Generator $trail) {
$trail->parent('back.gitlab-projects.index');
$trail->push(
Str::ucfirst(trans('uwil-backend::crud.actions.create')),
route('back.gitlab-projects.create'),
);
});
Breadcrumbs::for('back.gitlab-projects.edit', function (Generator $trail) {
$trail->parent('back.gitlab-projects.index');
$trail->push(
Str::ucfirst(trans('uwil-backend::crud.actions.edit')),
route('back.gitlab-projects.edit', [
'gitlab_project' => request()->gitlab_project,
])
);
});
Breadcrumbs::for('back.gitlab-projects.duplicate', function (Generator $trail) {
$trail->parent('back.gitlab-projects.index');
$trail->push(Str::ucfirst(trans('uwil-backend::crud.actions.duplicate')));
});
Breadcrumbs::for('back.gitlab-projects.show', function (Generator $trail) {
$trail->parent('back.gitlab-projects.index');
$trail->push(
Str::ucfirst(trans('uwil-backend::crud.actions.visualize')),
route('back.gitlab-projects.show', [
'gitlab_project' => request()->gitlab_project,
]),
);
});
Breadcrumbs::for('back.gitlab-projects.trash', function (Generator $trail) {
$trail->parent('back.gitlab-projects.index');
$trail->push(Str::ucfirst(trans('uwil-backend::back.archives')));
});