mirror of
https://github.com/umami-software/umami.git
synced 2026-02-13 17:15:37 +01:00
Merge c3332552d7 into 860e6390f1
This commit is contained in:
commit
d18c069fe5
9 changed files with 212 additions and 53 deletions
|
|
@ -0,0 +1,5 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "link"
|
||||||
|
ADD COLUMN "og_description" VARCHAR(500),
|
||||||
|
ADD COLUMN "og_image_url" VARCHAR(500),
|
||||||
|
ADD COLUMN "og_title" VARCHAR(500);
|
||||||
|
|
@ -277,15 +277,18 @@ model Revenue {
|
||||||
}
|
}
|
||||||
|
|
||||||
model Link {
|
model Link {
|
||||||
id String @id() @unique() @map("link_id") @db.Uuid
|
id String @id() @unique() @map("link_id") @db.Uuid
|
||||||
name String @db.VarChar(100)
|
name String @db.VarChar(100)
|
||||||
url String @db.VarChar(500)
|
url String @db.VarChar(500)
|
||||||
slug String @unique() @db.VarChar(100)
|
slug String @unique() @db.VarChar(100)
|
||||||
userId String? @map("user_id") @db.Uuid
|
ogTitle String? @map("og_title") @db.VarChar(500)
|
||||||
teamId String? @map("team_id") @db.Uuid
|
ogDescription String? @map("og_description") @db.VarChar(500)
|
||||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
ogImageUrl String? @map("og_image_url") @db.VarChar(500)
|
||||||
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
|
userId String? @map("user_id") @db.Uuid
|
||||||
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
|
teamId String? @map("team_id") @db.Uuid
|
||||||
|
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
|
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
|
||||||
|
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
|
||||||
|
|
||||||
user User? @relation("user", fields: [userId], references: [id])
|
user User? @relation("user", fields: [userId], references: [id])
|
||||||
team Team? @relation(fields: [teamId], references: [id])
|
team Team? @relation(fields: [teamId], references: [id])
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
import { isbot } from 'isbot';
|
||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { POST } from '@/app/api/send/route';
|
import { POST } from '@/app/api/send/route';
|
||||||
import type { Link } from '@/generated/prisma/client';
|
import type { Link } from '@/generated/prisma/client';
|
||||||
|
|
@ -7,6 +8,23 @@ import redis from '@/lib/redis';
|
||||||
import { notFound } from '@/lib/response';
|
import { notFound } from '@/lib/response';
|
||||||
import { findLink } from '@/queries/prisma';
|
import { findLink } from '@/queries/prisma';
|
||||||
|
|
||||||
|
function escapeHtml(str: string): string {
|
||||||
|
return str
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function metaTag(property: string, content: string | undefined, isName = false): string {
|
||||||
|
if (!content) return '';
|
||||||
|
const escaped = escapeHtml(content);
|
||||||
|
return isName
|
||||||
|
? `<meta name="${property}" content="${escaped}">`
|
||||||
|
: `<meta property="${property}" content="${escaped}">`;
|
||||||
|
}
|
||||||
|
|
||||||
export async function GET(request: Request, { params }: { params: Promise<{ slug: string }> }) {
|
export async function GET(request: Request, { params }: { params: Promise<{ slug: string }> }) {
|
||||||
const { slug } = await params;
|
const { slug } = await params;
|
||||||
|
|
||||||
|
|
@ -40,6 +58,48 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userAgent = request.headers.get('user-agent') || '';
|
||||||
|
|
||||||
|
if (isbot(userAgent)) {
|
||||||
|
const ogTitle = link.ogTitle || link.name;
|
||||||
|
const ogDescription = link.ogDescription || undefined;
|
||||||
|
const ogImageUrl = link.ogImageUrl || undefined;
|
||||||
|
const twitterCard = ogImageUrl ? 'summary_large_image' : 'summary';
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>${escapeHtml(ogTitle)}</title>
|
||||||
|
${metaTag('title', ogTitle, true)}
|
||||||
|
${metaTag('description', ogDescription, true)}
|
||||||
|
${metaTag('og:type', 'website')}
|
||||||
|
${metaTag('og:site_name', 'Umami')}
|
||||||
|
${metaTag('og:title', ogTitle)}
|
||||||
|
${metaTag('og:url', request.url)}
|
||||||
|
${metaTag('og:description', ogDescription)}
|
||||||
|
${metaTag('og:image', ogImageUrl)}
|
||||||
|
<meta name="twitter:card" content="${twitterCard}">
|
||||||
|
${metaTag('twitter:title', ogTitle, true)}
|
||||||
|
${metaTag('twitter:description', ogDescription, true)}
|
||||||
|
${metaTag('twitter:image', ogImageUrl, true)}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>Redirecting to ${escapeHtml(link.url)}...</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'content-type': 'text/html',
|
||||||
|
'cache-control': 's-maxage=300, stale-while-revalidate',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
type: 'event',
|
type: 'event',
|
||||||
payload: {
|
payload: {
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,10 @@ import {
|
||||||
Row,
|
Row,
|
||||||
TextField,
|
TextField,
|
||||||
} from '@umami/react-zen';
|
} from '@umami/react-zen';
|
||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useConfig, useLinkQuery, useMessages } from '@/components/hooks';
|
import { useConfig, useLinkQuery, useMessages } from '@/components/hooks';
|
||||||
import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery';
|
import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery';
|
||||||
import { RefreshCw } from '@/components/icons';
|
import { ChevronDown, ChevronRight, RefreshCw } from '@/components/icons';
|
||||||
import { LINKS_URL } from '@/lib/constants';
|
import { LINKS_URL } from '@/lib/constants';
|
||||||
import { getRandomChars } from '@/lib/generate';
|
import { getRandomChars } from '@/lib/generate';
|
||||||
import { isValidUrl } from '@/lib/url';
|
import { isValidUrl } from '@/lib/url';
|
||||||
|
|
@ -42,10 +42,15 @@ export function LinkEditForm({
|
||||||
const { linksUrl } = useConfig();
|
const { linksUrl } = useConfig();
|
||||||
const hostUrl = linksUrl || LINKS_URL;
|
const hostUrl = linksUrl || LINKS_URL;
|
||||||
const { data, isLoading } = useLinkQuery(linkId);
|
const { data, isLoading } = useLinkQuery(linkId);
|
||||||
const [slug, setSlug] = useState(generateId());
|
const [initialSlug] = useState(() => generateId());
|
||||||
|
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||||
|
|
||||||
const handleSubmit = async (data: any) => {
|
const handleSubmit = async (formData: any) => {
|
||||||
await mutateAsync(data, {
|
const { slug: formSlug, ...rest } = formData;
|
||||||
|
// Only include slug if creating new link or if it was modified
|
||||||
|
const payload = !linkId || formSlug !== data?.slug ? formData : rest;
|
||||||
|
|
||||||
|
await mutateAsync(payload, {
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
toast(formatMessage(messages.saved));
|
toast(formatMessage(messages.saved));
|
||||||
touch('links');
|
touch('links');
|
||||||
|
|
@ -55,13 +60,7 @@ export function LinkEditForm({
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSlug = () => {
|
const handleSlug = () => generateId();
|
||||||
const slug = generateId();
|
|
||||||
|
|
||||||
setSlug(slug);
|
|
||||||
|
|
||||||
return slug;
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkUrl = (url: string) => {
|
const checkUrl = (url: string) => {
|
||||||
if (!isValidUrl(url)) {
|
if (!isValidUrl(url)) {
|
||||||
|
|
@ -70,19 +69,35 @@ export function LinkEditForm({
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
setSlug(data.slug);
|
|
||||||
}
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
if (linkId && isLoading) {
|
if (linkId && isLoading) {
|
||||||
return <Loading placement="absolute" />;
|
return <Loading placement="absolute" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={handleSubmit} error={getErrorMessage(error)} defaultValues={{ slug, ...data }}>
|
<Form
|
||||||
{({ setValue }) => {
|
onSubmit={handleSubmit}
|
||||||
|
error={getErrorMessage(error)}
|
||||||
|
{...(linkId
|
||||||
|
? {
|
||||||
|
values: {
|
||||||
|
...data,
|
||||||
|
ogTitle: data?.ogTitle || '',
|
||||||
|
ogDescription: data?.ogDescription || '',
|
||||||
|
ogImageUrl: data?.ogImageUrl || '',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
defaultValues: {
|
||||||
|
slug: initialSlug,
|
||||||
|
ogTitle: '',
|
||||||
|
ogDescription: '',
|
||||||
|
ogImageUrl: '',
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{({ setValue, watch }) => {
|
||||||
|
const currentSlug = watch('slug') ?? initialSlug;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormField
|
<FormField
|
||||||
|
|
@ -101,25 +116,15 @@ export function LinkEditForm({
|
||||||
<TextField placeholder="https://example.com" autoComplete="off" />
|
<TextField placeholder="https://example.com" autoComplete="off" />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField
|
|
||||||
name="slug"
|
|
||||||
rules={{
|
|
||||||
required: formatMessage(labels.required),
|
|
||||||
}}
|
|
||||||
style={{ display: 'none' }}
|
|
||||||
>
|
|
||||||
<input type="hidden" />
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<Column>
|
<Column>
|
||||||
<Label>{formatMessage(labels.link)}</Label>
|
<Label>{formatMessage(labels.link)}</Label>
|
||||||
<Row alignItems="center" gap>
|
<Row alignItems="center" gap>
|
||||||
<TextField
|
<TextField
|
||||||
value={`${hostUrl}/${slug}`}
|
value={`${hostUrl}/${currentSlug}`}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
isReadOnly
|
isReadOnly
|
||||||
allowCopy
|
allowCopy
|
||||||
style={{ width: '100%' }}
|
style={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="quiet"
|
variant="quiet"
|
||||||
|
|
@ -132,6 +137,44 @@ export function LinkEditForm({
|
||||||
</Row>
|
</Row>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
|
<Row
|
||||||
|
alignItems="center"
|
||||||
|
gap="2"
|
||||||
|
style={{ cursor: 'pointer', userSelect: 'none' }}
|
||||||
|
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||||
|
>
|
||||||
|
<Icon size="sm">{showAdvanced ? <ChevronDown /> : <ChevronRight />}</Icon>
|
||||||
|
<Label style={{ cursor: 'pointer' }}>{formatMessage(labels.advanced)}</Label>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{showAdvanced && (
|
||||||
|
<Column gap="3">
|
||||||
|
<FormField label={formatMessage(labels.title)} name="ogTitle">
|
||||||
|
<TextField autoComplete="off" />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label={formatMessage(labels.description)} name="ogDescription">
|
||||||
|
<TextField autoComplete="off" />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label={formatMessage(labels.imageUrl)} name="ogImageUrl">
|
||||||
|
<TextField autoComplete="off" />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
label={formatMessage(labels.path)}
|
||||||
|
name="slug"
|
||||||
|
rules={{
|
||||||
|
required: formatMessage(labels.required),
|
||||||
|
minLength: { value: 4, message: formatMessage(labels.tooShort) },
|
||||||
|
maxLength: { value: 100, message: formatMessage(labels.tooLong) },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextField autoComplete="off" minLength={4} maxLength={100} />
|
||||||
|
</FormField>
|
||||||
|
</Column>
|
||||||
|
)}
|
||||||
|
|
||||||
<Row justifyContent="flex-end" paddingTop="3" gap="3">
|
<Row justifyContent="flex-end" paddingTop="3" gap="3">
|
||||||
{onClose && (
|
{onClose && (
|
||||||
<Button isDisabled={isPending} onPress={onClose}>
|
<Button isDisabled={isPending} onPress={onClose}>
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,10 @@ export async function POST(request: Request, { params }: { params: Promise<{ lin
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
url: z.string().optional(),
|
url: z.string().optional(),
|
||||||
slug: z.string().min(8).optional(),
|
slug: z.string().min(4).optional(),
|
||||||
|
ogTitle: z.string().max(500).optional(),
|
||||||
|
ogDescription: z.string().max(500).optional(),
|
||||||
|
ogImageUrl: z.union([z.string().max(500).pipe(z.url()), z.literal('')]).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { auth, body, error } = await parseRequest(request, schema);
|
const { auth, body, error } = await parseRequest(request, schema);
|
||||||
|
|
@ -36,14 +39,21 @@ export async function POST(request: Request, { params }: { params: Promise<{ lin
|
||||||
}
|
}
|
||||||
|
|
||||||
const { linkId } = await params;
|
const { linkId } = await params;
|
||||||
const { name, url, slug } = body;
|
const { name, url, slug, ogTitle, ogDescription, ogImageUrl } = body;
|
||||||
|
|
||||||
if (!(await canUpdateLink(auth, linkId))) {
|
if (!(await canUpdateLink(auth, linkId))) {
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await updateLink(linkId, { name, url, slug });
|
const result = await updateLink(linkId, {
|
||||||
|
name,
|
||||||
|
url,
|
||||||
|
slug,
|
||||||
|
ogTitle,
|
||||||
|
ogDescription,
|
||||||
|
ogImageUrl,
|
||||||
|
});
|
||||||
|
|
||||||
return Response.json(result);
|
return Response.json(result);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,10 @@ export async function POST(request: Request) {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
name: z.string().max(100),
|
name: z.string().max(100),
|
||||||
url: z.string().max(500),
|
url: z.string().max(500),
|
||||||
slug: z.string().max(100),
|
slug: z.string().min(4).max(100),
|
||||||
|
ogTitle: z.string().max(500).optional(),
|
||||||
|
ogDescription: z.string().max(500).optional(),
|
||||||
|
ogImageUrl: z.union([z.string().max(500).pipe(z.url()), z.literal('')]).optional(),
|
||||||
teamId: z.string().nullable().optional(),
|
teamId: z.string().nullable().optional(),
|
||||||
id: z.uuid().nullable().optional(),
|
id: z.uuid().nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
@ -40,7 +43,7 @@ export async function POST(request: Request) {
|
||||||
return error();
|
return error();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id, name, url, slug, teamId } = body;
|
const { id, name, url, slug, ogTitle, ogDescription, ogImageUrl, teamId } = body;
|
||||||
|
|
||||||
if ((teamId && !(await canCreateTeamWebsite(auth, teamId))) || !(await canCreateWebsite(auth))) {
|
if ((teamId && !(await canCreateTeamWebsite(auth, teamId))) || !(await canCreateWebsite(auth))) {
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
|
|
@ -51,6 +54,9 @@ export async function POST(request: Request) {
|
||||||
name,
|
name,
|
||||||
url,
|
url,
|
||||||
slug,
|
slug,
|
||||||
|
ogTitle,
|
||||||
|
ogDescription,
|
||||||
|
ogImageUrl,
|
||||||
teamId,
|
teamId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
14
src/components/common/ExternalLink.module.css
Normal file
14
src/components/common/ExternalLink.module.css
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
.link {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconLink {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--base-color-10);
|
||||||
|
}
|
||||||
|
|
@ -1,23 +1,36 @@
|
||||||
import { Icon, Row, Text } from '@umami/react-zen';
|
'use client';
|
||||||
|
import { Icon, Row, Text, useToast } from '@umami/react-zen';
|
||||||
import Link, { type LinkProps } from 'next/link';
|
import Link, { type LinkProps } from 'next/link';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
import { useMessages } from '@/components/hooks';
|
||||||
import { ExternalLink as LinkIcon } from '@/components/icons';
|
import { ExternalLink as LinkIcon } from '@/components/icons';
|
||||||
|
import styles from './ExternalLink.module.css';
|
||||||
|
|
||||||
export function ExternalLink({
|
export function ExternalLink({
|
||||||
href,
|
href,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: LinkProps & { href: string; children: ReactNode }) {
|
}: Omit<LinkProps, 'href'> & { href: string; children: ReactNode }) {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
|
const handleCopy = () => {
|
||||||
|
navigator.clipboard.writeText(href);
|
||||||
|
toast(formatMessage(labels.copied));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row alignItems="center" overflow="hidden" gap>
|
<Row alignItems="center" overflow="hidden" gap>
|
||||||
<Text title={href} truncate>
|
<Text title={href} truncate>
|
||||||
<Link {...props} href={href} target="_blank">
|
<span onClick={handleCopy} className={styles.link}>
|
||||||
{children}
|
{children}
|
||||||
</Link>
|
</span>
|
||||||
</Text>
|
</Text>
|
||||||
<Icon size="sm" strokeColor="muted">
|
<Link {...props} href={href} target="_blank" className={styles.iconLink}>
|
||||||
<LinkIcon />
|
<Icon>
|
||||||
</Icon>
|
<LinkIcon />
|
||||||
|
</Icon>
|
||||||
|
</Link>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ export const labels = defineMessages({
|
||||||
ok: { id: 'label.ok', defaultMessage: 'OK' },
|
ok: { id: 'label.ok', defaultMessage: 'OK' },
|
||||||
unknown: { id: 'label.unknown', defaultMessage: 'Unknown' },
|
unknown: { id: 'label.unknown', defaultMessage: 'Unknown' },
|
||||||
required: { id: 'label.required', defaultMessage: 'Required' },
|
required: { id: 'label.required', defaultMessage: 'Required' },
|
||||||
|
tooShort: { id: 'label.too-short', defaultMessage: 'Too short' },
|
||||||
|
tooLong: { id: 'label.too-long', defaultMessage: 'Too long' },
|
||||||
save: { id: 'label.save', defaultMessage: 'Save' },
|
save: { id: 'label.save', defaultMessage: 'Save' },
|
||||||
cancel: { id: 'label.cancel', defaultMessage: 'Cancel' },
|
cancel: { id: 'label.cancel', defaultMessage: 'Cancel' },
|
||||||
continue: { id: 'label.continue', defaultMessage: 'Continue' },
|
continue: { id: 'label.continue', defaultMessage: 'Continue' },
|
||||||
|
|
@ -355,6 +357,7 @@ export const labels = defineMessages({
|
||||||
saveCohort: { id: 'label.save-cohort', defaultMessage: 'Save as cohort' },
|
saveCohort: { id: 'label.save-cohort', defaultMessage: 'Save as cohort' },
|
||||||
analysis: { id: 'label.analysis', defaultMessage: 'Analysis' },
|
analysis: { id: 'label.analysis', defaultMessage: 'Analysis' },
|
||||||
destinationUrl: { id: 'label.destination-url', defaultMessage: 'Destination URL' },
|
destinationUrl: { id: 'label.destination-url', defaultMessage: 'Destination URL' },
|
||||||
|
imageUrl: { id: 'label.image-url', defaultMessage: 'Image URL' },
|
||||||
audience: { id: 'label.audience', defaultMessage: 'Audience' },
|
audience: { id: 'label.audience', defaultMessage: 'Audience' },
|
||||||
invalidUrl: { id: 'label.invalid-url', defaultMessage: 'Invalid URL' },
|
invalidUrl: { id: 'label.invalid-url', defaultMessage: 'Invalid URL' },
|
||||||
environment: { id: 'label.environment', defaultMessage: 'Environment' },
|
environment: { id: 'label.environment', defaultMessage: 'Environment' },
|
||||||
|
|
@ -363,6 +366,8 @@ export const labels = defineMessages({
|
||||||
support: { id: 'label.support', defaultMessage: 'Support' },
|
support: { id: 'label.support', defaultMessage: 'Support' },
|
||||||
documentation: { id: 'label.documentation', defaultMessage: 'Documentation' },
|
documentation: { id: 'label.documentation', defaultMessage: 'Documentation' },
|
||||||
switchAccount: { id: 'label.switch-account', defaultMessage: 'Switch account' },
|
switchAccount: { id: 'label.switch-account', defaultMessage: 'Switch account' },
|
||||||
|
advanced: { id: 'label.advanced', defaultMessage: 'Advanced' },
|
||||||
|
copied: { id: 'label.copied', defaultMessage: 'Copied to clipboard' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const messages = defineMessages({
|
export const messages = defineMessages({
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue