Skip to content

Commit 21091f9

Browse files
appflowyclaude
andauthored
Chart view v2 (#329)
* feat: chart view * chore: optimize load * test: mock Pro subscription in chart-type tests Premium chart types (Line/Donut/Horizontal Bar) get aria-label="<type> (Upgrade Required)" when isPro=false, breaking getByRole strict matches in CI (localhost backend resolves as official-hosted). Stub the billing endpoints so the chart-type rows render unlocked and clickable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: route Chart layout in ViewModal and refresh chart fields on mutation ViewModal's layout switch only mapped Grid/Board/Calendar to DatabaseView, so Chart pages opened in the modal rendered nothing. Add the case so modal routing matches AppPage's fall-through. useChartData depended on the `fields` Y.Map by identity, but Yjs mutates the map in place — so adding a groupable field while a chart was mounted left `groupableFields` and the X-axis validation stale. Subscribe via `observeDeep` and bump a clock to invalidate the memo. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: materialize Y.Map group columns to plain objects in useGroup The cloud serializes each inner group column under a view's `groups[i].groups` as a Y.Map with `id`/`visible` keys (via `GroupMap`/`GroupSettingMap` HashMaps), but the array's type signature claims `Y.Array<{ id, visible }>`. Downstream consumers — `useRowsByGroup`'s visibility filter and `useGetBoardHiddenGroup`'s hidden filter — read `.id` and `.visible` as plain properties, which always returned `undefined`, so every column got filtered out and freshly created Board databases rendered as blank. Materialize the Y.Map columns into plain `GroupColumn` objects when setting state in `useGroup`. Fixes the 15 board/row-document/view- consistency tests that hit this code path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 05d105c commit 21091f9

60 files changed

Lines changed: 4246 additions & 108 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@
160160
"react-window": "^1.8.10",
161161
"react-zoom-pan-pinch": "^3.6.1",
162162
"react18-input-otp": "^1.1.2",
163+
"recharts": "^2.15.0",
163164
"redux": "^4.2.1",
164165
"rehype-parse": "^9.0.1",
165166
"remark-gfm": "^4.0.1",
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/**
2+
* Chart settings — aggregation interactions.
3+
*
4+
* Verifies the desktop-parity behavior in
5+
* `src/components/database/components/settings/ChartLayoutSettings.tsx`:
6+
*
7+
* - Count is the default aggregation; the Y-Axis section is hidden.
8+
* - Aggregation labels appear in the desktop order
9+
* (Count → Count values → Sum → Average → Min → Max → Median).
10+
* - Selecting any non-Count aggregation reveals the Y-Axis section.
11+
* - Switching back to Count hides the Y-Axis section again.
12+
* - Count values requires a Y-Axis field too (matches desktop's
13+
* `_needsYAxisField`, which excludes only Count).
14+
*/
15+
import { expect, test } from '@playwright/test';
16+
17+
import {
18+
addChartViewTab,
19+
openChartSettings,
20+
selectAggregation,
21+
} from '../../support/chart-test-helpers';
22+
import { signInAndCreateDatabaseView } from '../../support/database-ui-helpers';
23+
import { ChartSettingsSelectors } from '../../support/selectors';
24+
import { generateRandomEmail } from '../../support/test-config';
25+
26+
test.describe('Chart settings — Aggregation', () => {
27+
test.beforeEach(async ({ page }) => {
28+
page.on('pageerror', (err) => {
29+
if (
30+
err.message.includes('Minified React error') ||
31+
err.message.includes('View not found') ||
32+
err.message.includes('No workspace or service found') ||
33+
err.message.includes('ResizeObserver loop')
34+
) {
35+
return;
36+
}
37+
});
38+
39+
await page.setViewportSize({ width: 1440, height: 900 });
40+
});
41+
42+
test('shows Count selected and hides the Y-Axis section by default', async ({
43+
page,
44+
request,
45+
}) => {
46+
// Given: a fresh chart view sitting on top of a grid
47+
const testEmail = generateRandomEmail();
48+
49+
await signInAndCreateDatabaseView(page, request, testEmail, 'Grid');
50+
await addChartViewTab(page);
51+
await openChartSettings(page);
52+
53+
// Then: Count is selected (its row carries the right-aligned tick) and the
54+
// Y-Axis section is not rendered yet.
55+
const countItem = ChartSettingsSelectors.aggregationItem(page, 'Count');
56+
57+
await expect(countItem).toBeVisible();
58+
await expect(countItem.locator('svg')).toBeVisible();
59+
await expect(ChartSettingsSelectors.yAxisLabel(page)).toHaveCount(0);
60+
});
61+
62+
test('lists the seven aggregations in desktop order', async ({ page, request }) => {
63+
const testEmail = generateRandomEmail();
64+
65+
await signInAndCreateDatabaseView(page, request, testEmail, 'Grid');
66+
await addChartViewTab(page);
67+
await openChartSettings(page);
68+
69+
// Each label should be present in the menu.
70+
for (const label of ['Count', 'Count values', 'Sum', 'Average', 'Min', 'Max', 'Median']) {
71+
await expect(ChartSettingsSelectors.aggregationItem(page, label)).toBeVisible();
72+
}
73+
74+
// Order: scoped to the Aggregation list region (skip the X-Axis fields).
75+
// We check the *relative* positions of the seven aggregation items rather
76+
// than the entire menuitem index, since the menu also includes X-Axis
77+
// field rows above the Aggregation section.
78+
const positions = await Promise.all(
79+
['Count', 'Count values', 'Sum', 'Average', 'Min', 'Max', 'Median'].map(async (label) => {
80+
const box = await ChartSettingsSelectors.aggregationItem(page, label).boundingBox();
81+
82+
return { label, top: box?.y ?? -1 };
83+
})
84+
);
85+
const tops = positions.map((p) => p.top);
86+
87+
expect(tops.every((t) => t > 0)).toBe(true);
88+
for (let i = 1; i < tops.length; i++) {
89+
expect(tops[i]).toBeGreaterThan(tops[i - 1]);
90+
}
91+
});
92+
93+
test('selecting Sum reveals the Y-Axis section and moves the tick', async ({
94+
page,
95+
request,
96+
}) => {
97+
const testEmail = generateRandomEmail();
98+
99+
await signInAndCreateDatabaseView(page, request, testEmail, 'Grid');
100+
await addChartViewTab(page);
101+
await openChartSettings(page);
102+
103+
// When: switching aggregation to Sum. selectAggregation closes the menu
104+
// afterwards so the Yjs write can settle.
105+
await selectAggregation(page, 'Sum');
106+
107+
// Reopen and assert state
108+
await openChartSettings(page);
109+
110+
// Then: the Y-Axis section is now visible
111+
await expect(ChartSettingsSelectors.yAxisLabel(page)).toBeVisible({ timeout: 5000 });
112+
113+
// And: the tick is on Sum (not Count)
114+
await expect(
115+
ChartSettingsSelectors.aggregationItem(page, 'Sum').locator('svg')
116+
).toBeVisible();
117+
await expect(
118+
ChartSettingsSelectors.aggregationItem(page, 'Count').locator('svg')
119+
).toHaveCount(0);
120+
});
121+
122+
test('Count values also reveals the Y-Axis section', async ({ page, request }) => {
123+
// Desktop's `_needsYAxisField` excludes only Count — Count values counts
124+
// distinct values *of* the Y-Axis field, so it requires one too.
125+
const testEmail = generateRandomEmail();
126+
127+
await signInAndCreateDatabaseView(page, request, testEmail, 'Grid');
128+
await addChartViewTab(page);
129+
await openChartSettings(page);
130+
131+
await selectAggregation(page, 'Count values');
132+
await openChartSettings(page);
133+
134+
await expect(ChartSettingsSelectors.yAxisLabel(page)).toBeVisible({ timeout: 5000 });
135+
await expect(
136+
ChartSettingsSelectors.aggregationItem(page, 'Count values').locator('svg')
137+
).toBeVisible();
138+
});
139+
140+
test('switching back to Count hides the Y-Axis section', async ({ page, request }) => {
141+
const testEmail = generateRandomEmail();
142+
143+
await signInAndCreateDatabaseView(page, request, testEmail, 'Grid');
144+
await addChartViewTab(page);
145+
await openChartSettings(page);
146+
147+
// Move to Sum first, confirm Y-Axis is visible
148+
await selectAggregation(page, 'Sum');
149+
await openChartSettings(page);
150+
await expect(ChartSettingsSelectors.yAxisLabel(page)).toBeVisible({ timeout: 5000 });
151+
152+
// Then switch back to Count
153+
await selectAggregation(page, 'Count');
154+
await openChartSettings(page);
155+
156+
await expect(ChartSettingsSelectors.yAxisLabel(page)).toHaveCount(0);
157+
await expect(
158+
ChartSettingsSelectors.aggregationItem(page, 'Count').locator('svg')
159+
).toBeVisible();
160+
});
161+
});
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/**
2+
* Chart database view — basic tests.
3+
*
4+
* Migrated from: cypress/e2e/database/chart-basic.cy.ts (chart_view branch)
5+
*
6+
* Covers: create chart from sidebar, create chart view from grid, render with
7+
* data, empty-category fallback, and view-tab switching.
8+
*/
9+
import { expect, test } from '@playwright/test';
10+
11+
import { signInAndWaitForApp } from '../../support/auth-flow-helpers';
12+
import {
13+
addChartViewTab,
14+
setSelectOptionOnRow,
15+
waitForChartReady,
16+
} from '../../support/chart-test-helpers';
17+
import { signInAndCreateDatabaseView } from '../../support/database-ui-helpers';
18+
import {
19+
AddPageSelectors,
20+
ChartSelectors,
21+
DatabaseGridSelectors,
22+
DatabaseViewSelectors,
23+
} from '../../support/selectors';
24+
import { generateRandomEmail } from '../../support/test-config';
25+
26+
test.describe('Database Chart View Basic', () => {
27+
test.beforeEach(async ({ page }) => {
28+
page.on('pageerror', (err) => {
29+
if (
30+
err.message.includes('Minified React error') ||
31+
err.message.includes('View not found') ||
32+
err.message.includes('No workspace or service found') ||
33+
err.message.includes('ResizeObserver loop')
34+
) {
35+
return;
36+
}
37+
});
38+
39+
await page.setViewportSize({ width: 1280, height: 720 });
40+
});
41+
42+
test('should create a chart database from the sidebar', async ({ page, request }) => {
43+
const testEmail = generateRandomEmail();
44+
45+
// Given: a signed-in user
46+
await signInAndWaitForApp(page, request, testEmail);
47+
await expect(page).toHaveURL(/\/app/, { timeout: 30000 });
48+
await page.waitForTimeout(3000);
49+
50+
// When: clicking "+" then "Chart" in the sidebar
51+
await AddPageSelectors.inlineAddButton(page).first().click({ force: true });
52+
await page.waitForTimeout(1000);
53+
await AddPageSelectors.addChartButton(page).click({ force: true });
54+
await page.waitForTimeout(5000);
55+
56+
// Then: a Chart view is rendered
57+
await waitForChartReady(page);
58+
});
59+
60+
test('should create a chart view from an existing grid database', async ({
61+
page,
62+
request,
63+
}) => {
64+
const testEmail = generateRandomEmail();
65+
66+
// Given: a grid database
67+
await signInAndCreateDatabaseView(page, request, testEmail, 'Grid');
68+
69+
// When: adding a Chart view tab
70+
await addChartViewTab(page);
71+
72+
// Then: the Chart view is rendered
73+
await expect(ChartSelectors.chart(page)).toBeVisible({ timeout: 15000 });
74+
});
75+
76+
test('should display chart with data when rows have select option values', async ({
77+
page,
78+
request,
79+
}) => {
80+
const testEmail = generateRandomEmail();
81+
82+
// Given: a grid with two rows tagged with different SingleSelect options
83+
await signInAndCreateDatabaseView(page, request, testEmail, 'Grid');
84+
await setSelectOptionOnRow(page, 0, 'Option A');
85+
await setSelectOptionOnRow(page, 1, 'Option B');
86+
87+
// When: opening a Chart view on top of that grid
88+
await addChartViewTab(page);
89+
90+
// Then: the Recharts wrapper is mounted (chart actually rendered)
91+
await waitForChartReady(page);
92+
});
93+
94+
test('should show empty category when rows have no select option value', async ({
95+
page,
96+
request,
97+
}) => {
98+
const testEmail = generateRandomEmail();
99+
100+
// Given: a grid with no values entered into the Type field
101+
await signInAndCreateDatabaseView(page, request, testEmail, 'Grid');
102+
103+
// When: switching to Chart view
104+
await addChartViewTab(page);
105+
106+
// Then: chart renders and includes the "No Type" empty category label
107+
await waitForChartReady(page);
108+
await expect(ChartSelectors.chart(page)).toContainText('No Type');
109+
});
110+
111+
test('should switch between Grid and Chart views', async ({ page, request }) => {
112+
const testEmail = generateRandomEmail();
113+
114+
// Given: a grid with a Chart view tab
115+
await signInAndCreateDatabaseView(page, request, testEmail, 'Grid');
116+
await addChartViewTab(page);
117+
await expect(ChartSelectors.chart(page)).toBeVisible({ timeout: 15000 });
118+
119+
// When: clicking back on the first (Grid) tab
120+
const tabs = DatabaseViewSelectors.viewTab(page);
121+
122+
await tabs.first().click({ force: true });
123+
await page.waitForTimeout(1000);
124+
125+
// Then: the grid is the active view
126+
await expect(tabs.first()).toHaveAttribute('data-state', 'active');
127+
await expect(DatabaseGridSelectors.grid(page)).toBeVisible();
128+
129+
// When: clicking the second (Chart) tab again
130+
await tabs.nth(1).click({ force: true });
131+
await page.waitForTimeout(1000);
132+
133+
// Then: the chart is active again
134+
await expect(tabs.nth(1)).toHaveAttribute('data-state', 'active');
135+
await expect(ChartSelectors.chart(page)).toBeVisible();
136+
});
137+
});

0 commit comments

Comments
 (0)