Admin section updates.

This commit is contained in:
Mike Cao 2025-07-10 20:04:17 -07:00
parent 87449ece9e
commit 1b81074752
20 changed files with 274 additions and 647 deletions

File diff suppressed because it is too large Load diff

View file

@ -19,22 +19,22 @@ export function SideNav(props: any) {
const links = [
{
label: formatMessage(labels.websites),
href: '/websites',
href: renderUrl('/websites', false),
icon: <Globe />,
},
{
label: formatMessage(labels.boards),
href: '/boards',
href: renderUrl('/boards', false),
icon: <LayoutDashboard />,
},
{
label: formatMessage(labels.links),
href: '/links',
href: renderUrl('/links', false),
icon: <LinkIcon />,
},
{
label: formatMessage(labels.pixels),
href: '/pixels',
href: renderUrl('/pixels', false),
icon: <Grid2X2 />,
},
{
@ -57,7 +57,7 @@ export function SideNav(props: any) {
<SidebarSection>
{links.map(({ href, label, icon }) => {
return (
<Link key={href} href={renderUrl(href, false)} role="button">
<Link key={href} href={href} role="button">
<SidebarItem label={label} icon={icon} isSelected={pathname.startsWith(href)} />
</Link>
);

View file

@ -1,10 +1,11 @@
'use client';
import { TeamDetails } from '@/app/(main)/teams/[teamId]/settings/team/TeamDetails';
import { TeamProvider } from '@/app/(main)/teams/[teamId]/TeamProvider';
export function AdminTeamPage({ teamId }: { teamId: string }) {
return (
<>
<TeamProvider teamId={teamId}>
<TeamDetails teamId={teamId} />
</>
</TeamProvider>
);
}

View file

@ -1,15 +1,10 @@
import { AdminTeamPage } from './AdminTeamPage';
import { TeamProvider } from '@/app/(main)/teams/[teamId]/TeamProvider';
import { Metadata } from 'next';
export default async function ({ params }: { params: Promise<{ teamId: string }> }) {
const { teamId } = await params;
return (
<TeamProvider teamId={teamId}>
<AdminTeamPage teamId={teamId} />
</TeamProvider>
);
return <AdminTeamPage teamId={teamId} />;
}
export const metadata: Metadata = {

View file

@ -8,5 +8,5 @@ export default async function ({ params }: { params: Promise<{ userId: string }>
}
export const metadata: Metadata = {
title: 'Users',
title: 'User',
};

View file

@ -0,0 +1,11 @@
'use client';
import { WebsiteSettings } from '@/app/(main)/settings/websites/[websiteId]/WebsiteSettings';
import { WebsiteProvider } from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
export function AdminWebsitePage({ websiteId }: { websiteId: string }) {
return (
<WebsiteProvider websiteId={websiteId}>
<WebsiteSettings websiteId={websiteId} />
</WebsiteProvider>
);
}

View file

@ -0,0 +1,12 @@
import { AdminWebsitePage } from './AdminWebsitePage';
import { Metadata } from 'next';
export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
const { websiteId } = await params;
return <AdminWebsitePage websiteId={websiteId} />;
}
export const metadata: Metadata = {
title: 'Website',
};

View file

@ -1,11 +0,0 @@
.filters {
display: flex;
justify-content: flex-start;
align-items: flex-start;
}
.tag {
text-align: center;
margin-bottom: 10px;
margin-inline-end: 20px;
}

View file

@ -1,38 +0,0 @@
import { Button, Icon, Text } from '@umami/react-zen';
import { Close } from '@/components/icons';
import styles from './WebsiteTags.module.css';
export function WebsiteTags({
items = [],
websites = [],
onClick,
}: {
items: any[];
websites: any[];
onClick: (e: Event) => void;
}) {
if (websites.length === 0) {
return null;
}
return (
<div className={styles.filters}>
{websites.map(websiteId => {
const website = items.find(a => a.id === websiteId);
return (
<div key={websiteId} className={styles.tag}>
<Button onPress={() => onClick(websiteId)} variant="primary" size="sm">
<Text>
<b>{`${website.name}`}</b>
</Text>
<Icon>
<Close />
</Icon>
</Button>
</div>
);
})}
</div>
);
}

View file

@ -19,19 +19,21 @@ export function WebsiteData({ websiteId, onSave }: { websiteId: string; onSave?:
const { touch } = useModified();
const { teamId, renderUrl } = useNavigation();
const router = useRouter();
const { data } = useUserTeamsQuery(user.id);
const { data: teams } = useUserTeamsQuery(user.id);
const canTransferWebsite =
(
!teamId &&
data.filter(({ teamUser }) =>
teamUser.find(
({ role, userId }) =>
[ROLES.teamOwner, ROLES.teamManager].includes(role) && userId === user.id,
),
)
(!teamId &&
teams?.data?.filter(({ teamUser }) =>
teamUser.find(
({ role, userId }) =>
[ROLES.teamOwner, ROLES.teamManager].includes(role) && userId === user.id,
),
)) ||
[]
).length > 0 ||
(teamId &&
!!data
!!teams?.data
?.find(({ id }) => id === teamId)
?.teamUser.find(({ role, userId }) => role === ROLES.teamOwner && userId === user.id));

View file

@ -6,17 +6,13 @@ import {
Switch,
FormSubmitButton,
Column,
Icon,
Grid,
Label,
useToast,
TooltipTrigger,
Tooltip,
Row,
} from '@umami/react-zen';
import { useState } from 'react';
import { getRandomChars } from '@/lib/crypto';
import { useApi, useMessages, useModified } from '@/components/hooks';
import { Refresh } from '@/components/icons';
const generateId = () => getRandomChars(16);
@ -70,24 +66,19 @@ export function WebsiteShareForm({ websiteId, shareId, onSave, onClose }: Websit
{id && (
<Column>
<Label>{formatMessage(labels.shareUrl)}</Label>
<Grid columns="1fr auto" gap>
<TextField value={url} isReadOnly allowCopy />
<TooltipTrigger>
<Button onPress={handleGenerate} variant="quiet" size="sm">
<Icon>
<Refresh />
</Icon>
</Button>
<Tooltip>{formatMessage(labels.regenerate)}</Tooltip>
</TooltipTrigger>
</Grid>
<TextField value={url} isReadOnly allowCopy />
</Column>
)}
<FormButtons>
<Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>
<FormSubmitButton isDisabled={false} isLoading={isPending}>
{formatMessage(labels.save)}
</FormSubmitButton>
<FormButtons justifyContent="space-between">
<Row>
{id && <Button onPress={handleGenerate}>{formatMessage(labels.regenerate)}</Button>}
</Row>
<Row>
{onClose && <Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>}
<FormSubmitButton isDisabled={false} isLoading={isPending}>
{formatMessage(labels.save)}
</FormSubmitButton>
</Row>
</FormButtons>
</Column>
</Form>

View file

@ -31,15 +31,16 @@ export function WebsiteTransferForm({
const { mutate, error } = useMutation({
mutationFn: (data: any) => post(`/websites/${websiteId}/transfer`, data),
});
const { result, query } = useUserTeamsQuery(user.id);
const { data: teams, isLoading } = useUserTeamsQuery(user.id);
const isTeamWebsite = !!website?.teamId;
const items = result.data.filter(({ teamUser }) =>
teamUser.find(
({ role, userId }) =>
[ROLES.teamOwner, ROLES.teamManager].includes(role) && userId === user.id,
),
);
const items =
teams?.data?.filter(({ teamUser }) =>
teamUser.find(
({ role, userId }) =>
[ROLES.teamOwner, ROLES.teamManager].includes(role) && userId === user.id,
),
) || [];
const handleSubmit = async () => {
mutate(
@ -60,7 +61,7 @@ export function WebsiteTransferForm({
setTeamId(key as string);
};
if (query.isLoading) {
if (isLoading) {
return <Loading icon="dots" position="center" />;
}

View file

@ -1,12 +1,15 @@
import { useContext, useState } from 'react';
import { Column, Tabs, TabList, Tab, TabPanel } from '@umami/react-zen';
import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider';
import { useLoginQuery, useMessages } from '@/components/hooks';
import { SectionHeader } from '@/components/common/SectionHeader';
import { ROLES } from '@/lib/constants';
import { useContext, useState } from 'react';
import { Column, Tabs, TabList, Tab, TabPanel } from '@umami/react-zen';
import { Users } from '@/components/icons';
import { TeamLeaveButton } from '@/app/(main)/settings/teams/TeamLeaveButton';
import { TeamManage } from './TeamManage';
import { TeamEditForm } from './TeamEditForm';
import { TeamWebsitesDataTable } from '@/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesDataTable';
import { TeamMembersDataTable } from '@/app/(main)/teams/[teamId]/settings/members/TeamMembersDataTable';
export function TeamDetails({ teamId }: { teamId: string }) {
const team = useContext(TeamContext);
@ -26,17 +29,25 @@ export function TeamDetails({ teamId }: { teamId: string }) {
return (
<Column gap>
<SectionHeader title={team?.name}>
<SectionHeader title={team?.name} icon={<Users />}>
{!isTeamOwner && <TeamLeaveButton teamId={team.id} teamName={team.name} />}
</SectionHeader>
<Tabs selectedKey={tab} onSelectionChange={(value: any) => setTab(value)}>
<TabList>
<Tab id="details">{formatMessage(labels.details)}</Tab>
<Tab id="members">{formatMessage(labels.members)}</Tab>
<Tab id="websites">{formatMessage(labels.websites)}</Tab>
{isTeamOwner && <Tab id="manage">{formatMessage(labels.manage)}</Tab>}
</TabList>
<TabPanel id="details">
<TeamEditForm teamId={teamId} allowEdit={canEdit} />
</TabPanel>
<TabPanel id="members">
<TeamMembersDataTable teamId={teamId} allowEdit />
</TabPanel>
<TabPanel id="websites">
<TeamWebsitesDataTable teamId={teamId} allowEdit />
</TabPanel>
<TabPanel id="manage">
<TeamManage teamId={teamId} />
</TabPanel>

View file

@ -6,6 +6,7 @@ import {
TextField,
Button,
useToast,
Text,
} from '@umami/react-zen';
import { getRandomChars } from '@/lib/crypto';
import { useContext } from 'react';
@ -47,8 +48,7 @@ export function TeamEditForm({ teamId, allowEdit }: { teamId: string; allowEdit?
label={formatMessage(labels.name)}
rules={{ required: formatMessage(labels.required) }}
>
{allowEdit && <TextField />}
{!allowEdit && team?.name}
{allowEdit ? <TextField /> : <Text>{team?.name}</Text>}
</FormField>
{!cloudMode && allowEdit && (
<FormField name="accessCode" label={formatMessage(labels.accessCode)}>

View file

@ -32,6 +32,9 @@ export async function GET(request: Request) {
},
},
},
omit: {
password: true,
},
orderBy: {
createdAt: 'desc',
},

View file

@ -18,7 +18,7 @@ export function SectionHeader({
return (
<Row {...props} justifyContent="space-between" alignItems="center" height="60px">
<Row gap="3" alignItems="center">
{icon && <Icon>{icon}</Icon>}
{icon && <Icon size="md">{icon}</Icon>}
{title && <Heading size="3">{title}</Heading>}
{description && <Text color="muted">{description}</Text>}
</Row>

View file

@ -1,6 +1,6 @@
import { useApi } from '../useApi';
import { useModified } from '../useModified';
import { usePagedQuery } from '@/components/hooks';
import { usePagedQuery } from '../usePagedQuery';
import { ReactQueryOptions } from '@/lib/types';
export function useTeamsQuery(params?: Record<string, any>, options?: ReactQueryOptions) {
@ -8,7 +8,7 @@ export function useTeamsQuery(params?: Record<string, any>, options?: ReactQuery
const { modified } = useModified(`teams`);
return usePagedQuery({
queryKey: ['websites', { modified, ...params }],
queryKey: ['teams:admin', { modified, ...params }],
queryFn: pageParams => {
return get(`/admin/teams`, {
...params,

View file

@ -7,7 +7,7 @@ export function useUsersQuery() {
const { modified } = useModified(`users`);
return usePagedQuery({
queryKey: ['users', { modified }],
queryKey: ['users:admin', { modified }],
queryFn: (pageParams: any) => {
return get('/admin/users', {
...pageParams,

View file

@ -8,7 +8,7 @@ export function useWebsitesQuery(params?: Record<string, any>, options?: ReactQu
const { modified } = useModified(`websites`);
return usePagedQuery({
queryKey: ['websites', { modified, ...params }],
queryKey: ['websites:admin', { modified, ...params }],
queryFn: pageParams => {
return get(`/admin/websites`, {
...pageParams,

View file

@ -24,7 +24,6 @@ export * from '@/app/(main)/settings/teams/TeamsDataTable';
export * from '@/app/(main)/settings/teams/TeamsHeader';
export * from '@/app/(main)/settings/teams/TeamsJoinButton';
export * from '@/app/(main)/settings/teams/TeamsTable';
export * from '@/app/(main)/settings/teams/WebsiteTags';
export * from '@/app/(main)/settings/websites/[websiteId]/WebsiteShareForm';
export * from '@/app/(main)/settings/websites/[websiteId]/WebsiteTrackingCode';