Compare commits

..
147 Commits
Author SHA1 Message Date
Abdul RahmanandGitHub 3dc759bd91 fix: resolve react warning when updating state during render in SettingsPublicDomainsListCard (#16700)
Closes #15154
2025-12-19 13:47:58 +01:00
Paul RastoinandGitHub 6f2ff06a35 Refactor workspace creation (#16689)
# Introduction
Created an env variable to will inject a feature flag during any new
workspace init to be created through v2
2025-12-19 12:47:31 +01:00
32bb69c52f changing location of lint rule (#16703)
rule moved after discussion with @charlesBochet (previous PR was already
merged)

---------

Co-authored-by: Abdullah <125115953+mabdullahabaid@users.noreply.github.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
Co-authored-by: Marie <51697796+ijreilly@users.noreply.github.com>
2025-12-19 10:28:39 +00:00
de907f8c81 fix: mdast-util-to-hast has unsanitized class attribute (#16699)
Resolves [Dependabot Alert
333](https://github.com/twentyhq/twenty/security/dependabot/333).

Used yarn up in recursive mode to bump up version from 13.2.0 to 13.2.1.

---------

Co-authored-by: guillim <guigloo@msn.com>
2025-12-19 10:12:22 +00:00
MarieandGitHub afaf47cdf5 Read some endpoints on replica db (behind feature flag) (#16677)
Next steps: 
- move some workers' activities to replica db
2025-12-19 10:48:18 +01:00
Lucas BordeauandGitHub 1d2aba5b22 Make view bar filter dropdown field select scrollable (#16684)
From PR https://github.com/twentyhq/twenty/pull/16640

Fixes https://github.com/twentyhq/twenty/issues/16637

QA 



https://github.com/user-attachments/assets/e6651309-0b10-4085-ba1f-e8d25bab1aa5
2025-12-19 10:25:36 +01:00
21695d743b i18n - translations (#16692)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-18 19:22:12 +01:00
Paul RastoinandGitHub 8acfa60412 Fix standard agent and roles deletion command (#16686)
# Introduction
Caught red handed, introduced a failing command in
https://github.com/twentyhq/twenty/pull/16499 that was failing even in
system build which is should not
2025-12-18 19:14:24 +01:00
efc1aa7c83 i18n - translations (#16690)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-18 18:36:53 +01:00
Baptiste DevessierandGitHub b5ebc44fee Add field widget (#16609)
- Ability to display the details of a field
- The field can be edited (relations edition will be supported later)
- For now, the widget configuration stores the name of the field instead
of its fieldMetadataId. A hook resolves the fieldMetadataId from the
list of fields and the provided name. This will be replaced once we
migrate the configuration to the backend.

## Demo


https://github.com/user-attachments/assets/ab7efbda-66b2-46c1-b641-c350977c31dd

## Remaining to do

- Edition of relations
2025-12-18 17:25:51 +00:00
Thomas TrompetteandGitHub 79a03b8041 Fix ts error resolve rich text (#16688)
As title
2025-12-18 18:00:42 +01:00
4fd79e0c38 i18n - translations (#16687)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-18 17:53:23 +01:00
Thomas TrompetteGitHubcubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
b2d2babbb9 Add pattern for variable tag in tiptap (#16652)
Since we now store rich text value in blocknote rather than markdown,
variables need to be resolved accordingly.

Replacing the variable tag pattern
`{"type":"variableTag","attrs":\{"variable":"(\{\{[^{}]+\}\})"\}\}` by a
blocknote text `{"type":"text","text":"${escapedText}"}`

Fixes https://github.com/twentyhq/twenty/issues/16583

To test :
- build a workflow that creates a note/ sends an email with a variable
in the body
- make sure the result is properly formatted once run

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2025-12-18 17:24:13 +01:00
636cec0f59 i18n - docs translations (#16685)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-18 17:22:17 +01:00
Raphaël BosiandGitHub b1320830b5 [DASHBOARDS] Fix settings color palette (#16681)
Before, the settings color palette was hardcoded according to the Figma
design, now we generate it dynamically with the same util used by the
chart so it always corresponds to the same color. Even if we update the
graph color registry, it will be reflected inside the settings.

<img width="1512" height="741" alt="image"
src="https://github.com/user-attachments/assets/fac2d433-62b3-4b00-a362-cebbbe9f8aca"
/>
2025-12-18 17:11:13 +01:00
Paul RastoinandGitHub 38785cd4e9 Refactor seed to use twenty-standard application (#16598)
# Introduction
In this pull-request we introduce a service dedicated to the
twenty-standard app installation, we will later be able to re-use
existing logic to be more generic and allow any app installation.
For the moment sticking to this usage
https://github.com/twentyhq/core-team-issues/issues/1995

## Encountered issues
- We decided not to migrate deprecated fields ( also they will become
custom field for any existing workspace having them in the future )
- duplicate criteria
- wrong search index declaration
- forgotten isSearchable
- Attachement seed
- Restored standardId

## Note
For the moment we're still searching through standardId for code that
run on both existing and new workspaces.
For code running on new workspace exclusively we're searching using
universalIdentifier
We will standardize universalIdentifier usage later when we've migratred
all the existing workspaces

## Workspace creation
Will handle workspace creation the same way in another PR

Related https://github.com/twentyhq/twenty/pull/15065

## TODO
- [ ] Double all frontend hardcoded queries to not refer to deprecated
fields especially attachments
2025-12-18 17:08:55 +01:00
d7fc9387a0 i18n - translations (#16682)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-18 16:36:39 +01:00
EtienneandGitHub 65f0a5bb18 Billing - fix duplicate key value violates unique constraint "IndexOnActiveSubscriptionPerWorkspace" (#16676)
[Sentry event
example](https://twenty-v7.sentry.io/issues/6606854024/events/4a78b9d1a9d5468e897ec0f90a643112/)


fixes https://github.com/twentyhq/twenty/issues/16573
2025-12-18 15:59:07 +01:00
5984be992d i18n - translations (#16678)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-18 15:58:15 +01:00
EtienneandGitHub e93187fded Billing - Fix subscription_schedule release (#16669)
At subscription_schedule released, a subscription_schedule.update event
is received in webhook stripe controller but not handled. [Sentry event
example

](https://twenty-v7.sentry.io/issues/6606854024/events/e737c528a55048e5981756a4fad9028f/)

Payload example ⬇️ 
```
{
  "object": {
    "id": "sub_sched_1SfeABHDlIZyMfEDBhgJUCN2",
    "object": "subscription_schedule",
     ....
    "phases": [
      {
        "add_invoice_items": [],
        "application_fee_percent": null,
        "automatic_tax": {
          "disabled_reason": null,
          "enabled": false,
          "liability": null
        },
        "billing_cycle_anchor": null,
        "billing_thresholds": {
          "amount_gte": 10000,
          "reset_billing_cycle_anchor": false
        },
        "collection_method": "charge_automatically",
        "coupon": null,
        "currency": "usd",
        "default_payment_method": null,
        "default_tax_rates": [],
        "description": null,
        "discounts": [],
        "end_date": 1768731003,
        "invoice_settings": {
          "account_tax_ids": null,
          "days_until_due": null,
          "issuer": {
            "type": "self"
          }
        },
        "items": [
          {
            "billing_thresholds": null,
            "discounts": [],
            "metadata": {},
            "plan": "price_1SChAfHDlIZyMfEDCP3pGHHv",
            "price": "price_1SChAfHDlIZyMfEDCP3pGHHv",
            "quantity": 1,
            "tax_rates": []
          },
          {
            "billing_thresholds": null,
            "discounts": [],
            "metadata": {},
            "plan": "price_1SChAaHDlIZyMfEDkGCDIIjm",
            "price": "price_1SChAaHDlIZyMfEDkGCDIIjm",
            "tax_rates": []
          }
        ],
        "metadata": {},
        "on_behalf_of": null,
        "proration_behavior": "none",
        "start_date": 1766053422,
        "transfer_data": null,
        "trial_end": null
      }
    ],
    "released_at": 1768731003,
    "released_subscription": "sub_1Sfe0oHDlIZyMfEDs1lYui6v", // here !
    "subscription": null,
    "test_clock": "clock_1SfeT1HDlIZyMfEDNnlhPmOM"
  },
  "previous_attributes": {
    "released_subscription": null,
    "subscription": "sub_1Sfe0oHDlIZyMfEDs1lYui6v"
  }
}
```

related to https://github.com/twentyhq/twenty/issues/16573
2025-12-18 15:57:56 +01:00
Raphaël BosiandGitHub 01ffca0cef [DASHBOARDS] Add the ability to click to create a widget (#16673)
## Video QA


https://github.com/user-attachments/assets/813a2040-6ee9-418c-b7da-1126c9720446
2025-12-18 14:37:22 +00:00
4bdd866a20 Fix/close filter by enter (#16643)
Fixes #16636 

Added useCloseDropdown() hook and set onEnter prop to
onEnter={closeDropDown()} using dropdownID

EDIT from @charlesBochet after refactoring:
- ObjectDropdownFilters are used in 3 places: Main Filter menu,
EditableChip, AdvancedFilters
- deprecate vectorSearch in view filter area, we are not using them, we
are doing a anyField filter now. While refactoring the points below, I
did not want to maintain vectorSearch as it was not used anymore
- stop confusing the dropdownId (which is an id to interact with a
specific dropdown) and componentInstanceIds (which is used to scope
component states) for EditableFilter case
- I haven't fixed the confusion for MainFilter case
- It was already handled for AdvancedFilter case

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
2025-12-18 15:25:56 +01:00
martmullandGitHub 52cf3775b3 Fix subscription cross tenant issue (#16670)
As title
2025-12-18 15:22:17 +01:00
c36bb8f3a1 i18n - docs translations (#16667)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-18 13:23:08 +01:00
df456cfa2f i18n - translations (#16666)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-18 13:01:49 +01:00
Lucas BordeauandGitHub ce7f0c03f7 Fixed command menu and main container layout (#16665)
This PR fixes https://github.com/twentyhq/twenty/issues/16645

It solves two problems : 
- Command menu for mobile was outside of its proper place, it should
have been portaled instead of lifted that high in the hierarchy, which
is done here, thus avoiding context issues.
- CSS was odd due to a code path removing main container styling for
mobile display, everything has been cleaned with explicit and durable
naming. I used "main container layout" instead of "page layout" to
disambiguate from page layout feature.

## Before 

<img width="567" height="907" alt="image"
src="https://github.com/user-attachments/assets/73a335f6-d5b6-4e8a-a33f-73aa624c7ca5"
/>


## After 

<img width="566" height="907" alt="image"
src="https://github.com/user-attachments/assets/0433b7d8-c8de-4b2b-b3fd-0d8d45b92d75"
/>
2025-12-18 12:48:31 +01:00
Raphaël BosiandGitHub 52604e1c52 Always enable drag selection when side panel is opened (#16664)
We disabled that before because the side panel was always on top of the
content. But now it makes sense to enable this.
2025-12-18 11:21:44 +00:00
Paul RastoinandGitHub 47a8a15598 Metadata modules for PageLayoutTab PageLayoutWidget (#16662)
# Introduction
Creating dedicated folders and module for both `page-layout-tab` and
`page-layout-widget`

The addition diff with deletion is due to the module being added
2025-12-18 11:15:29 +00:00
Raphaël BosiandGitHub db960cf509 Fix command menu input text color (#16661)
## Before
<img width="798" height="164" alt="CleanShot 2025-12-18 at 11 48 15@2x"
src="https://github.com/user-attachments/assets/f67c0d4b-8480-4649-8e46-f8ff6c9bacca"
/>

## After
<img width="400" height="76" alt="image"
src="https://github.com/user-attachments/assets/502a378b-ce89-4992-b5da-3fe6f4413feb"
/>
2025-12-18 10:57:50 +00:00
Félix MalfaitandGitHub daaa009fb4 fix: prevent text overflow in view picker and record table action row (#16658)
## Summary
Fixes text overflow issues in the UI that were particularly visible with
longer translations (e.g., French).

## Changes

### RecordTableActionRow
- Added `white-space: nowrap` to prevent the 'Add New' text from
wrapping to multiple lines
- Removed the fixed `width: 100px` that was causing overflow issues

### ViewPickerDropdown
- Fixed flexbox layout to properly handle long view names
- Added `flex-shrink: 0` to the icon container and adornments to prevent
them from shrinking
- Added `min-width: 0` to the view name container for proper text
truncation
- Removed `display: inline-block` and `vertical-align: middle` which
don't work in flex containers

## Before
- 'Ajouter Nouveau' was displayed on two lines
- View names could push the count to a new line
- Icons could shrink when view names were long

## After
- Text stays on one line with proper ellipsis truncation
- Layout remains stable regardless of text length
- Icons maintain their size
2025-12-18 11:38:14 +01:00
34a1c64b7e i18n - docs translations (#16660)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-18 11:21:25 +01:00
3a1e2618e2 i18n - translations (#16657)
Created by Github action

---------

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-18 11:01:06 +01:00
b4afbaefff I18n Translations (#16656)
Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-18 10:41:08 +01:00
Félix MalfaitandGitHub 12544da527 feat: move support links to workspace switcher (#16653)
## Summary
Move the Support and Documentation links from the bottom left navigation
drawer to the workspace switcher dropdown menu.

## Changes
- **Workspace Switcher**: Added Support (conditional) and Documentation
links before Log out
- **Settings Navigation**: Added Support and Documentation links in the
Other section
- **Support visibility**: Support link only appears when FrontChat is
configured (not waiting for script to load)
- **FrontChat loading**: Created `SupportChatEffect` component to ensure
FrontChat script loads at app startup for popup notifications
- **Bug fix**: Fixed settings navigation items without a path appearing
highlighted
- **Cleanup**: Removed unused `SupportDropdown`, `SupportButton`, and
`SupportButtonSkeletonLoader` components
- **Hidden**: Temporarily commented out Integrations page

## Screenshots
The Support and Documentation links now appear in:
1. Workspace switcher dropdown (top left)
2. Settings navigation (Other section)
2025-12-18 10:09:10 +01:00
Félix MalfaitandGitHub 4fe8e3d3b6 feat: add resizable navigation drawer and command menu panels (#16612)
## Summary

Adds Notion-style resizable panels for the navigation drawer (left
sidebar) and command menu (right panel).

## Behavior

- **Hover** at panel edge → resize cursor appears
- **Click** → collapse/close the panel
- **Drag** → resize the panel (5px movement threshold to distinguish
from click)

## Constraints

| Panel | Min | Max | Default | Collapse Threshold |
|-------|-----|-----|---------|-------------------|
| Navigation Drawer | 180px | 350px | 220px | 150px |
| Command Menu | 320px | 600px | 400px | 250px |

## Performance Optimizations

- **CSS variables** for smooth 60fps resize (no React re-renders during
drag)
- **Table resize observer disabled** during panel resize to prevent
expensive recalculations
- **React.memo wrapper** on page body to prevent unnecessary re-renders

## Architecture

- `useResizablePanel` hook following the same pattern as
`useResizeTableHeader`
- `ResizablePanelEdge` - resize handle positioned at panel edge
- `ResizablePanelGap` - resize handle in the gap between panels
- `cssVariableEffect` - Recoil effect to sync CSS variables with state

## Refactoring

- Split `recoil-effects.ts` into separate files in `utils/recoil/` (one
export per file)
- Persist panel widths to localStorage via existing `localStorageEffect`
2025-12-18 09:09:21 +01:00
Félix MalfaitandGitHub 6682d4eb02 fix: bill all events in BillingUsageService.billUsage() instead of only the first one (#16650)
## Summary

Fixes a bug where `BillingUsageService.billUsage()` only sent the first
event from the `billingEvents` array to Stripe, silently ignoring all
subsequent events.

## Bug Description

The `billUsage()` method accepts an array of `BillingUsageEvent[]` but
was only processing `billingEvents[0]`, causing:
- Customers to be undercharged for their usage
- Revenue loss due to unbilled events
- Incorrect usage tracking

## Fix

Changed the implementation to use `Promise.all()` to send all events in
the array concurrently to Stripe.

## Before
```typescript
await this.stripeBillingMeterEventService.sendBillingMeterEvent({
  eventName: billingEvents[0].eventName,  // Only first event
  value: billingEvents[0].value,
  stripeCustomerId: workspaceStripeCustomer.stripeCustomerId,
  dimensions: billingEvents[0].dimensions,
});
```

## After
```typescript
await Promise.all(
  billingEvents.map((event) =>
    this.stripeBillingMeterEventService.sendBillingMeterEvent({
      eventName: event.eventName,
      value: event.value,
      stripeCustomerId: workspaceStripeCustomer.stripeCustomerId,
      dimensions: event.dimensions,
    }),
  ),
);
```
2025-12-18 08:38:15 +01:00
Félix MalfaitandGitHub 4e15f7fe13 Use parameterized query in getRemoteTableLocalName (#16651)
This PR updates the `isNameAvailable` function in
`getRemoteTableLocalName` to use parameterized queries instead of string
interpolation when querying the information_schema.

**Changes:**
- Replaced template literal interpolation with PostgreSQL's `$1`, `$2`
placeholder syntax
- Parameters are now passed as a separate array argument to
`dataSource.query()`

This follows best practices for database queries.
2025-12-18 08:37:43 +01:00
Félix MalfaitandGitHub 1e615f7102 fix: ensure QueryRunner is released in workspace schema operations (#16649)
## Summary

Fixes a database connection leak in `WorkspaceDataSourceService` where
`QueryRunner.release()` was not being called when schema operations
failed.

## Problem

The `createWorkspaceDBSchema` and `deleteWorkspaceDBSchema` methods use
TypeORM's QueryRunner but didn't wrap the operations in
try-catch-finally blocks. When schema operations fail (e.g., permission
denied, schema conflicts), the `QueryRunner.release()` method was never
called.

**Impact:** Failed schema operations leak database connections, which
can exhaust the connection pool and cause the application to hang or
crash under load.

## Solution

Wrap both methods in try-finally blocks to ensure
`queryRunner.release()` is always called, regardless of whether the
operation succeeds or fails.

## Changes

- `createWorkspaceDBSchema`: Wrapped in try-finally to ensure connection
release
- `deleteWorkspaceDBSchema`: Wrapped in try-finally to ensure connection
release
2025-12-18 08:26:07 +01:00
Félix MalfaitandGitHub 75ae2b401b feat: soft disable LOCAL code interpreter driver in production (#16647)
## Summary

Instead of throwing an error at server startup when LOCAL code
interpreter is configured in production, we now return a DisabledDriver
that only throws when the code interpreter is actually used.

## Changes

- Created `DisabledDriver` class that implements `CodeInterpreterDriver`
but throws an error only when `execute()` is called
- Added `DISABLED` to the `CodeInterpreterDriverType` enum
- Updated the factory to return a `DISABLED` driver config instead of
throwing when LOCAL is used in production
- Updated the module to handle the new `DISABLED` driver type

## Motivation

Many users don't need the code interpreter feature and want to deploy to
production without configuring E2B. Previously, the server would crash
at startup if `CODE_INTERPRETER_TYPE=LOCAL` was set in production.

**Before:** Server crashes at startup in production if
`CODE_INTERPRETER_TYPE=LOCAL`

**After:** Server starts fine. The error only occurs if someone actually
tries to **use** the code interpreter feature, at which point they get a
clear error message explaining how to configure E2B.
2025-12-18 08:03:13 +01:00
e5f655fcae i18n - docs translations (#16646)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-17 23:20:56 +01:00
82b0ee5995 i18n - translations (#16644)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-17 22:46:52 +01:00
Félix MalfaitandGitHub 1088f7bbab feat: add lingui/no-unlocalized-strings ESLint rule and fix translations (#16610)
## Summary
This PR adds the `lingui/no-unlocalized-strings` ESLint rule to detect
untranslated strings and fixes translation issues across multiple
components.

## Changes

### ESLint Configuration (`eslint.config.react.mjs`)
- Added comprehensive `ignore` patterns for non-translatable strings
(CSS values, HTML attributes, technical identifiers)
- Added `ignoreNames` for props that don't need translation (className,
data-*, aria-*, etc.)
- Added `ignoreFunctions` for console methods, URL APIs, and other
non-user-facing functions
- Disabled rule for debug files, storybook, and test files

### Components Fixed (~19 files)
- Object record components (field inputs, pickers, merge dialogs)
- Settings components (accounts, admin panel)
- Serverless function components
- Record table and title cell components

## Status
🚧 **Work in Progress** - ~124 files remaining to fix

This PR is being submitted as draft to allow progressive fixing of
remaining translation issues.

## Testing
- Run `npx eslint "src/**/*.tsx"` in `packages/twenty-front` to check
remaining issues
2025-12-17 22:08:33 +01:00
Abdullah.andGitHub c13b955a36 fix: hide GraphQL stack traces and messages in production (#16593)
Resolves [Code Scanning Alert
180](https://github.com/twentyhq/twenty/security/code-scanning/180).

- Normalize unexpected GraphQL errors in convertExceptionToGraphql to a
generic "Internal Server Error" instead of exposing exception.name
directly to clients.
- Only attach stack and response (original error message) in
development, so production responses don’t leak internal class names,
implementation details, or stack traces, while observability is
preserved via `ExceptionHandlerService`/Sentry.
- Keep behavior consistent with `convertHttpExceptionToGraphql`, which
also only exposes detailed response and stack information when `NODE_ENV
=== DEVELOPMENT`.
2025-12-17 18:29:40 +01:00
Raphaël BosiandGitHub b0e256221a Create typeguards for widget configurations (#16627)
We checked for widget types by doing `configuration?.__typename ===
'LineChartConfiguration'` which made the code difficult to read.

In this PR, I introduce type guards for each widget type.

Note: the configuration type is `WidgetConfiguration |
FieldsConfiguration` for now but should be changed to
`WidgetConfiguration` when @Devessier adds FieldsConfiguration to the
backend type `WidgetConfiguration`.
2025-12-17 17:48:34 +01:00
a560e8ac83 feat: 🎸 added higher resoulution options in the dateTime Filter (#16548)
Title: "feat: Add second, minute & hour resolution options to relative
date Filter action"
---

## Summary

This PR enables support for smaller time units — **Seconds, Minutes, and
Hours** — in the *Relative Date* filter used in workflows, rather than
being limited to days only.

---

## What Changed

This PR extends the relative date filter to include support for the
following units:

✔️ `SECOND`  
✔️ `MINUTE`  
✔️ `HOUR`  
✔️ (Existing: `DAY`, `WEEK`, `MONTH`, etc.)

Changes include:

- Adding `SECOND`, `MINUTE`, and `HOUR` options to the internal relative
date unit enum/constant.
- Updating utility functions and parsers to correctly interpret and
evaluate these new units.
- Enhancing existing tests and adding new tests to cover second, minute,
and hour relative filters.

---

## Testing

New and updated tests include:

- Unit tests for serialization of relative filter values including
seconds, minutes, and hours.
- Workflow filter evaluation tests that verify minute/hour resolution
behaves correctly.

Tests are included in the changeset.

---

## Backward Compatibility

This change is fully backward compatible:

- All existing relative date filters using days or larger units behave
exactly as before.
- Adding finer units does not alter existing stored data or workflow
definitions.

---

##  Issue Reference

Fixes: **twentyhq/twenty#15525**  

<img width="1909" height="896" alt="image"
src="https://github.com/user-attachments/assets/328d03dc-ca0b-4c3f-84e5-58961c178398"
/>

---------

Co-authored-by: Guillim <guillim@users.noreply.github.com>
Co-authored-by: guillim <guigloo@msn.com>
2025-12-17 16:46:11 +00:00
Raphaël BosiandGitHub 100d4f7f42 [DASHBOARDS] Hide tab menu items instead of disabling them (#16629)
## Before

<img width="818" height="1052" alt="CleanShot_2025-12-15_at_14 29 302x"
src="https://github.com/user-attachments/assets/5eceefce-559d-4710-8fa7-118ca32a4dc7"
/>

## After


https://github.com/user-attachments/assets/b6559600-d7cd-47a8-9ef7-e9c638637cb0
2025-12-17 17:42:58 +01:00
Abdullah.andGitHub 1f1a1ea138 fix(workflows): align variable regex with validation and prevent ReDoS (#16607)
**What this fixes:**
- Addresses a CodeQL security finding: the regex used to find variables
in workflow strings could be slow on malicious inputs (ReDoS).
- Two alerts: [Code Scanning
181](https://github.com/twentyhq/twenty/security/code-scanning/181) and
[Code Scanning
182](https://github.com/twentyhq/twenty/security/code-scanning/182)

**Context:**
- Our workflow system lets users insert variables like `{{user.name}}`
or `{{trigger.properties.after.name}}` into strings and JSON (HTTP
request bodies, record field values, etc.).
- The `variable-resolver.ts` module scans these strings and replaces
variables with actual values.
- Our validation (`isValidVariable`) already enforces that variables
contain no `{` or `}` inside them (only simple property paths like
`user.name`).

**The change:**
- Updated the regex from `/\{\{(.*?)\}\}/g` to `/\{\{([^{}]+)\}\}/g` to
match our validation pattern.
- This removes the ReDoS risk and aligns the resolver with the
validation contract.

**Why this is safe:**
- All supported workflow usage (simple variable paths) continues to
work.
- Both `match` and `replace` behave the same for valid variables.
- Only unsupported patterns with nested braces (e.g., `{{foo {bar}}}`)
would stop matching, which isn't part of our supported syntax anyway.
2025-12-17 17:13:55 +01:00
59d3a14922 Common API - Add tests on position field validation at creation (#16630)
Co-authored-by: guillim <guigloo@msn.com>
2025-12-17 16:02:37 +00:00
Paul RastoinandGitHub 0e6a8c04c4 Async validators and additional cache maps in v2 Builder (#16618)
# Introduction
@bosiraphael has to introduce async validators and feature flag
contextual validator
In this way in this PR we make all entity validators asyncable
Also added an `additionalCacheDataMaps` to the low level args validators
2025-12-17 15:43:25 +01:00
Raphaël BosiandGitHub ca976afa10 Fix drag and drop in dropdown (#16622)
## Bug description

Due to the dropdown using floating ui, there was an offset on the
draggable item when the drag and drop was implemented inside a
scrollable dropdown.

## Video QA

### Before


https://github.com/user-attachments/assets/fe4b4c36-39ae-4d26-9e19-85d5ffd23b30


### After


https://github.com/user-attachments/assets/1f00b9b0-c231-49cb-a232-75ca42a98e8c
2025-12-17 13:41:37 +00:00
martmullandGitHub 967298fe09 Remove position input from zapier (#16616)
As title
Fixes zapier tests
2025-12-17 14:39:51 +01:00
Charles BochetandGitHub a60f750ed7 Follow up on FieldInput fix (#16611)
Follow up on https://github.com/twentyhq/twenty/pull/16603
2025-12-17 14:16:14 +01:00
EtienneandGitHub 2e62bb133b Second Action Button - fix (#16613)
Merged https://github.com/twentyhq/twenty/pull/16582 without testing
case when fieldDefinition.metadata.settings?.clickAction is not set
(default value)

To test : 
When 
<img width="548" height="537" alt="Screenshot 2025-12-17 at 09 37 26"
src="https://github.com/user-attachments/assets/96a85093-24bb-4436-9000-340cd020e343"
/>
is set (or default)
In table view you can copy cell content
<img width="252" height="77" alt="Screenshot 2025-12-17 at 09 36 58"
src="https://github.com/user-attachments/assets/44b938f3-74b8-4d58-8b1a-344b4f39bfbd"
/>
2025-12-17 10:57:30 +00:00
6de424bc32 handle localization and property parameters in CalDAV iCal parsing (#16519)
Co-authored-by: guillim <guigloo@msn.com>
2025-12-17 11:47:16 +01:00
2717afce59 i18n - docs translations (#16620)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-17 11:21:57 +01:00
Lucky GoyalandGitHub 659ed6b080 feat: add full path label tooltip for workflow filter field (#16580)
# Closes Issue: Can't distinguish between fields with identical names
(#16285)

There was a UX bug in the workflow filter interface where **two
variables coming from different steps but sharing the same field name**
were displayed identically. This made it difficult for users to tell
them apart when used in a filter group, leading to confusion when
building workflows.

---

# Fix: Add Full Path Label Tooltip for Workflow Filter Field

- Adds a **tooltip/label showing the complete path** so users can
distinguish fields from different workflow steps even if they share the
same name.

## UX Improvements
<img width="495" height="288" alt="image"
src="https://github.com/user-attachments/assets/fa26f381-835b-4d14-bf73-f04b59c8d0b5"
/>
2025-12-17 11:13:10 +01:00
30c2c22fc2 i18n - translations (#16614)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-17 09:52:49 +01:00
d43e21b695 Feature 15797 add secondary action button (#16582)
This is a fix for #15797 

This pull request is to replace PR #16307 and to extend #16265 

Just to repeat, this PR does the following -> 
**Table Cell Button and Edit Button Improvements**
- Enhanced RecordTableCellButton to support a secondary action and icon,
enabling both the primary and secondary actions based on the selected
action mode.
- Updated RecordTableCellEditButton to determine the action mode for
actionable fields, and provide both copy and navigate actions as
primary/secondary buttons, with appropriate feedback.

## When primary function is to copy
<img width="1817" height="939" alt="image"
src="https://github.com/user-attachments/assets/7ec6c6aa-80d8-402b-a210-519163d39ef6"
/>


## When primary function is to open link
<img width="1784" height="942" alt="image"
src="https://github.com/user-attachments/assets/dfe0fcf1-ba72-4083-a5f9-7165a03db3df"
/>

Hey @etiennejouan, please have a look!
Thank you

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2025-12-17 09:13:00 +01:00
MarieandGitHub 0145920d7e e2e tests (#16533)
In this PR, 
- current basic E2E tests are fixed, and some were added, covering some
basic scenarios
- some tests avec been commented out, until we decide whether they are
worth fixing

The next steps are
- evaluate the flakiness of the tests. Once they've proved not to be
flaky, we should add more tests + re-write the current ones not using
aria-label (cf @lucasbordeau indication).
- We will add them back to the development flow
2025-12-17 08:48:17 +01:00
martmullandGitHub 1cbbd04761 Function trigger updates 2 (#16608)
- Improves route trigger job performances
- expose function params types

## Before

<img width="938" height="271" alt="image"
src="https://github.com/user-attachments/assets/5752ba64-f31d-44ed-974d-536e63458f2c"
/>


## After

<img width="1000" height="559" alt="image"
src="https://github.com/user-attachments/assets/b1f4927a-5f43-49f0-a606-244c72356772"
/>
2025-12-16 18:20:26 +01:00
Charles BochetandGitHub 58c85e7cca Fix bug on Links, Array, Emails, Phones Field inputs (#16603)
The following PR introduced several bugs:
https://github.com/twentyhq/twenty/pull/16042



https://github.com/user-attachments/assets/2c0fd211-3579-4a22-9a5f-dcec9cbe6a2e
2025-12-16 18:18:15 +01:00
Raphaël BosiandGitHub 576019e465 [DASHBOARDS] Rotate ticks on bar and line charts (#16528)
## Description

3 steps depending on the widget width. From bigger to tighter space:
- Fully shown horizontal text
- Rotated text
- Rotated text with skipped ticks to avoid overlapping

Also created common files for all constants for bar and pie charts.
Since a lot of them are shared, they can be inherited from a common
file.

## Video QA


https://github.com/user-attachments/assets/fd58d412-1a8b-4bd6-a420-4c03767e98d5
2025-12-16 16:39:42 +00:00
Raphaël BosiandGitHub 5a5bf112cf Update dashboard Icons (#16605)
New Icons:

<img width="790" height="84" alt="CleanShot 2025-12-16 at 15 53 45@2x"
src="https://github.com/user-attachments/assets/6700efd4-2ae9-4521-8fe9-ed43b3cef438"
/>

<img width="794" height="72" alt="CleanShot 2025-12-16 at 15 54 19@2x"
src="https://github.com/user-attachments/assets/ac4ecc88-4012-463f-875a-0bbb29d09ce3"
/>

<img width="184" height="76" alt="CleanShot 2025-12-16 at 15 53 56@2x"
src="https://github.com/user-attachments/assets/8e8353b0-90d5-4543-9d16-88b3eafdc1ae"
/>
2025-12-16 16:57:22 +01:00
martmullandGitHub 9b00084fb0 Vendor twenty-shared into twenty-sdk (#16592)
twenty-shared is not bundled properly when deploying twenty-sdk to npm.
This aims to fix that issue
2025-12-16 16:48:38 +01:00
38a0bba57b i18n - docs translations (#16600)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-16 15:21:59 +01:00
a6127c2128 i18n - translations (#16596)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-16 15:04:08 +01:00
Félix MalfaitandGitHub 04c596817a feat(server): enforce userFriendlyMessage on all exceptions (#16589)
## Summary
This PR enforces that all custom exceptions must provide a
`userFriendlyMessage`, ensuring end users always see readable error
messages.

## Changes

### Core Changes
- **`CustomException` simplified**: Removed the `ForceFriendlyMessage`
generic parameter - `userFriendlyMessage` is now always required
- **Type safety**: The constructor now requires `{ userFriendlyMessage:
MessageDescriptor }` (no longer optional)

### Updated Files
- **74+ exception classes** updated to provide default user-friendly
messages using Lingui `msg` macro
- Each exception class has a sensible fallback message (e.g., `msg\`An
authentication error occurred.\``)
- Exception classes that had code-specific message maps retain their
behavior

## Benefits
- **Compile-time enforcement**: Forgetting to add a user-friendly
message now causes a TypeScript error
- **Better UX**: End users always see a localized, human-readable error
message
- **Simpler API**: No more boolean generic parameter to think about

## Testing
- `npx nx run twenty-server:typecheck` passes
- `npx nx run twenty-server:lint` passes

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Enforces `userFriendlyMessage` on `CustomException` and updates all
exception classes to supply localized default messages, with
filters/tests adjusted accordingly.
> 
> - **Core**:
> - Enforce required `userFriendlyMessage` in `CustomException` (remove
optional generic; constructor now requires `{ userFriendlyMessage:
MessageDescriptor }`).
> - **Exceptions**:
> - Update ~70+ exception classes to set default localized messages via
Lingui `msg` maps and pass them in constructors (e.g., `AuthException`,
`ObjectMetadataException`, `FieldMetadataException`, etc.).
> - Add fallback messages where needed (e.g., `INTERNAL_SERVER_ERROR` or
domain-specific defaults).
> - **HTTP/GraphQL Filters**:
> - Ensure fallbacks create `UnknownException` with `msg` for
user-friendly text in REST/GraphQL exception filters.
> - **Tests**:
>   - Adjust unit tests to pass `userFriendlyMessage` to exceptions.
> - Update Jest snapshots to include `extensions.userFriendlyMessage` or
message objects where applicable.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
221004fdfc. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
2025-12-16 14:44:27 +01:00
martmullandGitHub 3ac72683da Update migration command (#16578)
Avoid pg_ table request
2025-12-16 13:35:03 +00:00
Abdullah.andGitHub d998e3b92c Display a CTA to view the existing duplicate when adding a duplicate email/domain. (#16483)
Closes [89](https://github.com/twentyhq/core-team-issues/issues/89)

## Problem

When users attempt to create or update a record with a duplicate value
for a unique field (e.g., duplicate email or domain name), they receive
a generic error message: "This record already exists. Please check your
data and try again." This provides no actionable way to locate and view
the existing conflicting record, forcing users to manually search for
it.

## Solution

This PR enhances duplicate key constraint error handling to
automatically detect the conflicting record and display a "View existing
record" link in the error notification. When clicked, users are
navigated directly to the existing record's detail page.

## Backend Changes

### 1. PostgreSQL Error Parsing
(`parse-postgres-constraint-error.util.ts`)
- Extracts structured information from PostgreSQL `QueryFailedError`
messages.

### 2. Conflicting Record Lookup (`find-conflicting-record.util.ts`)
- Queries the database to find the existing record with the conflicting
value

### 3. Error Handling Orchestration
(`handle-duplicate-key-error.util.ts`)
- Parses PostgreSQL error to extract column name and conflicting value
- Attempts to find the conflicting record
- Enriches `TwentyORMException` with `conflictingRecordId` and
`conflictingObjectNameSingular` if found

### 4. Exception Computation Updates (`compute-twenty-orm-exception.ts`)
- Made function `async` and added optional `entityManager` and
`internalContext` parameters
- Needed to support async database queries for conflicting record lookup

### 5. GraphQL Error Handler
(`twenty-orm-graphql-api-exception-handler.util.ts`)
- **Changes**: Enhanced `DUPLICATE_ENTRY_DETECTED` case to include
`conflictingRecordId` and `conflictingObjectNameSingular` in GraphQL
error extensions

## Frontend Changes

### 1. Error Extraction Utility
(`get-conflicting-record-from-apollo-error.util.ts`)
- Accesses GraphQL error extensions
- Validates that both `conflictingRecordId` and
`conflictingObjectNameSingular` exist and are strings
- Returns `null` if validation fails

### 2. SnackBar Enhancement (`useSnackBar.ts`)
- Extracts conflicting record info from Apollo error
- Constructs URL using `getAppPath` utility
- Adds link object to snackbar options with text "View existing record"

<img width="931" height="858" alt="image"
src="https://github.com/user-attachments/assets/28137dc7-18ab-4ffe-b669-1f2d4ec264d1"
/>
2025-12-16 14:27:34 +01:00
Paul RastoinandGitHub 75bba5a8ee Standard Agent, Role, Role target (#16499)
# Introduction
In this pullrequest have been migrated to the flat standard entities:
Role, Agent and RoleTargets.

## What happens
- Removed createStandardMorph tool util in favor of dynamic typing of
`createMorphOrRelationStandardField`
- Implemented a command to remove standard agents and their role that
has been removed in https://github.com/twentyhq/twenty/pull/16513 also
added a default role target to data manipulator role to the only
remaining agent
- Implemented an agent deleteMany service handler
2025-12-16 14:17:31 +01:00
Abdullah.andGitHub 553efe0cee fix: incomplete URL substring sanitization in linkedin-browser-extension. (#16586)
Tighten LinkedIn browser extension popup URL handling by parsing the
active tab URL and matching on protocol, hostname, and pathname instead
of substring checks, preventing logic from running on non-LinkedIn
hosts.

Resolves [Code Scanning Alert
209](https://github.com/twentyhq/twenty/security/code-scanning/209) and
[Code Scanning Alert
210](https://github.com/twentyhq/twenty/security/code-scanning/210).
2025-12-16 13:22:35 +01:00
martmullandGitHub 48a7a24380 Update hello-world twenty-sdk version (#16579)
yarn lock update after cli package release
2025-12-15 19:42:30 +01:00
e8fd5fa3ed Added relative date filter to dashboards (#16292)
This PR adds relative date filter operand to dashboard filters : 

<img width="1685" height="558" alt="image"
src="https://github.com/user-attachments/assets/a1f927e7-8c99-4171-b487-4b6a28779547"
/>

It also refactors the logic to add timezone and first day of the week
taking users preferences.

It has been tested on workflows and regular advanced filters also.


For step filters, which use a JSON format to store relative date
filters, I kept the current way to handle it.

There are a few workspaces that use a relative date filter in step
filter, so we want to avoid a migration, and instead handle both code
paths, and refactor everything to JSON later.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2025-12-15 18:51:57 +01:00
Abdul RahmanandGitHub 31467f2173 Fix action menu modals rendering inside dropdown containers (#16478)
Closes #16363

- Updated modals to open in the full screen by default


<img width="1356" height="940" alt="Screenshot 2025-12-11 at 12 15
15 AM"
src="https://github.com/user-attachments/assets/a27bf2f2-8778-457a-a4ea-e3f3f30de302"
/>
2025-12-15 18:36:24 +01:00
nitinandGitHub 9c3cd959ba [Dashboards] add legend hover highlight for graph widgets (#16551)
closes https://github.com/twentyhq/core-team-issues/issues/1959

line - 


https://github.com/user-attachments/assets/3fcefd47-c065-488c-a9e1-e820e8a03349

bar - 


https://github.com/user-attachments/assets/a138e376-9304-46b9-b6ba-c642f7b85ae2

pie - 



https://github.com/user-attachments/assets/cc008d0e-ec7f-47a3-ac14-5fef56426f6a

onClick - 



https://github.com/user-attachments/assets/94c1791d-b820-449c-81f9-c3d10361b694
2025-12-15 17:28:51 +00:00
Abdul RahmanandGitHub 289e8bf1d4 fix: ensure unique GraphQL schema caching per API key (#16411)
## Description

This PR fixes an issue where the GraphQL schema was being incorrectly
cached and shared across different API keys within the same workspace.
This resulted in the `createdBy` field (Actor) from the first API key's
request being erroneously attributed to subsequent requests made by
different API keys.

## Changes

- Updated the `@graphql-yoga/nestjs` patch to include the request's
`Authorization` header in the schema cache key generation logic.
- This ensures that every unique authentication token (and thus every
unique API key) generates a distinct cache entry, preventing schema
context collisions.

Closes #15093
2025-12-15 18:23:33 +01:00
EtienneandGitHub 0ecb60f50b E2B var env fix (#16576) 2025-12-15 18:20:22 +01:00
nitinandGitHub cd0318ea65 revert bundle size increase (#16575) 2025-12-15 18:13:05 +01:00
MarieandGitHub a0b963ef86 Remove viewGroup.fieldMetadataId (#16571)
Final step of
https://github.com/orgs/twentyhq/projects/1/views/8?pane=issue&itemId=142348748&issue=twentyhq%7Ccore-team-issues%7C1965

Removing viewGroup.fieldMetadataId.
It's already not used in FE anymore
2025-12-15 17:58:45 +01:00
neo773andGitHub 66d242da24 prevent filtering out messages sent by user when their handle resembes group email (#16508) 2025-12-15 17:58:25 +01:00
martmullandGitHub e289f3056e 1895 extensibility v1 application tokens 3 (#16504)
- moves applicationRoleId to application entity
- add new `APPLICATION` FieldActorSource and `APPLICATION`
JwtTokenTypeEnum value
- create a new token with applicationId when executing a function
- when applicationId is in token, check for application.defaultRole
permissions
-use twenty-shared types in `twenty-sdk/application`
- create a new import from generate called "Twenty" that you can use
directly without having to set TWENTY_API_KEY AND TWENTY_API_URL (keep
metadata or core parameter only)
- provide to serverless unique one time BEARER TOKEN to run it

Result
<img width="977" height="566" alt="image"
src="https://github.com/user-attachments/assets/e78428a0-5b13-4975-aa13-58ee3b32450c"
/>

<img width="910" height="596" alt="image"
src="https://github.com/user-attachments/assets/6ec72bf5-7655-4093-a45e-ad269595a324"
/>

<img width="741" height="568" alt="image"
src="https://github.com/user-attachments/assets/7683944c-fd79-4417-8fb2-8e4815cc112f"
/>
2025-12-15 17:44:23 +01:00
e33f18bfa8 i18n - translations (#16574)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-15 17:22:24 +01:00
039a3d99a8 Fix APIKey typing (#16541)
Following this https://github.com/twentyhq/twenty/pull/16540
Those were not failing (yet) but were wrongly typed and error prone

---------

Co-authored-by: Guillim <guillim@users.noreply.github.com>
2025-12-15 16:05:28 +00:00
nitinandGitHub fc74a06f53 [Dashboards] add hide empty category option for pie charts (#16550)
closes
https://discord.com/channels/1130383047699738754/1445403391722520677


https://github.com/user-attachments/assets/980225a8-153e-4b49-89f7-5944227c8fdb


toggle label ie `Hide empty category` -- debatable
2025-12-15 15:40:46 +00:00
nitinandGitHub 75f2c26d3f lazy load rich text widget (#16569) 2025-12-15 16:30:54 +01:00
Thomas TrompetteandGitHub 95e0793f81 Compute output schema on frontend (#16530)
Fixes https://github.com/twentyhq/core-team-issues/issues/1382

Current issue : all step output schemas are computed and stored on
backend side. Which means that, when the database schema is updated -
like a field creation - steps needs to be deleted an recreated. Which is
invisible to users.

Solution : schema generation is moved on frontend side

1. Coming on the page the first time, the schema is populated for all
steps except a few ones that are handled differently (Code, Webhook,
http node, Agent)

2. A separated state allow to determine if a step needs a recomputation.

3. The user only needs a refresh to see the whole schema re-computed

Follow-up:
- check if remaining backend steps could be moved to runtime
computation. But Code will still require storage.
- Clean backend service that is not used anymore
2025-12-15 16:20:35 +01:00
Félix MalfaitandGitHub 2e104c8e76 feat(ai): add code interpreter for AI data analysis (#16559)
## Summary

- Add code interpreter tool that enables AI to execute Python code for
data analysis, CSV processing, and chart generation
- Support for both local (development) and E2B (sandboxed production)
execution drivers
- Real-time streaming of stdout/stderr and generated files
- Frontend components for displaying code execution results with
expandable sections

## Code Quality Improvements

- Extract `getMimeType` to shared utility to reduce code duplication
between drivers
- Fix security issue: escape single quotes/backslashes in E2B driver env
variable injection
- Add `buildExecutionState` helper to reduce duplicated state object
construction
- Add `DEFAULT_CODE_INTERPRETER_TIMEOUT_MS` constant for consistency
- Fix lingui linting warning and TypeScript theme errors in frontend

## Test Plan

- [ ] Test code interpreter with local driver in development
- [ ] Test code interpreter with E2B driver in production environment
- [ ] Verify streaming output displays correctly in chat UI
- [ ] Verify generated files (charts, CSVs) are uploaded and
downloadable
- [ ] Test file upload flow (CSV, Excel) triggers code interpreter

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Updates generated i18n catalogs for Polish and pseudo-English, adding
strings for code execution/output (code interpreter) and various UI
messages, with minor text adjustments.
> 
> - **Localization**:
> - **Generated catalogs**: Refresh `locales/generated/pl-PL.ts` and
`locales/generated/pseudo-en.ts`.
> - Add strings for code execution/output (e.g., code, copy code/output,
running/waiting states, download files, generated files, Python code
execution).
> - Include new UI texts (errors, prompts, menus) and minor text
corrections.
>   - No changes to `pt-BR`; other files unchanged functionally.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
befc13d02c. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
2025-12-15 16:11:24 +01:00
Charles BochetandGitHub 4281a71f40 Increase bundle size to 6.9MB (#16568)
Likely a missing lazy load in:
https://github.com/twentyhq/twenty/pull/16437

Let's unblock and fix separately
2025-12-15 15:25:22 +01:00
EtienneandGitHub 3e55e772d4 Update null command (#16567)
Remove version condition to re-run the command on upgraded workspaces on
which it fails
2025-12-15 13:55:32 +00:00
neo773andGitHub a83732d7a6 fix messaging error parsing (#16448) 2025-12-15 13:33:37 +01:00
Abhishek KumarandGitHub 042972d7b2 fix(workflow): line break not supported by Send Email Nodes (#16561)
Closes #16557

Tiptap Editor (which the Send Email Node uses) , creates a content json
with type 'hardBreak' for line breaks.

The was no rederer defined for this `hardBreak` node type, so the
`renderNode` function was ignoring that node (returning null).

**Fix :** Added a renderer for  `hardBreak` node type.
2025-12-15 13:24:31 +01:00
d7b34fc636 feat(workflow): improve Add node discovery (#16547)
## Description

This PR improves the discoverability of the 'Add node' action in the
workflow builder by making it always visible in the action menu.

### Changes:
1. **Pin the 'Add node' action** - Changed \isPinned\ from \ alse\ to \
rue\ for the \ADD_NODE\ action so it's always visible and easily
accessible when viewing a workflow
2. **Unpin 'Add to favorites' and 'Remove from favorites'** - These
actions are less frequently used in the workflow context, so they've
been unpinned to make room for the more relevant 'Add node' CTA

### Files Changed:
-
\packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/WorkflowActionsConfig.tsx\

Fixes #16538

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Pins `ADD_NODE` in the workflow action menu and unpins
`ADD_TO_FAVORITES`/`REMOVE_FROM_FAVORITES` to prioritize
workflow-related actions.
> 
> - **Workflow action menu config (`WorkflowActionsConfig.tsx`)**:
> - **Pinning**: Set `isPinned: true` for
`WorkflowSingleRecordActionKeys.ADD_NODE`.
> - **Unpinning**: Set `isPinned: false` for
`SingleRecordActionKeys.ADD_TO_FAVORITES` and
`SingleRecordActionKeys.REMOVE_FROM_FAVORITES` under
`propertiesToOverwrite`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
67b65df0a7. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2025-12-15 11:30:56 +01:00
Paul RastoinandGitHub 8c79353bfb Fix rename index collides with existing v2 index (#16560)
## Introduction
When migrating a v1 index name to v2 it might collide with an existing
v2 index
In this case we remove both the v1 metadata and pg index

In case of a metadata and pg_index desync this might occur too late in
the process that's why we have two fallback
One computing the v1 deletion from the metadata and another one in the
catch block of the v1 to migration transaction commit
2025-12-15 11:06:09 +01:00
Abdullah.andGitHub cf6fb419e0 fix: ensure deactivated object records do not appear in search (#16532)
Deactivating an object still allowed querying its records in search.

<img width="1512" height="856" alt="image"
src="https://github.com/user-attachments/assets/0a1ba36f-1ecf-44d3-8e97-294bb4e81e4f"
/>

<br />
<br />

This PR introduces a minor improvement to not query records of
deactivated objects.

<img width="1512" height="856" alt="image"
src="https://github.com/user-attachments/assets/f5fd1377-2fda-4653-b3cc-30a59d52e4cf"
/>
2025-12-15 10:42:22 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9fd2853315 Bump @types/unzipper from 0.10.10 to 0.10.11 (#16554)
Bumps
[@types/unzipper](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/unzipper)
from 0.10.10 to 0.10.11.
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/unzipper">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@types/unzipper&package-manager=npm_and_yarn&previous-version=0.10.10&new-version=0.10.11)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-15 10:25:53 +01:00
1119e3d77e fix(twenty-shared): preserve special characters in URLs (#16312)
# Fix: URL Encoding Bug & Code Refactor

## Issue

**Bug**: URLs with encoded characters (e.g., `%20` for spaces) were
being double-encoded or incorrectly processed in
[lowercaseUrlOriginAndRemoveTrailingSlash](cci:1://file:///Users/apple/WebstormProjects/twenty/packages/twenty-shared/src/utils/url/lowercaseUrlOriginAndRemoveTrailingSlash.ts:4:0-16:2),
causing URL mismatches and potential data integrity issues.

**Build Error**: `TS2307: Cannot find module
'src/modules/messaging/message-import-manager/drivers/imap/utils/safe-decode-uri-component.util'`

## Root Cause Analysis

### 1. Missing URL Decoding
The
[lowercaseUrlOriginAndRemoveTrailingSlash](cci:1://file:///Users/apple/WebstormProjects/twenty/packages/twenty-shared/src/utils/url/lowercaseUrlOriginAndRemoveTrailingSlash.ts:4:0-16:2)
function was processing URLs without properly decoding URI components.
When URLs contained encoded characters like `%20`, `%2F`, etc., they
weren't being normalized correctly.

### 2. Invalid Cross-Package Import
The fix attempted to import
[safeDecodeURIComponent](cci:1://file:///Users/apple/WebstormProjects/twenty/packages/twenty-shared/src/utils/url/safeDecodeURIComponent.ts:0:0-6:2)
from `twenty-server`:

```typescript
import { safeDecodeURIComponent } from 'src/modules/messaging/message-import-manager/drivers/imap/utils/safe-decode-uri-component.util';
```

This failed because:
- The file resides in `twenty-shared`, a separate package
- TypeScript cannot resolve internal paths from another package
- The monorepo uses package exports (`twenty-shared/utils`) for
cross-package imports, not direct file paths

## Solution

### 1. Bug Fix: Added Safe URI Decoding

Updated
[lowercaseUrlOriginAndRemoveTrailingSlash.ts](cci:7://file:///Users/apple/WebstormProjects/twenty/packages/twenty-shared/src/utils/url/lowercaseUrlOriginAndRemoveTrailingSlash.ts:0:0-0:0)
to properly decode URL components:

```typescript
export const lowercaseUrlOriginAndRemoveTrailingSlash = (rawUrl: string) => {
  const url = getURLSafely(rawUrl);

  if (!isDefined(url)) {
    return rawUrl;
  }

  const lowercaseOrigin = url.origin.toLowerCase();
  const path =
    safeDecodeURIComponent(url.pathname) + 
    safeDecodeURIComponent(url.search) + 
    url.hash;

  return (lowercaseOrigin + path).replace(/\/$/, '');
};
```

The
[safeDecodeURIComponent](cci:1://file:///Users/apple/WebstormProjects/twenty/packages/twenty-shared/src/utils/url/safeDecodeURIComponent.ts:0:0-6:2)
wrapper handles malformed URI sequences gracefully by returning the
original string if decoding fails, preventing runtime crashes.

### 2. Refactor: Consolidated Shared Utility

**Before**: Duplicate utility existed in `twenty-server`
```
twenty-server/src/modules/messaging/.../utils/safe-decode-uri-component.util.ts
```

**After**: Single source of truth in `twenty-shared`
```
twenty-shared/src/utils/url/safeDecodeURIComponent.ts
```

This follows the established pattern in the codebase where shared
utilities live in `twenty-shared` and are imported via subpath exports:

```typescript
// In twenty-server
import { safeDecodeURIComponent } from 'twenty-shared/utils';

// In twenty-shared (local import)
import { safeDecodeURIComponent } from './safeDecodeURIComponent';
```

## Files Changed

| File | Action | Description |
|------|--------|-------------|
|
[twenty-shared/src/utils/url/safeDecodeURIComponent.ts](cci:7://file:///Users/apple/WebstormProjects/twenty/packages/twenty-shared/src/utils/url/safeDecodeURIComponent.ts:0:0-0:0)
| **Created** | New shared utility (moved from twenty-server) |
|
[twenty-shared/src/utils/url/index.ts](cci:7://file:///Users/apple/WebstormProjects/twenty/packages/twenty-shared/src/utils/url/index.ts:0:0-0:0)
| **Modified** | Added export for
[safeDecodeURIComponent](cci:1://file:///Users/apple/WebstormProjects/twenty/packages/twenty-shared/src/utils/url/safeDecodeURIComponent.ts:0:0-6:2)
|
|
[twenty-shared/src/utils/url/lowercaseUrlOriginAndRemoveTrailingSlash.ts](cci:7://file:///Users/apple/WebstormProjects/twenty/packages/twenty-shared/src/utils/url/lowercaseUrlOriginAndRemoveTrailingSlash.ts:0:0-0:0)
| **Modified** | Fixed import path, now uses local relative import |
| `twenty-server/.../imap-message-text-extractor.service.ts` |
**Modified** | Updated import to use `twenty-shared/utils` |
| `twenty-server/.../safe-decode-uri-component.util.ts` | **Deleted** |
Removed duplicate utility |

## The Utility

```typescript
// safeDecodeURIComponent.ts
export const safeDecodeURIComponent = (text: string): string => {
  try {
    return decodeURIComponent(text);
  } catch {
    return text;
  }
};
```

This wrapper is necessary because `decodeURIComponent()` throws a
`URIError` on malformed sequences (e.g., `%E0%A4%A`). The safe version
returns the original string instead of crashing.

## Testing

- **342 tests passed** in `twenty-shared`
- `lowercaseUrlOriginAndRemoveTrailingSlash.test.ts` validates URL
normalization behavior
- No regressions in existing functionality

## Impact

- **Bug Fixed**: URLs with encoded characters are now properly
normalized
- **Code Quality**: Eliminated code duplication between packages
- **Maintainability**: Single source of truth for URI decoding utility
- **Build**: Resolved TS2307 compilation error

---------

Co-authored-by: Joker <apple@Apples-MacBook-Pro.local>
2025-12-15 08:41:57 +00:00
Paul RastoinandGitHub d189e3f57e Fix rename command error code edge case (#16556) 2025-12-15 08:36:35 +00:00
da9ade72c8 i18n - translations (#16545)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-12 19:22:27 +01:00
7f50486de5 Facebook links: display profile instead of full url (#16414)
Like we do for LinkedIn/Twitter

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2025-12-12 18:49:58 +01:00
EtienneandGitHub d36d0d49d1 Investigate - Slow transformRichTextV2 function (#16535)
Investigate CPU overloading ...

related to https://github.com/twentyhq/twenty/issues/15916
2025-12-12 18:39:01 +01:00
nitinandGitHub 44f0cfdd9e [Dashboards] Rich text editor frontend (#16437)
closes https://github.com/twentyhq/core-team-issues/issues/1894
2025-12-12 23:06:59 +05:30
Charles BochetandGitHub afcca283c4 Prevent table columns to be too narrow (#16542)
😬
<img width="453" height="312" alt="image"
src="https://github.com/user-attachments/assets/d233a430-8364-4f82-bae9-4324690bac9a"
/>
2025-12-12 18:15:25 +01:00
WeikoandGitHub 62408e7fd3 Fix APIKey in search resolver for get role (#16540)
Fixes https://github.com/twentyhq/twenty/issues/16534

Typing was wrong, apiKey from request object is ApiKeyEntity and not a
string which was then failing when using
```typescript
const roleId = apiKeyRoleMap[apiKeyId];

    if (!isDefined(roleId)) {
      throw new ApiKeyException(
        `API key ${apiKeyId} has no role assigned`,
        ApiKeyExceptionCode.API_KEY_NO_ROLE_ASSIGNED,
      );
    }
```

error
```
API key [object Object] has no role assigned"
```

## Before
<img width="963" height="316" alt="Screenshot 2025-12-12 at 17 37 06"
src="https://github.com/user-attachments/assets/761a75c6-1bac-48d7-b8cd-3356e9a56028"
/>

## After
<img width="905" height="313" alt="Screenshot 2025-12-12 at 17 36 44"
src="https://github.com/user-attachments/assets/475b7106-713c-408e-99d0-1447599f7152"
/>
2025-12-12 17:54:07 +01:00
ee47a773bd Fixed Apollo cache bug (#16523)
Fixes https://github.com/twentyhq/twenty/issues/16520

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2025-12-12 17:45:50 +01:00
Paul RastoinandGitHub f0af947331 [RenameIndexCommand] Fix orphaned index edge case (#16539)
We've been facing orphaned metadata index that would result in alter
error, now if we encounter one we just delete it
2025-12-12 16:44:00 +00:00
WeikoandGitHub 0035fc1145 Fix rest metadata version missing (#16536)
## Context

The REST pipeline re-fetches/sets the metadata version later in
RestApiBaseHandler#getObjectMetadata by reading from DB and seeding the
cache if it’s missing. That’s the place that actually needs it and
already handles the “undefined” case.

This also mirror the graphql path that was updated 3 weeks ago
2025-12-12 17:25:57 +01:00
3e17140385 i18n - docs translations (#16537)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-12 17:21:40 +01:00
Paul RastoinandGitHub 59663ec34e fix(server): non blocking CleanEmptyStringNullInTextFieldsCommand error (#16529)
# Introduction
Getting query timeout on prod upgrade for workspace that has huge
timeline activity table
2025-12-12 16:02:23 +01:00
4b292ac780 i18n - translations (#16527)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-12 15:21:11 +01:00
Raphaël BosiandGitHub d30453f38f [DASHBOARDS] Fix date order by (#16521)
## Bug description

The date order by was always set to date ascending regardless of the
setting. This PR fixes this and removes the possibility to sort by value
for date fields as it doesn't really make sense.

## Video QA


https://github.com/user-attachments/assets/cffc69e7-bee0-4944-88bb-bdf24fe0dd54
2025-12-12 14:55:44 +01:00
6acdde72ef Fix fetch more notes (#16442)
fixes : https://github.com/twentyhq/twenty/issues/16320

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2025-12-12 14:43:21 +01:00
8dbcd506ed feat: add feature to customize onClick behaviour for phone, email and links data type (#16265)
Fixes Issue: #15797

This PR adds a configurable click behavior setting for Phone, Email, and
Links field types, allowing users to customize what happens when
clicking on these fields.

A new option (**Click Behaviour**) to configure the default behaviour
for onClick of data is added in the settings page (Settings → Data Model
→ Object → Field Edit) for Phone, Email and Links data types.

Users can now choose between two actions:
- **Copy to clipboard**: Copies the value to clipboard with a success
toast message
- **Open as link**: Opens the value as a link (tel:, mailto:, or
http/https)

The default behaviour is persisted for all these three types(phone- Copy
to clipboard, email & links: open link) to maintain backward
compatibility.

**Screenshots :** 
<img width="2084" height="1736" alt="image"
src="https://github.com/user-attachments/assets/eb5d129c-e3e0-4334-b426-eb38d8a4840c"
/>

<img width="1474" height="1428" alt="image"
src="https://github.com/user-attachments/assets/487f4e44-6151-4254-bc5c-5398be5fc087"
/>
<img width="1594" height="1398" alt="image"
src="https://github.com/user-attachments/assets/abdbc213-7223-4d57-809e-4d55c90cda80"
/>

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2025-12-12 14:34:47 +01:00
Félix MalfaitandGitHub 4f91b48470 feat(ai): add context usage display to AI chat (BREAKING: deploy server first) (#16518)
## Summary

- Add a context usage indicator to the AI chat interface inspired by
Vercel's AI SDK Context component
- Display token consumption, context window utilization percentage, and
estimated cost in credits
- Show a circular progress ring with percentage, revealing detailed
breakdown on hover

## Changes

### Backend
- Stream usage metadata (tokens, model config) via `messageMetadata`
callback in `agent-chat-streaming.service.ts`
- Return model config from `chat-execution.service.ts`
- Add usage and model types to `ExtendedUIMessage` metadata

### Frontend
- New `ContextUsageProgressRing` component - circular SVG progress
indicator
- New `AIChatContextUsageButton` component with hover card showing:
  - Progress bar with used/total tokens
  - Input/output token counts with credit costs
  - Total credits consumed
- Track cumulative usage in Recoil state (`agentChatUsageState`)
- Reset usage when creating new chat thread
- Integrate button into `AIChatTab`

## Test plan

- [ ] Open AI chat and send a message
- [ ] Verify the context usage button appears with percentage
- [ ] Hover over the button to see detailed breakdown
- [ ] Verify credits are calculated correctly
- [ ] Create a new chat thread and verify usage resets to 0
2025-12-12 13:42:00 +01:00
ec243e9874 i18n - docs translations (#16524)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-12 13:22:59 +01:00
19f20cd37b i18n - translations (#16522)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-12 12:20:58 +01:00
EtienneandGitHub c274adf4d8 Fix raw json field display in read only (#16502)
Preview
<img width="1185" height="617" alt="Screenshot 2025-12-11 at 15 54 07"
src="https://github.com/user-attachments/assets/863fb58c-04df-4e4d-bf8c-0045c833466c"
/>

Fixes https://github.com/twentyhq/twenty/issues/16311
2025-12-12 11:27:02 +01:00
WeikoandGitHub bd8ed03990 Add TTL eviction to local data cache (#16510)
We keep different versions of our cache to avoid race conditions but we
never evict stale data. This PR should fix that
2025-12-12 10:14:46 +01:00
Abdul RahmanandGitHub 4b2a604ef0 Filter BAD_USER_INPUT errors from Sentry (#16511)
Closes #15561
2025-12-12 09:08:50 +01:00
Abdul RahmanandGitHub 865265d0d9 fix: Display locked UI for restricted email thread messages (#16512)
Closes #15289 

<img width="410" height="609" alt="Screenshot 2025-12-12 at 1 08 46 AM"
src="https://github.com/user-attachments/assets/34ea73e6-8664-4b59-96ad-3c15d04e148c"
/>
2025-12-12 09:05:52 +01:00
Félix MalfaitandGitHub 5f4f4c0af8 feat(ai): add dashboard tools for AI chat (#16517)
## Summary

- Implements real tools for the dashboard-building skill to create and
manage dashboards through the AI chat interface
- Adds 6 new dashboard tools: `create_complete_dashboard`,
`list_dashboards`, `get_dashboard`, `add_dashboard_widget`,
`update_dashboard_widget`, `delete_dashboard_widget`
- Improves widget configuration robustness with typed Zod schemas and
discriminated unions for graph types

## Key Changes

**New Dashboard Tools:**
- `create_complete_dashboard` - Creates a dashboard with layout, tab,
and widgets in a single call
- `list_dashboards` - Lists all dashboards in the workspace
- `get_dashboard` - Gets full dashboard details including tabs and
widget configurations
- `add_dashboard_widget` - Adds a widget to an existing dashboard tab
- `update_dashboard_widget` - Updates widget properties or configuration
- `delete_dashboard_widget` - Removes a widget from a dashboard

**Widget Configuration Improvements:**
- Typed Zod schemas for each chart type (AGGREGATE, BAR, LINE, PIE)
- Discriminated union validation based on `graphType`
- Widget-level error handling for partial success when creating
dashboards
- Clear documentation about required `objectMetadataId` and field UUIDs

**Skill Documentation Updates:**
- Updated `dashboard-building.skill.ts` with critical guidance about
looking up field metadata first
- Added workflow instructions: use `list_object_metadata_items` before
creating GRAPH widgets
- Practical grid layout recommendations

## Test plan

- [ ] Create a new dashboard via AI chat
- [ ] Verify widgets display data correctly when proper field IDs are
provided
- [ ] Test adding/updating/deleting widgets on existing dashboards
- [ ] Verify error messages are helpful when configuration is incorrect
2025-12-12 07:35:14 +01:00
Félix MalfaitandGitHub 70a78aafe9 feat(ai): replace agent search with skills system (#16513)
## Summary

- Replace the agent search mechanism with a new skills-based system
- Add a `skills` module with predefined skill definitions that the AI
can load on demand
- Remove specialized agents (workflow-builder, data-manipulator,
dashboard-builder, metadata-builder, researcher), keeping only the
helper agent
- Add `recordReferences` to workflow creation tool for chip linking in
the UI

## Changes

### New Skills Module
- `skill-definitions.ts` - Contains 5 skill definitions with detailed
instructions
- `skills.service.ts` - Service to get skills by name
- `load-skill.tool.ts` - Tool for AI to load skills explicitly

### Removed
- `agent-search.tool.ts` - Replaced by skill loading
- Specialized agent definitions (converted to skills)

### Updated
- Chat execution now shows skill catalog in system prompt
- Workflow creation returns `recordReferences` for UI linking

## Test plan
- [ ] Verify AI can load skills using `load_skill` tool
- [ ] Verify skill content is returned correctly
- [ ] Verify workflow creation shows clickable chip in chat
- [ ] Verify helper agent still works
2025-12-12 06:51:25 +01:00
4d90a8f6e3 i18n - docs translations (#16516)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-12 02:07:40 +01:00
14b2487d25 i18n - translations (#16515)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-12 00:01:10 +01:00
aad581a353 i18n - translations (#16514)
Created by Github action

---------

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-11 23:21:05 +01:00
Félix MalfaitandGitHub a13727335b feat(ai): refresh AI models with deprecation support and multi-provider defaults (BREAKING: deploy server before frontend please) (#16503)
## Summary

- Add latest AI models from OpenAI (GPT-4.1, o3, o4-mini), Anthropic
(Claude 4.5 Opus/Sonnet/Haiku), and xAI (Grok 4.1)
- Mark deprecated models (GPT-4o, GPT-4o-mini, GPT-4-turbo, Claude Opus
4, Claude Sonnet 4) with a `deprecated` flag
- Split AI models into separate files per provider for better
maintainability
- Support comma-separated default model lists for automatic fallback
across providers (works out of the box for self-hosters regardless of
which provider they configure)
- Filter deprecated models from dropdown selection while keeping them
functional for existing agents

## Changes

### New Models Added
| Provider | Models |
|----------|--------|
| OpenAI | gpt-4.1, gpt-4.1-mini, o3, o4-mini |
| Anthropic | claude-opus-4-5, claude-sonnet-4-5, claude-haiku-4-5 |
| xAI | grok-4-1-fast-reasoning |

### Deprecated Models
- gpt-4o, gpt-4o-mini, gpt-4-turbo (OpenAI)
- claude-opus-4-20250514, claude-sonnet-4-20250514 (Anthropic)

### Config Changes
Default model configs now support comma-separated fallback lists:
-
`DEFAULT_AI_SPEED_MODEL_ID=gpt-4.1-mini,claude-haiku-4-5-20251001,grok-3-mini`
-
`DEFAULT_AI_PERFORMANCE_MODEL_ID=gpt-4.1,claude-sonnet-4-5-20250929,grok-4`

## Test plan

- [x] Unit tests pass
- [x] Typecheck passes
- [x] Lint passes
- [ ] Verify deprecated models don't appear in model dropdowns
- [ ] Verify agents with deprecated models still work correctly
- [ ] Verify default model fallback works when only one provider is
configured
2025-12-11 21:47:38 +01:00
GuillimandGitHub 999bc84b17 fix(server): Favortites, attachments, timeline... (#16509)
fix(server): system objects like Favortites, attachments, timeline...
should be system fields
2025-12-11 18:28:14 +01:00
WeikoandGitHub 083a038f8a Deprecate workspace datasoure (#16507) 2025-12-11 18:27:49 +01:00
Raphaël BosiandGitHub 3dd2684254 Fix dashboard duplication (#16505)
Use `UUID!`
2025-12-11 16:47:51 +01:00
Raphaël BosiandGitHub 2da56c1886 Fix default order by (#16492)
Always apply an order by on the group by
2025-12-11 15:46:48 +01:00
7f9e948e05 i18n - docs translations (#16501)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-11 15:21:36 +01:00
ec8773437e Improved table flash on reload (#16419)
This PR fixes https://github.com/twentyhq/core-team-issues/issues/1732

There is still some room for improvement but the main goal is reached :
removing the flash effect each time the table virtualization has to
recompute due to an update after initial loading.

# QA

## Create 


https://github.com/user-attachments/assets/1b4fc307-42ce-4ba6-b557-68ac7fcad40f

## Update with sort


https://github.com/user-attachments/assets/e0700f44-8926-4395-8ab8-b32773f21de8

## Update with filter


https://github.com/user-attachments/assets/d325f85a-1a7b-4366-aac3-250331be7575

## Soft delete


https://github.com/user-attachments/assets/2c980183-c637-4aa7-a0ca-244e61396b20

## Restore and destroy


https://github.com/user-attachments/assets/01af9ec7-b442-4686-a1d7-ea1fc543fe62

## Drag & drop


https://github.com/user-attachments/assets/bba76bdb-d4ec-433d-b44e-37fdb9952b06

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2025-12-11 15:14:20 +01:00
Félix MalfaitandGitHub 3cea19baf4 feat(ai): add view management tools for AI chat (#16495)
## Summary

Adds a new **VIEW** tool category for the AI chat, enabling it to work
with views:

- **get-views**: List views in the workspace, optionally filtered by
object metadata ID
- **get-view-query-parameters**: Convert a view's filters and sorts into
GraphQL query parameters that can be passed to existing `find_*` data
tools
- **create-view**, **update-view**, **delete-view**: CRUD operations for
view management

### Key design decisions

1. **No pagination duplication**: Instead of creating a
`find-records-from-view` tool that would duplicate pagination logic,
`get-view-query-parameters` returns filter/sort parameters that the AI
can pass to existing record-fetching tools.

2. **Permission model**:
- Read tools (get-views, get-view-query-parameters) are available to all
users
   - Write tools require the `VIEW` permission
   - UNLISTED views can only be modified by their creator

3. **Leverages existing utilities**: Uses
`computeRecordGqlOperationFilter` from `twenty-shared` for filter
conversion.

### Files changed

- Added `ViewToolProvider`, `ViewToolsFactory`, and
`ViewQueryParamsService`
- Added `VIEW` to `ToolCategory` enum and tool registry
- Updated `chat-execution.service.ts` to include view tools in the
catalog and pass viewId in browsing context
- Extracted shared `formatValidationErrors` utility to reduce
duplication

## Test plan

- [x] Unit tests for `ViewToolsFactory` 
- [x] Unit tests for `ViewQueryParamsService`
- [x] Lint and typecheck pass
2025-12-11 15:05:42 +01:00
2943125410 i18n - translations (#16500)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-11 15:01:58 +01:00
Paul RastoinandGitHub f06a5ccacd Remove sync metadata from upgrade (#16491)
# Introduction
Related https://github.com/twentyhq/core-team-issues/issues/1910
From now on the upgrade won't integrate any sync metadata as it's going
to be deprecated very soon
Any updates to about to removes workspace-entity or standard flat entity
will require a dedicated upgrade command, what we've already started
doing during the 1.13 sprint, until we have totally migrated the v2 to
be workspace agnostic
2025-12-11 14:50:56 +01:00
Baptiste DevessierandGitHub 9c1197c0b2 fix: constrain side column width (#16498)
## Before


https://github.com/user-attachments/assets/18bfd8b0-7636-40b2-aa64-24c84ab74009

## After

<img width="3456" height="2160" alt="CleanShot 2025-12-11 at 14 13
23@2x"
src="https://github.com/user-attachments/assets/e10dc51c-afdb-4145-8ed5-0aaadfebb48b"
/>
2025-12-11 14:32:26 +01:00
GuillimandGitHub b83dc3aaff TimelineActivity migration to morph (#15652)
# TimelineActivity migration to morph


- Creates `timelineActivities2` relations on Company, Dashboard, Note,
Opportunity, Person, Task, Workflow, WorkflowRun, and WorkflowVersion
entities with proper metadata and cascade delete behavior.

It was required to create standard fields as well since the
mapObjectMetadataByUniqueIdentifier needs it. otherwise the fields won't
be considered

- Feature Flag `IS_TIMELINE_ACTIVITY_MIGRATED` necessary to have the two
states in parallel. It is used as a stamp once the migration has been
run

- Migration is done using the coreDataSource. Why ?
even though is unsafe to use, the first implementation of the migration
took forever on each workspace. See [this
commit](https://github.com/twentyhq/twenty/pull/15652/commits/477011e8d7d4c580f79ba7ec4a8fb002a3ec86b2)

The plan for this complex migration is as follows :

![plan](https://github.com/user-attachments/assets/51c63ea6-fb0d-40b4-b99d-3e0b35a204e6)
Note: we will need to rename fields in the release 1.12 (there is no
easy way to do all this in one release)
2025-12-11 14:28:26 +01:00
Baptiste DevessierandGitHub c13ef0c4a4 Fix overflow text with tooltip (#16490)
This PR allows the `OverflowingTextWithTooltip` component to take
complex content as parameter. It fixes the buggy display of long records
pinned as favorites.


https://github.com/user-attachments/assets/0224da09-8547-4def-927d-3f545406287e

Created extensive stories to test every case. Cleaned the component.

## Demo


https://github.com/user-attachments/assets/a8f257b5-2baf-42da-897b-a76b8a9ab664
2025-12-11 13:49:56 +01:00
918b145fb5 i18n - docs translations (#16497)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-11 13:43:50 +01:00
310bf5fe9a i18n - translations (#16496)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-11 13:02:10 +01:00
Raphaël BosiandGitHub 5af2d37253 [DASHBOARDS] Dashboard duplication (#16291)
## Description

- Created a Dashboard duplication action
- Created a new duplication custom resolver
- Created the service using the v2 of the API
- Created the integration tests following the v2 methodology

## Video QA


https://github.com/user-attachments/assets/e409951a-5946-4da0-91a0-1f7d2ecadb08
2025-12-11 11:15:15 +00:00
6c728fff78 i18n - docs translations (#16489)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-11 11:21:26 +01:00
5a697e914b i18n - translations (#16487)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-11 11:01:08 +01:00
Félix MalfaitandGitHub bc57b8ee4e feat(ai): add browsing context and fix tool loading (#16476)
## Summary

- Add `BrowsingContext` type to automatically pass what the user is
currently viewing (recordPage or listView) to the AI chat
- Simplify context architecture: remove toggleable context UI, make it
automatic and invisible to the user
- Fix tool loading: add `unionOf` handling in
`getDatabaseToolsForObject` and fix regex ordering so `find_one_*` tools
are properly registered
- Use plural names for find tools (`find_people` vs `find_one_person`)
for better semantics
- Clean up unused components and states

## Changes

### Frontend
- New `BrowsingContext` type and `useGetBrowsingContext` hook to gather
context from Recoil state
- Simplified `useAgentChat` to use the new browsing context
- Removed toggleable context UI components
(`AgentChatContextRecordPreview`, `SendMessageWithRecordsContextButton`,
etc.)
- Removed `isAgentChatCurrentContextActiveState`

### Backend
- New `BrowsingContextType` for recordPage and listView contexts
- Updated `ChatExecutionService` to build context from browsing context
- Fixed `tool-registry.service.ts`:
  - Added `unionOf` handling in permission config
- Fixed regex ordering (`find_one` before `find`) so tools load
correctly
- Use plural names for search tools (`find_people` instead of
`find_person`)

## Test plan

- [x] Typecheck passes
- [x] Lint passes
- [ ] Test AI chat on record page - should show context in system prompt
- [ ] Test AI chat on list view - should show view name and filters
- [ ] Test `find_one_*` tools now load correctly
- [ ] Test `find_*` tools use plural naming
2025-12-11 10:19:27 +01:00
GuillimandGitHub 5daef28328 Morph INPUT integration test (#16464)
- Adding a new target to the test setup suite
- We add the MORPH_RELATION to the list of types we wanna test in the
current test suites

Important:
I noticed the REST API was not working well with Morph relations. I
created [an
issue](https://github.com/twentyhq/core-team-issues/issues/2002) to work
on that. Within that timeframe, I `xdescribed` the related tests

Fixes https://github.com/twentyhq/core-team-issues/issues/1327
2025-12-11 10:10:30 +01:00
Thomas TrompetteandGitHub 7021cd41ea Use rich text editor in form field (#16474)
Fix https://github.com/twentyhq/twenty/issues/15948

Before
<img width="982" height="505" alt="Capture d’écran 2025-12-10 à 17 26
17"
src="https://github.com/user-attachments/assets/661a8205-cb1f-4b4b-8f0a-813c09ac9d16"
/>

After
<img width="982" height="767" alt="Capture d’écran 2025-12-10 à 17 25
49"
src="https://github.com/user-attachments/assets/58fa83a8-890e-42cb-a0bd-c5f6d7890d37"
/>
2025-12-11 08:43:29 +01:00
1077cf1560 i18n - translations (#16482)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2025-12-11 01:40:49 +01:00
MarieandGitHub 7a7f36d38c [Fix] Forbid default value removal for non-nullable select field (#16465)
Permanently fixes https://github.com/twentyhq/private-issues/issues/389

On a non-nullable select field, default value should not be removable,
otherwise users won't be able to create new records from the interface.
This PR enforces this in FE and BE.


https://github.com/user-attachments/assets/1dfa2bcc-b9df-4a00-9915-679d36ec1b25
2025-12-11 00:53:48 +01:00
2018 changed files with 175557 additions and 24192 deletions
+6 -2
View File
@@ -56,7 +56,9 @@ npx nx storybook:build twenty-front # Build Storybook
npx nx storybook:serve-and-test:static # Run Storybook tests
# Development
npx nx lint twenty-front # Run linter
npx nx lint:changed twenty-front # Lint changed files only (fastest)
npx nx lint:changed twenty-front --configuration=fix # Auto-fix changed files
npx nx lint twenty-front # Lint all files (slower)
npx nx typecheck twenty-front # Type checking
npx nx run twenty-front:graphql:generate # Generate GraphQL types
```
@@ -70,7 +72,9 @@ npx nx run twenty-server:database:migrate:prod # Run migrations
# Development
npx nx run twenty-server:start # Start the server
npx nx run twenty-server:lint # Run linter (add --fix to auto-fix)
npx nx lint:changed twenty-server # Lint changed files only (fastest)
npx nx lint:changed twenty-server --configuration=fix # Auto-fix changed files
npx nx run twenty-server:lint # Lint all files (slower)
npx nx run twenty-server:typecheck # Type checking
npx nx run twenty-server:test # Run unit tests
npx nx run twenty-server:test:integration:with-db-reset # Run integration tests
+15 -2
View File
@@ -12,7 +12,13 @@ alwaysApply: true
npx nx run twenty-front:build
npx nx run twenty-server:test
# Run target for all projects
# Lint changed files only (recommended - much faster!)
npx nx lint:changed twenty-front # Lint only files changed vs main
npx nx lint:changed twenty-server
npx nx lint:changed twenty-front --configuration=fix # Auto-fix changed files
npx nx lint:changed twenty-front --configuration=staged # Lint staged files
# Run target for all projects (slower)
npx nx run-many --target=build --all
npx nx run-many --target=test --projects=twenty-front,twenty-server
@@ -43,12 +49,19 @@ npx nx g @nx/react:component my-component
}
```
## Linting Strategy
For faster development, always prefer linting only changed files:
- Use `npx nx lint:changed <project>` to lint only files changed vs main branch
- Use `--configuration=fix` to auto-fix issues in changed files
- Use `--configuration=staged` to lint only staged files (useful for pre-commit hooks)
- Only use `npx nx lint <project>` when you need to lint the entire project
## Dependency Graph
```bash
# View project dependencies
npx nx graph
# Check what's affected by changes
# Check what's affected by changes (runs target on affected projects)
npx nx affected --target=test
npx nx affected --target=build --base=main
```
+3 -1
View File
@@ -27,7 +27,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
task: [lint, typecheck, test, build]
task: [lint, typecheck, test]
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
@@ -39,6 +39,8 @@ jobs:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/workflows/actions/yarn-install
- name: Build
run: npx nx build create-twenty-app
- name: Run ${{ matrix.task }} task
uses: ./.github/workflows/actions/nx-affected
with:
+2 -2
View File
@@ -78,7 +78,7 @@ jobs:
- name: Setup environment files
run: |
cp packages/twenty-front/.env.example packages/twenty-front/.env
npx nx reset:env twenty-server
npx nx reset:env:e2e-testing-server twenty-server
- name: Build frontend
run: NODE_ENV=production NODE_OPTIONS="--max-old-space-size=10240" npx nx build twenty-front
@@ -103,7 +103,7 @@ jobs:
npm_config_yes=true npx serve -s packages/twenty-front/build -l 3001 &
echo "Waiting for frontend to be ready..."
timeout 60 bash -c 'until curl -s http://localhost:3001; do sleep 2; done'
- name: Start worker
run: |
npx nx run twenty-server:worker &
+23
View File
@@ -185,6 +185,28 @@ jobs:
uses: ./.github/workflows/actions/save-cache
with:
key: ${{ steps.restore-task-cache.outputs.cache-primary-key }}
front-build:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
env:
NODE_OPTIONS: "--max-old-space-size=10240"
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: 0
- name: Install dependencies
uses: ./.github/workflows/actions/yarn-install
- name: Front / Write .env
run: npx nx reset:env twenty-front
- name: Build frontend
run: npx nx build twenty-front
ci-front-status-check:
if: always() && !cancelled()
timeout-minutes: 5
@@ -193,6 +215,7 @@ jobs:
[
changed-files-check,
front-task,
front-build,
front-chromatic-deployment,
merge-reports-and-check-coverage,
front-sb-test,
+3 -1
View File
@@ -27,7 +27,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
task: [lint, typecheck, test, build]
task: [lint, typecheck, test]
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
@@ -39,6 +39,8 @@ jobs:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/workflows/actions/yarn-install
- name: Build
run: npx nx build twenty-sdk
- name: Run ${{ matrix.task }} task
uses: ./.github/workflows/actions/nx-affected
with:
+2 -2
View File
@@ -171,7 +171,7 @@ jobs:
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5]
shard: [1, 2, 3, 4, 5, 6]
services:
postgres:
image: twentycrm/twenty-postgres-spilo
@@ -209,7 +209,7 @@ jobs:
ANALYTICS_ENABLED: true
CLICKHOUSE_URL: "http://default:clickhousePassword@localhost:8123/twenty"
CLICKHOUSE_PASSWORD: clickhousePassword
SHARD_COUNTER: 5
SHARD_COUNTER: 6
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
+10 -4
View File
@@ -36,10 +36,16 @@ When testing the UI end to end, click on "Continue with Email" and use the prefi
### Code Quality
```bash
# Linting
npx nx lint twenty-front # Frontend linting
npx nx lint twenty-server # Backend linting
npx nx lint twenty-front --fix # Auto-fix linting issues
# Linting (changed files only - fastest)
npx nx lint:changed twenty-front # Lint only changed files vs main
npx nx lint:changed twenty-server # Lint only changed files vs main
npx nx lint:changed twenty-front --configuration=fix # Auto-fix changed files
npx nx lint:changed twenty-front --configuration=staged # Lint staged files only
# Linting (full project)
npx nx lint twenty-front # Lint all files in frontend
npx nx lint twenty-server # Lint all files in backend
npx nx lint twenty-front --fix # Auto-fix all linting issues
# Type checking
npx nx typecheck twenty-front
-4
View File
@@ -75,10 +75,6 @@ export default [
sourceTag: 'scope:frontend',
onlyDependOnLibsWithTags: ['scope:shared', 'scope:frontend'],
},
{
sourceTag: 'scope:zapier',
onlyDependOnLibsWithTags: ['scope:shared'],
},
],
},
],
+489 -4
View File
@@ -128,6 +128,375 @@ export default [
additionalHooks: 'useRecoilCallback',
},
],
// Lingui - detect untranslated strings
'lingui/no-unlocalized-strings': [
'error',
{
ignore: [
// Ignore strings which are a single "word" (no spaces) and don't start with uppercase
'^(?![A-Z])\\S+$',
// Ignore UPPERCASE literals (constants, env vars)
'^[A-Z0-9_-]+$',
// Ignore strings that look like code/technical (contain special chars)
'^[\\s]*$', // whitespace only
'.*[{}/<>].*', // contains code-like characters
'^\\d+(\\.\\d+)?(px|rem|em|%|vh|vw|s|ms)?$', // CSS units
'^[\\d.]+(px|rem|em|%|vh|vw|fr|s|ms)?(\\s+[\\d.]+(px|rem|em|%|vh|vw|fr|s|ms)?)*$', // CSS values like "200px 1fr 20px"
'^#[0-9a-fA-F]{3,8}$', // hex colors
'^rgba?\\(.*\\)$', // rgb/rgba colors
'^(auto|none|inherit|initial|unset|flex|grid|block|inline|inline-block|relative|absolute|fixed|sticky)$', // CSS keywords
'^color:.*$', // CSS color declarations
'^font-.*$', // CSS font declarations
'^\\d+$', // numbers only
'^https?:\\/\\/.*', // URLs
'^@.*', // @ mentions or decorators
'^\\/.*', // paths starting with /
'^[HhMmSsYyDdAaPp:.,\\s-]+$', // date format patterns (HH:mm, yyyy-MM-dd, etc.)
'^Arrow(Up|Down|Left|Right)$', // keyboard keys
'^(Enter|Escape|Tab|Space|Backspace|Delete)$', // keyboard keys
'^Text$', // clipboard data type
'^(allow-|sandbox)', // iframe sandbox values
'^Id$', // technical identifier suffix (e.g., fieldNameId)
'^(string|number|boolean|void|any|unknown|never|object)$', // TypeScript type keywords
'^(Dark|Light)$', // color schemes
'^translate\\(.*\\)$', // CSS transform strings
'^svg .*$', // CSS selectors
'^Icon[A-Z]\\w*$', // Icon names like IconDefault, IconTable, IconSettings
'^\\w*Icon$', // Icon names that end with Icon like FieldIcon
'^%c.*$', // Console format strings
// Common item IDs for selectable lists
'^(Group|CalendarView|CalendarDateField|Compact view)$',
'^(Layout|Visibility|Fields|Delete view|Copy link to view|Create custom view)$',
'^(GroupBy|Sort|HideEmptyGroups|HiddenGroups)$',
// HTTP headers and auth (technical, not user-facing)
'^Authorization$',
'^Bearer .*',
// Allow object keys that are technical identifiers
'^(topLeft|topRight|bottomLeft|bottomRight)$',
// Color schemes and CSS media queries
'^System$',
'^\\(prefers-color-scheme:',
// GraphQL query names (used in refetchQueries)
'^Get[A-Z]\\w*$',
// React Context names (technical identifiers)
'.*Context$',
// SVG paths (geometric coordinates, not translatable)
'^M[0-9 LML]+$',
'^[ML][0-9 ]+$',
// Database ordering values (technical, backend API)
'^(Asc|Desc)Nulls(First|Last)$',
// Calendar response status values (backend enum values, not user-facing)
'^(Yes|No|Maybe)$',
// Email validation error prefixes (combined with dynamic content)
'^Invalid email(s)?:',
// GraphQL type construction patterns
'.*FilterInput$',
'.*OrderByInput.*',
'^\\$filter.*',
'^\\$orderBy.*',
'^\\$after.*',
'^\\$before.*',
'^\\$first.*',
'^\\$last.*',
// Logger names (technical identifiers)
'^Twenty(-\\w+)?$',
// Cookie names and cookie string patterns
'^twenty_session_id$',
'^; domain=',
// Context names for createRequiredContext
'^[A-Z][a-zA-Z]+$',
// JSON-like filter patterns
'^%"type":',
'^%"objectNameSingular":',
],
ignoreNames: [
// HTML/React attributes that shouldn't be translated
{ regex: { pattern: 'className', flags: 'i' } },
{ regex: { pattern: 'styleName', flags: 'i' } },
{ regex: { pattern: 'testId', flags: 'i' } },
'data-testid',
'dataTestId',
'src',
'srcSet',
'href',
'target',
'rel',
'type',
'id',
'key',
'name',
'htmlFor',
'width',
'height',
'fill',
'stroke',
'viewBox',
'clipPath',
'd', // SVG path
'transform',
'displayName',
'defaultValue',
'to', // router links
'path',
'pathname',
'hash',
'componentInstanceId',
'hotkeyScope',
'dropdownId',
'recoilScopeId',
'modalId',
'dialogId',
'itemId',
'selectableItemIdArray',
'listenerId',
'focusId',
'color', // color prop values
'variant', // component variants
'size', // size prop values
'position', // position values
'align', // alignment values
'justify', // justification values
'direction', // direction values
'orientation', // orientation values
'status', // status values
'state', // state values
'mode', // mode values
'accent', // accent values
// CSS-related props
'gridAutoColumns',
'gridAutoRows',
'gridTemplateColumns',
'gridTemplateRows',
'gridColumn',
'gridRow',
'gap',
'margin',
'padding',
'border',
'borderRadius',
'boxShadow',
'flex',
'flexDirection',
'flexWrap',
'justifyContent',
'alignItems',
'alignContent',
'overflow',
'display',
'cursor',
'zIndex',
'opacity',
'fontWeight',
'fontSize',
'lineHeight',
'textAlign',
'textDecoration',
'whiteSpace',
'wordBreak',
'objectFit',
'backgroundSize',
'backgroundPosition',
'minWidth',
'maxWidth',
'minHeight',
'maxHeight',
'mobileGridAutoColumns',
'tabletGridAutoColumns',
// Styled components
'css',
'theme',
'animation',
'transition',
// GraphQL
'query',
'mutation',
'subscription',
'fragment',
'operationName',
'variables',
'__typename',
// Technical identifiers
'fieldName',
'columnName',
'objectNameSingular',
'objectNamePlural',
'metadataId',
'nameSingular',
'namePlural',
// Event types
'eventName',
'event',
'action',
'actionType',
// Icon names
'iconName',
{ regex: { pattern: '^Icon[A-Z]' } },
// UPPER_CASE names (constants)
{ regex: { pattern: '^[A-Z][A-Z0-9_]*$' } },
// Sort direction values (backend API)
'orderBy',
{ regex: { pattern: '^(Asc|Desc)(NullsFirst|NullsLast)?$' } },
// HTTP headers (technical, not user-facing)
'Authorization',
],
ignoreFunctions: [
// Console and logging
'console.*',
'*.log',
'*.warn',
'*.error',
'*.debug',
'*.info',
'*.trace',
'logDebug',
'formatTitle',
// Error handling (technical messages, not user-facing)
'Error',
'TypeError',
'RangeError',
'SyntaxError',
'throw',
'assertUnreachable',
'CustomError',
'parseInitialBlocknote',
// Testing
'describe',
'it',
'test',
'expect',
'jest.*',
'*.toBe',
'*.toEqual',
'*.toContain',
'*.toMatch',
'*.toThrow',
// React/Libraries internals
'require',
'import',
'styled',
'styled.*',
'css',
'keyframes',
'createGlobalStyle',
// Router
'useNavigate',
'navigate',
'useLocation',
'useParams',
// Date formatting (patterns are not translatable)
'format',
'formatDate',
'formatDateTime',
'formatTime',
'parseISO',
'parse',
// Navigation
'useNavigationSection',
// Recoil
'atom',
'atomFamily',
'selector',
'selectorFamily',
'useSetRecoilState',
'useRecoilState',
'useRecoilValue',
// GraphQL operations
'gql',
'useQuery',
'useMutation',
'useLazyQuery',
'useSubscription',
// Type checking and validation
'*.includes',
'*.indexOf',
'*.startsWith',
'*.endsWith',
'*.split',
'*.join',
'*.match',
'*.replace',
'*.test',
'Object.keys',
'Object.values',
'Object.entries',
'Array.isArray',
// DOM operations
'*.getElementById',
'*.getElementsByClassName',
'*.querySelector',
'*.querySelectorAll',
'*.getAttribute',
'*.setAttribute',
'*.addEventListener',
'*.removeEventListener',
'*.dispatchEvent',
'*.createElement',
// Storage
'localStorage.*',
'sessionStorage.*',
'searchParams.*',
'*.get',
'*.set',
'*.has',
'*.delete',
// Misc utilities
'cva',
'cn',
'clsx',
'classNames',
'track',
'*.postMessage',
'*.dispatch',
'*.commit',
// Event handlers (typically receive enum values, not user-facing text)
'onChange',
'onClick',
'onSelect',
'onSubmit',
'onFocus',
'onBlur',
'onKeyDown',
'onKeyUp',
'onMouseEnter',
'onMouseLeave',
// Logging functions (technical messages, not user-facing)
'logError',
'logDebug',
'logInfo',
'logWarn',
'loggerLink',
// Context creation (technical names)
'createRequiredContext',
// GraphQL refetch queries (technical identifiers)
'refetchQueries',
],
},
],
},
},
@@ -175,7 +544,7 @@ export default [
'@typescript-eslint/ban-ts-comment': 'error',
'@typescript-eslint/consistent-type-imports': [
'error',
{
{
prefer: 'type-imports',
fixStyle: 'inline-type-imports'
},
@@ -208,11 +577,115 @@ export default [
},
},
// Storybook files
// Storybook files and story-related files
{
files: ['*.stories.@(ts|tsx|js|jsx)'],
files: [
'**/*.stories.ts',
'**/*.stories.tsx',
'**/*.stories.js',
'**/*.stories.jsx',
'**/__stories__/**/*',
],
rules: {
'@typescript-eslint/no-non-null-assertion': 'off',
'lingui/no-unlocalized-strings': 'off',
},
},
// Debug files - development only, not user-facing
{
files: [
'**/Debug*.tsx',
'**/*Debug*.tsx',
'**/*DebugDisplay*.tsx',
'**/*DebugHelper*.tsx',
'**/*DebugObserver*.tsx',
],
rules: {
'lingui/no-unlocalized-strings': 'off',
},
},
// Testing utilities and mock data - not user-facing
{
files: [
'**/testing/**/*.tsx',
'**/testing/**/*.ts',
'**/__mocks__/**/*',
'**/*mock*.ts',
'**/*Mock*.ts',
'**/perf/**/*',
],
rules: {
'lingui/no-unlocalized-strings': 'off',
},
},
// Constants files - technical values, not user-facing
{
files: [
'**/constants/**/*.ts',
'**/*.constants.ts',
'**/validation-schemas/**/*.ts',
'**/*Schema.ts',
'**/*-schema.ts',
],
rules: {
'lingui/no-unlocalized-strings': 'off',
},
},
// Layout configuration files - titles are translated at consumption time
{
files: ['**/layouts/**/*.ts'],
rules: {
'lingui/no-unlocalized-strings': 'off',
},
},
// Service files - contain technical strings (logger names, HTTP headers, etc.)
{
files: ['**/services/**/*.ts'],
rules: {
'lingui/no-unlocalized-strings': 'off',
},
},
// State files - contain technical default values
{
files: ['**/states/**/*.ts'],
rules: {
'lingui/no-unlocalized-strings': 'off',
},
},
// Utility files - technical/developer-facing
{
files: [
'**/utils/**/*.ts',
'**/*Utils.ts',
'**/*-utils.ts',
'**/*Util.ts',
'**/*-util.ts',
'**/errors/**/*.ts',
'**/*Error.ts',
'**/*-error.ts',
],
rules: {
'lingui/no-unlocalized-strings': 'off',
},
},
// Config and setup files - not user-facing
{
files: [
'**/*.config.ts',
'**/*.config.js',
'**/vite.config.ts',
'**/.storybook/**/*',
],
rules: {
'lingui/no-unlocalized-strings': 'off',
},
},
@@ -250,7 +723,18 @@ export default [
// Test files
{
files: [
'*.test.@(ts|tsx|js|jsx)',
'**/*.test.ts',
'**/*.test.tsx',
'**/*.test.js',
'**/*.test.jsx',
'**/*.spec.ts',
'**/*.spec.tsx',
'**/*.spec.js',
'**/*.spec.jsx',
'**/__tests__/**/*.ts',
'**/__tests__/**/*.tsx',
'**/__mocks__/**/*.ts',
'**/__mocks__/**/*.tsx',
],
languageOptions: {
globals: {
@@ -266,6 +750,7 @@ export default [
},
rules: {
'@typescript-eslint/no-non-null-assertion': 'off',
'lingui/no-unlocalized-strings': 'off',
},
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "create-twenty-app",
"version": "0.2.0",
"version": "0.2.2",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",
@@ -127,7 +127,7 @@ const createPackageJson = async ({
auth: 'twenty auth login',
},
dependencies: {
'twenty-sdk': '0.2.0',
'twenty-sdk': '0.2.2',
},
devDependencies: {
'@types/node': '^24.7.2',
@@ -12,19 +12,28 @@ function App() {
useEffect(() => {
browser.tabs.query({ active: true, currentWindow: true }, function(tabs) {
const currentTab = tabs[0];
if(currentTab.url?.includes('https://www.linkedin.com/in')) {
sendMessage('getPersonviaRelay').then(data => {
setValue({...data, type: 'person' })
})
}
if(currentTab.url?.includes('https://www.linkedin.com/company')) {
sendMessage('getCompanyviaRelay').then(data => {
setValue({...data, type: 'company'})
})
if (!currentTab.url) {
return;
}
let url = new URL(currentTab.url);
const isLinkedinHost =
url.protocol === 'https:' && url.hostname === 'www.linkedin.com';
if (isLinkedinHost && url.pathname.startsWith('/in/')) {
sendMessage('getPersonviaRelay').then((data) => {
setValue({ ...data, type: 'person' });
});
}
if (isLinkedinHost && url.pathname.startsWith('/company/')) {
sendMessage('getCompanyviaRelay').then((data) => {
setValue({ ...data, type: 'company' });
});
}
});
}, []);
const isPersonValue = (val: Value): val is PersonValue => {
@@ -38,25 +47,29 @@ function App() {
return (
<StyledMain>
<h1>{JSON.stringify(value)}</h1>
{
isPersonValue(value) &&
<button onClick={async () => {
await sendMessage('createPerson', {
firstName: value.firstName,
lastName: value.lastName,
});
}}>save person to twenty</button>
}
{
isCompanyValue(value) &&
<button onClick={async () => {
await sendMessage('createCompany', {
name: value.companyName
});
}}>save company to twenty</button>
}
{isPersonValue(value) && (
<button
onClick={async () => {
await sendMessage('createPerson', {
firstName: value.firstName,
lastName: value.lastName,
});
}}
>
save person to twenty
</button>
)}
{isCompanyValue(value) && (
<button
onClick={async () => {
await sendMessage('createCompany', {
name: value.companyName,
});
}}
>
save company to twenty
</button>
)}
</StyledMain>
);
}
@@ -1,6 +1,6 @@
{
"name": "hello-world",
"version": "0.1.0",
"version": "0.2.1",
"license": "MIT",
"engines": {
"node": "^24.5.0",
@@ -17,7 +17,7 @@
"auth": "twenty auth login"
},
"dependencies": {
"twenty-sdk": "0.1.2"
"twenty-sdk": "0.2.2"
},
"devDependencies": {
"@types/node": "^24.7.2"
@@ -1,21 +1,30 @@
import { type FunctionConfig } from 'twenty-sdk';
import { createClient } from '../../generated';
import type {
FunctionConfig,
DatabaseEventPayload,
ObjectRecordCreateEvent,
CronPayload,
} from 'twenty-sdk';
import Twenty, { type Person } from '../../generated';
export const main = async (params: { recipient?: string }) => {
type CreateNewPostCardParams =
| { name?: string }
| DatabaseEventPayload<ObjectRecordCreateEvent<Person>>
| CronPayload;
export const main = async (params: CreateNewPostCardParams) => {
try {
const client = createClient({
url: `${process.env.TWENTY_API_URL}/graphql`,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.TWENTY_API_KEY}`,
},
});
const client = new Twenty();
const name =
'name' in params
? params.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
const createPostCard = await client.mutation({
createPostCard: {
__args: {
data: {
name: params.recipient ?? 'Hello-world',
name,
},
},
name: true,
@@ -6,17 +6,14 @@ const config: ApplicationConfig = {
description: 'A simple hello world app',
icon: 'IconWorld',
applicationVariables: {
TWENTY_API_KEY: {
universalIdentifier: 'dedc53eb-9c12-4fe2-ba86-4a2add19d305',
description: 'Twenty API Key',
isSecret: true,
},
TWENTY_API_URL: {
universalIdentifier: 'ef8ab489-e68a-4841-b402-261f440e6185',
description: 'Twenty API Url',
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
description: 'Default recipient name for postcards',
value: 'Alex Karp',
isSecret: false,
},
},
functionRoleUniversalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
};
export default config;
@@ -0,0 +1,34 @@
import { PermissionFlag, type RoleConfig } from 'twenty-sdk';
export const functionRole: RoleConfig = {
universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
canBeAssignedToApplications: true,
objectPermissions: [
{
objectNameSingular: 'postCard',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
},
],
fieldPermissions: [
{
objectNameSingular: 'postCard',
fieldName: 'content',
canReadFieldValue: false,
canUpdateFieldValue: false,
},
],
permissionFlags: [PermissionFlag.APPLICATIONS],
};
+15 -5
View File
@@ -1021,6 +1021,15 @@ __metadata:
languageName: node
linkType: hard
"graphql-sse@npm:^2.5.4":
version: 2.6.0
resolution: "graphql-sse@npm:2.6.0"
peerDependencies:
graphql: ">=0.11 <=16"
checksum: 10c0/e05f0b5c8539d61e5ce34af8e0bb418c02bf922d6a7f9232a9abd53c77df5654684ca14f674ac771645c8fba9c37ce666c5d06f46eef54ea07e63653468065b0
languageName: node
linkType: hard
"graphql@npm:^16.6.0, graphql@npm:^16.8.1":
version: 16.12.0
resolution: "graphql@npm:16.12.0"
@@ -1074,7 +1083,7 @@ __metadata:
resolution: "hello-world@workspace:."
dependencies:
"@types/node": "npm:^24.7.2"
twenty-sdk: "npm:0.1.2"
twenty-sdk: "npm:0.2.1"
languageName: unknown
linkType: soft
@@ -1895,9 +1904,9 @@ __metadata:
languageName: node
linkType: hard
"twenty-sdk@npm:0.1.2":
version: 0.1.2
resolution: "twenty-sdk@npm:0.1.2"
"twenty-sdk@npm:0.2.1":
version: 0.2.1
resolution: "twenty-sdk@npm:0.2.1"
dependencies:
"@genql/cli": "npm:^3.0.3"
axios: "npm:^1.6.0"
@@ -1907,6 +1916,7 @@ __metadata:
dotenv: "npm:^16.4.0"
fs-extra: "npm:^11.2.0"
graphql: "npm:^16.8.1"
graphql-sse: "npm:^2.5.4"
inquirer: "npm:^10.0.0"
jsonc-parser: "npm:^3.2.0"
lodash.camelcase: "npm:^4.3.0"
@@ -1917,7 +1927,7 @@ __metadata:
uuid: "npm:^13.0.0"
bin:
twenty: dist/cli.cjs
checksum: 10c0/18cb8c589b6d6c6e59d58c28eae333718d62df485d827ba41e99421ef9fb359c3f71fa42d80b1e5463d2140b68de084aee81bf541e70c0d04a246c332e2978e7
checksum: 10c0/5a4f4674129cc747e8e5091bf75025260db20087f76f1cee49896a073c8cf83b8c95e1dce3f2f2cbaaf9570aa1886c4c9a5585f0e61e0ee79552eab4d292fb4e
languageName: node
linkType: hard
@@ -126,6 +126,16 @@ Figma プラットフォームの学習に関するより包括的な詳細と
これは推奨拡張機能の一部です。
これは推奨拡張機能の一部です。
これは推奨拡張機能の一部です。
これは推奨拡張機能の一部です。
これは推奨拡張機能の一部です。
これは推奨拡張機能の一部です。
これは推奨拡張機能の一部です。
これは推奨拡張機能の一部です。
これは推奨拡張機能の一部です。
これは推奨拡張機能の一部です。
これは推奨拡張機能の一部です。
これは推奨拡張機能の一部です。
これは推奨拡張機能の一部です。
## コラボレーション
@@ -158,7 +158,7 @@ SSLHTTPS)は、特定のブラウザ機能が正しく動作するために
2. **.env ファイルを更新**
.env`ファイルを開き、`SERVER_URL\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\`を更新します:
.env`ファイルを開き、`SERVER_URL\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\`を更新します:
```ini
SERVER_URL=http(s)://your-domain-or-ip:your-port
@@ -30,7 +30,7 @@ Twentyは、日常をサポートする最適なデータモデルを形成す
既存のオブジェクトの特性に過ぎないもの(例:会社の「業種」や機会の「ステータス」)はフィールドにします。 カテゴリ、ラベル、属性にはフィールドが最適です。 カテゴリ、ラベル、属性にはフィールドが最適です。 カテゴリ、ラベル、属性にはフィールドが最適です。 カテゴリ、ラベル、属性にはフィールドが最適です。 カテゴリ、ラベル、属性にはフィールドが最適です。 カテゴリ、ラベル、属性にはフィールドが最適です。 カテゴリ、ラベル、属性にはフィールドが最適です。 カテゴリ、ラベル、属性にはフィールドが最適です。 カテゴリ、ラベル、属性にはフィールドが最適です。 カテゴリ、ラベル、属性にはフィールドが最適です。 カテゴリ、ラベル、属性にはフィールドが最適です。 カテゴリ、ラベル、属性にはフィールドが最適です。 カテゴリ、ラベル、属性にはフィールドが最適です。 カテゴリ、ラベル、属性にはフィールドが最適です。 カテゴリ、ラベル、属性にはフィールドが最適です。 カテゴリ、ラベル、属性にはフィールドが最適です。 カテゴリ、ラベル、属性にはフィールドが最適です。 カテゴリ、ラベル、属性にはフィールドが最適です。 カテゴリ、ラベル、属性にはフィールドが最適です。 カテゴリ、ラベル、属性にはフィールドが最適です。 カテゴリ、ラベル、属性にはフィールドが最適です。 カテゴリ、ラベル、属性にはフィールドが最適です。 カテゴリ、ラベル、属性にはフィールドが最適です。
**3. 単独で存在する場合には新しいオブジェクトを作成してください。**
概念が独自のライフサイクル、プロパティ、または関係を持つ場合、それは通常オブジェクトに値します。 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば:
概念が独自のライフサイクル、プロパティ、または関係を持つ場合、それは通常オブジェクトに値します。 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば: 例えば:
- **プロジェクト**:独自の期限、所有者、タスクを持つもの
- **サブスクリプション**:会社、製品、請求書を結ぶもの
@@ -51,7 +51,7 @@ npx nx run twenty-front:storybook:coverage # (needs yarn storybook:serve:dev to
## Технологический стек
The project has a clean and simple stack, with minimal boilerplate code.
Проект имеет чистый и простой стек с минимальным кодом.
**Приложение**
@@ -78,7 +78,7 @@ const PageListeningEnter = () => {
### Прослушивание горячих клавиш в компоненте типа модального окна
For this example we'll use a modal component that listens for the Escape key to tell its parent to close it.
Для этого примера мы используем модальный компонент, который прослушивает ключ Escape, чтобы сообщить своему родителю закрыть его.
В этом случае взаимодействие пользователя меняет область операций.
@@ -45,7 +45,7 @@ sectionInfo: 配置您的Twenty工作空间设置和偏好
刪除您的帳號將永久移除您對所有工作空間的訪問。
刪除您的帳號將永久移除您對所有工作空間的訪問。 此操作無法撤銷,您將失去所有工作空間權限,如您只想退出特定的團隊,應考慮退出個別的工作空間。 刪除您的帳號將永久移除您對所有工作空間的訪問。
刪除您的帳號將永久移除您對所有工作空間的訪問。 此操作無法撤銷,您將失去所有工作空間權限,如您只想退出特定的團隊,應考慮退出個別的工作空間。
</Warning> 刪除您的帳號將永久移除您對所有工作空間的訪問。
</Warning> 刪除您的帳號將永久移除您對所有工作空間的訪問。
刪除您的帳號將永久移除您對所有工作空間的訪問。 此操作無法撤銷,您將失去所有工作空間權限,如您只想退出特定的團隊,應考慮退出個別的工作空間。
</Warning>
+1 -1
View File
@@ -3,4 +3,4 @@ FRONTEND_BASE_URL=http://localhost:3001
BACKEND_BASE_URL=http://localhost:3000
DEFAULT_LOGIN=tim@apple.dev
DEFAULT_PASSWORD=tim@apple.dev
WEBSITE_URL=https://twenty.com
WEBSITE_URL=https://twenty.com
@@ -27,6 +27,7 @@ export const test = base.extend<{ screenshotHook: void }>({
},
{ auto: true },
],
baseURL: process.env.LINK ? new URL(process.env.LINK).origin : 'http://localhost:3001',
});
export { expect } from '@playwright/test';
@@ -1,5 +1,5 @@
import { Page } from '@playwright/test';
import { getAuthToken } from '../utils/getAuthToken';
import { type Page } from '@playwright/test';
import { getAccessAuthToken } from '../utils/getAccessAuthToken';
import { backendGraphQLUrl } from './backend';
export const createWorkflow = async ({
@@ -11,7 +11,7 @@ export const createWorkflow = async ({
workflowId: string;
workflowName: string;
}) => {
const { authToken } = await getAuthToken(page);
const { authToken } = await getAccessAuthToken(page);
return page.request.post(backendGraphQLUrl, {
headers: {
@@ -1,5 +1,5 @@
import { Page } from '@playwright/test';
import { getAuthToken } from '../utils/getAuthToken';
import { type Page } from '@playwright/test';
import { getAccessAuthToken } from '../utils/getAccessAuthToken';
import { backendGraphQLUrl } from './backend';
export const deleteWorkflow = async ({
@@ -9,7 +9,7 @@ export const deleteWorkflow = async ({
page: Page;
workflowId: string;
}) => {
const { authToken } = await getAuthToken(page);
const { authToken } = await getAccessAuthToken(page);
return page.request.post(backendGraphQLUrl, {
headers: {
@@ -1,5 +1,5 @@
import { Page } from '@playwright/test';
import { getAuthToken } from '../utils/getAuthToken';
import { type Page } from '@playwright/test';
import { getAccessAuthToken } from '../utils/getAccessAuthToken';
import { backendGraphQLUrl } from './backend';
export const destroyWorkflow = async ({
@@ -9,7 +9,7 @@ export const destroyWorkflow = async ({
page: Page;
workflowId: string;
}) => {
const { authToken } = await getAuthToken(page);
const { authToken } = await getAccessAuthToken(page);
return page.request.post(backendGraphQLUrl, {
headers: {
@@ -0,0 +1,30 @@
import { type Page } from '@playwright/test';
const decodeToken = (cookie: any) =>
JSON.parse(decodeURIComponent(cookie.value)).accessOrWorkspaceAgnosticToken
?.token;
const decodePayload = (jwt: string) =>
JSON.parse(Buffer.from(jwt.split('.')[1], 'base64url').toString());
export const getAccessAuthToken = async (page: Page) => {
const storageState = await page.context().storageState();
const tokenCookies = storageState.cookies.filter(
(cookie) => cookie.name === 'tokenPair',
);
if (!tokenCookies) {
throw new Error('No auth cookie found');
}
const accessTokenCookie = tokenCookies.find(
(cookie) => {
const payload = decodePayload(decodeToken(cookie) ?? '');
return payload.type === 'ACCESS';
}
);
const token = JSON.parse(decodeURIComponent(accessTokenCookie?.value ?? '')).accessOrWorkspaceAgnosticToken
.token;
return { authToken: token };
};
@@ -1,15 +0,0 @@
import { Page } from '@playwright/test';
export const getAuthToken = async (page: Page) => {
const storageState = await page.context().storageState();
const authCookie = storageState.cookies.find(
(cookie) => cookie.name === 'tokenPair',
);
if (!authCookie) {
throw new Error('No auth cookie found');
}
const token = JSON.parse(decodeURIComponent(authCookie.value)).accessOrWorkspaceAgnosticToken
.token;
return { authToken: token };
};
@@ -34,7 +34,10 @@ export default defineConfig({
expect: {
timeout: 5000,
},
reporter: process.env.CI ? 'github' : 'list',
reporter: [
[process.env.CI ? 'github' : 'list'],
['./reporters/log-summary-reporter.ts'],
],
projects: [
{
name: 'setup',
@@ -0,0 +1,54 @@
import type {
Reporter,
TestCase,
TestResult,
} from '@playwright/test/reporter';
class LogSummaryReporter implements Reporter {
private passed: string[] = [];
private failed: string[] = [];
onTestEnd(test: TestCase, result: TestResult): void {
const name = test.titlePath().join(' ');
if (result.status === 'passed') {
this.passed.push(name);
return;
}
if (result.status === 'failed' || result.status === 'timedOut') {
this.failed.push(name);
}
}
onEnd(): void {
const passedSet = new Set(this.passed);
const failedSet = new Set(this.failed);
const flaky: string[] = [];
for (const testName of passedSet) {
if (failedSet.has(testName)) {
flaky.push(testName);
}
}
const uniquePassed = this.passed.filter((testName) => !flaky.includes(testName));
const uniqueFailed = this.failed.filter((testName) => !flaky.includes(testName));
console.log('\n=== Playwright summary ===');
if (uniquePassed.length) {
console.log('Passed:');
uniquePassed.forEach((testName) => console.log(`${testName}`));
}
if (uniqueFailed.length) {
console.log('Failed:');
uniqueFailed.forEach((testName) => console.log(`${testName}`));
}
if (flaky.length) {
console.log('Flaky:');
flaky.forEach((testName) => console.log(` ⚠️ ${testName}`));
}
}
}
export default LogSummaryReporter;
@@ -49,12 +49,10 @@ test('Sign up with invite link via email', async ({
await leftMenu.goToSettings();
await settingsPage.goToProfileSection();
await profileSection.deleteAccount();
await expect(page.getByText('Account Deletion')).toBeVisible();
await confirmationModal.typePlaceholderToInput();
await confirmationModal.clickConfirmButton();
await Promise.all([
page.waitForURL('/welcome'),
confirmationModal.clickConfirmButton(),
]);
await page.waitForURL('**/welcome');
});
});
@@ -0,0 +1,54 @@
import { expect, test } from '../lib/fixtures/screenshot';
test.describe.serial('Create Kanban View', () => {
test('Create Industry Select Field', async ({ page }) => {
await page.getByRole('link', { name: 'Settings' }).click();
await page.getByRole('link', { name: 'Data model' }).click();
await page.getByRole('link', { name: 'Opportunities' }).click();
await page.getByRole('button', { name: 'Add Field' }).click();
await page.getByRole('link', { name: 'Select', exact: true }).click();
await page.getByRole('textbox', { name: 'Employees' }).click();
await page.getByRole('textbox', { name: 'Employees' }).fill('Industry');
await page.getByRole('textbox').nth(1).click();
await page.getByRole('textbox').nth(1).press('ControlOrMeta+a');
await page.getByRole('textbox').nth(1).fill('Food');
await page.getByRole('button', { name: 'Add option' }).click();
await page.getByRole('button', { name: 'Option 2' }).getByRole('textbox').fill('Tech');
await page.getByRole('button', { name: 'Add option' }).click();
await page.getByRole('button', { name: 'Option 3' }).getByRole('textbox').fill('Travel');
await page.getByRole('button', { name: 'Save' }).click();
await page.waitForURL('**/objects/opportunities');
await page.waitForSelector('text=Industry');
await expect(page.getByText('Industry')).toBeVisible();
});
test('Create Kanban View from Industry Select Field', async ({ page }) => {
await page.getByRole('link', { name: 'Opportunities' }).click();
await page.getByRole('button', { name: 'All Opportunities ·' }).click();
await page.getByText('Add view').click();
await page.getByRole('textbox').press('ControlOrMeta+a');
await page.getByRole('textbox').fill('By industry');
await page.getByRole('button', { name: 'Table', exact: true }).click();
await page.getByText('Kanban').click();
await page.locator('[aria-controls="view-picker-kanban-field-options"]').click();
await page.getByRole('option', { name: 'Industry' }).click();
// Use exact: true to ensure we only click the button with the label "Create"
await page.getByRole('button', { name: 'Create new view' }).click();
await expect(page.getByText('Food')).toBeVisible();
await expect(page.getByText('Tech')).toBeVisible();
await expect(page.getByText('Travel')).toBeVisible();
await expect(page.getByText('No value')).toBeVisible();
const byIndustryElements = await page.locator('text=By industry').all();
expect(byIndustryElements.length).toBeGreaterThanOrEqual(1);
for (const element of byIndustryElements) {
await expect(element).toBeVisible();
}
await page.getByText('Options').click();
await page.getByText('Group', { exact: true }).click();
await Promise.all([page.getByTestId('hide-group-').click(),
page.waitForRequest((req) => {
return req.url().includes('/metadata') &&
req.method() === 'POST';
})]);
await expect(page.getByText('No value')).not.toBeVisible();
});
})
@@ -0,0 +1,158 @@
import { expect, test } from '../lib/fixtures/screenshot';
import { backendGraphQLUrl } from '../lib/requests/backend';
import { getAccessAuthToken } from '../lib/utils/getAccessAuthToken';
const query = `query FindOnePerson($objectRecordId: UUID!) {
person(
filter: {or: [{deletedAt: {is: NULL}}, {deletedAt: {is: NOT_NULL}}], id: {eq: $objectRecordId}}
) {
company {
name
}
emails {
primaryEmail
additionalEmails
__typename
}
id
intro
jobTitle
linkedinLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
__typename
}
name {
firstName
lastName
__typename
}
performanceRating
phones {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
__typename
}
position
workPreference
updatedAt
}
}`
test('Create and update record', async ({ page }) => {
await page.getByRole('link', { name: 'People' }).click();
await page.getByRole('button', { name: 'Create new record' }).click();
// Generate a random email for testing
const randomEmail = `testuser_${Math.random().toString(36).substring(2, 10)}@example.com`;
// Fill first name and last name
const firstNameInput = page.getByRole('textbox', { name: 'First name' })
await expect(firstNameInput).toBeFocused();
await firstNameInput.fill('John');
const lastNameInput = page.getByPlaceholder('Last name');
await expect(lastNameInput).toBeVisible();
await lastNameInput.fill('Doe');
await lastNameInput.press('Enter');
// Focus on recordFieldList
const recordFieldList = page.getByTestId('record-fields-list-container');
await expect(recordFieldList).toBeVisible();
await recordFieldList.getByText('Emails').first().click();
// Fill email
const emailInput = recordFieldList.getByText('Emails').nth(1);
await expect(emailInput).toBeVisible();
await emailInput.click({ force: true });
await page.getByPlaceholder('Email').fill(randomEmail);
await page.keyboard.press('Enter');
await page.keyboard.press('Escape');
await recordFieldList.getByText('Emails').first().click();
// Fill intro
const introInput = recordFieldList.getByText('Intro').nth(1);
await expect(introInput).toBeVisible();
await introInput.click({ force: true });
await introInput.click({ force: true });
await page.getByPlaceholder('Intro').fill('This is an intro');
await page.getByPlaceholder('Intro').press('Enter');
// Fill URL
const urlInput = recordFieldList.getByText('Linkedin').nth(1);
await expect(urlInput).toBeVisible();
await urlInput.click({ force: true });
await page.getByPlaceholder('URL').fill('linkedin.com/johndoe');
await page.getByPlaceholder('URL').press('Enter');
// Click on 4th star to rate
recordFieldList.getByText('Performance Rating').first().click({ force: true });
const ratingContainer = recordFieldList.locator('div[aria-label="Rating"]');
await ratingContainer.locator('svg').nth(3).click({force: true});
// Fill phone field
const phoneInput = recordFieldList.getByText('Phones').nth(1);
await expect(phoneInput).toBeVisible();
await phoneInput.click({ force: true });
await page.getByPlaceholder('Phone').fill('+336 1 122 3344');
await page.getByPlaceholder('Phone').press('Enter');
// Fill work preference
await recordFieldList.getByText('Work Preference').first().click({force: true});
await recordFieldList.getByText('Work Preference').nth(1).click({force: true});
const options = page.getByRole('listbox');
await options.getByText('Hybrid').first().click({force: true});
recordFieldList.getByText('Work Preference').first().click({force: true});
// Fill company relation
const companyRelationHeader = page.getByTestId('company-relation');
await expect(companyRelationHeader).toBeVisible();
await companyRelationHeader.locator('.tabler-icon-pencil').click();
await page.getByRole('textbox', { name: 'Search' }).fill('Goog');
await expect(page.getByRole('option', { name: 'Google' })).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: 'Google' }).click({force: true})
]);
const body = await updatePersonResponse.json()
const newPersonId = body.data.updatePerson.id;
// Check data was saved
const { authToken } = await getAccessAuthToken(page);
const findOnePersonResponse = await page.request.post(backendGraphQLUrl, {
headers: {
Authorization: `Bearer ${authToken}`,
},
data: {
operationName: 'FindOnePerson',
query,
variables: {
objectRecordId: newPersonId,
}
},
});
const findOnePersonReponseBody = await findOnePersonResponse.json();
expect(findOnePersonReponseBody.data.person.name.firstName).toBe('John');
expect(findOnePersonReponseBody.data.person.name.lastName).toBe('Doe');
expect(findOnePersonReponseBody.data.person.emails.primaryEmail).toBe(randomEmail);
expect(findOnePersonReponseBody.data.person.intro).toBe('This is an intro');
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('Google');
});
@@ -29,7 +29,13 @@ test('Login test', async ({ loginPage, page }) => {
await loginPage.typePassword(process.env.DEFAULT_PASSWORD);
await page.waitForLoadState('networkidle');
await loginPage.clickSignInButton();
await page.waitForLoadState('networkidle');
await expect(page.getByText(/Welcome to .+/)).not.toBeVisible();
await expect(page.getByText('Choose a workspace')).toBeVisible();
await page.getByText('Apple', {exact: true}).click();
await page.waitForFunction(() => window.location.href.includes('verify'));
await page.waitForFunction(() => !window.location.href.includes('verify'));
process.env.LINK = page.url();
},
);
@@ -37,6 +43,5 @@ test('Login test', async ({ loginPage, page }) => {
await page.context().storageState({
path: path.resolve(__dirname, '..', '.auth', 'user.json'),
});
process.env.LINK = page.url();
});
});
@@ -1,11 +1,11 @@
import { expect, test } from '@playwright/test';
import { expect, test } from '../lib/fixtures/screenshot';
import { deleteWorkflow } from '../lib/requests/delete-workflow';
import { destroyWorkflow } from '../lib/requests/destroy-workflow';
test('Create workflow', async ({ page }) => {
const NEW_WORKFLOW_NAME = 'Test Workflow';
await page.goto('/');
await page.goto(process.env.LINK);
const workflowsLink = page.getByRole('link', { name: 'Workflows' });
await workflowsLink.click();
@@ -25,7 +25,7 @@ test('Create workflow', async ({ page }) => {
return requestBody.operationName === 'CreateOneWorkflow';
}),
createWorkflowButton.click(),
createWorkflowButton.click()
]);
const recordName = page.getByTestId('top-bar-title').getByText('Untitled');
@@ -1,150 +1,150 @@
import { expect } from '@playwright/test';
import { test } from '../lib/fixtures/blank-workflow';
// import { expect } from '@playwright/test';
// import { test } from '../lib/fixtures/blank-workflow';
test('The workflow run visualizer shows the executed draft version without the last draft changes', async ({
workflowVisualizer,
page,
}) => {
await workflowVisualizer.createInitialTrigger('manual');
// test('The workflow run visualizer shows the executed draft version without the last draft changes', async ({
// workflowVisualizer,
// page,
// }) => {
// await workflowVisualizer.createInitialTrigger('manual');
const manualTriggerAvailabilitySelect = page.getByRole('button', {
name: 'When record is selected',
});
// const manualTriggerAvailabilitySelect = page.getByRole('button', {
// name: 'When record is selected',
// });
await manualTriggerAvailabilitySelect.click();
// await manualTriggerAvailabilitySelect.click();
const alwaysAvailableOption = page.getByText('When no record is selected');
// const alwaysAvailableOption = page.getByText('When no record is selected');
await alwaysAvailableOption.click();
// await alwaysAvailableOption.click();
await workflowVisualizer.closeSidePanel();
// await workflowVisualizer.closeSidePanel();
const { createdStepId: firstStepId } =
await workflowVisualizer.createStep('create-record');
// const { createdStepId: firstStepId } =
// await workflowVisualizer.createStep('create-record');
await workflowVisualizer.closeSidePanel();
// await workflowVisualizer.closeSidePanel();
const launchTestButton = page.getByLabel(workflowVisualizer.workflowName);
// const launchTestButton = page.getByLabel(workflowVisualizer.workflowName);
await launchTestButton.click();
// await launchTestButton.click();
await workflowVisualizer.closeSidePanel();
// await workflowVisualizer.closeSidePanel();
await workflowVisualizer.deleteStep(firstStepId);
// await workflowVisualizer.deleteStep(firstStepId);
await page.goto('/objects/workflowRuns');
// await page.goto('/objects/workflowRuns');
const recordTableRowForWorkflowRun = page
.getByRole('row', {
name: workflowVisualizer.workflowName,
})
.first();
// const recordTableRowForWorkflowRun = page
// .getByRole('row', {
// name: workflowVisualizer.workflowName,
// })
// .first();
const linkToWorkflowRun = recordTableRowForWorkflowRun
.getByRole('link', {
name: workflowVisualizer.workflowName,
})
.first();
// const linkToWorkflowRun = recordTableRowForWorkflowRun
// .getByRole('link', {
// name: workflowVisualizer.workflowName,
// })
// .first();
await linkToWorkflowRun.click({ force: true });
// await linkToWorkflowRun.click({ force: true });
const workflowRunNameElement = page
.getByText(`#1 - ${workflowVisualizer.workflowName}`)
.nth(1);
// const workflowRunNameElement = page
// .getByText(`#1 - ${workflowVisualizer.workflowName}`)
// .nth(1);
await expect(workflowRunNameElement).toBeVisible();
// await expect(workflowRunNameElement).toBeVisible();
const executedFirstStepNode = workflowVisualizer.getStepNode(firstStepId);
// const executedFirstStepNode = workflowVisualizer.getStepNode(firstStepId);
await expect(executedFirstStepNode).toBeVisible();
// await expect(executedFirstStepNode).toBeVisible();
await executedFirstStepNode.click();
// await executedFirstStepNode.click();
await expect(workflowVisualizer.stepHeaderInCommandMenu).toContainText(
'Create Record',
);
});
// await expect(workflowVisualizer.stepHeaderInCommandMenu).toContainText(
// 'Create Record',
// );
// });
test('Workflow Runs with a pending form step can be opened in the side panel and then in full screen', async ({
workflowVisualizer,
page,
}) => {
await workflowVisualizer.createInitialTrigger('manual');
// test('Workflow Runs with a pending form step can be opened in the side panel and then in full screen', async ({
// workflowVisualizer,
// page,
// }) => {
// await workflowVisualizer.createInitialTrigger('manual');
const manualTriggerAvailabilitySelect = page.getByRole('button', {
name: 'When record is selected',
});
// const manualTriggerAvailabilitySelect = page.getByRole('button', {
// name: 'When record is selected',
// });
await manualTriggerAvailabilitySelect.click();
// await manualTriggerAvailabilitySelect.click();
const alwaysAvailableOption = page.getByText('When no record is selected');
// const alwaysAvailableOption = page.getByText('When no record is selected');
await alwaysAvailableOption.click();
// await alwaysAvailableOption.click();
await workflowVisualizer.closeSidePanel();
// await workflowVisualizer.closeSidePanel();
const { createdStepId: firstStepId } =
await workflowVisualizer.createStep('form');
// const { createdStepId: firstStepId } =
// await workflowVisualizer.createStep('form');
const addFormFieldButton = page.getByText('Add Field', { exact: true });
// const addFormFieldButton = page.getByText('Add Field', { exact: true });
await addFormFieldButton.click();
// await addFormFieldButton.click();
await workflowVisualizer.closeSidePanel();
// await workflowVisualizer.closeSidePanel();
const launchTestButton = page.getByLabel(workflowVisualizer.workflowName);
// const launchTestButton = page.getByLabel(workflowVisualizer.workflowName);
await launchTestButton.click();
// await launchTestButton.click();
const workflowRunName = `#1 - ${workflowVisualizer.workflowName}`;
// const workflowRunName = `#1 - ${workflowVisualizer.workflowName}`;
await expect(workflowVisualizer.stepHeaderInCommandMenu).toContainText(
'Form',
{
timeout: 30_000,
},
);
// await expect(workflowVisualizer.stepHeaderInCommandMenu).toContainText(
// 'Form',
// {
// timeout: 30_000,
// },
// );
await workflowVisualizer.goBackInCommandMenu.click();
// await workflowVisualizer.goBackInCommandMenu.click();
const workflowRunNameInCommandMenu =
workflowVisualizer.commandMenu.getByText(workflowRunName);
// const workflowRunNameInCommandMenu =
// workflowVisualizer.commandMenu.getByText(workflowRunName);
await expect(workflowRunNameInCommandMenu).toBeVisible();
// await expect(workflowRunNameInCommandMenu).toBeVisible();
await workflowVisualizer.commandMenu
.locator(workflowVisualizer.triggerNode)
.click();
// await workflowVisualizer.commandMenu
// .locator(workflowVisualizer.triggerNode)
// .click();
await expect(workflowVisualizer.stepHeaderInCommandMenu).toContainText(
'Launch manually',
);
// await expect(workflowVisualizer.stepHeaderInCommandMenu).toContainText(
// 'Launch manually',
// );
await workflowVisualizer.goBackInCommandMenu.click();
// await workflowVisualizer.goBackInCommandMenu.click();
const formStep = workflowVisualizer.commandMenu.locator(
workflowVisualizer.getStepNode(firstStepId),
);
// const formStep = workflowVisualizer.commandMenu.locator(
// workflowVisualizer.getStepNode(firstStepId),
// );
await formStep.click();
// await formStep.click();
await workflowVisualizer.goBackInCommandMenu.click();
// await workflowVisualizer.goBackInCommandMenu.click();
const openInFullScreenButton = workflowVisualizer.commandMenu.getByRole(
'button',
{ name: 'Open' },
);
// const openInFullScreenButton = workflowVisualizer.commandMenu.getByRole(
// 'button',
// { name: 'Open' },
// );
await openInFullScreenButton.click();
// await openInFullScreenButton.click();
const workflowRunNameInShowPage = page
.getByText(`#1 - ${workflowVisualizer.workflowName}`)
.nth(1);
// const workflowRunNameInShowPage = page
// .getByText(`#1 - ${workflowVisualizer.workflowName}`)
// .nth(1);
await expect(workflowRunNameInShowPage).toBeVisible();
// await expect(workflowRunNameInShowPage).toBeVisible();
// Expect the side panel to be opened by default on the form.
await expect(workflowVisualizer.stepHeaderInCommandMenu).toContainText(
'Form',
);
});
// // Expect the side panel to be opened by default on the form.
// await expect(workflowVisualizer.stepHeaderInCommandMenu).toContainText(
// 'Form',
// );
// });
@@ -1,163 +1,161 @@
import { expect } from '@playwright/test';
import { test } from '../lib/fixtures/blank-workflow';
test('Use an old version as draft', async ({ workflowVisualizer, page }) => {
await workflowVisualizer.createInitialTrigger('record-created');
// test('Use an old version as draft', async ({ workflowVisualizer, page }) => {
// await workflowVisualizer.createInitialTrigger('record-created');
await workflowVisualizer.createStep('create-record');
// await workflowVisualizer.createStep('create-record');
await workflowVisualizer.background.click();
// await workflowVisualizer.background.click();
await workflowVisualizer.activateWorkflowButton.click();
// await workflowVisualizer.activateWorkflowButton.click();
await expect(workflowVisualizer.workflowStatus).toHaveText('Active');
// await expect(workflowVisualizer.workflowStatus).toHaveText('Active');
await workflowVisualizer.createStep('delete-record');
// await workflowVisualizer.createStep('delete-record');
await expect(workflowVisualizer.workflowStatus).toHaveText('Draft');
// await expect(workflowVisualizer.workflowStatus).toHaveText('Draft');
await workflowVisualizer.closeSidePanel();
// await workflowVisualizer.closeSidePanel();
await workflowVisualizer.activateWorkflowButton.click();
// await workflowVisualizer.activateWorkflowButton.click();
await expect(workflowVisualizer.workflowStatus).toHaveText('Active');
// await expect(workflowVisualizer.workflowStatus).toHaveText('Active');
await expect(workflowVisualizer.triggerNode).toContainText(
'Record is created',
);
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
'Create Record',
'Delete Record',
]);
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(2);
await expect(workflowVisualizer.useAsDraftButton).not.toBeVisible();
// await expect(workflowVisualizer.triggerNode).toContainText(
// 'Record is created',
// );
// await expect(workflowVisualizer.getAllStepNodes()).toContainText([
// 'Create Record',
// 'Delete Record',
// ]);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(2);
// await expect(workflowVisualizer.useAsDraftButton).not.toBeVisible();
const workflowsLink = page.getByRole('link', { name: 'Workflows' });
await workflowsLink.click();
// const workflowsLink = page.getByRole('link', { name: 'Workflows' });
// await workflowsLink.click();
await workflowVisualizer.setWorkflowsOpenInMode('record-page');
// await workflowVisualizer.setWorkflowsOpenInMode('record-page');
const recordTableRowForWorkflow = page.getByRole('row', {
name: workflowVisualizer.workflowName,
});
// const recordTableRowForWorkflow = page.getByRole('row', {
// name: workflowVisualizer.workflowName,
// });
const linkToWorkflow = recordTableRowForWorkflow.getByRole('link', {
name: workflowVisualizer.workflowName,
});
await expect(linkToWorkflow).toBeVisible();
// const linkToWorkflow = recordTableRowForWorkflow.getByRole('link', {
// name: workflowVisualizer.workflowName,
// });
// await expect(linkToWorkflow).toBeVisible();
const linkToFirstWorkflowVersion = recordTableRowForWorkflow.getByRole(
'link',
{
name: 'v1',
},
);
// const linkToFirstWorkflowVersion = recordTableRowForWorkflow.getByRole(
// 'link',
// {
// name: 'v1',
// },
// );
await linkToFirstWorkflowVersion.click({ force: true });
// await linkToFirstWorkflowVersion.click({ force: true });
await expect(workflowVisualizer.workflowStatus).toHaveText('Archived');
await expect(workflowVisualizer.useAsDraftButton).toBeVisible();
await expect(workflowVisualizer.triggerNode).toContainText(
'Record is created',
);
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
'Create Record',
]);
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
// await expect(workflowVisualizer.workflowStatus).toHaveText('Archived');
// await expect(workflowVisualizer.useAsDraftButton).toBeVisible();
// await expect(workflowVisualizer.triggerNode).toContainText(
// 'Record is created',
// );
// await expect(workflowVisualizer.getAllStepNodes()).toContainText([
// 'Create Record',
// ]);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
await workflowVisualizer.useAsDraftButton.click();
// await workflowVisualizer.useAsDraftButton.click();
await page.waitForURL(`/object/workflow/${workflowVisualizer.workflowId}`);
// await page.waitForURL(`/object/workflow/${workflowVisualizer.workflowId}`);
await expect(workflowVisualizer.workflowStatus).toHaveText('Draft');
await expect(workflowVisualizer.useAsDraftButton).not.toBeVisible();
await expect(workflowVisualizer.triggerNode).toContainText(
'Record is created',
);
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
'Create Record',
]);
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
});
// await expect(workflowVisualizer.workflowStatus).toHaveText('Draft');
// await expect(workflowVisualizer.useAsDraftButton).not.toBeVisible();
// await expect(workflowVisualizer.triggerNode).toContainText(
// 'Record is created',
// );
// await expect(workflowVisualizer.getAllStepNodes()).toContainText([
// 'Create Record',
// ]);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
// });
test('Use an old version as draft while having a pending draft version', async ({
workflowVisualizer,
page,
}) => {
await workflowVisualizer.createInitialTrigger('record-created');
// test('Use an old version as draft while having a pending draft version', async ({
// workflowVisualizer,
// page,
// }) => {
// await workflowVisualizer.createInitialTrigger('record-created');
await workflowVisualizer.createStep('create-record');
// await workflowVisualizer.createStep('create-record');
await workflowVisualizer.background.click();
// await workflowVisualizer.background.click();
await workflowVisualizer.activateWorkflowButton.click();
// await workflowVisualizer.activateWorkflowButton.click();
await expect(workflowVisualizer.workflowStatus).toHaveText('Active');
// await expect(workflowVisualizer.workflowStatus).toHaveText('Active');
await workflowVisualizer.createStep('delete-record');
// await workflowVisualizer.createStep('delete-record');
await expect(workflowVisualizer.workflowStatus).toHaveText('Draft');
// await expect(workflowVisualizer.workflowStatus).toHaveText('Draft');
await expect(workflowVisualizer.triggerNode).toContainText(
'Record is created',
);
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
'Create Record',
'Delete Record',
]);
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(2);
await expect(workflowVisualizer.useAsDraftButton).not.toBeVisible();
// await expect(workflowVisualizer.triggerNode).toContainText(
// 'Record is created',
// );
// await expect(workflowVisualizer.getAllStepNodes()).toContainText([
// 'Create Record',
// 'Delete Record',
// ]);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(2);
// await expect(workflowVisualizer.useAsDraftButton).not.toBeVisible();
await workflowVisualizer.closeSidePanel();
// await workflowVisualizer.closeSidePanel();
const workflowsLink = page.getByRole('link', { name: 'Workflows' });
await workflowsLink.click();
// const workflowsLink = page.getByRole('link', { name: 'Workflows' });
// await workflowsLink.click();
const recordTableRowForWorkflow = page.getByRole('row', {
name: workflowVisualizer.workflowName,
});
// const recordTableRowForWorkflow = page.getByRole('row', {
// name: workflowVisualizer.workflowName,
// });
const linkToWorkflow = recordTableRowForWorkflow.getByRole('link', {
name: workflowVisualizer.workflowName,
});
await expect(linkToWorkflow).toBeVisible();
// const linkToWorkflow = recordTableRowForWorkflow.getByRole('link', {
// name: workflowVisualizer.workflowName,
// });
// await expect(linkToWorkflow).toBeVisible();
const linkToFirstWorkflowVersion = recordTableRowForWorkflow.getByRole(
'link',
{
name: 'v1',
},
);
// const linkToFirstWorkflowVersion = recordTableRowForWorkflow.getByRole(
// 'link',
// {
// name: 'v1',
// },
// );
await linkToFirstWorkflowVersion.click({ force: true });
// await linkToFirstWorkflowVersion.click({ force: true });
await expect(workflowVisualizer.workflowStatus).toHaveText('Active');
await expect(workflowVisualizer.useAsDraftButton).toBeVisible();
await expect(workflowVisualizer.triggerNode).toContainText(
'Record is created',
);
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
'Create Record',
]);
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
// await expect(workflowVisualizer.workflowStatus).toHaveText('Active');
// await expect(workflowVisualizer.useAsDraftButton).toBeVisible();
// await expect(workflowVisualizer.triggerNode).toContainText(
// 'Record is created',
// );
// await expect(workflowVisualizer.getAllStepNodes()).toContainText([
// 'Create Record',
// ]);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
await workflowVisualizer.useAsDraftButton.click();
// await workflowVisualizer.useAsDraftButton.click();
await expect(workflowVisualizer.overrideDraftButton).toBeVisible();
// await expect(workflowVisualizer.overrideDraftButton).toBeVisible();
await workflowVisualizer.overrideDraftButton.click();
// await workflowVisualizer.overrideDraftButton.click();
await page.waitForURL(`/object/workflow/${workflowVisualizer.workflowId}`);
// await page.waitForURL(`/object/workflow/${workflowVisualizer.workflowId}`);
await expect(workflowVisualizer.workflowStatus).toHaveText('Draft');
await expect(workflowVisualizer.useAsDraftButton).not.toBeVisible();
await expect(workflowVisualizer.triggerNode).toContainText(
'Record is created',
);
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
'Create Record',
]);
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
await expect(workflowVisualizer.activateWorkflowButton).toBeVisible();
await expect(workflowVisualizer.discardDraftButton).toBeVisible();
});
// await expect(workflowVisualizer.workflowStatus).toHaveText('Draft');
// await expect(workflowVisualizer.useAsDraftButton).not.toBeVisible();
// await expect(workflowVisualizer.triggerNode).toContainText(
// 'Record is created',
// );
// await expect(workflowVisualizer.getAllStepNodes()).toContainText([
// 'Create Record',
// ]);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
// await expect(workflowVisualizer.activateWorkflowButton).toBeVisible();
// await expect(workflowVisualizer.discardDraftButton).toBeVisible();
// });
@@ -1,218 +1,218 @@
import { expect } from '@playwright/test';
import { test } from '../lib/fixtures/blank-workflow';
// import { expect } from '@playwright/test';
// import { test } from '../lib/fixtures/blank-workflow';
test('Create workflow with every possible step', async ({
workflowVisualizer,
page,
}) => {
await workflowVisualizer.createInitialTrigger('record-created');
// test('Create workflow with every possible step', async ({
// workflowVisualizer,
// page,
// }) => {
// await workflowVisualizer.createInitialTrigger('record-created');
await workflowVisualizer.createStep('create-record');
await workflowVisualizer.createStep('update-record');
await workflowVisualizer.createStep('delete-record');
await workflowVisualizer.createStep('code');
await workflowVisualizer.createStep('send-email');
// await workflowVisualizer.createStep('create-record');
// await workflowVisualizer.createStep('update-record');
// await workflowVisualizer.createStep('delete-record');
// await workflowVisualizer.createStep('code');
// await workflowVisualizer.createStep('send-email');
await workflowVisualizer.background.click();
// await workflowVisualizer.background.click();
const draftWorkflowStatus =
workflowVisualizer.workflowStatus.getByText('Draft');
// const draftWorkflowStatus =
// workflowVisualizer.workflowStatus.getByText('Draft');
await expect(draftWorkflowStatus).toBeVisible();
// await expect(draftWorkflowStatus).toBeVisible();
await workflowVisualizer.activateWorkflowButton.click();
// await workflowVisualizer.activateWorkflowButton.click();
const activeWorkflowStatus =
workflowVisualizer.workflowStatus.getByText('Active');
// const activeWorkflowStatus =
// workflowVisualizer.workflowStatus.getByText('Active');
await expect(draftWorkflowStatus).not.toBeVisible();
await expect(activeWorkflowStatus).toBeVisible();
await expect(workflowVisualizer.activateWorkflowButton).not.toBeVisible();
await expect(workflowVisualizer.deactivateWorkflowButton).toBeVisible();
});
// await expect(draftWorkflowStatus).not.toBeVisible();
// await expect(activeWorkflowStatus).toBeVisible();
// await expect(workflowVisualizer.activateWorkflowButton).not.toBeVisible();
// await expect(workflowVisualizer.deactivateWorkflowButton).toBeVisible();
// });
test('Delete steps from draft version', async ({
workflowVisualizer,
page,
}) => {
await workflowVisualizer.createInitialTrigger('record-created');
// test('Delete steps from draft version', async ({
// workflowVisualizer,
// page,
// }) => {
// await workflowVisualizer.createInitialTrigger('record-created');
const { createdStepId: firstStepId } =
await workflowVisualizer.createStep('create-record');
const { createdStepId: secondStepId } =
await workflowVisualizer.createStep('update-record');
const { createdStepId: thirdStepId } =
await workflowVisualizer.createStep('delete-record');
const { createdStepId: fourthStepId } =
await workflowVisualizer.createStep('code');
const { createdStepId: fifthStepId } =
await workflowVisualizer.createStep('send-email');
// const { createdStepId: firstStepId } =
// await workflowVisualizer.createStep('create-record');
// const { createdStepId: secondStepId } =
// await workflowVisualizer.createStep('update-record');
// const { createdStepId: thirdStepId } =
// await workflowVisualizer.createStep('delete-record');
// const { createdStepId: fourthStepId } =
// await workflowVisualizer.createStep('code');
// const { createdStepId: fifthStepId } =
// await workflowVisualizer.createStep('send-email');
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
'Create Record',
'Update Record',
'Delete Record',
'Code - Serverless Function',
'Send Email',
]);
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(5);
// await expect(workflowVisualizer.getAllStepNodes()).toContainText([
// 'Create Record',
// 'Update Record',
// 'Delete Record',
// 'Code - Serverless Function',
// 'Send Email',
// ]);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(5);
await workflowVisualizer.deleteStep(firstStepId);
// await workflowVisualizer.deleteStep(firstStepId);
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
'Update Record',
'Delete Record',
'Code - Serverless Function',
'Send Email',
]);
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(4);
// await expect(workflowVisualizer.getAllStepNodes()).toContainText([
// 'Update Record',
// 'Delete Record',
// 'Code - Serverless Function',
// 'Send Email',
// ]);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(4);
await workflowVisualizer.deleteStep(fifthStepId);
// await workflowVisualizer.deleteStep(fifthStepId);
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
'Update Record',
'Delete Record',
'Code - Serverless Function',
]);
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(3);
// await expect(workflowVisualizer.getAllStepNodes()).toContainText([
// 'Update Record',
// 'Delete Record',
// 'Code - Serverless Function',
// ]);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(3);
await workflowVisualizer.deleteStep(secondStepId);
// await workflowVisualizer.deleteStep(secondStepId);
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
'Delete Record',
'Code - Serverless Function',
]);
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(2);
// await expect(workflowVisualizer.getAllStepNodes()).toContainText([
// 'Delete Record',
// 'Code - Serverless Function',
// ]);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(2);
await workflowVisualizer.deleteStep(fourthStepId);
// await workflowVisualizer.deleteStep(fourthStepId);
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
'Delete Record',
]);
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
// await expect(workflowVisualizer.getAllStepNodes()).toContainText([
// 'Delete Record',
// ]);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
await workflowVisualizer.deleteStep(thirdStepId);
// await workflowVisualizer.deleteStep(thirdStepId);
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(0);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(0);
await Promise.all([
page.reload(),
// await Promise.all([
// page.reload(),
expect(workflowVisualizer.triggerNode).toBeVisible(),
]);
// expect(workflowVisualizer.triggerNode).toBeVisible(),
// ]);
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(0);
});
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(0);
// });
test('Add a step to an active version', async ({
workflowVisualizer,
page,
}) => {
await workflowVisualizer.createInitialTrigger('record-created');
// test('Add a step to an active version', async ({
// workflowVisualizer,
// page,
// }) => {
// await workflowVisualizer.createInitialTrigger('record-created');
await workflowVisualizer.createStep('create-record');
// await workflowVisualizer.createStep('create-record');
await expect(workflowVisualizer.workflowStatus).toHaveText('Draft');
// await expect(workflowVisualizer.workflowStatus).toHaveText('Draft');
await workflowVisualizer.background.click();
// await workflowVisualizer.background.click();
await Promise.all([
expect(workflowVisualizer.workflowStatus).toHaveText('Active'),
// await Promise.all([
// expect(workflowVisualizer.workflowStatus).toHaveText('Active'),
workflowVisualizer.activateWorkflowButton.click(),
]);
// workflowVisualizer.activateWorkflowButton.click(),
// ]);
await expect(workflowVisualizer.activateWorkflowButton).not.toBeVisible();
// await expect(workflowVisualizer.activateWorkflowButton).not.toBeVisible();
const assertEndState = async () => {
await expect(workflowVisualizer.workflowStatus).toHaveText('Active');
await expect(workflowVisualizer.triggerNode).toContainText(
'Record is created',
);
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
'Create Record',
]);
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
};
// const assertEndState = async () => {
// await expect(workflowVisualizer.workflowStatus).toHaveText('Active');
// await expect(workflowVisualizer.triggerNode).toContainText(
// 'Record is created',
// );
// await expect(workflowVisualizer.getAllStepNodes()).toContainText([
// 'Create Record',
// ]);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
// };
await assertEndState();
// await assertEndState();
await page.reload();
// await page.reload();
await assertEndState();
});
// await assertEndState();
// });
test('Replace the trigger of an active version', async ({
workflowVisualizer,
page,
}) => {
await workflowVisualizer.createInitialTrigger('record-created');
// test('Replace the trigger of an active version', async ({
// workflowVisualizer,
// page,
// }) => {
// await workflowVisualizer.createInitialTrigger('record-created');
await workflowVisualizer.createStep('create-record');
// await workflowVisualizer.createStep('create-record');
await workflowVisualizer.background.click();
// await workflowVisualizer.background.click();
await Promise.all([
expect(workflowVisualizer.workflowStatus).toHaveText('Active'),
// await Promise.all([
// expect(workflowVisualizer.workflowStatus).toHaveText('Active'),
workflowVisualizer.activateWorkflowButton.click(),
]);
// workflowVisualizer.activateWorkflowButton.click(),
// ]);
await Promise.all([
expect(workflowVisualizer.workflowStatus).toHaveText('Draft'),
// await Promise.all([
// expect(workflowVisualizer.workflowStatus).toHaveText('Draft'),
workflowVisualizer.deleteTrigger(),
]);
// workflowVisualizer.deleteTrigger(),
// ]);
await workflowVisualizer.createInitialTrigger('record-deleted');
// await workflowVisualizer.createInitialTrigger('record-deleted');
await workflowVisualizer.background.click();
// await workflowVisualizer.background.click();
await Promise.all([
expect(workflowVisualizer.workflowStatus).toHaveText('Active'),
// await Promise.all([
// expect(workflowVisualizer.workflowStatus).toHaveText('Active'),
workflowVisualizer.activateWorkflowButton.click(),
]);
// workflowVisualizer.activateWorkflowButton.click(),
// ]);
await page.reload();
// await page.reload();
await expect(workflowVisualizer.triggerNode).toContainText(
'Record is deleted',
);
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
'Create Record',
]);
});
// await expect(workflowVisualizer.triggerNode).toContainText(
// 'Record is deleted',
// );
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
// await expect(workflowVisualizer.getAllStepNodes()).toContainText([
// 'Create Record',
// ]);
// });
test("Nodes can't be deleted by pressing Backspace or Delete keys", async ({
workflowVisualizer,
page,
}) => {
await workflowVisualizer.triggerNode.click();
// test("Nodes can't be deleted by pressing Backspace or Delete keys", async ({
// workflowVisualizer,
// page,
// }) => {
// await workflowVisualizer.triggerNode.click();
await page.keyboard.press('Backspace');
await page.keyboard.press('Delete');
// await page.keyboard.press('Backspace');
// await page.keyboard.press('Delete');
await expect(workflowVisualizer.triggerNode).toBeVisible();
// await expect(workflowVisualizer.triggerNode).toBeVisible();
const { createdStepId: firstStepId } =
await workflowVisualizer.createStep('create-record');
const firstStep = workflowVisualizer.getStepNode(firstStepId);
// const { createdStepId: firstStepId } =
// await workflowVisualizer.createStep('create-record');
// const firstStep = workflowVisualizer.getStepNode(firstStepId);
await firstStep.click();
// await firstStep.click();
await expect(workflowVisualizer.getDeleteNodeButton(firstStep)).toBeVisible();
// await expect(workflowVisualizer.getDeleteNodeButton(firstStep)).toBeVisible();
await page.keyboard.press('Backspace');
await page.keyboard.press('Delete');
// await page.keyboard.press('Backspace');
// await page.keyboard.press('Delete');
await expect(firstStep).toBeVisible();
// await expect(firstStep).toBeVisible();
await workflowVisualizer.addStepButton.click();
// await workflowVisualizer.addStepButton.click();
await page.keyboard.press('Backspace');
await page.keyboard.press('Delete');
// await page.keyboard.press('Backspace');
// await page.keyboard.press('Delete');
await expect(workflowVisualizer.addStepButton).toBeVisible();
});
// await expect(workflowVisualizer.addStepButton).toBeVisible();
// });
+9 -1
View File
@@ -34,4 +34,12 @@ export default [
'@nx/dependency-checks': 'error',
},
},
];
// Disable lingui rule for email templates
{
files: ['**/*.email.tsx', '**/twenty-emails/**/*.tsx', '**/twenty-emails/**/*style.ts'],
rules: {
'lingui/no-unlocalized-strings': 'off',
},
},
];
@@ -9,11 +9,11 @@ import { HighlightedText } from 'src/components/HighlightedText';
import { Link } from 'src/components/Link';
import { MainText } from 'src/components/MainText';
import { Title } from 'src/components/Title';
import { DEFAULT_WORKSPACE_LOGO } from 'src/constants/DefaultWorkspaceLogo';
import { capitalize } from 'src/utils/capitalize';
import { createI18nInstance } from 'src/utils/i18n.utils';
import { type APP_LOCALES } from 'twenty-shared/translations';
import { getImageAbsoluteURI } from 'twenty-shared/utils';
import { DEFAULT_WORKSPACE_LOGO } from 'src/constants/DefaultWorkspaceLogo';
type SendApprovedAccessDomainValidationProps = {
link: string;
@@ -1 +1 @@
/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"Suspended Workspace\":[\"Espaço de Trabalho Suspenso\"],\"Dear {userName},\":[\"Caro \",[\"userName\"],\",\"],\"Hello,\":[\"Olá,\"],\"It appears that your workspace <0>{workspaceDisplayName}</0> has been suspended for {daysSinceInactive} days.\":[\"Parece que seu workspace <0>\",[\"workspaceDisplayName\"],\"</0> foi suspenso por \",[\"daysSinceInactive\"],\" dias.\"],\"The workspace will be deactivated in {remainingDays} {dayOrDays}, and all its data will be deleted.\":[\"O workspace será desativado em \",[\"remainingDays\"],\" \",[\"dayOrDays\"],\", e todos os seus dados serão excluídos.\"],\"If you wish to continue using Twenty, please update your subscription within the next {remainingDays} {dayOrDays}.\":[\"Se quiser continuar usando o Twenty, atualize sua assinatura nos próximos \",[\"remainingDays\"],\" \",[\"dayOrDays\"],\".\"],\"Update your subscription\":[\"Atualize sua assinatura\"],\"Validate domain\":[\"Validar domínio\"],\"{senderName} (<0>{senderEmail}</0>): Please validate this domain to allow users with <1>@{domain}</1> email addresses to join your workspace without requiring an invitation.\":[[\"senderName\"],\" (<0>\",[\"senderEmail\"],\"</0>): Por favor, valide este domínio para permitir que usuários com endereços de e-mail <1>@\",[\"domain\"],\"</1> entrem no seu espaço de trabalho sem precisar de um convite.\"],\"Test email\":[\"Email de teste\"],\"Join your team on Twenty\":[\"Junte-se à sua equipe no Twenty\"],\"{senderName} (<0>{senderEmail}</0>) has invited you to join a workspace called <1>{workspaceName}</1>.\":[[\"senderName\"],\" (<0>\",[\"senderEmail\"],\"</0>) convidou você para se juntar a um espaço de trabalho chamado <1>\",[\"workspaceName\"],\"</1>.\"],\"Accept invite\":[\"Aceitar convite\"],\"Confirm your new email address\":[\"Confirm your new email address\"],\"Confirm your email address\":[\"Confirme seu endereço de e-mail\"],\"Confirm new email\":[\"Confirm new email\"],\"Verify Email\":[\"Verificar e-mail\"],\"Password updated\":[\"Senha atualizada\"],\"This is a confirmation that password for your account ({email}) was successfully changed on {formattedDate}.\":[\"Esta é uma confirmação de que a senha da sua conta (\",[\"email\"],\") foi alterada com sucesso em \",[\"formattedDate\"],\".\"],\"If you did not initiate this change, please contact your workspace owner immediately.\":[\"Se você não iniciou essa alteração, entre em contato com o proprietário do workspace imediatamente.\"],\"Connect to Twenty\":[\"Conecte-se ao Twenty\"],\"Reset your password 🗝\":[\"Redefina sua senha 🗝\"],\"Set your password 🗝\":[\"Defina sua senha 🗝\"],\"Reset\":[\"Redefinir\"],\"Set\":[\"Definir\"],\"This link is only valid for the next {duration}. If the link does not work, you can use the login verification link directly:\":[\"Este link é válido apenas para os próximos \",[\"duration\"],\". Se o link não funcionar, você pode usar o link de verificação de login diretamente:\"],\"Deleted Workspace\":[\"Espaço de Trabalho Excluído\"],\"Your workspace <0>{workspaceDisplayName}</0> has been deleted as your subscription expired {daysSinceInactive} days ago.\":[\"Seu workspace <0>\",[\"workspaceDisplayName\"],\"</0> foi excluído porque sua assinatura expirou há \",[\"daysSinceInactive\"],\" dias.\"],\"All data in this workspace has been permanently deleted.\":[\"Todos os dados deste workspace foram excluídos permanentemente.\"],\"If you wish to use Twenty again, you can create a new workspace.\":[\"Se quiser usar o Twenty novamente, você pode criar um novo workspace.\"],\"Create a new workspace\":[\"Criar um novo espaço de trabalho\"],\"What is Twenty?\":[\"O que é Twenty?\"],\"It's a CRM, a software to help businesses manage their customer data and relationships efficiently.\":[\"É um CRM, um software para ajudar empresas a gerenciar seus dados de clientes e os relacionamentos de maneira eficiente.\"],\"Website\":[\"Site\"],\"Visit Twenty's website\":[\"Visite o website do Twenty\"],\"Github\":[\"Github\"],\"Visit Twenty's GitHub repository\":[\"Visite o repositório GitHub do Twenty\"],\"User guide\":[\"Guia do Usuário\"],\"Read Twenty's user guide\":[\"Leia o guia do usuário do Twenty\"],\"Developers\":[\"Desenvolvedores\"],\"Visit Twenty's developer documentation\":[\"Visite a documentação do desenvolvedor do Twenty\"],\"Twenty.com, Public Benefit Corporation\":[\"Twenty.com, Empresa de Benefício Público\"],\"San Francisco / Paris\":[\"São Francisco / Paris\"]}")as Messages;
/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"Suspended Workspace\":[\"Espaço de Trabalho Suspenso\"],\"Dear {userName},\":[\"Caro \",[\"userName\"],\",\"],\"Hello,\":[\"Olá,\"],\"It appears that your workspace <0>{workspaceDisplayName}</0> has been suspended for {daysSinceInactive} days.\":[\"Parece que seu workspace <0>\",[\"workspaceDisplayName\"],\"</0> foi suspenso por \",[\"daysSinceInactive\"],\" dias.\"],\"The workspace will be deactivated in {remainingDays} {dayOrDays}, and all its data will be deleted.\":[\"O workspace será desativado em \",[\"remainingDays\"],\" \",[\"dayOrDays\"],\", e todos os seus dados serão excluídos.\"],\"If you wish to continue using Twenty, please update your subscription within the next {remainingDays} {dayOrDays}.\":[\"Se quiser continuar usando o Twenty, atualize sua assinatura nos próximos \",[\"remainingDays\"],\" \",[\"dayOrDays\"],\".\"],\"Update your subscription\":[\"Atualize sua assinatura\"],\"Validate domain\":[\"Validar domínio\"],\"{senderName} (<0>{senderEmail}</0>): Please validate this domain to allow users with <1>@{domain}</1> email addresses to join your workspace without requiring an invitation.\":[[\"senderName\"],\" (<0>\",[\"senderEmail\"],\"</0>): Por favor, valide este domínio para permitir que usuários com endereços de e-mail <1>@\",[\"domain\"],\"</1> entrem no seu espaço de trabalho sem precisar de um convite.\"],\"Test email\":[\"Email de teste\"],\"Join your team on Twenty\":[\"Junte-se à sua equipe no Twenty\"],\"{senderName} (<0>{senderEmail}</0>) has invited you to join a workspace called <1>{workspaceName}</1>.\":[[\"senderName\"],\" (<0>\",[\"senderEmail\"],\"</0>) convidou você para se juntar a um espaço de trabalho chamado <1>\",[\"workspaceName\"],\"</1>.\"],\"Accept invite\":[\"Aceitar convite\"],\"Confirm your new email address\":[\"Confirme seu novo endereço de e-mail\"],\"Confirm your email address\":[\"Confirme seu endereço de e-mail\"],\"Confirm new email\":[\"Confirme o novo email\"],\"Verify Email\":[\"Verificar e-mail\"],\"Password updated\":[\"Senha atualizada\"],\"This is a confirmation that password for your account ({email}) was successfully changed on {formattedDate}.\":[\"Esta é uma confirmação de que a senha da sua conta (\",[\"email\"],\") foi alterada com sucesso em \",[\"formattedDate\"],\".\"],\"If you did not initiate this change, please contact your workspace owner immediately.\":[\"Se você não iniciou essa alteração, entre em contato com o proprietário do workspace imediatamente.\"],\"Connect to Twenty\":[\"Conecte-se ao Twenty\"],\"Reset your password 🗝\":[\"Redefina sua senha 🗝\"],\"Set your password 🗝\":[\"Defina sua senha 🗝\"],\"Reset\":[\"Redefinir\"],\"Set\":[\"Definir\"],\"This link is only valid for the next {duration}. If the link does not work, you can use the login verification link directly:\":[\"Este link é válido apenas para os próximos \",[\"duration\"],\". Se o link não funcionar, você pode usar o link de verificação de login diretamente:\"],\"Deleted Workspace\":[\"Espaço de Trabalho Excluído\"],\"Your workspace <0>{workspaceDisplayName}</0> has been deleted as your subscription expired {daysSinceInactive} days ago.\":[\"Seu workspace <0>\",[\"workspaceDisplayName\"],\"</0> foi excluído porque sua assinatura expirou há \",[\"daysSinceInactive\"],\" dias.\"],\"All data in this workspace has been permanently deleted.\":[\"Todos os dados deste workspace foram excluídos permanentemente.\"],\"If you wish to use Twenty again, you can create a new workspace.\":[\"Se quiser usar o Twenty novamente, você pode criar um novo workspace.\"],\"Create a new workspace\":[\"Criar um novo espaço de trabalho\"],\"What is Twenty?\":[\"O que é Twenty?\"],\"It's a CRM, a software to help businesses manage their customer data and relationships efficiently.\":[\"É um CRM, um software para ajudar empresas a gerenciar seus dados de clientes e os relacionamentos de maneira eficiente.\"],\"Website\":[\"Site\"],\"Visit Twenty's website\":[\"Visite o website do Twenty\"],\"Github\":[\"Github\"],\"Visit Twenty's GitHub repository\":[\"Visite o repositório GitHub do Twenty\"],\"User guide\":[\"Guia do Usuário\"],\"Read Twenty's user guide\":[\"Leia o guia do usuário do Twenty\"],\"Developers\":[\"Desenvolvedores\"],\"Visit Twenty's developer documentation\":[\"Visite a documentação do desenvolvedor do Twenty\"],\"Twenty.com, Public Benefit Corporation\":[\"Twenty.com, Empresa de Benefício Público\"],\"San Francisco / Paris\":[\"São Francisco / Paris\"]}")as Messages;
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -91,7 +91,7 @@ msgstr "Aceitar convite"
#. js-lingui-explicit-id
#: src/emails/send-email-verification-link.email.tsx
msgid "Confirm your new email address"
msgstr ""
msgstr "Confirme seu novo endereço de e-mail"
#. js-lingui-explicit-id
#: src/emails/send-email-verification-link.email.tsx
@@ -101,7 +101,7 @@ msgstr "Confirme seu endereço de e-mail"
#. js-lingui-explicit-id
#: src/emails/send-email-verification-link.email.tsx
msgid "Confirm new email"
msgstr ""
msgstr "Confirme o novo email"
#. js-lingui-explicit-id
#: src/emails/send-email-verification-link.email.tsx
+2 -2
View File
@@ -91,7 +91,7 @@ msgstr "Принять приглашение"
#. js-lingui-explicit-id
#: src/emails/send-email-verification-link.email.tsx
msgid "Confirm your new email address"
msgstr ""
msgstr "Подтвердите ваш новый адрес электронной почты"
#. js-lingui-explicit-id
#: src/emails/send-email-verification-link.email.tsx
@@ -101,7 +101,7 @@ msgstr "Подтвердите ваш адрес электронной почт
#. js-lingui-explicit-id
#: src/emails/send-email-verification-link.email.tsx
msgid "Confirm new email"
msgstr ""
msgstr "Подтвердить новый адрес эл. почты"
#. js-lingui-explicit-id
#: src/emails/send-email-verification-link.email.tsx
@@ -0,0 +1,6 @@
import { type JSONContent } from '@tiptap/core';
import { type ReactNode } from 'react';
export const hardBreak = (_node: JSONContent): ReactNode => {
return <br />;
};
@@ -1,14 +1,15 @@
import { type JSONContent } from '@tiptap/core';
import { Fragment, type ReactNode } from 'react';
import { TIPTAP_NODE_TYPES, type TipTapNodeType } from 'twenty-shared/utils';
import { bulletList } from '../nodes/bullet-list';
import { hardBreak } from '../nodes/hard-break';
import { heading } from '../nodes/heading';
import { image } from '../nodes/image';
import { listItem } from '../nodes/list-item';
import { orderedList } from '../nodes/ordered-list';
import { paragraph } from '../nodes/paragraph';
import { text } from '../nodes/text';
import { variableTag } from '../nodes/variable-tag';
import { bulletList } from '../nodes/bullet-list';
import { listItem } from '../nodes/list-item';
import { orderedList } from '../nodes/ordered-list';
const NODE_RENDERERS = {
[TIPTAP_NODE_TYPES.PARAGRAPH]: paragraph,
@@ -19,6 +20,7 @@ const NODE_RENDERERS = {
[TIPTAP_NODE_TYPES.BULLET_LIST]: bulletList,
[TIPTAP_NODE_TYPES.ORDERED_LIST]: orderedList,
[TIPTAP_NODE_TYPES.LIST_ITEM]: listItem,
[TIPTAP_NODE_TYPES.HARD_BREAK]: hardBreak,
};
const renderNode = (node: JSONContent): ReactNode => {
+1 -1
View File
@@ -11,7 +11,7 @@ const modulesCoverage = {
branches: 25,
statements: 43,
lines: 44,
functions: 38,
functions: 37,
include: ['src/modules/**/*'],
exclude: ['src/**/*.ts'],
};
+16
View File
@@ -59,6 +59,22 @@
"fix": {}
}
},
"lint:changed": {
"executor": "nx:run-commands",
"cache": false,
"options": {
"command": "git diff --name-only --diff-filter=d {args.base} | grep -E '\\.(ts|tsx|js|jsx)$' | grep '^packages/twenty-front/' | xargs -r npx eslint --config packages/twenty-front/eslint.config.mjs",
"base": "main"
},
"configurations": {
"fix": {
"command": "git diff --name-only --diff-filter=d {args.base} | grep -E '\\.(ts|tsx|js|jsx)$' | grep '^packages/twenty-front/' | xargs -r npx eslint --config packages/twenty-front/eslint.config.mjs --fix"
},
"staged": {
"command": "git diff --cached --name-only --diff-filter=d | grep -E '\\.(ts|tsx|js|jsx)$' | grep '^packages/twenty-front/' | xargs -r npx eslint --config packages/twenty-front/eslint.config.mjs"
}
}
},
"fmt": {
"options": {
"files": "src"
@@ -70,9 +70,14 @@ export type Agent = {
export type AgentChatThread = {
__typename?: 'AgentChatThread';
contextWindowTokens?: Maybe<Scalars['Int']>;
createdAt: Scalars['DateTime'];
id: Scalars['UUID'];
title?: Maybe<Scalars['String']>;
totalInputCredits: Scalars['Int'];
totalInputTokens: Scalars['Int'];
totalOutputCredits: Scalars['Int'];
totalOutputTokens: Scalars['Int'];
updatedAt: Scalars['DateTime'];
};
@@ -236,6 +241,8 @@ export type Application = {
agents: Array<Agent>;
applicationVariables: Array<ApplicationVariable>;
canBeUninstalled: Scalars['Boolean'];
defaultServerlessFunctionRole?: Maybe<Role>;
defaultServerlessFunctionRoleId?: Maybe<Scalars['String']>;
description: Scalars['String'];
id: Scalars['UUID'];
name: Scalars['String'];
@@ -571,6 +578,7 @@ export type CheckUserExistOutput = {
export type ClientAiModelConfig = {
__typename?: 'ClientAIModelConfig';
deprecated?: Maybe<Scalars['Boolean']>;
inputCostPer1kTokensInCredits: Scalars['Float'];
label: Scalars['String'];
modelId: Scalars['String'];
@@ -618,6 +626,7 @@ export enum ConfigVariablesGroup {
BILLING_CONFIG = 'BILLING_CONFIG',
CAPTCHA_CONFIG = 'CAPTCHA_CONFIG',
CLOUDFLARE_CONFIG = 'CLOUDFLARE_CONFIG',
CODE_INTERPRETER_CONFIG = 'CODE_INTERPRETER_CONFIG',
EMAIL_SETTINGS = 'EMAIL_SETTINGS',
EXCEPTION_HANDLER = 'EXCEPTION_HANDLER',
GOOGLE_AUTH = 'GOOGLE_AUTH',
@@ -1211,6 +1220,16 @@ export type DuplicateWorkflowVersionStepInput = {
workflowVersionId: Scalars['String'];
};
export type DuplicatedDashboard = {
__typename?: 'DuplicatedDashboard';
createdAt: Scalars['String'];
id: Scalars['UUID'];
pageLayoutId?: Maybe<Scalars['UUID']>;
position: Scalars['Float'];
title?: Maybe<Scalars['String']>;
updatedAt: Scalars['String'];
};
export type EditSsoInput = {
id: Scalars['UUID'];
status: SsoIdentityProviderStatus;
@@ -1295,10 +1314,13 @@ export enum FeatureFlagKey {
IS_PAGE_LAYOUT_ENABLED = 'IS_PAGE_LAYOUT_ENABLED',
IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
IS_READ_ON_REPLICA_ENABLED = 'IS_READ_ON_REPLICA_ENABLED',
IS_RECORD_PAGE_LAYOUT_ENABLED = 'IS_RECORD_PAGE_LAYOUT_ENABLED',
IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED',
IS_TIMELINE_ACTIVITY_MIGRATED = 'IS_TIMELINE_ACTIVITY_MIGRATED',
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
IS_WORKFLOW_RUN_STOPPAGE_ENABLED = 'IS_WORKFLOW_RUN_STOPPAGE_ENABLED'
IS_WORKFLOW_RUN_STOPPAGE_ENABLED = 'IS_WORKFLOW_RUN_STOPPAGE_ENABLED',
IS_WORKSPACE_CREATION_V2_ENABLED = 'IS_WORKSPACE_CREATION_V2_ENABLED'
}
export type Field = {
@@ -1852,6 +1874,7 @@ export type Mutation = {
destroyPageLayoutWidget: Scalars['Boolean'];
disablePostgresProxy: PostgresCredentials;
dismissReconnectAccountBanner: Scalars['Boolean'];
duplicateDashboard: DuplicatedDashboard;
duplicateWorkflow: WorkflowVersionDto;
duplicateWorkflowVersionStep: WorkflowVersionStepChanges;
editSSOIdentityProvider: EditSsoOutput;
@@ -2390,6 +2413,11 @@ export type MutationDismissReconnectAccountBannerArgs = {
};
export type MutationDuplicateDashboardArgs = {
id: Scalars['UUID'];
};
export type MutationDuplicateWorkflowArgs = {
input: DuplicateWorkflowInput;
};
@@ -3128,6 +3156,7 @@ export enum PermissionFlagType {
API_KEYS_AND_WEBHOOKS = 'API_KEYS_AND_WEBHOOKS',
APPLICATIONS = 'APPLICATIONS',
BILLING = 'BILLING',
CODE_INTERPRETER_TOOL = 'CODE_INTERPRETER_TOOL',
CONNECTED_ACCOUNTS = 'CONNECTED_ACCOUNTS',
DATA_MODEL = 'DATA_MODEL',
DOWNLOAD_FILE = 'DOWNLOAD_FILE',
@@ -3162,6 +3191,7 @@ export type PieChartConfiguration = {
graphType: GraphType;
groupByFieldMetadataId: Scalars['UUID'];
groupBySubFieldName?: Maybe<Scalars['String']>;
hideEmptyCategory?: Maybe<Scalars['Boolean']>;
orderBy?: Maybe<GraphOrderBy>;
showCenterMetric?: Maybe<Scalars['Boolean']>;
timezone?: Maybe<Scalars['String']>;
@@ -4754,6 +4784,7 @@ export type WidgetConfiguration = AggregateChartConfiguration | BarChartConfigur
export enum WidgetType {
CALENDAR = 'CALENDAR',
EMAILS = 'EMAILS',
FIELD = 'FIELD',
FIELDS = 'FIELDS',
FIELD_RICH_TEXT = 'FIELD_RICH_TEXT',
FILES = 'FILES',
@@ -5096,7 +5127,7 @@ export type GetChatMessagesQuery = { __typename?: 'Query', chatMessages: Array<{
export type GetChatThreadsQueryVariables = Exact<{ [key: string]: never; }>;
export type GetChatThreadsQuery = { __typename?: 'Query', chatThreads: Array<{ __typename?: 'AgentChatThread', id: string, title?: string | null, createdAt: string, updatedAt: string }> };
export type GetChatThreadsQuery = { __typename?: 'Query', chatThreads: Array<{ __typename?: 'AgentChatThread', id: string, title?: string | null, totalInputTokens: number, totalOutputTokens: number, contextWindowTokens?: number | null, totalInputCredits: number, totalOutputCredits: number, createdAt: string, updatedAt: string }> };
export type TrackAnalyticsMutationVariables = Exact<{
type: AnalyticsType;
@@ -7704,6 +7735,11 @@ export const GetChatThreadsDocument = gql`
chatThreads {
id
title
totalInputTokens
totalOutputTokens
contextWindowTokens
totalInputCredits
totalOutputCredits
createdAt
updatedAt
}
+32 -1
View File
@@ -70,9 +70,14 @@ export type Agent = {
export type AgentChatThread = {
__typename?: 'AgentChatThread';
contextWindowTokens?: Maybe<Scalars['Int']>;
createdAt: Scalars['DateTime'];
id: Scalars['UUID'];
title?: Maybe<Scalars['String']>;
totalInputCredits: Scalars['Int'];
totalInputTokens: Scalars['Int'];
totalOutputCredits: Scalars['Int'];
totalOutputTokens: Scalars['Int'];
updatedAt: Scalars['DateTime'];
};
@@ -236,6 +241,8 @@ export type Application = {
agents: Array<Agent>;
applicationVariables: Array<ApplicationVariable>;
canBeUninstalled: Scalars['Boolean'];
defaultServerlessFunctionRole?: Maybe<Role>;
defaultServerlessFunctionRoleId?: Maybe<Scalars['String']>;
description: Scalars['String'];
id: Scalars['UUID'];
name: Scalars['String'];
@@ -571,6 +578,7 @@ export type CheckUserExistOutput = {
export type ClientAiModelConfig = {
__typename?: 'ClientAIModelConfig';
deprecated?: Maybe<Scalars['Boolean']>;
inputCostPer1kTokensInCredits: Scalars['Float'];
label: Scalars['String'];
modelId: Scalars['String'];
@@ -618,6 +626,7 @@ export enum ConfigVariablesGroup {
BILLING_CONFIG = 'BILLING_CONFIG',
CAPTCHA_CONFIG = 'CAPTCHA_CONFIG',
CLOUDFLARE_CONFIG = 'CLOUDFLARE_CONFIG',
CODE_INTERPRETER_CONFIG = 'CODE_INTERPRETER_CONFIG',
EMAIL_SETTINGS = 'EMAIL_SETTINGS',
EXCEPTION_HANDLER = 'EXCEPTION_HANDLER',
GOOGLE_AUTH = 'GOOGLE_AUTH',
@@ -1194,6 +1203,16 @@ export type DuplicateWorkflowVersionStepInput = {
workflowVersionId: Scalars['String'];
};
export type DuplicatedDashboard = {
__typename?: 'DuplicatedDashboard';
createdAt: Scalars['String'];
id: Scalars['UUID'];
pageLayoutId?: Maybe<Scalars['UUID']>;
position: Scalars['Float'];
title?: Maybe<Scalars['String']>;
updatedAt: Scalars['String'];
};
export type EditSsoInput = {
id: Scalars['UUID'];
status: SsoIdentityProviderStatus;
@@ -1278,10 +1297,13 @@ export enum FeatureFlagKey {
IS_PAGE_LAYOUT_ENABLED = 'IS_PAGE_LAYOUT_ENABLED',
IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
IS_READ_ON_REPLICA_ENABLED = 'IS_READ_ON_REPLICA_ENABLED',
IS_RECORD_PAGE_LAYOUT_ENABLED = 'IS_RECORD_PAGE_LAYOUT_ENABLED',
IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED',
IS_TIMELINE_ACTIVITY_MIGRATED = 'IS_TIMELINE_ACTIVITY_MIGRATED',
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
IS_WORKFLOW_RUN_STOPPAGE_ENABLED = 'IS_WORKFLOW_RUN_STOPPAGE_ENABLED'
IS_WORKFLOW_RUN_STOPPAGE_ENABLED = 'IS_WORKFLOW_RUN_STOPPAGE_ENABLED',
IS_WORKSPACE_CREATION_V2_ENABLED = 'IS_WORKSPACE_CREATION_V2_ENABLED'
}
export type Field = {
@@ -1825,6 +1847,7 @@ export type Mutation = {
destroyPageLayoutWidget: Scalars['Boolean'];
disablePostgresProxy: PostgresCredentials;
dismissReconnectAccountBanner: Scalars['Boolean'];
duplicateDashboard: DuplicatedDashboard;
duplicateWorkflow: WorkflowVersionDto;
duplicateWorkflowVersionStep: WorkflowVersionStepChanges;
editSSOIdentityProvider: EditSsoOutput;
@@ -2342,6 +2365,11 @@ export type MutationDismissReconnectAccountBannerArgs = {
};
export type MutationDuplicateDashboardArgs = {
id: Scalars['UUID'];
};
export type MutationDuplicateWorkflowArgs = {
input: DuplicateWorkflowInput;
};
@@ -3049,6 +3077,7 @@ export enum PermissionFlagType {
API_KEYS_AND_WEBHOOKS = 'API_KEYS_AND_WEBHOOKS',
APPLICATIONS = 'APPLICATIONS',
BILLING = 'BILLING',
CODE_INTERPRETER_TOOL = 'CODE_INTERPRETER_TOOL',
CONNECTED_ACCOUNTS = 'CONNECTED_ACCOUNTS',
DATA_MODEL = 'DATA_MODEL',
DOWNLOAD_FILE = 'DOWNLOAD_FILE',
@@ -3083,6 +3112,7 @@ export type PieChartConfiguration = {
graphType: GraphType;
groupByFieldMetadataId: Scalars['UUID'];
groupBySubFieldName?: Maybe<Scalars['String']>;
hideEmptyCategory?: Maybe<Scalars['Boolean']>;
orderBy?: Maybe<GraphOrderBy>;
showCenterMetric?: Maybe<Scalars['Boolean']>;
timezone?: Maybe<Scalars['String']>;
@@ -4573,6 +4603,7 @@ export type WidgetConfiguration = AggregateChartConfiguration | BarChartConfigur
export enum WidgetType {
CALENDAR = 'CALENDAR',
EMAILS = 'EMAILS',
FIELD = 'FIELD',
FIELDS = 'FIELDS',
FIELD_RICH_TEXT = 'FIELD_RICH_TEXT',
FILES = 'FILES',
@@ -3,7 +3,7 @@ import { motion } from 'framer-motion';
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
import { NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/NavDrawerWidths';
import { NAVIGATION_DRAWER_CONSTRAINTS } from '@/ui/layout/resizable-panel/constants/NavigationDrawerConstraints';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useTheme } from '@emotion/react';
import { ANIMATION } from 'twenty-ui/theme';
@@ -54,9 +54,7 @@ export const LeftPanelSkeletonLoader = () => {
<StyledAnimatedContainer
initial={false}
animate={{
width: isMobile
? NAV_DRAWER_WIDTHS.menu.mobile.collapsed
: NAV_DRAWER_WIDTHS.menu.desktop.expanded,
width: isMobile ? 0 : NAVIGATION_DRAWER_CONSTRAINTS.default,
opacity: isMobile ? 0 : 1,
}}
transition={{ duration: ANIMATION.duration.fast }}
@@ -2,7 +2,7 @@ import styled from '@emotion/styled';
import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/NavDrawerWidths';
import { NAVIGATION_DRAWER_CONSTRAINTS } from '@/ui/layout/resizable-panel/constants/NavigationDrawerConstraints';
import { MOBILE_VIEWPORT } from 'twenty-ui/theme';
import { LeftPanelSkeletonLoader } from '~/loading/components/LeftPanelSkeletonLoader';
import { RightPanelSkeletonLoader } from '~/loading/components/RightPanelSkeletonLoader';
@@ -14,7 +14,7 @@ const StyledContainer = styled.div`
flex-direction: row;
gap: 12px;
height: 100dvh;
min-width: ${NAV_DRAWER_WIDTHS.menu.desktop.expanded}px;
min-width: ${NAVIGATION_DRAWER_CONSTRAINTS.default}px;
width: 100%;
padding: 12px 8px 12px 8px;
overflow: hidden;
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
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

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