Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions frontend/src/v2/__tests__/V2ConnectorsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,48 @@ describe('V2ConnectorsPage', () => {
expect(container.querySelectorAll('.v2-connector-row__glyph')).toHaveLength(3);
});

// TASK-156. A linked Slack stores its workspace in `teamName`
// (slackOAuthService) and never in `chatTitle`, so the row read "Slack ·
// linked to Ops" beside the word Slack. The workspace name belongs on the
// detail line, not in the name slot — the name stays the platform.
describe('the row names the Slack workspace (TASK-156)', () => {
const slackRow = (config, podId = { _id: 'p2', name: 'Ops' }) => ({
_id: 'i-slack',
installationId: 'install-slack-u1',
type: 'slack',
status: 'connected',
createdAt: new Date(Date.now() - 3_600_000).toISOString(),
updatedAt: new Date().toISOString(),
config,
podId,
});

it('reads the teamName when the link carries no chatTitle', async () => {
mockGets([slackRow({ teamName: 'Acme', liveRelay: false })]);
renderPage();

expect(await screen.findByText('Acme · linked to Ops')).toBeInTheDocument();
expect(screen.queryByText('Slack · linked to Ops')).toBeNull();
const row = screen.getByText('Acme · linked to Ops').closest('.v2-connector-row');
expect(row?.querySelector('.v2-connector-row__name')?.textContent).toContain('Slack');
});

it('reads the teamName on the not-linked variant too', async () => {
mockGets([slackRow({ teamName: 'Acme', liveRelay: false }, null)]);
renderPage();

expect(await screen.findByText('Acme · not linked to a pod')).toBeInTheDocument();
});

it('keeps chatTitle winning when the link carries both', async () => {
mockGets([slackRow({ chatTitle: 'Rewire crew', teamName: 'Acme', liveRelay: false })]);
renderPage();

expect(await screen.findByText('Rewire crew · linked to Ops')).toBeInTheDocument();
expect(screen.queryByText('Acme · linked to Ops')).toBeNull();
});
});

