mirror of
https://github.com/umami-software/umami.git
synced 2025-12-06 01:18:00 +01:00
New schema for pixels and links.
This commit is contained in:
parent
c60e8b3d23
commit
88639dfe83
67 changed files with 993 additions and 208 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -11,7 +11,6 @@ node_modules
|
||||||
# next.js
|
# next.js
|
||||||
/.next/
|
/.next/
|
||||||
/out/
|
/out/
|
||||||
/prisma/
|
|
||||||
/src/generated/
|
/src/generated/
|
||||||
|
|
||||||
# production
|
# production
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE `segment` (
|
|
||||||
`segment_id` VARCHAR(36) NOT NULL,
|
|
||||||
`website_id` VARCHAR(36) NOT NULL,
|
|
||||||
`type` VARCHAR(200) NOT NULL,
|
|
||||||
`name` VARCHAR(200) NOT NULL,
|
|
||||||
`parameters` JSON NOT NULL,
|
|
||||||
`created_at` TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
|
|
||||||
`updated_at` TIMESTAMP(0) NULL,
|
|
||||||
|
|
||||||
UNIQUE INDEX `segment_segment_id_key`(`segment_id`),
|
|
||||||
INDEX `segment_website_id_idx`(`website_id`),
|
|
||||||
PRIMARY KEY (`segment_id`)
|
|
||||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE `report` MODIFY `parameters` JSON NOT NULL;
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE `revenue` (
|
|
||||||
`revenue_id` VARCHAR(36) NOT NULL,
|
|
||||||
`website_id` VARCHAR(36) NOT NULL,
|
|
||||||
`session_id` VARCHAR(36) NOT NULL,
|
|
||||||
`event_id` VARCHAR(36) NOT NULL,
|
|
||||||
`event_name` VARCHAR(50) NOT NULL,
|
|
||||||
`currency` VARCHAR(100) NOT NULL,
|
|
||||||
`revenue` DECIMAL(19, 4) NULL,
|
|
||||||
`created_at` TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
|
|
||||||
|
|
||||||
UNIQUE INDEX `revenue_revenue_id_key`(`revenue_id`),
|
|
||||||
INDEX `revenue_website_id_idx`(`website_id`),
|
|
||||||
INDEX `revenue_session_id_idx`(`session_id`),
|
|
||||||
INDEX `revenue_website_id_created_at_idx`(`website_id`, `created_at`),
|
|
||||||
INDEX `revenue_website_id_session_id_created_at_idx`(`website_id`, `session_id`, `created_at`),
|
|
||||||
PRIMARY KEY (`revenue_id`)
|
|
||||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
||||||
|
|
@ -13,10 +13,10 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"dev-turbo": "next dev -p 3001 --turbopack",
|
"dev-turbo": "next dev -p 3001 --turbopack",
|
||||||
"build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app",
|
"build": "npm-run-all check-env build-db-client check-db build-tracker build-geo build-app",
|
||||||
"build-turbo": "npm-run-all check-env build-db check-db build-tracker build-geo build-app-turbo",
|
"build-turbo": "npm-run-all check-env build-db-client check-db build-tracker build-geo build-app-turbo",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"build-docker": "npm-run-all build-db build-tracker build-geo build-app",
|
"build-docker": "npm-run-all build-db-client build-tracker build-geo build-app",
|
||||||
"start-docker": "npm-run-all check-db update-tracker set-routes-manifest start-server",
|
"start-docker": "npm-run-all check-db update-tracker set-routes-manifest start-server",
|
||||||
"start-env": "node scripts/start-env.js",
|
"start-env": "node scripts/start-env.js",
|
||||||
"start-server": "node server.js",
|
"start-server": "node server.js",
|
||||||
|
|
@ -26,7 +26,6 @@
|
||||||
"build-components": "rollup -c rollup.components.config.js",
|
"build-components": "rollup -c rollup.components.config.js",
|
||||||
"build-tracker": "rollup -c rollup.tracker.config.js",
|
"build-tracker": "rollup -c rollup.tracker.config.js",
|
||||||
"build-prisma-client": "node scripts/build-prisma-client.js",
|
"build-prisma-client": "node scripts/build-prisma-client.js",
|
||||||
"build-db": "npm-run-all copy-db-files build-db-client",
|
|
||||||
"build-lang": "npm-run-all format-lang compile-lang download-country-names download-language-names clean-lang",
|
"build-lang": "npm-run-all format-lang compile-lang download-country-names download-language-names clean-lang",
|
||||||
"build-geo": "node scripts/build-geo.js",
|
"build-geo": "node scripts/build-geo.js",
|
||||||
"build-db-schema": "prisma db pull",
|
"build-db-schema": "prisma db pull",
|
||||||
|
|
|
||||||
67
prisma/migrations/14_add_link_and_pixel/migration.sql
Normal file
67
prisma/migrations/14_add_link_and_pixel/migration.sql
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "report" ALTER COLUMN "type" SET DATA TYPE VARCHAR(50);
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "revenue" ALTER COLUMN "currency" SET DATA TYPE VARCHAR(10);
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "segment" ALTER COLUMN "type" SET DATA TYPE VARCHAR(50);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "link" (
|
||||||
|
"link_id" UUID NOT NULL,
|
||||||
|
"name" VARCHAR(100) NOT NULL,
|
||||||
|
"url" VARCHAR(500) NOT NULL,
|
||||||
|
"slug" VARCHAR(100) NOT NULL,
|
||||||
|
"user_id" UUID,
|
||||||
|
"team_id" UUID,
|
||||||
|
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ(6),
|
||||||
|
"deleted_at" TIMESTAMPTZ(6),
|
||||||
|
|
||||||
|
CONSTRAINT "link_pkey" PRIMARY KEY ("link_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "pixel" (
|
||||||
|
"pixel_id" UUID NOT NULL,
|
||||||
|
"name" VARCHAR(100) NOT NULL,
|
||||||
|
"slug" VARCHAR(100) NOT NULL,
|
||||||
|
"user_id" UUID,
|
||||||
|
"team_id" UUID,
|
||||||
|
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ(6),
|
||||||
|
"deleted_at" TIMESTAMPTZ(6),
|
||||||
|
|
||||||
|
CONSTRAINT "pixel_pkey" PRIMARY KEY ("pixel_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "link_link_id_key" ON "link"("link_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "link_slug_idx" ON "link"("slug");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "link_user_id_idx" ON "link"("user_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "link_team_id_idx" ON "link"("team_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "link_created_at_idx" ON "link"("created_at");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "pixel_pixel_id_key" ON "pixel"("pixel_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "pixel_slug_idx" ON "pixel"("slug");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "pixel_user_id_idx" ON "pixel"("user_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "pixel_team_id_idx" ON "pixel"("team_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "pixel_created_at_idx" ON "pixel"("created_at");
|
||||||
|
|
@ -20,10 +20,12 @@ model User {
|
||||||
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
|
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
|
||||||
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
|
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
|
||||||
|
|
||||||
websiteUser Website[] @relation("user")
|
websites Website[] @relation("user")
|
||||||
websiteCreateUser Website[] @relation("createUser")
|
createdBy Website[] @relation("createUser")
|
||||||
teamUser TeamUser[]
|
links Link[] @relation("user")
|
||||||
report Report[]
|
pixels Pixel[] @relation("user")
|
||||||
|
teams TeamUser[]
|
||||||
|
reports Report[]
|
||||||
|
|
||||||
@@map("user")
|
@@map("user")
|
||||||
}
|
}
|
||||||
|
|
@ -42,7 +44,7 @@ model Session {
|
||||||
distinctId String? @map("distinct_id") @db.VarChar(50)
|
distinctId String? @map("distinct_id") @db.VarChar(50)
|
||||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
|
|
||||||
websiteEvent WebsiteEvent[]
|
websiteEvents WebsiteEvent[]
|
||||||
sessionData SessionData[]
|
sessionData SessionData[]
|
||||||
revenue Revenue[]
|
revenue Revenue[]
|
||||||
|
|
||||||
|
|
@ -77,10 +79,10 @@ model Website {
|
||||||
createUser User? @relation("createUser", fields: [createdBy], references: [id])
|
createUser User? @relation("createUser", fields: [createdBy], references: [id])
|
||||||
team Team? @relation(fields: [teamId], references: [id])
|
team Team? @relation(fields: [teamId], references: [id])
|
||||||
eventData EventData[]
|
eventData EventData[]
|
||||||
report Report[]
|
reports Report[]
|
||||||
revenue Revenue[]
|
revenue Revenue[]
|
||||||
|
segments Segment[]
|
||||||
sessionData SessionData[]
|
sessionData SessionData[]
|
||||||
segment Segment[]
|
|
||||||
|
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
@@index([teamId])
|
@@index([teamId])
|
||||||
|
|
@ -192,8 +194,10 @@ model Team {
|
||||||
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
|
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
|
||||||
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
|
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
|
||||||
|
|
||||||
website Website[]
|
websites Website[]
|
||||||
teamUser TeamUser[]
|
members TeamUser[]
|
||||||
|
links Link[]
|
||||||
|
pixels Pixel[]
|
||||||
|
|
||||||
@@index([accessCode])
|
@@index([accessCode])
|
||||||
@@map("team")
|
@@map("team")
|
||||||
|
|
@ -219,7 +223,7 @@ model Report {
|
||||||
id String @id() @unique() @map("report_id") @db.Uuid
|
id String @id() @unique() @map("report_id") @db.Uuid
|
||||||
userId String @map("user_id") @db.Uuid
|
userId String @map("user_id") @db.Uuid
|
||||||
websiteId String @map("website_id") @db.Uuid
|
websiteId String @map("website_id") @db.Uuid
|
||||||
type String @db.VarChar(200)
|
type String @db.VarChar(50)
|
||||||
name String @db.VarChar(200)
|
name String @db.VarChar(200)
|
||||||
description String @db.VarChar(500)
|
description String @db.VarChar(500)
|
||||||
parameters Json
|
parameters Json
|
||||||
|
|
@ -239,7 +243,7 @@ model Report {
|
||||||
model Segment {
|
model Segment {
|
||||||
id String @id() @unique() @map("segment_id") @db.Uuid
|
id String @id() @unique() @map("segment_id") @db.Uuid
|
||||||
websiteId String @map("website_id") @db.Uuid
|
websiteId String @map("website_id") @db.Uuid
|
||||||
type String @db.VarChar(200)
|
type String @db.VarChar(50)
|
||||||
name String @db.VarChar(200)
|
name String @db.VarChar(200)
|
||||||
parameters Json
|
parameters Json
|
||||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
|
|
@ -257,7 +261,7 @@ model Revenue {
|
||||||
sessionId String @map("session_id") @db.Uuid
|
sessionId String @map("session_id") @db.Uuid
|
||||||
eventId String @map("event_id") @db.Uuid
|
eventId String @map("event_id") @db.Uuid
|
||||||
eventName String @map("event_name") @db.VarChar(50)
|
eventName String @map("event_name") @db.VarChar(50)
|
||||||
currency String @db.VarChar(100)
|
currency String @db.VarChar(10)
|
||||||
revenue Decimal? @db.Decimal(19, 4)
|
revenue Decimal? @db.Decimal(19, 4)
|
||||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
|
|
||||||
|
|
@ -270,3 +274,44 @@ model Revenue {
|
||||||
@@index([websiteId, sessionId, createdAt])
|
@@index([websiteId, sessionId, createdAt])
|
||||||
@@map("revenue")
|
@@map("revenue")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Link {
|
||||||
|
id String @id() @unique() @map("link_id") @db.Uuid
|
||||||
|
name String @db.VarChar(100)
|
||||||
|
url String @db.VarChar(500)
|
||||||
|
slug String @db.VarChar(100)
|
||||||
|
userId String? @map("user_id") @db.Uuid
|
||||||
|
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])
|
||||||
|
team Team? @relation(fields: [teamId], references: [id])
|
||||||
|
|
||||||
|
@@index([slug])
|
||||||
|
@@index([userId])
|
||||||
|
@@index([teamId])
|
||||||
|
@@index([createdAt])
|
||||||
|
@@map("link")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Pixel {
|
||||||
|
id String @id() @unique() @map("pixel_id") @db.Uuid
|
||||||
|
name String @db.VarChar(100)
|
||||||
|
slug String @db.VarChar(100)
|
||||||
|
userId String? @map("user_id") @db.Uuid
|
||||||
|
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])
|
||||||
|
team Team? @relation(fields: [teamId], references: [id])
|
||||||
|
|
||||||
|
@@index([slug])
|
||||||
|
@@index([userId])
|
||||||
|
@@index([teamId])
|
||||||
|
@@index([createdAt])
|
||||||
|
@@map("pixel")
|
||||||
|
}
|
||||||
|
|
@ -7,21 +7,13 @@ import semver from 'semver';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
import { PrismaPg } from '@prisma/adapter-pg';
|
import { PrismaPg } from '@prisma/adapter-pg';
|
||||||
|
|
||||||
|
const MIN_VERSION = '9.4.0';
|
||||||
|
|
||||||
if (process.env.SKIP_DB_CHECK) {
|
if (process.env.SKIP_DB_CHECK) {
|
||||||
console.log('Skipping database check.');
|
console.log('Skipping database check.');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDatabaseType(url = process.env.DATABASE_URL) {
|
|
||||||
const type = url && url.split(':')[0];
|
|
||||||
|
|
||||||
if (type === 'postgres') {
|
|
||||||
return 'postgresql';
|
|
||||||
}
|
|
||||||
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = new URL(process.env.DATABASE_URL);
|
const url = new URL(process.env.DATABASE_URL);
|
||||||
|
|
||||||
const adapter = new PrismaPg(
|
const adapter = new PrismaPg(
|
||||||
|
|
@ -61,35 +53,15 @@ async function checkDatabaseVersion() {
|
||||||
const query = await prisma.$queryRaw`select version() as version`;
|
const query = await prisma.$queryRaw`select version() as version`;
|
||||||
const version = semver.valid(semver.coerce(query[0].version));
|
const version = semver.valid(semver.coerce(query[0].version));
|
||||||
|
|
||||||
const databaseType = getDatabaseType();
|
if (semver.lt(version, MIN_VERSION)) {
|
||||||
const minVersion = databaseType === 'postgresql' ? '9.4.0' : '5.7.0';
|
|
||||||
|
|
||||||
if (semver.lt(version, minVersion)) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Database version is not compatible. Please upgrade ${databaseType} version to ${minVersion} or greater`,
|
`Database version is not compatible. Please upgrade to ${MIN_VERSION} or greater.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
success('Database version check successful.');
|
success('Database version check successful.');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkV1Tables() {
|
|
||||||
try {
|
|
||||||
// check for v1 migrations before v2 release date
|
|
||||||
const record =
|
|
||||||
await prisma.$queryRaw`select * from _prisma_migrations where started_at < '2023-04-17'`;
|
|
||||||
|
|
||||||
if (record.length > 0) {
|
|
||||||
error(
|
|
||||||
'Umami v1 tables detected. For how to upgrade from v1 to v2 go to https://umami.is/docs/migrate-v1-v2.',
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function applyMigration() {
|
async function applyMigration() {
|
||||||
if (!process.env.SKIP_DB_MIGRATION) {
|
if (!process.env.SKIP_DB_MIGRATION) {
|
||||||
console.log(execSync('prisma migrate deploy').toString());
|
console.log(execSync('prisma migrate deploy').toString());
|
||||||
|
|
@ -100,13 +72,7 @@ async function applyMigration() {
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
let err = false;
|
let err = false;
|
||||||
for (const fn of [
|
for (const fn of [checkEnv, checkConnection, checkDatabaseVersion, applyMigration]) {
|
||||||
checkEnv,
|
|
||||||
checkConnection,
|
|
||||||
checkDatabaseVersion,
|
|
||||||
checkV1Tables,
|
|
||||||
applyMigration,
|
|
||||||
]) {
|
|
||||||
try {
|
try {
|
||||||
await fn();
|
await fn();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
/* eslint-disable no-console */
|
|
||||||
import 'dotenv/config';
|
|
||||||
import fse from 'fs-extra';
|
|
||||||
import path from 'node:path';
|
|
||||||
import del from 'del';
|
|
||||||
|
|
||||||
function getDatabaseType(url = process.env.DATABASE_URL) {
|
|
||||||
const type = process.env.DATABASE_TYPE || (url && url.split(':')[0]);
|
|
||||||
|
|
||||||
if (type === 'postgres') {
|
|
||||||
return 'postgresql';
|
|
||||||
}
|
|
||||||
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
|
|
||||||
const databaseType = getDatabaseType();
|
|
||||||
|
|
||||||
if (!databaseType || !['mysql', 'postgresql'].includes(databaseType)) {
|
|
||||||
throw new Error('Missing or invalid database');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Database type detected: ${databaseType}`);
|
|
||||||
|
|
||||||
const src = path.resolve(process.cwd(), `db/${databaseType}`);
|
|
||||||
const dest = path.resolve(process.cwd(), 'prisma');
|
|
||||||
|
|
||||||
del.sync(dest);
|
|
||||||
|
|
||||||
fse.copySync(src, dest);
|
|
||||||
|
|
||||||
console.log(`Copied ${src} to ${dest}`);
|
|
||||||
|
|
@ -83,6 +83,9 @@ export function SideNav(props: SidebarProps) {
|
||||||
{!isCollapsed && !hasNav && <PanelButton />}
|
{!isCollapsed && !hasNav && <PanelButton />}
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
</SidebarSection>
|
</SidebarSection>
|
||||||
|
<SidebarSection style={{ paddingTop: 0, paddingBottom: 0 }}>
|
||||||
|
<TeamsButton showText={!hasNav && !isCollapsed} />
|
||||||
|
</SidebarSection>
|
||||||
<SidebarSection flexGrow={1}>
|
<SidebarSection flexGrow={1}>
|
||||||
{links.map(({ id, path, label, icon }) => {
|
{links.map(({ id, path, label, icon }) => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -101,9 +104,6 @@ export function SideNav(props: SidebarProps) {
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</SidebarSection>
|
</SidebarSection>
|
||||||
<SidebarSection>
|
|
||||||
<TeamsButton showText={!hasNav && !isCollapsed} />
|
|
||||||
</SidebarSection>
|
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { Column } from '@umami/react-zen';
|
import { Column } from '@umami/react-zen';
|
||||||
import Link from 'next/link';
|
|
||||||
import { PageHeader } from '@/components/common/PageHeader';
|
import { PageHeader } from '@/components/common/PageHeader';
|
||||||
import { PageBody } from '@/components/common/PageBody';
|
import { PageBody } from '@/components/common/PageBody';
|
||||||
import { BoardAddButton } from './BoardAddButton';
|
import { BoardAddButton } from './BoardAddButton';
|
||||||
|
|
@ -12,9 +11,6 @@ export function BoardsPage() {
|
||||||
<PageHeader title="My Boards">
|
<PageHeader title="My Boards">
|
||||||
<BoardAddButton />
|
<BoardAddButton />
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Link href="/teams/3a97e34a-7f9d-4de2-8754-ed81714b528d/boards/86d4095c-a2a8-4fc8-9521-103e858e2b41">
|
|
||||||
Board 1
|
|
||||||
</Link>
|
|
||||||
</Column>
|
</Column>
|
||||||
</PageBody>
|
</PageBody>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
31
src/app/(main)/links/LinkAddButton.tsx
Normal file
31
src/app/(main)/links/LinkAddButton.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { useMessages, useModified } from '@/components/hooks';
|
||||||
|
import { Button, Icon, Modal, Dialog, DialogTrigger, Text, useToast } from '@umami/react-zen';
|
||||||
|
import { Plus } from '@/components/icons';
|
||||||
|
import { LinkEditForm } from './LinkEditForm';
|
||||||
|
|
||||||
|
export function LinkAddButton({ teamId }: { teamId?: string }) {
|
||||||
|
const { formatMessage, labels, messages } = useMessages();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const { touch } = useModified();
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
toast(formatMessage(messages.saved));
|
||||||
|
touch('links');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogTrigger>
|
||||||
|
<Button data-test="button-website-add" variant="primary">
|
||||||
|
<Icon>
|
||||||
|
<Plus />
|
||||||
|
</Icon>
|
||||||
|
<Text>{formatMessage(labels.addLink)}</Text>
|
||||||
|
</Button>
|
||||||
|
<Modal>
|
||||||
|
<Dialog title={formatMessage(labels.addLink)} style={{ width: 600 }}>
|
||||||
|
{({ close }) => <LinkEditForm teamId={teamId} onSave={handleSave} onClose={close} />}
|
||||||
|
</Dialog>
|
||||||
|
</Modal>
|
||||||
|
</DialogTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
55
src/app/(main)/links/LinkDeleteButton.tsx
Normal file
55
src/app/(main)/links/LinkDeleteButton.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { Dialog } from '@umami/react-zen';
|
||||||
|
import { ActionButton } from '@/components/input/ActionButton';
|
||||||
|
import { Trash } from '@/components/icons';
|
||||||
|
import { ConfirmationForm } from '@/components/common/ConfirmationForm';
|
||||||
|
import { messages } from '@/components/messages';
|
||||||
|
import { useApi, useMessages, useModified } from '@/components/hooks';
|
||||||
|
|
||||||
|
export function LinkDeleteButton({
|
||||||
|
linkId,
|
||||||
|
websiteId,
|
||||||
|
name,
|
||||||
|
onSave,
|
||||||
|
}: {
|
||||||
|
linkId: string;
|
||||||
|
websiteId: string;
|
||||||
|
name: string;
|
||||||
|
onSave?: () => void;
|
||||||
|
}) {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { del, useMutation } = useApi();
|
||||||
|
const { mutate, isPending, error } = useMutation({
|
||||||
|
mutationFn: () => del(`/websites/${websiteId}/links/${linkId}`),
|
||||||
|
});
|
||||||
|
const { touch } = useModified();
|
||||||
|
|
||||||
|
const handleConfirm = (close: () => void) => {
|
||||||
|
mutate(null, {
|
||||||
|
onSuccess: () => {
|
||||||
|
touch('links');
|
||||||
|
onSave?.();
|
||||||
|
close();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionButton tooltip={formatMessage(labels.delete)} icon={<Trash />}>
|
||||||
|
<Dialog title={formatMessage(labels.confirm)} style={{ width: 400 }}>
|
||||||
|
{({ close }) => (
|
||||||
|
<ConfirmationForm
|
||||||
|
message={formatMessage(messages.confirmRemove, {
|
||||||
|
target: name,
|
||||||
|
})}
|
||||||
|
isLoading={isPending}
|
||||||
|
error={error}
|
||||||
|
onConfirm={handleConfirm.bind(null, close)}
|
||||||
|
onClose={close}
|
||||||
|
buttonLabel={formatMessage(labels.delete)}
|
||||||
|
buttonVariant="danger"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
</ActionButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
src/app/(main)/links/LinkEditButton.tsx
Normal file
19
src/app/(main)/links/LinkEditButton.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { ActionButton } from '@/components/input/ActionButton';
|
||||||
|
import { Edit } from '@/components/icons';
|
||||||
|
import { Dialog } from '@umami/react-zen';
|
||||||
|
import { LinkEditForm } from './LinkEditForm';
|
||||||
|
import { useMessages } from '@/components/hooks';
|
||||||
|
|
||||||
|
export function LinkEditButton({ linkId }: { linkId: string }) {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionButton tooltip={formatMessage(labels.edit)} icon={<Edit />}>
|
||||||
|
<Dialog title={formatMessage(labels.link)} style={{ width: 800 }}>
|
||||||
|
{({ close }) => {
|
||||||
|
return <LinkEditForm linkId={linkId} onClose={close} />;
|
||||||
|
}}
|
||||||
|
</Dialog>
|
||||||
|
</ActionButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
113
src/app/(main)/links/LinkEditForm.tsx
Normal file
113
src/app/(main)/links/LinkEditForm.tsx
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormField,
|
||||||
|
FormSubmitButton,
|
||||||
|
Row,
|
||||||
|
TextField,
|
||||||
|
Button,
|
||||||
|
Text,
|
||||||
|
Label,
|
||||||
|
Column,
|
||||||
|
Icon,
|
||||||
|
Loading,
|
||||||
|
} from '@umami/react-zen';
|
||||||
|
import { useConfig, useLinkQuery } from '@/components/hooks';
|
||||||
|
import { useMessages } from '@/components/hooks';
|
||||||
|
import { Refresh } from '@/components/icons';
|
||||||
|
import { getRandomChars } from '@/lib/crypto';
|
||||||
|
import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery';
|
||||||
|
|
||||||
|
const generateId = () => getRandomChars(9);
|
||||||
|
|
||||||
|
export function LinkEditForm({
|
||||||
|
linkId,
|
||||||
|
teamId,
|
||||||
|
onSave,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
linkId?: string;
|
||||||
|
teamId?: string;
|
||||||
|
onSave?: () => void;
|
||||||
|
onClose?: () => void;
|
||||||
|
}) {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { mutate, error, isPending } = useUpdateQuery('/links', { id: linkId, teamId });
|
||||||
|
const { linkDomain } = useConfig();
|
||||||
|
const { data, isLoading } = useLinkQuery(linkId);
|
||||||
|
|
||||||
|
const handleSubmit = async (data: any) => {
|
||||||
|
mutate(data, {
|
||||||
|
onSuccess: async () => {
|
||||||
|
onSave?.();
|
||||||
|
onClose?.();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (linkId && !isLoading) {
|
||||||
|
return <Loading position="page" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
error={error?.message}
|
||||||
|
defaultValues={{ slug: generateId(), ...data }}
|
||||||
|
>
|
||||||
|
{({ setValue }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
label={formatMessage(labels.name)}
|
||||||
|
name="name"
|
||||||
|
rules={{ required: formatMessage(labels.required) }}
|
||||||
|
>
|
||||||
|
<TextField autoComplete="off" />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
label={formatMessage(labels.destinationUrl)}
|
||||||
|
name="url"
|
||||||
|
rules={{ required: formatMessage(labels.required) }}
|
||||||
|
>
|
||||||
|
<TextField placeholder="https://example.com" autoComplete="off" />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<Column>
|
||||||
|
<Label>{formatMessage(labels.link)}</Label>
|
||||||
|
<Row alignItems="center" gap>
|
||||||
|
<Text>{linkDomain || window.location.origin}/</Text>
|
||||||
|
<FormField
|
||||||
|
name="slug"
|
||||||
|
rules={{
|
||||||
|
required: formatMessage(labels.required),
|
||||||
|
}}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
<TextField autoComplete="off" isReadOnly />
|
||||||
|
</FormField>
|
||||||
|
<Button
|
||||||
|
variant="quiet"
|
||||||
|
onPress={() => setValue('slug', generateId(), { shouldDirty: true })}
|
||||||
|
>
|
||||||
|
<Icon>
|
||||||
|
<Refresh />
|
||||||
|
</Icon>
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Row justifyContent="flex-end" paddingTop="3" gap="3">
|
||||||
|
{onClose && (
|
||||||
|
<Button isDisabled={isPending} onPress={onClose}>
|
||||||
|
{formatMessage(labels.cancel)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<FormSubmitButton isDisabled={false}>{formatMessage(labels.save)}</FormSubmitButton>
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
src/app/(main)/links/LinksDataTable.tsx
Normal file
14
src/app/(main)/links/LinksDataTable.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { useLinksQuery, useNavigation } from '@/components/hooks';
|
||||||
|
import { LinksTable } from './LinksTable';
|
||||||
|
import { DataGrid } from '@/components/common/DataGrid';
|
||||||
|
|
||||||
|
export function LinksDataTable() {
|
||||||
|
const { teamId } = useNavigation();
|
||||||
|
const query = useLinksQuery({ teamId });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataGrid query={query} allowSearch={true} autoFocus={false} allowPaging={true}>
|
||||||
|
{({ data }) => <LinksTable data={data} />}
|
||||||
|
</DataGrid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -2,22 +2,24 @@
|
||||||
import { PageBody } from '@/components/common/PageBody';
|
import { PageBody } from '@/components/common/PageBody';
|
||||||
import { Column } from '@umami/react-zen';
|
import { Column } from '@umami/react-zen';
|
||||||
import { PageHeader } from '@/components/common/PageHeader';
|
import { PageHeader } from '@/components/common/PageHeader';
|
||||||
import { BoardAddButton } from '@/app/(main)/boards/BoardAddButton';
|
import { LinkAddButton } from './LinkAddButton';
|
||||||
import Link from 'next/link';
|
import { useMessages, useNavigation } from '@/components/hooks';
|
||||||
import { useMessages } from '@/components/hooks';
|
import { LinksDataTable } from '@/app/(main)/links/LinksDataTable';
|
||||||
|
import { Panel } from '@/components/common/Panel';
|
||||||
|
|
||||||
export function LinksPage() {
|
export function LinksPage() {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { teamId } = useNavigation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageBody>
|
<PageBody>
|
||||||
<Column>
|
<Column gap="6">
|
||||||
<PageHeader title={formatMessage(labels.links)}>
|
<PageHeader title={formatMessage(labels.links)}>
|
||||||
<BoardAddButton />
|
<LinkAddButton teamId={teamId} />
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Link href="/teams/3a97e34a-7f9d-4de2-8754-ed81714b528d/boards/86d4095c-a2a8-4fc8-9521-103e858e2b41">
|
<Panel>
|
||||||
Board 1
|
<LinksDataTable />
|
||||||
</Link>
|
</Panel>
|
||||||
</Column>
|
</Column>
|
||||||
</PageBody>
|
</PageBody>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
37
src/app/(main)/links/LinksTable.tsx
Normal file
37
src/app/(main)/links/LinksTable.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { DataTable, DataColumn, Row } from '@umami/react-zen';
|
||||||
|
import { useMessages, useNavigation } from '@/components/hooks';
|
||||||
|
import { Empty } from '@/components/common/Empty';
|
||||||
|
import { DateDistance } from '@/components/common/DateDistance';
|
||||||
|
import { LinkEditButton } from './LinkEditButton';
|
||||||
|
import { LinkDeleteButton } from './LinkDeleteButton';
|
||||||
|
|
||||||
|
export function LinksTable({ data = [] }) {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { websiteId } = useNavigation();
|
||||||
|
|
||||||
|
if (data.length === 0) {
|
||||||
|
return <Empty />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataTable data={data}>
|
||||||
|
<DataColumn id="name" label={formatMessage(labels.name)} />
|
||||||
|
<DataColumn id="url" label={formatMessage(labels.destinationUrl)} />
|
||||||
|
<DataColumn id="created" label={formatMessage(labels.created)}>
|
||||||
|
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
|
||||||
|
</DataColumn>
|
||||||
|
<DataColumn id="action" align="end" width="100px">
|
||||||
|
{(row: any) => {
|
||||||
|
const { id, name } = row;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row>
|
||||||
|
<LinkEditButton linkId={id} />
|
||||||
|
<LinkDeleteButton linkId={id} websiteId={websiteId} name={name} />
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</DataColumn>
|
||||||
|
</DataTable>
|
||||||
|
);
|
||||||
|
}
|
||||||
32
src/app/(main)/pixels/PixelAddButton.tsx
Normal file
32
src/app/(main)/pixels/PixelAddButton.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { useMessages, useModified, useNavigation } from '@/components/hooks';
|
||||||
|
import { Button, Icon, Modal, Dialog, DialogTrigger, Text, useToast } from '@umami/react-zen';
|
||||||
|
import { Plus } from '@/components/icons';
|
||||||
|
import { PixelAddForm } from './PixelAddForm';
|
||||||
|
|
||||||
|
export function PixelAddButton() {
|
||||||
|
const { formatMessage, labels, messages } = useMessages();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const { touch } = useModified();
|
||||||
|
const { teamId } = useNavigation();
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
toast(formatMessage(messages.saved));
|
||||||
|
touch('boards');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogTrigger>
|
||||||
|
<Button data-test="button-website-add" variant="primary">
|
||||||
|
<Icon>
|
||||||
|
<Plus />
|
||||||
|
</Icon>
|
||||||
|
<Text>{formatMessage(labels.addPixel)}</Text>
|
||||||
|
</Button>
|
||||||
|
<Modal>
|
||||||
|
<Dialog title={formatMessage(labels.addPixel)} style={{ width: 400 }}>
|
||||||
|
{({ close }) => <PixelAddForm teamId={teamId} onSave={handleSave} onClose={close} />}
|
||||||
|
</Dialog>
|
||||||
|
</Modal>
|
||||||
|
</DialogTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
62
src/app/(main)/pixels/PixelAddForm.tsx
Normal file
62
src/app/(main)/pixels/PixelAddForm.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { Form, FormField, FormSubmitButton, Row, TextField, Button } from '@umami/react-zen';
|
||||||
|
import { useApi } from '@/components/hooks';
|
||||||
|
import { DOMAIN_REGEX } from '@/lib/constants';
|
||||||
|
import { useMessages } from '@/components/hooks';
|
||||||
|
|
||||||
|
export function PixelAddForm({
|
||||||
|
teamId,
|
||||||
|
onSave,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
teamId?: string;
|
||||||
|
onSave?: () => void;
|
||||||
|
onClose?: () => void;
|
||||||
|
}) {
|
||||||
|
const { formatMessage, labels, messages } = useMessages();
|
||||||
|
const { post, useMutation } = useApi();
|
||||||
|
const { mutate, error, isPending } = useMutation({
|
||||||
|
mutationFn: (data: any) => post('/pixels', { ...data, teamId }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = async (data: any) => {
|
||||||
|
mutate(data, {
|
||||||
|
onSuccess: async () => {
|
||||||
|
onSave?.();
|
||||||
|
onClose?.();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form onSubmit={handleSubmit} error={error?.message}>
|
||||||
|
<FormField
|
||||||
|
label={formatMessage(labels.name)}
|
||||||
|
name="name"
|
||||||
|
rules={{ required: formatMessage(labels.required) }}
|
||||||
|
>
|
||||||
|
<TextField autoComplete="off" />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
label={formatMessage(labels.domain)}
|
||||||
|
name="domain"
|
||||||
|
rules={{
|
||||||
|
required: formatMessage(labels.required),
|
||||||
|
pattern: { value: DOMAIN_REGEX, message: formatMessage(messages.invalidDomain) },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextField autoComplete="off" />
|
||||||
|
</FormField>
|
||||||
|
<Row justifyContent="flex-end" paddingTop="3" gap="3">
|
||||||
|
{onClose && (
|
||||||
|
<Button isDisabled={isPending} onPress={onClose}>
|
||||||
|
{formatMessage(labels.cancel)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<FormSubmitButton data-test="button-submit" isDisabled={false}>
|
||||||
|
{formatMessage(labels.save)}
|
||||||
|
</FormSubmitButton>
|
||||||
|
</Row>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -2,8 +2,7 @@
|
||||||
import { PageBody } from '@/components/common/PageBody';
|
import { PageBody } from '@/components/common/PageBody';
|
||||||
import { Column } from '@umami/react-zen';
|
import { Column } from '@umami/react-zen';
|
||||||
import { PageHeader } from '@/components/common/PageHeader';
|
import { PageHeader } from '@/components/common/PageHeader';
|
||||||
import { BoardAddButton } from '@/app/(main)/boards/BoardAddButton';
|
import { PixelAddButton } from './PixelAddButton';
|
||||||
import Link from 'next/link';
|
|
||||||
import { useMessages } from '@/components/hooks';
|
import { useMessages } from '@/components/hooks';
|
||||||
|
|
||||||
export function PixelsPage() {
|
export function PixelsPage() {
|
||||||
|
|
@ -13,11 +12,8 @@ export function PixelsPage() {
|
||||||
<PageBody>
|
<PageBody>
|
||||||
<Column>
|
<Column>
|
||||||
<PageHeader title={formatMessage(labels.pixels)}>
|
<PageHeader title={formatMessage(labels.pixels)}>
|
||||||
<BoardAddButton />
|
<PixelAddButton />
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Link href="/teams/3a97e34a-7f9d-4de2-8754-ed81714b528d/boards/86d4095c-a2a8-4fc8-9521-103e858e2b41">
|
|
||||||
Board 1
|
|
||||||
</Link>
|
|
||||||
</Column>
|
</Column>
|
||||||
</PageBody>
|
</PageBody>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -21,13 +21,13 @@ export function TeamsTable({
|
||||||
{(row: any) => <Link href={`/settings/teams/${row.id}`}>{row.name}</Link>}
|
{(row: any) => <Link href={`/settings/teams/${row.id}`}>{row.name}</Link>}
|
||||||
</DataColumn>
|
</DataColumn>
|
||||||
<DataColumn id="owner" label={formatMessage(labels.owner)}>
|
<DataColumn id="owner" label={formatMessage(labels.owner)}>
|
||||||
{(row: any) => row.teamUser.find(({ role }) => role === ROLES.teamOwner)?.user?.username}
|
{(row: any) => row.users.find(({ role }) => role === ROLES.teamOwner)?.user?.username}
|
||||||
</DataColumn>
|
</DataColumn>
|
||||||
<DataColumn id="websites" label={formatMessage(labels.websites)}>
|
<DataColumn id="websites" label={formatMessage(labels.websites)} align="end">
|
||||||
{(row: any) => row._count.website}
|
{(row: any) => row._count.websites}
|
||||||
</DataColumn>
|
</DataColumn>
|
||||||
<DataColumn id="members" label={formatMessage(labels.members)}>
|
<DataColumn id="members" label={formatMessage(labels.members)} align="end">
|
||||||
{(row: any) => row._count.teamUser}
|
{(row: any) => row._count.users}
|
||||||
</DataColumn>
|
</DataColumn>
|
||||||
{showActions ? (
|
{showActions ? (
|
||||||
<DataColumn id="action" label=" " align="end">
|
<DataColumn id="action" label=" " align="end">
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,17 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider';
|
|
||||||
import { TeamMembersDataTable } from './TeamMembersDataTable';
|
import { TeamMembersDataTable } from './TeamMembersDataTable';
|
||||||
import { SectionHeader } from '@/components/common/SectionHeader';
|
import { SectionHeader } from '@/components/common/SectionHeader';
|
||||||
import { useLoginQuery, useMessages } from '@/components/hooks';
|
import { useLoginQuery, useMessages, useTeam } from '@/components/hooks';
|
||||||
import { ROLES } from '@/lib/constants';
|
import { ROLES } from '@/lib/constants';
|
||||||
import { useContext } from 'react';
|
|
||||||
import { Column } from '@umami/react-zen';
|
import { Column } from '@umami/react-zen';
|
||||||
|
|
||||||
export function TeamMembersPage({ teamId }: { teamId: string }) {
|
export function TeamMembersPage({ teamId }: { teamId: string }) {
|
||||||
const team = useContext(TeamContext);
|
const team = useTeam();
|
||||||
const { user } = useLoginQuery();
|
const { user } = useLoginQuery();
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
const canEdit =
|
const canEdit =
|
||||||
team?.teamUser?.find(
|
team?.members?.find(
|
||||||
({ userId, role }) =>
|
({ userId, role }) =>
|
||||||
(role === ROLES.teamOwner || role === ROLES.teamManager) && userId === user.id,
|
(role === ROLES.teamOwner || role === ROLES.teamManager) && userId === user.id,
|
||||||
) && user.role !== ROLES.viewOnly;
|
) && user.role !== ROLES.viewOnly;
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ export function WebsitesTable({
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
{allowEdit && (
|
{allowEdit && (
|
||||||
<MenuItem href={renderUrl(`/settings/websites/${websiteId}`)}>
|
<MenuItem href={renderUrl(`/websites/${websiteId}/settings`)}>
|
||||||
<Row alignItems="center" gap>
|
<Row alignItems="center" gap>
|
||||||
<Icon data-test="link-button-edit">
|
<Icon data-test="link-button-edit">
|
||||||
<SquarePen />
|
<SquarePen />
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { Column, Icon, Row, Text } from '@umami/react-zen';
|
|
||||||
import { WebsiteSettingsPage } from '@/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage';
|
import { WebsiteSettingsPage } from '@/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage';
|
||||||
import { LinkButton } from '@/components/common/LinkButton';
|
|
||||||
import { Arrow } from '@/components/icons';
|
|
||||||
import { useNavigation } from '@/components/hooks';
|
|
||||||
|
|
||||||
export function SettingsPage({ websiteId }: { websiteId: string }) {
|
export function SettingsPage({ websiteId }: { websiteId: string }) {
|
||||||
const { pathname } = useNavigation();
|
return <WebsiteSettingsPage websiteId={websiteId} />;
|
||||||
return (
|
|
||||||
<Column gap>
|
|
||||||
<Row marginTop="3">
|
|
||||||
<LinkButton href={pathname.replace('/settings', '')}>
|
|
||||||
<Row alignItems="center" gap>
|
|
||||||
<Icon rotate={180}>
|
|
||||||
<Arrow />
|
|
||||||
</Icon>
|
|
||||||
<Text>Back</Text>
|
|
||||||
</Row>
|
|
||||||
</LinkButton>
|
|
||||||
</Row>
|
|
||||||
<WebsiteSettingsPage websiteId={websiteId} />
|
|
||||||
</Column>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
export type Config = {
|
export type Config = {
|
||||||
faviconUrl: string | undefined;
|
faviconUrl?: string;
|
||||||
privateMode: boolean;
|
privateMode: boolean;
|
||||||
telemetryDisabled: boolean;
|
telemetryDisabled: boolean;
|
||||||
trackerScriptName: string | undefined;
|
trackerScriptName?: string;
|
||||||
updatesDisabled: boolean;
|
updatesDisabled: boolean;
|
||||||
|
linkDomain?: string;
|
||||||
|
pixelDomain?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getConfig(): Promise<Config> {
|
export async function getConfig(): Promise<Config> {
|
||||||
|
|
@ -15,6 +17,7 @@ export async function getConfig(): Promise<Config> {
|
||||||
telemetryDisabled: !!process.env.DISABLE_TELEMETRY,
|
telemetryDisabled: !!process.env.DISABLE_TELEMETRY,
|
||||||
trackerScriptName: process.env.TRACKER_SCRIPT_NAME,
|
trackerScriptName: process.env.TRACKER_SCRIPT_NAME,
|
||||||
updatesDisabled: !!process.env.DISABLE_UPDATES,
|
updatesDisabled: !!process.env.DISABLE_UPDATES,
|
||||||
loginDisabled: !!process.env.DISABLE_LOGIN,
|
linkDomain: process.env.LINK_DOMAIN,
|
||||||
|
pixelDomain: process.env.PIXEL_DOMAIN,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
84
src/app/api/links/[linkId]/route.ts
Normal file
84
src/app/api/links/[linkId]/route.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { canUpdateWebsite, canDeleteWebsite, canViewWebsite } from '@/lib/auth';
|
||||||
|
import { SHARE_ID_REGEX } from '@/lib/constants';
|
||||||
|
import { parseRequest } from '@/lib/request';
|
||||||
|
import { ok, json, unauthorized, serverError } from '@/lib/response';
|
||||||
|
import { deleteWebsite, getWebsite, updateWebsite } from '@/queries';
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: Promise<{ websiteId: string }> },
|
||||||
|
) {
|
||||||
|
const { auth, error } = await parseRequest(request);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return error();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { websiteId } = await params;
|
||||||
|
|
||||||
|
if (!(await canViewWebsite(auth, websiteId))) {
|
||||||
|
return unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
const website = await getWebsite(websiteId);
|
||||||
|
|
||||||
|
return json(website);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: Promise<{ websiteId: string }> },
|
||||||
|
) {
|
||||||
|
const schema = z.object({
|
||||||
|
name: z.string().optional(),
|
||||||
|
domain: z.string().optional(),
|
||||||
|
shareId: z.string().regex(SHARE_ID_REGEX).nullable().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { auth, body, error } = await parseRequest(request, schema);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return error();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { websiteId } = await params;
|
||||||
|
const { name, domain, shareId } = body;
|
||||||
|
|
||||||
|
if (!(await canUpdateWebsite(auth, websiteId))) {
|
||||||
|
return unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const website = await updateWebsite(websiteId, { name, domain, shareId });
|
||||||
|
|
||||||
|
return Response.json(website);
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.message.includes('Unique constraint') && e.message.includes('share_id')) {
|
||||||
|
return serverError(new Error('That share ID is already taken.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return serverError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: Promise<{ websiteId: string }> },
|
||||||
|
) {
|
||||||
|
const { auth, error } = await parseRequest(request);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return error();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { websiteId } = await params;
|
||||||
|
|
||||||
|
if (!(await canDeleteWebsite(auth, websiteId))) {
|
||||||
|
return unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteWebsite(websiteId);
|
||||||
|
|
||||||
|
return ok();
|
||||||
|
}
|
||||||
64
src/app/api/links/route.ts
Normal file
64
src/app/api/links/route.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { canCreateTeamWebsite, canCreateWebsite } from '@/lib/auth';
|
||||||
|
import { json, unauthorized } from '@/lib/response';
|
||||||
|
import { uuid } from '@/lib/crypto';
|
||||||
|
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||||
|
import { pagingParams, searchParams } from '@/lib/schema';
|
||||||
|
import { createLink, getUserLinks } from '@/queries';
|
||||||
|
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
const schema = z.object({
|
||||||
|
...pagingParams,
|
||||||
|
...searchParams,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { auth, query, error } = await parseRequest(request, schema);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return error();
|
||||||
|
}
|
||||||
|
|
||||||
|
const filters = await getQueryFilters(query);
|
||||||
|
|
||||||
|
const result = await getUserLinks(auth.user.id, filters);
|
||||||
|
|
||||||
|
return json(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const schema = z.object({
|
||||||
|
name: z.string().max(100),
|
||||||
|
url: z.string().max(500),
|
||||||
|
slug: z.string().max(100),
|
||||||
|
teamId: z.string().nullable().optional(),
|
||||||
|
id: z.string().uuid().nullable().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { auth, body, error } = await parseRequest(request, schema);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return error();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id, name, url, slug, teamId } = body;
|
||||||
|
|
||||||
|
if ((teamId && !(await canCreateTeamWebsite(auth, teamId))) || !(await canCreateWebsite(auth))) {
|
||||||
|
return unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: any = {
|
||||||
|
id: id ?? uuid(),
|
||||||
|
name,
|
||||||
|
url,
|
||||||
|
slug,
|
||||||
|
teamId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!teamId) {
|
||||||
|
data.userId = auth.user.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await createLink(data);
|
||||||
|
|
||||||
|
return json(result);
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,7 @@ import { z } from 'zod';
|
||||||
import { pagingParams } from '@/lib/schema';
|
import { pagingParams } from '@/lib/schema';
|
||||||
import { getUserTeams } from '@/queries';
|
import { getUserTeams } from '@/queries';
|
||||||
import { json } from '@/lib/response';
|
import { json } from '@/lib/response';
|
||||||
import { parseRequest } from '@/lib/request';
|
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
|
|
@ -15,7 +15,9 @@ export async function GET(request: Request) {
|
||||||
return error();
|
return error();
|
||||||
}
|
}
|
||||||
|
|
||||||
const teams = await getUserTeams(auth.user.id, query);
|
const filters = await getQueryFilters(query);
|
||||||
|
|
||||||
|
const teams = await getUserTeams(auth.user.id, filters);
|
||||||
|
|
||||||
return json(teams);
|
return json(teams);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { z } from 'zod';
|
||||||
import { pagingParams } from '@/lib/schema';
|
import { pagingParams } from '@/lib/schema';
|
||||||
import { getUserWebsites } from '@/queries';
|
import { getUserWebsites } from '@/queries';
|
||||||
import { json } from '@/lib/response';
|
import { json } from '@/lib/response';
|
||||||
import { parseRequest } from '@/lib/request';
|
import { parseRequest, getQueryFilters } from '@/lib/request';
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
|
|
@ -15,7 +15,9 @@ export async function GET(request: Request) {
|
||||||
return error();
|
return error();
|
||||||
}
|
}
|
||||||
|
|
||||||
const websites = await getUserWebsites(auth.user.id, query);
|
const filters = await getQueryFilters(query);
|
||||||
|
|
||||||
|
const websites = await getUserWebsites(auth.user.id, filters);
|
||||||
|
|
||||||
return json(websites);
|
return json(websites);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
0
src/app/api/pixels/[pixelId]/route.ts
Normal file
0
src/app/api/pixels/[pixelId]/route.ts
Normal file
0
src/app/api/pixels/route.ts
Normal file
0
src/app/api/pixels/route.ts
Normal file
29
src/app/api/teams/[teamId]/links/route.ts
Normal file
29
src/app/api/teams/[teamId]/links/route.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { unauthorized, json } from '@/lib/response';
|
||||||
|
import { canViewTeam } from '@/lib/auth';
|
||||||
|
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||||
|
import { pagingParams, searchParams } from '@/lib/schema';
|
||||||
|
import { getTeamLinks } from '@/queries';
|
||||||
|
|
||||||
|
export async function GET(request: Request, { params }: { params: Promise<{ teamId: string }> }) {
|
||||||
|
const schema = z.object({
|
||||||
|
...pagingParams,
|
||||||
|
...searchParams,
|
||||||
|
});
|
||||||
|
const { teamId } = await params;
|
||||||
|
const { auth, query, error } = await parseRequest(request, schema);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return error();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await canViewTeam(auth, teamId))) {
|
||||||
|
return unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
const filters = await getQueryFilters(query);
|
||||||
|
|
||||||
|
const websites = await getTeamLinks(teamId, filters);
|
||||||
|
|
||||||
|
return json(websites);
|
||||||
|
}
|
||||||
29
src/app/api/teams/[teamId]/pixels/pixels.ts
Normal file
29
src/app/api/teams/[teamId]/pixels/pixels.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { unauthorized, json } from '@/lib/response';
|
||||||
|
import { canViewTeam } from '@/lib/auth';
|
||||||
|
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||||
|
import { pagingParams, searchParams } from '@/lib/schema';
|
||||||
|
import { getTeamPixels } from '@/queries';
|
||||||
|
|
||||||
|
export async function GET(request: Request, { params }: { params: Promise<{ teamId: string }> }) {
|
||||||
|
const schema = z.object({
|
||||||
|
...pagingParams,
|
||||||
|
...searchParams,
|
||||||
|
});
|
||||||
|
const { teamId } = await params;
|
||||||
|
const { auth, query, error } = await parseRequest(request, schema);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return error();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await canViewTeam(auth, teamId))) {
|
||||||
|
return unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
const filters = await getQueryFilters(query);
|
||||||
|
|
||||||
|
const websites = await getTeamPixels(teamId, filters);
|
||||||
|
|
||||||
|
return json(websites);
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { unauthorized, json } from '@/lib/response';
|
import { unauthorized, json } from '@/lib/response';
|
||||||
import { canViewTeam } from '@/lib/auth';
|
import { canViewTeam } from '@/lib/auth';
|
||||||
import { parseRequest } from '@/lib/request';
|
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||||
import { pagingParams, searchParams } from '@/lib/schema';
|
import { pagingParams, searchParams } from '@/lib/schema';
|
||||||
import { getTeamWebsites } from '@/queries';
|
import { getTeamWebsites } from '@/queries';
|
||||||
|
|
||||||
|
|
@ -21,7 +21,9 @@ export async function GET(request: Request, { params }: { params: Promise<{ team
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
const websites = await getTeamWebsites(teamId, query);
|
const filters = await getQueryFilters(query);
|
||||||
|
|
||||||
|
const websites = await getTeamWebsites(teamId, filters);
|
||||||
|
|
||||||
return json(websites);
|
return json(websites);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { z } from 'zod';
|
||||||
import { unauthorized, json } from '@/lib/response';
|
import { unauthorized, json } from '@/lib/response';
|
||||||
import { getUserWebsites } from '@/queries/prisma/website';
|
import { getUserWebsites } from '@/queries/prisma/website';
|
||||||
import { pagingParams, searchParams } from '@/lib/schema';
|
import { pagingParams, searchParams } from '@/lib/schema';
|
||||||
import { parseRequest } from '@/lib/request';
|
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||||
|
|
||||||
export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) {
|
export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
|
|
@ -22,7 +22,9 @@ export async function GET(request: Request, { params }: { params: Promise<{ user
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
const websites = await getUserWebsites(userId, query);
|
const filters = await getQueryFilters(query);
|
||||||
|
|
||||||
|
const websites = await getUserWebsites(userId, filters);
|
||||||
|
|
||||||
return json(websites);
|
return json(websites);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,11 @@ export * from './queries/useEventDataQuery';
|
||||||
export * from './queries/useEventDataEventsQuery';
|
export * from './queries/useEventDataEventsQuery';
|
||||||
export * from './queries/useEventDataPropertiesQuery';
|
export * from './queries/useEventDataPropertiesQuery';
|
||||||
export * from './queries/useEventDataValuesQuery';
|
export * from './queries/useEventDataValuesQuery';
|
||||||
|
export * from './queries/useLinkQuery';
|
||||||
|
export * from './queries/useLinksQuery';
|
||||||
export * from './queries/useLoginQuery';
|
export * from './queries/useLoginQuery';
|
||||||
|
export * from './queries/usePixelQuery';
|
||||||
|
export * from './queries/usePixelsQuery';
|
||||||
export * from './queries/useRealtimeQuery';
|
export * from './queries/useRealtimeQuery';
|
||||||
export * from './queries/useResultQuery';
|
export * from './queries/useResultQuery';
|
||||||
export * from './queries/useReportQuery';
|
export * from './queries/useReportQuery';
|
||||||
|
|
@ -49,8 +53,10 @@ export * from './useLanguageNames';
|
||||||
export * from './useLocale';
|
export * from './useLocale';
|
||||||
export * from './useMessages';
|
export * from './useMessages';
|
||||||
export * from './useModified';
|
export * from './useModified';
|
||||||
|
export * from './useNavigation';
|
||||||
export * from './usePagedQuery';
|
export * from './usePagedQuery';
|
||||||
export * from './useRegionNames';
|
export * from './useRegionNames';
|
||||||
export * from './useSticky';
|
export * from './useSticky';
|
||||||
export * from './useNavigation';
|
export * from './useTeam';
|
||||||
export * from './useTimezone';
|
export * from './useTimezone';
|
||||||
|
export * from './useWebsite';
|
||||||
|
|
|
||||||
15
src/components/hooks/queries/useLinkQuery.ts
Normal file
15
src/components/hooks/queries/useLinkQuery.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { useApi } from '../useApi';
|
||||||
|
import { useModified } from '../useModified';
|
||||||
|
|
||||||
|
export function useLinkQuery(linkId: string) {
|
||||||
|
const { get, useQuery } = useApi();
|
||||||
|
const { modified } = useModified(`link:${linkId}`);
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['link', { linkId, modified }],
|
||||||
|
queryFn: () => {
|
||||||
|
return get(`/link/${linkId}`);
|
||||||
|
},
|
||||||
|
enabled: !!linkId,
|
||||||
|
});
|
||||||
|
}
|
||||||
15
src/components/hooks/queries/useLinksQuery.ts
Normal file
15
src/components/hooks/queries/useLinksQuery.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { useApi } from '../useApi';
|
||||||
|
import { usePagedQuery } from '../usePagedQuery';
|
||||||
|
import { useModified } from '../useModified';
|
||||||
|
import { ReactQueryOptions } from '@/lib/types';
|
||||||
|
|
||||||
|
export function useLinksQuery({ teamId }: { teamId?: string }, options?: ReactQueryOptions<any>) {
|
||||||
|
const { modified } = useModified('links');
|
||||||
|
const { get } = useApi();
|
||||||
|
|
||||||
|
return usePagedQuery({
|
||||||
|
queryKey: ['links', { teamId, modified }],
|
||||||
|
queryFn: async () => get(teamId ? `/teams/${teamId}/links` : '/links'),
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
15
src/components/hooks/queries/usePixelQuery.ts
Normal file
15
src/components/hooks/queries/usePixelQuery.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { useApi } from '../useApi';
|
||||||
|
import { useModified } from '../useModified';
|
||||||
|
|
||||||
|
export function usePixelQuery(pixelId: string) {
|
||||||
|
const { get, useQuery } = useApi();
|
||||||
|
const { modified } = useModified(`pixel:${pixelId}`);
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['pixel', { pixelId, modified }],
|
||||||
|
queryFn: () => {
|
||||||
|
return get(`/pixel/${pixelId}`);
|
||||||
|
},
|
||||||
|
enabled: !!pixelId,
|
||||||
|
});
|
||||||
|
}
|
||||||
19
src/components/hooks/queries/usePixelsQuery.ts
Normal file
19
src/components/hooks/queries/usePixelsQuery.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { useApi } from '../useApi';
|
||||||
|
import { usePagedQuery } from '../usePagedQuery';
|
||||||
|
import { useModified } from '../useModified';
|
||||||
|
import { ReactQueryOptions } from '@/lib/types';
|
||||||
|
|
||||||
|
export function usePixelsQuery(
|
||||||
|
{ websiteId, type }: { websiteId: string; type?: string },
|
||||||
|
options?: ReactQueryOptions<any>,
|
||||||
|
) {
|
||||||
|
const { modified } = useModified(`pixels:${type}`);
|
||||||
|
const { get } = useApi();
|
||||||
|
|
||||||
|
return usePagedQuery({
|
||||||
|
queryKey: ['pixels', { websiteId, type, modified }],
|
||||||
|
queryFn: async () => get('/pixels', { websiteId, type }),
|
||||||
|
enabled: !!websiteId && !!type,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
11
src/components/hooks/queries/useUpdateQuery.ts
Normal file
11
src/components/hooks/queries/useUpdateQuery.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { useApi, useModified } from '@/components/hooks';
|
||||||
|
|
||||||
|
export function useUpdateQuery(path: string, params?: Record<string, any>) {
|
||||||
|
const { post, useMutation } = useApi();
|
||||||
|
const { mutate, isPending, error } = useMutation({
|
||||||
|
mutationFn: (data: Record<string, any>) => post(path, { ...data, ...params }),
|
||||||
|
});
|
||||||
|
const { touch } = useModified();
|
||||||
|
|
||||||
|
return { mutate, isPending, error, touch };
|
||||||
|
}
|
||||||
6
src/components/hooks/useTeam.ts
Normal file
6
src/components/hooks/useTeam.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { TeamContext } from '@/app/(main)/settings/teams/[teamId]/TeamProvider';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
|
||||||
|
export function useTeam() {
|
||||||
|
return useContext(TeamContext);
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,7 @@ export function PanelButton(props: ButtonProps) {
|
||||||
{...props}
|
{...props}
|
||||||
style={{ padding: 0 }}
|
style={{ padding: 0 }}
|
||||||
>
|
>
|
||||||
<Icon>
|
<Icon strokeColor="muted">
|
||||||
<PanelLeft />
|
<PanelLeft />
|
||||||
</Icon>
|
</Icon>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import {
|
||||||
Pressable,
|
Pressable,
|
||||||
} from '@umami/react-zen';
|
} from '@umami/react-zen';
|
||||||
import { useLoginQuery, useMessages, useUserTeamsQuery, useNavigation } from '@/components/hooks';
|
import { useLoginQuery, useMessages, useUserTeamsQuery, useNavigation } from '@/components/hooks';
|
||||||
import { Chevron, User, Users, LogOut } from '@/components/icons';
|
import { Chevron, User, Users } from '@/components/icons';
|
||||||
|
|
||||||
export function TeamsButton({ showText = true }: { showText?: boolean }) {
|
export function TeamsButton({ showText = true }: { showText?: boolean }) {
|
||||||
const { user } = useLoginQuery();
|
const { user } = useLoginQuery();
|
||||||
|
|
@ -79,17 +79,6 @@ export function TeamsButton({ showText = true }: { showText?: boolean }) {
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</MenuSection>
|
</MenuSection>
|
||||||
<MenuSeparator />
|
|
||||||
<MenuSection>
|
|
||||||
<MenuItem id="logout">
|
|
||||||
<Row alignItems="center" gap>
|
|
||||||
<Icon size="sm">
|
|
||||||
<LogOut />
|
|
||||||
</Icon>
|
|
||||||
<Text wrap="nowrap">{formatMessage(labels.logout)}</Text>
|
|
||||||
</Row>
|
|
||||||
</MenuItem>
|
|
||||||
</MenuSection>
|
|
||||||
</Menu>
|
</Menu>
|
||||||
</Box>
|
</Box>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
|
||||||
|
|
@ -325,9 +325,13 @@ export const labels = defineMessages({
|
||||||
other: { id: 'label.other', defaultMessage: 'Other' },
|
other: { id: 'label.other', defaultMessage: 'Other' },
|
||||||
boards: { id: 'label.boards', defaultMessage: 'Boards' },
|
boards: { id: 'label.boards', defaultMessage: 'Boards' },
|
||||||
apply: { id: 'label.apply', defaultMessage: 'Apply' },
|
apply: { id: 'label.apply', defaultMessage: 'Apply' },
|
||||||
|
link: { id: 'label.link', defaultMessage: 'Link' },
|
||||||
links: { id: 'label.links', defaultMessage: 'Links' },
|
links: { id: 'label.links', defaultMessage: 'Links' },
|
||||||
|
pixel: { id: 'label.pixel', defaultMessage: 'Pixel' },
|
||||||
pixels: { id: 'label.pixels', defaultMessage: 'Pixels' },
|
pixels: { id: 'label.pixels', defaultMessage: 'Pixels' },
|
||||||
addBoard: { id: 'label.add-board', defaultMessage: 'Add board' },
|
addBoard: { id: 'label.add-board', defaultMessage: 'Add board' },
|
||||||
|
addLink: { id: 'label.add-link', defaultMessage: 'Add link' },
|
||||||
|
addPixel: { id: 'label.add-pixel', defaultMessage: 'Add pixel' },
|
||||||
maximize: { id: 'label.maximize', defaultMessage: 'Maximize' },
|
maximize: { id: 'label.maximize', defaultMessage: 'Maximize' },
|
||||||
remaining: { id: 'label.remaining', defaultMessage: 'Remaining' },
|
remaining: { id: 'label.remaining', defaultMessage: 'Remaining' },
|
||||||
conversion: { id: 'label.conversion', defaultMessage: 'Conversion' },
|
conversion: { id: 'label.conversion', defaultMessage: 'Conversion' },
|
||||||
|
|
@ -347,6 +351,7 @@ export const labels = defineMessages({
|
||||||
saveSegment: { id: 'label.save-segment', defaultMessage: 'Save as segment' },
|
saveSegment: { id: 'label.save-segment', defaultMessage: 'Save as segment' },
|
||||||
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' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const messages = defineMessages({
|
export const messages = defineMessages({
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
export * from '@/queries/prisma/link';
|
||||||
|
export * from '@/queries/prisma/pixel';
|
||||||
export * from '@/queries/prisma/report';
|
export * from '@/queries/prisma/report';
|
||||||
export * from '@/queries/prisma/segment';
|
export * from '@/queries/prisma/segment';
|
||||||
export * from '@/queries/prisma/team';
|
export * from '@/queries/prisma/team';
|
||||||
|
|
|
||||||
71
src/queries/prisma/link.ts
Normal file
71
src/queries/prisma/link.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { Prisma, Link } from '@prisma/client';
|
||||||
|
import prisma from '@/lib/prisma';
|
||||||
|
import { PageResult, QueryFilters } from '@/lib/types';
|
||||||
|
|
||||||
|
async function findLink(criteria: Prisma.LinkFindUniqueArgs): Promise<Link> {
|
||||||
|
return prisma.client.link.findUnique(criteria);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLink(linkId: string): Promise<Link> {
|
||||||
|
return findLink({
|
||||||
|
where: {
|
||||||
|
id: linkId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLinks(
|
||||||
|
criteria: Prisma.LinkFindManyArgs,
|
||||||
|
filters: QueryFilters = {},
|
||||||
|
): Promise<PageResult<Link[]>> {
|
||||||
|
const { search } = filters;
|
||||||
|
|
||||||
|
const where: Prisma.LinkWhereInput = {
|
||||||
|
...criteria.where,
|
||||||
|
...prisma.getSearchParameters(search, [{ name: 'contains' }]),
|
||||||
|
};
|
||||||
|
|
||||||
|
return prisma.pagedQuery('link', { ...criteria, where }, filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserLinks(
|
||||||
|
userId: string,
|
||||||
|
filters?: QueryFilters,
|
||||||
|
): Promise<PageResult<Link[]>> {
|
||||||
|
return getLinks(
|
||||||
|
{
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
filters,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTeamLinks(
|
||||||
|
teamId: string,
|
||||||
|
filters?: QueryFilters,
|
||||||
|
): Promise<PageResult<Link[]>> {
|
||||||
|
return getLinks(
|
||||||
|
{
|
||||||
|
where: {
|
||||||
|
teamId,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
filters,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createLink(data: Prisma.LinkUncheckedCreateInput): Promise<Link> {
|
||||||
|
return prisma.client.link.create({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateLink(linkId: string, data: any): Promise<Link> {
|
||||||
|
return prisma.client.link.update({ where: { id: linkId }, data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteLink(linkId: string): Promise<Link> {
|
||||||
|
return prisma.client.link.delete({ where: { id: linkId } });
|
||||||
|
}
|
||||||
69
src/queries/prisma/pixel.ts
Normal file
69
src/queries/prisma/pixel.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { Prisma, Pixel } from '@prisma/client';
|
||||||
|
import prisma from '@/lib/prisma';
|
||||||
|
import { PageResult, QueryFilters } from '@/lib/types';
|
||||||
|
|
||||||
|
async function findPixel(criteria: Prisma.PixelFindUniqueArgs): Promise<Pixel> {
|
||||||
|
return prisma.client.pixel.findUnique(criteria);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPixel(pixelId: string): Promise<Pixel> {
|
||||||
|
return findPixel({
|
||||||
|
where: {
|
||||||
|
id: pixelId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPixels(
|
||||||
|
criteria: Prisma.PixelFindManyArgs,
|
||||||
|
filters: QueryFilters = {},
|
||||||
|
): Promise<PageResult<Pixel[]>> {
|
||||||
|
const { search } = filters;
|
||||||
|
|
||||||
|
const where: Prisma.PixelWhereInput = {
|
||||||
|
...criteria.where,
|
||||||
|
...prisma.getSearchParameters(search, [{ name: 'contains' }]),
|
||||||
|
};
|
||||||
|
|
||||||
|
return prisma.pagedQuery('pixel', { ...criteria, where }, filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserPixels(
|
||||||
|
userId: string,
|
||||||
|
filters?: QueryFilters,
|
||||||
|
): Promise<PageResult<Pixel[]>> {
|
||||||
|
return getPixels(
|
||||||
|
{
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
filters,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTeamPixels(
|
||||||
|
teamId: string,
|
||||||
|
filters?: QueryFilters,
|
||||||
|
): Promise<PageResult<Pixel[]>> {
|
||||||
|
return getPixels(
|
||||||
|
{
|
||||||
|
where: {
|
||||||
|
teamId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
filters,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createPixel(data: Prisma.PixelUncheckedCreateInput): Promise<Pixel> {
|
||||||
|
return prisma.client.pixel.create({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updatePixel(pixelId: string, data: any): Promise<Pixel> {
|
||||||
|
return prisma.client.pixel.update({ where: { id: pixelId }, data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deletePixel(pixelId: string): Promise<Pixel> {
|
||||||
|
return prisma.client.pixel.delete({ where: { id: pixelId } });
|
||||||
|
}
|
||||||
|
|
@ -16,7 +16,7 @@ export async function getTeam(teamId: string, options: { includeMembers?: boolea
|
||||||
where: {
|
where: {
|
||||||
id: teamId,
|
id: teamId,
|
||||||
},
|
},
|
||||||
...(includeMembers && { include: { teamUser: true } }),
|
...(includeMembers && { include: { members: true } }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,12 +47,12 @@ export async function getUserTeams(userId: string, filters: QueryFilters) {
|
||||||
{
|
{
|
||||||
where: {
|
where: {
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
teamUser: {
|
members: {
|
||||||
some: { userId },
|
some: { userId },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
teamUser: {
|
members: {
|
||||||
include: {
|
include: {
|
||||||
user: {
|
user: {
|
||||||
select: {
|
select: {
|
||||||
|
|
@ -64,10 +64,10 @@ export async function getUserTeams(userId: string, filters: QueryFilters) {
|
||||||
},
|
},
|
||||||
_count: {
|
_count: {
|
||||||
select: {
|
select: {
|
||||||
website: {
|
websites: {
|
||||||
where: { deletedAt: null },
|
where: { deletedAt: null },
|
||||||
},
|
},
|
||||||
teamUser: {
|
members: {
|
||||||
where: {
|
where: {
|
||||||
user: { deletedAt: null },
|
user: { deletedAt: null },
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { Prisma, Website } from '@prisma/client';
|
||||||
import redis from '@/lib/redis';
|
import redis from '@/lib/redis';
|
||||||
import prisma from '@/lib/prisma';
|
import prisma from '@/lib/prisma';
|
||||||
import { PageResult, QueryFilters } from '@/lib/types';
|
import { PageResult, QueryFilters } from '@/lib/types';
|
||||||
import WebsiteFindManyArgs = Prisma.WebsiteFindManyArgs;
|
|
||||||
import { ROLES } from '@/lib/constants';
|
import { ROLES } from '@/lib/constants';
|
||||||
|
|
||||||
async function findWebsite(criteria: Prisma.WebsiteFindUniqueArgs): Promise<Website> {
|
async function findWebsite(criteria: Prisma.WebsiteFindUniqueArgs): Promise<Website> {
|
||||||
|
|
@ -27,7 +26,7 @@ export async function getSharedWebsite(shareId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getWebsites(
|
export async function getWebsites(
|
||||||
criteria: WebsiteFindManyArgs,
|
criteria: Prisma.WebsiteFindManyArgs,
|
||||||
filters: QueryFilters,
|
filters: QueryFilters,
|
||||||
): Promise<PageResult<Website[]>> {
|
): Promise<PageResult<Website[]>> {
|
||||||
const { search } = filters;
|
const { search } = filters;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue