Refactored to use app folder.

This commit is contained in:
Mike Cao 2023-09-29 05:29:22 -07:00
parent 40cfcd41e9
commit 9a52cdd2e1
258 changed files with 2025 additions and 2258 deletions

View File

@ -6,7 +6,7 @@ const pkg = require('./package.json');
const contentSecurityPolicy = `
default-src 'self';
img-src *;
script-src 'self' 'unsafe-eval';
script-src 'self' 'unsafe-eval' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
connect-src 'self' api.umami.is;
frame-ancestors 'self' ${process.env.ALLOWED_FRAME_URLS};
@ -74,16 +74,20 @@ if (process.env.CLOUD_MODE && process.env.CLOUD_URL && process.env.DISABLE_LOGIN
});
}
const basePath = process.env.BASE_PATH;
/** @type {import('next').NextConfig} */
const config = {
env: {
cloudMode: process.env.CLOUD_MODE,
basePath: basePath || '',
cloudMode: !!process.env.CLOUD_MODE,
cloudUrl: process.env.CLOUD_URL,
configUrl: '/config',
currentVersion: pkg.version,
defaultLocale: process.env.DEFAULT_LOCALE,
isProduction: process.env.NODE_ENV === 'production',
},
basePath: process.env.BASE_PATH,
basePath,
output: 'standalone',
eslint: {
ignoreDuringBuilds: true,
@ -92,11 +96,23 @@ const config = {
ignoreBuildErrors: true,
},
webpack(config) {
config.module.rules.push({
test: /\.svg$/,
issuer: /\.{js|jsx|ts|tsx}$/,
use: ['@svgr/webpack'],
});
const fileLoaderRule = config.module.rules.find(rule => rule.test?.test?.('.svg'));
config.module.rules.push(
{
...fileLoaderRule,
test: /\.svg$/i,
resourceQuery: /url/,
},
{
test: /\.svg$/i,
issuer: fileLoaderRule.issuer,
resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] },
use: ['@svgr/webpack'],
},
);
fileLoaderRule.exclude = /\.svg$/i;
config.resolve.alias['public'] = path.resolve('./public');

View File

@ -64,7 +64,7 @@
"@fontsource/inter": "^4.5.15",
"@prisma/client": "5.3.1",
"@tanstack/react-query": "^4.33.0",
"@umami/prisma-client": "^0.2.0",
"@umami/prisma-client": "^0.3.0",
"@umami/redis-client": "^0.15.0",
"chalk": "^4.1.1",
"chart.js": "^4.2.1",
@ -91,7 +91,7 @@
"kafkajs": "^2.1.0",
"maxmind": "^4.3.6",
"moment-timezone": "^0.5.35",
"next": "13.5.2",
"next": "13.5.3",
"next-basics": "^0.36.0",
"node-fetch": "^3.2.8",
"npm-run-all": "^4.1.5",
@ -100,7 +100,7 @@
"react-beautiful-dnd": "^13.1.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.4",
"react-intl": "^5.24.7",
"react-intl": "^6.4.7",
"react-simple-maps": "^2.3.0",
"react-spring": "^9.4.4",
"react-use-measure": "^2.0.4",
@ -123,12 +123,12 @@
"@rollup/plugin-node-resolve": "^15.2.0",
"@rollup/plugin-replace": "^5.0.2",
"@svgr/rollup": "^8.1.0",
"@svgr/webpack": "^6.2.1",
"@svgr/webpack": "^8.1.0",
"@types/node": "^18.11.9",
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.8",
"@typescript-eslint/eslint-plugin": "^5.50.0",
"@typescript-eslint/parser": "^5.50.0",
"@typescript-eslint/eslint-plugin": "^6.7.3",
"@typescript-eslint/parser": "^6.7.3",
"cross-env": "^7.0.3",
"esbuild": "^0.17.17",
"eslint": "^8.33.0",
@ -138,8 +138,8 @@
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-prettier": "^4.0.0",
"extract-react-intl-messages": "^4.1.1",
"husky": "^7.0.0",
"lint-staged": "^11.0.0",
"husky": "^8.0.3",
"lint-staged": "^14.0.1",
"postcss": "^8.4.21",
"postcss-flexbugs-fixes": "^5.0.2",
"postcss-import": "^15.1.0",

58
src/app/(app)/NavBar.js Normal file
View File

@ -0,0 +1,58 @@
'use client';
import { Icon, Text } from 'react-basics';
import Link from 'next/link';
import classNames from 'classnames';
import Icons from 'components/icons';
import ThemeButton from 'components/input/ThemeButton';
import LanguageButton from 'components/input/LanguageButton';
import ProfileButton from 'components/input/ProfileButton';
import useMessages from 'components/hooks/useMessages';
import HamburgerButton from 'components/common/HamburgerButton';
import { usePathname } from 'next/navigation';
import styles from './NavBar.module.css';
export function NavBar() {
const pathname = usePathname();
const { formatMessage, labels } = useMessages();
const links = [
{ label: formatMessage(labels.dashboard), url: '/dashboard' },
{ label: formatMessage(labels.websites), url: '/websites' },
{ label: formatMessage(labels.reports), url: '/reports' },
{ label: formatMessage(labels.settings), url: '/settings' },
].filter(n => n);
return (
<div className={styles.navbar}>
<div className={styles.logo}>
<Icon size="lg">
<Icons.Logo />
</Icon>
<Text>umami</Text>
</div>
<div className={styles.links}>
{links.map(({ url, label }) => {
return (
<Link
key={url}
href={url}
className={classNames({ [styles.selected]: pathname.startsWith(url) })}
>
<Text>{label}</Text>
</Link>
);
})}
</div>
<div className={styles.actions}>
<ThemeButton />
<LanguageButton />
<ProfileButton />
</div>
<div className={styles.mobile}>
<HamburgerButton />
</div>
</div>
);
}
export default NavBar;

View File

@ -1,7 +1,7 @@
.navbar {
display: grid;
grid-template-columns: max-content 1fr 1fr;
position: relative;
display: flex;
flex-direction: row;
align-items: center;
height: 60px;
background: var(--base75);
@ -9,17 +9,6 @@
padding: 0 20px;
}
.left,
.right {
display: flex;
flex-direction: row;
align-items: center;
}
.right {
justify-content: flex-end;
}
.logo {
display: flex;
flex-direction: row;
@ -35,29 +24,24 @@
flex-direction: row;
gap: 30px;
padding: 0 40px;
flex: 1;
font-weight: 700;
max-height: 60px;
}
.links a {
display: flex;
align-items: center;
gap: 10px;
line-height: 60px;
.links a,
.links a:active,
.links a:visited {
color: var(--font-color200);
line-height: 60px;
border-bottom: 2px solid transparent;
}
.links span {
white-space: nowrap;
}
.links a:hover {
color: var(--font-color100);
border-bottom: 2px solid var(--primary400);
}
.links .selected {
.links a.selected {
color: var(--font-color100);
border-bottom: 2px solid var(--primary400);
}
@ -68,7 +52,6 @@
flex-direction: row;
align-items: center;
justify-content: flex-end;
min-width: 0;
}
.mobile {
@ -76,6 +59,10 @@
}
@media only screen and (max-width: 768px) {
.navbar {
grid-template-columns: repeat(2, 1fr);
}
.links,
.actions {
display: none;

27
src/app/(app)/Shell.tsx Normal file
View File

@ -0,0 +1,27 @@
'use client';
import Script from 'next/script';
import { usePathname } from 'next/navigation';
import UpdateNotice from 'components/common/UpdateNotice';
import { useRequireLogin, useConfig } from 'components/hooks';
export function Shell({ children }) {
const { user } = useRequireLogin();
const config = useConfig();
const pathname = usePathname();
if (!user || !config) {
return null;
}
return (
<>
{children}
<UpdateNotice user={user} config={config} />
{process.env.NODE_ENV === 'production' && !pathname.includes('/share/') && (
<Script src={`telemetry.js`} />
)}
</>
);
}
export default Shell;

View File

@ -2,11 +2,11 @@ import WebsiteSelect from 'components/input/WebsiteSelect';
import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader';
import EventsChart from 'components/metrics/EventsChart';
import WebsiteChart from 'components/pages/websites/WebsiteChart';
import WebsiteChart from '../websites/[id]/WebsiteChart';
import useApi from 'components/hooks/useApi';
import Head from 'next/head';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useRouter } from 'next/navigation';
import Script from 'next/script';
import { Button, Column, Row } from 'react-basics';
import styles from './TestConsole.module.css';

View File

@ -0,0 +1,15 @@
import TestConsole from '../TestConsole';
async function getEnabled() {
return !!process.env.ENABLE_TEST_CONSOLE;
}
export default async function ConsolePage() {
const enabled = await getEnabled();
if (!enabled) {
return null;
}
return <TestConsole />;
}

View File

@ -1,11 +1,11 @@
import { Button, Icon, Icons, Text } from 'react-basics';
'use client';
import { Button, Icon, Icons, Loading, Text } from 'react-basics';
import Link from 'next/link';
import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader';
import Pager from 'components/common/Pager';
import WebsiteChartList from 'components/pages/websites/WebsiteChartList';
import DashboardSettingsButton from 'components/pages/dashboard/DashboardSettingsButton';
import DashboardEdit from 'components/pages/dashboard/DashboardEdit';
import WebsiteChartList from '../websites/[id]/WebsiteChartList';
import DashboardSettingsButton from 'app/(app)/dashboard/DashboardSettingsButton';
import DashboardEdit from 'app/(app)/dashboard/DashboardEdit';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import useApi from 'components/hooks/useApi';
import useDashboard from 'store/dashboard';
@ -20,18 +20,18 @@ export function Dashboard() {
const { get, useQuery } = useApi();
const { page, handlePageChange } = useApiFilter();
const pageSize = 10;
const {
data: result,
isLoading,
error,
} = useQuery(['websites', page, pageSize], () =>
const { data: result, isLoading } = useQuery(['websites', page, pageSize], () =>
get('/websites', { includeTeams: 1, page, pageSize }),
);
const { data, count } = result || {};
const hasData = data && data?.length !== 0;
if (isLoading) {
return <Loading size="lg" />;
}
return (
<Page loading={isLoading} error={error}>
<>
<PageHeader title={formatMessage(labels.dashboard)}>
{!editing && hasData && <DashboardSettingsButton />}
</PageHeader>
@ -63,7 +63,7 @@ export function Dashboard() {
)}
</>
)}
</Page>
</>
);
}

View File

@ -0,0 +1,10 @@
import Dashboard from 'app/(app)/dashboard/Dashboard';
import { Metadata } from 'next';
export default function () {
return <Dashboard />;
}
export const metadata: Metadata = {
title: 'Dashboard | umami',
};

20
src/app/(app)/layout.tsx Normal file
View File

@ -0,0 +1,20 @@
'use client';
import Shell from './Shell';
import NavBar from './NavBar';
import Page from 'components/layout/Page';
import styles from './layout.module.css';
export default function AppLayout({ children }) {
return (
<Shell>
<main className={styles.layout}>
<nav className={styles.nav}>
<NavBar />
</nav>
<section className={styles.body}>
<Page>{children}</Page>
</section>
</main>
</Shell>
);
}

View File

@ -1,20 +1,18 @@
'use client';
import { createContext } from 'react';
import Page from 'components/layout/Page';
import styles from './reports.module.css';
import { useReport } from 'components/hooks';
import styles from './Report.module.css';
export const ReportContext = createContext(null);
export function Report({ reportId, defaultParameters, children, ...props }) {
const report = useReport(reportId, defaultParameters);
//console.log({ report });
return (
<ReportContext.Provider value={{ ...report }}>
<Page {...props} className={styles.container}>
<div {...props} className={styles.container}>
{children}
</Page>
</div>
</ReportContext.Provider>
);
}

View File

@ -1,4 +1,4 @@
import styles from './reports.module.css';
import styles from './Report.module.css';
export function ReportBody({ children }) {
return <div className={styles.body}>{children}</div>;

View File

@ -1,11 +1,11 @@
import { useContext } from 'react';
import { useRouter } from 'next/router';
import { useRouter } from 'next/navigation';
import { Icon, LoadingButton, InlineEditField, useToasts } from 'react-basics';
import PageHeader from 'components/layout/PageHeader';
import { useMessages, useApi } from 'components/hooks';
import { ReportContext } from './Report';
import styles from './ReportHeader.module.css';
import reportStyles from './reports.module.css';
import reportStyles from './Report.module.css';
export function ReportHeader({ icon }) {
const { report, updateReport } = useContext(ReportContext);

View File

@ -1,4 +1,4 @@
import styles from './reports.module.css';
import styles from './Report.module.css';
export function ReportMenu({ children }) {
return <div className={styles.menu}>{children}</div>;

View File

@ -0,0 +1,24 @@
'use client';
import PageHeader from 'components/layout/PageHeader';
import Link from 'next/link';
import { Button, Icon, Icons, Text } from 'react-basics';
import { useMessages } from 'components/hooks';
export function ReportsHeader() {
const { formatMessage, labels } = useMessages();
return (
<PageHeader title={formatMessage(labels.reports)}>
<Link href="/reports/create">
<Button variant="primary">
<Icon>
<Icons.Plus />
</Icon>
<Text>{formatMessage(labels.createReport)}</Text>
</Button>
</Link>
</PageHeader>
);
}
export default ReportsHeader;

View File

@ -0,0 +1,37 @@
'use client';
import { useApi } from 'components/hooks';
import ReportsTable from './ReportsTable';
import useFilterQuery from 'components/hooks/useFilterQuery';
import DataTable from 'components/common/DataTable';
function useReports() {
const { get, del, useMutation } = useApi();
const { mutate } = useMutation(reportId => del(`/reports/${reportId}`));
const reports = useFilterQuery(['reports'], params => get(`/reports`, params));
const deleteReport = id => {
mutate(id, {
onSuccess: () => {
reports.refetch();
},
});
};
return { reports, deleteReport };
}
export default function ReportsList() {
const { reports, deleteReport } = useReports();
const handleDelete = async (id, callback) => {
await deleteReport(id);
await reports.refetch();
callback?.();
};
return (
<DataTable {...reports.getProps()}>
{({ data }) => <ReportsTable data={data} showDomain={true} onDelete={handleDelete} />}
</DataTable>
);
}

View File

@ -0,0 +1,26 @@
'use client';
import FunnelReport from '../funnel/FunnelReport';
import EventDataReport from '../event-data/EventDataReport';
import InsightsReport from '../insights/InsightsReport';
import RetentionReport from '../retention/RetentionReport';
import { useApi } from 'components/hooks';
const reports = {
funnel: FunnelReport,
'event-data': EventDataReport,
insights: InsightsReport,
retention: RetentionReport,
};
export default function ReportDetails({ reportId }) {
const { get, useQuery } = useApi();
const { data: report } = useQuery(['reports', reportId], () => get(`/reports/${reportId}`));
if (!report) {
return null;
}
const ReportComponent = reports[report.type];
return <ReportComponent reportId={reportId} />;
}

View File

@ -0,0 +1,14 @@
import ReportDetails from './ReportDetails';
import { Metadata } from 'next';
export default function ReportDetailsPage({ params: { id } }) {
if (!id) {
return null;
}
return <ReportDetails reportId={id} />;
}
export const metadata: Metadata = {
title: 'Reports | umami',
};

View File

@ -1,6 +1,6 @@
'use client';
import Link from 'next/link';
import { Button, Icons, Text, Icon } from 'react-basics';
import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader';
import Funnel from 'assets/funnel.svg';
import Lightbulb from 'assets/lightbulb.svg';
@ -57,7 +57,7 @@ export function ReportTemplates({ showHeader = true }) {
];
return (
<Page>
<>
{showHeader && <PageHeader title={formatMessage(labels.reports)} />}
<div className={styles.reports}>
{reports.map(({ title, description, url, icon }) => {
@ -66,7 +66,7 @@ export function ReportTemplates({ showHeader = true }) {
);
})}
</div>
</Page>
</>
);
}

View File

@ -0,0 +1,10 @@
import ReportTemplates from './ReportTemplates';
import { Metadata } from 'next';
export default function ReportsCreatePage() {
return <ReportTemplates />;
}
export const metadata: Metadata = {
title: 'Create Report | umami',
};

View File

@ -1,7 +1,7 @@
import { useContext, useRef } from 'react';
import { useApi, useMessages } from 'components/hooks';
import { Form, FormRow, FormButtons, SubmitButton, PopupTrigger, Icon, Popup } from 'react-basics';
import { ReportContext } from 'components/pages/reports/Report';
import { ReportContext } from '../Report';
import Empty from 'components/common/Empty';
import { DATA_TYPES, REPORT_PARAMETERS } from 'lib/constants';
import Icons from 'components/icons';

View File

@ -13,7 +13,7 @@ import {
} from 'react-basics';
import Icons from 'components/icons';
import UrlAddForm from './UrlAddForm';
import { ReportContext } from 'components/pages/reports/Report';
import { ReportContext } from '../Report';
import BaseParameters from '../BaseParameters';
import ParameterList from '../ParameterList';
import PopupForm from '../PopupForm';

View File

@ -1,3 +1,4 @@
'use client';
import FunnelChart from './FunnelChart';
import FunnelTable from './FunnelTable';
import FunnelParameters from './FunnelParameters';

View File

@ -0,0 +1,10 @@
import FunnelReport from './FunnelReport';
import { Metadata } from 'next';
export default function FunnelReportPage() {
return <FunnelReport reportId={null} />;
}
export const metadata: Metadata = {
title: 'Funnel Report | umami',
};

View File

@ -10,7 +10,7 @@ import {
Popup,
TooltipPopup,
} from 'react-basics';
import { ReportContext } from 'components/pages/reports/Report';
import { ReportContext } from '../Report';
import Icons from 'components/icons';
import BaseParameters from '../BaseParameters';
import ParameterList from '../ParameterList';

View File

@ -1,3 +1,4 @@
'use client';
import Report from '../Report';
import ReportHeader from '../ReportHeader';
import ReportMenu from '../ReportMenu';

View File

@ -0,0 +1,10 @@
import InsightsReport from './InsightsReport';
import { Metadata } from 'next';
export default function () {
return <InsightsReport reportId={null} />;
}
export const metadata: Metadata = {
title: 'Insights Report | umami',
};

View File

@ -0,0 +1,14 @@
import ReportsHeader from './ReportsHeader';
import ReportsList from './ReportsList';
export default function ReportsPage() {
return (
<>
<ReportsHeader />
<ReportsList />
</>
);
}
export const metadata = {
title: 'Reports | umami',
};

View File

@ -1,7 +1,7 @@
import { useContext, useRef } from 'react';
import { useMessages } from 'components/hooks';
import { Form, FormButtons, FormRow, SubmitButton } from 'react-basics';
import { ReportContext } from 'components/pages/reports/Report';
import { ReportContext } from '../Report';
import { MonthSelect } from 'components/input/MonthSelect';
import BaseParameters from '../BaseParameters';
import { parseDateRange } from 'lib/date';

View File

@ -1,3 +1,4 @@
'use client';
import RetentionTable from './RetentionTable';
import RetentionParameters from './RetentionParameters';
import Report from '../Report';

View File

@ -0,0 +1,9 @@
import RetentionReport from './RetentionReport';
export default function () {
return <RetentionReport reportId={null} />;
}
export const metadata = {
title: 'Create Report | umami',
};

View File

@ -1,6 +1,6 @@
import classNames from 'classnames';
import { Menu, Item } from 'react-basics';
import { useRouter } from 'next/router';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
import styles from './SideNav.module.css';
@ -11,13 +11,13 @@ export function SideNav({
scroll = false,
onSelect = () => {},
}) {
const { asPath } = useRouter();
const pathname = usePathname();
return (
<Menu items={items} selectedKey={selectedKey} className={styles.menu} onSelect={onSelect}>
{({ key, label, url }) => (
<Item
key={key}
className={classNames(styles.item, { [styles.selected]: asPath.startsWith(url) })}
className={classNames(styles.item, { [styles.selected]: pathname.startsWith(url) })}
>
<Link href={url} shallow={shallow} scroll={scroll}>
{label}

View File

@ -1,6 +1,11 @@
.layout {
display: grid;
grid-template-columns: max-content 1fr;
gap: 20px;
}
.menu {
display: flex;
flex-direction: column;
width: 240px;
padding-top: 40px;
padding-right: 20px;
}

View File

@ -1,13 +1,13 @@
import { Row, Column } from 'react-basics';
import { useRouter } from 'next/router';
import SideNav from './SideNav';
'use client';
import { usePathname } from 'next/navigation';
import useUser from 'components/hooks/useUser';
import useMessages from 'components/hooks/useMessages';
import styles from './SettingsLayout.module.css';
import SideNav from './SideNav';
import styles from './layout.module.css';
export function SettingsLayout({ children }) {
export default function SettingsLayout({ children }) {
const { user } = useUser();
const { pathname } = useRouter();
const pathname = usePathname();
const { formatMessage, labels } = useMessages();
const cloudMode = Boolean(process.env.cloudMode);
@ -21,17 +21,13 @@ export function SettingsLayout({ children }) {
const getKey = () => items.find(({ url }) => pathname === url)?.key;
return (
<Row>
<div className={styles.layout}>
{!cloudMode && (
<Column className={styles.menu} defaultSize={12} md={4} lg={3} xl={2}>
<div className={styles.menu}>
<SideNav items={items} shallow={true} selectedKey={getKey()} />
</Column>
</div>
)}
<Column className={styles.content} defaultSize={12} md={8} lg={9} xl={10}>
{children}
</Column>
</Row>
<div className={styles.content}>{children}</div>
</div>
);
}
export default SettingsLayout;

View File

@ -1,5 +1,5 @@
import { Button, Icon, Text, useToasts, ModalTrigger, Modal } from 'react-basics';
import PasswordEditForm from 'components/pages/settings/profile/PasswordEditForm';
import PasswordEditForm from 'app/(app)/settings/profile/PasswordEditForm';
import Icons from 'components/icons';
import useMessages from 'components/hooks/useMessages';

View File

@ -0,0 +1,11 @@
'use client';
import PageHeader from 'components/layout/PageHeader';
import { useMessages } from 'components/hooks';
export function ProfileHeader() {
const { formatMessage, labels } = useMessages();
return <PageHeader title={formatMessage(labels.profile)}></PageHeader>;
}
export default ProfileHeader;

View File

@ -1,14 +1,15 @@
'use client';
import { Form, FormRow } from 'react-basics';
import TimezoneSetting from 'components/pages/settings/profile/TimezoneSetting';
import DateRangeSetting from 'components/pages/settings/profile/DateRangeSetting';
import LanguageSetting from 'components/pages/settings/profile/LanguageSetting';
import ThemeSetting from 'components/pages/settings/profile/ThemeSetting';
import TimezoneSetting from 'app/(app)/settings/profile/TimezoneSetting';
import DateRangeSetting from 'app/(app)/settings/profile/DateRangeSetting';
import LanguageSetting from 'app/(app)/settings/profile/LanguageSetting';
import ThemeSetting from 'app/(app)/settings/profile/ThemeSetting';
import PasswordChangeButton from './PasswordChangeButton';
import useUser from 'components/hooks/useUser';
import useMessages from 'components/hooks/useMessages';
import { ROLES } from 'lib/constants';
export function ProfileDetails() {
export function ProfileSettings() {
const { user } = useUser();
const { formatMessage, labels } = useMessages();
const cloudMode = Boolean(process.env.cloudMode);
@ -58,4 +59,4 @@ export function ProfileDetails() {
);
}
export default ProfileDetails;
export default ProfileSettings;

View File

@ -0,0 +1,11 @@
import ProfileHeader from './ProfileHeader';
import ProfileSettings from './ProfileSettings';
export default function () {
return (
<>
<ProfileHeader />
<ProfileSettings />
</>
);
}

View File

@ -0,0 +1,25 @@
import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics';
import useMessages from 'components/hooks/useMessages';
import TeamDeleteForm from './TeamDeleteForm';
export function TeamDeleteButton({ teamId, teamName, onDelete }) {
const { formatMessage, labels } = useMessages();
return (
<ModalTrigger>
<Button>
<Icon>
<Icons.Trash />
</Icon>
<Text>{formatMessage(labels.delete)}</Text>
</Button>
<Modal title={formatMessage(labels.deleteTeam)}>
{close => (
<TeamDeleteForm teamId={teamId} teamName={teamName} onSave={onDelete} onClose={close} />
)}
</Modal>
</ModalTrigger>
);
}
export default TeamDeleteButton;

View File

@ -0,0 +1,35 @@
import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics';
import useMessages from 'components/hooks/useMessages';
import useLocale from 'components/hooks/useLocale';
import useUser from 'components/hooks/useUser';
import TeamDeleteForm from './TeamLeaveForm';
export function TeamLeaveButton({ teamId, teamName, onLeave }) {
const { formatMessage, labels } = useMessages();
const { dir } = useLocale();
const { user } = useUser();
return (
<ModalTrigger>
<Button>
<Icon rotate={dir === 'rtl' ? 180 : 0}>
<Icons.ArrowRight />
</Icon>
<Text>{formatMessage(labels.leave)}</Text>
</Button>
<Modal title={formatMessage(labels.leaveTeam)}>
{close => (
<TeamDeleteForm
teamId={teamId}
userId={user.id}
teamName={teamName}
onSave={onLeave}
onClose={close}
/>
)}
</Modal>
</ModalTrigger>
);
}
export default TeamLeaveButton;

View File

@ -0,0 +1,24 @@
import { Button, Icon, Modal, ModalTrigger, Text } from 'react-basics';
import Icons from 'components/icons';
import useMessages from 'components/hooks/useMessages';
import TeamAddForm from './TeamAddForm';
export function TeamsAddButton({ onAdd }) {
const { formatMessage, labels } = useMessages();
return (
<ModalTrigger>
<Button variant="primary">
<Icon>
<Icons.Plus />
</Icon>
<Text>{formatMessage(labels.createTeam)}</Text>
</Button>
<Modal title={formatMessage(labels.createTeam)}>
{close => <TeamAddForm onSave={onAdd} onClose={close} />}
</Modal>
</ModalTrigger>
);
}
export default TeamsAddButton;

View File

@ -0,0 +1,24 @@
'use client';
import { Flexbox } from 'react-basics';
import PageHeader from 'components/layout/PageHeader';
import { ROLES } from 'lib/constants';
import useUser from 'components/hooks/useUser';
import useMessages from 'components/hooks/useMessages';
import TeamsJoinButton from './TeamsJoinButton';
import TeamsAddButton from './TeamsAddButton';
export function TeamsHeader() {
const { formatMessage, labels } = useMessages();
const { user } = useUser();
return (
<PageHeader title={formatMessage(labels.teams)}>
<Flexbox gap={10}>
<TeamsJoinButton />
{user.role !== ROLES.viewOnly && <TeamsAddButton />}
</Flexbox>
</PageHeader>
);
}
export default TeamsHeader;

View File

@ -0,0 +1,29 @@
import { Button, Icon, Modal, ModalTrigger, Text, useToasts } from 'react-basics';
import Icons from 'components/icons';
import useMessages from 'components/hooks/useMessages';
import TeamJoinForm from './TeamJoinForm';
export function TeamsJoinButton() {
const { formatMessage, labels, messages } = useMessages();
const { showToast } = useToasts();
const handleJoin = () => {
showToast({ message: formatMessage(messages.saved), variant: 'success' });
};
return (
<ModalTrigger>
<Button variant="secondary">
<Icon>
<Icons.AddUser />
</Icon>
<Text>{formatMessage(labels.joinTeam)}</Text>
</Button>
<Modal title={formatMessage(labels.joinTeam)}>
{close => <TeamJoinForm onSave={handleJoin} onClose={close} />}
</Modal>
</ModalTrigger>
);
}
export default TeamsJoinButton;

View File

@ -0,0 +1,19 @@
'use client';
import DataTable from 'components/common/DataTable';
import TeamsTable from 'app/(app)/settings/teams/TeamsTable';
import useApi from 'components/hooks/useApi';
import useFilterQuery from 'components/hooks/useFilterQuery';
export function TeamsList() {
const { get } = useApi();
const filterQuery = useFilterQuery(['teams'], params => {
return get(`/teams`, {
...params,
});
});
const { getProps } = filterQuery;
return <DataTable {...getProps()}>{({ data }) => <TeamsTable data={data} />}</DataTable>;
}
export default TeamsList;

View File

@ -0,0 +1,46 @@
'use client';
import useMessages from 'components/hooks/useMessages';
import useUser from 'components/hooks/useUser';
import { ROLES } from 'lib/constants';
import Link from 'next/link';
import { Button, Flexbox, GridColumn, GridTable, Icon, Icons, Text } from 'react-basics';
import TeamDeleteButton from './TeamDeleteButton';
import TeamLeaveButton from './TeamLeaveButton';
export function TeamsTable({ data = [] }) {
const { formatMessage, labels } = useMessages();
const { user } = useUser();
return (
<GridTable data={data}>
<GridColumn name="name" label={formatMessage(labels.name)} />
<GridColumn name="owner" label={formatMessage(labels.owner)}>
{row => row.teamUser.find(({ role }) => role === ROLES.teamOwner)?.user?.username}
</GridColumn>
<GridColumn name="action" label=" " alignment="end">
{row => {
const { id, name, teamUser } = row;
const owner = teamUser.find(({ role }) => role === ROLES.teamOwner);
const showDelete = user.id === owner?.userId;
return (
<Flexbox gap={10}>
<Link href={`/settings/teams/${id}`}>
<Button>
<Icon>
<Icons.Edit />
</Icon>
<Text>{formatMessage(labels.view)}</Text>
</Button>
</Link>
{showDelete && <TeamDeleteButton teamId={id} teamName={name} />}
{!showDelete && <TeamLeaveButton teamId={id} teamName={name} />}
</Flexbox>
);
}}
</GridColumn>
</GridTable>
);
}
export default TeamsTable;

View File

@ -1,7 +1,7 @@
import useApi from 'components/hooks/useApi';
import { useRef, useState } from 'react';
import { Button, Dropdown, Form, FormButtons, FormRow, Item, SubmitButton } from 'react-basics';
import WebsiteTags from './WebsiteTags';
import WebsiteTags from '../WebsiteTags';
import useMessages from 'components/hooks/useMessages';
export function TeamAddWebsiteForm({ teamId, onSave, onClose }) {

View File

@ -1,8 +1,8 @@
import { Loading, useToasts } from 'react-basics';
import TeamMembersTable from 'components/pages/settings/teams/TeamMembersTable';
import useApi from 'components/hooks/useApi';
import useMessages from 'components/hooks/useMessages';
import useApiFilter from 'components/hooks/useApiFilter';
import TeamMembersTable from './TeamMembersTable';
export function TeamMembers({ teamId, readOnly }) {
const { showToast } = useToasts();

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