Updated website, team and user save.

This commit is contained in:
Mike Cao 2024-01-29 03:15:22 -08:00
parent 2fa50892d8
commit fec81695e8
15 changed files with 128 additions and 117 deletions

View File

@ -4,6 +4,7 @@ import { Item, Loading, Tabs, Flexbox } from 'react-basics';
import TeamsContext from 'app/(main)/teams/TeamsContext'; import TeamsContext from 'app/(main)/teams/TeamsContext';
import PageHeader from 'components/layout/PageHeader'; import PageHeader from 'components/layout/PageHeader';
import { ROLES } from 'lib/constants'; import { ROLES } from 'lib/constants';
import Icons from 'components/icons';
import { useLogin, useTeam, useMessages } from 'components/hooks'; import { useLogin, useTeam, useMessages } from 'components/hooks';
import TeamEditForm from './TeamEditForm'; import TeamEditForm from './TeamEditForm';
import TeamMembers from './TeamMembers'; import TeamMembers from './TeamMembers';
@ -27,7 +28,7 @@ export function TeamSettings({ teamId }: { teamId: string }) {
return ( return (
<TeamsContext.Provider value={team}> <TeamsContext.Provider value={team}>
<Flexbox direction="column"> <Flexbox direction="column">
<PageHeader title={team?.name} /> <PageHeader title={team?.name} icon={<Icons.Users />} />
<Tabs <Tabs
selectedKey={tab} selectedKey={tab}
onSelect={(value: any) => setTab(value)} onSelect={(value: any) => setTab(value)}

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import DataTable from 'components/common/DataTable'; import DataTable from 'components/common/DataTable';
import { useUsers } from 'components/hooks';
import UsersTable from './UsersTable'; import UsersTable from './UsersTable';
import useUsers from 'components/hooks/queries/useUsers';
export function UsersDataTable({ showActions }: { showActions?: boolean }) { export function UsersDataTable({ showActions }: { showActions?: boolean }) {
const queryResult = useUsers(); const queryResult = useUsers();

View File

@ -1,6 +1,7 @@
'use client'; 'use client';
import { Key, useState } from 'react'; import { Key, useState } from 'react';
import { Item, Loading, Tabs } from 'react-basics'; import { Item, Loading, Tabs } from 'react-basics';
import Icons from 'components/icons';
import UserEditForm from '../UserEditForm'; import UserEditForm from '../UserEditForm';
import PageHeader from 'components/layout/PageHeader'; import PageHeader from 'components/layout/PageHeader';
import { useMessages, useUser } from 'components/hooks'; import { useMessages, useUser } from 'components/hooks';
@ -17,7 +18,7 @@ export function UserSettings({ userId }: { userId: string }) {
return ( return (
<> <>
<PageHeader title={user?.username} /> <PageHeader title={user?.username} icon={<Icons.User />} />
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30, fontSize: 14 }}> <Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30, fontSize: 14 }}>
<Item key="details">{formatMessage(labels.details)}</Item> <Item key="details">{formatMessage(labels.details)}</Item>
<Item key="websites">{formatMessage(labels.websites)}</Item> <Item key="websites">{formatMessage(labels.websites)}</Item>

View File

@ -10,8 +10,6 @@ import {
import { useApi } from 'components/hooks'; import { useApi } from 'components/hooks';
import { DOMAIN_REGEX } from 'lib/constants'; import { DOMAIN_REGEX } from 'lib/constants';
import { useMessages } from 'components/hooks'; import { useMessages } from 'components/hooks';
import { useContext } from 'react';
import SettingsContext from '../SettingsContext';
export function WebsiteAddForm({ export function WebsiteAddForm({
teamId, teamId,
@ -23,10 +21,9 @@ export function WebsiteAddForm({
onClose?: () => void; onClose?: () => void;
}) { }) {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const { websitesUrl } = useContext(SettingsContext);
const { post, useMutation } = useApi(); const { post, useMutation } = useApi();
const { mutate, error, isPending } = useMutation({ const { mutate, error, isPending } = useMutation({
mutationFn: (data: any) => post(websitesUrl, { ...data, teamId }), mutationFn: (data: any) => post('/websites', { ...data, teamId }),
}); });
const handleSubmit = async (data: any) => { const handleSubmit = async (data: any) => {

View File

@ -1,39 +1,23 @@
'use client'; 'use client';
import { useState, Key } from 'react'; import { useState, Key } from 'react';
import { Item, Tabs, useToasts, Button, Text, Icon, Icons, Loading } from 'react-basics'; import { Item, Tabs, Button, Text, Icon, Loading } from 'react-basics';
import { useRouter } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import Icons from 'components/icons';
import PageHeader from 'components/layout/PageHeader'; import PageHeader from 'components/layout/PageHeader';
import WebsiteEditForm from './[id]/WebsiteEditForm'; import WebsiteEditForm from './[id]/WebsiteEditForm';
import WebsiteData from './[id]/WebsiteData'; import WebsiteData from './[id]/WebsiteData';
import TrackingCode from './[id]/TrackingCode'; import TrackingCode from './[id]/TrackingCode';
import ShareUrl from './[id]/ShareUrl'; import ShareUrl from './[id]/ShareUrl';
import { useWebsite, useMessages } from 'components/hooks'; import { useWebsite, useMessages } from 'components/hooks';
import { touch } from 'store/cache'; import WebsiteContext from 'app/(main)/websites/[id]/WebsiteContext';
export function WebsiteSettings({ websiteId, openExternal = false }) { export function WebsiteSettings({ websiteId, openExternal = false }) {
const router = useRouter(); const { formatMessage, labels } = useMessages();
const { formatMessage, labels, messages } = useMessages(); const { data: website, isLoading, refetch } = useWebsite(websiteId, { gcTime: 0 });
const { showToast } = useToasts();
const { data: website, isLoading } = useWebsite(websiteId, { gcTime: 0 });
const [tab, setTab] = useState<Key>('details'); const [tab, setTab] = useState<Key>('details');
const showSuccess = () => {
showToast({ message: formatMessage(messages.saved), variant: 'success' });
};
const handleSave = () => { const handleSave = () => {
showSuccess(); refetch();
touch('websites');
};
const handleReset = async (value: string) => {
if (value === 'delete') {
router.push('/settings/websites');
} else if (value === 'reset') {
showSuccess();
}
}; };
if (isLoading) { if (isLoading) {
@ -41,8 +25,8 @@ export function WebsiteSettings({ websiteId, openExternal = false }) {
} }
return ( return (
<> <WebsiteContext.Provider value={website}>
<PageHeader title={website?.name}> <PageHeader title={website?.name} icon={<Icons.Globe />}>
<Link href={`/websites/${websiteId}`} target={openExternal ? '_blank' : null}> <Link href={`/websites/${websiteId}`} target={openExternal ? '_blank' : null}>
<Button variant="primary"> <Button variant="primary">
<Icon> <Icon>
@ -58,13 +42,11 @@ export function WebsiteSettings({ websiteId, openExternal = false }) {
<Item key="share">{formatMessage(labels.shareUrl)}</Item> <Item key="share">{formatMessage(labels.shareUrl)}</Item>
<Item key="data">{formatMessage(labels.data)}</Item> <Item key="data">{formatMessage(labels.data)}</Item>
</Tabs> </Tabs>
{tab === 'details' && ( {tab === 'details' && <WebsiteEditForm website={website} onSave={handleSave} />}
<WebsiteEditForm websiteId={websiteId} data={website} onSave={handleSave} />
)}
{tab === 'tracking' && <TrackingCode websiteId={websiteId} />} {tab === 'tracking' && <TrackingCode websiteId={websiteId} />}
{tab === 'share' && <ShareUrl websiteId={websiteId} data={website} onSave={handleSave} />} {tab === 'share' && <ShareUrl website={website} onSave={handleSave} />}
{tab === 'data' && <WebsiteData websiteId={websiteId} onSave={handleReset} />} {tab === 'data' && <WebsiteData websiteId={websiteId} />}
</> </WebsiteContext.Provider>
); );
} }

View File

@ -2,16 +2,7 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import WebsitesTable from 'app/(main)/settings/websites/WebsitesTable'; import WebsitesTable from 'app/(main)/settings/websites/WebsitesTable';
import DataTable from 'components/common/DataTable'; import DataTable from 'components/common/DataTable';
import useWebsites from 'components/hooks/queries/useWebsites'; import { useWebsites } from 'components/hooks';
export interface WebsitesDataTableProps {
userId?: string;
teamId?: string;
allowEdit?: boolean;
allowView?: boolean;
showActions?: boolean;
children?: ReactNode;
}
export function WebsitesDataTable({ export function WebsitesDataTable({
userId, userId,
@ -20,13 +11,21 @@ export function WebsitesDataTable({
allowView = true, allowView = true,
showActions = true, showActions = true,
children, children,
}: WebsitesDataTableProps) { }: {
userId?: string;
teamId?: string;
allowEdit?: boolean;
allowView?: boolean;
showActions?: boolean;
children?: ReactNode;
}) {
const queryResult = useWebsites({ userId, teamId }); const queryResult = useWebsites({ userId, teamId });
return ( return (
<DataTable queryResult={queryResult}> <DataTable queryResult={queryResult}>
{({ data }) => ( {({ data }) => (
<WebsitesTable <WebsitesTable
teamId={teamId}
data={data} data={data}
showActions={showActions} showActions={showActions}
allowEdit={allowEdit} allowEdit={allowEdit}

View File

@ -46,7 +46,7 @@ export function WebsitesTable({
</Link> </Link>
)} )}
{allowView && ( {allowView && (
<Link href={teamId ? `/team/${teamId}/websites/${id}` : `/websites/${id}`}> <Link href={teamId ? `/teams/${teamId}/websites/${id}` : `/websites/${id}`}>
<Button> <Button>
<Icon> <Icon>
<Icons.External /> <Icons.External />

View File

@ -1,68 +1,63 @@
import { Website } from '@prisma/client';
import { import {
Form, Form,
FormRow, FormRow,
FormButtons, FormButtons,
Flexbox, Flexbox,
TextField, TextField,
SubmitButton,
Button, Button,
Toggle, Toggle,
LoadingButton,
useToasts,
} from 'react-basics'; } from 'react-basics';
import { useContext, useEffect, useMemo, useRef, useState } from 'react'; import { useContext, useState } from 'react';
import { getRandomChars } from 'next-basics'; import { getRandomChars } from 'next-basics';
import { useApi, useMessages } from 'components/hooks'; import { useApi, useMessages } from 'components/hooks';
import SettingsContext from '../../SettingsContext'; import SettingsContext from 'app/(main)/settings/SettingsContext';
const generateId = () => getRandomChars(16); const generateId = () => getRandomChars(16);
export function ShareUrl({ websiteId, data, onSave }) { export function ShareUrl({ website, onSave }: { website: Website; onSave?: () => void }) {
const ref = useRef(null); const { domain, shareId } = website;
const { shareUrl } = useContext(SettingsContext); const { hostUrl } = useContext(SettingsContext);
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const { name, shareId } = data;
const [id, setId] = useState(shareId); const [id, setId] = useState(shareId);
const { showToast } = useToasts();
const { post, useMutation } = useApi(); const { post, useMutation } = useApi();
const { mutate, error } = useMutation({ const { mutate, error, isPending } = useMutation({
mutationFn: (data: any) => post(`/websites/${websiteId}`, data), mutationFn: (data: any) => post(`/websites/${website.id}`, data),
}); });
const url = useMemo(
() => `${shareUrl}${process.env.basePath}/share/${id}/${encodeURIComponent(name)}`,
[id, name],
);
const handleSubmit = async (data: any) => { const url = `${hostUrl || location.origin}${
mutate(data, { process.env.basePath
onSuccess: async () => { }/share/${id}/${encodeURIComponent(domain)}`;
onSave(data);
ref.current.reset(data);
},
});
};
const handleGenerate = () => { const handleGenerate = () => {
const id = generateId(); setId(generateId());
ref.current.setValue('shareId', id, {
shouldValidate: true,
shouldDirty: true,
});
setId(id);
}; };
const handleCheck = (checked: boolean) => { const handleCheck = (checked: boolean) => {
const data = { shareId: checked ? generateId() : null }; const data = { shareId: checked ? generateId() : null };
mutate(data, { mutate(data, {
onSuccess: async () => { onSuccess: async () => {
onSave(data); onSave?.();
showToast({ message: formatMessage(messages.saved), variant: 'success' });
}, },
}); });
setId(data.shareId); setId(data.shareId);
}; };
useEffect(() => { const handleSave = () => {
if (id && id !== shareId) { mutate(
ref.current.setValue('shareId', id); { shareId: id },
} {
}, [id, shareId]); onSuccess: async () => {
showToast({ message: formatMessage(messages.saved), variant: 'success' });
onSave?.();
},
},
);
};
return ( return (
<> <>
@ -70,7 +65,7 @@ export function ShareUrl({ websiteId, data, onSave }) {
{formatMessage(labels.enableShareUrl)} {formatMessage(labels.enableShareUrl)}
</Toggle> </Toggle>
{id && ( {id && (
<Form key={websiteId} ref={ref} onSubmit={handleSubmit} error={error} values={data}> <Form error={error}>
<FormRow> <FormRow>
<p>{formatMessage(messages.shareUrl)}</p> <p>{formatMessage(messages.shareUrl)}</p>
<Flexbox gap={10}> <Flexbox gap={10}>
@ -79,7 +74,14 @@ export function ShareUrl({ websiteId, data, onSave }) {
</Flexbox> </Flexbox>
</FormRow> </FormRow>
<FormButtons> <FormButtons>
<SubmitButton variant="primary">{formatMessage(labels.save)}</SubmitButton> <LoadingButton
variant="primary"
disabled={id === shareId}
isLoading={isPending}
onClick={handleSave}
>
{formatMessage(labels.save)}
</LoadingButton>
</FormButtons> </FormButtons>
</Form> </Form>
)} )}

View File

@ -1,23 +1,23 @@
import { Button, Modal, ModalTrigger, ActionForm } from 'react-basics'; import { Button, Modal, ModalTrigger, ActionForm, useToasts } from 'react-basics';
import { useRouter } from 'next/navigation';
import { useMessages } from 'components/hooks';
import WebsiteDeleteForm from './WebsiteDeleteForm'; import WebsiteDeleteForm from './WebsiteDeleteForm';
import WebsiteResetForm from './WebsiteResetForm'; import WebsiteResetForm from './WebsiteResetForm';
import { useMessages } from 'components/hooks'; import { touch } from 'store/cache';
export function WebsiteData({ export function WebsiteData({ websiteId, onSave }: { websiteId: string; onSave?: () => void }) {
websiteId,
onSave,
}: {
websiteId: string;
onSave?: (value: string) => void;
}) {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const router = useRouter();
const { showToast } = useToasts();
const handleReset = async () => { const handleReset = async () => {
onSave('reset'); showToast({ message: formatMessage(messages.saved), variant: 'success' });
onSave?.();
}; };
const handleDelete = async () => { const handleDelete = async () => {
onSave('delete'); touch('websites');
router.push('/settings/websites');
}; };
return ( return (

View File

@ -1,6 +1,4 @@
import { useApi, useMessages } from 'components/hooks'; import { useApi, useMessages } from 'components/hooks';
import { useContext } from 'react';
import SettingsContext from '../../SettingsContext';
import TypeConfirmationForm from 'components/common/TypeConfirmationForm'; import TypeConfirmationForm from 'components/common/TypeConfirmationForm';
const CONFIRM_VALUE = 'DELETE'; const CONFIRM_VALUE = 'DELETE';
@ -15,10 +13,9 @@ export function WebsiteDeleteForm({
onClose?: () => void; onClose?: () => void;
}) { }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { websitesUrl } = useContext(SettingsContext);
const { del, useMutation } = useApi(); const { del, useMutation } = useApi();
const { mutate, isPending, error } = useMutation({ const { mutate, isPending, error } = useMutation({
mutationFn: (data: any) => del(`${websitesUrl}/${websiteId}`, data), mutationFn: (data: any) => del(`/websites/${websiteId}`, data),
}); });
const handleConfirm = async () => { const handleConfirm = async () => {

View File

@ -1,40 +1,46 @@
import { SubmitButton, Form, FormInput, FormRow, FormButtons, TextField } from 'react-basics'; import { Website } from '@prisma/client';
import { useContext, useRef } from 'react'; import { useRef } from 'react';
import { useApi } from 'components/hooks'; import {
SubmitButton,
Form,
FormInput,
FormRow,
FormButtons,
TextField,
useToasts,
} from 'react-basics';
import { useApi, useMessages } from 'components/hooks';
import { DOMAIN_REGEX } from 'lib/constants'; import { DOMAIN_REGEX } from 'lib/constants';
import { useMessages } from 'components/hooks';
import SettingsContext from '../../SettingsContext';
export function WebsiteEditForm({ export function WebsiteEditForm({
websiteId, website,
data,
onSave, onSave,
}: { }: {
websiteId: string; website: Website;
data: any[];
onSave?: (data: any) => void; onSave?: (data: any) => void;
}) { }) {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const { websitesUrl } = useContext(SettingsContext);
const { post, useMutation } = useApi(); const { post, useMutation } = useApi();
const { mutate, error } = useMutation({ const { mutate, error } = useMutation({
mutationFn: (data: any) => post(`${websitesUrl}/${websiteId}`, data), mutationFn: (data: any) => post(`/websites/${website.id}`, data),
}); });
const ref = useRef(null); const ref = useRef(null);
const { showToast } = useToasts();
const handleSubmit = async (data: any) => { const handleSubmit = async (data: any) => {
mutate(data, { mutate(data, {
onSuccess: async () => { onSuccess: async () => {
showToast({ message: formatMessage(messages.saved), variant: 'success' });
ref.current.reset(data); ref.current.reset(data);
onSave(data); onSave?.(data);
}, },
}); });
}; };
return ( return (
<Form ref={ref} onSubmit={handleSubmit} error={error} values={data}> <Form ref={ref} onSubmit={handleSubmit} error={error} values={website}>
<FormRow label={formatMessage(labels.websiteId)}> <FormRow label={formatMessage(labels.websiteId)}>
<TextField value={websiteId} readOnly allowCopy /> <TextField value={website.id} readOnly allowCopy />
</FormRow> </FormRow>
<FormRow label={formatMessage(labels.name)}> <FormRow label={formatMessage(labels.name)}>
<FormInput name="name" rules={{ required: formatMessage(labels.required) }}> <FormInput name="name" rules={{ required: formatMessage(labels.required) }}>

View File

@ -0,0 +1,5 @@
import WebsiteDetails from 'app/(main)/websites/[id]/WebsiteDetails';
export default function TeamWebsitePage({ params: { websiteId } }) {
return <WebsiteDetails websiteId={websiteId} />;
}

View File

@ -0,0 +1,6 @@
'use client';
import { createContext } from 'react';
export const WebsiteContext = createContext(null);
export default WebsiteContext;

View File

@ -27,6 +27,11 @@
flex: 1; flex: 1;
} }
.icon {
color: var(--base700);
margin-right: 1rem;
}
.actions { .actions {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;

View File

@ -1,16 +1,26 @@
import classNames from 'classnames'; import classNames from 'classnames';
import React, { ReactNode } from 'react'; import React, { ReactNode } from 'react';
import { Icon } from 'react-basics';
import styles from './PageHeader.module.css'; import styles from './PageHeader.module.css';
export interface PageHeaderProps { export function PageHeader({
title,
icon,
className,
children,
}: {
title?: ReactNode; title?: ReactNode;
icon?: ReactNode;
className?: string; className?: string;
children?: ReactNode; children?: ReactNode;
} }) {
export function PageHeader({ title, className, children }: PageHeaderProps) {
return ( return (
<div className={classNames(styles.header, className)}> <div className={classNames(styles.header, className)}>
{icon && (
<Icon size="lg" className={styles.icon}>
{icon}
</Icon>
)}
{title && <div className={styles.title}>{title}</div>} {title && <div className={styles.title}>{title}</div>}
<div className={styles.actions}>{children}</div> <div className={styles.actions}>{children}</div>
</div> </div>