b338a7a1d2
## Summary
Two intertwined streams of work:
### UI — discovery hero pattern, settings shell, AI/API redesign
- **Generalize `SettingsDiscoveryHeroCard`** and use it on Layout, Data
Model, Apps, AI, API/Webhooks, Members. Drops 4 per-page wrapper files
(`SettingsObjectCoverImage`, `SettingsLayoutCoverImage`,
`SettingsLayoutCustomizeVideoModal`,
`SettingsDataModelVisualizeVideoModal`). Each page now supplies cover
src, modal id, and tab list.
- **Modal**: swap `<video>` placeholder for the Vimeo iframe pattern
from `twenty-docs`, per-tab `vimeoId`. Drop the parallel border-bottom
on the header (TabList draws its own baseline) and the grey background
behind the video. Note: Vimeo's embed allowlist applies — the iframes
load with the correct URL on `localhost` but the player itself requires
the video owner to allow the dev/staging domains in Vimeo settings.
- **AI page** rebuilt into a Cockpit pattern (Overview / Models / Skills
/ Tools / Usage). New `SettingsAiOverviewTab` with default Smart/Fast
pickers, at-a-glance stats, and an MCP signpost that deep-links to
`/settings/api-webhooks#mcp`. System Prompt link moved under Models.
Advanced tab removed.
- **API & Webhooks** now has 4 tabs (Playground / MCP / API Keys /
Webhooks). Hero card above tabs. Playground tab inverted to "Core API" /
"Metadata API" sections, each containing REST + GraphQL cards — schema
is the meaningful axis, protocol is secondary. Hash deep-link sync
delegated to the shared `TabListFromUrlOptionalEffect`.
- **Settings shell**: unified drawer outer padding (kill `isSettings`
branch), extract `CollapsibleNavigationDrawerSection`, add `iconColor`
on settings nav items, fix Exit Settings button alignment, 880px content
cap.
### Backend — strategy C: ephemeral playground token
The legacy paste-your-API-key flow is replaced by an on-demand
short-lived token scoped to the calling user's permissions. No shared
"Playground" API key to manage or revoke.
- New `JwtTokenTypeEnum.PLAYGROUND`. `PlaygroundTokenJwtPayload =
Omit<AccessTokenJwtPayload, 'type' | impersonation fields>` so any
future ACCESS claim flows through automatically.
- `AccessTokenService.generatePlaygroundToken` signs an access-shaped
JWT with `type: PLAYGROUND` and a configurable short TTL. A shared
private `resolveTokenSubject` helper parallelizes the user / workspace /
userWorkspace lookups for both generators.
- `JwtAuthStrategy.validateAccessToken` widened to accept
`AccessTokenJwtPayload | PlaygroundTokenJwtPayload`; impersonation gated
on `payload.type === ACCESS` so the union narrows without `as unknown
as` casts. The two branches in `validate()` collapse into one.
- New `PLAYGROUND_TOKEN_EXPIRES_IN` config var (default `2h`).
- New `generatePlaygroundToken` mutation (`WorkspaceAuthGuard`, no args,
returns `AuthToken`).
- Frontend `useOpenPlayground` hook centralizes mint → atom write →
navigate, with Apollo `onError` snackbar and a "use cached PLAYGROUND
token if still fresh" short-circuit (decodes via `jwt-decode`, checks
both `type` AND `exp`). Old API_KEY tokens left in localStorage from the
prior paste-form flow are rejected on `type` alone and force a re-mint —
this is what was causing the "This API Key is revoked" symptom on stale
browsers.
### Drive-by cleanups
- `PlaygroundToken` DTO removed (identical shape to `AuthToken` already
in use).
- 5 `customize-sidebar.webm` imports and the dead placeholder pipeline
removed.
## Test plan
### Discovery hero
- [ ] `/settings/layout`, `/settings/data-model`,
`/settings/applications`, `/settings/ai`, `/settings/api-webhooks`,
`/settings/members` each render the discovery hero card with its
illustration + play button + tabbed modal
- [ ] Modal tabs show the correct Vimeo embed URL per tab; aspect ratio
stays at 1440/900; no parallel border-bottom jog at the tab baseline
- [ ] AI Overview tab shows Smart/Fast model pickers + stats grid + MCP
signpost card; the MCP card lands on `/settings/api-webhooks#mcp` with
the MCP tab active
### API playground (ephemeral token)
- [ ] With an empty `playgroundApiKeyState` in localStorage, clicking
REST or GraphQL playground card opens the playground and the cached
token has `type: "PLAYGROUND"` with ~2h exp
- [ ] Clicking the card again within the freshness window does **not**
re-mint (`iat` / fingerprint stable across visits)
- [ ] Planting a fake API_KEY-shaped JWT in localStorage and clicking
the card forces a fresh mint (old token rejected on `type`)
- [ ] `GET /rest/companies?limit=1` with the cached token returns 200 +
real data
- [ ] `POST /graphql { __typename }` returns 200
### Settings shell
- [ ] Settings nav matches main app drawer padding; sections collapse;
Exit Settings button aligns with the workspace links above
- [ ] Active nav items have a right-gap (cleaner active state)
- [ ] Content area capped at 880px
### Verify
- [ ] `npx nx typecheck twenty-front` passes
- [ ] `npx nx typecheck twenty-server` passes
- [ ] `npx nx lint:diff-with-main twenty-front` passes
- [ ] `npx nx lint:diff-with-main twenty-server` passes
121 lines
3.7 KiB
TypeScript
121 lines
3.7 KiB
TypeScript
import { useEnterLayoutCustomizationMode } from '@/layout-customization/hooks/useEnterLayoutCustomizationMode';
|
|
import { SettingsDiscoveryHeroCard } from '@/settings/components/SettingsDiscoveryHeroCard';
|
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
|
import { SettingsLayoutItemsStats } from '@/settings/layout/components/SettingsLayoutItemsStats';
|
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
|
import { useLingui } from '@lingui/react/macro';
|
|
import { useMemo } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { AppPath, SettingsPath } from 'twenty-shared/types';
|
|
import { getSettingsPath } from 'twenty-shared/utils';
|
|
import {
|
|
H2Title,
|
|
IconAppWindow,
|
|
IconCommand,
|
|
IconLayoutDashboard,
|
|
IconLayoutSidebarLeftExpand,
|
|
IconPencil,
|
|
IconTable,
|
|
} from 'twenty-ui/display';
|
|
import { Button } from 'twenty-ui/input';
|
|
import { Section } from 'twenty-ui/layout';
|
|
import customizeIllustrationDark from '~/pages/settings/layout/assets/customize-illustration-dark.png';
|
|
import customizeIllustrationLight from '~/pages/settings/layout/assets/customize-illustration-light.png';
|
|
|
|
const SETTINGS_LAYOUT_HERO_INSTANCE_ID_PREFIX = 'settings-layout-hero';
|
|
|
|
export const SettingsLayout = () => {
|
|
const { t } = useLingui();
|
|
const navigate = useNavigate();
|
|
const { enterLayoutCustomizationMode } = useEnterLayoutCustomizationMode();
|
|
|
|
// Match the in-app customization entry points: arm customization mode before
|
|
// landing in the app, otherwise the user just drops into the app normally.
|
|
// Skip navigation when entry was blocked (e.g. a dashboard is mid-edit) so we
|
|
// don't yank the user away after the warning.
|
|
const handleCustomize = () => {
|
|
if (enterLayoutCustomizationMode()) {
|
|
navigate(AppPath.Index);
|
|
}
|
|
};
|
|
|
|
// TODO: replace placeholder demo videos per tab
|
|
const heroTabs = useMemo(
|
|
() => [
|
|
{
|
|
id: 'sidebar',
|
|
title: t`Sidebar`,
|
|
Icon: IconLayoutSidebarLeftExpand,
|
|
vimeoId: '1185511790',
|
|
},
|
|
{
|
|
id: 'record-page',
|
|
title: t`Record page`,
|
|
Icon: IconAppWindow,
|
|
vimeoId: '1185511790',
|
|
},
|
|
{
|
|
id: 'command-menu',
|
|
title: t`Command menu`,
|
|
Icon: IconCommand,
|
|
vimeoId: '1185416775',
|
|
},
|
|
{
|
|
id: 'views',
|
|
title: t`Views`,
|
|
Icon: IconTable,
|
|
vimeoId: '1145648745',
|
|
},
|
|
{
|
|
id: 'dashboards',
|
|
title: t`Dashboards`,
|
|
Icon: IconLayoutDashboard,
|
|
vimeoId: '1185511768',
|
|
},
|
|
],
|
|
[t],
|
|
);
|
|
|
|
return (
|
|
<SubMenuTopBarContainer
|
|
title={t`Layout`}
|
|
actionButton={
|
|
<Button
|
|
title={t`Customize`}
|
|
variant="primary"
|
|
accent="blue"
|
|
size="small"
|
|
Icon={IconPencil}
|
|
onClick={handleCustomize}
|
|
/>
|
|
}
|
|
links={[
|
|
{
|
|
children: t`Workspace`,
|
|
href: getSettingsPath(SettingsPath.Workspace),
|
|
},
|
|
{ children: t`Layout` },
|
|
]}
|
|
>
|
|
<SettingsPageContainer>
|
|
<Section>
|
|
<SettingsDiscoveryHeroCard
|
|
lightSrc={customizeIllustrationLight}
|
|
darkSrc={customizeIllustrationDark}
|
|
instanceIdPrefix={SETTINGS_LAYOUT_HERO_INSTANCE_ID_PREFIX}
|
|
tabs={heroTabs}
|
|
playButtonAriaLabel={t`Watch customization demo`}
|
|
/>
|
|
</Section>
|
|
<Section>
|
|
<H2Title
|
|
title={t`Overview`}
|
|
description={t`All the layout items declared on your workspace`}
|
|
/>
|
|
<SettingsLayoutItemsStats />
|
|
</Section>
|
|
</SettingsPageContainer>
|
|
</SubMenuTopBarContainer>
|
|
);
|
|
};
|