mirror of
https://github.com/kremalicious/umami.git
synced 2024-11-21 17:37:00 +01:00
commit
88da20ea7f
1
.gitignore
vendored
1
.gitignore
vendored
@ -22,6 +22,7 @@ node_modules
|
||||
# misc
|
||||
.DS_Store
|
||||
.idea
|
||||
.yarn
|
||||
*.iml
|
||||
*.log
|
||||
.vscode
|
||||
|
@ -4,4 +4,9 @@ export default defineConfig({
|
||||
e2e: {
|
||||
baseUrl: 'http://localhost:3000',
|
||||
},
|
||||
// default username / password on init
|
||||
env: {
|
||||
umami_user: 'admin',
|
||||
umami_password: 'umami',
|
||||
},
|
||||
});
|
||||
|
@ -43,9 +43,10 @@ services:
|
||||
- CYPRESS_umami_user=admin
|
||||
- CYPRESS_umami_password=umami
|
||||
volumes:
|
||||
- ../tsconfig.json:/tsconfig.json
|
||||
- ./tsconfig.json:/tsconfig.json
|
||||
- ../cypress.config.ts:/cypress.config.ts
|
||||
- ./:/cypress
|
||||
- ../node_modules/:/node_modules
|
||||
- ../src/lib/crypto.ts:/src/lib/crypto.ts
|
||||
volumes:
|
||||
umami-db-data:
|
||||
|
@ -6,8 +6,12 @@ describe('Login tests', () => {
|
||||
},
|
||||
() => {
|
||||
cy.visit('/login');
|
||||
cy.getDataTest('input-username').find('input').type(Cypress.env('umami_user'));
|
||||
cy.getDataTest('input-password').find('input').type(Cypress.env('umami_password'));
|
||||
cy.getDataTest('input-username').find('input').click();
|
||||
cy.getDataTest('input-username').find('input').type(Cypress.env('umami_user'), { delay: 50 });
|
||||
cy.getDataTest('input-password').find('input').click();
|
||||
cy.getDataTest('input-password')
|
||||
.find('input')
|
||||
.type(Cypress.env('umami_password'), { delay: 50 });
|
||||
cy.getDataTest('button-submit').click();
|
||||
cy.url().should('eq', Cypress.config().baseUrl + '/dashboard');
|
||||
cy.getDataTest('button-profile').click();
|
||||
|
@ -10,8 +10,10 @@ describe('Website tests', () => {
|
||||
cy.visit('/settings/websites');
|
||||
cy.getDataTest('button-website-add').click();
|
||||
cy.contains(/Add website/i).should('be.visible');
|
||||
cy.getDataTest('input-name').find('input').wait(500).type('Add test', { delay: 50 });
|
||||
cy.getDataTest('input-domain').find('input').wait(500).type('addtest.com', { delay: 50 });
|
||||
cy.getDataTest('input-name').find('input').click();
|
||||
cy.getDataTest('input-name').find('input').type('Add test', { delay: 50 });
|
||||
cy.getDataTest('input-domain').find('input').click();
|
||||
cy.getDataTest('input-domain').find('input').type('addtest.com', { delay: 50 });
|
||||
cy.getDataTest('button-submit').click();
|
||||
cy.get('td[label="Name"]').should('contain.text', 'Add test');
|
||||
cy.get('td[label="Domain"]').should('contain.text', 'addtest.com');
|
||||
@ -26,10 +28,10 @@ describe('Website tests', () => {
|
||||
cy.deleteWebsite(websiteId);
|
||||
});
|
||||
cy.visit('/settings/websites');
|
||||
cy.contains('Add test').should('not.exist');
|
||||
cy.contains(/Add test/i).should('not.exist');
|
||||
});
|
||||
|
||||
it.only('Edit a website', () => {
|
||||
it('Edit a website', () => {
|
||||
// prep data
|
||||
cy.addWebsite('Update test', 'updatetest.com');
|
||||
cy.visit('/settings/websites');
|
||||
@ -37,16 +39,12 @@ describe('Website tests', () => {
|
||||
// edit website
|
||||
cy.getDataTest('link-button-edit').first().click();
|
||||
cy.contains(/Details/i).should('be.visible');
|
||||
cy.getDataTest('input-name')
|
||||
.find('input')
|
||||
.wait(500)
|
||||
.clear()
|
||||
.type('Updated website', { delay: 50 });
|
||||
cy.getDataTest('input-domain')
|
||||
.find('input')
|
||||
.wait(500)
|
||||
.clear()
|
||||
.type('updatedwebsite.com', { delay: 50 });
|
||||
cy.getDataTest('input-name').find('input').click();
|
||||
cy.getDataTest('input-name').find('input').clear();
|
||||
cy.getDataTest('input-name').find('input').type('Updated website', { delay: 50 });
|
||||
cy.getDataTest('input-domain').find('input').click();
|
||||
cy.getDataTest('input-domain').find('input').clear();
|
||||
cy.getDataTest('input-domain').find('input').type('updatedwebsite.com', { delay: 50 });
|
||||
cy.getDataTest('button-submit').click({ force: true });
|
||||
cy.getDataTest('input-name').find('input').should('have.value', 'Updated website');
|
||||
cy.getDataTest('input-domain').find('input').should('have.value', 'updatedwebsite.com');
|
||||
@ -69,7 +67,7 @@ describe('Website tests', () => {
|
||||
cy.deleteWebsite(websiteId);
|
||||
});
|
||||
cy.visit('/settings/websites');
|
||||
cy.contains('Add test').should('not.exist');
|
||||
cy.contains(/Add test/i).should('not.exist');
|
||||
});
|
||||
|
||||
it('Delete a website', () => {
|
||||
@ -86,6 +84,6 @@ describe('Website tests', () => {
|
||||
cy.contains(/Type DELETE in the box below to confirm./i).should('be.visible');
|
||||
cy.get('input[name="confirm"').type('DELETE');
|
||||
cy.get('button[type="submit"]').click();
|
||||
cy.contains('Delete test').should('not.exist');
|
||||
cy.contains(/Delete test/i).should('not.exist');
|
||||
});
|
||||
});
|
||||
|
90
db/clickhouse/migrations/02_add_visit_id.sql
Normal file
90
db/clickhouse/migrations/02_add_visit_id.sql
Normal file
@ -0,0 +1,90 @@
|
||||
CREATE TABLE umami.website_event_join
|
||||
(
|
||||
session_id UUID,
|
||||
visit_id UUID,
|
||||
created_at DateTime('UTC')
|
||||
)
|
||||
engine = MergeTree
|
||||
ORDER BY (session_id, created_at)
|
||||
SETTINGS index_granularity = 8192;
|
||||
|
||||
INSERT INTO umami.website_event_join
|
||||
SELECT DISTINCT
|
||||
s.session_id,
|
||||
generateUUIDv4() visit_id,
|
||||
s.created_at
|
||||
FROM (SELECT DISTINCT session_id,
|
||||
date_trunc('hour', created_at) created_at
|
||||
FROM website_event) s;
|
||||
|
||||
-- create new table
|
||||
CREATE TABLE umami.website_event_new
|
||||
(
|
||||
website_id UUID,
|
||||
session_id UUID,
|
||||
visit_id UUID,
|
||||
event_id UUID,
|
||||
hostname LowCardinality(String),
|
||||
browser LowCardinality(String),
|
||||
os LowCardinality(String),
|
||||
device LowCardinality(String),
|
||||
screen LowCardinality(String),
|
||||
language LowCardinality(String),
|
||||
country LowCardinality(String),
|
||||
subdivision1 LowCardinality(String),
|
||||
subdivision2 LowCardinality(String),
|
||||
city String,
|
||||
url_path String,
|
||||
url_query String,
|
||||
referrer_path String,
|
||||
referrer_query String,
|
||||
referrer_domain String,
|
||||
page_title String,
|
||||
event_type UInt32,
|
||||
event_name String,
|
||||
created_at DateTime('UTC'),
|
||||
job_id UUID
|
||||
)
|
||||
engine = MergeTree
|
||||
ORDER BY (website_id, session_id, created_at)
|
||||
SETTINGS index_granularity = 8192;
|
||||
|
||||
INSERT INTO umami.website_event_new
|
||||
SELECT we.website_id,
|
||||
we.session_id,
|
||||
j.visit_id,
|
||||
we.event_id,
|
||||
we.hostname,
|
||||
we.browser,
|
||||
we.os,
|
||||
we.device,
|
||||
we.screen,
|
||||
we.language,
|
||||
we.country,
|
||||
we.subdivision1,
|
||||
we.subdivision2,
|
||||
we.city,
|
||||
we.url_path,
|
||||
we.url_query,
|
||||
we.referrer_path,
|
||||
we.referrer_query,
|
||||
we.referrer_domain,
|
||||
we.page_title,
|
||||
we.event_type,
|
||||
we.event_name,
|
||||
we.created_at,
|
||||
we.job_id
|
||||
FROM umami.website_event we
|
||||
JOIN umami.website_event_join j
|
||||
ON we.session_id = j.session_id
|
||||
and date_trunc('hour', we.created_at) = j.created_at
|
||||
|
||||
RENAME TABLE umami.website_event TO umami.website_event_old;
|
||||
RENAME TABLE umami.website_event_new TO umami.website_event;
|
||||
|
||||
/*
|
||||
|
||||
DROP TABLE umami.website_event_old
|
||||
DROP TABLE umami.website_event_join
|
||||
|
||||
*/
|
@ -3,6 +3,7 @@ CREATE TABLE umami.website_event
|
||||
(
|
||||
website_id UUID,
|
||||
session_id UUID,
|
||||
visit_id UUID,
|
||||
event_id UUID,
|
||||
--sessions
|
||||
hostname LowCardinality(String),
|
||||
|
22
db/mysql/migrations/05_add_visit_id/migration.sql
Normal file
22
db/mysql/migrations/05_add_visit_id/migration.sql
Normal file
@ -0,0 +1,22 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE `website_event` ADD COLUMN `visit_id` VARCHAR(36) NULL;
|
||||
|
||||
UPDATE `website_event` we
|
||||
JOIN (SELECT DISTINCT
|
||||
s.session_id,
|
||||
s.visit_time,
|
||||
BIN_TO_UUID(RANDOM_BYTES(16) & 0xffffffffffff0fff3fffffffffffffff | 0x00000000000040008000000000000000) uuid
|
||||
FROM (SELECT DISTINCT session_id,
|
||||
DATE_FORMAT(created_at, '%Y-%m-%d %H:00:00') visit_time
|
||||
FROM `website_event`) s) a
|
||||
ON we.session_id = a.session_id and DATE_FORMAT(we.created_at, '%Y-%m-%d %H:00:00') = a.visit_time
|
||||
SET we.visit_id = a.uuid
|
||||
WHERE we.visit_id IS NULL;
|
||||
|
||||
ALTER TABLE `website_event` MODIFY `visit_id` VARCHAR(36) NOT NULL;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX `website_event_visit_id_idx` ON `website_event`(`visit_id`);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX `website_event_website_id_visit_id_created_at_idx` ON `website_event`(`website_id`, `visit_id`, `created_at`);
|
@ -92,6 +92,7 @@ model WebsiteEvent {
|
||||
id String @id() @map("event_id") @db.VarChar(36)
|
||||
websiteId String @map("website_id") @db.VarChar(36)
|
||||
sessionId String @map("session_id") @db.VarChar(36)
|
||||
visitId String @map("visit_id") @db.VarChar(36)
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
|
||||
urlPath String @map("url_path") @db.VarChar(500)
|
||||
urlQuery String? @map("url_query") @db.VarChar(500)
|
||||
@ -107,6 +108,7 @@ model WebsiteEvent {
|
||||
|
||||
@@index([createdAt])
|
||||
@@index([sessionId])
|
||||
@@index([visitId])
|
||||
@@index([websiteId])
|
||||
@@index([websiteId, createdAt])
|
||||
@@index([websiteId, createdAt, urlPath])
|
||||
@ -115,6 +117,7 @@ model WebsiteEvent {
|
||||
@@index([websiteId, createdAt, pageTitle])
|
||||
@@index([websiteId, createdAt, eventName])
|
||||
@@index([websiteId, sessionId, createdAt])
|
||||
@@index([websiteId, visitId, createdAt])
|
||||
@@map("website_event")
|
||||
}
|
||||
|
||||
|
22
db/postgresql/migrations/05_add_visit_id/migration.sql
Normal file
22
db/postgresql/migrations/05_add_visit_id/migration.sql
Normal file
@ -0,0 +1,22 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "website_event" ADD COLUMN "visit_id" UUID NULL;
|
||||
|
||||
UPDATE "website_event" we
|
||||
SET visit_id = a.uuid
|
||||
FROM (SELECT DISTINCT
|
||||
s.session_id,
|
||||
s.visit_time,
|
||||
gen_random_uuid() uuid
|
||||
FROM (SELECT DISTINCT session_id,
|
||||
date_trunc('hour', created_at) visit_time
|
||||
FROM "website_event") s) a
|
||||
WHERE we.session_id = a.session_id
|
||||
and date_trunc('hour', we.created_at) = a.visit_time;
|
||||
|
||||
ALTER TABLE "website_event" ALTER COLUMN "visit_id" SET NOT NULL;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "website_event_visit_id_idx" ON "website_event"("visit_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "website_event_website_id_visit_id_created_at_idx" ON "website_event"("website_id", "visit_id", "created_at");
|
@ -92,6 +92,7 @@ model WebsiteEvent {
|
||||
id String @id() @map("event_id") @db.Uuid
|
||||
websiteId String @map("website_id") @db.Uuid
|
||||
sessionId String @map("session_id") @db.Uuid
|
||||
visitId String @map("visit_id") @db.Uuid
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
urlPath String @map("url_path") @db.VarChar(500)
|
||||
urlQuery String? @map("url_query") @db.VarChar(500)
|
||||
@ -107,6 +108,7 @@ model WebsiteEvent {
|
||||
|
||||
@@index([createdAt])
|
||||
@@index([sessionId])
|
||||
@@index([visitId])
|
||||
@@index([websiteId])
|
||||
@@index([websiteId, createdAt])
|
||||
@@index([websiteId, createdAt, urlPath])
|
||||
@ -115,6 +117,7 @@ model WebsiteEvent {
|
||||
@@index([websiteId, createdAt, pageTitle])
|
||||
@@index([websiteId, createdAt, eventName])
|
||||
@@index([websiteId, sessionId, createdAt])
|
||||
@@index([websiteId, visitId, createdAt])
|
||||
@@map("website_event")
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,7 @@ const frameAncestors = process.env.ALLOWED_FRAME_URLS || '';
|
||||
const disableLogin = process.env.DISABLE_LOGIN || '';
|
||||
const disableUI = process.env.DISABLE_UI || '';
|
||||
const hostURL = process.env.HOST_URL || '';
|
||||
const privateMode = process.env.PRIVATE_MODE || '';
|
||||
|
||||
const contentSecurityPolicy = [
|
||||
`default-src 'self'`,
|
||||
@ -120,6 +121,7 @@ const config = {
|
||||
disableLogin,
|
||||
disableUI,
|
||||
hostURL,
|
||||
privateMode,
|
||||
},
|
||||
basePath,
|
||||
output: 'standalone',
|
||||
|
15
package.json
15
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "umami",
|
||||
"version": "2.10.2",
|
||||
"version": "2.11.0",
|
||||
"description": "A simple, fast, privacy-focused alternative to Google Analytics.",
|
||||
"author": "Umami Software, Inc. <hello@umami.is>",
|
||||
"license": "MIT",
|
||||
@ -66,14 +66,14 @@
|
||||
"dependencies": {
|
||||
"@clickhouse/client": "^0.2.2",
|
||||
"@fontsource/inter": "^4.5.15",
|
||||
"@prisma/client": "5.9.1",
|
||||
"@prisma/client": "5.11.0",
|
||||
"@prisma/extension-read-replicas": "^0.3.0",
|
||||
"@react-spring/web": "^9.7.3",
|
||||
"@tanstack/react-query": "^5.12.2",
|
||||
"@tanstack/react-query": "^5.28.6",
|
||||
"@umami/prisma-client": "^0.14.0",
|
||||
"@umami/redis-client": "^0.18.0",
|
||||
"chalk": "^4.1.1",
|
||||
"chart.js": "^4.2.1",
|
||||
"chart.js": "^4.4.2",
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
"classnames": "^2.3.1",
|
||||
"colord": "^2.9.2",
|
||||
@ -98,11 +98,11 @@
|
||||
"maxmind": "^4.3.6",
|
||||
"md5": "^2.3.0",
|
||||
"moment-timezone": "^0.5.35",
|
||||
"next": "14.1.0",
|
||||
"next": "14.1.4",
|
||||
"next-basics": "^0.39.0",
|
||||
"node-fetch": "^3.2.8",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prisma": "5.9.1",
|
||||
"prisma": "5.11.0",
|
||||
"react": "^18.2.0",
|
||||
"react-basics": "^0.123.0",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
@ -115,7 +115,6 @@
|
||||
"request-ip": "^3.3.0",
|
||||
"semver": "^7.5.4",
|
||||
"thenby": "^1.3.4",
|
||||
"timezone-support": "^2.0.2",
|
||||
"uuid": "^9.0.0",
|
||||
"yup": "^0.32.11",
|
||||
"zustand": "^4.3.8"
|
||||
@ -176,6 +175,6 @@
|
||||
"tar": "^6.1.2",
|
||||
"ts-jest": "^29.1.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.1.6"
|
||||
"typescript": "^5.4.3"
|
||||
}
|
||||
}
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Erstellt"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Erstellt"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -32,7 +32,7 @@
|
||||
"label.add-member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Add member"
|
||||
"value": "Añadir miembro"
|
||||
}
|
||||
],
|
||||
"label.add-website": [
|
||||
@ -215,6 +215,12 @@
|
||||
"value": "Creado"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -272,7 +278,7 @@
|
||||
"label.delete-report": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Delete report"
|
||||
"value": "Eliminar reporte"
|
||||
}
|
||||
],
|
||||
"label.delete-team": [
|
||||
@ -464,7 +470,7 @@
|
||||
"label.insights-description": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Dive deeper into your data by using segments and filters."
|
||||
"value": "Profundice en sus datos mediante el uso de segmentos y filtros."
|
||||
}
|
||||
],
|
||||
"label.is": [
|
||||
@ -482,7 +488,7 @@
|
||||
"label.is-not-set": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Is not set"
|
||||
"value": "No está establecido"
|
||||
}
|
||||
],
|
||||
"label.is-set": [
|
||||
@ -588,7 +594,7 @@
|
||||
"label.manage": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Manage"
|
||||
"value": "Administrar"
|
||||
}
|
||||
],
|
||||
"label.max": [
|
||||
@ -600,7 +606,7 @@
|
||||
"label.member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Member"
|
||||
"value": "Miembro"
|
||||
}
|
||||
],
|
||||
"label.members": [
|
||||
@ -630,7 +636,7 @@
|
||||
"label.my-account": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "My account"
|
||||
"value": "Mi cuenta"
|
||||
}
|
||||
],
|
||||
"label.my-websites": [
|
||||
@ -842,7 +848,7 @@
|
||||
"label.remove-member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Remove member"
|
||||
"value": "Eliminar miembro"
|
||||
}
|
||||
],
|
||||
"label.reports": [
|
||||
@ -926,7 +932,7 @@
|
||||
"label.select-role": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Select role"
|
||||
"value": "Seleccionar rol"
|
||||
}
|
||||
],
|
||||
"label.select-website": [
|
||||
@ -1004,7 +1010,7 @@
|
||||
"label.team-view-only": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Team view only"
|
||||
"value": "Vista solo del equipo"
|
||||
}
|
||||
],
|
||||
"label.team-websites": [
|
||||
@ -1088,13 +1094,13 @@
|
||||
"label.transfer": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer"
|
||||
"value": "Transferir"
|
||||
}
|
||||
],
|
||||
"label.transfer-website": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer website"
|
||||
"value": "Transferir sitio web"
|
||||
}
|
||||
],
|
||||
"label.true": [
|
||||
@ -1232,7 +1238,7 @@
|
||||
"message.action-confirmation": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Type "
|
||||
"value": "Escriba "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -1240,7 +1246,7 @@
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": " in the box below to confirm."
|
||||
"value": " en el cuadro a continuación para confirmar."
|
||||
}
|
||||
],
|
||||
"message.active-users": [
|
||||
@ -1308,7 +1314,7 @@
|
||||
"message.confirm-remove": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Are you sure you want to remove "
|
||||
"value": "¿Estás seguro de que desea eliminar "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -1336,7 +1342,7 @@
|
||||
"message.delete-team-warning": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Deleting a team will also delete all team websites."
|
||||
"value": "Al eliminar un equipo, también se eliminarán todos los sitios web del equipo."
|
||||
}
|
||||
],
|
||||
"message.delete-website-warning": [
|
||||
@ -1532,7 +1538,7 @@
|
||||
"message.transfer-team-website-to-user": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer this website to your account?"
|
||||
"value": "¿Transferir este sitio web a su cuenta?"
|
||||
}
|
||||
],
|
||||
"message.transfer-user-website-to-team": [
|
||||
@ -1544,13 +1550,13 @@
|
||||
"message.transfer-website": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer website ownership to your account or another team."
|
||||
"value": "Seleccione el equipo al que transferir este sitio web."
|
||||
}
|
||||
],
|
||||
"message.triggered-event": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Triggered event"
|
||||
"value": "Evento lanzado"
|
||||
}
|
||||
],
|
||||
"message.user-deleted": [
|
||||
@ -1562,7 +1568,7 @@
|
||||
"message.viewed-page": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Viewed page"
|
||||
"value": "Página vista"
|
||||
}
|
||||
],
|
||||
"message.visitor-log": [
|
||||
@ -1602,7 +1608,7 @@
|
||||
"message.visitors-dropped-off": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Visitors dropped off"
|
||||
"value": "Los visitantes salieron"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -32,7 +32,7 @@
|
||||
"label.add-member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Add member"
|
||||
"value": "Ajouter un membre"
|
||||
}
|
||||
],
|
||||
"label.add-website": [
|
||||
@ -215,6 +215,12 @@
|
||||
"value": "Créé"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Crée par"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -272,7 +278,7 @@
|
||||
"label.delete-report": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Delete report"
|
||||
"value": "Supprimer le rapport"
|
||||
}
|
||||
],
|
||||
"label.delete-team": [
|
||||
@ -362,7 +368,7 @@
|
||||
"label.edit-member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Edit member"
|
||||
"value": "Modifier le membre"
|
||||
}
|
||||
],
|
||||
"label.enable-share-url": [
|
||||
@ -580,7 +586,7 @@
|
||||
"label.manage": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Manage"
|
||||
"value": "Gérer"
|
||||
}
|
||||
],
|
||||
"label.max": [
|
||||
@ -592,7 +598,7 @@
|
||||
"label.member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Member"
|
||||
"value": "Membre"
|
||||
}
|
||||
],
|
||||
"label.members": [
|
||||
@ -622,7 +628,7 @@
|
||||
"label.my-account": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "My account"
|
||||
"value": "Mon compte"
|
||||
}
|
||||
],
|
||||
"label.my-websites": [
|
||||
@ -646,7 +652,7 @@
|
||||
"label.none": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Aucun·e"
|
||||
"value": "Aucun"
|
||||
}
|
||||
],
|
||||
"label.number-of-records": [
|
||||
@ -834,7 +840,7 @@
|
||||
"label.remove-member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Remove member"
|
||||
"value": "Retirer le membre"
|
||||
}
|
||||
],
|
||||
"label.reports": [
|
||||
@ -918,7 +924,7 @@
|
||||
"label.select-role": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Select role"
|
||||
"value": "Choisir un rôle"
|
||||
}
|
||||
],
|
||||
"label.select-website": [
|
||||
@ -1080,13 +1086,13 @@
|
||||
"label.transfer": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer"
|
||||
"value": "Transférer"
|
||||
}
|
||||
],
|
||||
"label.transfer-website": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer website"
|
||||
"value": "Transférer le site"
|
||||
}
|
||||
],
|
||||
"label.true": [
|
||||
@ -1224,7 +1230,7 @@
|
||||
"message.action-confirmation": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Type "
|
||||
"value": "Taper "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -1232,7 +1238,7 @@
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": " in the box below to confirm."
|
||||
"value": " ci-dessous pour confirmer."
|
||||
}
|
||||
],
|
||||
"message.active-users": [
|
||||
@ -1304,7 +1310,7 @@
|
||||
"message.confirm-remove": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Are you sure you want to remove "
|
||||
"value": "Êtes-vous sûr de vouloir retirer "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -1312,7 +1318,7 @@
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": "?"
|
||||
"value": " ?"
|
||||
}
|
||||
],
|
||||
"message.confirm-reset": [
|
||||
@ -1332,7 +1338,7 @@
|
||||
"message.delete-team-warning": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Deleting a team will also delete all team websites."
|
||||
"value": "Supprimer une équipe supprimera aussi tous les sites de cette équipe."
|
||||
}
|
||||
],
|
||||
"message.delete-website-warning": [
|
||||
@ -1520,19 +1526,19 @@
|
||||
"message.transfer-team-website-to-user": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer this website to your account?"
|
||||
"value": "Transférer ce site sur votre compte ?"
|
||||
}
|
||||
],
|
||||
"message.transfer-user-website-to-team": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Select the team to transfer this website to."
|
||||
"value": "Choisir l'équipe à laquelle transférer ce site."
|
||||
}
|
||||
],
|
||||
"message.transfer-website": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer website ownership to your account or another team."
|
||||
"value": "Transférer la propriété du site sur votre compte ou à une autre équipe."
|
||||
}
|
||||
],
|
||||
"message.triggered-event": [
|
||||
@ -1550,7 +1556,7 @@
|
||||
"message.viewed-page": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Viewed page"
|
||||
"value": "Page vue"
|
||||
}
|
||||
],
|
||||
"message.visitor-log": [
|
||||
@ -1590,7 +1596,7 @@
|
||||
"message.visitors-dropped-off": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Visitors dropped off"
|
||||
"value": "Visiteurs ont abandonné"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -32,7 +32,7 @@
|
||||
"label.add-member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Add member"
|
||||
"value": "メンバーの追加"
|
||||
}
|
||||
],
|
||||
"label.add-website": [
|
||||
@ -188,7 +188,7 @@
|
||||
"label.create": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Create"
|
||||
"value": "作成"
|
||||
}
|
||||
],
|
||||
"label.create-report": [
|
||||
@ -215,6 +215,12 @@
|
||||
"value": "作成されました"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -272,7 +278,7 @@
|
||||
"label.delete-report": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Delete report"
|
||||
"value": "レポートの削除"
|
||||
}
|
||||
],
|
||||
"label.delete-team": [
|
||||
@ -362,7 +368,7 @@
|
||||
"label.edit-member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Edit member"
|
||||
"value": "メンバーの編集"
|
||||
}
|
||||
],
|
||||
"label.enable-share-url": [
|
||||
@ -410,13 +416,13 @@
|
||||
"label.filter": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Filter"
|
||||
"value": "フィルター"
|
||||
}
|
||||
],
|
||||
"label.filter-combined": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "統合"
|
||||
"value": "結合"
|
||||
}
|
||||
],
|
||||
"label.filter-raw": [
|
||||
@ -434,13 +440,13 @@
|
||||
"label.funnel": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "分析"
|
||||
"value": "ファネル"
|
||||
}
|
||||
],
|
||||
"label.funnel-description": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Understand the conversion and drop-off rate of users."
|
||||
"value": "ユーザーのコンバージョン率と離脱率を分析します。"
|
||||
}
|
||||
],
|
||||
"label.greater-than": [
|
||||
@ -458,13 +464,13 @@
|
||||
"label.insights": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "見通し"
|
||||
"value": "インサイト"
|
||||
}
|
||||
],
|
||||
"label.insights-description": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Dive deeper into your data by using segments and filters."
|
||||
"value": "セグメントとフィルタを使用して、データをさらに詳しく分析します。"
|
||||
}
|
||||
],
|
||||
"label.is": [
|
||||
@ -588,7 +594,7 @@
|
||||
"label.manage": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Manage"
|
||||
"value": "管理"
|
||||
}
|
||||
],
|
||||
"label.max": [
|
||||
@ -600,7 +606,7 @@
|
||||
"label.member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Member"
|
||||
"value": "メンバー"
|
||||
}
|
||||
],
|
||||
"label.members": [
|
||||
@ -630,7 +636,7 @@
|
||||
"label.my-account": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "My account"
|
||||
"value": "マイアカウント"
|
||||
}
|
||||
],
|
||||
"label.my-websites": [
|
||||
@ -842,7 +848,7 @@
|
||||
"label.remove-member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Remove member"
|
||||
"value": "メンバーの削除"
|
||||
}
|
||||
],
|
||||
"label.reports": [
|
||||
@ -872,13 +878,13 @@
|
||||
"label.retention": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "保持"
|
||||
"value": "リテンション"
|
||||
}
|
||||
],
|
||||
"label.retention-description": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Measure your website stickiness by tracking how often users return."
|
||||
"value": "ユーザーの再訪問回数を記録して、Webサイトのリテンション率を計測します。"
|
||||
}
|
||||
],
|
||||
"label.role": [
|
||||
@ -908,13 +914,13 @@
|
||||
"label.search": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Search"
|
||||
"value": "検索"
|
||||
}
|
||||
],
|
||||
"label.select": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Select"
|
||||
"value": "選択"
|
||||
}
|
||||
],
|
||||
"label.select-date": [
|
||||
@ -926,7 +932,7 @@
|
||||
"label.select-role": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Select role"
|
||||
"value": "ロールを選択"
|
||||
}
|
||||
],
|
||||
"label.select-website": [
|
||||
@ -1004,7 +1010,7 @@
|
||||
"label.team-view-only": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Team view only"
|
||||
"value": "チーム表示のみ"
|
||||
}
|
||||
],
|
||||
"label.team-websites": [
|
||||
@ -1088,13 +1094,13 @@
|
||||
"label.transfer": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer"
|
||||
"value": "移管"
|
||||
}
|
||||
],
|
||||
"label.transfer-website": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer website"
|
||||
"value": "Webサイトの移管"
|
||||
}
|
||||
],
|
||||
"label.true": [
|
||||
@ -1232,7 +1238,7 @@
|
||||
"message.action-confirmation": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Type "
|
||||
"value": "承認する場合は、下のフォームに「"
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -1240,7 +1246,7 @@
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": " in the box below to confirm."
|
||||
"value": "」と入力してください。"
|
||||
}
|
||||
],
|
||||
"message.active-users": [
|
||||
@ -1298,17 +1304,13 @@
|
||||
}
|
||||
],
|
||||
"message.confirm-remove": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Are you sure you want to remove "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"value": "target"
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": "?"
|
||||
"value": "を削除してもよろしいですか?"
|
||||
}
|
||||
],
|
||||
"message.confirm-reset": [
|
||||
@ -1324,7 +1326,7 @@
|
||||
"message.delete-team-warning": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Deleting a team will also delete all team websites."
|
||||
"value": "チームを削除すると、そのチームが管理しているWebサイトもすべて削除されます。"
|
||||
}
|
||||
],
|
||||
"message.delete-website-warning": [
|
||||
@ -1526,25 +1528,25 @@
|
||||
"message.transfer-team-website-to-user": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer this website to your account?"
|
||||
"value": "このWebサイトをあなたのアカウントに移管しますか?"
|
||||
}
|
||||
],
|
||||
"message.transfer-user-website-to-team": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Select the team to transfer this website to."
|
||||
"value": "このWebサイトを移管するチームを選択してください。"
|
||||
}
|
||||
],
|
||||
"message.transfer-website": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer website ownership to your account or another team."
|
||||
"value": "Webサイトの所有権を自分のアカウントまたは別のチームへ移管します。"
|
||||
}
|
||||
],
|
||||
"message.triggered-event": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Triggered event"
|
||||
"value": "トリガーされたイベント"
|
||||
}
|
||||
],
|
||||
"message.user-deleted": [
|
||||
@ -1556,7 +1558,7 @@
|
||||
"message.viewed-page": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Viewed page"
|
||||
"value": "閲覧されたページ"
|
||||
}
|
||||
],
|
||||
"message.visitor-log": [
|
||||
@ -1596,7 +1598,7 @@
|
||||
"message.visitors-dropped-off": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Visitors dropped off"
|
||||
"value": "訪問者の離脱率"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Үүсгэсэн"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "ပြုလုပ်ပြီးသော"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Gemaakt"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Utworzony"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Criado"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -215,6 +215,12 @@
|
||||
"value": "Создано"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Ustvarjeno"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Skapad"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -32,7 +32,7 @@
|
||||
"label.add-member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Add member"
|
||||
"value": "添加成员"
|
||||
}
|
||||
],
|
||||
"label.add-website": [
|
||||
@ -215,6 +215,12 @@
|
||||
"value": "已创建"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "创建者"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -272,7 +278,7 @@
|
||||
"label.delete-report": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Delete report"
|
||||
"value": "删除报告"
|
||||
}
|
||||
],
|
||||
"label.delete-team": [
|
||||
@ -362,7 +368,7 @@
|
||||
"label.edit-member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Edit member"
|
||||
"value": "编辑成员"
|
||||
}
|
||||
],
|
||||
"label.enable-share-url": [
|
||||
@ -588,7 +594,7 @@
|
||||
"label.manage": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Manage"
|
||||
"value": "管理"
|
||||
}
|
||||
],
|
||||
"label.max": [
|
||||
@ -600,7 +606,7 @@
|
||||
"label.member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Member"
|
||||
"value": "成员"
|
||||
}
|
||||
],
|
||||
"label.members": [
|
||||
@ -630,7 +636,7 @@
|
||||
"label.my-account": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "My account"
|
||||
"value": "我的账户"
|
||||
}
|
||||
],
|
||||
"label.my-websites": [
|
||||
@ -700,7 +706,7 @@
|
||||
"label.os": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "OS"
|
||||
"value": "操作系统"
|
||||
}
|
||||
],
|
||||
"label.overview": [
|
||||
@ -718,7 +724,7 @@
|
||||
"label.page-of": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "总"
|
||||
"value": "总 "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -726,7 +732,7 @@
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": "中的第"
|
||||
"value": " 中的第 "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -734,7 +740,7 @@
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": "页"
|
||||
"value": " 页"
|
||||
}
|
||||
],
|
||||
"label.page-views": [
|
||||
@ -850,7 +856,7 @@
|
||||
"label.remove-member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Remove member"
|
||||
"value": "移除成员"
|
||||
}
|
||||
],
|
||||
"label.reports": [
|
||||
@ -922,7 +928,7 @@
|
||||
"label.select": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Select"
|
||||
"value": "选择"
|
||||
}
|
||||
],
|
||||
"label.select-date": [
|
||||
@ -934,7 +940,7 @@
|
||||
"label.select-role": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Select role"
|
||||
"value": "选择角色"
|
||||
}
|
||||
],
|
||||
"label.select-website": [
|
||||
@ -1012,7 +1018,7 @@
|
||||
"label.team-view-only": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Team view only"
|
||||
"value": "仅团队视图"
|
||||
}
|
||||
],
|
||||
"label.team-websites": [
|
||||
@ -1096,13 +1102,13 @@
|
||||
"label.transfer": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer"
|
||||
"value": "转移"
|
||||
}
|
||||
],
|
||||
"label.transfer-website": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer website"
|
||||
"value": "转移网站"
|
||||
}
|
||||
],
|
||||
"label.true": [
|
||||
@ -1240,7 +1246,7 @@
|
||||
"message.action-confirmation": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Type "
|
||||
"value": "在下面的框中输入 "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -1248,7 +1254,7 @@
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": " in the box below to confirm."
|
||||
"value": " 以确认。"
|
||||
}
|
||||
],
|
||||
"message.active-users": [
|
||||
@ -1296,7 +1302,7 @@
|
||||
"message.confirm-remove": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Are you sure you want to remove "
|
||||
"value": "您确定要移除 "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -1304,7 +1310,7 @@
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": "?"
|
||||
"value": " ?"
|
||||
}
|
||||
],
|
||||
"message.confirm-reset": [
|
||||
@ -1318,13 +1324,13 @@
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": " 的数据吗?"
|
||||
"value": " 的数据吗?"
|
||||
}
|
||||
],
|
||||
"message.delete-team-warning": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Deleting a team will also delete all team websites."
|
||||
"value": "删除团队也会删除所有团队的网站。"
|
||||
}
|
||||
],
|
||||
"message.delete-website-warning": [
|
||||
@ -1346,7 +1352,7 @@
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": "上的"
|
||||
"value": " 上的 "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -1388,7 +1394,7 @@
|
||||
"message.new-version-available": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Umami的新版本"
|
||||
"value": "Umami 的新版本 "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -1396,7 +1402,7 @@
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": "已推出!"
|
||||
"value": " 已推出!"
|
||||
}
|
||||
],
|
||||
"message.no-data-available": [
|
||||
@ -1456,7 +1462,7 @@
|
||||
"message.reset-website": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "如果确定重置该网站, 请在下面的输入框中输入 "
|
||||
"value": "如果确定重置该网站,请在下面的输入框中输入 "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -1520,25 +1526,25 @@
|
||||
"message.transfer-team-website-to-user": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer this website to your account?"
|
||||
"value": "将该网站转入您的账户?"
|
||||
}
|
||||
],
|
||||
"message.transfer-user-website-to-team": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Select the team to transfer this website to."
|
||||
"value": "选择要将该网站转移到哪个团队。"
|
||||
}
|
||||
],
|
||||
"message.transfer-website": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer website ownership to your account or another team."
|
||||
"value": "将网站所有权转移到您的账户或其他团队。"
|
||||
}
|
||||
],
|
||||
"message.triggered-event": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Triggered event"
|
||||
"value": "触发事件"
|
||||
}
|
||||
],
|
||||
"message.user-deleted": [
|
||||
@ -1550,13 +1556,13 @@
|
||||
"message.viewed-page": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Viewed page"
|
||||
"value": "已浏览页面"
|
||||
}
|
||||
],
|
||||
"message.visitor-log": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "来自"
|
||||
"value": "来自 "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -1564,7 +1570,7 @@
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": "的访客在搭载 "
|
||||
"value": " 的访客在搭载 "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -1572,7 +1578,7 @@
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": " 的"
|
||||
"value": " 的 "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -1580,7 +1586,7 @@
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": "上使用 "
|
||||
"value": " 上使用 "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -1594,7 +1600,7 @@
|
||||
"message.visitors-dropped-off": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Visitors dropped off"
|
||||
"value": "访客减少"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "已建立"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -10,7 +10,8 @@ export default {
|
||||
},
|
||||
plugins: [
|
||||
replace({
|
||||
'/api/send': process.env.COLLECT_API_ENDPOINT || '/api/send',
|
||||
'__COLLECT_API_HOST__': process.env.COLLECT_API_HOST || '',
|
||||
'__COLLECT_API_ENDPOINT__': process.env.COLLECT_API_ENDPOINT || '/api/send',
|
||||
delimiters: ['', ''],
|
||||
preventAssignment: true,
|
||||
}),
|
||||
|
@ -14,7 +14,7 @@ import styles from './NavBar.module.css';
|
||||
export function NavBar() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { pathname, router } = useNavigation();
|
||||
const { renderTeamUrl } = useTeamUrl();
|
||||
const { teamId, renderTeamUrl } = useTeamUrl();
|
||||
|
||||
const cloudMode = !!process.env.cloudMode;
|
||||
|
||||
@ -34,25 +34,38 @@ export function NavBar() {
|
||||
label: formatMessage(labels.settings),
|
||||
url: renderTeamUrl('/settings'),
|
||||
children: [
|
||||
...(teamId
|
||||
? [
|
||||
{
|
||||
label: formatMessage(labels.team),
|
||||
url: renderTeamUrl('/settings/team'),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: formatMessage(labels.websites),
|
||||
url: '/settings/websites',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.teams),
|
||||
url: '/settings/teams',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.users),
|
||||
url: '/settings/users',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.profile),
|
||||
url: '/profile',
|
||||
url: renderTeamUrl('/settings/websites'),
|
||||
},
|
||||
...(!teamId
|
||||
? [
|
||||
{
|
||||
label: formatMessage(labels.teams),
|
||||
url: renderTeamUrl('/settings/teams'),
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.users),
|
||||
url: '/settings/users',
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
label: formatMessage(labels.members),
|
||||
url: renderTeamUrl('/settings/members'),
|
||||
},
|
||||
]),
|
||||
],
|
||||
},
|
||||
cloudMode && {
|
||||
{
|
||||
label: formatMessage(labels.profile),
|
||||
url: '/profile',
|
||||
},
|
||||
@ -94,6 +107,7 @@ export function NavBar() {
|
||||
<ProfileButton />
|
||||
</div>
|
||||
<div className={styles.mobile}>
|
||||
<TeamsButton onChange={handleTeamChange} showText={false} />
|
||||
<HamburgerButton menuItems={menuItems} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,14 +1,17 @@
|
||||
.notice {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
gap: 20px;
|
||||
margin: 80px auto;
|
||||
margin: 60px auto;
|
||||
align-self: center;
|
||||
background: var(--base50);
|
||||
padding: 20px;
|
||||
border: 1px solid var(--base300);
|
||||
border-radius: var(--border-radius);
|
||||
z-index: var(--z-index-popup);
|
||||
z-index: 9999;
|
||||
box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
|
@ -4,9 +4,9 @@ import { Button } from 'react-basics';
|
||||
import { setItem } from 'next-basics';
|
||||
import useStore, { checkVersion } from 'store/version';
|
||||
import { REPO_URL, VERSION_CHECK } from 'lib/constants';
|
||||
import styles from './UpdateNotice.module.css';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import styles from './UpdateNotice.module.css';
|
||||
|
||||
export function UpdateNotice({ user, config }) {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
@ -16,8 +16,9 @@ export function UpdateNotice({ user, config }) {
|
||||
const allowUpdate =
|
||||
user?.isAdmin &&
|
||||
!config?.updatesDisabled &&
|
||||
!config?.cloudMode &&
|
||||
!pathname.includes('/share/') &&
|
||||
!process.env.cloudMode &&
|
||||
!process.env.privateMode &&
|
||||
!dismissed;
|
||||
|
||||
const updateCheck = useCallback(() => {
|
||||
|
3
src/app/(main)/profile/DateRangeSetting.module.css
Normal file
3
src/app/(main)/profile/DateRangeSetting.module.css
Normal file
@ -0,0 +1,3 @@
|
||||
.field {
|
||||
width: 200px;
|
||||
}
|
@ -3,6 +3,7 @@ import { Button, Flexbox } from 'react-basics';
|
||||
import { useDateRange, useMessages } from 'components/hooks';
|
||||
import { DEFAULT_DATE_RANGE } from 'lib/constants';
|
||||
import { DateRange } from 'lib/types';
|
||||
import styles from './DateRangeSetting.module.css';
|
||||
|
||||
export function DateRangeSetting() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
@ -13,8 +14,9 @@ export function DateRangeSetting() {
|
||||
const handleReset = () => setDateRange(DEFAULT_DATE_RANGE);
|
||||
|
||||
return (
|
||||
<Flexbox gap={10}>
|
||||
<Flexbox gap={10} width={300}>
|
||||
<DateFilter
|
||||
className={styles.field}
|
||||
value={value}
|
||||
startDate={dateRange.startDate}
|
||||
endDate={dateRange.endDate}
|
||||
|
@ -20,7 +20,7 @@ export function LanguageSetting() {
|
||||
|
||||
const handleReset = () => saveLocale(DEFAULT_LOCALE);
|
||||
|
||||
const renderValue = (value: string | number) => languages[value].label;
|
||||
const renderValue = (value: string | number) => languages?.[value]?.label;
|
||||
|
||||
return (
|
||||
<Flexbox gap={10}>
|
||||
|
@ -1,3 +1,7 @@
|
||||
.dropdown {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
div.menu {
|
||||
max-height: 300px;
|
||||
width: 300px;
|
||||
|
@ -1,26 +1,29 @@
|
||||
import { useState } from 'react';
|
||||
import { Dropdown, Item, Button, Flexbox } from 'react-basics';
|
||||
import { listTimeZones } from 'timezone-support';
|
||||
import moment from 'moment-timezone';
|
||||
import { useTimezone, useMessages } from 'components/hooks';
|
||||
import { getTimezone } from 'lib/date';
|
||||
import styles from './TimezoneSetting.module.css';
|
||||
|
||||
const timezones = moment.tz.names();
|
||||
|
||||
export function TimezoneSetting() {
|
||||
const [search, setSearch] = useState('');
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [timezone, saveTimezone] = useTimezone();
|
||||
const { timezone, saveTimezone } = useTimezone();
|
||||
const options = search
|
||||
? listTimeZones().filter(n => n.toLowerCase().includes(search.toLowerCase()))
|
||||
: listTimeZones();
|
||||
? timezones.filter(n => n.toLowerCase().includes(search.toLowerCase()))
|
||||
: timezones;
|
||||
|
||||
const handleReset = () => saveTimezone(getTimezone());
|
||||
|
||||
return (
|
||||
<Flexbox gap={10}>
|
||||
<Dropdown
|
||||
className={styles.dropdown}
|
||||
items={options}
|
||||
value={timezone}
|
||||
onChange={saveTimezone}
|
||||
onChange={(value: any) => saveTimezone(value)}
|
||||
menuProps={{ className: styles.menu }}
|
||||
allowSearch={true}
|
||||
onSearch={setSearch}
|
||||
|
@ -1,4 +1,5 @@
|
||||
'use client';
|
||||
import { Metadata } from 'next';
|
||||
import ReportsHeader from './ReportsHeader';
|
||||
import ReportsDataTable from './ReportsDataTable';
|
||||
|
||||
@ -10,6 +11,7 @@ export default function ReportsPage({ teamId }: { teamId: string }) {
|
||||
</>
|
||||
);
|
||||
}
|
||||
export const metadata = {
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Reports',
|
||||
};
|
||||
|
@ -1,38 +0,0 @@
|
||||
.menu {
|
||||
width: 360px;
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.item:hover {
|
||||
background: var(--base75);
|
||||
}
|
||||
|
||||
.type {
|
||||
color: var(--font-color300);
|
||||
}
|
||||
|
||||
.selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.popup {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.filter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
min-width: 60px;
|
||||
}
|
@ -3,9 +3,6 @@ import { createPortal } from 'react-dom';
|
||||
import { REPORT_PARAMETERS } from 'lib/constants';
|
||||
import PopupForm from './PopupForm';
|
||||
import FieldSelectForm from './FieldSelectForm';
|
||||
import FieldAggregateForm from './FieldAggregateForm';
|
||||
import FieldFilterForm from './FieldFilterForm';
|
||||
import styles from './FieldAddForm.module.css';
|
||||
|
||||
export function FieldAddForm({
|
||||
fields = [],
|
||||
@ -18,7 +15,11 @@ export function FieldAddForm({
|
||||
onAdd: (group: string, value: string) => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [selected, setSelected] = useState<{ name: string; type: string; value: string }>();
|
||||
const [selected, setSelected] = useState<{
|
||||
name: string;
|
||||
type: string;
|
||||
value: string;
|
||||
}>();
|
||||
|
||||
const handleSelect = (value: any) => {
|
||||
const { type } = value;
|
||||
@ -38,14 +39,8 @@ export function FieldAddForm({
|
||||
};
|
||||
|
||||
return createPortal(
|
||||
<PopupForm className={styles.popup}>
|
||||
<PopupForm>
|
||||
{!selected && <FieldSelectForm fields={fields} onSelect={handleSelect} />}
|
||||
{selected && group === REPORT_PARAMETERS.fields && (
|
||||
<FieldAggregateForm {...selected} onSelect={handleSave} />
|
||||
)}
|
||||
{selected && group === REPORT_PARAMETERS.filters && (
|
||||
<FieldFilterForm {...selected} onSelect={handleSave} />
|
||||
)}
|
||||
</PopupForm>,
|
||||
document.body,
|
||||
);
|
||||
|
@ -0,0 +1,36 @@
|
||||
.menu {
|
||||
position: absolute;
|
||||
max-width: 300px;
|
||||
max-height: 210px;
|
||||
}
|
||||
|
||||
.filter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.text {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.selected {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 16px;
|
||||
white-space: nowrap;
|
||||
min-width: 200px;
|
||||
font-weight: 900;
|
||||
background: var(--base100);
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search {
|
||||
position: relative;
|
||||
}
|
224
src/app/(main)/reports/[reportId]/FieldFilterEditForm.tsx
Normal file
224
src/app/(main)/reports/[reportId]/FieldFilterEditForm.tsx
Normal file
@ -0,0 +1,224 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import {
|
||||
Form,
|
||||
FormRow,
|
||||
Item,
|
||||
Flexbox,
|
||||
Dropdown,
|
||||
Button,
|
||||
SearchField,
|
||||
TextField,
|
||||
Text,
|
||||
Icon,
|
||||
Icons,
|
||||
Menu,
|
||||
Loading,
|
||||
} from 'react-basics';
|
||||
import { useMessages, useFilters, useFormat, useLocale, useWebsiteValues } from 'components/hooks';
|
||||
import { OPERATORS } from 'lib/constants';
|
||||
import { isEqualsOperator } from 'lib/params';
|
||||
import styles from './FieldFilterEditForm.module.css';
|
||||
|
||||
export interface FieldFilterFormProps {
|
||||
websiteId?: string;
|
||||
name: string;
|
||||
label?: string;
|
||||
type: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
operator?: string;
|
||||
defaultValue?: string;
|
||||
onChange?: (filter: { name: string; type: string; operator: string; value: string }) => void;
|
||||
allowFilterSelect?: boolean;
|
||||
isNew?: boolean;
|
||||
}
|
||||
|
||||
export default function FieldFilterEditForm({
|
||||
websiteId,
|
||||
name,
|
||||
label,
|
||||
type,
|
||||
startDate,
|
||||
endDate,
|
||||
operator: defaultOperator = 'eq',
|
||||
defaultValue = '',
|
||||
onChange,
|
||||
allowFilterSelect = true,
|
||||
isNew,
|
||||
}: FieldFilterFormProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [operator, setOperator] = useState(defaultOperator);
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const isEquals = isEqualsOperator(operator);
|
||||
const [search, setSearch] = useState('');
|
||||
const [selected, setSelected] = useState(isEquals ? value : '');
|
||||
const { filters } = useFilters();
|
||||
const { formatValue } = useFormat();
|
||||
const { locale } = useLocale();
|
||||
const isDisabled = !operator || (isEquals && !selected) || (!isEquals && !value);
|
||||
const {
|
||||
data: values = [],
|
||||
isLoading,
|
||||
refetch,
|
||||
} = useWebsiteValues({
|
||||
websiteId,
|
||||
type: name,
|
||||
startDate,
|
||||
endDate,
|
||||
search,
|
||||
});
|
||||
|
||||
const formattedValues = useMemo(() => {
|
||||
if (!values) {
|
||||
return {};
|
||||
}
|
||||
const formatted = {};
|
||||
const format = (val: string) => {
|
||||
formatted[val] = formatValue(val, name);
|
||||
return formatted[val];
|
||||
};
|
||||
|
||||
if (values?.length !== 1) {
|
||||
const { compare } = new Intl.Collator(locale, { numeric: true });
|
||||
values.sort((a, b) => compare(formatted[a] ?? format(a), formatted[b] ?? format(b)));
|
||||
} else {
|
||||
format(values[0]);
|
||||
}
|
||||
|
||||
return formatted;
|
||||
}, [formatValue, locale, name, values]);
|
||||
|
||||
const filteredValues = useMemo(() => {
|
||||
return value
|
||||
? values.filter((n: string | number) =>
|
||||
formattedValues[n].toLowerCase().includes(value.toLowerCase()),
|
||||
)
|
||||
: values;
|
||||
}, [value, formattedValues]);
|
||||
|
||||
const renderFilterValue = (value: any) => {
|
||||
return filters.find((filter: { value: any }) => filter.value === value)?.label;
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
onChange({ name, type, operator, value: isEquals ? selected : value });
|
||||
};
|
||||
|
||||
const handleMenuSelect = (value: string) => {
|
||||
setSelected(value);
|
||||
setShowMenu(false);
|
||||
};
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
setSearch(value);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setSelected('');
|
||||
setValue('');
|
||||
setSearch('');
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleOperatorChange = (value: any) => {
|
||||
setOperator(value);
|
||||
|
||||
if ([OPERATORS.equals, OPERATORS.notEquals].includes(value)) {
|
||||
setValue('');
|
||||
} else {
|
||||
setSelected('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
window.setTimeout(() => setShowMenu(false), 500);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<FormRow label={label} className={styles.filter}>
|
||||
<Flexbox gap={10}>
|
||||
{allowFilterSelect && (
|
||||
<Dropdown
|
||||
className={styles.dropdown}
|
||||
items={filters.filter(f => f.type === type)}
|
||||
value={operator}
|
||||
renderValue={renderFilterValue}
|
||||
onChange={handleOperatorChange}
|
||||
>
|
||||
{({ value, label }) => {
|
||||
return <Item key={value}>{label}</Item>;
|
||||
}}
|
||||
</Dropdown>
|
||||
)}
|
||||
{selected && isEquals && (
|
||||
<div className={styles.selected} onClick={handleReset}>
|
||||
<Text>{formatValue(selected, name)}</Text>
|
||||
<Icon>
|
||||
<Icons.Close />
|
||||
</Icon>
|
||||
</div>
|
||||
)}
|
||||
{!selected && isEquals && (
|
||||
<div className={styles.search}>
|
||||
<SearchField
|
||||
className={styles.text}
|
||||
value={value}
|
||||
placeholder={formatMessage(labels.enter)}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
onSearch={handleSearch}
|
||||
delay={500}
|
||||
onFocus={() => setShowMenu(true)}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
{showMenu && (
|
||||
<ResultsMenu
|
||||
values={filteredValues}
|
||||
type={name}
|
||||
isLoading={isLoading}
|
||||
onSelect={handleMenuSelect}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!selected && !isEquals && (
|
||||
<TextField
|
||||
className={styles.text}
|
||||
value={value}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</Flexbox>
|
||||
<Button variant="primary" onClick={handleAdd} disabled={isDisabled}>
|
||||
{formatMessage(isNew ? labels.add : labels.update)}
|
||||
</Button>
|
||||
</FormRow>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
const ResultsMenu = ({ values, type, isLoading, onSelect }) => {
|
||||
const { formatValue } = useFormat();
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Menu className={styles.menu} variant="popup">
|
||||
<Item>
|
||||
<Loading icon="dots" position="center" />
|
||||
</Item>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
if (!values?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu className={styles.menu} variant="popup" onSelect={onSelect}>
|
||||
{values?.map((value: any) => {
|
||||
return <Item key={value}>{formatValue(value, type)}</Item>;
|
||||
})}
|
||||
</Menu>
|
||||
);
|
||||
};
|
@ -1,24 +0,0 @@
|
||||
.popup {
|
||||
display: flex;
|
||||
max-width: 300px;
|
||||
max-height: 400px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.popup > div {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.filter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.menu {
|
||||
min-width: 200px;
|
||||
}
|
@ -1,102 +0,0 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { Form, FormRow, Item, Flexbox, Dropdown, Button } from 'react-basics';
|
||||
import { useMessages, useFilters, useFormat, useLocale } from 'components/hooks';
|
||||
import styles from './FieldFilterForm.module.css';
|
||||
|
||||
export interface FieldFilterFormProps {
|
||||
name: string;
|
||||
label?: string;
|
||||
type: string;
|
||||
values?: any[];
|
||||
onSelect?: (key: any) => void;
|
||||
allowFilterSelect?: boolean;
|
||||
}
|
||||
|
||||
export default function FieldFilterForm({
|
||||
name,
|
||||
label,
|
||||
type,
|
||||
values,
|
||||
onSelect,
|
||||
allowFilterSelect = true,
|
||||
}: FieldFilterFormProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [filter, setFilter] = useState('eq');
|
||||
const [value, setValue] = useState();
|
||||
const { getFilters } = useFilters();
|
||||
const { formatValue } = useFormat();
|
||||
const { locale } = useLocale();
|
||||
const filters = getFilters(type);
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const formattedValues = useMemo(() => {
|
||||
const formatted = {};
|
||||
const format = (val: string) => {
|
||||
formatted[val] = formatValue(val, name);
|
||||
return formatted[val];
|
||||
};
|
||||
if (values.length !== 1) {
|
||||
const { compare } = new Intl.Collator(locale, { numeric: true });
|
||||
values.sort((a, b) => compare(formatted[a] ?? format(a), formatted[b] ?? format(b)));
|
||||
} else {
|
||||
format(values[0]);
|
||||
}
|
||||
return formatted;
|
||||
}, [formatValue, locale, name, values]);
|
||||
|
||||
const filteredValues = useMemo(() => {
|
||||
return search ? values.filter(n => n.includes(search)) : values;
|
||||
}, [search, formattedValues]);
|
||||
|
||||
const renderFilterValue = value => {
|
||||
return filters.find(f => f.value === value)?.label;
|
||||
};
|
||||
|
||||
const renderValue = value => {
|
||||
return formattedValues[value];
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
onSelect({ name, type, filter, value });
|
||||
};
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<FormRow label={label} className={styles.filter}>
|
||||
<Flexbox gap={10}>
|
||||
{allowFilterSelect && (
|
||||
<Dropdown
|
||||
className={styles.dropdown}
|
||||
items={filters}
|
||||
value={filter}
|
||||
renderValue={renderFilterValue}
|
||||
onChange={(key: any) => setFilter(key)}
|
||||
>
|
||||
{({ value, label }) => {
|
||||
return <Item key={value}>{label}</Item>;
|
||||
}}
|
||||
</Dropdown>
|
||||
)}
|
||||
<Dropdown
|
||||
className={styles.dropdown}
|
||||
popupProps={{ className: styles.popup }}
|
||||
menuProps={{ className: styles.menu }}
|
||||
items={filteredValues}
|
||||
value={value}
|
||||
renderValue={renderValue}
|
||||
onChange={(key: any) => setValue(key)}
|
||||
allowSearch={true}
|
||||
onSearch={setSearch}
|
||||
>
|
||||
{(value: string) => {
|
||||
return <Item key={value}>{formattedValues[value]}</Item>;
|
||||
}}
|
||||
</Dropdown>
|
||||
</Flexbox>
|
||||
<Button variant="primary" onClick={handleAdd} disabled={!filter || !value}>
|
||||
{formatMessage(labels.add)}
|
||||
</Button>
|
||||
</FormRow>
|
||||
</Form>
|
||||
);
|
||||
}
|
63
src/app/(main)/reports/[reportId]/FieldParameters.tsx
Normal file
63
src/app/(main)/reports/[reportId]/FieldParameters.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import { useFields, useMessages } from 'components/hooks';
|
||||
import Icons from 'components/icons';
|
||||
import { useContext } from 'react';
|
||||
import { Button, FormRow, Icon, Popup, PopupTrigger } from 'react-basics';
|
||||
import FieldSelectForm from '../[reportId]/FieldSelectForm';
|
||||
import ParameterList from '../[reportId]/ParameterList';
|
||||
import PopupForm from '../[reportId]/PopupForm';
|
||||
import { ReportContext } from './Report';
|
||||
|
||||
export function FieldParameters() {
|
||||
const { report, updateReport } = useContext(ReportContext);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { parameters } = report || {};
|
||||
const { fields } = parameters || {};
|
||||
const { fields: fieldOptions } = useFields();
|
||||
|
||||
const handleAdd = (value: { name: any }) => {
|
||||
if (!fields.find(({ name }) => name === value.name)) {
|
||||
updateReport({ parameters: { fields: fields.concat(value) } });
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = (name: string) => {
|
||||
updateReport({ parameters: { fields: fields.filter(f => f.name !== name) } });
|
||||
};
|
||||
|
||||
const AddButton = () => {
|
||||
return (
|
||||
<PopupTrigger>
|
||||
<Button size="sm">
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Popup position="bottom" alignment="start">
|
||||
<PopupForm>
|
||||
<FieldSelectForm
|
||||
fields={fieldOptions.filter(({ name }) => !fields.find(f => f.name === name))}
|
||||
onSelect={handleAdd}
|
||||
showType={false}
|
||||
/>
|
||||
</PopupForm>
|
||||
</Popup>
|
||||
</PopupTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormRow label={formatMessage(labels.fields)} action={<AddButton />}>
|
||||
<ParameterList>
|
||||
{fields.map(({ name }) => {
|
||||
return (
|
||||
<ParameterList.Item key={name} onRemove={() => handleRemove(name)}>
|
||||
{fieldOptions.find(f => f.name === name)?.label}
|
||||
</ParameterList.Item>
|
||||
);
|
||||
})}
|
||||
</ParameterList>
|
||||
</FormRow>
|
||||
);
|
||||
}
|
||||
|
||||
export default FieldParameters;
|
@ -0,0 +1,40 @@
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--base800);
|
||||
border: 1px solid var(--base300);
|
||||
font-weight: 900;
|
||||
padding: 2px 8px;
|
||||
border-radius: 5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.op {
|
||||
color: var(--blue900);
|
||||
background-color: var(--blue100);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
padding: 2px 8px;
|
||||
border-radius: 5px;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: var(--base900);
|
||||
background-color: var(--base100);
|
||||
font-weight: 900;
|
||||
padding: 2px 8px;
|
||||
border-radius: 5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.edit {
|
||||
margin-top: 20px;
|
||||
}
|
136
src/app/(main)/reports/[reportId]/FilterParameters.tsx
Normal file
136
src/app/(main)/reports/[reportId]/FilterParameters.tsx
Normal file
@ -0,0 +1,136 @@
|
||||
import { useContext } from 'react';
|
||||
import { useMessages, useFormat, useFilters, useFields } from 'components/hooks';
|
||||
import Icons from 'components/icons';
|
||||
import { Button, FormRow, Icon, Popup, PopupTrigger } from 'react-basics';
|
||||
import FilterSelectForm from '../[reportId]/FilterSelectForm';
|
||||
import ParameterList from '../[reportId]/ParameterList';
|
||||
import PopupForm from '../[reportId]/PopupForm';
|
||||
import { ReportContext } from './Report';
|
||||
import FieldFilterEditForm from '../[reportId]/FieldFilterEditForm';
|
||||
import { isSearchOperator } from 'lib/params';
|
||||
import styles from './FilterParameters.module.css';
|
||||
|
||||
export function FilterParameters() {
|
||||
const { report, updateReport } = useContext(ReportContext);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { formatValue } = useFormat();
|
||||
const { parameters } = report || {};
|
||||
const { websiteId, filters, dateRange } = parameters || {};
|
||||
const { fields } = useFields();
|
||||
|
||||
const handleAdd = (value: { name: any }) => {
|
||||
if (!filters.find(({ name }) => name === value.name)) {
|
||||
updateReport({ parameters: { filters: filters.concat(value) } });
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = (name: string) => {
|
||||
updateReport({ parameters: { filters: filters.filter(f => f.name !== name) } });
|
||||
};
|
||||
|
||||
const handleChange = (close: () => void, filter: { name: any }) => {
|
||||
updateReport({
|
||||
parameters: {
|
||||
filters: filters.map(f => {
|
||||
if (filter.name === f.name) {
|
||||
return filter;
|
||||
}
|
||||
return f;
|
||||
}),
|
||||
},
|
||||
});
|
||||
close();
|
||||
};
|
||||
|
||||
const AddButton = () => {
|
||||
return (
|
||||
<PopupTrigger>
|
||||
<Button size="sm">
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Popup position="bottom" alignment="start">
|
||||
<PopupForm>
|
||||
<FilterSelectForm
|
||||
websiteId={websiteId}
|
||||
fields={fields.filter(({ name }) => !filters.find(f => f.name === name))}
|
||||
onChange={handleAdd}
|
||||
/>
|
||||
</PopupForm>
|
||||
</Popup>
|
||||
</PopupTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormRow label={formatMessage(labels.filters)} action={<AddButton />}>
|
||||
<ParameterList>
|
||||
{filters.map(
|
||||
({ name, operator, value }: { name: string; operator: string; value: string }) => {
|
||||
const label = fields.find(f => f.name === name)?.label;
|
||||
const isSearch = isSearchOperator(operator);
|
||||
|
||||
return (
|
||||
<ParameterList.Item key={name} onRemove={() => handleRemove(name)}>
|
||||
<FilterParameter
|
||||
startDate={dateRange.startDate}
|
||||
endDate={dateRange.endDate}
|
||||
websiteId={websiteId}
|
||||
name={name}
|
||||
label={label}
|
||||
operator={operator}
|
||||
value={isSearch ? value : formatValue(value, name)}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</ParameterList.Item>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</ParameterList>
|
||||
</FormRow>
|
||||
);
|
||||
}
|
||||
|
||||
const FilterParameter = ({
|
||||
websiteId,
|
||||
name,
|
||||
label,
|
||||
operator,
|
||||
value,
|
||||
type = 'string',
|
||||
startDate,
|
||||
endDate,
|
||||
onChange,
|
||||
}) => {
|
||||
const { operatorLabels } = useFilters();
|
||||
|
||||
return (
|
||||
<PopupTrigger>
|
||||
<div className={styles.item}>
|
||||
<div className={styles.label}>{label}</div>
|
||||
<div className={styles.op}>{operatorLabels[operator]}</div>
|
||||
<div className={styles.value}>{value}</div>
|
||||
</div>
|
||||
<Popup className={styles.edit} alignment="start">
|
||||
{(close: any) => (
|
||||
<PopupForm>
|
||||
<FieldFilterEditForm
|
||||
websiteId={websiteId}
|
||||
name={name}
|
||||
label={label}
|
||||
type={type}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
operator={operator}
|
||||
defaultValue={value}
|
||||
onChange={onChange.bind(null, close)}
|
||||
/>
|
||||
</PopupForm>
|
||||
)}
|
||||
</Popup>
|
||||
</PopupTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilterParameters;
|
@ -1,59 +1,41 @@
|
||||
import { useState } from 'react';
|
||||
import { Loading } from 'react-basics';
|
||||
import { subDays } from 'date-fns';
|
||||
import FieldSelectForm from './FieldSelectForm';
|
||||
import FieldFilterForm from './FieldFilterForm';
|
||||
import { useApi } from 'components/hooks';
|
||||
|
||||
function useValues(websiteId: string, type: string) {
|
||||
const now = Date.now();
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, error, isLoading } = useQuery({
|
||||
queryKey: ['websites:values', websiteId, type],
|
||||
queryFn: () =>
|
||||
get(`/websites/${websiteId}/values`, {
|
||||
type,
|
||||
startAt: +subDays(now, 90),
|
||||
endAt: now,
|
||||
}),
|
||||
enabled: !!(websiteId && type),
|
||||
});
|
||||
|
||||
return { data, error, isLoading };
|
||||
}
|
||||
import FieldFilterEditForm from './FieldFilterEditForm';
|
||||
import { useDateRange } from 'components/hooks';
|
||||
|
||||
export interface FilterSelectFormProps {
|
||||
websiteId: string;
|
||||
items: any[];
|
||||
onSelect?: (key: any) => void;
|
||||
websiteId?: string;
|
||||
fields: any[];
|
||||
onChange?: (filter: { name: string; type: string; operator: string; value: string }) => void;
|
||||
allowFilterSelect?: boolean;
|
||||
}
|
||||
|
||||
export default function FilterSelectForm({
|
||||
websiteId,
|
||||
items,
|
||||
onSelect,
|
||||
fields,
|
||||
onChange,
|
||||
allowFilterSelect,
|
||||
}: FilterSelectFormProps) {
|
||||
const [field, setField] = useState<{ name: string; label: string; type: string }>();
|
||||
const { data, isLoading } = useValues(websiteId, field?.name);
|
||||
const [{ startDate, endDate }] = useDateRange(websiteId);
|
||||
|
||||
if (!field) {
|
||||
return <FieldSelectForm fields={items} onSelect={setField} showType={false} />;
|
||||
return <FieldSelectForm fields={fields} onSelect={setField} showType={false} />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading position="center" icon="dots" />;
|
||||
}
|
||||
const { name, label, type } = field;
|
||||
|
||||
return (
|
||||
<FieldFilterForm
|
||||
name={field?.name}
|
||||
label={field?.label}
|
||||
type={field?.type}
|
||||
values={data}
|
||||
onSelect={onSelect}
|
||||
<FieldFilterEditForm
|
||||
websiteId={websiteId}
|
||||
name={name}
|
||||
label={label}
|
||||
type={type}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onChange={onChange}
|
||||
allowFilterSelect={allowFilterSelect}
|
||||
isNew={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -13,9 +13,4 @@
|
||||
border: 1px solid var(--base400);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 1px 1px 1px var(--base400);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
align-self: center;
|
||||
}
|
||||
|
@ -1,40 +1,47 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Icon, TooltipPopup } from 'react-basics';
|
||||
import { Icon } from 'react-basics';
|
||||
import Icons from 'components/icons';
|
||||
import Empty from 'components/common/Empty';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import styles from './ParameterList.module.css';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export interface ParameterListProps {
|
||||
items: any[];
|
||||
children?: ReactNode | ((item: any) => ReactNode);
|
||||
onRemove: (index: number, e: any) => void;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export function ParameterList({ items = [], children, onRemove }: ParameterListProps) {
|
||||
export function ParameterList({ children }: ParameterListProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<div className={styles.list}>
|
||||
{!items.length && <Empty message={formatMessage(labels.none)} />}
|
||||
{items.map((item, index) => {
|
||||
return (
|
||||
<div key={index} className={styles.item}>
|
||||
{typeof children === 'function' ? children(item) : item}
|
||||
<TooltipPopup
|
||||
className={styles.icon}
|
||||
label={formatMessage(labels.remove)}
|
||||
position="right"
|
||||
>
|
||||
<Icon onClick={onRemove.bind(null, index)}>
|
||||
<Icons.Close />
|
||||
</Icon>
|
||||
</TooltipPopup>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{!children && <Empty message={formatMessage(labels.none)} />}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Item = ({
|
||||
children,
|
||||
className,
|
||||
onClick,
|
||||
onRemove,
|
||||
}: {
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
onRemove?: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<div className={classNames(styles.item, className)} onClick={onClick}>
|
||||
{children}
|
||||
<Icon onClick={onRemove}>
|
||||
<Icons.Close />
|
||||
</Icon>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ParameterList.Item = Item;
|
||||
|
||||
export default ParameterList;
|
||||
|
@ -1,5 +1,4 @@
|
||||
.form {
|
||||
position: absolute;
|
||||
background: var(--base50);
|
||||
min-width: 300px;
|
||||
padding: 20px;
|
||||
|
@ -2,4 +2,5 @@
|
||||
display: grid;
|
||||
grid-template-rows: max-content 1fr;
|
||||
grid-template-columns: max-content 1fr;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ export function Report({
|
||||
className,
|
||||
}: {
|
||||
reportId: string;
|
||||
defaultParameters: { [key: string]: any };
|
||||
defaultParameters: { type: string; parameters: { [key: string]: any } };
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
|
@ -1,28 +0,0 @@
|
||||
import FunnelReport from '../funnel/FunnelReport';
|
||||
import EventDataReport from '../event-data/EventDataReport';
|
||||
import InsightsReport from '../insights/InsightsReport';
|
||||
import RetentionReport from '../retention/RetentionReport';
|
||||
import { useApi } from 'components/hooks';
|
||||
|
||||
const reports = {
|
||||
funnel: FunnelReport,
|
||||
'event-data': EventDataReport,
|
||||
insights: InsightsReport,
|
||||
retention: RetentionReport,
|
||||
};
|
||||
|
||||
export default function ReportDetails({ reportId }: { reportId: string }) {
|
||||
const { get, useQuery } = useApi();
|
||||
const { data: report } = useQuery({
|
||||
queryKey: ['reports', reportId],
|
||||
queryFn: () => get(`/reports/${reportId}`),
|
||||
});
|
||||
|
||||
if (!report) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ReportComponent = reports[report.type];
|
||||
|
||||
return <ReportComponent reportId={reportId} />;
|
||||
}
|
@ -1,6 +1,27 @@
|
||||
'use client';
|
||||
import ReportDetails from './ReportDetails';
|
||||
import FunnelReport from '../funnel/FunnelReport';
|
||||
import EventDataReport from '../event-data/EventDataReport';
|
||||
import InsightsReport from '../insights/InsightsReport';
|
||||
import RetentionReport from '../retention/RetentionReport';
|
||||
import UTMReport from '../utm/UTMReport';
|
||||
import { useReport } from 'components/hooks';
|
||||
|
||||
export default function ReportPage({ reportId }) {
|
||||
return <ReportDetails reportId={reportId} />;
|
||||
const reports = {
|
||||
funnel: FunnelReport,
|
||||
'event-data': EventDataReport,
|
||||
insights: InsightsReport,
|
||||
retention: RetentionReport,
|
||||
utm: UTMReport,
|
||||
};
|
||||
|
||||
export default function ReportPage({ reportId }: { reportId: string }) {
|
||||
const { report } = useReport(reportId);
|
||||
|
||||
if (!report) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ReportComponent = reports[report.type];
|
||||
|
||||
return <ReportComponent reportId={reportId} />;
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import PageHeader from 'components/layout/PageHeader';
|
||||
import Funnel from 'assets/funnel.svg';
|
||||
import Lightbulb from 'assets/lightbulb.svg';
|
||||
import Magnet from 'assets/magnet.svg';
|
||||
import Tag from 'assets/tag.svg';
|
||||
import styles from './ReportTemplates.module.css';
|
||||
import { useMessages, useTeamUrl } from 'components/hooks';
|
||||
|
||||
@ -30,6 +31,12 @@ export function ReportTemplates({ showHeader = true }: { showHeader?: boolean })
|
||||
url: renderTeamUrl('/reports/retention'),
|
||||
icon: <Magnet />,
|
||||
},
|
||||
{
|
||||
title: formatMessage(labels.utm),
|
||||
description: formatMessage(labels.utmDescription),
|
||||
url: renderTeamUrl('/reports/utm'),
|
||||
icon: <Tag />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
@ -60,10 +60,9 @@ export function EventDataParameters() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = (group: string, index: number) => {
|
||||
const handleRemove = (group: string) => {
|
||||
const data = [...parameterData[group]];
|
||||
data.splice(index, 1);
|
||||
updateReport({ parameters: { [group]: data } });
|
||||
updateReport({ parameters: { [group]: data.filter(({ name }) => name !== group) } });
|
||||
};
|
||||
|
||||
const AddButton = ({ group, onAdd }) => {
|
||||
@ -104,29 +103,28 @@ export function EventDataParameters() {
|
||||
label={label}
|
||||
action={<AddButton group={group} onAdd={handleAdd} />}
|
||||
>
|
||||
<ParameterList
|
||||
items={parameterData[group]}
|
||||
onRemove={index => handleRemove(group, index)}
|
||||
>
|
||||
{({ name, value }) => {
|
||||
<ParameterList>
|
||||
{parameterData[group].map(({ name, value }) => {
|
||||
return (
|
||||
<div className={styles.parameter}>
|
||||
{group === REPORT_PARAMETERS.fields && (
|
||||
<>
|
||||
<div>{name}</div>
|
||||
<div className={styles.op}>{value}</div>
|
||||
</>
|
||||
)}
|
||||
{group === REPORT_PARAMETERS.filters && (
|
||||
<>
|
||||
<div>{name}</div>
|
||||
<div className={styles.op}>{value[0]}</div>
|
||||
<div>{value[1]}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<ParameterList.Item key={name} onRemove={() => handleRemove(group)}>
|
||||
<div className={styles.parameter}>
|
||||
{group === REPORT_PARAMETERS.fields && (
|
||||
<>
|
||||
<div>{name}</div>
|
||||
<div className={styles.op}>{value}</div>
|
||||
</>
|
||||
)}
|
||||
{group === REPORT_PARAMETERS.filters && (
|
||||
<>
|
||||
<div>{name}</div>
|
||||
<div className={styles.op}>{value[0]}</div>
|
||||
<div>{value[1]}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ParameterList.Item>
|
||||
);
|
||||
}}
|
||||
})}
|
||||
</ParameterList>
|
||||
</FormRow>
|
||||
);
|
||||
|
@ -37,12 +37,12 @@
|
||||
.card {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 700;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
@ -51,19 +51,16 @@
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
background: var(--base900);
|
||||
height: 50px;
|
||||
height: 30px;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--base700);
|
||||
}
|
||||
|
||||
.value {
|
||||
color: var(--base50);
|
||||
margin-inline-end: 20px;
|
||||
color: var(--base600);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.track {
|
||||
@ -72,13 +69,33 @@
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.item {
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--base300);
|
||||
font-size: 20px;
|
||||
color: var(--base900);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.metric {
|
||||
color: var(--base700);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin: 10px 0;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.visitors {
|
||||
color: var(--base900);
|
||||
font-size: 24px;
|
||||
font-weight: 900;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.percent {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
@ -2,8 +2,8 @@ import { useContext } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
import styles from './FunnelChart.module.css';
|
||||
import { formatLongNumber } from 'lib/format';
|
||||
import styles from './FunnelChart.module.css';
|
||||
|
||||
export interface FunnelChartProps {
|
||||
className?: string;
|
||||
@ -18,35 +18,33 @@ export function FunnelChart({ className }: FunnelChartProps) {
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.chart, className)}>
|
||||
{data?.map(({ url, visitors, dropped, dropoff, remaining }, index: number) => {
|
||||
{data?.map(({ type, value, visitors, dropped, dropoff, remaining }, index: number) => {
|
||||
return (
|
||||
<div key={url} className={styles.step}>
|
||||
<div key={index} className={styles.step}>
|
||||
<div className={styles.num}>{index + 1}</div>
|
||||
<div className={styles.card}>
|
||||
<div className={styles.header}>
|
||||
<span className={styles.label}>{formatMessage(labels.viewedPage)}:</span>
|
||||
<span className={styles.item}>{url}</span>
|
||||
<span className={styles.label}>
|
||||
{formatMessage(type === 'url' ? labels.viewedPage : labels.triggeredEvent)}
|
||||
</span>
|
||||
<span className={styles.item}>{value}</span>
|
||||
</div>
|
||||
<div className={styles.metric}>
|
||||
<div>
|
||||
<span className={styles.visitors}>{formatLongNumber(visitors)}</span>
|
||||
{formatMessage(labels.visitors)}
|
||||
</div>
|
||||
<div className={styles.percent}>{(remaining * 100).toFixed(2)}%</div>
|
||||
</div>
|
||||
<div className={styles.track}>
|
||||
<div className={styles.bar} style={{ width: `${remaining * 100}%` }}>
|
||||
<span className={styles.value}>
|
||||
{remaining > 0.1 && `${(remaining * 100).toFixed(2)}%`}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.bar} style={{ width: `${remaining * 100}%` }}></div>
|
||||
</div>
|
||||
<div className={styles.info}>
|
||||
<div>
|
||||
<b>{formatLongNumber(visitors)}</b>
|
||||
<span> {formatMessage(labels.visitors)}</span>
|
||||
<span> ({(remaining * 100).toFixed(2)}%)</span>
|
||||
{dropoff > 0 && (
|
||||
<div className={styles.info}>
|
||||
<b>{formatLongNumber(dropped)}</b> {formatMessage(labels.visitorsDroppedOff)} (
|
||||
{(dropoff * 100).toFixed(2)}%)
|
||||
</div>
|
||||
{dropoff > 0 && (
|
||||
<div>
|
||||
<b>{formatLongNumber(dropped)}</b> {formatMessage(labels.visitorsDroppedOff)} (
|
||||
{(dropoff * 100).toFixed(2)}%)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
16
src/app/(main)/reports/funnel/FunnelParameters.module.css
Normal file
16
src/app/(main)/reports/funnel/FunnelParameters.module.css
Normal file
@ -0,0 +1,16 @@
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.type {
|
||||
color: var(--base700);
|
||||
}
|
||||
|
||||
.value {
|
||||
display: flex;
|
||||
align-self: center;
|
||||
gap: 20px;
|
||||
}
|
@ -10,50 +10,65 @@ import {
|
||||
Popup,
|
||||
SubmitButton,
|
||||
TextField,
|
||||
Button,
|
||||
} from 'react-basics';
|
||||
import Icons from 'components/icons';
|
||||
import UrlAddForm from './UrlAddForm';
|
||||
import FunnelStepAddForm from './FunnelStepAddForm';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
import BaseParameters from '../[reportId]/BaseParameters';
|
||||
import ParameterList from '../[reportId]/ParameterList';
|
||||
import PopupForm from '../[reportId]/PopupForm';
|
||||
import styles from './FunnelParameters.module.css';
|
||||
|
||||
export function FunnelParameters() {
|
||||
const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const { id, parameters } = report || {};
|
||||
const { websiteId, dateRange, urls } = parameters || {};
|
||||
const queryDisabled = !websiteId || !dateRange || urls?.length < 2;
|
||||
const { websiteId, dateRange, steps } = parameters || {};
|
||||
const queryDisabled = !websiteId || !dateRange || steps?.length < 2;
|
||||
|
||||
const handleSubmit = (data: any, e: any) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!queryDisabled) {
|
||||
runReport(data);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddUrl = (url: string) => {
|
||||
updateReport({ parameters: { urls: parameters.urls.concat(url) } });
|
||||
const handleAddStep = (step: { type: string; value: string }) => {
|
||||
updateReport({ parameters: { steps: parameters.steps.concat(step) } });
|
||||
};
|
||||
|
||||
const handleRemoveUrl = (index: number, e: any) => {
|
||||
e.stopPropagation();
|
||||
const urls = [...parameters.urls];
|
||||
urls.splice(index, 1);
|
||||
updateReport({ parameters: { urls } });
|
||||
const handleUpdateStep = (
|
||||
close: () => void,
|
||||
index: number,
|
||||
step: { type: string; value: string },
|
||||
) => {
|
||||
const steps = [...parameters.steps];
|
||||
steps[index] = step;
|
||||
updateReport({ parameters: { steps } });
|
||||
close();
|
||||
};
|
||||
|
||||
const AddUrlButton = () => {
|
||||
const handleRemoveStep = (index: number) => {
|
||||
const steps = [...parameters.steps];
|
||||
delete steps[index];
|
||||
updateReport({ parameters: { steps: steps.filter(n => n) } });
|
||||
};
|
||||
|
||||
const AddStepButton = () => {
|
||||
return (
|
||||
<PopupTrigger>
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
<Popup position="right" alignment="start">
|
||||
<Button>
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Popup alignment="start">
|
||||
<PopupForm>
|
||||
<UrlAddForm onAdd={handleAddUrl} />
|
||||
<FunnelStepAddForm onChange={handleAddStep} />
|
||||
</PopupForm>
|
||||
</Popup>
|
||||
</PopupTrigger>
|
||||
@ -71,11 +86,37 @@ export function FunnelParameters() {
|
||||
<TextField autoComplete="off" />
|
||||
</FormInput>
|
||||
</FormRow>
|
||||
<FormRow label={formatMessage(labels.urls)} action={<AddUrlButton />}>
|
||||
<ParameterList
|
||||
items={urls}
|
||||
onRemove={(index: number, e: any) => handleRemoveUrl(index, e)}
|
||||
/>
|
||||
<FormRow label={formatMessage(labels.steps)} action={<AddStepButton />}>
|
||||
<ParameterList>
|
||||
{steps.map((step: { type: string; value: string }, index: number) => {
|
||||
return (
|
||||
<PopupTrigger key={index}>
|
||||
<ParameterList.Item
|
||||
className={styles.item}
|
||||
onRemove={() => handleRemoveStep(index)}
|
||||
>
|
||||
<div className={styles.value}>
|
||||
<div className={styles.type}>
|
||||
<Icon>{step.type === 'url' ? <Icons.Eye /> : <Icons.Bolt />}</Icon>
|
||||
</div>
|
||||
<div>{step.value}</div>
|
||||
</div>
|
||||
</ParameterList.Item>
|
||||
<Popup alignment="start">
|
||||
{(close: () => void) => (
|
||||
<PopupForm>
|
||||
<FunnelStepAddForm
|
||||
type={step.type}
|
||||
value={step.value}
|
||||
onChange={handleUpdateStep.bind(null, close, index)}
|
||||
/>
|
||||
</PopupForm>
|
||||
)}
|
||||
</Popup>
|
||||
</PopupTrigger>
|
||||
);
|
||||
})}
|
||||
</ParameterList>
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<SubmitButton variant="primary" disabled={queryDisabled} isLoading={isRunning}>
|
||||
|
@ -9,7 +9,7 @@ import { REPORT_TYPES } from 'lib/constants';
|
||||
|
||||
const defaultParameters = {
|
||||
type: REPORT_TYPES.funnel,
|
||||
parameters: { window: 60, urls: [] },
|
||||
parameters: { window: 60, steps: [] },
|
||||
};
|
||||
|
||||
export default function FunnelReport({ reportId }: { reportId?: string }) {
|
||||
|
@ -0,0 +1,7 @@
|
||||
.dropdown {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 200px;
|
||||
}
|
80
src/app/(main)/reports/funnel/FunnelStepAddForm.tsx
Normal file
80
src/app/(main)/reports/funnel/FunnelStepAddForm.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import { useState } from 'react';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { Button, FormRow, TextField, Flexbox, Dropdown, Item } from 'react-basics';
|
||||
import styles from './FunnelStepAddForm.module.css';
|
||||
|
||||
export interface FunnelStepAddFormProps {
|
||||
type?: string;
|
||||
value?: string;
|
||||
onChange?: (step: { type: string; value: string }) => void;
|
||||
}
|
||||
|
||||
export function FunnelStepAddForm({
|
||||
type: defaultType = 'url',
|
||||
value: defaultValue = '',
|
||||
onChange,
|
||||
}: FunnelStepAddFormProps) {
|
||||
const [type, setType] = useState(defaultType);
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const items = [
|
||||
{ label: formatMessage(labels.url), value: 'url' },
|
||||
{ label: formatMessage(labels.event), value: 'event' },
|
||||
];
|
||||
const isDisabled = !type || !value;
|
||||
|
||||
const handleSave = () => {
|
||||
onChange({ type, value });
|
||||
setValue('');
|
||||
};
|
||||
|
||||
const handleChange = e => {
|
||||
setValue(e.target.value);
|
||||
};
|
||||
|
||||
const handleKeyDown = e => {
|
||||
if (e.key === 'Enter') {
|
||||
e.stopPropagation();
|
||||
handleSave();
|
||||
}
|
||||
};
|
||||
|
||||
const renderTypeValue = (value: any) => {
|
||||
return items.find(item => item.value === value)?.label;
|
||||
};
|
||||
|
||||
return (
|
||||
<Flexbox direction="column" gap={10}>
|
||||
<FormRow label={formatMessage(defaultValue ? labels.update : labels.add)}>
|
||||
<Flexbox gap={10}>
|
||||
<Dropdown
|
||||
className={styles.dropdown}
|
||||
items={items}
|
||||
value={type}
|
||||
renderValue={renderTypeValue}
|
||||
onChange={(value: any) => setType(value)}
|
||||
>
|
||||
{({ value, label }) => {
|
||||
return <Item key={value}>{label}</Item>;
|
||||
}}
|
||||
</Dropdown>
|
||||
<TextField
|
||||
className={styles.input}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
autoFocus={true}
|
||||
autoComplete="off"
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</Flexbox>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<Button variant="primary" onClick={handleSave} disabled={isDisabled}>
|
||||
{formatMessage(defaultValue ? labels.update : labels.add)}
|
||||
</Button>
|
||||
</FormRow>
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
|
||||
export default FunnelStepAddForm;
|
@ -1,14 +0,0 @@
|
||||
.form {
|
||||
position: absolute;
|
||||
background: var(--base50);
|
||||
width: 300px;
|
||||
padding: 30px;
|
||||
margin-top: 10px;
|
||||
border: 1px solid var(--base400);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 0 0 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { Button, Form, FormRow, TextField, Flexbox } from 'react-basics';
|
||||
import styles from './UrlAddForm.module.css';
|
||||
|
||||
export interface UrlAddFormProps {
|
||||
defaultValue?: string;
|
||||
onAdd?: (url: string) => void;
|
||||
}
|
||||
|
||||
export function UrlAddForm({ defaultValue = '', onAdd }: UrlAddFormProps) {
|
||||
const [url, setUrl] = useState(defaultValue);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const handleSave = () => {
|
||||
onAdd(url);
|
||||
setUrl('');
|
||||
};
|
||||
|
||||
const handleChange = e => {
|
||||
setUrl(e.target.value);
|
||||
};
|
||||
|
||||
const handleKeyDown = e => {
|
||||
if (e.key === 'Enter') {
|
||||
e.stopPropagation();
|
||||
handleSave();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<FormRow label={formatMessage(labels.url)}>
|
||||
<Flexbox gap={10}>
|
||||
<TextField
|
||||
className={styles.input}
|
||||
value={url}
|
||||
onChange={handleChange}
|
||||
autoFocus={true}
|
||||
autoComplete="off"
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
<Button variant="primary" onClick={handleSave}>
|
||||
{formatMessage(labels.add)}
|
||||
</Button>
|
||||
</Flexbox>
|
||||
</FormRow>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default UrlAddForm;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user