Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21972e8052 | ||
|
|
493830204a | ||
|
|
dd84ab25df | ||
|
|
fd044ba9a2 | ||
|
|
69fe6fcadd | ||
|
|
0ded15b363 | ||
|
|
708e53d829 | ||
|
|
e2c85b5af0 | ||
|
|
c6d4162b73 | ||
|
|
93de331428 | ||
|
|
24055527c8 | ||
|
|
c58bf9cf06 | ||
|
|
07a4cf2d26 | ||
|
|
1cfdd2e8ed | ||
|
|
6f4f7a1198 | ||
|
|
0626f3e469 | ||
|
|
630f3a0fd7 | ||
|
|
b813d64324 | ||
|
|
c9ca4dfa14 | ||
|
|
2dfa742543 | ||
|
|
e618cc6cf9 | ||
|
|
bb9e3c44a1 | ||
|
|
77d4bd9158 | ||
|
|
49af539032 |
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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');
|
||||
|
||||
});
|
||||
|
||||
@@ -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}',
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
+6
-6
@@ -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 =
|
||||
|
||||
+1
-1
@@ -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
-1
@@ -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';
|
||||
|
||||
+2
-2
@@ -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';
|
||||
|
||||
+1
@@ -168,6 +168,7 @@ export const useSaveLayoutCustomization = () => {
|
||||
saveFieldsWidgetGroups,
|
||||
exitLayoutCustomizationMode,
|
||||
enqueueErrorSnackBar,
|
||||
isRecordPageLayoutEditingEnabled,
|
||||
store,
|
||||
t,
|
||||
]);
|
||||
|
||||
+8
-8
@@ -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,
|
||||
});
|
||||
|
||||
+1
-1
@@ -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,
|
||||
});
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ export const useOpenAddItemToFolderPage = () => {
|
||||
});
|
||||
navigateSidePanel({
|
||||
page: SidePanelPages.NavigationMenuAddItem,
|
||||
pageTitle: t`New sidebar item`,
|
||||
pageTitle: t`New menu item`,
|
||||
pageIcon: IconColumnInsertRight,
|
||||
resetNavigationStack,
|
||||
});
|
||||
|
||||
+7
-7
@@ -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,
|
||||
});
|
||||
|
||||
+1
-1
@@ -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
Reference in New Issue
Block a user