fix: 🚨 resolve last eslint errors

This commit is contained in:
Alexis Gousseau 2026-02-12 15:42:22 +01:00
parent 3a4a96cdce
commit 3a893cd03c
137 changed files with 5239 additions and 4734 deletions

View file

@ -66,5 +66,10 @@ module.exports = {
},
],
'prettier/prettier': 'error',
"no-console": [
"warn", {
allow: ["warn", "error"],
},
],
},
}

View file

@ -62,6 +62,7 @@ class DocumentController extends Controller
/** Custom pagination */
$docModels = $this->paginate($query);
return Inertia::render('Document/Index', [
'documentsModel' => CommonResource::collection($docModels),
'searchFields' => $searchFields,
@ -160,6 +161,16 @@ class DocumentController extends Controller
});
}
/**
* Duplicate the specified resource.
*
* @param \App\Models\Document $document
* @return \Inertia\Response
*/
public function duplicate(Document $document): \Inertia\Response
{
return $this->create($document->replicate());
}
/**
* Destroy the specified resource.

29
package-lock.json generated
View file

@ -28,6 +28,7 @@
"@tiptap/extension-image": "^3.17.0",
"@tiptap/extension-invisible-characters": "^3.17.1",
"@tiptap/extension-italic": "^3.17.0",
"@tiptap/extension-link": "^3.19.0",
"@tiptap/extension-list": "^3.17.1",
"@tiptap/extension-ordered-list": "^3.17.0",
"@tiptap/extension-placeholder": "^3.17.0",
@ -47,6 +48,7 @@
"@types/react-dom": "^19.1.7",
"axios": "^1.13.2",
"chart.js": "^4.5.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
@ -55,6 +57,7 @@
"laravel-vite-plugin": "^1.3.0",
"lucide-react": "^0.503.0",
"motion": "^12.23.12",
"prosemirror-state": "^1.4.4",
"react-color": "^2.19.3",
"react-day-picker": "^9.11.1",
"react-dropzone": "^14.3.8",
@ -86,7 +89,6 @@
"@vitejs/plugin-react": "^4.7.0",
"autoprefixer": "^10.4.21",
"baseline-browser-mapping": "^2.9.19",
"class-variance-authority": "^0.7.1",
"concurrently": "^9.2.0",
"eslint": "^8.57.1",
"eslint-config-airbnb": "^19.0.4",
@ -4023,16 +4025,16 @@
}
},
"node_modules/@tiptap/core": {
"version": "3.17.1",
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.17.1.tgz",
"integrity": "sha512-f8hB9MzXqsuXoF9qXEDEH5Fb3VgwhEFMBMfk9EKN88l5adri6oM8mt2XOWVxVVssjpEW0177zXSLPKWzoS/vrw==",
"version": "3.19.0",
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.19.0.tgz",
"integrity": "sha512-bpqELwPW+DG8gWiD8iiFtSl4vIBooG5uVJod92Qxn3rA9nFatyXRr4kNbMJmOZ66ezUvmCjXVe/5/G4i5cyzKA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/pm": "^3.17.1"
"@tiptap/pm": "^3.19.0"
}
},
"node_modules/@tiptap/extension-blockquote": {
@ -4282,9 +4284,9 @@
}
},
"node_modules/@tiptap/extension-link": {
"version": "3.17.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.17.0.tgz",
"integrity": "sha512-qGL/s7RxgWL1dY3ZPsPtBYQsfiNpwnqoK5dVQ7NbqjphK1EF/Q4uBRVM3CKMZiRBHdRlSVZewBycHs4uyAKPbA==",
"version": "3.19.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.19.0.tgz",
"integrity": "sha512-HEGDJnnCPfr7KWu7Dsq+eRRe/mBCsv6DuI+7fhOCLDJjjKzNgrX2abbo/zG3D/4lCVFaVb+qawgJubgqXR/Smw==",
"license": "MIT",
"dependencies": {
"linkifyjs": "^4.3.2"
@ -4294,8 +4296,8 @@
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^3.17.0",
"@tiptap/pm": "^3.17.0"
"@tiptap/core": "^3.19.0",
"@tiptap/pm": "^3.19.0"
}
},
"node_modules/@tiptap/extension-list": {
@ -4539,9 +4541,9 @@
}
},
"node_modules/@tiptap/pm": {
"version": "3.17.1",
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.17.1.tgz",
"integrity": "sha512-UyVLkN8axV/zop6Se2DCBJRu5DM21X0XEQvwEC5P/vk8eC9OcQZ3FLtxeYy2ZjpAZUzBGLw0/BGsmEip/n7olw==",
"version": "3.19.0",
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.19.0.tgz",
"integrity": "sha512-789zcnM4a8OWzvbD2DL31d0wbSm9BVeO/R7PLQwLIGysDI3qzrcclyZ8yhqOEVuvPitRRwYLq+mY14jz7kY4cw==",
"license": "MIT",
"dependencies": {
"prosemirror-changeset": "^2.3.0",
@ -6088,7 +6090,6 @@
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
"integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"clsx": "^2.1.1"

View file

@ -10,6 +10,7 @@
"eslint-fix": "node ./node_modules/.bin/eslint --config './.eslintrc.cjs' --max-warnings 0 resources/ts --ext .ts,.tsx --fix",
"eslint-ci": "node ./node_modules/.bin/eslint --config './.eslintrc.cjs' --max-warnings 0 resources/ts --ext .ts,.tsx -f junit",
"stylelint": "node ./node_modules/.bin/stylelint --config './.stylelintrc.json' --max-warnings 0 '**/resources/**/*.scss'",
"stylelint-fix": "node ./node_modules/.bin/stylelint --config './.stylelintrc.json' --max-warnings 0 '**/resources/**/*.scss' --fix",
"stylelint-ci": "node ./node_modules/.bin/stylelint --config './.stylelintrc.json' --max-warnings 0 '**/resources/**/*.scss' -f json"
},
"devDependencies": {
@ -33,7 +34,6 @@
"@vitejs/plugin-react": "^4.7.0",
"autoprefixer": "^10.4.21",
"baseline-browser-mapping": "^2.9.19",
"class-variance-authority": "^0.7.1",
"concurrently": "^9.2.0",
"eslint": "^8.57.1",
"eslint-config-airbnb": "^19.0.4",
@ -86,6 +86,7 @@
"@tiptap/extension-image": "^3.17.0",
"@tiptap/extension-invisible-characters": "^3.17.1",
"@tiptap/extension-italic": "^3.17.0",
"@tiptap/extension-link": "^3.19.0",
"@tiptap/extension-list": "^3.17.1",
"@tiptap/extension-ordered-list": "^3.17.0",
"@tiptap/extension-placeholder": "^3.17.0",
@ -105,6 +106,7 @@
"@types/react-dom": "^19.1.7",
"axios": "^1.13.2",
"chart.js": "^4.5.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
@ -113,6 +115,7 @@
"laravel-vite-plugin": "^1.3.0",
"lucide-react": "^0.503.0",
"motion": "^12.23.12",
"prosemirror-state": "^1.4.4",
"react-color": "^2.19.3",
"react-day-picker": "^9.11.1",
"react-dropzone": "^14.3.8",

View file

@ -0,0 +1,101 @@
.sketch-picker {
display: flex;
width: 100% !important;
flex-direction: column;
padding: 0 !important;
border: 0 !important;
box-shadow: none !important;
}
.sketch-picker,
.sketch-picker .flexbox-fix {
gap: 0.375rem;
}
.sketch-picker > div,
.sketch-picker .flexbox-fix > div {
padding: 0 !important;
margin: 0 !important;
}
.sketch-picker > div:first-child {
border-radius: calc(var(--radius) - 2px);
aspect-ratio: 1 / 1;
}
.sketch-picker > div:last-child {
justify-content: space-between;
padding-top: 0.375rem !important;
}
.sketch-picker > div:nth-child(3) > div {
flex: auto !important;
}
.sketch-picker > div:last-child > div {
width: 20px !important;
height: 20px !important;
}
.sketch-picker > div:last-child > div > span > div {
border-radius: calc(var(--radius) - 2px) !important;
box-shadow: none !important;
outline: 1px solid hsl(var(--input)) !important;
}
.sketch-picker > div:nth-child(2) > div:first-child {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.sketch-picker > div:nth-child(2) > div:last-child {
display: none;
}
.sketch-picker > div:nth-child(3) > div:first-child {
width: 16rem;
}
.sketch-picker > div:nth-child(2) > div:first-child > div {
height: 20px !important;
border-radius: calc(var(--radius) - 2px);
margin: 0 !important;
cursor: pointer;
outline: 1px solid hsl(var(--input));
}
/* stylelint-disable-next-line selector-max-compound-selectors */
.sketch-picker > div:first-child > div > div > div:last-child > div {
width: 10px !important;
height: 10px !important;
border-radius: 50% !important;
background-color: #fff;
box-shadow: none !important;
transform: translate(-5px, -5px) !important;
}
/* stylelint-disable-next-line selector-max-compound-selectors */
.sketch-picker > div:nth-child(2) > div:first-child > div > div > div > div > div {
width: 0.5rem !important;
height: 20px !important;
border: 1px hsl(var(--input)) solid;
border-radius: 0 !important;
margin-top: 0 !important;
background: #fff !important;
box-shadow: none !important;
transform: translateX(-4px) !important;
}
.sketch-picker input {
width: 100% !important;
height: 2.5rem;
padding: 0.5rem 0.75rem !important;
border: 1px hsl(var(--input)) solid !important;
border-radius: calc(var(--radius) - 2px) !important;
background-color: hsl(var(--background));
box-shadow: none !important;
color: var(--foreground);
outline: none;
text-align: center;
}

View file

@ -12,19 +12,27 @@ type ApplicationLogoProps = {
isCollapsed?: boolean;
};
/**
* Get the logo.
*
* @param isCollapsed boolean
* @param forceLight boolean
* @param theme string
* @return string
*/
function getLogo(isCollapsed: boolean, forceLight: boolean, theme: string) {
if (isCollapsed) return logoTinyUwilCMS;
if (forceLight || theme === "dark") return logoUwilCMSLight;
return logoUwilCMSDark;
}
export default function ApplicationLogo({
forceLight = false,
isCollapsed = false,
}: ApplicationLogoProps) {
const { theme } = useTheme();
const wrapperClasses = isCollapsed ? "h-8" : "w-full max-w-[10rem] mx-auto";
const logo = isCollapsed
? logoTinyUwilCMS
: forceLight
? logoUwilCMSLight
: theme === "dark"
? logoUwilCMSLight
: logoUwilCMSDark;
const logo = getLogo(isCollapsed, forceLight, theme);
return (
<Link

View file

@ -6,23 +6,25 @@ import { Badge } from "@/components/ui/badge";
// * Other
import DataTable from "@/back/Components/Table/DataTable";
import DataTableFilterButton from "@/back/Components/Table/DataTableFilterButton";
import { CompanyModel, Resource } from "@/back/types/global";
import { Views } from "@/lib/utils";
import { useFormatedDate } from "@/hooks/use-formated-value";
import { Resource } from "@/back/types/global";
import { CompanyModel } from "@/back/types/others";
import { GetFormatedDate, GetTranslation, Views } from "@/lib/utils";
import useTrans from "@/hooks/use-translations";
import str from "@/hooks/use-string";
interface Props {
companies: Resource;
}
function AssociatedCompaniesList(props: Props) {
const { companies } = props;
const columns = [
const getColumns = () => {
return [
{
accessorKey: "name",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(useTrans(`validation.attributes.${column.id}`))
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
@ -36,7 +38,7 @@ function AssociatedCompaniesList(props: Props) {
accessorKey: "published",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(useTrans(`validation.custom.${column.id}`))
label={str(GetTranslation(`validation.custom.${column.id}`))
.ucFirst()
.value()}
field={column.id}
@ -57,7 +59,7 @@ function AssociatedCompaniesList(props: Props) {
<XIcon size={15} className="mr-1" />
)}
{str(
useTrans(
GetTranslation(
row.getValue("published") ? "back.yes" : "back.no",
),
)
@ -70,7 +72,9 @@ function AssociatedCompaniesList(props: Props) {
accessorKey: "updated_at",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(useTrans(`validation.attributes.${column.id}`))
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
@ -78,11 +82,16 @@ function AssociatedCompaniesList(props: Props) {
),
cell: ({ row }: { row: Row<CompanyModel> }) => (
<Badge variant="secondary">
{useFormatedDate(row.getValue("updated_at"))}
{GetFormatedDate(row.getValue("updated_at"))}
</Badge>
),
},
];
};
export default function AssociatedCompaniesList(props: Props) {
const { companies } = props;
const columns = getColumns();
return (
<>
@ -110,5 +119,3 @@ function AssociatedCompaniesList(props: Props) {
</>
);
}
export default AssociatedCompaniesList;

View file

@ -1,6 +1,11 @@
// * Packages
import { Link } from "@inertiajs/react";
import { CSSProperties, MouseEventHandler, ReactNode } from "react";
import {
ButtonHTMLAttributes,
CSSProperties,
MouseEventHandler,
ReactNode,
} from "react";
import { VariantProps } from "class-variance-authority";
// * Components shadcn
import { Button, buttonVariants } from "@/components/ui/button";
@ -9,12 +14,10 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
// * Other
import { cn } from "@/lib/utils";
type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
type ButtonSize = VariantProps<typeof buttonVariants>["size"];
type ButtonType = React.ButtonHTMLAttributes<HTMLButtonElement>["type"];
type ButtonType = ButtonHTMLAttributes<HTMLButtonElement>["type"];
type BtnOptionnalLinkWithTooltipProps = {
link?: string | null;
@ -27,7 +30,7 @@ type BtnOptionnalLinkWithTooltipProps = {
externalLink?: boolean;
disabled?: boolean;
style?: CSSProperties;
onClick?: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
onClick?: MouseEventHandler<HTMLButtonElement>;
};
export default function BtnOptionnalLinkWithTooltip({
@ -44,6 +47,22 @@ export default function BtnOptionnalLinkWithTooltip({
onClick,
...props
}: BtnOptionnalLinkWithTooltipProps) {
let innerContent: ReactNode = content;
if (link !== null) {
if (externalLink) {
innerContent = disabled ? (
<Button disabled>{content}</Button>
) : (
<a href={link} target="_blank" rel="noreferrer">
{content}
</a>
);
} else {
innerContent = <Link href={link}>{content}</Link>;
}
}
return (
<Tooltip>
<TooltipTrigger asChild>
@ -56,26 +75,10 @@ export default function BtnOptionnalLinkWithTooltip({
disabled={disabled}
onClick={onClick}
style={style}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
>
{link !== null ? (
externalLink ? (
<a
href={!disabled ? link : undefined}
target="_blank"
rel="noreferrer"
className={cn({
"pointer-events-none opacity-50": disabled,
})}
>
{content}
</a>
) : (
<Link href={link}>{content}</Link>
)
) : (
content
)}
{innerContent}
</Button>
</TooltipTrigger>
<TooltipContent>{tooltipContent}</TooltipContent>

View file

@ -7,43 +7,39 @@ import str from "@/hooks/use-string";
import DataTable from "@/back/Components/Table/DataTable";
import useTrans from "@/hooks/use-translations";
import DataTableFilterButton from "@/back/Components/Table/DataTableFilterButton";
import {
AppEnv,
DocumentModel,
Resource,
TranslationField,
} from "@/back/types/global";
import { Views } from "@/lib/utils";
import { renderTranslationCell } from "@/lib/translationCellFallback";
import RenderTranslationCell from "@/back/Components/Table/RenderTranslationCell";
import { AppEnv, Resource } from "@/back/types/global";
import { TranslationField } from "@/back/types/i18n";
import { DocumentModel } from "@/back/types/others";
import { GetTranslation, Views } from "@/lib/utils";
interface Props {
documents: Resource;
}
function AssociatedDocumentList(props: Props) {
const { documents } = props;
const { appEnv } = usePage().props as unknown as {
appEnv: AppEnv;
};
const documentColumns = [
const getColumns = (appEnv: AppEnv) => {
return [
{
accessorKey: "name",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(useTrans(`validation.attributes.${column.id}`))
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({ row }: { row: Row<DocumentModel> }) => {
return renderTranslationCell(row.getValue("name"));
},
cell: ({ row }: { row: Row<DocumentModel> }) => (
<RenderTranslationCell value={row.getValue("name")} />
),
},
{
accessorKey: "path",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(useTrans(`validation.custom.${column.id}`))
label={str(GetTranslation(`validation.custom.${column.id}`))
.ucFirst()
.value()}
field={column.id}
@ -71,12 +67,20 @@ function AssociatedDocumentList(props: Props) {
</a>
) : (
<p className="text-muted-foreground italic m-0">
{useTrans("back.table_not_known")}
{GetTranslation("back.table_not_known")}
</p>
);
},
},
];
};
export default function AssociatedDocumentList(props: Props) {
const { documents } = props;
const { appEnv } = usePage().props as unknown as {
appEnv: AppEnv;
};
const documentColumns = getColumns(appEnv);
return (
<>
@ -104,5 +108,3 @@ function AssociatedDocumentList(props: Props) {
</>
);
}
export default AssociatedDocumentList;

View file

@ -7,16 +7,10 @@ import EnumFilter from "@/back/Components/Filters/EnumFilter";
import SwitchFilter from "@/back/Components/Filters/SwitchFilter";
import MultiSelectRelationFilter from "@/back/Components/Filters/MultiSelectRelationFilter";
import FiltersWrapper from "@/back/Components/FiltersWrapper";
import {
DocumentCategoryModel,
DocumentTagModel,
LaravelEnum,
} from "@/back/types/global";
import { LaravelEnum } from "@/back/types/global";
function Filters() {
const modelName = usePage().props.modelName as string;
const categories = usePage().props.categories as DocumentCategoryModel[];
const tags = usePage().props.tags as DocumentTagModel[];
const enumDocumentStatus = usePage().props
.enumDocumentStatus as LaravelEnum;

View file

@ -1,6 +1,5 @@
// * Packages
import { usePage } from "@inertiajs/react";
import { useEffect, useRef } from "react";
// * Other
import SimpleString from "@/back/Components/Inputs/SimpleString";
import SimpleNumber from "@/back/Components/Inputs/SimpleNumber";
@ -14,24 +13,23 @@ import SimpleColorPicker from "@/back/Components/Inputs/SimpleColorPicker";
import SimpleYoutubeId from "@/back/Components/Inputs/SimpleYoutubeId";
import SimpleVimeoId from "@/back/Components/Inputs/SimpleVimeoId";
import EnumSelect from "@/back/Components/Inputs/EnumSelect";
import { ModelSelect } from "@/back/Components/Inputs/ModelSelect";
import StaticRouteSelect from "@/back/Components/Inputs/StaticRouteSelect";
import DynamicWrapperModelSelect from "@/back/Components/DynamicPages/DynamicWrapperModelSelect";
import { LaravelEnum } from "@/back/types/global";
import {
ConfigField,
DynamicFieldTypes,
DynamicInternalField,
LaravelEnum,
} from "@/back/types/global";
} from "@/back/types/dynamic-pages";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
import {
extractExcludedIdFromExists,
extractLaravelValidationValue,
extractMinMaxFromValidation,
extractRequiredValidation,
extractTableFromExists,
} from "@/hooks/use-laravel-validation";
/* eslint-disable no-unused-vars */
type DynamicInputsProps = {
name: string;
config: ConfigField[];
@ -44,6 +42,7 @@ type DynamicInputsProps = {
reset: (name?: string) => void;
parentErrors?: Record<string, string> | null;
};
/* eslint-enable no-unused-vars */
// Allows to get specific input from enum value.
export default function DynamicInputs({
@ -332,55 +331,14 @@ export default function DynamicInputs({
);
// Model select.
case enumDynamicFieldTypes.model.value:
// Extract only the model name from the validation.
const targetModel =
extractTableFromExists(field.validation) ||
"dynamic_pages";
// Add a custom prefix in the last part of the name.
const newName = fieldName.replace(
/([^.\s]+)$/,
"reserved_$1",
);
// Set a state at false.
const initializedRef = useRef(false);
// Set the model name in the custom key only once.
useEffect(() => {
if (initializedRef.current) return;
initializedRef.current = true;
// If the value isn't already present in content.
if (
content &&
!content[
`reserved_${field.key}` as keyof typeof content
]
) {
setData(newName, targetModel);
}
}, [newName, targetModel]);
return (
<ModelSelect
name={fieldName}
multiple={false}
closeOnSelect
responsive
targetModel={targetModel}
label={getTrans(field.label)}
exclude={extractExcludedIdFromExists(
field.validation,
)}
placeholder="ex: John Doe"
defaultValues={
fieldValue ? [Number(fieldValue)] : []
}
required={extractRequiredValidation(
field.validation,
)}
<DynamicWrapperModelSelect
field={field}
fieldName={fieldName}
fieldValue={fieldValue}
content={content}
setData={setData}
getTrans={getTrans}
/>
);
// Static route select.

View file

@ -1,6 +1,5 @@
// * Packages
import { CSS } from "@dnd-kit/utilities";
import { GripVerticalIcon, TrashIcon } from "lucide-react";
import {
closestCenter,
DndContext,
@ -15,8 +14,9 @@ import {
useSortable,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { useState } from "react";
import { usePage } from "@inertiajs/react";
import { useState } from "react";
import { GripVerticalIcon, TrashIcon } from "lucide-react";
// * Components shadcn
import {
Accordion,
@ -35,241 +35,17 @@ import {
extractMinMaxFromValidation,
extractRequiredValidation,
} from "@/hooks/use-laravel-validation";
import { ConfigField, DynamicInternalField } from "@/back/types/global";
import { cn } from "@/lib/utils";
type DynamicListProps = {
name: string;
parentName: string;
value: DynamicInternalField[];
enumLabel: string;
field: ConfigField;
content: DynamicInternalField | null;
isChildren: boolean;
setData: (
name: string,
value: string | boolean | File | object | null,
) => void;
reset: (name?: string) => void;
};
// Allows to get recursive list inputs.
export default function DynamicList({
name,
parentName,
value,
enumLabel,
field,
content,
isChildren,
setData,
reset,
}: DynamicListProps) {
// Notice user there must be children config.
if (!field.children && field.children!.length > 0) {
return (
<p className="text-muted-foreground text-sm">
{useTrans("back.list_children_needed", {
params: { enumLabel },
})}
</p>
);
}
// Errors.
const { errors } = usePage().props;
const [localErrors, setLocalErrors] =
useState<Record<string, string>>(errors);
// Drag & drop module for user interactions (mouse, touch, keyboard or other).
const sensors = useSensors(useSensor(PointerSensor));
// Define a local identifier for each element to enable unique identification during drag and drop.
const localValue = value.map((elmt, index) => {
return { ...elmt, id: elmt.id ?? index + 1 };
});
// Order all elements depending on order values.
const sortedListItems = [...localValue].sort((a, b) => a.order - b.order);
// Extract some values from laravel rules validation.
const isRequired = extractRequiredValidation(field.validation);
const configLimitMax =
extractMinMaxFromValidation(field.validation).max ?? 1;
// Accordion open index.
const [openIndex, setOpenIndex] = useState<string>("");
/**
* Get all elements without each unique identification values.
* @param fields DynamicInternalField[]
* @returns Omit<DynamicInternalField, 'id'>[]
*/
const removeIds = (
fields: DynamicInternalField[],
): Omit<DynamicInternalField, "id">[] => {
return fields.map(({ id, ...rest }) => {
const newObj: any = {};
for (const key in rest) {
const value = rest[key as keyof typeof rest];
// If it is an array, we treat it recursively.
if (Array.isArray(value)) {
newObj[key] = removeIds(value);
}
// Otherwise, we keep the value as it is.
else {
newObj[key] = value;
}
}
return newObj;
});
};
/**
* Remove a specific element from the list.
* @param parentName string
* @param fieldKey string
* @param index number
* @return void
*/
const handleRemoveField = (
parentName: string,
fieldKey: string,
index: number,
) => {
// Get the current list of elements.
const list = Array.isArray(content?.[fieldKey as keyof typeof content])
? // @ts-ignore
[...content[fieldKey as keyof typeof content]]
: [];
// Remove the targeted element.
list.splice(index, 1);
// Save the new list updated.
setData(`${parentName}.${fieldKey}`, list);
// Reset all errors because of alter elements list.
setLocalErrors({});
};
/**
* Reorder the elements list.
* @param event DragEndEvent
* @return void
*/
const handleOnDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
// Check the unique identification value.
if (!over || active.id === over.id) return;
// Get the two elements that are switchable by drag and drop.
const oldIndex = sortedListItems.findIndex((i) => i.id === active.id);
const newIndex = sortedListItems.findIndex((i) => i.id === over.id);
// Reorder all elements list to update visually all elements.
const reorderedList = arrayMove(
sortedListItems,
oldIndex,
newIndex,
).map((item, index) => ({
...item,
order: index + 1,
}));
// Save the new ordered list elements.
setData(`${parentName}.${field.key}`, reorderedList);
};
/**
* Add a new field to the list.
* @return void
*/
const handleAddNewField = () => {
// Set new element with unique identification and order.
const newItem = Object.fromEntries(
(field.children ?? []).map((child) => [child.key, ""]),
);
const maxOrder =
Math.max(0, ...sortedListItems.map((field) => field.order ?? 0)) +
1;
const maxIndex =
Math.max(0, ...sortedListItems.map((field) => field.id ?? 0)) + 1;
const newIndex = value.length;
const newItemWithOrder = { ...newItem, id: maxIndex, order: maxOrder };
// Save the list with the new element.
setData(`${parentName}.${field.key}.${newIndex}`, newItemWithOrder);
};
return (
<div className="col-span-1 md:col-span-2">
<Label htmlFor={parentName}>
{field.label + (isRequired ? " *" : "")}
</Label>
{value.length > 0 && (
<Accordion
type="single"
collapsible
className="w-full"
value={openIndex}
onValueChange={setOpenIndex}
>
{/*
The main provider for dnd-kit.
It manages the entire drag-and-drop system: sensors, movement, collision detection, and lifecycle callbacks (onDragStart, onDragEnd, etc.).
*/}
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleOnDragEnd}
>
{/*
Defines a reorderable list area.
It registers sortable items and applies the chosen sorting strategy (vertical, horizontal, grid) to handle item repositioning.
*/}
<SortableContext
items={sortedListItems.map((item) => item.id)}
strategy={verticalListSortingStrategy}
>
{/* Use the recursive component for each elements. */}
{sortedListItems.map((item, itemIndex) => (
<SortableItem
key={item.id}
id={item.id}
item={item}
itemIndex={itemIndex}
name={name}
parentName={parentName}
field={field}
setData={setData}
reset={reset}
removeField={handleRemoveField}
parentAccordionState={openIndex}
setParentAccordionState={setOpenIndex}
localErrors={localErrors}
/>
))}
</SortableContext>
</DndContext>
</Accordion>
)}
<AddNewFieldBtn
array={sortedListItems}
limitMax={configLimitMax}
onClick={handleAddNewField}
variant={isChildren ? "secondary" : "default"}
/>
</div>
);
}
import { ConfigField, DynamicInternalField } from "@/back/types/dynamic-pages";
import { cn, GetTranslation } from "@/lib/utils";
/* eslint-disable no-unused-vars */
type SortableItemProps = {
id: number;
item: any;
item: DynamicInternalField | null;
itemIndex: number;
name: string;
parentName: string;
field: any;
field: ConfigField;
setData: (
name: string,
value: string | boolean | File | object | null,
@ -280,6 +56,7 @@ type SortableItemProps = {
setParentAccordionState: (key: string) => void;
localErrors: Record<string, string>;
};
/* eslint-enable no-unused-vars */
// Recursive component.
function SortableItem({
@ -350,8 +127,10 @@ function SortableItem({
variant="outline"
size="icon"
disabled={parentAccordionState !== ""}
/* eslint-disable react/jsx-props-no-spreading */
{...listeners}
{...attributes}
/* eslint-enable react/jsx-props-no-spreading */
/>
</div>
{/* Define a delete button to destroy the current element. */}
@ -385,15 +164,17 @@ function SortableItem({
</div>
<AccordionContent className="pt-4 pb-1 px-1">
{/* Get specific input from the config value. */}
<DynamicInputs
name={`${name}.${itemIndex}`}
config={field.children}
content={item}
setData={setData}
reset={reset}
parentErrors={localErrors}
isChildren
/>
{field.children?.length && (
<DynamicInputs
name={`${name}.${itemIndex}`}
config={field.children}
content={item}
setData={setData}
reset={reset}
parentErrors={localErrors}
isChildren
/>
)}
</AccordionContent>
</CardContent>
</Card>
@ -401,3 +182,221 @@ function SortableItem({
</div>
);
}
/* eslint-disable no-unused-vars */
type DynamicListProps = {
name: string;
parentName: string;
value: DynamicInternalField[];
enumLabel: string;
field: ConfigField;
content: DynamicInternalField | null;
isChildren: boolean;
setData: (
name: string,
value: string | boolean | File | object | null,
) => void;
reset: (name?: string) => void;
};
/* eslint-enable no-unused-vars */
// Allows to get recursive list inputs.
export default function DynamicList({
name,
parentName,
value,
enumLabel,
field,
content,
isChildren,
setData,
reset,
}: DynamicListProps) {
// Errors.
const { errors } = usePage().props;
const [localErrors, setLocalErrors] =
useState<Record<string, string>>(errors);
// Drag & drop module for user interactions (mouse, touch, keyboard or other).
const sensors = useSensors(useSensor(PointerSensor));
// Define a local identifier for each element to enable unique identification during drag and drop.
const localValue = value.map((elmt, index) => {
return { ...elmt, id: elmt.id ?? index + 1 };
});
// Order all elements depending on order values.
const sortedListItems = [...localValue].sort((a, b) => a.order - b.order);
// Extract some values from laravel rules validation.
const isRequired = extractRequiredValidation(field.validation);
const configLimitMax =
extractMinMaxFromValidation(field.validation).max ?? 1;
// Accordion open index.
const [openIndex, setOpenIndex] = useState<string>("");
// Notice user there must be children config.
if (!field.children && field.children!.length > 0) {
return (
<p className="text-muted-foreground text-sm">
{GetTranslation("back.list_children_needed", {
params: { enumLabel },
})}
</p>
);
}
/**
* Remove a specific element from the list.
* @param currentParentName string
* @param fieldKey string
* @param index number
* @return void
*/
const handleRemoveField = (
currentParentName: string,
fieldKey: string,
index: number,
) => {
// Get the current list of elements.
const list = Array.isArray(content?.[fieldKey as keyof typeof content])
? // @ts-expect-error Weird error type.
[...content[fieldKey as keyof typeof content]]
: [];
// Remove the targeted element.
list.splice(index, 1);
// Save the new list updated.
setData(`${currentParentName}.${fieldKey}`, list);
// Reset all errors because of alter elements list.
setLocalErrors({});
};
/**
* Reorder the elements list.
* @param event DragEndEvent
* @return void
*/
const handleOnDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
// Check the unique identification value.
if (!over || active.id === over.id) return;
// Get the two elements that are switchable by drag and drop.
const oldIndex = sortedListItems.findIndex((i) => i.id === active.id);
const newIndex = sortedListItems.findIndex((i) => i.id === over.id);
// Reorder all elements list to update visually all elements.
const reorderedList = arrayMove(
sortedListItems,
oldIndex,
newIndex,
).map((item, index) => ({
...item,
order: index + 1,
}));
// Save the new ordered list elements.
setData(`${parentName}.${field.key}`, reorderedList);
};
/**
* Add a new field to the list.
* @return void
*/
const handleAddNewField = () => {
// Set new element with unique identification and order.
const newItem = Object.fromEntries(
(field.children ?? []).map((child) => [child.key, ""]),
);
const maxOrder =
Math.max(
0,
...sortedListItems.map(
(currentField) => currentField.order ?? 0,
),
) + 1;
const maxIndex =
Math.max(
0,
...sortedListItems.map((currentField) => currentField.id ?? 0),
) + 1;
const newIndex = value.length;
const newItemWithOrder = { ...newItem, id: maxIndex, order: maxOrder };
// Save the list with the new element.
setData(`${parentName}.${field.key}.${newIndex}`, newItemWithOrder);
};
return (
<div className="col-span-1 md:col-span-2">
<Label htmlFor={parentName}>
{field.label + (isRequired ? " *" : "")}
</Label>
{value.length > 0 && (
<Accordion
type="single"
collapsible
className="w-full"
value={openIndex}
onValueChange={setOpenIndex}
>
{/*
The main provider for dnd-kit.
It manages the entire drag-and-drop system: sensors, movement, collision detection, and lifecycle callbacks (onDragStart, onDragEnd, etc.).
*/}
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleOnDragEnd}
>
{/*
Defines a reorderable list area.
It registers sortable items and applies the chosen sorting strategy (vertical, horizontal, grid) to handle item repositioning.
*/}
<SortableContext
items={sortedListItems.map((item) => item.id)}
strategy={verticalListSortingStrategy}
>
{/* Use the recursive component for each elements. */}
{sortedListItems.map((item, itemIndex) => (
<SortableItem
key={item.id}
id={item.id}
item={item}
itemIndex={itemIndex}
name={name}
parentName={parentName}
field={field}
setData={setData}
reset={reset}
removeField={(
currentParentName,
fieldKey,
index,
) =>
handleRemoveField(
currentParentName,
fieldKey,
index,
)
}
parentAccordionState={openIndex}
setParentAccordionState={setOpenIndex}
localErrors={localErrors}
/>
))}
</SortableContext>
</DndContext>
</Accordion>
)}
<AddNewFieldBtn
array={sortedListItems}
limitMax={configLimitMax}
onClick={handleAddNewField}
variant={isChildren ? "secondary" : "default"}
/>
</div>
);
}

View file

@ -10,8 +10,125 @@ import {
// * Other
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
import { ConfigField } from "@/back/types/global";
import { cn } from "@/lib/utils";
import { ConfigField } from "@/back/types/dynamic-pages";
import { cn, GetTranslation } from "@/lib/utils";
type ChildItem = Record<string, string | number | object | ChildItem[]>;
const RenderRows = (
currentValue: string | number | object,
currentConfig: ConfigField[],
hasBorders: boolean = false,
path: string = "",
): ReactNode => {
const tableNotKnownTrans = str(useTrans("back.table_not_known"))
.ucFirst()
.value();
return currentConfig.map((field, index) => {
const hasChildren = !!field.children?.length;
const accordionKey = path
? `${path}.${field.key}-${index}`
: `${field.key}-${index}`;
const rowContent = (currentHasChildren: boolean = false) => {
const cellClasses = "break-words whitespace-normal";
const placeholderClasses =
"text-sm text-muted-foreground break-words whitespace-normal";
const fieldValue =
currentValue[field.key as keyof typeof currentValue];
let cellContent: ReactNode;
if (currentHasChildren) {
const count = Array.isArray(fieldValue)
? Object.values(fieldValue).length
: 0;
cellContent = (
<p className={placeholderClasses}>
{GetTranslation("back.element", {
params: { count: String(count) },
choice: count > 1 ? 2 : 1,
})}
</p>
);
} else if (fieldValue !== null && fieldValue !== undefined) {
cellContent = (
<p className={cellClasses}>
{typeof fieldValue === "boolean"
? GetTranslation(
fieldValue ? "back.yes" : "back.no",
)
: fieldValue}
</p>
);
} else {
cellContent = (
<p className={placeholderClasses}>{tableNotKnownTrans}</p>
);
}
return (
<div className="grid grid-cols-[1fr_1fr_auto] items-center w-full gap-4">
<p className={cellClasses}>{field.label}</p>
{cellContent}
{!currentHasChildren && <div className="w-[16px]" />}
</div>
);
};
return (
<AccordionItem
key={accordionKey}
value={accordionKey}
className="border-none"
>
{hasChildren ? (
<AccordionTrigger
className={cn(
"gap-0 px-4",
{ "rounded-none border-b mb-2": hasBorders },
{ "py-2": !hasBorders },
)}
>
{rowContent(hasChildren)}
</AccordionTrigger>
) : (
<div className="px-4 py-2">{rowContent()}</div>
)}
{hasChildren &&
Array.isArray(
currentValue[field.key as keyof typeof currentValue],
) &&
Object.values(
currentValue[field.key as keyof typeof currentValue],
).length > 0 && (
<AccordionContent className="px-4">
<div className="space-y-1">
{Object.values(
currentValue[
field.key as keyof typeof currentValue
] as ChildItem[],
).map((childItem: ChildItem) => (
<div
key={String(childItem.id)}
className="border rounded-md"
>
{RenderRows(
childItem,
field.children!,
false,
`${accordionKey}-${childItem.id}`,
)}
</div>
))}
</div>
</AccordionContent>
)}
</AccordionItem>
);
});
};
type DynamicPageDetailsProps = {
value: string | object;
@ -28,114 +145,6 @@ export default function DynamicPageDetails({
const transValue = str(useTrans("validation.custom.value"))
.ucFirst()
.value();
const tableNotKnownTrans = str(useTrans("back.table_not_known"))
.ucFirst()
.value();
const RenderRows = (
value: string | number | object,
config: ConfigField[],
hasBorders: boolean = false,
path: string = "",
): ReactNode =>
config.map((field, index) => {
const hasChildren = !!field.children?.length;
const accordionKey = path
? `${path}.${field.key}-${index}`
: `${field.key}-${index}`;
const rowContent = (hasChildren: boolean = false) => {
const cellClasses = "break-words whitespace-normal";
const placeholderClasses =
"text-sm text-muted-foreground break-words whitespace-normal";
const fieldValue = value[field.key as keyof typeof value];
const cellContent = hasChildren ? (
(() => {
const count = Array.isArray(fieldValue)
? Object.values(fieldValue).length
: 0;
return (
<p className={placeholderClasses}>
{useTrans("back.element", {
params: { count: String(count) },
choice: count > 1 ? 2 : 1,
})}
</p>
);
})()
) : fieldValue !== null && fieldValue !== undefined ? (
<p className={cellClasses}>
{typeof fieldValue === "boolean"
? useTrans(fieldValue ? "back.yes" : "back.no")
: fieldValue}
</p>
) : (
<p className={placeholderClasses}>{tableNotKnownTrans}</p>
);
return (
<div className="grid grid-cols-[1fr_1fr_auto] items-center w-full gap-4">
<p className={cellClasses}>{field.label}</p>
{cellContent}
{!hasChildren && <div className="w-[16px]" />}
</div>
);
};
return (
<AccordionItem
key={accordionKey}
value={accordionKey}
className="border-none"
>
{hasChildren ? (
<AccordionTrigger
className={cn(
"gap-0 px-4",
{ "rounded-none border-b mb-2": hasBorders },
{ "py-2": !hasBorders },
)}
>
{rowContent(hasChildren)}
</AccordionTrigger>
) : (
<div className="px-4 py-2">{rowContent()}</div>
)}
{hasChildren &&
Array.isArray(value[field.key as keyof typeof value]) &&
Object.values(value[field.key as keyof typeof value])
.length > 0 && (
<AccordionContent className="px-4">
<div className="space-y-1">
{Object.values(
value[field.key as keyof typeof value],
).map(
(
childItem: any,
childIndex: number,
) => (
<div
key={childIndex}
className="border rounded-md"
>
{RenderRows(
childItem,
field.children!,
false,
`${accordionKey}-${childIndex}`,
)}
</div>
),
)}
</div>
</AccordionContent>
)}
</AccordionItem>
);
});
return (
<Accordion type="multiple" className="w-full divide-y">

View file

@ -0,0 +1,74 @@
// * Packages
import { useEffect, useRef } from "react";
// * Other
import { ModelSelect } from "@/back/Components/Inputs/ModelSelect";
import {
extractExcludedIdFromExists,
extractRequiredValidation,
extractTableFromExists,
} from "@/hooks/use-laravel-validation";
import { ConfigField, DynamicInternalField } from "@/back/types/dynamic-pages";
/* eslint-disable no-unused-vars */
type DynamicWrapperModelSelectProps = {
field: ConfigField;
fieldName: string;
fieldValue: string | number | boolean | File | Date;
content: DynamicInternalField | null;
setData: (
name: string,
value: string | number | boolean | File | object | null,
) => void;
getTrans: (fieldLabel: string) => string;
};
/* eslint-enable no-unused-vars */
export default function DynamicWrapperModelSelect({
field,
fieldName,
fieldValue,
content,
setData,
getTrans,
}: DynamicWrapperModelSelectProps) {
// Extract only the model name from the validation.
const targetModel =
extractTableFromExists(field.validation) || "dynamic_pages";
// Add a custom prefix in the last part of the name.
const newName = fieldName.replace(/([^.\s]+)$/, "reserved_$1");
// Set a state at false.
const initializedRef = useRef(false);
// Set the model name in the custom key only once.
useEffect(() => {
if (initializedRef.current) return;
initializedRef.current = true;
// If the value isn't already present in content.
if (
content &&
!content[`reserved_${field.key}` as keyof typeof content]
) {
setData(newName, targetModel);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [newName, targetModel]);
return (
<ModelSelect
name={fieldName}
multiple={false}
closeOnSelect
responsive
targetModel={targetModel}
label={getTrans(field.label)}
exclude={extractExcludedIdFromExists(field.validation)}
placeholder="ex: John Doe"
defaultValues={fieldValue ? [Number(fieldValue)] : []}
required={extractRequiredValidation(field.validation)}
setData={setData}
/>
);
}

View file

@ -11,7 +11,7 @@ import {
import { AppEnv } from "@/back/types/global";
import useTrans from "@/hooks/use-translations";
import str from "@/hooks/use-string";
import { cn } from "@/lib/utils";
import { cn, GetTranslation } from "@/lib/utils";
interface GoToFrontViewProps {
/** Model Name used to translate the helper */
@ -51,8 +51,13 @@ export default function GoToFrontView({
const cleanUrl = url.endsWith("/") ? url.slice(0, -1) : url;
let localePath = "";
if (addLocale && locale !== "fr") {
localePath = `/${locale}`;
}
const definitivePath = [
cleanUrl + (addLocale ? (locale === "fr" ? "" : `/${locale}`) : ""),
cleanUrl + localePath,
additionalGroupPath,
pathField,
]
@ -66,7 +71,7 @@ export default function GoToFrontView({
{isAvailable ? (
<>
{str(
useTrans("back.access_to_front_page", {
GetTranslation("back.access_to_front_page", {
params: {
model: modelName,
},
@ -95,11 +100,14 @@ export default function GoToFrontView({
) : (
<>
{str(
useTrans("back.access_to_front_page_when_published", {
params: {
model: modelName,
GetTranslation(
"back.access_to_front_page_when_published",
{
params: {
model: modelName,
},
},
}),
),
)
.ucFirst()
.value()}

View file

@ -31,8 +31,8 @@ export default function InputError({
<AlertCircleIcon className="h-4 w-4" />
{all && <AlertTitle>{errorsTrans}</AlertTitle>}
<AlertDescription>
{messages.map((errorMessage, errorMessageIndex) => {
return <p key={errorMessageIndex}>{errorMessage}</p>;
{messages.map((errorMessage) => {
return <p key={errorMessage}>{errorMessage}</p>;
})}
</AlertDescription>
</Alert>

View file

@ -14,8 +14,10 @@ import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithT
import useTrans from "@/hooks/use-translations";
import str from "@/hooks/use-string";
import { useToast } from "@/hooks/use-toast";
import { AppEnv, DynamicFieldTypes, FileValidation } from "@/back/types/global";
import { cn } from "@/lib/utils";
import { AppEnv } from "@/back/types/global";
import { DynamicFieldTypes } from "@/back/types/dynamic-pages";
import { FileValidation } from "@/back/types/others";
import { cn, GetTranslation } from "@/lib/utils";
type FileWithPreview = FileWithPath & {
preview: string;
@ -59,23 +61,35 @@ export default function CompleteInputFile(props: CompleteInputFileProps) {
fileName = "image",
hasRefreshBtn = true,
hasFile,
parentErrors,
} = props;
const { toast } = useToast();
const { errors } = usePage().props;
const { url } = usePage().props.appEnv as AppEnv;
const [isReset, setIsReset] = useState<boolean>(false);
const [isDialogOpen, setDialogOpen] = useState(false);
const [accept, setAccept] = useState<Record<string, Array<string>>>({
"image/*": [],
});
const [extensions, setExtensions] = useState<string>();
const finalFileName = `${fileName}.png`;
const transValueIntoFile = (localData: string | undefined | null) => {
if (localData) {
const fileName = getFileNameFromPath(localData);
return {
preview: `${url}/${localData}`,
name: fileName ?? "",
} as FileWithPreview;
}
return null;
const isRefreshDisabled = value != null && typeof value === "string";
const localErrors = parentErrors ?? errors;
const localValidationRules = {
width: validationRules?.width
? parseInt(validationRules.width, 10)
: 250,
height: validationRules?.height
? parseInt(validationRules?.height, 10)
: 250,
};
const localMaxFileSize = maxFileSize
? parseInt(maxFileSize, 10) * 1024
: 50000;
/**
* Get the fileName from the path given by the database value
* @param path string|null
@ -89,7 +103,16 @@ export default function CompleteInputFile(props: CompleteInputFileProps) {
return last || null;
};
const isRefreshDisabled = value != null && typeof value === "string";
const transValueIntoFile = (localData: string | undefined | null) => {
if (localData) {
const currentFileName = getFileNameFromPath(localData);
return {
preview: `${url}/${localData}`,
name: currentFileName ?? "",
} as FileWithPreview;
}
return null;
};
const [selectedFile, setSelectedFile] = useState<FileWithPreview | null>(
() => {
@ -105,46 +128,6 @@ export default function CompleteInputFile(props: CompleteInputFileProps) {
},
);
const localErrors = props.parentErrors ?? errors;
const [isDialogOpen, setDialogOpen] = useState(false);
const [accept, setAccept] = useState<Record<string, Array<string>>>({
"image/*": [],
});
const [extensions, setExtensions] = useState<string>();
const localValidationRules = {
width: validationRules?.width
? parseInt(validationRules.width, 10)
: 250,
height: validationRules?.height
? parseInt(validationRules?.height, 10)
: 250,
};
const localMaxFileSize = maxFileSize
? parseInt(maxFileSize, 10) * 1024
: 50000;
// Translations
const transSelectImgZone = useTrans("back.image_cropper.select_img_zone");
const transValidationAllFormats = str(
useTrans("back.image_cropper.validation.all_formats"),
)
.ucFirst()
.value();
const getTransValidationAllowedFormats = (extensions: string) => {
return str(
useTrans("back.image_cropper.validation.allowed_formats", {
params: {
formats: extensions,
},
}),
)
.ucFirst()
.value();
};
// Fonction utilitaire pour formater la taille de fichier
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return "0 Bytes";
@ -156,6 +139,38 @@ export default function CompleteInputFile(props: CompleteInputFileProps) {
return `${currentBytes} ${currentSize}`;
};
// Translations
const transSelectImgZone = useTrans("back.image_cropper.select_img_zone");
const transValidationAllFormats = str(
useTrans("back.image_cropper.validation.all_formats"),
)
.ucFirst()
.value();
const transCropperFileUploaded = useTrans(
"back.image_cropper.state_file_uploaded",
);
const transCancel = useTrans("back.cancel");
const transRulesWeight = useTrans(
"back.image_cropper.validation.rules_weight",
{
params: {
size: formatFileSize(localMaxFileSize).toString(),
},
},
);
const transRefresh = useTrans("back.refresh_original");
const getTransValidationAllowedFormats = (targetExtensions: string) => {
return str(
GetTranslation("back.image_cropper.validation.allowed_formats", {
params: {
formats: targetExtensions,
},
}),
)
.ucFirst()
.value();
};
const buildDropzoneAccept = (): Record<string, string[]> => {
if (!allowedFormats) return {};
@ -322,11 +337,7 @@ export default function CompleteInputFile(props: CompleteInputFileProps) {
<div className="flex flex-col justify-center items-center">
<FileCheck />
<p className="text-center break-normal w-full mt-2">
{str(
useTrans(
"back.image_cropper.state_file_uploaded",
),
)
{str(transCropperFileUploaded)
.ucFirst()
.value()}
</p>
@ -342,7 +353,7 @@ export default function CompleteInputFile(props: CompleteInputFileProps) {
setSelectedFile(null);
setData(name, null);
}}
tooltipContent={str(useTrans("back.cancel"))
tooltipContent={str(transCancel)
.ucFirst()
.value()}
/>
@ -371,21 +382,7 @@ export default function CompleteInputFile(props: CompleteInputFileProps) {
extensions,
)
: transValidationAllFormats}
.{" "}
{str(
useTrans(
"back.image_cropper.validation.rules_weight",
{
params: {
size: formatFileSize(
localMaxFileSize,
).toString(),
},
},
),
)
.ucFirst()
.value()}
. {str(transRulesWeight).ucFirst().value()}
</small>
</div>
</Button>
@ -405,9 +402,7 @@ export default function CompleteInputFile(props: CompleteInputFileProps) {
}
}}
content={<RefreshCwIcon />}
tooltipContent={str(useTrans("back.refresh_original"))
.ucFirst()
.value()}
tooltipContent={str(transRefresh).ucFirst().value()}
/>
)}
</div>

View file

@ -12,7 +12,7 @@ import {
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
// * Other
import BtnOptionnalLinkWithTooltip from "@/back/Components//BtnOptionnalLinkWithTooltip";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import InputError from "@/back/Components/InputError";
import { AppEnv } from "@/back/types/global";
import { cn } from "@/lib/utils";
@ -29,7 +29,9 @@ type EnumSelectProps = {
required?: boolean;
disabled?: boolean;
parentErrors?: Record<string, string> | null;
// eslint-disable-next-line no-unused-vars
setData: (name: string, value: number | null) => void;
// eslint-disable-next-line no-unused-vars
onSelectValue?: (value: number | null) => void;
};
@ -53,7 +55,7 @@ export default function EnumSelect({
// List of options.
const initalOptions = Object.fromEntries(
Object.entries(options).filter(
([key, item]) => !exclude.includes(item.value),
([, item]) => !exclude.includes(item.value),
),
);
const [currentOptions, setCurrentOptions] =
@ -74,6 +76,9 @@ export default function EnumSelect({
// Component states.
const [open, setOpen] = useState<boolean>(false);
const transNoResults = useTrans("back.table_no_results");
const transDelete = useTrans("crud.actions.delete");
/**
* Get the identification field of specific option.
* @param option Model
@ -130,13 +135,14 @@ export default function EnumSelect({
}
const optionsFiltered = Object.fromEntries(
Object.entries(initalOptions).filter(([key, item]) =>
Object.entries(initalOptions).filter(([, item]) =>
item.label.toLowerCase().includes(search),
),
);
// Get updated options list with search.
setCurrentOptions(optionsFiltered);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [search, options]);
return (
@ -201,7 +207,7 @@ export default function EnumSelect({
>
{Object.values(currentOptions).length === 0 && (
<p className="text-sm text-muted-foreground text-center py-5">
{useTrans("back.table_no_results")}
{transNoResults}
</p>
)}
<div className="flex flex-col justify-between">
@ -229,11 +235,7 @@ export default function EnumSelect({
<BtnOptionnalLinkWithTooltip
content={<XIcon />}
tooltipContent={
<p>
{str(useTrans("crud.actions.delete"))
.ucFirst()
.value()}
</p>
<p>{str(transDelete).ucFirst().value()}</p>
}
type="button"
variant="destructive"

View file

@ -49,10 +49,10 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
// * Other
import useTrans, { UseTransOptions } from "@/hooks/use-translations";
import useTrans from "@/hooks/use-translations";
import str from "@/hooks/use-string";
import useDeviceType from "@/hooks/use-device-type";
import { cn } from "@/lib/utils";
import { cn, GetTranslation } from "@/lib/utils";
interface CropValidationRules {
width: number;
@ -166,13 +166,8 @@ export default function ImageCropper({
const deltaX = cropCenterX - imageCenterX;
const deltaY = cropCenterY - imageCenterY;
// Calculer le ratio de qualité : on veut travailler à la résolution originale
// Le ratio est : taille originale / taille affichée
const qualityRatio = 1 / displayScale;
// Taille du canvas temporaire basée sur la résolution originale de la zone croppée
// On limite toutefois pour éviter des canvas trop grands (max 4x la taille finale)
const maxRatio = Math.min(qualityRatio, 4);
const tmpWidth = Math.ceil(crop.width / displayScale);
const tmpHeight = Math.ceil(crop.height / displayScale);
@ -534,10 +529,6 @@ export default function ImageCropper({
.ucFirst()
.value();
const GetTranslation = (keyName: string, options?: UseTransOptions) => {
return useTrans(keyName, options);
};
// Filters (brightness, contrast, saturation, sepia, grayscale).
const imageFilterOptions = [
{

View file

@ -24,6 +24,8 @@ import {
ChevronLeftIcon,
ChevronRightIcon,
} from "lucide-react";
import axios, { isAxiosError } from "axios";
import { route } from "ziggy-js";
// * Components shadcn
import { Separator } from "@/components/ui/separator";
import { Button } from "@/components/ui/button";
@ -42,21 +44,14 @@ import {
} from "@/components/ui/command";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton";
// * Other
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import InputError from "@/back/Components/InputError";
import useTrans from "@/hooks/use-translations";
import str from "@/hooks/use-string";
import { cn } from "@/lib/utils";
import axios from "axios";
import { route } from "ziggy-js";
import {
AppEnv,
Model,
Paginator,
TranslationField,
} from "@/back/types/global";
import { Skeleton } from "@/components/ui/skeleton";
import { cn, GetTranslation } from "@/lib/utils";
import { AppEnv, DefaultModel, Paginator } from "@/back/types/global";
import { TranslationField } from "@/back/types/i18n";
/**
* Animation types and configurations
@ -115,13 +110,15 @@ interface ModelSelectProps
* Callback function triggered when the selected values change.
* Receives an array of the new selected values.
*/
// eslint-disable-next-line no-unused-vars
onValueChange?: (value: number[]) => void;
/**
* Callback function triggered when the selected values change.
* Receives a single or array of selected Model(s).
*/
onModelChange?: (value: Model[]) => void;
// eslint-disable-next-line no-unused-vars
onModelChange?: (value: DefaultModel[]) => void;
/** The default selected values when the component mounts. */
defaultValues?: number[];
@ -161,6 +158,7 @@ interface ModelSelectProps
* If true, renders the multi-select component as a child of another component.
* Optional, defaults to false.
*/
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
asChild?: boolean;
/**
@ -294,6 +292,7 @@ interface ModelSelectProps
/**
* The fonction to set the data in useForm inertia
*/
// eslint-disable-next-line no-unused-vars
setData: (name: string, value: number[] | number | null) => void;
/**
@ -357,6 +356,7 @@ export interface ModelSelectRef {
/**
* Set selected values programmatically
*/
// eslint-disable-next-line no-unused-vars
setSelectedValues: (values: number[]) => void;
/**
* Clear all selected values
@ -388,6 +388,7 @@ export const ModelSelect = forwardRef<ModelSelectRef, ModelSelectProps>(
animationConfig,
maxCount = 3,
modalPopover = false,
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
asChild = false,
className,
hideSelectAll = false,
@ -415,7 +416,9 @@ export const ModelSelect = forwardRef<ModelSelectRef, ModelSelectProps>(
const [selectedValues, setSelectedValues] =
useState<number[]>(defaultValues);
const [selectedModels, setSelectedModels] = useState<Model[]>([]);
const [selectedModels, setSelectedModels] = useState<DefaultModel[]>(
[],
);
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const [isAnimating, setIsAnimating] = useState(false);
@ -445,8 +448,8 @@ export const ModelSelect = forwardRef<ModelSelectRef, ModelSelectProps>(
const [loading, setLoading] = useState<boolean>(false);
const [initialLoading, setInitialLoading] = useState<boolean>(true);
const [options, setOptions] = useState<Model[]>([]);
const [optionsMerged, setOptionsMerged] = useState<Model[]>([]);
const [options, setOptions] = useState<DefaultModel[]>([]);
const [optionsMerged, setOptionsMerged] = useState<DefaultModel[]>([]);
// Custom by field parameter or predefined values (title, name or label).
const [dynamicField, setDynamicField] = useState<string | null>(
field ?? null,
@ -489,16 +492,6 @@ export const ModelSelect = forwardRef<ModelSelectRef, ModelSelectProps>(
return response.data;
};
/**
* Get specific page.
* @param pageNumber number
* @return void
*/
const goToPage = async (pageNumber: number) => {
if (pageNumber < 1 || pageNumber > lastPage) return;
await getData(pageNumber);
};
/**
* Get new options list with his target field.
* @param pageNumber number
@ -506,27 +499,30 @@ export const ModelSelect = forwardRef<ModelSelectRef, ModelSelectProps>(
*/
const getData = async (pageNumber = 1) => {
// Increment search id.
const requestId = ++requestIdRef.current;
requestIdRef.current += 1;
const requestId = requestIdRef.current;
setLoading(true);
try {
const data = (await fetchOptions(pageNumber)) as {
items: Paginator;
field: string;
model: Model[];
model: DefaultModel[];
};
// Ignore obsolete answers.
if (requestId !== requestIdRef.current) return;
const dataModel: Model[] = data.model
const dataModel: DefaultModel[] = data.model
? Object.values(data.model)
: [];
const dataItems: Model[] = Array.isArray(data?.items?.data)
const dataItems: DefaultModel[] = Array.isArray(
data?.items?.data,
)
? data.items.data
: [];
setOptionsMerged(() => {
const merged = [...dataModel, ...dataItems]
// Supprime les éléments undefined ou sans id
.filter(
(item): item is Model =>
(item): item is DefaultModel =>
!!item && item.id !== undefined,
)
// Supprime les doublons par id
@ -542,11 +538,14 @@ export const ModelSelect = forwardRef<ModelSelectRef, ModelSelectProps>(
setOptions(data.items.data ?? []);
setPage(data.items.current_page);
setLastPage(data.items.last_page ?? 1);
} catch (err: any) {
} catch (err: unknown) {
// Ignore obsolete errors.
if (requestId !== requestIdRef.current) return;
console.error(err);
setErrorsAjax(err.response?.data?.errors);
if (isAxiosError(err) && err.response?.data?.errors) {
setErrorsAjax(err.response?.data?.errors);
}
setOptions([]);
setLastPage(1);
} finally {
@ -557,23 +556,42 @@ export const ModelSelect = forwardRef<ModelSelectRef, ModelSelectProps>(
setInitialLoading(false);
}
};
/**
* Get specific page.
* @param pageNumber number
* @return void
*/
const goToPage = async (pageNumber: number) => {
if (pageNumber < 1 || pageNumber > lastPage) return;
await getData(pageNumber);
};
useEffect(() => {
if (disabled) {
return;
return undefined;
}
// Reset options list if empty search or null
if (targetModel && searchValue.length === 0) {
getData();
return;
return undefined;
}
// Nothing to do if search is below 3 caracters.
if (searchValue.length < 3) return;
if (searchValue.length < 3) return undefined;
// Get updated options list with search.
const handler = setTimeout(() => getData(), 300);
return () => clearTimeout(handler);
function cleanup(): void {
if (handler) {
clearTimeout(handler);
}
}
return cleanup;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchValue, targetModel]);
const announce = useCallback(
@ -595,15 +613,19 @@ export const ModelSelect = forwardRef<ModelSelectRef, ModelSelectProps>(
const selectedCountId = `${multiSelectId}-count`;
// Translations.
const transDelete = useTrans("crud.actions.delete");
const transClose = useTrans("back.close");
const transClear = useTrans("back.clear");
const transNoResults = useTrans("back.table_no_results");
const transSearchOptions = useTrans("back.multi_select.search_options");
const transSelectAll = useTrans("back.multi_select.select_all");
const transMore = useTrans("back.multi_select.more");
const transClosePopover = useTrans("back.multi_select.close_popover");
const transClearList = useTrans("back.multi_select.clear_list");
const transDelete = GetTranslation("crud.actions.delete");
const transClose = GetTranslation("back.close");
const transClear = GetTranslation("back.clear");
const transNoResults = GetTranslation("back.table_no_results");
const transSearchOptions = GetTranslation(
"back.multi_select.search_options",
);
const transSelectAll = GetTranslation("back.multi_select.select_all");
const transMore = GetTranslation("back.multi_select.more");
const transClosePopover = GetTranslation(
"back.multi_select.close_popover",
);
const transClearList = GetTranslation("back.multi_select.clear_list");
const prevDefaultValueRef = useRef<number[]>(defaultValues);
@ -622,6 +644,7 @@ export const ModelSelect = forwardRef<ModelSelectRef, ModelSelectProps>(
onValueChange(defaultValues);
}
setData(name, multiple ? defaultValues : defaultValues[0]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultValues, onValueChange]);
const buttonRef = useRef<HTMLButtonElement>(null);
@ -665,6 +688,7 @@ export const ModelSelect = forwardRef<ModelSelectRef, ModelSelectProps>(
}
},
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[resetToDefault, selectedValues, onValueChange],
);
@ -673,7 +697,7 @@ export const ModelSelect = forwardRef<ModelSelectRef, ModelSelectProps>(
>("desktop");
useEffect(() => {
if (typeof window === "undefined") return;
if (typeof window === "undefined") return undefined;
const handleResize = () => {
const width = window.innerWidth;
if (width < 640) {
@ -780,13 +804,12 @@ export const ModelSelect = forwardRef<ModelSelectRef, ModelSelectProps>(
return "";
};
const getAllOptions = useCallback((): Model[] => {
const getAllOptions = useCallback((): DefaultModel[] => {
if (optionsMerged.length === 0) return [];
let allOptions: Model[];
allOptions = optionsMerged;
const allOptions: DefaultModel[] = optionsMerged;
const valueSet = new Set<string>();
const duplicates: string[] = [];
const uniqueOptions: Model[] = [];
const uniqueOptions: DefaultModel[] = [];
allOptions.forEach((option) => {
if (valueSet.has(option.id.toString())) {
duplicates.push(option.id.toString());
@ -817,13 +840,11 @@ export const ModelSelect = forwardRef<ModelSelectRef, ModelSelectProps>(
);
}
return deduplicateOptions ? uniqueOptions : allOptions;
}, [options, deduplicateOptions]);
}, [optionsMerged, deduplicateOptions]);
const getOptionByValue = useCallback(
(id: number): Model | undefined => {
const option = getAllOptions().find(
(option) => option.id === id,
);
(id: number): DefaultModel | undefined => {
const option = getAllOptions().find((opt) => opt.id === id);
if (!option && process.env.NODE_ENV === "development") {
console.warn(
`MultiSelect: Option with value "${id}" not found in options list`,
@ -860,7 +881,7 @@ export const ModelSelect = forwardRef<ModelSelectRef, ModelSelectProps>(
}
};
const toggleOption = (option: Model) => {
const toggleOption = (option: DefaultModel) => {
if (disabled) return;
// MODE SINGLE SELECT
if (!multiple) {
@ -1067,73 +1088,77 @@ export const ModelSelect = forwardRef<ModelSelectRef, ModelSelectProps>(
}
}, [isPopoverOpen]);
useEffect(() => {
const selectedCount = selectedValues.length;
const allOptions = getAllOptions();
const totalOptions = allOptions.length;
if (selectedCount !== prevSelectedCount.current) {
const diff = selectedCount - prevSelectedCount.current;
if (diff > 0) {
const addedItems = selectedValues.slice(-diff);
const addedLabels = addedItems
.map(
(value) =>
allOptions.find((opt) => opt.id === value)?.[
dynamicField as keyof Model
],
)
.filter(Boolean);
useEffect(
() => {
const selectedCount = selectedValues.length;
const allOptions = getAllOptions();
const totalOptions = allOptions.length;
if (selectedCount !== prevSelectedCount.current) {
const diff = selectedCount - prevSelectedCount.current;
if (diff > 0) {
const addedItems = selectedValues.slice(-diff);
const addedLabels = addedItems
.map(
(value) =>
allOptions.find(
(opt) => opt.id === value,
)?.[dynamicField as keyof DefaultModel],
)
.filter(Boolean);
if (addedLabels.length === 1) {
if (addedLabels.length === 1) {
announce(
`${addedLabels[0]} selected. ${selectedCount} of ${totalOptions} options selected.`,
);
} else {
announce(
`${addedLabels.length} options selected. ${selectedCount} of ${totalOptions} total selected.`,
);
}
} else if (diff < 0) {
announce(
`${addedLabels[0]} selected. ${selectedCount} of ${totalOptions} options selected.`,
);
} else {
announce(
`${addedLabels.length} options selected. ${selectedCount} of ${totalOptions} total selected.`,
`Option removed. ${selectedCount} of ${totalOptions} options selected.`,
);
}
} else if (diff < 0) {
announce(
`Option removed. ${selectedCount} of ${totalOptions} options selected.`,
);
prevSelectedCount.current = selectedCount;
}
prevSelectedCount.current = selectedCount;
}
if (isPopoverOpen !== prevIsOpen.current) {
if (isPopoverOpen) {
announce(
`Dropdown opened. ${totalOptions} options available. Use arrow keys to navigate.`,
);
} else {
announce("Dropdown closed.");
if (isPopoverOpen !== prevIsOpen.current) {
if (isPopoverOpen) {
announce(
`Dropdown opened. ${totalOptions} options available. Use arrow keys to navigate.`,
);
} else {
announce("Dropdown closed.");
}
prevIsOpen.current = isPopoverOpen;
}
prevIsOpen.current = isPopoverOpen;
}
if (
searchValue !== prevSearchValue.current &&
searchValue !== undefined
) {
if (searchValue && isPopoverOpen) {
const filteredCount = allOptions.length;
if (
searchValue !== prevSearchValue.current &&
searchValue !== undefined
) {
if (searchValue && isPopoverOpen) {
const filteredCount = allOptions.length;
announce(
`${filteredCount} option${
filteredCount === 1 ? "" : "s"
} found for "${searchValue}"`,
);
announce(
`${filteredCount} option${
filteredCount === 1 ? "" : "s"
} found for "${searchValue}"`,
);
}
prevSearchValue.current = searchValue;
}
prevSearchValue.current = searchValue;
}
}, [
selectedValues,
isPopoverOpen,
searchValue,
announce,
getAllOptions,
]);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[
selectedValues,
isPopoverOpen,
searchValue,
announce,
getAllOptions,
],
);
useEffect(() => {
if (
@ -1161,10 +1186,10 @@ export const ModelSelect = forwardRef<ModelSelectRef, ModelSelectProps>(
}
}, [minSelection, maxSelection, multiple]);
const getModelField = (option: Model): string => {
const value = option[dynamicField as keyof Model] as unknown as
| TranslationField
| string;
const getModelField = (option: DefaultModel): string => {
const value = option[
dynamicField as keyof DefaultModel
] as unknown as TranslationField | string;
if (typeof value === "object") return String(value[locale] ?? "");
return String(value ?? "");
};
@ -1209,7 +1234,7 @@ export const ModelSelect = forwardRef<ModelSelectRef, ModelSelectProps>(
} selected: ${selectedValues
.map((value) => {
const option = getOptionByValue(value);
if (!option) return;
if (!option) return undefined;
return getModelField(option);
})
.filter(Boolean)
@ -1218,6 +1243,7 @@ export const ModelSelect = forwardRef<ModelSelectRef, ModelSelectProps>(
<PopoverTrigger asChild>
<Button
ref={buttonRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...buttonProps}
onClick={handleTogglePopover}
disabled={disabled}
@ -1247,225 +1273,7 @@ export const ModelSelect = forwardRef<ModelSelectRef, ModelSelectProps>(
maxWidth: `min(${widthConstraints.maxWidth}, 100%)`,
}}
>
{selectedValues.length > 0 ? (
<>
{initialLoading ? (
<Skeleton className="w-64 h-3 rounded-full" />
) : (
<div className="flex justify-between items-stretch w-full gap-2">
<div
className={cn(
"flex items-center gap-1",
singleLine
? "overflow-x-auto multiselect-singleline-scroll"
: "flex-wrap",
responsiveSettings.compactMode &&
"gap-0.5",
)}
>
{selectedValues
.slice(
0,
responsiveSettings.maxCount,
)
.map((value) => {
const option =
getOptionByValue(
value,
);
if (!option) {
return null;
}
const badgeStyle: CSSProperties =
{
animationDuration: `${animation}s`,
};
return multiple ? (
<Badge
key={value}
className={cn(
getBadgeAnimationClass(),
modelSelectVariants(
{
variant:
multiple
? variant
: "ghost",
},
),
responsiveSettings.compactMode &&
"text-xs px-1.5 py-0.5",
screenSize ===
"mobile" &&
"max-w-[120px] truncate",
singleLine &&
"flex-shrink-0 whitespace-nowrap",
"[&>svg]:pointer-events-auto",
)}
style={{
...badgeStyle,
animationDuration: `${
animationConfig?.duration ||
animation
}s`,
animationDelay: `${animationConfig?.delay || 0}s`,
}}
>
<span
className={cn(
screenSize ===
"mobile" &&
"truncate",
)}
>
{getModelField(
option,
)}
</span>
<div
role="button"
tabIndex={0}
onClick={(
event,
) => {
event.stopPropagation();
toggleOption(
option,
);
}}
onKeyDown={(
event,
) => {
if (
event.key ===
"Enter" ||
event.key ===
" "
) {
event.preventDefault();
event.stopPropagation();
toggleOption(
option,
);
}
}}
aria-label={`Remove ${option[dynamicField as keyof Model]} from selection`}
className="ml-2 h-4 w-4 cursor-pointer hover:bg-white/20 rounded-sm -m-0.5 focus:outline-none focus:ring-1 focus:ring-white/50"
>
<XCircleIcon
className={cn(
"h-3 w-3",
responsiveSettings.compactMode &&
"h-2.5 w-2.5",
)}
/>
</div>
</Badge>
) : (
<p
className={cn(
"flex items-center overflow-hidden whitespace-nowrap max-w-[180px]",
singleLine &&
"flex-shrink-0",
screenSize ===
"mobile" &&
"max-w-[120px]",
responsiveSettings.compactMode &&
"text-xs px-1.5 py-0.5",
)}
>
<span className="truncate">
{getModelField(
option,
)}
</span>
</p>
);
})
.filter(Boolean)}
{selectedValues.length >
responsiveSettings.maxCount && (
<Badge
className={cn(
"bg-transparent text-foreground border-foreground/1 hover:bg-transparent",
getBadgeAnimationClass(),
modelSelectVariants(
{
variant,
},
),
responsiveSettings.compactMode &&
"text-xs px-1.5 py-0.5",
singleLine &&
"flex-shrink-0 whitespace-nowrap",
"[&>svg]:pointer-events-auto",
)}
style={{
animationDuration: `${animationConfig?.duration || animation}s`,
animationDelay: `${animationConfig?.delay || 0}s`,
}}
>
{`+ ${
selectedValues.length -
responsiveSettings.maxCount
} ${transMore}`}
<XCircleIcon
className={cn(
"ml-2 h-4 w-4 cursor-pointer",
responsiveSettings.compactMode &&
"ml-1 h-3 w-3",
)}
onClick={(
event,
) => {
event.stopPropagation();
clearExtraOptions();
}}
/>
</Badge>
)}
</div>
<div className="flex items-center justify-between">
{!required && (
<Button
role="button"
variant="ghost"
tabIndex={0}
onClick={(event) => {
event.stopPropagation();
handleClear();
}}
onKeyDown={(event) => {
if (
event.key ===
"Enter" ||
event.key ===
" "
) {
event.preventDefault();
event.stopPropagation();
handleClear();
}
}}
aria-label={`Clear all ${selectedValues.length} selected options`}
className="flex items-center justify-center h-4 w-4 p-0 me-2 cursor-pointer transition-opacity opacity-50 group-hover:opacity-60 hover:opacity-100 rounded-sm"
>
<XIcon className="h-4 w-4" />
</Button>
)}
<Separator
orientation="vertical"
className="flex min-h-4 h-full"
/>
<ChevronsUpDownIcon
className="h-4 ms-2 cursor-pointer opacity-50 group-hover/button:opacity-60"
aria-hidden="true"
/>
</div>
</div>
)}
</>
) : (
{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">
{placeholder}
@ -1473,6 +1281,215 @@ export const ModelSelect = forwardRef<ModelSelectRef, ModelSelectProps>(
<ChevronsUpDownIcon className="h-4 cursor-pointer opacity-50 group-hover/button:opacity-60" />
</div>
)}
{selectedValues.length > 0 &&
(initialLoading ? (
<Skeleton className="w-64 h-3 rounded-full" />
) : (
<div className="flex justify-between items-stretch w-full gap-2">
<div
className={cn(
"flex items-center gap-1",
singleLine
? "overflow-x-auto multiselect-singleline-scroll"
: "flex-wrap",
responsiveSettings.compactMode &&
"gap-0.5",
)}
>
{selectedValues
.slice(
0,
responsiveSettings.maxCount,
)
.map((value) => {
const option =
getOptionByValue(value);
if (!option) {
return null;
}
const badgeStyle: CSSProperties =
{
animationDuration: `${animation}s`,
};
return multiple ? (
<Badge
key={value}
className={cn(
getBadgeAnimationClass(),
modelSelectVariants(
{
variant:
multiple
? variant
: "ghost",
},
),
responsiveSettings.compactMode &&
"text-xs px-1.5 py-0.5",
screenSize ===
"mobile" &&
"max-w-[120px] truncate",
singleLine &&
"flex-shrink-0 whitespace-nowrap",
"[&>svg]:pointer-events-auto",
)}
style={{
...badgeStyle,
animationDuration: `${
animationConfig?.duration ||
animation
}s`,
animationDelay: `${animationConfig?.delay || 0}s`,
}}
>
<span
className={cn(
screenSize ===
"mobile" &&
"truncate",
)}
>
{getModelField(
option,
)}
</span>
<div
role="button"
tabIndex={0}
onClick={(
event,
) => {
event.stopPropagation();
toggleOption(
option,
);
}}
onKeyDown={(
event,
) => {
if (
event.key ===
"Enter" ||
event.key ===
" "
) {
event.preventDefault();
event.stopPropagation();
toggleOption(
option,
);
}
}}
aria-label={`Remove ${option[dynamicField as keyof DefaultModel]} from selection`}
className="ml-2 h-4 w-4 cursor-pointer hover:bg-white/20 rounded-sm -m-0.5 focus:outline-none focus:ring-1 focus:ring-white/50"
>
<XCircleIcon
className={cn(
"h-3 w-3",
responsiveSettings.compactMode &&
"h-2.5 w-2.5",
)}
/>
</div>
</Badge>
) : (
<p
className={cn(
"flex items-center overflow-hidden whitespace-nowrap max-w-[180px]",
singleLine &&
"flex-shrink-0",
screenSize ===
"mobile" &&
"max-w-[120px]",
responsiveSettings.compactMode &&
"text-xs px-1.5 py-0.5",
)}
>
<span className="truncate">
{getModelField(
option,
)}
</span>
</p>
);
})
.filter(Boolean)}
{selectedValues.length >
responsiveSettings.maxCount && (
<Badge
className={cn(
"bg-transparent text-foreground border-foreground/1 hover:bg-transparent",
getBadgeAnimationClass(),
modelSelectVariants({
variant,
}),
responsiveSettings.compactMode &&
"text-xs px-1.5 py-0.5",
singleLine &&
"flex-shrink-0 whitespace-nowrap",
"[&>svg]:pointer-events-auto",
)}
style={{
animationDuration: `${animationConfig?.duration || animation}s`,
animationDelay: `${animationConfig?.delay || 0}s`,
}}
>
{`+ ${
selectedValues.length -
responsiveSettings.maxCount
} ${transMore}`}
<XCircleIcon
className={cn(
"ml-2 h-4 w-4 cursor-pointer",
responsiveSettings.compactMode &&
"ml-1 h-3 w-3",
)}
onClick={(event) => {
event.stopPropagation();
clearExtraOptions();
}}
/>
</Badge>
)}
</div>
<div className="flex items-center justify-between">
{!required && (
<Button
role="button"
variant="ghost"
tabIndex={0}
onClick={(event) => {
event.stopPropagation();
handleClear();
}}
onKeyDown={(event) => {
if (
event.key ===
"Enter" ||
event.key === " "
) {
event.preventDefault();
event.stopPropagation();
handleClear();
}
}}
aria-label={`Clear all ${selectedValues.length} selected options`}
className="flex items-center justify-center h-4 w-4 p-0 me-2 cursor-pointer transition-opacity opacity-50 group-hover:opacity-60 hover:opacity-100 rounded-sm"
>
<XIcon className="h-4 w-4" />
</Button>
)}
<Separator
orientation="vertical"
className="flex min-h-4 h-full"
/>
<ChevronsUpDownIcon
className="h-4 ms-2 cursor-pointer opacity-50 group-hover/button:opacity-60"
aria-hidden="true"
/>
</div>
</div>
))}
</Button>
</PopoverTrigger>
<PopoverContent
@ -1655,14 +1672,14 @@ export const ModelSelect = forwardRef<ModelSelectRef, ModelSelectProps>(
content={
<>
<ChevronLeftIcon />
{useTrans(
{GetTranslation(
"pagination.previous",
)}
</>
}
tooltipContent={
<p>
{useTrans(
{GetTranslation(
"pagination.specific_page",
{
params: {
@ -1692,13 +1709,15 @@ export const ModelSelect = forwardRef<ModelSelectRef, ModelSelectProps>(
<BtnOptionnalLinkWithTooltip
content={
<>
{useTrans("pagination.next")}
{GetTranslation(
"pagination.next",
)}
<ChevronRightIcon />
</>
}
tooltipContent={
<p>
{useTrans(
{GetTranslation(
"pagination.specific_page",
{
params: {

View file

@ -142,6 +142,7 @@ interface MultiSelectProps
* Callback function triggered when the selected values change.
* Receives an array of the new selected values.
*/
// eslint-disable-next-line no-unused-vars
onValueChange?: (value: string[]) => void;
/** The default selected values when the component mounts. */
@ -182,6 +183,7 @@ interface MultiSelectProps
* If true, renders the multi-select component as a child of another component.
* Optional, defaults to false.
*/
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
asChild?: boolean;
/**
@ -315,6 +317,7 @@ interface MultiSelectProps
/**
* The fonction to set the data in useForm inertia
*/
// eslint-disable-next-line no-unused-vars
setData: (name: string, value: string[]) => void;
/**
@ -362,6 +365,7 @@ export interface MultiSelectRef {
/**
* Set selected values programmatically
*/
// eslint-disable-next-line no-unused-vars
setSelectedValues: (values: string[]) => void;
/**
* Clear all selected values
@ -393,6 +397,7 @@ export const MultiSelect = forwardRef<MultiSelectRef, MultiSelectProps>(
animationConfig,
maxCount = 3,
modalPopover = false,
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
asChild = false,
className,
hideSelectAll = false,
@ -482,6 +487,7 @@ export const MultiSelect = forwardRef<MultiSelectRef, MultiSelectProps>(
onValueChange(defaultValue);
}
setData(name, defaultValue);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultValue, onValueChange]);
const buttonRef = useRef<HTMLButtonElement>(null);
@ -525,6 +531,7 @@ export const MultiSelect = forwardRef<MultiSelectRef, MultiSelectProps>(
}
},
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[resetToDefault, selectedValues, onValueChange],
);
@ -533,7 +540,7 @@ export const MultiSelect = forwardRef<MultiSelectRef, MultiSelectProps>(
>("desktop");
useEffect(() => {
if (typeof window === "undefined") return;
if (typeof window === "undefined") return undefined;
const handleResize = () => {
const width = window.innerWidth;
if (width < 640) {
@ -686,7 +693,7 @@ export const MultiSelect = forwardRef<MultiSelectRef, MultiSelectProps>(
const getOptionByValue = useCallback(
(value: string): MultiSelectOption | undefined => {
const option = getAllOptions().find(
(option) => option.value === value,
(opt) => opt.value === value,
);
if (!option && process.env.NODE_ENV === "development") {
console.warn(
@ -1053,6 +1060,7 @@ export const MultiSelect = forwardRef<MultiSelectRef, MultiSelectProps>(
<PopoverTrigger asChild>
<Button
ref={buttonRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
onClick={handleTogglePopover}
disabled={disabled}
@ -1159,6 +1167,7 @@ export const MultiSelect = forwardRef<MultiSelectRef, MultiSelectProps>(
customStyle?.iconColor &&
"text-current",
)}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(customStyle?.iconColor && {
style: {
color: customStyle.iconColor,

View file

@ -31,6 +31,7 @@ type SimpleColorPickerProps = {
required?: boolean;
disabled?: boolean;
parentErrors?: Record<string, string> | null;
/* eslint-disable no-unused-vars */
setData: (name: string, value: string | null) => void;
};
@ -87,15 +88,15 @@ export default function SimpleColorPicker({
/**
* Convert ColorResult → HEXA.
*
* @param color ColorResult
* @param targetColor ColorResult
* @return string
*/
function toHEXA(color: ColorResult): string {
const r = color.rgb.r.toString(16).padStart(2, "0");
const g = color.rgb.g.toString(16).padStart(2, "0");
const b = color.rgb.b.toString(16).padStart(2, "0");
function toHEXA(targetColor: ColorResult): string {
const r = targetColor.rgb.r.toString(16).padStart(2, "0");
const g = targetColor.rgb.g.toString(16).padStart(2, "0");
const b = targetColor.rgb.b.toString(16).padStart(2, "0");
const a =
Math.round((color.rgb.a ?? 1) * 255)
Math.round((targetColor.rgb.a ?? 1) * 255)
.toString(16)
.padStart(2, "0") ?? "ff";
return `#${r}${g}${b}${a}`;
@ -143,11 +144,11 @@ export default function SimpleColorPicker({
/**
* Update color via picker.
*
* @param color ColorResult
* @param targetColor ColorResult
* @return void
*/
function handleChangeComplete(color: ColorResult): void {
const hex = toHEXA(color);
function handleChangeComplete(targetColor: ColorResult): void {
const hex = toHEXA(targetColor);
setColor(hex);
setData(name, hex);
}
@ -195,6 +196,7 @@ export default function SimpleColorPicker({
useEffect(() => {
if (value !== color) setColor(value);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
return (
@ -234,99 +236,13 @@ export default function SimpleColorPicker({
<PopoverContent className="p-2">
<SketchPicker
color={String(color || initialColor.current)}
onChange={handleChange}
onChangeComplete={handleChangeComplete}
onChange={(newColor) => handleChange(newColor)}
onChangeComplete={(newCompleteColor) =>
handleChangeComplete(newCompleteColor)
}
disableAlpha={false}
presetColors={presetColors}
/>
{/* Force custom style */}
<style>
{`
.sketch-picker {
display: flex;
flex-direction: column;
padding: 0 !important;
border: 0 !important;
box-shadow: none !important;
width: 100% !important;
}
.sketch-picker, .sketch-picker .flexbox-fix {
gap: 0.375rem;
}
.sketch-picker > div, .sketch-picker .flexbox-fix > div {
padding: 0 !important;
margin: 0 !important;
}
.sketch-picker > div:first-child {
aspect-ratio: 1 / 1;
border-radius: calc(var(--radius) - 2px);
}
.sketch-picker > div:first-child > div > div > div:last-child > div {
width: 10px !important;
height: 10px !important;
box-shadow: none !important;
border-radius: 50% !important;
transform: translate(-5px, -5px) !important;
background-color: white;
}
.sketch-picker > div:nth-child(2) > div:first-child {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.sketch-picker > div:nth-child(2) > div:first-child > div {
height: 20px !important;
outline: 1px solid hsl(var(--input));
border-radius: calc(var(--radius) - 2px);
margin: 0rem !important;
cursor: pointer;
}
.sketch-picker > div:nth-child(2) > div:first-child > div > div > div > div > div {
width: 0.5rem !important;
height: 20px !important;
border-radius: 0 !important;
box-shadow: none !important;
background: rgb(255, 255, 255) !important;
transform: translateX(-4px) !important;
margin-top: 0 !important;
border: 1px hsl(var(--input)) solid;
}
.sketch-picker > div:nth-child(2) > div:last-child {
display: none;
}
.sketch-picker > div:nth-child(3) > div:first-child {
width: 16rem;
}
.sketch-picker > div:nth-child(3) > div {
flex: auto !important;
}
.sketch-picker > div:last-child {
justify-content: space-between;
padding-top: 0.375rem !important;
}
.sketch-picker > div:last-child > div {
width: 20px !important;
height: 20px !important;
}
.sketch-picker > div:last-child > div > span > div {
box-shadow: none !important;
outline: 1px solid hsl(var(--input)) !important;
border-radius: calc(var(--radius) - 2px) !important;
}
.sketch-picker input {
width: 100% !important;
height: 2.5rem;
border-radius: calc(var(--radius) - 2px) !important;
border: 1px hsl(var(--input)) solid !important;
background-color: hsl(var(--background));
padding: 0.5rem 0.75rem !important;
color: var(--foreground);
box-shadow: none !important;
outline: none;
text-align: center;
}
`}
</style>
</PopoverContent>
</Popover>
{/* Random Color */}
@ -337,7 +253,7 @@ export default function SimpleColorPicker({
type="button"
variant="secondary"
size="icon"
onClick={handleRandom}
onClick={() => handleRandom()}
disabled={disabled}
/>
)}
@ -348,7 +264,7 @@ export default function SimpleColorPicker({
type="button"
variant="secondary"
size="icon"
onClick={handleReset}
onClick={() => handleReset()}
disabled={disabled}
/>
{!required && (
@ -358,7 +274,7 @@ export default function SimpleColorPicker({
type="button"
variant="destructive"
size="icon"
onClick={handleClear}
onClick={() => handleClear()}
disabled={disabled}
/>
)}

View file

@ -1,7 +1,8 @@
// * Other
import { fr, enUS } from "date-fns/locale";
import InputError from "@/back/Components/InputError";
import { usePage } from "@inertiajs/react";
import { CalendarIcon, RefreshCcw } from "lucide-react";
// * Components shadcn
import { Calendar } from "@/components/ui/calendar";
import {
Popover,
@ -9,14 +10,14 @@ import {
PopoverTrigger,
} from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
import { CalendarIcon, RefreshCcw } from "lucide-react";
import { cn } from "@/lib/utils";
import { useFormatedDate } from "@/hooks/use-formated-value";
import useLocale from "@/store/use-locale-store";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
// * Other
import InputError from "@/back/Components/InputError";
import { cn, GetFormatedDate } from "@/lib/utils";
import str from "@/hooks/use-string";
import useLocale from "@/store/use-locale-store";
import useTrans from "@/hooks/use-translations";
type SimpleDatePickerProps = {
@ -102,7 +103,7 @@ export default function SimpleDatePicker({
>
<span>
{date && date !== undefined
? useFormatedDate(
? GetFormatedDate(
date?.toString(),
useLocale.use.timezone(),
hasTime,

View file

@ -56,6 +56,7 @@ export default function SimpleNumber({
useEffect(() => {
setInputValue(safeValue);
setData(name, String(safeValue));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [safeValue]);
return (

View file

@ -2,10 +2,10 @@
import { usePage } from "@inertiajs/react";
// * Components shadcn
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
// * Other
import InputError from "@/back/Components/InputError";
import { cn } from "@/lib/utils";
import { Label } from "@/components/ui/label";
type SimpleSwitchProps = {
name: string;

View file

@ -1,6 +1,7 @@
// * Other
import SimpleString from "@/back/Components/Inputs/SimpleString";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
import SimpleString from "./SimpleString";
type SimpleVimeoIdProps = {
className?: string;
@ -19,5 +20,6 @@ export default function SimpleVimeoId(props: SimpleVimeoIdProps) {
const label = str(useTrans("validation.custom.vimeo_id")).ucFirst().value();
const localProps = { pattern, label, ...props };
// eslint-disable-next-line react/jsx-props-no-spreading
return <SimpleString {...localProps} />;
}

View file

@ -1,6 +1,7 @@
// * Other
import SimpleString from "@/back/Components/Inputs/SimpleString";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
import SimpleString from "./SimpleString";
type SimpleYoutubeIdProps = {
className?: string;
@ -21,5 +22,6 @@ export default function SimpleYoutubeId(props: SimpleYoutubeIdProps) {
.value();
const localProps = { pattern, label, ...props };
// eslint-disable-next-line react/jsx-props-no-spreading
return <SimpleString {...localProps} />;
}

View file

@ -1,17 +1,21 @@
// * Packages
import { GavelIcon } from "lucide-react";
// * Other
import SimpleString from "@/back/Components/Inputs/SimpleString";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
import { GavelIcon } from "lucide-react";
import SimpleString from "./SimpleString";
import BtnOptionnalLinkWithTooltip from "../BtnOptionnalLinkWithTooltip";
interface Props {
setData: (name: string, value: string) => void;
interface SlugifycatorProps {
value: string;
targetValue: string;
wordCounter?: boolean;
disabled?: boolean;
// eslint-disable-next-line no-unused-vars
setData: (name: string, value: string) => void;
}
function Slugifycator(props: Props) {
export default function Slugifycator(props: SlugifycatorProps) {
const {
value,
targetValue,
@ -50,5 +54,3 @@ function Slugifycator(props: Props) {
</div>
);
}
export default Slugifycator;

View file

@ -1,31 +1,34 @@
// * Packages
import { usePage } from "@inertiajs/react";
// * Other
import EnumSelect from "@/back/Components/Inputs/EnumSelect";
import { LaravelEnum } from "@/back/types/global";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
import EnumSelect from "./EnumSelect";
interface Props {
name: string;
selected: number;
required: boolean;
label: string;
// eslint-disable-next-line no-unused-vars
setData: (name: string, value: number | null) => void;
}
function StaticRouteSelect(props: Props) {
export default function StaticRouteSelect(props: Props) {
const { name, selected, required, label, setData } = props;
const enumStaticRoutes = usePage().props
.enumStaticRoutes as LaravelEnum | null;
if (!enumStaticRoutes)
return str(useTrans("back.static_route.no_route_defined"))
.ucFirst()
.value();
const transSelectValue = str(useTrans("back.static_route.select_value"))
.ucFirst()
.value();
const transNoRoutes = str(useTrans("back.static_route.no_route_defined"))
.ucFirst()
.value();
if (!enumStaticRoutes) return transNoRoutes;
return (
<EnumSelect
@ -39,5 +42,3 @@ function StaticRouteSelect(props: Props) {
/>
);
}
export default StaticRouteSelect;

View file

@ -8,7 +8,7 @@ import ImageNodeView, {
} from "@/back/Components/Inputs/Wysiwyg/Extensions/Image/ImageNodeView";
import { imageDetails } from "@/back/Components/Inputs/Wysiwyg/Extensions/Image/ImageUtils";
export const Image = TiptapImage.extend({
const Image = TiptapImage.extend({
// Define attributes we need.
addAttributes() {
return {
@ -40,9 +40,11 @@ export const Image = TiptapImage.extend({
alt: img.getAttribute("alt"),
width: parseInt(
img.getAttribute("width") || imageDetails.width,
10,
),
height: parseInt(
img.getAttribute("height") || imageDetails.height,
10,
),
style: null,
caption: figcaption?.textContent,
@ -58,11 +60,13 @@ export const Image = TiptapImage.extend({
// Define the structure of the render.
// Use for render html in front files.
renderHTML({ node, HTMLAttributes }) {
delete HTMLAttributes.caption;
delete HTMLAttributes.float;
const attrsCopy = { ...HTMLAttributes };
delete attrsCopy.caption;
delete attrsCopy.float;
const style = [
HTMLAttributes.style ?? "margin: 0px;",
attrsCopy.style ?? "margin: 0px;",
node.attrs.float
? FLOAT_STYLES[node.attrs.float as FloatOption]
: null,
@ -70,12 +74,14 @@ export const Image = TiptapImage.extend({
.filter(Boolean)
.join(" ");
delete HTMLAttributes.style;
delete attrsCopy.style;
return [
"figure",
{ style },
["img", HTMLAttributes],
["img", attrsCopy],
...(node.attrs.caption ? [["figcaption", node.attrs.caption]] : []),
];
},
});
export default Image;

View file

@ -27,6 +27,7 @@ import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithT
import CompleteInputFile from "@/back/Components/Inputs/CompleteInputFile";
import useTrans from "@/hooks/use-translations";
import str from "@/hooks/use-string";
import { GetTranslation } from "@/lib/utils";
type ImageProps = {
/** Wysiwyg editor instance. */
@ -81,13 +82,15 @@ export default function ImageButton({ editor, disabled }: ImageProps) {
const transCaption = useTrans("back.wysiwyg.image.caption");
const transValidation = (attribute: string) => {
return {
min: useTrans("validation.min.string", {
min: GetTranslation("validation.min.string", {
params: { attribute, min: "3" },
}),
max: useTrans("validation.max.string", {
max: GetTranslation("validation.max.string", {
params: { attribute, max: "255" },
}),
string: useTrans("validation.string", { params: { attribute } }),
string: GetTranslation("validation.string", {
params: { attribute },
}),
};
};
const transValidationAlt = transValidation(transAlt);
@ -142,7 +145,7 @@ export default function ImageButton({ editor, disabled }: ImageProps) {
)
.refine(
(file) =>
file.size <= parseInt(imageDetails.size) * 1024 * 1024,
file.size <= parseInt(imageDetails.size, 10) * 1024 * 1024,
{
message: useTrans("validation.size.file", {
params: {
@ -154,6 +157,73 @@ export default function ImageButton({ editor, disabled }: ImageProps) {
),
});
/**
* Set new errors from object errors
*
* @param err unknown
* @return void
*/
const setNewErrors = (err: unknown): void => {
const fieldErrors: typeof errors = {};
const fieldMapping: Record<string, string> = {
upload: "image",
inputFile: "image",
alt: "alt",
caption: "caption",
};
// Manage zod errors.
if (err instanceof z.ZodError) {
err.issues.forEach((error) => {
const key = error.path[0] as string | undefined;
const mappedKey =
(key && fieldMapping[key]) ??
(error.path.length === 0 ? "form" : undefined);
if (mappedKey) fieldErrors[mappedKey] = error.message;
});
setErrors(fieldErrors);
return;
}
// Manage laravel errors.
if (err && typeof err === "object" && "response" in err) {
const currentErrors = err as {
response: {
data: {
errors: Record<string, string>;
};
};
};
Object.entries(currentErrors.response.data.errors).forEach(
([laravelField, [firstMessage]]) => {
const componentField = fieldMapping[laravelField];
if (componentField && firstMessage) {
fieldErrors[componentField] = firstMessage;
}
},
);
setErrors(fieldErrors);
}
};
/**
* Reset all states and values in the component.
*
* @return void
*/
const resetComponent = () => {
setFile(null);
setInputText({
alt: "",
caption: null,
hasCaption: false,
});
setInputType(ImageTypes[0]);
setErrors({});
setLoading(false);
};
/**
* Upload image in storage.
*
@ -239,73 +309,6 @@ export default function ImageButton({ editor, disabled }: ImageProps) {
}
};
/**
* Set new errors from object errors
*
* @param err unknown
* @return void
*/
const setNewErrors = (err: unknown): void => {
const fieldErrors: typeof errors = {};
const fieldMapping: Record<string, string> = {
upload: "image",
inputFile: "image",
alt: "alt",
caption: "caption",
};
// Manage zod errors.
if (err instanceof z.ZodError) {
err.issues.forEach((error) => {
const key = error.path[0] as string | undefined;
const mappedKey =
(key && fieldMapping[key]) ??
(error.path.length === 0 ? "form" : undefined);
if (mappedKey) fieldErrors[mappedKey] = error.message;
});
setErrors(fieldErrors);
return;
}
// Manage laravel errors.
if (err && typeof err === "object" && "response" in err) {
const currentErrors = err as {
response: {
data: {
errors: Record<string, string>;
};
};
};
Object.entries(currentErrors.response.data.errors).forEach(
([laravelField, messages]) => {
const componentField = fieldMapping[laravelField];
if (componentField && messages.length > 0) {
fieldErrors[componentField] = messages[0];
}
},
);
setErrors(fieldErrors);
}
};
/**
* Reset all states and values in the component.
*
* @return void
*/
const resetComponent = () => {
setFile(null);
setInputText({
alt: "",
caption: null,
hasCaption: false,
});
setInputType(ImageTypes[0]);
setErrors({});
setLoading(false);
};
/**
* Update url value from string input.
*
@ -336,6 +339,7 @@ export default function ImageButton({ editor, disabled }: ImageProps) {
caption: inputText.hasCaption ? inputText.caption : null,
hasCaption: inputText.hasCaption,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [inputText.hasCaption]);
return (
@ -397,7 +401,7 @@ export default function ImageButton({ editor, disabled }: ImageProps) {
<div className={type.style} />
</div>
<p className="text-sm font-medium leading-none">
{useTrans(
{GetTranslation(
`back.wysiwyg.image.${type.name}`,
)}
</p>
@ -413,7 +417,7 @@ export default function ImageButton({ editor, disabled }: ImageProps) {
.ucFirst()
.value()}
setData={handleSetImage}
reset={(name) => console.log("reset", name)}
reset={() => {}}
value={null}
required
maxFileSize="5120"

View file

@ -10,6 +10,7 @@ type ImageInputsProps = {
caption: string | null;
hasCaption: boolean;
};
// eslint-disable-next-line no-unused-vars
onChange: (value: ImageInputsProps["value"]) => void;
errors?: Record<string, string>;
loading?: boolean;
@ -23,6 +24,8 @@ export default function ImageInputs({
loading = false,
disabled = false,
}: ImageInputsProps) {
const transImageCaption = useTrans("back.wysiwyg.image.caption");
return (
<>
{/* Input alt attribute of the image. */}
@ -61,9 +64,7 @@ export default function ImageInputs({
{value.hasCaption && (
<SimpleString
name="caption"
label={str(useTrans("back.wysiwyg.image.caption"))
.ucFirst()
.value()}
label={str(transImageCaption).ucFirst().value()}
placeholder="ex: John Doe"
required={false}
value={value.caption ?? ""}

View file

@ -11,6 +11,7 @@ import {
ChevronsLeftRightEllipsisIcon,
} from "lucide-react";
import { useEffect, useRef, useState } from "react";
import type { MouseEvent as ReactMouseEvent } from "react";
import z from "zod";
// * Components shadcn
import { Button } from "@/components/ui/button";
@ -37,19 +38,9 @@ import {
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import useTrans from "@/hooks/use-translations";
import str from "@/hooks/use-string";
import { cn } from "@/lib/utils";
import { cn, GetTranslation } from "@/lib/utils";
import { parseStyleString } from "@/hooks/use-inline-style";
type ImageAttrs = {
src: string;
alt: string;
width: number;
height: number;
caption: string | null;
style: string;
float: FloatOption;
};
export type AllowedAligns = "left" | "center" | "right";
export type FloatOption = "left" | "right";
@ -88,6 +79,16 @@ const PLACEMENT_STYLES: Record<PlacementValue, string> = {
"float-right": FLOAT_STYLES.right,
};
type ImageAttrs = {
src: string;
alt: string;
width: number;
height: number;
caption: string | null;
style: string;
float: FloatOption;
};
export default function ImageNodeView(props: NodeViewProps) {
const { node, updateAttributes, editor, selected } = props;
const attrs = node.attrs as ImageAttrs;
@ -124,13 +125,15 @@ export default function ImageNodeView(props: NodeViewProps) {
const transCaption = useTrans("back.wysiwyg.image.caption");
const transValidation = (attribute: string) => {
return {
min: useTrans("validation.min.string", {
min: GetTranslation("validation.min.string", {
params: { attribute, min: "3" },
}),
max: useTrans("validation.max.string", {
max: GetTranslation("validation.max.string", {
params: { attribute, max: "255" },
}),
string: useTrans("validation.string", { params: { attribute } }),
string: GetTranslation("validation.string", {
params: { attribute },
}),
};
};
const transValidationAlt = transValidation(transAlt);
@ -184,7 +187,7 @@ export default function ImageNodeView(props: NodeViewProps) {
* @param event MouseEvent
* @returns
*/
const onMouseDown = (event: React.MouseEvent) => {
const onMouseDown = (event: ReactMouseEvent) => {
event.preventDefault();
event.stopPropagation();
@ -245,6 +248,31 @@ export default function ImageNodeView(props: NodeViewProps) {
resizeState.current = null;
};
/**
* Set new errors from object errors
*
* @param err unknown
* @return void
*/
const setNewErrors = (err: unknown): void => {
const fieldErrors: typeof errors = {};
const fieldMapping: Record<string, string> = {
alt: "alt",
caption: "caption",
};
if (err instanceof z.ZodError) {
err.issues.forEach((error) => {
const key = error.path[0] as string | undefined;
const mappedKey =
(key && fieldMapping[key]) ??
(error.path.length === 0 ? "form" : undefined);
if (mappedKey) fieldErrors[mappedKey] = error.message;
});
setErrors(fieldErrors);
}
};
/**
* Update image attributes.
*
@ -274,30 +302,6 @@ export default function ImageNodeView(props: NodeViewProps) {
setNewErrors(error);
}
};
/**
* Set new errors from object errors
*
* @param err unknown
* @return void
*/
const setNewErrors = (err: unknown): void => {
const fieldErrors: typeof errors = {};
const fieldMapping: Record<string, string> = {
alt: "alt",
caption: "caption",
};
if (err instanceof z.ZodError) {
err.issues.forEach((error) => {
const key = error.path[0] as string | undefined;
const mappedKey =
(key && fieldMapping[key]) ??
(error.path.length === 0 ? "form" : undefined);
if (mappedKey) fieldErrors[mappedKey] = error.message;
});
setErrors(fieldErrors);
}
};
/**
* Close modal and reset component.
@ -339,7 +343,7 @@ export default function ImageNodeView(props: NodeViewProps) {
// Update the focus on @tiptap editor.
useEffect(() => {
if (!editor) return;
if (!editor) return undefined;
editor.on("focus", handleFocus);
editor.on("blur", handleBlur);
@ -352,7 +356,7 @@ export default function ImageNodeView(props: NodeViewProps) {
// Reset events when resizing.
useEffect(() => {
if (!isResizing) return;
if (!isResizing) return undefined;
window.addEventListener("mousemove", onMouseMove);
window.addEventListener("mouseup", onMouseUp);
@ -361,6 +365,7 @@ export default function ImageNodeView(props: NodeViewProps) {
window.removeEventListener("mousemove", onMouseMove);
window.removeEventListener("mouseup", onMouseUp);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isResizing]);
// Set default value to the caption.
@ -370,6 +375,7 @@ export default function ImageNodeView(props: NodeViewProps) {
caption: inputText.hasCaption ? inputText.caption : null,
hasCaption: inputText.hasCaption,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [inputText.hasCaption]);
// Set height of header + @tiptap toolbar editor with resize event.
@ -434,7 +440,7 @@ export default function ImageNodeView(props: NodeViewProps) {
</SelectTrigger>
<SelectContent align="end">
<SelectItem value="default" disabled>
{useTrans("back.wysiwyg.image.position")}
{GetTranslation("back.wysiwyg.image.position")}
</SelectItem>
{PLACEMENT_OPTIONS.map((value) => {
const [type, dir] = value.split("-") as [
@ -463,7 +469,7 @@ export default function ImageNodeView(props: NodeViewProps) {
{value === "float-right" && (
<ArrowRightLeftIcon className="w-4 h-4" />
)}
{useTrans(
{GetTranslation(
`back.wysiwyg.image.${str(label).slug().value()}`,
)}
</div>

View file

@ -1,9 +1,11 @@
// * Packages
import "@tiptap/core";
// Set custom function attributes.
declare module "@tiptap/core" {
interface Commands<ReturnType> {
Image: {
// eslint-disable-next-line no-unused-vars
setImage: (attrs: {
src: string;
alt: string;

View file

@ -1,11 +1,8 @@
// * Packages
import { Plugin, PluginKey } from "prosemirror-state";
import { mergeAttributes, getMarkRange, CommandProps } from "@tiptap/react";
import { Plugin, PluginKey, TextSelection } from "prosemirror-state";
import { mergeAttributes, getMarkRange } from "@tiptap/react";
import type { LinkOptions as TiptapLinkOptions } from "@tiptap/extension-link";
import { Link as TiptapLink } from "@tiptap/extension-link";
import { TextSelection } from "prosemirror-state";
export interface LinkOptions extends TiptapLinkOptions {}
export type LinkAttrs = {
text: string;
@ -13,7 +10,7 @@ export type LinkAttrs = {
modelToken: string | null;
};
export const Link = TiptapLink.extend<LinkOptions>({
export const Link = TiptapLink.extend<TiptapLinkOptions>({
addCommands() {
return {
setLink:
@ -34,38 +31,47 @@ export const Link = TiptapLink.extend<LinkOptions>({
});
if (linkFound) {
return commands.command(({ tr, state, dispatch }) => {
// Get the existing mark.
let existingMark = null;
state.doc.nodesBetween(from, to, (node) => {
if (node.marks) {
const found = node.marks.find(
(m) => m.type.name === this.name,
);
if (found) existingMark = found;
}
});
return commands.command(
({ tr, state: editorState, dispatch }) => {
// Get the existing mark.
let existingMark = null;
editorState.doc.nodesBetween(
from,
to,
(node) => {
if (node.marks) {
const found = node.marks.find(
(m) =>
m.type.name === this.name,
);
if (found) existingMark = found;
}
},
);
if (!existingMark) return false;
if (!existingMark) return false;
// Remove the existing text.
tr.delete(from, to);
// Remove the existing text.
tr.delete(from, to);
// Insert the new text.
tr.insertText(linkAttrs.text, from);
// Insert the new text.
tr.insertText(linkAttrs.text, from);
// Set the mark with new attributes.
tr.addMark(
from,
from + linkAttrs.text.length,
state.schema.marks[this.name].create(linkAttrs),
);
// Set the mark with new attributes.
tr.addMark(
from,
from + linkAttrs.text.length,
state.schema.marks[this.name].create(
linkAttrs,
),
);
// Apply the modification.
if (dispatch) dispatch(tr);
// Apply the modification.
if (dispatch) dispatch(tr);
return true;
});
return true;
},
);
}
// Otherwise, create a new link like before.
@ -77,7 +83,7 @@ export const Link = TiptapLink.extend<LinkOptions>({
},
unsetLink:
() =>
({ state, dispatch, tr, commands }) => {
({ state, dispatch, tr }) => {
const { $from } = state.selection;
const markType = state.schema.marks[this.name];

View file

@ -1,6 +1,6 @@
// * Packages
import { useEffect, useState } from "react";
import axios from "axios";
import axios, { isAxiosError } from "axios";
import { route } from "ziggy-js";
import { ExternalLinkIcon, LinkIcon, TrashIcon } from "lucide-react";
import { Editor } from "@tiptap/react";
@ -23,11 +23,11 @@ import EnumSelect from "@/back/Components/Inputs/EnumSelect";
import SimpleString from "@/back/Components/Inputs/SimpleString";
import SimpleSwitch from "@/back/Components/Inputs/SimpleSwitch";
import GoToFrontView from "@/back/Components/GoToFrontView";
import { ModelSelect } from "@/back/Components/Inputs/ModelSelect";
import useTrans from "@/hooks/use-translations";
import str from "@/hooks/use-string";
import { AppEnv, LaravelEnum } from "@/back/types/global";
import { cn } from "@/lib/utils";
import { ModelSelect } from "../../../ModelSelect";
import { cn, GetTranslation } from "@/lib/utils";
// Custom type with field entry.
type AddFieldToRecord<T extends Record<string, object>> = {
@ -58,10 +58,12 @@ type RetrieveFromTokenResponse = {
};
// All link types.
enum LinkType {
external = 1,
internal = 2,
}
const LinkType = {
external: 1,
internal: 2,
} as const;
type LinkTypeValue = (typeof LinkType)[keyof typeof LinkType];
// Component props.
type LinkProps = {
@ -88,7 +90,7 @@ export default function LinkButton({ editor, disabled }: LinkProps) {
const [modelToken, setModelToken] = useState<string | null>(null);
// Type of selected link.
const [selectedType, setSelectedType] = useState<LinkType>(
const [selectedType, setSelectedType] = useState<LinkTypeValue>(
LinkType.external,
);
// Value of the selected enum.
@ -124,11 +126,11 @@ export default function LinkButton({ editor, disabled }: LinkProps) {
// Utility for generating validation messages.
const createMinRule = (attribute: string, min: number) =>
useTrans("validation.min.string", {
GetTranslation("validation.min.string", {
params: { attribute, min: min.toString() },
});
const createMaxRule = (attribute: string, max: number) =>
useTrans("validation.max.string", {
GetTranslation("validation.max.string", {
params: { attribute, max: max.toString() },
});
@ -153,47 +155,30 @@ export default function LinkButton({ editor, disabled }: LinkProps) {
.string()
.min(3, createMinRule(textAttribute, 3))
.max(255, createMaxRule(textAttribute, 255)),
inputUrl: z
.string()
.min(3, createMinRule(urlAttribute, 3))
.max(255, createMaxRule(urlAttribute, 255))
// Assert it's not an internal url.
.pipe(
z.url({
protocol: /^https?$/,
hostname: z.regexes.domain,
message: useTrans("back.wysiwyg.link.validation.url"),
}),
)
.refine(
(val) => {
try {
const parsed = new URL(val);
return parsed.hostname !== new URL(url).hostname;
} catch {
return false;
}
const parsedUrl = new URL(val);
const currentHostname = new URL(url).hostname;
return parsedUrl.hostname !== currentHostname;
},
{
message: useTrans(
"back.wysiwyg.link.validation.use_internal",
),
},
)
// Assert it's a valid url.
.refine(
(val) => {
try {
new URL(val);
return true;
} catch {
return false;
}
},
{
message: useTrans("back.wysiwyg.link.validation.url"),
},
)
// Assert there is domain and tld.
.refine(
(val) =>
/^(?:https?:\/\/)?([a-z0-9-]+\.)+[a-z]{2,}$/i.test(val),
{
message: useTrans("back.wysiwyg.link.validation.url"),
},
),
});
@ -231,6 +216,24 @@ export default function LinkButton({ editor, disabled }: LinkProps) {
return response.data;
};
/**
* Reset all states and values in the component.
*
* @return void
*/
const resetComponent = () => {
setSelectedType(LinkType.external);
setSelectedEnumValue(null);
setSelectedModelId(null);
setSelectedEntryUrl(null);
setInputUrl("");
setInputText("");
setInputBlank(false);
setEditingLink(false);
setErrors({});
setLoading(false);
};
/**
* Add new link.
*
@ -301,24 +304,6 @@ export default function LinkButton({ editor, disabled }: LinkProps) {
setTimeout(() => resetComponent(), animationTime);
};
/**
* Reset all states and values in the component.
*
* @return void
*/
const resetComponent = () => {
setSelectedType(LinkType.external);
setSelectedEnumValue(null);
setSelectedModelId(null);
setSelectedEntryUrl(null);
setInputUrl("");
setInputText("");
setInputBlank(false);
setEditingLink(false);
setErrors({});
setLoading(false);
};
/**
* Update selected enum and update states.
*
@ -347,12 +332,16 @@ export default function LinkButton({ editor, disabled }: LinkProps) {
selectedEntry: value,
modelName: selectedEnum?.name,
})) as ModelAccessResponse;
console.log(data);
setSelectedEntryUrl(data.url);
setModelToken(data.token);
} catch (err: any) {
} catch (err: unknown) {
console.error(err);
setErrors((prev) => ({ ...prev, ...err.response?.data?.errors }));
if (isAxiosError(err) && err.response?.data?.errors) {
setErrors((prev) => ({
...prev,
...err.response?.data.errors,
}));
}
} finally {
setSelectedModelId(value);
setLoading(false);
@ -411,7 +400,7 @@ export default function LinkButton({ editor, disabled }: LinkProps) {
const handler = async ({
href,
text,
modelToken,
modelToken, // eslint-disable-line no-shadow
blank,
}: {
href: string;
@ -449,12 +438,14 @@ export default function LinkButton({ editor, disabled }: LinkProps) {
setSelectedModelId(data.modelId);
setSelectedEntryUrl(href);
setModelToken(modelToken);
} catch (err: any) {
} catch (err: unknown) {
console.error(err);
setErrors((prev) => ({
...prev,
...err.response?.data?.errors,
}));
if (isAxiosError(err) && err.response?.data?.errors) {
setErrors((prev) => ({
...prev,
...err.response?.data.errors,
}));
}
}
} else {
setSelectedType(LinkType.external);
@ -471,8 +462,17 @@ export default function LinkButton({ editor, disabled }: LinkProps) {
return () => {
editor.off("open-link-editor", handler);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [editor]);
let path = "";
if (selectedEntryUrl) {
path = selectedEntryUrl.startsWith("/")
? selectedEntryUrl.slice(1)
: selectedEntryUrl;
}
return (
<Dialog
open={open}
@ -538,7 +538,9 @@ export default function LinkButton({ editor, disabled }: LinkProps) {
<EnumSelect
key={resetState}
name="modelName"
label={useTrans("back.wysiwyg.link.enum")}
label={GetTranslation(
"back.wysiwyg.link.enum",
)}
placeholder="ex: page dynamique"
selected={selectedEnumValue}
options={enumModelsList}
@ -558,7 +560,9 @@ export default function LinkButton({ editor, disabled }: LinkProps) {
multiple={false}
targetModel={selectedEnum?.name}
field={selectedEnum?.field}
label={useTrans("back.wysiwyg.link.entry")}
label={GetTranslation(
"back.wysiwyg.link.entry",
)}
placeholder="ex: John Doe"
defaultValues={
selectedModelId ? [selectedModelId] : []
@ -584,22 +588,16 @@ export default function LinkButton({ editor, disabled }: LinkProps) {
)
.lowerCase()
.value()}
pathField={
selectedEntryUrl
? selectedEntryUrl.startsWith(
"/",
)
? selectedEntryUrl.slice(1)
: selectedEntryUrl
: ""
}
pathField={path}
isAvailable
isColumn
addLocale={false}
/>
) : (
<small className="text-start">
{useTrans("back.wysiwyg.link.link")}
{GetTranslation(
"back.wysiwyg.link.link",
)}
</small>
)}
</>
@ -608,7 +606,9 @@ export default function LinkButton({ editor, disabled }: LinkProps) {
{/* Url input. */}
<SimpleString
name="link-url"
label={useTrans("back.wysiwyg.link.url")}
label={GetTranslation(
"back.wysiwyg.link.url",
)}
value={inputUrl}
type="url"
disabled={loading}
@ -624,7 +624,7 @@ export default function LinkButton({ editor, disabled }: LinkProps) {
content={
<ExternalLinkIcon className="w-4 h-4 m-auto" />
}
tooltipContent={useTrans(
tooltipContent={GetTranslation(
"back.wysiwyg.link.access_url",
)}
className="cursor-pointer"
@ -640,7 +640,7 @@ export default function LinkButton({ editor, disabled }: LinkProps) {
{/* String input. */}
<SimpleString
name="link-text"
label={useTrans("back.wysiwyg.link.text")}
label={GetTranslation("back.wysiwyg.link.text")}
value={inputText}
disabled={loading}
setData={(_, value) => handleUpdateText(value)}
@ -648,7 +648,7 @@ export default function LinkButton({ editor, disabled }: LinkProps) {
/>
{/* Boolean input. */}
<SimpleSwitch
label={useTrans("back.wysiwyg.link.switch")}
label={GetTranslation("back.wysiwyg.link.switch")}
name="link-blank"
checked={inputBlank}
disabled={loading}
@ -659,10 +659,10 @@ export default function LinkButton({ editor, disabled }: LinkProps) {
<DialogFooter className="space-x-0 sm:space-x-0 gap-1.5 px-2 pb-2">
{/* Cancel button. */}
<BtnOptionnalLinkWithTooltip
content={str(useTrans("back.wysiwyg.link.cancel"))
content={str(GetTranslation("back.wysiwyg.link.cancel"))
.ucFirst()
.value()}
tooltipContent={useTrans(
tooltipContent={GetTranslation(
"back.wysiwyg.link.tooltip_cancel",
)}
className="cursor-pointer"
@ -674,10 +674,10 @@ export default function LinkButton({ editor, disabled }: LinkProps) {
/>
{/* Save button. */}
<BtnOptionnalLinkWithTooltip
content={str(useTrans("back.wysiwyg.link.insert"))
content={str(GetTranslation("back.wysiwyg.link.insert"))
.ucFirst()
.value()}
tooltipContent={useTrans(
tooltipContent={GetTranslation(
"back.wysiwyg.link.tooltip_insert",
)}
className="cursor-pointer"
@ -689,7 +689,7 @@ export default function LinkButton({ editor, disabled }: LinkProps) {
{/* Delete button. */}
<BtnOptionnalLinkWithTooltip
content={<TrashIcon className="w-4 h-4 m-auto" />}
tooltipContent={useTrans(
tooltipContent={GetTranslation(
"back.wysiwyg.link.tooltip_delete",
)}
className="cursor-pointer"

View file

@ -1,8 +1,10 @@
// * Packages
import "@tiptap/core";
declare module "@tiptap/core" {
interface Commands<ReturnType> {
Link: {
// eslint-disable-next-line no-unused-vars
setLink: (attrs: {
href: string;
text: string;

View file

@ -1,3 +1,4 @@
// * Packages
import "@tiptap/core";
declare module "@tiptap/core" {

View file

@ -29,6 +29,28 @@ export default function FindReplaceButton({ editor }: { editor: Editor }) {
// Duration of modal closing animation.
const animationTime = 200;
/**
* Reset the component.
*
* @return void
*/
const resetComponent = (): void => {
setSearchText("");
setReplaceText("");
setMatchCase(false);
};
/**
* Close modal and reset component.
*
* @param status boolean
* @return void
*/
const closeModal = (status: boolean = false): void => {
setOpen(status);
setTimeout(() => resetComponent(), animationTime);
};
/**
* Search and replace specific text.
*
@ -53,28 +75,6 @@ export default function FindReplaceButton({ editor }: { editor: Editor }) {
closeModal();
};
/**
* Reset the component.
*
* @return void
*/
const resetComponent = (): void => {
setSearchText("");
setReplaceText("");
setMatchCase(false);
};
/**
* Close modal and reset component.
*
* @param status boolean
* @return void
*/
const closeModal = (status: boolean = false): void => {
setOpen(status);
setTimeout(() => resetComponent(), animationTime);
};
return (
<Dialog open={open} onOpenChange={(status) => closeModal(status)}>
<DialogTrigger asChild>

View file

@ -2,7 +2,7 @@
import { Extension } from "@tiptap/core";
import { Plugin } from "prosemirror-state";
export const ShortcutBoundary = Extension.create({
const ShortcutBoundary = Extension.create({
addProseMirrorPlugins() {
return [
new Plugin({
@ -22,3 +22,5 @@ export const ShortcutBoundary = Extension.create({
];
},
});
export default ShortcutBoundary;

View file

@ -54,18 +54,18 @@ export default function TableButton({ editor, disabled }: TableProps) {
);
const insert = (
rows: number | undefined = undefined,
cols: number | undefined = undefined,
expectedRows: number | undefined = undefined,
expectedCols: number | undefined = undefined,
): void => {
if (!editor.can().insertTable()) return;
// Insert the current table in the editor.
// Insert the expected table in the editor.
editor
.chain()
.focus()
.insertTable({
rows,
cols,
rows: expectedRows,
cols: expectedCols,
withHeaderRow: header,
})
.run();
@ -86,16 +86,6 @@ export default function TableButton({ editor, disabled }: TableProps) {
setHeader(true);
};
/**
* Insert tableClose modal and reset component.
*
* @return void
*/
const handleInsertTable = (): void => {
insert();
closeModal();
};
/**
* Close modal and reset component.
*
@ -106,6 +96,16 @@ export default function TableButton({ editor, disabled }: TableProps) {
setTimeout(() => resetComponent(), animationTime);
};
/**
* Insert tableClose modal and reset component.
*
* @return void
*/
const handleInsertTable = (): void => {
insert();
closeModal();
};
// Set size of the current table on hover.
useEffect(() => {
setHover({ row: rows, column: cols });
@ -113,7 +113,7 @@ export default function TableButton({ editor, disabled }: TableProps) {
// Press the key event to use the arrow keys to set the table size.
useEffect(() => {
if (!open) return;
if (!open) return undefined;
const onKeyDown = (event: KeyboardEvent) => {
const active = document.activeElement as HTMLInputElement;
@ -164,13 +164,16 @@ export default function TableButton({ editor, disabled }: TableProps) {
event.preventDefault();
setOpen(false);
break;
default:
break;
}
};
// Redefine the keydown event due to table resize.
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, [open, insert]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open]);
return (
<Dialog
@ -265,13 +268,17 @@ export default function TableButton({ editor, disabled }: TableProps) {
(_, i) => {
const row = Math.floor(i / gridSize) + 1;
const column = (i % gridSize) + 1;
const active =
hover &&
row <= hover.row &&
column <= hover.column;
return (
<div
key={i}
<button
key={`${row}-${column}`}
type="button"
aria-label={`${row} x ${column}`}
onMouseEnter={() => {
setHover({ row, column });
setRows(row);

View file

@ -17,14 +17,13 @@ import TextAlign from "@tiptap/extension-text-align";
// * Components shadcn
import { Label } from "@/components/ui/label";
// * Other
import { ShortcutBoundary } from "@/back/Components/Inputs/Wysiwyg/Extensions/ShortCuts/ShortcutBoundary";
import ShortcutBoundary from "@/back/Components/Inputs/Wysiwyg/Extensions/ShortCuts/ShortcutBoundary";
import { Link } from "@/back/Components/Inputs/Wysiwyg/Extensions/Link/Link";
import { Image } from "@/back/Components/Inputs/Wysiwyg/Extensions/Image/Image";
import Image from "@/back/Components/Inputs/Wysiwyg/Extensions/Image/Image";
import InputError from "@/back/Components/InputError";
import WordCounter from "@/back/Components/WordCounter";
import Toolbar from "@/back/Components/Inputs/Wysiwyg/Toolbar";
import useTrans from "@/hooks/use-translations";
import { cn } from "@/lib/utils";
import { cn, GetTranslation } from "@/lib/utils";
type SimpleWysiwygProps = {
className?: string;
@ -38,6 +37,7 @@ type SimpleWysiwygProps = {
required?: boolean;
disabled?: boolean;
parentErrors?: Record<string, string> | null;
// eslint-disable-next-line no-unused-vars
setData: (name: string, value: string) => void;
};
@ -106,7 +106,7 @@ export default function SimpleWysiwyg({
TableHeader,
TableCell,
Placeholder.configure({
placeholder: useTrans(placeholder),
placeholder: GetTranslation(placeholder),
}),
TextAlign.configure({
types: ["heading", "paragraph"],
@ -119,6 +119,7 @@ export default function SimpleWysiwyg({
Image,
Link,
],
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
@ -131,11 +132,13 @@ export default function SimpleWysiwyg({
editorProps: {
attributes: { id, required: String(required), model },
},
onUpdate: ({ editor }) => onChangeContent(editor.getHTML()),
onUpdate: ({ editor: currentEditor }) =>
onChangeContent(currentEditor.getHTML()),
});
setEditor(e);
return () => e.destroy();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Sync content if parent change.

View file

@ -45,9 +45,8 @@ import LinkButton from "@/back/Components/Inputs/Wysiwyg/Extensions/Link/LinkBut
import TableButton from "@/back/Components/Inputs/Wysiwyg/Extensions/Table/TableButton";
import FindReplaceButton from "@/back/Components/Inputs/Wysiwyg/Extensions/Replace/FindReplaceButton";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import useTrans from "@/hooks/use-translations";
import str from "@/hooks/use-string";
import { cn } from "@/lib/utils";
import { cn, GetTranslation } from "@/lib/utils";
type AllowedHeadings = 1 | 2 | 3 | 4 | 5 | 6;
@ -109,7 +108,7 @@ export default function Toolbar({ editor }: ToolbarProps) {
// Force re-render editor on select to update activated extensions.
useEffect(() => {
if (!editor) return;
if (!editor) return undefined;
const update = () => forceUpdate((value) => value + 1);
@ -148,7 +147,7 @@ export default function Toolbar({ editor }: ToolbarProps) {
>
<BtnOptionnalLinkWithTooltip
content={<UndoIcon className="w-4 h-4 m-auto" />}
tooltipContent={str(useTrans("back.wysiwyg.undo"))
tooltipContent={str(GetTranslation("back.wysiwyg.undo"))
.ucFirst()
.value()}
onClick={(event) => {
@ -162,7 +161,7 @@ export default function Toolbar({ editor }: ToolbarProps) {
/>
<BtnOptionnalLinkWithTooltip
content={<RedoIcon className="w-4 h-4 m-auto" />}
tooltipContent={str(useTrans("back.wysiwyg.redo"))
tooltipContent={str(GetTranslation("back.wysiwyg.redo"))
.ucFirst()
.value()}
onClick={(event) => {
@ -177,7 +176,7 @@ export default function Toolbar({ editor }: ToolbarProps) {
<Separator orientation="vertical" className="!h-auto" />
<BtnOptionnalLinkWithTooltip
content={<BoldIcon className="w-4 h-4 m-auto" />}
tooltipContent={str(useTrans("back.wysiwyg.bold"))
tooltipContent={str(GetTranslation("back.wysiwyg.bold"))
.ucFirst()
.value()}
onClick={(event) => {
@ -192,7 +191,7 @@ export default function Toolbar({ editor }: ToolbarProps) {
/>
<BtnOptionnalLinkWithTooltip
content={<ItalicIcon className="w-4 h-4 m-auto" />}
tooltipContent={str(useTrans("back.wysiwyg.italic"))
tooltipContent={str(GetTranslation("back.wysiwyg.italic"))
.ucFirst()
.value()}
onClick={(event) => {
@ -207,7 +206,7 @@ export default function Toolbar({ editor }: ToolbarProps) {
/>
<BtnOptionnalLinkWithTooltip
content={<StrikethroughIcon className="w-4 h-4 m-auto" />}
tooltipContent={str(useTrans("back.wysiwyg.strike"))
tooltipContent={str(GetTranslation("back.wysiwyg.strike"))
.ucFirst()
.value()}
onClick={(event) => {
@ -222,7 +221,7 @@ export default function Toolbar({ editor }: ToolbarProps) {
/>
<BtnOptionnalLinkWithTooltip
content={<UnderlineIcon className="w-4 h-4 m-auto" />}
tooltipContent={str(useTrans("back.wysiwyg.underline"))
tooltipContent={str(GetTranslation("back.wysiwyg.underline"))
.ucFirst()
.value()}
onClick={(event) => {
@ -237,7 +236,7 @@ export default function Toolbar({ editor }: ToolbarProps) {
/>
<BtnOptionnalLinkWithTooltip
content={<QuoteIcon className="w-4 h-4 m-auto" />}
tooltipContent={str(useTrans("back.wysiwyg.blockquote"))
tooltipContent={str(GetTranslation("back.wysiwyg.blockquote"))
.ucFirst()
.value()}
onClick={(event) => {
@ -277,18 +276,18 @@ export default function Toolbar({ editor }: ToolbarProps) {
</SelectTrigger>
</TooltipTrigger>
<TooltipContent>
{useTrans("back.wysiwyg.heading")}
{GetTranslation("back.wysiwyg.heading")}
</TooltipContent>
</Tooltip>
<SelectContent>
{/* Define default paragraph value to reset heading */}
<SelectItem value="paragraph">
{useTrans("back.wysiwyg.heading_paragraph")}
{GetTranslation("back.wysiwyg.heading_paragraph")}
</SelectItem>
{/* List of headings */}
{headingLevels.map((level) => (
<SelectItem key={level} value={level.toString()}>
{useTrans("back.wysiwyg.heading_x", {
{GetTranslation("back.wysiwyg.heading_x", {
params: { hx: String(level) },
})}
</SelectItem>
@ -298,7 +297,7 @@ export default function Toolbar({ editor }: ToolbarProps) {
<Separator orientation="vertical" className="!h-auto" />
<BtnOptionnalLinkWithTooltip
content={<ListIcon className="w-4 h-4 m-auto" />}
tooltipContent={str(useTrans("back.wysiwyg.bullet_list"))
tooltipContent={str(GetTranslation("back.wysiwyg.bullet_list"))
.ucFirst()
.value()}
onClick={(event) => {
@ -313,7 +312,7 @@ export default function Toolbar({ editor }: ToolbarProps) {
/>
<BtnOptionnalLinkWithTooltip
content={<ListOrderedIcon className="w-4 h-4 m-auto" />}
tooltipContent={str(useTrans("back.wysiwyg.ordered_list"))
tooltipContent={str(GetTranslation("back.wysiwyg.ordered_list"))
.ucFirst()
.value()}
onClick={(event) => {
@ -328,7 +327,7 @@ export default function Toolbar({ editor }: ToolbarProps) {
/>
<BtnOptionnalLinkWithTooltip
content={<ListTodoIcon className="w-4 h-4 m-auto" />}
tooltipContent={str(useTrans("back.wysiwyg.task_list"))
tooltipContent={str(GetTranslation("back.wysiwyg.task_list"))
.ucFirst()
.value()}
onClick={(event) => {
@ -366,7 +365,7 @@ export default function Toolbar({ editor }: ToolbarProps) {
</SelectTrigger>
</TooltipTrigger>
<TooltipContent>
{useTrans("back.wysiwyg.align")}
{GetTranslation("back.wysiwyg.align")}
</TooltipContent>
</Tooltip>
<SelectContent>
@ -377,7 +376,9 @@ export default function Toolbar({ editor }: ToolbarProps) {
>
<div className="flex flex-row jsutify-center items-center gap-1.5">
<align.icon className="w-4 h-4" />
{useTrans(`back.wysiwyg.align_${align.value}`)}
{GetTranslation(
`back.wysiwyg.align_${align.value}`,
)}
</div>
</SelectItem>
))}
@ -386,7 +387,7 @@ export default function Toolbar({ editor }: ToolbarProps) {
<Separator orientation="vertical" className="!h-auto" />
<BtnOptionnalLinkWithTooltip
content={<SubscriptIcon className="w-4 h-4 m-auto" />}
tooltipContent={str(useTrans("back.wysiwyg.subscript"))
tooltipContent={str(GetTranslation("back.wysiwyg.subscript"))
.ucFirst()
.value()}
onClick={(event) => {
@ -401,7 +402,7 @@ export default function Toolbar({ editor }: ToolbarProps) {
/>
<BtnOptionnalLinkWithTooltip
content={<SuperscriptIcon className="w-4 h-4 m-auto" />}
tooltipContent={str(useTrans("back.wysiwyg.superscript"))
tooltipContent={str(GetTranslation("back.wysiwyg.superscript"))
.ucFirst()
.value()}
onClick={(event) => {
@ -417,7 +418,9 @@ export default function Toolbar({ editor }: ToolbarProps) {
<Separator orientation="vertical" className="!h-auto" />
<BtnOptionnalLinkWithTooltip
content={<MinusIcon className="w-4 h-4 m-auto" />}
tooltipContent={str(useTrans("back.wysiwyg.horizontal_rule"))
tooltipContent={str(
GetTranslation("back.wysiwyg.horizontal_rule"),
)
.ucFirst()
.value()}
onClick={(event) => {
@ -436,7 +439,7 @@ export default function Toolbar({ editor }: ToolbarProps) {
<BtnOptionnalLinkWithTooltip
content={<PilcrowIcon className="w-4 h-4 m-auto" />}
tooltipContent={str(
useTrans("back.wysiwyg.invisible_characters"),
GetTranslation("back.wysiwyg.invisible_characters"),
)
.ucFirst()
.value()}
@ -456,7 +459,7 @@ export default function Toolbar({ editor }: ToolbarProps) {
<FindReplaceButton editor={editor} />
<BtnOptionnalLinkWithTooltip
content={<TextSelectIcon className="w-4 h-4 m-auto" />}
tooltipContent={str(useTrans("back.wysiwyg.select_all"))
tooltipContent={str(GetTranslation("back.wysiwyg.select_all"))
.ucFirst()
.value()}
onClick={(event) => {
@ -469,7 +472,9 @@ export default function Toolbar({ editor }: ToolbarProps) {
/>
<BtnOptionnalLinkWithTooltip
content={<RemoveFormattingIcon className="w-4 h-4 m-auto" />}
tooltipContent={str(useTrans("back.wysiwyg.clear_selection"))
tooltipContent={str(
GetTranslation("back.wysiwyg.clear_selection"),
)
.ucFirst()
.value()}
onClick={(event) => {
@ -493,7 +498,9 @@ export default function Toolbar({ editor }: ToolbarProps) {
/>
<BtnOptionnalLinkWithTooltip
content={<DeleteIcon className="w-4 h-4 m-auto" />}
tooltipContent={str(useTrans("back.wysiwyg.delete_selection"))
tooltipContent={str(
GetTranslation("back.wysiwyg.delete_selection"),
)
.ucFirst()
.value()}
onClick={(event) => {

View file

@ -1,3 +1,4 @@
// * Packages
import "@tiptap/core";
declare module "@tiptap/core" {

View file

@ -16,7 +16,7 @@ import {
import { AppEnv, Theme } from "@/back/types/global";
import { useTheme } from "@/back/providers/ThemeProvider";
import useTrans from "@/hooks/use-translations";
import { cn } from "@/lib/utils";
import { cn, GetTranslation } from "@/lib/utils";
type AppSettingsProps = {
className?: string;
@ -32,9 +32,7 @@ export default function AppSettings({ className = "" }: AppSettingsProps) {
e.preventDefault();
post(route(`back.lang.set`));
};
const GetTranslation = (name: string) => {
return useTrans(name);
};
const mods = [
{
name: useTrans("back.light"),
@ -79,13 +77,13 @@ export default function AppSettings({ className = "" }: AppSettingsProps) {
<Button className="text-muted-foreground" variant="ghost">
<SettingsIcon className="h-[1.2rem] w-[1.2rem]" />
<span className="hidden lg:block">
{useTrans("back.settings")}
{GetTranslation("back.settings")}
</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" className="w-fit me-1">
<p className="text-center text-muted-foreground text-sm font-medium leading-7">
{useTrans("back.mods")}
{GetTranslation("back.mods")}
</p>
<div className="flex flex-row justify-center items-center gap-1">
{mods.map((mod) => {
@ -115,7 +113,7 @@ export default function AppSettings({ className = "" }: AppSettingsProps) {
<>
<DropdownMenuSeparator />
<p className="text-center text-muted-foreground text-sm font-medium leading-7">
{useTrans("back.languages")}
{GetTranslation("back.languages")}
</p>
<form
className="grid grid-cols-2 justify-center items-center gap-1"

View file

@ -79,19 +79,45 @@ type NavListProps = Array<{
/** Lucide icon of the button. */
icon: ElementType;
/** Define some actions in side popover (like create/trash). */
// eslint-disable-next-line no-unused-vars
actions?: (model: string) => ReactNode;
}>;
}>;
type CustomDropdownMenuItemProps = {
/** Link of the action. */
route: string;
targetRoute: string;
/** Lucide icon of the action. */
icon?: ElementType;
icon: ElementType;
/** Translation of the action. */
trans?: string;
trans: string;
};
/**
* Sidenav action button.
*
* @param string targetRoute
* @param ElementType icon
* @param string trans
* @return ReactNode
*/
function CustomDropdownMenuItem({
targetRoute,
icon: Icon,
trans,
}: CustomDropdownMenuItemProps): ReactNode {
return (
<DropdownMenuItem className="p-0">
<Button variant="ghost" asChild>
<Link href={targetRoute} className="w-full">
<Icon />
{trans}
</Link>
</Button>
</DropdownMenuItem>
);
}
export default function AppSidebar() {
const { appEnv, auth, enumUserRole } = usePage().props as unknown as {
appEnv: AppEnv;
@ -102,29 +128,103 @@ export default function AppSidebar() {
const { state } = useSidebar();
const prevStateRef = useRef(state);
const isCollapsed = state === "collapsed";
// Translations.
const transDefaultSideAction = str(
useTrans(`crud.actions.${Views.CREATION.label}`),
)
.ucFirst()
.value();
const transPages = useTrans("back.pages");
const transDashboard = useTrans("back.dashboard");
const transWebsite = useTrans("back.your_website");
const transDynamicPages = useTrans("models.classes.dynamic-page", {
choice: 2,
});
const transModules = useTrans("back.modules");
const transNews = useTrans("models.classes.news", { choice: 2 });
const transNewsCategories = useTrans("models.classes.news-category", {
choice: 2,
});
const transNewsTags = useTrans("models.classes.news-tag", { choice: 2 });
const transContactCount = useTrans("models.classes.contact-count");
const transDocuments = useTrans("models.classes.document", { choice: 2 });
const transDocumentCategories = useTrans(
"models.classes.document-category",
{ choice: 2 },
);
const transDocumentTags = useTrans("models.classes.document-tag", {
choice: 2,
});
const transCompanies = useTrans("models.classes.company", { choice: 2 });
const transCompanyActivities = useTrans("models.classes.company-activity", {
choice: 2,
});
const transCompanySectors = useTrans("models.classes.company-sector", {
choice: 2,
});
const transAdmin = useTrans("enums.user-role.admin");
const transUsers = useTrans("models.classes.user");
const transActivityLogs = useTrans("models.classes.activity-log");
const transPageConfigs = useTrans("models.classes.page-config", {
choice: 2,
});
const transArchives = str(useTrans("back.archives")).ucFirst().value();
const transTelescope = useTrans("back.telescope");
/**
* Get sidenav actions.
*
* @param ReactNode items
* @param string model
* @return
*/
const filterActions = ({
items,
model,
}: {
items?: ReactNode[];
model?: string;
}): ReactNode => {
let computedItems = items;
// Set create action by default.
if (!computedItems && model) {
computedItems = [
authPolicies[model].create && (
<CustomDropdownMenuItem
key="create"
targetRoute={route(`back.${model}.create`)}
icon={Views.CREATION.icon}
trans={transDefaultSideAction}
/>
),
];
}
const visibleItems = computedItems
? computedItems.filter(Boolean)
: null;
return visibleItems && visibleItems.length > 0 ? visibleItems : null;
};
const navList: NavListProps = [
{
title: useTrans("back.pages"),
title: transPages,
show: true,
links: [
{
title: useTrans("back.dashboard"),
title: transDashboard,
model: "dashboard",
show: true,
icon: HomeIcon,
},
{
title: useTrans("back.your_website"),
title: transWebsite,
link: "/",
show: true,
icon: SquareArrowOutUpRightIcon,
},
{
title: str(
useTrans("models.classes.dynamic-page", { choice: 2 }),
)
.ucFirst()
.value(),
title: str(transDynamicPages).ucFirst().value(),
model: "dynamic-pages",
show: authPolicies.dynamicPages.viewAny,
icon: StickyNoteIcon,
@ -133,14 +233,20 @@ export default function AppSidebar() {
items: [
authPolicies.dynamicPages.create && (
<CustomDropdownMenuItem
route={route(`back.${model}.create`)}
targetRoute={route(
`back.${model}.create`,
)}
icon={Views.CREATION.icon}
trans={transDefaultSideAction}
/>
),
authPolicies.dynamicPages.viewTrash && (
<CustomDropdownMenuItem
route={route(`back.${model}.trash`)}
targetRoute={route(
`back.${model}.trash`,
)}
icon={Views.ARCHIVATION.icon}
trans={str(useTrans("back.archives"))
trans={str(transArchives)
.ucFirst()
.value()}
/>
@ -151,15 +257,13 @@ export default function AppSidebar() {
],
},
{
title: useTrans("back.modules"),
title: transModules,
show:
authPolicies.news.viewAny ||
auth.policies.contact_counts.viewAny,
links: [
{
title: str(useTrans("models.classes.news", { choice: 2 }))
.ucFirst()
.value(),
title: str(transNews).ucFirst().value(),
model: "news",
activeCondition: [
route().current("back.news-categories.*"),
@ -172,44 +276,42 @@ export default function AppSidebar() {
items: [
authPolicies.news.create && (
<CustomDropdownMenuItem
route={route(`back.${model}.create`)}
targetRoute={route(
`back.${model}.create`,
)}
icon={Views.CREATION.icon}
trans={transDefaultSideAction}
/>
),
authPolicies.news.viewTrash && (
<CustomDropdownMenuItem
route={route(`back.${model}.trash`)}
targetRoute={route(
`back.${model}.trash`,
)}
icon={Views.ARCHIVATION.icon}
trans={str(useTrans("back.archives"))
trans={str(transArchives)
.ucFirst()
.value()}
/>
),
authPolicies.newsCategories.viewAny && (
<CustomDropdownMenuItem
route={route(
targetRoute={route(
`back.news-categories.index`,
)}
icon={FolderIcon}
trans={str(
useTrans(
"models.classes.news-category",
{ choice: 2 },
),
)
trans={str(transNewsCategories)
.ucFirst()
.value()}
/>
),
authPolicies.newsTags.viewAny && (
<CustomDropdownMenuItem
route={route(`back.news-tags.index`)}
targetRoute={route(
`back.news-tags.index`,
)}
icon={TagIcon}
trans={str(
useTrans(
"models.classes.news-tag",
{ choice: 2 },
),
)
trans={str(transNewsTags)
.ucFirst()
.value()}
/>
@ -218,19 +320,13 @@ export default function AppSidebar() {
}),
},
{
title: str(useTrans("models.classes.contact-count"))
.ucFirst()
.value(),
title: str(transContactCount).ucFirst().value(),
model: "contact-counts",
icon: ContactIcon,
show: auth.policies.contact_counts.viewAny,
},
{
title: str(
useTrans("models.classes.document", { choice: 2 }),
)
.ucFirst()
.value(),
title: str(transDocuments).ucFirst().value(),
model: "documents",
activeCondition: [
route().current("back.document-categories.*"),
@ -243,21 +339,20 @@ export default function AppSidebar() {
items: [
authPolicies.documents.create && (
<CustomDropdownMenuItem
route={route(`back.${model}.create`)}
targetRoute={route(
`back.${model}.create`,
)}
icon={Views.CREATION.icon}
trans={transDefaultSideAction}
/>
),
authPolicies.documentCategories.create && (
<CustomDropdownMenuItem
icon={FolderIcon}
route={route(
targetRoute={route(
`back.document-categories.index`,
)}
trans={str(
useTrans(
"models.classes.document-category",
{ choice: 2 },
),
)
trans={str(transDocumentCategories)
.ucFirst()
.value()}
/>
@ -265,15 +360,10 @@ export default function AppSidebar() {
authPolicies.documentTags.create && (
<CustomDropdownMenuItem
icon={TagIcon}
route={route(
targetRoute={route(
`back.document-tags.index`,
)}
trans={str(
useTrans(
"models.classes.document-tag",
{ choice: 2 },
),
)
trans={str(transDocumentTags)
.ucFirst()
.value()}
/>
@ -282,12 +372,7 @@ export default function AppSidebar() {
}),
},
{
title: str(
useTrans("models.classes.company", { choice: 2 }),
)
.ucFirst()
.plural()
.value(),
title: str(transCompanies).ucFirst().plural().value(),
model: "companies",
activeCondition: [
route().current("back.company-activities.*"),
@ -300,14 +385,20 @@ export default function AppSidebar() {
items: [
authPolicies.companies.create && (
<CustomDropdownMenuItem
route={route(`back.${model}.create`)}
targetRoute={route(
`back.${model}.create`,
)}
icon={Views.CREATION.icon}
trans={transDefaultSideAction}
/>
),
authPolicies.companies.viewTrash && (
<CustomDropdownMenuItem
route={route(`back.${model}.trash`)}
targetRoute={route(
`back.${model}.trash`,
)}
icon={Views.ARCHIVATION.icon}
trans={str(useTrans("back.archives"))
trans={str(transArchives)
.ucFirst()
.value()}
/>
@ -315,15 +406,10 @@ export default function AppSidebar() {
authPolicies.companyActivities.viewAny && (
<CustomDropdownMenuItem
icon={RollerCoasterIcon}
trans={str(
useTrans(
"models.classes.company-activity",
{ choice: 2 },
),
)
trans={str(transCompanyActivities)
.ucFirst()
.value()}
route={route(
targetRoute={route(
`back.company-activities.index`,
)}
/>
@ -331,15 +417,10 @@ export default function AppSidebar() {
authPolicies.companySectors.viewAny && (
<CustomDropdownMenuItem
icon={RadarIcon}
trans={str(
useTrans(
"models.classes.company-sector",
{ choice: 2 },
),
)
trans={str(transCompanySectors)
.ucFirst()
.value()}
route={route(
targetRoute={route(
`back.company-sectors.index`,
)}
/>
@ -350,34 +431,24 @@ export default function AppSidebar() {
],
},
{
title: str(useTrans("enums.user-role.admin")).plural().value(),
title: str(transAdmin).plural().value(),
show: auth.user.role === enumUserRole.conceptor.value,
links: [
{
title: str(useTrans("models.classes.user"))
.ucFirst()
.plural()
.value(),
title: str(transUsers).ucFirst().plural().value(),
model: "users",
icon: UsersIcon,
show: authPolicies.users.viewAny,
actions: (model: string) => filterActions({ model }),
},
{
title: str(useTrans("models.classes.activity-log"))
.ucFirst()
.plural()
.value(),
title: str(transActivityLogs).ucFirst().plural().value(),
model: "activity-logs",
icon: HistoryIcon,
show: authPolicies.activityLogs.viewAny,
},
{
title: str(
useTrans("models.classes.page-config", { choice: 2 }),
)
.ucFirst()
.value(),
title: str(transPageConfigs).ucFirst().value(),
model: "page-configs",
show: authPolicies.pageConfigs.viewAny,
icon: CogIcon,
@ -386,23 +457,27 @@ export default function AppSidebar() {
items: [
authPolicies.pageConfigs.create && (
<CustomDropdownMenuItem
route={route(`back.${model}.create`)}
targetRoute={route(
`back.${model}.create`,
)}
icon={Views.CREATION.icon}
trans={transDefaultSideAction}
/>
),
authPolicies.pageConfigs.viewTrash && (
<CustomDropdownMenuItem
route={route(`back.${model}.trash`)}
targetRoute={route(
`back.${model}.trash`,
)}
icon={Views.ARCHIVATION.icon}
trans={str(useTrans("back.archives"))
.ucFirst()
.value()}
trans={transArchives}
/>
),
],
}),
},
{
title: useTrans("back.telescope"),
title: transTelescope,
link: route().has("telescope")
? route("telescope")
: undefined,
@ -413,49 +488,6 @@ export default function AppSidebar() {
},
];
const filterActions = ({
items,
model,
}: {
items?: ReactNode[];
model?: string;
}) => {
// Set create action by default.
if (!items && model) {
items = [
authPolicies[model].create && (
<CustomDropdownMenuItem
key="create"
route={route(`back.${model}.create`)}
/>
),
];
}
const visibleItems = items ? items.filter(Boolean) : null;
return visibleItems && visibleItems.length > 0 ? (
<>{visibleItems}</>
) : null;
};
function CustomDropdownMenuItem({
route,
icon: Icon = Views.CREATION.icon,
trans = str(useTrans(`crud.actions.${Views.CREATION.label}`))
.ucFirst()
.value(),
}: CustomDropdownMenuItemProps) {
return (
<DropdownMenuItem className="p-0">
<Button variant="ghost" asChild>
<Link href={route} className="w-full">
<Icon />
{trans}
</Link>
</Button>
</DropdownMenuItem>
);
}
useEffect(() => {
if (prevStateRef.current !== state) {
const elmts = document.querySelectorAll(".sidebar-transition");
@ -497,110 +529,110 @@ export default function AppSidebar() {
<CollapsibleContent>
<SidebarGroupContent>
<SidebarMenu>
{navGroup.links.map(
(navLink) => {
if (navLink.show) {
const isActive =
route().current(
`back.${navLink.model}.*`,
) ||
navLink.activeCondition?.some(
Boolean,
);
const sidebarButton =
(
<SidebarMenuButton
className={
isActive
? "text-secondary-foreground"
: "text-primary-foreground"
{navGroup.links
.filter(
(navLink) =>
navLink.show,
)
.map((navLink) => {
const isActive =
route().current(
`back.${navLink.model}.*`,
) ||
navLink.activeCondition?.some(
Boolean,
);
const sidebarButton = (
<SidebarMenuButton
className={
isActive
? "text-secondary-foreground"
: "text-primary-foreground"
}
asChild
>
{navLink.link ? (
<a
href={
navLink.link
}
asChild
target="_blank"
rel="noreferrer"
>
{navLink.link ? (
<a
href={
navLink.link
}
target="_blank"
rel="noreferrer"
>
<navLink.icon />
{
navLink.title
}
</a>
) : (
<Link
href={route(
`back.${navLink.model}.index`,
)}
className="bg-transparent"
>
<navLink.icon />
{
navLink.title
}
</Link>
<navLink.icon />
{
navLink.title
}
</a>
) : (
<Link
href={route(
`back.${navLink.model}.index`,
)}
</SidebarMenuButton>
);
return (
<SidebarMenuItem
key={
navLink.title
}
className={`transition-colors rounded-md ${isActive ? "bg-secondary text-secondary-foreground" : "bg-opacity-0"}`}
>
{isCollapsed ? (
<Tooltip>
<TooltipTrigger
className="bg-transparent"
>
<navLink.icon />
{
navLink.title
}
</Link>
)}
</SidebarMenuButton>
);
return (
<SidebarMenuItem
key={
navLink.title
}
className={`transition-colors rounded-md ${isActive ? "bg-secondary text-secondary-foreground" : "bg-opacity-0"}`}
>
{isCollapsed ? (
<Tooltip>
<TooltipTrigger
asChild
>
{
sidebarButton
}
</TooltipTrigger>
<TooltipContent side="right">
<p>
{
navLink.title
}
</p>
</TooltipContent>
</Tooltip>
) : (
sidebarButton
)}
{navLink.model &&
navLink.actions?.(
navLink.model,
) && (
<DropdownMenu>
<DropdownMenuTrigger
asChild
>
{
sidebarButton
}
</TooltipTrigger>
<TooltipContent side="right">
<p>
{
navLink.title
}
</p>
</TooltipContent>
</Tooltip>
) : (
sidebarButton
<SidebarMenuAction
className={`${isActive ? "text-secondary-foreground" : ""} !top-0 !right-0 px-4 h-full`}
>
<MoreHorizontalIcon />
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
side="right"
align="start"
>
{navLink.actions(
navLink.model,
)}
</DropdownMenuContent>
</DropdownMenu>
)}
{navLink.model &&
navLink.actions?.(
navLink.model,
) && (
<DropdownMenu>
<DropdownMenuTrigger
asChild
>
<SidebarMenuAction
className={`${isActive ? "text-secondary-foreground" : ""} !top-0 !right-0 px-4 h-full`}
>
<MoreHorizontalIcon />
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
side="right"
align="start"
>
{navLink.actions(
navLink.model,
)}
</DropdownMenuContent>
</DropdownMenu>
)}
</SidebarMenuItem>
);
}
},
)}
</SidebarMenuItem>
);
})}
</SidebarMenu>
</SidebarGroupContent>
</CollapsibleContent>

View file

@ -1,8 +1,9 @@
// * Packages
import { Link, useForm } from "@inertiajs/react";
import { route } from "ziggy-js";
import { FormEvent, useState } from "react";
import { FormEvent, ReactNode, useState } from "react";
import { MoreHorizontalIcon } from "lucide-react";
import type { VisitOptions } from "@inertiajs/core";
// * Components shadcn
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
@ -27,13 +28,281 @@ import { useFormatedDate } from "@/hooks/use-formated-value";
import { useToast } from "@/hooks/use-toast";
import useTrans from "@/hooks/use-translations";
import str from "@/hooks/use-string";
import { Model, Views as ViewsType } from "@/back/types/global";
import { DefaultModel, Views as ViewsType } from "@/back/types/global";
import { Views } from "@/lib/utils";
import { ToastActionElement, ToastProps } from "@/components/ui/toast";
interface Translations {
successfulArchivation: string;
successfulArchivationDetails: string;
canceledArchivation: string;
canceledArchivationDetails: string;
successfulRestoration: string;
successfulRestorationDetails: string;
canceledRestoration: string;
canceledRestorationDetails: string;
successfulDeletion: string;
successfulDeletionDetails: string;
canceledDeletion: string;
canceledDeletionDetails: string;
}
type ToasterToast = ToastProps & {
id: string;
title?: ReactNode;
description?: ReactNode;
action?: ToastActionElement;
};
/**
* Button for GET action.
* @param action ViewsType[number]
* @param modelRoute string
* @param model DefaultModel
* @returns Element
*/
function GetAction({
action,
modelRoute,
model,
}: {
action: ViewsType[number];
modelRoute: string;
model: DefaultModel;
}) {
return (
<Button asChild variant="ghost" className="w-full">
<Link
href={route(`back.${modelRoute}.${action.value}`, model.id)}
className="w-full"
>
<action.icon />
{str(useTrans(`crud.actions.${action.label}`))
.ucFirst()
.value()}
</Link>
</Button>
);
}
/**
* Action call on model.
* @param event FormEvent<HTMLFormElement>
* @param action ViewsType[number]
* @param modelRoute string
* @param model DefaultModel
* @param setOpen (value: boolean) => void
* @param translations Translations
* @param toast
*/
const actionOnModel = (
/* eslint-disable no-unused-vars */
event: FormEvent<HTMLFormElement>,
action: ViewsType[number],
modelRoute: string,
model: DefaultModel,
setOpen: (value: boolean) => void,
translations: Translations,
toast: ({ ...props }: Omit<ToasterToast, "id">) => {
id: string;
dismiss: () => void;
update: (propsChild: ToasterToast) => void;
},
destroy: (url: string, options?: VisitOptions) => void,
patch: (url: string, options?: VisitOptions) => void,
/* eslint-enable no-unused-vars */
) => {
event.preventDefault();
const routeAction = route(`back.${modelRoute}.${action.value}`, model.id);
// All post actions mapping.
const actionMap: Record<
ViewsType[number]["label"],
{
method: typeof destroy | typeof patch;
success: { title: string; description: string };
error: { title: string; description: string };
}
> = {
[Views.ARCHIVATION.label]: {
method: destroy,
success: {
title: translations.successfulArchivation,
description: translations.successfulArchivationDetails,
},
error: {
title: translations.canceledArchivation,
description: translations.canceledArchivationDetails,
},
},
[Views.RESTORATION.label]: {
method: patch,
success: {
title: translations.successfulRestoration,
description: translations.successfulRestorationDetails,
},
error: {
title: translations.canceledRestoration,
description: translations.canceledRestorationDetails,
},
},
[Views.DELETION.label || Views.FORCEDELETION.label]: {
method: destroy,
success: {
title: translations.successfulDeletion,
description: translations.successfulDeletionDetails,
},
error: {
title: translations.canceledDeletion,
description: translations.canceledDeletionDetails,
},
},
};
// Check if route exist.
const config = actionMap[action.label];
if (!config) {
throw new Error(`Unknown action: ${action.label}`);
}
// Close the dropdown.
setOpen(false);
// Execute the call.
config.method(routeAction, {
preserveScroll: true,
onSuccess: () => toast(config.success),
onError: () => toast({ variant: "destructive", ...config.error }),
});
};
/**
* Button for POST action.
* @param action ViewsType[number]
* @param modelRoute string
* @param model DefaultModel
* @param setOpen (value: boolean) => void
* @returns Element
*/
function PostAction({
action,
modelRoute,
model,
setOpen,
}: {
action: ViewsType[number];
modelRoute: string;
model: DefaultModel;
// eslint-disable-next-line no-unused-vars
setOpen: (value: boolean) => void;
}) {
const { delete: destroy, patch } = useForm();
const isRestoreAction = action.label === Views.RESTORATION.label;
const isArchiveAction = action.label === Views.ARCHIVATION.label;
const btnVariantType = isRestoreAction ? "ghost" : "destructive";
let translationKeyBtnConfirm: string;
const { toast } = useToast();
if (isRestoreAction) {
translationKeyBtnConfirm = "back.yes_restore";
} else if (isArchiveAction) {
translationKeyBtnConfirm = "back.yes_archive";
} else {
translationKeyBtnConfirm = "back.yes_delete";
}
const btnConfirm = useTrans(translationKeyBtnConfirm);
let translationKeyBtnDesc = "back.lost_data";
if (isRestoreAction) {
translationKeyBtnDesc = "back.restore_data";
} else if (isArchiveAction) {
translationKeyBtnDesc = "back.trash_data";
}
const btnDesc = useTrans(translationKeyBtnDesc);
// Translations for actionOnModel.
const translations: Translations = {
successfulArchivation: useTrans("crud.messages.successful_archivation"),
successfulArchivationDetails: useTrans(
"crud.messages.successful_archivation_details",
),
canceledArchivation: useTrans("crud.messages.canceled_archivation"),
canceledArchivationDetails: useTrans(
"crud.messages.canceled_archivation_details",
),
successfulRestoration: useTrans("crud.messages.successful_restoration"),
successfulRestorationDetails: useTrans(
"crud.messages.successful_restoration_details",
),
canceledRestoration: useTrans("crud.messages.canceled_restoration"),
canceledRestorationDetails: useTrans(
"crud.messages.canceled_restoration_details",
),
successfulDeletion: useTrans("crud.messages.successful_deletion"),
successfulDeletionDetails: useTrans(
"crud.messages.successful_deletion_details",
),
canceledDeletion: useTrans("crud.messages.canceled_deletion"),
canceledDeletionDetails: useTrans(
"crud.messages.canceled_deletion_details",
),
};
return (
<Dialog>
<DialogTrigger asChild>
<Button variant={btnVariantType} className="w-full">
<action.icon />
{str(useTrans(`crud.actions.${action.label}`))
.ucFirst()
.value()}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{useTrans("back.are_you_sure")}</DialogTitle>
<DialogDescription>{btnDesc}</DialogDescription>
</DialogHeader>
<DialogFooter className="items-center sm:justify-start">
<DialogClose asChild>
<Button variant="secondary">
{useTrans("back.no_cancel")}
</Button>
</DialogClose>
<form
onSubmit={(event) =>
actionOnModel(
event,
action,
modelRoute,
model,
setOpen,
translations,
toast,
destroy,
patch,
)
}
className="w-fit mb-2 sm:mb-0"
>
<Button type="submit" variant={btnVariantType}>
{btnConfirm}
</Button>
</form>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
interface ModelActionsProps {
actions?: ViewsType;
modelRoute: string;
model: Model;
model: DefaultModel;
modelUpdatedAt?: string;
inDialog?: boolean;
message?: string;
@ -54,225 +323,52 @@ export default function ModelActions({
(action) =>
model.policies[action.policy as keyof typeof model.policies],
);
const { delete: destroy, patch } = useForm();
const { toast } = useToast();
// Translations.
const successfulArchivation = useTrans(
"crud.messages.successful_archivation",
);
const successfulArchivationDetails = useTrans(
"crud.messages.successful_archivation_details",
);
const canceledArchivation = useTrans("crud.messages.canceled_archivation");
const canceledArchivationDetails = useTrans(
"crud.messages.canceled_archivation_details",
);
const successfulRestoration = useTrans(
"crud.messages.successful_restoration",
);
const successfulRestorationDetails = useTrans(
"crud.messages.successful_restoration_details",
);
const canceledRestoration = useTrans("crud.messages.canceled_restoration");
const canceledRestorationDetails = useTrans(
"crud.messages.canceled_restoration_details",
);
const successfulDeletion = useTrans("crud.messages.successful_deletion");
const successfulDeletionDetails = useTrans(
"crud.messages.successful_deletion_details",
);
const canceledDeletion = useTrans("crud.messages.canceled_deletion");
const canceledDeletionDetails = useTrans(
"crud.messages.canceled_deletion_details",
);
/**
* Action call on model.
* @param event FormEvent<HTMLFormElement>
* @param action ViewsType[number]
*/
const actionOnModel = (
event: FormEvent<HTMLFormElement>,
action: ViewsType[number],
) => {
event.preventDefault();
const routeAction = route(
`back.${modelRoute}.${action.value}`,
model.id,
);
// All post actions mapping.
const actionMap: Record<
ViewsType[number]["label"],
{
method: typeof destroy | typeof patch;
success: { title: string; description: string };
error: { title: string; description: string };
}
> = {
[Views.ARCHIVATION.label]: {
method: destroy,
success: {
title: successfulArchivation,
description: successfulArchivationDetails,
},
error: {
title: canceledArchivation,
description: canceledArchivationDetails,
},
},
[Views.RESTORATION.label]: {
method: patch,
success: {
title: successfulRestoration,
description: successfulRestorationDetails,
},
error: {
title: canceledRestoration,
description: canceledRestorationDetails,
},
},
[Views.DELETION.label || Views.FORCEDELETION.label]: {
method: destroy,
success: {
title: successfulDeletion,
description: successfulDeletionDetails,
},
error: {
title: canceledDeletion,
description: canceledDeletionDetails,
},
},
};
// Check if route exist.
const config = actionMap[action.label];
if (!config) {
throw new Error(`Unknown action: ${action.label}`);
}
// Close the dropdown.
setOpen(false);
// Execute the call.
config.method(routeAction, {
preserveScroll: true,
onSuccess: () => toast(config.success),
onError: () => toast({ variant: "destructive", ...config.error }),
});
};
/**
* Get all action buttons.
* @returns Element[]
*/
const AllBtnActions = () => {
return allowedActions.map((action, index) => (
<div key={index} className="w-full">
{action.value === Views.ARCHIVATION.value ||
action.value === Views.RESTORATION.value ||
action.value === Views.DELETION.value ||
action.value === Views.FORCEDELETION.value ? (
inDialog && index != 0 ? (
<>
<DropdownMenuSeparator />
<PostAction action={action} />
</>
) : (
<PostAction action={action} />
)
) : (
<GetAction action={action} />
)}
</div>
));
const postActionValues = new Set([
Views.ARCHIVATION.value,
Views.RESTORATION.value,
Views.DELETION.value,
Views.FORCEDELETION.value,
]);
return allowedActions.map((action) => {
const isPostAction = postActionValues.has(action.value);
const showSeparator =
inDialog &&
isPostAction &&
action.value !== allowedActions[0].value;
const content = isPostAction ? (
<>
{showSeparator && <DropdownMenuSeparator />}
<PostAction
action={action}
model={model}
modelRoute={modelRoute}
setOpen={setOpen}
/>
</>
) : (
<GetAction
action={action}
model={model}
modelRoute={modelRoute}
/>
);
return (
<div key={`${action.value}-${action.label}`} className="w-full">
{content}
</div>
);
});
};
/**
* Button for POST action.
* @param action ViewsType[number]
* @returns Element
*/
function PostAction({ action }: { action: ViewsType[number] }) {
const isRestoreAction = action.label === Views.RESTORATION.label;
const isArchiveAction = action.label === Views.ARCHIVATION.label;
const btnVariantType = isRestoreAction ? "ghost" : "destructive";
const btnConfirm = useTrans(
isRestoreAction
? "back.yes_restore"
: isArchiveAction
? "back.yes_archive"
: "back.yes_delete",
);
const btnDesc = useTrans(
isRestoreAction
? "back.restore_data"
: isArchiveAction
? "back.trash_data"
: "back.lost_data",
);
return (
<Dialog>
<DialogTrigger asChild>
<Button variant={btnVariantType} className="w-full">
<action.icon />
{str(useTrans(`crud.actions.${action.label}`))
.ucFirst()
.value()}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
{useTrans("back.are_you_sure")}
</DialogTitle>
<DialogDescription>{btnDesc}</DialogDescription>
</DialogHeader>
<DialogFooter className="items-center sm:justify-start">
<DialogClose asChild>
<Button variant="secondary">
{useTrans("back.no_cancel")}
</Button>
</DialogClose>
<form
onSubmit={(event) => actionOnModel(event, action)}
className="w-fit mb-2 sm:mb-0"
>
<Button type="submit" variant={btnVariantType}>
{btnConfirm}
</Button>
</form>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
/**
* Button for GET action.
* @param action ViewsType[number]
* @returns Element
*/
function GetAction({ action }: { action: ViewsType[number] }) {
return (
<Button asChild variant="ghost" className="w-full">
<Link
href={route(`back.${modelRoute}.${action.value}`, model.id)}
className="w-full"
>
<action.icon />
{str(useTrans(`crud.actions.${action.label}`))
.ucFirst()
.value()}
</Link>
</Button>
);
}
return (
<div
className={`flex justify-between items-center ${

View file

@ -1,49 +1,57 @@
// * Packages
import { Row } from "@tanstack/react-table";
import { usePage } from "@inertiajs/react";
import { NewspaperIcon } from "lucide-react";
// * Components shadcn
import { Badge } from "@/components/ui/badge";
// * Other
import DataTable from "@/back/Components/Table/DataTable";
import DataTableFilterButton from "@/back/Components/Table/DataTableFilterButton";
import { AppEnv, NewsModel, Resource } from "@/back/types/global";
import { Resource } from "@/back/types/global";
import { NewsModel } from "@/back/types/others";
import NewsStatusBadge from "@/back/Components/News/NewsStatusBadge";
import { renderTranslationCell } from "@/lib/translationCellFallback";
import RenderTranslationCell from "@/back/Components/Table/RenderTranslationCell";
import { Views } from "@/lib/utils";
import { useFormatedDate } from "@/hooks/use-formated-value";
import useTrans from "@/hooks/use-translations";
import useTrans, { UseTransOptions } from "@/hooks/use-translations";
import str from "@/hooks/use-string";
interface Props {
news: Resource;
}
function AssociatedNewsList(props: Props) {
const { news } = props;
const { appEnv } = usePage().props as unknown as {
appEnv: AppEnv;
};
const columns = [
const getColumns = () => {
const GetTranslation = (name: string, options?: UseTransOptions) => {
return useTrans(name, options);
};
const GetFormatedDate = (
date: string,
timezone?: string,
time?: boolean,
) => {
return useFormatedDate(date, timezone, time);
};
return [
{
accessorKey: "title",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(useTrans(`validation.attributes.${column.id}`))
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({ row }: { row: Row<NewsModel> }) => {
return renderTranslationCell(row.getValue("title"));
},
cell: ({ row }: { row: Row<NewsModel> }) => (
<RenderTranslationCell value={row.getValue("title")} />
),
},
{
accessorKey: "status",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(useTrans(`validation.custom.${column.id}`))
label={str(GetTranslation(`validation.custom.${column.id}`))
.ucFirst()
.value()}
field={column.id}
@ -51,7 +59,7 @@ function AssociatedNewsList(props: Props) {
),
cell: ({ row }: { row: Row<NewsModel> }) => {
return (
<NewsStatusBadge value={parseInt(row.getValue("status"))} />
<NewsStatusBadge value={Number(row.getValue("status"))} />
);
},
},
@ -59,7 +67,9 @@ function AssociatedNewsList(props: Props) {
accessorKey: "updated_at",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(useTrans(`validation.attributes.${column.id}`))
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
@ -67,11 +77,16 @@ function AssociatedNewsList(props: Props) {
),
cell: ({ row }: { row: Row<NewsModel> }) => (
<Badge variant="secondary">
{useFormatedDate(row.getValue("updated_at"))}
{GetFormatedDate(row.getValue("updated_at"))}
</Badge>
),
},
];
};
function AssociatedNewsList(props: Props) {
const { news } = props;
const columns = getColumns();
return (
<>

View file

@ -5,7 +5,10 @@ import { VariantProps } from "class-variance-authority";
import { Button, buttonVariants } from "@/components/ui/button";
// * Other
import useTrans from "@/hooks/use-translations";
import { ConfigInternalField, DynamicInternalField } from "@/back/types/global";
import {
ConfigInternalField,
DynamicInternalField,
} from "@/back/types/dynamic-pages";
type AddNewFieldBtnProps = {
array: ConfigInternalField[] | DynamicInternalField[] | undefined;

View file

@ -11,7 +11,7 @@ import {
// * Other
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
import { ConfigField, DynamicFieldTypes } from "@/back/types/global";
import { ConfigField, DynamicFieldTypes } from "@/back/types/dynamic-pages";
import { cn } from "@/lib/utils";
type ConfigDetailsProps = {
@ -43,7 +43,7 @@ export default function ConfigDetails({ config }: ConfigDetailsProps) {
const hasChildren = !!field.children?.length;
const accordionKey = `${field.key}-${index}`;
const rowContent = (hasChildren: boolean = false) => (
const rowContent = (targetHasChildren: boolean = false) => (
<div className="grid grid-cols-[1fr_1fr_1fr_auto] items-center w-full gap-4">
<p className="break-words">
{getEnumDynamicFieldType(field.type)?.label}
@ -55,7 +55,7 @@ export default function ConfigDetails({ config }: ConfigDetailsProps) {
<div className="text-sm text-muted-foreground break-words">
{field.validation || "—"}
</div>
{!hasChildren && <div className="w-[16px]" />}
{!targetHasChildren && <div className="w-[16px]" />}
</div>
);
@ -80,7 +80,7 @@ export default function ConfigDetails({ config }: ConfigDetailsProps) {
)}
{hasChildren && (
<AccordionContent className="pl-4">
<AccordionContent>
<div className="border rounded-md space-y-1">
{RenderRows(field.children!, false)}
</div>

View file

@ -24,7 +24,7 @@ import {
ConfigField,
ConfigInternalField,
DynamicFieldTypes,
} from "@/back/types/global";
} from "@/back/types/dynamic-pages";
import useTrans from "@/hooks/use-translations";
import str from "@/hooks/use-string";
@ -71,7 +71,8 @@ export default function DynamicFields({
let currentId = startId;
const withIds = fields.map((field) => {
const id = currentId++;
currentId += 1;
const id = currentId;
// Add the key in sensitive array.
const currentParentKey = `${parentKey}.${field.key}`;
@ -197,18 +198,6 @@ export default function DynamicFields({
);
};
/**
* Remove a specific field.
* @param fieldId string
* @returns void
*/
const removeField = (fieldId: number): void => {
const targetField = fields.filter((field) => field.id === fieldId)[0];
removeSensitiveField(targetField.key);
setFields((prev) => prev.filter((field) => field.id !== fieldId));
};
/**
* Remove sensitive field list.
* @param keyToRemove string
@ -221,6 +210,18 @@ export default function DynamicFields({
setLocalErrors({});
};
/**
* Remove a specific field.
* @param fieldId string
* @returns void
*/
const removeField = (fieldId: number): void => {
const targetField = fields.filter((field) => field.id === fieldId)[0];
removeSensitiveField(targetField.key);
setFields((prev) => prev.filter((field) => field.id !== fieldId));
};
/**
* Save the new order.
* @param event DragEndEvent
@ -249,11 +250,13 @@ export default function DynamicFields({
/**
* Remove all local ids.
* @param fields ConfigInternalField[]
* @param currentFields ConfigInternalField[]
* @returns
*/
const removeIds = (fields: ConfigInternalField[]): ConfigField[] => {
return fields.map(({ id, children, ...rest }) => ({
const removeIds = (currentFields: ConfigInternalField[]): ConfigField[] => {
// Delete the id, keep only the children if they are present, and keep the rest.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
return currentFields.map(({ id, children, ...rest }) => ({
...rest,
...(children ? { children: removeIds(children) } : {}),
})) as ConfigField[];
@ -267,11 +270,13 @@ export default function DynamicFields({
setData(name, payload);
prevValueRef.current = payloadStr;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fields]);
// Update fields when copying config from another locale.
useEffect(() => {
setFields(() => addIdsRecursively(value ?? []));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedConfig]);
return (

View file

@ -1,3 +1,4 @@
// * Packages
import { Dispatch, SetStateAction, useState } from "react";
import {
DndContext,
@ -33,11 +34,12 @@ import {
ConfigField,
ConfigInternalField,
DynamicFieldTypes,
} from "@/back/types/global";
} from "@/back/types/dynamic-pages";
import useTrans from "@/hooks/use-translations";
import str from "@/hooks/use-string";
import { cn } from "@/lib/utils";
/* eslint-disable no-unused-vars */
type FieldGroupProps = {
currentId: number;
currentIndex: number;
@ -62,6 +64,7 @@ type FieldGroupProps = {
setParentAccordionState: (key: string) => void;
limitMaxChildren: number;
};
/* eslint-enable no-unused-vars */
export default function FieldGroup({
currentId,
@ -131,6 +134,9 @@ export default function FieldGroup({
: currentField.key;
});
//* Translations
const transKeyAlreadyPresent = useTrans("validation.key_already_present");
// Set the complete key (prefix + editable) that will be sent to the backend.
const completeKey = `${enumCurrentBase}${editableKey}`;
@ -217,7 +223,7 @@ export default function FieldGroup({
*/
const updatingType = (value: string) => {
const selectedType = Object.values(enumDynamicFieldTypes).find(
(def) => def.value === parseInt(value),
(def) => def.value === Number(value),
);
if (selectedType) {
const resetCommon = {
@ -259,8 +265,10 @@ export default function FieldGroup({
variant="outline"
size="icon"
disabled={parentAccordionState !== ""}
/* eslint-disable react/jsx-props-no-spreading */
{...attributes}
{...listeners}
/* eslint-enable react/jsx-props-no-spreading */
/>
</div>
<div className="flex flex-row gap-1.5">
@ -352,7 +360,7 @@ export default function FieldGroup({
)}
placeholder="ex: field_1"
value={editableKey}
setData={(_, value) => {
setData={(_unused, value) => {
const newCompleteKey = `${enumCurrentBase}${value}`;
if (
sensitiveField.includes(
@ -362,15 +370,14 @@ export default function FieldGroup({
setLocalErrors((prev) => ({
...prev,
[`${currentFieldKey}.${currentIndex}.key`]:
useTrans(
"validation.key_already_present",
),
transKeyAlreadyPresent,
}));
} else {
setLocalErrors((prev) => {
const {
[`${currentFieldKey}.${currentIndex}.key`]:
_,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
removedKey,
...rest
} = prev;
return rest;

View file

@ -1,5 +1,5 @@
// * Packages
import { useRef, useState } from "react";
import { ChangeEvent, DragEvent, useRef, useState } from "react";
import {
JsonEditor,
IconReplacements,
@ -41,6 +41,7 @@ type JsonObject = {
};
type ImportConfigJsonProps = {
// eslint-disable-next-line no-unused-vars
onConfirm: (data: JsonObject[], overwrite: boolean) => void;
};
@ -91,9 +92,25 @@ export default function ImportConfigJson({ onConfirm }: ImportConfigJsonProps) {
* @param key string
* @return string
*/
const getTranslation = (key: string): string =>
const useGetTranslation = (key: string): string =>
str(useTrans(key)).ucFirst().value();
const transJsonEditorMaxSize = useTrans(
"back.json_editor.validation.max_size",
{
params: { value: "2MB" },
},
);
const transJsonEditorMimes = useTrans("back.json_editor.validation.mimes", {
params: { values: ".json" },
});
const transJsonEditorInvalidFileJson = useTrans(
"back.json_editor.validation.invalid_file_json",
);
const transInvalidContentJson = useTrans(
"back.json_editor.validation.invalid_content_json",
);
// Custom icons.
const icons: IconReplacements = {
add: <PlusIcon className="w-4 h-4" />,
@ -107,26 +124,28 @@ export default function ImportConfigJson({ onConfirm }: ImportConfigJsonProps) {
// Custom translations.
const translations: LocalisedStrings = {
ITEM_SINGLE: getTranslation("back.json_editor.item_single"),
ITEMS_MULTIPLE: getTranslation("back.json_editor.item_multiple"),
KEY_NEW: getTranslation("back.json_editor.key_new"),
KEY_SELECT: getTranslation("back.json_editor.key_select"),
NO_KEY_OPTIONS: getTranslation("back.json_editor.no_key_options"),
ERROR_KEY_EXISTS: getTranslation("back.json_editor.error_key_exists"),
ERROR_INVALID_JSON: getTranslation(
ITEM_SINGLE: useGetTranslation("back.json_editor.item_single"),
ITEMS_MULTIPLE: useGetTranslation("back.json_editor.item_multiple"),
KEY_NEW: useGetTranslation("back.json_editor.key_new"),
KEY_SELECT: useGetTranslation("back.json_editor.key_select"),
NO_KEY_OPTIONS: useGetTranslation("back.json_editor.no_key_options"),
ERROR_KEY_EXISTS: useGetTranslation(
"back.json_editor.error_key_exists",
),
ERROR_INVALID_JSON: useGetTranslation(
"back.json_editor.error_invalid_json",
),
ERROR_UPDATE: getTranslation("back.json_editor.error_update"),
ERROR_DELETE: getTranslation("back.json_editor.error_delete"),
ERROR_ADD: getTranslation("back.json_editor.error_add"),
DEFAULT_STRING: getTranslation("back.json_editor.default_string"),
DEFAULT_NEW_KEY: getTranslation("back.json_editor.default_new_key"),
SHOW_LESS: getTranslation("back.json_editor.show_less"),
EMPTY_STRING: getTranslation("back.json_editor.empty_string"),
TOOLTIP_COPY: getTranslation("back.json_editor.copy"),
TOOLTIP_EDIT: getTranslation("back.json_editor.edit"),
TOOLTIP_DELETE: getTranslation("back.json_editor.delete"),
TOOLTIP_ADD: getTranslation("back.json_editor.add"),
ERROR_UPDATE: useGetTranslation("back.json_editor.error_update"),
ERROR_DELETE: useGetTranslation("back.json_editor.error_delete"),
ERROR_ADD: useGetTranslation("back.json_editor.error_add"),
DEFAULT_STRING: useGetTranslation("back.json_editor.default_string"),
DEFAULT_NEW_KEY: useGetTranslation("back.json_editor.default_new_key"),
SHOW_LESS: useGetTranslation("back.json_editor.show_less"),
EMPTY_STRING: useGetTranslation("back.json_editor.empty_string"),
TOOLTIP_COPY: useGetTranslation("back.json_editor.copy"),
TOOLTIP_EDIT: useGetTranslation("back.json_editor.edit"),
TOOLTIP_DELETE: useGetTranslation("back.json_editor.delete"),
TOOLTIP_ADD: useGetTranslation("back.json_editor.add"),
};
/**
@ -137,7 +156,6 @@ export default function ImportConfigJson({ onConfirm }: ImportConfigJsonProps) {
const openFilePicker = (): void => {
inputRef.current?.click();
};
/**
* Set json from content file.
*
@ -148,12 +166,7 @@ export default function ImportConfigJson({ onConfirm }: ImportConfigJsonProps) {
if (file.size > maxSize) {
setGlobalErrors((prev) => ({
...prev,
"global-error": useTrans(
"back.json_editor.validation.max_size",
{
params: { value: "2MB" },
},
),
"global-error": transJsonEditorMaxSize,
}));
return;
}
@ -161,9 +174,7 @@ export default function ImportConfigJson({ onConfirm }: ImportConfigJsonProps) {
if (!file.name.endsWith(".json")) {
setGlobalErrors((prev) => ({
...prev,
"global-error": useTrans("back.json_editor.validation.mimes", {
params: { values: ".json" },
}),
"global-error": transJsonEditorMimes,
}));
return;
}
@ -181,20 +192,33 @@ export default function ImportConfigJson({ onConfirm }: ImportConfigJsonProps) {
} catch {
setGlobalErrors((prev) => ({
...prev,
"global-error": useTrans(
"back.json_editor.validation.invalid_file_json",
),
"global-error": transJsonEditorInvalidFileJson,
}));
}
};
/**
* Reset component.
*
* @return void
*/
const resetComponent = (): void => {
setIsOpen(false);
setTimeout(() => {
setJsonText([]);
setGlobalErrors({});
setEditorErrors({});
setOverwrite(false);
}, 200);
};
/**
* Action when changing file.
*
* @param event
* @return void
*/
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
const handleFileChange = (e: ChangeEvent<HTMLInputElement>): void => {
const file = e.target.files?.[0];
if (file) processFile(file);
};
@ -205,7 +229,7 @@ export default function ImportConfigJson({ onConfirm }: ImportConfigJsonProps) {
* @param event
* @return void
*/
const handleDrop = (event: React.DragEvent<HTMLDivElement>): void => {
const handleDrop = (event: DragEvent<HTMLDivElement>): void => {
event.preventDefault();
setIsDragging(false);
@ -227,28 +251,11 @@ export default function ImportConfigJson({ onConfirm }: ImportConfigJsonProps) {
} catch {
setEditorErrors((prev) => ({
...prev,
"editor-error": useTrans(
"back.json_editor.validation.invalid_content_json",
),
"editor-error": transInvalidContentJson,
}));
}
};
/**
* Reset component.
*
* @return void
*/
const resetComponent = (): void => {
setIsOpen(false);
setTimeout(() => {
setJsonText([]);
setGlobalErrors({});
setEditorErrors({});
setOverwrite(false);
}, 200);
};
return (
<>
{/* Dropzone */}
@ -269,7 +276,7 @@ export default function ImportConfigJson({ onConfirm }: ImportConfigJsonProps) {
setIsDragging(true);
}}
onDragLeave={() => setIsDragging(false)}
// @ts-ignore
// @ts-expect-error function return are not compatible for ts
onDrop={handleDrop}
>
<input
@ -309,10 +316,10 @@ export default function ImportConfigJson({ onConfirm }: ImportConfigJsonProps) {
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>
{getTranslation("back.json_editor.modal_title")}
{useGetTranslation("back.json_editor.modal_title")}
</DialogTitle>
<DialogDescription>
{getTranslation(
{useGetTranslation(
"back.json_editor.modal_description",
)}
</DialogDescription>
@ -333,17 +340,19 @@ export default function ImportConfigJson({ onConfirm }: ImportConfigJsonProps) {
<InputError message={editorErrors["editor-error"]} />
<DialogFooter className="sm:justify-between">
<SimpleSwitch
label={getTranslation("back.json_editor.overwrite")}
label={useGetTranslation(
"back.json_editor.overwrite",
)}
name="overwrite"
checked={overwrite}
setData={(_, value) => setOverwrite(value)}
/>
<div className="flex justify-center items-center gap-1.5">
<BtnOptionnalLinkWithTooltip
content={getTranslation(
content={useGetTranslation(
"back.json_editor.cancel",
)}
tooltipContent={getTranslation(
tooltipContent={useGetTranslation(
"back.json_editor.tooltip_cancel",
)}
type="button"
@ -352,10 +361,10 @@ export default function ImportConfigJson({ onConfirm }: ImportConfigJsonProps) {
onClick={resetComponent}
/>
<BtnOptionnalLinkWithTooltip
content={getTranslation(
content={useGetTranslation(
"back.json_editor.validate",
)}
tooltipContent={getTranslation(
tooltipContent={useGetTranslation(
"back.json_editor.tooltip_validate",
)}
type="button"

View file

@ -2,22 +2,13 @@
import { Paginator } from "@/back/types/global";
import PaginationCustom from "@/back/Components/Pagination/PaginationCustom";
import PaginationPerPage from "@/back/Components/Pagination/PaginationPerPage";
import useTrans from "@/hooks/use-translations";
import { GetTranslation } from "@/lib/utils";
export default function PaginationWrapper({
modelsPaginate,
}: {
modelsPaginate: Paginator;
}) {
const GetTranslation = () => {
return useTrans("pagination.informations", {
params: {
total: String(modelsPaginate.total),
page: String(modelsPaginate.last_page),
},
choice: modelsPaginate.last_page > 1 ? 2 : 1,
});
};
return (
modelsPaginate.total > 1 && (
<div className="flex flex-col xl:flex-row justify-between items-center mt-4">
@ -26,7 +17,13 @@ export default function PaginationWrapper({
total={modelsPaginate.total}
/>
<p className="text-sm text-muted-foreground my-2 xl:my-0">
{GetTranslation()}
{GetTranslation("pagination.informations", {
params: {
total: String(modelsPaginate.total),
page: String(modelsPaginate.last_page),
},
choice: modelsPaginate.last_page > 1 ? 2 : 1,
})}
</p>
<PaginationCustom links={modelsPaginate.links} />
</div>

View file

@ -1,6 +1,7 @@
// * Packages
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
import BtnOptionnalLinkWithTooltip from "./BtnOptionnalLinkWithTooltip";
function SubmitButton({
isInDialog = false,

View file

@ -1,3 +1,6 @@
// * Packages
import { ReactNode } from "react";
// * Components shadcn
import {
Table,
TableBody,
@ -6,9 +9,9 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
// * Other
import useTrans from "@/hooks/use-translations";
import { Model } from "@/back/types/global";
import { ReactNode } from "react";
interface DetailsTableProps {
columns: {

View file

@ -1,7 +1,10 @@
import { AppEnv, TranslationField } from "@/back/types/global";
import useTrans from "@/hooks/use-translations";
// * Packages
import { usePage } from "@inertiajs/react";
import { ReactNode } from "react";
// * Other
import { AppEnv } from "@/back/types/global";
import { TranslationField } from "@/back/types/i18n";
import useTrans from "@/hooks/use-translations";
/**
* Resolves the best translation value for the current locale.
@ -14,9 +17,9 @@ import { ReactNode } from "react";
* @return An object with the resolved value and optional fallback locale,
* or null if no translation is available
*/
const getTranslationValue = (
function useGetTranslationValue(
value?: TranslationField,
): { value: string; locale?: string } | null => {
): { value: string; locale?: string } | null {
const { appEnv } = usePage().props as unknown as {
appEnv: AppEnv;
};
@ -29,7 +32,7 @@ const getTranslationValue = (
if (!fallback) return null;
return { value: fallback[1], locale: fallback[0] };
};
}
/**
* Renders a table cell displaying a translated value.
@ -41,15 +44,16 @@ const getTranslationValue = (
* @param value - Translation field containing localized values
* @return A ReactNode ready to be rendered inside a table cell
*/
export const renderTranslationCell = (value?: TranslationField): ReactNode => {
const result = getTranslationValue(value);
export default function RenderTranslationCell({
value,
}: {
value?: TranslationField;
}): ReactNode {
const result = useGetTranslationValue(value);
const notKnown = useTrans("back.table_not_known");
if (!result) {
return (
<p className="text-muted-foreground italic m-0">
{useTrans("back.table_not_known")}
</p>
);
return <p className="text-muted-foreground italic m-0">{notKnown}</p>;
}
if (!result.locale) {
@ -62,4 +66,4 @@ export const renderTranslationCell = (value?: TranslationField): ReactNode => {
{result.value}
</span>
);
};
}

View file

@ -1,3 +1,4 @@
// * Packages
import useTrans from "@/hooks/use-translations";
export default function WordCounter({ value }: { value: string }) {

View file

@ -19,38 +19,6 @@ type AuthenticatedLayoutProps = {
children: ReactNode;
};
export default function AuthenticatedLayout({
title,
beforeNode,
afterNode,
children,
}: AuthenticatedLayoutProps) {
const isCollapsed =
typeof window !== "undefined"
? localStorage.getItem("vite-ui-collapsed")
: null;
const { locale } = usePage().props.appEnv as unknown as {
locale: string;
};
return (
<ThemeProvider key={locale}>
<SidebarProvider defaultOpen={!isCollapsed}>
<TooltipProvider>
<LayoutContent
title={title}
beforeNode={beforeNode}
afterNode={afterNode}
>
{children}
</LayoutContent>
</TooltipProvider>
</SidebarProvider>
</ThemeProvider>
);
}
function LayoutContent({
title,
beforeNode,
@ -59,11 +27,13 @@ function LayoutContent({
}: Omit<AuthenticatedLayoutProps, "children"> & { children: ReactNode }) {
const { open } = useSidebar();
const [isSm, setIsSm] = useState(false);
const sidebarVar = !isSm
? open
let sidebarVar = "0px";
if (!isSm) {
sidebarVar = open
? "var(--sidebar-width)"
: "var(--sidebar-width-icon)"
: "0px";
: "var(--sidebar-width-icon)";
}
useEffect(() => {
const mediaQuery = window.matchMedia("(max-width: 767px)");
@ -109,3 +79,35 @@ function LayoutContent({
</>
);
}
export default function AuthenticatedLayout({
title,
beforeNode,
afterNode,
children,
}: AuthenticatedLayoutProps) {
const isCollapsed =
typeof window !== "undefined"
? localStorage.getItem("vite-ui-collapsed")
: null;
const { locale } = usePage().props.appEnv as unknown as {
locale: string;
};
return (
<ThemeProvider key={locale}>
<SidebarProvider defaultOpen={!isCollapsed}>
<TooltipProvider>
<LayoutContent
title={title}
beforeNode={beforeNode}
afterNode={afterNode}
>
{children}
</LayoutContent>
</TooltipProvider>
</SidebarProvider>
</ThemeProvider>
);
}

View file

@ -4,8 +4,8 @@ import { ReactNode } from "react";
import { Card } from "@/components/ui/card";
// * Other
import ApplicationLogo from "@/back/Components/ApplicationLogo";
import { ThemeProvider } from "@/back/providers/ThemeProvider";
import AppSettings from "@/back/Components/Layout/AppSettings";
import { ThemeProvider } from "@/back/providers/ThemeProvider";
export default function GuestLayout({ children }: { children: ReactNode }) {
return (

View file

@ -9,57 +9,59 @@ import DataTable from "@/back/Components/Table/DataTable";
import DataTableSearch from "@/back/Components/Table/DataTableSearch";
import DataTableFilterButton from "@/back/Components/Table/DataTableFilterButton";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import { Views } from "@/lib/utils";
import { ActivityLogModel, Resource } from "@/back/types/global";
import { GetFormatedDate, GetTranslation, Views } from "@/lib/utils";
import { Resource } from "@/back/types/global";
import { ActivityLogModel } from "@/back/types/activity-logs";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
import { useFormatedDate } from "@/hooks/use-formated-value";
import { Row } from "@tanstack/react-table";
type ActivityLogsEventEnumProps = Record<
string,
{ value: number; name: string; label: string; tailwindclass: string }
>;
type ActivityLogsIndexProps = {
activitylogModels: Resource;
searchFields: string;
activityLogsEventEnum: Record<
string,
{ value: number; name: string; label: string; tailwindclass: string }
>;
activityLogsEventEnum: ActivityLogsEventEnumProps;
};
export default function Index({
activitylogModels,
searchFields,
activityLogsEventEnum,
}: ActivityLogsIndexProps) {
const actionModels = [Views.VISUALIZATION];
const columns = [
const getColumns = (activityLogsEventEnum: ActivityLogsEventEnumProps) => {
return [
{
accessorKey: "event",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(
useTrans(`activity-logs.attributes.${column.id}`),
GetTranslation(`activity-logs.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({
row,
}: {
row: { original: ActivityLogModel; getValue: Function };
}) => {
const currentEvent = Object.values(
activityLogsEventEnum,
).filter((index) => index.value === row.getValue("event"));
const bgClass = `bg-${currentEvent[0].tailwindclass}`;
const hoverClass = `hover:bg-${currentEvent[0].tailwindclass}/90`;
const textClass = `text-${currentEvent[0].tailwindclass}-foreground`;
return currentEvent[0] ? (
cell: ({ row }: { row: Row<ActivityLogModel> }) => {
const eventValue = row.getValue<number>("event");
const currentEvent = Object.values(activityLogsEventEnum).find(
(event) => event.value === eventValue,
);
if (!currentEvent) {
return GetTranslation("activity-logs.event_not_match");
}
const bgClass = `bg-${currentEvent.tailwindclass}`;
const hoverClass = `hover:bg-${currentEvent.tailwindclass}/90`;
const textClass = `text-${currentEvent.tailwindclass}-foreground`;
return (
<Badge className={`${textClass} ${bgClass} ${hoverClass}`}>
{str(useTrans(currentEvent[0].label)).ucFirst().value()}
{str(GetTranslation(currentEvent.label))
.ucFirst()
.value()}
</Badge>
) : (
useTrans("activity-logs.event_not_match")
);
},
},
@ -68,50 +70,62 @@ export default function Index({
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(
useTrans(`activity-logs.attributes.${column.id}`),
GetTranslation(`activity-logs.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({
row,
}: {
row: { original: ActivityLogModel; getValue: Function };
}) => {
if (row.original.user)
cell: ({ row }: { row: Row<ActivityLogModel> }) => {
const {
user,
user_id: userId,
is_console: isConsole,
is_anonymous: isAnonymous,
} = row.original;
if (user) {
return (
<BtnOptionnalLinkWithTooltip
link={route("back.users.show", {
user: row.original.user_id,
user: userId,
})}
content={
<>
<UserIcon />
{`${row.original.user.first_name} ${row.original.user.last_name}`}
{`${user.first_name} ${user.last_name}`}
</>
}
tooltipContent={
<p>
{useTrans("activity-logs.access_model", {
params: {
model: useTrans(
"models.classes.user",
),
{GetTranslation(
"activity-logs.access_model",
{
params: {
model: GetTranslation(
"models.classes.user",
),
},
},
})}
)}
</p>
}
size="sm"
variant="secondary"
/>
);
if (row.original.is_console)
return useTrans("activity-logs.user_in_console");
if (row.original.is_anonymous)
return useTrans("activity-logs.user_anonym");
return useTrans("activity-logs.user_deleted");
}
if (isConsole) {
return GetTranslation("activity-logs.user_in_console");
}
if (isAnonymous) {
return GetTranslation("activity-logs.user_anonym");
}
return GetTranslation("activity-logs.user_deleted");
},
},
{
@ -119,72 +133,89 @@ export default function Index({
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(
useTrans(`activity-logs.attributes.${column.id}`),
GetTranslation(`activity-logs.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({
row,
}: {
row: { getValue: Function; original: ActivityLogModel };
}) => {
const modelSplit = row.getValue("model_class").split("\\");
cell: ({ row }: { row: Row<ActivityLogModel> }) => {
const modelClass = row.getValue<string>("model_class");
const { event, model_id: modelId } = row.original;
const modelSplit = modelClass.split("\\");
const modelName = modelSplit[modelSplit.length - 1];
const model = str(modelName).kebabCase().value();
return row.original.event !==
activityLogsEventEnum.deleted.value ? (
<BtnOptionnalLinkWithTooltip
link={route(
`back.${str(model).plural().value()}.show`,
row.original.model_id,
)}
content={
<>
<FileTextIcon />
{row.getValue("model_class")}
</>
}
tooltipContent={
<p>
{useTrans("activity-logs.access_model", {
params: {
model: useTrans(
`activity-logs.attributes.model_class`,
),
},
})}
</p>
}
size="sm"
variant="secondary"
/>
) : (
<Badge variant="outline">
{row.getValue("model_class")}
</Badge>
);
if (event !== activityLogsEventEnum.deleted.value) {
return (
<BtnOptionnalLinkWithTooltip
link={route(
`back.${str(model).plural().value()}.show`,
modelId,
)}
content={
<>
<FileTextIcon />
{modelClass}
</>
}
tooltipContent={
<p>
{GetTranslation(
"activity-logs.access_model",
{
params: {
model: GetTranslation(
"activity-logs.attributes.model_class",
),
},
},
)}
</p>
}
size="sm"
variant="secondary"
/>
);
}
return <Badge variant="outline">{modelClass}</Badge>;
},
},
{
accessorKey: "created_at",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(useTrans(`validation.attributes.${column.id}`))
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({ row }: { row: { getValue: Function } }) => (
<Badge variant="secondary">
{useFormatedDate(row.getValue("created_at"))}
</Badge>
),
cell: ({ row }: { row: Row<ActivityLogModel> }) => {
const createdAt = row.getValue<string>("created_at");
return (
<Badge variant="secondary">
{GetFormatedDate(createdAt)}
</Badge>
);
},
},
];
};
export default function Index({
activitylogModels,
searchFields,
activityLogsEventEnum,
}: ActivityLogsIndexProps) {
const actionModels = [Views.VISUALIZATION];
const columns = getColumns(activityLogsEventEnum);
return (
<AuthenticatedLayout
title={str(useTrans("models.classes.activity-log"))

View file

@ -9,36 +9,30 @@ 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 {
ActivityLogModel,
AuthProp,
SingleResource,
} from "@/back/types/global";
import { AuthProp, SingleResource } from "@/back/types/global";
import { ActivityLogModel } from "@/back/types/activity-logs";
import str from "@/hooks/use-string";
import { useFormatedDate } from "@/hooks/use-formated-value";
import useTrans from "@/hooks/use-translations";
import { GetFormatedDate, GetTranslation } from "@/lib/utils";
type ActivityLogsEventEnumProps = Record<
string,
{ value: number; name: string; label: string; tailwindclass: string }
>;
type ActivityLogsShowProps = {
data: SingleResource;
activityLogsEventEnum: Record<
string,
{ value: number; name: string; label: string; tailwindclass: string }
>;
activityLogsEventEnum: ActivityLogsEventEnumProps;
};
export default function Show({
data,
activityLogsEventEnum,
}: ActivityLogsShowProps) {
const { auth } = usePage().props as unknown as {
auth: AuthProp;
};
const { viewAny } = auth.policies.activityLogs;
const activityLogModel = data.data as ActivityLogModel;
const columns = [
const getColumns = (
activityLogModel: ActivityLogModel,
activityLogsEventEnum: ActivityLogsEventEnumProps,
) => {
return [
{
accessorKey: "event",
header: str(useTrans("activity-logs.attributes.event"))
header: str(GetTranslation("activity-logs.attributes.event"))
.ucFirst()
.value(),
cell: ({ value }: { value: number }) => {
@ -53,22 +47,24 @@ export default function Show({
<Badge
className={`${textClass} ${bgClass} ${hoverClass}`}
>
{str(useTrans(currentEvent[0].label))
{str(GetTranslation(currentEvent[0].label))
.ucFirst()
.value()}
</Badge>
) : (
useTrans("activity-logs.event_not_match")
GetTranslation("activity-logs.event_not_match")
);
}
<span className="text-sm italic text-muted-foreground">
{useTrans("back.table_not_known")}
</span>;
return (
<span className="text-sm italic text-muted-foreground">
{GetTranslation("back.table_not_known")}
</span>
);
},
},
{
accessorKey: "user_id",
header: str(useTrans("activity-logs.attributes.user_id"))
header: str(GetTranslation("activity-logs.attributes.user_id"))
.ucFirst()
.value(),
cell: ({ value }: { value: number }) => {
@ -84,13 +80,16 @@ export default function Show({
}
tooltipContent={
<p>
{useTrans("activity-logs.access_model", {
params: {
model: useTrans(
"models.classes.user",
),
{GetTranslation(
"activity-logs.access_model",
{
params: {
model: GetTranslation(
"models.classes.user",
),
},
},
})}
)}
</p>
}
size="sm"
@ -98,15 +97,15 @@ export default function Show({
/>
);
if (activityLogModel.is_console)
return useTrans("activity-logs.user_in_console");
return GetTranslation("activity-logs.user_in_console");
if (activityLogModel.is_anonymous)
return useTrans("activity-logs.user_anonym");
return useTrans("activity-logs.user_deleted");
return GetTranslation("activity-logs.user_anonym");
return GetTranslation("activity-logs.user_deleted");
},
},
{
accessorKey: "model_class",
header: str(useTrans("activity-logs.attributes.model_class"))
header: str(GetTranslation("activity-logs.attributes.model_class"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => {
@ -128,9 +127,9 @@ export default function Show({
}
tooltipContent={
<p>
{useTrans("activity-logs.access_model", {
{GetTranslation("activity-logs.access_model", {
params: {
model: useTrans(
model: GetTranslation(
"activity-logs.attributes.model_class",
),
},
@ -147,18 +146,20 @@ export default function Show({
},
{
accessorKey: "modifications",
header: str(useTrans("activity-logs.attributes.modifications"))
header: str(
GetTranslation("activity-logs.attributes.modifications"),
)
.ucFirst()
.value(),
cell: ({ value }: { value: object }) => {
return value && Object.entries(value).length ? (
<ul className="list-disc ps-4">
{Object.entries(value).map((value, index) => {
{Object.entries(value).map(([val, text]) => {
return (
<li key={index}>
{value[0]}{" "}
<li key={val}>
{val}{" "}
<span className="text-muted-foreground">
({value[1]})
({text})
</span>
</li>
);
@ -166,22 +167,34 @@ export default function Show({
</ul>
) : (
<span className="text-sm italic text-muted-foreground">
{useTrans("activity-logs.no_changes")}
{GetTranslation("activity-logs.no_changes")}
</span>
);
},
},
{
accessorKey: "created_at",
header: str(useTrans("validation.attributes.created_at"))
header: str(GetTranslation("validation.attributes.created_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{useFormatedDate(value)}</Badge>
<Badge variant="secondary">{GetFormatedDate(value)}</Badge>
),
},
];
};
export default function Show({
data,
activityLogsEventEnum,
}: ActivityLogsShowProps) {
const { auth } = usePage().props as unknown as {
auth: AuthProp;
};
const { viewAny } = auth.policies.activityLogs;
const activityLogModel = data.data as ActivityLogModel;
const columns = getColumns(activityLogModel, activityLogsEventEnum);
const transBackToList = useTrans("back.crud_back_to_list");
const beforeNode = () => {
return (
viewAny && (
@ -189,9 +202,7 @@ export default function Show({
<BtnOptionnalLinkWithTooltip
link={route("back.activity-logs.index")}
content={<ArrowLeftIcon />}
tooltipContent={
<p>{useTrans("back.crud_back_to_list")}</p>
}
tooltipContent={<p>{transBackToList}</p>}
/>
</div>
)

View file

@ -8,7 +8,8 @@ import Form from "@/back/Pages/Companies/Form";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import useTrans from "@/hooks/use-translations";
import { Actions, Methods } from "@/lib/enums";
import { AuthProp, CompanyModel, SingleResource } from "@/back/types/global";
import { AuthProp, SingleResource } from "@/back/types/global";
import { CompanyModel } from "@/back/types/others";
import str from "@/hooks/use-string";
export default function Create({

View file

@ -9,7 +9,8 @@ import Form from "@/back/Pages/Companies/Form";
import useTrans from "@/hooks/use-translations";
import { Actions, Methods } from "@/lib/enums";
import { Views } from "@/lib/utils";
import { AuthProp, CompanyModel, SingleResource } from "@/back/types/global";
import { AuthProp, SingleResource } from "@/back/types/global";
import { CompanyModel } from "@/back/types/others";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import str from "@/hooks/use-string";

View file

@ -11,7 +11,8 @@ import SimpleWysiwyg from "@/back/Components/Inputs/Wysiwyg/SimpleWysiwyg";
import useTrans from "@/hooks/use-translations";
import str from "@/hooks/use-string";
import InputError from "@/back/Components/InputError";
import { CompanyModel, DynamicFieldTypes } from "@/back/types/global";
import { DynamicFieldTypes } from "@/back/types/dynamic-pages";
import { CompanyModel } from "@/back/types/others";
import SubmitButton from "@/back/Components/SubmitButton";
import useCrudSubmit from "@/hooks/use-crud-submit";
import { ModelSelect } from "@/back/Components/Inputs/ModelSelect";
@ -37,7 +38,7 @@ export default function Form({
name: model?.name,
logo: model?.logo ?? "",
responsable: model?.responsable ?? "",
url: model?.url ?? "",
url: model?.url ?? null,
informations: model?.informations,
employees: model?.employees ?? 0,
latitude: model?.latitude,
@ -99,7 +100,7 @@ export default function Form({
.ucFirst()
.value()}
setData={setData}
// @ts-ignore
// @ts-expect-error Types diff between custom reset and useForm reset.
reset={reset}
value={data.logo}
required={false}
@ -145,7 +146,7 @@ export default function Form({
type="url"
placeholder="ex: https://..."
required={false}
value={data.url}
value={data.url ?? ""}
setData={setData}
/>
<SimpleWysiwyg

View file

@ -20,50 +20,29 @@ 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 { Views } from "@/lib/utils";
import { GetFormatedDate, GetTranslation, Views } from "@/lib/utils";
import { AppEnv, AuthProp, Resource } from "@/back/types/global";
import {
AppEnv,
AuthProp,
CompanyActivityModel,
CompanyModel,
CompanySectorModel,
Resource,
} from "@/back/types/global";
} from "@/back/types/others";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
export default function Index({
companyModels,
searchFields,
modelName,
}: {
companyModels: Resource;
searchFields: string;
modelName: string;
}) {
const { appEnv, auth } = usePage().props as unknown as {
appEnv: AppEnv;
auth: AuthProp;
};
const { viewAny, viewTrash, create } = auth.policies.companies;
const { viewAny: viewAnyCompanyActivities } =
auth.policies.companyActivities;
const { viewAny: viewAnyCompanySectors } = auth.policies.companySectors;
const isArchiveRoute = route().current(`back.${modelName}.trash`);
const actionModels = isArchiveRoute
? [Views.RESTORATION, Views.FORCEDELETION]
: [
Views.VISUALIZATION,
Views.EDITION,
Views.DUPLICATION,
Views.ARCHIVATION,
];
const columns = [
const getColumns = (
appEnv: AppEnv,
viewAnyCompanyActivities: boolean,
viewAnyCompanySectors: boolean,
) => {
return [
{
accessorKey: "name",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(useTrans(`validation.attributes.${column.id}`))
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
@ -76,7 +55,7 @@ export default function Index({
header: () => (
<p className="text-center">
{str(
useTrans("models.classes.company-activity", {
GetTranslation("models.classes.company-activity", {
choice: 1,
}),
)
@ -106,9 +85,9 @@ export default function Index({
}
tooltipContent={
<p>
{useTrans("back.access_model", {
{GetTranslation("back.access_model", {
params: {
model: useTrans(
model: GetTranslation(
"models.classes.company-activity",
{ choice: 1 },
),
@ -128,7 +107,7 @@ export default function Index({
</div>
) : (
<p className="text-muted-foreground italic m-0">
{useTrans("back.table_not_known")}
{GetTranslation("back.table_not_known")}
</p>
);
},
@ -138,7 +117,7 @@ export default function Index({
header: () => (
<p className="text-center">
{str(
useTrans("models.classes.company-sector", {
GetTranslation("models.classes.company-sector", {
choice: 1,
}),
)
@ -168,9 +147,9 @@ export default function Index({
}
tooltipContent={
<p>
{useTrans("back.access_model", {
{GetTranslation("back.access_model", {
params: {
model: useTrans(
model: GetTranslation(
"models.classes.company-sector",
{ choice: 1 },
),
@ -190,7 +169,7 @@ export default function Index({
</div>
) : (
<p className="text-muted-foreground italic m-0">
{useTrans("back.table_not_known")}
{GetTranslation("back.table_not_known")}
</p>
);
},
@ -199,11 +178,11 @@ export default function Index({
accessorKey: "coords",
header: () => (
<p className="text-center">
{str(useTrans("validation.custom.latitude"))
{str(GetTranslation("validation.custom.latitude"))
.ucFirst()
.value()}
/
{str(useTrans("validation.custom.longitude"))
{str(GetTranslation("validation.custom.longitude"))
.ucFirst()
.value()}
</p>
@ -220,7 +199,7 @@ export default function Index({
accessorKey: "published",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(useTrans(`validation.custom.${column.id}`))
label={str(GetTranslation(`validation.custom.${column.id}`))
.ucFirst()
.value()}
field={column.id}
@ -241,7 +220,7 @@ export default function Index({
<XIcon size={15} className="mr-1" />
)}
{str(
useTrans(
GetTranslation(
row.getValue("published") ? "back.yes" : "back.no",
),
)
@ -250,7 +229,58 @@ export default function Index({
</Badge>
),
},
{
accessorKey: "updated_at",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({ row }: { row: Row<CompanyModel> }) => (
<Badge variant="secondary">
{GetFormatedDate(row.getValue("updated_at"))}
</Badge>
),
},
];
};
export default function Index({
companyModels,
searchFields,
modelName,
}: {
companyModels: Resource;
searchFields: string;
modelName: string;
}) {
const { appEnv, auth } = usePage().props as unknown as {
appEnv: AppEnv;
auth: AuthProp;
};
const { viewAny, viewTrash, create } = auth.policies.companies;
const { viewAny: viewAnyCompanyActivities } =
auth.policies.companyActivities;
const { viewAny: viewAnyCompanySectors } = auth.policies.companySectors;
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(
appEnv,
viewAnyCompanyActivities,
viewAnyCompanySectors,
);
const transModel = useTrans(
`models.classes.${str(modelName).singular().value()}`,
@ -262,7 +292,7 @@ export default function Index({
const transTitle = str(isArchiveRoute ? transArchiveTitle : transModel)
.ucFirst()
.value();
const transBackToList = useTrans("back.crud_back_to_list");
const beforeNode = () => {
return (
isArchiveRoute &&
@ -271,14 +301,28 @@ export default function Index({
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.index`)}
content={<ArrowLeftIcon />}
tooltipContent={
<p>{useTrans("back.crud_back_to_list")}</p>
}
tooltipContent={<p>{transBackToList}</p>}
/>
</div>
)
);
};
const transAccessToCompanyActivity = useTrans("back.crud_access_to", {
params: {
target: useTrans("models.classes.company-activity", { choice: 2 }),
},
});
const transAccessToCompanySector = useTrans("back.crud_access_to", {
params: {
target: useTrans("models.classes.company-sector", { choice: 2 }),
},
});
const transAccessToArchived = useTrans("back.crud_access_to", {
params: {
target: useTrans("back.archived_entries"),
},
});
const transAddEntry = useTrans("back.crud_add_entry");
const afterNode = () => {
return (
!isArchiveRoute && (
@ -288,16 +332,7 @@ export default function Index({
link={route(`back.company-activities.index`)}
content={<RollerCoasterIcon />}
tooltipContent={
<p>
{useTrans("back.crud_access_to", {
params: {
target: useTrans(
"models.classes.company-activity",
{ choice: 2 },
),
},
})}
</p>
<p>{transAccessToCompanyActivity}</p>
}
variant="outline"
/>
@ -306,18 +341,7 @@ export default function Index({
<BtnOptionnalLinkWithTooltip
link={route(`back.company-sectors.index`)}
content={<RadarIcon />}
tooltipContent={
<p>
{useTrans("back.crud_access_to", {
params: {
target: useTrans(
"models.classes.company-sector",
{ choice: 2 },
),
},
})}
</p>
}
tooltipContent={<p>{transAccessToCompanySector}</p>}
variant="outline"
/>
)}
@ -325,17 +349,7 @@ export default function Index({
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.trash`)}
content={<ArchiveIcon />}
tooltipContent={
<p>
{useTrans("back.crud_access_to", {
params: {
target: useTrans(
"back.archived_entries",
),
},
})}
</p>
}
tooltipContent={<p>{transAccessToArchived}</p>}
variant="outline"
/>
)}
@ -343,9 +357,7 @@ export default function Index({
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.create`)}
content={<PlusIcon />}
tooltipContent={
<p>{useTrans("back.crud_add_entry")}</p>
}
tooltipContent={<p>{transAddEntry}</p>}
/>
)}
</div>

View file

@ -9,19 +9,181 @@ 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 { Views } from "@/lib/utils";
import { useFormatedDate } from "@/hooks/use-formated-value";
import { GetFormatedDate, GetTranslation, Views } from "@/lib/utils";
import { AppEnv, AuthProp, SingleResource } from "@/back/types/global";
import {
AppEnv,
AuthProp,
CompanyActivityModel,
CompanyModel,
CompanySectorModel,
SingleResource,
} from "@/back/types/global";
} from "@/back/types/others";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
const getColumns = (appEnv: AppEnv, auth: AuthProp) => {
return [
{
accessorKey: "name",
header: str(GetTranslation("validation.attributes.name"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => value,
},
{
accessorKey: "logo",
header: str(GetTranslation("validation.custom.logo"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => value,
},
{
accessorKey: "responsable",
header: str(GetTranslation("validation.custom.responsable"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => value,
},
{
accessorKey: "url",
header: str(GetTranslation("validation.custom.url"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => value,
},
{
accessorKey: "informations",
header: str(GetTranslation("validation.custom.informations"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => value,
},
{
accessorKey: "employees",
header: str(GetTranslation("validation.custom.employees"))
.ucFirst()
.value(),
cell: ({ value }: { value: number }) => value,
},
{
accessorKey: "latitude",
header: str(GetTranslation("validation.custom.latitude"))
.ucFirst()
.value(),
cell: ({ value }: { value: number }) => value,
},
{
accessorKey: "longitude",
header: str(GetTranslation("validation.custom.longitude"))
.ucFirst()
.value(),
cell: ({ value }: { value: number }) => value,
},
{
accessorKey: "company_activity",
header: str(
GetTranslation("models.classes.company-activity", {
choice: 1,
}),
)
.ucFirst()
.value(),
cell: ({ value }: { value: CompanyActivityModel }) =>
auth.policies.companyActivities.viewAny ? (
<BtnOptionnalLinkWithTooltip
link={route("back.company-activities.show", value)}
content={
<>
<RollerCoasterIcon />
<span>{value.name[appEnv.locale]}</span>
</>
}
tooltipContent={
<p>
{GetTranslation("back.access_model", {
params: {
model: GetTranslation(
"models.classes.company-activity",
{ choice: 1 },
),
},
})}
</p>
}
type="button"
size="default"
variant="secondary"
/>
) : (
<span>{value.name[appEnv.locale]}</span>
),
},
{
accessorKey: "company_sector",
header: str(
GetTranslation("models.classes.company-sector", { choice: 1 }),
)
.ucFirst()
.value(),
cell: ({ value }: { value: CompanySectorModel | null }) =>
value !== null &&
(auth.policies.companySectors.viewAny ? (
<BtnOptionnalLinkWithTooltip
link={route("back.company-sectors.show", value)}
content={
<>
<RadarIcon />
<span>{value.name[appEnv.locale]}</span>
</>
}
tooltipContent={
<p>
{GetTranslation("back.access_model", {
params: {
model: GetTranslation(
"models.classes.company-sector",
{ choice: 1 },
),
},
})}
</p>
}
type="button"
size="default"
variant="secondary"
/>
) : (
<span>{value.name[appEnv.locale]}</span>
)),
},
{
accessorKey: "created_at",
header: str(GetTranslation("validation.attributes.created_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{GetFormatedDate(value)}</Badge>
),
},
{
accessorKey: "updated_at",
header: str(GetTranslation("validation.attributes.updated_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{GetFormatedDate(value)}</Badge>
),
},
{
accessorKey: "deleted_at",
header: str(GetTranslation("validation.attributes.deleted_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{GetFormatedDate(value)}</Badge>
),
},
];
};
export default function Show({
data,
modelName,
@ -44,163 +206,8 @@ export default function Show({
}),
},
});
const columns = [
{
accessorKey: "name",
header: str(useTrans("validation.attributes.name"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => value,
},
{
accessorKey: "logo",
header: str(useTrans("validation.custom.logo")).ucFirst().value(),
cell: ({ value }: { value: string }) => value,
},
{
accessorKey: "responsable",
header: str(useTrans("validation.custom.responsable"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => value,
},
{
accessorKey: "url",
header: str(useTrans("validation.custom.url")).ucFirst().value(),
cell: ({ value }: { value: string }) => value,
},
{
accessorKey: "informations",
header: str(useTrans("validation.custom.informations"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => value,
},
{
accessorKey: "employees",
header: str(useTrans("validation.custom.employees"))
.ucFirst()
.value(),
cell: ({ value }: { value: number }) => value,
},
{
accessorKey: "latitude",
header: str(useTrans("validation.custom.latitude"))
.ucFirst()
.value(),
cell: ({ value }: { value: number }) => value,
},
{
accessorKey: "longitude",
header: str(useTrans("validation.custom.longitude"))
.ucFirst()
.value(),
cell: ({ value }: { value: number }) => value,
},
{
accessorKey: "company_activity",
header: str(
useTrans("models.classes.company-activity", { choice: 1 }),
)
.ucFirst()
.value(),
cell: ({ value }: { value: CompanyActivityModel }) =>
auth.policies.companyActivities.viewAny ? (
<BtnOptionnalLinkWithTooltip
link={route("back.company-activities.show", value)}
content={
<>
<RollerCoasterIcon />
<span>{value.name[appEnv.locale]}</span>
</>
}
tooltipContent={
<p>
{useTrans("back.access_model", {
params: {
model: useTrans(
"models.classes.company-activity",
{ choice: 1 },
),
},
})}
</p>
}
type="button"
size="default"
variant="secondary"
/>
) : (
<span>{value.name[appEnv.locale]}</span>
),
},
{
accessorKey: "company_sector",
header: str(
useTrans("models.classes.company-sector", { choice: 1 }),
)
.ucFirst()
.value(),
cell: ({ value }: { value: CompanySectorModel | null }) =>
value !== null &&
(auth.policies.companySectors.viewAny ? (
<BtnOptionnalLinkWithTooltip
link={route("back.company-sectors.show", value)}
content={
<>
<RadarIcon />
<span>{value.name[appEnv.locale]}</span>
</>
}
tooltipContent={
<p>
{useTrans("back.access_model", {
params: {
model: useTrans(
"models.classes.company-sector",
{ choice: 1 },
),
},
})}
</p>
}
type="button"
size="default"
variant="secondary"
/>
) : (
<span>{value.name[appEnv.locale]}</span>
)),
},
{
accessorKey: "created_at",
header: str(useTrans("validation.attributes.created_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{useFormatedDate(value)}</Badge>
),
},
{
accessorKey: "updated_at",
header: str(useTrans("validation.attributes.updated_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{useFormatedDate(value)}</Badge>
),
},
{
accessorKey: "deleted_at",
header: str(useTrans("validation.attributes.deleted_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{useFormatedDate(value)}</Badge>
),
},
];
const columns = getColumns(appEnv, auth);
const transBackToList = useTrans("back.crud_back_to_list");
const beforeNode = () => {
return (
viewAny && (
@ -208,9 +215,7 @@ export default function Show({
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.index`)}
content={<ArrowLeftIcon />}
tooltipContent={
<p>{useTrans("back.crud_back_to_list")}</p>
}
tooltipContent={<p>{transBackToList}</p>}
/>
</div>
)

View file

@ -8,11 +8,8 @@ import Form from "@/back/Pages/CompanyActivities/Form";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import useTrans from "@/hooks/use-translations";
import { Actions, Methods } from "@/lib/enums";
import {
AuthProp,
CompanyActivityModel,
SingleResource,
} from "@/back/types/global";
import { AuthProp, SingleResource } from "@/back/types/global";
import { CompanyActivityModel } from "@/back/types/others";
import str from "@/hooks/use-string";
export default function Create({

View file

@ -9,11 +9,8 @@ import Form from "@/back/Pages/CompanyActivities/Form";
import useTrans from "@/hooks/use-translations";
import { Actions, Methods } from "@/lib/enums";
import { Views } from "@/lib/utils";
import {
AuthProp,
CompanyActivityModel,
SingleResource,
} from "@/back/types/global";
import { AuthProp, SingleResource } from "@/back/types/global";
import { CompanyActivityModel } from "@/back/types/others";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import str from "@/hooks/use-string";

View file

@ -10,7 +10,8 @@ import SubmitButton from "@/back/Components/SubmitButton";
import useTrans from "@/hooks/use-translations";
import str from "@/hooks/use-string";
import InputError from "@/back/Components/InputError";
import { AppEnv, CompanyActivityModel } from "@/back/types/global";
import { AppEnv } from "@/back/types/global";
import { CompanyActivityModel } from "@/back/types/others";
import useCrudSubmit from "@/hooks/use-crud-submit";
export default function Form({

View file

@ -23,54 +23,36 @@ 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 { Views } from "@/lib/utils";
import { AuthProp, CompanyActivityModel, Resource } from "@/back/types/global";
import { GetFormatedDate, GetTranslation, Views } from "@/lib/utils";
import { AuthProp, Resource } from "@/back/types/global";
import { CompanyActivityModel } from "@/back/types/others";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
import { renderTranslationCell } from "@/lib/translationCellFallback";
import RenderTranslationCell from "@/back/Components/Table/RenderTranslationCell";
export default function Index({
companyActivityModels,
searchFields,
modelName,
}: {
companyActivityModels: Resource;
searchFields: string;
modelName: string;
}) {
const { auth } = usePage().props as unknown as {
auth: AuthProp;
};
const { viewAny, viewTrash, create } = auth.policies.companyActivities;
const isArchiveRoute = route().current(`back.${modelName}.trash`);
const actionModels = isArchiveRoute
? [Views.RESTORATION, Views.FORCEDELETION]
: [
Views.VISUALIZATION,
Views.EDITION,
Views.DUPLICATION,
Views.ARCHIVATION,
];
const columns = [
const getColumns = () => {
return [
{
accessorKey: "name",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(useTrans(`validation.attributes.${column.id}`))
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({ row }: { row: Row<CompanyActivityModel> }) => {
return renderTranslationCell(row.getValue("name"));
},
cell: ({ row }: { row: Row<CompanyActivityModel> }) => (
<RenderTranslationCell value={row.getValue("name")} />
),
},
{
accessorKey: "color",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(useTrans(`validation.custom.${column.id}`))
label={str(GetTranslation(`validation.custom.${column.id}`))
.ucFirst()
.value()}
field={column.id}
@ -97,7 +79,7 @@ export default function Index({
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(
useTrans("models.classes.company", { choice: 2 }),
GetTranslation("models.classes.company", { choice: 2 }),
)
.ucFirst()
.value()}
@ -112,7 +94,7 @@ export default function Index({
accessorKey: "published",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(useTrans(`validation.custom.${column.id}`))
label={str(GetTranslation(`validation.custom.${column.id}`))
.ucFirst()
.value()}
field={column.id}
@ -133,7 +115,7 @@ export default function Index({
<XIcon size={15} className="mr-1" />
)}
{str(
useTrans(
GetTranslation(
row.getValue("published") ? "back.yes" : "back.no",
),
)
@ -142,7 +124,50 @@ export default function Index({
</Badge>
),
},
{
accessorKey: "updated_at",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({ row }: { row: Row<CompanyActivityModel> }) => (
<Badge variant="secondary">
{GetFormatedDate(row.getValue("updated_at"))}
</Badge>
),
},
];
};
export default function Index({
companyActivityModels,
searchFields,
modelName,
}: {
companyActivityModels: Resource;
searchFields: string;
modelName: string;
}) {
const { auth } = usePage().props as unknown as {
auth: AuthProp;
};
const { viewAny, viewTrash, create } = auth.policies.companyActivities;
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();
const transModel = useTrans(
`models.classes.${str(modelName).singular().value()}`,
@ -154,7 +179,7 @@ export default function Index({
const transTitle = str(isArchiveRoute ? transArchiveTitle : transModel)
.ucFirst()
.value();
const transBackToList = useTrans("back.crud_back_to_list");
const beforeNode = () => {
return (
isArchiveRoute &&
@ -163,14 +188,18 @@ export default function Index({
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.index`)}
content={<ArrowLeftIcon />}
tooltipContent={
<p>{useTrans("back.crud_back_to_list")}</p>
}
tooltipContent={<p>{transBackToList}</p>}
/>
</div>
)
);
};
const transAccessToArchived = useTrans("back.crud_access_to", {
params: {
target: useTrans("back.archived_entries"),
},
});
const transAddEntry = useTrans("back.crud_add_entry");
const afterNode = () => {
return (
!isArchiveRoute && (
@ -179,17 +208,7 @@ export default function Index({
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.trash`)}
content={<ArchiveIcon />}
tooltipContent={
<p>
{useTrans("back.crud_access_to", {
params: {
target: useTrans(
"back.archived_entries",
),
},
})}
</p>
}
tooltipContent={<p>{transAccessToArchived}</p>}
variant="outline"
/>
)}
@ -197,9 +216,7 @@ export default function Index({
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.create`)}
content={<PlusIcon />}
tooltipContent={
<p>{useTrans("back.crud_add_entry")}</p>
}
tooltipContent={<p>{transAddEntry}</p>}
/>
)}
</div>

View file

@ -1,6 +1,7 @@
// * Packages
import { usePage } from "@inertiajs/react";
import { route } from "ziggy-js";
import { ArrowLeftIcon, CheckIcon, XIcon } from "lucide-react";
// * Components shadcn
import { Badge } from "@/components/ui/badge";
// * Other
@ -9,19 +10,86 @@ import DetailsTable from "@/back/Components/Table/DetailsTable";
import ModelActions from "@/back/Components/ModelActions";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import AssociatedCompaniesList from "@/back/Components/AssociatedCompaniesList";
import { Views } from "@/lib/utils";
import { useFormatedDate } from "@/hooks/use-formated-value";
import {
AuthProp,
CompanyActivityModel,
Resource,
SingleResource,
TranslationField,
} from "@/back/types/global";
import RenderTranslationCell from "@/back/Components/Table/RenderTranslationCell";
import { GetFormatedDate, GetTranslation, Views } from "@/lib/utils";
import { AuthProp, Resource, SingleResource } from "@/back/types/global";
import { TranslationField } from "@/back/types/i18n";
import { CompanyActivityModel } from "@/back/types/others";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
import { ArrowLeftIcon, CheckIcon, XIcon } from "lucide-react";
import { renderTranslationCell } from "@/lib/translationCellFallback";
const getColumns = () => {
return [
{
accessorKey: "name",
header: str(GetTranslation("validation.attributes.name"))
.ucFirst()
.value(),
cell: ({ value }: { value: TranslationField }) => (
<RenderTranslationCell value={value} />
),
},
{
accessorKey: "color",
header: str(GetTranslation("validation.custom.color"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => value,
},
{
accessorKey: "created_at",
header: str(GetTranslation("validation.attributes.created_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{GetFormatedDate(value)}</Badge>
),
},
{
accessorKey: "published",
header: str(GetTranslation("validation.custom.published"))
.ucFirst()
.value(),
cell: ({ value }: { value: boolean }) => (
<Badge
variant="outline"
className={`border-0 ${
value
? "bg-green-200 dark:bg-green-900"
: "bg-red-200 dark:bg-red-900"
}`}
>
{value ? (
<CheckIcon size={15} className="mr-1" />
) : (
<XIcon size={15} className="mr-1" />
)}
{str(GetTranslation(value ? "back.yes" : "back.no"))
.ucFirst()
.value()}
</Badge>
),
},
{
accessorKey: "updated_at",
header: str(GetTranslation("validation.attributes.updated_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{GetFormatedDate(value)}</Badge>
),
},
{
accessorKey: "deleted_at",
header: str(GetTranslation("validation.attributes.deleted_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{GetFormatedDate(value)}</Badge>
),
},
];
};
export default function Show({
data,
@ -46,75 +114,8 @@ export default function Show({
}),
},
});
const columns = [
{
accessorKey: "name",
header: str(useTrans("validation.attributes.name"))
.ucFirst()
.value(),
cell: ({ value }: { value: TranslationField }) => {
return renderTranslationCell(value);
},
},
{
accessorKey: "color",
header: str(useTrans("validation.custom.color")).ucFirst().value(),
cell: ({ value }: { value: string }) => value,
},
{
accessorKey: "created_at",
header: str(useTrans("validation.attributes.created_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{useFormatedDate(value)}</Badge>
),
},
{
accessorKey: "published",
header: str(useTrans("validation.custom.published"))
.ucFirst()
.value(),
cell: ({ value }: { value: boolean }) => (
<Badge
variant="outline"
className={`border-0 ${
value
? "bg-green-200 dark:bg-green-900"
: "bg-red-200 dark:bg-red-900"
}`}
>
{value ? (
<CheckIcon size={15} className="mr-1" />
) : (
<XIcon size={15} className="mr-1" />
)}
{str(useTrans(value ? "back.yes" : "back.no"))
.ucFirst()
.value()}
</Badge>
),
},
{
accessorKey: "updated_at",
header: str(useTrans("validation.attributes.updated_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{useFormatedDate(value)}</Badge>
),
},
{
accessorKey: "deleted_at",
header: str(useTrans("validation.attributes.deleted_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{useFormatedDate(value)}</Badge>
),
},
];
const columns = getColumns();
const transBackToList = useTrans("back.crud_back_to_list");
const beforeNode = () => {
return (
viewAny && (
@ -122,9 +123,7 @@ export default function Show({
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.index`)}
content={<ArrowLeftIcon />}
tooltipContent={
<p>{useTrans("back.crud_back_to_list")}</p>
}
tooltipContent={<p>{transBackToList}</p>}
/>
</div>
)
@ -132,16 +131,7 @@ export default function Show({
};
return (
<AuthenticatedLayout
title={useTrans("crud.helpers.visualization_model", {
params: {
model: useTrans("models.classes.company-activity", {
choice: 1,
}),
},
})}
beforeNode={beforeNode()}
>
<AuthenticatedLayout title={transTitle} beforeNode={beforeNode()}>
<ModelActions
actions={actionModels}
modelRoute={modelName}

View file

@ -8,11 +8,8 @@ import Form from "@/back/Pages/CompanySectors/Form";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import useTrans from "@/hooks/use-translations";
import { Actions, Methods } from "@/lib/enums";
import {
AuthProp,
CompanySectorModel,
SingleResource,
} from "@/back/types/global";
import { AuthProp, SingleResource } from "@/back/types/global";
import { CompanySectorModel } from "@/back/types/others";
import str from "@/hooks/use-string";
export default function Create({

View file

@ -9,11 +9,8 @@ import Form from "@/back/Pages/CompanySectors/Form";
import useTrans from "@/hooks/use-translations";
import { Actions, Methods } from "@/lib/enums";
import { Views } from "@/lib/utils";
import {
AuthProp,
CompanySectorModel,
SingleResource,
} from "@/back/types/global";
import { AuthProp, SingleResource } from "@/back/types/global";
import { CompanySectorModel } from "@/back/types/others";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import str from "@/hooks/use-string";

View file

@ -9,7 +9,8 @@ import SubmitButton from "@/back/Components/SubmitButton";
import useTrans from "@/hooks/use-translations";
import str from "@/hooks/use-string";
import InputError from "@/back/Components/InputError";
import { AppEnv, CompanySectorModel } from "@/back/types/global";
import { AppEnv } from "@/back/types/global";
import { CompanySectorModel } from "@/back/types/others";
import useCrudSubmit from "@/hooks/use-crud-submit";
export default function Form({

View file

@ -18,16 +18,101 @@ 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 { Views } from "@/lib/utils";
import {
AppEnv,
AuthProp,
CompanySectorModel,
Resource,
} from "@/back/types/global";
import RenderTranslationCell from "@/back/Components/Table/RenderTranslationCell";
import { GetFormatedDate, GetTranslation, Views } from "@/lib/utils";
import { AppEnv, AuthProp, Resource } from "@/back/types/global";
import { CompanySectorModel } from "@/back/types/others";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
import { renderTranslationCell } from "@/lib/translationCellFallback";
const getColumns = () => {
return [
{
accessorKey: "name",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({ row }: { row: Row<CompanySectorModel> }) => (
<RenderTranslationCell value={row.getValue("name")} />
),
},
{
accessorKey: "companies",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(
GetTranslation("models.classes.company", { choice: 2 }),
)
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({ row }: { row: Row<CompanySectorModel> }) => {
return <p>{row.original.companies.length}</p>;
},
},
{
accessorKey: "published",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(GetTranslation(`validation.custom.${column.id}`))
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({ row }: { row: Row<CompanySectorModel> }) => (
<Badge
variant="outline"
className={`border-0 ${
row.getValue("published")
? "bg-green-200 dark:bg-green-900"
: "bg-red-200 dark:bg-red-900"
}`}
>
{row.getValue("published") ? (
<CheckIcon size={15} className="mr-1" />
) : (
<XIcon size={15} className="mr-1" />
)}
{str(
GetTranslation(
row.getValue("published") ? "back.yes" : "back.no",
),
)
.ucFirst()
.value()}
</Badge>
),
},
{
accessorKey: "updated_at",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({ row }: { row: Row<CompanySectorModel> }) => (
<Badge variant="secondary">
{GetFormatedDate(row.getValue("updated_at"))}
</Badge>
),
},
];
};
export default function Index({
companySectorModels,
@ -52,72 +137,7 @@ export default function Index({
Views.DUPLICATION,
Views.ARCHIVATION,
];
const columns = [
{
accessorKey: "name",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(useTrans(`validation.attributes.${column.id}`))
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({ row }: { row: Row<CompanySectorModel> }) => {
return renderTranslationCell(row.getValue("name"));
},
},
{
accessorKey: "companies",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(
useTrans("models.classes.company", { choice: 2 }),
)
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({ row }: { row: Row<CompanySectorModel> }) => {
return <p>{row.original.companies.length}</p>;
},
},
{
accessorKey: "published",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(useTrans(`validation.custom.${column.id}`))
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({ row }: { row: Row<CompanySectorModel> }) => (
<Badge
variant="outline"
className={`border-0 ${
row.getValue("published")
? "bg-green-200 dark:bg-green-900"
: "bg-red-200 dark:bg-red-900"
}`}
>
{row.getValue("published") ? (
<CheckIcon size={15} className="mr-1" />
) : (
<XIcon size={15} className="mr-1" />
)}
{str(
useTrans(
row.getValue("published") ? "back.yes" : "back.no",
),
)
.ucFirst()
.value()}
</Badge>
),
},
];
const columns = getColumns();
const transModel = useTrans(
`models.classes.${str(modelName).singular().value()}`,
@ -130,6 +150,7 @@ export default function Index({
.ucFirst()
.value();
const transBackToList = useTrans("back.crud_back_to_list");
const beforeNode = () => {
return (
isArchiveRoute &&
@ -138,14 +159,18 @@ export default function Index({
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.index`)}
content={<ArrowLeftIcon />}
tooltipContent={
<p>{useTrans("back.crud_back_to_list")}</p>
}
tooltipContent={<p>{transBackToList}</p>}
/>
</div>
)
);
};
const transAccessToArchived = useTrans("back.crud_access_to", {
params: {
target: useTrans("back.archived_entries"),
},
});
const transAddEntry = useTrans("back.crud_add_entry");
const afterNode = () => {
return (
!isArchiveRoute && (
@ -154,17 +179,7 @@ export default function Index({
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.trash`)}
content={<ArchiveIcon />}
tooltipContent={
<p>
{useTrans("back.crud_access_to", {
params: {
target: useTrans(
"back.archived_entries",
),
},
})}
</p>
}
tooltipContent={<p>{transAccessToArchived}</p>}
variant="outline"
/>
)}
@ -172,9 +187,7 @@ export default function Index({
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.create`)}
content={<PlusIcon />}
tooltipContent={
<p>{useTrans("back.crud_add_entry")}</p>
}
tooltipContent={<p>{transAddEntry}</p>}
/>
)}
</div>

View file

@ -10,18 +10,79 @@ import DetailsTable from "@/back/Components/Table/DetailsTable";
import ModelActions from "@/back/Components/ModelActions";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import AssociatedCompaniesList from "@/back/Components/AssociatedCompaniesList";
import { Views } from "@/lib/utils";
import { useFormatedDate } from "@/hooks/use-formated-value";
import {
AuthProp,
CompanySectorModel,
Resource,
SingleResource,
TranslationField,
} from "@/back/types/global";
import RenderTranslationCell from "@/back/Components/Table/RenderTranslationCell";
import { GetFormatedDate, GetTranslation, Views } from "@/lib/utils";
import { AuthProp, Resource, SingleResource } from "@/back/types/global";
import { CompanySectorModel } from "@/back/types/others";
import { TranslationField } from "@/back/types/i18n";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
import { renderTranslationCell } from "@/lib/translationCellFallback";
const getColumns = () => {
return [
{
accessorKey: "name",
header: str(GetTranslation("validation.attributes.name"))
.ucFirst()
.value(),
cell: ({ value }: { value: TranslationField }) => (
<RenderTranslationCell value={value} />
),
},
{
accessorKey: "published",
header: str(GetTranslation("validation.custom.published"))
.ucFirst()
.value(),
cell: ({ value }: { value: boolean }) => (
<Badge
variant="outline"
className={`border-0 ${
value
? "bg-green-200 dark:bg-green-900"
: "bg-red-200 dark:bg-red-900"
}`}
>
{value ? (
<CheckIcon size={15} className="mr-1" />
) : (
<XIcon size={15} className="mr-1" />
)}
{str(GetTranslation(value ? "back.yes" : "back.no"))
.ucFirst()
.value()}
</Badge>
),
},
{
accessorKey: "created_at",
header: str(GetTranslation("validation.attributes.created_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{GetFormatedDate(value)}</Badge>
),
},
{
accessorKey: "updated_at",
header: str(GetTranslation("validation.attributes.updated_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{GetFormatedDate(value)}</Badge>
),
},
{
accessorKey: "deleted_at",
header: str(GetTranslation("validation.attributes.deleted_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{GetFormatedDate(value)}</Badge>
),
},
];
};
export default function Show({
data,
@ -46,70 +107,8 @@ export default function Show({
}),
},
});
const columns = [
{
accessorKey: "name",
header: str(useTrans("validation.attributes.name"))
.ucFirst()
.value(),
cell: ({ value }: { value: TranslationField }) => {
return renderTranslationCell(value);
},
},
{
accessorKey: "published",
header: str(useTrans("validation.custom.published"))
.ucFirst()
.value(),
cell: ({ value }: { value: boolean }) => (
<Badge
variant="outline"
className={`border-0 ${
value
? "bg-green-200 dark:bg-green-900"
: "bg-red-200 dark:bg-red-900"
}`}
>
{value ? (
<CheckIcon size={15} className="mr-1" />
) : (
<XIcon size={15} className="mr-1" />
)}
{str(useTrans(value ? "back.yes" : "back.no"))
.ucFirst()
.value()}
</Badge>
),
},
{
accessorKey: "created_at",
header: str(useTrans("validation.attributes.created_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{useFormatedDate(value)}</Badge>
),
},
{
accessorKey: "updated_at",
header: str(useTrans("validation.attributes.updated_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{useFormatedDate(value)}</Badge>
),
},
{
accessorKey: "deleted_at",
header: str(useTrans("validation.attributes.deleted_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{useFormatedDate(value)}</Badge>
),
},
];
const columns = getColumns();
const transBackToList = useTrans("back.crud_back_to_list");
const beforeNode = () => {
return (
viewAny && (
@ -117,9 +116,7 @@ export default function Show({
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.index`)}
content={<ArrowLeftIcon />}
tooltipContent={
<p>{useTrans("back.crud_back_to_list")}</p>
}
tooltipContent={<p>{transBackToList}</p>}
/>
</div>
)

View file

@ -1,27 +1,24 @@
// * Packages
import { Row } from "@tanstack/react-table";
// * Components shadcn
import { Badge } from "@/components/ui/badge";
// * Other
import AuthenticatedLayout from "@/back/Layouts/AuthenticatedLayout";
import DataTable from "@/back/Components/Table/DataTable";
import { ContactCountModel, LaravelEnum, Resource } from "@/back/types/global";
import { LaravelEnum, Resource } from "@/back/types/global";
import { ContactCountModel } from "@/back/types/others";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
import { GetFormatedDate, GetTranslation } from "@/lib/utils";
export default function Index({
contactCountModels,
contactFormType,
modelName,
}: {
contactCountModels: Resource;
contactFormType: LaravelEnum;
modelName: string;
}) {
const columns = [
const getColumns = (contactFormType: LaravelEnum) => {
return [
{
accessorKey: "type",
header: ({ column }: { column: { id: string } }) => (
<p className="text-center">
{str(
useTrans(
GetTranslation(
`validation.custom.contact_count.${column.id}`,
),
)
@ -33,7 +30,7 @@ export default function Index({
const type = Object.values(contactFormType).find(
(item) => item.value === row.getValue("type"),
);
if (!type) return;
if (!type) return "";
return (
<p className="text-muted-foreground italic m-0">
{str(type.label).ucFirst().value()}
@ -46,7 +43,7 @@ export default function Index({
header: ({ column }: { column: { id: string } }) => (
<p className="text-center">
{str(
useTrans(
GetTranslation(
`validation.custom.contact_count.${column.id}`,
),
)
@ -55,7 +52,34 @@ export default function Index({
</p>
),
},
{
accessorKey: "updated_at",
header: ({ column }: { column: { id: string } }) => (
<p className="text-center">
{str(GetTranslation(`validation.attributes.${column.id}`))
.ucFirst()
.value()}
</p>
),
cell: ({ row }: { row: Row<ContactCountModel> }) => (
<Badge variant="secondary">
{GetFormatedDate(row.getValue("updated_at"))}
</Badge>
),
},
];
};
export default function Index({
contactCountModels,
contactFormType,
modelName,
}: {
contactCountModels: Resource;
contactFormType: LaravelEnum;
modelName: string;
}) {
const columns = getColumns(contactFormType);
const transModel = useTrans(
`models.classes.${str(modelName).singular().value()}`,

View file

@ -8,7 +8,8 @@ import AuthenticatedLayout from "@/back/Layouts/AuthenticatedLayout";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import useTrans from "@/hooks/use-translations";
import { Actions, Methods } from "@/lib/enums";
import { AuthProp, DocumentModel, SingleResource } from "@/back/types/global";
import { AuthProp, SingleResource } from "@/back/types/global";
import { DocumentModel } from "@/back/types/others";
import Form from "@/back/Pages/Document/Form";
import str from "@/hooks/use-string";

View file

@ -10,7 +10,8 @@ import useTrans from "@/hooks/use-translations";
import { Actions, Methods } from "@/lib/enums";
import Form from "@/back/Pages/Document/Form";
import { Views } from "@/lib/utils";
import { AuthProp, DocumentModel, SingleResource } from "@/back/types/global";
import { AuthProp, SingleResource } from "@/back/types/global";
import { DocumentModel } from "@/back/types/others";
import str from "@/hooks/use-string";
export default function Edit({
@ -22,7 +23,11 @@ export default function Edit({
}) {
const { viewAny } = (usePage().props.auth as AuthProp).policies.documents;
const docsModel = data.data as DocumentModel;
const actionModels = [Views.VISUALIZATION, Views.DELETION];
const actionModels = [
Views.VISUALIZATION,
Views.DUPLICATION,
Views.DELETION,
];
const singularModelName = str(modelName).singular().value();
// Translations.

View file

@ -13,12 +13,9 @@ import SubmitButton from "@/back/Components/SubmitButton";
import SimpleSwitch from "@/back/Components/Inputs/SimpleSwitch";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import { ModelSelect } from "@/back/Components/Inputs/ModelSelect";
import {
AppEnv,
DynamicFieldTypes,
DocumentModel,
LaravelEnum,
} from "@/back/types/global";
import { AppEnv, LaravelEnum } from "@/back/types/global";
import { DynamicFieldTypes } from "@/back/types/dynamic-pages";
import { DocumentModel } from "@/back/types/others";
import useTrans from "@/hooks/use-translations";
import str from "@/hooks/use-string";
import { extractIdsFromDatabasePivotRelation } from "@/lib/mapPivotOptions";
@ -69,7 +66,7 @@ export default function Form({
action,
model,
post,
data
data,
});
const originalDataPath =
@ -95,6 +92,23 @@ export default function Form({
}
};
//* TRANSLATIONS
const transAssociatedIssuPathLabel = useTrans("back.form_associate_to", {
params: {
attribute: useTrans("validation.custom.issuu_path"),
},
choice: 1,
});
const transAssociatedIssuPathPlaceholder = useTrans(
"back.issuu.issuu_path.placeholder",
);
const transAssociatePathLabel = useTrans("back.form_associate_to", {
params: {
attribute: useTrans("validation.custom.file"),
},
choice: 1,
});
const transRefreshOriginal = useTrans("back.refresh_original");
return (
<form onSubmit={submitCrud}>
<InputError all />
@ -214,23 +228,12 @@ export default function Form({
{data.is_issuu ? (
<SimpleString
name="issuu_path"
label={str(
useTrans("back.form_associate_to", {
params: {
attribute: useTrans(
"validation.custom.issuu_path",
),
},
choice: 1,
}),
)
label={str(transAssociatedIssuPathLabel)
.ucFirst()
.value()}
required={false}
placeholder={str(
useTrans(
"back.issuu.issuu_path.placeholder",
),
transAssociatedIssuPathPlaceholder,
)
.ucFirst()
.value()}
@ -243,21 +246,12 @@ export default function Form({
fileName="path"
modelRoute={modelName}
name="path"
label={str(
useTrans("back.form_associate_to", {
params: {
attribute: useTrans(
"validation.custom.file",
),
},
choice: 1,
}),
)
label={str(transAssociatePathLabel)
.ucFirst()
.value()}
setData={setData}
hasRefreshBtn={false}
// @ts-ignore
// @ts-expect-error Types diff between custom reset and useForm reset.
reset={reset}
value={data.path}
required={false}
@ -272,15 +266,13 @@ export default function Form({
variant="secondary"
onClick={refreshFileFromDb}
content={<RefreshCwIcon />}
tooltipContent={str(
useTrans("back.refresh_original"),
)
tooltipContent={str(transRefreshOriginal)
.ucFirst()
.value()}
/>
)}
</div>
{(!data.is_issuu && model?.path && originalDataPath) && (
{!data.is_issuu && model?.path && originalDataPath && (
<a
target="_blank"
className="text-blue-500 underline text-xs"

View file

@ -18,44 +18,22 @@ import DataTableFilterButton from "@/back/Components/Table/DataTableFilterButton
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import DocumentStatusBadge from "@/back/Components/Documents/DocumentStatusBadge";
import Filters from "@/back/Components/Documents/Filters";
import { cn, Views } from "@/lib/utils";
import { renderTranslationCell } from "@/lib/translationCellFallback";
import { useFormatedDate } from "@/hooks/use-formated-value";
import {
AppEnv,
AuthProp,
DocumentCategoryModel,
DocumentModel,
Resource,
TranslationField,
} from "@/back/types/global";
import { cn, GetFormatedDate, GetTranslation, Views } from "@/lib/utils";
import RenderTranslationCell from "@/back/Components/Table/RenderTranslationCell";
import { AppEnv, AuthProp, Resource } from "@/back/types/global";
import { DocumentModel, DocumentCategoryModel } from "@/back/types/others";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
export default function Index({
documentsModel,
searchFields,
modelName,
}: {
documentsModel: Resource;
searchFields: string;
modelName: string;
}) {
const { auth, appEnv } = usePage().props as unknown as {
auth: AuthProp;
appEnv: AppEnv;
};
const { create } = auth.policies.documents;
const { viewAny: viewAnyDocumentCategories } =
auth.policies.documentCategories;
const { viewAny: viewAnyDocumentTags } = auth.policies.documentTags;
const actionModels = [Views.VISUALIZATION, Views.EDITION, Views.DELETION];
const columns = [
const getColumns = (appEnv: AppEnv, viewAnyDocumentCategories: boolean) => {
return [
{
accessorKey: "name",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(useTrans(`validation.attributes.${column.id}`))
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
@ -85,16 +63,18 @@ export default function Index({
</TooltipTrigger>
<TooltipContent>
<p>
{str(useTrans("back.document.no_link"))
{str(
GetTranslation(
"back.document.no_link",
),
)
.ucFirst()
.value()}
</p>
</TooltipContent>
</Tooltip>
)}
{renderTranslationCell(
row.getValue("name") as TranslationField,
)}
<RenderTranslationCell value={row.getValue("name")} />
</div>
);
},
@ -104,7 +84,7 @@ export default function Index({
header: () => (
<p className="text-center">
{str(
useTrans("models.classes.document-category", {
GetTranslation("models.classes.document-category", {
choice: 2,
}),
)
@ -135,14 +115,17 @@ export default function Index({
}
tooltipContent={
<p>
{useTrans("back.access_model", {
params: {
model: useTrans(
"models.classes.document-category",
{ choice: 1 },
),
{GetTranslation(
"back.access_model",
{
params: {
model: GetTranslation(
"models.classes.document-category",
{ choice: 1 },
),
},
},
})}
)}
</p>
}
type="button"
@ -158,7 +141,7 @@ export default function Index({
</div>
) : (
<p className="text-muted-foreground italic m-0">
{useTrans("back.table_not_known")}
{GetTranslation("back.table_not_known")}
</p>
);
},
@ -167,7 +150,7 @@ export default function Index({
accessorKey: "status",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(useTrans(`validation.custom.${column.id}`))
label={str(GetTranslation(`validation.custom.${column.id}`))
.ucFirst()
.value()}
field={column.id}
@ -176,7 +159,7 @@ export default function Index({
cell: ({ row }: { row: Row<DocumentModel> }) => {
return (
<DocumentStatusBadge
value={parseInt(row.getValue("status"))}
value={Number(row.getValue("status"))}
/>
);
},
@ -185,7 +168,7 @@ export default function Index({
accessorKey: "published_at",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(useTrans(`validation.custom.${column.id}`))
label={str(GetTranslation(`validation.custom.${column.id}`))
.ucFirst()
.value()}
field={column.id}
@ -193,7 +176,7 @@ export default function Index({
),
cell: ({ row }: { row: Row<DocumentModel> }) => (
<Badge variant="secondary">
{useFormatedDate(row.getValue("published_at"))}
{GetFormatedDate(row.getValue("published_at"))}
</Badge>
),
},
@ -201,7 +184,9 @@ export default function Index({
accessorKey: "updated_at",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(useTrans(`validation.attributes.${column.id}`))
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
@ -209,17 +194,48 @@ export default function Index({
),
cell: ({ row }: { row: Row<DocumentModel> }) => (
<Badge variant="secondary">
{useFormatedDate(row.getValue("updated_at"))}
{GetFormatedDate(row.getValue("updated_at"))}
</Badge>
),
},
];
};
export default function Index({
documentsModel,
searchFields,
modelName,
}: {
documentsModel: Resource;
searchFields: string;
modelName: string;
}) {
const { auth, appEnv } = usePage().props as unknown as {
auth: AuthProp;
appEnv: AppEnv;
};
const { create } = auth.policies.documents;
const { viewAny: viewAnyDocumentCategories } =
auth.policies.documentCategories;
const { viewAny: viewAnyDocumentTags } = auth.policies.documentTags;
const actionModels = [Views.VISUALIZATION, Views.EDITION, Views.DELETION];
const columns = getColumns(appEnv, viewAnyDocumentTags);
const transModel = useTrans(
`models.classes.${str(modelName).singular().value()}`,
{ choice: 2 },
);
const transTitle = str(transModel).ucFirst().value();
const transAccessToDocumentCategory = useTrans("back.crud_access_to", {
params: {
target: useTrans("models.classes.document-category", { choice: 2 }),
},
});
const transAccessToDocumentTag = useTrans("back.crud_access_to", {
params: {
target: useTrans("models.classes.document-tag", { choice: 2 }),
},
});
const transAddEntry = useTrans("back.crud_add_entry");
const afterNode = () => {
return (
<div className="flex justify-end items-center gap-x-1.5">
@ -227,18 +243,7 @@ export default function Index({
<BtnOptionnalLinkWithTooltip
link={route(`back.document-categories.index`)}
content={<FolderIcon />}
tooltipContent={
<p>
{useTrans("back.crud_access_to", {
params: {
target: useTrans(
"models.classes.document-category",
{ choice: 2 },
),
},
})}
</p>
}
tooltipContent={<p>{transAccessToDocumentCategory}</p>}
variant="outline"
/>
)}
@ -246,18 +251,7 @@ export default function Index({
<BtnOptionnalLinkWithTooltip
link={route(`back.document-tags.index`)}
content={<TagIcon />}
tooltipContent={
<p>
{useTrans("back.crud_access_to", {
params: {
target: useTrans(
"models.classes.document-tag",
{ choice: 2 },
),
},
})}
</p>
}
tooltipContent={<p>{transAccessToDocumentTag}</p>}
variant="outline"
/>
)}
@ -265,9 +259,7 @@ export default function Index({
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.create`)}
content={<PlusIcon />}
tooltipContent={
<p>{useTrans("back.crud_add_entry")}</p>
}
tooltipContent={<p>{transAddEntry}</p>}
/>
)}
</div>

View file

@ -8,24 +8,273 @@ import { Badge } from "@/components/ui/badge";
import AuthenticatedLayout from "@/back/Layouts/AuthenticatedLayout";
import DetailsTable from "@/back/Components/Table/DetailsTable";
import ModelActions from "@/back/Components/ModelActions";
import { Views } from "@/lib/utils";
import { useFormatedDate } from "@/hooks/use-formated-value";
import { GetFormatedDate, GetTranslation, Views } from "@/lib/utils";
import { AppEnv, AuthProp, SingleResource } from "@/back/types/global";
import {
AppEnv,
AuthProp,
DocumentCategoryModel,
DocumentModel,
DocumentCategoryModel,
DocumentTagModel,
SingleResource,
TranslationField,
} from "@/back/types/global";
} from "@/back/types/others";
import { TranslationField } from "@/back/types/i18n";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import useLocale from "@/store/use-locale-store";
import DocumentStatusBadge from "@/back/Components/Documents/DocumentStatusBadge";
import YearsEnumeBadge from "@/back/Components/Documents/DocumentExerciceBadge";
const getColumns = (
appEnv: AppEnv,
viewAnyDocumentCategories: boolean,
viewAnyDocumentTags: boolean,
) => {
return [
{
accessorKey: "name",
header: str(GetTranslation("validation.attributes.name"))
.ucFirst()
.value(),
cell: ({ value }: { value: TranslationField }) =>
value[appEnv.locale] ? (
value[appEnv.locale]
) : (
<p className="text-muted-foreground italic m-0">
{GetTranslation("back.table_not_known")}
</p>
),
},
{
accessorKey: "path",
header: str(GetTranslation("validation.custom.file"))
.ucFirst()
.value(),
cell: ({ value }: { value: TranslationField }) =>
value[appEnv.locale] ? (
<a
target="_blank"
className="text-blue-500 underline"
href={`${appEnv.url}/${value[appEnv.locale]}`}
rel="noreferrer"
>
{`${appEnv.url}/${value[appEnv.locale]}`}
</a>
) : (
<p className="text-muted-foreground italic m-0">
{GetTranslation("back.table_not_known")}
</p>
),
},
{
accessorKey: "issuu_path",
header: str(GetTranslation("validation.custom.issuu_path"))
.ucFirst()
.value(),
cell: ({ value }: { value: TranslationField }) =>
value[appEnv.locale] ? (
<a
target="_blank"
className="text-blue-500 underline"
href={value[appEnv.locale]}
rel="noreferrer"
>
{value[appEnv.locale]}
</a>
) : (
<p className="text-muted-foreground italic m-0">
{GetTranslation("back.table_not_known")}
</p>
),
},
{
accessorKey: "exercice",
header: str(
GetTranslation("back.associated_model", {
params: {
model: GetTranslation(
"models.classes.document-exercice",
{
choice: 2,
},
),
},
choice: 2,
}),
)
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => {
return <YearsEnumeBadge value={Number(value)} />;
},
},
{
accessorKey: "status",
header: str(GetTranslation("validation.custom.status"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => {
return <DocumentStatusBadge value={Number(value)} />;
},
},
{
accessorKey: "categories",
header: str(
GetTranslation("models.classes.document-category", {
choice: 2,
}),
)
.ucFirst()
.value(),
cell: ({ value }: { value?: DocumentCategoryModel[] }) => {
return value !== undefined && value.length > 0 ? (
<div className="flex flex-row justify-start items-center gap-1.5">
{value.map((item) =>
viewAnyDocumentCategories ? (
<BtnOptionnalLinkWithTooltip
link={route(
"back.document-categories.show",
item,
)}
content={
<>
<FolderIcon />
<span>
{item.name[appEnv.locale]}
</span>
</>
}
tooltipContent={
<p>
{GetTranslation(
"back.access_model",
{
params: {
model: GetTranslation(
"models.classes.document-category",
{ choice: 1 },
),
},
},
)}
</p>
}
type="button"
size="default"
variant="secondary"
/>
) : (
<Badge variant="outline">
{item.name[appEnv.locale]}
</Badge>
),
)}
</div>
) : (
<p className="text-muted-foreground italic m-0">
{GetTranslation("back.table_not_known")}
</p>
);
},
},
{
accessorKey: "tags",
header: str(
GetTranslation("models.classes.document-tag", { choice: 2 }),
)
.ucFirst()
.value(),
cell: ({ value }: { value?: DocumentTagModel[] }) => {
return value !== undefined && value.length > 0 ? (
<div className="flex flex-row justify-start items-center gap-1.5">
{value.map((item) =>
viewAnyDocumentTags ? (
<BtnOptionnalLinkWithTooltip
link={route(
"back.document-tags.show",
item,
)}
content={
<>
<TagIcon />
<span>
{item.name[appEnv.locale]}
</span>
</>
}
tooltipContent={
<p>
{GetTranslation(
"back.access_model",
{
params: {
model: GetTranslation(
"models.classes.document-tag",
{ choice: 1 },
),
},
},
)}
</p>
}
type="button"
size="default"
variant="secondary"
/>
) : (
<Badge variant="outline">
{item.name[appEnv.locale]}
</Badge>
),
)}
</div>
) : (
<p className="text-muted-foreground italic m-0">
{GetTranslation("back.table_not_known")}
</p>
);
},
},
{
accessorKey: "created_at",
header: str(GetTranslation("validation.attributes.created_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => {
return (
<Badge variant="secondary">{GetFormatedDate(value)}</Badge>
);
},
},
{
accessorKey: "published_at",
header: str(GetTranslation("validation.custom.published_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">
{GetFormatedDate(value, undefined, true)}
</Badge>
),
},
{
accessorKey: "unpublished_at",
header: str(GetTranslation("validation.custom.unpublished_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{GetFormatedDate(value)}</Badge>
),
},
{
accessorKey: "updated_at",
header: str(GetTranslation("validation.attributes.updated_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{GetFormatedDate(value)}</Badge>
),
},
];
};
export default function Show({
data,
modelName,
@ -51,236 +300,13 @@ export default function Show({
}),
},
});
const columns = [
{
accessorKey: "name",
header: str(useTrans("validation.attributes.name"))
.ucFirst()
.value(),
cell: ({ value }: { value: TranslationField }) =>
value[appEnv.locale] ? (
value[appEnv.locale]
) : (
<p className="text-muted-foreground italic m-0">
{useTrans("back.table_not_known")}
</p>
),
},
{
accessorKey: "path",
header: str(useTrans("validation.custom.file")).ucFirst().value(),
cell: ({ value }: { value: TranslationField }) =>
value[appEnv.locale] ? (
<a
target="_blank"
className="text-blue-500 underline"
href={`${appEnv.url}/${value[appEnv.locale]}`}
rel="noreferrer"
>
{`${appEnv.url}/${value[appEnv.locale]}`}
</a>
) : (
<p className="text-muted-foreground italic m-0">
{useTrans("back.table_not_known")}
</p>
),
},
{
accessorKey: "issuu_path",
header: str(useTrans("validation.custom.issuu_path"))
.ucFirst()
.value(),
cell: ({ value }: { value: TranslationField }) =>
value[appEnv.locale] ? (
<a
target="_blank"
className="text-blue-500 underline"
href={value[appEnv.locale]}
rel="noreferrer"
>
{value[appEnv.locale]}
</a>
) : (
<p className="text-muted-foreground italic m-0">
{useTrans("back.table_not_known")}
</p>
),
},
{
accessorKey: "exercice",
header: str(
useTrans("back.associated_model", {
params: {
model: useTrans("models.classes.document-exercice", {
choice: 2,
}),
},
choice: 2,
}),
)
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => {
return <YearsEnumeBadge value={parseInt(value)} />;
},
},
{
accessorKey: "status",
header: str(useTrans("validation.custom.status")).ucFirst().value(),
cell: ({ value }: { value: string }) => {
return <DocumentStatusBadge value={parseInt(value)} />;
},
},
{
accessorKey: "categories",
header: str(
useTrans("models.classes.document-category", { choice: 2 }),
)
.ucFirst()
.value(),
cell: ({ value }: { value?: DocumentCategoryModel[] }) => {
return value !== undefined && value.length > 0 ? (
<div className="flex flex-row justify-start items-center gap-1.5">
{value.map((item) =>
viewAnyDocumentCategories ? (
<BtnOptionnalLinkWithTooltip
link={route(
"back.document-categories.show",
item,
)}
content={
<>
<FolderIcon />
<span>
{item.name[appEnv.locale]}
</span>
</>
}
tooltipContent={
<p>
{useTrans("back.access_model", {
params: {
model: useTrans(
"models.classes.document-category",
{ choice: 1 },
),
},
})}
</p>
}
type="button"
size="default"
variant="secondary"
/>
) : (
<Badge variant="outline">
{item.name[appEnv.locale]}
</Badge>
),
)}
</div>
) : (
<p className="text-muted-foreground italic m-0">
{useTrans("back.table_not_known")}
</p>
);
},
},
{
accessorKey: "tags",
header: str(useTrans("models.classes.document-tag", { choice: 2 }))
.ucFirst()
.value(),
cell: ({ value }: { value?: DocumentTagModel[] }) => {
return value !== undefined && value.length > 0 ? (
<div className="flex flex-row justify-start items-center gap-1.5">
{value.map((item) =>
viewAnyDocumentTags ? (
<BtnOptionnalLinkWithTooltip
link={route(
"back.document-tags.show",
item,
)}
content={
<>
<TagIcon />
<span>
{item.name[appEnv.locale]}
</span>
</>
}
tooltipContent={
<p>
{useTrans("back.access_model", {
params: {
model: useTrans(
"models.classes.document-tag",
{ choice: 1 },
),
},
})}
</p>
}
type="button"
size="default"
variant="secondary"
/>
) : (
<Badge variant="outline">
{item.name[appEnv.locale]}
</Badge>
),
)}
</div>
) : (
<p className="text-muted-foreground italic m-0">
{useTrans("back.table_not_known")}
</p>
);
},
},
{
accessorKey: "created_at",
header: str(useTrans("validation.attributes.created_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => {
return (
<Badge variant="secondary">{useFormatedDate(value)}</Badge>
);
},
},
{
accessorKey: "published_at",
header: str(useTrans("validation.custom.published_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">
{useFormatedDate(value, undefined, true)}
</Badge>
),
},
{
accessorKey: "unpublished_at",
header: str(useTrans("validation.custom.unpublished_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{useFormatedDate(value)}</Badge>
),
},
{
accessorKey: "updated_at",
header: str(useTrans("validation.attributes.updated_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{useFormatedDate(value)}</Badge>
),
},
];
const columns = getColumns(
appEnv,
viewAnyDocumentCategories,
viewAnyDocumentTags,
);
const transBackToList = useTrans("back.crud_back_to_list");
const beforeNode = () => {
return (
viewAny && (
@ -288,9 +314,7 @@ export default function Show({
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.index`)}
content={<ArrowLeft />}
tooltipContent={
<p>{useTrans("back.crud_back_to_list")}</p>
}
tooltipContent={<p>{transBackToList}</p>}
/>
</div>
)

View file

@ -7,11 +7,8 @@ import AuthenticatedLayout from "@/back/Layouts/AuthenticatedLayout";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import useTrans from "@/hooks/use-translations";
import { Actions, Methods } from "@/lib/enums";
import {
AuthProp,
DocumentCategoryModel,
SingleResource,
} from "@/back/types/global";
import { AuthProp, SingleResource } from "@/back/types/global";
import { DocumentCategoryModel } from "@/back/types/others";
import Form from "@/back/Pages/DocumentCategory/Form";
import str from "@/hooks/use-string";

View file

@ -1,6 +1,7 @@
// * Packages
import { usePage } from "@inertiajs/react";
import { route } from "ziggy-js";
import { ArrowLeft } from "lucide-react";
// * Other
import AuthenticatedLayout from "@/back/Layouts/AuthenticatedLayout";
import ModelActions from "@/back/Components/ModelActions";
@ -9,12 +10,8 @@ import useTrans from "@/hooks/use-translations";
import { Actions, Methods } from "@/lib/enums";
import Form from "@/back/Pages/DocumentCategory/Form";
import { Views } from "@/lib/utils";
import {
AuthProp,
DocumentCategoryModel,
SingleResource,
} from "@/back/types/global";
import { ArrowLeft } from "lucide-react";
import { AuthProp, SingleResource } from "@/back/types/global";
import { DocumentCategoryModel } from "@/back/types/others";
import str from "@/hooks/use-string";
export default function Edit({

View file

@ -5,7 +5,8 @@ import { BookTextIcon } from "lucide-react";
import FieldsetWrapper from "@/back/Components/FieldsetWrapper";
import InputError from "@/back/Components/InputError";
import SimpleString from "@/back/Components/Inputs/SimpleString";
import { AppEnv, DocumentCategoryModel } from "@/back/types/global";
import { AppEnv } from "@/back/types/global";
import { DocumentCategoryModel } from "@/back/types/others";
import useTrans from "@/hooks/use-translations";
import str from "@/hooks/use-string";
import useCrudSubmit from "@/hooks/use-crud-submit";

View file

@ -11,18 +11,79 @@ import DataTable from "@/back/Components/Table/DataTable";
import DataTableSearch from "@/back/Components/Table/DataTableSearch";
import DataTableFilterButton from "@/back/Components/Table/DataTableFilterButton";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import RenderTranslationCell from "@/back/Components/Table/RenderTranslationCell";
import SoftdeleteWarning from "@/back/Components/SoftdeleteWarning";
import { Views } from "@/lib/utils";
import { useFormatedDate } from "@/hooks/use-formated-value";
import {
AppEnv,
AuthProp,
DocumentCategoryModel,
Resource,
} from "@/back/types/global";
import { GetFormatedDate, GetTranslation, Views } from "@/lib/utils";
import { AppEnv, AuthProp, Resource } from "@/back/types/global";
import { DocumentCategoryModel } from "@/back/types/others";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
import { renderTranslationCell } from "@/lib/translationCellFallback";
const getColumns = () => {
return [
{
accessorKey: "name",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({ row }: { row: Row<DocumentCategoryModel> }) => (
<RenderTranslationCell value={row.getValue("name")} />
),
},
{
accessorKey: "documents",
header: () => (
<p className="w-full text-center">
{str(
GetTranslation("back.associated_model", {
params: {
model: GetTranslation(
"models.classes.document",
{
choice: 2,
},
),
},
choice: 2,
}),
)
.ucFirst()
.value()}
</p>
),
cell: ({ row }: { row: Row<DocumentCategoryModel> }) => (
<div className="w-full flex justify-center">
<p>{row.original.documents_count ?? 0}</p>
</div>
),
},
{
accessorKey: "updated_at",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({ row }: { row: Row<DocumentCategoryModel> }) => (
<Badge variant="secondary">
{GetFormatedDate(row.getValue("updated_at"))}
</Badge>
),
},
];
};
export default function Index({
documentCategoryModel,
@ -47,62 +108,7 @@ export default function Index({
Views.DUPLICATION,
Views.ARCHIVATION,
];
const columns = [
{
accessorKey: "name",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(useTrans(`validation.attributes.${column.id}`))
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({ row }: { row: Row<DocumentCategoryModel> }) => {
return renderTranslationCell(row.getValue("name"));
},
},
{
accessorKey: "documents",
header: () => (
<p className="w-full text-center">
{str(
useTrans("back.associated_model", {
params: {
model: useTrans("models.classes.document", {
choice: 2,
}),
},
choice: 2,
}),
)
.ucFirst()
.value()}
</p>
),
cell: ({ row }: { row: Row<DocumentCategoryModel> }) => (
<div className="w-full flex justify-center">
<p>{row.original.documents_count ?? 0}</p>
</div>
),
},
{
accessorKey: "updated_at",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(useTrans(`validation.attributes.${column.id}`))
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({ row }: { row: Row<DocumentCategoryModel> }) => (
<Badge variant="secondary">
{useFormatedDate(row.getValue("updated_at"))}
</Badge>
),
},
];
const columns = getColumns();
const transModel = useTrans(
`models.classes.${str(modelName).singular().value()}`,
{ choice: 2 },
@ -114,6 +120,7 @@ export default function Index({
.ucFirst()
.value();
const transBackToList = useTrans("back.crud_back_to_list");
const beforeNode = () => {
return (
isArchiveRoute &&
@ -122,14 +129,18 @@ export default function Index({
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.index`)}
content={<ArrowLeftIcon />}
tooltipContent={
<p>{useTrans("back.crud_back_to_list")}</p>
}
tooltipContent={<p>{transBackToList}</p>}
/>
</div>
)
);
};
const transAccessToArchived = useTrans("back.crud_access_to", {
params: {
target: useTrans("back.archived_entries"),
},
});
const transAddEntry = useTrans("back.crud_add_entry");
const afterNode = () => {
return (
!isArchiveRoute && (
@ -138,17 +149,7 @@ export default function Index({
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.trash`)}
content={<ArchiveIcon />}
tooltipContent={
<p>
{useTrans("back.crud_access_to", {
params: {
target: useTrans(
"back.archived_entries",
),
},
})}
</p>
}
tooltipContent={<p>{transAccessToArchived}</p>}
variant="outline"
/>
)}
@ -156,9 +157,7 @@ export default function Index({
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.create`)}
content={<Plus />}
tooltipContent={
<p>{useTrans("back.crud_add_entry")}</p>
}
tooltipContent={<p>{transAddEntry}</p>}
/>
)}
</div>

View file

@ -8,21 +8,68 @@ import { Badge } from "@/components/ui/badge";
import AuthenticatedLayout from "@/back/Layouts/AuthenticatedLayout";
import DetailsTable from "@/back/Components/Table/DetailsTable";
import ModelActions from "@/back/Components/ModelActions";
import { Views } from "@/lib/utils";
import { useFormatedDate } from "@/hooks/use-formated-value";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import AssociatedDocumentList from "@/back/Components/Documents/AssociatedDocumentList";
import RenderTranslationCell from "@/back/Components/Table/RenderTranslationCell";
import { GetFormatedDate, GetTranslation, Views } from "@/lib/utils";
import {
AppEnv,
AuthProp,
DocumentTagModel,
Resource,
SingleResource,
TranslationField,
} from "@/back/types/global";
import { DocumentTagModel } from "@/back/types/others";
import { TranslationField } from "@/back/types/i18n";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import AssociatedDocumentList from "@/back/Components/Documents/AssociatedDocumentList";
import { renderTranslationCell } from "@/lib/translationCellFallback";
const getColumns = (appEnv: AppEnv) => {
return [
{
accessorKey: "name",
header: str(GetTranslation("validation.attributes.name"))
.ucFirst()
.value(),
cell: ({ value }: { value: TranslationField }) => (
<RenderTranslationCell value={value} />
),
},
{
accessorKey: "slug",
header: str(GetTranslation("validation.custom.slug"))
.ucFirst()
.value(),
cell: ({ value }: { value: TranslationField }) =>
value[appEnv.locale] ? (
value[appEnv.locale]
) : (
<p className="text-muted-foreground italic m-0">
{GetTranslation("back.table_not_known")}
</p>
),
},
{
accessorKey: "created_at",
header: str(GetTranslation("validation.attributes.created_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => {
return (
<Badge variant="secondary">{GetFormatedDate(value)}</Badge>
);
},
},
{
accessorKey: "updated_at",
header: str(GetTranslation("validation.attributes.updated_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{GetFormatedDate(value)}</Badge>
),
},
];
};
export default function Show({
data,
@ -48,50 +95,9 @@ export default function Show({
}),
},
});
const columns = [
{
accessorKey: "name",
header: str(useTrans("validation.attributes.name"))
.ucFirst()
.value(),
cell: ({ value }: { value: TranslationField }) => {
return renderTranslationCell(value);
},
},
{
accessorKey: "slug",
header: str(useTrans("validation.custom.slug")).ucFirst().value(),
cell: ({ value }: { value: TranslationField }) =>
value[appEnv.locale] ? (
value[appEnv.locale]
) : (
<p className="text-muted-foreground italic m-0">
{useTrans("back.table_not_known")}
</p>
),
},
{
accessorKey: "created_at",
header: str(useTrans("validation.attributes.created_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => {
return (
<Badge variant="secondary">{useFormatedDate(value)}</Badge>
);
},
},
{
accessorKey: "updated_at",
header: str(useTrans("validation.attributes.updated_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{useFormatedDate(value)}</Badge>
),
},
];
const columns = getColumns(appEnv);
const transBackToList = useTrans("back.crud_back_to_list");
const beforeNode = () => {
return (
viewAny && (
@ -99,9 +105,7 @@ export default function Show({
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.index`)}
content={<ArrowLeft />}
tooltipContent={
<p>{useTrans("back.crud_back_to_list")}</p>
}
tooltipContent={<p>{transBackToList}</p>}
/>
</div>
)

View file

@ -7,11 +7,8 @@ import AuthenticatedLayout from "@/back/Layouts/AuthenticatedLayout";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import useTrans from "@/hooks/use-translations";
import { Actions, Methods } from "@/lib/enums";
import {
AuthProp,
DocumentTagModel,
SingleResource,
} from "@/back/types/global";
import { AuthProp, SingleResource } from "@/back/types/global";
import { DocumentTagModel } from "@/back/types/others";
import Form from "@/back/Pages/DocumentTag/Form";
import str from "@/hooks/use-string";

View file

@ -10,11 +10,8 @@ import useTrans from "@/hooks/use-translations";
import { Actions, Methods } from "@/lib/enums";
import Form from "@/back/Pages/DocumentTag/Form";
import { Views } from "@/lib/utils";
import {
AuthProp,
DocumentTagModel,
SingleResource,
} from "@/back/types/global";
import { AuthProp, SingleResource } from "@/back/types/global";
import { DocumentTagModel } from "@/back/types/others";
import str from "@/hooks/use-string";
export default function Edit({

View file

@ -5,8 +5,8 @@ import { BookTextIcon } from "lucide-react";
import FieldsetWrapper from "@/back/Components/FieldsetWrapper";
import InputError from "@/back/Components/InputError";
import SimpleString from "@/back/Components/Inputs/SimpleString";
import { Methods } from "@/lib/enums";
import { AppEnv, DocumentTagModel } from "@/back/types/global";
import { AppEnv } from "@/back/types/global";
import { DocumentTagModel } from "@/back/types/others";
import useTrans from "@/hooks/use-translations";
import str from "@/hooks/use-string";
import useCrudSubmit from "@/hooks/use-crud-submit";
@ -43,7 +43,7 @@ export default function Form({
action,
model,
post,
data
data,
});
return (

View file

@ -16,62 +16,37 @@ import DataTable from "@/back/Components/Table/DataTable";
import DataTableSearch from "@/back/Components/Table/DataTableSearch";
import DataTableFilterButton from "@/back/Components/Table/DataTableFilterButton";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import RenderTranslationCell from "@/back/Components/Table/RenderTranslationCell";
import SoftdeleteWarning from "@/back/Components/SoftdeleteWarning";
import { Views } from "@/lib/utils";
import { useFormatedDate } from "@/hooks/use-formated-value";
import {
AppEnv,
AuthProp,
DocumentTagModel,
Resource,
} from "@/back/types/global";
import { GetFormatedDate, GetTranslation, Views } from "@/lib/utils";
import { AppEnv, AuthProp, Resource } from "@/back/types/global";
import { DocumentTagModel } from "@/back/types/others";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
import { renderTranslationCell } from "@/lib/translationCellFallback";
export default function Index({
documentTagModel,
searchFields,
modelName,
}: {
documentTagModel: Resource;
searchFields: string;
modelName: string;
}) {
const { auth } = usePage().props as unknown as {
auth: AuthProp;
appEnv: AppEnv;
};
const { viewAny, viewTrash, create } = auth.policies.documentTags;
const isArchiveRoute = route().current(`back.${modelName}.trash`);
const actionModels = isArchiveRoute
? [Views.RESTORATION, Views.FORCEDELETION]
: [
Views.VISUALIZATION,
Views.EDITION,
Views.DUPLICATION,
Views.ARCHIVATION,
];
const columns = [
const getColumns = () => {
return [
{
accessorKey: "name",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(useTrans(`validation.attributes.${column.id}`))
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
/>
),
cell: ({ row }: { row: Row<DocumentTagModel> }) => {
return renderTranslationCell(row.getValue("name"));
},
cell: ({ row }: { row: Row<DocumentTagModel> }) => (
<RenderTranslationCell value={row.getValue("name")} />
),
},
{
accessorKey: "color",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(useTrans(`validation.custom.${column.id}`))
label={str(GetTranslation(`validation.custom.${column.id}`))
.ucFirst()
.value()}
field={column.id}
@ -98,11 +73,14 @@ export default function Index({
header: () => (
<p className="w-full text-center">
{str(
useTrans("back.associated_model", {
GetTranslation("back.associated_model", {
params: {
model: useTrans("models.classes.document", {
choice: 2,
}),
model: GetTranslation(
"models.classes.document",
{
choice: 2,
},
),
},
choice: 2,
}),
@ -121,7 +99,9 @@ export default function Index({
accessorKey: "updated_at",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(useTrans(`validation.attributes.${column.id}`))
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
@ -129,11 +109,37 @@ export default function Index({
),
cell: ({ row }: { row: Row<DocumentTagModel> }) => (
<Badge variant="secondary">
{useFormatedDate(row.getValue("updated_at"))}
{GetFormatedDate(row.getValue("updated_at"))}
</Badge>
),
},
];
};
export default function Index({
documentTagModel,
searchFields,
modelName,
}: {
documentTagModel: Resource;
searchFields: string;
modelName: string;
}) {
const { auth } = usePage().props as unknown as {
auth: AuthProp;
appEnv: AppEnv;
};
const { viewAny, viewTrash, create } = auth.policies.documentTags;
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();
const transModel = useTrans(
`models.classes.${str(modelName).singular().value()}`,
{ choice: 2 },
@ -145,6 +151,7 @@ export default function Index({
.ucFirst()
.value();
const transBackToList = useTrans("back.crud_back_to_list");
const beforeNode = () => {
return (
isArchiveRoute &&
@ -153,15 +160,19 @@ export default function Index({
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.index`)}
content={<ArrowLeft />}
tooltipContent={
<p>{useTrans("back.crud_back_to_list")}</p>
}
tooltipContent={<p>{transBackToList}</p>}
/>
</div>
)
);
};
const transAccessToArchived = useTrans("back.crud_access_to", {
params: {
target: useTrans("back.archived_entries"),
},
});
const transAddEntry = useTrans("back.crud_add_entry");
const afterNode = () => {
return (
!isArchiveRoute && (
@ -170,17 +181,7 @@ export default function Index({
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.trash`)}
content={<Archive />}
tooltipContent={
<p>
{useTrans("back.crud_access_to", {
params: {
target: useTrans(
"back.archived_entries",
),
},
})}
</p>
}
tooltipContent={<p>{transAccessToArchived}</p>}
variant="outline"
/>
)}
@ -188,9 +189,7 @@ export default function Index({
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.create`)}
content={<Plus />}
tooltipContent={
<p>{useTrans("back.crud_add_entry")}</p>
}
tooltipContent={<p>{transAddEntry}</p>}
/>
)}
</div>

View file

@ -1,6 +1,7 @@
// * Packages
import { usePage } from "@inertiajs/react";
import { route } from "ziggy-js";
import { ArrowLeft } from "lucide-react";
// * Components shadcn
import { Badge } from "@/components/ui/badge";
import {
@ -12,22 +13,89 @@ import {
import AuthenticatedLayout from "@/back/Layouts/AuthenticatedLayout";
import DetailsTable from "@/back/Components/Table/DetailsTable";
import ModelActions from "@/back/Components/ModelActions";
import { Views } from "@/lib/utils";
import { useFormatedDate } from "@/hooks/use-formated-value";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import AssociatedDocumentList from "@/back/Components/Documents/AssociatedDocumentList";
import RenderTranslationCell from "@/back/Components/Table/RenderTranslationCell";
import { GetFormatedDate, GetTranslation, Views } from "@/lib/utils";
import {
AppEnv,
AuthProp,
DocumentTagModel,
Resource,
SingleResource,
TranslationField,
} from "@/back/types/global";
import { DocumentTagModel } from "@/back/types/others";
import { TranslationField } from "@/back/types/i18n";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import { ArrowLeft } from "lucide-react";
import AssociatedDocumentList from "@/back/Components/Documents/AssociatedDocumentList";
import { renderTranslationCell } from "@/lib/translationCellFallback";
const getColumns = (appEnv: AppEnv) => {
return [
{
accessorKey: "name",
header: str(GetTranslation("validation.attributes.name"))
.ucFirst()
.value(),
cell: ({ value }: { value: TranslationField }) => (
<RenderTranslationCell value={value} />
),
},
{
accessorKey: "slug",
header: str(GetTranslation("validation.custom.slug"))
.ucFirst()
.value(),
cell: ({ value }: { value: TranslationField }) =>
value[appEnv.locale] ? (
value[appEnv.locale]
) : (
<p className="text-muted-foreground italic m-0">
{GetTranslation("back.table_not_known")}
</p>
),
},
{
accessorKey: "color",
header: str(GetTranslation("validation.custom.color"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => {
return (
<Tooltip>
<TooltipTrigger asChild>
<div
className="w-8 h-8 rounded-md border"
style={{
backgroundColor: value,
}}
/>
</TooltipTrigger>
<TooltipContent>{value}</TooltipContent>
</Tooltip>
);
},
},
{
accessorKey: "created_at",
header: str(GetTranslation("validation.attributes.created_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => {
return (
<Badge variant="secondary">{GetFormatedDate(value)}</Badge>
);
},
},
{
accessorKey: "updated_at",
header: str(GetTranslation("validation.attributes.updated_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{GetFormatedDate(value)}</Badge>
),
},
];
};
export default function Show({
data,
@ -53,69 +121,9 @@ export default function Show({
}),
},
});
const columns = [
{
accessorKey: "name",
header: str(useTrans("validation.attributes.name"))
.ucFirst()
.value(),
cell: ({ value }: { value: TranslationField }) => {
return renderTranslationCell(value);
},
},
{
accessorKey: "slug",
header: str(useTrans("validation.custom.slug")).ucFirst().value(),
cell: ({ value }: { value: TranslationField }) =>
value[appEnv.locale] ? (
value[appEnv.locale]
) : (
<p className="text-muted-foreground italic m-0">
{useTrans("back.table_not_known")}
</p>
),
},
{
accessorKey: "color",
header: str(useTrans("validation.custom.color")).ucFirst().value(),
cell: ({ value }: { value: string }) => {
return (
<Tooltip>
<TooltipTrigger asChild>
<div
className="w-8 h-8 rounded-md border"
style={{
backgroundColor: value,
}}
/>
</TooltipTrigger>
<TooltipContent>{value}</TooltipContent>
</Tooltip>
);
},
},
{
accessorKey: "created_at",
header: str(useTrans("validation.attributes.created_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => {
return (
<Badge variant="secondary">{useFormatedDate(value)}</Badge>
);
},
},
{
accessorKey: "updated_at",
header: str(useTrans("validation.attributes.updated_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{useFormatedDate(value)}</Badge>
),
},
];
const columns = getColumns(appEnv);
const transBackToList = useTrans("back.crud_back_to_list");
const beforeNode = () => {
return (
viewAny && (
@ -123,9 +131,7 @@ export default function Show({
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.index`)}
content={<ArrowLeft />}
tooltipContent={
<p>{useTrans("back.crud_back_to_list")}</p>
}
tooltipContent={<p>{transBackToList}</p>}
/>
</div>
)

View file

@ -8,11 +8,8 @@ import Form from "@/back/Pages/DynamicPages/Form";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import useTrans from "@/hooks/use-translations";
import { Actions, Methods } from "@/lib/enums";
import {
AuthProp,
DynamicPageModel,
SingleResource,
} from "@/back/types/global";
import { AuthProp, SingleResource } from "@/back/types/global";
import { DynamicPageModel } from "@/back/types/dynamic-pages";
import str from "@/hooks/use-string";
export default function Create({

View file

@ -10,11 +10,8 @@ import Form from "@/back/Pages/DynamicPages/Form";
import useTrans from "@/hooks/use-translations";
import { Actions, Methods } from "@/lib/enums";
import { Views } from "@/lib/utils";
import {
AuthProp,
SingleResource,
DynamicPageModel,
} from "@/back/types/global";
import { AuthProp, SingleResource } from "@/back/types/global";
import { DynamicPageModel } from "@/back/types/dynamic-pages";
import str from "@/hooks/use-string";
export default function Edit({

View file

@ -34,14 +34,12 @@ import GoToFrontView from "@/back/Components/GoToFrontView";
import useTrans from "@/hooks/use-translations";
import InputError from "@/back/Components/InputError";
import str from "@/hooks/use-string";
import { AppEnv, AuthProp, LaravelEnum } from "@/back/types/global";
import {
AppEnv,
AuthProp,
ConfigField,
DynamicPageModel,
LaravelEnum,
PageConfigModel,
} from "@/back/types/global";
} from "@/back/types/dynamic-pages";
import SubmitButton from "@/back/Components/SubmitButton";
import useCrudSubmit from "@/hooks/use-crud-submit";
import Slugifycator from "@/back/Components/Inputs/Slugifycator";
@ -75,9 +73,9 @@ export default function Form({
.pageConfigModels as Array<PageConfigModel>;
// Models filtered based on the presence of the locale.
const filteredPageConfigModels = pageConfigModels.filter(
(model) =>
model.label?.[locale] !== undefined &&
model.config?.[locale] !== undefined,
(pageConfigModel) =>
pageConfigModel.label?.[locale] !== undefined &&
pageConfigModel.config?.[locale] !== undefined,
);
// Set empty config by default.
@ -121,8 +119,24 @@ export default function Form({
model: useTrans("models.classes.page-config", { choice: 1 }),
},
});
const transValidationRootLabel = useTrans("validation.custom.root");
const transValidationParentPageLabel = useTrans("back.form_associate_to", {
params: {
attribute: useTrans("validation.custom.parent_page"),
},
choice: 2,
});
const transAssociateToStatus = useTrans("back.form_associate_to", {
params: {
attribute: useTrans("validation.custom.status"),
},
choice: 1,
});
const transFormTitleIsLocked = useTrans("back.form_title_isLocked");
const transFormSubtitleIsLocked = useTrans("back.form_subtitle_isLocked");
const getConfig = (id: number) =>
configOptions.find((objet) => objet.id == id)?.config[
configOptions.find((objet) => objet.id === id)?.config[
locale
] as ConfigField[];
@ -137,7 +151,8 @@ export default function Form({
isLocked: model?.isLocked ?? false,
isRoot: model?.isRoot ?? false,
});
const defaultIsRootValue =
!data.isRoot && data.page_id ? [data.page_id] : [];
const submit = useCrudSubmit({
method,
modelRoute,
@ -149,27 +164,31 @@ export default function Form({
});
function getFullSlugPath(
models: DynamicPageModel[],
selectedGroup: string | null,
locale: string = "fr",
targetModels: DynamicPageModel[],
currentSelectedGroup: string | null,
currentLocale: string = "fr",
): string | null {
if (!selectedGroup) return null;
if (!currentSelectedGroup) return null;
// Trouver le modèle sélectionné à partir de son title
const current = models.find(
(m) =>
m.title[locale]?.toLowerCase() === selectedGroup.toLowerCase(),
const current = targetModels.find(
(targetModel) =>
targetModel.title[currentLocale]?.toLowerCase() ===
currentSelectedGroup.toLowerCase(),
);
if (!current) return null;
// Fonction récursive pour reconstruire le chemin
const buildPath = (model: DynamicPageModel): string => {
const parent = model.page_id
? models.find((m) => m.id === model.page_id)
const buildPath = (modelParent: DynamicPageModel): string => {
const parent = modelParent.page_id
? targetModels.find(
(targetModel) => targetModel.id === modelParent.page_id,
)
: null;
const currentTranslatedModelSlug = model.slug[locale] ?? "";
const currentTranslatedModelSlug =
modelParent.slug[currentLocale] ?? "";
if (!parent) return currentTranslatedModelSlug;
@ -211,18 +230,7 @@ export default function Form({
<EnumSelect
name="status"
options={enumDynamicPageStatus}
label={str(
useTrans("back.form_associate_to", {
params: {
attribute: useTrans(
"validation.custom.status",
),
},
choice: 1,
}),
)
.ucFirst()
.value()}
label={str(transAssociateToStatus).ucFirst().value()}
placeholder={`ex: ${enumDynamicPageStatus.draft.label}`}
selected={data.status}
setData={setData}
@ -238,7 +246,7 @@ export default function Form({
{isConceptor && (
<div className="col-span-2">
<SimpleSwitch
label={str(useTrans("validation.custom.root"))
label={str(transValidationRootLabel)
.ucFirst()
.value()}
name="isRoot"
@ -247,7 +255,7 @@ export default function Form({
setSelectedGroup(null);
setData("page_id", null);
setData("slug", "");
// @ts-ignore
// @ts-expect-error TS2345: 'name' type is dynamic, TypeScript cannot infer the correct key
setData(name, value);
}}
/>
@ -261,26 +269,11 @@ export default function Form({
multiple={false}
field="title"
closeOnSelect
label={str(
useTrans("back.form_associate_to", {
params: {
attribute: useTrans(
"validation.custom.parent_page",
),
},
choice: 2,
}),
)
label={str(transValidationParentPageLabel)
.ucFirst()
.value()}
placeholder="ex: "
defaultValues={
!data.isRoot
? data.page_id
? [data.page_id]
: []
: []
}
defaultValues={defaultIsRootValue}
exclude={model?.id}
required={false}
setData={setData}
@ -430,7 +423,7 @@ export default function Form({
: null
}
setData={setData}
// @ts-ignore
// @ts-expect-error Types diff between custom reset and useForm reset.
reset={reset}
/>
)}
@ -438,9 +431,9 @@ export default function Form({
{isConceptor && (
<FieldsetWrapper
className="gap-4"
title={useTrans("back.form_title_isLocked")}
title={transFormTitleIsLocked}
icon={<ShieldAlertIcon className="mr-1" />}
description={useTrans("back.form_subtitle_isLocked")}
description={transFormSubtitleIsLocked}
inline
>
<SimpleSwitch

View file

@ -27,51 +27,27 @@ import DataTableFilterButton from "@/back/Components/Table/DataTableFilterButton
import SoftdeleteWarning from "@/back/Components/SoftdeleteWarning";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import DynamicStatusBadge from "@/back/Components/DynamicPages/DynamicStatusBadge";
import { useFormatedDate } from "@/hooks/use-formated-value";
import { Views } from "@/lib/utils";
import {
AppEnv,
AuthProp,
DynamicPageModel,
PageConfigModel,
Resource,
TranslationField,
} from "@/back/types/global";
import RenderTranslationCell from "@/back/Components/Table/RenderTranslationCell";
import { GetFormatedDate, GetTranslation, Views } from "@/lib/utils";
import { AppEnv, AuthProp, Resource } from "@/back/types/global";
import { DynamicPageModel, PageConfigModel } from "@/back/types/dynamic-pages";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
import { renderTranslationCell } from "@/lib/translationCellFallback";
export default function Index({
dynamicPageModels,
searchFields,
modelName,
}: {
dynamicPageModels: Resource;
searchFields: string;
modelName: string;
}) {
const { auth, appEnv } = usePage().props as unknown as {
auth: AuthProp;
appEnv: AppEnv;
};
const { viewAny, viewTrash, create } = auth.policies.dynamicPages;
const { viewAny: viewAnyPageConfigs } = auth.policies.pageConfigs;
const isArchiveRoute = route().current(`back.${modelName}.trash`);
const actionModels = isArchiveRoute
? [Views.RESTORATION, Views.FORCEDELETION]
: [
Views.VISUALIZATION,
Views.EDITION,
Views.DUPLICATION,
Views.ARCHIVATION,
];
const columns = [
const getColumns = (
appEnv: AppEnv,
modelName: string,
viewAny: boolean,
viewAnyPageConfigs: boolean,
) => {
return [
{
accessorKey: "title",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(useTrans(`validation.attributes.${column.id}`))
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
@ -79,16 +55,14 @@ export default function Index({
),
cell: ({ row }: { row: Row<DynamicPageModel> }) => (
<div className="flex justify-center items-center">
{renderTranslationCell(
row.getValue("title") as TranslationField,
)}
<RenderTranslationCell value={row.getValue("title")} />
{row.original.isRoot && (
<Tooltip>
<TooltipTrigger asChild>
<HouseIcon size={15} className="ms-1" />
</TooltipTrigger>
<TooltipContent>
{useTrans("back.page_root")}
{GetTranslation("back.page_root")}
</TooltipContent>
</Tooltip>
)}
@ -99,7 +73,7 @@ export default function Index({
accessorKey: "parent_page",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(useTrans(`validation.custom.${column.id}`))
label={str(GetTranslation(`validation.custom.${column.id}`))
.ucFirst()
.value()}
field={column.id}
@ -127,9 +101,9 @@ export default function Index({
}
tooltipContent={
<p>
{useTrans("back.access_model", {
{GetTranslation("back.access_model", {
params: {
model: useTrans(
model: GetTranslation(
"models.classes.dynamic-page",
{ choice: 1 },
),
@ -155,7 +129,7 @@ export default function Index({
/>
</TooltipTrigger>
<TooltipContent>
{useTrans("back.group_archived")}
{GetTranslation("back.group_archived")}
</TooltipContent>
</Tooltip>
</div>
@ -165,7 +139,7 @@ export default function Index({
}
return (
<p className="text-muted-foreground italic m-0">
{useTrans("back.table_not_known")}
{GetTranslation("back.table_not_known")}
</p>
);
},
@ -174,7 +148,7 @@ export default function Index({
accessorKey: "config",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(useTrans(`validation.custom.${column.id}`))
label={str(GetTranslation(`validation.custom.${column.id}`))
.ucFirst()
.value()}
field={column.id}
@ -202,9 +176,9 @@ export default function Index({
}
tooltipContent={
<p>
{useTrans("back.access_model", {
{GetTranslation("back.access_model", {
params: {
model: useTrans(
model: GetTranslation(
"models.classes.page-config",
{ choice: 1 },
),
@ -230,7 +204,7 @@ export default function Index({
/>
</TooltipTrigger>
<TooltipContent>
{useTrans("back.config_archived")}
{GetTranslation("back.config_archived")}
</TooltipContent>
</Tooltip>
</div>
@ -240,7 +214,7 @@ export default function Index({
}
return (
<p className="text-muted-foreground italic m-0">
{useTrans("back.table_not_known")}
{GetTranslation("back.table_not_known")}
</p>
);
},
@ -249,7 +223,7 @@ export default function Index({
accessorKey: "status",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(useTrans(`validation.custom.${column.id}`))
label={str(GetTranslation(`validation.custom.${column.id}`))
.ucFirst()
.value()}
field={column.id}
@ -264,7 +238,7 @@ export default function Index({
<LockIcon size={15} className="ms-1" />
</TooltipTrigger>
<TooltipContent>
{useTrans("back.page_locked")}
{GetTranslation("back.page_locked")}
</TooltipContent>
</Tooltip>
)}
@ -275,7 +249,9 @@ export default function Index({
accessorKey: "updated_at",
header: ({ column }: { column: { id: string } }) => (
<DataTableFilterButton
label={str(useTrans(`validation.attributes.${column.id}`))
label={str(
GetTranslation(`validation.attributes.${column.id}`),
)
.ucFirst()
.value()}
field={column.id}
@ -283,11 +259,39 @@ export default function Index({
),
cell: ({ row }: { row: Row<DynamicPageModel> }) => (
<Badge variant="secondary">
{useFormatedDate(row.getValue("updated_at"))}
{GetFormatedDate(row.getValue("updated_at"))}
</Badge>
),
},
];
};
export default function Index({
dynamicPageModels,
searchFields,
modelName,
}: {
dynamicPageModels: Resource;
searchFields: string;
modelName: string;
}) {
const { auth, appEnv } = usePage().props as unknown as {
auth: AuthProp;
appEnv: AppEnv;
};
const { viewAny, viewTrash, create } = auth.policies.dynamicPages;
const { viewAny: viewAnyPageConfigs } = auth.policies.pageConfigs;
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(appEnv, modelName, viewAny, viewAnyPageConfigs);
const transModel = useTrans(
`models.classes.${str(modelName).singular().value()}`,
@ -299,7 +303,7 @@ export default function Index({
const transTitle = str(isArchiveRoute ? transArchiveTitle : transModel)
.ucFirst()
.value();
const transBackToList = useTrans("back.crud_back_to_list");
const beforeNode = () => {
return (
isArchiveRoute &&
@ -308,14 +312,18 @@ export default function Index({
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.index`)}
content={<ArrowLeftIcon />}
tooltipContent={
<p>{useTrans("back.crud_back_to_list")}</p>
}
tooltipContent={<p>{transBackToList}</p>}
/>
</div>
)
);
};
const transAccessToArchived = useTrans("back.crud_access_to", {
params: {
target: useTrans("back.archived_entries"),
},
});
const transAddEntry = useTrans("back.crud_add_entry");
const afterNode = () => {
return (
!isArchiveRoute && (
@ -324,17 +332,7 @@ export default function Index({
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.trash`)}
content={<ArchiveIcon />}
tooltipContent={
<p>
{useTrans("back.crud_access_to", {
params: {
target: useTrans(
"back.archived_entries",
),
},
})}
</p>
}
tooltipContent={<p>{transAccessToArchived}</p>}
variant="outline"
/>
)}
@ -342,9 +340,7 @@ export default function Index({
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.create`)}
content={<PlusIcon />}
tooltipContent={
<p>{useTrans("back.crud_add_entry")}</p>
}
tooltipContent={<p>{transAddEntry}</p>}
/>
)}
</div>

View file

@ -16,50 +16,40 @@ import DetailsTable from "@/back/Components/Table/DetailsTable";
import ModelActions from "@/back/Components/ModelActions";
import DynamicStatusBadge from "@/back/Components/DynamicPages/DynamicStatusBadge";
import RecursiveContent from "@/back/Components/DynamicPages/DynamicPageDetails";
import { Views } from "@/lib/utils";
import { useFormatedDate } from "@/hooks/use-formated-value";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import RenderTranslationCell from "@/back/Components/Table/RenderTranslationCell";
import { GetFormatedDate, GetTranslation, Views } from "@/lib/utils";
import { AppEnv, AuthProp, SingleResource } from "@/back/types/global";
import {
AppEnv,
AuthProp,
DynamicPageModel,
DynamicPageModelWithRelations,
PageConfigModel,
SingleResource,
TranslationField,
} from "@/back/types/global";
} from "@/back/types/dynamic-pages";
import { TranslationField } from "@/back/types/i18n";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import { renderTranslationCell } from "@/lib/translationCellFallback";
export default function Show({
data,
modelName,
}: {
data: SingleResource;
modelName: string;
}) {
const { auth, appEnv } = usePage().props as unknown as {
auth: AuthProp;
appEnv: AppEnv;
};
const { viewAny } = auth.policies.dynamicPages;
const dynamicPageModel = data.data as DynamicPageModel;
const actionModels = [Views.EDITION, Views.DUPLICATION, Views.ARCHIVATION];
const columns = [
const getColumns = (
appEnv: AppEnv,
auth: AuthProp,
modelName: string,
dynamicPageModel: DynamicPageModelWithRelations,
) => {
return [
{
accessorKey: "title",
header: str(useTrans("validation.attributes.title"))
header: str(GetTranslation("validation.attributes.title"))
.ucFirst()
.value(),
cell: ({ value }: { value: TranslationField }) =>
renderTranslationCell(value),
cell: ({ value }: { value: TranslationField }) => (
<RenderTranslationCell value={value} />
),
},
{
accessorKey: "parent_page",
header: str(useTrans("validation.custom.parent_page"))
header: str(GetTranslation("validation.custom.parent_page"))
.ucFirst()
.value(),
cell: ({ value }: { value: DynamicPageModel }) =>
cell: ({ value }: { value: DynamicPageModelWithRelations }) =>
auth.policies.dynamicPages.viewAny ? (
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.show`, value)}
@ -71,9 +61,9 @@ export default function Show({
}
tooltipContent={
<p>
{useTrans("back.access_model", {
{GetTranslation("back.access_model", {
params: {
model: useTrans(
model: GetTranslation(
`models.classes.${str(modelName).singular().value()}`,
{ choice: 1 },
),
@ -91,7 +81,9 @@ export default function Show({
},
{
accessorKey: "config",
header: str(useTrans("validation.custom.config")).ucFirst().value(),
header: str(GetTranslation("validation.custom.config"))
.ucFirst()
.value(),
cell: ({ value }: { value: PageConfigModel }) =>
auth.policies.pageConfigs.viewAny ? (
<BtnOptionnalLinkWithTooltip
@ -104,9 +96,9 @@ export default function Show({
}
tooltipContent={
<p>
{useTrans("back.access_model", {
{GetTranslation("back.access_model", {
params: {
model: useTrans(
model: GetTranslation(
"models.classes.page-config",
{ choice: 1 },
),
@ -124,7 +116,7 @@ export default function Show({
},
{
accessorKey: "content",
header: str(useTrans("validation.attributes.content"))
header: str(GetTranslation("validation.attributes.content"))
.ucFirst()
.value(),
cell: ({ value }: { value: TranslationField<object | string> }) => {
@ -149,21 +141,25 @@ export default function Show({
}
return (
<span className="text-sm italic text-muted-foreground">
{tableNotKnownTrans}
{GetTranslation("back.table_not_known")}
</span>
);
},
},
{
accessorKey: "status",
header: str(useTrans(`validation.custom.status`)).ucFirst().value(),
header: str(GetTranslation(`validation.custom.status`))
.ucFirst()
.value(),
cell: ({ value }: { value: number }) => (
<DynamicStatusBadge value={value} />
),
},
{
accessorKey: "isLocked",
header: str(useTrans("validation.custom.locked")).ucFirst().value(),
header: str(GetTranslation("validation.custom.locked"))
.ucFirst()
.value(),
cell: ({ value }: { value: boolean }) => (
<Badge
variant="outline"
@ -174,7 +170,7 @@ export default function Show({
) : (
<XIcon size={15} className="mr-1" />
)}
{str(useTrans(value ? "back.yes" : "back.no"))
{str(GetTranslation(value ? "back.yes" : "back.no"))
.ucFirst()
.value()}
</Badge>
@ -182,7 +178,9 @@ export default function Show({
},
{
accessorKey: "isRoot",
header: str(useTrans("validation.custom.root")).ucFirst().value(),
header: str(GetTranslation("validation.custom.root"))
.ucFirst()
.value(),
cell: ({ value }: { value: boolean }) => (
<Badge
variant="outline"
@ -193,7 +191,7 @@ export default function Show({
) : (
<XIcon size={15} className="mr-1" />
)}
{str(useTrans(value ? "back.yes" : "back.no"))
{str(GetTranslation(value ? "back.yes" : "back.no"))
.ucFirst()
.value()}
</Badge>
@ -201,32 +199,49 @@ export default function Show({
},
{
accessorKey: "created_at",
header: str(useTrans("validation.attributes.created_at"))
header: str(GetTranslation("validation.attributes.created_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{useFormatedDate(value)}</Badge>
<Badge variant="secondary">{GetFormatedDate(value)}</Badge>
),
},
{
accessorKey: "updated_at",
header: str(useTrans("validation.attributes.updated_at"))
header: str(GetTranslation("validation.attributes.updated_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{useFormatedDate(value)}</Badge>
<Badge variant="secondary">{GetFormatedDate(value)}</Badge>
),
},
{
accessorKey: "deleted_at",
header: str(useTrans("validation.attributes.deleted_at"))
header: str(GetTranslation("validation.attributes.deleted_at"))
.ucFirst()
.value(),
cell: ({ value }: { value: string }) => (
<Badge variant="secondary">{useFormatedDate(value)}</Badge>
<Badge variant="secondary">{GetFormatedDate(value)}</Badge>
),
},
];
};
export default function Show({
data,
modelName,
}: {
data: SingleResource;
modelName: string;
}) {
const { auth, appEnv } = usePage().props as unknown as {
auth: AuthProp;
appEnv: AppEnv;
};
const { viewAny } = auth.policies.dynamicPages;
const dynamicPageModel = data.data as DynamicPageModelWithRelations;
const actionModels = [Views.EDITION, Views.DUPLICATION, Views.ARCHIVATION];
const columns = getColumns(appEnv, auth, modelName, dynamicPageModel);
const singularModelName = str(modelName).singular().value();
const transTitle = useTrans("crud.helpers.visualization_model", {
params: {
@ -235,8 +250,8 @@ export default function Show({
}),
},
});
const tableNotKnownTrans = useTrans("back.table_not_known");
const transBackToList = useTrans("back.crud_back_to_list");
const beforeNode = () => {
return (
viewAny && (
@ -244,9 +259,7 @@ export default function Show({
<BtnOptionnalLinkWithTooltip
link={route(`back.${modelName}.index`)}
content={<ArrowLeftIcon />}
tooltipContent={
<p>{useTrans("back.crud_back_to_list")}</p>
}
tooltipContent={<p>{transBackToList}</p>}
/>
</div>
)

View file

@ -8,7 +8,8 @@ import Form from "@/back/Pages/News/Form";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import useTrans from "@/hooks/use-translations";
import { Actions, Methods } from "@/lib/enums";
import { AuthProp, NewsModel, SingleResource } from "@/back/types/global";
import { AuthProp, SingleResource } from "@/back/types/global";
import { NewsModel } from "@/back/types/others";
export default function Create({
data,

View file

@ -9,7 +9,8 @@ import Form from "@/back/Pages/News/Form";
import useTrans from "@/hooks/use-translations";
import { Actions, Methods } from "@/lib/enums";
import { Views } from "@/lib/utils";
import { AuthProp, NewsModel, SingleResource } from "@/back/types/global";
import { AuthProp, SingleResource } from "@/back/types/global";
import { NewsModel } from "@/back/types/others";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
export default function Edit({

Some files were not shown because too many files have changed in this diff Show more