mirror of
https://github.com/umami-software/umami.git
synced 2026-02-05 13:17:19 +01:00
Converted user and website settings.
This commit is contained in:
parent
4c24e54fdd
commit
b5c6194f36
59 changed files with 363 additions and 554 deletions
|
|
@ -1,11 +1,11 @@
|
|||
import { ReactNode } from 'react';
|
||||
import { Button, LoadingButton, Form, FormButtons } from 'react-basics';
|
||||
import { Row, Button, FormSubmitButton, Form, FormButtons } from '@umami/react-zen';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
|
||||
export interface ConfirmationFormProps {
|
||||
message: ReactNode;
|
||||
buttonLabel?: ReactNode;
|
||||
buttonVariant?: 'none' | 'primary' | 'secondary' | 'quiet' | 'danger';
|
||||
buttonVariant?: 'primary' | 'quiet' | 'danger';
|
||||
isLoading?: boolean;
|
||||
error?: string | Error;
|
||||
onConfirm?: () => void;
|
||||
|
|
@ -24,13 +24,13 @@ export function ConfirmationForm({
|
|||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<Form error={error}>
|
||||
<p>{message}</p>
|
||||
<FormButtons flex>
|
||||
<LoadingButton isLoading={isLoading} onClick={onConfirm} variant={buttonVariant}>
|
||||
<Form onSubmit={onConfirm} error={error}>
|
||||
<Row marginY="4">{message}</Row>
|
||||
<FormButtons>
|
||||
<Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||
<FormSubmitButton isLoading={isLoading} variant={buttonVariant}>
|
||||
{buttonLabel || formatMessage(labels.ok)}
|
||||
</LoadingButton>
|
||||
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||
</FormSubmitButton>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import { ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Loading, SearchField } from 'react-basics';
|
||||
import { Loading, SearchField, Row, Column } from '@umami/react-zen';
|
||||
import { useMessages, useNavigation } from '@/components/hooks';
|
||||
import { Empty } from '@/components/common/Empty';
|
||||
import { Pager } from '@/components/common/Pager';
|
||||
import { PagedQueryResult } from '@/lib/types';
|
||||
import styles from './DataTable.module.css';
|
||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||
import { PagedQueryResult } from '@/lib/types';
|
||||
|
||||
const DEFAULT_SEARCH_DELAY = 600;
|
||||
|
||||
|
|
@ -20,7 +18,7 @@ export interface DataTableProps {
|
|||
children: ReactNode | ((data: any) => ReactNode);
|
||||
}
|
||||
|
||||
export function DataTable({
|
||||
export function DataGrid({
|
||||
queryResult,
|
||||
searchDelay = 600,
|
||||
allowSearch = true,
|
||||
|
|
@ -30,12 +28,8 @@ export function DataTable({
|
|||
children,
|
||||
}: DataTableProps) {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const {
|
||||
result,
|
||||
params,
|
||||
setParams,
|
||||
query: { error, isLoading, isFetched },
|
||||
} = queryResult || {};
|
||||
const { result, params, setParams, query } = queryResult || {};
|
||||
const { error, isLoading, isFetched } = query || {};
|
||||
const { page, pageSize, count, data } = result || {};
|
||||
const { search } = params || {};
|
||||
const hasData = Boolean(!isLoading && data?.length);
|
||||
|
|
@ -43,45 +37,38 @@ export function DataTable({
|
|||
const { router, renderUrl } = useNavigation();
|
||||
|
||||
const handleSearch = (search: string) => {
|
||||
setParams({ ...params, search, page: params.page ? page : 1 });
|
||||
setParams({ ...params, search });
|
||||
};
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setParams({ ...params, search, page });
|
||||
setParams({ ...params, page });
|
||||
router.push(renderUrl({ page }));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{allowSearch && (hasData || search) && (
|
||||
<SearchField
|
||||
className={styles.search}
|
||||
value={search}
|
||||
onSearch={handleSearch}
|
||||
delay={searchDelay || DEFAULT_SEARCH_DELAY}
|
||||
autoFocus={autoFocus}
|
||||
placeholder={formatMessage(labels.search)}
|
||||
/>
|
||||
<Row width="280px" alignItems="center" marginBottom="6">
|
||||
<SearchField
|
||||
value={search}
|
||||
onSearch={handleSearch}
|
||||
delay={searchDelay || DEFAULT_SEARCH_DELAY}
|
||||
autoFocus={autoFocus}
|
||||
placeholder={formatMessage(labels.search)}
|
||||
/>
|
||||
</Row>
|
||||
)}
|
||||
<LoadingPanel data={data} isLoading={isLoading} isFetched={isFetched} error={error}>
|
||||
<div
|
||||
className={classNames(styles.body, {
|
||||
[styles.status]: isLoading || noResults || !hasData,
|
||||
})}
|
||||
>
|
||||
<Column>
|
||||
{hasData ? (typeof children === 'function' ? children(result) : children) : null}
|
||||
{isLoading && <Loading position="page" />}
|
||||
{!isLoading && !hasData && !search && (renderEmpty ? renderEmpty() : <Empty />)}
|
||||
{!isLoading && noResults && <Empty message={formatMessage(messages.noResultsFound)} />}
|
||||
</div>
|
||||
</Column>
|
||||
{allowPaging && hasData && (
|
||||
<Pager
|
||||
className={styles.pager}
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
count={count}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
<Row marginTop="6">
|
||||
<Pager page={page} pageSize={pageSize} count={count} onPageChange={handlePageChange} />
|
||||
</Row>
|
||||
)}
|
||||
</LoadingPanel>
|
||||
</>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Loading } from 'react-basics';
|
||||
import { Loading } from '@umami/react-zen';
|
||||
import { ErrorMessage } from '@/components/common/ErrorMessage';
|
||||
import { Empty } from '@/components/common/Empty';
|
||||
import styles from './LoadingPanel.module.css';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import classNames from 'classnames';
|
||||
import { Button, Icon, Icons } from 'react-basics';
|
||||
import { Button, Icon, Icons, Row, Text } from '@umami/react-zen';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import styles from './Pager.module.css';
|
||||
|
||||
export interface PagerProps {
|
||||
page: string | number;
|
||||
|
|
@ -11,7 +9,7 @@ export interface PagerProps {
|
|||
className?: string;
|
||||
}
|
||||
|
||||
export function Pager({ page, pageSize, count, onPageChange, className }: PagerProps) {
|
||||
export function Pager({ page, pageSize, count, onPageChange }: PagerProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const maxPage = pageSize && count ? Math.ceil(+count / +pageSize) : 0;
|
||||
const lastPage = page === maxPage;
|
||||
|
|
@ -34,24 +32,21 @@ export function Pager({ page, pageSize, count, onPageChange, className }: PagerP
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.pager, className)}>
|
||||
<div className={styles.count}>{formatMessage(labels.numberOfRecords, { x: count })}</div>
|
||||
<div className={styles.nav}>
|
||||
<Button onClick={() => handlePageChange(-1)} disabled={firstPage}>
|
||||
<Icon rotate={90}>
|
||||
<Icons.ChevronDown />
|
||||
<Row alignItems="center" justifyContent="space-between" gap="3" flexGrow={1}>
|
||||
<Text>{formatMessage(labels.numberOfRecords, { x: count })}</Text>
|
||||
<Row alignItems="center" justifyContent="flex-end" gap="3">
|
||||
<Text>{formatMessage(labels.pageOf, { current: page, total: maxPage })}</Text>
|
||||
<Button onPress={() => handlePageChange(-1)} isDisabled={firstPage}>
|
||||
<Icon size="sm" rotate={180}>
|
||||
<Icons.Chevron />
|
||||
</Icon>
|
||||
</Button>
|
||||
<div className={styles.text}>
|
||||
{formatMessage(labels.pageOf, { current: page, total: maxPage })}
|
||||
</div>
|
||||
<Button onClick={() => handlePageChange(1)} disabled={lastPage}>
|
||||
<Icon rotate={270}>
|
||||
<Icons.ChevronDown />
|
||||
<Button onPress={() => handlePageChange(1)} isDisabled={lastPage}>
|
||||
<Icon size="sm">
|
||||
<Icons.Chevron />
|
||||
</Icon>
|
||||
</Button>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
</Row>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { useApp } from '@/store/app';
|
|||
const selector = (state: { shareToken: { token?: string } }) => state.shareToken;
|
||||
|
||||
async function handleResponse(res: FetchResponse): Promise<any> {
|
||||
if (!res.ok) {
|
||||
if (res.error) {
|
||||
const { message, code } = res?.error?.error || {};
|
||||
return Promise.reject(new Error(code || message || 'Unexpectd error.'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
}
|
||||
|
||||
.selected {
|
||||
color: var(--font-color);
|
||||
font-weight: 700;
|
||||
background: var(--blue100);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,18 @@
|
|||
import { ReactNode } from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { SideNav } from '@/components/layout/SideNav';
|
||||
import styles from './MenuLayout.module.css';
|
||||
import { Grid, Column } from '@umami/react-zen';
|
||||
import { MenuNav } from '@/components/layout/MenuNav';
|
||||
|
||||
export function MenuLayout({ items = [], children }: { items: any[]; children: ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const cloudMode = !!process.env.cloudMode;
|
||||
|
||||
const getKey = () => items.find(({ url }) => pathname === url)?.key;
|
||||
|
||||
return (
|
||||
<div className={styles.layout}>
|
||||
<Grid columns="auto 1fr" gap="5">
|
||||
{!cloudMode && (
|
||||
<div className={styles.menu}>
|
||||
<SideNav items={items} shallow={true} selectedKey={getKey()} />
|
||||
</div>
|
||||
<Column width="240px">
|
||||
<MenuNav items={items} shallow={true} />
|
||||
</Column>
|
||||
)}
|
||||
<div className={styles.content}>{children}</div>
|
||||
</div>
|
||||
<Column>{children}</Column>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
29
src/components/layout/MenuNav.tsx
Normal file
29
src/components/layout/MenuNav.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { List, ListItem, Text } from '@umami/react-zen';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
|
||||
export interface SideNavProps {
|
||||
items: any[];
|
||||
shallow?: boolean;
|
||||
scroll?: boolean;
|
||||
}
|
||||
|
||||
export function MenuNav({ items, shallow = true, scroll = false }: SideNavProps) {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<List>
|
||||
{items.map(({ key, label, url }) => {
|
||||
const isSelected = pathname.startsWith(url);
|
||||
|
||||
return (
|
||||
<ListItem key={key}>
|
||||
<Link href={url} shallow={shallow} scroll={scroll}>
|
||||
<Text weight={isSelected ? 'bold' : 'regular'}>{label}</Text>
|
||||
</Link>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
.group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: var(--base600);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 10px 20px;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.expanded .body {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border-inline-end: 2px solid var(--base200);
|
||||
padding: 1rem 2rem;
|
||||
gap: var(--size500);
|
||||
font-weight: 600;
|
||||
width: 200px;
|
||||
margin-inline-end: -2px;
|
||||
}
|
||||
|
||||
a.item {
|
||||
color: var(--base700);
|
||||
}
|
||||
|
||||
.item.selected {
|
||||
color: var(--base900);
|
||||
border-inline-end-color: var(--primary400);
|
||||
background: var(--blue100);
|
||||
}
|
||||
|
||||
.item:hover {
|
||||
color: var(--base900);
|
||||
}
|
||||
|
||||
.minimized .text,
|
||||
.minimized .header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.minimized .item {
|
||||
width: 60px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.divider:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
border-top: 1px solid var(--base300);
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.minimized .divider:before {
|
||||
width: 60px;
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
import { useState } from 'react';
|
||||
import { Icon, Text, TooltipPopup } from 'react-basics';
|
||||
import classNames from 'classnames';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { Icons } from '@/components/icons';
|
||||
import styles from './NavGroup.module.css';
|
||||
|
||||
export interface NavGroupProps {
|
||||
title: string;
|
||||
items: any[];
|
||||
defaultExpanded?: boolean;
|
||||
allowExpand?: boolean;
|
||||
minimized?: boolean;
|
||||
}
|
||||
|
||||
export function NavGroup({
|
||||
title,
|
||||
items,
|
||||
defaultExpanded = true,
|
||||
allowExpand = true,
|
||||
minimized = false,
|
||||
}: NavGroupProps) {
|
||||
const pathname = usePathname();
|
||||
const [expanded, setExpanded] = useState(defaultExpanded);
|
||||
|
||||
const handleExpand = () => setExpanded(state => !state);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.group, {
|
||||
[styles.expanded]: expanded,
|
||||
[styles.minimized]: minimized,
|
||||
})}
|
||||
>
|
||||
{title && (
|
||||
<div className={styles.header} onClick={allowExpand ? handleExpand : undefined}>
|
||||
<Text>{title}</Text>
|
||||
<Icon size="sm" rotate={expanded ? 0 : -90}>
|
||||
<Icons.ChevronDown />
|
||||
</Icon>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.body}>
|
||||
{items.map(({ label, url, icon, divider }) => {
|
||||
return (
|
||||
<TooltipPopup key={label} label={label} position="right" disabled={!minimized}>
|
||||
<Link
|
||||
href={url}
|
||||
className={classNames(styles.item, {
|
||||
[styles.divider]: divider,
|
||||
[styles.selected]: pathname.startsWith(url),
|
||||
})}
|
||||
>
|
||||
<Icon>{icon}</Icon>
|
||||
<Text className={styles.text}>{label}</Text>
|
||||
</Link>
|
||||
</TooltipPopup>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,29 +1,23 @@
|
|||
import { ReactNode } from 'react';
|
||||
import { Heading, Icon, Breadcrumbs, Breadcrumb, Row } from '@umami/react-zen';
|
||||
import { Heading, Icon, Row } from '@umami/react-zen';
|
||||
|
||||
export function PageHeader({
|
||||
title,
|
||||
icon,
|
||||
breadcrumb,
|
||||
children,
|
||||
}: {
|
||||
title?: ReactNode;
|
||||
icon?: ReactNode;
|
||||
className?: string;
|
||||
breadcrumb?: ReactNode;
|
||||
children?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumb>{breadcrumb}</Breadcrumb>
|
||||
</Breadcrumbs>
|
||||
<Row justifyContent="space-between" paddingY="6">
|
||||
<Row justifyContent="space-between" alignItems="center" paddingBottom="6">
|
||||
<Row gap="3">
|
||||
{icon && <Icon size="lg">{icon}</Icon>}
|
||||
|
||||
{title && <Heading>{title}</Heading>}
|
||||
<Row justifyContent="flex-end">{children}</Row>
|
||||
</Row>
|
||||
</>
|
||||
<Row justifyContent="flex-end">{children}</Row>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
import classNames from 'classnames';
|
||||
import { Menu, Item } from 'react-basics';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import styles from './SideNav.module.css';
|
||||
|
||||
export interface SideNavProps {
|
||||
selectedKey: string;
|
||||
items: any[];
|
||||
shallow?: boolean;
|
||||
scroll?: boolean;
|
||||
className?: string;
|
||||
onSelect?: () => void;
|
||||
}
|
||||
|
||||
export function SideNav({
|
||||
selectedKey,
|
||||
items,
|
||||
shallow = true,
|
||||
scroll = false,
|
||||
className,
|
||||
onSelect = () => {},
|
||||
}: SideNavProps) {
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
<Menu
|
||||
items={items}
|
||||
selectedKey={selectedKey}
|
||||
className={classNames(styles.menu, className)}
|
||||
onSelect={onSelect}
|
||||
>
|
||||
{({ key, label, url }) => (
|
||||
<Item
|
||||
key={key}
|
||||
className={classNames(styles.item, { [styles.selected]: pathname.startsWith(url) })}
|
||||
>
|
||||
<Link href={url} shallow={shallow} scroll={scroll}>
|
||||
{label}
|
||||
</Link>
|
||||
</Item>
|
||||
)}
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue