From 2fa50892d84a19cda1fe94c1065172a7c158a86e Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 29 Jan 2024 01:32:05 -0800 Subject: [PATCH] Updated query hooks for teams and websites. --- next.config.js | 53 +++++--- src/app/(main)/dashboard/Dashboard.tsx | 9 +- src/app/(main)/reports/ReportDeleteButton.tsx | 29 +++-- src/app/(main)/reports/ReportsTable.tsx | 5 +- src/app/(main)/reports/funnel/FunnelChart.tsx | 3 +- src/app/(main)/settings/layout.tsx | 5 +- .../settings/profile/DateRangeSetting.tsx | 3 +- .../settings/profile/LanguageSetting.tsx | 3 +- .../settings/profile/PasswordEditForm.tsx | 3 +- .../settings/profile/ProfileSettings.tsx | 5 +- .../settings/profile/TimezoneSetting.tsx | 3 +- src/app/(main)/settings/teams/TeamAddForm.tsx | 3 +- .../(main)/settings/teams/TeamDeleteForm.tsx | 43 +++--- .../(main)/settings/teams/TeamJoinForm.tsx | 3 +- .../(main)/settings/teams/TeamLeaveButton.tsx | 8 +- .../(main)/settings/teams/TeamLeaveForm.tsx | 30 ++--- .../(main)/settings/teams/TeamsDataTable.tsx | 3 +- src/app/(main)/settings/teams/TeamsHeader.tsx | 5 +- src/app/(main)/settings/teams/TeamsTable.tsx | 5 +- .../(main)/settings/teams/[id]/TeamData.tsx | 8 +- .../settings/teams/[id]/TeamEditForm.tsx | 19 ++- .../teams/[id]/TeamMemberRemoveButton.tsx | 3 +- .../settings/teams/[id]/TeamMembers.tsx | 3 +- .../settings/teams/[id]/TeamMembersTable.tsx | 5 +- .../settings/teams/[id]/TeamSettings.tsx | 81 +++++------- .../teams/[id]/TeamWebsiteRemoveButton.tsx | 3 +- src/app/(main)/settings/users/UserAddForm.tsx | 3 +- .../settings/users/UserDeleteButton.tsx | 7 +- .../(main)/settings/users/UserDeleteForm.tsx | 38 +++--- .../(main)/settings/users/UserEditForm.tsx | 9 +- .../(main)/settings/users/UserWebsites.tsx | 28 ---- .../(main)/settings/users/UsersDataTable.tsx | 13 +- src/app/(main)/settings/users/UsersTable.tsx | 8 +- .../settings/users/[id]/UserSettings.tsx | 50 ++----- .../settings/users/[id]/UserWebsites.tsx | 18 +++ .../settings/websites/WebsiteSettings.tsx | 40 ++---- src/app/(main)/settings/websites/Websites.tsx | 4 +- .../settings/websites/WebsitesTable.tsx | 12 +- .../settings/websites/[id]/ShareUrl.tsx | 7 +- .../settings/websites/[id]/TrackingCode.tsx | 3 +- .../websites/[id]/WebsiteDeleteForm.tsx | 51 +++----- .../websites/[id]/WebsiteResetForm.tsx | 55 +++----- .../teams/[id]/websites/TeamWebsites.tsx | 14 -- src/app/(main)/teams/[id]/websites/page.tsx | 4 +- src/app/(main)/websites/WebsitesBrowse.tsx | 4 +- src/components/common/ConfirmDeleteForm.tsx | 35 ----- src/components/common/ConfirmationForm.tsx | 39 ++++++ .../common/TypeConfirmationForm.tsx | 58 +++++++++ src/components/hooks/index.js | 2 + src/components/hooks/queries/useLogin.ts | 7 +- src/components/hooks/queries/useUser.ts | 16 ++- src/components/hooks/queries/useUsers.ts | 19 +++ src/components/hooks/queries/useWebsite.ts | 3 +- src/components/hooks/queries/useWebsites.ts | 2 +- src/components/hooks/useDateRange.ts | 4 +- src/components/input/ProfileButton.tsx | 4 +- src/components/messages.ts | 2 + src/index.ts | 2 +- src/pages/api/admin/users.ts | 15 ++- src/queries/admin/user.ts | 122 +++++++++--------- src/store/cache.ts | 6 +- 61 files changed, 508 insertions(+), 539 deletions(-) delete mode 100644 src/app/(main)/settings/users/UserWebsites.tsx create mode 100644 src/app/(main)/settings/users/[id]/UserWebsites.tsx delete mode 100644 src/app/(main)/teams/[id]/websites/TeamWebsites.tsx delete mode 100644 src/components/common/ConfirmDeleteForm.tsx create mode 100644 src/components/common/ConfirmationForm.tsx create mode 100644 src/components/common/TypeConfirmationForm.tsx create mode 100644 src/components/hooks/queries/useUsers.ts diff --git a/next.config.js b/next.config.js index e684ae97..4a316433 100644 --- a/next.config.js +++ b/next.config.js @@ -3,13 +3,25 @@ require('dotenv').config(); const path = require('path'); const pkg = require('./package.json'); +const basePath = process.env.BASE_PATH || ''; +const forceSSL = process.env.FORCE_SSL || ''; +const collectApiEndpoint = process.env.COLLECT_API_ENDPOINT || ''; +const defaultLocale = process.env.DEFAULT_LOCALE || ''; +const trackerScriptName = process.env.TRACKER_SCRIPT_NAME || ''; +const cloudMode = process.env.CLOUD_MODE || ''; +const cloudUrl = process.env.CLOUD_URL || ''; +const frameAncestors = process.env.ALLOWED_FRAME_URLS || ''; +const disableLogin = process.env.DISABLE_LOGIN || ''; +const disableUI = process.env.DISABLE_UI || ''; +const hostURL = process.env.HOST_URL || ''; + const contentSecurityPolicy = [ `default-src 'self'`, `img-src *`, `script-src 'self' 'unsafe-eval' 'unsafe-inline'`, `style-src 'self' 'unsafe-inline'`, `connect-src 'self' api.umami.is cloud.umami.is`, - `frame-ancestors 'self' ${process.env.ALLOWED_FRAME_URLS || ''}`, + `frame-ancestors 'self' ${frameAncestors}`, ]; const headers = [ @@ -26,7 +38,7 @@ const headers = [ }, ]; -if (process.env.FORCE_SSL) { +if (forceSSL) { headers.push({ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload', @@ -35,15 +47,15 @@ if (process.env.FORCE_SSL) { const rewrites = []; -if (process.env.COLLECT_API_ENDPOINT) { +if (collectApiEndpoint) { rewrites.push({ - source: process.env.COLLECT_API_ENDPOINT, + source: collectApiEndpoint, destination: '/api/send', }); } -if (process.env.TRACKER_SCRIPT_NAME) { - const names = process.env.TRACKER_SCRIPT_NAME?.split(',').map(name => name.trim()); +if (trackerScriptName) { + const names = trackerScriptName?.split(',').map(name => name.trim()); if (names) { names.forEach(name => { @@ -58,36 +70,37 @@ if (process.env.TRACKER_SCRIPT_NAME) { const redirects = [ { source: '/settings', - destination: process.env.CLOUD_MODE - ? `${process.env.CLOUD_URL}/settings/websites` - : '/settings/websites', + destination: cloudMode ? `${cloudUrl}/settings/websites` : '/settings/websites', + permanent: true, + }, + { + source: '/teams/:id', + destination: '/teams/:id/websites', permanent: true, }, ]; -if (process.env.CLOUD_MODE && process.env.CLOUD_URL && process.env.DISABLE_LOGIN) { +if (cloudMode && cloudUrl && disableLogin) { redirects.push({ source: '/login', - destination: process.env.CLOUD_URL, + destination: cloudUrl, permanent: false, }); } -const basePath = process.env.BASE_PATH; - /** @type {import('next').NextConfig} */ const config = { reactStrictMode: false, env: { - basePath: basePath || '', - cloudMode: process.env.CLOUD_MODE || '', - cloudUrl: process.env.CLOUD_URL || '', + basePath, + cloudMode, + cloudUrl, configUrl: '/config', currentVersion: pkg.version, - defaultLocale: process.env.DEFAULT_LOCALE || '', - disableLogin: process.env.DISABLE_LOGIN || '', - disableUI: process.env.DISABLE_UI || '', - hostUrl: process.env.HOST_URL || '', + defaultLocale, + disableLogin, + disableUI, + hostURL, }, basePath, output: 'standalone', diff --git a/src/app/(main)/dashboard/Dashboard.tsx b/src/app/(main)/dashboard/Dashboard.tsx index b6ab0617..e4d8671a 100644 --- a/src/app/(main)/dashboard/Dashboard.tsx +++ b/src/app/(main)/dashboard/Dashboard.tsx @@ -3,20 +3,17 @@ import { Button, Icon, Icons, Loading, Text } from 'react-basics'; import Link from 'next/link'; import PageHeader from 'components/layout/PageHeader'; import Pager from 'components/common/Pager'; -import WebsiteChartList from '../../(main)/websites/[id]/WebsiteChartList'; +import WebsiteChartList from 'app/(main)/websites/[id]/WebsiteChartList'; import DashboardSettingsButton from 'app/(main)/dashboard/DashboardSettingsButton'; import DashboardEdit from 'app/(main)/dashboard/DashboardEdit'; import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; import { useApi } from 'components/hooks'; import useDashboard from 'store/dashboard'; -import { useMessages } from 'components/hooks'; -import { useLocale } from 'components/hooks'; -import { useFilterQuery } from 'components/hooks'; -import { useUser } from 'components/hooks'; +import { useMessages, useLocale, useLogin, useFilterQuery } from 'components/hooks'; export function Dashboard() { const { formatMessage, labels, messages } = useMessages(); - const { user } = useUser(); + const { user } = useLogin(); const { showCharts, editing } = useDashboard(); const { dir } = useLocale(); const { get } = useApi(); diff --git a/src/app/(main)/reports/ReportDeleteButton.tsx b/src/app/(main)/reports/ReportDeleteButton.tsx index 32ec819e..b880ef6f 100644 --- a/src/app/(main)/reports/ReportDeleteButton.tsx +++ b/src/app/(main)/reports/ReportDeleteButton.tsx @@ -1,7 +1,7 @@ import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics'; -import ConfirmDeleteForm from 'components/common/ConfirmDeleteForm'; import { useApi, useMessages } from 'components/hooks'; -import { setValue } from 'store/cache'; +import { touch } from 'store/cache'; +import ConfirmationForm from 'components/common/ConfirmationForm'; export function ReportDeleteButton({ reportId, @@ -12,14 +12,16 @@ export function ReportDeleteButton({ reportName: string; onDelete?: () => void; }) { - const { formatMessage, labels } = useMessages(); + const { formatMessage, labels, messages, FormattedMessage } = useMessages(); const { del, useMutation } = useApi(); - const { mutate } = useMutation({ mutationFn: reportId => del(`/reports/${reportId}`) }); + const { mutate, isPending, error } = useMutation({ + mutationFn: reportId => del(`/reports/${reportId}`), + }); const handleConfirm = (close: () => void) => { mutate(reportId as any, { onSuccess: () => { - setValue('reports', Date.now()); + touch('reports'); onDelete?.(); close(); }, @@ -28,16 +30,23 @@ export function ReportDeleteButton({ return ( - - - {close => ( - + {(close: () => void) => ( + {reportName} }} + /> + } + isLoading={isPending} + error={error} onConfirm={handleConfirm.bind(null, close)} onClose={close} /> diff --git a/src/app/(main)/reports/ReportsTable.tsx b/src/app/(main)/reports/ReportsTable.tsx index 3a5f7497..d8283271 100644 --- a/src/app/(main)/reports/ReportsTable.tsx +++ b/src/app/(main)/reports/ReportsTable.tsx @@ -1,13 +1,12 @@ import { GridColumn, GridTable, Icon, Icons, Text, useBreakpoint } from 'react-basics'; import LinkButton from 'components/common/LinkButton'; -import { useMessages } from 'components/hooks'; -import { useUser } from 'components/hooks'; +import { useMessages, useLogin } from 'components/hooks'; import { REPORT_TYPES } from 'lib/constants'; import ReportDeleteButton from './ReportDeleteButton'; export function ReportsTable({ data = [], showDomain }: { data: any[]; showDomain?: boolean }) { const { formatMessage, labels } = useMessages(); - const { user } = useUser(); + const { user } = useLogin(); const breakpoint = useBreakpoint(); return ( diff --git a/src/app/(main)/reports/funnel/FunnelChart.tsx b/src/app/(main)/reports/funnel/FunnelChart.tsx index ae4bc2ea..e401e3cb 100644 --- a/src/app/(main)/reports/funnel/FunnelChart.tsx +++ b/src/app/(main)/reports/funnel/FunnelChart.tsx @@ -1,7 +1,6 @@ import { JSX, useCallback, useContext, useMemo } from 'react'; import { Loading, StatusLight } from 'react-basics'; -import { useMessages } from 'components/hooks'; -import { useTheme } from 'components/hooks'; +import { useMessages, useTheme } from 'components/hooks'; import BarChart from 'components/metrics/BarChart'; import { formatLongNumber } from 'lib/format'; import { ReportContext } from '../[id]/Report'; diff --git a/src/app/(main)/settings/layout.tsx b/src/app/(main)/settings/layout.tsx index 2c0547a5..2f80a5d0 100644 --- a/src/app/(main)/settings/layout.tsx +++ b/src/app/(main)/settings/layout.tsx @@ -1,12 +1,11 @@ 'use client'; import { usePathname } from 'next/navigation'; -import { useUser } from 'components/hooks'; -import { useMessages } from 'components/hooks'; +import { useLogin, useMessages } from 'components/hooks'; import SideNav from 'components/layout/SideNav'; import styles from './layout.module.css'; export default function SettingsLayout({ children }) { - const { user } = useUser(); + const { user } = useLogin(); const pathname = usePathname(); const { formatMessage, labels } = useMessages(); const cloudMode = !!process.env.cloudMode; diff --git a/src/app/(main)/settings/profile/DateRangeSetting.tsx b/src/app/(main)/settings/profile/DateRangeSetting.tsx index f71bc71d..a1ae7bc7 100644 --- a/src/app/(main)/settings/profile/DateRangeSetting.tsx +++ b/src/app/(main)/settings/profile/DateRangeSetting.tsx @@ -1,8 +1,7 @@ import DateFilter from 'components/input/DateFilter'; import { Button, Flexbox } from 'react-basics'; -import { useDateRange } from 'components/hooks'; +import { useDateRange, useMessages } from 'components/hooks'; import { DEFAULT_DATE_RANGE } from 'lib/constants'; -import { useMessages } from 'components/hooks'; import { DateRange } from 'lib/types'; export function DateRangeSetting() { diff --git a/src/app/(main)/settings/profile/LanguageSetting.tsx b/src/app/(main)/settings/profile/LanguageSetting.tsx index a1a5b11b..40546f4d 100644 --- a/src/app/(main)/settings/profile/LanguageSetting.tsx +++ b/src/app/(main)/settings/profile/LanguageSetting.tsx @@ -1,9 +1,8 @@ import { useState } from 'react'; import { Button, Dropdown, Item, Flexbox } from 'react-basics'; -import { useLocale } from 'components/hooks'; +import { useLocale, useMessages } from 'components/hooks'; import { DEFAULT_LOCALE } from 'lib/constants'; import { languages } from 'lib/lang'; -import { useMessages } from 'components/hooks'; import styles from './LanguageSetting.module.css'; export function LanguageSetting() { diff --git a/src/app/(main)/settings/profile/PasswordEditForm.tsx b/src/app/(main)/settings/profile/PasswordEditForm.tsx index 597379fc..1402efa2 100644 --- a/src/app/(main)/settings/profile/PasswordEditForm.tsx +++ b/src/app/(main)/settings/profile/PasswordEditForm.tsx @@ -1,7 +1,6 @@ import { useRef } from 'react'; import { Form, FormRow, FormInput, FormButtons, PasswordField, Button } from 'react-basics'; -import { useApi } from 'components/hooks'; -import { useMessages } from 'components/hooks'; +import { useApi, useMessages } from 'components/hooks'; export function PasswordEditForm({ onSave, onClose }) { const { formatMessage, labels, messages } = useMessages(); diff --git a/src/app/(main)/settings/profile/ProfileSettings.tsx b/src/app/(main)/settings/profile/ProfileSettings.tsx index e93536e8..45641e72 100644 --- a/src/app/(main)/settings/profile/ProfileSettings.tsx +++ b/src/app/(main)/settings/profile/ProfileSettings.tsx @@ -5,12 +5,11 @@ import DateRangeSetting from 'app/(main)/settings/profile/DateRangeSetting'; import LanguageSetting from 'app/(main)/settings/profile/LanguageSetting'; import ThemeSetting from 'app/(main)/settings/profile/ThemeSetting'; import PasswordChangeButton from './PasswordChangeButton'; -import { useUser } from 'components/hooks'; -import { useMessages } from 'components/hooks'; +import { useLogin, useMessages } from 'components/hooks'; import { ROLES } from 'lib/constants'; export function ProfileSettings() { - const { user } = useUser(); + const { user } = useLogin(); const { formatMessage, labels } = useMessages(); const cloudMode = Boolean(process.env.cloudMode); diff --git a/src/app/(main)/settings/profile/TimezoneSetting.tsx b/src/app/(main)/settings/profile/TimezoneSetting.tsx index ca14f356..671125f4 100644 --- a/src/app/(main)/settings/profile/TimezoneSetting.tsx +++ b/src/app/(main)/settings/profile/TimezoneSetting.tsx @@ -1,8 +1,7 @@ import { useState } from 'react'; import { Dropdown, Item, Button, Flexbox } from 'react-basics'; import { listTimeZones } from 'timezone-support'; -import { useTimezone } from 'components/hooks'; -import { useMessages } from 'components/hooks'; +import { useTimezone, useMessages } from 'components/hooks'; import { getTimezone } from 'lib/date'; import styles from './TimezoneSetting.module.css'; diff --git a/src/app/(main)/settings/teams/TeamAddForm.tsx b/src/app/(main)/settings/teams/TeamAddForm.tsx index a9b229ba..24a8d96e 100644 --- a/src/app/(main)/settings/teams/TeamAddForm.tsx +++ b/src/app/(main)/settings/teams/TeamAddForm.tsx @@ -8,8 +8,7 @@ import { SubmitButton, } from 'react-basics'; import { setValue } from 'store/cache'; -import { useApi } from 'components/hooks'; -import { useMessages } from 'components/hooks'; +import { useApi, useMessages } from 'components/hooks'; export function TeamAddForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) { const { formatMessage, labels } = useMessages(); diff --git a/src/app/(main)/settings/teams/TeamDeleteForm.tsx b/src/app/(main)/settings/teams/TeamDeleteForm.tsx index e2ee6b65..0193f2f3 100644 --- a/src/app/(main)/settings/teams/TeamDeleteForm.tsx +++ b/src/app/(main)/settings/teams/TeamDeleteForm.tsx @@ -1,29 +1,28 @@ -import { Button, Form, FormButtons, SubmitButton } from 'react-basics'; -import { useApi } from 'components/hooks'; -import { useMessages } from 'components/hooks'; -import { setValue } from 'store/cache'; +import { useApi, useMessages } from 'components/hooks'; +import { touch } from 'store/cache'; +import TypeConfirmationForm from 'components/common/TypeConfirmationForm'; + +const CONFIRM_VALUE = 'DELETE'; export function TeamDeleteForm({ teamId, - teamName, onSave, onClose, }: { teamId: string; - teamName: string; - onSave: () => void; - onClose: () => void; + onSave?: () => void; + onClose?: () => void; }) { - const { formatMessage, labels, messages, FormattedMessage } = useMessages(); + const { labels, formatMessage } = useMessages(); const { del, useMutation } = useApi(); const { mutate, error, isPending } = useMutation({ mutationFn: (data: any) => del(`/teams/${teamId}`, data), }); - const handleSubmit = async data => { - mutate(data, { + const handleConfirm = async () => { + mutate(null, { onSuccess: async () => { - setValue('teams', Date.now()); + touch('teams'); onSave?.(); onClose?.(); }, @@ -31,17 +30,15 @@ export function TeamDeleteForm({ }; return ( -
-

- {teamName} }} /> -

- - - {formatMessage(labels.delete)} - - - -
+ ); } diff --git a/src/app/(main)/settings/teams/TeamJoinForm.tsx b/src/app/(main)/settings/teams/TeamJoinForm.tsx index 094eaec5..9a91d1a5 100644 --- a/src/app/(main)/settings/teams/TeamJoinForm.tsx +++ b/src/app/(main)/settings/teams/TeamJoinForm.tsx @@ -8,8 +8,7 @@ import { Button, SubmitButton, } from 'react-basics'; -import { useApi } from 'components/hooks'; -import { useMessages } from 'components/hooks'; +import { useApi, useMessages } from 'components/hooks'; import { setValue } from 'store/cache'; export function TeamJoinForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) { diff --git a/src/app/(main)/settings/teams/TeamLeaveButton.tsx b/src/app/(main)/settings/teams/TeamLeaveButton.tsx index bc5c3229..8b246a3b 100644 --- a/src/app/(main)/settings/teams/TeamLeaveButton.tsx +++ b/src/app/(main)/settings/teams/TeamLeaveButton.tsx @@ -1,7 +1,5 @@ import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics'; -import { useMessages } from 'components/hooks'; -import { useLocale } from 'components/hooks'; -import { useUser } from 'components/hooks'; +import { useMessages, useLocale, useLogin } from 'components/hooks'; import TeamDeleteForm from './TeamLeaveForm'; export function TeamLeaveButton({ @@ -15,7 +13,7 @@ export function TeamLeaveButton({ }) { const { formatMessage, labels } = useMessages(); const { dir } = useLocale(); - const { user } = useUser(); + const { user } = useLogin(); return ( @@ -26,7 +24,7 @@ export function TeamLeaveButton({ {formatMessage(labels.leave)} - {close => ( + {(close: () => void) => ( del(`/teams/${teamId}/users/${userId}`), }); - const handleSubmit = async () => { + const handleConfirm = async () => { mutate(null, { onSuccess: async () => { - setValue('team:members', Date.now()); + touch('team:members'); onSave(); onClose(); }, @@ -33,17 +32,16 @@ export function TeamLeaveForm({ }; return ( -
-

+ {teamName} }} /> -

- - - {formatMessage(labels.leave)} - - - -
+ } + onConfirm={handleConfirm} + onClose={onClose} + isLoading={isPending} + error={error} + /> ); } diff --git a/src/app/(main)/settings/teams/TeamsDataTable.tsx b/src/app/(main)/settings/teams/TeamsDataTable.tsx index b2ac7e16..ea8bab37 100644 --- a/src/app/(main)/settings/teams/TeamsDataTable.tsx +++ b/src/app/(main)/settings/teams/TeamsDataTable.tsx @@ -1,8 +1,7 @@ 'use client'; import DataTable from 'components/common/DataTable'; import TeamsTable from 'app/(main)/settings/teams/TeamsTable'; -import { useApi } from 'components/hooks'; -import { useFilterQuery } from 'components/hooks'; +import { useApi, useFilterQuery } from 'components/hooks'; import useCache from 'store/cache'; export function TeamsDataTable() { diff --git a/src/app/(main)/settings/teams/TeamsHeader.tsx b/src/app/(main)/settings/teams/TeamsHeader.tsx index f7d49bd5..0148d97b 100644 --- a/src/app/(main)/settings/teams/TeamsHeader.tsx +++ b/src/app/(main)/settings/teams/TeamsHeader.tsx @@ -2,14 +2,13 @@ import { Flexbox } from 'react-basics'; import PageHeader from 'components/layout/PageHeader'; import { ROLES } from 'lib/constants'; -import { useUser } from 'components/hooks'; -import { useMessages } from 'components/hooks'; +import { useLogin, useMessages } from 'components/hooks'; import TeamsJoinButton from './TeamsJoinButton'; import TeamsAddButton from './TeamsAddButton'; export function TeamsHeader({ allowCreate = true }: { allowCreate?: boolean }) { const { formatMessage, labels } = useMessages(); - const { user } = useUser(); + const { user } = useLogin(); return ( diff --git a/src/app/(main)/settings/teams/TeamsTable.tsx b/src/app/(main)/settings/teams/TeamsTable.tsx index 7cc73ed3..82ba054d 100644 --- a/src/app/(main)/settings/teams/TeamsTable.tsx +++ b/src/app/(main)/settings/teams/TeamsTable.tsx @@ -1,13 +1,12 @@ 'use client'; import { Button, GridColumn, GridTable, Icon, Icons, Text, useBreakpoint } from 'react-basics'; import Link from 'next/link'; -import { useMessages } from 'components/hooks'; -import { useUser } from 'components/hooks'; +import { useMessages, useLogin } from 'components/hooks'; import { ROLES } from 'lib/constants'; export function TeamsTable({ data = [] }: { data: any[] }) { const { formatMessage, labels } = useMessages(); - const { user } = useUser(); + const { user } = useLogin(); const breakpoint = useBreakpoint(); return ( diff --git a/src/app/(main)/settings/teams/[id]/TeamData.tsx b/src/app/(main)/settings/teams/[id]/TeamData.tsx index 14b20dc3..003737f0 100644 --- a/src/app/(main)/settings/teams/[id]/TeamData.tsx +++ b/src/app/(main)/settings/teams/[id]/TeamData.tsx @@ -2,11 +2,9 @@ import { ActionForm, Button, Modal, ModalTrigger } from 'react-basics'; import { useMessages } from 'components/hooks'; import TeamDeleteForm from '../TeamDeleteForm'; -export function TeamData({ teamId }: { teamId: string; onSave?: (value: string) => void }) { +export function TeamData({ teamId }: { teamId: string }) { const { formatMessage, labels, messages } = useMessages(); - const handleSave = () => {}; - return ( - {(close: () => void) => ( - - )} + {(close: () => void) => }
diff --git a/src/app/(main)/settings/teams/[id]/TeamEditForm.tsx b/src/app/(main)/settings/teams/[id]/TeamEditForm.tsx index 4f728af4..6cc8de5d 100644 --- a/src/app/(main)/settings/teams/[id]/TeamEditForm.tsx +++ b/src/app/(main)/settings/teams/[id]/TeamEditForm.tsx @@ -7,28 +7,37 @@ import { TextField, Button, Flexbox, + useToasts, } from 'react-basics'; import { getRandomChars } from 'next-basics'; import { useRef, useState } from 'react'; -import { useApi } from 'components/hooks'; -import { useMessages } from 'components/hooks'; +import { useApi, useMessages } from 'components/hooks'; const generateId = () => getRandomChars(16); -export function TeamEditForm({ teamId, data, onSave, readOnly }) { - const { formatMessage, labels } = useMessages(); +export function TeamEditForm({ + teamId, + data, + readOnly, +}: { + teamId: string; + data?: { name: string; accessCode: string }; + readOnly?: boolean; +}) { + const { formatMessage, labels, messages } = useMessages(); const { post, useMutation } = useApi(); const { mutate, error } = useMutation({ mutationFn: (data: any) => post(`/teams/${teamId}`, data), }); const ref = useRef(null); const [accessCode, setAccessCode] = useState(data.accessCode); + const { showToast } = useToasts(); const handleSubmit = async (data: any) => { mutate(data, { onSuccess: async () => { ref.current.reset(data); - onSave?.(data); + showToast({ message: formatMessage(messages.saved), variant: 'success' }); }, }); }; diff --git a/src/app/(main)/settings/teams/[id]/TeamMemberRemoveButton.tsx b/src/app/(main)/settings/teams/[id]/TeamMemberRemoveButton.tsx index 9b33ed6b..a5e05bd7 100644 --- a/src/app/(main)/settings/teams/[id]/TeamMemberRemoveButton.tsx +++ b/src/app/(main)/settings/teams/[id]/TeamMemberRemoveButton.tsx @@ -1,5 +1,4 @@ -import { useApi } from 'components/hooks'; -import { useMessages } from 'components/hooks'; +import { useApi, useMessages } from 'components/hooks'; import { Icon, Icons, LoadingButton, Text } from 'react-basics'; import { setValue } from 'store/cache'; diff --git a/src/app/(main)/settings/teams/[id]/TeamMembers.tsx b/src/app/(main)/settings/teams/[id]/TeamMembers.tsx index 42286bc0..cd6965c5 100644 --- a/src/app/(main)/settings/teams/[id]/TeamMembers.tsx +++ b/src/app/(main)/settings/teams/[id]/TeamMembers.tsx @@ -1,5 +1,4 @@ -import { useApi } from 'components/hooks'; -import { useFilterQuery } from 'components/hooks'; +import { useApi, useFilterQuery } from 'components/hooks'; import DataTable from 'components/common/DataTable'; import useCache from 'store/cache'; import TeamMembersTable from './TeamMembersTable'; diff --git a/src/app/(main)/settings/teams/[id]/TeamMembersTable.tsx b/src/app/(main)/settings/teams/[id]/TeamMembersTable.tsx index c4c333ce..93d7c521 100644 --- a/src/app/(main)/settings/teams/[id]/TeamMembersTable.tsx +++ b/src/app/(main)/settings/teams/[id]/TeamMembersTable.tsx @@ -1,6 +1,5 @@ import { GridColumn, GridTable, useBreakpoint } from 'react-basics'; -import { useMessages } from 'components/hooks'; -import { useUser } from 'components/hooks'; +import { useMessages, useLogin } from 'components/hooks'; import { ROLES } from 'lib/constants'; import TeamMemberRemoveButton from './TeamMemberRemoveButton'; @@ -14,7 +13,7 @@ export function TeamMembersTable({ readOnly: boolean; }) { const { formatMessage, labels } = useMessages(); - const { user } = useUser(); + const { user } = useLogin(); const breakpoint = useBreakpoint(); const roles = { diff --git a/src/app/(main)/settings/teams/[id]/TeamSettings.tsx b/src/app/(main)/settings/teams/[id]/TeamSettings.tsx index 44f6d221..ca45f0da 100644 --- a/src/app/(main)/settings/teams/[id]/TeamSettings.tsx +++ b/src/app/(main)/settings/teams/[id]/TeamSettings.tsx @@ -1,66 +1,49 @@ 'use client'; -import { useEffect, useState } from 'react'; -import { Item, Loading, Tabs, useToasts, Flexbox } from 'react-basics'; +import { useState } from 'react'; +import { Item, Loading, Tabs, Flexbox } from 'react-basics'; +import TeamsContext from 'app/(main)/teams/TeamsContext'; import PageHeader from 'components/layout/PageHeader'; import { ROLES } from 'lib/constants'; -import { useUser } from 'components/hooks'; -import { useApi } from 'components/hooks'; -import { useMessages } from 'components/hooks'; +import { useLogin, useTeam, useMessages } from 'components/hooks'; import TeamEditForm from './TeamEditForm'; import TeamMembers from './TeamMembers'; import TeamWebsites from './TeamWebsites'; import TeamData from './TeamData'; export function TeamSettings({ teamId }: { teamId: string }) { - const { formatMessage, labels, messages } = useMessages(); - const { user } = useUser(); - const [values, setValues] = useState(null); + const { formatMessage, labels } = useMessages(); + const { user } = useLogin(); + const { data: team, isLoading } = useTeam(teamId); const [tab, setTab] = useState('details'); - const { get, useQuery } = useApi(); - const { showToast } = useToasts(); - const { data, isLoading } = useQuery({ - queryKey: ['team', teamId], - queryFn: () => { - if (teamId) { - return get(`/teams/${teamId}`); - } - }, - }); - const canEdit = data?.teamUser?.find( + + if (isLoading) { + return ; + } + + const canEdit = team?.teamUser?.find( ({ userId, role }) => role === ROLES.teamOwner && userId === user.id, ); - const handleSave = data => { - showToast({ message: formatMessage(messages.saved), variant: 'success' }); - setValues(state => ({ ...state, ...data })); - }; - - useEffect(() => { - if (data) { - setValues(data); - } - }, [data]); - - if (isLoading || !values) { - return ; - } - return ( - - - setTab(value)} style={{ marginBottom: 30 }}> - {formatMessage(labels.details)} - {formatMessage(labels.members)} - {formatMessage(labels.websites)} - {formatMessage(labels.data)} - - {tab === 'details' && ( - - )} - {tab === 'members' && } - {tab === 'websites' && } - {canEdit && tab === 'data' && } - + + + + setTab(value)} + style={{ marginBottom: 30 }} + > + {formatMessage(labels.details)} + {formatMessage(labels.members)} + {formatMessage(labels.websites)} + {formatMessage(labels.data)} + + {tab === 'details' && } + {tab === 'members' && } + {tab === 'websites' && } + {canEdit && tab === 'data' && } + + ); } diff --git a/src/app/(main)/settings/teams/[id]/TeamWebsiteRemoveButton.tsx b/src/app/(main)/settings/teams/[id]/TeamWebsiteRemoveButton.tsx index 76ffdae4..336e151a 100644 --- a/src/app/(main)/settings/teams/[id]/TeamWebsiteRemoveButton.tsx +++ b/src/app/(main)/settings/teams/[id]/TeamWebsiteRemoveButton.tsx @@ -1,5 +1,4 @@ -import { useApi } from 'components/hooks'; -import { useMessages } from 'components/hooks'; +import { useApi, useMessages } from 'components/hooks'; import { Icon, Icons, LoadingButton, Text } from 'react-basics'; export function TeamWebsiteRemoveButton({ teamId, websiteId, onSave }) { diff --git a/src/app/(main)/settings/users/UserAddForm.tsx b/src/app/(main)/settings/users/UserAddForm.tsx index 0b15e422..b50d24cd 100644 --- a/src/app/(main)/settings/users/UserAddForm.tsx +++ b/src/app/(main)/settings/users/UserAddForm.tsx @@ -10,9 +10,8 @@ import { SubmitButton, Button, } from 'react-basics'; -import { useApi } from 'components/hooks'; +import { useApi, useMessages } from 'components/hooks'; import { ROLES } from 'lib/constants'; -import { useMessages } from 'components/hooks'; export function UserAddForm({ onSave, onClose }) { const { post, useMutation } = useApi(); diff --git a/src/app/(main)/settings/users/UserDeleteButton.tsx b/src/app/(main)/settings/users/UserDeleteButton.tsx index 197ddbe5..9f1f8459 100644 --- a/src/app/(main)/settings/users/UserDeleteButton.tsx +++ b/src/app/(main)/settings/users/UserDeleteButton.tsx @@ -1,6 +1,5 @@ import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics'; -import { useMessages } from 'components/hooks'; -import { useUser } from 'components/hooks'; +import { useMessages, useLogin } from 'components/hooks'; import UserDeleteForm from './UserDeleteForm'; export function UserDeleteButton({ @@ -13,11 +12,11 @@ export function UserDeleteButton({ onDelete?: () => void; }) { const { formatMessage, labels } = useMessages(); - const { user } = useUser(); + const { user } = useLogin(); return ( - - - + } + onConfirm={handleConfirm} + onClose={onClose} + buttonLabel={formatMessage(labels.delete)} + isLoading={isPending} + error={error} + /> ); } diff --git a/src/app/(main)/settings/users/UserEditForm.tsx b/src/app/(main)/settings/users/UserEditForm.tsx index cc8014d7..70c3061a 100644 --- a/src/app/(main)/settings/users/UserEditForm.tsx +++ b/src/app/(main)/settings/users/UserEditForm.tsx @@ -9,9 +9,8 @@ import { SubmitButton, PasswordField, } from 'react-basics'; -import { useApi } from 'components/hooks'; +import { useApi, useMessages } from 'components/hooks'; import { ROLES } from 'lib/constants'; -import { useMessages } from 'components/hooks'; export function UserEditForm({ userId, @@ -19,8 +18,8 @@ export function UserEditForm({ onSave, }: { userId: string; - data: any[]; - onSave: (data: any) => void; + data: object; + onSave?: (data: any) => void; }) { const { formatMessage, labels, messages } = useMessages(); const { post, useMutation } = useApi(); @@ -44,7 +43,7 @@ export function UserEditForm({ }); }; - const renderValue = value => { + const renderValue = (value: string) => { if (value === ROLES.user) { return formatMessage(labels.user); } diff --git a/src/app/(main)/settings/users/UserWebsites.tsx b/src/app/(main)/settings/users/UserWebsites.tsx deleted file mode 100644 index da7ba7a2..00000000 --- a/src/app/(main)/settings/users/UserWebsites.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import Page from 'components/layout/Page'; -import { useApi } from 'components/hooks'; -import WebsitesTable from 'app/(main)/settings/websites/WebsitesTable'; -import { useFilterQuery } from 'components/hooks'; -import DataTable from 'components/common/DataTable'; - -export function UserWebsites({ userId }) { - const { get } = useApi(); - const queryResult = useFilterQuery({ - queryKey: ['user:websites', userId], - queryFn: (params: any) => get(`/users/${userId}/websites`, params), - }); - const hasData = queryResult.result && queryResult.result.data.length !== 0; - - return ( - - {hasData && ( - - {({ data }) => ( - - )} - - )} - - ); -} - -export default UserWebsites; diff --git a/src/app/(main)/settings/users/UsersDataTable.tsx b/src/app/(main)/settings/users/UsersDataTable.tsx index ed37787d..54fae59d 100644 --- a/src/app/(main)/settings/users/UsersDataTable.tsx +++ b/src/app/(main)/settings/users/UsersDataTable.tsx @@ -1,17 +1,10 @@ 'use client'; -import { useApi } from 'components/hooks'; -import { useFilterQuery } from 'components/hooks'; import DataTable from 'components/common/DataTable'; import UsersTable from './UsersTable'; -import useCache from 'store/cache'; +import useUsers from 'components/hooks/queries/useUsers'; -export function UsersDataTable({ showActions }: { showActions: boolean }) { - const { get } = useApi(); - const modified = useCache((state: any) => state?.users); - const queryResult = useFilterQuery({ - queryKey: ['users', { modified }], - queryFn: (params: { [key: string]: any }) => get(`/admin/users`, params), - }); +export function UsersDataTable({ showActions }: { showActions?: boolean }) { + const queryResult = useUsers(); return ( diff --git a/src/app/(main)/settings/users/UsersTable.tsx b/src/app/(main)/settings/users/UsersTable.tsx index 036c3de6..156a91d4 100644 --- a/src/app/(main)/settings/users/UsersTable.tsx +++ b/src/app/(main)/settings/users/UsersTable.tsx @@ -2,8 +2,7 @@ import { Button, Text, Icon, Icons, GridTable, GridColumn, useBreakpoint } from import { formatDistance } from 'date-fns'; import Link from 'next/link'; import { ROLES } from 'lib/constants'; -import { useMessages } from 'components/hooks'; -import { useLocale } from 'components/hooks'; +import { useMessages, useLocale } from 'components/hooks'; import UserDeleteButton from './UserDeleteButton'; export function UsersTable({ @@ -35,12 +34,16 @@ export function UsersTable({ }) } + + {row => row._count.website} + {showActions && ( {row => { const { id, username } = row; return ( <> + - ); }} diff --git a/src/app/(main)/settings/users/[id]/UserSettings.tsx b/src/app/(main)/settings/users/[id]/UserSettings.tsx index 070b8aa2..a43587d7 100644 --- a/src/app/(main)/settings/users/[id]/UserSettings.tsx +++ b/src/app/(main)/settings/users/[id]/UserSettings.tsx @@ -1,58 +1,28 @@ 'use client'; -import { Key, useEffect, useState } from 'react'; -import { Item, Loading, Tabs, useToasts } from 'react-basics'; +import { Key, useState } from 'react'; +import { Item, Loading, Tabs } from 'react-basics'; import UserEditForm from '../UserEditForm'; import PageHeader from 'components/layout/PageHeader'; -import { useApi } from 'components/hooks'; -import { useMessages } from 'components/hooks'; -import UserWebsites from '../UserWebsites'; +import { useMessages, useUser } from 'components/hooks'; +import UserWebsites from './UserWebsites'; -export function UserSettings({ userId }) { - const { formatMessage, labels, messages } = useMessages(); - const [edit, setEdit] = useState(false); - const [values, setValues] = useState(null); +export function UserSettings({ userId }: { userId: string }) { + const { formatMessage, labels } = useMessages(); const [tab, setTab] = useState('details'); - const { get, useQuery } = useApi(); - const { showToast } = useToasts(); - const { data, isLoading } = useQuery({ - queryKey: ['user', userId], - queryFn: () => { - if (userId) { - return get(`/users/${userId}`); - } - }, - gcTime: 0, - }); + const { data: user, isLoading } = useUser(userId, { gcTime: 0 }); - const handleSave = (data: any) => { - showToast({ message: formatMessage(messages.saved), variant: 'success' }); - if (data) { - setValues(state => ({ ...state, ...data })); - } - - if (edit) { - setEdit(false); - } - }; - - useEffect(() => { - if (data) { - setValues(data); - } - }, [data]); - - if (isLoading || !values) { + if (isLoading) { return ; } return ( <> - + {formatMessage(labels.details)} {formatMessage(labels.websites)} - {tab === 'details' && } + {tab === 'details' && } {tab === 'websites' && } ); diff --git a/src/app/(main)/settings/users/[id]/UserWebsites.tsx b/src/app/(main)/settings/users/[id]/UserWebsites.tsx new file mode 100644 index 00000000..429195cd --- /dev/null +++ b/src/app/(main)/settings/users/[id]/UserWebsites.tsx @@ -0,0 +1,18 @@ +'use client'; +import WebsitesTable from 'app/(main)/settings/websites/WebsitesTable'; +import DataTable from 'components/common/DataTable'; +import { useWebsites } from 'components/hooks'; + +export function UserWebsites({ userId }) { + const queryResult = useWebsites({ userId }); + + return ( + + {({ data }) => ( + + )} + + ); +} + +export default UserWebsites; diff --git a/src/app/(main)/settings/websites/WebsiteSettings.tsx b/src/app/(main)/settings/websites/WebsiteSettings.tsx index 031307b0..791f7e4b 100644 --- a/src/app/(main)/settings/websites/WebsiteSettings.tsx +++ b/src/app/(main)/settings/websites/WebsiteSettings.tsx @@ -1,5 +1,5 @@ 'use client'; -import { useContext, useEffect, useState, Key } from 'react'; +import { useState, Key } from 'react'; import { Item, Tabs, useToasts, Button, Text, Icon, Icons, Loading } from 'react-basics'; import { useRouter } from 'next/navigation'; import Link from 'next/link'; @@ -8,56 +8,42 @@ import WebsiteEditForm from './[id]/WebsiteEditForm'; import WebsiteData from './[id]/WebsiteData'; import TrackingCode from './[id]/TrackingCode'; import ShareUrl from './[id]/ShareUrl'; -import { useApi } from 'components/hooks'; -import { useMessages } from 'components/hooks'; -import SettingsContext from '../SettingsContext'; +import { useWebsite, useMessages } from 'components/hooks'; +import { touch } from 'store/cache'; export function WebsiteSettings({ websiteId, openExternal = false }) { const router = useRouter(); const { formatMessage, labels, messages } = useMessages(); - const { get, useQuery } = useApi(); const { showToast } = useToasts(); - const { websitesUrl, websitesPath, settingsPath } = useContext(SettingsContext); - const { data, isLoading } = useQuery({ - queryKey: ['website', websiteId], - queryFn: () => get(`${websitesUrl}/${websiteId}`), - enabled: !!websiteId, - gcTime: 0, - }); - const [values, setValues] = useState(null); + + const { data: website, isLoading } = useWebsite(websiteId, { gcTime: 0 }); const [tab, setTab] = useState('details'); const showSuccess = () => { showToast({ message: formatMessage(messages.saved), variant: 'success' }); }; - const handleSave = (data: any) => { + const handleSave = () => { showSuccess(); - setValues((state: any) => ({ ...state, ...data })); + touch('websites'); }; const handleReset = async (value: string) => { if (value === 'delete') { - router.push(settingsPath); + router.push('/settings/websites'); } else if (value === 'reset') { showSuccess(); } }; - useEffect(() => { - if (data) { - setValues(data); - } - }, [data]); - - if (isLoading || !values) { + if (isLoading) { return ; } return ( <> - - + + - - + ); } diff --git a/src/app/(main)/settings/websites/[id]/WebsiteResetForm.tsx b/src/app/(main)/settings/websites/[id]/WebsiteResetForm.tsx index 86077c1c..c43f3efb 100644 --- a/src/app/(main)/settings/websites/[id]/WebsiteResetForm.tsx +++ b/src/app/(main)/settings/websites/[id]/WebsiteResetForm.tsx @@ -1,16 +1,5 @@ -import { - Button, - Form, - FormRow, - FormButtons, - FormInput, - SubmitButton, - TextField, -} from 'react-basics'; -import { useApi } from 'components/hooks'; -import { useMessages } from 'components/hooks'; -import { useContext } from 'react'; -import SettingsContext from '../../SettingsContext'; +import { useApi, useMessages } from 'components/hooks'; +import TypeConfirmationForm from 'components/common/TypeConfirmationForm'; const CONFIRM_VALUE = 'RESET'; @@ -23,40 +12,30 @@ export function WebsiteResetForm({ onSave?: () => void; onClose?: () => void; }) { - const { formatMessage, labels, messages, FormattedMessage } = useMessages(); - const { websitesUrl } = useContext(SettingsContext); + const { formatMessage, labels } = useMessages(); const { post, useMutation } = useApi(); - const { mutate, error } = useMutation({ - mutationFn: (data: any) => post(`${websitesUrl}/${websiteId}/reset`, data), + const { mutate, isPending, error } = useMutation({ + mutationFn: (data: any) => post(`/websites/${websiteId}/reset`, data), }); - const handleSubmit = async (data: any) => { - mutate(data, { + const handleConfirm = async () => { + mutate(null, { onSuccess: async () => { - onSave(); - onClose(); + onSave?.(); + onClose?.(); }, }); }; return ( -
-

- {CONFIRM_VALUE} }} - /> -

- - value === CONFIRM_VALUE }}> - - - - - {formatMessage(labels.reset)} - - -
+ ); } diff --git a/src/app/(main)/teams/[id]/websites/TeamWebsites.tsx b/src/app/(main)/teams/[id]/websites/TeamWebsites.tsx deleted file mode 100644 index 443e4b91..00000000 --- a/src/app/(main)/teams/[id]/websites/TeamWebsites.tsx +++ /dev/null @@ -1,14 +0,0 @@ -'use client'; -import { useContext } from 'react'; -import TeamsContext from 'app/(main)/teams/TeamsContext'; -import WebsitesDataTable from 'app/(main)/settings/websites/WebsitesDataTable'; - -export default function TeamWebsites() { - const team = useContext(TeamsContext); - - if (!team) { - return null; - } - - return ; -} diff --git a/src/app/(main)/teams/[id]/websites/page.tsx b/src/app/(main)/teams/[id]/websites/page.tsx index 386c1354..aed17d84 100644 --- a/src/app/(main)/teams/[id]/websites/page.tsx +++ b/src/app/(main)/teams/[id]/websites/page.tsx @@ -1,11 +1,11 @@ -import TeamWebsites from './TeamWebsites'; +import WebsitesDataTable from 'app/(main)/settings/websites/WebsitesDataTable'; import WebsitesHeader from 'app/(main)/settings/websites/WebsitesHeader'; export default function TeamWebsitesPage({ params: { id } }: { params: { id: string } }) { return ( <> - + ); } diff --git a/src/app/(main)/websites/WebsitesBrowse.tsx b/src/app/(main)/websites/WebsitesBrowse.tsx index 612da39b..30e22618 100644 --- a/src/app/(main)/websites/WebsitesBrowse.tsx +++ b/src/app/(main)/websites/WebsitesBrowse.tsx @@ -1,9 +1,9 @@ 'use client'; import WebsitesDataTable from '../settings/websites/WebsitesDataTable'; -import { useUser } from 'components/hooks'; +import { useLogin } from 'components/hooks'; export function WebsitesBrowse() { - const { user } = useUser(); + const { user } = useLogin(); const allowEdit = !process.env.cloudMode; return ; diff --git a/src/components/common/ConfirmDeleteForm.tsx b/src/components/common/ConfirmDeleteForm.tsx deleted file mode 100644 index f6908a77..00000000 --- a/src/components/common/ConfirmDeleteForm.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { useState } from 'react'; -import { Button, LoadingButton, Form, FormButtons } from 'react-basics'; -import { useMessages } from 'components/hooks'; - -export interface ConfirmDeleteFormProps { - name: string; - onConfirm?: () => void; - onClose?: () => void; -} - -export function ConfirmDeleteForm({ name, onConfirm, onClose }: ConfirmDeleteFormProps) { - const [loading, setLoading] = useState(false); - const { formatMessage, labels, messages, FormattedMessage } = useMessages(); - - const handleConfirm = () => { - setLoading(true); - onConfirm(); - }; - - return ( -
-

- {name} }} /> -

- - - {formatMessage(labels.delete)} - - - -
- ); -} - -export default ConfirmDeleteForm; diff --git a/src/components/common/ConfirmationForm.tsx b/src/components/common/ConfirmationForm.tsx new file mode 100644 index 00000000..26b4ff24 --- /dev/null +++ b/src/components/common/ConfirmationForm.tsx @@ -0,0 +1,39 @@ +import { ReactNode } from 'react'; +import { Button, LoadingButton, Form, FormButtons } from 'react-basics'; +import { useMessages } from 'components/hooks'; + +export interface ConfirmationFormProps { + message: ReactNode; + buttonLabel?: ReactNode; + buttonVariant?: 'none' | 'primary' | 'secondary' | 'quiet' | 'danger'; + isLoading?: boolean; + error?: string | Error; + onConfirm?: () => void; + onClose?: () => void; +} + +export function ConfirmationForm({ + message, + buttonLabel, + buttonVariant, + isLoading, + error, + onConfirm, + onClose, +}: ConfirmationFormProps) { + const { formatMessage, labels } = useMessages(); + + return ( +
+

{message}

+ + + {buttonLabel || formatMessage(labels.ok)} + + + +
+ ); +} + +export default ConfirmationForm; diff --git a/src/components/common/TypeConfirmationForm.tsx b/src/components/common/TypeConfirmationForm.tsx new file mode 100644 index 00000000..2dfb2dff --- /dev/null +++ b/src/components/common/TypeConfirmationForm.tsx @@ -0,0 +1,58 @@ +import { + Button, + Form, + FormButtons, + FormRow, + FormInput, + TextField, + SubmitButton, +} from 'react-basics'; +import { useMessages } from 'components/hooks'; + +export function TypeConfirmationForm({ + confirmationValue, + buttonLabel, + buttonVariant, + isLoading, + error, + onConfirm, + onClose, +}: { + confirmationValue: string; + buttonLabel?: string; + buttonVariant?: 'none' | 'primary' | 'secondary' | 'quiet' | 'danger'; + isLoading?: boolean; + error?: string | Error; + onConfirm?: () => void; + onClose?: () => void; +}) { + const { formatMessage, labels, messages, FormattedMessage } = useMessages(); + + if (!confirmationValue) { + return null; + } + + return ( +
+

+ {confirmationValue} }} + /> +

+ + value === confirmationValue }}> + + + + + + {buttonLabel || formatMessage(labels.ok)} + + + +
+ ); +} + +export default TypeConfirmationForm; diff --git a/src/components/hooks/index.js b/src/components/hooks/index.js index 3370d307..cdb05a6d 100644 --- a/src/components/hooks/index.js +++ b/src/components/hooks/index.js @@ -8,7 +8,9 @@ export * from './queries/useShareToken'; export * from './queries/useTeam'; export * from './queries/useTeamWebsites'; export * from './queries/useUser'; +export * from './queries/useUsers'; export * from './queries/useWebsite'; +export * from './queries/useWebsites'; export * from './useCountryNames'; export * from './useDateRange'; export * from './useDocumentClick'; diff --git a/src/components/hooks/queries/useLogin.ts b/src/components/hooks/queries/useLogin.ts index ec8978b2..af9eba85 100644 --- a/src/components/hooks/queries/useLogin.ts +++ b/src/components/hooks/queries/useLogin.ts @@ -1,9 +1,11 @@ +import useStore, { setUser } from 'store/app'; import useApi from './useApi'; -import useUser from './useUser'; + +const selector = (state: { user: any }) => state.user; export function useLogin() { const { get, useQuery } = useApi(); - const { user, setUser } = useUser(); + const user = useStore(selector); const query = useQuery({ queryKey: ['login'], @@ -14,6 +16,7 @@ export function useLogin() { return data; }, + enabled: !user, }); return { user, ...query }; diff --git a/src/components/hooks/queries/useUser.ts b/src/components/hooks/queries/useUser.ts index fb36d1bc..61c22ecd 100644 --- a/src/components/hooks/queries/useUser.ts +++ b/src/components/hooks/queries/useUser.ts @@ -1,11 +1,13 @@ -import useStore, { setUser } from 'store/app'; +import useApi from './useApi'; -const selector = (state: { user: any }) => state.user; - -export function useUser() { - const user = useStore(selector); - - return { user, setUser }; +export function useUser(userId: string, options?: { [key: string]: any }) { + const { get, useQuery } = useApi(); + return useQuery({ + queryKey: ['users', userId], + queryFn: () => get(`/users/${userId}`), + enabled: !!userId, + ...options, + }); } export default useUser; diff --git a/src/components/hooks/queries/useUsers.ts b/src/components/hooks/queries/useUsers.ts new file mode 100644 index 00000000..b1273814 --- /dev/null +++ b/src/components/hooks/queries/useUsers.ts @@ -0,0 +1,19 @@ +import useApi from './useApi'; +import useFilterQuery from './useFilterQuery'; +import useCache from 'store/cache'; + +export function useUsers() { + const { get } = useApi(); + const modified = useCache((state: any) => state?.users); + + return useFilterQuery({ + queryKey: ['users', { modified }], + queryFn: (params: any) => { + return get('/admin/users', { + ...params, + }); + }, + }); +} + +export default useUsers; diff --git a/src/components/hooks/queries/useWebsite.ts b/src/components/hooks/queries/useWebsite.ts index d18e96ba..386607eb 100644 --- a/src/components/hooks/queries/useWebsite.ts +++ b/src/components/hooks/queries/useWebsite.ts @@ -1,11 +1,12 @@ import useApi from './useApi'; -export function useWebsite(websiteId: string) { +export function useWebsite(websiteId: string, options?: { [key: string]: any }) { const { get, useQuery } = useApi(); return useQuery({ queryKey: ['websites', websiteId], queryFn: () => get(`/websites/${websiteId}`), enabled: !!websiteId, + ...options, }); } diff --git a/src/components/hooks/queries/useWebsites.ts b/src/components/hooks/queries/useWebsites.ts index abb0ca4f..575ef236 100644 --- a/src/components/hooks/queries/useWebsites.ts +++ b/src/components/hooks/queries/useWebsites.ts @@ -9,7 +9,7 @@ export function useWebsites({ userId, teamId }: { userId?: string; teamId?: stri return useFilterQuery({ queryKey: ['websites', { userId, teamId, modified }], queryFn: (params: any) => { - return get(teamId ? `/teams/${teamId}/websites` : '/websites', { + return get(teamId ? `/teams/${teamId}/websites` : `/users/${userId}/websites`, { ...params, }); }, diff --git a/src/components/hooks/useDateRange.ts b/src/components/hooks/useDateRange.ts index e7a31930..7e3b8bcb 100644 --- a/src/components/hooks/useDateRange.ts +++ b/src/components/hooks/useDateRange.ts @@ -4,8 +4,8 @@ import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE } from 'lib/constants'; import websiteStore, { setWebsiteDateRange } from 'store/websites'; import appStore, { setDateRange } from 'store/app'; import { DateRange } from 'lib/types'; -import useLocale from './useLocale'; -import { useApi } from 'components/hooks'; +import { useLocale } from './useLocale'; +import { useApi } from './queries/useApi'; export function useDateRange(websiteId?: string) { const { get } = useApi(); diff --git a/src/components/input/ProfileButton.tsx b/src/components/input/ProfileButton.tsx index 17d2b519..c79d4b6f 100644 --- a/src/components/input/ProfileButton.tsx +++ b/src/components/input/ProfileButton.tsx @@ -2,14 +2,14 @@ import { Icon, Button, PopupTrigger, Popup, Menu, Item, Text } from 'react-basic import { useRouter } from 'next/navigation'; import Icons from 'components/icons'; import { useMessages } from 'components/hooks'; -import { useUser } from 'components/hooks'; +import { useLogin } from 'components/hooks'; import { useLocale } from 'components/hooks'; import { CURRENT_VERSION } from 'lib/constants'; import styles from './ProfileButton.module.css'; export function ProfileButton() { const { formatMessage, labels } = useMessages(); - const { user } = useUser(); + const { user } = useLogin(); const router = useRouter(); const { dir } = useLocale(); const cloudMode = Boolean(process.env.cloudMode); diff --git a/src/components/messages.ts b/src/components/messages.ts index 97cb4e1a..15d2c442 100644 --- a/src/components/messages.ts +++ b/src/components/messages.ts @@ -1,6 +1,7 @@ import { defineMessages } from 'react-intl'; export const labels = defineMessages({ + ok: { id: 'label.ok', defaultMessage: 'OK' }, unknown: { id: 'label.unknown', defaultMessage: 'Unknown' }, required: { id: 'label.required', defaultMessage: 'Required' }, save: { id: 'label.save', defaultMessage: 'Save' }, @@ -50,6 +51,7 @@ export const labels = defineMessages({ websiteId: { id: 'label.website-id', defaultMessage: 'Website ID' }, resetWebsite: { id: 'label.reset-website', defaultMessage: 'Reset website' }, deleteWebsite: { id: 'label.delete-website', defaultMessage: 'Delete website' }, + deleteReport: { id: 'label.delete-report', defaultMessage: 'Delete report' }, reset: { id: 'label.reset', defaultMessage: 'Reset' }, addWebsite: { id: 'label.add-website', defaultMessage: 'Add website' }, addMember: { id: 'label.add-member', defaultMessage: 'Add member' }, diff --git a/src/index.ts b/src/index.ts index 7f83248f..9222e5bd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -50,7 +50,7 @@ export * from 'app/(main)/settings/websites/WebsitesTable'; export * from 'app/(main)/settings/SettingsContext'; -export * from 'components/common/ConfirmDeleteForm'; +export * from 'components/common/TypeConfirmationForm'; export * from 'components/common/DataTable'; export * from 'components/common/Empty'; export * from 'components/common/ErrorBoundary'; diff --git a/src/pages/api/admin/users.ts b/src/pages/api/admin/users.ts index d16895b7..3ded1167 100644 --- a/src/pages/api/admin/users.ts +++ b/src/pages/api/admin/users.ts @@ -44,7 +44,20 @@ export default async ( const { page, query, pageSize } = req.query; - const users = await getUsers({ page, query, pageSize: +pageSize || undefined }); + const users = await getUsers( + { page, query, pageSize: +pageSize || undefined }, + { + include: { + _count: { + select: { + website: { + where: { deletedAt: null }, + }, + }, + }, + }, + }, + ); return ok(res, users); } diff --git a/src/queries/admin/user.ts b/src/queries/admin/user.ts index 29d94ec8..51dda2d4 100644 --- a/src/queries/admin/user.ts +++ b/src/queries/admin/user.ts @@ -84,7 +84,7 @@ export async function getUsers( ...pageFilters, ...(options?.include && { include: options.include }), }) - .then(a => { + .then((a: { [x: string]: any; password: any }[]) => { return a.map(({ password, ...rest }) => rest); }); @@ -163,7 +163,7 @@ export async function deleteUser( User, ] > { - const { client } = prisma; + const { client, transaction } = prisma; const cloudMode = process.env.CLOUD_MODE; const websites = await client.website.findMany({ @@ -190,7 +190,7 @@ export async function deleteUser( const teamIds = teams.map(a => a.id); if (cloudMode) { - return client.transaction([ + return transaction([ client.website.updateMany({ data: { deletedAt: new Date(), @@ -209,70 +209,68 @@ export async function deleteUser( ]); } - return client - .transaction([ - client.eventData.deleteMany({ - where: { websiteId: { in: websiteIds } }, - }), - client.websiteEvent.deleteMany({ - where: { websiteId: { in: websiteIds } }, - }), - client.session.deleteMany({ - where: { websiteId: { in: websiteIds } }, - }), - client.teamUser.deleteMany({ - where: { - OR: [ - { - teamId: { - in: teamIds, - }, + return transaction([ + client.eventData.deleteMany({ + where: { websiteId: { in: websiteIds } }, + }), + client.websiteEvent.deleteMany({ + where: { websiteId: { in: websiteIds } }, + }), + client.session.deleteMany({ + where: { websiteId: { in: websiteIds } }, + }), + client.teamUser.deleteMany({ + where: { + OR: [ + { + teamId: { + in: teamIds, }, - { - userId, - }, - ], - }, - }), - client.team.deleteMany({ - where: { - id: { - in: teamIds, }, + { + userId, + }, + ], + }, + }), + client.team.deleteMany({ + where: { + id: { + in: teamIds, }, - }), - client.report.deleteMany({ - where: { - OR: [ - { - websiteId: { - in: websiteIds, - }, + }, + }), + client.report.deleteMany({ + where: { + OR: [ + { + websiteId: { + in: websiteIds, }, - { - userId, - }, - ], - }, - }), - client.website.deleteMany({ - where: { id: { in: websiteIds } }, - }), - client.user.delete({ - where: { - id: userId, - }, - }), - ]) - .then(async (data: any) => { - if (cache.enabled) { - const ids = websites.map(a => a.id); + }, + { + userId, + }, + ], + }, + }), + client.website.deleteMany({ + where: { id: { in: websiteIds } }, + }), + client.user.delete({ + where: { + id: userId, + }, + }), + ]).then(async (data: any) => { + if (cache.enabled) { + const ids = websites.map(a => a.id); - for (let i = 0; i < ids.length; i++) { - await cache.deleteWebsite(`website:${ids[i]}`); - } + for (let i = 0; i < ids.length; i++) { + await cache.deleteWebsite(`website:${ids[i]}`); } + } - return data; - }); + return data; + }); } diff --git a/src/store/cache.ts b/src/store/cache.ts index b0ef4b81..0391455d 100644 --- a/src/store/cache.ts +++ b/src/store/cache.ts @@ -2,8 +2,12 @@ import { create } from 'zustand'; const store = create(() => ({})); -export function setValue(key, value) { +export function setValue(key: string, value: any) { store.setState({ [key]: value }); } +export function touch(key: string) { + setValue(key, Date.now()); +} + export default store;