it('offers a new code from the row and aside when the Telegram code has expired', async () => {
mockGets([{ ...connectors[0], config: { connectCode: 'abc123' } }]);
axios.post.mockResolvedValue({ data: {} });
Expand Down
41 changes: 37 additions & 4 deletions frontend/src/v2/__tests__/v2-layout-invariants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1798,8 +1798,39 @@ describe('v2 layout invariants (CSS rule presence)', () => {
expect(ruleBody(v2, '.v2-connectors__content')).toContain('grid-template-columns: minmax(min-content, 1fr) minmax(240px, 400px)');
expect(ruleBody(v2, '.v2-connector-row__details')).toContain('padding-right: 8px');
const connectors = ruleBody(v2, '.v2-connectors');
expect(connectors).toContain('min-height: calc(100vh - 86px)');
// TASK-156, inverting #1547: a list card is as tall as its rows. The page
// scrolls; no card holds empty space. ux-lead measured 564px of card around
// 262px of rows at 1200, and a Tools card of 718px around one 86px row with
// the GitHub grant open.
expect(connectors).not.toContain('min-height');
expect(connectors).not.toContain('max-width');
const connectorsContent = ruleBody(v2, '.v2-connectors__content');
expect(connectorsContent).toContain('align-items: start');
expect(connectorsContent).not.toContain('flex: 1');
expect(ruleBody(v2, '.v2-connectors__rows')).not.toContain('flex: 1');
// sprint-review's #1869 note: ruleBody reads the FIRST line-start match, so a
// second `.v2-connectors { min-height: … }` written later in the sheet passes
// every assertion above while the browser paints the stretch again — equal
// specificity, later rule wins. Scan EVERY declaration block by selector, so
// position cannot hide one. Matching is on the last compound's class tokens
// (so `.v2-root .v2-connectors` counts, and `.v2-connectors__content` does
// not); a rule scoped some other way is still not covered.
const layoutBlocks = (className: string) =>
Array.from(v2.matchAll(/([^{}]+)\{([^{}]*)\}/g))
.filter(([, sels]) =>
sels
.split(',')
.some((sel) => (sel.trim().split(/\s+/).pop() ?? '').split('.').includes(className)),
)
.map(([, , body]) => body);
// Non-vacuous: each selector's scan must still find the rule it is about.
for (const cls of ['v2-connectors', 'v2-connectors__content', 'v2-connectors__rows']) {
expect(layoutBlocks(cls).length).toBeGreaterThan(0);
}
// A list card is as tall as its rows wherever the rule is written.
expect(layoutBlocks('v2-connectors').filter((b) => b.includes('min-height'))).toEqual([]);
expect(layoutBlocks('v2-connectors__content').filter((b) => b.includes('flex: 1'))).toEqual([]);
expect(layoutBlocks('v2-connectors__rows').filter((b) => b.includes('flex: 1'))).toEqual([]);
expect(ruleBody(v2, '.v2-connectors__header p')).not.toContain('var(--v2-font-mono)');
expect(ruleBody(v2, '.v2-connector-row__glyph')).toContain('width: 20px');
expect(ruleBody(v2, '.v2-connector-row__glyph')).toContain('color: inherit');
Expand Down Expand Up @@ -1833,12 +1864,14 @@ describe('v2 layout invariants (CSS rule presence)', () => {
expect(ruleBody(v2, '.v2-tools__trail-line')).toContain('var(--v2-font-mono)');
expect(ruleBody(v2, '.v2-tools__count strong')).toContain('font-size: 22px');
expect(ruleBody(v2, '.v2-tools__count span')).toContain('font-size: 11px');
expect(connectorCss).toMatch(/@media \(max-width: 1010px\) \{[\s\S]*?\.v2-connectors \{ min-height: 0; \}/);
expect(connectorCss).toMatch(/@media \(max-width: 1010px\) \{[\s\S]*?\.v2-connectors__content \{ grid-template-columns: minmax\(0, 1fr\); gap: 24px;/);
expect(connectorCss).toMatch(/@media \(max-width: 1010px\) \{[\s\S]*?\.v2-connectors__rows \{ flex: none; \}/);
expect(connectorCss).toMatch(/@media \(max-width: 760px\) \{[\s\S]*?\.v2-connector-row \{ grid-template-columns: minmax\(0, 1fr\) auto;/);
expect(connectorCss).toMatch(/@media \(max-width: 760px\) \{[\s\S]*?\.v2-connectors__content \{ grid-template-columns: minmax\(0, 1fr\);/);
expect(connectorCss).toMatch(/@media \(max-width: 760px\) \{[\s\S]*?\.v2-connectors \{ min-height: 0; gap: 24px; margin: -12px -18px 0;/);
expect(connectorCss).toMatch(/@media \(max-width: 760px\) \{[\s\S]*?\.v2-connectors \{ gap: 24px; margin: -12px -18px 0;/);
// The Tools list opt-out is dead once nothing stretches. Matched as a regex on
// the Connectors section, not through ruleBody: with the rule gone ruleBody
// returns '' and would have passed this assertion without reading anything.
expect(connectorCss).not.toMatch(/\.v2-tools \{[^}]*flex: none/);
expect(connectorCss).toMatch(/\.v2-root button\.v2-connector-row__selection \{ grid-column: 1 \/ -1;/);
expect(connectorCss).toMatch(/\.v2-root button\.v2-connector-row__selection \.v2-connector-row__details \{ grid-column: 1 \/ -1; grid-row: 2;/);
expect(connectorCss).toMatch(/\.v2-connector-row:not\(.v2-connector-row--dead\):not\(.v2-connector-row--not-enabled\) \.v2-connector-row__detail \{ display: none; \}/);
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/v2/components/V2ConnectorsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ interface ConnectorGate {

interface ConnectorConfig {
chatTitle?: string;
// A linked Slack stores its workspace name here (slackOAuthService), never in
// chatTitle — so without this the row read "Slack · linked to …" beside the
// word Slack (TASK-156).
teamName?: string;
connectCode?: string;
connectCodeExpiresAt?: string;
liveRelay?: boolean;
Expand Down Expand Up @@ -605,7 +609,7 @@ const V2ConnectorsPage: React.FC = () => {
const started = ageLine('started', connector.createdAt);
const isTelegram = connector.type === 'telegram';
const isSlack = connector.type === 'slack';
const title = connector.config?.chatTitle || TYPE_LABELS[connector.type] || connector.type;
const title = connector.config?.chatTitle || connector.config?.teamName || TYPE_LABELS[connector.type] || connector.type;

if (connector.status === 'error') {
return {
Expand Down
15 changes: 5 additions & 10 deletions frontend/src/v2/v2.css
Original file line number Diff line number Diff line change
Expand Up @@ -9686,9 +9686,6 @@ body.modern-ui.v2-canvas {
display: flex;
flex-direction: column;
gap: 28px;
/* The channel list is the page's content card, not a short island in its
shell. The feature body is the inset pane less 28px/42px vertical pad. */
min-height: calc(100vh - 86px);
}
.v2-connectors__header h1 {
margin: 0;
Expand All @@ -9706,9 +9703,9 @@ body.modern-ui.v2-canvas {
line-height: 20px;
}
.v2-connectors__loading { color: var(--v2-text-tertiary); font-family: var(--v2-font-mono); font-size: 12px; }
.v2-connectors__content { display: grid; flex: 1; min-height: 0; grid-template-columns: minmax(min-content, 1fr) minmax(240px, 400px); gap: 40px; align-items: stretch; }
.v2-connectors__content { display: grid; grid-template-columns: minmax(min-content, 1fr) minmax(240px, 400px); gap: 40px; align-items: start; }
.v2-connectors__main { display: flex; min-width: 0; min-height: 0; flex-direction: column; }
.v2-connectors__rows { flex: 1; min-height: 0; overflow: hidden; border: 1px solid var(--v2-border-soft); border-radius: var(--v2-radius-sm); }
.v2-connectors__rows { overflow: hidden; border: 1px solid var(--v2-border-soft); border-radius: var(--v2-radius-sm); }

.v2-connector-row {
display: grid;
Expand Down Expand Up @@ -9890,8 +9887,8 @@ body.modern-ui.v2-canvas {

/* Tools — the second list (tools plan §6). Rows reuse .v2-connector-row; the
aside reuses the connector aside cards. Outcomes are words in muted mono,
never a coloured word. */
.v2-tools { flex: none; }
never a coloured word. A list card is as tall as its rows (TASK-156): neither
list stretches, so the page scrolls instead of a card holding empty space. */
.v2-tools__head { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; margin-bottom: 12px; }
.v2-tools__title { margin: 0; color: var(--v2-ink); font-family: var(--v2-font-display); font-size: 20px; font-weight: 800; letter-spacing: -0.02em; }
.v2-tools__count-line { color: var(--v2-text-tertiary); font-family: var(--v2-font-mono); font-size: 12px; line-height: 16px; }
Expand Down Expand Up @@ -9923,12 +9920,10 @@ body.modern-ui.v2-canvas {
.v2-tools__hint { color: var(--v2-text-secondary); font-size: 12px; line-height: 18px; }
.v2-tools__hint code { display: inline-block; margin: 0 6px 4px 0; padding: 1px 6px; border: 1px solid var(--v2-border-soft); border-radius: var(--v2-radius-sm); font-family: var(--v2-font-mono); font-size: 12px; white-space: nowrap; }
@media (max-width: 1010px) {
.v2-connectors { min-height: 0; }
.v2-connectors__content { grid-template-columns: minmax(0, 1fr); gap: 24px; }
.v2-connectors__rows { flex: none; }
}
@media (max-width: 760px) {
.v2-connectors { min-height: 0; gap: 24px; margin: -12px -18px 0; }
.v2-connectors { gap: 24px; margin: -12px -18px 0; }
.v2-connectors__content { grid-template-columns: minmax(0, 1fr); gap: 24px; }
.v2-connector-row { grid-template-columns: minmax(0, 1fr) auto; min-height: 0; padding: 14px 12px; }
.v2-root button.v2-connector-row__selection { grid-column: 1 / -1; grid-template-columns: minmax(0, 1fr) auto; row-gap: 4px; }
Expand Down
Loading