Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 21972e8052 Fix dashboard Rich Text widget not entering edit mode on click
https://sonarly.com/issue/17823?type=bug

Clicking a Rich Text widget on a dashboard never sets its editing state, making it impossible to type or edit any content in the widget.

Fix: Added `setPageLayoutEditingWidgetId(widgetId)` to the default fallback path in `useEditPageLayoutWidget.ts` (line 69).

**Root cause:** Commit `2a6fcfcfb3` ("Side Panel Sub Page Framework®") moved `setPageLayoutEditingWidgetId(widgetId)` from being called unconditionally at the top of `handleEditWidget` to being called only inside IFRAME/GRAPH/FIELDS-specific branches. The `STANDALONE_RICH_TEXT` widget type falls through to the default path, which only closed the side panel without setting the editing widget ID.

**What this fixes:** When a user clicks on a Rich Text widget in dashboard edit mode, `pageLayoutEditingWidgetId` is now correctly set to the widget's ID. This makes `isThisWidgetBeingEdited` true in `StandaloneRichTextWidget.tsx`, which sets `isEditable` to true, allowing the BlockNote editor to accept input.

**Why this approach:** This matches the pre-regression pattern where the editing widget ID was set for ALL widget types. Widget types with dedicated side panel settings (IFRAME, GRAPH, FIELDS) handle navigation + ID setting in their own branches and return early, so they are unaffected by this change.
2026-03-24 09:34:16 +00:00
493830204a [AI] Fix new chat button stacking side panel and auto-focus editor (#18875)
closes
https://discord.com/channels/1130383047699738754/1480984504737861853
https://discord.com/channels/1130383047699738754/1481210013950279740

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-03-24 09:26:33 +01:00
Félix MalfaitandGitHub dd84ab25df chore: optimize app-dev Docker image and add CI test (#18856)
## Summary

- **Reduce app-dev image size** by stripping ~60MB of build artifacts
not needed at runtime from the server build stage: `.js.map` source maps
(29MB), `.d.ts` type declarations (9MB), compiled test files (14MB), and
unused package source directories (~9MB).
- **Add CI smoke test** for the `twenty-app-dev` all-in-one Docker
image, running in parallel with the existing docker-compose test. Builds
the image, starts the container, and verifies `/healthz` returns 200.

## Test plan

- [x] Built image locally and verified server, worker, Postgres, and
Redis all start correctly
- [x] Verified `/healthz` returns 200 and frontend serves at `/`
- [ ] CI `test-compose` job passes (existing test, renamed from `test`)
- [ ] CI `test-app-dev` job passes (new parallel job)

Made with [Cursor](https://cursor.com)
2026-03-24 08:44:30 +01:00
fd044ba9a2 chore: sync AI model catalog from models.dev (#18885)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

This PR updates pricing, context windows, and model availability based
on the latest data.
New models meeting inclusion criteria (tool calling, pricing data,
context limits) are added automatically.
Deprecated models are detected based on cost-efficiency within the same
model family.

**Please review before merging** — verify no critical models were
incorrectly deprecated.

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-03-24 07:19:06 +01:00
Charles BochetandGitHub 69fe6fcadd chore: add explicit return type to getBackgroundColor (#18878)
Trivial typing improvement to trigger CI Front + CI UI visual regression
pipelines.
2026-03-24 01:10:22 +01:00
Charles BochetandGitHub 0ded15b363 Add visual regression CI for twenty-ui (#18877)
## Summary

- New workflow `ci-visual-regression.yaml` that runs on PRs touching
`twenty-ui` or `twenty-shared`
- Builds `twenty-ui` storybook and uploads the tarball as a GitHub
Actions artifact
- Dispatches to `twentyhq/ci-privileged` which handles the pixel-diff
comparison and posts a PR comment

### Flow

```
twenty CI (this PR)          ci-privileged                  pixel-perfect
─────────────────          ──────────────                  ──────────────
Build storybook
Upload artifact
Dispatch ──────────────►  Download artifact
                          Upload to S3 (OIDC)
                          POST /import-from-storage ────►  Import build
                          POST /diffs/run ──────────────►  Screenshots + diff
                          ◄──────────────────────────────  Diff report JSON
                          Post PR comment
```

Companion PRs:
- https://github.com/twentyhq/ci-privileged/pull/1 (ci-privileged
workflow)
- https://github.com/twentyhq/twenty-infra/pull/497 (OIDC trust + Helm
cleanup)
- https://github.com/twentyhq/pixel-perfect/pull/5 (API simplification)

## Test plan

- [ ] Merge companion PRs first and configure secrets/environments
- [ ] Open a test PR touching twenty-ui, verify storybook builds and
dispatch fires
- [ ] Verify visual regression comment appears on the PR
2026-03-24 00:15:45 +01:00
Charles BochetandGitHub 708e53d829 Fix multi-select option removal crashing when records contain removed values (#18871)
## Summary

- Fixes a bug where removing an option from a MULTI_SELECT field fails
with `invalid input value for enum` when existing records contain the
removed value alongside surviving values.
- The root cause was the `ELSE` branch in `updateArrayEnum` which tried
to cast removed enum values (e.g. `DISTRIBUTOR`) to the new enum type
that no longer includes them.
- The fix replaces the `ELSE` cast with a NULL-producing implicit CASE
default and uses `array_agg(...) FILTER (WHERE mapped_value IS NOT
NULL)` to silently strip removed values from existing arrays.

### Before (bug)
```sql
-- ELSE branch tries to cast removed value to new enum → crash
CASE unnest_value::text
  WHEN 'IMPL' THEN 'IMPL'::new_enum
  WHEN 'APP' THEN 'APP'::new_enum
  ELSE unnest_value::text::new_enum  -- 'DISTRIBUTOR' fails here
END
```

### After (fix)
```sql
-- No ELSE: removed values produce NULL, filtered out by array_agg
SELECT array_agg(mapped_value) FILTER (WHERE mapped_value IS NOT NULL)
FROM (
  SELECT CASE unnest_value::text
    WHEN 'IMPL' THEN 'IMPL'::new_enum
    WHEN 'APP' THEN 'APP'::new_enum
  END AS mapped_value
  FROM unnest(old_column) AS unnest_value
) enum_mapping
```
2026-03-23 20:06:34 +00:00
e2c85b5af0 Fix workspace dropdown truncation and center auth titles (#18869)
# Before

<img width="543" height="315" alt="CleanShot 2026-03-23 at 18 37 17"
src="https://github.com/user-attachments/assets/3a8233f8-507d-40e3-b4f0-0aae325bc4a8"
/>


# After

<img width="230" height="167" alt="CleanShot 2026-03-23 at 18 38 06"
src="https://github.com/user-attachments/assets/dda637d8-766a-4dc4-9909-78edef866c88"
/>

+ video: (long & short title)


https://github.com/user-attachments/assets/5373d168-6ffc-495b-909c-27fc1e68e712


also fixed text not centered in onboarding

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-23 21:06:14 +01:00
Thomas TrompetteandGitHub c6d4162b73 Fix: Background color selected row + border bottom settings tab (#18870)
<img width="1285" height="73" alt="Capture d’écran 2026-03-23 à 18 58
09"
src="https://github.com/user-attachments/assets/a01cfc8e-6492-4ebe-a47a-b504a73e616c"
/>
<img width="574" height="59" alt="Capture d’écran 2026-03-23 à 18 58
39"
src="https://github.com/user-attachments/assets/4947ec02-e151-48fb-87e9-86dbc0ed4fe9"
/>
2026-03-23 19:22:28 +00:00
93de331428 i18n - translations (#18873)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-23 19:36:47 +01:00
Abdul RahmanandGitHub 24055527c8 Rename "New sidebar item" to "New menu item" (#18859) 2026-03-23 18:21:41 +00:00
EtienneandGitHub c58bf9cf06 Fix e2e test (#18866)
Three feature flags changed the UI:
IS_NAVIGATION_MENU_ITEM_ENABLED — Sidebar no longer has a default
"People" link → navigate via URL instead
IS_COMMAND_MENU_ITEM_ENABLED — Button label is now "Create new Person"
(interpolated from object name) instead of static "Create new record"
IS_JUNCTION_RELATIONS_ENABLED — Company is now a junction relation
("Previous Companies") displayed inline, no longer a boxed
dynamic-relation-widget on the record page
2026-03-23 16:56:15 +00:00
Baptiste DevessierandGitHub 07a4cf2d26 Ensure command backfills all relations as Field widget (#18858)
\+ add missing Field widget for workflow relations

Copies the logic of the injectRelationWidgetsIntoLayout front-end
function
2026-03-23 17:49:05 +01:00
Raphaël BosiandGitHub 1cfdd2e8ed Update BackfillCommandMenuItemsCommand with workflow backfill (#18848)
Updated upgrade command to also backfill command menu items for
workflows.
This has been done in the same command because we need to enable the
feature flag once both operations are complete: the creation of the
standard command menu items and the creation of workflow command menu
items.
2026-03-23 17:48:50 +01:00
Félix MalfaitGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>EtienneEtienne
6f4f7a1198 fix: add security headers to file serving endpoints to prevent stored XSS (#18857)
## Summary

- File serving endpoints (`GET file/:fileFolder/:id` and `GET
public-assets/...`) were piping S3/local file streams directly to the
response without any HTTP headers, allowing a stored XSS attack via
uploaded HTML files rendered inline on the CRM origin.
- Adds `Content-Type`, `Content-Disposition`, and
`X-Content-Type-Options: nosniff` headers to all file serving responses.
Only known-safe MIME types (images, PDF, plain text, audio, video) are
served inline; everything else (HTML, SVG, XML, etc.) forces
`Content-Disposition: attachment` to trigger download instead of
rendering.
- New `setFileResponseHeaders` utility with an explicit allowlist of
inline-safe MIME types.

## Test plan

- [x] Unit tests pass (9 tests including 2 new ones: header assertions
and attachment-disposition for HTML)
- [x] Lint clean (`lint:diff-with-main`)
- [x] Typecheck clean (`nx typecheck twenty-server`)
- [ ] Manual: upload an HTML file via `uploadWorkflowFile`, access the
returned URL — should download instead of rendering
- [ ] Manual: upload a PNG image, access the URL — should render inline
with correct `Content-Type: image/png`
- [ ] Manual: verify `X-Content-Type-Options: nosniff` header is present
on all file responses


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Etienne <etiennejouan@users.noreply.github.com>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-03-23 16:19:16 +01:00
Charles BochetandGitHub 0626f3e469 Fix deploy: remove stale @ai-sdk/groq from yarn.lock (#18863)
## Summary

- The front deploy to S3 is failing because `@ai-sdk/groq` was removed
from `packages/twenty-server/package.json` but the `yarn.lock` was never
updated
2026-03-23 15:39:35 +01:00
Félix MalfaitandGitHub 630f3a0fd7 chore: trigger automerge for AI catalog sync PRs (#18855)
## Summary
- Adds a `repository-dispatch` step to the AI catalog sync workflow
(`ci-ai-catalog-sync.yaml`) that notifies `twenty-infra` when a sync PR
is created
- This allows the automerge workflow in `twenty-infra` to reactively
merge the PR instead of waiting for the next cron run

## Test plan
- [ ] Verify `TWENTY_INFRA_TOKEN` secret is available (same one used by
i18n workflows)
- [ ] Trigger the AI catalog sync workflow manually and confirm the
dispatch fires

Made with [Cursor](https://cursor.com)
2026-03-23 13:33:19 +01:00
b813d64324 chore: sync AI model catalog from models.dev (#18854)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

This PR updates pricing, context windows, and model availability based
on the latest data.
New models meeting inclusion criteria (tool calling, pricing data,
context limits) are added automatically.
Deprecated models are detected based on cost-efficiency within the same
model family.

**Please review before merging** — verify no critical models were
incorrectly deprecated.

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-03-23 13:14:09 +01:00
Félix MalfaitandGitHub c9ca4dfa14 fix: run AI catalog sync as standalone script to avoid DB dependency (#18853)
## Summary
- The `ci-ai-catalog-sync` cron workflow was failing because the
`ai:sync-models-dev` NestJS command bootstraps the full app, which tries
to connect to PostgreSQL — unavailable in CI.
- Converted the sync logic to a standalone `ts-node` script
(`scripts/ai-sync-models-dev.ts`) that runs without NestJS, eliminating
the database dependency.
- Removed the `Build twenty-server` step from the workflow since it's no
longer needed, making the job faster.

## Test plan
- [x] Verified the standalone script runs successfully locally via `npx
nx run twenty-server:ts-node-no-deps-transpile-only --
./scripts/ai-sync-models-dev.ts`
- [x] Verified `--dry-run` flag works correctly
- [x] Verified the output `ai-providers.json` is correctly written with
valid JSON (135 models across 5 providers)
- [x] Verified the script passes linting with zero errors
- [ ] CI should pass without requiring a database service

Fixes:
https://github.com/twentyhq/twenty/actions/runs/23424202182/job/68135439740

Made with [Cursor](https://cursor.com)
2026-03-23 13:07:53 +01:00
Félix MalfaitandGitHub 2dfa742543 chore: improve i18n workflow to prevent stale compiled translations (#18850)
## Summary

- **Add `lingui:compile` to Dockerfile** before both the server and
frontend build stages, ensuring compiled translation catalogs are always
fresh regardless of git state
- **Add `repository-dispatch` to i18n workflows** (`i18n-push.yaml` and
`i18n-pull.yaml`) to trigger reactive automerge in `twenty-infra` when
the i18n PR is ready, replacing the 15-minute polling approach

## Context

Users sometimes see "Uncompiled message detected" errors because
releases can be cut from `main` before the i18n PR (with freshly
compiled translation catalogs) has been merged. This creates a race
condition between new translatable strings landing on `main` and their
compiled catalogs being available.

These changes fix this in two ways:
1. **Safety net in builds**: Every Docker build now compiles
translations before building, so even if compiled catalogs in git are
stale, the build artifact is always correct
2. **Faster i18n PR merges**: Instead of a 15-minute cron polling for
i18n PRs, the workflows now notify `twenty-infra` immediately when
translations are ready, reducing merge latency from ~15 minutes to ~1
minute

Companion PR in twenty-infra: twentyhq/twenty-infra
(feat/i18n-reactive-automerge)

## Test plan

- [ ] Verify `TWENTY_INFRA_TOKEN` secret is available to i18n workflows
- [ ] Docker build still succeeds with the added `lingui:compile` steps
- [ ] i18n-push triggers automerge in twenty-infra after pushing changes
- [ ] i18n-pull triggers automerge in twenty-infra after pulling
translations


Made with [Cursor](https://cursor.com)
2026-03-23 12:53:31 +01:00
e618cc6cf9 i18n - translations (#18846)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-23 10:36:08 +01:00
Félix MalfaitandGitHub bb9e3c44a1 Show AI provider sections regardless of billing status (#18845)
## Summary

- Removes the `isBillingEnabled` guard that was hiding the Providers and
Custom Providers sections in the Admin AI settings
- The `GET_AI_PROVIDERS` query was being skipped when billing was
enabled, and the provider UI sections were conditionally hidden —
there's no reason to gate provider configuration behind billing status
- Cleans up the now-unused `billingState` and `useAtomStateValue`
imports

## Test plan

- [ ] Verify Providers and Custom Providers sections are visible in
Admin > AI settings on cloud (billing enabled)
- [ ] Verify they remain visible on self-hosted (billing disabled)


Made with [Cursor](https://cursor.com)
2026-03-23 10:29:32 +01:00
77d4bd9158 Add billing usage analytics dashboard with ClickHouse integration (#18592)
## Summary
This PR adds a comprehensive billing usage analytics feature that
provides detailed breakdowns of credit consumption across execution
types, users, resources, and time periods. The implementation includes a
new ClickHouse-backed analytics service, GraphQL API endpoint, and a
frontend dashboard component.

## Key Changes

### Backend
- **New BillingAnalyticsService**: Queries ClickHouse for usage
breakdowns by user, resource, execution type, and time series data
- **BillingEventWriterService**: Writes billing events to ClickHouse for
analytics while maintaining best-effort semantics (never blocks Stripe
billing)
- **ClickHouse Schema**: Added `billingEvent` table with 3-year TTL for
storing detailed billing event data
- **GraphQL Resolver**: New `getBillingAnalytics` query that aggregates
usage data for the current billing period, protected by feature flag and
billing permissions
- **Enhanced BillingUsageEvent**: Added `userWorkspaceId` field to track
per-user credit consumption
- **AI Billing Integration**: Updated AI billing service to pass
`userWorkspaceId` when recording usage events

### Frontend
- **SettingsBillingAnalyticsSection**: New component displaying:
  - Usage breakdown by execution type with progress bars
  - Daily usage time series chart (28-day view)
  - Per-user credit consumption breakdown
  - Per-resource (agent/workflow) credit consumption breakdown
- **SettingsUsage Page**: Dedicated page for viewing usage analytics
- **GraphQL Query**: `GetBillingAnalytics` query with generated hooks
- **Navigation**: Added Usage menu item in settings (feature-flagged)
- **Mock Data**: Included screenshot mock data for preview/testing

### Feature Flag
- Added `IS_USAGE_ANALYTICS_ENABLED` feature flag to control visibility
and access to analytics features

## Implementation Details
- Analytics data is queried in parallel for performance
- ClickHouse writes are non-blocking to ensure billing operations never
fail
- Progress bars use dynamic coloring from a predefined palette
- Time series visualization normalizes bar heights relative to max value
- Empty state handling when no analytics data is available
- Responsive UI with proper text truncation for long names

https://claude.ai/code/session_01Y1EqrX6PFq3EJxJq89h7DF

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-23 10:28:23 +01:00
Félix MalfaitandGitHub 49af539032 Fix migration crash: agent.modelId NOT NULL violation (#18844)
## Summary

- The `MigrateModelIdsToCompositeFormat` migration was setting
`agent.modelId = NULL`, but the column has a `NOT NULL` constraint —
causing the migration to fail on deploy.
- Fixed by setting `modelId` to the `'default-smart-model'` sentinel
value instead of NULL. The runtime already resolves this sentinel
dynamically via `getEffectiveModelConfig` / `isDefaultModelSentinel`, so
agents correctly fall back to the admin-configured smart model.

## Test plan

- [ ] Run `npx nx run twenty-server:database:migrate:prod` — migration
should complete without errors
- [ ] Verify agents still resolve to the correct model at runtime
(sentinel → `getDefaultPerformanceModel()`)


Made with [Cursor](https://cursor.com)
2026-03-23 09:44:41 +01:00
275 changed files with 12564 additions and 4888 deletions
+9 -4
View File
@@ -27,11 +27,8 @@ jobs:
- name: Build dependencies
run: npx nx build twenty-shared
- name: Build twenty-server
run: npx nx build twenty-server
- name: Run catalog sync
run: npx nx run twenty-server:command-no-deps ai:sync-models-dev
run: npx nx run twenty-server:ts-node-no-deps-transpile-only -- ./scripts/ai-sync-models-dev.ts
- name: Check for changes
id: changes
@@ -61,3 +58,11 @@ jobs:
base: main
labels: ai, automated
delete-branch: true
- name: Trigger automerge
if: steps.changes.outputs.changed == 'true'
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
repository: twentyhq/twenty-infra
event-type: automated-pr-ready
+34
View File
@@ -151,6 +151,40 @@ jobs:
# npx nyc merge coverage-artifacts ${{ env.PATH_TO_COVERAGE }}/coverage-storybook.json
# - name: Checking coverage
# run: npx nx storybook:coverage twenty-front --checkCoverage=true --configuration=${{ matrix.storybook_scope }}
visual-regression-dispatch:
needs: front-sb-build
if: github.event_name == 'pull_request'
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Download storybook build
uses: actions/download-artifact@v4
with:
name: storybook-static
path: storybook-static
- name: Package storybook
run: tar -czf /tmp/storybook-twenty-front.tar.gz -C storybook-static .
- name: Upload storybook tarball
uses: actions/upload-artifact@v4
with:
name: storybook-twenty-front-tarball
path: /tmp/storybook-twenty-front.tar.gz
retention-days: 1
- name: Dispatch to ci-privileged
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
repository: twentyhq/ci-privileged
event-type: visual-regression
client-payload: >-
{
"pr_number": "${{ github.event.pull_request.number }}",
"run_id": "${{ github.run_id }}",
"repo": "${{ github.repository }}",
"project": "twenty-front",
"branch": "${{ github.head_ref }}",
"commit": "${{ github.event.pull_request.head.sha }}"
}
front-task:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
+58 -5
View File
@@ -1,4 +1,4 @@
name: CI Docker Compose
name: CI Docker
permissions:
contents: read
@@ -19,7 +19,7 @@ jobs:
files: |
packages/twenty-docker/**
docker-compose.yml
test:
test-compose:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
@@ -30,10 +30,10 @@ jobs:
- name: Run compose
run: |
echo "Patching docker-compose.yml..."
# change image to localbuild using yq
yq eval 'del(.services.server.image)' -i docker-compose.yml
yq eval '.services.server.build.context = "../../"' -i docker-compose.yml
yq eval '.services.server.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i docker-compose.yml
yq eval '.services.server.build.target = "twenty"' -i docker-compose.yml
yq eval '.services.server.restart = "no"' -i docker-compose.yml
echo "Setting up .env file..."
@@ -89,11 +89,64 @@ jobs:
echo "Still waiting for server... (${count}/300s)"
done
working-directory: ./packages/twenty-docker/
ci-test-docker-compose-status-check:
test-app-dev:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Create frontend placeholder
run: |
mkdir -p packages/twenty-front/build
echo '<html><body>CI placeholder</body></html>' > packages/twenty-front/build/index.html
- name: Build app-dev image
run: |
docker build \
--target twenty-app-dev \
-f packages/twenty-docker/twenty/Dockerfile \
-t twenty-app-dev-ci \
.
- name: Start container
run: |
docker run -d --name twenty-app-dev \
-p 3000:3000 \
twenty-app-dev-ci
docker logs twenty-app-dev -f &
- name: Wait for server health
run: |
echo "Waiting for twenty-app-dev to become healthy..."
count=0
while true; do
status=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/healthz 2>/dev/null || echo "000")
if [ "$status" = "200" ]; then
echo "Server is healthy!"
curl -s http://localhost:3000/healthz
break
fi
container_status=$(docker inspect --format='{{.State.Status}}' twenty-app-dev 2>/dev/null || echo "unknown")
if [ "$container_status" = "exited" ]; then
echo "Container exited unexpectedly"
docker logs twenty-app-dev
exit 1
fi
count=$((count+1))
if [ $count -gt 300 ]; then
echo "Server did not become healthy within 5 minutes"
docker logs twenty-app-dev
exit 1
fi
echo "Still waiting... (${count}/300s) [HTTP ${status}]"
sleep 1
done
ci-test-docker-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, test]
needs: [changed-files-check, test-compose, test-app-dev]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
+146
View File
@@ -0,0 +1,146 @@
name: CI UI
on:
pull_request:
merge_group:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
if: github.event_name != 'merge_group'
uses: ./.github/workflows/changed-files.yaml
with:
files: |
package.json
yarn.lock
packages/twenty-ui/**
packages/twenty-shared/**
ui-task:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
strategy:
matrix:
task: [lint, typecheck, test]
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Run ${{ matrix.task }}
run: npx nx ${{ matrix.task }} twenty-ui
ui-sb-build:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build storybook
run: npx nx storybook:build twenty-ui
- name: Upload storybook build
uses: actions/upload-artifact@v4
with:
name: storybook-twenty-ui
path: packages/twenty-ui/storybook-static
retention-days: 1
ui-sb-test:
timeout-minutes: 30
runs-on: ubuntu-latest
needs: ui-sb-build
env:
STORYBOOK_URL: http://localhost:6007
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build dependencies
run: npx nx build twenty-shared
- name: Download storybook build
uses: actions/download-artifact@v4
with:
name: storybook-twenty-ui
path: packages/twenty-ui/storybook-static
- name: Install Playwright
run: |
cd packages/twenty-ui
npx playwright install
- name: Serve storybook & run tests
run: |
npx http-server packages/twenty-ui/storybook-static --port 6007 --silent &
timeout 30 bash -c 'until curl -sf http://localhost:6007 > /dev/null 2>&1; do sleep 1; done'
npx nx storybook:test twenty-ui
visual-regression-dispatch:
needs: ui-sb-build
if: github.event_name == 'pull_request'
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Download storybook build
uses: actions/download-artifact@v4
with:
name: storybook-twenty-ui
path: storybook-static
- name: Package storybook
run: tar -czf /tmp/storybook-twenty-ui.tar.gz -C storybook-static .
- name: Upload storybook tarball
uses: actions/upload-artifact@v4
with:
name: storybook-twenty-ui-tarball
path: /tmp/storybook-twenty-ui.tar.gz
retention-days: 1
- name: Dispatch to ci-privileged
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
repository: twentyhq/ci-privileged
event-type: visual-regression
client-payload: >-
{
"pr_number": "${{ github.event.pull_request.number }}",
"run_id": "${{ github.run_id }}",
"repo": "${{ github.repository }}",
"project": "twenty-ui",
"branch": "${{ github.head_ref }}",
"commit": "${{ github.event.pull_request.head.sha }}"
}
ci-ui-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs:
[
changed-files-check,
ui-task,
ui-sb-build,
ui-sb-test,
]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1
+8
View File
@@ -150,3 +150,11 @@ jobs:
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger i18n automerge
if: github.event_name != 'pull_request' && steps.check_changes.outputs.changes_detected == 'true'
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
repository: twentyhq/twenty-infra
event-type: i18n-pr-ready
+8
View File
@@ -138,3 +138,11 @@ jobs:
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger i18n automerge
if: steps.compile_translations.outputs.changes_detected == 'true'
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
repository: twentyhq/twenty-infra
event-type: i18n-pr-ready
+8
View File
@@ -102,3 +102,11 @@ jobs:
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger i18n automerge
if: steps.check_extract_changes.outputs.changes_detected == 'true' || steps.check_compile_changes.outputs.changes_detected == 'true'
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
repository: twentyhq/twenty-infra
event-type: i18n-pr-ready
@@ -24,10 +24,12 @@ jobs:
yq eval 'del(.services.server.image)' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.server.build.context = "../../"' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.server.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.server.build.target = "twenty"' -i packages/twenty-docker/docker-compose.yml
yq eval 'del(.services.worker.image)' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.worker.build.context = "../../"' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.worker.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.worker.build.target = "twenty"' -i packages/twenty-docker/docker-compose.yml
echo "Adding SIGN_IN_PREFILLED environment variable to server service..."
yq eval '.services.server.environment.SIGN_IN_PREFILLED = "${SIGN_IN_PREFILLED}"' -i packages/twenty-docker/docker-compose.yml
+1 -1
View File
@@ -18,7 +18,7 @@ DOCKER_NETWORK=twenty_network
# =============================================================================
prod-build:
@cd ../.. && docker build -f ./packages/twenty-docker/twenty/Dockerfile --platform $(PLATFORM) --tag twenty:$(TAG) . && cd -
@cd ../.. && docker build --target twenty -f ./packages/twenty-docker/twenty/Dockerfile --platform $(PLATFORM) --tag twenty:$(TAG) . && cd -
prod-run:
@docker run -d -p 3000:3000 --name twenty twenty:$(TAG)
@@ -1,130 +0,0 @@
ARG APP_VERSION
# === Stage 1: Common dependencies ===
FROM node:22-alpine AS common-deps
WORKDIR /app
COPY ./package.json ./yarn.lock ./.yarnrc.yml ./tsconfig.base.json ./nx.json /app/
COPY ./.yarn/releases /app/.yarn/releases
COPY ./.yarn/patches /app/.yarn/patches
COPY ./packages/twenty-emails/package.json /app/packages/twenty-emails/
COPY ./packages/twenty-server/package.json /app/packages/twenty-server/
COPY ./packages/twenty-server/patches /app/packages/twenty-server/patches
COPY ./packages/twenty-ui/package.json /app/packages/twenty-ui/
COPY ./packages/twenty-shared/package.json /app/packages/twenty-shared/
COPY ./packages/twenty-front/package.json /app/packages/twenty-front/
COPY ./packages/twenty-sdk/package.json /app/packages/twenty-sdk/
RUN yarn && yarn cache clean && npx nx reset
# === Stage 2: Build server from source ===
FROM common-deps AS twenty-server-build
COPY ./packages/twenty-emails /app/packages/twenty-emails
COPY ./packages/twenty-shared /app/packages/twenty-shared
COPY ./packages/twenty-ui /app/packages/twenty-ui
COPY ./packages/twenty-sdk /app/packages/twenty-sdk
COPY ./packages/twenty-server /app/packages/twenty-server
RUN npx nx run twenty-server:build
RUN yarn workspaces focus --production twenty-emails twenty-shared twenty-sdk twenty-server
# === Stage 3: Build frontend from source ===
# Requires ~10GB Docker memory. To skip, pre-build on host first:
# npx nx build twenty-front
# The COPY below will pick up packages/twenty-front/build/ if it exists.
FROM common-deps AS twenty-front-build
COPY --from=twenty-server-build /app/package.json /tmp/.build-sentinel
COPY ./packages/twenty-front /app/packages/twenty-front
COPY ./packages/twenty-ui /app/packages/twenty-ui
COPY ./packages/twenty-shared /app/packages/twenty-shared
COPY ./packages/twenty-sdk /app/packages/twenty-sdk
RUN if [ -d /app/packages/twenty-front/build ]; then \
echo "Using pre-built frontend from host"; \
else \
NODE_OPTIONS="--max-old-space-size=8192" npx nx build twenty-front; \
fi
# === Stage 4: s6-overlay ===
FROM alpine:3.20 AS s6-fetch
ARG S6_OVERLAY_VERSION=3.2.0.2
ARG TARGETARCH
RUN if [ "$TARGETARCH" = "arm64" ]; then echo "aarch64" > /tmp/s6arch; \
else echo "x86_64" > /tmp/s6arch; fi
RUN S6_ARCH=$(cat /tmp/s6arch) && \
wget -O /tmp/s6-overlay-noarch.tar.xz \
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz" && \
wget -O /tmp/s6-overlay-noarch.tar.xz.sha256 \
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz.sha256" && \
wget -O /tmp/s6-overlay-arch.tar.xz \
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz" && \
wget -O /tmp/s6-overlay-arch.tar.xz.sha256 \
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz.sha256" && \
cd /tmp && \
NOARCH_SUM=$(awk '{print $1}' s6-overlay-noarch.tar.xz.sha256) && \
ARCH_SUM=$(awk '{print $1}' s6-overlay-arch.tar.xz.sha256) && \
echo "$NOARCH_SUM s6-overlay-noarch.tar.xz" | sha256sum -c - && \
echo "$ARCH_SUM s6-overlay-arch.tar.xz" | sha256sum -c -
# === Stage 5: Final all-in-one image ===
FROM node:22-alpine
# s6-overlay
COPY --from=s6-fetch /tmp/s6-overlay-noarch.tar.xz /tmp/
COPY --from=s6-fetch /tmp/s6-overlay-arch.tar.xz /tmp/
RUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz \
&& tar -C / -Jxpf /tmp/s6-overlay-arch.tar.xz \
&& rm /tmp/s6-overlay-*.tar.xz
# Infrastructure: Postgres, Redis, and utilities
RUN apk add --no-cache \
postgresql16 postgresql16-contrib \
redis \
curl jq su-exec
# tsx for database setup scripts
RUN npm install -g tsx
# Twenty application (both server and frontend built from source)
COPY --from=twenty-server-build /app /app
COPY --from=twenty-front-build /app/packages/twenty-front/build /app/packages/twenty-server/dist/front
# s6 service definitions
COPY packages/twenty-docker/twenty-app-dev/rootfs/ /
# Data directories
RUN mkdir -p /data/postgres /data/redis /app/.local-storage \
&& chown -R postgres:postgres /data/postgres \
&& chown 1000:1000 /data/redis /app/.local-storage
ARG REACT_APP_SERVER_BASE_URL
ARG APP_VERSION=0.0.0
# Tell s6-overlay to preserve container environment variables
ENV S6_KEEP_ENV=1
# Environment defaults
ENV PG_DATABASE_URL=postgres://twenty:twenty@localhost:5432/default \
SERVER_URL=http://localhost:2020 \
REDIS_URL=redis://localhost:6379 \
STORAGE_TYPE=local \
APP_SECRET=twenty-app-dev-secret-not-for-production \
REACT_APP_SERVER_BASE_URL=$REACT_APP_SERVER_BASE_URL \
APP_VERSION=$APP_VERSION \
NODE_ENV=development \
NODE_PORT=3000 \
DISABLE_DB_MIGRATIONS=true \
DISABLE_CRON_JOBS_REGISTRATION=true \
IS_BILLING_ENABLED=false \
SIGN_IN_PREFILLED=true
EXPOSE 3000
VOLUME ["/data/postgres", "/app/.local-storage"]
LABEL org.opencontainers.image.source=https://github.com/twentyhq/twenty
LABEL org.opencontainers.image.description="All-in-one Twenty image for local development and SDK usage. Includes PostgreSQL, Redis, server, and worker."
ENTRYPOINT ["/init"]
@@ -32,7 +32,7 @@ has_schema=$(PGPASSWORD=twenty psql -h localhost -U twenty -d default -tAc \
if [ "$has_schema" = "f" ]; then
echo "Database appears to be empty, running initial setup..."
NODE_OPTIONS="--max-old-space-size=1500" tsx ./scripts/setup-db.ts
NODE_OPTIONS="--max-old-space-size=1500" node ./dist/scripts/setup-db.js
fi
# Always run migrations (idempotent — skips already-applied ones)
+159 -21
View File
@@ -1,9 +1,11 @@
# Base image for common dependencies
# ===========================================================================
# Shared build stages (used by both targets)
# ===========================================================================
FROM node:24-alpine AS common-deps
WORKDIR /app
# Copy only the necessary files for dependency resolution
COPY ./package.json ./yarn.lock ./.yarnrc.yml ./tsconfig.base.json ./nx.json /app/
COPY ./.yarn/releases /app/.yarn/releases
COPY ./.yarn/patches /app/.yarn/patches
@@ -16,25 +18,38 @@ COPY ./packages/twenty-shared/package.json /app/packages/twenty-shared/
COPY ./packages/twenty-front/package.json /app/packages/twenty-front/
COPY ./packages/twenty-sdk/package.json /app/packages/twenty-sdk/
# Install all dependencies
RUN yarn && yarn cache clean && npx nx reset
# Build the back
FROM common-deps AS twenty-server-build
# Copy sourcecode after installing dependences to accelerate subsequents builds
COPY ./packages/twenty-emails /app/packages/twenty-emails
COPY ./packages/twenty-shared /app/packages/twenty-shared
COPY ./packages/twenty-ui /app/packages/twenty-ui
COPY ./packages/twenty-sdk /app/packages/twenty-sdk
COPY ./packages/twenty-server /app/packages/twenty-server
RUN npx nx run twenty-server:lingui:extract && \
npx nx run twenty-server:lingui:compile && \
npx nx run twenty-emails:lingui:extract && \
npx nx run twenty-emails:lingui:compile
RUN npx nx run twenty-server:build
# Bundle setup-db script into a standalone JS file so the final image
# doesn't need tsx or the TypeScript source tree at runtime.
RUN npx esbuild packages/twenty-server/scripts/setup-db.ts \
--bundle --platform=node --outfile=packages/twenty-server/dist/scripts/setup-db.js \
--external:typeorm --external:dotenv --external:pg
# Clean server build output (type declarations and compiled tests are not needed at runtime;
# source maps are kept because twenty-infra extracts them from the image for Sentry uploads)
RUN find /app/packages/twenty-server/dist -name '*.d.ts' -delete \
&& rm -rf /app/packages/twenty-server/dist/packages/twenty-server/test
RUN yarn workspaces focus --production twenty-emails twenty-shared twenty-sdk twenty-server
# Build the front
FROM common-deps AS twenty-front-build
ARG REACT_APP_SERVER_BASE_URL
@@ -43,18 +58,26 @@ COPY ./packages/twenty-front /app/packages/twenty-front
COPY ./packages/twenty-ui /app/packages/twenty-ui
COPY ./packages/twenty-shared /app/packages/twenty-shared
COPY ./packages/twenty-sdk /app/packages/twenty-sdk
RUN npx nx build twenty-front
RUN npx nx run twenty-front:lingui:extract && \
npx nx run twenty-front:lingui:compile
# To skip the memory-intensive frontend build, pre-build on the host:
# npx nx build twenty-front
# The check below will use packages/twenty-front/build/ if it already exists.
RUN if [ -d /app/packages/twenty-front/build ]; then \
echo "Using pre-built frontend from host"; \
else \
NODE_OPTIONS="--max-old-space-size=8192" npx nx build twenty-front; \
fi
# Final stage: Run the application
# ===========================================================================
# Target: twenty (production)
# docker build --target twenty -f packages/twenty-docker/twenty/Dockerfile .
# ===========================================================================
FROM node:24-alpine AS twenty
# Used to run healthcheck in docker
RUN apk add --no-cache curl jq
RUN npm install -g tsx
RUN apk add --no-cache postgresql-client
RUN apk add --no-cache curl jq postgresql-client
COPY ./packages/twenty-docker/twenty/entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
@@ -66,20 +89,135 @@ ENV REACT_APP_SERVER_BASE_URL=$REACT_APP_SERVER_BASE_URL
ARG APP_VERSION
ENV APP_VERSION=$APP_VERSION
# Copy built applications from previous stages
COPY --chown=1000 --from=twenty-server-build /app /app
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-server /app/packages/twenty-server
# Workspace root config
COPY --chown=1000 --from=twenty-server-build /app/package.json /app/yarn.lock /app/.yarnrc.yml /app/
COPY --chown=1000 --from=twenty-server-build /app/tsconfig.base.json /app/nx.json /app/
COPY --chown=1000 --from=twenty-server-build /app/.yarn /app/.yarn
COPY --chown=1000 --from=twenty-server-build /app/node_modules /app/node_modules
# Server package (compiled dist + package.json only, no src/)
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-server/package.json /app/packages/twenty-server/
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-server/dist /app/packages/twenty-server/dist
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-server/patches /app/packages/twenty-server/patches
# Workspace packages (dist + package.json; node_modules symlinks resolve to these)
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-shared/package.json /app/packages/twenty-shared/
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-shared/dist /app/packages/twenty-shared/dist
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-emails/package.json /app/packages/twenty-emails/
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-emails/dist /app/packages/twenty-emails/dist
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-sdk/package.json /app/packages/twenty-sdk/
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-ui/package.json /app/packages/twenty-ui/
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-front/package.json /app/packages/twenty-front/
# Frontend static build
COPY --chown=1000 --from=twenty-front-build /app/packages/twenty-front/build /app/packages/twenty-server/dist/front
# Set metadata and labels
LABEL org.opencontainers.image.source=https://github.com/twentyhq/twenty
LABEL org.opencontainers.image.description="This image provides a consistent and reproducible environment for the backend and frontend, ensuring it deploys faster and runs the same way regardless of the deployment environment."
LABEL org.opencontainers.image.description="Production Twenty image with backend and frontend."
RUN mkdir -p /app/.local-storage /app/packages/twenty-server/.local-storage && \
chown -R 1000:1000 /app
chown 1000:1000 /app/.local-storage /app/packages/twenty-server/.local-storage
# Use non root user with uid 1000
USER 1000
CMD ["node", "dist/main"]
ENTRYPOINT ["/app/entrypoint.sh"]
# ===========================================================================
# Target: twenty-app-dev (all-in-one with Postgres + Redis)
# docker build --target twenty-app-dev -f packages/twenty-docker/twenty/Dockerfile .
# ===========================================================================
FROM alpine:3.20 AS s6-fetch
ARG S6_OVERLAY_VERSION=3.2.0.2
ARG TARGETARCH
RUN if [ "$TARGETARCH" = "arm64" ]; then echo "aarch64" > /tmp/s6arch; \
else echo "x86_64" > /tmp/s6arch; fi
RUN S6_ARCH=$(cat /tmp/s6arch) && \
wget -O /tmp/s6-overlay-noarch.tar.xz \
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz" && \
wget -O /tmp/s6-overlay-noarch.tar.xz.sha256 \
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz.sha256" && \
wget -O /tmp/s6-overlay-arch.tar.xz \
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz" && \
wget -O /tmp/s6-overlay-arch.tar.xz.sha256 \
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz.sha256" && \
cd /tmp && \
NOARCH_SUM=$(awk '{print $1}' s6-overlay-noarch.tar.xz.sha256) && \
ARCH_SUM=$(awk '{print $1}' s6-overlay-arch.tar.xz.sha256) && \
echo "$NOARCH_SUM s6-overlay-noarch.tar.xz" | sha256sum -c - && \
echo "$ARCH_SUM s6-overlay-arch.tar.xz" | sha256sum -c -
FROM node:24-alpine AS twenty-app-dev
# s6-overlay
COPY --from=s6-fetch /tmp/s6-overlay-noarch.tar.xz /tmp/
COPY --from=s6-fetch /tmp/s6-overlay-arch.tar.xz /tmp/
RUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz \
&& tar -C / -Jxpf /tmp/s6-overlay-arch.tar.xz \
&& rm /tmp/s6-overlay-*.tar.xz
RUN apk add --no-cache \
postgresql16 postgresql16-contrib \
redis \
curl jq su-exec
# Workspace root config
COPY --from=twenty-server-build /app/package.json /app/yarn.lock /app/.yarnrc.yml /app/
COPY --from=twenty-server-build /app/tsconfig.base.json /app/nx.json /app/
COPY --from=twenty-server-build /app/.yarn /app/.yarn
COPY --from=twenty-server-build /app/node_modules /app/node_modules
# Server package (compiled dist + package.json only, no src/)
COPY --from=twenty-server-build /app/packages/twenty-server/package.json /app/packages/twenty-server/
COPY --from=twenty-server-build /app/packages/twenty-server/dist /app/packages/twenty-server/dist
COPY --from=twenty-server-build /app/packages/twenty-server/patches /app/packages/twenty-server/patches
# Workspace packages (dist + package.json; node_modules symlinks resolve to these)
COPY --from=twenty-server-build /app/packages/twenty-shared/package.json /app/packages/twenty-shared/
COPY --from=twenty-server-build /app/packages/twenty-shared/dist /app/packages/twenty-shared/dist
COPY --from=twenty-server-build /app/packages/twenty-emails/package.json /app/packages/twenty-emails/
COPY --from=twenty-server-build /app/packages/twenty-emails/dist /app/packages/twenty-emails/dist
COPY --from=twenty-server-build /app/packages/twenty-sdk/package.json /app/packages/twenty-sdk/
COPY --from=twenty-server-build /app/packages/twenty-ui/package.json /app/packages/twenty-ui/
COPY --from=twenty-server-build /app/packages/twenty-front/package.json /app/packages/twenty-front/
# Frontend static build
COPY --from=twenty-front-build /app/packages/twenty-front/build /app/packages/twenty-server/dist/front
# Source maps are not needed in the dev image (no Sentry)
RUN find /app/packages/twenty-server/dist -name '*.js.map' -delete
# s6 service definitions
COPY packages/twenty-docker/twenty-app-dev/rootfs/ /
RUN mkdir -p /data/postgres /data/redis /app/.local-storage \
&& chown -R postgres:postgres /data/postgres \
&& chown 1000:1000 /data/redis /app/.local-storage
ARG REACT_APP_SERVER_BASE_URL
ARG APP_VERSION=0.0.0
ENV S6_KEEP_ENV=1
ENV PG_DATABASE_URL=postgres://twenty:twenty@localhost:5432/default \
SERVER_URL=http://localhost:2020 \
REDIS_URL=redis://localhost:6379 \
STORAGE_TYPE=local \
APP_SECRET=twenty-app-dev-secret-not-for-production \
REACT_APP_SERVER_BASE_URL=$REACT_APP_SERVER_BASE_URL \
APP_VERSION=$APP_VERSION \
NODE_ENV=development \
NODE_PORT=3000 \
DISABLE_DB_MIGRATIONS=true \
DISABLE_CRON_JOBS_REGISTRATION=true \
IS_BILLING_ENABLED=false \
SIGN_IN_PREFILLED=true
EXPOSE 3000
VOLUME ["/data/postgres", "/app/.local-storage"]
LABEL org.opencontainers.image.source=https://github.com/twentyhq/twenty
LABEL org.opencontainers.image.description="All-in-one Twenty image for local development and SDK usage. Includes PostgreSQL, Redis, server, and worker."
ENTRYPOINT ["/init"]
+1 -1
View File
@@ -13,7 +13,7 @@ setup_and_migrate_db() {
has_schema=$(psql -tAc "SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'core')" ${PG_DATABASE_URL})
if [ "$has_schema" = "f" ]; then
echo "Database appears to be empty, running migrations."
NODE_OPTIONS="--max-old-space-size=1500" tsx ./scripts/setup-db.ts
NODE_OPTIONS="--max-old-space-size=1500" node ./dist/scripts/setup-db.js
yarn database:migrate:prod
fi
@@ -6,8 +6,14 @@ const query = `query FindOnePerson($objectRecordId: UUID!) {
person(
filter: {or: [{deletedAt: {is: NULL}}, {deletedAt: {is: NOT_NULL}}], id: {eq: $objectRecordId}}
) {
company {
name
previousCompanies {
edges {
node {
company {
name
}
}
}
}
emails {
primaryEmail
@@ -43,8 +49,8 @@ const query = `query FindOnePerson($objectRecordId: UUID!) {
}`
test('Create and update record', async ({ page }) => {
await page.getByRole('link', { name: 'People' }).click();
await page.getByRole('button', { name: 'Create new record' }).click();
await page.goto('/objects/people');
await page.getByRole('button', { name: 'Create new Person' }).click();
// Generate a random email for testing
const randomEmail = `testuser_${Math.random().toString(36).substring(2, 10)}@example.com`;
@@ -107,29 +113,17 @@ test('Create and update record', async ({ page }) => {
await options.getByText('Hybrid').first().click({force: true});
recordFieldList.getByText('Work Preference').first().click({force: true});
// Fill company relation
const companyRelationWidget = page.getByTestId(/dynamic-relation-widget-.+-Company/);
await expect(companyRelationWidget).toBeVisible();
// Fill previous companies
await recordFieldList.getByText('Previous Companies').first().click({force: true});
await recordFieldList.getByText('Previous Companies').nth(1).click({force: true});
await page.getByPlaceholder('Search').fill('VMw');
await page.getByRole('listbox').first().getByText('VMware').click({force: true});
await page.keyboard.press('Escape');
await companyRelationWidget.hover();
await companyRelationWidget.locator('.tabler-icon-pencil').click();
await page.getByRole('textbox', { name: 'Search' }).fill('VMw');
await expect(page.getByRole('option', { name: 'VMware' })).toBeVisible();
const [updatePersonResponse] = await Promise.all([
page.waitForResponse(async (response) => {
if (!response.url().endsWith('/graphql')) {
return false;
}
const requestBody = response.request().postDataJSON();
return requestBody.operationName === 'UpdateOnePerson';
}),
await page.getByRole('option', { name: 'VMware' }).click({force: true})
]);
const body = await updatePersonResponse.json()
const newPersonId = body.data.updatePerson.id;
// Open full record page to get person ID
await page.getByRole('button', { name: /^Open/ }).click();
await page.waitForURL(/\/object\/person\//);
const newPersonId = page.url().match(/\/object\/person\/([a-f0-9-]+)/)?.[1];
// Check data was saved
const { authToken } = await getAccessAuthToken(page);
@@ -155,6 +149,6 @@ test('Create and update record', async ({ page }) => {
expect(findOnePersonReponseBody.data.person.linkedinLink.primaryLinkUrl).toBe('linkedin.com/johndoe');
expect(findOnePersonReponseBody.data.person.phones.primaryPhoneNumber).toBe('611223344');
expect(findOnePersonReponseBody.data.person.workPreference).toEqual(['HYBRID']);
expect(findOnePersonReponseBody.data.person.company.name).toBe('VMware');
expect(findOnePersonReponseBody.data.person.previousCompanies.edges[0].node.company.name).toBe('VMware');
});
+2
View File
@@ -38,6 +38,7 @@
},
"lingui:extract": {
"executor": "nx:run-commands",
"dependsOn": ["^build"],
"options": {
"cwd": "{projectRoot}",
"command": "lingui extract --overwrite --clean"
@@ -45,6 +46,7 @@
},
"lingui:compile": {
"executor": "nx:run-commands",
"dependsOn": ["^build"],
"options": {
"cwd": "{projectRoot}",
"command": "lingui compile --typescript"
@@ -10,13 +10,11 @@ module.exports = {
'./src/modules/views/graphql/**/*.{ts,tsx}',
'./src/modules/ai/graphql/**/*.{ts,tsx}',
'./src/modules/applications/graphql/**/*.{ts,tsx}',
'./src/modules/application-variables/graphql/**/*.{ts,tsx}',
'./src/modules/workspace/graphql/**/*.{ts,tsx}',
'./src/modules/workspace-member/graphql/**/*.{ts,tsx}',
'./src/modules/workspace-invitation/graphql/**/*.{ts,tsx}',
'./src/modules/billing/graphql/**/*.{ts,tsx}',
'./src/modules/settings/**/graphql/**/*.{ts,tsx}',
'./src/modules/logic-functions/graphql/**/*.{ts,tsx}',
+2
View File
@@ -292,6 +292,7 @@
},
"lingui:extract": {
"executor": "nx:run-commands",
"dependsOn": ["^build"],
"options": {
"cwd": "{projectRoot}",
"command": "lingui extract --overwrite --clean"
@@ -299,6 +300,7 @@
},
"lingui:compile": {
"executor": "nx:run-commands",
"dependsOn": ["^build"],
"options": {
"cwd": "{projectRoot}",
"command": "lingui compile --typescript"
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,5 @@
import { AIChatBanner } from '@/ai/components/AIChatBanner';
import { useEndSubscriptionTrialPeriod } from '@/billing/hooks/useEndSubscriptionTrialPeriod';
import { useEndSubscriptionTrialPeriod } from '@/settings/billing/hooks/useEndSubscriptionTrialPeriod';
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMap';
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
@@ -13,7 +13,7 @@ import {
agentChatUsageState,
type AgentChatLastMessageUsage,
} from '@/ai/states/agentChatUsageState';
import { SettingsBillingLabelValueItem } from '@/billing/components/internal/SettingsBillingLabelValueItem';
import { SettingsBillingLabelValueItem } from '@/settings/billing/components/internal/SettingsBillingLabelValueItem';
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { formatNumber } from '~/utils/format/formatNumber';
@@ -1,7 +1,7 @@
import { type Editor } from '@tiptap/react';
import { useEffect } from 'react';
import { focusEditorAfterMigrateState } from '@/ai/states/focusEditorAfterMigrateState';
import { shouldFocusChatEditorState } from '@/ai/states/shouldFocusChatEditorState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
type AIChatEditorFocusEffectProps = {
@@ -11,24 +11,24 @@ type AIChatEditorFocusEffectProps = {
export const AIChatEditorFocusEffect = ({
editor,
}: AIChatEditorFocusEffectProps) => {
const [focusEditorAfterMigrate, setFocusEditorAfterMigrate] = useAtomState(
focusEditorAfterMigrateState,
const [shouldFocusChatEditor, setShouldFocusChatEditor] = useAtomState(
shouldFocusChatEditorState,
);
useEffect(() => {
if (!focusEditorAfterMigrate || !editor) {
if (!shouldFocusChatEditor || !editor) {
return;
}
const rafId = requestAnimationFrame(() => {
editor.commands.focus('end');
setFocusEditorAfterMigrate(false);
setShouldFocusChatEditor(false);
});
return () => {
cancelAnimationFrame(rafId);
};
}, [focusEditorAfterMigrate, editor, setFocusEditorAfterMigrate]);
}, [shouldFocusChatEditor, editor, setShouldFocusChatEditor]);
return null;
};
@@ -8,7 +8,7 @@ import { agentChatInputState } from '@/ai/states/agentChatInputState';
import { agentChatUsageState } from '@/ai/states/agentChatUsageState';
import { currentAIChatThreadState } from '@/ai/states/currentAIChatThreadState';
import { currentAIChatThreadTitleState } from '@/ai/states/currentAIChatThreadTitleState';
import { focusEditorAfterMigrateState } from '@/ai/states/focusEditorAfterMigrateState';
import { shouldFocusChatEditorState } from '@/ai/states/shouldFocusChatEditorState';
import { hasTriggeredCreateForDraftState } from '@/ai/states/hasTriggeredCreateForDraftState';
import { isCreatingChatThreadState } from '@/ai/states/isCreatingChatThreadState';
import { isCreatingForFirstSendState } from '@/ai/states/isCreatingForFirstSendState';
@@ -60,7 +60,7 @@ export const useCreateAgentChatThread = () => {
[newThreadId]: newDraft,
[AGENT_CHAT_NEW_THREAD_DRAFT_KEY]: '',
}));
store.set(focusEditorAfterMigrateState.atom, true);
store.set(shouldFocusChatEditorState.atom, true);
store.set(skipMessagesSkeletonUntilLoadedState.atom, true);
store.set(threadIdCreatedFromDraftState.atom, newThreadId);
} else {
@@ -8,6 +8,7 @@ import { agentChatInputState } from '@/ai/states/agentChatInputState';
import { agentChatUsageState } from '@/ai/states/agentChatUsageState';
import { currentAIChatThreadState } from '@/ai/states/currentAIChatThreadState';
import { currentAIChatThreadTitleState } from '@/ai/states/currentAIChatThreadTitleState';
import { shouldFocusChatEditorState } from '@/ai/states/shouldFocusChatEditorState';
import { hasTriggeredCreateForDraftState } from '@/ai/states/hasTriggeredCreateForDraftState';
import { threadIdCreatedFromDraftState } from '@/ai/states/threadIdCreatedFromDraftState';
import { useOpenAskAIPageInSidePanel } from '@/side-panel/hooks/useOpenAskAIPageInSidePanel';
@@ -48,7 +49,8 @@ export const useSwitchToNewAIChat = () => {
setAgentChatInput(newChatDraft);
setCurrentAIChatThreadTitle(null);
setAgentChatUsage(null);
openAskAIPage({ resetNavigationStack: false });
openAskAIPage();
store.set(shouldFocusChatEditorState.atom, true);
};
return { switchToNewChat };
@@ -1,6 +0,0 @@
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
export const focusEditorAfterMigrateState = createAtomState<boolean>({
key: 'ai/focusEditorAfterMigrateState',
defaultValue: false,
});
@@ -0,0 +1,6 @@
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
export const shouldFocusChatEditorState = createAtomState<boolean>({
key: 'ai/shouldFocusChatEditorState',
defaultValue: false,
});
@@ -259,6 +259,18 @@ const SettingsBilling = lazy(() =>
})),
);
const SettingsUsage = lazy(() =>
import('~/pages/settings/SettingsUsage').then((module) => ({
default: module.SettingsUsage,
})),
);
const SettingsUsageUserDetail = lazy(() =>
import('~/pages/settings/SettingsUsageUserDetail').then((module) => ({
default: module.SettingsUsageUserDetail,
})),
);
const SettingsObjects = lazy(() =>
import('~/pages/settings/data-model/SettingsObjects').then((module) => ({
default: module.SettingsObjects,
@@ -529,6 +541,19 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => (
element={<SettingsLogicFunctionDetail />}
/>
<Route path={SettingsPath.Billing} element={<SettingsBilling />} />
<Route
element={
<SettingsProtectedRouteWrapper
requiredFeatureFlag={FeatureFlagKey.IS_USAGE_ANALYTICS_ENABLED}
/>
}
>
<Route path={SettingsPath.Usage} element={<SettingsUsage />} />
<Route
path={SettingsPath.UsageUserDetail}
element={<SettingsUsageUserDetail />}
/>
</Route>
<Route
path={SettingsPath.Subdomain}
element={<SettingsSubdomainPage />}
@@ -4,7 +4,7 @@ import { billingCheckoutSessionState } from '@/auth/states/billingCheckoutSessio
import { returnToPathState } from '@/auth/states/returnToPathState';
import { type BillingCheckoutSession } from '@/auth/types/billingCheckoutSession.type';
import { isValidReturnToPath } from '@/auth/utils/isValidReturnToPath';
import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/billing/constants/BillingCheckoutSessionDefaultValue';
import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/settings/billing/constants/BillingCheckoutSessionDefaultValue';
import deepEqual from 'deep-equal';
import { useStore } from 'jotai';
@@ -15,6 +15,7 @@ const StyledTitle = styled.div<Pick<TitleProps, 'noMarginTop'>>`
margin-bottom: ${themeCssVariables.spacing[4]};
margin-top: ${({ noMarginTop }) =>
!noMarginTop ? themeCssVariables.spacing[4] : '0'};
text-align: center;
`;
export const Title = ({
@@ -1,5 +1,5 @@
import { type BillingCheckoutSession } from '@/auth/types/billingCheckoutSession.type';
import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/billing/constants/BillingCheckoutSessionDefaultValue';
import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/settings/billing/constants/BillingCheckoutSessionDefaultValue';
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
export const billingCheckoutSessionState =
@@ -2,7 +2,7 @@ import { useCallback } from 'react';
import { billingCheckoutSessionState } from '@/auth/states/billingCheckoutSessionState';
import { returnToPathState } from '@/auth/states/returnToPathState';
import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/billing/constants/BillingCheckoutSessionDefaultValue';
import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/settings/billing/constants/BillingCheckoutSessionDefaultValue';
import { isNonEmptyString } from '@sniptt/guards';
import { useStore } from 'jotai';
@@ -1,4 +1,4 @@
import { useEndSubscriptionTrialPeriod } from '@/billing/hooks/useEndSubscriptionTrialPeriod';
import { useEndSubscriptionTrialPeriod } from '@/settings/billing/hooks/useEndSubscriptionTrialPeriod';
import { InformationBanner } from '@/information-banner/components/InformationBanner';
import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMap';
import { useLingui } from '@lingui/react/macro';
@@ -1,5 +1,5 @@
import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/billing/constants/BillingCheckoutSessionDefaultValue';
import { useHandleCheckoutSession } from '@/billing/hooks/useHandleCheckoutSession';
import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/settings/billing/constants/BillingCheckoutSessionDefaultValue';
import { useHandleCheckoutSession } from '@/settings/billing/hooks/useHandleCheckoutSession';
import { InformationBanner } from '@/information-banner/components/InformationBanner';
import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMap';
import { t } from '@lingui/core/macro';
@@ -168,6 +168,7 @@ export const useSaveLayoutCustomization = () => {
saveFieldsWidgetGroups,
exitLayoutCustomizationMode,
enqueueErrorSnackBar,
isRecordPageLayoutEditingEnabled,
store,
t,
]);
@@ -15,19 +15,18 @@ import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useEnterLayoutCustomizationMode } from '@/layout-customization/hooks/useEnterLayoutCustomizationMode';
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/common/constants/FolderIconDefault';
import { NavigationMenuItemType, SidePanelPages } from 'twenty-shared/types';
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel';
import { useSortedNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useSortedNavigationMenuItems';
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/common/states/openNavigationMenuItemFolderIdsState';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemInEditModeState';
import { preloadNavigationMenuItemDndKit } from '@/navigation-menu-item/display/dnd/preloadNavigationMenuItemDndKit';
import {
type NavigationMenuItemClickParams,
useNavigationMenuItemSectionItems,
} from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems';
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/common/states/openNavigationMenuItemFolderIdsState';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemInEditModeState';
import { useSortedNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useSortedNavigationMenuItems';
import { WorkspaceSectionContainer } from '@/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionContainer';
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink';
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/display/utils/getNavigationMenuItemLabel';
import { preloadNavigationMenuItemDndKit } from '@/navigation-menu-item/display/dnd/preloadNavigationMenuItemDndKit';
import { WorkspaceSectionContainer } from '@/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionContainer';
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
@@ -35,6 +34,7 @@ import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { NavigationMenuItemType, SidePanelPages } from 'twenty-shared/types';
const StyledRightIconsContainer = styled.div`
align-items: center;
@@ -166,7 +166,7 @@ export const WorkspaceSection = () => {
event?.stopPropagation();
navigateSidePanel({
page: SidePanelPages.NavigationMenuAddItem,
pageTitle: t`New sidebar item`,
pageTitle: t`New menu item`,
pageIcon: IconColumnInsertRight,
resetNavigationStack: true,
});
@@ -32,7 +32,7 @@ export const WorkspaceSectionAddMenuItemButton = () => {
setSelectedNavigationMenuItemInEditMode(null);
navigateSidePanel({
page: SidePanelPages.NavigationMenuAddItem,
pageTitle: t`New sidebar item`,
pageTitle: t`New menu item`,
pageIcon: IconColumnInsertRight,
resetNavigationStack: true,
});
@@ -34,7 +34,7 @@ export const useOpenAddItemToFolderPage = () => {
});
navigateSidePanel({
page: SidePanelPages.NavigationMenuAddItem,
pageTitle: t`New sidebar item`,
pageTitle: t`New menu item`,
pageIcon: IconColumnInsertRight,
resetNavigationStack,
});
@@ -2,15 +2,15 @@ import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import { IconColumnInsertRight } from 'twenty-ui/display';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
import { type OrganizeActionsProps } from '@/navigation-menu-item/edit/side-panel/components/SidePanelEditOrganizeActions';
import { useNavigationMenuItemMoveRemove } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemMoveRemove';
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState';
import { useNavigationMenuItemSectionItems } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems';
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/common/states/addMenuItemInsertionContextState';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemInEditModeState';
import { type AddMenuItemInsertionContext } from '@/navigation-menu-item/common/types/AddMenuItemInsertionContext';
import { useNavigationMenuItemSectionItems } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems';
import { useNavigationMenuItemMoveRemove } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemMoveRemove';
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState';
import { type OrganizeActionsProps } from '@/navigation-menu-item/edit/side-panel/components/SidePanelEditOrganizeActions';
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { SidePanelPages } from 'twenty-shared/types';
@@ -119,7 +119,7 @@ export const useNavigationMenuItemEditOrganizeActions =
setAddMenuItemInsertionContext(context);
navigateSidePanel({
page: SidePanelPages.NavigationMenuAddItem,
pageTitle: t`New sidebar item`,
pageTitle: t`New menu item`,
pageIcon: IconColumnInsertRight,
resetNavigationStack: true,
});
@@ -15,7 +15,7 @@ const StyledTr = styled.div<{
div.table-cell,
div.table-cell-0-0 {
&:not(:first-of-type) {
background-color: ${themeCssVariables.background.tertiary};
background-color: ${themeCssVariables.accent.quaternary};
border-bottom: 1px solid ${themeCssVariables.border.color.medium};
border-color: ${themeCssVariables.border.color.medium};
}

Some files were not shown because too many files have changed in this diff Show More