diff --git a/app/Http/Controllers/Back/GitlabProjectController.php b/app/Http/Controllers/Back/GitlabProjectController.php new file mode 100644 index 0000000..6115748 --- /dev/null +++ b/app/Http/Controllers/Back/GitlabProjectController.php @@ -0,0 +1,245 @@ +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 */ + $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 */ + $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 */ + $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)), + ])); + } +} diff --git a/app/Http/Middlewares/HandleInertiaRequests.php b/app/Http/Middlewares/HandleInertiaRequests.php index 12af070..f089b44 100644 --- a/app/Http/Middlewares/HandleInertiaRequests.php +++ b/app/Http/Middlewares/HandleInertiaRequests.php @@ -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(), diff --git a/app/Http/Requests/Back/GitlabProjects/StoreGitlabProjectRequest.php b/app/Http/Requests/Back/GitlabProjects/StoreGitlabProjectRequest.php new file mode 100644 index 0000000..401c10e --- /dev/null +++ b/app/Http/Requests/Back/GitlabProjects/StoreGitlabProjectRequest.php @@ -0,0 +1,51 @@ + + */ + 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 + */ + public function attributes(): array + { + return [ + 'name' => trans('validation.custom.name'), + 'url' => trans('validation.custom.url'), + 'gitlab_id' => trans('validation.custom.gitlab_id'), + ]; + } +} diff --git a/app/Http/Requests/Back/GitlabProjects/UpdateGitlabProjectRequest.php b/app/Http/Requests/Back/GitlabProjects/UpdateGitlabProjectRequest.php new file mode 100644 index 0000000..b3a3f87 --- /dev/null +++ b/app/Http/Requests/Back/GitlabProjects/UpdateGitlabProjectRequest.php @@ -0,0 +1,48 @@ +route('gitlab_project')); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + #[\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()), + ], + ]); + } +} diff --git a/app/Models/GitlabProject.php b/app/Models/GitlabProject.php new file mode 100644 index 0000000..d4980de --- /dev/null +++ b/app/Models/GitlabProject.php @@ -0,0 +1,51 @@ + */ + use HasFactory; + use HasModelNaming; + use HasTranslations; + use SoftDeletes; + + /** + * The attributes that are mass assignable. + * + * @var list + */ + protected $fillable = [ + 'name', + 'url', + 'gitlab_id', + ]; + + /** + * The translatable columns. + * + * @var list + */ + public $translatable = [ + 'name', + ]; +} diff --git a/app/Policies/GitlabProjectPolicy.php b/app/Policies/GitlabProjectPolicy.php new file mode 100644 index 0000000..e25dd58 --- /dev/null +++ b/app/Policies/GitlabProjectPolicy.php @@ -0,0 +1,112 @@ +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); + } +} diff --git a/database/factories/GitlabProjectFactory.php b/database/factories/GitlabProjectFactory.php new file mode 100644 index 0000000..d698b8e --- /dev/null +++ b/database/factories/GitlabProjectFactory.php @@ -0,0 +1,33 @@ + + */ + protected $model = GitlabProject::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + "name" => fake()->unique()->words(3, true), + "url" => fake()->unique()->url(), + "gitlab_id" => fake()->unique()->numberBetween(1, 1000), + ]; + } +} diff --git a/database/migrations/2026_04_30_155423_create_gitlab_projects_table.php b/database/migrations/2026_04_30_155423_create_gitlab_projects_table.php new file mode 100644 index 0000000..3de26d2 --- /dev/null +++ b/database/migrations/2026_04_30_155423_create_gitlab_projects_table.php @@ -0,0 +1,36 @@ +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'); + } +}; diff --git a/database/seeders/Classic/GitlabProjectSeeder.php b/database/seeders/Classic/GitlabProjectSeeder.php new file mode 100644 index 0000000..1ef8038 --- /dev/null +++ b/database/seeders/Classic/GitlabProjectSeeder.php @@ -0,0 +1,25 @@ +create(); + } +} diff --git a/lang/fr/models.php b/lang/fr/models.php index 8ea3346..753b922 100644 --- a/lang/fr/models.php +++ b/lang/fr/models.php @@ -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", ], ]; diff --git a/resources/ts/back/Components/Layout/Sidebar/Links/GitlabProjects.tsx b/resources/ts/back/Components/Layout/Sidebar/Links/GitlabProjects.tsx new file mode 100644 index 0000000..47aa43f --- /dev/null +++ b/resources/ts/back/Components/Layout/Sidebar/Links/GitlabProjects.tsx @@ -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 && ( + + + + ), + auth.policies.gitlabProjects.viewTrash && ( + + + + ), + ], + }), + }; + + return { link, group: "modules" }; +} diff --git a/resources/ts/back/Pages/GitlabProjects/Create.tsx b/resources/ts/back/Pages/GitlabProjects/Create.tsx new file mode 100644 index 0000000..a4c2d84 --- /dev/null +++ b/resources/ts/back/Pages/GitlabProjects/Create.tsx @@ -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 && ( +
+ } + tooltipContent={

{transBackToList}

} + /> +
+ ) + ); + }; + + return ( + +
+ + ); +} diff --git a/resources/ts/back/Pages/GitlabProjects/Edit.tsx b/resources/ts/back/Pages/GitlabProjects/Edit.tsx new file mode 100644 index 0000000..ab1a4b9 --- /dev/null +++ b/resources/ts/back/Pages/GitlabProjects/Edit.tsx @@ -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 && ( +
+ } + tooltipContent={

{transBackToList}

} + /> +
+ ) + ); + }; + + return ( + + + + + ); +} diff --git a/resources/ts/back/Pages/GitlabProjects/Form.tsx b/resources/ts/back/Pages/GitlabProjects/Form.tsx new file mode 100644 index 0000000..458368c --- /dev/null +++ b/resources/ts/back/Pages/GitlabProjects/Form.tsx @@ -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 ( + + + } + description={useTrans("uwil-backend::back.form_subtitle_global")} + className="sm:grid-cols-2 gap-4" + > + + +
+ + {isAValidUrl(data.url) && ( + } + tooltipContent={GetTranslation("back.access_url")} + link={data.url} + externalLink + type="button" + size="icon" + /> + )} +
+
+
+ +
+ + ); +} diff --git a/resources/ts/back/Pages/GitlabProjects/Index.tsx b/resources/ts/back/Pages/GitlabProjects/Index.tsx new file mode 100644 index 0000000..afd8b7d --- /dev/null +++ b/resources/ts/back/Pages/GitlabProjects/Index.tsx @@ -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 } }) => ( + + ), + cell: ({ row }: { row: Row }) => ( + + ), + }, + { + accessorKey: "url", + header: ({ column }: { column: { id: string } }) => ( + + ), + cell: ({ row }: { row: Row }) => ( + + + + {row.getValue("url")} + + + {GetTranslation("back.access_url")} + + ), + }, + { + accessorKey: "updated_at", + header: ({ column }: { column: { id: string } }) => ( + + ), + cell: ({ row }: { row: Row }) => ( + + {GetFormatedDate(row.getValue("updated_at"))} + + ), + }, + ]; +}; + +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 && ( +
+ } + tooltipContent={

{transCrudBackList}

} + /> +
+ ) + ); + }; + 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 && ( +
+ {viewTrash && ( + } + tooltipContent={

{transAccessTo}

} + variant="outline" + /> + )} + {create && ( + } + tooltipContent={

{transCrudAddEntry}

} + /> + )} +
+ ) + ); + }; + + return ( + + {isArchiveRoute && } + + + + ); +} diff --git a/resources/ts/back/Pages/GitlabProjects/Show.tsx b/resources/ts/back/Pages/GitlabProjects/Show.tsx new file mode 100644 index 0000000..3a23d29 --- /dev/null +++ b/resources/ts/back/Pages/GitlabProjects/Show.tsx @@ -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 }) => ( + + ), + }, + { + accessorKey: "url", + header: str(GetTranslation("validation.custom.url")).ucFirst().value(), + cell: ({ value }: { value: string }) => ( + + + + {value} + + + {GetTranslation("back.access_url")} + + ), + }, + { + 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 }) => ( + {GetFormatedDate(value)} + ), + }, + { + accessorKey: "updated_at", + header: str( + GetTranslation("uwil-backend::validation.attributes.updated_at"), + ) + .ucFirst() + .value(), + cell: ({ value }: { value: string }) => ( + {GetFormatedDate(value)} + ), + }, + { + accessorKey: "deleted_at", + header: str( + GetTranslation("uwil-backend::validation.attributes.deleted_at"), + ) + .ucFirst() + .value(), + cell: ({ value }: { value: string }) => ( + {GetFormatedDate(value)} + ), + }, + ]; +}; + +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 && ( +
+ } + tooltipContent={

{transCrudBackToList}

} + /> +
+ ) + ); + }; + + return ( + + + {/* @ts-expect-error Weird error type on cell. */} + + + ); +} diff --git a/resources/ts/back/types/project.d.ts b/resources/ts/back/types/project.d.ts index a0005dc..8538852 100644 --- a/resources/ts/back/types/project.d.ts +++ b/resources/ts/back/types/project.d.ts @@ -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; +}; diff --git a/resources/ts/lib/utils.tsx b/resources/ts/lib/utils.tsx index 1d4957c..2d9c270 100644 --- a/resources/ts/lib/utils.tsx +++ b/resources/ts/lib/utils.tsx @@ -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; + } +}; diff --git a/routes/back/gitlab-projects.php b/routes/back/gitlab-projects.php new file mode 100644 index 0000000..b4d49b7 --- /dev/null +++ b/routes/back/gitlab-projects.php @@ -0,0 +1,14 @@ +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'); diff --git a/routes/breadcrumbs/gitlab-projects.php b/routes/breadcrumbs/gitlab-projects.php new file mode 100644 index 0000000..4273250 --- /dev/null +++ b/routes/breadcrumbs/gitlab-projects.php @@ -0,0 +1,45 @@ +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'))); +});