Introduce prettier

This commit is contained in:
Takeshi Kimata
2024-05-19 21:06:49 +09:00
parent 75eeebefda
commit c5ba3b9a00
52 changed files with 884 additions and 1509 deletions

View File

@@ -1,7 +1,7 @@
"use client"; 'use client';
import { useState } from "react"; import { useState } from 'react';
import { Button } from "@nextui-org/button"; import { Button } from '@nextui-org/button';
export const Counter = () => { export const Counter = () => {
const [count, setCount] = useState(0); const [count, setCount] = useState(0);

View File

@@ -1,19 +1,8 @@
import * as React from "react"; import * as React from 'react';
import { IconSvgProps } from "@/types"; import { IconSvgProps } from '@/types';
export const Logo: React.FC<IconSvgProps> = ({ export const Logo: React.FC<IconSvgProps> = ({ size = 36, width, height, ...props }) => (
size = 36, <svg fill="none" height={size || height} viewBox="0 0 32 32" width={size || width} {...props}>
width,
height,
...props
}) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 32 32"
width={size || width}
{...props}
>
<path <path
clipRule="evenodd" clipRule="evenodd"
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z" d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
@@ -23,19 +12,9 @@ export const Logo: React.FC<IconSvgProps> = ({
</svg> </svg>
); );
export const DiscordIcon: React.FC<IconSvgProps> = ({ export const DiscordIcon: React.FC<IconSvgProps> = ({ size = 24, width, height, ...props }) => {
size = 24,
width,
height,
...props
}) => {
return ( return (
<svg <svg height={size || height} viewBox="0 0 24 24" width={size || width} {...props}>
height={size || height}
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<path <path
d="M14.82 4.26a10.14 10.14 0 0 0-.53 1.1 14.66 14.66 0 0 0-4.58 0 10.14 10.14 0 0 0-.53-1.1 16 16 0 0 0-4.13 1.3 17.33 17.33 0 0 0-3 11.59 16.6 16.6 0 0 0 5.07 2.59A12.89 12.89 0 0 0 8.23 18a9.65 9.65 0 0 1-1.71-.83 3.39 3.39 0 0 0 .42-.33 11.66 11.66 0 0 0 10.12 0q.21.18.42.33a10.84 10.84 0 0 1-1.71.84 12.41 12.41 0 0 0 1.08 1.78 16.44 16.44 0 0 0 5.06-2.59 17.22 17.22 0 0 0-3-11.59 16.09 16.09 0 0 0-4.09-1.35zM8.68 14.81a1.94 1.94 0 0 1-1.8-2 1.93 1.93 0 0 1 1.8-2 1.93 1.93 0 0 1 1.8 2 1.93 1.93 0 0 1-1.8 2zm6.64 0a1.94 1.94 0 0 1-1.8-2 1.93 1.93 0 0 1 1.8-2 1.92 1.92 0 0 1 1.8 2 1.92 1.92 0 0 1-1.8 2z" d="M14.82 4.26a10.14 10.14 0 0 0-.53 1.1 14.66 14.66 0 0 0-4.58 0 10.14 10.14 0 0 0-.53-1.1 16 16 0 0 0-4.13 1.3 17.33 17.33 0 0 0-3 11.59 16.6 16.6 0 0 0 5.07 2.59A12.89 12.89 0 0 0 8.23 18a9.65 9.65 0 0 1-1.71-.83 3.39 3.39 0 0 0 .42-.33 11.66 11.66 0 0 0 10.12 0q.21.18.42.33a10.84 10.84 0 0 1-1.71.84 12.41 12.41 0 0 0 1.08 1.78 16.44 16.44 0 0 0 5.06-2.59 17.22 17.22 0 0 0-3-11.59 16.09 16.09 0 0 0-4.09-1.35zM8.68 14.81a1.94 1.94 0 0 1-1.8-2 1.93 1.93 0 0 1 1.8-2 1.93 1.93 0 0 1 1.8 2 1.93 1.93 0 0 1-1.8 2zm6.64 0a1.94 1.94 0 0 1-1.8-2 1.93 1.93 0 0 1 1.8-2 1.92 1.92 0 0 1 1.8 2 1.92 1.92 0 0 1-1.8 2z"
fill="currentColor" fill="currentColor"
@@ -44,19 +23,9 @@ export const DiscordIcon: React.FC<IconSvgProps> = ({
); );
}; };
export const TwitterIcon: React.FC<IconSvgProps> = ({ export const TwitterIcon: React.FC<IconSvgProps> = ({ size = 24, width, height, ...props }) => {
size = 24,
width,
height,
...props
}) => {
return ( return (
<svg <svg height={size || height} viewBox="0 0 24 24" width={size || width} {...props}>
height={size || height}
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<path <path
d="M19.633 7.997c.013.175.013.349.013.523 0 5.325-4.053 11.461-11.46 11.461-2.282 0-4.402-.661-6.186-1.809.324.037.636.05.973.05a8.07 8.07 0 0 0 5.001-1.721 4.036 4.036 0 0 1-3.767-2.793c.249.037.499.062.761.062.361 0 .724-.05 1.061-.137a4.027 4.027 0 0 1-3.23-3.953v-.05c.537.299 1.16.486 1.82.511a4.022 4.022 0 0 1-1.796-3.354c0-.748.199-1.434.548-2.032a11.457 11.457 0 0 0 8.306 4.215c-.062-.3-.1-.611-.1-.923a4.026 4.026 0 0 1 4.028-4.028c1.16 0 2.207.486 2.943 1.272a7.957 7.957 0 0 0 2.556-.973 4.02 4.02 0 0 1-1.771 2.22 8.073 8.073 0 0 0 2.319-.624 8.645 8.645 0 0 1-2.019 2.083z" d="M19.633 7.997c.013.175.013.349.013.523 0 5.325-4.053 11.461-11.46 11.461-2.282 0-4.402-.661-6.186-1.809.324.037.636.05.973.05a8.07 8.07 0 0 0 5.001-1.721 4.036 4.036 0 0 1-3.767-2.793c.249.037.499.062.761.062.361 0 .724-.05 1.061-.137a4.027 4.027 0 0 1-3.23-3.953v-.05c.537.299 1.16.486 1.82.511a4.022 4.022 0 0 1-1.796-3.354c0-.748.199-1.434.548-2.032a11.457 11.457 0 0 0 8.306 4.215c-.062-.3-.1-.611-.1-.923a4.026 4.026 0 0 1 4.028-4.028c1.16 0 2.207.486 2.943 1.272a7.957 7.957 0 0 0 2.556-.973 4.02 4.02 0 0 1-1.771 2.22 8.073 8.073 0 0 0 2.319-.624 8.645 8.645 0 0 1-2.019 2.083z"
fill="currentColor" fill="currentColor"
@@ -65,19 +34,9 @@ export const TwitterIcon: React.FC<IconSvgProps> = ({
); );
}; };
export const GithubIcon: React.FC<IconSvgProps> = ({ export const GithubIcon: React.FC<IconSvgProps> = ({ size = 24, width, height, ...props }) => {
size = 24,
width,
height,
...props
}) => {
return ( return (
<svg <svg height={size || height} viewBox="0 0 24 24" width={size || width} {...props}>
height={size || height}
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<path <path
clipRule="evenodd" clipRule="evenodd"
d="M12.026 2c-5.509 0-9.974 4.465-9.974 9.974 0 4.406 2.857 8.145 6.821 9.465.499.09.679-.217.679-.481 0-.237-.008-.865-.011-1.696-2.775.602-3.361-1.338-3.361-1.338-.452-1.152-1.107-1.459-1.107-1.459-.905-.619.069-.605.069-.605 1.002.07 1.527 1.028 1.527 1.028.89 1.524 2.336 1.084 2.902.829.091-.645.351-1.085.635-1.334-2.214-.251-4.542-1.107-4.542-4.93 0-1.087.389-1.979 1.024-2.675-.101-.253-.446-1.268.099-2.64 0 0 .837-.269 2.742 1.021a9.582 9.582 0 0 1 2.496-.336 9.554 9.554 0 0 1 2.496.336c1.906-1.291 2.742-1.021 2.742-1.021.545 1.372.203 2.387.099 2.64.64.696 1.024 1.587 1.024 2.675 0 3.833-2.33 4.675-4.552 4.922.355.308.675.916.675 1.846 0 1.334-.012 2.41-.012 2.737 0 .267.178.577.687.479C19.146 20.115 22 16.379 22 11.974 22 6.465 17.535 2 12.026 2z" d="M12.026 2c-5.509 0-9.974 4.465-9.974 9.974 0 4.406 2.857 8.145 6.821 9.465.499.09.679-.217.679-.481 0-.237-.008-.865-.011-1.696-2.775.602-3.361-1.338-3.361-1.338-.452-1.152-1.107-1.459-1.107-1.459-.905-.619.069-.605.069-.605 1.002.07 1.527 1.028 1.527 1.028.89 1.524 2.336 1.084 2.902.829.091-.645.351-1.085.635-1.334-2.214-.251-4.542-1.107-4.542-4.93 0-1.087.389-1.979 1.024-2.675-.101-.253-.446-1.268.099-2.64 0 0 .837-.269 2.742 1.021a9.582 9.582 0 0 1 2.496-.336 9.554 9.554 0 0 1 2.496.336c1.906-1.291 2.742-1.021 2.742-1.021.545 1.372.203 2.387.099 2.64.64.696 1.024 1.587 1.024 2.675 0 3.833-2.33 4.675-4.552 4.922.355.308.675.916.675 1.846 0 1.334-.012 2.41-.012 2.737 0 .267.178.577.687.479C19.146 20.115 22 16.379 22 11.974 22 6.465 17.535 2 12.026 2z"
@@ -88,12 +47,7 @@ export const GithubIcon: React.FC<IconSvgProps> = ({
); );
}; };
export const MoonFilledIcon = ({ export const MoonFilledIcon = ({ size = 24, width, height, ...props }: IconSvgProps) => (
size = 24,
width,
height,
...props
}: IconSvgProps) => (
<svg <svg
aria-hidden="true" aria-hidden="true"
focusable="false" focusable="false"
@@ -110,12 +64,7 @@ export const MoonFilledIcon = ({
</svg> </svg>
); );
export const SunFilledIcon = ({ export const SunFilledIcon = ({ size = 24, width, height, ...props }: IconSvgProps) => (
size = 24,
width,
height,
...props
}: IconSvgProps) => (
<svg <svg
aria-hidden="true" aria-hidden="true"
focusable="false" focusable="false"
@@ -132,12 +81,7 @@ export const SunFilledIcon = ({
</svg> </svg>
); );
export const HeartFilledIcon = ({ export const HeartFilledIcon = ({ size = 24, width, height, ...props }: IconSvgProps) => (
size = 24,
width,
height,
...props
}: IconSvgProps) => (
<svg <svg
aria-hidden="true" aria-hidden="true"
focusable="false" focusable="false"
@@ -175,12 +119,6 @@ export const SearchIcon = (props: IconSvgProps) => (
strokeLinejoin="round" strokeLinejoin="round"
strokeWidth="2" strokeWidth="2"
/> />
<path <path d="M22 22L20 20" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" />
d="M22 22L20 20"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
/>
</svg> </svg>
); );

View File

@@ -1,53 +1,37 @@
"use client"; 'use client';
import { FC } from "react"; import { FC } from 'react';
import { VisuallyHidden } from "@react-aria/visually-hidden"; import { VisuallyHidden } from '@react-aria/visually-hidden';
import { SwitchProps, useSwitch } from "@nextui-org/react"; import { SwitchProps, useSwitch } from '@nextui-org/react';
import { useTheme } from "next-themes"; import { useTheme } from 'next-themes';
import { useIsSSR } from "@react-aria/ssr"; import { useIsSSR } from '@react-aria/ssr';
import clsx from "clsx"; import clsx from 'clsx';
import { SunFilledIcon, MoonFilledIcon } from "@/components/icons"; import { SunFilledIcon, MoonFilledIcon } from '@/components/icons';
export interface ThemeSwitchProps { export interface ThemeSwitchProps {
className?: string; className?: string;
classNames?: SwitchProps["classNames"]; classNames?: SwitchProps['classNames'];
} }
export const ThemeSwitch: FC<ThemeSwitchProps> = ({ export const ThemeSwitch: FC<ThemeSwitchProps> = ({ className, classNames }) => {
className,
classNames,
}) => {
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme();
const isSSR = useIsSSR(); const isSSR = useIsSSR();
const onChange = () => { const onChange = () => {
theme === "light" ? setTheme("dark") : setTheme("light"); theme === 'light' ? setTheme('dark') : setTheme('light');
}; };
const { const { Component, slots, isSelected, getBaseProps, getInputProps, getWrapperProps } = useSwitch({
Component, isSelected: theme === 'light' || isSSR,
slots, 'aria-label': `Switch to ${theme === 'light' || isSSR ? 'dark' : 'light'} mode`,
isSelected,
getBaseProps,
getInputProps,
getWrapperProps,
} = useSwitch({
isSelected: theme === "light" || isSSR,
"aria-label": `Switch to ${
theme === "light" || isSSR ? "dark" : "light"
} mode`,
onChange, onChange,
}); });
return ( return (
<Component <Component
{...getBaseProps({ {...getBaseProps({
className: clsx( className: clsx('px-px transition-opacity hover:opacity-80 cursor-pointer', className, classNames?.base),
"px-px transition-opacity hover:opacity-80 cursor-pointer",
className,
classNames?.base
),
})} })}
> >
<VisuallyHidden> <VisuallyHidden>
@@ -58,25 +42,21 @@ export const ThemeSwitch: FC<ThemeSwitchProps> = ({
className={slots.wrapper({ className={slots.wrapper({
class: clsx( class: clsx(
[ [
"w-auto h-auto", 'w-auto h-auto',
"bg-transparent", 'bg-transparent',
"rounded-lg", 'rounded-lg',
"flex items-center justify-center", 'flex items-center justify-center',
"group-data-[selected=true]:bg-transparent", 'group-data-[selected=true]:bg-transparent',
"!text-default-500", '!text-default-500',
"pt-px", 'pt-px',
"px-0", 'px-0',
"mx-0", 'mx-0',
], ],
classNames?.wrapper classNames?.wrapper
), ),
})} })}
> >
{!isSelected || isSSR ? ( {!isSelected || isSSR ? <SunFilledIcon size={22} /> : <MoonFilledIcon size={22} />}
<SunFilledIcon size={22} />
) : (
<MoonFilledIcon size={22} />
)}
</div> </div>
</Component> </Component>
); );

View File

@@ -1,22 +1,10 @@
"use client"; 'use client';
import { import { Button, DropdownTrigger, Dropdown, DropdownMenu, DropdownItem } from '@nextui-org/react';
Button, import { User, ChevronDown, PenTool, ArrowRightFromLine, ArrowRightToLine } from 'lucide-react';
DropdownTrigger, import { useContext } from 'react';
Dropdown, import { TokenContext } from './TokenProvider';
DropdownMenu, import { useRouter } from '@/src/navigation';
DropdownItem, import { AccountDropDownMessages } from '@/types/user';
} from "@nextui-org/react";
import {
User,
ChevronDown,
PenTool,
ArrowRightFromLine,
ArrowRightToLine,
} from "lucide-react";
import { useContext } from "react";
import { TokenContext } from "./TokenProvider";
import { useRouter } from "@/src/navigation";
import { AccountDropDownMessages } from "@/types/user";
type Props = { type Props = {
messages: AccountDropDownMessages; messages: AccountDropDownMessages;
@@ -35,15 +23,15 @@ export default function DropdownAccount({ messages, locale }: Props) {
const signinItems = [ const signinItems = [
{ {
uid: "account", uid: 'account',
title: messages.account, title: messages.account,
icon: <User size={16} />, icon: <User size={16} />,
onPress: () => { onPress: () => {
router.push("/account", { locale: locale }); router.push('/account', { locale: locale });
}, },
}, },
{ {
uid: "signout", uid: 'signout',
title: messages.signOut, title: messages.signOut,
icon: <ArrowRightFromLine size={16} />, icon: <ArrowRightFromLine size={16} />,
onPress: signOut, onPress: signOut,
@@ -52,19 +40,19 @@ export default function DropdownAccount({ messages, locale }: Props) {
const signoutItems = [ const signoutItems = [
{ {
uid: "signin", uid: 'signin',
title: messages.signIn, title: messages.signIn,
icon: <ArrowRightToLine size={16} />, icon: <ArrowRightToLine size={16} />,
onPress: () => { onPress: () => {
router.push("/account/signin", { locale: locale }); router.push('/account/signin', { locale: locale });
}, },
}, },
{ {
uid: "signup", uid: 'signup',
title: messages.signUp, title: messages.signUp,
icon: <PenTool size={16} />, icon: <PenTool size={16} />,
onPress: () => { onPress: () => {
router.push("/account/signup", { locale: locale }); router.push('/account/signup', { locale: locale });
}, },
}, },
]; ];
@@ -72,37 +60,20 @@ export default function DropdownAccount({ messages, locale }: Props) {
return ( return (
<Dropdown> <Dropdown>
<DropdownTrigger> <DropdownTrigger>
<Button <Button size="sm" variant="light" startContent={<User size={16} />} endContent={<ChevronDown size={16} />}>
size="sm" {context.token && context.token.user ? context.token.user.username : messages.signIn}
variant="light"
startContent={<User size={16} />}
endContent={<ChevronDown size={16} />}
>
{context.token && context.token.user
? context.token.user.username
: messages.signIn}
</Button> </Button>
</DropdownTrigger> </DropdownTrigger>
{context.token && context.token.user ? ( {context.token && context.token.user ? (
<DropdownMenu aria-label="account actions when sign in"> <DropdownMenu aria-label="account actions when sign in">
{signinItems.map((entry) => ( {signinItems.map((entry) => (
<DropdownItem <DropdownItem key={entry.uid} title={entry.title} startContent={entry.icon} onPress={entry.onPress} />
key={entry.uid}
title={entry.title}
startContent={entry.icon}
onPress={entry.onPress}
/>
))} ))}
</DropdownMenu> </DropdownMenu>
) : ( ) : (
<DropdownMenu aria-label="account actions when sign out"> <DropdownMenu aria-label="account actions when sign out">
{signoutItems.map((entry) => ( {signoutItems.map((entry) => (
<DropdownItem <DropdownItem key={entry.uid} title={entry.title} startContent={entry.icon} onPress={entry.onPress} />
key={entry.uid}
title={entry.title}
startContent={entry.icon}
onPress={entry.onPress}
/>
))} ))}
</DropdownMenu> </DropdownMenu>
)} )}

View File

@@ -1,18 +1,12 @@
"use client"; 'use client';
import { import { Button, DropdownTrigger, Dropdown, DropdownMenu, DropdownItem } from '@nextui-org/react';
Button, import { Globe, ChevronDown } from 'lucide-react';
DropdownTrigger, import { usePathname } from 'next/navigation';
Dropdown, import { useRouter } from '@/src/navigation';
DropdownMenu,
DropdownItem,
} from "@nextui-org/react";
import { Globe, ChevronDown } from "lucide-react";
import { usePathname } from "next/navigation";
import { useRouter } from "@/src/navigation";
const locales = [ const locales = [
{ code: "en", name: "English" }, { code: 'en', name: 'English' },
{ code: "ja", name: "日本語" }, { code: 'ja', name: '日本語' },
]; ];
export default function DropdownLanguage(params: { locale: string }) { export default function DropdownLanguage(params: { locale: string }) {
@@ -23,7 +17,7 @@ export default function DropdownLanguage(params: { locale: string }) {
let newPathname; let newPathname;
if (pathname.length < 4) { if (pathname.length < 4) {
// when root path // when root path
router.push("/", { locale: nextLocale }); router.push('/', { locale: nextLocale });
} else { } else {
// when not root path, trim first "/en" from pathname = "/en/projects" // when not root path, trim first "/en" from pathname = "/en/projects"
newPathname = pathname.slice(params.locale.length + 1); newPathname = pathname.slice(params.locale.length + 1);
@@ -34,22 +28,13 @@ export default function DropdownLanguage(params: { locale: string }) {
return ( return (
<Dropdown> <Dropdown>
<DropdownTrigger> <DropdownTrigger>
<Button <Button size="sm" variant="light" startContent={<Globe size={16} />} endContent={<ChevronDown size={16} />}>
size="sm" {locales.find((locale) => locale.code === params.locale)?.name || params.locale}
variant="light"
startContent={<Globe size={16} />}
endContent={<ChevronDown size={16} />}
>
{locales.find((locale) => locale.code === params.locale)?.name ||
params.locale}
</Button> </Button>
</DropdownTrigger> </DropdownTrigger>
<DropdownMenu aria-label="lacales"> <DropdownMenu aria-label="lacales">
{locales.map((entry) => ( {locales.map((entry) => (
<DropdownItem <DropdownItem key={entry.code} onClick={() => changeLocale(entry.code)}>
key={entry.code}
onClick={() => changeLocale(entry.code)}
>
{entry.name} {entry.name}
</DropdownItem> </DropdownItem>
))} ))}

View File

@@ -8,37 +8,37 @@ import {
NavbarMenuItem, NavbarMenuItem,
Chip, Chip,
Link as NextUiLink, Link as NextUiLink,
} from "@nextui-org/react"; } from '@nextui-org/react';
import { MoveUpRight } from "lucide-react"; import { MoveUpRight } from 'lucide-react';
import { useTranslations } from "next-intl"; import { useTranslations } from 'next-intl';
import { Link } from "@/src/navigation"; import { Link } from '@/src/navigation';
import { ThemeSwitch } from "@/components/theme-switch"; import { ThemeSwitch } from '@/components/theme-switch';
import { GithubIcon } from "@/components/icons"; import { GithubIcon } from '@/components/icons';
import Image from "next/image"; import Image from 'next/image';
import DropdownAccount from "./DropdownAccount"; import DropdownAccount from './DropdownAccount';
import DropdownLanguage from "./DropdownLanguage"; import DropdownLanguage from './DropdownLanguage';
export default function Header(params: { locale: string }) { export default function Header(params: { locale: string }) {
const t = useTranslations("Header"); const t = useTranslations('Header');
const messages = { const messages = {
account: t("account"), account: t('account'),
signUp: t("signup"), signUp: t('signup'),
signIn: t("signin"), signIn: t('signin'),
signOut: t("signout"), signOut: t('signout'),
}; };
// Links shown Header or slider // Links shown Header or slider
const commonLinks = [ const commonLinks = [
{ {
uid: "projects", uid: 'projects',
href: "/projects", href: '/projects',
label: t("projects"), label: t('projects'),
isExternal: false, isExternal: false,
}, },
{ {
uid: "docs", uid: 'docs',
href: "https://kimatata.github.io/TestPlat/docs/getstarted/selfhost", href: 'https://kimatata.github.io/TestPlat/docs/getstarted/selfhost',
label: t("docs"), label: t('docs'),
isExternal: true, isExternal: true,
}, },
]; ];
@@ -47,17 +47,8 @@ export default function Header(params: { locale: string }) {
<NextUINavbar maxWidth="full" position="sticky" className="bg-inherit"> <NextUINavbar maxWidth="full" position="sticky" className="bg-inherit">
<NavbarContent className="basis-1/5 sm:basis-full" justify="start"> <NavbarContent className="basis-1/5 sm:basis-full" justify="start">
<NavbarBrand as="li" className="gap-3 max-w-fit"> <NavbarBrand as="li" className="gap-3 max-w-fit">
<Link <Link className="flex justify-start items-center gap-1" href="/" locale={params.locale}>
className="flex justify-start items-center gap-1" <Image src="/favicon/android-chrome-192x192.png" width={32} height={32} alt="Logo" />
href="/"
locale={params.locale}
>
<Image
src="/favicon/android-chrome-192x192.png"
width={32}
height={32}
alt="Logo"
/>
<p className="font-bold text-inherit">TestPlat</p> <p className="font-bold text-inherit">TestPlat</p>
</Link> </Link>
</NavbarBrand> </NavbarBrand>
@@ -82,7 +73,7 @@ export default function Header(params: { locale: string }) {
showAnchorIcon showAnchorIcon
anchorIcon={<MoveUpRight size={12} className="ms-1" />} anchorIcon={<MoveUpRight size={12} className="ms-1" />}
> >
{t("docs")} {t('docs')}
</NextUiLink> </NextUiLink>
</NavbarItem> </NavbarItem>
) : ( ) : (
@@ -100,11 +91,7 @@ export default function Header(params: { locale: string }) {
</NavbarContent> </NavbarContent>
<NavbarContent className="basis-1 pl-4" justify="end"> <NavbarContent className="basis-1 pl-4" justify="end">
<NextUiLink <NextUiLink isExternal href="https://github.com/kimatata/TestPlat" aria-label="Github">
isExternal
href="https://github.com/kimatata/TestPlat"
aria-label="Github"
>
<GithubIcon className="text-default-500" /> <GithubIcon className="text-default-500" />
</NextUiLink> </NextUiLink>
<ThemeSwitch /> <ThemeSwitch />
@@ -126,12 +113,8 @@ export default function Header(params: { locale: string }) {
{commonLinks.map((link) => {commonLinks.map((link) =>
link.isExternal ? ( link.isExternal ? (
<NavbarMenuItem key={link.uid}> <NavbarMenuItem key={link.uid}>
<NextUiLink <NextUiLink href={link.href} showAnchorIcon anchorIcon={<MoveUpRight size={12} className="ms-1" />}>
href={link.href} {t('docs')}
showAnchorIcon
anchorIcon={<MoveUpRight size={12} className="ms-1" />}
>
{t("docs")}
</NextUiLink> </NextUiLink>
</NavbarMenuItem> </NavbarMenuItem>
) : ( ) : (

View File

@@ -1,7 +1,7 @@
"use client"; 'use client';
import { title } from "@/components/primitives"; import { title } from '@/components/primitives';
import { useState, useEffect } from "react"; import { useState, useEffect } from 'react';
import { Tabs, Tab, Image } from "@nextui-org/react"; import { Tabs, Tab, Image } from '@nextui-org/react';
type PaneDemoImagesMessages = { type PaneDemoImagesMessages = {
title: string; title: string;
@@ -14,28 +14,28 @@ type Props = {
}; };
export default function DemoImages({ messages }: Props) { export default function DemoImages({ messages }: Props) {
const [currentTab, setcurrentTab] = useState("edit"); const [currentTab, setcurrentTab] = useState('edit');
const [currentImage, setcurrentImage] = useState({ const [currentImage, setcurrentImage] = useState({
src: "/top/caseEdit.png", src: '/top/caseEdit.png',
alt: messages.caseEdit, alt: messages.caseEdit,
}); });
const tabs = [ const tabs = [
{ {
key: "edit", key: 'edit',
title: messages.caseEdit, title: messages.caseEdit,
src: "/top/caseEdit.png", src: '/top/caseEdit.png',
alt: messages.caseEdit, alt: messages.caseEdit,
}, },
{ {
key: "home", key: 'home',
title: messages.caseHome, title: messages.caseHome,
src: "/top/caseHome.png", src: '/top/caseHome.png',
alt: messages.caseHome, alt: messages.caseHome,
}, },
{ {
key: "run", key: 'run',
title: messages.caseRun, title: messages.caseRun,
src: "/top/caseRun.png", src: '/top/caseRun.png',
alt: messages.caseRun, alt: messages.caseRun,
}, },
]; ];
@@ -50,11 +50,11 @@ export default function DemoImages({ messages }: Props) {
<> <>
<div className="flex flex-wrap lg:text-left text-center"> <div className="flex flex-wrap lg:text-left text-center">
<div className="w-full lg:w-5/12 p-4"> <div className="w-full lg:w-5/12 p-4">
<h2 className={title({ size: "sm" })}>{messages.title}</h2> <h2 className={title({ size: 'sm' })}>{messages.title}</h2>
<br /> <br />
<Tabs <Tabs
aria-label="Options" aria-label="Options"
color={"primary"} color={'primary'}
radius="full" radius="full"
size="md" size="md"
className="mt-8" className="mt-8"
@@ -68,13 +68,7 @@ export default function DemoImages({ messages }: Props) {
</div> </div>
<div className="flex justify-center w-full lg:w-7/12 p-4"> <div className="flex justify-center w-full lg:w-7/12 p-4">
<Image <Image src={currentImage.src} alt={currentImage.alt} shadow="md" height={500} className="max-w-full" />
src={currentImage.src}
alt={currentImage.alt}
shadow="md"
height={500}
className="max-w-full"
/>
</div> </div>
</div> </div>
</> </>

View File

@@ -1,30 +1,30 @@
import { title, subtitle } from "@/components/primitives"; import { title, subtitle } from '@/components/primitives';
import { Card, CardHeader, CardBody, Avatar } from "@nextui-org/react"; import { Card, CardHeader, CardBody, Avatar } from '@nextui-org/react';
import { Scale, Folder, Check, Globe } from "lucide-react"; import { Scale, Folder, Check, Globe } from 'lucide-react';
import { useTranslations } from "next-intl"; import { useTranslations } from 'next-intl';
export default function MainTitle() { export default function MainTitle() {
const t = useTranslations("Index"); const t = useTranslations('Index');
const features = [ const features = [
{ {
title: t("oss_title"), title: t('oss_title'),
detail: t("oss_detail"), detail: t('oss_detail'),
icon: <Scale size={24} color="#52e280" />, icon: <Scale size={24} color="#52e280" />,
}, },
{ {
title: t("organize_title"), title: t('organize_title'),
detail: t("organize_detail"), detail: t('organize_detail'),
icon: <Folder size={24} color="#52e280" />, icon: <Folder size={24} color="#52e280" />,
}, },
{ {
title: t("usability_title"), title: t('usability_title'),
detail: t("usability_detail"), detail: t('usability_detail'),
icon: <Check size={24} color="#52e280" />, icon: <Check size={24} color="#52e280" />,
}, },
{ {
title: t("universal_title"), title: t('universal_title'),
detail: t("universal_detail"), detail: t('universal_detail'),
icon: <Globe size={24} color="#52e280" />, icon: <Globe size={24} color="#52e280" />,
}, },
]; ];
@@ -36,12 +36,7 @@ export default function MainTitle() {
<Card key={feature.title} className="max-w-[300px] min-h-[180px]"> <Card key={feature.title} className="max-w-[300px] min-h-[180px]">
<CardHeader className="flex gap-3"> <CardHeader className="flex gap-3">
<div> <div>
<Avatar <Avatar className="bg-green-100" showFallback src="" fallback={feature.icon} />
className="bg-green-100"
showFallback
src=""
fallback={feature.icon}
/>
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<h4 className="font-bold text-large">{feature.title}</h4> <h4 className="font-bold text-large">{feature.title}</h4>

View File

@@ -1,22 +1,22 @@
import { title, subtitle } from "@/components/primitives"; import { title, subtitle } from '@/components/primitives';
import { Button, Link as NextUiLink } from "@nextui-org/react"; import { Button, Link as NextUiLink } from '@nextui-org/react';
import { MoveUpRight } from "lucide-react"; import { MoveUpRight } from 'lucide-react';
import { Link } from "@/src/navigation"; import { Link } from '@/src/navigation';
import { useTranslations } from "next-intl"; import { useTranslations } from 'next-intl';
type Props = { type Props = {
locale: string; locale: string;
}; };
export default function MainTitle({ locale }: Props) { export default function MainTitle({ locale }: Props) {
const t = useTranslations("Index"); const t = useTranslations('Index');
return ( return (
<div className="md:text-left text-center"> <div className="md:text-left text-center">
<h1 <h1
className={title({ className={title({
color: "green", color: 'green',
class: "lg:text-7xl md:text-7xl sm:text-7xl text-7xl", class: 'lg:text-7xl md:text-7xl sm:text-7xl text-7xl',
})} })}
> >
TestPlat TestPlat
@@ -25,29 +25,23 @@ export default function MainTitle({ locale }: Props) {
<br /> <br />
<h1 <h1
className={title({ className={title({
class: "lg:text-5xl md:text-5xl sm:text-5xl text-5xl", class: 'lg:text-5xl md:text-5xl sm:text-5xl text-5xl',
})} })}
> >
{t("oss_tcmt")} {t('oss_tcmt')}
<br /> <br />
{t("web_application")} {t('web_application')}
</h1> </h1>
<h4 className={subtitle({ class: "mt-4" })}> <h4 className={subtitle({ class: 'mt-4' })}>{t('integrate_and_manage')}</h4>
{t("integrate_and_manage")}
</h4>
<div className="mt-5"> <div className="mt-5">
<Link href={`/projects/`} locale={locale}> <Link href={`/projects/`} locale={locale}>
<Button color="primary" radius="full" className="px-0"> <Button color="primary" radius="full" className="px-0">
{t("demo")} {t('demo')}
</Button> </Button>
</Link> </Link>
<NextUiLink <NextUiLink isExternal href="https://kimatata.github.io/TestPlat/docs/getstarted/selfhost" aria-label="docs">
isExternal
href="https://kimatata.github.io/TestPlat/docs/getstarted/selfhost"
aria-label="docs"
>
<Button <Button
color="primary" color="primary"
variant="bordered" variant="bordered"
@@ -55,16 +49,11 @@ export default function MainTitle({ locale }: Props) {
className="ms-2" className="ms-2"
endContent={<MoveUpRight size={12} />} endContent={<MoveUpRight size={12} />}
> >
{t("get_started")} {t('get_started')}
</Button> </Button>
</NextUiLink> </NextUiLink>
<NextUiLink <NextUiLink size="sm" isExternal href="https://github.com/kimatata/TestPlat" aria-label="Github">
size="sm"
isExternal
href="https://github.com/kimatata/TestPlat"
aria-label="Github"
>
<Button <Button
color="primary" color="primary"
variant="bordered" variant="bordered"

View File

@@ -1,11 +1,11 @@
"use client"; 'use client';
import { createContext, useState, useEffect } from "react"; import { createContext, useState, useEffect } from 'react';
import { TokenType } from "@/types/user"; import { TokenType } from '@/types/user';
import { TokenProps } from "@/types/user"; import { TokenProps } from '@/types/user';
import { useRouter, usePathname } from "@/src/navigation"; import { useRouter, usePathname } from '@/src/navigation';
const LOCAL_STORAGE_KEY = "testplat-auth-token"; const LOCAL_STORAGE_KEY = 'testplat-auth-token';
const privatePaths = ["/account"]; const privatePaths = ['/account'];
function storeTokenToLocalStorage(token: TokenType) { function storeTokenToLocalStorage(token: TokenType) {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(token)); localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(token));
@@ -23,7 +23,7 @@ const TokenProvider = ({ locale, children }: TokenProps) => {
const pathname = usePathname(); const pathname = usePathname();
const [token, setToken] = useState<TokenType>({ const [token, setToken] = useState<TokenType>({
access_token: "", access_token: '',
user: null, user: null,
}); });
const tokenContext = { const tokenContext = {
@@ -57,11 +57,7 @@ const TokenProvider = ({ locale, children }: TokenProps) => {
restoreTokenFromLocalStorage(); restoreTokenFromLocalStorage();
}, []); }, []);
return ( return <TokenContext.Provider value={tokenContext}>{children}</TokenContext.Provider>;
<TokenContext.Provider value={tokenContext}>
{children}
</TokenContext.Provider>
);
}; };
export { TokenContext }; export { TokenContext };

View File

@@ -1,7 +1,7 @@
"use client"; 'use client';
import { useContext } from "react"; import { useContext } from 'react';
import { Avatar, Card, CardHeader, CardBody, Divider } from "@nextui-org/react"; import { Avatar, Card, CardHeader, CardBody, Divider } from '@nextui-org/react';
import { TokenContext } from "../TokenProvider"; import { TokenContext } from '../TokenProvider';
type AccountPageMessages = { type AccountPageMessages = {
yourProjects: string; yourProjects: string;
@@ -20,19 +20,10 @@ export default function AccountPage({ messages, locale }: Props) {
{context.token && context.token.user && ( {context.token && context.token.user && (
<Card className="w-[600px] mt-16 mx-3"> <Card className="w-[600px] mt-16 mx-3">
<CardHeader className="flex gap-6"> <CardHeader className="flex gap-6">
<Avatar <Avatar isBordered radius="full" className="w-16 h-16 text-large" src={context.token.user.avatarPath} />
isBordered
radius="full"
className="w-16 h-16 text-large"
src={context.token.user.avatarPath}
/>
<div className="flex flex-col"> <div className="flex flex-col">
<p className="text-2xl font-bold"> <p className="text-2xl font-bold">{context.token.user.username}</p>
{context.token.user.username} <p className="text-lg text-default-500">{context.token.user.email}</p>
</p>
<p className="text-lg text-default-500">
{context.token.user.email}
</p>
</div> </div>
</CardHeader> </CardHeader>
<Divider /> <Divider />

View File

@@ -1,15 +1,15 @@
"use client"; 'use client';
import React from "react"; import React from 'react';
import { useState, useContext } from "react"; import { useState, useContext } from 'react';
import { Input, Button, Card, CardHeader, CardBody } from "@nextui-org/react"; import { Input, Button, Card, CardHeader, CardBody } from '@nextui-org/react';
import { Link } from "@/src/navigation"; import { Link } from '@/src/navigation';
import { ChevronRight, Eye, EyeOff } from "lucide-react"; import { ChevronRight, Eye, EyeOff } from 'lucide-react';
import { UserType, AuthMessages } from "@/types/user"; import { UserType, AuthMessages } from '@/types/user';
import { roles } from "@/config/selection"; import { roles } from '@/config/selection';
import { signUp, signIn, getRandomAvatarPath } from "./authControl"; import { signUp, signIn, getRandomAvatarPath } from './authControl';
import { isValidEmail, isValidPassword } from "./validate"; import { isValidEmail, isValidPassword } from './validate';
import { TokenContext } from "../TokenProvider"; import { TokenContext } from '../TokenProvider';
import { useRouter } from "@/src/navigation"; import { useRouter } from '@/src/navigation';
type Props = { type Props = {
isSignup: Boolean; isSignup: Boolean;
@@ -22,18 +22,17 @@ export default function AuthPage({ isSignup, messages, locale }: Props) {
const context = useContext(TokenContext); const context = useContext(TokenContext);
const [user, setUser] = useState<UserType>({ const [user, setUser] = useState<UserType>({
id: null, id: null,
email: "", email: '',
password: "", password: '',
username: "", username: '',
role: roles.findIndex((entry) => entry.uid === "user"), role: roles.findIndex((entry) => entry.uid === 'user'),
avatarPath: "", avatarPath: '',
}); });
const [confirmPassword, setConfirmPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState('');
const [errorMessage, setErrorMessage] = useState(""); const [errorMessage, setErrorMessage] = useState('');
const [isPasswordVisible, setIsPasswordVisible] = useState(false); const [isPasswordVisible, setIsPasswordVisible] = useState(false);
const togglePasswordVisibility = () => const togglePasswordVisibility = () => setIsPasswordVisible(!isPasswordVisible);
setIsPasswordVisible(!isPasswordVisible);
const validate = async () => { const validate = async () => {
if (!isValidEmail(user.email)) { if (!isValidEmail(user.email)) {
@@ -83,31 +82,22 @@ export default function AuthPage({ isSignup, messages, locale }: Props) {
context.setToken(token); context.setToken(token);
context.storeTokenToLocalStorage(token); context.storeTokenToLocalStorage(token);
router.push("/account", { locale: locale }); router.push('/account', { locale: locale });
}; };
return ( return (
<Card className="w-[480px] mt-16"> <Card className="w-[480px] mt-16">
<CardHeader className="px-4 pt-4 pb-0 flex justify-between"> <CardHeader className="px-4 pt-4 pb-0 flex justify-between">
<h4 className="font-bold text-large">{messages.title}</h4> <h4 className="font-bold text-large">{messages.title}</h4>
<Link <Link href={isSignup ? '/account/signin' : '/account/signup'} locale={locale}>
href={isSignup ? "/account/signin" : "/account/signup"} <Button color="primary" variant="light" endContent={<ChevronRight size={16} />}>
locale={locale}
>
<Button
color="primary"
variant="light"
endContent={<ChevronRight size={16} />}
>
{messages.linkTitle} {messages.linkTitle}
</Button> </Button>
</Link> </Link>
</CardHeader> </CardHeader>
<CardBody className="overflow-visible px-4 pt-0 pb-4"> <CardBody className="overflow-visible px-4 pt-0 pb-4">
<form> <form>
{errorMessage && ( {errorMessage && <div className="my-3 text-danger">{errorMessage}</div>}
<div className="my-3 text-danger">{errorMessage}</div>
)}
<Input <Input
isRequired isRequired
type="email" type="email"
@@ -139,15 +129,11 @@ export default function AuthPage({ isSignup, messages, locale }: Props) {
<Input <Input
label={messages.password} label={messages.password}
variant="bordered" variant="bordered"
autoComplete={isSignup ? "new-password" : "current-password"} autoComplete={isSignup ? 'new-password' : 'current-password'}
className="mt-3" className="mt-3"
type={isPasswordVisible ? "text" : "password"} type={isPasswordVisible ? 'text' : 'password'}
endContent={ endContent={
<button <button className="focus:outline-none" type="button" onClick={togglePasswordVisibility}>
className="focus:outline-none"
type="button"
onClick={togglePasswordVisibility}
>
{isPasswordVisible ? <Eye size={20} /> : <EyeOff size={20} />} {isPasswordVisible ? <Eye size={20} /> : <EyeOff size={20} />}
</button> </button>
} }
@@ -164,13 +150,9 @@ export default function AuthPage({ isSignup, messages, locale }: Props) {
variant="bordered" variant="bordered"
autoComplete="new-password" autoComplete="new-password"
className="mt-3" className="mt-3"
type={isPasswordVisible ? "text" : "password"} type={isPasswordVisible ? 'text' : 'password'}
endContent={ endContent={
<button <button className="focus:outline-none" type="button" onClick={togglePasswordVisibility}>
className="focus:outline-none"
type="button"
onClick={togglePasswordVisibility}
>
{isPasswordVisible ? <Eye size={20} /> : <EyeOff size={20} />} {isPasswordVisible ? <Eye size={20} /> : <EyeOff size={20} />}
</button> </button>
} }

View File

@@ -1,8 +1,4 @@
export default function AuthLayout({ export default function AuthLayout({ children }: { children: React.ReactNode }) {
children,
}: {
children: React.ReactNode;
}) {
return ( return (
<> <>
<div className="w-full flex items-center justify-center">{children}</div> <div className="w-full flex items-center justify-center">{children}</div>

View File

@@ -1,9 +1,9 @@
import { useTranslations } from "next-intl"; import { useTranslations } from 'next-intl';
import AccountPage from "./AccountPage"; import AccountPage from './AccountPage';
export default function Page(params: { locale: string }) { export default function Page(params: { locale: string }) {
const t = useTranslations("Auth"); const t = useTranslations('Auth');
const messages = { const messages = {
yourProjects: t("your_projects"), yourProjects: t('your_projects'),
}; };
return <AccountPage messages={messages} locale={params.locale} />; return <AccountPage messages={messages} locale={params.locale} />;

View File

@@ -1,24 +1,24 @@
import AuthPage from "../authPage"; import AuthPage from '../authPage';
import { useTranslations } from "next-intl"; import { useTranslations } from 'next-intl';
export default function Page(params: { locale: string }) { export default function Page(params: { locale: string }) {
const t = useTranslations("Auth"); const t = useTranslations('Auth');
const messages = { const messages = {
title: t("signin"), title: t('signin'),
linkTitle: t("or_signup"), linkTitle: t('or_signup'),
submitTitle: t("signin"), submitTitle: t('signin'),
email: t("email"), email: t('email'),
username: t("username"), username: t('username'),
password: t("password"), password: t('password'),
confirmPassword: t("confirm_password"), confirmPassword: t('confirm_password'),
invalidEmail: t("invalid_email"), invalidEmail: t('invalid_email'),
invalidPassword: t("invalid_password"), invalidPassword: t('invalid_password'),
usernameEmpty: t("username_empty"), usernameEmpty: t('username_empty'),
passwordDoesNotMatch: t("password_not_match"), passwordDoesNotMatch: t('password_not_match'),
EmailAlreadyExist: t("email_already_exist"), EmailAlreadyExist: t('email_already_exist'),
emailNotExist: t("email_not_exist"), emailNotExist: t('email_not_exist'),
signupError: t("signup_error"), signupError: t('signup_error'),
signinError: t("signin_error"), signinError: t('signin_error'),
}; };
return ( return (
<> <>

View File

@@ -1,24 +1,24 @@
import AuthPage from "../authPage"; import AuthPage from '../authPage';
import { useTranslations } from "next-intl"; import { useTranslations } from 'next-intl';
export default function Page(params: { locale: string }) { export default function Page(params: { locale: string }) {
const t = useTranslations("Auth"); const t = useTranslations('Auth');
const messages = { const messages = {
title: t("signup"), title: t('signup'),
linkTitle: t("or_signin"), linkTitle: t('or_signin'),
submitTitle: t("signup"), submitTitle: t('signup'),
email: t("email"), email: t('email'),
username: t("username"), username: t('username'),
password: t("password"), password: t('password'),
confirmPassword: t("confirm_password"), confirmPassword: t('confirm_password'),
invalidEmail: t("invalid_email"), invalidEmail: t('invalid_email'),
invalidPassword: t("invalid_password"), invalidPassword: t('invalid_password'),
usernameEmpty: t("username_empty"), usernameEmpty: t('username_empty'),
passwordDoesNotMatch: t("password_not_match"), passwordDoesNotMatch: t('password_not_match'),
EmailAlreadyExist: t("email_already_exist"), EmailAlreadyExist: t('email_already_exist'),
emailNotExist: t("email_not_exist"), emailNotExist: t('email_not_exist'),
signupError: t("signup_error"), signupError: t('signup_error'),
signinError: t("signin_error"), signinError: t('signin_error'),
}; };
return ( return (
<> <>

View File

@@ -1,14 +1,8 @@
"use client"; 'use client';
import { useEffect } from "react"; import { useEffect } from 'react';
export default function Error({ export default function Error({ error, reset }: { error: Error; reset: () => void }) {
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
useEffect(() => { useEffect(() => {
// Log the error to an error reporting service // Log the error to an error reporting service
console.error(error); console.error(error);

View File

@@ -1,19 +1,19 @@
import "@/styles/globals.css"; import '@/styles/globals.css';
import { fontSans } from "@/config/fonts"; import { fontSans } from '@/config/fonts';
import { Providers } from "./providers"; import { Providers } from './providers';
import Header from "./Header"; import Header from './Header';
import clsx from "clsx"; import clsx from 'clsx';
import { getTranslations } from "next-intl/server"; import { getTranslations } from 'next-intl/server';
export async function generateMetadata({ params: { locale } }) { export async function generateMetadata({ params: { locale } }) {
const t = await getTranslations({ locale, namespace: "Header" }); const t = await getTranslations({ locale, namespace: 'Header' });
return { return {
title: t("title"), title: t('title'),
description: t("description"), description: t('description'),
icons: { icons: {
icon: "/favicon/favicon.ico", icon: '/favicon/favicon.ico',
shortcut: "/favicon/favicon-16x16.png", shortcut: '/favicon/favicon-16x16.png',
apple: "/favicon/apple-touch-icon.png", apple: '/favicon/apple-touch-icon.png',
}, },
}; };
} }
@@ -28,16 +28,8 @@ export default function RootLayout({
return ( return (
<html lang={locale} suppressHydrationWarning> <html lang={locale} suppressHydrationWarning>
<head /> <head />
<body <body className={clsx('min-h-[calc(100vh-64px)] bg-background font-sans antialiased', fontSans.variable)}>
className={clsx( <Providers themeProps={{ attribute: 'class', defaultTheme: 'light' }} tokenProps={{ locale: locale }}>
"min-h-[calc(100vh-64px)] bg-background font-sans antialiased",
fontSans.variable
)}
>
<Providers
themeProps={{ attribute: "class", defaultTheme: "light" }}
tokenProps={{ locale: locale }}
>
<div className="relative flex flex-col min-h-screen light:bg-neutral-50 dark:bg-neutral-800"> <div className="relative flex flex-col min-h-screen light:bg-neutral-50 dark:bg-neutral-800">
<Header locale={locale} /> <Header locale={locale} />
<main>{children}</main> <main>{children}</main>

View File

@@ -1,17 +1,17 @@
import { useTranslations } from "next-intl"; import { useTranslations } from 'next-intl';
import { Divider } from "@nextui-org/react"; import { Divider } from '@nextui-org/react';
import PaneMainTitle from "./PaneMainTitle"; import PaneMainTitle from './PaneMainTitle';
import PaneMainFeatures from "./PaneMainFeatures"; import PaneMainFeatures from './PaneMainFeatures';
import PaneDemoImages from "./PaneDemoImages"; import PaneDemoImages from './PaneDemoImages';
export default function Home(params: { locale: string }) { export default function Home(params: { locale: string }) {
const t = useTranslations("Index"); const t = useTranslations('Index');
const messages = { const messages = {
title: t("organize_test_cases"), title: t('organize_test_cases'),
caseEdit: t("case_edit"), caseEdit: t('case_edit'),
caseHome: t("case_home"), caseHome: t('case_home'),
caseRun: t("case_run"), caseRun: t('case_run'),
}; };
return ( return (
@@ -24,30 +24,30 @@ export default function Home(params: { locale: string }) {
<div className="w-full md:w-5/12 p-4"> <div className="w-full md:w-5/12 p-4">
<div <div
style={{ style={{
position: "relative", position: 'relative',
textAlign: "center", textAlign: 'center',
}} }}
> >
<span <span
style={{ style={{
fontSize: "10rem", fontSize: '10rem',
position: "relative", position: 'relative',
zIndex: 1, zIndex: 1,
marginLeft: "2.5rem", marginLeft: '2.5rem',
}} }}
> >
</span> </span>
<div <div
style={{ style={{
position: "absolute", position: 'absolute',
top: 0, top: 0,
left: 0, left: 0,
width: "100%", width: '100%',
height: "100%", height: '100%',
filter: "blur(48px)", filter: 'blur(48px)',
zIndex: 0, zIndex: 0,
background: "linear-gradient(to bottom, #ffecd2, #fcb69f)", background: 'linear-gradient(to bottom, #ffecd2, #fcb69f)',
}} }}
></div> ></div>
</div> </div>
@@ -57,7 +57,7 @@ export default function Home(params: { locale: string }) {
<div <div
className="flex flex-wrap flex-col items-center" className="flex flex-wrap flex-col items-center"
style={{ style={{
marginTop: "6rem", marginTop: '6rem',
}} }}
> >
<PaneMainFeatures /> <PaneMainFeatures />

View File

@@ -1,17 +1,8 @@
"use client"; 'use client';
import React from "react"; import React from 'react';
import { useState, useEffect } from "react"; import { useState, useEffect } from 'react';
import { import { Button, Input, Textarea, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from '@nextui-org/react';
Button, import { ProjectType, ProjectsMessages } from '@/types/project';
Input,
Textarea,
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
} from "@nextui-org/react";
import { ProjectType, ProjectsMessages } from "@/types/project";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@@ -21,23 +12,17 @@ type Props = {
messages: ProjectsMessages; messages: ProjectsMessages;
}; };
export default function ProjectDialog({ export default function ProjectDialog({ isOpen, editingProject, onCancel, onSubmit, messages }: Props) {
isOpen,
editingProject,
onCancel,
onSubmit,
messages,
}: Props) {
const [projectName, setProjectName] = useState({ const [projectName, setProjectName] = useState({
text: editingProject ? editingProject.name : "", text: editingProject ? editingProject.name : '',
isValid: false, isValid: false,
errorMessage: "", errorMessage: '',
}); });
const [projectDetail, setProjectDetail] = useState({ const [projectDetail, setProjectDetail] = useState({
text: editingProject ? editingProject.detail : "", text: editingProject ? editingProject.detail : '',
isValid: false, isValid: false,
errorMessage: "", errorMessage: '',
}); });
useEffect(() => { useEffect(() => {
@@ -49,17 +34,17 @@ export default function ProjectDialog({
setProjectDetail({ setProjectDetail({
...projectDetail, ...projectDetail,
text: editingProject.detail ? editingProject.detail : "", text: editingProject.detail ? editingProject.detail : '',
}); });
} else { } else {
setProjectName({ setProjectName({
...projectName, ...projectName,
text: "", text: '',
}); });
setProjectDetail({ setProjectDetail({
...projectDetail, ...projectDetail,
text: "", text: '',
}); });
} }
}, [editingProject]); }, [editingProject]);
@@ -67,20 +52,20 @@ export default function ProjectDialog({
const clear = () => { const clear = () => {
setProjectName({ setProjectName({
isValid: false, isValid: false,
text: "", text: '',
errorMessage: "", errorMessage: '',
}); });
setProjectDetail({ setProjectDetail({
isValid: false, isValid: false,
text: "", text: '',
errorMessage: "", errorMessage: '',
}); });
}; };
const validate = () => { const validate = () => {
if (!projectName.text) { if (!projectName.text) {
setProjectName({ setProjectName({
text: "", text: '',
isValid: false, isValid: false,
errorMessage: messages.pleaseEnter, errorMessage: messages.pleaseEnter,
}); });
@@ -100,9 +85,7 @@ export default function ProjectDialog({
}} }}
> >
<ModalContent> <ModalContent>
<ModalHeader className="flex flex-col gap-1"> <ModalHeader className="flex flex-col gap-1">{messages.project}</ModalHeader>
{messages.project}
</ModalHeader>
<ModalBody> <ModalBody>
<Input <Input
type="text" type="text"

View File

@@ -1,16 +1,11 @@
"use client"; 'use client';
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react';
import { Button } from "@nextui-org/react"; import { Button } from '@nextui-org/react';
import { Plus } from "lucide-react"; import { Plus } from 'lucide-react';
import { ProjectType, ProjectsMessages } from "@/types/project"; import { ProjectType, ProjectsMessages } from '@/types/project';
import ProjectsTable from "./ProjectsTable"; import ProjectsTable from './ProjectsTable';
import ProjectDialog from "./ProjectDialog"; import ProjectDialog from './ProjectDialog';
import { import { fetchProjects, createProject, updateProject, deleteProject } from './projectsControl';
fetchProjects,
createProject,
updateProject,
deleteProject,
} from "./projectsControl";
export type Props = { export type Props = {
messages: ProjectsMessages; messages: ProjectsMessages;
@@ -26,7 +21,7 @@ export default function ProjectsPage({ messages, locale }: Props) {
const data = await fetchProjects(); const data = await fetchProjects();
setProjects(data); setProjects(data);
} catch (error: any) { } catch (error: any) {
console.error("Error in effect:", error.message); console.error('Error in effect:', error.message);
} }
} }
@@ -35,9 +30,7 @@ export default function ProjectsPage({ messages, locale }: Props) {
// dialog // dialog
const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false); const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false);
const [editingProject, setEditingProject] = useState<ProjectType | null>( const [editingProject, setEditingProject] = useState<ProjectType | null>(null);
null
);
const openDialogForCreate = () => { const openDialogForCreate = () => {
setIsProjectDialogOpen(true); setIsProjectDialogOpen(true);
setEditingProject(null); setEditingProject(null);
@@ -50,14 +43,8 @@ export default function ProjectsPage({ messages, locale }: Props) {
const onSubmit = async (name: string, detail: string) => { const onSubmit = async (name: string, detail: string) => {
if (editingProject) { if (editingProject) {
const updatedProject = await updateProject( const updatedProject = await updateProject(editingProject.id, name, detail);
editingProject.id, const updatedProjects = projects.map((project) => (project.id === updatedProject.id ? updatedProject : project));
name,
detail
);
const updatedProjects = projects.map((project) =>
project.id === updatedProject.id ? updatedProject : project
);
setProjects(updatedProjects); setProjects(updatedProjects);
} else { } else {
const newProject = await createProject(name, detail); const newProject = await createProject(name, detail);
@@ -76,7 +63,7 @@ export default function ProjectsPage({ messages, locale }: Props) {
await deleteProject(projectId); await deleteProject(projectId);
setProjects(projects.filter((project) => project.id !== projectId)); setProjects(projects.filter((project) => project.id !== projectId));
} catch (error: any) { } catch (error: any) {
console.error("Error deleting project:", error); console.error('Error deleting project:', error);
} }
}; };
@@ -85,12 +72,7 @@ export default function ProjectsPage({ messages, locale }: Props) {
<div className="w-full p-3 flex items-center justify-between"> <div className="w-full p-3 flex items-center justify-between">
<h3 className="font-bold">{messages.projectList}</h3> <h3 className="font-bold">{messages.projectList}</h3>
<div> <div>
<Button <Button startContent={<Plus size={16} />} size="sm" color="primary" onClick={openDialogForCreate}>
startContent={<Plus size={16} />}
size="sm"
color="primary"
onClick={openDialogForCreate}
>
{messages.newProject} {messages.newProject}
</Button> </Button>
</div> </div>

View File

@@ -1,4 +1,4 @@
import { useState, useMemo, useCallback } from "react"; import { useState, useMemo, useCallback } from 'react';
import { import {
Table, Table,
TableHeader, TableHeader,
@@ -12,11 +12,11 @@ import {
DropdownMenu, DropdownMenu,
DropdownItem, DropdownItem,
SortDescriptor, SortDescriptor,
} from "@nextui-org/react"; } from '@nextui-org/react';
import { Link, NextUiLinkClasses } from "@/src/navigation"; import { Link, NextUiLinkClasses } from '@/src/navigation';
import { MoreVertical } from "lucide-react"; import { MoreVertical } from 'lucide-react';
import { ProjectType, ProjectsMessages } from "@/types/project"; import { ProjectType, ProjectsMessages } from '@/types/project';
import dayjs from "dayjs"; import dayjs from 'dayjs';
type Props = { type Props = {
projects: ProjectType[]; projects: ProjectType[];
@@ -26,24 +26,18 @@ type Props = {
locale: string; locale: string;
}; };
export default function ProjectsTable({ export default function ProjectsTable({ projects, onEditProject, onDeleteProject, messages, locale }: Props) {
projects,
onEditProject,
onDeleteProject,
messages,
locale,
}: Props) {
const headerColumns = [ const headerColumns = [
{ name: messages.id, uid: "id", sortable: true }, { name: messages.id, uid: 'id', sortable: true },
{ name: messages.name, uid: "name", sortable: true }, { name: messages.name, uid: 'name', sortable: true },
{ name: messages.detail, uid: "detail", sortable: true }, { name: messages.detail, uid: 'detail', sortable: true },
{ name: messages.lastUpdate, uid: "updatedAt", sortable: true }, { name: messages.lastUpdate, uid: 'updatedAt', sortable: true },
{ name: messages.actions, uid: "actions" }, { name: messages.actions, uid: 'actions' },
]; ];
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({ const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
column: "id", column: 'id',
direction: "ascending", direction: 'ascending',
}); });
const sortedItems = useMemo(() => { const sortedItems = useMemo(() => {
@@ -52,31 +46,27 @@ export default function ProjectsTable({
const second = b[sortDescriptor.column as keyof ProjectType] as number; const second = b[sortDescriptor.column as keyof ProjectType] as number;
const cmp = first < second ? -1 : first > second ? 1 : 0; const cmp = first < second ? -1 : first > second ? 1 : 0;
return sortDescriptor.direction === "descending" ? -cmp : cmp; return sortDescriptor.direction === 'descending' ? -cmp : cmp;
}); });
}, [sortDescriptor, projects]); }, [sortDescriptor, projects]);
const truncateText = (text: string, maxLength: number) => { const truncateText = (text: string, maxLength: number) => {
return text.length > maxLength ? text.slice(0, maxLength) + "..." : text; return text.length > maxLength ? text.slice(0, maxLength) + '...' : text;
}; };
const renderCell = useCallback((project: ProjectType, columnKey: Key) => { const renderCell = useCallback((project: ProjectType, columnKey: Key) => {
const cellValue = project[columnKey as keyof ProjectType]; const cellValue = project[columnKey as keyof ProjectType];
switch (columnKey) { switch (columnKey) {
case "id": case 'id':
return <span>{cellValue}</span>; return <span>{cellValue}</span>;
case "name": case 'name':
return ( return (
<Link <Link href={`/projects/${project.id}/home`} locale={locale} className={NextUiLinkClasses}>
href={`/projects/${project.id}/home`}
locale={locale}
className={NextUiLinkClasses}
>
{cellValue} {cellValue}
</Link> </Link>
); );
case "detail": case 'detail':
const maxLength = 20; const maxLength = 20;
const truncatedValue = truncateText(cellValue, maxLength); const truncatedValue = truncateText(cellValue, maxLength);
return ( return (
@@ -84,9 +74,9 @@ export default function ProjectsTable({
<div>{truncatedValue}</div> <div>{truncatedValue}</div>
</div> </div>
); );
case "updatedAt": case 'updatedAt':
return <span>{dayjs(cellValue).format("YYYY/MM/DD HH:mm")}</span>; return <span>{dayjs(cellValue).format('YYYY/MM/DD HH:mm')}</span>;
case "actions": case 'actions':
return ( return (
<Dropdown> <Dropdown>
<DropdownTrigger> <DropdownTrigger>
@@ -95,13 +85,8 @@ export default function ProjectsTable({
</Button> </Button>
</DropdownTrigger> </DropdownTrigger>
<DropdownMenu aria-label="project actions"> <DropdownMenu aria-label="project actions">
<DropdownItem onClick={() => onEditProject(project)}> <DropdownItem onClick={() => onEditProject(project)}>{messages.editProject}</DropdownItem>
{messages.editProject} <DropdownItem className="text-danger" onClick={() => onDeleteProject(project.id)}>
</DropdownItem>
<DropdownItem
className="text-danger"
onClick={() => onDeleteProject(project.id)}
>
{messages.deleteProject} {messages.deleteProject}
</DropdownItem> </DropdownItem>
</DropdownMenu> </DropdownMenu>
@@ -114,18 +99,18 @@ export default function ProjectsTable({
const classNames = useMemo( const classNames = useMemo(
() => ({ () => ({
wrapper: ["max-w-3xl"], wrapper: ['max-w-3xl'],
th: ["bg-transparent", "text-default-500", "border-b", "border-divider"], th: ['bg-transparent', 'text-default-500', 'border-b', 'border-divider'],
td: [ td: [
// changing the rows border radius // changing the rows border radius
// first // first
"group-data-[first=true]:first:before:rounded-none", 'group-data-[first=true]:first:before:rounded-none',
"group-data-[first=true]:last:before:rounded-none", 'group-data-[first=true]:last:before:rounded-none',
// middle // middle
"group-data-[middle=true]:before:rounded-none", 'group-data-[middle=true]:before:rounded-none',
// last // last
"group-data-[last=true]:first:before:rounded-none", 'group-data-[last=true]:first:before:rounded-none',
"group-data-[last=true]:last:before:rounded-none", 'group-data-[last=true]:last:before:rounded-none',
], ],
}), }),
[] []
@@ -144,7 +129,7 @@ export default function ProjectsTable({
{(column) => ( {(column) => (
<TableColumn <TableColumn
key={column.uid} key={column.uid}
align={column.uid === "actions" ? "center" : "start"} align={column.uid === 'actions' ? 'center' : 'start'}
allowsSorting={column.sortable} allowsSorting={column.sortable}
> >
{column.name} {column.name}
@@ -153,11 +138,7 @@ export default function ProjectsTable({
</TableHeader> </TableHeader>
<TableBody emptyContent={messages.noProjectsFound} items={sortedItems}> <TableBody emptyContent={messages.noProjectsFound} items={sortedItems}>
{(item) => ( {(item) => (
<TableRow key={item.id}> <TableRow key={item.id}>{(columnKey) => <TableCell>{renderCell(item, columnKey)}</TableCell>}</TableRow>
{(columnKey) => (
<TableCell>{renderCell(item, columnKey)}</TableCell>
)}
</TableRow>
)} )}
</TableBody> </TableBody>
</Table> </Table>

View File

@@ -1,10 +1,10 @@
"use client"; 'use client';
import { useState, useEffect } from "react"; import { useState, useEffect } from 'react';
import { Listbox, ListboxItem } from "@nextui-org/react"; import { Listbox, ListboxItem } from '@nextui-org/react';
import { Home, Files, FlaskConical } from "lucide-react"; import { Home, Files, FlaskConical } from 'lucide-react';
import { usePathname, useRouter } from "@/src/navigation"; import { usePathname, useRouter } from '@/src/navigation';
import useGetCurrentIds from "@/utils/useGetCurrentIds"; import useGetCurrentIds from '@/utils/useGetCurrentIds';
import { ProjectMessages } from "@/types/project"; import { ProjectMessages } from '@/types/project';
export type Props = { export type Props = {
messages: ProjectMessages; messages: ProjectMessages;
@@ -16,28 +16,28 @@ export default function Sidebar({ messages, locale }: Props) {
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const [currentKey, setCurrentTab] = useState("home"); const [currentKey, setCurrentTab] = useState('home');
const baseClass = "p-3"; const baseClass = 'p-3';
const selectedClass = `${baseClass} bg-neutral-200 dark:bg-neutral-700`; const selectedClass = `${baseClass} bg-neutral-200 dark:bg-neutral-700`;
const handleTabClick = (key: string) => { const handleTabClick = (key: string) => {
if (key === "home") { if (key === 'home') {
router.push(`/projects/${projectId}/home`, { locale: locale }); router.push(`/projects/${projectId}/home`, { locale: locale });
} else if (key === "cases") { } else if (key === 'cases') {
router.push(`/projects/${projectId}/folders`, { locale: locale }); router.push(`/projects/${projectId}/folders`, { locale: locale });
} else if (key === "runs") { } else if (key === 'runs') {
router.push(`/projects/${projectId}/runs`, { locale: locale }); router.push(`/projects/${projectId}/runs`, { locale: locale });
} }
}; };
useEffect(() => { useEffect(() => {
const handleRouteChange = (currentPath: string) => { const handleRouteChange = (currentPath: string) => {
if (currentPath.includes("home")) { if (currentPath.includes('home')) {
setCurrentTab("home"); setCurrentTab('home');
} else if (currentPath.includes("folders")) { } else if (currentPath.includes('folders')) {
setCurrentTab("cases"); setCurrentTab('cases');
} else if (currentPath.includes("runs")) { } else if (currentPath.includes('runs')) {
setCurrentTab("runs"); setCurrentTab('runs');
} }
}; };
@@ -46,17 +46,17 @@ export default function Sidebar({ messages, locale }: Props) {
const tabItems = [ const tabItems = [
{ {
key: "home", key: 'home',
text: messages.home, text: messages.home,
startContent: <Home strokeWidth={1} size={28} />, startContent: <Home strokeWidth={1} size={28} />,
}, },
{ {
key: "cases", key: 'cases',
text: messages.testCases, text: messages.testCases,
startContent: <Files strokeWidth={1} size={28} />, startContent: <Files strokeWidth={1} size={28} />,
}, },
{ {
key: "runs", key: 'runs',
text: messages.testRuns, text: messages.testRuns,
startContent: <FlaskConical strokeWidth={1} size={28} />, startContent: <FlaskConical strokeWidth={1} size={28} />,
}, },

View File

@@ -1,17 +1,8 @@
"use client"; 'use client';
import React from "react"; import React from 'react';
import { useState, useEffect } from "react"; import { useState, useEffect } from 'react';
import { import { Button, Input, Textarea, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from '@nextui-org/react';
Button, import { FolderType, FoldersMessages } from '@/types/folder';
Input,
Textarea,
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
} from "@nextui-org/react";
import { FolderType, FoldersMessages } from "@/types/folder";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@@ -21,23 +12,17 @@ type Props = {
messages: FoldersMessages; messages: FoldersMessages;
}; };
export default function FolderDialog({ export default function FolderDialog({ isOpen, editingFolder, onCancel, onSubmit, messages }: Props) {
isOpen,
editingFolder,
onCancel,
onSubmit,
messages,
}: Props) {
const [folderName, setFolderName] = useState({ const [folderName, setFolderName] = useState({
text: editingFolder ? editingFolder.name : "", text: editingFolder ? editingFolder.name : '',
isValid: false, isValid: false,
errorMessage: "", errorMessage: '',
}); });
const [folderDetail, setFolderDetail] = useState({ const [folderDetail, setFolderDetail] = useState({
text: editingFolder ? editingFolder.detail : "", text: editingFolder ? editingFolder.detail : '',
isValid: false, isValid: false,
errorMessage: "", errorMessage: '',
}); });
useEffect(() => { useEffect(() => {
@@ -49,17 +34,17 @@ export default function FolderDialog({
setFolderDetail({ setFolderDetail({
...folderDetail, ...folderDetail,
text: editingFolder.detail ? editingFolder.detail : "", text: editingFolder.detail ? editingFolder.detail : '',
}); });
} else { } else {
setFolderName({ setFolderName({
...folderName, ...folderName,
text: "", text: '',
}); });
setFolderDetail({ setFolderDetail({
...folderDetail, ...folderDetail,
text: "", text: '',
}); });
} }
}, [editingFolder]); }, [editingFolder]);
@@ -67,20 +52,20 @@ export default function FolderDialog({
const clear = () => { const clear = () => {
setFolderName({ setFolderName({
isValid: false, isValid: false,
text: "", text: '',
errorMessage: "", errorMessage: '',
}); });
setFolderDetail({ setFolderDetail({
isValid: false, isValid: false,
text: "", text: '',
errorMessage: "", errorMessage: '',
}); });
}; };
const validate = () => { const validate = () => {
if (!folderName.text) { if (!folderName.text) {
setFolderName({ setFolderName({
text: "", text: '',
isValid: false, isValid: false,
errorMessage: messages.pleaseEnter, errorMessage: messages.pleaseEnter,
}); });
@@ -100,9 +85,7 @@ export default function FolderDialog({
}} }}
> >
<ModalContent> <ModalContent>
<ModalHeader className="flex flex-col gap-1"> <ModalHeader className="flex flex-col gap-1">{messages.folder}</ModalHeader>
{messages.folder}
</ModalHeader>
<ModalBody> <ModalBody>
<Input <Input
type="text" type="text"

View File

@@ -1,12 +1,6 @@
import { import { Button, Dropdown, DropdownTrigger, DropdownMenu, DropdownItem } from '@nextui-org/react';
Button, import { MoreVertical } from 'lucide-react';
Dropdown, import { FolderType, FoldersMessages } from '@/types/folder';
DropdownTrigger,
DropdownMenu,
DropdownItem,
} from "@nextui-org/react";
import { MoreVertical } from "lucide-react";
import { FolderType, FoldersMessages } from "@/types/folder";
type Props = { type Props = {
folder: FolderType; folder: FolderType;
@@ -15,12 +9,7 @@ type Props = {
messages: FoldersMessages; messages: FoldersMessages;
}; };
export default function FolderEditMenu({ export default function FolderEditMenu({ folder, onEditClick, onDeleteClick, messages }: Props) {
folder,
onEditClick,
onDeleteClick,
messages,
}: Props) {
return ( return (
<Dropdown> <Dropdown>
<DropdownTrigger> <DropdownTrigger>
@@ -32,12 +21,7 @@ export default function FolderEditMenu({
<DropdownItem key="edit" onClick={() => onEditClick(folder)}> <DropdownItem key="edit" onClick={() => onEditClick(folder)}>
{messages.editFolder} {messages.editFolder}
</DropdownItem> </DropdownItem>
<DropdownItem <DropdownItem key="delete" className="text-danger" color="danger" onClick={() => onDeleteClick(folder.id)}>
key="delete"
className="text-danger"
color="danger"
onClick={() => onDeleteClick(folder.id)}
>
{messages.deleteFolder} {messages.deleteFolder}
</DropdownItem> </DropdownItem>
</DropdownMenu> </DropdownMenu>

View File

@@ -1,20 +1,15 @@
"use client"; 'use client';
import React from "react"; import React from 'react';
import { FolderType, FoldersMessages } from "@/types/folder"; import { FolderType, FoldersMessages } from '@/types/folder';
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react';
import { Button, Listbox, ListboxItem } from "@nextui-org/react"; import { Button, Listbox, ListboxItem } from '@nextui-org/react';
import { Folder, Plus } from "lucide-react"; import { Folder, Plus } from 'lucide-react';
import { usePathname, useRouter } from "@/src/navigation"; import { usePathname, useRouter } from '@/src/navigation';
import useGetCurrentIds from "@/utils/useGetCurrentIds"; import useGetCurrentIds from '@/utils/useGetCurrentIds';
import FolderDialog from "./FolderDialog"; import FolderDialog from './FolderDialog';
import FolderEditMenu from "./FolderEditMenu"; import FolderEditMenu from './FolderEditMenu';
import { import { fetchFolders, createFolder, updateFolder, deleteFolder } from './foldersControl';
fetchFolders,
createFolder,
updateFolder,
deleteFolder,
} from "./foldersControl";
type Props = { type Props = {
projectId: string; projectId: string;
@@ -45,16 +40,8 @@ export default function FoldersPane({ projectId, messages, locale }: Props) {
const onSubmit = async (name: string, detail: string) => { const onSubmit = async (name: string, detail: string) => {
if (editingFolder) { if (editingFolder) {
const updatedProject = await updateFolder( const updatedProject = await updateFolder(editingFolder.id, name, detail, projectId, null);
editingFolder.id, const updatedProjects = folders.map((project) => (project.id === updatedProject.id ? updatedProject : project));
name,
detail,
projectId,
null
);
const updatedProjects = folders.map((project) =>
project.id === updatedProject.id ? updatedProject : project
);
setFolders(updatedProjects); setFolders(updatedProjects);
} else { } else {
const newProject = await createFolder(name, detail, projectId, null); const newProject = await createFolder(name, detail, projectId, null);
@@ -79,28 +66,23 @@ export default function FoldersPane({ projectId, messages, locale }: Props) {
const data = await fetchFolders(projectId); const data = await fetchFolders(projectId);
setFolders(data); setFolders(data);
const selectedFolderFromUrl = data.find( const selectedFolderFromUrl = data.find((folder) => folder.id === folderId);
(folder) => folder.id === folderId
);
setSelectedFolder(selectedFolderFromUrl); setSelectedFolder(selectedFolderFromUrl);
// Redirect to the smallest folder ID page if the path is "projects/[projectId]/folders // Redirect to the smallest folder ID page if the path is "projects/[projectId]/folders
if (pathname === `/projects/${projectId}/folders`) { if (pathname === `/projects/${projectId}/folders`) {
const smallestFolderId = Math.min(...data.map((folder) => folder.id)); const smallestFolderId = Math.min(...data.map((folder) => folder.id));
router.push( router.push(`/projects/${projectId}/folders/${smallestFolderId}/cases`, { locale: locale });
`/projects/${projectId}/folders/${smallestFolderId}/cases`,
{ locale: locale }
);
} }
} catch (error: any) { } catch (error: any) {
console.error("Error in effect:", error.message); console.error('Error in effect:', error.message);
} }
} }
fetchDataEffect(); fetchDataEffect();
}, [folderId]); }, [folderId]);
const baseClass = ""; const baseClass = '';
const selectedClass = `${baseClass} bg-neutral-200 dark:bg-neutral-700`; const selectedClass = `${baseClass} bg-neutral-200 dark:bg-neutral-700`;
return ( return (
@@ -119,18 +101,9 @@ export default function FoldersPane({ projectId, messages, locale }: Props) {
{folders.map((folder, index) => ( {folders.map((folder, index) => (
<ListboxItem <ListboxItem
key={index} key={index}
onClick={() => onClick={() => router.push(`/projects/${projectId}/folders/${folder.id}/cases`, { locale: locale })}
router.push(
`/projects/${projectId}/folders/${folder.id}/cases`,
{ locale: locale }
)
}
startContent={<Folder size={20} color="#F7C24E" fill="#F7C24E" />} startContent={<Folder size={20} color="#F7C24E" fill="#F7C24E" />}
className={ className={selectedFolder && folder.id === selectedFolder.id ? selectedClass : baseClass}
selectedFolder && folder.id === selectedFolder.id
? selectedClass
: baseClass
}
endContent={ endContent={
<FolderEditMenu <FolderEditMenu
folder={folder} folder={folder}

View File

@@ -1,8 +1,8 @@
"use client"; 'use client';
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react';
import TestCaseTable from "./TestCaseTable"; import TestCaseTable from './TestCaseTable';
import { fetchCases, createCase, deleteCase, deleteCases } from "./caseControl"; import { fetchCases, createCase, deleteCase, deleteCases } from './caseControl';
import { CasesMessages } from "@/types/case"; import { CasesMessages } from '@/types/case';
type Props = { type Props = {
projectId: string; projectId: string;
@@ -11,12 +11,7 @@ type Props = {
locale: string; locale: string;
}; };
export default function CasesPane({ export default function CasesPane({ projectId, folderId, messages, locale }: Props) {
projectId,
folderId,
messages,
locale,
}: Props) {
const [cases, setCases] = useState([]); const [cases, setCases] = useState([]);
useEffect(() => { useEffect(() => {
async function fetchDataEffect() { async function fetchDataEffect() {
@@ -24,7 +19,7 @@ export default function CasesPane({
const data = await fetchCases(folderId); const data = await fetchCases(folderId);
setCases(data); setCases(data);
} catch (error: any) { } catch (error: any) {
console.error("Error in effect:", error.message); console.error('Error in effect:', error.message);
} }
} }

View File

@@ -1,4 +1,4 @@
import { useState, useMemo, useCallback } from "react"; import { useState, useMemo, useCallback } from 'react';
import { import {
Table, Table,
TableHeader, TableHeader,
@@ -13,11 +13,11 @@ import {
DropdownItem, DropdownItem,
Selection, Selection,
SortDescriptor, SortDescriptor,
} from "@nextui-org/react"; } from '@nextui-org/react';
import { Link, NextUiLinkClasses } from "@/src/navigation"; import { Link, NextUiLinkClasses } from '@/src/navigation';
import { Plus, MoreVertical, Trash, Circle } from "lucide-react"; import { Plus, MoreVertical, Trash, Circle } from 'lucide-react';
import { CasesMessages } from "@/types/case"; import { CasesMessages } from '@/types/case';
import { priorities } from "@/config/selection"; import { priorities } from '@/config/selection';
type Case = { type Case = {
id: number; id: number;
@@ -55,16 +55,16 @@ export default function TestCaseTable({
locale, locale,
}: Props) { }: Props) {
const headerColumns = [ const headerColumns = [
{ name: messages.id, uid: "id", sortable: true }, { name: messages.id, uid: 'id', sortable: true },
{ name: messages.title, uid: "title", sortable: true }, { name: messages.title, uid: 'title', sortable: true },
{ name: messages.priority, uid: "priority", sortable: true }, { name: messages.priority, uid: 'priority', sortable: true },
{ name: messages.actions, uid: "actions" }, { name: messages.actions, uid: 'actions' },
]; ];
const [selectedKeys, setSelectedKeys] = useState<Selection>(new Set([])); const [selectedKeys, setSelectedKeys] = useState<Selection>(new Set([]));
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({ const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
column: "id", column: 'id',
direction: "ascending", direction: 'ascending',
}); });
const sortedItems = useMemo(() => { const sortedItems = useMemo(() => {
@@ -73,7 +73,7 @@ export default function TestCaseTable({
const second = b[sortDescriptor.column as keyof Case] as number; const second = b[sortDescriptor.column as keyof Case] as number;
const cmp = first < second ? -1 : first > second ? 1 : 0; const cmp = first < second ? -1 : first > second ? 1 : 0;
return sortDescriptor.direction === "descending" ? -cmp : cmp; return sortDescriptor.direction === 'descending' ? -cmp : cmp;
}); });
}, [sortDescriptor, cases]); }, [sortDescriptor, cases]);
@@ -81,15 +81,11 @@ export default function TestCaseTable({
const cellValue = testCase[columnKey as keyof Case]; const cellValue = testCase[columnKey as keyof Case];
switch (columnKey) { switch (columnKey) {
case "id": case 'id':
return <span>{cellValue}</span>; return <span>{cellValue}</span>;
case "title": case 'title':
return ( return (
<Button <Button size="sm" variant="light" className="data-[hover=true]:bg-transparent">
size="sm"
variant="light"
className="data-[hover=true]:bg-transparent"
>
<Link <Link
href={`/projects/${projectId}/folders/${testCase.folderId}/cases/${testCase.id}`} href={`/projects/${projectId}/folders/${testCase.folderId}/cases/${testCase.id}`}
locale={locale} locale={locale}
@@ -99,18 +95,14 @@ export default function TestCaseTable({
</Link> </Link>
</Button> </Button>
); );
case "priority": case 'priority':
return ( return (
<div className="flex items-center"> <div className="flex items-center">
<Circle <Circle size={8} color={priorities[cellValue].color} fill={priorities[cellValue].color} />
size={8}
color={priorities[cellValue].color}
fill={priorities[cellValue].color}
/>
<div className="ms-3">{messages[priorities[cellValue].uid]}</div> <div className="ms-3">{messages[priorities[cellValue].uid]}</div>
</div> </div>
); );
case "actions": case 'actions':
return ( return (
<Dropdown> <Dropdown>
<DropdownTrigger> <DropdownTrigger>
@@ -119,10 +111,7 @@ export default function TestCaseTable({
</Button> </Button>
</DropdownTrigger> </DropdownTrigger>
<DropdownMenu aria-label="test case actions"> <DropdownMenu aria-label="test case actions">
<DropdownItem <DropdownItem className="text-danger" onClick={() => onDeleteCase(testCase.id)}>
className="text-danger"
onClick={() => onDeleteCase(testCase.id)}
>
{messages.deleteCase} {messages.deleteCase}
</DropdownItem> </DropdownItem>
</DropdownMenu> </DropdownMenu>
@@ -135,25 +124,25 @@ export default function TestCaseTable({
const classNames = useMemo( const classNames = useMemo(
() => ({ () => ({
wrapper: ["max-w-3xl"], wrapper: ['max-w-3xl'],
th: ["bg-transparent", "text-default-500", "border-b", "border-divider"], th: ['bg-transparent', 'text-default-500', 'border-b', 'border-divider'],
td: [ td: [
// changing the rows border radius // changing the rows border radius
// first // first
"group-data-[first=true]:first:before:rounded-none", 'group-data-[first=true]:first:before:rounded-none',
"group-data-[first=true]:last:before:rounded-none", 'group-data-[first=true]:last:before:rounded-none',
// middle // middle
"group-data-[middle=true]:before:rounded-none", 'group-data-[middle=true]:before:rounded-none',
// last // last
"group-data-[last=true]:first:before:rounded-none", 'group-data-[last=true]:first:before:rounded-none',
"group-data-[last=true]:last:before:rounded-none", 'group-data-[last=true]:last:before:rounded-none',
], ],
}), }),
[] []
); );
const onDeleteCasesClick = async () => { const onDeleteCasesClick = async () => {
if (selectedKeys === "all") { if (selectedKeys === 'all') {
const allKeys = sortedItems.map((item) => item.id); const allKeys = sortedItems.map((item) => item.id);
onDeleteCases(allKeys); onDeleteCases(allKeys);
} else { } else {
@@ -167,7 +156,7 @@ export default function TestCaseTable({
<div className="border-b-1 dark:border-neutral-700 w-full p-3 flex items-center justify-between"> <div className="border-b-1 dark:border-neutral-700 w-full p-3 flex items-center justify-between">
<h3 className="font-bold">{messages.testCaseList}</h3> <h3 className="font-bold">{messages.testCaseList}</h3>
<div> <div>
{(selectedKeys.size > 0 || selectedKeys === "all") && ( {(selectedKeys.size > 0 || selectedKeys === 'all') && (
<Button <Button
startContent={<Trash size={16} />} startContent={<Trash size={16} />}
size="sm" size="sm"
@@ -178,12 +167,7 @@ export default function TestCaseTable({
{messages.delete} {messages.delete}
</Button> </Button>
)} )}
<Button <Button startContent={<Plus size={16} />} size="sm" color="primary" onClick={onCreateCase}>
startContent={<Plus size={16} />}
size="sm"
color="primary"
onClick={onCreateCase}
>
{messages.newTestCase} {messages.newTestCase}
</Button> </Button>
</div> </div>
@@ -204,7 +188,7 @@ export default function TestCaseTable({
{(column) => ( {(column) => (
<TableColumn <TableColumn
key={column.uid} key={column.uid}
align={column.uid === "actions" ? "center" : "start"} align={column.uid === 'actions' ? 'center' : 'start'}
allowsSorting={column.sortable} allowsSorting={column.sortable}
> >
{column.name} {column.name}
@@ -213,11 +197,7 @@ export default function TestCaseTable({
</TableHeader> </TableHeader>
<TableBody emptyContent={messages.noCasesFound} items={sortedItems}> <TableBody emptyContent={messages.noCasesFound} items={sortedItems}>
{(item) => ( {(item) => (
<TableRow key={item.id}> <TableRow key={item.id}>{(columnKey) => <TableCell>{renderCell(item, columnKey)}</TableCell>}</TableRow>
{(columnKey) => (
<TableCell>{renderCell(item, columnKey)}</TableCell>
)}
</TableRow>
)} )}
</TableBody> </TableBody>
</Table> </Table>

View File

@@ -1,16 +1,13 @@
import { Image, Button, Tooltip, Card, CardBody } from "@nextui-org/react"; import { Image, Button, Tooltip, Card, CardBody } from '@nextui-org/react';
import { AttachmentType, CaseMessages } from "@/types/case"; import { AttachmentType, CaseMessages } from '@/types/case';
import { Trash, ArrowDownToLine } from "lucide-react"; import { Trash, ArrowDownToLine } from 'lucide-react';
import { isImage } from "./isImage"; import { isImage } from './isImage';
type Props = { type Props = {
attachments: AttachmentType[]; attachments: AttachmentType[];
onAttachmentDownload: ( onAttachmentDownload: (attachmentId: number, downloadFileName: string) => void;
attachmentId: number,
downloadFileName: string
) => void;
onAttachmentDelete: (attachmentId: number) => void; onAttachmentDelete: (attachmentId: number) => void;
messages: CaseMessages, messages: CaseMessages;
}; };
export default function CaseAttachmentsEditor({ export default function CaseAttachmentsEditor({
@@ -35,11 +32,7 @@ export default function CaseAttachmentsEditor({
{images.map((image, index) => ( {images.map((image, index) => (
<Card key={index} radius="sm" className="mt-2 me-2 max-w-md"> <Card key={index} radius="sm" className="mt-2 me-2 max-w-md">
<CardBody> <CardBody>
<Image <Image alt={image.title} src={image.path} className="object-cover h-40 w-40" />
alt={image.title}
src={image.path}
className="object-cover h-40 w-40"
/>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p>{image.title}</p> <p>{image.title}</p>
<Tooltip content={messages.delete}> <Tooltip content={messages.delete}>

View File

@@ -1,39 +1,27 @@
"use client"; 'use client';
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react';
import { import { Input, Textarea, Select, SelectItem, Button, Divider, Tooltip } from '@nextui-org/react';
Input, import { useRouter } from '@/src/navigation';
Textarea, import { Save, Plus, ArrowLeft, ArrowUpFromLine, Circle } from 'lucide-react';
Select, import { priorities, testTypes, templates } from '@/config/selection';
SelectItem, import CaseStepsEditor from './CaseStepsEditor';
Button, import CaseAttachmentsEditor from './CaseAttachmentsEditor';
Divider, import { CaseType, AttachmentType, CaseMessages } from '@/types/case';
Tooltip, import { fetchCase, updateCase } from '../caseControl';
} from "@nextui-org/react"; import { fetchCreateStep, fetchDeleteStep } from './stepControl';
import { useRouter } from "@/src/navigation"; import { fetchCreateAttachments, fetchDownloadAttachment, fetchDeleteAttachment } from './attachmentControl';
import { Save, Plus, ArrowLeft, ArrowUpFromLine, Circle } from "lucide-react";
import { priorities, testTypes, templates } from "@/config/selection";
import CaseStepsEditor from "./CaseStepsEditor";
import CaseAttachmentsEditor from "./CaseAttachmentsEditor";
import { CaseType, AttachmentType, CaseMessages } from "@/types/case";
import { fetchCase, updateCase } from "../caseControl";
import { fetchCreateStep, fetchDeleteStep } from "./stepControl";
import {
fetchCreateAttachments,
fetchDownloadAttachment,
fetchDeleteAttachment,
} from "./attachmentControl";
const defaultTestCase = { const defaultTestCase = {
id: 0, id: 0,
title: "", title: '',
state: 0, state: 0,
priority: 0, priority: 0,
type: 0, type: 0,
automationStatus: 0, automationStatus: 0,
description: "", description: '',
template: 0, template: 0,
preConditions: "", preConditions: '',
expectedResults: "", expectedResults: '',
folderId: 0, folderId: 0,
}; };
@@ -45,13 +33,7 @@ type Props = {
locale: string; locale: string;
}; };
export default function CaseEditor({ export default function CaseEditor({ projectId, folderId, caseId, messages, locale }: Props) {
projectId,
folderId,
caseId,
messages,
locale,
}: Props) {
const [testCase, setTestCase] = useState<CaseType>(defaultTestCase); const [testCase, setTestCase] = useState<CaseType>(defaultTestCase);
const [isTitleInvalid, setIsTitleInvalid] = useState<boolean>(false); const [isTitleInvalid, setIsTitleInvalid] = useState<boolean>(false);
const [isUpdating, setIsUpdating] = useState<boolean>(false); const [isUpdating, setIsUpdating] = useState<boolean>(false);
@@ -122,10 +104,7 @@ export default function CaseEditor({
handleFetchCreateAttachments(caseId, event.target.files); handleFetchCreateAttachments(caseId, event.target.files);
}; };
const handleFetchCreateAttachments = async ( const handleFetchCreateAttachments = async (caseId: number, files: File[]) => {
caseId: number,
files: File[]
) => {
const newAttachments = await fetchCreateAttachments(caseId, files); const newAttachments = await fetchCreateAttachments(caseId, files);
if (newAttachments) { if (newAttachments) {
@@ -147,9 +126,7 @@ export default function CaseEditor({
const onAttachmentDelete = async (attachmentId: number) => { const onAttachmentDelete = async (attachmentId: number) => {
await fetchDeleteAttachment(attachmentId); await fetchDeleteAttachment(attachmentId);
const filteredAttachments = testCase.Attachments.filter( const filteredAttachments = testCase.Attachments.filter((attachment) => attachment.id !== attachmentId);
(attachment) => attachment.id !== attachmentId
);
setTestCase({ setTestCase({
...testCase, ...testCase,
@@ -163,7 +140,7 @@ export default function CaseEditor({
const data = await fetchCase(caseId); const data = await fetchCase(caseId);
setTestCase(data); setTestCase(data);
} catch (error: any) { } catch (error: any) {
console.error("Error in effect:", error.message); console.error('Error in effect:', error.message);
} }
} }
@@ -179,12 +156,7 @@ export default function CaseEditor({
isIconOnly isIconOnly
size="sm" size="sm"
className="rounded-full bg-neutral-50 dark:bg-neutral-600" className="rounded-full bg-neutral-50 dark:bg-neutral-600"
onPress={() => onPress={() => router.push(`/projects/${projectId}/folders/${folderId}/cases`, { locale: locale })}
router.push(
`/projects/${projectId}/folders/${folderId}/cases`,
{ locale: locale }
)
}
> >
<ArrowLeft size={16} /> <ArrowLeft size={16} />
</Button> </Button>
@@ -215,7 +187,7 @@ export default function CaseEditor({
label={messages.title} label={messages.title}
value={testCase.title} value={testCase.title}
isInvalid={isTitleInvalid} isInvalid={isTitleInvalid}
errorMessage={isTitleInvalid ? messages.pleaseEnterTitle : ""} errorMessage={isTitleInvalid ? messages.pleaseEnterTitle : ''}
onChange={(e) => { onChange={(e) => {
setTestCase({ ...testCase, title: e.target.value }); setTestCase({ ...testCase, title: e.target.value });
}} }}
@@ -241,17 +213,11 @@ export default function CaseEditor({
selectedKeys={[priorities[testCase.priority].uid]} selectedKeys={[priorities[testCase.priority].uid]}
onSelectionChange={(e) => { onSelectionChange={(e) => {
const selectedUid = e.anchorKey; const selectedUid = e.anchorKey;
const index = priorities.findIndex( const index = priorities.findIndex((priority) => priority.uid === selectedUid);
(priority) => priority.uid === selectedUid
);
setTestCase({ ...testCase, priority: index }); setTestCase({ ...testCase, priority: index });
}} }}
startContent={ startContent={
<Circle <Circle size={8} color={priorities[testCase.priority].color} fill={priorities[testCase.priority].color} />
size={8}
color={priorities[testCase.priority].color}
fill={priorities[testCase.priority].color}
/>
} }
label={messages.priority} label={messages.priority}
className="mt-3 max-w-xs" className="mt-3 max-w-xs"
@@ -271,9 +237,7 @@ export default function CaseEditor({
selectedKeys={[testTypes[testCase.type].uid]} selectedKeys={[testTypes[testCase.type].uid]}
onSelectionChange={(e) => { onSelectionChange={(e) => {
const selectedUid = e.anchorKey; const selectedUid = e.anchorKey;
const index = testTypes.findIndex( const index = testTypes.findIndex((type) => type.uid === selectedUid);
(type) => type.uid === selectedUid
);
setTestCase({ ...testCase, type: index }); setTestCase({ ...testCase, type: index });
}} }}
label={messages.type} label={messages.type}
@@ -294,9 +258,7 @@ export default function CaseEditor({
selectedKeys={[templates[testCase.template].uid]} selectedKeys={[templates[testCase.template].uid]}
onSelectionChange={(e) => { onSelectionChange={(e) => {
const selectedUid = e.anchorKey; const selectedUid = e.anchorKey;
const index = templates.findIndex( const index = templates.findIndex((template) => template.uid === selectedUid);
(template) => template.uid === selectedUid
);
setTestCase({ ...testCase, template: index }); setTestCase({ ...testCase, template: index });
}} }}
label={messages.template} label={messages.template}
@@ -311,7 +273,7 @@ export default function CaseEditor({
</div> </div>
<Divider className="my-6" /> <Divider className="my-6" />
{templates[testCase.template].uid === "text" ? ( {templates[testCase.template].uid === 'text' ? (
<div> <div>
<h6 className="font-bold">{messages.testDetail}</h6> <h6 className="font-bold">{messages.testDetail}</h6>
<div className="flex"> <div className="flex">
@@ -377,10 +339,9 @@ export default function CaseEditor({
<h6 className="font-bold">{messages.attachments}</h6> <h6 className="font-bold">{messages.attachments}</h6>
<CaseAttachmentsEditor <CaseAttachmentsEditor
attachments={testCase.Attachments} attachments={testCase.Attachments}
onAttachmentDownload={( onAttachmentDownload={(attachmentId: number, downloadFileName: string) =>
attachmentId: number, fetchDownloadAttachment(attachmentId, downloadFileName)
downloadFileName: string }
) => fetchDownloadAttachment(attachmentId, downloadFileName)}
onAttachmentDelete={onAttachmentDelete} onAttachmentDelete={onAttachmentDelete}
messages={messages} messages={messages}
/> />
@@ -399,17 +360,9 @@ export default function CaseEditor({
<span className="font-semibold">{messages.clickToUpload}</span> <span className="font-semibold">{messages.clickToUpload}</span>
<span>{messages.orDragAndDrop}</span> <span>{messages.orDragAndDrop}</span>
</p> </p>
<p className="text-xs text-neutral-500 dark:text-neutral-400"> <p className="text-xs text-neutral-500 dark:text-neutral-400">{messages.maxFileSize}: 50 MB</p>
{messages.maxFileSize}: 50 MB
</p>
</div> </div>
<input <input id="dropzone-file" type="file" className="hidden" onChange={handleInput} multiple />
id="dropzone-file"
type="file"
className="hidden"
onChange={handleInput}
multiple
/>
</label> </label>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,6 @@
import { Textarea, Button, Tooltip } from "@nextui-org/react"; import { Textarea, Button, Tooltip } from '@nextui-org/react';
import { CaseMessages, StepType } from "@/types/case"; import { CaseMessages, StepType } from '@/types/case';
import { Plus, Trash } from "lucide-react"; import { Plus, Trash } from 'lucide-react';
type Props = { type Props = {
steps: StepType[]; steps: StepType[];
@@ -10,13 +10,7 @@ type Props = {
messages: CaseMessages; messages: CaseMessages;
}; };
export default function StepsEditor({ export default function StepsEditor({ steps, onStepUpdate, onStepPlus, onStepDelete, messages }: Props) {
steps,
onStepUpdate,
onStepPlus,
onStepDelete,
messages,
}: Props) {
// sort steps by junction table's column // sort steps by junction table's column
const sortedSteps = steps.slice().sort((a, b) => { const sortedSteps = steps.slice().sort((a, b) => {
const stepNoA = a.caseSteps.stepNo; const stepNoA = a.caseSteps.stepNo;

View File

@@ -1,5 +1,5 @@
import CaseEditor from "./CaseEditor"; import CaseEditor from './CaseEditor';
import { useTranslations } from "next-intl"; import { useTranslations } from 'next-intl';
export default function Page({ export default function Page({
params, params,
@@ -11,53 +11,53 @@ export default function Page({
locale: string; locale: string;
}; };
}) { }) {
const t = useTranslations("Case"); const t = useTranslations('Case');
const messages = { const messages = {
backToCases: t("back_to_cases"), backToCases: t('back_to_cases'),
updating: t("updating"), updating: t('updating'),
update: t("update"), update: t('update'),
basic: t("basic"), basic: t('basic'),
title: t("title"), title: t('title'),
pleaseEnterTitle: t("please_enter_title"), pleaseEnterTitle: t('please_enter_title'),
description: t("description"), description: t('description'),
testCaseDescription: t("test_case_description"), testCaseDescription: t('test_case_description'),
priority: t("priority"), priority: t('priority'),
critical: t("critical"), critical: t('critical'),
high: t("high"), high: t('high'),
medium: t("medium"), medium: t('medium'),
low: t("low"), low: t('low'),
type: t("type"), type: t('type'),
other: t("other"), other: t('other'),
security: t("security"), security: t('security'),
performance: t("performance"), performance: t('performance'),
accessibility: t("accessibility"), accessibility: t('accessibility'),
functional: t("functional"), functional: t('functional'),
acceptance: t("acceptance"), acceptance: t('acceptance'),
usability: t("usability"), usability: t('usability'),
smokeSanity: t("smoke_sanity"), smokeSanity: t('smoke_sanity'),
compatibility: t("compatibility"), compatibility: t('compatibility'),
destructive: t("destructive"), destructive: t('destructive'),
regression: t("regression"), regression: t('regression'),
automated: t("automated"), automated: t('automated'),
manual: t("manual"), manual: t('manual'),
template: t("template"), template: t('template'),
testDetail: t("test_detail"), testDetail: t('test_detail'),
preconditions: t("preconditions"), preconditions: t('preconditions'),
step: t("step"), step: t('step'),
text: t("text"), text: t('text'),
steps: t("steps"), steps: t('steps'),
newStep: t("new_step"), newStep: t('new_step'),
detailsOfTheStep: t("details_of_the_step"), detailsOfTheStep: t('details_of_the_step'),
expectedResult: t("expected_result"), expectedResult: t('expected_result'),
deleteThisStep: t("delete_this_step"), deleteThisStep: t('delete_this_step'),
insertStep: t("insert_step"), insertStep: t('insert_step'),
attachments: t("attachments"), attachments: t('attachments'),
delete: t("delete"), delete: t('delete'),
download: t("download"), download: t('download'),
deleteFile: t("delete_file"), deleteFile: t('delete_file'),
clickToUpload: t("click_to_upload"), clickToUpload: t('click_to_upload'),
orDragAndDrop: t("or_drag_and_drop"), orDragAndDrop: t('or_drag_and_drop'),
maxFileSize: t("max_file_size"), maxFileSize: t('max_file_size'),
}; };
return ( return (

View File

@@ -1,37 +1,28 @@
import CasesPane from "./CasesPane"; import CasesPane from './CasesPane';
import { useTranslations } from "next-intl"; import { useTranslations } from 'next-intl';
export default function Page({ export default function Page({ params }: { params: { projectId: string; folderId: string; locale: string } }) {
params, const t = useTranslations('Cases');
}: {
params: { projectId: string; folderId: string; locale: string };
}) {
const t = useTranslations("Cases");
const messages = { const messages = {
testCaseList: t("test_case_list"), testCaseList: t('test_case_list'),
id: t("id"), id: t('id'),
title: t("title"), title: t('title'),
priority: t("priority"), priority: t('priority'),
actions: t("actions"), actions: t('actions'),
deleteCase: t("delete_case"), deleteCase: t('delete_case'),
delete: t("delete"), delete: t('delete'),
newTestCase: t("new_test_case"), newTestCase: t('new_test_case'),
status: t("status"), status: t('status'),
critical: t("critical"), critical: t('critical'),
high: t("high"), high: t('high'),
medium: t("medium"), medium: t('medium'),
low: t("low"), low: t('low'),
noCasesFound: t("no_cases_found"), noCasesFound: t('no_cases_found'),
}; };
return ( return (
<> <>
<CasesPane <CasesPane projectId={params.projectId} folderId={params.folderId} locale={params.locale} messages={messages} />
projectId={params.projectId}
folderId={params.folderId}
locale={params.locale}
messages={messages}
/>
</> </>
); );
} }

View File

@@ -1,5 +1,5 @@
import FoldersPane from "./FoldersPane"; import FoldersPane from './FoldersPane';
import { useTranslations } from "next-intl"; import { useTranslations } from 'next-intl';
export default function FoldersLayout({ export default function FoldersLayout({
children, children,
@@ -8,27 +8,23 @@ export default function FoldersLayout({
children: React.ReactNode; children: React.ReactNode;
params: { projectId: string; locale: string }; params: { projectId: string; locale: string };
}) { }) {
const t = useTranslations("Folders"); const t = useTranslations('Folders');
const messages = { const messages = {
folder: t("folder"), folder: t('folder'),
newFolder: t("new_folder"), newFolder: t('new_folder'),
editFolder: t("edit_folder"), editFolder: t('edit_folder'),
deleteFolder: t("delete_folder"), deleteFolder: t('delete_folder'),
folderName: t("folder_name"), folderName: t('folder_name'),
folderDetail: t("folder_detail"), folderDetail: t('folder_detail'),
close: t("close"), close: t('close'),
create: t("create"), create: t('create'),
update: t("update"), update: t('update'),
pleaseEnter: t("please_enter"), pleaseEnter: t('please_enter'),
}; };
return ( return (
<div className="flex w-full"> <div className="flex w-full">
<FoldersPane <FoldersPane projectId={params.projectId} messages={messages} locale={params.locale} />
projectId={params.projectId}
messages={messages}
locale={params.locale}
/>
<div className="flex-grow w-full">{children}</div> <div className="flex-grow w-full">{children}</div>
</div> </div>
); );

View File

@@ -1,7 +1,3 @@
export default function Page() { export default function Page() {
return ( return <>This is folders page.</>;
<> }
This is folders page.
</>
);
}

View File

@@ -1,10 +1,10 @@
import React from "react"; import React from 'react';
import { useState, useEffect } from "react"; import { useState, useEffect } from 'react';
import dynamic from "next/dynamic"; import dynamic from 'next/dynamic';
import { priorities } from "@/config/selection"; import { priorities } from '@/config/selection';
import { CasePriorityCountType } from "@/types/case"; import { CasePriorityCountType } from '@/types/case';
import { HomeMessages } from "./page"; import { HomeMessages } from './page';
const Chart = dynamic(() => import("react-apexcharts"), { ssr: false }); const Chart = dynamic(() => import('react-apexcharts'), { ssr: false });
type Props = { type Props = {
priorityCounts: CasePriorityCountType[]; priorityCounts: CasePriorityCountType[];
@@ -12,11 +12,7 @@ type Props = {
theme: string; theme: string;
}; };
export default function TestPriorityDonutChart({ export default function TestPriorityDonutChart({ priorityCounts, messages, theme }: Props) {
priorityCounts,
messages,
theme,
}: Props) {
const [chartData, setChartData] = useState({ const [chartData, setChartData] = useState({
series: [], series: [],
options: { options: {
@@ -38,10 +34,10 @@ export default function TestPriorityDonutChart({
const legend = { const legend = {
labels: { labels: {
colors: priorities.map((entry) => { colors: priorities.map((entry) => {
if (theme === "light") { if (theme === 'light') {
return "black"; return 'black';
} else { } else {
return "white"; return 'white';
} }
}), }),
}, },
@@ -57,13 +53,5 @@ export default function TestPriorityDonutChart({
updateChartDate(); updateChartDate();
}, [priorityCounts, theme]); }, [priorityCounts, theme]);
return ( return <Chart options={chartData.options} series={chartData.series} type="donut" width={'100%'} height={'100%'} />;
<Chart
options={chartData.options}
series={chartData.series}
type="donut"
width={"100%"}
height={"100%"}
/>
);
} }

View File

@@ -1,9 +1,9 @@
import React from "react"; import React from 'react';
import { useState, useEffect } from "react"; import { useState, useEffect } from 'react';
import dynamic from "next/dynamic"; import dynamic from 'next/dynamic';
import { ProgressSeriesType } from "@/types/run"; import { ProgressSeriesType } from '@/types/run';
import { testRunCaseStatus } from "@/config/selection"; import { testRunCaseStatus } from '@/config/selection';
const Chart = dynamic(() => import("react-apexcharts"), { ssr: false }); const Chart = dynamic(() => import('react-apexcharts'), { ssr: false });
type Props = { type Props = {
progressSeries: ProgressSeriesType[]; progressSeries: ProgressSeriesType[];
@@ -11,11 +11,7 @@ type Props = {
theme: string; theme: string;
}; };
export default function TestProgressBarChart({ export default function TestProgressBarChart({ progressSeries, progressCategories, theme }: Props) {
progressSeries,
progressCategories,
theme,
}: Props) {
const [chartData, setChartData] = useState({ const [chartData, setChartData] = useState({
series: [], series: [],
options: { options: {
@@ -28,14 +24,14 @@ export default function TestProgressBarChart({
const updateChartDate = () => { const updateChartDate = () => {
if (progressSeries) { if (progressSeries) {
const legendsLabelColors = testRunCaseStatus.map((itr) => { const legendsLabelColors = testRunCaseStatus.map((itr) => {
if (theme === "light") { if (theme === 'light') {
return "black"; return 'black';
} else { } else {
return "white"; return 'white';
} }
}); });
const xaxisLabelColor = theme === "light" ? "black" : "white"; const xaxisLabelColor = theme === 'light' ? 'black' : 'white';
setChartData({ setChartData({
series: progressSeries, series: progressSeries,
@@ -47,7 +43,7 @@ export default function TestProgressBarChart({
stacked: true, stacked: true,
}, },
legend: { legend: {
position: "right", position: 'right',
labels: { labels: {
colors: legendsLabelColors, colors: legendsLabelColors,
}, },
@@ -56,7 +52,7 @@ export default function TestProgressBarChart({
return itr.chartColor; return itr.chartColor;
}), }),
xaxis: { xaxis: {
type: "datetime", type: 'datetime',
categories: progressCategories, categories: progressCategories,
labels: { labels: {
style: { style: {
@@ -75,13 +71,5 @@ export default function TestProgressBarChart({
updateChartDate(); updateChartDate();
}, [progressSeries, progressCategories, theme]); }, [progressSeries, progressCategories, theme]);
return ( return <Chart options={chartData.options} series={chartData.series} type="bar" width={'100%'} height={'100%'} />;
<Chart
options={chartData.options}
series={chartData.series}
type="bar"
width={"100%"}
height={"100%"}
/>
);
} }

View File

@@ -1,10 +1,10 @@
import React from "react"; import React from 'react';
import { useState, useEffect } from "react"; import { useState, useEffect } from 'react';
import dynamic from "next/dynamic"; import dynamic from 'next/dynamic';
import { testTypes } from "@/config/selection"; import { testTypes } from '@/config/selection';
import { CaseTypeCountType } from "@/types/case"; import { CaseTypeCountType } from '@/types/case';
import { HomeMessages } from "./page"; import { HomeMessages } from './page';
const Chart = dynamic(() => import("react-apexcharts"), { ssr: false }); const Chart = dynamic(() => import('react-apexcharts'), { ssr: false });
type Props = { type Props = {
typesCounts: CaseTypeCountType[]; typesCounts: CaseTypeCountType[];
@@ -12,11 +12,7 @@ type Props = {
theme: string; theme: string;
}; };
export default function TestTypesDonutChart({ export default function TestTypesDonutChart({ typesCounts, messages, theme }: Props) {
typesCounts,
messages,
theme,
}: Props) {
const [chartData, setChartData] = useState({ const [chartData, setChartData] = useState({
series: [], series: [],
options: { options: {
@@ -38,10 +34,10 @@ export default function TestTypesDonutChart({
const legend = { const legend = {
labels: { labels: {
colors: testTypes.map((entry) => { colors: testTypes.map((entry) => {
if (theme === "light") { if (theme === 'light') {
return "black"; return 'black';
} else { } else {
return "white"; return 'white';
} }
}), }),
}, },
@@ -57,13 +53,5 @@ export default function TestTypesDonutChart({
updateChartDate(); updateChartDate();
}, [typesCounts, theme]); }, [typesCounts, theme]);
return ( return <Chart options={chartData.options} series={chartData.series} type="donut" width={'100%'} height={'100%'} />;
<Chart
options={chartData.options}
series={chartData.series}
type="donut"
width={"100%"}
height={"100%"}
/>
);
} }

View File

@@ -1,31 +1,26 @@
"use client"; 'use client';
import { useState, useEffect } from "react"; import { useState, useEffect } from 'react';
import { Divider } from "@nextui-org/react"; import { Divider } from '@nextui-org/react';
import { title, subtitle } from "@/components/primitives"; import { title, subtitle } from '@/components/primitives';
import { Card, CardBody, Chip } from "@nextui-org/react"; import { Card, CardBody, Chip } from '@nextui-org/react';
import { Folder, Clipboard, FlaskConical } from "lucide-react"; import { Folder, Clipboard, FlaskConical } from 'lucide-react';
import { CaseTypeCountType, CasePriorityCountType } from "@/types/case"; import { CaseTypeCountType, CasePriorityCountType } from '@/types/case';
import { ProgressSeriesType } from "@/types/run"; import { ProgressSeriesType } from '@/types/run';
import { HomeMessages } from "./page"; import { HomeMessages } from './page';
import { import { aggregateBasicInfo, aggregateTestPriority, aggregateTestType, aggregateProgress } from './aggregate';
aggregateBasicInfo, import TestTypesChart from './TestTypesDonutChart';
aggregateTestPriority, import TestPriorityChart from './TestPriorityDonutChart';
aggregateTestType, import TestProgressBarChart from './TestProgressColumnChart';
aggregateProgress, import Config from '@/config/config';
} from "./aggregate"; import { useTheme } from 'next-themes';
import TestTypesChart from "./TestTypesDonutChart";
import TestPriorityChart from "./TestPriorityDonutChart";
import TestProgressBarChart from "./TestProgressColumnChart";
import Config from "@/config/config";
import { useTheme } from "next-themes";
const apiServer = Config.apiServer; const apiServer = Config.apiServer;
async function fetchProject(url) { async function fetchProject(url) {
try { try {
const response = await fetch(url, { const response = await fetch(url, {
method: "GET", method: 'GET',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
}); });
@@ -36,7 +31,7 @@ async function fetchProject(url) {
const data = await response.json(); const data = await response.json();
return data; return data;
} catch (error: any) { } catch (error: any) {
console.error("Error fetching data:", error.message); console.error('Error fetching data:', error.message);
} }
} }
@@ -48,8 +43,8 @@ type Props = {
export function Home({ projectId, messages }: Props) { export function Home({ projectId, messages }: Props) {
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme();
const [project, setProject] = useState({ const [project, setProject] = useState({
name: "", name: '',
detail: "", detail: '',
Folders: [{ Cases: [] }], Folders: [{ Cases: [] }],
Runs: [{ RunCases: [] }], Runs: [{ RunCases: [] }],
}); });
@@ -57,8 +52,7 @@ export function Home({ projectId, messages }: Props) {
const [caseNum, setCaseNum] = useState(0); const [caseNum, setCaseNum] = useState(0);
const [runNum, setRunNum] = useState(0); const [runNum, setRunNum] = useState(0);
const [typesCounts, setTypesCounts] = useState<CaseTypeCountType[]>(); const [typesCounts, setTypesCounts] = useState<CaseTypeCountType[]>();
const [priorityCounts, setPriorityCounts] = const [priorityCounts, setPriorityCounts] = useState<CasePriorityCountType[]>();
useState<CasePriorityCountType[]>();
const [progressCategories, setProgressCategories] = useState<string[]>(); const [progressCategories, setProgressCategories] = useState<string[]>();
const [progressSeries, setProgressSeries] = useState<ProgressSeriesType[]>(); const [progressSeries, setProgressSeries] = useState<ProgressSeriesType[]>();
const url = `${apiServer}/home/${projectId}`; const url = `${apiServer}/home/${projectId}`;
@@ -69,7 +63,7 @@ export function Home({ projectId, messages }: Props) {
const data = await fetchProject(url); const data = await fetchProject(url);
setProject(data); setProject(data);
} catch (error: any) { } catch (error: any) {
console.error("Error in effect:", error.message); console.error('Error in effect:', error.message);
} }
} }
@@ -99,68 +93,41 @@ export function Home({ projectId, messages }: Props) {
return ( return (
<div className="container mx-auto max-w-5xl pt-6 px-6 flex-grow"> <div className="container mx-auto max-w-5xl pt-6 px-6 flex-grow">
<h1 className={title({ size: "sm" })}>{project.name}</h1> <h1 className={title({ size: 'sm' })}>{project.name}</h1>
<div className="mt-4"> <div className="mt-4">
<Chip <Chip variant="flat" startContent={<Folder size={16} />} className="px-3">
variant="flat"
startContent={<Folder size={16} />}
className="px-3"
>
{folderNum} {messages.folders} {folderNum} {messages.folders}
</Chip> </Chip>
<Chip <Chip variant="flat" startContent={<Clipboard size={16} />} className="px-3 ms-2">
variant="flat"
startContent={<Clipboard size={16} />}
className="px-3 ms-2"
>
{caseNum} {messages.testCases} {caseNum} {messages.testCases}
</Chip> </Chip>
<Chip <Chip variant="flat" startContent={<FlaskConical size={16} />} className="px-3 ms-2">
variant="flat"
startContent={<FlaskConical size={16} />}
className="px-3 ms-2"
>
{runNum} {messages.testRuns} {runNum} {messages.testRuns}
</Chip> </Chip>
</div> </div>
{project.detail && ( {project.detail && (
<Card <Card className="mt-3 bg-neutral-100 dark:bg-neutral-700 dark:text-white" shadow="none">
className="mt-3 bg-neutral-100 dark:bg-neutral-700 dark:text-white"
shadow="none"
>
<CardBody>{project.detail}</CardBody> <CardBody>{project.detail}</CardBody>
</Card> </Card>
)} )}
<Divider className="my-8" /> <Divider className="my-8" />
<h2 className={subtitle()}>{messages.progress}</h2> <h2 className={subtitle()}>{messages.progress}</h2>
<div style={{ height: "18rem" }}> <div style={{ height: '18rem' }}>
<TestProgressBarChart <TestProgressBarChart progressSeries={progressSeries} progressCategories={progressCategories} theme={theme} />
progressSeries={progressSeries}
progressCategories={progressCategories}
theme={theme}
/>
</div> </div>
<Divider className="my-12" /> <Divider className="my-12" />
<h2 className={subtitle()}>{messages.testClassification}</h2> <h2 className={subtitle()}>{messages.testClassification}</h2>
<div className="flex pb-20"> <div className="flex pb-20">
<div style={{ width: "32rem", height: "18rem" }}> <div style={{ width: '32rem', height: '18rem' }}>
<h3>{messages.byType}</h3> <h3>{messages.byType}</h3>
<TestTypesChart <TestTypesChart typesCounts={typesCounts} messages={messages} theme={theme} />
typesCounts={typesCounts}
messages={messages}
theme={theme}
/>
</div> </div>
<div style={{ width: "30rem", height: "18rem" }}> <div style={{ width: '30rem', height: '18rem' }}>
<h3>{messages.byPriority}</h3> <h3>{messages.byPriority}</h3>
<TestPriorityChart <TestPriorityChart priorityCounts={priorityCounts} messages={messages} theme={theme} />
priorityCounts={priorityCounts}
messages={messages}
theme={theme}
/>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,5 @@
import { Home } from "./home"; import { Home } from './home';
import { useTranslations } from "next-intl"; import { useTranslations } from 'next-intl';
export type HomeMessages = { export type HomeMessages = {
folders: string; folders: string;
@@ -35,37 +35,37 @@ export type HomeMessages = {
}; };
export default function Page({ params }: { params: { projectId: string } }) { export default function Page({ params }: { params: { projectId: string } }) {
const t = useTranslations("Home"); const t = useTranslations('Home');
const messages = { const messages = {
folders: t("Folders"), folders: t('Folders'),
testCases: t("test_cases"), testCases: t('test_cases'),
testRuns: t("test_runs"), testRuns: t('test_runs'),
progress: t("progress"), progress: t('progress'),
untested: t("untested"), untested: t('untested'),
passed: t("passed"), passed: t('passed'),
failed: t("failed"), failed: t('failed'),
retest: t("retest"), retest: t('retest'),
skipped: t("skipped"), skipped: t('skipped'),
testClassification: t("test_classification"), testClassification: t('test_classification'),
byType: t("by_type"), byType: t('by_type'),
byPriority: t("by_priority"), byPriority: t('by_priority'),
other: t("other"), other: t('other'),
security: t("security"), security: t('security'),
performance: t("performance"), performance: t('performance'),
accessibility: t("accessibility"), accessibility: t('accessibility'),
functional: t("functional"), functional: t('functional'),
acceptance: t("acceptance"), acceptance: t('acceptance'),
usability: t("usability"), usability: t('usability'),
smokeSanity: t("smoke_sanity"), smokeSanity: t('smoke_sanity'),
compatibility: t("compatibility"), compatibility: t('compatibility'),
destructive: t("destructive"), destructive: t('destructive'),
regression: t("regression"), regression: t('regression'),
automated: t("automated"), automated: t('automated'),
manual: t("manual"), manual: t('manual'),
critical: t("critical"), critical: t('critical'),
high: t("high"), high: t('high'),
medium: t("medium"), medium: t('medium'),
low: t("low"), low: t('low'),
}; };
return ( return (
<> <>

View File

@@ -1,5 +1,5 @@
import Sidebar from "./Sidebar"; import Sidebar from './Sidebar';
import { useTranslations } from "next-intl"; import { useTranslations } from 'next-intl';
export default function SidebarLayout({ export default function SidebarLayout({
children, children,
@@ -8,11 +8,11 @@ export default function SidebarLayout({
children: React.ReactNode; children: React.ReactNode;
params: { locale: string }; params: { locale: string };
}) { }) {
const t = useTranslations("Project"); const t = useTranslations('Project');
const messages = { const messages = {
home: t("home"), home: t('home'),
testCases: t("test_cases"), testCases: t('test_cases'),
testRuns: t("test_runs"), testRuns: t('test_runs'),
}; };
return ( return (

View File

@@ -1,10 +1,10 @@
"use client"; 'use client';
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react';
import { Button } from "@nextui-org/react"; import { Button } from '@nextui-org/react';
import { Plus } from "lucide-react"; import { Plus } from 'lucide-react';
import RunsTable from "./RunsTable"; import RunsTable from './RunsTable';
import { fetchRuns, createRun, deleteRun } from "./runsControl"; import { fetchRuns, createRun, deleteRun } from './runsControl';
import { RunsMessages } from "@/types/run"; import { RunsMessages } from '@/types/run';
type Props = { type Props = {
projectId: string; projectId: string;
@@ -21,7 +21,7 @@ export default function RunsPage({ projectId, locale, messages }: Props) {
const data = await fetchRuns(projectId); const data = await fetchRuns(projectId);
setRuns(data); setRuns(data);
} catch (error: any) { } catch (error: any) {
console.error("Error in effect:", error.message); console.error('Error in effect:', error.message);
} }
} }
@@ -35,7 +35,7 @@ export default function RunsPage({ projectId, locale, messages }: Props) {
updateRuns.push(newRun); updateRuns.push(newRun);
setRuns(updateRuns); setRuns(updateRuns);
} catch (error: any) { } catch (error: any) {
console.error("Error deleting run:", error); console.error('Error deleting run:', error);
} }
}; };
@@ -45,7 +45,7 @@ export default function RunsPage({ projectId, locale, messages }: Props) {
const data = await fetchRuns(projectId); const data = await fetchRuns(projectId);
setRuns(data); setRuns(data);
} catch (error: any) { } catch (error: any) {
console.error("Error deleting run:", error); console.error('Error deleting run:', error);
} }
}; };
@@ -54,24 +54,13 @@ export default function RunsPage({ projectId, locale, messages }: Props) {
<div className="w-full p-3 flex items-center justify-between"> <div className="w-full p-3 flex items-center justify-between">
<h3 className="font-bold">{messages.runList}</h3> <h3 className="font-bold">{messages.runList}</h3>
<div> <div>
<Button <Button startContent={<Plus size={16} />} size="sm" color="primary" onClick={onCreateClick}>
startContent={<Plus size={16} />}
size="sm"
color="primary"
onClick={onCreateClick}
>
{messages.newRun} {messages.newRun}
</Button> </Button>
</div> </div>
</div> </div>
<RunsTable <RunsTable projectId={projectId} runs={runs} onDeleteRun={onDeleteClick} messages={messages} locale={locale} />
projectId={projectId}
runs={runs}
onDeleteRun={onDeleteClick}
messages={messages}
locale={locale}
/>
</div> </div>
); );
} }

View File

@@ -1,4 +1,4 @@
import { useState, useMemo, useCallback } from "react"; import { useState, useMemo, useCallback } from 'react';
import { import {
Table, Table,
TableHeader, TableHeader,
@@ -12,11 +12,11 @@ import {
DropdownMenu, DropdownMenu,
DropdownItem, DropdownItem,
SortDescriptor, SortDescriptor,
} from "@nextui-org/react"; } from '@nextui-org/react';
import { Link, NextUiLinkClasses } from "@/src/navigation"; import { Link, NextUiLinkClasses } from '@/src/navigation';
import { MoreVertical } from "lucide-react"; import { MoreVertical } from 'lucide-react';
import { RunsMessages, RunType } from "@/types/run"; import { RunsMessages, RunType } from '@/types/run';
import dayjs from "dayjs"; import dayjs from 'dayjs';
type Props = { type Props = {
projectId: string; projectId: string;
@@ -26,24 +26,18 @@ type Props = {
locale: string; locale: string;
}; };
export default function RunsTable({ export default function RunsTable({ projectId, runs, onDeleteRun, messages, locale }: Props) {
projectId,
runs,
onDeleteRun,
messages,
locale,
}: Props) {
const headerColumns = [ const headerColumns = [
{ name: messages.id, uid: "id", sortable: true }, { name: messages.id, uid: 'id', sortable: true },
{ name: messages.name, uid: "name", sortable: true }, { name: messages.name, uid: 'name', sortable: true },
{ name: messages.description, uid: "description", sortable: true }, { name: messages.description, uid: 'description', sortable: true },
{ name: messages.lastUpdate, uid: "updatedAt", sortable: true }, { name: messages.lastUpdate, uid: 'updatedAt', sortable: true },
{ name: messages.actions, uid: "actions" }, { name: messages.actions, uid: 'actions' },
]; ];
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({ const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
column: "id", column: 'id',
direction: "ascending", direction: 'ascending',
}); });
const sortedItems = useMemo(() => { const sortedItems = useMemo(() => {
@@ -52,31 +46,27 @@ export default function RunsTable({
const second = b[sortDescriptor.column as keyof RunType] as number; const second = b[sortDescriptor.column as keyof RunType] as number;
const cmp = first < second ? -1 : first > second ? 1 : 0; const cmp = first < second ? -1 : first > second ? 1 : 0;
return sortDescriptor.direction === "descending" ? -cmp : cmp; return sortDescriptor.direction === 'descending' ? -cmp : cmp;
}); });
}, [sortDescriptor, runs]); }, [sortDescriptor, runs]);
const truncateText = (text: string, maxLength: number) => { const truncateText = (text: string, maxLength: number) => {
return text.length > maxLength ? text.slice(0, maxLength) + "..." : text; return text.length > maxLength ? text.slice(0, maxLength) + '...' : text;
}; };
const renderCell = useCallback((run: RunType, columnKey: Key) => { const renderCell = useCallback((run: RunType, columnKey: Key) => {
const cellValue = run[columnKey as keyof RunType]; const cellValue = run[columnKey as keyof RunType];
switch (columnKey) { switch (columnKey) {
case "id": case 'id':
return <span>{cellValue}</span>; return <span>{cellValue}</span>;
case "name": case 'name':
return ( return (
<Link <Link href={`/projects/${projectId}/runs/${run.id}`} locale={locale} className={NextUiLinkClasses}>
href={`/projects/${projectId}/runs/${run.id}`}
locale={locale}
className={NextUiLinkClasses}
>
{cellValue} {cellValue}
</Link> </Link>
); );
case "detail": case 'detail':
const maxLength = 20; const maxLength = 20;
const truncatedValue = truncateText(cellValue, maxLength); const truncatedValue = truncateText(cellValue, maxLength);
return ( return (
@@ -84,9 +74,9 @@ export default function RunsTable({
<div>{truncatedValue}</div> <div>{truncatedValue}</div>
</div> </div>
); );
case "updatedAt": case 'updatedAt':
return <span>{dayjs(cellValue).format("YYYY/MM/DD HH:mm")}</span>; return <span>{dayjs(cellValue).format('YYYY/MM/DD HH:mm')}</span>;
case "actions": case 'actions':
return ( return (
<Dropdown> <Dropdown>
<DropdownTrigger> <DropdownTrigger>
@@ -95,10 +85,7 @@ export default function RunsTable({
</Button> </Button>
</DropdownTrigger> </DropdownTrigger>
<DropdownMenu aria-label="run actions"> <DropdownMenu aria-label="run actions">
<DropdownItem <DropdownItem className="text-danger" onClick={() => onDeleteRun(run.id)}>
className="text-danger"
onClick={() => onDeleteRun(run.id)}
>
{messages.deleteRun} {messages.deleteRun}
</DropdownItem> </DropdownItem>
</DropdownMenu> </DropdownMenu>
@@ -111,18 +98,18 @@ export default function RunsTable({
const classNames = useMemo( const classNames = useMemo(
() => ({ () => ({
wrapper: ["max-w-3xl"], wrapper: ['max-w-3xl'],
th: ["bg-transparent", "text-default-500", "border-b", "border-divider"], th: ['bg-transparent', 'text-default-500', 'border-b', 'border-divider'],
td: [ td: [
// changing the rows border radius // changing the rows border radius
// first // first
"group-data-[first=true]:first:before:rounded-none", 'group-data-[first=true]:first:before:rounded-none',
"group-data-[first=true]:last:before:rounded-none", 'group-data-[first=true]:last:before:rounded-none',
// middle // middle
"group-data-[middle=true]:before:rounded-none", 'group-data-[middle=true]:before:rounded-none',
// last // last
"group-data-[last=true]:first:before:rounded-none", 'group-data-[last=true]:first:before:rounded-none',
"group-data-[last=true]:last:before:rounded-none", 'group-data-[last=true]:last:before:rounded-none',
], ],
}), }),
[] []
@@ -141,7 +128,7 @@ export default function RunsTable({
{(column) => ( {(column) => (
<TableColumn <TableColumn
key={column.uid} key={column.uid}
align={column.uid === "actions" ? "center" : "start"} align={column.uid === 'actions' ? 'center' : 'start'}
allowsSorting={column.sortable} allowsSorting={column.sortable}
> >
{column.name} {column.name}
@@ -150,11 +137,7 @@ export default function RunsTable({
</TableHeader> </TableHeader>
<TableBody emptyContent={messages.noRunsFound} items={sortedItems}> <TableBody emptyContent={messages.noRunsFound} items={sortedItems}>
{(item) => ( {(item) => (
<TableRow key={item.id}> <TableRow key={item.id}>{(columnKey) => <TableCell>{renderCell(item, columnKey)}</TableCell>}</TableRow>
{(columnKey) => (
<TableCell>{renderCell(item, columnKey)}</TableCell>
)}
</TableRow>
)} )}
</TableBody> </TableBody>
</Table> </Table>

View File

@@ -1,7 +1,7 @@
"use client"; 'use client';
import React from "react"; import React from 'react';
import { useState, useEffect } from "react"; import { useState, useEffect } from 'react';
import { useRouter } from "@/src/navigation"; import { useRouter } from '@/src/navigation';
import { import {
Button, Button,
Input, Input,
@@ -17,28 +17,14 @@ import {
Dropdown, Dropdown,
DropdownMenu, DropdownMenu,
DropdownItem, DropdownItem,
} from "@nextui-org/react"; } from '@nextui-org/react';
import { import { Save, ArrowLeft, Folder, ChevronDown, CopyPlus, CopyMinus, RotateCw } from 'lucide-react';
Save, import RunProgressChart from './RunPregressDonutChart';
ArrowLeft, import TestCaseSelector from './TestCaseSelector';
Folder, import { testRunStatus } from '@/config/selection';
ChevronDown, import { RunType, RunCaseType, RunCaseInfoType, RunStatusCountType, RunMessages } from '@/types/run';
CopyPlus, import { CaseType } from '@/types/case';
CopyMinus, import { FolderType } from '@/types/folder';
RotateCw,
} from "lucide-react";
import RunProgressChart from "./RunPregressDonutChart";
import TestCaseSelector from "./TestCaseSelector";
import { testRunStatus } from "@/config/selection";
import {
RunType,
RunCaseType,
RunCaseInfoType,
RunStatusCountType,
RunMessages,
} from "@/types/run";
import { CaseType } from "@/types/case";
import { FolderType } from "@/types/folder";
import { import {
fetchRun, fetchRun,
updateRun, updateRun,
@@ -48,16 +34,16 @@ import {
bulkCreateRunCases, bulkCreateRunCases,
deleteRunCase, deleteRunCase,
bulkDeleteRunCases, bulkDeleteRunCases,
} from "../runsControl"; } from '../runsControl';
import { fetchFolders } from "../../folders/foldersControl"; import { fetchFolders } from '../../folders/foldersControl';
import { fetchCases } from "../../folders/[folderId]/cases/caseControl"; import { fetchCases } from '../../folders/[folderId]/cases/caseControl';
import { useTheme } from "next-themes"; import { useTheme } from 'next-themes';
const defaultTestRun = { const defaultTestRun = {
id: 0, id: 0,
name: "", name: '',
configurations: 0, configurations: 0,
description: "", description: '',
state: 0, state: 0,
projectId: 0, projectId: 0,
}; };
@@ -69,18 +55,12 @@ type Props = {
locale: string; locale: string;
}; };
export default function RunEditor({ export default function RunEditor({ projectId, runId, messages, locale }: Props) {
projectId,
runId,
messages,
locale,
}: Props) {
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme();
const [testRun, setTestRun] = useState<RunType>(defaultTestRun); const [testRun, setTestRun] = useState<RunType>(defaultTestRun);
const [folders, setFolders] = useState([]); const [folders, setFolders] = useState([]);
const [runCases, setRunCases] = useState<RunCaseType[]>([]); const [runCases, setRunCases] = useState<RunCaseType[]>([]);
const [runStatusCounts, setRunStatusCounts] = const [runStatusCounts, setRunStatusCounts] = useState<RunStatusCountType[]>();
useState<RunStatusCountType[]>();
const [selectedKeys, setSelectedKeys] = useState<Selection>(new Set([])); const [selectedKeys, setSelectedKeys] = useState<Selection>(new Set([]));
const [selectedFolder, setSelectedFolder] = useState<FolderType>({}); const [selectedFolder, setSelectedFolder] = useState<FolderType>({});
const [testcases, setTestCases] = useState<CaseType[]>([]); const [testcases, setTestCases] = useState<CaseType[]>([]);
@@ -102,7 +82,7 @@ export default function RunEditor({
setFolders(foldersData); setFolders(foldersData);
setSelectedFolder(foldersData[0]); setSelectedFolder(foldersData[0]);
} catch (error: any) { } catch (error: any) {
console.error("Error in effect:", error.message); console.error('Error in effect:', error.message);
} }
} }
@@ -120,9 +100,7 @@ export default function RunEditor({
// Check if each testCase has an association with testRun // Check if each testCase has an association with testRun
// and add "isIncluded" property // and add "isIncluded" property
const updatedTestCasesData = testCasesData.map((testCase) => { const updatedTestCasesData = testCasesData.map((testCase) => {
const runCase = latestRunCases.find( const runCase = latestRunCases.find((runCase) => runCase.caseId === testCase.id);
(runCase) => runCase.caseId === testCase.id
);
const isIncluded = runCase ? true : false; const isIncluded = runCase ? true : false;
const runStatus = runCase ? runCase.status : 0; const runStatus = runCase ? runCase.status : 0;
@@ -135,7 +113,7 @@ export default function RunEditor({
setTestCases(updatedTestCasesData); setTestCases(updatedTestCasesData);
} catch (error: any) { } catch (error: any) {
console.error("Error fetching cases data:", error.message); console.error('Error fetching cases data:', error.message);
} }
} }
} }
@@ -155,10 +133,7 @@ export default function RunEditor({
}); });
}; };
const handleIncludeExcludeCase = async ( const handleIncludeExcludeCase = async (isInclude: boolean, clickedTestCaseId: number) => {
isInclude: boolean,
clickedTestCaseId: number
) => {
if (isInclude) { if (isInclude) {
const createdRunCase = await createRunCase(runId, clickedTestCaseId); const createdRunCase = await createRunCase(runId, clickedTestCaseId);
setRunCases((prevRunCases) => { setRunCases((prevRunCases) => {
@@ -167,9 +142,7 @@ export default function RunEditor({
} else { } else {
await deleteRunCase(runId, clickedTestCaseId); await deleteRunCase(runId, clickedTestCaseId);
setRunCases((prevRunCases) => { setRunCases((prevRunCases) => {
return prevRunCases.filter( return prevRunCases.filter((runCase) => runCase.caseId !== clickedTestCaseId);
(runCase) => runCase.caseId !== clickedTestCaseId
);
}); });
} }
@@ -185,7 +158,7 @@ export default function RunEditor({
const handleBulkIncludeExcludeCases = async (isInclude: boolean) => { const handleBulkIncludeExcludeCases = async (isInclude: boolean) => {
let keys: number[] = []; let keys: number[] = [];
if (selectedKeys === "all") { if (selectedKeys === 'all') {
keys = testcases.map((item) => item.id); keys = testcases.map((item) => item.id);
} else { } else {
keys = Array.from(selectedKeys).map(Number); keys = Array.from(selectedKeys).map(Number);
@@ -218,7 +191,7 @@ export default function RunEditor({
setSelectedKeys(new Set([])); setSelectedKeys(new Set([]));
}; };
const baseClass = ""; const baseClass = '';
const selectedClass = `${baseClass} bg-neutral-200 dark:bg-neutral-700`; const selectedClass = `${baseClass} bg-neutral-200 dark:bg-neutral-700`;
return ( return (
@@ -230,9 +203,7 @@ export default function RunEditor({
isIconOnly isIconOnly
size="sm" size="sm"
className="rounded-full bg-neutral-50 dark:bg-neutral-600" className="rounded-full bg-neutral-50 dark:bg-neutral-600"
onPress={() => onPress={() => router.push(`/projects/${projectId}/runs`, { locale: locale })}
router.push(`/projects/${projectId}/runs`, { locale: locale })
}
> >
<ArrowLeft size={16} /> <ArrowLeft size={16} />
</Button> </Button>
@@ -272,11 +243,7 @@ export default function RunEditor({
</Tooltip> </Tooltip>
</div> </div>
<RunProgressChart <RunProgressChart statusCounts={runStatusCounts} messages={messages} theme={theme} />
statusCounts={runStatusCounts}
messages={messages}
theme={theme}
/>
</div> </div>
</div> </div>
<div className="flex-grow"> <div className="flex-grow">
@@ -287,7 +254,7 @@ export default function RunEditor({
label={messages.title} label={messages.title}
value={testRun.name} value={testRun.name}
isInvalid={isNameInvalid} isInvalid={isNameInvalid}
errorMessage={isNameInvalid ? messages.pleaseEnter : ""} errorMessage={isNameInvalid ? messages.pleaseEnter : ''}
onChange={(e) => { onChange={(e) => {
setTestRun({ ...testRun, name: e.target.value }); setTestRun({ ...testRun, name: e.target.value });
}} }}
@@ -312,9 +279,7 @@ export default function RunEditor({
selectedKeys={[testRunStatus[testRun.state].uid]} selectedKeys={[testRunStatus[testRun.state].uid]}
onSelectionChange={(e) => { onSelectionChange={(e) => {
const selectedUid = e.anchorKey; const selectedUid = e.anchorKey;
const index = testRunStatus.findIndex( const index = testRunStatus.findIndex((template) => template.uid === selectedUid);
(template) => template.uid === selectedUid
);
setTestRun({ ...testRun, state: index }); setTestRun({ ...testRun, state: index });
}} }}
label={messages.status} label={messages.status}
@@ -334,14 +299,10 @@ export default function RunEditor({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h6 className="h-8 font-bold">{messages.selectTestCase}</h6> <h6 className="h-8 font-bold">{messages.selectTestCase}</h6>
<div> <div>
{(selectedKeys.size > 0 || selectedKeys === "all") && ( {(selectedKeys.size > 0 || selectedKeys === 'all') && (
<Dropdown> <Dropdown>
<DropdownTrigger> <DropdownTrigger>
<Button <Button size="sm" color="primary" endContent={<ChevronDown size={16} />}>
size="sm"
color="primary"
endContent={<ChevronDown size={16} />}
>
{messages.testCaseSelection} {messages.testCaseSelection}
</Button> </Button>
</DropdownTrigger> </DropdownTrigger>
@@ -371,14 +332,8 @@ export default function RunEditor({
<ListboxItem <ListboxItem
key={index} key={index}
onClick={() => setSelectedFolder(folder)} onClick={() => setSelectedFolder(folder)}
startContent={ startContent={<Folder size={20} color="#F7C24E" fill="#F7C24E" />}
<Folder size={20} color="#F7C24E" fill="#F7C24E" /> className={selectedFolder && folder.id === selectedFolder.id ? selectedClass : baseClass}
}
className={
selectedFolder && folder.id === selectedFolder.id
? selectedClass
: baseClass
}
> >
{folder.name} {folder.name}
</ListboxItem> </ListboxItem>
@@ -391,12 +346,8 @@ export default function RunEditor({
selectedKeys={selectedKeys} selectedKeys={selectedKeys}
onSelectionChange={setSelectedKeys} onSelectionChange={setSelectedKeys}
onStatusChange={handleChangeStatus} onStatusChange={handleChangeStatus}
onIncludeCase={(includeTestId) => onIncludeCase={(includeTestId) => handleIncludeExcludeCase(true, includeTestId)}
handleIncludeExcludeCase(true, includeTestId) onExcludeCase={(excludeCaseId) => handleIncludeExcludeCase(false, excludeCaseId)}
}
onExcludeCase={(excludeCaseId) =>
handleIncludeExcludeCase(false, excludeCaseId)
}
messages={messages} messages={messages}
/> />
</div> </div>

View File

@@ -1,9 +1,9 @@
import React from "react"; import React from 'react';
import { useState, useEffect } from "react"; import { useState, useEffect } from 'react';
import dynamic from "next/dynamic"; import dynamic from 'next/dynamic';
import { testRunCaseStatus } from "@/config/selection"; import { testRunCaseStatus } from '@/config/selection';
import { RunStatusCountType, RunMessages } from "@/types/run"; import { RunStatusCountType, RunMessages } from '@/types/run';
const Chart = dynamic(() => import("react-apexcharts"), { ssr: false }); const Chart = dynamic(() => import('react-apexcharts'), { ssr: false });
type Props = { type Props = {
statusCounts: RunStatusCountType[]; statusCounts: RunStatusCountType[];
@@ -11,11 +11,7 @@ type Props = {
theme: string; theme: string;
}; };
export default function RunProgressDounut({ export default function RunProgressDounut({ statusCounts, messages, theme }: Props) {
statusCounts,
messages,
theme,
}: Props) {
const [chartData, setChartData] = useState({ const [chartData, setChartData] = useState({
series: [], series: [],
options: { options: {
@@ -37,10 +33,10 @@ export default function RunProgressDounut({
const legend = { const legend = {
labels: { labels: {
colors: testRunCaseStatus.map((entry) => { colors: testRunCaseStatus.map((entry) => {
if (theme === "light") { if (theme === 'light') {
return "black"; return 'black';
} else { } else {
return "white"; return 'white';
} }
}), }),
}, },
@@ -56,13 +52,5 @@ export default function RunProgressDounut({
updateChartDate(); updateChartDate();
}, [statusCounts, theme]); }, [statusCounts, theme]);
return ( return <Chart options={chartData.options} series={chartData.series} type="donut" width={'100%'} height={'100%'} />;
<Chart
options={chartData.options}
series={chartData.series}
type="donut"
width={"100%"}
height={"100%"}
/>
);
} }

View File

@@ -1,4 +1,4 @@
import { useState, useMemo } from "react"; import { useState, useMemo } from 'react';
import { import {
Table, Table,
TableHeader, TableHeader,
@@ -13,7 +13,7 @@ import {
DropdownItem, DropdownItem,
Selection, Selection,
SortDescriptor, SortDescriptor,
} from "@nextui-org/react"; } from '@nextui-org/react';
import { import {
ChevronDown, ChevronDown,
MoreVertical, MoreVertical,
@@ -24,10 +24,10 @@ import {
CircleDashed, CircleDashed,
CircleX, CircleX,
CircleSlash2, CircleSlash2,
} from "lucide-react"; } from 'lucide-react';
import { priorities, testRunCaseStatus } from "@/config/selection"; import { priorities, testRunCaseStatus } from '@/config/selection';
import { CaseType } from "@/types/case"; import { CaseType } from '@/types/case';
import { RunsMessages } from "@/types/run"; import { RunsMessages } from '@/types/run';
type Props = { type Props = {
cases: CaseType[]; cases: CaseType[];
@@ -49,16 +49,16 @@ export default function TestCaseSelector({
messages, messages,
}: Props) { }: Props) {
const headerColumns = [ const headerColumns = [
{ name: messages.id, uid: "id", sortable: true }, { name: messages.id, uid: 'id', sortable: true },
{ name: messages.title, uid: "title", sortable: true }, { name: messages.title, uid: 'title', sortable: true },
{ name: messages.priority, uid: "priority", sortable: true }, { name: messages.priority, uid: 'priority', sortable: true },
{ name: messages.status, uid: "runStatus", sortable: true }, { name: messages.status, uid: 'runStatus', sortable: true },
{ name: messages.actions, uid: "actions" }, { name: messages.actions, uid: 'actions' },
]; ];
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({ const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
column: "id", column: 'id',
direction: "ascending", direction: 'ascending',
}); });
const sortedItems = useMemo(() => { const sortedItems = useMemo(() => {
@@ -67,23 +67,23 @@ export default function TestCaseSelector({
const second = b[sortDescriptor.column as keyof CaseType] as number; const second = b[sortDescriptor.column as keyof CaseType] as number;
const cmp = first < second ? -1 : first > second ? 1 : 0; const cmp = first < second ? -1 : first > second ? 1 : 0;
return sortDescriptor.direction === "descending" ? -cmp : cmp; return sortDescriptor.direction === 'descending' ? -cmp : cmp;
}); });
}, [sortDescriptor, cases]); }, [sortDescriptor, cases]);
const notIncludedCaseClass = "text-neutral-200 dark:text-neutral-600"; const notIncludedCaseClass = 'text-neutral-200 dark:text-neutral-600';
const chipBaseClass = "flex items-center text-default-600"; const chipBaseClass = 'flex items-center text-default-600';
const renderStatusIcon = (uid: string) => { const renderStatusIcon = (uid: string) => {
if (uid === "untested") { if (uid === 'untested') {
return <Circle size={16} color="#d4d4d8" />; return <Circle size={16} color="#d4d4d8" />;
} else if (uid === "passed") { } else if (uid === 'passed') {
return <CircleCheck size={16} color="#17c964" />; return <CircleCheck size={16} color="#17c964" />;
} else if (uid === "retest") { } else if (uid === 'retest') {
return <CircleDashed size={16} color="#f5a524" />; return <CircleDashed size={16} color="#f5a524" />;
} else if (uid === "failed") { } else if (uid === 'failed') {
return <CircleX size={16} color="#f31260" />; return <CircleX size={16} color="#f31260" />;
} else if (uid === "skipped") { } else if (uid === 'skipped') {
return <CircleSlash2 size={16} color="#52525b" />; return <CircleSlash2 size={16} color="#52525b" />;
} }
}; };
@@ -93,22 +93,18 @@ export default function TestCaseSelector({
const isIncluded = testCase.isIncluded; const isIncluded = testCase.isIncluded;
switch (columnKey) { switch (columnKey) {
case "priority": case 'priority':
return ( return (
<div <div className={isIncluded ? chipBaseClass : chipBaseClass + notIncludedCaseClass}>
className={
isIncluded ? chipBaseClass : chipBaseClass + notIncludedCaseClass
}
>
<Circle <Circle
size={8} size={8}
color={isIncluded ? priorities[cellValue].color : "#d4d4d8"} color={isIncluded ? priorities[cellValue].color : '#d4d4d8'}
fill={isIncluded ? priorities[cellValue].color : "#d4d4d8"} fill={isIncluded ? priorities[cellValue].color : '#d4d4d8'}
/> />
<div className="ms-3">{messages[priorities[cellValue].uid]}</div> <div className="ms-3">{messages[priorities[cellValue].uid]}</div>
</div> </div>
); );
case "runStatus": case 'runStatus':
return ( return (
<Dropdown> <Dropdown>
<DropdownTrigger> <DropdownTrigger>
@@ -116,15 +112,10 @@ export default function TestCaseSelector({
size="sm" size="sm"
variant="light" variant="light"
isDisabled={!isIncluded} isDisabled={!isIncluded}
startContent={ startContent={isIncluded && renderStatusIcon(testRunCaseStatus[cellValue].uid)}
isIncluded &&
renderStatusIcon(testRunCaseStatus[cellValue].uid)
}
endContent={isIncluded && <ChevronDown size={16} />} endContent={isIncluded && <ChevronDown size={16} />}
> >
<span className="w-12"> <span className="w-12">{isIncluded && messages[testRunCaseStatus[cellValue].uid]}</span>
{isIncluded && messages[testRunCaseStatus[cellValue].uid]}
</span>
</Button> </Button>
</DropdownTrigger> </DropdownTrigger>
<DropdownMenu aria-label="test case actions"> <DropdownMenu aria-label="test case actions">
@@ -140,7 +131,7 @@ export default function TestCaseSelector({
</DropdownMenu> </DropdownMenu>
</Dropdown> </Dropdown>
); );
case "actions": case 'actions':
return ( return (
<Dropdown> <Dropdown>
<DropdownTrigger> <DropdownTrigger>
@@ -173,18 +164,18 @@ export default function TestCaseSelector({
const classNames = useMemo( const classNames = useMemo(
() => ({ () => ({
wrapper: ["min-w-3xl"], wrapper: ['min-w-3xl'],
th: ["bg-transparent", "text-default-500", "border-b", "border-divider"], th: ['bg-transparent', 'text-default-500', 'border-b', 'border-divider'],
td: [ td: [
// changing the rows border radius // changing the rows border radius
// first // first
"group-data-[first=true]:first:before:rounded-none", 'group-data-[first=true]:first:before:rounded-none',
"group-data-[first=true]:last:before:rounded-none", 'group-data-[first=true]:last:before:rounded-none',
// middle // middle
"group-data-[middle=true]:before:rounded-none", 'group-data-[middle=true]:before:rounded-none',
// last // last
"group-data-[last=true]:first:before:rounded-none", 'group-data-[last=true]:first:before:rounded-none',
"group-data-[last=true]:last:before:rounded-none", 'group-data-[last=true]:last:before:rounded-none',
], ],
}), }),
[] []
@@ -211,7 +202,7 @@ export default function TestCaseSelector({
{(column) => ( {(column) => (
<TableColumn <TableColumn
key={column.uid} key={column.uid}
align={column.uid === "actions" ? "center" : "start"} align={column.uid === 'actions' ? 'center' : 'start'}
allowsSorting={column.sortable} allowsSorting={column.sortable}
> >
{column.name} {column.name}
@@ -220,13 +211,8 @@ export default function TestCaseSelector({
</TableHeader> </TableHeader>
<TableBody emptyContent={messages.noCasesFound} items={sortedItems}> <TableBody emptyContent={messages.noCasesFound} items={sortedItems}>
{(item) => ( {(item) => (
<TableRow <TableRow key={item.id} className={!item.isIncluded ? notIncludedCaseClass : ''}>
key={item.id} {(columnKey) => <TableCell>{renderCell(item, columnKey)}</TableCell>}
className={!item.isIncluded ? notIncludedCaseClass : ""}
>
{(columnKey) => (
<TableCell>{renderCell(item, columnKey)}</TableCell>
)}
</TableRow> </TableRow>
)} )}
</TableBody> </TableBody>

View File

@@ -1,53 +1,42 @@
import RunEditor from "./RunEditor"; import RunEditor from './RunEditor';
import { useTranslations } from "next-intl"; import { useTranslations } from 'next-intl';
export default function Page({ export default function Page({ params }: { params: { projectId: string; runId: string; locale: string } }) {
params, const t = useTranslations('Run');
}: {
params: { projectId: string; runId: string; locale: string };
}) {
const t = useTranslations("Run");
const messages = { const messages = {
backToRuns: t("back_to_runs"), backToRuns: t('back_to_runs'),
updating: t("updating"), updating: t('updating'),
update: t("update"), update: t('update'),
progress: t("progress"), progress: t('progress'),
refresh: t("refresh"), refresh: t('refresh'),
id: t("id"), id: t('id'),
title: t("title"), title: t('title'),
pleaseEnter: t("please_enter"), pleaseEnter: t('please_enter'),
description: t("description"), description: t('description'),
new: t("new"), new: t('new'),
inProgress: t("inProgress"), inProgress: t('inProgress'),
underReview: t("underReview"), underReview: t('underReview'),
rejected: t("rejected"), rejected: t('rejected'),
done: t("done"), done: t('done'),
closed: t("closed"), closed: t('closed'),
priority: t("priority"), priority: t('priority'),
actions: t("actions"), actions: t('actions'),
status: t("status"), status: t('status'),
critical: t("critical"), critical: t('critical'),
high: t("high"), high: t('high'),
medium: t("medium"), medium: t('medium'),
low: t("low"), low: t('low'),
untested: t("untested"), untested: t('untested'),
passed: t("passed"), passed: t('passed'),
failed: t("failed"), failed: t('failed'),
retest: t("retest"), retest: t('retest'),
skipped: t("skipped"), skipped: t('skipped'),
selectTestCase: t("select_test_case"), selectTestCase: t('select_test_case'),
testCaseSelection: t("test_case_selection"), testCaseSelection: t('test_case_selection'),
includeInRun: t("include_in_run"), includeInRun: t('include_in_run'),
excludeFromRun: t("exclude_from_run"), excludeFromRun: t('exclude_from_run'),
noCasesFound: t("no_cases_found"), noCasesFound: t('no_cases_found'),
}; };
return ( return <RunEditor projectId={params.projectId} runId={params.runId} messages={messages} locale={params.locale} />;
<RunEditor
projectId={params.projectId}
runId={params.runId}
messages={messages}
locale={params.locale}
/>
);
} }

View File

@@ -1,31 +1,23 @@
import RunsPage from "./RunsPage"; import RunsPage from './RunsPage';
import { useTranslations } from "next-intl"; import { useTranslations } from 'next-intl';
export default function Page({ export default function Page({ params }: { params: { projectId: string; locale: string } }) {
params, const t = useTranslations('Runs');
}: {
params: { projectId: string; locale: string };
}) {
const t = useTranslations("Runs");
const messages = { const messages = {
runList: t("run_list"), runList: t('run_list'),
id: t("id"), id: t('id'),
name: t("name"), name: t('name'),
description: t("description"), description: t('description'),
lastUpdate: t("last_update"), lastUpdate: t('last_update'),
actions: t("actions"), actions: t('actions'),
newRun: t("new_run"), newRun: t('new_run'),
deleteRun: t("delete_run"), deleteRun: t('delete_run'),
noRunsFound: t("no_runs_found"), noRunsFound: t('no_runs_found'),
}; };
return ( return (
<> <>
<RunsPage <RunsPage projectId={params.projectId} locale={params.locale} messages={messages} />
projectId={params.projectId}
locale={params.locale}
messages={messages}
/>
</> </>
); );
} }

View File

@@ -1,7 +1,3 @@
export default function ProjectsLayout({ export default function ProjectsLayout({ children }: { children: React.ReactNode }) {
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>; return <>{children}</>;
} }

View File

@@ -1,26 +1,26 @@
import ProjectsPage from "./ProjectsPage"; import ProjectsPage from './ProjectsPage';
import { useTranslations } from "next-intl"; import { useTranslations } from 'next-intl';
export default function Page(params: { locale: string }) { export default function Page(params: { locale: string }) {
const t = useTranslations("Projects"); const t = useTranslations('Projects');
const messages = { const messages = {
projectList: t("projectList"), projectList: t('projectList'),
project: t("project"), project: t('project'),
newProject: t("new_project"), newProject: t('new_project'),
editProject: t("edit_project"), editProject: t('edit_project'),
deleteProject: t("delete_project"), deleteProject: t('delete_project'),
id: t("id"), id: t('id'),
name: t("name"), name: t('name'),
detail: t("detail"), detail: t('detail'),
lastUpdate: t("last_update"), lastUpdate: t('last_update'),
actions: t("actions"), actions: t('actions'),
projectName: t("project_name"), projectName: t('project_name'),
projectDetail: t("project_detail"), projectDetail: t('project_detail'),
close: t("close"), close: t('close'),
create: t("create"), create: t('create'),
update: t("update"), update: t('update'),
pleaseEnter: t("please_enter"), pleaseEnter: t('please_enter'),
noProjectsFound: t("no_projects_found"), noProjectsFound: t('no_projects_found'),
}; };
return ( return (
<> <>

View File

@@ -1,13 +1,13 @@
"use client"; 'use client';
import * as React from "react"; import * as React from 'react';
import { NextUIProvider } from "@nextui-org/react"; import { NextUIProvider } from '@nextui-org/react';
import { useRouter } from "next/navigation"; import { useRouter } from 'next/navigation';
import { ThemeProvider as NextThemesProvider } from "next-themes"; import { ThemeProvider as NextThemesProvider } from 'next-themes';
import { ThemeProviderProps } from "next-themes/dist/types"; import { ThemeProviderProps } from 'next-themes/dist/types';
import TokenProvider from "./TokenProvider"; import TokenProvider from './TokenProvider';
import { TokenProps } from "@/types/user"; import { TokenProps } from '@/types/user';
export interface ProvidersProps { export interface ProvidersProps {
children: React.ReactNode; children: React.ReactNode;

View File

@@ -3,7 +3,7 @@
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
"format": "prettier --write frontend/**/*.{js,ts,json} backend/**/*.{js,ts,json}", "format": "prettier --write frontend/**/*.{js,ts,jsx,tsx,json} backend/**/*.{js,ts,json}",
"test": "vitest", "test": "vitest",
"coverage": "vitest run --coverage" "coverage": "vitest run --coverage"
}, },