mirror of
https://github.com/umami-software/umami.git
synced 2026-02-17 02:55:38 +01:00
Fixed imports.
This commit is contained in:
parent
5f4b83b09c
commit
7838204186
41 changed files with 49 additions and 15298 deletions
|
|
@ -4,7 +4,7 @@ import { useWebsite } from '@/components/hooks/useWebsite';
|
|||
import { Share, Edit } from '@/components/icons';
|
||||
import { Favicon } from '@/components/common/Favicon';
|
||||
import { ActiveUsers } from '@/components/metrics/ActiveUsers';
|
||||
import { WebsiteShareForm } from '@/app/(main)/settings/websites/[websiteId]/WebsiteShareForm';
|
||||
import { WebsiteShareForm } from '@/app/(main)/websites/[websiteId]/settings/WebsiteShareForm';
|
||||
import { useMessages, useNavigation } from '@/components/hooks';
|
||||
import { LinkButton } from '@/components/common/LinkButton';
|
||||
|
||||
|
|
|
|||
104
src/app/(main)/websites/[websiteId]/settings/WebsiteData.tsx
Normal file
104
src/app/(main)/websites/[websiteId]/settings/WebsiteData.tsx
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { Button, Modal, DialogTrigger, Dialog, Column } from '@umami/react-zen';
|
||||
import {
|
||||
useLoginQuery,
|
||||
useMessages,
|
||||
useModified,
|
||||
useUserTeamsQuery,
|
||||
useNavigation,
|
||||
} from '@/components/hooks';
|
||||
import { WebsiteDeleteForm } from './WebsiteDeleteForm';
|
||||
import { WebsiteResetForm } from './WebsiteResetForm';
|
||||
import { WebsiteTransferForm } from './WebsiteTransferForm';
|
||||
import { ActionForm } from '@/components/common/ActionForm';
|
||||
import { ROLES } from '@/lib/constants';
|
||||
|
||||
export function WebsiteData({ websiteId, onSave }: { websiteId: string; onSave?: () => void }) {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { user } = useLoginQuery();
|
||||
const { touch } = useModified();
|
||||
const { router, pathname, teamId, renderUrl } = useNavigation();
|
||||
const { data: teams } = useUserTeamsQuery(user.id);
|
||||
const isAdmin = pathname.startsWith('/admin');
|
||||
|
||||
const canTransferWebsite =
|
||||
(
|
||||
(!teamId &&
|
||||
teams?.data?.filter(({ members }) =>
|
||||
members.find(
|
||||
({ role, userId }) =>
|
||||
[ROLES.teamOwner, ROLES.teamManager].includes(role) && userId === user.id,
|
||||
),
|
||||
)) ||
|
||||
[]
|
||||
).length > 0 ||
|
||||
(teamId &&
|
||||
!!teams?.data
|
||||
?.find(({ id }) => id === teamId)
|
||||
?.members.find(({ role, userId }) => role === ROLES.teamOwner && userId === user.id));
|
||||
|
||||
const handleSave = () => {
|
||||
touch('websites');
|
||||
onSave?.();
|
||||
router.push(renderUrl(`/settings/websites`));
|
||||
};
|
||||
|
||||
const handleReset = async () => {
|
||||
onSave?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<Column gap="6">
|
||||
{!isAdmin && (
|
||||
<ActionForm
|
||||
label={formatMessage(labels.transferWebsite)}
|
||||
description={formatMessage(messages.transferWebsite)}
|
||||
>
|
||||
<DialogTrigger>
|
||||
<Button isDisabled={!canTransferWebsite}>{formatMessage(labels.transfer)}</Button>
|
||||
<Modal>
|
||||
<Dialog title={formatMessage(labels.transferWebsite)} style={{ width: 400 }}>
|
||||
{({ close }) => (
|
||||
<WebsiteTransferForm websiteId={websiteId} onSave={handleSave} onClose={close} />
|
||||
)}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</DialogTrigger>
|
||||
</ActionForm>
|
||||
)}
|
||||
|
||||
<ActionForm
|
||||
label={formatMessage(labels.resetWebsite)}
|
||||
description={formatMessage(messages.resetWebsiteWarning)}
|
||||
>
|
||||
<DialogTrigger>
|
||||
<Button>{formatMessage(labels.reset)}</Button>
|
||||
<Modal>
|
||||
<Dialog title={formatMessage(labels.resetWebsite)} style={{ width: 400 }}>
|
||||
{({ close }) => (
|
||||
<WebsiteResetForm websiteId={websiteId} onSave={handleReset} onClose={close} />
|
||||
)}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</DialogTrigger>
|
||||
</ActionForm>
|
||||
|
||||
<ActionForm
|
||||
label={formatMessage(labels.deleteWebsite)}
|
||||
description={formatMessage(messages.deleteWebsiteWarning)}
|
||||
>
|
||||
<DialogTrigger>
|
||||
<Button data-test="button-delete" variant="danger">
|
||||
{formatMessage(labels.delete)}
|
||||
</Button>
|
||||
<Modal>
|
||||
<Dialog title={formatMessage(labels.deleteWebsite)} style={{ width: 400 }}>
|
||||
{({ close }) => (
|
||||
<WebsiteDeleteForm websiteId={websiteId} onSave={handleSave} onClose={close} />
|
||||
)}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</DialogTrigger>
|
||||
</ActionForm>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import { useApi, useMessages, useModified } from '@/components/hooks';
|
||||
import { TypeConfirmationForm } from '@/components/common/TypeConfirmationForm';
|
||||
|
||||
const CONFIRM_VALUE = 'DELETE';
|
||||
|
||||
export function WebsiteDeleteForm({
|
||||
websiteId,
|
||||
onSave,
|
||||
onClose,
|
||||
}: {
|
||||
websiteId: string;
|
||||
onSave?: () => void;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { del, useMutation } = useApi();
|
||||
const { mutate, isPending, error } = useMutation({
|
||||
mutationFn: () => del(`/websites/${websiteId}`),
|
||||
});
|
||||
const { touch } = useModified();
|
||||
|
||||
const handleConfirm = async () => {
|
||||
mutate(null, {
|
||||
onSuccess: async () => {
|
||||
touch('websites');
|
||||
touch(`websites:${websiteId}`);
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<TypeConfirmationForm
|
||||
confirmationValue={CONFIRM_VALUE}
|
||||
onConfirm={handleConfirm}
|
||||
onClose={onClose}
|
||||
isLoading={isPending}
|
||||
error={error}
|
||||
buttonLabel={formatMessage(labels.delete)}
|
||||
buttonVariant="danger"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import { useContext } from 'react';
|
||||
import {
|
||||
FormSubmitButton,
|
||||
Form,
|
||||
FormField,
|
||||
FormButtons,
|
||||
TextField,
|
||||
useToast,
|
||||
} from '@umami/react-zen';
|
||||
import { useApi, useMessages, useModified } from '@/components/hooks';
|
||||
import { DOMAIN_REGEX } from '@/lib/constants';
|
||||
import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
|
||||
|
||||
export function WebsiteEditForm({ websiteId, onSave }: { websiteId: string; onSave?: () => void }) {
|
||||
const website = useContext(WebsiteContext);
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { post, useMutation } = useApi();
|
||||
const { toast } = useToast();
|
||||
const { touch } = useModified();
|
||||
|
||||
const { mutate, error } = useMutation({
|
||||
mutationFn: (data: any) => post(`/websites/${websiteId}`, data),
|
||||
});
|
||||
|
||||
const handleSubmit = async (data: any) => {
|
||||
mutate(data, {
|
||||
onSuccess: async () => {
|
||||
toast(formatMessage(messages.saved));
|
||||
touch(`website:${website.id}`);
|
||||
onSave?.();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit} error={error} values={website}>
|
||||
<FormField name="id" label={formatMessage(labels.websiteId)}>
|
||||
<TextField data-test="text-field-websiteId" value={website?.id} isReadOnly allowCopy />
|
||||
</FormField>
|
||||
<FormField
|
||||
label={formatMessage(labels.name)}
|
||||
data-test="input-name"
|
||||
name="name"
|
||||
rules={{ required: formatMessage(labels.required) }}
|
||||
>
|
||||
<TextField />
|
||||
</FormField>
|
||||
<FormField
|
||||
label={formatMessage(labels.domain)}
|
||||
data-test="input-domain"
|
||||
name="domain"
|
||||
rules={{
|
||||
required: formatMessage(labels.required),
|
||||
pattern: {
|
||||
value: DOMAIN_REGEX,
|
||||
message: formatMessage(messages.invalidDomain),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TextField />
|
||||
</FormField>
|
||||
<FormButtons>
|
||||
<FormSubmitButton data-test="button-submit" variant="primary">
|
||||
{formatMessage(labels.save)}
|
||||
</FormSubmitButton>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import { useApi, useMessages } from '@/components/hooks';
|
||||
import { TypeConfirmationForm } from '@/components/common/TypeConfirmationForm';
|
||||
|
||||
const CONFIRM_VALUE = 'RESET';
|
||||
|
||||
export function WebsiteResetForm({
|
||||
websiteId,
|
||||
onSave,
|
||||
onClose,
|
||||
}: {
|
||||
websiteId: string;
|
||||
onSave?: () => void;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { post, useMutation } = useApi();
|
||||
const { mutate, isPending, error } = useMutation({
|
||||
mutationFn: (data: any) => post(`/websites/${websiteId}/reset`, data),
|
||||
});
|
||||
|
||||
const handleConfirm = async () => {
|
||||
mutate(null, {
|
||||
onSuccess: async () => {
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<TypeConfirmationForm
|
||||
confirmationValue={CONFIRM_VALUE}
|
||||
onConfirm={handleConfirm}
|
||||
onClose={onClose}
|
||||
isLoading={isPending}
|
||||
error={error}
|
||||
buttonLabel={formatMessage(labels.reset)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { useContext } from 'react';
|
||||
import { Tabs, TabList, Tab, TabPanel } from '@umami/react-zen';
|
||||
import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { WebsiteShareForm } from './WebsiteShareForm';
|
||||
import { WebsiteTrackingCode } from './WebsiteTrackingCode';
|
||||
import { WebsiteData } from './WebsiteData';
|
||||
import { WebsiteEditForm } from './WebsiteEditForm';
|
||||
|
||||
export function WebsiteSettings({ websiteId }: { websiteId: string; openExternal?: boolean }) {
|
||||
const website = useContext(WebsiteContext);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<Tabs>
|
||||
<TabList>
|
||||
<Tab id="details">{formatMessage(labels.details)}</Tab>
|
||||
<Tab id="tracking">{formatMessage(labels.trackingCode)}</Tab>
|
||||
<Tab id="share"> {formatMessage(labels.shareUrl)}</Tab>
|
||||
<Tab id="manage">{formatMessage(labels.manage)}</Tab>
|
||||
</TabList>
|
||||
<TabPanel id="details" style={{ width: 500 }}>
|
||||
<WebsiteEditForm websiteId={websiteId} />
|
||||
</TabPanel>
|
||||
<TabPanel id="tracking">
|
||||
<WebsiteTrackingCode websiteId={websiteId} />
|
||||
</TabPanel>
|
||||
<TabPanel id="share" style={{ width: 500 }}>
|
||||
<WebsiteShareForm websiteId={websiteId} shareId={website.shareId} />
|
||||
</TabPanel>
|
||||
<TabPanel id="manage">
|
||||
<WebsiteData websiteId={websiteId} />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { useContext } from 'react';
|
||||
import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
|
||||
import { PageHeader } from '@/components/common/PageHeader';
|
||||
import { Globe } from '@/components/icons';
|
||||
|
||||
export function WebsiteSettingsHeader() {
|
||||
const website = useContext(WebsiteContext);
|
||||
|
||||
return <PageHeader title={website?.name} icon={<Globe />} />;
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
import {
|
||||
Form,
|
||||
FormButtons,
|
||||
TextField,
|
||||
Button,
|
||||
Switch,
|
||||
FormSubmitButton,
|
||||
Column,
|
||||
Label,
|
||||
useToast,
|
||||
Row,
|
||||
} from '@umami/react-zen';
|
||||
import { useState } from 'react';
|
||||
import { getRandomChars } from '@/lib/crypto';
|
||||
import { useApi, useMessages, useModified } from '@/components/hooks';
|
||||
|
||||
const generateId = () => getRandomChars(16);
|
||||
|
||||
export interface WebsiteShareFormProps {
|
||||
websiteId: string;
|
||||
shareId?: string;
|
||||
onSave?: () => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export function WebsiteShareForm({ websiteId, shareId, onSave, onClose }: WebsiteShareFormProps) {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const [id, setId] = useState(shareId);
|
||||
const { post, useMutation } = useApi();
|
||||
const { mutate, error, isPending } = useMutation({
|
||||
mutationFn: (data: any) => post(`/websites/${websiteId}`, data),
|
||||
});
|
||||
const { touch } = useModified();
|
||||
const { toast } = useToast();
|
||||
|
||||
const url = `${window?.location.origin || ''}${process.env.basePath || ''}/share/${id}`;
|
||||
|
||||
const handleGenerate = () => {
|
||||
setId(generateId());
|
||||
};
|
||||
|
||||
const handleSwitch = () => {
|
||||
setId(id ? null : generateId());
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const data = {
|
||||
shareId: id,
|
||||
};
|
||||
mutate(data, {
|
||||
onSuccess: async () => {
|
||||
toast(formatMessage(messages.saved));
|
||||
touch(`website:${websiteId}`);
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSave} error={error} values={{ url }}>
|
||||
<Column gap>
|
||||
<Switch isSelected={!!id} onChange={handleSwitch}>
|
||||
{formatMessage(labels.enableShareUrl)}
|
||||
</Switch>
|
||||
{id && (
|
||||
<Column>
|
||||
<Label>{formatMessage(labels.shareUrl)}</Label>
|
||||
<TextField value={url} isReadOnly allowCopy />
|
||||
</Column>
|
||||
)}
|
||||
<FormButtons justifyContent="space-between">
|
||||
<Row>
|
||||
{id && <Button onPress={handleGenerate}>{formatMessage(labels.regenerate)}</Button>}
|
||||
</Row>
|
||||
<Row alignItems="center" gap>
|
||||
{onClose && <Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>}
|
||||
<FormSubmitButton isDisabled={false} isLoading={isPending}>
|
||||
{formatMessage(labels.save)}
|
||||
</FormSubmitButton>
|
||||
</Row>
|
||||
</FormButtons>
|
||||
</Column>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { TextField, Text, Column } from '@umami/react-zen';
|
||||
import { useMessages, useConfig } from '@/components/hooks';
|
||||
|
||||
const SCRIPT_NAME = 'script.js';
|
||||
|
||||
export function WebsiteTrackingCode({
|
||||
websiteId,
|
||||
hostUrl,
|
||||
}: {
|
||||
websiteId: string;
|
||||
hostUrl?: string;
|
||||
}) {
|
||||
const { formatMessage, messages } = useMessages();
|
||||
const config = useConfig();
|
||||
|
||||
const trackerScriptName =
|
||||
config?.trackerScriptName?.split(',')?.map((n: string) => n.trim())?.[0] || SCRIPT_NAME;
|
||||
|
||||
const url = trackerScriptName?.startsWith('http')
|
||||
? trackerScriptName
|
||||
: `${hostUrl || window?.location.origin || ''}${
|
||||
process.env.basePath || ''
|
||||
}/${trackerScriptName}`;
|
||||
|
||||
const code = `<script defer src="${url}" data-website-id="${websiteId}"></script>`;
|
||||
|
||||
return (
|
||||
<Column gap>
|
||||
<Text>{formatMessage(messages.trackingCode)}</Text>
|
||||
<TextField value={code} isReadOnly allowCopy asTextArea resize="none" />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
import { Key, useContext, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormButtons,
|
||||
FormField,
|
||||
FormSubmitButton,
|
||||
Loading,
|
||||
Select,
|
||||
ListItem,
|
||||
Text,
|
||||
} from '@umami/react-zen';
|
||||
import { useApi, useLoginQuery, useMessages, useUserTeamsQuery } from '@/components/hooks';
|
||||
import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
|
||||
import { ROLES } from '@/lib/constants';
|
||||
|
||||
export function WebsiteTransferForm({
|
||||
websiteId,
|
||||
onSave,
|
||||
onClose,
|
||||
}: {
|
||||
websiteId: string;
|
||||
onSave?: () => void;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
const { user } = useLoginQuery();
|
||||
const website = useContext(WebsiteContext);
|
||||
const [teamId, setTeamId] = useState<string>(null);
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { post, useMutation } = useApi();
|
||||
const { mutate, error } = useMutation({
|
||||
mutationFn: (data: any) => post(`/websites/${websiteId}/transfer`, data),
|
||||
});
|
||||
const { data: teams, isLoading } = useUserTeamsQuery(user.id);
|
||||
const isTeamWebsite = !!website?.teamId;
|
||||
|
||||
const items =
|
||||
teams?.data?.filter(({ teamUser }) =>
|
||||
teamUser.find(
|
||||
({ role, userId }) =>
|
||||
[ROLES.teamOwner, ROLES.teamManager].includes(role) && userId === user.id,
|
||||
),
|
||||
) || [];
|
||||
|
||||
const handleSubmit = async () => {
|
||||
mutate(
|
||||
{
|
||||
userId: website.teamId ? user.id : undefined,
|
||||
teamId: website.userId ? teamId : undefined,
|
||||
},
|
||||
{
|
||||
onSuccess: async () => {
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const handleChange = (key: Key) => {
|
||||
setTeamId(key as string);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading icon="dots" position="center" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit} error={error} values={{ teamId }}>
|
||||
<Text>
|
||||
{formatMessage(
|
||||
isTeamWebsite ? messages.transferTeamWebsiteToUser : messages.transferUserWebsiteToTeam,
|
||||
)}
|
||||
</Text>
|
||||
<FormField name="teamId">
|
||||
{!isTeamWebsite && (
|
||||
<Select onSelectionChange={handleChange} selectedKey={teamId}>
|
||||
{items.map(({ id, name }) => {
|
||||
return (
|
||||
<ListItem key={`${id}!!!!`} id={`${id}????`}>
|
||||
{name}
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
)}
|
||||
</FormField>
|
||||
<FormButtons>
|
||||
<Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||
<FormSubmitButton variant="primary" isDisabled={!isTeamWebsite && !teamId}>
|
||||
{formatMessage(labels.transfer)}
|
||||
</FormSubmitButton>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue