diff --git a/frontend/src/v2/__tests__/V2ConnectorsPage.test.tsx b/frontend/src/v2/__tests__/V2ConnectorsPage.test.tsx index 10f597ee7..9b673c12f 100644 --- a/frontend/src/v2/__tests__/V2ConnectorsPage.test.tsx +++ b/frontend/src/v2/__tests__/V2ConnectorsPage.test.tsx @@ -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: {} }); diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index ccacc74bf..2ff956c9a 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -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'); @@ -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; \}/); diff --git a/frontend/src/v2/components/V2ConnectorsPage.tsx b/frontend/src/v2/components/V2ConnectorsPage.tsx index 85120a2b3..146c14217 100644 --- a/frontend/src/v2/components/V2ConnectorsPage.tsx +++ b/frontend/src/v2/components/V2ConnectorsPage.tsx @@ -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; @@ -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 { diff --git a/frontend/src/v2/v2.css b/frontend/src/v2/v2.css index 143aa6d1e..3b327953a 100644 --- a/frontend/src/v2/v2.css +++ b/frontend/src/v2/v2.css @@ -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; @@ -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; @@ -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; } @@ -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; }