Introduce prettier
This commit is contained in:
@@ -1,22 +1,10 @@
|
||||
"use client";
|
||||
import {
|
||||
Button,
|
||||
DropdownTrigger,
|
||||
Dropdown,
|
||||
DropdownMenu,
|
||||
DropdownItem,
|
||||
} 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";
|
||||
'use client';
|
||||
import { Button, DropdownTrigger, Dropdown, DropdownMenu, DropdownItem } 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 = {
|
||||
messages: AccountDropDownMessages;
|
||||
@@ -35,15 +23,15 @@ export default function DropdownAccount({ messages, locale }: Props) {
|
||||
|
||||
const signinItems = [
|
||||
{
|
||||
uid: "account",
|
||||
uid: 'account',
|
||||
title: messages.account,
|
||||
icon: <User size={16} />,
|
||||
onPress: () => {
|
||||
router.push("/account", { locale: locale });
|
||||
router.push('/account', { locale: locale });
|
||||
},
|
||||
},
|
||||
{
|
||||
uid: "signout",
|
||||
uid: 'signout',
|
||||
title: messages.signOut,
|
||||
icon: <ArrowRightFromLine size={16} />,
|
||||
onPress: signOut,
|
||||
@@ -52,19 +40,19 @@ export default function DropdownAccount({ messages, locale }: Props) {
|
||||
|
||||
const signoutItems = [
|
||||
{
|
||||
uid: "signin",
|
||||
uid: 'signin',
|
||||
title: messages.signIn,
|
||||
icon: <ArrowRightToLine size={16} />,
|
||||
onPress: () => {
|
||||
router.push("/account/signin", { locale: locale });
|
||||
router.push('/account/signin', { locale: locale });
|
||||
},
|
||||
},
|
||||
{
|
||||
uid: "signup",
|
||||
uid: 'signup',
|
||||
title: messages.signUp,
|
||||
icon: <PenTool size={16} />,
|
||||
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 (
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="light"
|
||||
startContent={<User size={16} />}
|
||||
endContent={<ChevronDown size={16} />}
|
||||
>
|
||||
{context.token && context.token.user
|
||||
? context.token.user.username
|
||||
: messages.signIn}
|
||||
<Button size="sm" variant="light" startContent={<User size={16} />} endContent={<ChevronDown size={16} />}>
|
||||
{context.token && context.token.user ? context.token.user.username : messages.signIn}
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
{context.token && context.token.user ? (
|
||||
<DropdownMenu aria-label="account actions when sign in">
|
||||
{signinItems.map((entry) => (
|
||||
<DropdownItem
|
||||
key={entry.uid}
|
||||
title={entry.title}
|
||||
startContent={entry.icon}
|
||||
onPress={entry.onPress}
|
||||
/>
|
||||
<DropdownItem key={entry.uid} title={entry.title} startContent={entry.icon} onPress={entry.onPress} />
|
||||
))}
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<DropdownMenu aria-label="account actions when sign out">
|
||||
{signoutItems.map((entry) => (
|
||||
<DropdownItem
|
||||
key={entry.uid}
|
||||
title={entry.title}
|
||||
startContent={entry.icon}
|
||||
onPress={entry.onPress}
|
||||
/>
|
||||
<DropdownItem key={entry.uid} title={entry.title} startContent={entry.icon} onPress={entry.onPress} />
|
||||
))}
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
"use client";
|
||||
import {
|
||||
Button,
|
||||
DropdownTrigger,
|
||||
Dropdown,
|
||||
DropdownMenu,
|
||||
DropdownItem,
|
||||
} from "@nextui-org/react";
|
||||
import { Globe, ChevronDown } from "lucide-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useRouter } from "@/src/navigation";
|
||||
'use client';
|
||||
import { Button, DropdownTrigger, Dropdown, DropdownMenu, DropdownItem } from '@nextui-org/react';
|
||||
import { Globe, ChevronDown } from 'lucide-react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useRouter } from '@/src/navigation';
|
||||
|
||||
const locales = [
|
||||
{ code: "en", name: "English" },
|
||||
{ code: "ja", name: "日本語" },
|
||||
{ code: 'en', name: 'English' },
|
||||
{ code: 'ja', name: '日本語' },
|
||||
];
|
||||
|
||||
export default function DropdownLanguage(params: { locale: string }) {
|
||||
@@ -23,7 +17,7 @@ export default function DropdownLanguage(params: { locale: string }) {
|
||||
let newPathname;
|
||||
if (pathname.length < 4) {
|
||||
// when root path
|
||||
router.push("/", { locale: nextLocale });
|
||||
router.push('/', { locale: nextLocale });
|
||||
} else {
|
||||
// when not root path, trim first "/en" from pathname = "/en/projects"
|
||||
newPathname = pathname.slice(params.locale.length + 1);
|
||||
@@ -34,22 +28,13 @@ export default function DropdownLanguage(params: { locale: string }) {
|
||||
return (
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="light"
|
||||
startContent={<Globe size={16} />}
|
||||
endContent={<ChevronDown size={16} />}
|
||||
>
|
||||
{locales.find((locale) => locale.code === params.locale)?.name ||
|
||||
params.locale}
|
||||
<Button size="sm" variant="light" startContent={<Globe size={16} />} endContent={<ChevronDown size={16} />}>
|
||||
{locales.find((locale) => locale.code === params.locale)?.name || params.locale}
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="lacales">
|
||||
{locales.map((entry) => (
|
||||
<DropdownItem
|
||||
key={entry.code}
|
||||
onClick={() => changeLocale(entry.code)}
|
||||
>
|
||||
<DropdownItem key={entry.code} onClick={() => changeLocale(entry.code)}>
|
||||
{entry.name}
|
||||
</DropdownItem>
|
||||
))}
|
||||
|
||||
@@ -8,37 +8,37 @@ import {
|
||||
NavbarMenuItem,
|
||||
Chip,
|
||||
Link as NextUiLink,
|
||||
} from "@nextui-org/react";
|
||||
import { MoveUpRight } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Link } from "@/src/navigation";
|
||||
import { ThemeSwitch } from "@/components/theme-switch";
|
||||
import { GithubIcon } from "@/components/icons";
|
||||
import Image from "next/image";
|
||||
import DropdownAccount from "./DropdownAccount";
|
||||
import DropdownLanguage from "./DropdownLanguage";
|
||||
} from '@nextui-org/react';
|
||||
import { MoveUpRight } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Link } from '@/src/navigation';
|
||||
import { ThemeSwitch } from '@/components/theme-switch';
|
||||
import { GithubIcon } from '@/components/icons';
|
||||
import Image from 'next/image';
|
||||
import DropdownAccount from './DropdownAccount';
|
||||
import DropdownLanguage from './DropdownLanguage';
|
||||
|
||||
export default function Header(params: { locale: string }) {
|
||||
const t = useTranslations("Header");
|
||||
const t = useTranslations('Header');
|
||||
const messages = {
|
||||
account: t("account"),
|
||||
signUp: t("signup"),
|
||||
signIn: t("signin"),
|
||||
signOut: t("signout"),
|
||||
account: t('account'),
|
||||
signUp: t('signup'),
|
||||
signIn: t('signin'),
|
||||
signOut: t('signout'),
|
||||
};
|
||||
|
||||
// Links shown Header or slider
|
||||
const commonLinks = [
|
||||
{
|
||||
uid: "projects",
|
||||
href: "/projects",
|
||||
label: t("projects"),
|
||||
uid: 'projects',
|
||||
href: '/projects',
|
||||
label: t('projects'),
|
||||
isExternal: false,
|
||||
},
|
||||
{
|
||||
uid: "docs",
|
||||
href: "https://kimatata.github.io/TestPlat/docs/getstarted/selfhost",
|
||||
label: t("docs"),
|
||||
uid: 'docs',
|
||||
href: 'https://kimatata.github.io/TestPlat/docs/getstarted/selfhost',
|
||||
label: t('docs'),
|
||||
isExternal: true,
|
||||
},
|
||||
];
|
||||
@@ -47,17 +47,8 @@ export default function Header(params: { locale: string }) {
|
||||
<NextUINavbar maxWidth="full" position="sticky" className="bg-inherit">
|
||||
<NavbarContent className="basis-1/5 sm:basis-full" justify="start">
|
||||
<NavbarBrand as="li" className="gap-3 max-w-fit">
|
||||
<Link
|
||||
className="flex justify-start items-center gap-1"
|
||||
href="/"
|
||||
locale={params.locale}
|
||||
>
|
||||
<Image
|
||||
src="/favicon/android-chrome-192x192.png"
|
||||
width={32}
|
||||
height={32}
|
||||
alt="Logo"
|
||||
/>
|
||||
<Link className="flex justify-start items-center gap-1" 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>
|
||||
</Link>
|
||||
</NavbarBrand>
|
||||
@@ -82,7 +73,7 @@ export default function Header(params: { locale: string }) {
|
||||
showAnchorIcon
|
||||
anchorIcon={<MoveUpRight size={12} className="ms-1" />}
|
||||
>
|
||||
{t("docs")}
|
||||
{t('docs')}
|
||||
</NextUiLink>
|
||||
</NavbarItem>
|
||||
) : (
|
||||
@@ -100,11 +91,7 @@ export default function Header(params: { locale: string }) {
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarContent className="basis-1 pl-4" justify="end">
|
||||
<NextUiLink
|
||||
isExternal
|
||||
href="https://github.com/kimatata/TestPlat"
|
||||
aria-label="Github"
|
||||
>
|
||||
<NextUiLink isExternal href="https://github.com/kimatata/TestPlat" aria-label="Github">
|
||||
<GithubIcon className="text-default-500" />
|
||||
</NextUiLink>
|
||||
<ThemeSwitch />
|
||||
@@ -126,12 +113,8 @@ export default function Header(params: { locale: string }) {
|
||||
{commonLinks.map((link) =>
|
||||
link.isExternal ? (
|
||||
<NavbarMenuItem key={link.uid}>
|
||||
<NextUiLink
|
||||
href={link.href}
|
||||
showAnchorIcon
|
||||
anchorIcon={<MoveUpRight size={12} className="ms-1" />}
|
||||
>
|
||||
{t("docs")}
|
||||
<NextUiLink href={link.href} showAnchorIcon anchorIcon={<MoveUpRight size={12} className="ms-1" />}>
|
||||
{t('docs')}
|
||||
</NextUiLink>
|
||||
</NavbarMenuItem>
|
||||
) : (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
import { title } from "@/components/primitives";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Tabs, Tab, Image } from "@nextui-org/react";
|
||||
'use client';
|
||||
import { title } from '@/components/primitives';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Tabs, Tab, Image } from '@nextui-org/react';
|
||||
|
||||
type PaneDemoImagesMessages = {
|
||||
title: string;
|
||||
@@ -14,28 +14,28 @@ type Props = {
|
||||
};
|
||||
|
||||
export default function DemoImages({ messages }: Props) {
|
||||
const [currentTab, setcurrentTab] = useState("edit");
|
||||
const [currentTab, setcurrentTab] = useState('edit');
|
||||
const [currentImage, setcurrentImage] = useState({
|
||||
src: "/top/caseEdit.png",
|
||||
src: '/top/caseEdit.png',
|
||||
alt: messages.caseEdit,
|
||||
});
|
||||
const tabs = [
|
||||
{
|
||||
key: "edit",
|
||||
key: 'edit',
|
||||
title: messages.caseEdit,
|
||||
src: "/top/caseEdit.png",
|
||||
src: '/top/caseEdit.png',
|
||||
alt: messages.caseEdit,
|
||||
},
|
||||
{
|
||||
key: "home",
|
||||
key: 'home',
|
||||
title: messages.caseHome,
|
||||
src: "/top/caseHome.png",
|
||||
src: '/top/caseHome.png',
|
||||
alt: messages.caseHome,
|
||||
},
|
||||
{
|
||||
key: "run",
|
||||
key: 'run',
|
||||
title: messages.caseRun,
|
||||
src: "/top/caseRun.png",
|
||||
src: '/top/caseRun.png',
|
||||
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="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 />
|
||||
<Tabs
|
||||
aria-label="Options"
|
||||
color={"primary"}
|
||||
color={'primary'}
|
||||
radius="full"
|
||||
size="md"
|
||||
className="mt-8"
|
||||
@@ -68,13 +68,7 @@ export default function DemoImages({ messages }: Props) {
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center w-full lg:w-7/12 p-4">
|
||||
<Image
|
||||
src={currentImage.src}
|
||||
alt={currentImage.alt}
|
||||
shadow="md"
|
||||
height={500}
|
||||
className="max-w-full"
|
||||
/>
|
||||
<Image src={currentImage.src} alt={currentImage.alt} shadow="md" height={500} className="max-w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import { title, subtitle } from "@/components/primitives";
|
||||
import { Card, CardHeader, CardBody, Avatar } from "@nextui-org/react";
|
||||
import { Scale, Folder, Check, Globe } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { title, subtitle } from '@/components/primitives';
|
||||
import { Card, CardHeader, CardBody, Avatar } from '@nextui-org/react';
|
||||
import { Scale, Folder, Check, Globe } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function MainTitle() {
|
||||
const t = useTranslations("Index");
|
||||
const t = useTranslations('Index');
|
||||
|
||||
const features = [
|
||||
{
|
||||
title: t("oss_title"),
|
||||
detail: t("oss_detail"),
|
||||
title: t('oss_title'),
|
||||
detail: t('oss_detail'),
|
||||
icon: <Scale size={24} color="#52e280" />,
|
||||
},
|
||||
{
|
||||
title: t("organize_title"),
|
||||
detail: t("organize_detail"),
|
||||
title: t('organize_title'),
|
||||
detail: t('organize_detail'),
|
||||
icon: <Folder size={24} color="#52e280" />,
|
||||
},
|
||||
{
|
||||
title: t("usability_title"),
|
||||
detail: t("usability_detail"),
|
||||
title: t('usability_title'),
|
||||
detail: t('usability_detail'),
|
||||
icon: <Check size={24} color="#52e280" />,
|
||||
},
|
||||
{
|
||||
title: t("universal_title"),
|
||||
detail: t("universal_detail"),
|
||||
title: t('universal_title'),
|
||||
detail: t('universal_detail'),
|
||||
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]">
|
||||
<CardHeader className="flex gap-3">
|
||||
<div>
|
||||
<Avatar
|
||||
className="bg-green-100"
|
||||
showFallback
|
||||
src=""
|
||||
fallback={feature.icon}
|
||||
/>
|
||||
<Avatar className="bg-green-100" showFallback src="" fallback={feature.icon} />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<h4 className="font-bold text-large">{feature.title}</h4>
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { title, subtitle } from "@/components/primitives";
|
||||
import { Button, Link as NextUiLink } from "@nextui-org/react";
|
||||
import { MoveUpRight } from "lucide-react";
|
||||
import { Link } from "@/src/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { title, subtitle } from '@/components/primitives';
|
||||
import { Button, Link as NextUiLink } from '@nextui-org/react';
|
||||
import { MoveUpRight } from 'lucide-react';
|
||||
import { Link } from '@/src/navigation';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
type Props = {
|
||||
locale: string;
|
||||
};
|
||||
|
||||
export default function MainTitle({ locale }: Props) {
|
||||
const t = useTranslations("Index");
|
||||
const t = useTranslations('Index');
|
||||
|
||||
return (
|
||||
<div className="md:text-left text-center">
|
||||
<h1
|
||||
className={title({
|
||||
color: "green",
|
||||
class: "lg:text-7xl md:text-7xl sm:text-7xl text-7xl",
|
||||
color: 'green',
|
||||
class: 'lg:text-7xl md:text-7xl sm:text-7xl text-7xl',
|
||||
})}
|
||||
>
|
||||
TestPlat
|
||||
@@ -25,29 +25,23 @@ export default function MainTitle({ locale }: Props) {
|
||||
<br />
|
||||
<h1
|
||||
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 />
|
||||
{t("web_application")}
|
||||
{t('web_application')}
|
||||
</h1>
|
||||
<h4 className={subtitle({ class: "mt-4" })}>
|
||||
{t("integrate_and_manage")}
|
||||
</h4>
|
||||
<h4 className={subtitle({ class: 'mt-4' })}>{t('integrate_and_manage')}</h4>
|
||||
|
||||
<div className="mt-5">
|
||||
<Link href={`/projects/`} locale={locale}>
|
||||
<Button color="primary" radius="full" className="px-0">
|
||||
{t("demo")}
|
||||
{t('demo')}
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<NextUiLink
|
||||
isExternal
|
||||
href="https://kimatata.github.io/TestPlat/docs/getstarted/selfhost"
|
||||
aria-label="docs"
|
||||
>
|
||||
<NextUiLink isExternal href="https://kimatata.github.io/TestPlat/docs/getstarted/selfhost" aria-label="docs">
|
||||
<Button
|
||||
color="primary"
|
||||
variant="bordered"
|
||||
@@ -55,16 +49,11 @@ export default function MainTitle({ locale }: Props) {
|
||||
className="ms-2"
|
||||
endContent={<MoveUpRight size={12} />}
|
||||
>
|
||||
{t("get_started")}
|
||||
{t('get_started')}
|
||||
</Button>
|
||||
</NextUiLink>
|
||||
|
||||
<NextUiLink
|
||||
size="sm"
|
||||
isExternal
|
||||
href="https://github.com/kimatata/TestPlat"
|
||||
aria-label="Github"
|
||||
>
|
||||
<NextUiLink size="sm" isExternal href="https://github.com/kimatata/TestPlat" aria-label="Github">
|
||||
<Button
|
||||
color="primary"
|
||||
variant="bordered"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
import { createContext, useState, useEffect } from "react";
|
||||
import { TokenType } from "@/types/user";
|
||||
import { TokenProps } from "@/types/user";
|
||||
import { useRouter, usePathname } from "@/src/navigation";
|
||||
'use client';
|
||||
import { createContext, useState, useEffect } from 'react';
|
||||
import { TokenType } from '@/types/user';
|
||||
import { TokenProps } from '@/types/user';
|
||||
import { useRouter, usePathname } from '@/src/navigation';
|
||||
|
||||
const LOCAL_STORAGE_KEY = "testplat-auth-token";
|
||||
const privatePaths = ["/account"];
|
||||
const LOCAL_STORAGE_KEY = 'testplat-auth-token';
|
||||
const privatePaths = ['/account'];
|
||||
|
||||
function storeTokenToLocalStorage(token: TokenType) {
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(token));
|
||||
@@ -23,7 +23,7 @@ const TokenProvider = ({ locale, children }: TokenProps) => {
|
||||
const pathname = usePathname();
|
||||
|
||||
const [token, setToken] = useState<TokenType>({
|
||||
access_token: "",
|
||||
access_token: '',
|
||||
user: null,
|
||||
});
|
||||
const tokenContext = {
|
||||
@@ -57,11 +57,7 @@ const TokenProvider = ({ locale, children }: TokenProps) => {
|
||||
restoreTokenFromLocalStorage();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<TokenContext.Provider value={tokenContext}>
|
||||
{children}
|
||||
</TokenContext.Provider>
|
||||
);
|
||||
return <TokenContext.Provider value={tokenContext}>{children}</TokenContext.Provider>;
|
||||
};
|
||||
|
||||
export { TokenContext };
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
import { useContext } from "react";
|
||||
import { Avatar, Card, CardHeader, CardBody, Divider } from "@nextui-org/react";
|
||||
import { TokenContext } from "../TokenProvider";
|
||||
'use client';
|
||||
import { useContext } from 'react';
|
||||
import { Avatar, Card, CardHeader, CardBody, Divider } from '@nextui-org/react';
|
||||
import { TokenContext } from '../TokenProvider';
|
||||
|
||||
type AccountPageMessages = {
|
||||
yourProjects: string;
|
||||
@@ -20,19 +20,10 @@ export default function AccountPage({ messages, locale }: Props) {
|
||||
{context.token && context.token.user && (
|
||||
<Card className="w-[600px] mt-16 mx-3">
|
||||
<CardHeader className="flex gap-6">
|
||||
<Avatar
|
||||
isBordered
|
||||
radius="full"
|
||||
className="w-16 h-16 text-large"
|
||||
src={context.token.user.avatarPath}
|
||||
/>
|
||||
<Avatar isBordered radius="full" className="w-16 h-16 text-large" src={context.token.user.avatarPath} />
|
||||
<div className="flex flex-col">
|
||||
<p className="text-2xl font-bold">
|
||||
{context.token.user.username}
|
||||
</p>
|
||||
<p className="text-lg text-default-500">
|
||||
{context.token.user.email}
|
||||
</p>
|
||||
<p className="text-2xl font-bold">{context.token.user.username}</p>
|
||||
<p className="text-lg text-default-500">{context.token.user.email}</p>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<Divider />
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { useState, useContext } from "react";
|
||||
import { Input, Button, Card, CardHeader, CardBody } from "@nextui-org/react";
|
||||
import { Link } from "@/src/navigation";
|
||||
import { ChevronRight, Eye, EyeOff } from "lucide-react";
|
||||
import { UserType, AuthMessages } from "@/types/user";
|
||||
import { roles } from "@/config/selection";
|
||||
import { signUp, signIn, getRandomAvatarPath } from "./authControl";
|
||||
import { isValidEmail, isValidPassword } from "./validate";
|
||||
import { TokenContext } from "../TokenProvider";
|
||||
import { useRouter } from "@/src/navigation";
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { useState, useContext } from 'react';
|
||||
import { Input, Button, Card, CardHeader, CardBody } from '@nextui-org/react';
|
||||
import { Link } from '@/src/navigation';
|
||||
import { ChevronRight, Eye, EyeOff } from 'lucide-react';
|
||||
import { UserType, AuthMessages } from '@/types/user';
|
||||
import { roles } from '@/config/selection';
|
||||
import { signUp, signIn, getRandomAvatarPath } from './authControl';
|
||||
import { isValidEmail, isValidPassword } from './validate';
|
||||
import { TokenContext } from '../TokenProvider';
|
||||
import { useRouter } from '@/src/navigation';
|
||||
|
||||
type Props = {
|
||||
isSignup: Boolean;
|
||||
@@ -22,18 +22,17 @@ export default function AuthPage({ isSignup, messages, locale }: Props) {
|
||||
const context = useContext(TokenContext);
|
||||
const [user, setUser] = useState<UserType>({
|
||||
id: null,
|
||||
email: "",
|
||||
password: "",
|
||||
username: "",
|
||||
role: roles.findIndex((entry) => entry.uid === "user"),
|
||||
avatarPath: "",
|
||||
email: '',
|
||||
password: '',
|
||||
username: '',
|
||||
role: roles.findIndex((entry) => entry.uid === 'user'),
|
||||
avatarPath: '',
|
||||
});
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
||||
const togglePasswordVisibility = () =>
|
||||
setIsPasswordVisible(!isPasswordVisible);
|
||||
const togglePasswordVisibility = () => setIsPasswordVisible(!isPasswordVisible);
|
||||
|
||||
const validate = async () => {
|
||||
if (!isValidEmail(user.email)) {
|
||||
@@ -83,31 +82,22 @@ export default function AuthPage({ isSignup, messages, locale }: Props) {
|
||||
|
||||
context.setToken(token);
|
||||
context.storeTokenToLocalStorage(token);
|
||||
router.push("/account", { locale: locale });
|
||||
router.push('/account', { locale: locale });
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="w-[480px] mt-16">
|
||||
<CardHeader className="px-4 pt-4 pb-0 flex justify-between">
|
||||
<h4 className="font-bold text-large">{messages.title}</h4>
|
||||
<Link
|
||||
href={isSignup ? "/account/signin" : "/account/signup"}
|
||||
locale={locale}
|
||||
>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="light"
|
||||
endContent={<ChevronRight size={16} />}
|
||||
>
|
||||
<Link href={isSignup ? '/account/signin' : '/account/signup'} locale={locale}>
|
||||
<Button color="primary" variant="light" endContent={<ChevronRight size={16} />}>
|
||||
{messages.linkTitle}
|
||||
</Button>
|
||||
</Link>
|
||||
</CardHeader>
|
||||
<CardBody className="overflow-visible px-4 pt-0 pb-4">
|
||||
<form>
|
||||
{errorMessage && (
|
||||
<div className="my-3 text-danger">{errorMessage}</div>
|
||||
)}
|
||||
{errorMessage && <div className="my-3 text-danger">{errorMessage}</div>}
|
||||
<Input
|
||||
isRequired
|
||||
type="email"
|
||||
@@ -139,15 +129,11 @@ export default function AuthPage({ isSignup, messages, locale }: Props) {
|
||||
<Input
|
||||
label={messages.password}
|
||||
variant="bordered"
|
||||
autoComplete={isSignup ? "new-password" : "current-password"}
|
||||
autoComplete={isSignup ? 'new-password' : 'current-password'}
|
||||
className="mt-3"
|
||||
type={isPasswordVisible ? "text" : "password"}
|
||||
type={isPasswordVisible ? 'text' : 'password'}
|
||||
endContent={
|
||||
<button
|
||||
className="focus:outline-none"
|
||||
type="button"
|
||||
onClick={togglePasswordVisibility}
|
||||
>
|
||||
<button className="focus:outline-none" type="button" onClick={togglePasswordVisibility}>
|
||||
{isPasswordVisible ? <Eye size={20} /> : <EyeOff size={20} />}
|
||||
</button>
|
||||
}
|
||||
@@ -164,13 +150,9 @@ export default function AuthPage({ isSignup, messages, locale }: Props) {
|
||||
variant="bordered"
|
||||
autoComplete="new-password"
|
||||
className="mt-3"
|
||||
type={isPasswordVisible ? "text" : "password"}
|
||||
type={isPasswordVisible ? 'text' : 'password'}
|
||||
endContent={
|
||||
<button
|
||||
className="focus:outline-none"
|
||||
type="button"
|
||||
onClick={togglePasswordVisibility}
|
||||
>
|
||||
<button className="focus:outline-none" type="button" onClick={togglePasswordVisibility}>
|
||||
{isPasswordVisible ? <Eye size={20} /> : <EyeOff size={20} />}
|
||||
</button>
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
export default function AuthLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
export default function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<div className="w-full flex items-center justify-center">{children}</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useTranslations } from "next-intl";
|
||||
import AccountPage from "./AccountPage";
|
||||
import { useTranslations } from 'next-intl';
|
||||
import AccountPage from './AccountPage';
|
||||
export default function Page(params: { locale: string }) {
|
||||
const t = useTranslations("Auth");
|
||||
const t = useTranslations('Auth');
|
||||
const messages = {
|
||||
yourProjects: t("your_projects"),
|
||||
yourProjects: t('your_projects'),
|
||||
};
|
||||
|
||||
return <AccountPage messages={messages} locale={params.locale} />;
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import AuthPage from "../authPage";
|
||||
import { useTranslations } from "next-intl";
|
||||
import AuthPage from '../authPage';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function Page(params: { locale: string }) {
|
||||
const t = useTranslations("Auth");
|
||||
const t = useTranslations('Auth');
|
||||
const messages = {
|
||||
title: t("signin"),
|
||||
linkTitle: t("or_signup"),
|
||||
submitTitle: t("signin"),
|
||||
email: t("email"),
|
||||
username: t("username"),
|
||||
password: t("password"),
|
||||
confirmPassword: t("confirm_password"),
|
||||
invalidEmail: t("invalid_email"),
|
||||
invalidPassword: t("invalid_password"),
|
||||
usernameEmpty: t("username_empty"),
|
||||
passwordDoesNotMatch: t("password_not_match"),
|
||||
EmailAlreadyExist: t("email_already_exist"),
|
||||
emailNotExist: t("email_not_exist"),
|
||||
signupError: t("signup_error"),
|
||||
signinError: t("signin_error"),
|
||||
title: t('signin'),
|
||||
linkTitle: t('or_signup'),
|
||||
submitTitle: t('signin'),
|
||||
email: t('email'),
|
||||
username: t('username'),
|
||||
password: t('password'),
|
||||
confirmPassword: t('confirm_password'),
|
||||
invalidEmail: t('invalid_email'),
|
||||
invalidPassword: t('invalid_password'),
|
||||
usernameEmpty: t('username_empty'),
|
||||
passwordDoesNotMatch: t('password_not_match'),
|
||||
EmailAlreadyExist: t('email_already_exist'),
|
||||
emailNotExist: t('email_not_exist'),
|
||||
signupError: t('signup_error'),
|
||||
signinError: t('signin_error'),
|
||||
};
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import AuthPage from "../authPage";
|
||||
import { useTranslations } from "next-intl";
|
||||
import AuthPage from '../authPage';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function Page(params: { locale: string }) {
|
||||
const t = useTranslations("Auth");
|
||||
const t = useTranslations('Auth');
|
||||
const messages = {
|
||||
title: t("signup"),
|
||||
linkTitle: t("or_signin"),
|
||||
submitTitle: t("signup"),
|
||||
email: t("email"),
|
||||
username: t("username"),
|
||||
password: t("password"),
|
||||
confirmPassword: t("confirm_password"),
|
||||
invalidEmail: t("invalid_email"),
|
||||
invalidPassword: t("invalid_password"),
|
||||
usernameEmpty: t("username_empty"),
|
||||
passwordDoesNotMatch: t("password_not_match"),
|
||||
EmailAlreadyExist: t("email_already_exist"),
|
||||
emailNotExist: t("email_not_exist"),
|
||||
signupError: t("signup_error"),
|
||||
signinError: t("signin_error"),
|
||||
title: t('signup'),
|
||||
linkTitle: t('or_signin'),
|
||||
submitTitle: t('signup'),
|
||||
email: t('email'),
|
||||
username: t('username'),
|
||||
password: t('password'),
|
||||
confirmPassword: t('confirm_password'),
|
||||
invalidEmail: t('invalid_email'),
|
||||
invalidPassword: t('invalid_password'),
|
||||
usernameEmpty: t('username_empty'),
|
||||
passwordDoesNotMatch: t('password_not_match'),
|
||||
EmailAlreadyExist: t('email_already_exist'),
|
||||
emailNotExist: t('email_not_exist'),
|
||||
signupError: t('signup_error'),
|
||||
signinError: t('signin_error'),
|
||||
};
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function Error({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error;
|
||||
reset: () => void;
|
||||
}) {
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
useEffect(() => {
|
||||
// Log the error to an error reporting service
|
||||
console.error(error);
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import "@/styles/globals.css";
|
||||
import { fontSans } from "@/config/fonts";
|
||||
import { Providers } from "./providers";
|
||||
import Header from "./Header";
|
||||
import clsx from "clsx";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import '@/styles/globals.css';
|
||||
import { fontSans } from '@/config/fonts';
|
||||
import { Providers } from './providers';
|
||||
import Header from './Header';
|
||||
import clsx from 'clsx';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
export async function generateMetadata({ params: { locale } }) {
|
||||
const t = await getTranslations({ locale, namespace: "Header" });
|
||||
const t = await getTranslations({ locale, namespace: 'Header' });
|
||||
return {
|
||||
title: t("title"),
|
||||
description: t("description"),
|
||||
title: t('title'),
|
||||
description: t('description'),
|
||||
icons: {
|
||||
icon: "/favicon/favicon.ico",
|
||||
shortcut: "/favicon/favicon-16x16.png",
|
||||
apple: "/favicon/apple-touch-icon.png",
|
||||
icon: '/favicon/favicon.ico',
|
||||
shortcut: '/favicon/favicon-16x16.png',
|
||||
apple: '/favicon/apple-touch-icon.png',
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -28,16 +28,8 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang={locale} suppressHydrationWarning>
|
||||
<head />
|
||||
<body
|
||||
className={clsx(
|
||||
"min-h-[calc(100vh-64px)] bg-background font-sans antialiased",
|
||||
fontSans.variable
|
||||
)}
|
||||
>
|
||||
<Providers
|
||||
themeProps={{ attribute: "class", defaultTheme: "light" }}
|
||||
tokenProps={{ locale: locale }}
|
||||
>
|
||||
<body className={clsx('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">
|
||||
<Header locale={locale} />
|
||||
<main>{children}</main>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Divider } from "@nextui-org/react";
|
||||
import PaneMainTitle from "./PaneMainTitle";
|
||||
import PaneMainFeatures from "./PaneMainFeatures";
|
||||
import PaneDemoImages from "./PaneDemoImages";
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Divider } from '@nextui-org/react';
|
||||
import PaneMainTitle from './PaneMainTitle';
|
||||
import PaneMainFeatures from './PaneMainFeatures';
|
||||
import PaneDemoImages from './PaneDemoImages';
|
||||
|
||||
export default function Home(params: { locale: string }) {
|
||||
const t = useTranslations("Index");
|
||||
const t = useTranslations('Index');
|
||||
|
||||
const messages = {
|
||||
title: t("organize_test_cases"),
|
||||
caseEdit: t("case_edit"),
|
||||
caseHome: t("case_home"),
|
||||
caseRun: t("case_run"),
|
||||
title: t('organize_test_cases'),
|
||||
caseEdit: t('case_edit'),
|
||||
caseHome: t('case_home'),
|
||||
caseRun: t('case_run'),
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -24,30 +24,30 @@ export default function Home(params: { locale: string }) {
|
||||
<div className="w-full md:w-5/12 p-4">
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
textAlign: "center",
|
||||
position: 'relative',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "10rem",
|
||||
position: "relative",
|
||||
fontSize: '10rem',
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
marginLeft: "2.5rem",
|
||||
marginLeft: '2.5rem',
|
||||
}}
|
||||
>
|
||||
⚗️
|
||||
</span>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
filter: "blur(48px)",
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
filter: 'blur(48px)',
|
||||
zIndex: 0,
|
||||
background: "linear-gradient(to bottom, #ffecd2, #fcb69f)",
|
||||
background: 'linear-gradient(to bottom, #ffecd2, #fcb69f)',
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
@@ -57,7 +57,7 @@ export default function Home(params: { locale: string }) {
|
||||
<div
|
||||
className="flex flex-wrap flex-col items-center"
|
||||
style={{
|
||||
marginTop: "6rem",
|
||||
marginTop: '6rem',
|
||||
}}
|
||||
>
|
||||
<PaneMainFeatures />
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Textarea,
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
} from "@nextui-org/react";
|
||||
import { ProjectType, ProjectsMessages } from "@/types/project";
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button, Input, Textarea, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from '@nextui-org/react';
|
||||
import { ProjectType, ProjectsMessages } from '@/types/project';
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
@@ -21,23 +12,17 @@ type Props = {
|
||||
messages: ProjectsMessages;
|
||||
};
|
||||
|
||||
export default function ProjectDialog({
|
||||
isOpen,
|
||||
editingProject,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
messages,
|
||||
}: Props) {
|
||||
export default function ProjectDialog({ isOpen, editingProject, onCancel, onSubmit, messages }: Props) {
|
||||
const [projectName, setProjectName] = useState({
|
||||
text: editingProject ? editingProject.name : "",
|
||||
text: editingProject ? editingProject.name : '',
|
||||
isValid: false,
|
||||
errorMessage: "",
|
||||
errorMessage: '',
|
||||
});
|
||||
|
||||
const [projectDetail, setProjectDetail] = useState({
|
||||
text: editingProject ? editingProject.detail : "",
|
||||
text: editingProject ? editingProject.detail : '',
|
||||
isValid: false,
|
||||
errorMessage: "",
|
||||
errorMessage: '',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -49,17 +34,17 @@ export default function ProjectDialog({
|
||||
|
||||
setProjectDetail({
|
||||
...projectDetail,
|
||||
text: editingProject.detail ? editingProject.detail : "",
|
||||
text: editingProject.detail ? editingProject.detail : '',
|
||||
});
|
||||
} else {
|
||||
setProjectName({
|
||||
...projectName,
|
||||
text: "",
|
||||
text: '',
|
||||
});
|
||||
|
||||
setProjectDetail({
|
||||
...projectDetail,
|
||||
text: "",
|
||||
text: '',
|
||||
});
|
||||
}
|
||||
}, [editingProject]);
|
||||
@@ -67,20 +52,20 @@ export default function ProjectDialog({
|
||||
const clear = () => {
|
||||
setProjectName({
|
||||
isValid: false,
|
||||
text: "",
|
||||
errorMessage: "",
|
||||
text: '',
|
||||
errorMessage: '',
|
||||
});
|
||||
setProjectDetail({
|
||||
isValid: false,
|
||||
text: "",
|
||||
errorMessage: "",
|
||||
text: '',
|
||||
errorMessage: '',
|
||||
});
|
||||
};
|
||||
|
||||
const validate = () => {
|
||||
if (!projectName.text) {
|
||||
setProjectName({
|
||||
text: "",
|
||||
text: '',
|
||||
isValid: false,
|
||||
errorMessage: messages.pleaseEnter,
|
||||
});
|
||||
@@ -100,9 +85,7 @@ export default function ProjectDialog({
|
||||
}}
|
||||
>
|
||||
<ModalContent>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
{messages.project}
|
||||
</ModalHeader>
|
||||
<ModalHeader className="flex flex-col gap-1">{messages.project}</ModalHeader>
|
||||
<ModalBody>
|
||||
<Input
|
||||
type="text"
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "@nextui-org/react";
|
||||
import { Plus } from "lucide-react";
|
||||
import { ProjectType, ProjectsMessages } from "@/types/project";
|
||||
import ProjectsTable from "./ProjectsTable";
|
||||
import ProjectDialog from "./ProjectDialog";
|
||||
import {
|
||||
fetchProjects,
|
||||
createProject,
|
||||
updateProject,
|
||||
deleteProject,
|
||||
} from "./projectsControl";
|
||||
'use client';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button } from '@nextui-org/react';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { ProjectType, ProjectsMessages } from '@/types/project';
|
||||
import ProjectsTable from './ProjectsTable';
|
||||
import ProjectDialog from './ProjectDialog';
|
||||
import { fetchProjects, createProject, updateProject, deleteProject } from './projectsControl';
|
||||
|
||||
export type Props = {
|
||||
messages: ProjectsMessages;
|
||||
@@ -26,7 +21,7 @@ export default function ProjectsPage({ messages, locale }: Props) {
|
||||
const data = await fetchProjects();
|
||||
setProjects(data);
|
||||
} 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
|
||||
const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false);
|
||||
const [editingProject, setEditingProject] = useState<ProjectType | null>(
|
||||
null
|
||||
);
|
||||
const [editingProject, setEditingProject] = useState<ProjectType | null>(null);
|
||||
const openDialogForCreate = () => {
|
||||
setIsProjectDialogOpen(true);
|
||||
setEditingProject(null);
|
||||
@@ -50,14 +43,8 @@ export default function ProjectsPage({ messages, locale }: Props) {
|
||||
|
||||
const onSubmit = async (name: string, detail: string) => {
|
||||
if (editingProject) {
|
||||
const updatedProject = await updateProject(
|
||||
editingProject.id,
|
||||
name,
|
||||
detail
|
||||
);
|
||||
const updatedProjects = projects.map((project) =>
|
||||
project.id === updatedProject.id ? updatedProject : project
|
||||
);
|
||||
const updatedProject = await updateProject(editingProject.id, name, detail);
|
||||
const updatedProjects = projects.map((project) => (project.id === updatedProject.id ? updatedProject : project));
|
||||
setProjects(updatedProjects);
|
||||
} else {
|
||||
const newProject = await createProject(name, detail);
|
||||
@@ -76,7 +63,7 @@ export default function ProjectsPage({ messages, locale }: Props) {
|
||||
await deleteProject(projectId);
|
||||
setProjects(projects.filter((project) => project.id !== projectId));
|
||||
} 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">
|
||||
<h3 className="font-bold">{messages.projectList}</h3>
|
||||
<div>
|
||||
<Button
|
||||
startContent={<Plus size={16} />}
|
||||
size="sm"
|
||||
color="primary"
|
||||
onClick={openDialogForCreate}
|
||||
>
|
||||
<Button startContent={<Plus size={16} />} size="sm" color="primary" onClick={openDialogForCreate}>
|
||||
{messages.newProject}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useMemo, useCallback } from "react";
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
@@ -12,11 +12,11 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownItem,
|
||||
SortDescriptor,
|
||||
} from "@nextui-org/react";
|
||||
import { Link, NextUiLinkClasses } from "@/src/navigation";
|
||||
import { MoreVertical } from "lucide-react";
|
||||
import { ProjectType, ProjectsMessages } from "@/types/project";
|
||||
import dayjs from "dayjs";
|
||||
} from '@nextui-org/react';
|
||||
import { Link, NextUiLinkClasses } from '@/src/navigation';
|
||||
import { MoreVertical } from 'lucide-react';
|
||||
import { ProjectType, ProjectsMessages } from '@/types/project';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
type Props = {
|
||||
projects: ProjectType[];
|
||||
@@ -26,24 +26,18 @@ type Props = {
|
||||
locale: string;
|
||||
};
|
||||
|
||||
export default function ProjectsTable({
|
||||
projects,
|
||||
onEditProject,
|
||||
onDeleteProject,
|
||||
messages,
|
||||
locale,
|
||||
}: Props) {
|
||||
export default function ProjectsTable({ projects, onEditProject, onDeleteProject, messages, locale }: Props) {
|
||||
const headerColumns = [
|
||||
{ name: messages.id, uid: "id", sortable: true },
|
||||
{ name: messages.name, uid: "name", sortable: true },
|
||||
{ name: messages.detail, uid: "detail", sortable: true },
|
||||
{ name: messages.lastUpdate, uid: "updatedAt", sortable: true },
|
||||
{ name: messages.actions, uid: "actions" },
|
||||
{ name: messages.id, uid: 'id', sortable: true },
|
||||
{ name: messages.name, uid: 'name', sortable: true },
|
||||
{ name: messages.detail, uid: 'detail', sortable: true },
|
||||
{ name: messages.lastUpdate, uid: 'updatedAt', sortable: true },
|
||||
{ name: messages.actions, uid: 'actions' },
|
||||
];
|
||||
|
||||
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
|
||||
column: "id",
|
||||
direction: "ascending",
|
||||
column: 'id',
|
||||
direction: 'ascending',
|
||||
});
|
||||
|
||||
const sortedItems = useMemo(() => {
|
||||
@@ -52,31 +46,27 @@ export default function ProjectsTable({
|
||||
const second = b[sortDescriptor.column as keyof ProjectType] as number;
|
||||
const cmp = first < second ? -1 : first > second ? 1 : 0;
|
||||
|
||||
return sortDescriptor.direction === "descending" ? -cmp : cmp;
|
||||
return sortDescriptor.direction === 'descending' ? -cmp : cmp;
|
||||
});
|
||||
}, [sortDescriptor, projects]);
|
||||
|
||||
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 cellValue = project[columnKey as keyof ProjectType];
|
||||
|
||||
switch (columnKey) {
|
||||
case "id":
|
||||
case 'id':
|
||||
return <span>{cellValue}</span>;
|
||||
case "name":
|
||||
case 'name':
|
||||
return (
|
||||
<Link
|
||||
href={`/projects/${project.id}/home`}
|
||||
locale={locale}
|
||||
className={NextUiLinkClasses}
|
||||
>
|
||||
<Link href={`/projects/${project.id}/home`} locale={locale} className={NextUiLinkClasses}>
|
||||
{cellValue}
|
||||
</Link>
|
||||
);
|
||||
case "detail":
|
||||
case 'detail':
|
||||
const maxLength = 20;
|
||||
const truncatedValue = truncateText(cellValue, maxLength);
|
||||
return (
|
||||
@@ -84,9 +74,9 @@ export default function ProjectsTable({
|
||||
<div>{truncatedValue}</div>
|
||||
</div>
|
||||
);
|
||||
case "updatedAt":
|
||||
return <span>{dayjs(cellValue).format("YYYY/MM/DD HH:mm")}</span>;
|
||||
case "actions":
|
||||
case 'updatedAt':
|
||||
return <span>{dayjs(cellValue).format('YYYY/MM/DD HH:mm')}</span>;
|
||||
case 'actions':
|
||||
return (
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
@@ -95,13 +85,8 @@ export default function ProjectsTable({
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="project actions">
|
||||
<DropdownItem onClick={() => onEditProject(project)}>
|
||||
{messages.editProject}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
className="text-danger"
|
||||
onClick={() => onDeleteProject(project.id)}
|
||||
>
|
||||
<DropdownItem onClick={() => onEditProject(project)}>{messages.editProject}</DropdownItem>
|
||||
<DropdownItem className="text-danger" onClick={() => onDeleteProject(project.id)}>
|
||||
{messages.deleteProject}
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
@@ -114,18 +99,18 @@ export default function ProjectsTable({
|
||||
|
||||
const classNames = useMemo(
|
||||
() => ({
|
||||
wrapper: ["max-w-3xl"],
|
||||
th: ["bg-transparent", "text-default-500", "border-b", "border-divider"],
|
||||
wrapper: ['max-w-3xl'],
|
||||
th: ['bg-transparent', 'text-default-500', 'border-b', 'border-divider'],
|
||||
td: [
|
||||
// changing the rows border radius
|
||||
// first
|
||||
"group-data-[first=true]:first:before:rounded-none",
|
||||
"group-data-[first=true]:last:before:rounded-none",
|
||||
'group-data-[first=true]:first:before:rounded-none',
|
||||
'group-data-[first=true]:last:before:rounded-none',
|
||||
// middle
|
||||
"group-data-[middle=true]:before:rounded-none",
|
||||
'group-data-[middle=true]:before:rounded-none',
|
||||
// last
|
||||
"group-data-[last=true]:first:before:rounded-none",
|
||||
"group-data-[last=true]:last:before:rounded-none",
|
||||
'group-data-[last=true]:first:before:rounded-none',
|
||||
'group-data-[last=true]:last:before:rounded-none',
|
||||
],
|
||||
}),
|
||||
[]
|
||||
@@ -144,7 +129,7 @@ export default function ProjectsTable({
|
||||
{(column) => (
|
||||
<TableColumn
|
||||
key={column.uid}
|
||||
align={column.uid === "actions" ? "center" : "start"}
|
||||
align={column.uid === 'actions' ? 'center' : 'start'}
|
||||
allowsSorting={column.sortable}
|
||||
>
|
||||
{column.name}
|
||||
@@ -153,11 +138,7 @@ export default function ProjectsTable({
|
||||
</TableHeader>
|
||||
<TableBody emptyContent={messages.noProjectsFound} items={sortedItems}>
|
||||
{(item) => (
|
||||
<TableRow key={item.id}>
|
||||
{(columnKey) => (
|
||||
<TableCell>{renderCell(item, columnKey)}</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
<TableRow key={item.id}>{(columnKey) => <TableCell>{renderCell(item, columnKey)}</TableCell>}</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Listbox, ListboxItem } from "@nextui-org/react";
|
||||
import { Home, Files, FlaskConical } from "lucide-react";
|
||||
import { usePathname, useRouter } from "@/src/navigation";
|
||||
import useGetCurrentIds from "@/utils/useGetCurrentIds";
|
||||
import { ProjectMessages } from "@/types/project";
|
||||
'use client';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Listbox, ListboxItem } from '@nextui-org/react';
|
||||
import { Home, Files, FlaskConical } from 'lucide-react';
|
||||
import { usePathname, useRouter } from '@/src/navigation';
|
||||
import useGetCurrentIds from '@/utils/useGetCurrentIds';
|
||||
import { ProjectMessages } from '@/types/project';
|
||||
|
||||
export type Props = {
|
||||
messages: ProjectMessages;
|
||||
@@ -16,28 +16,28 @@ export default function Sidebar({ messages, locale }: Props) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const [currentKey, setCurrentTab] = useState("home");
|
||||
const baseClass = "p-3";
|
||||
const [currentKey, setCurrentTab] = useState('home');
|
||||
const baseClass = 'p-3';
|
||||
const selectedClass = `${baseClass} bg-neutral-200 dark:bg-neutral-700`;
|
||||
|
||||
const handleTabClick = (key: string) => {
|
||||
if (key === "home") {
|
||||
if (key === 'home') {
|
||||
router.push(`/projects/${projectId}/home`, { locale: locale });
|
||||
} else if (key === "cases") {
|
||||
} else if (key === 'cases') {
|
||||
router.push(`/projects/${projectId}/folders`, { locale: locale });
|
||||
} else if (key === "runs") {
|
||||
} else if (key === 'runs') {
|
||||
router.push(`/projects/${projectId}/runs`, { locale: locale });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleRouteChange = (currentPath: string) => {
|
||||
if (currentPath.includes("home")) {
|
||||
setCurrentTab("home");
|
||||
} else if (currentPath.includes("folders")) {
|
||||
setCurrentTab("cases");
|
||||
} else if (currentPath.includes("runs")) {
|
||||
setCurrentTab("runs");
|
||||
if (currentPath.includes('home')) {
|
||||
setCurrentTab('home');
|
||||
} else if (currentPath.includes('folders')) {
|
||||
setCurrentTab('cases');
|
||||
} else if (currentPath.includes('runs')) {
|
||||
setCurrentTab('runs');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -46,17 +46,17 @@ export default function Sidebar({ messages, locale }: Props) {
|
||||
|
||||
const tabItems = [
|
||||
{
|
||||
key: "home",
|
||||
key: 'home',
|
||||
text: messages.home,
|
||||
startContent: <Home strokeWidth={1} size={28} />,
|
||||
},
|
||||
{
|
||||
key: "cases",
|
||||
key: 'cases',
|
||||
text: messages.testCases,
|
||||
startContent: <Files strokeWidth={1} size={28} />,
|
||||
},
|
||||
{
|
||||
key: "runs",
|
||||
key: 'runs',
|
||||
text: messages.testRuns,
|
||||
startContent: <FlaskConical strokeWidth={1} size={28} />,
|
||||
},
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Textarea,
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
} from "@nextui-org/react";
|
||||
import { FolderType, FoldersMessages } from "@/types/folder";
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button, Input, Textarea, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from '@nextui-org/react';
|
||||
import { FolderType, FoldersMessages } from '@/types/folder';
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
@@ -21,23 +12,17 @@ type Props = {
|
||||
messages: FoldersMessages;
|
||||
};
|
||||
|
||||
export default function FolderDialog({
|
||||
isOpen,
|
||||
editingFolder,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
messages,
|
||||
}: Props) {
|
||||
export default function FolderDialog({ isOpen, editingFolder, onCancel, onSubmit, messages }: Props) {
|
||||
const [folderName, setFolderName] = useState({
|
||||
text: editingFolder ? editingFolder.name : "",
|
||||
text: editingFolder ? editingFolder.name : '',
|
||||
isValid: false,
|
||||
errorMessage: "",
|
||||
errorMessage: '',
|
||||
});
|
||||
|
||||
const [folderDetail, setFolderDetail] = useState({
|
||||
text: editingFolder ? editingFolder.detail : "",
|
||||
text: editingFolder ? editingFolder.detail : '',
|
||||
isValid: false,
|
||||
errorMessage: "",
|
||||
errorMessage: '',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -49,17 +34,17 @@ export default function FolderDialog({
|
||||
|
||||
setFolderDetail({
|
||||
...folderDetail,
|
||||
text: editingFolder.detail ? editingFolder.detail : "",
|
||||
text: editingFolder.detail ? editingFolder.detail : '',
|
||||
});
|
||||
} else {
|
||||
setFolderName({
|
||||
...folderName,
|
||||
text: "",
|
||||
text: '',
|
||||
});
|
||||
|
||||
setFolderDetail({
|
||||
...folderDetail,
|
||||
text: "",
|
||||
text: '',
|
||||
});
|
||||
}
|
||||
}, [editingFolder]);
|
||||
@@ -67,20 +52,20 @@ export default function FolderDialog({
|
||||
const clear = () => {
|
||||
setFolderName({
|
||||
isValid: false,
|
||||
text: "",
|
||||
errorMessage: "",
|
||||
text: '',
|
||||
errorMessage: '',
|
||||
});
|
||||
setFolderDetail({
|
||||
isValid: false,
|
||||
text: "",
|
||||
errorMessage: "",
|
||||
text: '',
|
||||
errorMessage: '',
|
||||
});
|
||||
};
|
||||
|
||||
const validate = () => {
|
||||
if (!folderName.text) {
|
||||
setFolderName({
|
||||
text: "",
|
||||
text: '',
|
||||
isValid: false,
|
||||
errorMessage: messages.pleaseEnter,
|
||||
});
|
||||
@@ -100,9 +85,7 @@ export default function FolderDialog({
|
||||
}}
|
||||
>
|
||||
<ModalContent>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
{messages.folder}
|
||||
</ModalHeader>
|
||||
<ModalHeader className="flex flex-col gap-1">{messages.folder}</ModalHeader>
|
||||
<ModalBody>
|
||||
<Input
|
||||
type="text"
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
DropdownTrigger,
|
||||
DropdownMenu,
|
||||
DropdownItem,
|
||||
} from "@nextui-org/react";
|
||||
import { MoreVertical } from "lucide-react";
|
||||
import { FolderType, FoldersMessages } from "@/types/folder";
|
||||
import { Button, Dropdown, DropdownTrigger, DropdownMenu, DropdownItem } from '@nextui-org/react';
|
||||
import { MoreVertical } from 'lucide-react';
|
||||
import { FolderType, FoldersMessages } from '@/types/folder';
|
||||
|
||||
type Props = {
|
||||
folder: FolderType;
|
||||
@@ -15,12 +9,7 @@ type Props = {
|
||||
messages: FoldersMessages;
|
||||
};
|
||||
|
||||
export default function FolderEditMenu({
|
||||
folder,
|
||||
onEditClick,
|
||||
onDeleteClick,
|
||||
messages,
|
||||
}: Props) {
|
||||
export default function FolderEditMenu({ folder, onEditClick, onDeleteClick, messages }: Props) {
|
||||
return (
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
@@ -32,12 +21,7 @@ export default function FolderEditMenu({
|
||||
<DropdownItem key="edit" onClick={() => onEditClick(folder)}>
|
||||
{messages.editFolder}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="delete"
|
||||
className="text-danger"
|
||||
color="danger"
|
||||
onClick={() => onDeleteClick(folder.id)}
|
||||
>
|
||||
<DropdownItem key="delete" className="text-danger" color="danger" onClick={() => onDeleteClick(folder.id)}>
|
||||
{messages.deleteFolder}
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { FolderType, FoldersMessages } from "@/types/folder";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button, Listbox, ListboxItem } from "@nextui-org/react";
|
||||
import { Folder, Plus } from "lucide-react";
|
||||
import { usePathname, useRouter } from "@/src/navigation";
|
||||
import useGetCurrentIds from "@/utils/useGetCurrentIds";
|
||||
import FolderDialog from "./FolderDialog";
|
||||
import FolderEditMenu from "./FolderEditMenu";
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { FolderType, FoldersMessages } from '@/types/folder';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button, Listbox, ListboxItem } from '@nextui-org/react';
|
||||
import { Folder, Plus } from 'lucide-react';
|
||||
import { usePathname, useRouter } from '@/src/navigation';
|
||||
import useGetCurrentIds from '@/utils/useGetCurrentIds';
|
||||
import FolderDialog from './FolderDialog';
|
||||
import FolderEditMenu from './FolderEditMenu';
|
||||
|
||||
import {
|
||||
fetchFolders,
|
||||
createFolder,
|
||||
updateFolder,
|
||||
deleteFolder,
|
||||
} from "./foldersControl";
|
||||
import { fetchFolders, createFolder, updateFolder, deleteFolder } from './foldersControl';
|
||||
|
||||
type Props = {
|
||||
projectId: string;
|
||||
@@ -45,16 +40,8 @@ export default function FoldersPane({ projectId, messages, locale }: Props) {
|
||||
|
||||
const onSubmit = async (name: string, detail: string) => {
|
||||
if (editingFolder) {
|
||||
const updatedProject = await updateFolder(
|
||||
editingFolder.id,
|
||||
name,
|
||||
detail,
|
||||
projectId,
|
||||
null
|
||||
);
|
||||
const updatedProjects = folders.map((project) =>
|
||||
project.id === updatedProject.id ? updatedProject : project
|
||||
);
|
||||
const updatedProject = await updateFolder(editingFolder.id, name, detail, projectId, null);
|
||||
const updatedProjects = folders.map((project) => (project.id === updatedProject.id ? updatedProject : project));
|
||||
setFolders(updatedProjects);
|
||||
} else {
|
||||
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);
|
||||
setFolders(data);
|
||||
|
||||
const selectedFolderFromUrl = data.find(
|
||||
(folder) => folder.id === folderId
|
||||
);
|
||||
const selectedFolderFromUrl = data.find((folder) => folder.id === folderId);
|
||||
setSelectedFolder(selectedFolderFromUrl);
|
||||
|
||||
// Redirect to the smallest folder ID page if the path is "projects/[projectId]/folders
|
||||
if (pathname === `/projects/${projectId}/folders`) {
|
||||
const smallestFolderId = Math.min(...data.map((folder) => folder.id));
|
||||
router.push(
|
||||
`/projects/${projectId}/folders/${smallestFolderId}/cases`,
|
||||
{ locale: locale }
|
||||
);
|
||||
router.push(`/projects/${projectId}/folders/${smallestFolderId}/cases`, { locale: locale });
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Error in effect:", error.message);
|
||||
console.error('Error in effect:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
fetchDataEffect();
|
||||
}, [folderId]);
|
||||
|
||||
const baseClass = "";
|
||||
const baseClass = '';
|
||||
const selectedClass = `${baseClass} bg-neutral-200 dark:bg-neutral-700`;
|
||||
|
||||
return (
|
||||
@@ -119,18 +101,9 @@ export default function FoldersPane({ projectId, messages, locale }: Props) {
|
||||
{folders.map((folder, index) => (
|
||||
<ListboxItem
|
||||
key={index}
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/projects/${projectId}/folders/${folder.id}/cases`,
|
||||
{ locale: locale }
|
||||
)
|
||||
}
|
||||
onClick={() => router.push(`/projects/${projectId}/folders/${folder.id}/cases`, { locale: locale })}
|
||||
startContent={<Folder size={20} color="#F7C24E" fill="#F7C24E" />}
|
||||
className={
|
||||
selectedFolder && folder.id === selectedFolder.id
|
||||
? selectedClass
|
||||
: baseClass
|
||||
}
|
||||
className={selectedFolder && folder.id === selectedFolder.id ? selectedClass : baseClass}
|
||||
endContent={
|
||||
<FolderEditMenu
|
||||
folder={folder}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import TestCaseTable from "./TestCaseTable";
|
||||
import { fetchCases, createCase, deleteCase, deleteCases } from "./caseControl";
|
||||
import { CasesMessages } from "@/types/case";
|
||||
'use client';
|
||||
import { useEffect, useState } from 'react';
|
||||
import TestCaseTable from './TestCaseTable';
|
||||
import { fetchCases, createCase, deleteCase, deleteCases } from './caseControl';
|
||||
import { CasesMessages } from '@/types/case';
|
||||
|
||||
type Props = {
|
||||
projectId: string;
|
||||
@@ -11,12 +11,7 @@ type Props = {
|
||||
locale: string;
|
||||
};
|
||||
|
||||
export default function CasesPane({
|
||||
projectId,
|
||||
folderId,
|
||||
messages,
|
||||
locale,
|
||||
}: Props) {
|
||||
export default function CasesPane({ projectId, folderId, messages, locale }: Props) {
|
||||
const [cases, setCases] = useState([]);
|
||||
useEffect(() => {
|
||||
async function fetchDataEffect() {
|
||||
@@ -24,7 +19,7 @@ export default function CasesPane({
|
||||
const data = await fetchCases(folderId);
|
||||
setCases(data);
|
||||
} 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 {
|
||||
Table,
|
||||
TableHeader,
|
||||
@@ -13,11 +13,11 @@ import {
|
||||
DropdownItem,
|
||||
Selection,
|
||||
SortDescriptor,
|
||||
} from "@nextui-org/react";
|
||||
import { Link, NextUiLinkClasses } from "@/src/navigation";
|
||||
import { Plus, MoreVertical, Trash, Circle } from "lucide-react";
|
||||
import { CasesMessages } from "@/types/case";
|
||||
import { priorities } from "@/config/selection";
|
||||
} from '@nextui-org/react';
|
||||
import { Link, NextUiLinkClasses } from '@/src/navigation';
|
||||
import { Plus, MoreVertical, Trash, Circle } from 'lucide-react';
|
||||
import { CasesMessages } from '@/types/case';
|
||||
import { priorities } from '@/config/selection';
|
||||
|
||||
type Case = {
|
||||
id: number;
|
||||
@@ -55,16 +55,16 @@ export default function TestCaseTable({
|
||||
locale,
|
||||
}: Props) {
|
||||
const headerColumns = [
|
||||
{ name: messages.id, uid: "id", sortable: true },
|
||||
{ name: messages.title, uid: "title", sortable: true },
|
||||
{ name: messages.priority, uid: "priority", sortable: true },
|
||||
{ name: messages.actions, uid: "actions" },
|
||||
{ name: messages.id, uid: 'id', sortable: true },
|
||||
{ name: messages.title, uid: 'title', sortable: true },
|
||||
{ name: messages.priority, uid: 'priority', sortable: true },
|
||||
{ name: messages.actions, uid: 'actions' },
|
||||
];
|
||||
|
||||
const [selectedKeys, setSelectedKeys] = useState<Selection>(new Set([]));
|
||||
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
|
||||
column: "id",
|
||||
direction: "ascending",
|
||||
column: 'id',
|
||||
direction: 'ascending',
|
||||
});
|
||||
|
||||
const sortedItems = useMemo(() => {
|
||||
@@ -73,7 +73,7 @@ export default function TestCaseTable({
|
||||
const second = b[sortDescriptor.column as keyof Case] as number;
|
||||
const cmp = first < second ? -1 : first > second ? 1 : 0;
|
||||
|
||||
return sortDescriptor.direction === "descending" ? -cmp : cmp;
|
||||
return sortDescriptor.direction === 'descending' ? -cmp : cmp;
|
||||
});
|
||||
}, [sortDescriptor, cases]);
|
||||
|
||||
@@ -81,15 +81,11 @@ export default function TestCaseTable({
|
||||
const cellValue = testCase[columnKey as keyof Case];
|
||||
|
||||
switch (columnKey) {
|
||||
case "id":
|
||||
case 'id':
|
||||
return <span>{cellValue}</span>;
|
||||
case "title":
|
||||
case 'title':
|
||||
return (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="light"
|
||||
className="data-[hover=true]:bg-transparent"
|
||||
>
|
||||
<Button size="sm" variant="light" className="data-[hover=true]:bg-transparent">
|
||||
<Link
|
||||
href={`/projects/${projectId}/folders/${testCase.folderId}/cases/${testCase.id}`}
|
||||
locale={locale}
|
||||
@@ -99,18 +95,14 @@ export default function TestCaseTable({
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
case "priority":
|
||||
case 'priority':
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<Circle
|
||||
size={8}
|
||||
color={priorities[cellValue].color}
|
||||
fill={priorities[cellValue].color}
|
||||
/>
|
||||
<Circle size={8} color={priorities[cellValue].color} fill={priorities[cellValue].color} />
|
||||
<div className="ms-3">{messages[priorities[cellValue].uid]}</div>
|
||||
</div>
|
||||
);
|
||||
case "actions":
|
||||
case 'actions':
|
||||
return (
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
@@ -119,10 +111,7 @@ export default function TestCaseTable({
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="test case actions">
|
||||
<DropdownItem
|
||||
className="text-danger"
|
||||
onClick={() => onDeleteCase(testCase.id)}
|
||||
>
|
||||
<DropdownItem className="text-danger" onClick={() => onDeleteCase(testCase.id)}>
|
||||
{messages.deleteCase}
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
@@ -135,25 +124,25 @@ export default function TestCaseTable({
|
||||
|
||||
const classNames = useMemo(
|
||||
() => ({
|
||||
wrapper: ["max-w-3xl"],
|
||||
th: ["bg-transparent", "text-default-500", "border-b", "border-divider"],
|
||||
wrapper: ['max-w-3xl'],
|
||||
th: ['bg-transparent', 'text-default-500', 'border-b', 'border-divider'],
|
||||
td: [
|
||||
// changing the rows border radius
|
||||
// first
|
||||
"group-data-[first=true]:first:before:rounded-none",
|
||||
"group-data-[first=true]:last:before:rounded-none",
|
||||
'group-data-[first=true]:first:before:rounded-none',
|
||||
'group-data-[first=true]:last:before:rounded-none',
|
||||
// middle
|
||||
"group-data-[middle=true]:before:rounded-none",
|
||||
'group-data-[middle=true]:before:rounded-none',
|
||||
// last
|
||||
"group-data-[last=true]:first:before:rounded-none",
|
||||
"group-data-[last=true]:last:before:rounded-none",
|
||||
'group-data-[last=true]:first:before:rounded-none',
|
||||
'group-data-[last=true]:last:before:rounded-none',
|
||||
],
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const onDeleteCasesClick = async () => {
|
||||
if (selectedKeys === "all") {
|
||||
if (selectedKeys === 'all') {
|
||||
const allKeys = sortedItems.map((item) => item.id);
|
||||
onDeleteCases(allKeys);
|
||||
} 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">
|
||||
<h3 className="font-bold">{messages.testCaseList}</h3>
|
||||
<div>
|
||||
{(selectedKeys.size > 0 || selectedKeys === "all") && (
|
||||
{(selectedKeys.size > 0 || selectedKeys === 'all') && (
|
||||
<Button
|
||||
startContent={<Trash size={16} />}
|
||||
size="sm"
|
||||
@@ -178,12 +167,7 @@ export default function TestCaseTable({
|
||||
{messages.delete}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
startContent={<Plus size={16} />}
|
||||
size="sm"
|
||||
color="primary"
|
||||
onClick={onCreateCase}
|
||||
>
|
||||
<Button startContent={<Plus size={16} />} size="sm" color="primary" onClick={onCreateCase}>
|
||||
{messages.newTestCase}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -204,7 +188,7 @@ export default function TestCaseTable({
|
||||
{(column) => (
|
||||
<TableColumn
|
||||
key={column.uid}
|
||||
align={column.uid === "actions" ? "center" : "start"}
|
||||
align={column.uid === 'actions' ? 'center' : 'start'}
|
||||
allowsSorting={column.sortable}
|
||||
>
|
||||
{column.name}
|
||||
@@ -213,11 +197,7 @@ export default function TestCaseTable({
|
||||
</TableHeader>
|
||||
<TableBody emptyContent={messages.noCasesFound} items={sortedItems}>
|
||||
{(item) => (
|
||||
<TableRow key={item.id}>
|
||||
{(columnKey) => (
|
||||
<TableCell>{renderCell(item, columnKey)}</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
<TableRow key={item.id}>{(columnKey) => <TableCell>{renderCell(item, columnKey)}</TableCell>}</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import { Image, Button, Tooltip, Card, CardBody } from "@nextui-org/react";
|
||||
import { AttachmentType, CaseMessages } from "@/types/case";
|
||||
import { Trash, ArrowDownToLine } from "lucide-react";
|
||||
import { isImage } from "./isImage";
|
||||
import { Image, Button, Tooltip, Card, CardBody } from '@nextui-org/react';
|
||||
import { AttachmentType, CaseMessages } from '@/types/case';
|
||||
import { Trash, ArrowDownToLine } from 'lucide-react';
|
||||
import { isImage } from './isImage';
|
||||
|
||||
type Props = {
|
||||
attachments: AttachmentType[];
|
||||
onAttachmentDownload: (
|
||||
attachmentId: number,
|
||||
downloadFileName: string
|
||||
) => void;
|
||||
onAttachmentDownload: (attachmentId: number, downloadFileName: string) => void;
|
||||
onAttachmentDelete: (attachmentId: number) => void;
|
||||
messages: CaseMessages,
|
||||
messages: CaseMessages;
|
||||
};
|
||||
|
||||
export default function CaseAttachmentsEditor({
|
||||
@@ -35,11 +32,7 @@ export default function CaseAttachmentsEditor({
|
||||
{images.map((image, index) => (
|
||||
<Card key={index} radius="sm" className="mt-2 me-2 max-w-md">
|
||||
<CardBody>
|
||||
<Image
|
||||
alt={image.title}
|
||||
src={image.path}
|
||||
className="object-cover h-40 w-40"
|
||||
/>
|
||||
<Image alt={image.title} src={image.path} className="object-cover h-40 w-40" />
|
||||
<div className="flex items-center justify-between">
|
||||
<p>{image.title}</p>
|
||||
<Tooltip content={messages.delete}>
|
||||
|
||||
@@ -1,39 +1,27 @@
|
||||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Input,
|
||||
Textarea,
|
||||
Select,
|
||||
SelectItem,
|
||||
Button,
|
||||
Divider,
|
||||
Tooltip,
|
||||
} from "@nextui-org/react";
|
||||
import { useRouter } from "@/src/navigation";
|
||||
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";
|
||||
'use client';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Input, Textarea, Select, SelectItem, Button, Divider, Tooltip } from '@nextui-org/react';
|
||||
import { useRouter } from '@/src/navigation';
|
||||
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 = {
|
||||
id: 0,
|
||||
title: "",
|
||||
title: '',
|
||||
state: 0,
|
||||
priority: 0,
|
||||
type: 0,
|
||||
automationStatus: 0,
|
||||
description: "",
|
||||
description: '',
|
||||
template: 0,
|
||||
preConditions: "",
|
||||
expectedResults: "",
|
||||
preConditions: '',
|
||||
expectedResults: '',
|
||||
folderId: 0,
|
||||
};
|
||||
|
||||
@@ -45,13 +33,7 @@ type Props = {
|
||||
locale: string;
|
||||
};
|
||||
|
||||
export default function CaseEditor({
|
||||
projectId,
|
||||
folderId,
|
||||
caseId,
|
||||
messages,
|
||||
locale,
|
||||
}: Props) {
|
||||
export default function CaseEditor({ projectId, folderId, caseId, messages, locale }: Props) {
|
||||
const [testCase, setTestCase] = useState<CaseType>(defaultTestCase);
|
||||
const [isTitleInvalid, setIsTitleInvalid] = useState<boolean>(false);
|
||||
const [isUpdating, setIsUpdating] = useState<boolean>(false);
|
||||
@@ -122,10 +104,7 @@ export default function CaseEditor({
|
||||
handleFetchCreateAttachments(caseId, event.target.files);
|
||||
};
|
||||
|
||||
const handleFetchCreateAttachments = async (
|
||||
caseId: number,
|
||||
files: File[]
|
||||
) => {
|
||||
const handleFetchCreateAttachments = async (caseId: number, files: File[]) => {
|
||||
const newAttachments = await fetchCreateAttachments(caseId, files);
|
||||
|
||||
if (newAttachments) {
|
||||
@@ -147,9 +126,7 @@ export default function CaseEditor({
|
||||
const onAttachmentDelete = async (attachmentId: number) => {
|
||||
await fetchDeleteAttachment(attachmentId);
|
||||
|
||||
const filteredAttachments = testCase.Attachments.filter(
|
||||
(attachment) => attachment.id !== attachmentId
|
||||
);
|
||||
const filteredAttachments = testCase.Attachments.filter((attachment) => attachment.id !== attachmentId);
|
||||
|
||||
setTestCase({
|
||||
...testCase,
|
||||
@@ -163,7 +140,7 @@ export default function CaseEditor({
|
||||
const data = await fetchCase(caseId);
|
||||
setTestCase(data);
|
||||
} 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
|
||||
size="sm"
|
||||
className="rounded-full bg-neutral-50 dark:bg-neutral-600"
|
||||
onPress={() =>
|
||||
router.push(
|
||||
`/projects/${projectId}/folders/${folderId}/cases`,
|
||||
{ locale: locale }
|
||||
)
|
||||
}
|
||||
onPress={() => router.push(`/projects/${projectId}/folders/${folderId}/cases`, { locale: locale })}
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
</Button>
|
||||
@@ -215,7 +187,7 @@ export default function CaseEditor({
|
||||
label={messages.title}
|
||||
value={testCase.title}
|
||||
isInvalid={isTitleInvalid}
|
||||
errorMessage={isTitleInvalid ? messages.pleaseEnterTitle : ""}
|
||||
errorMessage={isTitleInvalid ? messages.pleaseEnterTitle : ''}
|
||||
onChange={(e) => {
|
||||
setTestCase({ ...testCase, title: e.target.value });
|
||||
}}
|
||||
@@ -241,17 +213,11 @@ export default function CaseEditor({
|
||||
selectedKeys={[priorities[testCase.priority].uid]}
|
||||
onSelectionChange={(e) => {
|
||||
const selectedUid = e.anchorKey;
|
||||
const index = priorities.findIndex(
|
||||
(priority) => priority.uid === selectedUid
|
||||
);
|
||||
const index = priorities.findIndex((priority) => priority.uid === selectedUid);
|
||||
setTestCase({ ...testCase, priority: index });
|
||||
}}
|
||||
startContent={
|
||||
<Circle
|
||||
size={8}
|
||||
color={priorities[testCase.priority].color}
|
||||
fill={priorities[testCase.priority].color}
|
||||
/>
|
||||
<Circle size={8} color={priorities[testCase.priority].color} fill={priorities[testCase.priority].color} />
|
||||
}
|
||||
label={messages.priority}
|
||||
className="mt-3 max-w-xs"
|
||||
@@ -271,9 +237,7 @@ export default function CaseEditor({
|
||||
selectedKeys={[testTypes[testCase.type].uid]}
|
||||
onSelectionChange={(e) => {
|
||||
const selectedUid = e.anchorKey;
|
||||
const index = testTypes.findIndex(
|
||||
(type) => type.uid === selectedUid
|
||||
);
|
||||
const index = testTypes.findIndex((type) => type.uid === selectedUid);
|
||||
setTestCase({ ...testCase, type: index });
|
||||
}}
|
||||
label={messages.type}
|
||||
@@ -294,9 +258,7 @@ export default function CaseEditor({
|
||||
selectedKeys={[templates[testCase.template].uid]}
|
||||
onSelectionChange={(e) => {
|
||||
const selectedUid = e.anchorKey;
|
||||
const index = templates.findIndex(
|
||||
(template) => template.uid === selectedUid
|
||||
);
|
||||
const index = templates.findIndex((template) => template.uid === selectedUid);
|
||||
setTestCase({ ...testCase, template: index });
|
||||
}}
|
||||
label={messages.template}
|
||||
@@ -311,7 +273,7 @@ export default function CaseEditor({
|
||||
</div>
|
||||
|
||||
<Divider className="my-6" />
|
||||
{templates[testCase.template].uid === "text" ? (
|
||||
{templates[testCase.template].uid === 'text' ? (
|
||||
<div>
|
||||
<h6 className="font-bold">{messages.testDetail}</h6>
|
||||
<div className="flex">
|
||||
@@ -377,10 +339,9 @@ export default function CaseEditor({
|
||||
<h6 className="font-bold">{messages.attachments}</h6>
|
||||
<CaseAttachmentsEditor
|
||||
attachments={testCase.Attachments}
|
||||
onAttachmentDownload={(
|
||||
attachmentId: number,
|
||||
downloadFileName: string
|
||||
) => fetchDownloadAttachment(attachmentId, downloadFileName)}
|
||||
onAttachmentDownload={(attachmentId: number, downloadFileName: string) =>
|
||||
fetchDownloadAttachment(attachmentId, downloadFileName)
|
||||
}
|
||||
onAttachmentDelete={onAttachmentDelete}
|
||||
messages={messages}
|
||||
/>
|
||||
@@ -399,17 +360,9 @@ export default function CaseEditor({
|
||||
<span className="font-semibold">{messages.clickToUpload}</span>
|
||||
<span>{messages.orDragAndDrop}</span>
|
||||
</p>
|
||||
<p className="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
{messages.maxFileSize}: 50 MB
|
||||
</p>
|
||||
<p className="text-xs text-neutral-500 dark:text-neutral-400">{messages.maxFileSize}: 50 MB</p>
|
||||
</div>
|
||||
<input
|
||||
id="dropzone-file"
|
||||
type="file"
|
||||
className="hidden"
|
||||
onChange={handleInput}
|
||||
multiple
|
||||
/>
|
||||
<input id="dropzone-file" type="file" className="hidden" onChange={handleInput} multiple />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Textarea, Button, Tooltip } from "@nextui-org/react";
|
||||
import { CaseMessages, StepType } from "@/types/case";
|
||||
import { Plus, Trash } from "lucide-react";
|
||||
import { Textarea, Button, Tooltip } from '@nextui-org/react';
|
||||
import { CaseMessages, StepType } from '@/types/case';
|
||||
import { Plus, Trash } from 'lucide-react';
|
||||
|
||||
type Props = {
|
||||
steps: StepType[];
|
||||
@@ -10,13 +10,7 @@ type Props = {
|
||||
messages: CaseMessages;
|
||||
};
|
||||
|
||||
export default function StepsEditor({
|
||||
steps,
|
||||
onStepUpdate,
|
||||
onStepPlus,
|
||||
onStepDelete,
|
||||
messages,
|
||||
}: Props) {
|
||||
export default function StepsEditor({ steps, onStepUpdate, onStepPlus, onStepDelete, messages }: Props) {
|
||||
// sort steps by junction table's column
|
||||
const sortedSteps = steps.slice().sort((a, b) => {
|
||||
const stepNoA = a.caseSteps.stepNo;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import CaseEditor from "./CaseEditor";
|
||||
import { useTranslations } from "next-intl";
|
||||
import CaseEditor from './CaseEditor';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function Page({
|
||||
params,
|
||||
@@ -11,53 +11,53 @@ export default function Page({
|
||||
locale: string;
|
||||
};
|
||||
}) {
|
||||
const t = useTranslations("Case");
|
||||
const t = useTranslations('Case');
|
||||
const messages = {
|
||||
backToCases: t("back_to_cases"),
|
||||
updating: t("updating"),
|
||||
update: t("update"),
|
||||
basic: t("basic"),
|
||||
title: t("title"),
|
||||
pleaseEnterTitle: t("please_enter_title"),
|
||||
description: t("description"),
|
||||
testCaseDescription: t("test_case_description"),
|
||||
priority: t("priority"),
|
||||
critical: t("critical"),
|
||||
high: t("high"),
|
||||
medium: t("medium"),
|
||||
low: t("low"),
|
||||
type: t("type"),
|
||||
other: t("other"),
|
||||
security: t("security"),
|
||||
performance: t("performance"),
|
||||
accessibility: t("accessibility"),
|
||||
functional: t("functional"),
|
||||
acceptance: t("acceptance"),
|
||||
usability: t("usability"),
|
||||
smokeSanity: t("smoke_sanity"),
|
||||
compatibility: t("compatibility"),
|
||||
destructive: t("destructive"),
|
||||
regression: t("regression"),
|
||||
automated: t("automated"),
|
||||
manual: t("manual"),
|
||||
template: t("template"),
|
||||
testDetail: t("test_detail"),
|
||||
preconditions: t("preconditions"),
|
||||
step: t("step"),
|
||||
text: t("text"),
|
||||
steps: t("steps"),
|
||||
newStep: t("new_step"),
|
||||
detailsOfTheStep: t("details_of_the_step"),
|
||||
expectedResult: t("expected_result"),
|
||||
deleteThisStep: t("delete_this_step"),
|
||||
insertStep: t("insert_step"),
|
||||
attachments: t("attachments"),
|
||||
delete: t("delete"),
|
||||
download: t("download"),
|
||||
deleteFile: t("delete_file"),
|
||||
clickToUpload: t("click_to_upload"),
|
||||
orDragAndDrop: t("or_drag_and_drop"),
|
||||
maxFileSize: t("max_file_size"),
|
||||
backToCases: t('back_to_cases'),
|
||||
updating: t('updating'),
|
||||
update: t('update'),
|
||||
basic: t('basic'),
|
||||
title: t('title'),
|
||||
pleaseEnterTitle: t('please_enter_title'),
|
||||
description: t('description'),
|
||||
testCaseDescription: t('test_case_description'),
|
||||
priority: t('priority'),
|
||||
critical: t('critical'),
|
||||
high: t('high'),
|
||||
medium: t('medium'),
|
||||
low: t('low'),
|
||||
type: t('type'),
|
||||
other: t('other'),
|
||||
security: t('security'),
|
||||
performance: t('performance'),
|
||||
accessibility: t('accessibility'),
|
||||
functional: t('functional'),
|
||||
acceptance: t('acceptance'),
|
||||
usability: t('usability'),
|
||||
smokeSanity: t('smoke_sanity'),
|
||||
compatibility: t('compatibility'),
|
||||
destructive: t('destructive'),
|
||||
regression: t('regression'),
|
||||
automated: t('automated'),
|
||||
manual: t('manual'),
|
||||
template: t('template'),
|
||||
testDetail: t('test_detail'),
|
||||
preconditions: t('preconditions'),
|
||||
step: t('step'),
|
||||
text: t('text'),
|
||||
steps: t('steps'),
|
||||
newStep: t('new_step'),
|
||||
detailsOfTheStep: t('details_of_the_step'),
|
||||
expectedResult: t('expected_result'),
|
||||
deleteThisStep: t('delete_this_step'),
|
||||
insertStep: t('insert_step'),
|
||||
attachments: t('attachments'),
|
||||
delete: t('delete'),
|
||||
download: t('download'),
|
||||
deleteFile: t('delete_file'),
|
||||
clickToUpload: t('click_to_upload'),
|
||||
orDragAndDrop: t('or_drag_and_drop'),
|
||||
maxFileSize: t('max_file_size'),
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,37 +1,28 @@
|
||||
import CasesPane from "./CasesPane";
|
||||
import { useTranslations } from "next-intl";
|
||||
import CasesPane from './CasesPane';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function Page({
|
||||
params,
|
||||
}: {
|
||||
params: { projectId: string; folderId: string; locale: string };
|
||||
}) {
|
||||
const t = useTranslations("Cases");
|
||||
export default function Page({ params }: { params: { projectId: string; folderId: string; locale: string } }) {
|
||||
const t = useTranslations('Cases');
|
||||
const messages = {
|
||||
testCaseList: t("test_case_list"),
|
||||
id: t("id"),
|
||||
title: t("title"),
|
||||
priority: t("priority"),
|
||||
actions: t("actions"),
|
||||
deleteCase: t("delete_case"),
|
||||
delete: t("delete"),
|
||||
newTestCase: t("new_test_case"),
|
||||
status: t("status"),
|
||||
critical: t("critical"),
|
||||
high: t("high"),
|
||||
medium: t("medium"),
|
||||
low: t("low"),
|
||||
noCasesFound: t("no_cases_found"),
|
||||
testCaseList: t('test_case_list'),
|
||||
id: t('id'),
|
||||
title: t('title'),
|
||||
priority: t('priority'),
|
||||
actions: t('actions'),
|
||||
deleteCase: t('delete_case'),
|
||||
delete: t('delete'),
|
||||
newTestCase: t('new_test_case'),
|
||||
status: t('status'),
|
||||
critical: t('critical'),
|
||||
high: t('high'),
|
||||
medium: t('medium'),
|
||||
low: t('low'),
|
||||
noCasesFound: t('no_cases_found'),
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<CasesPane
|
||||
projectId={params.projectId}
|
||||
folderId={params.folderId}
|
||||
locale={params.locale}
|
||||
messages={messages}
|
||||
/>
|
||||
<CasesPane projectId={params.projectId} folderId={params.folderId} locale={params.locale} messages={messages} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import FoldersPane from "./FoldersPane";
|
||||
import { useTranslations } from "next-intl";
|
||||
import FoldersPane from './FoldersPane';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function FoldersLayout({
|
||||
children,
|
||||
@@ -8,27 +8,23 @@ export default function FoldersLayout({
|
||||
children: React.ReactNode;
|
||||
params: { projectId: string; locale: string };
|
||||
}) {
|
||||
const t = useTranslations("Folders");
|
||||
const t = useTranslations('Folders');
|
||||
const messages = {
|
||||
folder: t("folder"),
|
||||
newFolder: t("new_folder"),
|
||||
editFolder: t("edit_folder"),
|
||||
deleteFolder: t("delete_folder"),
|
||||
folderName: t("folder_name"),
|
||||
folderDetail: t("folder_detail"),
|
||||
close: t("close"),
|
||||
create: t("create"),
|
||||
update: t("update"),
|
||||
pleaseEnter: t("please_enter"),
|
||||
folder: t('folder'),
|
||||
newFolder: t('new_folder'),
|
||||
editFolder: t('edit_folder'),
|
||||
deleteFolder: t('delete_folder'),
|
||||
folderName: t('folder_name'),
|
||||
folderDetail: t('folder_detail'),
|
||||
close: t('close'),
|
||||
create: t('create'),
|
||||
update: t('update'),
|
||||
pleaseEnter: t('please_enter'),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full">
|
||||
<FoldersPane
|
||||
projectId={params.projectId}
|
||||
messages={messages}
|
||||
locale={params.locale}
|
||||
/>
|
||||
<FoldersPane projectId={params.projectId} messages={messages} locale={params.locale} />
|
||||
<div className="flex-grow w-full">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
This is folders page.
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <>This is folders page.</>;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { priorities } from "@/config/selection";
|
||||
import { CasePriorityCountType } from "@/types/case";
|
||||
import { HomeMessages } from "./page";
|
||||
const Chart = dynamic(() => import("react-apexcharts"), { ssr: false });
|
||||
import React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { priorities } from '@/config/selection';
|
||||
import { CasePriorityCountType } from '@/types/case';
|
||||
import { HomeMessages } from './page';
|
||||
const Chart = dynamic(() => import('react-apexcharts'), { ssr: false });
|
||||
|
||||
type Props = {
|
||||
priorityCounts: CasePriorityCountType[];
|
||||
@@ -12,11 +12,7 @@ type Props = {
|
||||
theme: string;
|
||||
};
|
||||
|
||||
export default function TestPriorityDonutChart({
|
||||
priorityCounts,
|
||||
messages,
|
||||
theme,
|
||||
}: Props) {
|
||||
export default function TestPriorityDonutChart({ priorityCounts, messages, theme }: Props) {
|
||||
const [chartData, setChartData] = useState({
|
||||
series: [],
|
||||
options: {
|
||||
@@ -38,10 +34,10 @@ export default function TestPriorityDonutChart({
|
||||
const legend = {
|
||||
labels: {
|
||||
colors: priorities.map((entry) => {
|
||||
if (theme === "light") {
|
||||
return "black";
|
||||
if (theme === 'light') {
|
||||
return 'black';
|
||||
} else {
|
||||
return "white";
|
||||
return 'white';
|
||||
}
|
||||
}),
|
||||
},
|
||||
@@ -57,13 +53,5 @@ export default function TestPriorityDonutChart({
|
||||
updateChartDate();
|
||||
}, [priorityCounts, theme]);
|
||||
|
||||
return (
|
||||
<Chart
|
||||
options={chartData.options}
|
||||
series={chartData.series}
|
||||
type="donut"
|
||||
width={"100%"}
|
||||
height={"100%"}
|
||||
/>
|
||||
);
|
||||
return <Chart options={chartData.options} series={chartData.series} type="donut" width={'100%'} height={'100%'} />;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { ProgressSeriesType } from "@/types/run";
|
||||
import { testRunCaseStatus } from "@/config/selection";
|
||||
const Chart = dynamic(() => import("react-apexcharts"), { ssr: false });
|
||||
import React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { ProgressSeriesType } from '@/types/run';
|
||||
import { testRunCaseStatus } from '@/config/selection';
|
||||
const Chart = dynamic(() => import('react-apexcharts'), { ssr: false });
|
||||
|
||||
type Props = {
|
||||
progressSeries: ProgressSeriesType[];
|
||||
@@ -11,11 +11,7 @@ type Props = {
|
||||
theme: string;
|
||||
};
|
||||
|
||||
export default function TestProgressBarChart({
|
||||
progressSeries,
|
||||
progressCategories,
|
||||
theme,
|
||||
}: Props) {
|
||||
export default function TestProgressBarChart({ progressSeries, progressCategories, theme }: Props) {
|
||||
const [chartData, setChartData] = useState({
|
||||
series: [],
|
||||
options: {
|
||||
@@ -28,14 +24,14 @@ export default function TestProgressBarChart({
|
||||
const updateChartDate = () => {
|
||||
if (progressSeries) {
|
||||
const legendsLabelColors = testRunCaseStatus.map((itr) => {
|
||||
if (theme === "light") {
|
||||
return "black";
|
||||
if (theme === 'light') {
|
||||
return 'black';
|
||||
} else {
|
||||
return "white";
|
||||
return 'white';
|
||||
}
|
||||
});
|
||||
|
||||
const xaxisLabelColor = theme === "light" ? "black" : "white";
|
||||
const xaxisLabelColor = theme === 'light' ? 'black' : 'white';
|
||||
|
||||
setChartData({
|
||||
series: progressSeries,
|
||||
@@ -47,7 +43,7 @@ export default function TestProgressBarChart({
|
||||
stacked: true,
|
||||
},
|
||||
legend: {
|
||||
position: "right",
|
||||
position: 'right',
|
||||
labels: {
|
||||
colors: legendsLabelColors,
|
||||
},
|
||||
@@ -56,7 +52,7 @@ export default function TestProgressBarChart({
|
||||
return itr.chartColor;
|
||||
}),
|
||||
xaxis: {
|
||||
type: "datetime",
|
||||
type: 'datetime',
|
||||
categories: progressCategories,
|
||||
labels: {
|
||||
style: {
|
||||
@@ -75,13 +71,5 @@ export default function TestProgressBarChart({
|
||||
updateChartDate();
|
||||
}, [progressSeries, progressCategories, theme]);
|
||||
|
||||
return (
|
||||
<Chart
|
||||
options={chartData.options}
|
||||
series={chartData.series}
|
||||
type="bar"
|
||||
width={"100%"}
|
||||
height={"100%"}
|
||||
/>
|
||||
);
|
||||
return <Chart options={chartData.options} series={chartData.series} type="bar" width={'100%'} height={'100%'} />;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { testTypes } from "@/config/selection";
|
||||
import { CaseTypeCountType } from "@/types/case";
|
||||
import { HomeMessages } from "./page";
|
||||
const Chart = dynamic(() => import("react-apexcharts"), { ssr: false });
|
||||
import React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { testTypes } from '@/config/selection';
|
||||
import { CaseTypeCountType } from '@/types/case';
|
||||
import { HomeMessages } from './page';
|
||||
const Chart = dynamic(() => import('react-apexcharts'), { ssr: false });
|
||||
|
||||
type Props = {
|
||||
typesCounts: CaseTypeCountType[];
|
||||
@@ -12,11 +12,7 @@ type Props = {
|
||||
theme: string;
|
||||
};
|
||||
|
||||
export default function TestTypesDonutChart({
|
||||
typesCounts,
|
||||
messages,
|
||||
theme,
|
||||
}: Props) {
|
||||
export default function TestTypesDonutChart({ typesCounts, messages, theme }: Props) {
|
||||
const [chartData, setChartData] = useState({
|
||||
series: [],
|
||||
options: {
|
||||
@@ -38,10 +34,10 @@ export default function TestTypesDonutChart({
|
||||
const legend = {
|
||||
labels: {
|
||||
colors: testTypes.map((entry) => {
|
||||
if (theme === "light") {
|
||||
return "black";
|
||||
if (theme === 'light') {
|
||||
return 'black';
|
||||
} else {
|
||||
return "white";
|
||||
return 'white';
|
||||
}
|
||||
}),
|
||||
},
|
||||
@@ -57,13 +53,5 @@ export default function TestTypesDonutChart({
|
||||
updateChartDate();
|
||||
}, [typesCounts, theme]);
|
||||
|
||||
return (
|
||||
<Chart
|
||||
options={chartData.options}
|
||||
series={chartData.series}
|
||||
type="donut"
|
||||
width={"100%"}
|
||||
height={"100%"}
|
||||
/>
|
||||
);
|
||||
return <Chart options={chartData.options} series={chartData.series} type="donut" width={'100%'} height={'100%'} />;
|
||||
}
|
||||
|
||||
@@ -1,31 +1,26 @@
|
||||
"use client";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Divider } from "@nextui-org/react";
|
||||
import { title, subtitle } from "@/components/primitives";
|
||||
import { Card, CardBody, Chip } from "@nextui-org/react";
|
||||
import { Folder, Clipboard, FlaskConical } from "lucide-react";
|
||||
import { CaseTypeCountType, CasePriorityCountType } from "@/types/case";
|
||||
import { ProgressSeriesType } from "@/types/run";
|
||||
import { HomeMessages } from "./page";
|
||||
import {
|
||||
aggregateBasicInfo,
|
||||
aggregateTestPriority,
|
||||
aggregateTestType,
|
||||
aggregateProgress,
|
||||
} from "./aggregate";
|
||||
import TestTypesChart from "./TestTypesDonutChart";
|
||||
import TestPriorityChart from "./TestPriorityDonutChart";
|
||||
import TestProgressBarChart from "./TestProgressColumnChart";
|
||||
import Config from "@/config/config";
|
||||
import { useTheme } from "next-themes";
|
||||
'use client';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Divider } from '@nextui-org/react';
|
||||
import { title, subtitle } from '@/components/primitives';
|
||||
import { Card, CardBody, Chip } from '@nextui-org/react';
|
||||
import { Folder, Clipboard, FlaskConical } from 'lucide-react';
|
||||
import { CaseTypeCountType, CasePriorityCountType } from '@/types/case';
|
||||
import { ProgressSeriesType } from '@/types/run';
|
||||
import { HomeMessages } from './page';
|
||||
import { aggregateBasicInfo, aggregateTestPriority, aggregateTestType, aggregateProgress } from './aggregate';
|
||||
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;
|
||||
|
||||
async function fetchProject(url) {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
method: 'GET',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -36,7 +31,7 @@ async function fetchProject(url) {
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} 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) {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const [project, setProject] = useState({
|
||||
name: "",
|
||||
detail: "",
|
||||
name: '',
|
||||
detail: '',
|
||||
Folders: [{ Cases: [] }],
|
||||
Runs: [{ RunCases: [] }],
|
||||
});
|
||||
@@ -57,8 +52,7 @@ export function Home({ projectId, messages }: Props) {
|
||||
const [caseNum, setCaseNum] = useState(0);
|
||||
const [runNum, setRunNum] = useState(0);
|
||||
const [typesCounts, setTypesCounts] = useState<CaseTypeCountType[]>();
|
||||
const [priorityCounts, setPriorityCounts] =
|
||||
useState<CasePriorityCountType[]>();
|
||||
const [priorityCounts, setPriorityCounts] = useState<CasePriorityCountType[]>();
|
||||
const [progressCategories, setProgressCategories] = useState<string[]>();
|
||||
const [progressSeries, setProgressSeries] = useState<ProgressSeriesType[]>();
|
||||
const url = `${apiServer}/home/${projectId}`;
|
||||
@@ -69,7 +63,7 @@ export function Home({ projectId, messages }: Props) {
|
||||
const data = await fetchProject(url);
|
||||
setProject(data);
|
||||
} 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 (
|
||||
<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">
|
||||
<Chip
|
||||
variant="flat"
|
||||
startContent={<Folder size={16} />}
|
||||
className="px-3"
|
||||
>
|
||||
<Chip variant="flat" startContent={<Folder size={16} />} className="px-3">
|
||||
{folderNum} {messages.folders}
|
||||
</Chip>
|
||||
<Chip
|
||||
variant="flat"
|
||||
startContent={<Clipboard size={16} />}
|
||||
className="px-3 ms-2"
|
||||
>
|
||||
<Chip variant="flat" startContent={<Clipboard size={16} />} className="px-3 ms-2">
|
||||
{caseNum} {messages.testCases}
|
||||
</Chip>
|
||||
<Chip
|
||||
variant="flat"
|
||||
startContent={<FlaskConical size={16} />}
|
||||
className="px-3 ms-2"
|
||||
>
|
||||
<Chip variant="flat" startContent={<FlaskConical size={16} />} className="px-3 ms-2">
|
||||
{runNum} {messages.testRuns}
|
||||
</Chip>
|
||||
</div>
|
||||
|
||||
{project.detail && (
|
||||
<Card
|
||||
className="mt-3 bg-neutral-100 dark:bg-neutral-700 dark:text-white"
|
||||
shadow="none"
|
||||
>
|
||||
<Card className="mt-3 bg-neutral-100 dark:bg-neutral-700 dark:text-white" shadow="none">
|
||||
<CardBody>{project.detail}</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Divider className="my-8" />
|
||||
<h2 className={subtitle()}>{messages.progress}</h2>
|
||||
<div style={{ height: "18rem" }}>
|
||||
<TestProgressBarChart
|
||||
progressSeries={progressSeries}
|
||||
progressCategories={progressCategories}
|
||||
theme={theme}
|
||||
/>
|
||||
<div style={{ height: '18rem' }}>
|
||||
<TestProgressBarChart progressSeries={progressSeries} progressCategories={progressCategories} theme={theme} />
|
||||
</div>
|
||||
|
||||
<Divider className="my-12" />
|
||||
<h2 className={subtitle()}>{messages.testClassification}</h2>
|
||||
<div className="flex pb-20">
|
||||
<div style={{ width: "32rem", height: "18rem" }}>
|
||||
<div style={{ width: '32rem', height: '18rem' }}>
|
||||
<h3>{messages.byType}</h3>
|
||||
<TestTypesChart
|
||||
typesCounts={typesCounts}
|
||||
messages={messages}
|
||||
theme={theme}
|
||||
/>
|
||||
<TestTypesChart typesCounts={typesCounts} messages={messages} theme={theme} />
|
||||
</div>
|
||||
<div style={{ width: "30rem", height: "18rem" }}>
|
||||
<div style={{ width: '30rem', height: '18rem' }}>
|
||||
<h3>{messages.byPriority}</h3>
|
||||
<TestPriorityChart
|
||||
priorityCounts={priorityCounts}
|
||||
messages={messages}
|
||||
theme={theme}
|
||||
/>
|
||||
<TestPriorityChart priorityCounts={priorityCounts} messages={messages} theme={theme} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Home } from "./home";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Home } from './home';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export type HomeMessages = {
|
||||
folders: string;
|
||||
@@ -35,37 +35,37 @@ export type HomeMessages = {
|
||||
};
|
||||
|
||||
export default function Page({ params }: { params: { projectId: string } }) {
|
||||
const t = useTranslations("Home");
|
||||
const t = useTranslations('Home');
|
||||
const messages = {
|
||||
folders: t("Folders"),
|
||||
testCases: t("test_cases"),
|
||||
testRuns: t("test_runs"),
|
||||
progress: t("progress"),
|
||||
untested: t("untested"),
|
||||
passed: t("passed"),
|
||||
failed: t("failed"),
|
||||
retest: t("retest"),
|
||||
skipped: t("skipped"),
|
||||
testClassification: t("test_classification"),
|
||||
byType: t("by_type"),
|
||||
byPriority: t("by_priority"),
|
||||
other: t("other"),
|
||||
security: t("security"),
|
||||
performance: t("performance"),
|
||||
accessibility: t("accessibility"),
|
||||
functional: t("functional"),
|
||||
acceptance: t("acceptance"),
|
||||
usability: t("usability"),
|
||||
smokeSanity: t("smoke_sanity"),
|
||||
compatibility: t("compatibility"),
|
||||
destructive: t("destructive"),
|
||||
regression: t("regression"),
|
||||
automated: t("automated"),
|
||||
manual: t("manual"),
|
||||
critical: t("critical"),
|
||||
high: t("high"),
|
||||
medium: t("medium"),
|
||||
low: t("low"),
|
||||
folders: t('Folders'),
|
||||
testCases: t('test_cases'),
|
||||
testRuns: t('test_runs'),
|
||||
progress: t('progress'),
|
||||
untested: t('untested'),
|
||||
passed: t('passed'),
|
||||
failed: t('failed'),
|
||||
retest: t('retest'),
|
||||
skipped: t('skipped'),
|
||||
testClassification: t('test_classification'),
|
||||
byType: t('by_type'),
|
||||
byPriority: t('by_priority'),
|
||||
other: t('other'),
|
||||
security: t('security'),
|
||||
performance: t('performance'),
|
||||
accessibility: t('accessibility'),
|
||||
functional: t('functional'),
|
||||
acceptance: t('acceptance'),
|
||||
usability: t('usability'),
|
||||
smokeSanity: t('smoke_sanity'),
|
||||
compatibility: t('compatibility'),
|
||||
destructive: t('destructive'),
|
||||
regression: t('regression'),
|
||||
automated: t('automated'),
|
||||
manual: t('manual'),
|
||||
critical: t('critical'),
|
||||
high: t('high'),
|
||||
medium: t('medium'),
|
||||
low: t('low'),
|
||||
};
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Sidebar from "./Sidebar";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Sidebar from './Sidebar';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function SidebarLayout({
|
||||
children,
|
||||
@@ -8,11 +8,11 @@ export default function SidebarLayout({
|
||||
children: React.ReactNode;
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const t = useTranslations("Project");
|
||||
const t = useTranslations('Project');
|
||||
const messages = {
|
||||
home: t("home"),
|
||||
testCases: t("test_cases"),
|
||||
testRuns: t("test_runs"),
|
||||
home: t('home'),
|
||||
testCases: t('test_cases'),
|
||||
testRuns: t('test_runs'),
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "@nextui-org/react";
|
||||
import { Plus } from "lucide-react";
|
||||
import RunsTable from "./RunsTable";
|
||||
import { fetchRuns, createRun, deleteRun } from "./runsControl";
|
||||
import { RunsMessages } from "@/types/run";
|
||||
'use client';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button } from '@nextui-org/react';
|
||||
import { Plus } from 'lucide-react';
|
||||
import RunsTable from './RunsTable';
|
||||
import { fetchRuns, createRun, deleteRun } from './runsControl';
|
||||
import { RunsMessages } from '@/types/run';
|
||||
|
||||
type Props = {
|
||||
projectId: string;
|
||||
@@ -21,7 +21,7 @@ export default function RunsPage({ projectId, locale, messages }: Props) {
|
||||
const data = await fetchRuns(projectId);
|
||||
setRuns(data);
|
||||
} 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);
|
||||
setRuns(updateRuns);
|
||||
} 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);
|
||||
setRuns(data);
|
||||
} 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">
|
||||
<h3 className="font-bold">{messages.runList}</h3>
|
||||
<div>
|
||||
<Button
|
||||
startContent={<Plus size={16} />}
|
||||
size="sm"
|
||||
color="primary"
|
||||
onClick={onCreateClick}
|
||||
>
|
||||
<Button startContent={<Plus size={16} />} size="sm" color="primary" onClick={onCreateClick}>
|
||||
{messages.newRun}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RunsTable
|
||||
projectId={projectId}
|
||||
runs={runs}
|
||||
onDeleteRun={onDeleteClick}
|
||||
messages={messages}
|
||||
locale={locale}
|
||||
/>
|
||||
<RunsTable projectId={projectId} runs={runs} onDeleteRun={onDeleteClick} messages={messages} locale={locale} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useMemo, useCallback } from "react";
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
@@ -12,11 +12,11 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownItem,
|
||||
SortDescriptor,
|
||||
} from "@nextui-org/react";
|
||||
import { Link, NextUiLinkClasses } from "@/src/navigation";
|
||||
import { MoreVertical } from "lucide-react";
|
||||
import { RunsMessages, RunType } from "@/types/run";
|
||||
import dayjs from "dayjs";
|
||||
} from '@nextui-org/react';
|
||||
import { Link, NextUiLinkClasses } from '@/src/navigation';
|
||||
import { MoreVertical } from 'lucide-react';
|
||||
import { RunsMessages, RunType } from '@/types/run';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
type Props = {
|
||||
projectId: string;
|
||||
@@ -26,24 +26,18 @@ type Props = {
|
||||
locale: string;
|
||||
};
|
||||
|
||||
export default function RunsTable({
|
||||
projectId,
|
||||
runs,
|
||||
onDeleteRun,
|
||||
messages,
|
||||
locale,
|
||||
}: Props) {
|
||||
export default function RunsTable({ projectId, runs, onDeleteRun, messages, locale }: Props) {
|
||||
const headerColumns = [
|
||||
{ name: messages.id, uid: "id", sortable: true },
|
||||
{ name: messages.name, uid: "name", sortable: true },
|
||||
{ name: messages.description, uid: "description", sortable: true },
|
||||
{ name: messages.lastUpdate, uid: "updatedAt", sortable: true },
|
||||
{ name: messages.actions, uid: "actions" },
|
||||
{ name: messages.id, uid: 'id', sortable: true },
|
||||
{ name: messages.name, uid: 'name', sortable: true },
|
||||
{ name: messages.description, uid: 'description', sortable: true },
|
||||
{ name: messages.lastUpdate, uid: 'updatedAt', sortable: true },
|
||||
{ name: messages.actions, uid: 'actions' },
|
||||
];
|
||||
|
||||
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
|
||||
column: "id",
|
||||
direction: "ascending",
|
||||
column: 'id',
|
||||
direction: 'ascending',
|
||||
});
|
||||
|
||||
const sortedItems = useMemo(() => {
|
||||
@@ -52,31 +46,27 @@ export default function RunsTable({
|
||||
const second = b[sortDescriptor.column as keyof RunType] as number;
|
||||
const cmp = first < second ? -1 : first > second ? 1 : 0;
|
||||
|
||||
return sortDescriptor.direction === "descending" ? -cmp : cmp;
|
||||
return sortDescriptor.direction === 'descending' ? -cmp : cmp;
|
||||
});
|
||||
}, [sortDescriptor, runs]);
|
||||
|
||||
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 cellValue = run[columnKey as keyof RunType];
|
||||
|
||||
switch (columnKey) {
|
||||
case "id":
|
||||
case 'id':
|
||||
return <span>{cellValue}</span>;
|
||||
case "name":
|
||||
case 'name':
|
||||
return (
|
||||
<Link
|
||||
href={`/projects/${projectId}/runs/${run.id}`}
|
||||
locale={locale}
|
||||
className={NextUiLinkClasses}
|
||||
>
|
||||
<Link href={`/projects/${projectId}/runs/${run.id}`} locale={locale} className={NextUiLinkClasses}>
|
||||
{cellValue}
|
||||
</Link>
|
||||
);
|
||||
case "detail":
|
||||
case 'detail':
|
||||
const maxLength = 20;
|
||||
const truncatedValue = truncateText(cellValue, maxLength);
|
||||
return (
|
||||
@@ -84,9 +74,9 @@ export default function RunsTable({
|
||||
<div>{truncatedValue}</div>
|
||||
</div>
|
||||
);
|
||||
case "updatedAt":
|
||||
return <span>{dayjs(cellValue).format("YYYY/MM/DD HH:mm")}</span>;
|
||||
case "actions":
|
||||
case 'updatedAt':
|
||||
return <span>{dayjs(cellValue).format('YYYY/MM/DD HH:mm')}</span>;
|
||||
case 'actions':
|
||||
return (
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
@@ -95,10 +85,7 @@ export default function RunsTable({
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="run actions">
|
||||
<DropdownItem
|
||||
className="text-danger"
|
||||
onClick={() => onDeleteRun(run.id)}
|
||||
>
|
||||
<DropdownItem className="text-danger" onClick={() => onDeleteRun(run.id)}>
|
||||
{messages.deleteRun}
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
@@ -111,18 +98,18 @@ export default function RunsTable({
|
||||
|
||||
const classNames = useMemo(
|
||||
() => ({
|
||||
wrapper: ["max-w-3xl"],
|
||||
th: ["bg-transparent", "text-default-500", "border-b", "border-divider"],
|
||||
wrapper: ['max-w-3xl'],
|
||||
th: ['bg-transparent', 'text-default-500', 'border-b', 'border-divider'],
|
||||
td: [
|
||||
// changing the rows border radius
|
||||
// first
|
||||
"group-data-[first=true]:first:before:rounded-none",
|
||||
"group-data-[first=true]:last:before:rounded-none",
|
||||
'group-data-[first=true]:first:before:rounded-none',
|
||||
'group-data-[first=true]:last:before:rounded-none',
|
||||
// middle
|
||||
"group-data-[middle=true]:before:rounded-none",
|
||||
'group-data-[middle=true]:before:rounded-none',
|
||||
// last
|
||||
"group-data-[last=true]:first:before:rounded-none",
|
||||
"group-data-[last=true]:last:before:rounded-none",
|
||||
'group-data-[last=true]:first:before:rounded-none',
|
||||
'group-data-[last=true]:last:before:rounded-none',
|
||||
],
|
||||
}),
|
||||
[]
|
||||
@@ -141,7 +128,7 @@ export default function RunsTable({
|
||||
{(column) => (
|
||||
<TableColumn
|
||||
key={column.uid}
|
||||
align={column.uid === "actions" ? "center" : "start"}
|
||||
align={column.uid === 'actions' ? 'center' : 'start'}
|
||||
allowsSorting={column.sortable}
|
||||
>
|
||||
{column.name}
|
||||
@@ -150,11 +137,7 @@ export default function RunsTable({
|
||||
</TableHeader>
|
||||
<TableBody emptyContent={messages.noRunsFound} items={sortedItems}>
|
||||
{(item) => (
|
||||
<TableRow key={item.id}>
|
||||
{(columnKey) => (
|
||||
<TableCell>{renderCell(item, columnKey)}</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
<TableRow key={item.id}>{(columnKey) => <TableCell>{renderCell(item, columnKey)}</TableCell>}</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter } from "@/src/navigation";
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from '@/src/navigation';
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
@@ -17,28 +17,14 @@ import {
|
||||
Dropdown,
|
||||
DropdownMenu,
|
||||
DropdownItem,
|
||||
} from "@nextui-org/react";
|
||||
import {
|
||||
Save,
|
||||
ArrowLeft,
|
||||
Folder,
|
||||
ChevronDown,
|
||||
CopyPlus,
|
||||
CopyMinus,
|
||||
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";
|
||||
} from '@nextui-org/react';
|
||||
import { Save, ArrowLeft, Folder, ChevronDown, CopyPlus, CopyMinus, 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 {
|
||||
fetchRun,
|
||||
updateRun,
|
||||
@@ -48,16 +34,16 @@ import {
|
||||
bulkCreateRunCases,
|
||||
deleteRunCase,
|
||||
bulkDeleteRunCases,
|
||||
} from "../runsControl";
|
||||
import { fetchFolders } from "../../folders/foldersControl";
|
||||
import { fetchCases } from "../../folders/[folderId]/cases/caseControl";
|
||||
import { useTheme } from "next-themes";
|
||||
} from '../runsControl';
|
||||
import { fetchFolders } from '../../folders/foldersControl';
|
||||
import { fetchCases } from '../../folders/[folderId]/cases/caseControl';
|
||||
import { useTheme } from 'next-themes';
|
||||
|
||||
const defaultTestRun = {
|
||||
id: 0,
|
||||
name: "",
|
||||
name: '',
|
||||
configurations: 0,
|
||||
description: "",
|
||||
description: '',
|
||||
state: 0,
|
||||
projectId: 0,
|
||||
};
|
||||
@@ -69,18 +55,12 @@ type Props = {
|
||||
locale: string;
|
||||
};
|
||||
|
||||
export default function RunEditor({
|
||||
projectId,
|
||||
runId,
|
||||
messages,
|
||||
locale,
|
||||
}: Props) {
|
||||
export default function RunEditor({ projectId, runId, messages, locale }: Props) {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const [testRun, setTestRun] = useState<RunType>(defaultTestRun);
|
||||
const [folders, setFolders] = useState([]);
|
||||
const [runCases, setRunCases] = useState<RunCaseType[]>([]);
|
||||
const [runStatusCounts, setRunStatusCounts] =
|
||||
useState<RunStatusCountType[]>();
|
||||
const [runStatusCounts, setRunStatusCounts] = useState<RunStatusCountType[]>();
|
||||
const [selectedKeys, setSelectedKeys] = useState<Selection>(new Set([]));
|
||||
const [selectedFolder, setSelectedFolder] = useState<FolderType>({});
|
||||
const [testcases, setTestCases] = useState<CaseType[]>([]);
|
||||
@@ -102,7 +82,7 @@ export default function RunEditor({
|
||||
setFolders(foldersData);
|
||||
setSelectedFolder(foldersData[0]);
|
||||
} 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
|
||||
// and add "isIncluded" property
|
||||
const updatedTestCasesData = testCasesData.map((testCase) => {
|
||||
const runCase = latestRunCases.find(
|
||||
(runCase) => runCase.caseId === testCase.id
|
||||
);
|
||||
const runCase = latestRunCases.find((runCase) => runCase.caseId === testCase.id);
|
||||
|
||||
const isIncluded = runCase ? true : false;
|
||||
const runStatus = runCase ? runCase.status : 0;
|
||||
@@ -135,7 +113,7 @@ export default function RunEditor({
|
||||
|
||||
setTestCases(updatedTestCasesData);
|
||||
} 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 (
|
||||
isInclude: boolean,
|
||||
clickedTestCaseId: number
|
||||
) => {
|
||||
const handleIncludeExcludeCase = async (isInclude: boolean, clickedTestCaseId: number) => {
|
||||
if (isInclude) {
|
||||
const createdRunCase = await createRunCase(runId, clickedTestCaseId);
|
||||
setRunCases((prevRunCases) => {
|
||||
@@ -167,9 +142,7 @@ export default function RunEditor({
|
||||
} else {
|
||||
await deleteRunCase(runId, clickedTestCaseId);
|
||||
setRunCases((prevRunCases) => {
|
||||
return prevRunCases.filter(
|
||||
(runCase) => runCase.caseId !== clickedTestCaseId
|
||||
);
|
||||
return prevRunCases.filter((runCase) => runCase.caseId !== clickedTestCaseId);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -185,7 +158,7 @@ export default function RunEditor({
|
||||
|
||||
const handleBulkIncludeExcludeCases = async (isInclude: boolean) => {
|
||||
let keys: number[] = [];
|
||||
if (selectedKeys === "all") {
|
||||
if (selectedKeys === 'all') {
|
||||
keys = testcases.map((item) => item.id);
|
||||
} else {
|
||||
keys = Array.from(selectedKeys).map(Number);
|
||||
@@ -218,7 +191,7 @@ export default function RunEditor({
|
||||
setSelectedKeys(new Set([]));
|
||||
};
|
||||
|
||||
const baseClass = "";
|
||||
const baseClass = '';
|
||||
const selectedClass = `${baseClass} bg-neutral-200 dark:bg-neutral-700`;
|
||||
|
||||
return (
|
||||
@@ -230,9 +203,7 @@ export default function RunEditor({
|
||||
isIconOnly
|
||||
size="sm"
|
||||
className="rounded-full bg-neutral-50 dark:bg-neutral-600"
|
||||
onPress={() =>
|
||||
router.push(`/projects/${projectId}/runs`, { locale: locale })
|
||||
}
|
||||
onPress={() => router.push(`/projects/${projectId}/runs`, { locale: locale })}
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
</Button>
|
||||
@@ -272,11 +243,7 @@ export default function RunEditor({
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<RunProgressChart
|
||||
statusCounts={runStatusCounts}
|
||||
messages={messages}
|
||||
theme={theme}
|
||||
/>
|
||||
<RunProgressChart statusCounts={runStatusCounts} messages={messages} theme={theme} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
@@ -287,7 +254,7 @@ export default function RunEditor({
|
||||
label={messages.title}
|
||||
value={testRun.name}
|
||||
isInvalid={isNameInvalid}
|
||||
errorMessage={isNameInvalid ? messages.pleaseEnter : ""}
|
||||
errorMessage={isNameInvalid ? messages.pleaseEnter : ''}
|
||||
onChange={(e) => {
|
||||
setTestRun({ ...testRun, name: e.target.value });
|
||||
}}
|
||||
@@ -312,9 +279,7 @@ export default function RunEditor({
|
||||
selectedKeys={[testRunStatus[testRun.state].uid]}
|
||||
onSelectionChange={(e) => {
|
||||
const selectedUid = e.anchorKey;
|
||||
const index = testRunStatus.findIndex(
|
||||
(template) => template.uid === selectedUid
|
||||
);
|
||||
const index = testRunStatus.findIndex((template) => template.uid === selectedUid);
|
||||
setTestRun({ ...testRun, state: index });
|
||||
}}
|
||||
label={messages.status}
|
||||
@@ -334,14 +299,10 @@ export default function RunEditor({
|
||||
<div className="flex items-center justify-between">
|
||||
<h6 className="h-8 font-bold">{messages.selectTestCase}</h6>
|
||||
<div>
|
||||
{(selectedKeys.size > 0 || selectedKeys === "all") && (
|
||||
{(selectedKeys.size > 0 || selectedKeys === 'all') && (
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
endContent={<ChevronDown size={16} />}
|
||||
>
|
||||
<Button size="sm" color="primary" endContent={<ChevronDown size={16} />}>
|
||||
{messages.testCaseSelection}
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
@@ -371,14 +332,8 @@ export default function RunEditor({
|
||||
<ListboxItem
|
||||
key={index}
|
||||
onClick={() => setSelectedFolder(folder)}
|
||||
startContent={
|
||||
<Folder size={20} color="#F7C24E" fill="#F7C24E" />
|
||||
}
|
||||
className={
|
||||
selectedFolder && folder.id === selectedFolder.id
|
||||
? selectedClass
|
||||
: baseClass
|
||||
}
|
||||
startContent={<Folder size={20} color="#F7C24E" fill="#F7C24E" />}
|
||||
className={selectedFolder && folder.id === selectedFolder.id ? selectedClass : baseClass}
|
||||
>
|
||||
{folder.name}
|
||||
</ListboxItem>
|
||||
@@ -391,12 +346,8 @@ export default function RunEditor({
|
||||
selectedKeys={selectedKeys}
|
||||
onSelectionChange={setSelectedKeys}
|
||||
onStatusChange={handleChangeStatus}
|
||||
onIncludeCase={(includeTestId) =>
|
||||
handleIncludeExcludeCase(true, includeTestId)
|
||||
}
|
||||
onExcludeCase={(excludeCaseId) =>
|
||||
handleIncludeExcludeCase(false, excludeCaseId)
|
||||
}
|
||||
onIncludeCase={(includeTestId) => handleIncludeExcludeCase(true, includeTestId)}
|
||||
onExcludeCase={(excludeCaseId) => handleIncludeExcludeCase(false, excludeCaseId)}
|
||||
messages={messages}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { testRunCaseStatus } from "@/config/selection";
|
||||
import { RunStatusCountType, RunMessages } from "@/types/run";
|
||||
const Chart = dynamic(() => import("react-apexcharts"), { ssr: false });
|
||||
import React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { testRunCaseStatus } from '@/config/selection';
|
||||
import { RunStatusCountType, RunMessages } from '@/types/run';
|
||||
const Chart = dynamic(() => import('react-apexcharts'), { ssr: false });
|
||||
|
||||
type Props = {
|
||||
statusCounts: RunStatusCountType[];
|
||||
@@ -11,11 +11,7 @@ type Props = {
|
||||
theme: string;
|
||||
};
|
||||
|
||||
export default function RunProgressDounut({
|
||||
statusCounts,
|
||||
messages,
|
||||
theme,
|
||||
}: Props) {
|
||||
export default function RunProgressDounut({ statusCounts, messages, theme }: Props) {
|
||||
const [chartData, setChartData] = useState({
|
||||
series: [],
|
||||
options: {
|
||||
@@ -37,10 +33,10 @@ export default function RunProgressDounut({
|
||||
const legend = {
|
||||
labels: {
|
||||
colors: testRunCaseStatus.map((entry) => {
|
||||
if (theme === "light") {
|
||||
return "black";
|
||||
if (theme === 'light') {
|
||||
return 'black';
|
||||
} else {
|
||||
return "white";
|
||||
return 'white';
|
||||
}
|
||||
}),
|
||||
},
|
||||
@@ -56,13 +52,5 @@ export default function RunProgressDounut({
|
||||
updateChartDate();
|
||||
}, [statusCounts, theme]);
|
||||
|
||||
return (
|
||||
<Chart
|
||||
options={chartData.options}
|
||||
series={chartData.series}
|
||||
type="donut"
|
||||
width={"100%"}
|
||||
height={"100%"}
|
||||
/>
|
||||
);
|
||||
return <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 {
|
||||
Table,
|
||||
TableHeader,
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
DropdownItem,
|
||||
Selection,
|
||||
SortDescriptor,
|
||||
} from "@nextui-org/react";
|
||||
} from '@nextui-org/react';
|
||||
import {
|
||||
ChevronDown,
|
||||
MoreVertical,
|
||||
@@ -24,10 +24,10 @@ import {
|
||||
CircleDashed,
|
||||
CircleX,
|
||||
CircleSlash2,
|
||||
} from "lucide-react";
|
||||
import { priorities, testRunCaseStatus } from "@/config/selection";
|
||||
import { CaseType } from "@/types/case";
|
||||
import { RunsMessages } from "@/types/run";
|
||||
} from 'lucide-react';
|
||||
import { priorities, testRunCaseStatus } from '@/config/selection';
|
||||
import { CaseType } from '@/types/case';
|
||||
import { RunsMessages } from '@/types/run';
|
||||
|
||||
type Props = {
|
||||
cases: CaseType[];
|
||||
@@ -49,16 +49,16 @@ export default function TestCaseSelector({
|
||||
messages,
|
||||
}: Props) {
|
||||
const headerColumns = [
|
||||
{ name: messages.id, uid: "id", sortable: true },
|
||||
{ name: messages.title, uid: "title", sortable: true },
|
||||
{ name: messages.priority, uid: "priority", sortable: true },
|
||||
{ name: messages.status, uid: "runStatus", sortable: true },
|
||||
{ name: messages.actions, uid: "actions" },
|
||||
{ name: messages.id, uid: 'id', sortable: true },
|
||||
{ name: messages.title, uid: 'title', sortable: true },
|
||||
{ name: messages.priority, uid: 'priority', sortable: true },
|
||||
{ name: messages.status, uid: 'runStatus', sortable: true },
|
||||
{ name: messages.actions, uid: 'actions' },
|
||||
];
|
||||
|
||||
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
|
||||
column: "id",
|
||||
direction: "ascending",
|
||||
column: 'id',
|
||||
direction: 'ascending',
|
||||
});
|
||||
|
||||
const sortedItems = useMemo(() => {
|
||||
@@ -67,23 +67,23 @@ export default function TestCaseSelector({
|
||||
const second = b[sortDescriptor.column as keyof CaseType] as number;
|
||||
const cmp = first < second ? -1 : first > second ? 1 : 0;
|
||||
|
||||
return sortDescriptor.direction === "descending" ? -cmp : cmp;
|
||||
return sortDescriptor.direction === 'descending' ? -cmp : cmp;
|
||||
});
|
||||
}, [sortDescriptor, cases]);
|
||||
|
||||
const notIncludedCaseClass = "text-neutral-200 dark:text-neutral-600";
|
||||
const chipBaseClass = "flex items-center text-default-600";
|
||||
const notIncludedCaseClass = 'text-neutral-200 dark:text-neutral-600';
|
||||
const chipBaseClass = 'flex items-center text-default-600';
|
||||
|
||||
const renderStatusIcon = (uid: string) => {
|
||||
if (uid === "untested") {
|
||||
if (uid === 'untested') {
|
||||
return <Circle size={16} color="#d4d4d8" />;
|
||||
} else if (uid === "passed") {
|
||||
} else if (uid === 'passed') {
|
||||
return <CircleCheck size={16} color="#17c964" />;
|
||||
} else if (uid === "retest") {
|
||||
} else if (uid === 'retest') {
|
||||
return <CircleDashed size={16} color="#f5a524" />;
|
||||
} else if (uid === "failed") {
|
||||
} else if (uid === 'failed') {
|
||||
return <CircleX size={16} color="#f31260" />;
|
||||
} else if (uid === "skipped") {
|
||||
} else if (uid === 'skipped') {
|
||||
return <CircleSlash2 size={16} color="#52525b" />;
|
||||
}
|
||||
};
|
||||
@@ -93,22 +93,18 @@ export default function TestCaseSelector({
|
||||
const isIncluded = testCase.isIncluded;
|
||||
|
||||
switch (columnKey) {
|
||||
case "priority":
|
||||
case 'priority':
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
isIncluded ? chipBaseClass : chipBaseClass + notIncludedCaseClass
|
||||
}
|
||||
>
|
||||
<div className={isIncluded ? chipBaseClass : chipBaseClass + notIncludedCaseClass}>
|
||||
<Circle
|
||||
size={8}
|
||||
color={isIncluded ? priorities[cellValue].color : "#d4d4d8"}
|
||||
fill={isIncluded ? priorities[cellValue].color : "#d4d4d8"}
|
||||
color={isIncluded ? priorities[cellValue].color : '#d4d4d8'}
|
||||
fill={isIncluded ? priorities[cellValue].color : '#d4d4d8'}
|
||||
/>
|
||||
<div className="ms-3">{messages[priorities[cellValue].uid]}</div>
|
||||
</div>
|
||||
);
|
||||
case "runStatus":
|
||||
case 'runStatus':
|
||||
return (
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
@@ -116,15 +112,10 @@ export default function TestCaseSelector({
|
||||
size="sm"
|
||||
variant="light"
|
||||
isDisabled={!isIncluded}
|
||||
startContent={
|
||||
isIncluded &&
|
||||
renderStatusIcon(testRunCaseStatus[cellValue].uid)
|
||||
}
|
||||
startContent={isIncluded && renderStatusIcon(testRunCaseStatus[cellValue].uid)}
|
||||
endContent={isIncluded && <ChevronDown size={16} />}
|
||||
>
|
||||
<span className="w-12">
|
||||
{isIncluded && messages[testRunCaseStatus[cellValue].uid]}
|
||||
</span>
|
||||
<span className="w-12">{isIncluded && messages[testRunCaseStatus[cellValue].uid]}</span>
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="test case actions">
|
||||
@@ -140,7 +131,7 @@ export default function TestCaseSelector({
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
);
|
||||
case "actions":
|
||||
case 'actions':
|
||||
return (
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
@@ -173,18 +164,18 @@ export default function TestCaseSelector({
|
||||
|
||||
const classNames = useMemo(
|
||||
() => ({
|
||||
wrapper: ["min-w-3xl"],
|
||||
th: ["bg-transparent", "text-default-500", "border-b", "border-divider"],
|
||||
wrapper: ['min-w-3xl'],
|
||||
th: ['bg-transparent', 'text-default-500', 'border-b', 'border-divider'],
|
||||
td: [
|
||||
// changing the rows border radius
|
||||
// first
|
||||
"group-data-[first=true]:first:before:rounded-none",
|
||||
"group-data-[first=true]:last:before:rounded-none",
|
||||
'group-data-[first=true]:first:before:rounded-none',
|
||||
'group-data-[first=true]:last:before:rounded-none',
|
||||
// middle
|
||||
"group-data-[middle=true]:before:rounded-none",
|
||||
'group-data-[middle=true]:before:rounded-none',
|
||||
// last
|
||||
"group-data-[last=true]:first:before:rounded-none",
|
||||
"group-data-[last=true]:last:before:rounded-none",
|
||||
'group-data-[last=true]:first:before:rounded-none',
|
||||
'group-data-[last=true]:last:before:rounded-none',
|
||||
],
|
||||
}),
|
||||
[]
|
||||
@@ -211,7 +202,7 @@ export default function TestCaseSelector({
|
||||
{(column) => (
|
||||
<TableColumn
|
||||
key={column.uid}
|
||||
align={column.uid === "actions" ? "center" : "start"}
|
||||
align={column.uid === 'actions' ? 'center' : 'start'}
|
||||
allowsSorting={column.sortable}
|
||||
>
|
||||
{column.name}
|
||||
@@ -220,13 +211,8 @@ export default function TestCaseSelector({
|
||||
</TableHeader>
|
||||
<TableBody emptyContent={messages.noCasesFound} items={sortedItems}>
|
||||
{(item) => (
|
||||
<TableRow
|
||||
key={item.id}
|
||||
className={!item.isIncluded ? notIncludedCaseClass : ""}
|
||||
>
|
||||
{(columnKey) => (
|
||||
<TableCell>{renderCell(item, columnKey)}</TableCell>
|
||||
)}
|
||||
<TableRow key={item.id} className={!item.isIncluded ? notIncludedCaseClass : ''}>
|
||||
{(columnKey) => <TableCell>{renderCell(item, columnKey)}</TableCell>}
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
|
||||
@@ -1,53 +1,42 @@
|
||||
import RunEditor from "./RunEditor";
|
||||
import { useTranslations } from "next-intl";
|
||||
import RunEditor from './RunEditor';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function Page({
|
||||
params,
|
||||
}: {
|
||||
params: { projectId: string; runId: string; locale: string };
|
||||
}) {
|
||||
const t = useTranslations("Run");
|
||||
export default function Page({ params }: { params: { projectId: string; runId: string; locale: string } }) {
|
||||
const t = useTranslations('Run');
|
||||
const messages = {
|
||||
backToRuns: t("back_to_runs"),
|
||||
updating: t("updating"),
|
||||
update: t("update"),
|
||||
progress: t("progress"),
|
||||
refresh: t("refresh"),
|
||||
id: t("id"),
|
||||
title: t("title"),
|
||||
pleaseEnter: t("please_enter"),
|
||||
description: t("description"),
|
||||
new: t("new"),
|
||||
inProgress: t("inProgress"),
|
||||
underReview: t("underReview"),
|
||||
rejected: t("rejected"),
|
||||
done: t("done"),
|
||||
closed: t("closed"),
|
||||
priority: t("priority"),
|
||||
actions: t("actions"),
|
||||
status: t("status"),
|
||||
critical: t("critical"),
|
||||
high: t("high"),
|
||||
medium: t("medium"),
|
||||
low: t("low"),
|
||||
untested: t("untested"),
|
||||
passed: t("passed"),
|
||||
failed: t("failed"),
|
||||
retest: t("retest"),
|
||||
skipped: t("skipped"),
|
||||
selectTestCase: t("select_test_case"),
|
||||
testCaseSelection: t("test_case_selection"),
|
||||
includeInRun: t("include_in_run"),
|
||||
excludeFromRun: t("exclude_from_run"),
|
||||
noCasesFound: t("no_cases_found"),
|
||||
backToRuns: t('back_to_runs'),
|
||||
updating: t('updating'),
|
||||
update: t('update'),
|
||||
progress: t('progress'),
|
||||
refresh: t('refresh'),
|
||||
id: t('id'),
|
||||
title: t('title'),
|
||||
pleaseEnter: t('please_enter'),
|
||||
description: t('description'),
|
||||
new: t('new'),
|
||||
inProgress: t('inProgress'),
|
||||
underReview: t('underReview'),
|
||||
rejected: t('rejected'),
|
||||
done: t('done'),
|
||||
closed: t('closed'),
|
||||
priority: t('priority'),
|
||||
actions: t('actions'),
|
||||
status: t('status'),
|
||||
critical: t('critical'),
|
||||
high: t('high'),
|
||||
medium: t('medium'),
|
||||
low: t('low'),
|
||||
untested: t('untested'),
|
||||
passed: t('passed'),
|
||||
failed: t('failed'),
|
||||
retest: t('retest'),
|
||||
skipped: t('skipped'),
|
||||
selectTestCase: t('select_test_case'),
|
||||
testCaseSelection: t('test_case_selection'),
|
||||
includeInRun: t('include_in_run'),
|
||||
excludeFromRun: t('exclude_from_run'),
|
||||
noCasesFound: t('no_cases_found'),
|
||||
};
|
||||
|
||||
return (
|
||||
<RunEditor
|
||||
projectId={params.projectId}
|
||||
runId={params.runId}
|
||||
messages={messages}
|
||||
locale={params.locale}
|
||||
/>
|
||||
);
|
||||
return <RunEditor projectId={params.projectId} runId={params.runId} messages={messages} locale={params.locale} />;
|
||||
}
|
||||
|
||||
@@ -1,31 +1,23 @@
|
||||
import RunsPage from "./RunsPage";
|
||||
import { useTranslations } from "next-intl";
|
||||
import RunsPage from './RunsPage';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function Page({
|
||||
params,
|
||||
}: {
|
||||
params: { projectId: string; locale: string };
|
||||
}) {
|
||||
const t = useTranslations("Runs");
|
||||
export default function Page({ params }: { params: { projectId: string; locale: string } }) {
|
||||
const t = useTranslations('Runs');
|
||||
const messages = {
|
||||
runList: t("run_list"),
|
||||
id: t("id"),
|
||||
name: t("name"),
|
||||
description: t("description"),
|
||||
lastUpdate: t("last_update"),
|
||||
actions: t("actions"),
|
||||
newRun: t("new_run"),
|
||||
deleteRun: t("delete_run"),
|
||||
noRunsFound: t("no_runs_found"),
|
||||
runList: t('run_list'),
|
||||
id: t('id'),
|
||||
name: t('name'),
|
||||
description: t('description'),
|
||||
lastUpdate: t('last_update'),
|
||||
actions: t('actions'),
|
||||
newRun: t('new_run'),
|
||||
deleteRun: t('delete_run'),
|
||||
noRunsFound: t('no_runs_found'),
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<RunsPage
|
||||
projectId={params.projectId}
|
||||
locale={params.locale}
|
||||
messages={messages}
|
||||
/>
|
||||
<RunsPage projectId={params.projectId} locale={params.locale} messages={messages} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
export default function ProjectsLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
export default function ProjectsLayout({ children }: { children: React.ReactNode }) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import ProjectsPage from "./ProjectsPage";
|
||||
import { useTranslations } from "next-intl";
|
||||
import ProjectsPage from './ProjectsPage';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function Page(params: { locale: string }) {
|
||||
const t = useTranslations("Projects");
|
||||
const t = useTranslations('Projects');
|
||||
const messages = {
|
||||
projectList: t("projectList"),
|
||||
project: t("project"),
|
||||
newProject: t("new_project"),
|
||||
editProject: t("edit_project"),
|
||||
deleteProject: t("delete_project"),
|
||||
id: t("id"),
|
||||
name: t("name"),
|
||||
detail: t("detail"),
|
||||
lastUpdate: t("last_update"),
|
||||
actions: t("actions"),
|
||||
projectName: t("project_name"),
|
||||
projectDetail: t("project_detail"),
|
||||
close: t("close"),
|
||||
create: t("create"),
|
||||
update: t("update"),
|
||||
pleaseEnter: t("please_enter"),
|
||||
noProjectsFound: t("no_projects_found"),
|
||||
projectList: t('projectList'),
|
||||
project: t('project'),
|
||||
newProject: t('new_project'),
|
||||
editProject: t('edit_project'),
|
||||
deleteProject: t('delete_project'),
|
||||
id: t('id'),
|
||||
name: t('name'),
|
||||
detail: t('detail'),
|
||||
lastUpdate: t('last_update'),
|
||||
actions: t('actions'),
|
||||
projectName: t('project_name'),
|
||||
projectDetail: t('project_detail'),
|
||||
close: t('close'),
|
||||
create: t('create'),
|
||||
update: t('update'),
|
||||
pleaseEnter: t('please_enter'),
|
||||
noProjectsFound: t('no_projects_found'),
|
||||
};
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import * as React from "react";
|
||||
import { NextUIProvider } from "@nextui-org/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import { ThemeProviderProps } from "next-themes/dist/types";
|
||||
import * as React from 'react';
|
||||
import { NextUIProvider } from '@nextui-org/react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
||||
import { ThemeProviderProps } from 'next-themes/dist/types';
|
||||
|
||||
import TokenProvider from "./TokenProvider";
|
||||
import { TokenProps } from "@/types/user";
|
||||
import TokenProvider from './TokenProvider';
|
||||
import { TokenProps } from '@/types/user';
|
||||
|
||||
export interface ProvidersProps {
|
||||
children: React.ReactNode;
|
||||
|
||||
Reference in New Issue
Block a user