fix: 🚨 resolve last eslint errors
This commit is contained in:
parent
3a4a96cdce
commit
3a893cd03c
137 changed files with 5239 additions and 4734 deletions
|
|
@ -66,5 +66,10 @@ module.exports = {
|
|||
},
|
||||
],
|
||||
'prettier/prettier': 'error',
|
||||
"no-console": [
|
||||
"warn", {
|
||||
allow: ["warn", "error"],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
29
package-lock.json
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
101
resources/sass/back/inputs/_color.scss
Normal file
101
resources/sass/back/inputs/_color.scss
Normal 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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -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()}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 ?? ""}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// * Packages
|
||||
import "@tiptap/core";
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// * Packages
|
||||
import "@tiptap/core";
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 ${
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
// * Packages
|
||||
import useTrans from "@/hooks/use-translations";
|
||||
|
||||
export default function WordCounter({ value }: { value: string }) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()}`,
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue