fix: 🐛 update models relations in views and duplicate action

This commit is contained in:
Alexis Gousseau 2026-05-20 15:02:43 +02:00
parent d3dcb95d5d
commit 4ce71d8595
No known key found for this signature in database
GPG key ID: 7F3C786EE12DBB72
17 changed files with 337 additions and 107 deletions

View file

@ -6,6 +6,7 @@ use App\Http\Requests\Back\Brands\StoreBrandRequest;
use App\Http\Requests\Back\Brands\UpdateBrandRequest;
use App\Models\Brand;
use App\Models\Site;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
@ -101,7 +102,9 @@ class BrandController extends BackendController
public function show(Brand $brand): \Inertia\Response
{
return $this->inertiaRender("back/Pages/$this->modelPage/Show", [
'data' => CommonResource::make($brand->load(['sites'])),
'data' => CommonResource::make($brand->load([
'sites' => fn($q) => $q->withoutGlobalScope(SoftDeletingScope::class),
])),
]);
}
@ -183,7 +186,10 @@ class BrandController extends BackendController
*/
public function duplicate(Brand $brand): \Inertia\Response
{
return $this->create($brand->replicate());
$brand->loadMissing(Brand::$cacheRelations ?? []);
$replica = $brand->replicate();
collect($brand->getRelations())->each(fn($value, $relation) => $replica->setRelation($relation, $value));
return $this->create($replica);
}
/**

View file

@ -167,7 +167,10 @@ class GitlabProjectController extends BackendController
*/
public function duplicate(GitlabProject $gitlab_project): \Inertia\Response
{
return $this->create($gitlab_project->replicate());
$gitlab_project->loadMissing(GitlabProject::$cacheRelations ?? []);
$replica = $gitlab_project->replicate();
collect($gitlab_project->getRelations())->each(fn($value, $relation) => $replica->setRelation($relation, $value));
return $this->create($replica);
}
/**

View file

@ -167,7 +167,10 @@ class InvoiceCompanyController extends BackendController
*/
public function duplicate(InvoiceCompany $invoice_company): \Inertia\Response
{
return $this->create($invoice_company->replicate());
$invoice_company->loadMissing(InvoiceCompany::$cacheRelations ?? []);
$replica = $invoice_company->replicate();
collect($invoice_company->getRelations())->each(fn($value, $relation) => $replica->setRelation($relation, $value));
return $this->create($replica);
}
/**

View file

@ -167,7 +167,10 @@ class InvoicePackageController extends BackendController
*/
public function duplicate(InvoicePackage $invoice_package): \Inertia\Response
{
return $this->create($invoice_package->replicate());
$invoice_package->loadMissing(InvoicePackage::$cacheRelations ?? []);
$replica = $invoice_package->replicate();
collect($invoice_package->getRelations())->each(fn($value, $relation) => $replica->setRelation($relation, $value));
return $this->create($replica);
}
/**

View file

@ -167,7 +167,10 @@ class NoticeController extends BackendController
*/
public function duplicate(Notice $notice): \Inertia\Response
{
return $this->create($notice->replicate());
$notice->loadMissing(Notice::$cacheRelations ?? []);
$replica = $notice->replicate();
collect($notice->getRelations())->each(fn($value, $relation) => $replica->setRelation($relation, $value));
return $this->create($replica);
}
/**

View file

@ -4,9 +4,9 @@ namespace App\Http\Controllers\Back;
use App\Http\Requests\Back\Sites\StoreSiteRequest;
use App\Http\Requests\Back\Sites\UpdateSiteRequest;
use App\Models\GitlabProject;
use App\Models\Site;
use App\Models\User;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Arr;
@ -34,13 +34,6 @@ class SiteController extends BackendController
*/
private \Illuminate\Support\Collection $userModelsClient;
/**
* Gitlab projects collection.
*
* @var \Illuminate\Support\Collection<int,\App\Models\GitlabProject>
*/
private \Illuminate\Support\Collection $gitlabProjectModels;
/**
* Create the controller instance.
*/
@ -59,7 +52,6 @@ class SiteController extends BackendController
$this->userModelsClient = $userModels
->filter(fn(User $user) => $user->role->value === UserRoleRegistry::get('client')->value)
->values();
$this->gitlabProjectModels = GitlabProject::getModelsCached();
}
/**
@ -117,7 +109,6 @@ class SiteController extends BackendController
'data' => CommonResource::make($site),
'userModelsProjectManager' => $this->userModelsProjectManager,
'userModelsClient' => $this->userModelsClient,
'gitlabProjectModels' => $this->gitlabProjectModels,
]);
}
@ -130,10 +121,13 @@ class SiteController extends BackendController
public function show(Site $site): \Inertia\Response
{
return $this->inertiaRender("back/Pages/$this->modelPage/Show", [
'data' => CommonResource::make($site->load(['projectManager', 'gitlabProject', 'users'])),
'data' => CommonResource::make($site->load([
'projectManager' => fn($q) => $q->withoutGlobalScope(SoftDeletingScope::class),
'gitlabProject' => fn($q) => $q->withoutGlobalScope(SoftDeletingScope::class),
'users' => fn($q) => $q->withoutGlobalScope(SoftDeletingScope::class),
])),
'userModelsProjectManager' => $this->userModelsProjectManager,
'userModelsClient' => $this->userModelsClient,
'gitlabProjectModels' => $this->gitlabProjectModels,
]);
}
@ -173,10 +167,13 @@ class SiteController extends BackendController
public function edit(Site $site): \Inertia\Response
{
return $this->inertiaRender("back/Pages/$this->modelPage/Edit", [
'data' => CommonResource::make($site->load(['projectManager', 'gitlabProject', 'users'])),
'data' => CommonResource::make($site->load([
'projectManager' => fn($q) => $q->withoutGlobalScope(SoftDeletingScope::class),
'gitlabProject' => fn($q) => $q->withoutGlobalScope(SoftDeletingScope::class),
'users' => fn($q) => $q->withoutGlobalScope(SoftDeletingScope::class),
])),
'userModelsProjectManager' => $this->userModelsProjectManager,
'userModelsClient' => $this->userModelsClient,
'gitlabProjectModels' => $this->gitlabProjectModels,
]);
}
@ -217,7 +214,10 @@ class SiteController extends BackendController
*/
public function duplicate(Site $site): \Inertia\Response
{
return $this->create($site->replicate());
$site->loadMissing(Site::$cacheRelations ?? []);
$replica = $site->replicate();
collect($site->getRelations())->each(fn($value, $relation) => $replica->setRelation($relation, $value));
return $this->create($replica);
}
/**

View file

@ -45,6 +45,15 @@ class Brand extends Model
'name',
];
/**
* Relations should be included in the cache.
*
* @var list<string>
*/
public static $cacheRelations = [
'sites',
];
// * RELATIONS.
/**

View file

@ -58,9 +58,10 @@ class Site extends Model
*
* @var list<string>
*/
protected static $cacheRelations = [
public static $cacheRelations = [
'projectManager',
'gitlabProject',
'users',
];
/**

View file

@ -15,6 +15,16 @@ use Uwil\LaravelBackend\Models\User as BackendUser;
*/
class User extends BackendUser
{
/**
* Relations should be included in the cache.
*
* @var list<string>
*/
public static $cacheRelations = [
'userPassword',
'sites',
];
/**
* Get the password for authentication.
* Retrieves the hashed password from the related user_passwords table.
@ -58,4 +68,20 @@ class User extends BackendUser
{
return $this->hasOne(UserPassword::class);
}
/**
* Get sites associated to the brand.
*
* @return \Illuminate\Database\Eloquent\Relations\MorphToMany<\App\Models\Site>
*/
public function sites(): \Illuminate\Database\Eloquent\Relations\MorphToMany
{
return $this->morphToMany(
Site::class,
'sitable',
'sitables',
'sitable_id',
'site_id',
)->withPivot('created_at');
}
}

View file

@ -320,10 +320,16 @@ interface ModelSelectProps
/**
* Make the Close button visible
* Optional, defaults to empty
* Optional, defaults to true
*/
closeButtonVisible?: boolean;
/**
* Make the Empty button visible
* Optional, defaults to true
*/
emptyButtonVisible?: boolean;
/**
* Model name
* Take the name of the model to fetch in the backend, using ajax function
@ -404,7 +410,8 @@ export const ModelSelect = forwardRef<ModelSelectRef, ModelSelectProps>(
deduplicateOptions = false,
resetOnDefaultValueChange = true,
closeOnSelect = false,
closeButtonVisible = false,
closeButtonVisible = true,
emptyButtonVisible = true,
targetModel,
field,
exclude = null,
@ -1228,7 +1235,7 @@ export const ModelSelect = forwardRef<ModelSelectRef, ModelSelectProps>(
>
{selectedValues.length === 0 && (
<div className="flex items-center justify-between w-full mx-auto gap-2">
<p className="text-start text-sm whitespace-pre-line">
<p className="text-start text-sm text-muted-foreground whitespace-pre-line">
{placeholder}
</p>
<ChevronsUpDownIcon className="h-4 cursor-pointer opacity-50 group-hover/button:opacity-60" />
@ -1611,10 +1618,10 @@ export const ModelSelect = forwardRef<ModelSelectRef, ModelSelectProps>(
</div>
)}
{(closeButtonVisible || selectedValues.length > 0) && (
{(closeButtonVisible || (emptyButtonVisible && selectedValues.length > 0)) && (
<CommandGroup className="border-t p-1">
<div className="flex items-center justify-between gap-1">
{selectedValues.length > 0 && (
{emptyButtonVisible && selectedValues.length > 0 && (
<BtnOptionnalLinkWithTooltip
content={str(transClear).ucFirst().value()}
tooltipContent={

View file

@ -1,5 +1,5 @@
// * Packages
import { InfoIcon } from "lucide-react";
import { BriefcaseBusinessIcon } from "lucide-react";
import { route } from "ziggy-js";
import { Link } from "@inertiajs/react";
// * Components shadcn
@ -36,7 +36,7 @@ export default function SidebarLinkBrands(auth: AuthProp): SidebarGroupLink {
const link: NavListLinksProps = {
title: str(transModel).ucFirst().value(),
model: modelName,
icon: InfoIcon,
icon: BriefcaseBusinessIcon,
show: auth.policies.brands.viewAny,
actions: (model: string) =>
filterActions({

View file

@ -1,20 +1,18 @@
// * Packages
import { useForm, usePage } from "@inertiajs/react";
import { BookTextIcon } from "lucide-react";
// * Components shadcn
//
// * Other
import FieldsetWrapper from "@/back/Components/FieldsetWrapper";
import SimpleString from "@/back/Components/Inputs/SimpleString";
import { ModelSelect } from "@/back/Components/Inputs/ModelSelect";
import useTrans from "@/hooks/use-translations";
import InputError from "@/back/Components/InputError";
import SubmitButton from "@/back/Components/SubmitButton";
import useCrudSubmit from "@/back/hooks/use-crud-submit";
import { BrandModel, SiteModel } from "@/back/types/project";
import { BrandModel } from "@/back/types/project";
import str from "@/hooks/use-string";
import { AppEnv } from "@/types/global";
import { MultiSelect } from "@/back/Components/Inputs/MultiSelect";
import { extractIdsFromDatabasePivotRelation, mapPivotOptions } from "@/lib/mapPivotOptions";
import { extractIdsFromDatabasePivotRelation } from "@/lib/mapPivotOptions";
export default function Form({
model,
@ -29,12 +27,8 @@ export default function Form({
action: string;
method: number;
}) {
const {
appEnv,
siteModels
} = usePage().props as unknown as {
const { appEnv } = usePage().props as unknown as {
appEnv: AppEnv;
siteModels: Array<SiteModel>;
};
/** Form section */
@ -56,8 +50,6 @@ export default function Form({
}),
});
const selectOptionsSites = mapPivotOptions(siteModels, model?.sites, 'name', appEnv.locale);
return (
<form id="config-form" onSubmit={submit}>
<InputError all />
@ -83,8 +75,10 @@ export default function Form({
value={data.name}
setData={setData}
/>
<MultiSelect
<ModelSelect
name="sites"
targetModel="sites"
field="name"
label={str(
useTrans("uwil-backend::back.form_associate_to", {
params: {
@ -97,22 +91,14 @@ export default function Form({
)
.ucFirst()
.value()}
placeholder={str(
useTrans("back.select_element", {
params: {
element: useTrans("models.classes.sites", {
choice: 2,
}),
},
}),
)
.ucFirst()
.value()}
options={selectOptionsSites.options}
defaultValue={selectOptionsSites.defaultValue}
placeholder="ex: John Doe"
defaultValues={data.sites}
setData={setData}
required={false}
closeButtonVisible
maxCount={5}
hideSelectAll={true}
required={true}
multiple={true}
closeButtonVisible={true}
/>
</FieldsetWrapper>
<div className="text-center">

View file

@ -1,9 +1,14 @@
// * Packages
import { usePage } from "@inertiajs/react";
import { route } from "ziggy-js";
import { ArrowLeftIcon } from "lucide-react";
import { ArrowLeftIcon, GlobeIcon, TriangleAlertIcon } 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";
@ -16,8 +21,10 @@ 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";
import { SiteModel } from "@/back/types/project";
import { AppEnv } from "@/types/global";
const getColumns = () => {
const getColumns = (appEnv: AppEnv, viewAnySites: boolean) => {
return [
{
accessorKey: "name",
@ -26,6 +33,97 @@ const getColumns = () => {
<RenderTranslationCell value={value} />
),
},
{
accessorKey: "sites",
header: str(GetTranslation("models.classes.sites", { choice: 2 }))
.ucFirst()
.value(),
cell: ({ value }: { value: Array<SiteModel> }) => {
if (value.length) {
const projectData = (siteModel: SiteModel) => (
<span>{siteModel.name[appEnv.locale]}</span>
);
return (
<div className="flex flex-row flex-wrap justify-start items-center gap-1.5">
{value.map((siteModel) => {
let content = (
<div className="flex flex-row justify-start items-center">
<Badge variant="outline">{projectData(siteModel)}</Badge>
{siteModel.deleted_at !== null && (
<Tooltip>
<TooltipTrigger asChild>
<TriangleAlertIcon
size={15}
className="text-destructive ms-1"
/>
</TooltipTrigger>
<TooltipContent>
{GetTranslation("back.model_archived", {
params: {
model: str(
GetTranslation(
"models.classes.gitlab-projects",
{
choice: 1,
},
),
)
.ucFirst()
.value(),
},
choice: 1,
})}
</TooltipContent>
</Tooltip>
)}
</div>
);
if (viewAnySites && siteModel.deleted_at === null) {
content = (
<BtnOptionnalLinkWithTooltip
link={route("back.sites.show", siteModel)}
content={
<>
<GlobeIcon />
{projectData(siteModel)}
</>
}
tooltipContent={
<p>
{GetTranslation("uwil-backend::back.access_model", {
params: {
model: GetTranslation(
"models.classes.sites",
{
choice: 1,
},
),
},
})}
</p>
}
type="button"
size="default"
variant="secondary"
/>
);
}
return (
<div className="flex flex-row flex-wrap justify-start items-center gap-1.5">
{content}
</div>
);
})}
</div>
);
}
return (
<p className="text-muted-foreground italic m-0">
{GetTranslation("uwil-backend::back.table_not_known")}
</p>
);
},
},
{
accessorKey: "created_at",
header: str(
@ -69,10 +167,12 @@ export default function Show({
data: SingleResource;
modelName: string;
}) {
const { auth } = usePage().props as unknown as {
const { auth, appEnv } = usePage().props as unknown as {
auth: AuthProp;
appEnv: AppEnv;
};
const { viewAny } = auth.policies.brands;
const { viewAny: viewAnySites } = auth.policies.sites;
const model = data.data as DefaultModel;
const actionModels = [Views.EDITION, Views.DUPLICATION, Views.ARCHIVATION];
const transTitle = useTrans(
@ -85,7 +185,7 @@ export default function Show({
},
},
);
const columns = getColumns();
const columns = getColumns(appEnv, viewAnySites);
// Translations.
const transCrudBackToList = useTrans("uwil-backend::back.crud_back_to_list");

View file

@ -8,7 +8,8 @@ import Form from "@/back/Pages/Sites/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";
import { AuthProp, SingleResource } from "@/back/types/global";
import { SiteModel } from "@/back/types/project";
export default function Create({
data,
@ -18,7 +19,7 @@ export default function Create({
modelName: string;
}) {
const { viewAny } = (usePage().props.auth as AuthProp).policies.sites;
const model = data.data as DefaultModel;
const model = data.data as SiteModel;
// Translations.
const transBackToList = useTrans("uwil-backend::back.crud_back_to_list");

View file

@ -9,8 +9,9 @@ import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithT
import Form from "@/back/Pages/Sites/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";
import Views from "@/back/lib/actions";
import { AuthProp, SingleResource } from "@/back/types/global";
import { SiteModel } from "@/back/types/project";
export default function Edit({
data,
@ -20,7 +21,7 @@ export default function Edit({
modelName: string;
}) {
const { viewAny } = (usePage().props.auth as AuthProp).policies.sites;
const model = data.data as DefaultModel;
const model = data.data as SiteModel;
const actionModels = [
Views.VISUALIZATION,
Views.DUPLICATION,

View file

@ -14,10 +14,11 @@ import {
} from "@/back/Components/Inputs/MultiSelect";
import { UserModel } from "@/back/types/global";
import str from "@/hooks/use-string";
import { GitlabProjectModel, SiteModel } from "@/back/types/project";
import { SiteModel } from "@/back/types/project";
import { AppEnv } from "@/types/global";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import { GetTranslation, isAValidUrl } from "@/lib/utils";
import { ModelSelect } from "@/back/Components/Inputs/ModelSelect";
export default function Form({
model,
@ -32,25 +33,20 @@ export default function Form({
action: string;
method: number;
}) {
const {
appEnv,
userModelsProjectManager,
userModelsClient,
gitlabProjectModels,
} = usePage().props as unknown as {
const { appEnv, userModelsProjectManager, userModelsClient } = usePage()
.props as unknown as {
appEnv: AppEnv;
userModelsProjectManager: Array<UserModel>;
userModelsClient: Array<UserModel>;
gitlabProjectModels: Array<GitlabProjectModel>;
};
/** Form section */
const { post, setData, data } = useForm({
name: model?.name ? model?.name[appEnv.locale] : undefined,
url_re7: model?.url_re7 ?? undefined,
url_prod: model?.url_prod ?? undefined,
project_manager_id: model?.project_manager_id ?? undefined,
gitlab_project_id: model?.gitlab_project_id ?? undefined,
url_re7: model?.url_re7,
url_prod: model?.url_prod,
project_manager_id: model?.project_manager_id,
gitlab_project_id: model?.gitlab_project_id,
users:
model && model.users
? model?.users?.map((user) => String(user.id))
@ -99,10 +95,6 @@ export default function Form({
userModelsClient,
(userModel) => `${userModel.first_name} ${userModel.last_name}`,
);
const selectOptionsProject = formatSelectOptions(
gitlabProjectModels,
(gitlabProjectModel) => gitlabProjectModel.name[appEnv.locale],
);
return (
<form id="config-form" onSubmit={submit}>
@ -155,9 +147,11 @@ export default function Form({
options={selectOptionsProjectManager}
multiple={false}
defaultValue={
data.project_manager_id && String(data.project_manager_id) !== ""
model?.project_manager?.deleted_at === null &&
data.project_manager_id &&
String(data.project_manager_id) !== ""
? [String(data.project_manager_id)]
: []
: undefined
}
setData={setData}
maxSelection={1}
@ -193,10 +187,12 @@ export default function Form({
options={selectOptionsClient}
defaultValue={data.users}
setData={setData}
closeButtonVisible
closeButtonVisible={true}
/>
<MultiSelect
<ModelSelect
name="gitlab_project_id"
targetModel="gitlab_projects"
field="name"
label={str(
useTrans("uwil-backend::back.form_associate_to", {
params: {
@ -209,29 +205,19 @@ export default function Form({
)
.ucFirst()
.value()}
placeholder={str(
useTrans("back.select_element", {
params: {
element: useTrans("models.classes.gitlab-projects", {
choice: 1,
}),
},
}),
)
.ucFirst()
.value()}
options={selectOptionsProject}
multiple={false}
defaultValue={
data.gitlab_project_id && String(data.gitlab_project_id) !== ""
? [String(data.gitlab_project_id)]
: []
placeholder="ex: John Doe"
defaultValues={
model?.gitlab_project?.deleted_at === null && data.gitlab_project_id
? [data.gitlab_project_id]
: undefined
}
setData={setData}
maxSelection={1}
singleLine
closeOnSelect
closeButtonVisible
maxCount={5}
hideSelectAll={true}
required={true}
multiple={false}
closeOnSelect={true}
emptyButtonVisible={false}
/>
</FieldsetWrapper>
<FieldsetWrapper

View file

@ -246,6 +246,101 @@ const getColumns = (viewAnyUsers: boolean, appEnv: AppEnv) => {
);
},
},
{
accessorKey: "users",
header: str(
GetTranslation("uwil-backend::models.classes.users", { choice: 2 }),
)
.ucFirst()
.value(),
cell: ({ value }: { value: Array<UserModel> }) => {
if (value.length) {
const projectData = (userModel: UserModel) => (
<span>
{userModel.first_name} {userModel.last_name}
</span>
);
return (
<div className="flex flex-row flex-wrap justify-start items-center gap-1.5">
{value.map((userModel) => {
let content = (
<div className="flex flex-row justify-start items-center">
<Badge variant="outline">{projectData(userModel)}</Badge>
{userModel.deleted_at !== null && (
<Tooltip>
<TooltipTrigger asChild>
<TriangleAlertIcon
size={15}
className="text-destructive ms-1"
/>
</TooltipTrigger>
<TooltipContent>
{GetTranslation("back.model_archived", {
params: {
model: str(
GetTranslation(
"models.classes.gitlab-projects",
{
choice: 1,
},
),
)
.ucFirst()
.value(),
},
choice: 1,
})}
</TooltipContent>
</Tooltip>
)}
</div>
);
if (viewAnyUsers && userModel.deleted_at === null) {
content = (
<BtnOptionnalLinkWithTooltip
link={route("back.users.show", userModel)}
content={
<>
<UserIcon />
{projectData(userModel)}
</>
}
tooltipContent={
<p>
{GetTranslation("uwil-backend::back.access_model", {
params: {
model: GetTranslation(
"uwil-backend::models.classes.users",
{
choice: 1,
},
),
},
})}
</p>
}
type="button"
size="default"
variant="secondary"
/>
);
}
return (
<div className="flex flex-row flex-wrap justify-start items-center gap-1.5">
{content}
</div>
);
})}
</div>
);
}
return (
<p className="text-muted-foreground italic m-0">
{GetTranslation("uwil-backend::back.table_not_known")}
</p>
);
},
},
{
accessorKey: "created_at",
header: str(