Introduce prettier
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
'use client';
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from 'react';
|
||||||
import { Button } from "@nextui-org/button";
|
import { Button } from '@nextui-org/button';
|
||||||
|
|
||||||
export const Counter = () => {
|
export const Counter = () => {
|
||||||
const [count, setCount] = useState(0);
|
const [count, setCount] = useState(0);
|
||||||
|
|||||||
@@ -1,19 +1,8 @@
|
|||||||
import * as React from "react";
|
import * as React from 'react';
|
||||||
import { IconSvgProps } from "@/types";
|
import { IconSvgProps } from '@/types';
|
||||||
|
|
||||||
export const Logo: React.FC<IconSvgProps> = ({
|
export const Logo: React.FC<IconSvgProps> = ({ size = 36, width, height, ...props }) => (
|
||||||
size = 36,
|
<svg fill="none" height={size || height} viewBox="0 0 32 32" width={size || width} {...props}>
|
||||||
width,
|
|
||||||
height,
|
|
||||||
...props
|
|
||||||
}) => (
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
height={size || height}
|
|
||||||
viewBox="0 0 32 32"
|
|
||||||
width={size || width}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
clipRule="evenodd"
|
clipRule="evenodd"
|
||||||
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
|
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
|
||||||
@@ -23,19 +12,9 @@ export const Logo: React.FC<IconSvgProps> = ({
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const DiscordIcon: React.FC<IconSvgProps> = ({
|
export const DiscordIcon: React.FC<IconSvgProps> = ({ size = 24, width, height, ...props }) => {
|
||||||
size = 24,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
...props
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg height={size || height} viewBox="0 0 24 24" width={size || width} {...props}>
|
||||||
height={size || height}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width={size || width}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
d="M14.82 4.26a10.14 10.14 0 0 0-.53 1.1 14.66 14.66 0 0 0-4.58 0 10.14 10.14 0 0 0-.53-1.1 16 16 0 0 0-4.13 1.3 17.33 17.33 0 0 0-3 11.59 16.6 16.6 0 0 0 5.07 2.59A12.89 12.89 0 0 0 8.23 18a9.65 9.65 0 0 1-1.71-.83 3.39 3.39 0 0 0 .42-.33 11.66 11.66 0 0 0 10.12 0q.21.18.42.33a10.84 10.84 0 0 1-1.71.84 12.41 12.41 0 0 0 1.08 1.78 16.44 16.44 0 0 0 5.06-2.59 17.22 17.22 0 0 0-3-11.59 16.09 16.09 0 0 0-4.09-1.35zM8.68 14.81a1.94 1.94 0 0 1-1.8-2 1.93 1.93 0 0 1 1.8-2 1.93 1.93 0 0 1 1.8 2 1.93 1.93 0 0 1-1.8 2zm6.64 0a1.94 1.94 0 0 1-1.8-2 1.93 1.93 0 0 1 1.8-2 1.92 1.92 0 0 1 1.8 2 1.92 1.92 0 0 1-1.8 2z"
|
d="M14.82 4.26a10.14 10.14 0 0 0-.53 1.1 14.66 14.66 0 0 0-4.58 0 10.14 10.14 0 0 0-.53-1.1 16 16 0 0 0-4.13 1.3 17.33 17.33 0 0 0-3 11.59 16.6 16.6 0 0 0 5.07 2.59A12.89 12.89 0 0 0 8.23 18a9.65 9.65 0 0 1-1.71-.83 3.39 3.39 0 0 0 .42-.33 11.66 11.66 0 0 0 10.12 0q.21.18.42.33a10.84 10.84 0 0 1-1.71.84 12.41 12.41 0 0 0 1.08 1.78 16.44 16.44 0 0 0 5.06-2.59 17.22 17.22 0 0 0-3-11.59 16.09 16.09 0 0 0-4.09-1.35zM8.68 14.81a1.94 1.94 0 0 1-1.8-2 1.93 1.93 0 0 1 1.8-2 1.93 1.93 0 0 1 1.8 2 1.93 1.93 0 0 1-1.8 2zm6.64 0a1.94 1.94 0 0 1-1.8-2 1.93 1.93 0 0 1 1.8-2 1.92 1.92 0 0 1 1.8 2 1.92 1.92 0 0 1-1.8 2z"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
@@ -44,19 +23,9 @@ export const DiscordIcon: React.FC<IconSvgProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TwitterIcon: React.FC<IconSvgProps> = ({
|
export const TwitterIcon: React.FC<IconSvgProps> = ({ size = 24, width, height, ...props }) => {
|
||||||
size = 24,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
...props
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg height={size || height} viewBox="0 0 24 24" width={size || width} {...props}>
|
||||||
height={size || height}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width={size || width}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
d="M19.633 7.997c.013.175.013.349.013.523 0 5.325-4.053 11.461-11.46 11.461-2.282 0-4.402-.661-6.186-1.809.324.037.636.05.973.05a8.07 8.07 0 0 0 5.001-1.721 4.036 4.036 0 0 1-3.767-2.793c.249.037.499.062.761.062.361 0 .724-.05 1.061-.137a4.027 4.027 0 0 1-3.23-3.953v-.05c.537.299 1.16.486 1.82.511a4.022 4.022 0 0 1-1.796-3.354c0-.748.199-1.434.548-2.032a11.457 11.457 0 0 0 8.306 4.215c-.062-.3-.1-.611-.1-.923a4.026 4.026 0 0 1 4.028-4.028c1.16 0 2.207.486 2.943 1.272a7.957 7.957 0 0 0 2.556-.973 4.02 4.02 0 0 1-1.771 2.22 8.073 8.073 0 0 0 2.319-.624 8.645 8.645 0 0 1-2.019 2.083z"
|
d="M19.633 7.997c.013.175.013.349.013.523 0 5.325-4.053 11.461-11.46 11.461-2.282 0-4.402-.661-6.186-1.809.324.037.636.05.973.05a8.07 8.07 0 0 0 5.001-1.721 4.036 4.036 0 0 1-3.767-2.793c.249.037.499.062.761.062.361 0 .724-.05 1.061-.137a4.027 4.027 0 0 1-3.23-3.953v-.05c.537.299 1.16.486 1.82.511a4.022 4.022 0 0 1-1.796-3.354c0-.748.199-1.434.548-2.032a11.457 11.457 0 0 0 8.306 4.215c-.062-.3-.1-.611-.1-.923a4.026 4.026 0 0 1 4.028-4.028c1.16 0 2.207.486 2.943 1.272a7.957 7.957 0 0 0 2.556-.973 4.02 4.02 0 0 1-1.771 2.22 8.073 8.073 0 0 0 2.319-.624 8.645 8.645 0 0 1-2.019 2.083z"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
@@ -65,19 +34,9 @@ export const TwitterIcon: React.FC<IconSvgProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GithubIcon: React.FC<IconSvgProps> = ({
|
export const GithubIcon: React.FC<IconSvgProps> = ({ size = 24, width, height, ...props }) => {
|
||||||
size = 24,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
...props
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg height={size || height} viewBox="0 0 24 24" width={size || width} {...props}>
|
||||||
height={size || height}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width={size || width}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
clipRule="evenodd"
|
clipRule="evenodd"
|
||||||
d="M12.026 2c-5.509 0-9.974 4.465-9.974 9.974 0 4.406 2.857 8.145 6.821 9.465.499.09.679-.217.679-.481 0-.237-.008-.865-.011-1.696-2.775.602-3.361-1.338-3.361-1.338-.452-1.152-1.107-1.459-1.107-1.459-.905-.619.069-.605.069-.605 1.002.07 1.527 1.028 1.527 1.028.89 1.524 2.336 1.084 2.902.829.091-.645.351-1.085.635-1.334-2.214-.251-4.542-1.107-4.542-4.93 0-1.087.389-1.979 1.024-2.675-.101-.253-.446-1.268.099-2.64 0 0 .837-.269 2.742 1.021a9.582 9.582 0 0 1 2.496-.336 9.554 9.554 0 0 1 2.496.336c1.906-1.291 2.742-1.021 2.742-1.021.545 1.372.203 2.387.099 2.64.64.696 1.024 1.587 1.024 2.675 0 3.833-2.33 4.675-4.552 4.922.355.308.675.916.675 1.846 0 1.334-.012 2.41-.012 2.737 0 .267.178.577.687.479C19.146 20.115 22 16.379 22 11.974 22 6.465 17.535 2 12.026 2z"
|
d="M12.026 2c-5.509 0-9.974 4.465-9.974 9.974 0 4.406 2.857 8.145 6.821 9.465.499.09.679-.217.679-.481 0-.237-.008-.865-.011-1.696-2.775.602-3.361-1.338-3.361-1.338-.452-1.152-1.107-1.459-1.107-1.459-.905-.619.069-.605.069-.605 1.002.07 1.527 1.028 1.527 1.028.89 1.524 2.336 1.084 2.902.829.091-.645.351-1.085.635-1.334-2.214-.251-4.542-1.107-4.542-4.93 0-1.087.389-1.979 1.024-2.675-.101-.253-.446-1.268.099-2.64 0 0 .837-.269 2.742 1.021a9.582 9.582 0 0 1 2.496-.336 9.554 9.554 0 0 1 2.496.336c1.906-1.291 2.742-1.021 2.742-1.021.545 1.372.203 2.387.099 2.64.64.696 1.024 1.587 1.024 2.675 0 3.833-2.33 4.675-4.552 4.922.355.308.675.916.675 1.846 0 1.334-.012 2.41-.012 2.737 0 .267.178.577.687.479C19.146 20.115 22 16.379 22 11.974 22 6.465 17.535 2 12.026 2z"
|
||||||
@@ -88,12 +47,7 @@ export const GithubIcon: React.FC<IconSvgProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MoonFilledIcon = ({
|
export const MoonFilledIcon = ({ size = 24, width, height, ...props }: IconSvgProps) => (
|
||||||
size = 24,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
...props
|
|
||||||
}: IconSvgProps) => (
|
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
@@ -110,12 +64,7 @@ export const MoonFilledIcon = ({
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const SunFilledIcon = ({
|
export const SunFilledIcon = ({ size = 24, width, height, ...props }: IconSvgProps) => (
|
||||||
size = 24,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
...props
|
|
||||||
}: IconSvgProps) => (
|
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
@@ -132,12 +81,7 @@ export const SunFilledIcon = ({
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const HeartFilledIcon = ({
|
export const HeartFilledIcon = ({ size = 24, width, height, ...props }: IconSvgProps) => (
|
||||||
size = 24,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
...props
|
|
||||||
}: IconSvgProps) => (
|
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
@@ -175,12 +119,6 @@ export const SearchIcon = (props: IconSvgProps) => (
|
|||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
/>
|
/>
|
||||||
<path
|
<path d="M22 22L20 20" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" />
|
||||||
d="M22 22L20 20"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="2"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,53 +1,37 @@
|
|||||||
"use client";
|
'use client';
|
||||||
|
|
||||||
import { FC } from "react";
|
import { FC } from 'react';
|
||||||
import { VisuallyHidden } from "@react-aria/visually-hidden";
|
import { VisuallyHidden } from '@react-aria/visually-hidden';
|
||||||
import { SwitchProps, useSwitch } from "@nextui-org/react";
|
import { SwitchProps, useSwitch } from '@nextui-org/react';
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from 'next-themes';
|
||||||
import { useIsSSR } from "@react-aria/ssr";
|
import { useIsSSR } from '@react-aria/ssr';
|
||||||
import clsx from "clsx";
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { SunFilledIcon, MoonFilledIcon } from "@/components/icons";
|
import { SunFilledIcon, MoonFilledIcon } from '@/components/icons';
|
||||||
|
|
||||||
export interface ThemeSwitchProps {
|
export interface ThemeSwitchProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
classNames?: SwitchProps["classNames"];
|
classNames?: SwitchProps['classNames'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ThemeSwitch: FC<ThemeSwitchProps> = ({
|
export const ThemeSwitch: FC<ThemeSwitchProps> = ({ className, classNames }) => {
|
||||||
className,
|
|
||||||
classNames,
|
|
||||||
}) => {
|
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
const isSSR = useIsSSR();
|
const isSSR = useIsSSR();
|
||||||
|
|
||||||
const onChange = () => {
|
const onChange = () => {
|
||||||
theme === "light" ? setTheme("dark") : setTheme("light");
|
theme === 'light' ? setTheme('dark') : setTheme('light');
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const { Component, slots, isSelected, getBaseProps, getInputProps, getWrapperProps } = useSwitch({
|
||||||
Component,
|
isSelected: theme === 'light' || isSSR,
|
||||||
slots,
|
'aria-label': `Switch to ${theme === 'light' || isSSR ? 'dark' : 'light'} mode`,
|
||||||
isSelected,
|
|
||||||
getBaseProps,
|
|
||||||
getInputProps,
|
|
||||||
getWrapperProps,
|
|
||||||
} = useSwitch({
|
|
||||||
isSelected: theme === "light" || isSSR,
|
|
||||||
"aria-label": `Switch to ${
|
|
||||||
theme === "light" || isSSR ? "dark" : "light"
|
|
||||||
} mode`,
|
|
||||||
onChange,
|
onChange,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component
|
<Component
|
||||||
{...getBaseProps({
|
{...getBaseProps({
|
||||||
className: clsx(
|
className: clsx('px-px transition-opacity hover:opacity-80 cursor-pointer', className, classNames?.base),
|
||||||
"px-px transition-opacity hover:opacity-80 cursor-pointer",
|
|
||||||
className,
|
|
||||||
classNames?.base
|
|
||||||
),
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<VisuallyHidden>
|
<VisuallyHidden>
|
||||||
@@ -58,25 +42,21 @@ export const ThemeSwitch: FC<ThemeSwitchProps> = ({
|
|||||||
className={slots.wrapper({
|
className={slots.wrapper({
|
||||||
class: clsx(
|
class: clsx(
|
||||||
[
|
[
|
||||||
"w-auto h-auto",
|
'w-auto h-auto',
|
||||||
"bg-transparent",
|
'bg-transparent',
|
||||||
"rounded-lg",
|
'rounded-lg',
|
||||||
"flex items-center justify-center",
|
'flex items-center justify-center',
|
||||||
"group-data-[selected=true]:bg-transparent",
|
'group-data-[selected=true]:bg-transparent',
|
||||||
"!text-default-500",
|
'!text-default-500',
|
||||||
"pt-px",
|
'pt-px',
|
||||||
"px-0",
|
'px-0',
|
||||||
"mx-0",
|
'mx-0',
|
||||||
],
|
],
|
||||||
classNames?.wrapper
|
classNames?.wrapper
|
||||||
),
|
),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{!isSelected || isSSR ? (
|
{!isSelected || isSSR ? <SunFilledIcon size={22} /> : <MoonFilledIcon size={22} />}
|
||||||
<SunFilledIcon size={22} />
|
|
||||||
) : (
|
|
||||||
<MoonFilledIcon size={22} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</Component>
|
</Component>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,22 +1,10 @@
|
|||||||
"use client";
|
'use client';
|
||||||
import {
|
import { Button, DropdownTrigger, Dropdown, DropdownMenu, DropdownItem } from '@nextui-org/react';
|
||||||
Button,
|
import { User, ChevronDown, PenTool, ArrowRightFromLine, ArrowRightToLine } from 'lucide-react';
|
||||||
DropdownTrigger,
|
import { useContext } from 'react';
|
||||||
Dropdown,
|
import { TokenContext } from './TokenProvider';
|
||||||
DropdownMenu,
|
import { useRouter } from '@/src/navigation';
|
||||||
DropdownItem,
|
import { AccountDropDownMessages } from '@/types/user';
|
||||||
} from "@nextui-org/react";
|
|
||||||
import {
|
|
||||||
User,
|
|
||||||
ChevronDown,
|
|
||||||
PenTool,
|
|
||||||
ArrowRightFromLine,
|
|
||||||
ArrowRightToLine,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useContext } from "react";
|
|
||||||
import { TokenContext } from "./TokenProvider";
|
|
||||||
import { useRouter } from "@/src/navigation";
|
|
||||||
import { AccountDropDownMessages } from "@/types/user";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
messages: AccountDropDownMessages;
|
messages: AccountDropDownMessages;
|
||||||
@@ -35,15 +23,15 @@ export default function DropdownAccount({ messages, locale }: Props) {
|
|||||||
|
|
||||||
const signinItems = [
|
const signinItems = [
|
||||||
{
|
{
|
||||||
uid: "account",
|
uid: 'account',
|
||||||
title: messages.account,
|
title: messages.account,
|
||||||
icon: <User size={16} />,
|
icon: <User size={16} />,
|
||||||
onPress: () => {
|
onPress: () => {
|
||||||
router.push("/account", { locale: locale });
|
router.push('/account', { locale: locale });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
uid: "signout",
|
uid: 'signout',
|
||||||
title: messages.signOut,
|
title: messages.signOut,
|
||||||
icon: <ArrowRightFromLine size={16} />,
|
icon: <ArrowRightFromLine size={16} />,
|
||||||
onPress: signOut,
|
onPress: signOut,
|
||||||
@@ -52,19 +40,19 @@ export default function DropdownAccount({ messages, locale }: Props) {
|
|||||||
|
|
||||||
const signoutItems = [
|
const signoutItems = [
|
||||||
{
|
{
|
||||||
uid: "signin",
|
uid: 'signin',
|
||||||
title: messages.signIn,
|
title: messages.signIn,
|
||||||
icon: <ArrowRightToLine size={16} />,
|
icon: <ArrowRightToLine size={16} />,
|
||||||
onPress: () => {
|
onPress: () => {
|
||||||
router.push("/account/signin", { locale: locale });
|
router.push('/account/signin', { locale: locale });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
uid: "signup",
|
uid: 'signup',
|
||||||
title: messages.signUp,
|
title: messages.signUp,
|
||||||
icon: <PenTool size={16} />,
|
icon: <PenTool size={16} />,
|
||||||
onPress: () => {
|
onPress: () => {
|
||||||
router.push("/account/signup", { locale: locale });
|
router.push('/account/signup', { locale: locale });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -72,37 +60,20 @@ export default function DropdownAccount({ messages, locale }: Props) {
|
|||||||
return (
|
return (
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
<Button
|
<Button size="sm" variant="light" startContent={<User size={16} />} endContent={<ChevronDown size={16} />}>
|
||||||
size="sm"
|
{context.token && context.token.user ? context.token.user.username : messages.signIn}
|
||||||
variant="light"
|
|
||||||
startContent={<User size={16} />}
|
|
||||||
endContent={<ChevronDown size={16} />}
|
|
||||||
>
|
|
||||||
{context.token && context.token.user
|
|
||||||
? context.token.user.username
|
|
||||||
: messages.signIn}
|
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownTrigger>
|
</DropdownTrigger>
|
||||||
{context.token && context.token.user ? (
|
{context.token && context.token.user ? (
|
||||||
<DropdownMenu aria-label="account actions when sign in">
|
<DropdownMenu aria-label="account actions when sign in">
|
||||||
{signinItems.map((entry) => (
|
{signinItems.map((entry) => (
|
||||||
<DropdownItem
|
<DropdownItem key={entry.uid} title={entry.title} startContent={entry.icon} onPress={entry.onPress} />
|
||||||
key={entry.uid}
|
|
||||||
title={entry.title}
|
|
||||||
startContent={entry.icon}
|
|
||||||
onPress={entry.onPress}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
) : (
|
) : (
|
||||||
<DropdownMenu aria-label="account actions when sign out">
|
<DropdownMenu aria-label="account actions when sign out">
|
||||||
{signoutItems.map((entry) => (
|
{signoutItems.map((entry) => (
|
||||||
<DropdownItem
|
<DropdownItem key={entry.uid} title={entry.title} startContent={entry.icon} onPress={entry.onPress} />
|
||||||
key={entry.uid}
|
|
||||||
title={entry.title}
|
|
||||||
startContent={entry.icon}
|
|
||||||
onPress={entry.onPress}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,18 +1,12 @@
|
|||||||
"use client";
|
'use client';
|
||||||
import {
|
import { Button, DropdownTrigger, Dropdown, DropdownMenu, DropdownItem } from '@nextui-org/react';
|
||||||
Button,
|
import { Globe, ChevronDown } from 'lucide-react';
|
||||||
DropdownTrigger,
|
import { usePathname } from 'next/navigation';
|
||||||
Dropdown,
|
import { useRouter } from '@/src/navigation';
|
||||||
DropdownMenu,
|
|
||||||
DropdownItem,
|
|
||||||
} from "@nextui-org/react";
|
|
||||||
import { Globe, ChevronDown } from "lucide-react";
|
|
||||||
import { usePathname } from "next/navigation";
|
|
||||||
import { useRouter } from "@/src/navigation";
|
|
||||||
|
|
||||||
const locales = [
|
const locales = [
|
||||||
{ code: "en", name: "English" },
|
{ code: 'en', name: 'English' },
|
||||||
{ code: "ja", name: "日本語" },
|
{ code: 'ja', name: '日本語' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function DropdownLanguage(params: { locale: string }) {
|
export default function DropdownLanguage(params: { locale: string }) {
|
||||||
@@ -23,7 +17,7 @@ export default function DropdownLanguage(params: { locale: string }) {
|
|||||||
let newPathname;
|
let newPathname;
|
||||||
if (pathname.length < 4) {
|
if (pathname.length < 4) {
|
||||||
// when root path
|
// when root path
|
||||||
router.push("/", { locale: nextLocale });
|
router.push('/', { locale: nextLocale });
|
||||||
} else {
|
} else {
|
||||||
// when not root path, trim first "/en" from pathname = "/en/projects"
|
// when not root path, trim first "/en" from pathname = "/en/projects"
|
||||||
newPathname = pathname.slice(params.locale.length + 1);
|
newPathname = pathname.slice(params.locale.length + 1);
|
||||||
@@ -34,22 +28,13 @@ export default function DropdownLanguage(params: { locale: string }) {
|
|||||||
return (
|
return (
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
<Button
|
<Button size="sm" variant="light" startContent={<Globe size={16} />} endContent={<ChevronDown size={16} />}>
|
||||||
size="sm"
|
{locales.find((locale) => locale.code === params.locale)?.name || params.locale}
|
||||||
variant="light"
|
|
||||||
startContent={<Globe size={16} />}
|
|
||||||
endContent={<ChevronDown size={16} />}
|
|
||||||
>
|
|
||||||
{locales.find((locale) => locale.code === params.locale)?.name ||
|
|
||||||
params.locale}
|
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownTrigger>
|
</DropdownTrigger>
|
||||||
<DropdownMenu aria-label="lacales">
|
<DropdownMenu aria-label="lacales">
|
||||||
{locales.map((entry) => (
|
{locales.map((entry) => (
|
||||||
<DropdownItem
|
<DropdownItem key={entry.code} onClick={() => changeLocale(entry.code)}>
|
||||||
key={entry.code}
|
|
||||||
onClick={() => changeLocale(entry.code)}
|
|
||||||
>
|
|
||||||
{entry.name}
|
{entry.name}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -8,37 +8,37 @@ import {
|
|||||||
NavbarMenuItem,
|
NavbarMenuItem,
|
||||||
Chip,
|
Chip,
|
||||||
Link as NextUiLink,
|
Link as NextUiLink,
|
||||||
} from "@nextui-org/react";
|
} from '@nextui-org/react';
|
||||||
import { MoveUpRight } from "lucide-react";
|
import { MoveUpRight } from 'lucide-react';
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from 'next-intl';
|
||||||
import { Link } from "@/src/navigation";
|
import { Link } from '@/src/navigation';
|
||||||
import { ThemeSwitch } from "@/components/theme-switch";
|
import { ThemeSwitch } from '@/components/theme-switch';
|
||||||
import { GithubIcon } from "@/components/icons";
|
import { GithubIcon } from '@/components/icons';
|
||||||
import Image from "next/image";
|
import Image from 'next/image';
|
||||||
import DropdownAccount from "./DropdownAccount";
|
import DropdownAccount from './DropdownAccount';
|
||||||
import DropdownLanguage from "./DropdownLanguage";
|
import DropdownLanguage from './DropdownLanguage';
|
||||||
|
|
||||||
export default function Header(params: { locale: string }) {
|
export default function Header(params: { locale: string }) {
|
||||||
const t = useTranslations("Header");
|
const t = useTranslations('Header');
|
||||||
const messages = {
|
const messages = {
|
||||||
account: t("account"),
|
account: t('account'),
|
||||||
signUp: t("signup"),
|
signUp: t('signup'),
|
||||||
signIn: t("signin"),
|
signIn: t('signin'),
|
||||||
signOut: t("signout"),
|
signOut: t('signout'),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Links shown Header or slider
|
// Links shown Header or slider
|
||||||
const commonLinks = [
|
const commonLinks = [
|
||||||
{
|
{
|
||||||
uid: "projects",
|
uid: 'projects',
|
||||||
href: "/projects",
|
href: '/projects',
|
||||||
label: t("projects"),
|
label: t('projects'),
|
||||||
isExternal: false,
|
isExternal: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
uid: "docs",
|
uid: 'docs',
|
||||||
href: "https://kimatata.github.io/TestPlat/docs/getstarted/selfhost",
|
href: 'https://kimatata.github.io/TestPlat/docs/getstarted/selfhost',
|
||||||
label: t("docs"),
|
label: t('docs'),
|
||||||
isExternal: true,
|
isExternal: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -47,17 +47,8 @@ export default function Header(params: { locale: string }) {
|
|||||||
<NextUINavbar maxWidth="full" position="sticky" className="bg-inherit">
|
<NextUINavbar maxWidth="full" position="sticky" className="bg-inherit">
|
||||||
<NavbarContent className="basis-1/5 sm:basis-full" justify="start">
|
<NavbarContent className="basis-1/5 sm:basis-full" justify="start">
|
||||||
<NavbarBrand as="li" className="gap-3 max-w-fit">
|
<NavbarBrand as="li" className="gap-3 max-w-fit">
|
||||||
<Link
|
<Link className="flex justify-start items-center gap-1" href="/" locale={params.locale}>
|
||||||
className="flex justify-start items-center gap-1"
|
<Image src="/favicon/android-chrome-192x192.png" width={32} height={32} alt="Logo" />
|
||||||
href="/"
|
|
||||||
locale={params.locale}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src="/favicon/android-chrome-192x192.png"
|
|
||||||
width={32}
|
|
||||||
height={32}
|
|
||||||
alt="Logo"
|
|
||||||
/>
|
|
||||||
<p className="font-bold text-inherit">TestPlat</p>
|
<p className="font-bold text-inherit">TestPlat</p>
|
||||||
</Link>
|
</Link>
|
||||||
</NavbarBrand>
|
</NavbarBrand>
|
||||||
@@ -82,7 +73,7 @@ export default function Header(params: { locale: string }) {
|
|||||||
showAnchorIcon
|
showAnchorIcon
|
||||||
anchorIcon={<MoveUpRight size={12} className="ms-1" />}
|
anchorIcon={<MoveUpRight size={12} className="ms-1" />}
|
||||||
>
|
>
|
||||||
{t("docs")}
|
{t('docs')}
|
||||||
</NextUiLink>
|
</NextUiLink>
|
||||||
</NavbarItem>
|
</NavbarItem>
|
||||||
) : (
|
) : (
|
||||||
@@ -100,11 +91,7 @@ export default function Header(params: { locale: string }) {
|
|||||||
</NavbarContent>
|
</NavbarContent>
|
||||||
|
|
||||||
<NavbarContent className="basis-1 pl-4" justify="end">
|
<NavbarContent className="basis-1 pl-4" justify="end">
|
||||||
<NextUiLink
|
<NextUiLink isExternal href="https://github.com/kimatata/TestPlat" aria-label="Github">
|
||||||
isExternal
|
|
||||||
href="https://github.com/kimatata/TestPlat"
|
|
||||||
aria-label="Github"
|
|
||||||
>
|
|
||||||
<GithubIcon className="text-default-500" />
|
<GithubIcon className="text-default-500" />
|
||||||
</NextUiLink>
|
</NextUiLink>
|
||||||
<ThemeSwitch />
|
<ThemeSwitch />
|
||||||
@@ -126,12 +113,8 @@ export default function Header(params: { locale: string }) {
|
|||||||
{commonLinks.map((link) =>
|
{commonLinks.map((link) =>
|
||||||
link.isExternal ? (
|
link.isExternal ? (
|
||||||
<NavbarMenuItem key={link.uid}>
|
<NavbarMenuItem key={link.uid}>
|
||||||
<NextUiLink
|
<NextUiLink href={link.href} showAnchorIcon anchorIcon={<MoveUpRight size={12} className="ms-1" />}>
|
||||||
href={link.href}
|
{t('docs')}
|
||||||
showAnchorIcon
|
|
||||||
anchorIcon={<MoveUpRight size={12} className="ms-1" />}
|
|
||||||
>
|
|
||||||
{t("docs")}
|
|
||||||
</NextUiLink>
|
</NextUiLink>
|
||||||
</NavbarMenuItem>
|
</NavbarMenuItem>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
'use client';
|
||||||
import { title } from "@/components/primitives";
|
import { title } from '@/components/primitives';
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from 'react';
|
||||||
import { Tabs, Tab, Image } from "@nextui-org/react";
|
import { Tabs, Tab, Image } from '@nextui-org/react';
|
||||||
|
|
||||||
type PaneDemoImagesMessages = {
|
type PaneDemoImagesMessages = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -14,28 +14,28 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function DemoImages({ messages }: Props) {
|
export default function DemoImages({ messages }: Props) {
|
||||||
const [currentTab, setcurrentTab] = useState("edit");
|
const [currentTab, setcurrentTab] = useState('edit');
|
||||||
const [currentImage, setcurrentImage] = useState({
|
const [currentImage, setcurrentImage] = useState({
|
||||||
src: "/top/caseEdit.png",
|
src: '/top/caseEdit.png',
|
||||||
alt: messages.caseEdit,
|
alt: messages.caseEdit,
|
||||||
});
|
});
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
key: "edit",
|
key: 'edit',
|
||||||
title: messages.caseEdit,
|
title: messages.caseEdit,
|
||||||
src: "/top/caseEdit.png",
|
src: '/top/caseEdit.png',
|
||||||
alt: messages.caseEdit,
|
alt: messages.caseEdit,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "home",
|
key: 'home',
|
||||||
title: messages.caseHome,
|
title: messages.caseHome,
|
||||||
src: "/top/caseHome.png",
|
src: '/top/caseHome.png',
|
||||||
alt: messages.caseHome,
|
alt: messages.caseHome,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "run",
|
key: 'run',
|
||||||
title: messages.caseRun,
|
title: messages.caseRun,
|
||||||
src: "/top/caseRun.png",
|
src: '/top/caseRun.png',
|
||||||
alt: messages.caseRun,
|
alt: messages.caseRun,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -50,11 +50,11 @@ export default function DemoImages({ messages }: Props) {
|
|||||||
<>
|
<>
|
||||||
<div className="flex flex-wrap lg:text-left text-center">
|
<div className="flex flex-wrap lg:text-left text-center">
|
||||||
<div className="w-full lg:w-5/12 p-4">
|
<div className="w-full lg:w-5/12 p-4">
|
||||||
<h2 className={title({ size: "sm" })}>{messages.title}</h2>
|
<h2 className={title({ size: 'sm' })}>{messages.title}</h2>
|
||||||
<br />
|
<br />
|
||||||
<Tabs
|
<Tabs
|
||||||
aria-label="Options"
|
aria-label="Options"
|
||||||
color={"primary"}
|
color={'primary'}
|
||||||
radius="full"
|
radius="full"
|
||||||
size="md"
|
size="md"
|
||||||
className="mt-8"
|
className="mt-8"
|
||||||
@@ -68,13 +68,7 @@ export default function DemoImages({ messages }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-center w-full lg:w-7/12 p-4">
|
<div className="flex justify-center w-full lg:w-7/12 p-4">
|
||||||
<Image
|
<Image src={currentImage.src} alt={currentImage.alt} shadow="md" height={500} className="max-w-full" />
|
||||||
src={currentImage.src}
|
|
||||||
alt={currentImage.alt}
|
|
||||||
shadow="md"
|
|
||||||
height={500}
|
|
||||||
className="max-w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
import { title, subtitle } from "@/components/primitives";
|
import { title, subtitle } from '@/components/primitives';
|
||||||
import { Card, CardHeader, CardBody, Avatar } from "@nextui-org/react";
|
import { Card, CardHeader, CardBody, Avatar } from '@nextui-org/react';
|
||||||
import { Scale, Folder, Check, Globe } from "lucide-react";
|
import { Scale, Folder, Check, Globe } from 'lucide-react';
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
export default function MainTitle() {
|
export default function MainTitle() {
|
||||||
const t = useTranslations("Index");
|
const t = useTranslations('Index');
|
||||||
|
|
||||||
const features = [
|
const features = [
|
||||||
{
|
{
|
||||||
title: t("oss_title"),
|
title: t('oss_title'),
|
||||||
detail: t("oss_detail"),
|
detail: t('oss_detail'),
|
||||||
icon: <Scale size={24} color="#52e280" />,
|
icon: <Scale size={24} color="#52e280" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("organize_title"),
|
title: t('organize_title'),
|
||||||
detail: t("organize_detail"),
|
detail: t('organize_detail'),
|
||||||
icon: <Folder size={24} color="#52e280" />,
|
icon: <Folder size={24} color="#52e280" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("usability_title"),
|
title: t('usability_title'),
|
||||||
detail: t("usability_detail"),
|
detail: t('usability_detail'),
|
||||||
icon: <Check size={24} color="#52e280" />,
|
icon: <Check size={24} color="#52e280" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("universal_title"),
|
title: t('universal_title'),
|
||||||
detail: t("universal_detail"),
|
detail: t('universal_detail'),
|
||||||
icon: <Globe size={24} color="#52e280" />,
|
icon: <Globe size={24} color="#52e280" />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -36,12 +36,7 @@ export default function MainTitle() {
|
|||||||
<Card key={feature.title} className="max-w-[300px] min-h-[180px]">
|
<Card key={feature.title} className="max-w-[300px] min-h-[180px]">
|
||||||
<CardHeader className="flex gap-3">
|
<CardHeader className="flex gap-3">
|
||||||
<div>
|
<div>
|
||||||
<Avatar
|
<Avatar className="bg-green-100" showFallback src="" fallback={feature.icon} />
|
||||||
className="bg-green-100"
|
|
||||||
showFallback
|
|
||||||
src=""
|
|
||||||
fallback={feature.icon}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<h4 className="font-bold text-large">{feature.title}</h4>
|
<h4 className="font-bold text-large">{feature.title}</h4>
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import { title, subtitle } from "@/components/primitives";
|
import { title, subtitle } from '@/components/primitives';
|
||||||
import { Button, Link as NextUiLink } from "@nextui-org/react";
|
import { Button, Link as NextUiLink } from '@nextui-org/react';
|
||||||
import { MoveUpRight } from "lucide-react";
|
import { MoveUpRight } from 'lucide-react';
|
||||||
import { Link } from "@/src/navigation";
|
import { Link } from '@/src/navigation';
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
locale: string;
|
locale: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function MainTitle({ locale }: Props) {
|
export default function MainTitle({ locale }: Props) {
|
||||||
const t = useTranslations("Index");
|
const t = useTranslations('Index');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="md:text-left text-center">
|
<div className="md:text-left text-center">
|
||||||
<h1
|
<h1
|
||||||
className={title({
|
className={title({
|
||||||
color: "green",
|
color: 'green',
|
||||||
class: "lg:text-7xl md:text-7xl sm:text-7xl text-7xl",
|
class: 'lg:text-7xl md:text-7xl sm:text-7xl text-7xl',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
TestPlat
|
TestPlat
|
||||||
@@ -25,29 +25,23 @@ export default function MainTitle({ locale }: Props) {
|
|||||||
<br />
|
<br />
|
||||||
<h1
|
<h1
|
||||||
className={title({
|
className={title({
|
||||||
class: "lg:text-5xl md:text-5xl sm:text-5xl text-5xl",
|
class: 'lg:text-5xl md:text-5xl sm:text-5xl text-5xl',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{t("oss_tcmt")}
|
{t('oss_tcmt')}
|
||||||
<br />
|
<br />
|
||||||
{t("web_application")}
|
{t('web_application')}
|
||||||
</h1>
|
</h1>
|
||||||
<h4 className={subtitle({ class: "mt-4" })}>
|
<h4 className={subtitle({ class: 'mt-4' })}>{t('integrate_and_manage')}</h4>
|
||||||
{t("integrate_and_manage")}
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
<Link href={`/projects/`} locale={locale}>
|
<Link href={`/projects/`} locale={locale}>
|
||||||
<Button color="primary" radius="full" className="px-0">
|
<Button color="primary" radius="full" className="px-0">
|
||||||
{t("demo")}
|
{t('demo')}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<NextUiLink
|
<NextUiLink isExternal href="https://kimatata.github.io/TestPlat/docs/getstarted/selfhost" aria-label="docs">
|
||||||
isExternal
|
|
||||||
href="https://kimatata.github.io/TestPlat/docs/getstarted/selfhost"
|
|
||||||
aria-label="docs"
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
@@ -55,16 +49,11 @@ export default function MainTitle({ locale }: Props) {
|
|||||||
className="ms-2"
|
className="ms-2"
|
||||||
endContent={<MoveUpRight size={12} />}
|
endContent={<MoveUpRight size={12} />}
|
||||||
>
|
>
|
||||||
{t("get_started")}
|
{t('get_started')}
|
||||||
</Button>
|
</Button>
|
||||||
</NextUiLink>
|
</NextUiLink>
|
||||||
|
|
||||||
<NextUiLink
|
<NextUiLink size="sm" isExternal href="https://github.com/kimatata/TestPlat" aria-label="Github">
|
||||||
size="sm"
|
|
||||||
isExternal
|
|
||||||
href="https://github.com/kimatata/TestPlat"
|
|
||||||
aria-label="Github"
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
"use client";
|
'use client';
|
||||||
import { createContext, useState, useEffect } from "react";
|
import { createContext, useState, useEffect } from 'react';
|
||||||
import { TokenType } from "@/types/user";
|
import { TokenType } from '@/types/user';
|
||||||
import { TokenProps } from "@/types/user";
|
import { TokenProps } from '@/types/user';
|
||||||
import { useRouter, usePathname } from "@/src/navigation";
|
import { useRouter, usePathname } from '@/src/navigation';
|
||||||
|
|
||||||
const LOCAL_STORAGE_KEY = "testplat-auth-token";
|
const LOCAL_STORAGE_KEY = 'testplat-auth-token';
|
||||||
const privatePaths = ["/account"];
|
const privatePaths = ['/account'];
|
||||||
|
|
||||||
function storeTokenToLocalStorage(token: TokenType) {
|
function storeTokenToLocalStorage(token: TokenType) {
|
||||||
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(token));
|
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(token));
|
||||||
@@ -23,7 +23,7 @@ const TokenProvider = ({ locale, children }: TokenProps) => {
|
|||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
const [token, setToken] = useState<TokenType>({
|
const [token, setToken] = useState<TokenType>({
|
||||||
access_token: "",
|
access_token: '',
|
||||||
user: null,
|
user: null,
|
||||||
});
|
});
|
||||||
const tokenContext = {
|
const tokenContext = {
|
||||||
@@ -57,11 +57,7 @@ const TokenProvider = ({ locale, children }: TokenProps) => {
|
|||||||
restoreTokenFromLocalStorage();
|
restoreTokenFromLocalStorage();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return <TokenContext.Provider value={tokenContext}>{children}</TokenContext.Provider>;
|
||||||
<TokenContext.Provider value={tokenContext}>
|
|
||||||
{children}
|
|
||||||
</TokenContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export { TokenContext };
|
export { TokenContext };
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
'use client';
|
||||||
import { useContext } from "react";
|
import { useContext } from 'react';
|
||||||
import { Avatar, Card, CardHeader, CardBody, Divider } from "@nextui-org/react";
|
import { Avatar, Card, CardHeader, CardBody, Divider } from '@nextui-org/react';
|
||||||
import { TokenContext } from "../TokenProvider";
|
import { TokenContext } from '../TokenProvider';
|
||||||
|
|
||||||
type AccountPageMessages = {
|
type AccountPageMessages = {
|
||||||
yourProjects: string;
|
yourProjects: string;
|
||||||
@@ -20,19 +20,10 @@ export default function AccountPage({ messages, locale }: Props) {
|
|||||||
{context.token && context.token.user && (
|
{context.token && context.token.user && (
|
||||||
<Card className="w-[600px] mt-16 mx-3">
|
<Card className="w-[600px] mt-16 mx-3">
|
||||||
<CardHeader className="flex gap-6">
|
<CardHeader className="flex gap-6">
|
||||||
<Avatar
|
<Avatar isBordered radius="full" className="w-16 h-16 text-large" src={context.token.user.avatarPath} />
|
||||||
isBordered
|
|
||||||
radius="full"
|
|
||||||
className="w-16 h-16 text-large"
|
|
||||||
src={context.token.user.avatarPath}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<p className="text-2xl font-bold">
|
<p className="text-2xl font-bold">{context.token.user.username}</p>
|
||||||
{context.token.user.username}
|
<p className="text-lg text-default-500">{context.token.user.email}</p>
|
||||||
</p>
|
|
||||||
<p className="text-lg text-default-500">
|
|
||||||
{context.token.user.email}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
"use client";
|
'use client';
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import { useState, useContext } from "react";
|
import { useState, useContext } from 'react';
|
||||||
import { Input, Button, Card, CardHeader, CardBody } from "@nextui-org/react";
|
import { Input, Button, Card, CardHeader, CardBody } from '@nextui-org/react';
|
||||||
import { Link } from "@/src/navigation";
|
import { Link } from '@/src/navigation';
|
||||||
import { ChevronRight, Eye, EyeOff } from "lucide-react";
|
import { ChevronRight, Eye, EyeOff } from 'lucide-react';
|
||||||
import { UserType, AuthMessages } from "@/types/user";
|
import { UserType, AuthMessages } from '@/types/user';
|
||||||
import { roles } from "@/config/selection";
|
import { roles } from '@/config/selection';
|
||||||
import { signUp, signIn, getRandomAvatarPath } from "./authControl";
|
import { signUp, signIn, getRandomAvatarPath } from './authControl';
|
||||||
import { isValidEmail, isValidPassword } from "./validate";
|
import { isValidEmail, isValidPassword } from './validate';
|
||||||
import { TokenContext } from "../TokenProvider";
|
import { TokenContext } from '../TokenProvider';
|
||||||
import { useRouter } from "@/src/navigation";
|
import { useRouter } from '@/src/navigation';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isSignup: Boolean;
|
isSignup: Boolean;
|
||||||
@@ -22,18 +22,17 @@ export default function AuthPage({ isSignup, messages, locale }: Props) {
|
|||||||
const context = useContext(TokenContext);
|
const context = useContext(TokenContext);
|
||||||
const [user, setUser] = useState<UserType>({
|
const [user, setUser] = useState<UserType>({
|
||||||
id: null,
|
id: null,
|
||||||
email: "",
|
email: '',
|
||||||
password: "",
|
password: '',
|
||||||
username: "",
|
username: '',
|
||||||
role: roles.findIndex((entry) => entry.uid === "user"),
|
role: roles.findIndex((entry) => entry.uid === 'user'),
|
||||||
avatarPath: "",
|
avatarPath: '',
|
||||||
});
|
});
|
||||||
const [confirmPassword, setConfirmPassword] = useState("");
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
const [errorMessage, setErrorMessage] = useState("");
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
|
|
||||||
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
||||||
const togglePasswordVisibility = () =>
|
const togglePasswordVisibility = () => setIsPasswordVisible(!isPasswordVisible);
|
||||||
setIsPasswordVisible(!isPasswordVisible);
|
|
||||||
|
|
||||||
const validate = async () => {
|
const validate = async () => {
|
||||||
if (!isValidEmail(user.email)) {
|
if (!isValidEmail(user.email)) {
|
||||||
@@ -83,31 +82,22 @@ export default function AuthPage({ isSignup, messages, locale }: Props) {
|
|||||||
|
|
||||||
context.setToken(token);
|
context.setToken(token);
|
||||||
context.storeTokenToLocalStorage(token);
|
context.storeTokenToLocalStorage(token);
|
||||||
router.push("/account", { locale: locale });
|
router.push('/account', { locale: locale });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-[480px] mt-16">
|
<Card className="w-[480px] mt-16">
|
||||||
<CardHeader className="px-4 pt-4 pb-0 flex justify-between">
|
<CardHeader className="px-4 pt-4 pb-0 flex justify-between">
|
||||||
<h4 className="font-bold text-large">{messages.title}</h4>
|
<h4 className="font-bold text-large">{messages.title}</h4>
|
||||||
<Link
|
<Link href={isSignup ? '/account/signin' : '/account/signup'} locale={locale}>
|
||||||
href={isSignup ? "/account/signin" : "/account/signup"}
|
<Button color="primary" variant="light" endContent={<ChevronRight size={16} />}>
|
||||||
locale={locale}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
color="primary"
|
|
||||||
variant="light"
|
|
||||||
endContent={<ChevronRight size={16} />}
|
|
||||||
>
|
|
||||||
{messages.linkTitle}
|
{messages.linkTitle}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody className="overflow-visible px-4 pt-0 pb-4">
|
<CardBody className="overflow-visible px-4 pt-0 pb-4">
|
||||||
<form>
|
<form>
|
||||||
{errorMessage && (
|
{errorMessage && <div className="my-3 text-danger">{errorMessage}</div>}
|
||||||
<div className="my-3 text-danger">{errorMessage}</div>
|
|
||||||
)}
|
|
||||||
<Input
|
<Input
|
||||||
isRequired
|
isRequired
|
||||||
type="email"
|
type="email"
|
||||||
@@ -139,15 +129,11 @@ export default function AuthPage({ isSignup, messages, locale }: Props) {
|
|||||||
<Input
|
<Input
|
||||||
label={messages.password}
|
label={messages.password}
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
autoComplete={isSignup ? "new-password" : "current-password"}
|
autoComplete={isSignup ? 'new-password' : 'current-password'}
|
||||||
className="mt-3"
|
className="mt-3"
|
||||||
type={isPasswordVisible ? "text" : "password"}
|
type={isPasswordVisible ? 'text' : 'password'}
|
||||||
endContent={
|
endContent={
|
||||||
<button
|
<button className="focus:outline-none" type="button" onClick={togglePasswordVisibility}>
|
||||||
className="focus:outline-none"
|
|
||||||
type="button"
|
|
||||||
onClick={togglePasswordVisibility}
|
|
||||||
>
|
|
||||||
{isPasswordVisible ? <Eye size={20} /> : <EyeOff size={20} />}
|
{isPasswordVisible ? <Eye size={20} /> : <EyeOff size={20} />}
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@@ -164,13 +150,9 @@ export default function AuthPage({ isSignup, messages, locale }: Props) {
|
|||||||
variant="bordered"
|
variant="bordered"
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
className="mt-3"
|
className="mt-3"
|
||||||
type={isPasswordVisible ? "text" : "password"}
|
type={isPasswordVisible ? 'text' : 'password'}
|
||||||
endContent={
|
endContent={
|
||||||
<button
|
<button className="focus:outline-none" type="button" onClick={togglePasswordVisibility}>
|
||||||
className="focus:outline-none"
|
|
||||||
type="button"
|
|
||||||
onClick={togglePasswordVisibility}
|
|
||||||
>
|
|
||||||
{isPasswordVisible ? <Eye size={20} /> : <EyeOff size={20} />}
|
{isPasswordVisible ? <Eye size={20} /> : <EyeOff size={20} />}
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
export default function AuthLayout({
|
export default function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="w-full flex items-center justify-center">{children}</div>
|
<div className="w-full flex items-center justify-center">{children}</div>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from 'next-intl';
|
||||||
import AccountPage from "./AccountPage";
|
import AccountPage from './AccountPage';
|
||||||
export default function Page(params: { locale: string }) {
|
export default function Page(params: { locale: string }) {
|
||||||
const t = useTranslations("Auth");
|
const t = useTranslations('Auth');
|
||||||
const messages = {
|
const messages = {
|
||||||
yourProjects: t("your_projects"),
|
yourProjects: t('your_projects'),
|
||||||
};
|
};
|
||||||
|
|
||||||
return <AccountPage messages={messages} locale={params.locale} />;
|
return <AccountPage messages={messages} locale={params.locale} />;
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
import AuthPage from "../authPage";
|
import AuthPage from '../authPage';
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
export default function Page(params: { locale: string }) {
|
export default function Page(params: { locale: string }) {
|
||||||
const t = useTranslations("Auth");
|
const t = useTranslations('Auth');
|
||||||
const messages = {
|
const messages = {
|
||||||
title: t("signin"),
|
title: t('signin'),
|
||||||
linkTitle: t("or_signup"),
|
linkTitle: t('or_signup'),
|
||||||
submitTitle: t("signin"),
|
submitTitle: t('signin'),
|
||||||
email: t("email"),
|
email: t('email'),
|
||||||
username: t("username"),
|
username: t('username'),
|
||||||
password: t("password"),
|
password: t('password'),
|
||||||
confirmPassword: t("confirm_password"),
|
confirmPassword: t('confirm_password'),
|
||||||
invalidEmail: t("invalid_email"),
|
invalidEmail: t('invalid_email'),
|
||||||
invalidPassword: t("invalid_password"),
|
invalidPassword: t('invalid_password'),
|
||||||
usernameEmpty: t("username_empty"),
|
usernameEmpty: t('username_empty'),
|
||||||
passwordDoesNotMatch: t("password_not_match"),
|
passwordDoesNotMatch: t('password_not_match'),
|
||||||
EmailAlreadyExist: t("email_already_exist"),
|
EmailAlreadyExist: t('email_already_exist'),
|
||||||
emailNotExist: t("email_not_exist"),
|
emailNotExist: t('email_not_exist'),
|
||||||
signupError: t("signup_error"),
|
signupError: t('signup_error'),
|
||||||
signinError: t("signin_error"),
|
signinError: t('signin_error'),
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
import AuthPage from "../authPage";
|
import AuthPage from '../authPage';
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
export default function Page(params: { locale: string }) {
|
export default function Page(params: { locale: string }) {
|
||||||
const t = useTranslations("Auth");
|
const t = useTranslations('Auth');
|
||||||
const messages = {
|
const messages = {
|
||||||
title: t("signup"),
|
title: t('signup'),
|
||||||
linkTitle: t("or_signin"),
|
linkTitle: t('or_signin'),
|
||||||
submitTitle: t("signup"),
|
submitTitle: t('signup'),
|
||||||
email: t("email"),
|
email: t('email'),
|
||||||
username: t("username"),
|
username: t('username'),
|
||||||
password: t("password"),
|
password: t('password'),
|
||||||
confirmPassword: t("confirm_password"),
|
confirmPassword: t('confirm_password'),
|
||||||
invalidEmail: t("invalid_email"),
|
invalidEmail: t('invalid_email'),
|
||||||
invalidPassword: t("invalid_password"),
|
invalidPassword: t('invalid_password'),
|
||||||
usernameEmpty: t("username_empty"),
|
usernameEmpty: t('username_empty'),
|
||||||
passwordDoesNotMatch: t("password_not_match"),
|
passwordDoesNotMatch: t('password_not_match'),
|
||||||
EmailAlreadyExist: t("email_already_exist"),
|
EmailAlreadyExist: t('email_already_exist'),
|
||||||
emailNotExist: t("email_not_exist"),
|
emailNotExist: t('email_not_exist'),
|
||||||
signupError: t("signup_error"),
|
signupError: t('signup_error'),
|
||||||
signinError: t("signin_error"),
|
signinError: t('signin_error'),
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
"use client";
|
'use client';
|
||||||
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
export default function Error({
|
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||||
error,
|
|
||||||
reset,
|
|
||||||
}: {
|
|
||||||
error: Error;
|
|
||||||
reset: () => void;
|
|
||||||
}) {
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Log the error to an error reporting service
|
// Log the error to an error reporting service
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import "@/styles/globals.css";
|
import '@/styles/globals.css';
|
||||||
import { fontSans } from "@/config/fonts";
|
import { fontSans } from '@/config/fonts';
|
||||||
import { Providers } from "./providers";
|
import { Providers } from './providers';
|
||||||
import Header from "./Header";
|
import Header from './Header';
|
||||||
import clsx from "clsx";
|
import clsx from 'clsx';
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from 'next-intl/server';
|
||||||
|
|
||||||
export async function generateMetadata({ params: { locale } }) {
|
export async function generateMetadata({ params: { locale } }) {
|
||||||
const t = await getTranslations({ locale, namespace: "Header" });
|
const t = await getTranslations({ locale, namespace: 'Header' });
|
||||||
return {
|
return {
|
||||||
title: t("title"),
|
title: t('title'),
|
||||||
description: t("description"),
|
description: t('description'),
|
||||||
icons: {
|
icons: {
|
||||||
icon: "/favicon/favicon.ico",
|
icon: '/favicon/favicon.ico',
|
||||||
shortcut: "/favicon/favicon-16x16.png",
|
shortcut: '/favicon/favicon-16x16.png',
|
||||||
apple: "/favicon/apple-touch-icon.png",
|
apple: '/favicon/apple-touch-icon.png',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -28,16 +28,8 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html lang={locale} suppressHydrationWarning>
|
<html lang={locale} suppressHydrationWarning>
|
||||||
<head />
|
<head />
|
||||||
<body
|
<body className={clsx('min-h-[calc(100vh-64px)] bg-background font-sans antialiased', fontSans.variable)}>
|
||||||
className={clsx(
|
<Providers themeProps={{ attribute: 'class', defaultTheme: 'light' }} tokenProps={{ locale: locale }}>
|
||||||
"min-h-[calc(100vh-64px)] bg-background font-sans antialiased",
|
|
||||||
fontSans.variable
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Providers
|
|
||||||
themeProps={{ attribute: "class", defaultTheme: "light" }}
|
|
||||||
tokenProps={{ locale: locale }}
|
|
||||||
>
|
|
||||||
<div className="relative flex flex-col min-h-screen light:bg-neutral-50 dark:bg-neutral-800">
|
<div className="relative flex flex-col min-h-screen light:bg-neutral-50 dark:bg-neutral-800">
|
||||||
<Header locale={locale} />
|
<Header locale={locale} />
|
||||||
<main>{children}</main>
|
<main>{children}</main>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from 'next-intl';
|
||||||
import { Divider } from "@nextui-org/react";
|
import { Divider } from '@nextui-org/react';
|
||||||
import PaneMainTitle from "./PaneMainTitle";
|
import PaneMainTitle from './PaneMainTitle';
|
||||||
import PaneMainFeatures from "./PaneMainFeatures";
|
import PaneMainFeatures from './PaneMainFeatures';
|
||||||
import PaneDemoImages from "./PaneDemoImages";
|
import PaneDemoImages from './PaneDemoImages';
|
||||||
|
|
||||||
export default function Home(params: { locale: string }) {
|
export default function Home(params: { locale: string }) {
|
||||||
const t = useTranslations("Index");
|
const t = useTranslations('Index');
|
||||||
|
|
||||||
const messages = {
|
const messages = {
|
||||||
title: t("organize_test_cases"),
|
title: t('organize_test_cases'),
|
||||||
caseEdit: t("case_edit"),
|
caseEdit: t('case_edit'),
|
||||||
caseHome: t("case_home"),
|
caseHome: t('case_home'),
|
||||||
caseRun: t("case_run"),
|
caseRun: t('case_run'),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -24,30 +24,30 @@ export default function Home(params: { locale: string }) {
|
|||||||
<div className="w-full md:w-5/12 p-4">
|
<div className="w-full md:w-5/12 p-4">
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
textAlign: "center",
|
textAlign: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
fontSize: "10rem",
|
fontSize: '10rem',
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
marginLeft: "2.5rem",
|
marginLeft: '2.5rem',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
⚗️
|
⚗️
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
width: "100%",
|
width: '100%',
|
||||||
height: "100%",
|
height: '100%',
|
||||||
filter: "blur(48px)",
|
filter: 'blur(48px)',
|
||||||
zIndex: 0,
|
zIndex: 0,
|
||||||
background: "linear-gradient(to bottom, #ffecd2, #fcb69f)",
|
background: 'linear-gradient(to bottom, #ffecd2, #fcb69f)',
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,7 +57,7 @@ export default function Home(params: { locale: string }) {
|
|||||||
<div
|
<div
|
||||||
className="flex flex-wrap flex-col items-center"
|
className="flex flex-wrap flex-col items-center"
|
||||||
style={{
|
style={{
|
||||||
marginTop: "6rem",
|
marginTop: '6rem',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PaneMainFeatures />
|
<PaneMainFeatures />
|
||||||
|
|||||||
@@ -1,17 +1,8 @@
|
|||||||
"use client";
|
'use client';
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from 'react';
|
||||||
import {
|
import { Button, Input, Textarea, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from '@nextui-org/react';
|
||||||
Button,
|
import { ProjectType, ProjectsMessages } from '@/types/project';
|
||||||
Input,
|
|
||||||
Textarea,
|
|
||||||
Modal,
|
|
||||||
ModalContent,
|
|
||||||
ModalHeader,
|
|
||||||
ModalBody,
|
|
||||||
ModalFooter,
|
|
||||||
} from "@nextui-org/react";
|
|
||||||
import { ProjectType, ProjectsMessages } from "@/types/project";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -21,23 +12,17 @@ type Props = {
|
|||||||
messages: ProjectsMessages;
|
messages: ProjectsMessages;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ProjectDialog({
|
export default function ProjectDialog({ isOpen, editingProject, onCancel, onSubmit, messages }: Props) {
|
||||||
isOpen,
|
|
||||||
editingProject,
|
|
||||||
onCancel,
|
|
||||||
onSubmit,
|
|
||||||
messages,
|
|
||||||
}: Props) {
|
|
||||||
const [projectName, setProjectName] = useState({
|
const [projectName, setProjectName] = useState({
|
||||||
text: editingProject ? editingProject.name : "",
|
text: editingProject ? editingProject.name : '',
|
||||||
isValid: false,
|
isValid: false,
|
||||||
errorMessage: "",
|
errorMessage: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [projectDetail, setProjectDetail] = useState({
|
const [projectDetail, setProjectDetail] = useState({
|
||||||
text: editingProject ? editingProject.detail : "",
|
text: editingProject ? editingProject.detail : '',
|
||||||
isValid: false,
|
isValid: false,
|
||||||
errorMessage: "",
|
errorMessage: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -49,17 +34,17 @@ export default function ProjectDialog({
|
|||||||
|
|
||||||
setProjectDetail({
|
setProjectDetail({
|
||||||
...projectDetail,
|
...projectDetail,
|
||||||
text: editingProject.detail ? editingProject.detail : "",
|
text: editingProject.detail ? editingProject.detail : '',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setProjectName({
|
setProjectName({
|
||||||
...projectName,
|
...projectName,
|
||||||
text: "",
|
text: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
setProjectDetail({
|
setProjectDetail({
|
||||||
...projectDetail,
|
...projectDetail,
|
||||||
text: "",
|
text: '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [editingProject]);
|
}, [editingProject]);
|
||||||
@@ -67,20 +52,20 @@ export default function ProjectDialog({
|
|||||||
const clear = () => {
|
const clear = () => {
|
||||||
setProjectName({
|
setProjectName({
|
||||||
isValid: false,
|
isValid: false,
|
||||||
text: "",
|
text: '',
|
||||||
errorMessage: "",
|
errorMessage: '',
|
||||||
});
|
});
|
||||||
setProjectDetail({
|
setProjectDetail({
|
||||||
isValid: false,
|
isValid: false,
|
||||||
text: "",
|
text: '',
|
||||||
errorMessage: "",
|
errorMessage: '',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const validate = () => {
|
const validate = () => {
|
||||||
if (!projectName.text) {
|
if (!projectName.text) {
|
||||||
setProjectName({
|
setProjectName({
|
||||||
text: "",
|
text: '',
|
||||||
isValid: false,
|
isValid: false,
|
||||||
errorMessage: messages.pleaseEnter,
|
errorMessage: messages.pleaseEnter,
|
||||||
});
|
});
|
||||||
@@ -100,9 +85,7 @@ export default function ProjectDialog({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader className="flex flex-col gap-1">
|
<ModalHeader className="flex flex-col gap-1">{messages.project}</ModalHeader>
|
||||||
{messages.project}
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
"use client";
|
'use client';
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from 'react';
|
||||||
import { Button } from "@nextui-org/react";
|
import { Button } from '@nextui-org/react';
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from 'lucide-react';
|
||||||
import { ProjectType, ProjectsMessages } from "@/types/project";
|
import { ProjectType, ProjectsMessages } from '@/types/project';
|
||||||
import ProjectsTable from "./ProjectsTable";
|
import ProjectsTable from './ProjectsTable';
|
||||||
import ProjectDialog from "./ProjectDialog";
|
import ProjectDialog from './ProjectDialog';
|
||||||
import {
|
import { fetchProjects, createProject, updateProject, deleteProject } from './projectsControl';
|
||||||
fetchProjects,
|
|
||||||
createProject,
|
|
||||||
updateProject,
|
|
||||||
deleteProject,
|
|
||||||
} from "./projectsControl";
|
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
messages: ProjectsMessages;
|
messages: ProjectsMessages;
|
||||||
@@ -26,7 +21,7 @@ export default function ProjectsPage({ messages, locale }: Props) {
|
|||||||
const data = await fetchProjects();
|
const data = await fetchProjects();
|
||||||
setProjects(data);
|
setProjects(data);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Error in effect:", error.message);
|
console.error('Error in effect:', error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,9 +30,7 @@ export default function ProjectsPage({ messages, locale }: Props) {
|
|||||||
|
|
||||||
// dialog
|
// dialog
|
||||||
const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false);
|
const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false);
|
||||||
const [editingProject, setEditingProject] = useState<ProjectType | null>(
|
const [editingProject, setEditingProject] = useState<ProjectType | null>(null);
|
||||||
null
|
|
||||||
);
|
|
||||||
const openDialogForCreate = () => {
|
const openDialogForCreate = () => {
|
||||||
setIsProjectDialogOpen(true);
|
setIsProjectDialogOpen(true);
|
||||||
setEditingProject(null);
|
setEditingProject(null);
|
||||||
@@ -50,14 +43,8 @@ export default function ProjectsPage({ messages, locale }: Props) {
|
|||||||
|
|
||||||
const onSubmit = async (name: string, detail: string) => {
|
const onSubmit = async (name: string, detail: string) => {
|
||||||
if (editingProject) {
|
if (editingProject) {
|
||||||
const updatedProject = await updateProject(
|
const updatedProject = await updateProject(editingProject.id, name, detail);
|
||||||
editingProject.id,
|
const updatedProjects = projects.map((project) => (project.id === updatedProject.id ? updatedProject : project));
|
||||||
name,
|
|
||||||
detail
|
|
||||||
);
|
|
||||||
const updatedProjects = projects.map((project) =>
|
|
||||||
project.id === updatedProject.id ? updatedProject : project
|
|
||||||
);
|
|
||||||
setProjects(updatedProjects);
|
setProjects(updatedProjects);
|
||||||
} else {
|
} else {
|
||||||
const newProject = await createProject(name, detail);
|
const newProject = await createProject(name, detail);
|
||||||
@@ -76,7 +63,7 @@ export default function ProjectsPage({ messages, locale }: Props) {
|
|||||||
await deleteProject(projectId);
|
await deleteProject(projectId);
|
||||||
setProjects(projects.filter((project) => project.id !== projectId));
|
setProjects(projects.filter((project) => project.id !== projectId));
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Error deleting project:", error);
|
console.error('Error deleting project:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,12 +72,7 @@ export default function ProjectsPage({ messages, locale }: Props) {
|
|||||||
<div className="w-full p-3 flex items-center justify-between">
|
<div className="w-full p-3 flex items-center justify-between">
|
||||||
<h3 className="font-bold">{messages.projectList}</h3>
|
<h3 className="font-bold">{messages.projectList}</h3>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button startContent={<Plus size={16} />} size="sm" color="primary" onClick={openDialogForCreate}>
|
||||||
startContent={<Plus size={16} />}
|
|
||||||
size="sm"
|
|
||||||
color="primary"
|
|
||||||
onClick={openDialogForCreate}
|
|
||||||
>
|
|
||||||
{messages.newProject}
|
{messages.newProject}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useMemo, useCallback } from "react";
|
import { useState, useMemo, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
@@ -12,11 +12,11 @@ import {
|
|||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
SortDescriptor,
|
SortDescriptor,
|
||||||
} from "@nextui-org/react";
|
} from '@nextui-org/react';
|
||||||
import { Link, NextUiLinkClasses } from "@/src/navigation";
|
import { Link, NextUiLinkClasses } from '@/src/navigation';
|
||||||
import { MoreVertical } from "lucide-react";
|
import { MoreVertical } from 'lucide-react';
|
||||||
import { ProjectType, ProjectsMessages } from "@/types/project";
|
import { ProjectType, ProjectsMessages } from '@/types/project';
|
||||||
import dayjs from "dayjs";
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
projects: ProjectType[];
|
projects: ProjectType[];
|
||||||
@@ -26,24 +26,18 @@ type Props = {
|
|||||||
locale: string;
|
locale: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ProjectsTable({
|
export default function ProjectsTable({ projects, onEditProject, onDeleteProject, messages, locale }: Props) {
|
||||||
projects,
|
|
||||||
onEditProject,
|
|
||||||
onDeleteProject,
|
|
||||||
messages,
|
|
||||||
locale,
|
|
||||||
}: Props) {
|
|
||||||
const headerColumns = [
|
const headerColumns = [
|
||||||
{ name: messages.id, uid: "id", sortable: true },
|
{ name: messages.id, uid: 'id', sortable: true },
|
||||||
{ name: messages.name, uid: "name", sortable: true },
|
{ name: messages.name, uid: 'name', sortable: true },
|
||||||
{ name: messages.detail, uid: "detail", sortable: true },
|
{ name: messages.detail, uid: 'detail', sortable: true },
|
||||||
{ name: messages.lastUpdate, uid: "updatedAt", sortable: true },
|
{ name: messages.lastUpdate, uid: 'updatedAt', sortable: true },
|
||||||
{ name: messages.actions, uid: "actions" },
|
{ name: messages.actions, uid: 'actions' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
|
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
|
||||||
column: "id",
|
column: 'id',
|
||||||
direction: "ascending",
|
direction: 'ascending',
|
||||||
});
|
});
|
||||||
|
|
||||||
const sortedItems = useMemo(() => {
|
const sortedItems = useMemo(() => {
|
||||||
@@ -52,31 +46,27 @@ export default function ProjectsTable({
|
|||||||
const second = b[sortDescriptor.column as keyof ProjectType] as number;
|
const second = b[sortDescriptor.column as keyof ProjectType] as number;
|
||||||
const cmp = first < second ? -1 : first > second ? 1 : 0;
|
const cmp = first < second ? -1 : first > second ? 1 : 0;
|
||||||
|
|
||||||
return sortDescriptor.direction === "descending" ? -cmp : cmp;
|
return sortDescriptor.direction === 'descending' ? -cmp : cmp;
|
||||||
});
|
});
|
||||||
}, [sortDescriptor, projects]);
|
}, [sortDescriptor, projects]);
|
||||||
|
|
||||||
const truncateText = (text: string, maxLength: number) => {
|
const truncateText = (text: string, maxLength: number) => {
|
||||||
return text.length > maxLength ? text.slice(0, maxLength) + "..." : text;
|
return text.length > maxLength ? text.slice(0, maxLength) + '...' : text;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderCell = useCallback((project: ProjectType, columnKey: Key) => {
|
const renderCell = useCallback((project: ProjectType, columnKey: Key) => {
|
||||||
const cellValue = project[columnKey as keyof ProjectType];
|
const cellValue = project[columnKey as keyof ProjectType];
|
||||||
|
|
||||||
switch (columnKey) {
|
switch (columnKey) {
|
||||||
case "id":
|
case 'id':
|
||||||
return <span>{cellValue}</span>;
|
return <span>{cellValue}</span>;
|
||||||
case "name":
|
case 'name':
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link href={`/projects/${project.id}/home`} locale={locale} className={NextUiLinkClasses}>
|
||||||
href={`/projects/${project.id}/home`}
|
|
||||||
locale={locale}
|
|
||||||
className={NextUiLinkClasses}
|
|
||||||
>
|
|
||||||
{cellValue}
|
{cellValue}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
case "detail":
|
case 'detail':
|
||||||
const maxLength = 20;
|
const maxLength = 20;
|
||||||
const truncatedValue = truncateText(cellValue, maxLength);
|
const truncatedValue = truncateText(cellValue, maxLength);
|
||||||
return (
|
return (
|
||||||
@@ -84,9 +74,9 @@ export default function ProjectsTable({
|
|||||||
<div>{truncatedValue}</div>
|
<div>{truncatedValue}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case "updatedAt":
|
case 'updatedAt':
|
||||||
return <span>{dayjs(cellValue).format("YYYY/MM/DD HH:mm")}</span>;
|
return <span>{dayjs(cellValue).format('YYYY/MM/DD HH:mm')}</span>;
|
||||||
case "actions":
|
case 'actions':
|
||||||
return (
|
return (
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
@@ -95,13 +85,8 @@ export default function ProjectsTable({
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownTrigger>
|
</DropdownTrigger>
|
||||||
<DropdownMenu aria-label="project actions">
|
<DropdownMenu aria-label="project actions">
|
||||||
<DropdownItem onClick={() => onEditProject(project)}>
|
<DropdownItem onClick={() => onEditProject(project)}>{messages.editProject}</DropdownItem>
|
||||||
{messages.editProject}
|
<DropdownItem className="text-danger" onClick={() => onDeleteProject(project.id)}>
|
||||||
</DropdownItem>
|
|
||||||
<DropdownItem
|
|
||||||
className="text-danger"
|
|
||||||
onClick={() => onDeleteProject(project.id)}
|
|
||||||
>
|
|
||||||
{messages.deleteProject}
|
{messages.deleteProject}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
@@ -114,18 +99,18 @@ export default function ProjectsTable({
|
|||||||
|
|
||||||
const classNames = useMemo(
|
const classNames = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
wrapper: ["max-w-3xl"],
|
wrapper: ['max-w-3xl'],
|
||||||
th: ["bg-transparent", "text-default-500", "border-b", "border-divider"],
|
th: ['bg-transparent', 'text-default-500', 'border-b', 'border-divider'],
|
||||||
td: [
|
td: [
|
||||||
// changing the rows border radius
|
// changing the rows border radius
|
||||||
// first
|
// first
|
||||||
"group-data-[first=true]:first:before:rounded-none",
|
'group-data-[first=true]:first:before:rounded-none',
|
||||||
"group-data-[first=true]:last:before:rounded-none",
|
'group-data-[first=true]:last:before:rounded-none',
|
||||||
// middle
|
// middle
|
||||||
"group-data-[middle=true]:before:rounded-none",
|
'group-data-[middle=true]:before:rounded-none',
|
||||||
// last
|
// last
|
||||||
"group-data-[last=true]:first:before:rounded-none",
|
'group-data-[last=true]:first:before:rounded-none',
|
||||||
"group-data-[last=true]:last:before:rounded-none",
|
'group-data-[last=true]:last:before:rounded-none',
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
[]
|
[]
|
||||||
@@ -144,7 +129,7 @@ export default function ProjectsTable({
|
|||||||
{(column) => (
|
{(column) => (
|
||||||
<TableColumn
|
<TableColumn
|
||||||
key={column.uid}
|
key={column.uid}
|
||||||
align={column.uid === "actions" ? "center" : "start"}
|
align={column.uid === 'actions' ? 'center' : 'start'}
|
||||||
allowsSorting={column.sortable}
|
allowsSorting={column.sortable}
|
||||||
>
|
>
|
||||||
{column.name}
|
{column.name}
|
||||||
@@ -153,11 +138,7 @@ export default function ProjectsTable({
|
|||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody emptyContent={messages.noProjectsFound} items={sortedItems}>
|
<TableBody emptyContent={messages.noProjectsFound} items={sortedItems}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<TableRow key={item.id}>
|
<TableRow key={item.id}>{(columnKey) => <TableCell>{renderCell(item, columnKey)}</TableCell>}</TableRow>
|
||||||
{(columnKey) => (
|
|
||||||
<TableCell>{renderCell(item, columnKey)}</TableCell>
|
|
||||||
)}
|
|
||||||
</TableRow>
|
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"use client";
|
'use client';
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from 'react';
|
||||||
import { Listbox, ListboxItem } from "@nextui-org/react";
|
import { Listbox, ListboxItem } from '@nextui-org/react';
|
||||||
import { Home, Files, FlaskConical } from "lucide-react";
|
import { Home, Files, FlaskConical } from 'lucide-react';
|
||||||
import { usePathname, useRouter } from "@/src/navigation";
|
import { usePathname, useRouter } from '@/src/navigation';
|
||||||
import useGetCurrentIds from "@/utils/useGetCurrentIds";
|
import useGetCurrentIds from '@/utils/useGetCurrentIds';
|
||||||
import { ProjectMessages } from "@/types/project";
|
import { ProjectMessages } from '@/types/project';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
messages: ProjectMessages;
|
messages: ProjectMessages;
|
||||||
@@ -16,28 +16,28 @@ export default function Sidebar({ messages, locale }: Props) {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
const [currentKey, setCurrentTab] = useState("home");
|
const [currentKey, setCurrentTab] = useState('home');
|
||||||
const baseClass = "p-3";
|
const baseClass = 'p-3';
|
||||||
const selectedClass = `${baseClass} bg-neutral-200 dark:bg-neutral-700`;
|
const selectedClass = `${baseClass} bg-neutral-200 dark:bg-neutral-700`;
|
||||||
|
|
||||||
const handleTabClick = (key: string) => {
|
const handleTabClick = (key: string) => {
|
||||||
if (key === "home") {
|
if (key === 'home') {
|
||||||
router.push(`/projects/${projectId}/home`, { locale: locale });
|
router.push(`/projects/${projectId}/home`, { locale: locale });
|
||||||
} else if (key === "cases") {
|
} else if (key === 'cases') {
|
||||||
router.push(`/projects/${projectId}/folders`, { locale: locale });
|
router.push(`/projects/${projectId}/folders`, { locale: locale });
|
||||||
} else if (key === "runs") {
|
} else if (key === 'runs') {
|
||||||
router.push(`/projects/${projectId}/runs`, { locale: locale });
|
router.push(`/projects/${projectId}/runs`, { locale: locale });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleRouteChange = (currentPath: string) => {
|
const handleRouteChange = (currentPath: string) => {
|
||||||
if (currentPath.includes("home")) {
|
if (currentPath.includes('home')) {
|
||||||
setCurrentTab("home");
|
setCurrentTab('home');
|
||||||
} else if (currentPath.includes("folders")) {
|
} else if (currentPath.includes('folders')) {
|
||||||
setCurrentTab("cases");
|
setCurrentTab('cases');
|
||||||
} else if (currentPath.includes("runs")) {
|
} else if (currentPath.includes('runs')) {
|
||||||
setCurrentTab("runs");
|
setCurrentTab('runs');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -46,17 +46,17 @@ export default function Sidebar({ messages, locale }: Props) {
|
|||||||
|
|
||||||
const tabItems = [
|
const tabItems = [
|
||||||
{
|
{
|
||||||
key: "home",
|
key: 'home',
|
||||||
text: messages.home,
|
text: messages.home,
|
||||||
startContent: <Home strokeWidth={1} size={28} />,
|
startContent: <Home strokeWidth={1} size={28} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "cases",
|
key: 'cases',
|
||||||
text: messages.testCases,
|
text: messages.testCases,
|
||||||
startContent: <Files strokeWidth={1} size={28} />,
|
startContent: <Files strokeWidth={1} size={28} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "runs",
|
key: 'runs',
|
||||||
text: messages.testRuns,
|
text: messages.testRuns,
|
||||||
startContent: <FlaskConical strokeWidth={1} size={28} />,
|
startContent: <FlaskConical strokeWidth={1} size={28} />,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,17 +1,8 @@
|
|||||||
"use client";
|
'use client';
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from 'react';
|
||||||
import {
|
import { Button, Input, Textarea, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from '@nextui-org/react';
|
||||||
Button,
|
import { FolderType, FoldersMessages } from '@/types/folder';
|
||||||
Input,
|
|
||||||
Textarea,
|
|
||||||
Modal,
|
|
||||||
ModalContent,
|
|
||||||
ModalHeader,
|
|
||||||
ModalBody,
|
|
||||||
ModalFooter,
|
|
||||||
} from "@nextui-org/react";
|
|
||||||
import { FolderType, FoldersMessages } from "@/types/folder";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -21,23 +12,17 @@ type Props = {
|
|||||||
messages: FoldersMessages;
|
messages: FoldersMessages;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function FolderDialog({
|
export default function FolderDialog({ isOpen, editingFolder, onCancel, onSubmit, messages }: Props) {
|
||||||
isOpen,
|
|
||||||
editingFolder,
|
|
||||||
onCancel,
|
|
||||||
onSubmit,
|
|
||||||
messages,
|
|
||||||
}: Props) {
|
|
||||||
const [folderName, setFolderName] = useState({
|
const [folderName, setFolderName] = useState({
|
||||||
text: editingFolder ? editingFolder.name : "",
|
text: editingFolder ? editingFolder.name : '',
|
||||||
isValid: false,
|
isValid: false,
|
||||||
errorMessage: "",
|
errorMessage: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [folderDetail, setFolderDetail] = useState({
|
const [folderDetail, setFolderDetail] = useState({
|
||||||
text: editingFolder ? editingFolder.detail : "",
|
text: editingFolder ? editingFolder.detail : '',
|
||||||
isValid: false,
|
isValid: false,
|
||||||
errorMessage: "",
|
errorMessage: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -49,17 +34,17 @@ export default function FolderDialog({
|
|||||||
|
|
||||||
setFolderDetail({
|
setFolderDetail({
|
||||||
...folderDetail,
|
...folderDetail,
|
||||||
text: editingFolder.detail ? editingFolder.detail : "",
|
text: editingFolder.detail ? editingFolder.detail : '',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setFolderName({
|
setFolderName({
|
||||||
...folderName,
|
...folderName,
|
||||||
text: "",
|
text: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
setFolderDetail({
|
setFolderDetail({
|
||||||
...folderDetail,
|
...folderDetail,
|
||||||
text: "",
|
text: '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [editingFolder]);
|
}, [editingFolder]);
|
||||||
@@ -67,20 +52,20 @@ export default function FolderDialog({
|
|||||||
const clear = () => {
|
const clear = () => {
|
||||||
setFolderName({
|
setFolderName({
|
||||||
isValid: false,
|
isValid: false,
|
||||||
text: "",
|
text: '',
|
||||||
errorMessage: "",
|
errorMessage: '',
|
||||||
});
|
});
|
||||||
setFolderDetail({
|
setFolderDetail({
|
||||||
isValid: false,
|
isValid: false,
|
||||||
text: "",
|
text: '',
|
||||||
errorMessage: "",
|
errorMessage: '',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const validate = () => {
|
const validate = () => {
|
||||||
if (!folderName.text) {
|
if (!folderName.text) {
|
||||||
setFolderName({
|
setFolderName({
|
||||||
text: "",
|
text: '',
|
||||||
isValid: false,
|
isValid: false,
|
||||||
errorMessage: messages.pleaseEnter,
|
errorMessage: messages.pleaseEnter,
|
||||||
});
|
});
|
||||||
@@ -100,9 +85,7 @@ export default function FolderDialog({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader className="flex flex-col gap-1">
|
<ModalHeader className="flex flex-col gap-1">{messages.folder}</ModalHeader>
|
||||||
{messages.folder}
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
import {
|
import { Button, Dropdown, DropdownTrigger, DropdownMenu, DropdownItem } from '@nextui-org/react';
|
||||||
Button,
|
import { MoreVertical } from 'lucide-react';
|
||||||
Dropdown,
|
import { FolderType, FoldersMessages } from '@/types/folder';
|
||||||
DropdownTrigger,
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownItem,
|
|
||||||
} from "@nextui-org/react";
|
|
||||||
import { MoreVertical } from "lucide-react";
|
|
||||||
import { FolderType, FoldersMessages } from "@/types/folder";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
folder: FolderType;
|
folder: FolderType;
|
||||||
@@ -15,12 +9,7 @@ type Props = {
|
|||||||
messages: FoldersMessages;
|
messages: FoldersMessages;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function FolderEditMenu({
|
export default function FolderEditMenu({ folder, onEditClick, onDeleteClick, messages }: Props) {
|
||||||
folder,
|
|
||||||
onEditClick,
|
|
||||||
onDeleteClick,
|
|
||||||
messages,
|
|
||||||
}: Props) {
|
|
||||||
return (
|
return (
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
@@ -32,12 +21,7 @@ export default function FolderEditMenu({
|
|||||||
<DropdownItem key="edit" onClick={() => onEditClick(folder)}>
|
<DropdownItem key="edit" onClick={() => onEditClick(folder)}>
|
||||||
{messages.editFolder}
|
{messages.editFolder}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem
|
<DropdownItem key="delete" className="text-danger" color="danger" onClick={() => onDeleteClick(folder.id)}>
|
||||||
key="delete"
|
|
||||||
className="text-danger"
|
|
||||||
color="danger"
|
|
||||||
onClick={() => onDeleteClick(folder.id)}
|
|
||||||
>
|
|
||||||
{messages.deleteFolder}
|
{messages.deleteFolder}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
"use client";
|
'use client';
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import { FolderType, FoldersMessages } from "@/types/folder";
|
import { FolderType, FoldersMessages } from '@/types/folder';
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from 'react';
|
||||||
import { Button, Listbox, ListboxItem } from "@nextui-org/react";
|
import { Button, Listbox, ListboxItem } from '@nextui-org/react';
|
||||||
import { Folder, Plus } from "lucide-react";
|
import { Folder, Plus } from 'lucide-react';
|
||||||
import { usePathname, useRouter } from "@/src/navigation";
|
import { usePathname, useRouter } from '@/src/navigation';
|
||||||
import useGetCurrentIds from "@/utils/useGetCurrentIds";
|
import useGetCurrentIds from '@/utils/useGetCurrentIds';
|
||||||
import FolderDialog from "./FolderDialog";
|
import FolderDialog from './FolderDialog';
|
||||||
import FolderEditMenu from "./FolderEditMenu";
|
import FolderEditMenu from './FolderEditMenu';
|
||||||
|
|
||||||
import {
|
import { fetchFolders, createFolder, updateFolder, deleteFolder } from './foldersControl';
|
||||||
fetchFolders,
|
|
||||||
createFolder,
|
|
||||||
updateFolder,
|
|
||||||
deleteFolder,
|
|
||||||
} from "./foldersControl";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@@ -45,16 +40,8 @@ export default function FoldersPane({ projectId, messages, locale }: Props) {
|
|||||||
|
|
||||||
const onSubmit = async (name: string, detail: string) => {
|
const onSubmit = async (name: string, detail: string) => {
|
||||||
if (editingFolder) {
|
if (editingFolder) {
|
||||||
const updatedProject = await updateFolder(
|
const updatedProject = await updateFolder(editingFolder.id, name, detail, projectId, null);
|
||||||
editingFolder.id,
|
const updatedProjects = folders.map((project) => (project.id === updatedProject.id ? updatedProject : project));
|
||||||
name,
|
|
||||||
detail,
|
|
||||||
projectId,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
const updatedProjects = folders.map((project) =>
|
|
||||||
project.id === updatedProject.id ? updatedProject : project
|
|
||||||
);
|
|
||||||
setFolders(updatedProjects);
|
setFolders(updatedProjects);
|
||||||
} else {
|
} else {
|
||||||
const newProject = await createFolder(name, detail, projectId, null);
|
const newProject = await createFolder(name, detail, projectId, null);
|
||||||
@@ -79,28 +66,23 @@ export default function FoldersPane({ projectId, messages, locale }: Props) {
|
|||||||
const data = await fetchFolders(projectId);
|
const data = await fetchFolders(projectId);
|
||||||
setFolders(data);
|
setFolders(data);
|
||||||
|
|
||||||
const selectedFolderFromUrl = data.find(
|
const selectedFolderFromUrl = data.find((folder) => folder.id === folderId);
|
||||||
(folder) => folder.id === folderId
|
|
||||||
);
|
|
||||||
setSelectedFolder(selectedFolderFromUrl);
|
setSelectedFolder(selectedFolderFromUrl);
|
||||||
|
|
||||||
// Redirect to the smallest folder ID page if the path is "projects/[projectId]/folders
|
// Redirect to the smallest folder ID page if the path is "projects/[projectId]/folders
|
||||||
if (pathname === `/projects/${projectId}/folders`) {
|
if (pathname === `/projects/${projectId}/folders`) {
|
||||||
const smallestFolderId = Math.min(...data.map((folder) => folder.id));
|
const smallestFolderId = Math.min(...data.map((folder) => folder.id));
|
||||||
router.push(
|
router.push(`/projects/${projectId}/folders/${smallestFolderId}/cases`, { locale: locale });
|
||||||
`/projects/${projectId}/folders/${smallestFolderId}/cases`,
|
|
||||||
{ locale: locale }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Error in effect:", error.message);
|
console.error('Error in effect:', error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchDataEffect();
|
fetchDataEffect();
|
||||||
}, [folderId]);
|
}, [folderId]);
|
||||||
|
|
||||||
const baseClass = "";
|
const baseClass = '';
|
||||||
const selectedClass = `${baseClass} bg-neutral-200 dark:bg-neutral-700`;
|
const selectedClass = `${baseClass} bg-neutral-200 dark:bg-neutral-700`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -119,18 +101,9 @@ export default function FoldersPane({ projectId, messages, locale }: Props) {
|
|||||||
{folders.map((folder, index) => (
|
{folders.map((folder, index) => (
|
||||||
<ListboxItem
|
<ListboxItem
|
||||||
key={index}
|
key={index}
|
||||||
onClick={() =>
|
onClick={() => router.push(`/projects/${projectId}/folders/${folder.id}/cases`, { locale: locale })}
|
||||||
router.push(
|
|
||||||
`/projects/${projectId}/folders/${folder.id}/cases`,
|
|
||||||
{ locale: locale }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
startContent={<Folder size={20} color="#F7C24E" fill="#F7C24E" />}
|
startContent={<Folder size={20} color="#F7C24E" fill="#F7C24E" />}
|
||||||
className={
|
className={selectedFolder && folder.id === selectedFolder.id ? selectedClass : baseClass}
|
||||||
selectedFolder && folder.id === selectedFolder.id
|
|
||||||
? selectedClass
|
|
||||||
: baseClass
|
|
||||||
}
|
|
||||||
endContent={
|
endContent={
|
||||||
<FolderEditMenu
|
<FolderEditMenu
|
||||||
folder={folder}
|
folder={folder}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"use client";
|
'use client';
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from 'react';
|
||||||
import TestCaseTable from "./TestCaseTable";
|
import TestCaseTable from './TestCaseTable';
|
||||||
import { fetchCases, createCase, deleteCase, deleteCases } from "./caseControl";
|
import { fetchCases, createCase, deleteCase, deleteCases } from './caseControl';
|
||||||
import { CasesMessages } from "@/types/case";
|
import { CasesMessages } from '@/types/case';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@@ -11,12 +11,7 @@ type Props = {
|
|||||||
locale: string;
|
locale: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CasesPane({
|
export default function CasesPane({ projectId, folderId, messages, locale }: Props) {
|
||||||
projectId,
|
|
||||||
folderId,
|
|
||||||
messages,
|
|
||||||
locale,
|
|
||||||
}: Props) {
|
|
||||||
const [cases, setCases] = useState([]);
|
const [cases, setCases] = useState([]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchDataEffect() {
|
async function fetchDataEffect() {
|
||||||
@@ -24,7 +19,7 @@ export default function CasesPane({
|
|||||||
const data = await fetchCases(folderId);
|
const data = await fetchCases(folderId);
|
||||||
setCases(data);
|
setCases(data);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Error in effect:", error.message);
|
console.error('Error in effect:', error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useMemo, useCallback } from "react";
|
import { useState, useMemo, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
@@ -13,11 +13,11 @@ import {
|
|||||||
DropdownItem,
|
DropdownItem,
|
||||||
Selection,
|
Selection,
|
||||||
SortDescriptor,
|
SortDescriptor,
|
||||||
} from "@nextui-org/react";
|
} from '@nextui-org/react';
|
||||||
import { Link, NextUiLinkClasses } from "@/src/navigation";
|
import { Link, NextUiLinkClasses } from '@/src/navigation';
|
||||||
import { Plus, MoreVertical, Trash, Circle } from "lucide-react";
|
import { Plus, MoreVertical, Trash, Circle } from 'lucide-react';
|
||||||
import { CasesMessages } from "@/types/case";
|
import { CasesMessages } from '@/types/case';
|
||||||
import { priorities } from "@/config/selection";
|
import { priorities } from '@/config/selection';
|
||||||
|
|
||||||
type Case = {
|
type Case = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -55,16 +55,16 @@ export default function TestCaseTable({
|
|||||||
locale,
|
locale,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const headerColumns = [
|
const headerColumns = [
|
||||||
{ name: messages.id, uid: "id", sortable: true },
|
{ name: messages.id, uid: 'id', sortable: true },
|
||||||
{ name: messages.title, uid: "title", sortable: true },
|
{ name: messages.title, uid: 'title', sortable: true },
|
||||||
{ name: messages.priority, uid: "priority", sortable: true },
|
{ name: messages.priority, uid: 'priority', sortable: true },
|
||||||
{ name: messages.actions, uid: "actions" },
|
{ name: messages.actions, uid: 'actions' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const [selectedKeys, setSelectedKeys] = useState<Selection>(new Set([]));
|
const [selectedKeys, setSelectedKeys] = useState<Selection>(new Set([]));
|
||||||
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
|
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
|
||||||
column: "id",
|
column: 'id',
|
||||||
direction: "ascending",
|
direction: 'ascending',
|
||||||
});
|
});
|
||||||
|
|
||||||
const sortedItems = useMemo(() => {
|
const sortedItems = useMemo(() => {
|
||||||
@@ -73,7 +73,7 @@ export default function TestCaseTable({
|
|||||||
const second = b[sortDescriptor.column as keyof Case] as number;
|
const second = b[sortDescriptor.column as keyof Case] as number;
|
||||||
const cmp = first < second ? -1 : first > second ? 1 : 0;
|
const cmp = first < second ? -1 : first > second ? 1 : 0;
|
||||||
|
|
||||||
return sortDescriptor.direction === "descending" ? -cmp : cmp;
|
return sortDescriptor.direction === 'descending' ? -cmp : cmp;
|
||||||
});
|
});
|
||||||
}, [sortDescriptor, cases]);
|
}, [sortDescriptor, cases]);
|
||||||
|
|
||||||
@@ -81,15 +81,11 @@ export default function TestCaseTable({
|
|||||||
const cellValue = testCase[columnKey as keyof Case];
|
const cellValue = testCase[columnKey as keyof Case];
|
||||||
|
|
||||||
switch (columnKey) {
|
switch (columnKey) {
|
||||||
case "id":
|
case 'id':
|
||||||
return <span>{cellValue}</span>;
|
return <span>{cellValue}</span>;
|
||||||
case "title":
|
case 'title':
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button size="sm" variant="light" className="data-[hover=true]:bg-transparent">
|
||||||
size="sm"
|
|
||||||
variant="light"
|
|
||||||
className="data-[hover=true]:bg-transparent"
|
|
||||||
>
|
|
||||||
<Link
|
<Link
|
||||||
href={`/projects/${projectId}/folders/${testCase.folderId}/cases/${testCase.id}`}
|
href={`/projects/${projectId}/folders/${testCase.folderId}/cases/${testCase.id}`}
|
||||||
locale={locale}
|
locale={locale}
|
||||||
@@ -99,18 +95,14 @@ export default function TestCaseTable({
|
|||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
case "priority":
|
case 'priority':
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Circle
|
<Circle size={8} color={priorities[cellValue].color} fill={priorities[cellValue].color} />
|
||||||
size={8}
|
|
||||||
color={priorities[cellValue].color}
|
|
||||||
fill={priorities[cellValue].color}
|
|
||||||
/>
|
|
||||||
<div className="ms-3">{messages[priorities[cellValue].uid]}</div>
|
<div className="ms-3">{messages[priorities[cellValue].uid]}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case "actions":
|
case 'actions':
|
||||||
return (
|
return (
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
@@ -119,10 +111,7 @@ export default function TestCaseTable({
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownTrigger>
|
</DropdownTrigger>
|
||||||
<DropdownMenu aria-label="test case actions">
|
<DropdownMenu aria-label="test case actions">
|
||||||
<DropdownItem
|
<DropdownItem className="text-danger" onClick={() => onDeleteCase(testCase.id)}>
|
||||||
className="text-danger"
|
|
||||||
onClick={() => onDeleteCase(testCase.id)}
|
|
||||||
>
|
|
||||||
{messages.deleteCase}
|
{messages.deleteCase}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
@@ -135,25 +124,25 @@ export default function TestCaseTable({
|
|||||||
|
|
||||||
const classNames = useMemo(
|
const classNames = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
wrapper: ["max-w-3xl"],
|
wrapper: ['max-w-3xl'],
|
||||||
th: ["bg-transparent", "text-default-500", "border-b", "border-divider"],
|
th: ['bg-transparent', 'text-default-500', 'border-b', 'border-divider'],
|
||||||
td: [
|
td: [
|
||||||
// changing the rows border radius
|
// changing the rows border radius
|
||||||
// first
|
// first
|
||||||
"group-data-[first=true]:first:before:rounded-none",
|
'group-data-[first=true]:first:before:rounded-none',
|
||||||
"group-data-[first=true]:last:before:rounded-none",
|
'group-data-[first=true]:last:before:rounded-none',
|
||||||
// middle
|
// middle
|
||||||
"group-data-[middle=true]:before:rounded-none",
|
'group-data-[middle=true]:before:rounded-none',
|
||||||
// last
|
// last
|
||||||
"group-data-[last=true]:first:before:rounded-none",
|
'group-data-[last=true]:first:before:rounded-none',
|
||||||
"group-data-[last=true]:last:before:rounded-none",
|
'group-data-[last=true]:last:before:rounded-none',
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onDeleteCasesClick = async () => {
|
const onDeleteCasesClick = async () => {
|
||||||
if (selectedKeys === "all") {
|
if (selectedKeys === 'all') {
|
||||||
const allKeys = sortedItems.map((item) => item.id);
|
const allKeys = sortedItems.map((item) => item.id);
|
||||||
onDeleteCases(allKeys);
|
onDeleteCases(allKeys);
|
||||||
} else {
|
} else {
|
||||||
@@ -167,7 +156,7 @@ export default function TestCaseTable({
|
|||||||
<div className="border-b-1 dark:border-neutral-700 w-full p-3 flex items-center justify-between">
|
<div className="border-b-1 dark:border-neutral-700 w-full p-3 flex items-center justify-between">
|
||||||
<h3 className="font-bold">{messages.testCaseList}</h3>
|
<h3 className="font-bold">{messages.testCaseList}</h3>
|
||||||
<div>
|
<div>
|
||||||
{(selectedKeys.size > 0 || selectedKeys === "all") && (
|
{(selectedKeys.size > 0 || selectedKeys === 'all') && (
|
||||||
<Button
|
<Button
|
||||||
startContent={<Trash size={16} />}
|
startContent={<Trash size={16} />}
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -178,12 +167,7 @@ export default function TestCaseTable({
|
|||||||
{messages.delete}
|
{messages.delete}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button startContent={<Plus size={16} />} size="sm" color="primary" onClick={onCreateCase}>
|
||||||
startContent={<Plus size={16} />}
|
|
||||||
size="sm"
|
|
||||||
color="primary"
|
|
||||||
onClick={onCreateCase}
|
|
||||||
>
|
|
||||||
{messages.newTestCase}
|
{messages.newTestCase}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -204,7 +188,7 @@ export default function TestCaseTable({
|
|||||||
{(column) => (
|
{(column) => (
|
||||||
<TableColumn
|
<TableColumn
|
||||||
key={column.uid}
|
key={column.uid}
|
||||||
align={column.uid === "actions" ? "center" : "start"}
|
align={column.uid === 'actions' ? 'center' : 'start'}
|
||||||
allowsSorting={column.sortable}
|
allowsSorting={column.sortable}
|
||||||
>
|
>
|
||||||
{column.name}
|
{column.name}
|
||||||
@@ -213,11 +197,7 @@ export default function TestCaseTable({
|
|||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody emptyContent={messages.noCasesFound} items={sortedItems}>
|
<TableBody emptyContent={messages.noCasesFound} items={sortedItems}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<TableRow key={item.id}>
|
<TableRow key={item.id}>{(columnKey) => <TableCell>{renderCell(item, columnKey)}</TableCell>}</TableRow>
|
||||||
{(columnKey) => (
|
|
||||||
<TableCell>{renderCell(item, columnKey)}</TableCell>
|
|
||||||
)}
|
|
||||||
</TableRow>
|
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
import { Image, Button, Tooltip, Card, CardBody } from "@nextui-org/react";
|
import { Image, Button, Tooltip, Card, CardBody } from '@nextui-org/react';
|
||||||
import { AttachmentType, CaseMessages } from "@/types/case";
|
import { AttachmentType, CaseMessages } from '@/types/case';
|
||||||
import { Trash, ArrowDownToLine } from "lucide-react";
|
import { Trash, ArrowDownToLine } from 'lucide-react';
|
||||||
import { isImage } from "./isImage";
|
import { isImage } from './isImage';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
attachments: AttachmentType[];
|
attachments: AttachmentType[];
|
||||||
onAttachmentDownload: (
|
onAttachmentDownload: (attachmentId: number, downloadFileName: string) => void;
|
||||||
attachmentId: number,
|
|
||||||
downloadFileName: string
|
|
||||||
) => void;
|
|
||||||
onAttachmentDelete: (attachmentId: number) => void;
|
onAttachmentDelete: (attachmentId: number) => void;
|
||||||
messages: CaseMessages,
|
messages: CaseMessages;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CaseAttachmentsEditor({
|
export default function CaseAttachmentsEditor({
|
||||||
@@ -35,11 +32,7 @@ export default function CaseAttachmentsEditor({
|
|||||||
{images.map((image, index) => (
|
{images.map((image, index) => (
|
||||||
<Card key={index} radius="sm" className="mt-2 me-2 max-w-md">
|
<Card key={index} radius="sm" className="mt-2 me-2 max-w-md">
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<Image
|
<Image alt={image.title} src={image.path} className="object-cover h-40 w-40" />
|
||||||
alt={image.title}
|
|
||||||
src={image.path}
|
|
||||||
className="object-cover h-40 w-40"
|
|
||||||
/>
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p>{image.title}</p>
|
<p>{image.title}</p>
|
||||||
<Tooltip content={messages.delete}>
|
<Tooltip content={messages.delete}>
|
||||||
|
|||||||
@@ -1,39 +1,27 @@
|
|||||||
"use client";
|
'use client';
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from 'react';
|
||||||
import {
|
import { Input, Textarea, Select, SelectItem, Button, Divider, Tooltip } from '@nextui-org/react';
|
||||||
Input,
|
import { useRouter } from '@/src/navigation';
|
||||||
Textarea,
|
import { Save, Plus, ArrowLeft, ArrowUpFromLine, Circle } from 'lucide-react';
|
||||||
Select,
|
import { priorities, testTypes, templates } from '@/config/selection';
|
||||||
SelectItem,
|
import CaseStepsEditor from './CaseStepsEditor';
|
||||||
Button,
|
import CaseAttachmentsEditor from './CaseAttachmentsEditor';
|
||||||
Divider,
|
import { CaseType, AttachmentType, CaseMessages } from '@/types/case';
|
||||||
Tooltip,
|
import { fetchCase, updateCase } from '../caseControl';
|
||||||
} from "@nextui-org/react";
|
import { fetchCreateStep, fetchDeleteStep } from './stepControl';
|
||||||
import { useRouter } from "@/src/navigation";
|
import { fetchCreateAttachments, fetchDownloadAttachment, fetchDeleteAttachment } from './attachmentControl';
|
||||||
import { Save, Plus, ArrowLeft, ArrowUpFromLine, Circle } from "lucide-react";
|
|
||||||
import { priorities, testTypes, templates } from "@/config/selection";
|
|
||||||
import CaseStepsEditor from "./CaseStepsEditor";
|
|
||||||
import CaseAttachmentsEditor from "./CaseAttachmentsEditor";
|
|
||||||
import { CaseType, AttachmentType, CaseMessages } from "@/types/case";
|
|
||||||
import { fetchCase, updateCase } from "../caseControl";
|
|
||||||
import { fetchCreateStep, fetchDeleteStep } from "./stepControl";
|
|
||||||
import {
|
|
||||||
fetchCreateAttachments,
|
|
||||||
fetchDownloadAttachment,
|
|
||||||
fetchDeleteAttachment,
|
|
||||||
} from "./attachmentControl";
|
|
||||||
|
|
||||||
const defaultTestCase = {
|
const defaultTestCase = {
|
||||||
id: 0,
|
id: 0,
|
||||||
title: "",
|
title: '',
|
||||||
state: 0,
|
state: 0,
|
||||||
priority: 0,
|
priority: 0,
|
||||||
type: 0,
|
type: 0,
|
||||||
automationStatus: 0,
|
automationStatus: 0,
|
||||||
description: "",
|
description: '',
|
||||||
template: 0,
|
template: 0,
|
||||||
preConditions: "",
|
preConditions: '',
|
||||||
expectedResults: "",
|
expectedResults: '',
|
||||||
folderId: 0,
|
folderId: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,13 +33,7 @@ type Props = {
|
|||||||
locale: string;
|
locale: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CaseEditor({
|
export default function CaseEditor({ projectId, folderId, caseId, messages, locale }: Props) {
|
||||||
projectId,
|
|
||||||
folderId,
|
|
||||||
caseId,
|
|
||||||
messages,
|
|
||||||
locale,
|
|
||||||
}: Props) {
|
|
||||||
const [testCase, setTestCase] = useState<CaseType>(defaultTestCase);
|
const [testCase, setTestCase] = useState<CaseType>(defaultTestCase);
|
||||||
const [isTitleInvalid, setIsTitleInvalid] = useState<boolean>(false);
|
const [isTitleInvalid, setIsTitleInvalid] = useState<boolean>(false);
|
||||||
const [isUpdating, setIsUpdating] = useState<boolean>(false);
|
const [isUpdating, setIsUpdating] = useState<boolean>(false);
|
||||||
@@ -122,10 +104,7 @@ export default function CaseEditor({
|
|||||||
handleFetchCreateAttachments(caseId, event.target.files);
|
handleFetchCreateAttachments(caseId, event.target.files);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFetchCreateAttachments = async (
|
const handleFetchCreateAttachments = async (caseId: number, files: File[]) => {
|
||||||
caseId: number,
|
|
||||||
files: File[]
|
|
||||||
) => {
|
|
||||||
const newAttachments = await fetchCreateAttachments(caseId, files);
|
const newAttachments = await fetchCreateAttachments(caseId, files);
|
||||||
|
|
||||||
if (newAttachments) {
|
if (newAttachments) {
|
||||||
@@ -147,9 +126,7 @@ export default function CaseEditor({
|
|||||||
const onAttachmentDelete = async (attachmentId: number) => {
|
const onAttachmentDelete = async (attachmentId: number) => {
|
||||||
await fetchDeleteAttachment(attachmentId);
|
await fetchDeleteAttachment(attachmentId);
|
||||||
|
|
||||||
const filteredAttachments = testCase.Attachments.filter(
|
const filteredAttachments = testCase.Attachments.filter((attachment) => attachment.id !== attachmentId);
|
||||||
(attachment) => attachment.id !== attachmentId
|
|
||||||
);
|
|
||||||
|
|
||||||
setTestCase({
|
setTestCase({
|
||||||
...testCase,
|
...testCase,
|
||||||
@@ -163,7 +140,7 @@ export default function CaseEditor({
|
|||||||
const data = await fetchCase(caseId);
|
const data = await fetchCase(caseId);
|
||||||
setTestCase(data);
|
setTestCase(data);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Error in effect:", error.message);
|
console.error('Error in effect:', error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,12 +156,7 @@ export default function CaseEditor({
|
|||||||
isIconOnly
|
isIconOnly
|
||||||
size="sm"
|
size="sm"
|
||||||
className="rounded-full bg-neutral-50 dark:bg-neutral-600"
|
className="rounded-full bg-neutral-50 dark:bg-neutral-600"
|
||||||
onPress={() =>
|
onPress={() => router.push(`/projects/${projectId}/folders/${folderId}/cases`, { locale: locale })}
|
||||||
router.push(
|
|
||||||
`/projects/${projectId}/folders/${folderId}/cases`,
|
|
||||||
{ locale: locale }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<ArrowLeft size={16} />
|
<ArrowLeft size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -215,7 +187,7 @@ export default function CaseEditor({
|
|||||||
label={messages.title}
|
label={messages.title}
|
||||||
value={testCase.title}
|
value={testCase.title}
|
||||||
isInvalid={isTitleInvalid}
|
isInvalid={isTitleInvalid}
|
||||||
errorMessage={isTitleInvalid ? messages.pleaseEnterTitle : ""}
|
errorMessage={isTitleInvalid ? messages.pleaseEnterTitle : ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setTestCase({ ...testCase, title: e.target.value });
|
setTestCase({ ...testCase, title: e.target.value });
|
||||||
}}
|
}}
|
||||||
@@ -241,17 +213,11 @@ export default function CaseEditor({
|
|||||||
selectedKeys={[priorities[testCase.priority].uid]}
|
selectedKeys={[priorities[testCase.priority].uid]}
|
||||||
onSelectionChange={(e) => {
|
onSelectionChange={(e) => {
|
||||||
const selectedUid = e.anchorKey;
|
const selectedUid = e.anchorKey;
|
||||||
const index = priorities.findIndex(
|
const index = priorities.findIndex((priority) => priority.uid === selectedUid);
|
||||||
(priority) => priority.uid === selectedUid
|
|
||||||
);
|
|
||||||
setTestCase({ ...testCase, priority: index });
|
setTestCase({ ...testCase, priority: index });
|
||||||
}}
|
}}
|
||||||
startContent={
|
startContent={
|
||||||
<Circle
|
<Circle size={8} color={priorities[testCase.priority].color} fill={priorities[testCase.priority].color} />
|
||||||
size={8}
|
|
||||||
color={priorities[testCase.priority].color}
|
|
||||||
fill={priorities[testCase.priority].color}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
label={messages.priority}
|
label={messages.priority}
|
||||||
className="mt-3 max-w-xs"
|
className="mt-3 max-w-xs"
|
||||||
@@ -271,9 +237,7 @@ export default function CaseEditor({
|
|||||||
selectedKeys={[testTypes[testCase.type].uid]}
|
selectedKeys={[testTypes[testCase.type].uid]}
|
||||||
onSelectionChange={(e) => {
|
onSelectionChange={(e) => {
|
||||||
const selectedUid = e.anchorKey;
|
const selectedUid = e.anchorKey;
|
||||||
const index = testTypes.findIndex(
|
const index = testTypes.findIndex((type) => type.uid === selectedUid);
|
||||||
(type) => type.uid === selectedUid
|
|
||||||
);
|
|
||||||
setTestCase({ ...testCase, type: index });
|
setTestCase({ ...testCase, type: index });
|
||||||
}}
|
}}
|
||||||
label={messages.type}
|
label={messages.type}
|
||||||
@@ -294,9 +258,7 @@ export default function CaseEditor({
|
|||||||
selectedKeys={[templates[testCase.template].uid]}
|
selectedKeys={[templates[testCase.template].uid]}
|
||||||
onSelectionChange={(e) => {
|
onSelectionChange={(e) => {
|
||||||
const selectedUid = e.anchorKey;
|
const selectedUid = e.anchorKey;
|
||||||
const index = templates.findIndex(
|
const index = templates.findIndex((template) => template.uid === selectedUid);
|
||||||
(template) => template.uid === selectedUid
|
|
||||||
);
|
|
||||||
setTestCase({ ...testCase, template: index });
|
setTestCase({ ...testCase, template: index });
|
||||||
}}
|
}}
|
||||||
label={messages.template}
|
label={messages.template}
|
||||||
@@ -311,7 +273,7 @@ export default function CaseEditor({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Divider className="my-6" />
|
<Divider className="my-6" />
|
||||||
{templates[testCase.template].uid === "text" ? (
|
{templates[testCase.template].uid === 'text' ? (
|
||||||
<div>
|
<div>
|
||||||
<h6 className="font-bold">{messages.testDetail}</h6>
|
<h6 className="font-bold">{messages.testDetail}</h6>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
@@ -377,10 +339,9 @@ export default function CaseEditor({
|
|||||||
<h6 className="font-bold">{messages.attachments}</h6>
|
<h6 className="font-bold">{messages.attachments}</h6>
|
||||||
<CaseAttachmentsEditor
|
<CaseAttachmentsEditor
|
||||||
attachments={testCase.Attachments}
|
attachments={testCase.Attachments}
|
||||||
onAttachmentDownload={(
|
onAttachmentDownload={(attachmentId: number, downloadFileName: string) =>
|
||||||
attachmentId: number,
|
fetchDownloadAttachment(attachmentId, downloadFileName)
|
||||||
downloadFileName: string
|
}
|
||||||
) => fetchDownloadAttachment(attachmentId, downloadFileName)}
|
|
||||||
onAttachmentDelete={onAttachmentDelete}
|
onAttachmentDelete={onAttachmentDelete}
|
||||||
messages={messages}
|
messages={messages}
|
||||||
/>
|
/>
|
||||||
@@ -399,17 +360,9 @@ export default function CaseEditor({
|
|||||||
<span className="font-semibold">{messages.clickToUpload}</span>
|
<span className="font-semibold">{messages.clickToUpload}</span>
|
||||||
<span>{messages.orDragAndDrop}</span>
|
<span>{messages.orDragAndDrop}</span>
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400">
|
<p className="text-xs text-neutral-500 dark:text-neutral-400">{messages.maxFileSize}: 50 MB</p>
|
||||||
{messages.maxFileSize}: 50 MB
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input id="dropzone-file" type="file" className="hidden" onChange={handleInput} multiple />
|
||||||
id="dropzone-file"
|
|
||||||
type="file"
|
|
||||||
className="hidden"
|
|
||||||
onChange={handleInput}
|
|
||||||
multiple
|
|
||||||
/>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Textarea, Button, Tooltip } from "@nextui-org/react";
|
import { Textarea, Button, Tooltip } from '@nextui-org/react';
|
||||||
import { CaseMessages, StepType } from "@/types/case";
|
import { CaseMessages, StepType } from '@/types/case';
|
||||||
import { Plus, Trash } from "lucide-react";
|
import { Plus, Trash } from 'lucide-react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
steps: StepType[];
|
steps: StepType[];
|
||||||
@@ -10,13 +10,7 @@ type Props = {
|
|||||||
messages: CaseMessages;
|
messages: CaseMessages;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function StepsEditor({
|
export default function StepsEditor({ steps, onStepUpdate, onStepPlus, onStepDelete, messages }: Props) {
|
||||||
steps,
|
|
||||||
onStepUpdate,
|
|
||||||
onStepPlus,
|
|
||||||
onStepDelete,
|
|
||||||
messages,
|
|
||||||
}: Props) {
|
|
||||||
// sort steps by junction table's column
|
// sort steps by junction table's column
|
||||||
const sortedSteps = steps.slice().sort((a, b) => {
|
const sortedSteps = steps.slice().sort((a, b) => {
|
||||||
const stepNoA = a.caseSteps.stepNo;
|
const stepNoA = a.caseSteps.stepNo;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import CaseEditor from "./CaseEditor";
|
import CaseEditor from './CaseEditor';
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
export default function Page({
|
export default function Page({
|
||||||
params,
|
params,
|
||||||
@@ -11,53 +11,53 @@ export default function Page({
|
|||||||
locale: string;
|
locale: string;
|
||||||
};
|
};
|
||||||
}) {
|
}) {
|
||||||
const t = useTranslations("Case");
|
const t = useTranslations('Case');
|
||||||
const messages = {
|
const messages = {
|
||||||
backToCases: t("back_to_cases"),
|
backToCases: t('back_to_cases'),
|
||||||
updating: t("updating"),
|
updating: t('updating'),
|
||||||
update: t("update"),
|
update: t('update'),
|
||||||
basic: t("basic"),
|
basic: t('basic'),
|
||||||
title: t("title"),
|
title: t('title'),
|
||||||
pleaseEnterTitle: t("please_enter_title"),
|
pleaseEnterTitle: t('please_enter_title'),
|
||||||
description: t("description"),
|
description: t('description'),
|
||||||
testCaseDescription: t("test_case_description"),
|
testCaseDescription: t('test_case_description'),
|
||||||
priority: t("priority"),
|
priority: t('priority'),
|
||||||
critical: t("critical"),
|
critical: t('critical'),
|
||||||
high: t("high"),
|
high: t('high'),
|
||||||
medium: t("medium"),
|
medium: t('medium'),
|
||||||
low: t("low"),
|
low: t('low'),
|
||||||
type: t("type"),
|
type: t('type'),
|
||||||
other: t("other"),
|
other: t('other'),
|
||||||
security: t("security"),
|
security: t('security'),
|
||||||
performance: t("performance"),
|
performance: t('performance'),
|
||||||
accessibility: t("accessibility"),
|
accessibility: t('accessibility'),
|
||||||
functional: t("functional"),
|
functional: t('functional'),
|
||||||
acceptance: t("acceptance"),
|
acceptance: t('acceptance'),
|
||||||
usability: t("usability"),
|
usability: t('usability'),
|
||||||
smokeSanity: t("smoke_sanity"),
|
smokeSanity: t('smoke_sanity'),
|
||||||
compatibility: t("compatibility"),
|
compatibility: t('compatibility'),
|
||||||
destructive: t("destructive"),
|
destructive: t('destructive'),
|
||||||
regression: t("regression"),
|
regression: t('regression'),
|
||||||
automated: t("automated"),
|
automated: t('automated'),
|
||||||
manual: t("manual"),
|
manual: t('manual'),
|
||||||
template: t("template"),
|
template: t('template'),
|
||||||
testDetail: t("test_detail"),
|
testDetail: t('test_detail'),
|
||||||
preconditions: t("preconditions"),
|
preconditions: t('preconditions'),
|
||||||
step: t("step"),
|
step: t('step'),
|
||||||
text: t("text"),
|
text: t('text'),
|
||||||
steps: t("steps"),
|
steps: t('steps'),
|
||||||
newStep: t("new_step"),
|
newStep: t('new_step'),
|
||||||
detailsOfTheStep: t("details_of_the_step"),
|
detailsOfTheStep: t('details_of_the_step'),
|
||||||
expectedResult: t("expected_result"),
|
expectedResult: t('expected_result'),
|
||||||
deleteThisStep: t("delete_this_step"),
|
deleteThisStep: t('delete_this_step'),
|
||||||
insertStep: t("insert_step"),
|
insertStep: t('insert_step'),
|
||||||
attachments: t("attachments"),
|
attachments: t('attachments'),
|
||||||
delete: t("delete"),
|
delete: t('delete'),
|
||||||
download: t("download"),
|
download: t('download'),
|
||||||
deleteFile: t("delete_file"),
|
deleteFile: t('delete_file'),
|
||||||
clickToUpload: t("click_to_upload"),
|
clickToUpload: t('click_to_upload'),
|
||||||
orDragAndDrop: t("or_drag_and_drop"),
|
orDragAndDrop: t('or_drag_and_drop'),
|
||||||
maxFileSize: t("max_file_size"),
|
maxFileSize: t('max_file_size'),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,37 +1,28 @@
|
|||||||
import CasesPane from "./CasesPane";
|
import CasesPane from './CasesPane';
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
export default function Page({
|
export default function Page({ params }: { params: { projectId: string; folderId: string; locale: string } }) {
|
||||||
params,
|
const t = useTranslations('Cases');
|
||||||
}: {
|
|
||||||
params: { projectId: string; folderId: string; locale: string };
|
|
||||||
}) {
|
|
||||||
const t = useTranslations("Cases");
|
|
||||||
const messages = {
|
const messages = {
|
||||||
testCaseList: t("test_case_list"),
|
testCaseList: t('test_case_list'),
|
||||||
id: t("id"),
|
id: t('id'),
|
||||||
title: t("title"),
|
title: t('title'),
|
||||||
priority: t("priority"),
|
priority: t('priority'),
|
||||||
actions: t("actions"),
|
actions: t('actions'),
|
||||||
deleteCase: t("delete_case"),
|
deleteCase: t('delete_case'),
|
||||||
delete: t("delete"),
|
delete: t('delete'),
|
||||||
newTestCase: t("new_test_case"),
|
newTestCase: t('new_test_case'),
|
||||||
status: t("status"),
|
status: t('status'),
|
||||||
critical: t("critical"),
|
critical: t('critical'),
|
||||||
high: t("high"),
|
high: t('high'),
|
||||||
medium: t("medium"),
|
medium: t('medium'),
|
||||||
low: t("low"),
|
low: t('low'),
|
||||||
noCasesFound: t("no_cases_found"),
|
noCasesFound: t('no_cases_found'),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CasesPane
|
<CasesPane projectId={params.projectId} folderId={params.folderId} locale={params.locale} messages={messages} />
|
||||||
projectId={params.projectId}
|
|
||||||
folderId={params.folderId}
|
|
||||||
locale={params.locale}
|
|
||||||
messages={messages}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import FoldersPane from "./FoldersPane";
|
import FoldersPane from './FoldersPane';
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
export default function FoldersLayout({
|
export default function FoldersLayout({
|
||||||
children,
|
children,
|
||||||
@@ -8,27 +8,23 @@ export default function FoldersLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
params: { projectId: string; locale: string };
|
params: { projectId: string; locale: string };
|
||||||
}) {
|
}) {
|
||||||
const t = useTranslations("Folders");
|
const t = useTranslations('Folders');
|
||||||
const messages = {
|
const messages = {
|
||||||
folder: t("folder"),
|
folder: t('folder'),
|
||||||
newFolder: t("new_folder"),
|
newFolder: t('new_folder'),
|
||||||
editFolder: t("edit_folder"),
|
editFolder: t('edit_folder'),
|
||||||
deleteFolder: t("delete_folder"),
|
deleteFolder: t('delete_folder'),
|
||||||
folderName: t("folder_name"),
|
folderName: t('folder_name'),
|
||||||
folderDetail: t("folder_detail"),
|
folderDetail: t('folder_detail'),
|
||||||
close: t("close"),
|
close: t('close'),
|
||||||
create: t("create"),
|
create: t('create'),
|
||||||
update: t("update"),
|
update: t('update'),
|
||||||
pleaseEnter: t("please_enter"),
|
pleaseEnter: t('please_enter'),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full">
|
<div className="flex w-full">
|
||||||
<FoldersPane
|
<FoldersPane projectId={params.projectId} messages={messages} locale={params.locale} />
|
||||||
projectId={params.projectId}
|
|
||||||
messages={messages}
|
|
||||||
locale={params.locale}
|
|
||||||
/>
|
|
||||||
<div className="flex-grow w-full">{children}</div>
|
<div className="flex-grow w-full">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return <>This is folders page.</>;
|
||||||
<>
|
}
|
||||||
This is folders page.
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React from "react";
|
import React from 'react';
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from 'react';
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from 'next/dynamic';
|
||||||
import { priorities } from "@/config/selection";
|
import { priorities } from '@/config/selection';
|
||||||
import { CasePriorityCountType } from "@/types/case";
|
import { CasePriorityCountType } from '@/types/case';
|
||||||
import { HomeMessages } from "./page";
|
import { HomeMessages } from './page';
|
||||||
const Chart = dynamic(() => import("react-apexcharts"), { ssr: false });
|
const Chart = dynamic(() => import('react-apexcharts'), { ssr: false });
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
priorityCounts: CasePriorityCountType[];
|
priorityCounts: CasePriorityCountType[];
|
||||||
@@ -12,11 +12,7 @@ type Props = {
|
|||||||
theme: string;
|
theme: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TestPriorityDonutChart({
|
export default function TestPriorityDonutChart({ priorityCounts, messages, theme }: Props) {
|
||||||
priorityCounts,
|
|
||||||
messages,
|
|
||||||
theme,
|
|
||||||
}: Props) {
|
|
||||||
const [chartData, setChartData] = useState({
|
const [chartData, setChartData] = useState({
|
||||||
series: [],
|
series: [],
|
||||||
options: {
|
options: {
|
||||||
@@ -38,10 +34,10 @@ export default function TestPriorityDonutChart({
|
|||||||
const legend = {
|
const legend = {
|
||||||
labels: {
|
labels: {
|
||||||
colors: priorities.map((entry) => {
|
colors: priorities.map((entry) => {
|
||||||
if (theme === "light") {
|
if (theme === 'light') {
|
||||||
return "black";
|
return 'black';
|
||||||
} else {
|
} else {
|
||||||
return "white";
|
return 'white';
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@@ -57,13 +53,5 @@ export default function TestPriorityDonutChart({
|
|||||||
updateChartDate();
|
updateChartDate();
|
||||||
}, [priorityCounts, theme]);
|
}, [priorityCounts, theme]);
|
||||||
|
|
||||||
return (
|
return <Chart options={chartData.options} series={chartData.series} type="donut" width={'100%'} height={'100%'} />;
|
||||||
<Chart
|
|
||||||
options={chartData.options}
|
|
||||||
series={chartData.series}
|
|
||||||
type="donut"
|
|
||||||
width={"100%"}
|
|
||||||
height={"100%"}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React from "react";
|
import React from 'react';
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from 'react';
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from 'next/dynamic';
|
||||||
import { ProgressSeriesType } from "@/types/run";
|
import { ProgressSeriesType } from '@/types/run';
|
||||||
import { testRunCaseStatus } from "@/config/selection";
|
import { testRunCaseStatus } from '@/config/selection';
|
||||||
const Chart = dynamic(() => import("react-apexcharts"), { ssr: false });
|
const Chart = dynamic(() => import('react-apexcharts'), { ssr: false });
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
progressSeries: ProgressSeriesType[];
|
progressSeries: ProgressSeriesType[];
|
||||||
@@ -11,11 +11,7 @@ type Props = {
|
|||||||
theme: string;
|
theme: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TestProgressBarChart({
|
export default function TestProgressBarChart({ progressSeries, progressCategories, theme }: Props) {
|
||||||
progressSeries,
|
|
||||||
progressCategories,
|
|
||||||
theme,
|
|
||||||
}: Props) {
|
|
||||||
const [chartData, setChartData] = useState({
|
const [chartData, setChartData] = useState({
|
||||||
series: [],
|
series: [],
|
||||||
options: {
|
options: {
|
||||||
@@ -28,14 +24,14 @@ export default function TestProgressBarChart({
|
|||||||
const updateChartDate = () => {
|
const updateChartDate = () => {
|
||||||
if (progressSeries) {
|
if (progressSeries) {
|
||||||
const legendsLabelColors = testRunCaseStatus.map((itr) => {
|
const legendsLabelColors = testRunCaseStatus.map((itr) => {
|
||||||
if (theme === "light") {
|
if (theme === 'light') {
|
||||||
return "black";
|
return 'black';
|
||||||
} else {
|
} else {
|
||||||
return "white";
|
return 'white';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const xaxisLabelColor = theme === "light" ? "black" : "white";
|
const xaxisLabelColor = theme === 'light' ? 'black' : 'white';
|
||||||
|
|
||||||
setChartData({
|
setChartData({
|
||||||
series: progressSeries,
|
series: progressSeries,
|
||||||
@@ -47,7 +43,7 @@ export default function TestProgressBarChart({
|
|||||||
stacked: true,
|
stacked: true,
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
position: "right",
|
position: 'right',
|
||||||
labels: {
|
labels: {
|
||||||
colors: legendsLabelColors,
|
colors: legendsLabelColors,
|
||||||
},
|
},
|
||||||
@@ -56,7 +52,7 @@ export default function TestProgressBarChart({
|
|||||||
return itr.chartColor;
|
return itr.chartColor;
|
||||||
}),
|
}),
|
||||||
xaxis: {
|
xaxis: {
|
||||||
type: "datetime",
|
type: 'datetime',
|
||||||
categories: progressCategories,
|
categories: progressCategories,
|
||||||
labels: {
|
labels: {
|
||||||
style: {
|
style: {
|
||||||
@@ -75,13 +71,5 @@ export default function TestProgressBarChart({
|
|||||||
updateChartDate();
|
updateChartDate();
|
||||||
}, [progressSeries, progressCategories, theme]);
|
}, [progressSeries, progressCategories, theme]);
|
||||||
|
|
||||||
return (
|
return <Chart options={chartData.options} series={chartData.series} type="bar" width={'100%'} height={'100%'} />;
|
||||||
<Chart
|
|
||||||
options={chartData.options}
|
|
||||||
series={chartData.series}
|
|
||||||
type="bar"
|
|
||||||
width={"100%"}
|
|
||||||
height={"100%"}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React from "react";
|
import React from 'react';
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from 'react';
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from 'next/dynamic';
|
||||||
import { testTypes } from "@/config/selection";
|
import { testTypes } from '@/config/selection';
|
||||||
import { CaseTypeCountType } from "@/types/case";
|
import { CaseTypeCountType } from '@/types/case';
|
||||||
import { HomeMessages } from "./page";
|
import { HomeMessages } from './page';
|
||||||
const Chart = dynamic(() => import("react-apexcharts"), { ssr: false });
|
const Chart = dynamic(() => import('react-apexcharts'), { ssr: false });
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
typesCounts: CaseTypeCountType[];
|
typesCounts: CaseTypeCountType[];
|
||||||
@@ -12,11 +12,7 @@ type Props = {
|
|||||||
theme: string;
|
theme: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TestTypesDonutChart({
|
export default function TestTypesDonutChart({ typesCounts, messages, theme }: Props) {
|
||||||
typesCounts,
|
|
||||||
messages,
|
|
||||||
theme,
|
|
||||||
}: Props) {
|
|
||||||
const [chartData, setChartData] = useState({
|
const [chartData, setChartData] = useState({
|
||||||
series: [],
|
series: [],
|
||||||
options: {
|
options: {
|
||||||
@@ -38,10 +34,10 @@ export default function TestTypesDonutChart({
|
|||||||
const legend = {
|
const legend = {
|
||||||
labels: {
|
labels: {
|
||||||
colors: testTypes.map((entry) => {
|
colors: testTypes.map((entry) => {
|
||||||
if (theme === "light") {
|
if (theme === 'light') {
|
||||||
return "black";
|
return 'black';
|
||||||
} else {
|
} else {
|
||||||
return "white";
|
return 'white';
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@@ -57,13 +53,5 @@ export default function TestTypesDonutChart({
|
|||||||
updateChartDate();
|
updateChartDate();
|
||||||
}, [typesCounts, theme]);
|
}, [typesCounts, theme]);
|
||||||
|
|
||||||
return (
|
return <Chart options={chartData.options} series={chartData.series} type="donut" width={'100%'} height={'100%'} />;
|
||||||
<Chart
|
|
||||||
options={chartData.options}
|
|
||||||
series={chartData.series}
|
|
||||||
type="donut"
|
|
||||||
width={"100%"}
|
|
||||||
height={"100%"}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,26 @@
|
|||||||
"use client";
|
'use client';
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from 'react';
|
||||||
import { Divider } from "@nextui-org/react";
|
import { Divider } from '@nextui-org/react';
|
||||||
import { title, subtitle } from "@/components/primitives";
|
import { title, subtitle } from '@/components/primitives';
|
||||||
import { Card, CardBody, Chip } from "@nextui-org/react";
|
import { Card, CardBody, Chip } from '@nextui-org/react';
|
||||||
import { Folder, Clipboard, FlaskConical } from "lucide-react";
|
import { Folder, Clipboard, FlaskConical } from 'lucide-react';
|
||||||
import { CaseTypeCountType, CasePriorityCountType } from "@/types/case";
|
import { CaseTypeCountType, CasePriorityCountType } from '@/types/case';
|
||||||
import { ProgressSeriesType } from "@/types/run";
|
import { ProgressSeriesType } from '@/types/run';
|
||||||
import { HomeMessages } from "./page";
|
import { HomeMessages } from './page';
|
||||||
import {
|
import { aggregateBasicInfo, aggregateTestPriority, aggregateTestType, aggregateProgress } from './aggregate';
|
||||||
aggregateBasicInfo,
|
import TestTypesChart from './TestTypesDonutChart';
|
||||||
aggregateTestPriority,
|
import TestPriorityChart from './TestPriorityDonutChart';
|
||||||
aggregateTestType,
|
import TestProgressBarChart from './TestProgressColumnChart';
|
||||||
aggregateProgress,
|
import Config from '@/config/config';
|
||||||
} from "./aggregate";
|
import { useTheme } from 'next-themes';
|
||||||
import TestTypesChart from "./TestTypesDonutChart";
|
|
||||||
import TestPriorityChart from "./TestPriorityDonutChart";
|
|
||||||
import TestProgressBarChart from "./TestProgressColumnChart";
|
|
||||||
import Config from "@/config/config";
|
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
const apiServer = Config.apiServer;
|
const apiServer = Config.apiServer;
|
||||||
|
|
||||||
async function fetchProject(url) {
|
async function fetchProject(url) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: "GET",
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -36,7 +31,7 @@ async function fetchProject(url) {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Error fetching data:", error.message);
|
console.error('Error fetching data:', error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,8 +43,8 @@ type Props = {
|
|||||||
export function Home({ projectId, messages }: Props) {
|
export function Home({ projectId, messages }: Props) {
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
const [project, setProject] = useState({
|
const [project, setProject] = useState({
|
||||||
name: "",
|
name: '',
|
||||||
detail: "",
|
detail: '',
|
||||||
Folders: [{ Cases: [] }],
|
Folders: [{ Cases: [] }],
|
||||||
Runs: [{ RunCases: [] }],
|
Runs: [{ RunCases: [] }],
|
||||||
});
|
});
|
||||||
@@ -57,8 +52,7 @@ export function Home({ projectId, messages }: Props) {
|
|||||||
const [caseNum, setCaseNum] = useState(0);
|
const [caseNum, setCaseNum] = useState(0);
|
||||||
const [runNum, setRunNum] = useState(0);
|
const [runNum, setRunNum] = useState(0);
|
||||||
const [typesCounts, setTypesCounts] = useState<CaseTypeCountType[]>();
|
const [typesCounts, setTypesCounts] = useState<CaseTypeCountType[]>();
|
||||||
const [priorityCounts, setPriorityCounts] =
|
const [priorityCounts, setPriorityCounts] = useState<CasePriorityCountType[]>();
|
||||||
useState<CasePriorityCountType[]>();
|
|
||||||
const [progressCategories, setProgressCategories] = useState<string[]>();
|
const [progressCategories, setProgressCategories] = useState<string[]>();
|
||||||
const [progressSeries, setProgressSeries] = useState<ProgressSeriesType[]>();
|
const [progressSeries, setProgressSeries] = useState<ProgressSeriesType[]>();
|
||||||
const url = `${apiServer}/home/${projectId}`;
|
const url = `${apiServer}/home/${projectId}`;
|
||||||
@@ -69,7 +63,7 @@ export function Home({ projectId, messages }: Props) {
|
|||||||
const data = await fetchProject(url);
|
const data = await fetchProject(url);
|
||||||
setProject(data);
|
setProject(data);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Error in effect:", error.message);
|
console.error('Error in effect:', error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,68 +93,41 @@ export function Home({ projectId, messages }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto max-w-5xl pt-6 px-6 flex-grow">
|
<div className="container mx-auto max-w-5xl pt-6 px-6 flex-grow">
|
||||||
<h1 className={title({ size: "sm" })}>{project.name}</h1>
|
<h1 className={title({ size: 'sm' })}>{project.name}</h1>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<Chip
|
<Chip variant="flat" startContent={<Folder size={16} />} className="px-3">
|
||||||
variant="flat"
|
|
||||||
startContent={<Folder size={16} />}
|
|
||||||
className="px-3"
|
|
||||||
>
|
|
||||||
{folderNum} {messages.folders}
|
{folderNum} {messages.folders}
|
||||||
</Chip>
|
</Chip>
|
||||||
<Chip
|
<Chip variant="flat" startContent={<Clipboard size={16} />} className="px-3 ms-2">
|
||||||
variant="flat"
|
|
||||||
startContent={<Clipboard size={16} />}
|
|
||||||
className="px-3 ms-2"
|
|
||||||
>
|
|
||||||
{caseNum} {messages.testCases}
|
{caseNum} {messages.testCases}
|
||||||
</Chip>
|
</Chip>
|
||||||
<Chip
|
<Chip variant="flat" startContent={<FlaskConical size={16} />} className="px-3 ms-2">
|
||||||
variant="flat"
|
|
||||||
startContent={<FlaskConical size={16} />}
|
|
||||||
className="px-3 ms-2"
|
|
||||||
>
|
|
||||||
{runNum} {messages.testRuns}
|
{runNum} {messages.testRuns}
|
||||||
</Chip>
|
</Chip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{project.detail && (
|
{project.detail && (
|
||||||
<Card
|
<Card className="mt-3 bg-neutral-100 dark:bg-neutral-700 dark:text-white" shadow="none">
|
||||||
className="mt-3 bg-neutral-100 dark:bg-neutral-700 dark:text-white"
|
|
||||||
shadow="none"
|
|
||||||
>
|
|
||||||
<CardBody>{project.detail}</CardBody>
|
<CardBody>{project.detail}</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Divider className="my-8" />
|
<Divider className="my-8" />
|
||||||
<h2 className={subtitle()}>{messages.progress}</h2>
|
<h2 className={subtitle()}>{messages.progress}</h2>
|
||||||
<div style={{ height: "18rem" }}>
|
<div style={{ height: '18rem' }}>
|
||||||
<TestProgressBarChart
|
<TestProgressBarChart progressSeries={progressSeries} progressCategories={progressCategories} theme={theme} />
|
||||||
progressSeries={progressSeries}
|
|
||||||
progressCategories={progressCategories}
|
|
||||||
theme={theme}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Divider className="my-12" />
|
<Divider className="my-12" />
|
||||||
<h2 className={subtitle()}>{messages.testClassification}</h2>
|
<h2 className={subtitle()}>{messages.testClassification}</h2>
|
||||||
<div className="flex pb-20">
|
<div className="flex pb-20">
|
||||||
<div style={{ width: "32rem", height: "18rem" }}>
|
<div style={{ width: '32rem', height: '18rem' }}>
|
||||||
<h3>{messages.byType}</h3>
|
<h3>{messages.byType}</h3>
|
||||||
<TestTypesChart
|
<TestTypesChart typesCounts={typesCounts} messages={messages} theme={theme} />
|
||||||
typesCounts={typesCounts}
|
|
||||||
messages={messages}
|
|
||||||
theme={theme}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div style={{ width: "30rem", height: "18rem" }}>
|
<div style={{ width: '30rem', height: '18rem' }}>
|
||||||
<h3>{messages.byPriority}</h3>
|
<h3>{messages.byPriority}</h3>
|
||||||
<TestPriorityChart
|
<TestPriorityChart priorityCounts={priorityCounts} messages={messages} theme={theme} />
|
||||||
priorityCounts={priorityCounts}
|
|
||||||
messages={messages}
|
|
||||||
theme={theme}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Home } from "./home";
|
import { Home } from './home';
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
export type HomeMessages = {
|
export type HomeMessages = {
|
||||||
folders: string;
|
folders: string;
|
||||||
@@ -35,37 +35,37 @@ export type HomeMessages = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function Page({ params }: { params: { projectId: string } }) {
|
export default function Page({ params }: { params: { projectId: string } }) {
|
||||||
const t = useTranslations("Home");
|
const t = useTranslations('Home');
|
||||||
const messages = {
|
const messages = {
|
||||||
folders: t("Folders"),
|
folders: t('Folders'),
|
||||||
testCases: t("test_cases"),
|
testCases: t('test_cases'),
|
||||||
testRuns: t("test_runs"),
|
testRuns: t('test_runs'),
|
||||||
progress: t("progress"),
|
progress: t('progress'),
|
||||||
untested: t("untested"),
|
untested: t('untested'),
|
||||||
passed: t("passed"),
|
passed: t('passed'),
|
||||||
failed: t("failed"),
|
failed: t('failed'),
|
||||||
retest: t("retest"),
|
retest: t('retest'),
|
||||||
skipped: t("skipped"),
|
skipped: t('skipped'),
|
||||||
testClassification: t("test_classification"),
|
testClassification: t('test_classification'),
|
||||||
byType: t("by_type"),
|
byType: t('by_type'),
|
||||||
byPriority: t("by_priority"),
|
byPriority: t('by_priority'),
|
||||||
other: t("other"),
|
other: t('other'),
|
||||||
security: t("security"),
|
security: t('security'),
|
||||||
performance: t("performance"),
|
performance: t('performance'),
|
||||||
accessibility: t("accessibility"),
|
accessibility: t('accessibility'),
|
||||||
functional: t("functional"),
|
functional: t('functional'),
|
||||||
acceptance: t("acceptance"),
|
acceptance: t('acceptance'),
|
||||||
usability: t("usability"),
|
usability: t('usability'),
|
||||||
smokeSanity: t("smoke_sanity"),
|
smokeSanity: t('smoke_sanity'),
|
||||||
compatibility: t("compatibility"),
|
compatibility: t('compatibility'),
|
||||||
destructive: t("destructive"),
|
destructive: t('destructive'),
|
||||||
regression: t("regression"),
|
regression: t('regression'),
|
||||||
automated: t("automated"),
|
automated: t('automated'),
|
||||||
manual: t("manual"),
|
manual: t('manual'),
|
||||||
critical: t("critical"),
|
critical: t('critical'),
|
||||||
high: t("high"),
|
high: t('high'),
|
||||||
medium: t("medium"),
|
medium: t('medium'),
|
||||||
low: t("low"),
|
low: t('low'),
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Sidebar from "./Sidebar";
|
import Sidebar from './Sidebar';
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
export default function SidebarLayout({
|
export default function SidebarLayout({
|
||||||
children,
|
children,
|
||||||
@@ -8,11 +8,11 @@ export default function SidebarLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
params: { locale: string };
|
params: { locale: string };
|
||||||
}) {
|
}) {
|
||||||
const t = useTranslations("Project");
|
const t = useTranslations('Project');
|
||||||
const messages = {
|
const messages = {
|
||||||
home: t("home"),
|
home: t('home'),
|
||||||
testCases: t("test_cases"),
|
testCases: t('test_cases'),
|
||||||
testRuns: t("test_runs"),
|
testRuns: t('test_runs'),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"use client";
|
'use client';
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from 'react';
|
||||||
import { Button } from "@nextui-org/react";
|
import { Button } from '@nextui-org/react';
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from 'lucide-react';
|
||||||
import RunsTable from "./RunsTable";
|
import RunsTable from './RunsTable';
|
||||||
import { fetchRuns, createRun, deleteRun } from "./runsControl";
|
import { fetchRuns, createRun, deleteRun } from './runsControl';
|
||||||
import { RunsMessages } from "@/types/run";
|
import { RunsMessages } from '@/types/run';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@@ -21,7 +21,7 @@ export default function RunsPage({ projectId, locale, messages }: Props) {
|
|||||||
const data = await fetchRuns(projectId);
|
const data = await fetchRuns(projectId);
|
||||||
setRuns(data);
|
setRuns(data);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Error in effect:", error.message);
|
console.error('Error in effect:', error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ export default function RunsPage({ projectId, locale, messages }: Props) {
|
|||||||
updateRuns.push(newRun);
|
updateRuns.push(newRun);
|
||||||
setRuns(updateRuns);
|
setRuns(updateRuns);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Error deleting run:", error);
|
console.error('Error deleting run:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ export default function RunsPage({ projectId, locale, messages }: Props) {
|
|||||||
const data = await fetchRuns(projectId);
|
const data = await fetchRuns(projectId);
|
||||||
setRuns(data);
|
setRuns(data);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Error deleting run:", error);
|
console.error('Error deleting run:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -54,24 +54,13 @@ export default function RunsPage({ projectId, locale, messages }: Props) {
|
|||||||
<div className="w-full p-3 flex items-center justify-between">
|
<div className="w-full p-3 flex items-center justify-between">
|
||||||
<h3 className="font-bold">{messages.runList}</h3>
|
<h3 className="font-bold">{messages.runList}</h3>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button startContent={<Plus size={16} />} size="sm" color="primary" onClick={onCreateClick}>
|
||||||
startContent={<Plus size={16} />}
|
|
||||||
size="sm"
|
|
||||||
color="primary"
|
|
||||||
onClick={onCreateClick}
|
|
||||||
>
|
|
||||||
{messages.newRun}
|
{messages.newRun}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RunsTable
|
<RunsTable projectId={projectId} runs={runs} onDeleteRun={onDeleteClick} messages={messages} locale={locale} />
|
||||||
projectId={projectId}
|
|
||||||
runs={runs}
|
|
||||||
onDeleteRun={onDeleteClick}
|
|
||||||
messages={messages}
|
|
||||||
locale={locale}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useMemo, useCallback } from "react";
|
import { useState, useMemo, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
@@ -12,11 +12,11 @@ import {
|
|||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
SortDescriptor,
|
SortDescriptor,
|
||||||
} from "@nextui-org/react";
|
} from '@nextui-org/react';
|
||||||
import { Link, NextUiLinkClasses } from "@/src/navigation";
|
import { Link, NextUiLinkClasses } from '@/src/navigation';
|
||||||
import { MoreVertical } from "lucide-react";
|
import { MoreVertical } from 'lucide-react';
|
||||||
import { RunsMessages, RunType } from "@/types/run";
|
import { RunsMessages, RunType } from '@/types/run';
|
||||||
import dayjs from "dayjs";
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@@ -26,24 +26,18 @@ type Props = {
|
|||||||
locale: string;
|
locale: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RunsTable({
|
export default function RunsTable({ projectId, runs, onDeleteRun, messages, locale }: Props) {
|
||||||
projectId,
|
|
||||||
runs,
|
|
||||||
onDeleteRun,
|
|
||||||
messages,
|
|
||||||
locale,
|
|
||||||
}: Props) {
|
|
||||||
const headerColumns = [
|
const headerColumns = [
|
||||||
{ name: messages.id, uid: "id", sortable: true },
|
{ name: messages.id, uid: 'id', sortable: true },
|
||||||
{ name: messages.name, uid: "name", sortable: true },
|
{ name: messages.name, uid: 'name', sortable: true },
|
||||||
{ name: messages.description, uid: "description", sortable: true },
|
{ name: messages.description, uid: 'description', sortable: true },
|
||||||
{ name: messages.lastUpdate, uid: "updatedAt", sortable: true },
|
{ name: messages.lastUpdate, uid: 'updatedAt', sortable: true },
|
||||||
{ name: messages.actions, uid: "actions" },
|
{ name: messages.actions, uid: 'actions' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
|
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
|
||||||
column: "id",
|
column: 'id',
|
||||||
direction: "ascending",
|
direction: 'ascending',
|
||||||
});
|
});
|
||||||
|
|
||||||
const sortedItems = useMemo(() => {
|
const sortedItems = useMemo(() => {
|
||||||
@@ -52,31 +46,27 @@ export default function RunsTable({
|
|||||||
const second = b[sortDescriptor.column as keyof RunType] as number;
|
const second = b[sortDescriptor.column as keyof RunType] as number;
|
||||||
const cmp = first < second ? -1 : first > second ? 1 : 0;
|
const cmp = first < second ? -1 : first > second ? 1 : 0;
|
||||||
|
|
||||||
return sortDescriptor.direction === "descending" ? -cmp : cmp;
|
return sortDescriptor.direction === 'descending' ? -cmp : cmp;
|
||||||
});
|
});
|
||||||
}, [sortDescriptor, runs]);
|
}, [sortDescriptor, runs]);
|
||||||
|
|
||||||
const truncateText = (text: string, maxLength: number) => {
|
const truncateText = (text: string, maxLength: number) => {
|
||||||
return text.length > maxLength ? text.slice(0, maxLength) + "..." : text;
|
return text.length > maxLength ? text.slice(0, maxLength) + '...' : text;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderCell = useCallback((run: RunType, columnKey: Key) => {
|
const renderCell = useCallback((run: RunType, columnKey: Key) => {
|
||||||
const cellValue = run[columnKey as keyof RunType];
|
const cellValue = run[columnKey as keyof RunType];
|
||||||
|
|
||||||
switch (columnKey) {
|
switch (columnKey) {
|
||||||
case "id":
|
case 'id':
|
||||||
return <span>{cellValue}</span>;
|
return <span>{cellValue}</span>;
|
||||||
case "name":
|
case 'name':
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link href={`/projects/${projectId}/runs/${run.id}`} locale={locale} className={NextUiLinkClasses}>
|
||||||
href={`/projects/${projectId}/runs/${run.id}`}
|
|
||||||
locale={locale}
|
|
||||||
className={NextUiLinkClasses}
|
|
||||||
>
|
|
||||||
{cellValue}
|
{cellValue}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
case "detail":
|
case 'detail':
|
||||||
const maxLength = 20;
|
const maxLength = 20;
|
||||||
const truncatedValue = truncateText(cellValue, maxLength);
|
const truncatedValue = truncateText(cellValue, maxLength);
|
||||||
return (
|
return (
|
||||||
@@ -84,9 +74,9 @@ export default function RunsTable({
|
|||||||
<div>{truncatedValue}</div>
|
<div>{truncatedValue}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case "updatedAt":
|
case 'updatedAt':
|
||||||
return <span>{dayjs(cellValue).format("YYYY/MM/DD HH:mm")}</span>;
|
return <span>{dayjs(cellValue).format('YYYY/MM/DD HH:mm')}</span>;
|
||||||
case "actions":
|
case 'actions':
|
||||||
return (
|
return (
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
@@ -95,10 +85,7 @@ export default function RunsTable({
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownTrigger>
|
</DropdownTrigger>
|
||||||
<DropdownMenu aria-label="run actions">
|
<DropdownMenu aria-label="run actions">
|
||||||
<DropdownItem
|
<DropdownItem className="text-danger" onClick={() => onDeleteRun(run.id)}>
|
||||||
className="text-danger"
|
|
||||||
onClick={() => onDeleteRun(run.id)}
|
|
||||||
>
|
|
||||||
{messages.deleteRun}
|
{messages.deleteRun}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
@@ -111,18 +98,18 @@ export default function RunsTable({
|
|||||||
|
|
||||||
const classNames = useMemo(
|
const classNames = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
wrapper: ["max-w-3xl"],
|
wrapper: ['max-w-3xl'],
|
||||||
th: ["bg-transparent", "text-default-500", "border-b", "border-divider"],
|
th: ['bg-transparent', 'text-default-500', 'border-b', 'border-divider'],
|
||||||
td: [
|
td: [
|
||||||
// changing the rows border radius
|
// changing the rows border radius
|
||||||
// first
|
// first
|
||||||
"group-data-[first=true]:first:before:rounded-none",
|
'group-data-[first=true]:first:before:rounded-none',
|
||||||
"group-data-[first=true]:last:before:rounded-none",
|
'group-data-[first=true]:last:before:rounded-none',
|
||||||
// middle
|
// middle
|
||||||
"group-data-[middle=true]:before:rounded-none",
|
'group-data-[middle=true]:before:rounded-none',
|
||||||
// last
|
// last
|
||||||
"group-data-[last=true]:first:before:rounded-none",
|
'group-data-[last=true]:first:before:rounded-none',
|
||||||
"group-data-[last=true]:last:before:rounded-none",
|
'group-data-[last=true]:last:before:rounded-none',
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
[]
|
[]
|
||||||
@@ -141,7 +128,7 @@ export default function RunsTable({
|
|||||||
{(column) => (
|
{(column) => (
|
||||||
<TableColumn
|
<TableColumn
|
||||||
key={column.uid}
|
key={column.uid}
|
||||||
align={column.uid === "actions" ? "center" : "start"}
|
align={column.uid === 'actions' ? 'center' : 'start'}
|
||||||
allowsSorting={column.sortable}
|
allowsSorting={column.sortable}
|
||||||
>
|
>
|
||||||
{column.name}
|
{column.name}
|
||||||
@@ -150,11 +137,7 @@ export default function RunsTable({
|
|||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody emptyContent={messages.noRunsFound} items={sortedItems}>
|
<TableBody emptyContent={messages.noRunsFound} items={sortedItems}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<TableRow key={item.id}>
|
<TableRow key={item.id}>{(columnKey) => <TableCell>{renderCell(item, columnKey)}</TableCell>}</TableRow>
|
||||||
{(columnKey) => (
|
|
||||||
<TableCell>{renderCell(item, columnKey)}</TableCell>
|
|
||||||
)}
|
|
||||||
</TableRow>
|
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
'use client';
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from 'react';
|
||||||
import { useRouter } from "@/src/navigation";
|
import { useRouter } from '@/src/navigation';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Input,
|
Input,
|
||||||
@@ -17,28 +17,14 @@ import {
|
|||||||
Dropdown,
|
Dropdown,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
} from "@nextui-org/react";
|
} from '@nextui-org/react';
|
||||||
import {
|
import { Save, ArrowLeft, Folder, ChevronDown, CopyPlus, CopyMinus, RotateCw } from 'lucide-react';
|
||||||
Save,
|
import RunProgressChart from './RunPregressDonutChart';
|
||||||
ArrowLeft,
|
import TestCaseSelector from './TestCaseSelector';
|
||||||
Folder,
|
import { testRunStatus } from '@/config/selection';
|
||||||
ChevronDown,
|
import { RunType, RunCaseType, RunCaseInfoType, RunStatusCountType, RunMessages } from '@/types/run';
|
||||||
CopyPlus,
|
import { CaseType } from '@/types/case';
|
||||||
CopyMinus,
|
import { FolderType } from '@/types/folder';
|
||||||
RotateCw,
|
|
||||||
} from "lucide-react";
|
|
||||||
import RunProgressChart from "./RunPregressDonutChart";
|
|
||||||
import TestCaseSelector from "./TestCaseSelector";
|
|
||||||
import { testRunStatus } from "@/config/selection";
|
|
||||||
import {
|
|
||||||
RunType,
|
|
||||||
RunCaseType,
|
|
||||||
RunCaseInfoType,
|
|
||||||
RunStatusCountType,
|
|
||||||
RunMessages,
|
|
||||||
} from "@/types/run";
|
|
||||||
import { CaseType } from "@/types/case";
|
|
||||||
import { FolderType } from "@/types/folder";
|
|
||||||
import {
|
import {
|
||||||
fetchRun,
|
fetchRun,
|
||||||
updateRun,
|
updateRun,
|
||||||
@@ -48,16 +34,16 @@ import {
|
|||||||
bulkCreateRunCases,
|
bulkCreateRunCases,
|
||||||
deleteRunCase,
|
deleteRunCase,
|
||||||
bulkDeleteRunCases,
|
bulkDeleteRunCases,
|
||||||
} from "../runsControl";
|
} from '../runsControl';
|
||||||
import { fetchFolders } from "../../folders/foldersControl";
|
import { fetchFolders } from '../../folders/foldersControl';
|
||||||
import { fetchCases } from "../../folders/[folderId]/cases/caseControl";
|
import { fetchCases } from '../../folders/[folderId]/cases/caseControl';
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from 'next-themes';
|
||||||
|
|
||||||
const defaultTestRun = {
|
const defaultTestRun = {
|
||||||
id: 0,
|
id: 0,
|
||||||
name: "",
|
name: '',
|
||||||
configurations: 0,
|
configurations: 0,
|
||||||
description: "",
|
description: '',
|
||||||
state: 0,
|
state: 0,
|
||||||
projectId: 0,
|
projectId: 0,
|
||||||
};
|
};
|
||||||
@@ -69,18 +55,12 @@ type Props = {
|
|||||||
locale: string;
|
locale: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RunEditor({
|
export default function RunEditor({ projectId, runId, messages, locale }: Props) {
|
||||||
projectId,
|
|
||||||
runId,
|
|
||||||
messages,
|
|
||||||
locale,
|
|
||||||
}: Props) {
|
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
const [testRun, setTestRun] = useState<RunType>(defaultTestRun);
|
const [testRun, setTestRun] = useState<RunType>(defaultTestRun);
|
||||||
const [folders, setFolders] = useState([]);
|
const [folders, setFolders] = useState([]);
|
||||||
const [runCases, setRunCases] = useState<RunCaseType[]>([]);
|
const [runCases, setRunCases] = useState<RunCaseType[]>([]);
|
||||||
const [runStatusCounts, setRunStatusCounts] =
|
const [runStatusCounts, setRunStatusCounts] = useState<RunStatusCountType[]>();
|
||||||
useState<RunStatusCountType[]>();
|
|
||||||
const [selectedKeys, setSelectedKeys] = useState<Selection>(new Set([]));
|
const [selectedKeys, setSelectedKeys] = useState<Selection>(new Set([]));
|
||||||
const [selectedFolder, setSelectedFolder] = useState<FolderType>({});
|
const [selectedFolder, setSelectedFolder] = useState<FolderType>({});
|
||||||
const [testcases, setTestCases] = useState<CaseType[]>([]);
|
const [testcases, setTestCases] = useState<CaseType[]>([]);
|
||||||
@@ -102,7 +82,7 @@ export default function RunEditor({
|
|||||||
setFolders(foldersData);
|
setFolders(foldersData);
|
||||||
setSelectedFolder(foldersData[0]);
|
setSelectedFolder(foldersData[0]);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Error in effect:", error.message);
|
console.error('Error in effect:', error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,9 +100,7 @@ export default function RunEditor({
|
|||||||
// Check if each testCase has an association with testRun
|
// Check if each testCase has an association with testRun
|
||||||
// and add "isIncluded" property
|
// and add "isIncluded" property
|
||||||
const updatedTestCasesData = testCasesData.map((testCase) => {
|
const updatedTestCasesData = testCasesData.map((testCase) => {
|
||||||
const runCase = latestRunCases.find(
|
const runCase = latestRunCases.find((runCase) => runCase.caseId === testCase.id);
|
||||||
(runCase) => runCase.caseId === testCase.id
|
|
||||||
);
|
|
||||||
|
|
||||||
const isIncluded = runCase ? true : false;
|
const isIncluded = runCase ? true : false;
|
||||||
const runStatus = runCase ? runCase.status : 0;
|
const runStatus = runCase ? runCase.status : 0;
|
||||||
@@ -135,7 +113,7 @@ export default function RunEditor({
|
|||||||
|
|
||||||
setTestCases(updatedTestCasesData);
|
setTestCases(updatedTestCasesData);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Error fetching cases data:", error.message);
|
console.error('Error fetching cases data:', error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,10 +133,7 @@ export default function RunEditor({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleIncludeExcludeCase = async (
|
const handleIncludeExcludeCase = async (isInclude: boolean, clickedTestCaseId: number) => {
|
||||||
isInclude: boolean,
|
|
||||||
clickedTestCaseId: number
|
|
||||||
) => {
|
|
||||||
if (isInclude) {
|
if (isInclude) {
|
||||||
const createdRunCase = await createRunCase(runId, clickedTestCaseId);
|
const createdRunCase = await createRunCase(runId, clickedTestCaseId);
|
||||||
setRunCases((prevRunCases) => {
|
setRunCases((prevRunCases) => {
|
||||||
@@ -167,9 +142,7 @@ export default function RunEditor({
|
|||||||
} else {
|
} else {
|
||||||
await deleteRunCase(runId, clickedTestCaseId);
|
await deleteRunCase(runId, clickedTestCaseId);
|
||||||
setRunCases((prevRunCases) => {
|
setRunCases((prevRunCases) => {
|
||||||
return prevRunCases.filter(
|
return prevRunCases.filter((runCase) => runCase.caseId !== clickedTestCaseId);
|
||||||
(runCase) => runCase.caseId !== clickedTestCaseId
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +158,7 @@ export default function RunEditor({
|
|||||||
|
|
||||||
const handleBulkIncludeExcludeCases = async (isInclude: boolean) => {
|
const handleBulkIncludeExcludeCases = async (isInclude: boolean) => {
|
||||||
let keys: number[] = [];
|
let keys: number[] = [];
|
||||||
if (selectedKeys === "all") {
|
if (selectedKeys === 'all') {
|
||||||
keys = testcases.map((item) => item.id);
|
keys = testcases.map((item) => item.id);
|
||||||
} else {
|
} else {
|
||||||
keys = Array.from(selectedKeys).map(Number);
|
keys = Array.from(selectedKeys).map(Number);
|
||||||
@@ -218,7 +191,7 @@ export default function RunEditor({
|
|||||||
setSelectedKeys(new Set([]));
|
setSelectedKeys(new Set([]));
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseClass = "";
|
const baseClass = '';
|
||||||
const selectedClass = `${baseClass} bg-neutral-200 dark:bg-neutral-700`;
|
const selectedClass = `${baseClass} bg-neutral-200 dark:bg-neutral-700`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -230,9 +203,7 @@ export default function RunEditor({
|
|||||||
isIconOnly
|
isIconOnly
|
||||||
size="sm"
|
size="sm"
|
||||||
className="rounded-full bg-neutral-50 dark:bg-neutral-600"
|
className="rounded-full bg-neutral-50 dark:bg-neutral-600"
|
||||||
onPress={() =>
|
onPress={() => router.push(`/projects/${projectId}/runs`, { locale: locale })}
|
||||||
router.push(`/projects/${projectId}/runs`, { locale: locale })
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<ArrowLeft size={16} />
|
<ArrowLeft size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -272,11 +243,7 @@ export default function RunEditor({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RunProgressChart
|
<RunProgressChart statusCounts={runStatusCounts} messages={messages} theme={theme} />
|
||||||
statusCounts={runStatusCounts}
|
|
||||||
messages={messages}
|
|
||||||
theme={theme}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-grow">
|
<div className="flex-grow">
|
||||||
@@ -287,7 +254,7 @@ export default function RunEditor({
|
|||||||
label={messages.title}
|
label={messages.title}
|
||||||
value={testRun.name}
|
value={testRun.name}
|
||||||
isInvalid={isNameInvalid}
|
isInvalid={isNameInvalid}
|
||||||
errorMessage={isNameInvalid ? messages.pleaseEnter : ""}
|
errorMessage={isNameInvalid ? messages.pleaseEnter : ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setTestRun({ ...testRun, name: e.target.value });
|
setTestRun({ ...testRun, name: e.target.value });
|
||||||
}}
|
}}
|
||||||
@@ -312,9 +279,7 @@ export default function RunEditor({
|
|||||||
selectedKeys={[testRunStatus[testRun.state].uid]}
|
selectedKeys={[testRunStatus[testRun.state].uid]}
|
||||||
onSelectionChange={(e) => {
|
onSelectionChange={(e) => {
|
||||||
const selectedUid = e.anchorKey;
|
const selectedUid = e.anchorKey;
|
||||||
const index = testRunStatus.findIndex(
|
const index = testRunStatus.findIndex((template) => template.uid === selectedUid);
|
||||||
(template) => template.uid === selectedUid
|
|
||||||
);
|
|
||||||
setTestRun({ ...testRun, state: index });
|
setTestRun({ ...testRun, state: index });
|
||||||
}}
|
}}
|
||||||
label={messages.status}
|
label={messages.status}
|
||||||
@@ -334,14 +299,10 @@ export default function RunEditor({
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h6 className="h-8 font-bold">{messages.selectTestCase}</h6>
|
<h6 className="h-8 font-bold">{messages.selectTestCase}</h6>
|
||||||
<div>
|
<div>
|
||||||
{(selectedKeys.size > 0 || selectedKeys === "all") && (
|
{(selectedKeys.size > 0 || selectedKeys === 'all') && (
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
<Button
|
<Button size="sm" color="primary" endContent={<ChevronDown size={16} />}>
|
||||||
size="sm"
|
|
||||||
color="primary"
|
|
||||||
endContent={<ChevronDown size={16} />}
|
|
||||||
>
|
|
||||||
{messages.testCaseSelection}
|
{messages.testCaseSelection}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownTrigger>
|
</DropdownTrigger>
|
||||||
@@ -371,14 +332,8 @@ export default function RunEditor({
|
|||||||
<ListboxItem
|
<ListboxItem
|
||||||
key={index}
|
key={index}
|
||||||
onClick={() => setSelectedFolder(folder)}
|
onClick={() => setSelectedFolder(folder)}
|
||||||
startContent={
|
startContent={<Folder size={20} color="#F7C24E" fill="#F7C24E" />}
|
||||||
<Folder size={20} color="#F7C24E" fill="#F7C24E" />
|
className={selectedFolder && folder.id === selectedFolder.id ? selectedClass : baseClass}
|
||||||
}
|
|
||||||
className={
|
|
||||||
selectedFolder && folder.id === selectedFolder.id
|
|
||||||
? selectedClass
|
|
||||||
: baseClass
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{folder.name}
|
{folder.name}
|
||||||
</ListboxItem>
|
</ListboxItem>
|
||||||
@@ -391,12 +346,8 @@ export default function RunEditor({
|
|||||||
selectedKeys={selectedKeys}
|
selectedKeys={selectedKeys}
|
||||||
onSelectionChange={setSelectedKeys}
|
onSelectionChange={setSelectedKeys}
|
||||||
onStatusChange={handleChangeStatus}
|
onStatusChange={handleChangeStatus}
|
||||||
onIncludeCase={(includeTestId) =>
|
onIncludeCase={(includeTestId) => handleIncludeExcludeCase(true, includeTestId)}
|
||||||
handleIncludeExcludeCase(true, includeTestId)
|
onExcludeCase={(excludeCaseId) => handleIncludeExcludeCase(false, excludeCaseId)}
|
||||||
}
|
|
||||||
onExcludeCase={(excludeCaseId) =>
|
|
||||||
handleIncludeExcludeCase(false, excludeCaseId)
|
|
||||||
}
|
|
||||||
messages={messages}
|
messages={messages}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React from "react";
|
import React from 'react';
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from 'react';
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from 'next/dynamic';
|
||||||
import { testRunCaseStatus } from "@/config/selection";
|
import { testRunCaseStatus } from '@/config/selection';
|
||||||
import { RunStatusCountType, RunMessages } from "@/types/run";
|
import { RunStatusCountType, RunMessages } from '@/types/run';
|
||||||
const Chart = dynamic(() => import("react-apexcharts"), { ssr: false });
|
const Chart = dynamic(() => import('react-apexcharts'), { ssr: false });
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
statusCounts: RunStatusCountType[];
|
statusCounts: RunStatusCountType[];
|
||||||
@@ -11,11 +11,7 @@ type Props = {
|
|||||||
theme: string;
|
theme: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RunProgressDounut({
|
export default function RunProgressDounut({ statusCounts, messages, theme }: Props) {
|
||||||
statusCounts,
|
|
||||||
messages,
|
|
||||||
theme,
|
|
||||||
}: Props) {
|
|
||||||
const [chartData, setChartData] = useState({
|
const [chartData, setChartData] = useState({
|
||||||
series: [],
|
series: [],
|
||||||
options: {
|
options: {
|
||||||
@@ -37,10 +33,10 @@ export default function RunProgressDounut({
|
|||||||
const legend = {
|
const legend = {
|
||||||
labels: {
|
labels: {
|
||||||
colors: testRunCaseStatus.map((entry) => {
|
colors: testRunCaseStatus.map((entry) => {
|
||||||
if (theme === "light") {
|
if (theme === 'light') {
|
||||||
return "black";
|
return 'black';
|
||||||
} else {
|
} else {
|
||||||
return "white";
|
return 'white';
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@@ -56,13 +52,5 @@ export default function RunProgressDounut({
|
|||||||
updateChartDate();
|
updateChartDate();
|
||||||
}, [statusCounts, theme]);
|
}, [statusCounts, theme]);
|
||||||
|
|
||||||
return (
|
return <Chart options={chartData.options} series={chartData.series} type="donut" width={'100%'} height={'100%'} />;
|
||||||
<Chart
|
|
||||||
options={chartData.options}
|
|
||||||
series={chartData.series}
|
|
||||||
type="donut"
|
|
||||||
width={"100%"}
|
|
||||||
height={"100%"}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useMemo } from "react";
|
import { useState, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
DropdownItem,
|
DropdownItem,
|
||||||
Selection,
|
Selection,
|
||||||
SortDescriptor,
|
SortDescriptor,
|
||||||
} from "@nextui-org/react";
|
} from '@nextui-org/react';
|
||||||
import {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
MoreVertical,
|
MoreVertical,
|
||||||
@@ -24,10 +24,10 @@ import {
|
|||||||
CircleDashed,
|
CircleDashed,
|
||||||
CircleX,
|
CircleX,
|
||||||
CircleSlash2,
|
CircleSlash2,
|
||||||
} from "lucide-react";
|
} from 'lucide-react';
|
||||||
import { priorities, testRunCaseStatus } from "@/config/selection";
|
import { priorities, testRunCaseStatus } from '@/config/selection';
|
||||||
import { CaseType } from "@/types/case";
|
import { CaseType } from '@/types/case';
|
||||||
import { RunsMessages } from "@/types/run";
|
import { RunsMessages } from '@/types/run';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
cases: CaseType[];
|
cases: CaseType[];
|
||||||
@@ -49,16 +49,16 @@ export default function TestCaseSelector({
|
|||||||
messages,
|
messages,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const headerColumns = [
|
const headerColumns = [
|
||||||
{ name: messages.id, uid: "id", sortable: true },
|
{ name: messages.id, uid: 'id', sortable: true },
|
||||||
{ name: messages.title, uid: "title", sortable: true },
|
{ name: messages.title, uid: 'title', sortable: true },
|
||||||
{ name: messages.priority, uid: "priority", sortable: true },
|
{ name: messages.priority, uid: 'priority', sortable: true },
|
||||||
{ name: messages.status, uid: "runStatus", sortable: true },
|
{ name: messages.status, uid: 'runStatus', sortable: true },
|
||||||
{ name: messages.actions, uid: "actions" },
|
{ name: messages.actions, uid: 'actions' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
|
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
|
||||||
column: "id",
|
column: 'id',
|
||||||
direction: "ascending",
|
direction: 'ascending',
|
||||||
});
|
});
|
||||||
|
|
||||||
const sortedItems = useMemo(() => {
|
const sortedItems = useMemo(() => {
|
||||||
@@ -67,23 +67,23 @@ export default function TestCaseSelector({
|
|||||||
const second = b[sortDescriptor.column as keyof CaseType] as number;
|
const second = b[sortDescriptor.column as keyof CaseType] as number;
|
||||||
const cmp = first < second ? -1 : first > second ? 1 : 0;
|
const cmp = first < second ? -1 : first > second ? 1 : 0;
|
||||||
|
|
||||||
return sortDescriptor.direction === "descending" ? -cmp : cmp;
|
return sortDescriptor.direction === 'descending' ? -cmp : cmp;
|
||||||
});
|
});
|
||||||
}, [sortDescriptor, cases]);
|
}, [sortDescriptor, cases]);
|
||||||
|
|
||||||
const notIncludedCaseClass = "text-neutral-200 dark:text-neutral-600";
|
const notIncludedCaseClass = 'text-neutral-200 dark:text-neutral-600';
|
||||||
const chipBaseClass = "flex items-center text-default-600";
|
const chipBaseClass = 'flex items-center text-default-600';
|
||||||
|
|
||||||
const renderStatusIcon = (uid: string) => {
|
const renderStatusIcon = (uid: string) => {
|
||||||
if (uid === "untested") {
|
if (uid === 'untested') {
|
||||||
return <Circle size={16} color="#d4d4d8" />;
|
return <Circle size={16} color="#d4d4d8" />;
|
||||||
} else if (uid === "passed") {
|
} else if (uid === 'passed') {
|
||||||
return <CircleCheck size={16} color="#17c964" />;
|
return <CircleCheck size={16} color="#17c964" />;
|
||||||
} else if (uid === "retest") {
|
} else if (uid === 'retest') {
|
||||||
return <CircleDashed size={16} color="#f5a524" />;
|
return <CircleDashed size={16} color="#f5a524" />;
|
||||||
} else if (uid === "failed") {
|
} else if (uid === 'failed') {
|
||||||
return <CircleX size={16} color="#f31260" />;
|
return <CircleX size={16} color="#f31260" />;
|
||||||
} else if (uid === "skipped") {
|
} else if (uid === 'skipped') {
|
||||||
return <CircleSlash2 size={16} color="#52525b" />;
|
return <CircleSlash2 size={16} color="#52525b" />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -93,22 +93,18 @@ export default function TestCaseSelector({
|
|||||||
const isIncluded = testCase.isIncluded;
|
const isIncluded = testCase.isIncluded;
|
||||||
|
|
||||||
switch (columnKey) {
|
switch (columnKey) {
|
||||||
case "priority":
|
case 'priority':
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={isIncluded ? chipBaseClass : chipBaseClass + notIncludedCaseClass}>
|
||||||
className={
|
|
||||||
isIncluded ? chipBaseClass : chipBaseClass + notIncludedCaseClass
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Circle
|
<Circle
|
||||||
size={8}
|
size={8}
|
||||||
color={isIncluded ? priorities[cellValue].color : "#d4d4d8"}
|
color={isIncluded ? priorities[cellValue].color : '#d4d4d8'}
|
||||||
fill={isIncluded ? priorities[cellValue].color : "#d4d4d8"}
|
fill={isIncluded ? priorities[cellValue].color : '#d4d4d8'}
|
||||||
/>
|
/>
|
||||||
<div className="ms-3">{messages[priorities[cellValue].uid]}</div>
|
<div className="ms-3">{messages[priorities[cellValue].uid]}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case "runStatus":
|
case 'runStatus':
|
||||||
return (
|
return (
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
@@ -116,15 +112,10 @@ export default function TestCaseSelector({
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant="light"
|
variant="light"
|
||||||
isDisabled={!isIncluded}
|
isDisabled={!isIncluded}
|
||||||
startContent={
|
startContent={isIncluded && renderStatusIcon(testRunCaseStatus[cellValue].uid)}
|
||||||
isIncluded &&
|
|
||||||
renderStatusIcon(testRunCaseStatus[cellValue].uid)
|
|
||||||
}
|
|
||||||
endContent={isIncluded && <ChevronDown size={16} />}
|
endContent={isIncluded && <ChevronDown size={16} />}
|
||||||
>
|
>
|
||||||
<span className="w-12">
|
<span className="w-12">{isIncluded && messages[testRunCaseStatus[cellValue].uid]}</span>
|
||||||
{isIncluded && messages[testRunCaseStatus[cellValue].uid]}
|
|
||||||
</span>
|
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownTrigger>
|
</DropdownTrigger>
|
||||||
<DropdownMenu aria-label="test case actions">
|
<DropdownMenu aria-label="test case actions">
|
||||||
@@ -140,7 +131,7 @@ export default function TestCaseSelector({
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
case "actions":
|
case 'actions':
|
||||||
return (
|
return (
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
@@ -173,18 +164,18 @@ export default function TestCaseSelector({
|
|||||||
|
|
||||||
const classNames = useMemo(
|
const classNames = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
wrapper: ["min-w-3xl"],
|
wrapper: ['min-w-3xl'],
|
||||||
th: ["bg-transparent", "text-default-500", "border-b", "border-divider"],
|
th: ['bg-transparent', 'text-default-500', 'border-b', 'border-divider'],
|
||||||
td: [
|
td: [
|
||||||
// changing the rows border radius
|
// changing the rows border radius
|
||||||
// first
|
// first
|
||||||
"group-data-[first=true]:first:before:rounded-none",
|
'group-data-[first=true]:first:before:rounded-none',
|
||||||
"group-data-[first=true]:last:before:rounded-none",
|
'group-data-[first=true]:last:before:rounded-none',
|
||||||
// middle
|
// middle
|
||||||
"group-data-[middle=true]:before:rounded-none",
|
'group-data-[middle=true]:before:rounded-none',
|
||||||
// last
|
// last
|
||||||
"group-data-[last=true]:first:before:rounded-none",
|
'group-data-[last=true]:first:before:rounded-none',
|
||||||
"group-data-[last=true]:last:before:rounded-none",
|
'group-data-[last=true]:last:before:rounded-none',
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
[]
|
[]
|
||||||
@@ -211,7 +202,7 @@ export default function TestCaseSelector({
|
|||||||
{(column) => (
|
{(column) => (
|
||||||
<TableColumn
|
<TableColumn
|
||||||
key={column.uid}
|
key={column.uid}
|
||||||
align={column.uid === "actions" ? "center" : "start"}
|
align={column.uid === 'actions' ? 'center' : 'start'}
|
||||||
allowsSorting={column.sortable}
|
allowsSorting={column.sortable}
|
||||||
>
|
>
|
||||||
{column.name}
|
{column.name}
|
||||||
@@ -220,13 +211,8 @@ export default function TestCaseSelector({
|
|||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody emptyContent={messages.noCasesFound} items={sortedItems}>
|
<TableBody emptyContent={messages.noCasesFound} items={sortedItems}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<TableRow
|
<TableRow key={item.id} className={!item.isIncluded ? notIncludedCaseClass : ''}>
|
||||||
key={item.id}
|
{(columnKey) => <TableCell>{renderCell(item, columnKey)}</TableCell>}
|
||||||
className={!item.isIncluded ? notIncludedCaseClass : ""}
|
|
||||||
>
|
|
||||||
{(columnKey) => (
|
|
||||||
<TableCell>{renderCell(item, columnKey)}</TableCell>
|
|
||||||
)}
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
|||||||
@@ -1,53 +1,42 @@
|
|||||||
import RunEditor from "./RunEditor";
|
import RunEditor from './RunEditor';
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
export default function Page({
|
export default function Page({ params }: { params: { projectId: string; runId: string; locale: string } }) {
|
||||||
params,
|
const t = useTranslations('Run');
|
||||||
}: {
|
|
||||||
params: { projectId: string; runId: string; locale: string };
|
|
||||||
}) {
|
|
||||||
const t = useTranslations("Run");
|
|
||||||
const messages = {
|
const messages = {
|
||||||
backToRuns: t("back_to_runs"),
|
backToRuns: t('back_to_runs'),
|
||||||
updating: t("updating"),
|
updating: t('updating'),
|
||||||
update: t("update"),
|
update: t('update'),
|
||||||
progress: t("progress"),
|
progress: t('progress'),
|
||||||
refresh: t("refresh"),
|
refresh: t('refresh'),
|
||||||
id: t("id"),
|
id: t('id'),
|
||||||
title: t("title"),
|
title: t('title'),
|
||||||
pleaseEnter: t("please_enter"),
|
pleaseEnter: t('please_enter'),
|
||||||
description: t("description"),
|
description: t('description'),
|
||||||
new: t("new"),
|
new: t('new'),
|
||||||
inProgress: t("inProgress"),
|
inProgress: t('inProgress'),
|
||||||
underReview: t("underReview"),
|
underReview: t('underReview'),
|
||||||
rejected: t("rejected"),
|
rejected: t('rejected'),
|
||||||
done: t("done"),
|
done: t('done'),
|
||||||
closed: t("closed"),
|
closed: t('closed'),
|
||||||
priority: t("priority"),
|
priority: t('priority'),
|
||||||
actions: t("actions"),
|
actions: t('actions'),
|
||||||
status: t("status"),
|
status: t('status'),
|
||||||
critical: t("critical"),
|
critical: t('critical'),
|
||||||
high: t("high"),
|
high: t('high'),
|
||||||
medium: t("medium"),
|
medium: t('medium'),
|
||||||
low: t("low"),
|
low: t('low'),
|
||||||
untested: t("untested"),
|
untested: t('untested'),
|
||||||
passed: t("passed"),
|
passed: t('passed'),
|
||||||
failed: t("failed"),
|
failed: t('failed'),
|
||||||
retest: t("retest"),
|
retest: t('retest'),
|
||||||
skipped: t("skipped"),
|
skipped: t('skipped'),
|
||||||
selectTestCase: t("select_test_case"),
|
selectTestCase: t('select_test_case'),
|
||||||
testCaseSelection: t("test_case_selection"),
|
testCaseSelection: t('test_case_selection'),
|
||||||
includeInRun: t("include_in_run"),
|
includeInRun: t('include_in_run'),
|
||||||
excludeFromRun: t("exclude_from_run"),
|
excludeFromRun: t('exclude_from_run'),
|
||||||
noCasesFound: t("no_cases_found"),
|
noCasesFound: t('no_cases_found'),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return <RunEditor projectId={params.projectId} runId={params.runId} messages={messages} locale={params.locale} />;
|
||||||
<RunEditor
|
|
||||||
projectId={params.projectId}
|
|
||||||
runId={params.runId}
|
|
||||||
messages={messages}
|
|
||||||
locale={params.locale}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,23 @@
|
|||||||
import RunsPage from "./RunsPage";
|
import RunsPage from './RunsPage';
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
export default function Page({
|
export default function Page({ params }: { params: { projectId: string; locale: string } }) {
|
||||||
params,
|
const t = useTranslations('Runs');
|
||||||
}: {
|
|
||||||
params: { projectId: string; locale: string };
|
|
||||||
}) {
|
|
||||||
const t = useTranslations("Runs");
|
|
||||||
const messages = {
|
const messages = {
|
||||||
runList: t("run_list"),
|
runList: t('run_list'),
|
||||||
id: t("id"),
|
id: t('id'),
|
||||||
name: t("name"),
|
name: t('name'),
|
||||||
description: t("description"),
|
description: t('description'),
|
||||||
lastUpdate: t("last_update"),
|
lastUpdate: t('last_update'),
|
||||||
actions: t("actions"),
|
actions: t('actions'),
|
||||||
newRun: t("new_run"),
|
newRun: t('new_run'),
|
||||||
deleteRun: t("delete_run"),
|
deleteRun: t('delete_run'),
|
||||||
noRunsFound: t("no_runs_found"),
|
noRunsFound: t('no_runs_found'),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RunsPage
|
<RunsPage projectId={params.projectId} locale={params.locale} messages={messages} />
|
||||||
projectId={params.projectId}
|
|
||||||
locale={params.locale}
|
|
||||||
messages={messages}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
export default function ProjectsLayout({
|
export default function ProjectsLayout({ children }: { children: React.ReactNode }) {
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
import ProjectsPage from "./ProjectsPage";
|
import ProjectsPage from './ProjectsPage';
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
export default function Page(params: { locale: string }) {
|
export default function Page(params: { locale: string }) {
|
||||||
const t = useTranslations("Projects");
|
const t = useTranslations('Projects');
|
||||||
const messages = {
|
const messages = {
|
||||||
projectList: t("projectList"),
|
projectList: t('projectList'),
|
||||||
project: t("project"),
|
project: t('project'),
|
||||||
newProject: t("new_project"),
|
newProject: t('new_project'),
|
||||||
editProject: t("edit_project"),
|
editProject: t('edit_project'),
|
||||||
deleteProject: t("delete_project"),
|
deleteProject: t('delete_project'),
|
||||||
id: t("id"),
|
id: t('id'),
|
||||||
name: t("name"),
|
name: t('name'),
|
||||||
detail: t("detail"),
|
detail: t('detail'),
|
||||||
lastUpdate: t("last_update"),
|
lastUpdate: t('last_update'),
|
||||||
actions: t("actions"),
|
actions: t('actions'),
|
||||||
projectName: t("project_name"),
|
projectName: t('project_name'),
|
||||||
projectDetail: t("project_detail"),
|
projectDetail: t('project_detail'),
|
||||||
close: t("close"),
|
close: t('close'),
|
||||||
create: t("create"),
|
create: t('create'),
|
||||||
update: t("update"),
|
update: t('update'),
|
||||||
pleaseEnter: t("please_enter"),
|
pleaseEnter: t('please_enter'),
|
||||||
noProjectsFound: t("no_projects_found"),
|
noProjectsFound: t('no_projects_found'),
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
"use client";
|
'use client';
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from 'react';
|
||||||
import { NextUIProvider } from "@nextui-org/react";
|
import { NextUIProvider } from '@nextui-org/react';
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from 'next/navigation';
|
||||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
||||||
import { ThemeProviderProps } from "next-themes/dist/types";
|
import { ThemeProviderProps } from 'next-themes/dist/types';
|
||||||
|
|
||||||
import TokenProvider from "./TokenProvider";
|
import TokenProvider from './TokenProvider';
|
||||||
import { TokenProps } from "@/types/user";
|
import { TokenProps } from '@/types/user';
|
||||||
|
|
||||||
export interface ProvidersProps {
|
export interface ProvidersProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"format": "prettier --write frontend/**/*.{js,ts,json} backend/**/*.{js,ts,json}",
|
"format": "prettier --write frontend/**/*.{js,ts,jsx,tsx,json} backend/**/*.{js,ts,json}",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"coverage": "vitest run --coverage"
|
"coverage": "vitest run --coverage"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user