Compare commits

..
Author SHA1 Message Date
sonarly-bot 52f09c76b6 fix(server): stop diffing removed rolePermissionFlag.flag
https://sonarly.com/issue/39742?type=bug

App install/sync fails for apps declaring role permission flags because migration update actions include `rolePermissionFlag.update.flag`, which TypeORM rejects after the 2.7 cutover removed that property from active entity metadata.

Fix: Implemented a targeted server-side fix in the migration diff layer so post-2.7 installs/syncs stop generating invalid `rolePermissionFlag.update.flag` payloads.

What changed:
1) `packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts`
- In the `rolePermissionFlag` property configuration, changed:
  - `flag.toCompare` from `true` to `false`.

Why this fixes the bug:
- The update payload is built from comparable properties. Marking `flag` as non-comparable prevents the diff engine from producing `update.flag`, which is the field TypeORM rejects after the 2.7 cutover removed/hidden that column from active metadata.

2) `packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec.ts.snap`
- Updated snapshot so `rolePermissionFlag.propertiesToCompare` no longer includes `"flag"`.

I also checked recent history for an existing exact fix (`git log --all --since='30 days ago'` + candidate commit inspection). There is related work (`d13cc7c3497`), but this branch still has `rolePermissionFlag.flag` set as comparable, so the exact root-cause fix was not present here.

Authored by Sonarly by autonomous analysis (run 45294).
2026-05-22 11:50:06 +00:00
c2a48a1cc2 i18n - docs translations (#20839)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-22 13:22:12 +02:00
37a1d3980f i18n - translations (#20838)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-22 12:26:58 +02:00
nitinandGitHub 05f58b2dba Fix onboarding modals spacings (#20682)
closes
https://discord.com/channels/1130383047699738754/1496415056085389422
2026-05-22 10:06:18 +00:00
Abdullah.andGitHub 4e0b69eb8d [Website] force-static releases page & move workspace bullet to pro self-host (#20834)
Add `export const dynamic = 'force-static'` to the releases page to
prevent runtime re-renders on Cloudflare Workers where fs is
unavailable, which caused stale "Releases were not found" errors after
ISR cache eviction.

Also moves the "Up to 5 workspaces" bullet from the Organization plan to
the Pro plan on the self-hosting pricing view.
2026-05-22 09:58:22 +00:00
99799073e9 i18n - translations (#20837)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-22 12:04:44 +02:00
Félix MalfaitandGitHub 084fa8eaba fix(server): auto-index target<X>Id join columns on polymorphic standard objects (#20820)
Closes #20726

## The bug

`timelineActivity` (and the three other polymorphic standard objects —
`attachment`, `noteTarget`, `taskTarget`) store relations as N nullable
`target<X>Id` columns, one per related object. Each one is a join key
queried as `WHERE target<X>Id IN (...) AND deletedAt IS NULL`.

For **built-in** related objects (Person, Company, Opportunity, …), each
`target<X>Id` column gets a BTREE index, declared statically in
`compute-{timelineActivity,attachment,noteTarget,taskTarget}-standard-flat-index-metadata.util.ts`.

For **custom** related objects, the same `target<CustomObject>Id` column
was added — **without an index**. On a `timelineActivity` table at
issue-reporter scale (~21.9M rows, 7.1 GB), this turned record loads
into 20–40s sequential scans and produced `QueryFailedError: Query read
timeout` for end users.

## Diagnosis

The morph/relation field generator
(`generateMorphOrRelationFlatFieldMetadataPair`) already creates a BTREE
index for the field that owns the join column and returns it alongside
the field metadata pair. The two user-driven entry points
(`fromRelationCreateFieldInput…`, `fromMorphRelationCreateFieldInput…`)
correctly destructure and propagate that index.

But the **custom-object creation path** —
`buildDefaultRelationFlatFieldMetadatasForCustomObject`, called when a
user creates a new custom object — destructured only `{
flatFieldMetadatas }` and threw away `indexMetadatas`. So every
`target<CustomObject>Id` column added to the four polymorphic standard
objects has been shipping unindexed since custom morph relations went
in.

## The fix

Three commits.

### 1. `fix(server): index target<CustomObject>Id columns on standard
polymorphic objects`

13 lines across 2 files.

-
`build-default-relation-flat-field-metadatas-for-custom-object.util.ts`
— also destructure `indexMetadatas` from the pair generator and
accumulate them into the returned record (new field
`standardTargetFlatIndexMetadatas`).
-
`from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util.ts`
— append the accumulated indexes to `flatIndexMetadataToCreate`. The
migration pipeline at `object-metadata.service.ts:559–562` already
passes `flatIndexMetadataToCreate` to the migration runner, so no
further wiring is needed.

From now on, creating a custom object also creates the four BTREE
indexes — one per polymorphic standard object's new
`target<CustomObject>Id` column — atomically with the rest of the
migration.

### 2. `feat(server): backfill workspace command for relation join
column indexes`

For existing workspaces whose custom objects were created before the
forward-fix.

`upgrade:2-8:backfill-relation-join-column-indexes` is a
`@RegisteredWorkspaceCommand('2.8.0', 1798100000000)` matching the
pattern from
`2-7-workspace-command-…-drop-connected-account-standard-object.command.ts`.

Per workspace:
1. Load `flatObjectMetadataMaps`, `flatFieldMetadataMaps`,
`flatIndexMaps` from the workspace cache.
2. Resolve the four polymorphic standard object IDs by `nameSingular`
against `DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS`.
3. Collect every field ID that's already covered by any existing index.
4. Filter `flatFieldMetadataMaps` to MORPH_RELATION fields on those four
objects whose `settings.relationType === MANY_TO_ONE` (i.e. owns a join
column) and whose ID isn't in the indexed set.
5. Generate a BTREE `UniversalFlatIndexMetadata` for each via
`generateIndexForFlatFieldMetadata` (same helper the forward-fix uses).
6. Create the indexes in the workspace schema with **CONCURRENTLY** (see
commit 3).
7. Submit the metadata through
`WorkspaceMigrationValidateBuildAndRunService` so it lands in
`indexMetadata` and the cache — same pipeline as a normal metadata
change. The pipeline's own `CREATE INDEX IF NOT EXISTS` no-ops because
the index already exists.

Properties:
- **Idempotent.** Re-running is a no-op once indexes exist.
- **Scoped.** Only the four polymorphic standard objects, only their
MANY_TO_ONE morph relation fields, only those with no covering index.
- **Same code path as the forward-fix.** The backfill produces exactly
the indexes the forward-fix would have created at custom-object creation
time.
- **`--dry-run` supported** via the base
`ActiveOrSuspendedWorkspaceCommandRunner`.

### 3. `feat(server): create index CONCURRENTLY in relation join column
backfill`

Adds an opt-in `concurrently` flag to
`WorkspaceSchemaIndexManagerService.createIndex` (threaded through
`createIndexInWorkspaceSchema`). When `true`, emits `CREATE INDEX
CONCURRENTLY IF NOT EXISTS …`. Defaults to `false` — every existing
caller keeps the current transactional `CREATE INDEX` behavior.

The backfill command opts in. It creates a QueryRunner **without**
`startTransaction()`, issues the CONCURRENTLY indexes one-by-one (each
waits for the previous to finish), then submits the metadata through the
normal migration pipeline whose own `CREATE INDEX IF NOT EXISTS` is now
a no-op.

Why not flip the default for the helper:
- `CREATE INDEX CONCURRENTLY` cannot run inside a transaction — Postgres
errors out. The migration pipeline calls `createIndex` from inside a
transactional schema migration.
- CONCURRENTLY doesn't roll back with the transaction. If the
surrounding migration fails, the index remains and you end up with
metadata/schema drift.
- Failed CONCURRENTLY builds leave an INVALID index behind that needs
manual `DROP`.
- UNIQUE indexes have different failure semantics under CONCURRENTLY
(deferred, not immediate).

So CONCURRENTLY is opt-in, used only where it's the right tool (post-hoc
backfills on populated tables).

## Decisions / tradeoffs

- **Single-column BTREE vs partial `WHERE deletedAt IS NULL` vs
composite.** Twenty's queries always include `deletedAt IS NULL`. A
partial index would be slightly better than a plain BTREE (smaller, no
wasted seeks on soft-deleted rows). This PR ships single-column to match
the existing built-in target index pattern, which already covers >95% of
the available speedup (the 20s→4ms drop the reporter saw comes from
having any index — composite/partial is a second-order effect).
Switching all relation indexes to partial is a separate, broader change.
- **CONCURRENTLY operator caveat.** If a CONCURRENTLY build is
interrupted (kill, connection drop, OOM), Postgres leaves the index as
INVALID. We deliberately don't probe `pg_index` for invalid leftovers on
every create — catalog-table queries can be slow at multi-tenant scale
and the failure mode is rare. Recovery is manual: `DROP INDEX <name>`
and re-run the backfill.
- **Forward-fix is not gated** behind a feature flag. The change is
metadata-pipeline-internal; before, custom-object creation silently
produced a degraded state. After, it produces the correct state. No new
public API, no behavioural change for end users besides the indexes
existing.

## Risk

- Forward-fix: changes only the metadata produced during custom-object
creation. New objects get four extra `FlatIndexMetadata` rows and four
extra `CREATE INDEX` statements during their creation migration. Tables
are empty at that point so the index builds in microseconds.
- Helper change: API-compatible, default behavior unchanged. The new
`concurrently` parameter is optional.
- Backfill: read-only state probe → CONCURRENTLY index creation (no
write blocking) → metadata insert via the normal migration pipeline.
Idempotent. Reverting is `DROP INDEX`.

## Test plan

- [ ] Verify forward-fix: create a custom object, confirm four new BTREE
indexes appear on `timelineActivity`, `attachment`, `noteTarget`,
`taskTarget` for the new `target<CustomObject>Id` columns, and that
`flatIndexMaps` has matching entries.
- [ ] Verify backfill on a workspace that had custom objects created
before the fix: run `--dry-run` first, confirm the expected indexes are
listed; then run for real, confirm the indexes appear in pg (and as
`indisvalid = true` in `pg_index`) and in `flatIndexMaps`. Re-run;
confirm no-op.
- [ ] Verify backfill on a clean workspace: should log "no missing
indexes" and exit.
- [ ] Verify CONCURRENTLY behavior under load: run backfill against a
workspace with active writes on `timelineActivity`; confirm
inserts/updates keep working during index build (no `ShareLock` waits in
`pg_stat_activity`).
- [ ] On the affected reporter-scale workspace, confirm `EXPLAIN
ANALYZE` switches from sequential scan to index scan and timeline
activity timeouts go away.
2026-05-22 11:56:33 +02:00
788d120b71 i18n - docs translations (#20833)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-22 11:30:11 +02:00
3128157988 i18n - translations (#20832)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-22 11:19:10 +02:00
nitinandGitHub 068d365731 feat(sdk): error on incompatible view filter operand at sync time (#20763)
view filters with mismatched operand + field type now error at sync --
was silently failing before
2026-05-22 09:01:40 +00:00
9b05e7fff4 chore: sync AI model catalog from models.dev (#20830)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

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

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

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-05-22 09:06:07 +02:00
157 changed files with 3246 additions and 466 deletions
@@ -3392,7 +3392,7 @@ input UpdateViewFilterGroupInput {
input CreateViewFilterInput {
id: UUID
fieldMetadataId: UUID!
operand: ViewFilterOperand = CONTAINS
operand: ViewFilterOperand
value: JSON!
viewFilterGroupId: UUID
positionInViewFilterGroup: Float
@@ -3500,7 +3500,7 @@ input UpsertViewWidgetViewFieldInput {
input UpsertViewWidgetViewFilterInput {
id: UUID
fieldMetadataId: UUID!
operand: ViewFilterOperand = CONTAINS
operand: ViewFilterOperand
value: JSON!
viewFilterGroupId: UUID
positionInViewFilterGroup: Float
@@ -1,7 +1,7 @@
---
title: Views
description: Ship pre-configured saved views — column order, filters, groups — for objects in your app.
icon: "list"
icon: 'list'
---
A **view** is a saved configuration for how records of an object are displayed: which fields appear, their order, whether they're visible, and any filters or groups applied. Use `defineView()` to ship pre-configured views with your app — typically a default index view for each custom object you create.
@@ -38,6 +38,60 @@ export default defineView({
- You can also declare `filters`, `filterGroups`, `groups`, and `fieldGroups` for advanced configurations.
- `position` controls ordering when multiple views exist for the same object.
## Filters
A view can ship with pre-applied filters. Each filter has three coordinates: the **field** being filtered, the **operand** (how to compare), and the **value** (what to compare against). All three must line up — using an operand that doesn't apply to a field type will be rejected at sync time.
```ts
import { ViewFilterOperand } from 'twenty-shared/types';
filters: [
{
universalIdentifier: '...',
fieldMetadataUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
operand: ViewFilterOperand.IS,
value: ['ACTIVE'],
},
],
```
### Supported operands per field type
| Field type | Supported operands |
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `TEXT`, `EMAILS`, `FULL_NAME`, `ADDRESS`, `LINKS`, `PHONES`, `RAW_JSON`, `FILES`, `ACTOR`, `ARRAY` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `ACTOR.source`, `ACTOR.workspaceMemberId` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `SELECT` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `MULTI_SELECT` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RELATION` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `NUMBER` | `IS`, `IS_NOT`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RATING` | `IS`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY`, `CURRENCY.amountMicros` | `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `BOOLEAN` | `IS` |
| `UUID` | `IS` |
| `TS_VECTOR` | `VECTOR_SEARCH` |
> Field types with similar names can use entirely different operands — `SELECT` and `MULTI_SELECT` being a common case.
### Value shape per operand
The `value` field is always a JSON-serializable value, but its expected shape depends on the operand:
| Operand family | Value shape | Example |
| ----------------------------------------------------- | ------------------------------ | ------------------------ |
| `IS`, `IS_NOT` on `SELECT` | array of option keys (strings) | `['ACTIVE', 'PENDING']` |
| `CONTAINS`, `DOES_NOT_CONTAIN` on `MULTI_SELECT` | array of option keys (strings) | `['TAG_A']` |
| `IS`, `IS_NOT` on `RELATION` | array of record IDs (uuids) | `['c5a1...']` |
| `CONTAINS`, `DOES_NOT_CONTAIN` on text-like fields | string | `'acme'` |
| `IS`, `IS_NOT` on `NUMBER` | string (the value) | `'5'` |
| `IS` on `RATING` / `UUID` | string (the value) | `'5'` |
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | string (the bound) | `'10'` |
| `IS`, `IS_BEFORE`, `IS_AFTER` on `DATE` / `DATE_TIME` | ISO 8601 string | `'2025-01-01T00:00:00Z'` |
| `IS_EMPTY`, `IS_NOT_EMPTY` | empty string | `''` |
| `IS` on `BOOLEAN` | `'true'` or `'false'` | `'true'` |
## How views show up in the UI
A view by itself isn't reachable from the sidebar. To make it appear there, pair it with a [navigation menu item](/developers/extend/apps/layout/navigation-menu-items) of type `VIEW` that points at the view's `universalIdentifier`. That's the canonical pattern: every custom object typically ships a default view + a sidebar entry that opens it.
@@ -38,6 +38,60 @@ export default defineView({
* Für erweiterte Konfigurationen können Sie außerdem `filters`, `filterGroups`, `groups` und `fieldGroups` deklarieren.
* `position` steuert die Reihenfolge, wenn mehrere Ansichten für dasselbe Objekt existieren.
## Filter
Eine Ansicht kann mit vorab angewendeten Filtern ausgeliefert werden. Jeder Filter hat drei Koordinaten: das **Feld**, das gefiltert wird, der **Operand** (wie verglichen wird) und der **Wert** (womit verglichen wird). Alle drei müssen übereinstimmen — die Verwendung eines Operanden, der nicht auf einen Feldtyp anwendbar ist, wird bei der Synchronisierung zurückgewiesen.
```ts
import { ViewFilterOperand } from 'twenty-shared/types';
filters: [
{
universalIdentifier: '...',
fieldMetadataUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
operand: ViewFilterOperand.IS,
value: ['ACTIVE'],
},
],
```
### Unterstützte Operanden pro Feldtyp
| Feldtyp | Unterstützte Operanden |
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `TEXT`, `EMAILS`, `FULL_NAME`, `ADDRESS`, `LINKS`, `PHONES`, `RAW_JSON`, `FILES`, `ACTOR`, `ARRAY` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `ACTOR.source`, `ACTOR.workspaceMemberId` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `SELECT` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `MULTI_SELECT` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RELATION` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `NUMBER` | `IS`, `IS_NOT`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RATING` | `IS`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY`, `CURRENCY.amountMicros` | `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `BOOLEAN` | `IS` |
| `UUID` | `IS` |
| `TS_VECTOR` | `VECTOR_SEARCH` |
> Feldtypen mit ähnlichen Namen können völlig unterschiedliche Operanden verwenden `SELECT` und `MULTI_SELECT` sind ein häufiges Beispiel.
### Wertstruktur je Operand
Das Feld `value` ist immer ein JSON-serialisierbarer Wert, aber die erwartete Struktur hängt vom Operanden ab:
| Operandenfamilie | Wertstruktur | Beispiel |
| -------------------------------------------------------- | ------------------------------------- | ------------------------ |
| `IS`, `IS_NOT` bei `SELECT` | Array von Optionsschlüsseln (Strings) | `['ACTIVE', 'PENDING']` |
| `CONTAINS`, `DOES_NOT_CONTAIN` bei `MULTI_SELECT` | Array von Optionsschlüsseln (Strings) | `['TAG_A']` |
| `IS`, `IS_NOT` bei `RELATION` | Array von Datensatz-IDs (UUIDs) | `['c5a1...']` |
| `CONTAINS`, `DOES_NOT_CONTAIN` bei textähnlichen Feldern | Zeichenkette | `'acme'` |
| `IS`, `IS_NOT` bei `NUMBER` | String (der Wert) | `'5'` |
| `IS` bei `RATING` / `UUID` | String (der Wert) | `'5'` |
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | String (der Grenzwert) | `'10'` |
| `IS`, `IS_BEFORE`, `IS_AFTER` bei `DATE` / `DATE_TIME` | ISO-8601-String | `'2025-01-01T00:00:00Z'` |
| `IS_EMPTY`, `IS_NOT_EMPTY` | leerer String | `''` |
| `IS` bei `BOOLEAN` | `'true'` oder `'false'` | `'true'` |
## Wie Ansichten in der UI angezeigt werden
Eine Ansicht für sich ist aus der Seitenleiste nicht erreichbar. Damit sie dort erscheint, verknüpfen Sie sie mit einem [Navigationsmenüeintrag](/l/de/developers/extend/apps/layout/navigation-menu-items) des Typs `VIEW`, der auf den `universalIdentifier` der Ansicht zeigt. Das ist das kanonische Muster: Jedes benutzerdefinierte Objekt liefert typischerweise eine Standardansicht plus einen Eintrag in der Seitenleiste, der sie öffnet.
@@ -38,6 +38,60 @@ export default defineView({
* Você também pode declarar `filters`, `filterGroups`, `groups` e `fieldGroups` para configurações avançadas.
* `position` controla a ordenação quando existem várias visualizações para o mesmo objeto.
## Filtros
Uma visualização pode vir com filtros pré-aplicados. Cada filtro tem três coordenadas: o **campo** a ser filtrado, o **operador** (como comparar) e o **valor** (com o que comparar). As três precisam estar alinhadas — usar um operador que não se aplica a um tipo de campo será rejeitado no momento da sincronização.
```ts
import { ViewFilterOperand } from 'twenty-shared/types';
filters: [
{
universalIdentifier: '...',
fieldMetadataUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
operand: ViewFilterOperand.IS,
value: ['ACTIVE'],
},
],
```
### Operadores compatíveis por tipo de campo
| Tipo de campo | Operandos suportados |
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `TEXT`, `EMAILS`, `FULL_NAME`, `ADDRESS`, `LINKS`, `PHONES`, `RAW_JSON`, `FILES`, `ACTOR`, `ARRAY` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `ACTOR.source`, `ACTOR.workspaceMemberId` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `SELECT` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `MULTI_SELECT` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RELATION` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `NUMBER` | `IS`, `IS_NOT`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RATING` | `IS`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY`, `CURRENCY.amountMicros` | `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `BOOLEAN` | `IS` |
| `UUID` | `IS` |
| `TS_VECTOR` | `VECTOR_SEARCH` |
> Tipos de campos com nomes semelhantes podem usar operandos completamente diferentes — `SELECT` e `MULTI_SELECT` sendo um caso comum.
### Formato do valor por operando
O campo `value` é sempre um valor serializável em JSON, mas o seu formato esperado depende do operando:
| Família do operando | Formato do valor | Exemplo |
| ----------------------------------------------------- | ----------------------------------- | ------------------------ |
| `IS`, `IS_NOT` em `SELECT` | array de chaves de opções (strings) | `['ACTIVE', 'PENDING']` |
| `CONTAINS`, `DOES_NOT_CONTAIN` em `MULTI_SELECT` | array de chaves de opções (strings) | `['TAG_A']` |
| `IS`, `IS_NOT` em `RELATION` | array de IDs de registros (uuids) | `['c5a1...']` |
| `CONTAINS`, `DOES_NOT_CONTAIN` em campos de texto | string | `'acme'` |
| `IS`, `IS_NOT` em `NUMBER` | string (o valor) | `'5'` |
| `IS` em `RATING` / `UUID` | string (o valor) | `'5'` |
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | string (o limite) | `'10'` |
| `IS`, `IS_BEFORE`, `IS_AFTER` em `DATE` / `DATE_TIME` | string ISO 8601 | `'2025-01-01T00:00:00Z'` |
| `IS_EMPTY`, `IS_NOT_EMPTY` | string vazia | `''` |
| `IS` em `BOOLEAN` | `'true'` ou `'false'` | `'true'` |
## Como as visualizações aparecem na UI
Uma visualização por si só não é acessível a partir da barra lateral. Para fazê-la aparecer lá, associe-a a um [item de menu de navegação](/l/pt/developers/extend/apps/layout/navigation-menu-items) do tipo `VIEW` que aponte para o `universalIdentifier` da visualização. Esse é o padrão canônico: cada objeto personalizado normalmente inclui uma visualização padrão + uma entrada na barra lateral que a abre.
@@ -38,6 +38,60 @@ export default defineView({
* Puteți declara, de asemenea, `filters`, `filterGroups`, `groups` și `fieldGroups` pentru configurații avansate.
* `position` controlează ordonarea atunci când există mai multe vizualizări pentru același obiect.
## Filtre
O vizualizare poate include filtre aplicate în prealabil. Fiecare filtru are trei coordonate: **câmpul** care este filtrat, **operandul** (cum se compară) și **valoarea** (față de ce se compară). Toate cele trei trebuie să se potrivească — folosirea unui operand care nu se aplică unui tip de câmp va fi respinsă în timpul sincronizării.
```ts
import { ViewFilterOperand } from 'twenty-shared/types';
filters: [
{
universalIdentifier: '...',
fieldMetadataUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
operand: ViewFilterOperand.IS,
value: ['ACTIVE'],
},
],
```
### Operanzi acceptați pentru fiecare tip de câmp
| Tipul câmpului | Operanzi acceptați |
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `TEXT`, `EMAILS`, `FULL_NAME`, `ADDRESS`, `LINKS`, `PHONES`, `RAW_JSON`, `FILES`, `ACTOR`, `ARRAY` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `ACTOR.source`, `ACTOR.workspaceMemberId` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `SELECT` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `MULTI_SELECT` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RELATION` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `NUMBER` | `IS`, `IS_NOT`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RATING` | `IS`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY`, `CURRENCY.amountMicros` | `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `BOOLEAN` | `IS` |
| `UUID` | `IS` |
| `TS_VECTOR` | `VECTOR_SEARCH` |
> Tipurile de câmp cu nume similare pot folosi operanzi complet diferiți — `SELECT` și `MULTI_SELECT` fiind un caz comun.
### Formă de valoare per operand
Câmpul `value` este întotdeauna o valoare serializabilă JSON, dar forma sa așteptată depinde de operand:
| Familie de operanzi | Formă de valoare | Exemplu |
| ----------------------------------------------------- | ---------------------------------------------- | ------------------------ |
| `IS`, `IS_NOT` pe `SELECT` | array de chei de opțiune (șiruri de caractere) | `['ACTIVE', 'PENDING']` |
| `CONTAINS`, `DOES_NOT_CONTAIN` pe `MULTI_SELECT` | array de chei de opțiune (șiruri de caractere) | `['TAG_A']` |
| `IS`, `IS_NOT` pe `RELATION` | array de ID-uri de înregistrare (uuid-uri) | `['c5a1...']` |
| `CONTAINS`, `DOES_NOT_CONTAIN` pe câmpuri de tip text | șir | `'acme'` |
| `IS`, `IS_NOT` pe `NUMBER` | șir de caractere (valoarea) | `'5'` |
| `IS` pe `RATING` / `UUID` | șir de caractere (valoarea) | `'5'` |
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | șir de caractere (limita) | `'10'` |
| `IS`, `IS_BEFORE`, `IS_AFTER` pe `DATE` / `DATE_TIME` | șir în format ISO 8601 | `'2025-01-01T00:00:00Z'` |
| `IS_EMPTY`, `IS_NOT_EMPTY` | șir gol | `''` |
| `IS` pe `BOOLEAN` | `'true'` sau `'false'` | `'true'` |
## Cum apar vizualizările în interfața utilizatorului
O vizualizare, de una singură, nu este accesibilă din bara laterală. Pentru a o face să apară acolo, asociați-o cu un [element de meniu de navigare](/l/ro/developers/extend/apps/layout/navigation-menu-items) de tip `VIEW` care indică către `universalIdentifier` al vizualizării. Acesta este modelul canonic: fiecare obiect personalizat livrează, de obicei, o vizualizare implicită + o intrare în bara laterală care o deschide.
@@ -38,6 +38,60 @@ export default defineView({
* Также вы можете определить `filters`, `filterGroups`, `groups` и `fieldGroups` для более продвинутых конфигураций.
* `position` управляет порядком, когда для одного и того же объекта существует несколько представлений.
## Фильтры
Представление может поставляться с заранее примененными фильтрами. У каждого фильтра есть три координаты: **поле**, по которому выполняется фильтрация, **операнд** (как сравнивать) и **значение** (с чем сравнивать). Все три должны совпадать — использование операнда, который не подходит для типа поля, будет отклонено во время синхронизации.
```ts
import { ViewFilterOperand } from 'twenty-shared/types';
filters: [
{
universalIdentifier: '...',
fieldMetadataUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
operand: ViewFilterOperand.IS,
value: ['ACTIVE'],
},
],
```
### Поддерживаемые операнды по типу поля
| Тип поля | Поддерживаемые операнды |
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `TEXT`, `EMAILS`, `FULL_NAME`, `ADDRESS`, `LINKS`, `PHONES`, `RAW_JSON`, `FILES`, `ACTOR`, `ARRAY` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `ACTOR.source`, `ACTOR.workspaceMemberId` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `SELECT` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `MULTI_SELECT` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RELATION` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `NUMBER` | `IS`, `IS_NOT`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RATING` | `IS`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY`, `CURRENCY.amountMicros` | `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `BOOLEAN` | `IS` |
| `UUID` | `IS` |
| `TS_VECTOR` | `VECTOR_SEARCH` |
> Типы полей с похожими названиями могут использовать совершенно разные операнды — типичный пример: `SELECT` и `MULTI_SELECT`.
### Форма значения для каждого операнда
Поле `value` всегда содержит значение, сериализуемое в JSON, но ожидаемая форма зависит от операнда:
| Семейство операндов | Форма значения | Пример |
| ------------------------------------------------------ | ------------------------------------- | ------------------------ |
| `IS`, `IS_NOT` для `SELECT` | массив ключей опций (строки) | `['ACTIVE', 'PENDING']` |
| `CONTAINS`, `DOES_NOT_CONTAIN` для `MULTI_SELECT` | массив ключей опций (строки) | `['TAG_A']` |
| `IS`, `IS_NOT` для `RELATION` | массив идентификаторов записей (uuid) | `['c5a1...']` |
| `CONTAINS`, `DOES_NOT_CONTAIN` для текстовых полей | строка | `'acme'` |
| `IS`, `IS_NOT` для `NUMBER` | строка (значение) | `'5'` |
| `IS` для `RATING` / `UUID` | строка (значение) | `'5'` |
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | строка (граница) | `'10'` |
| `IS`, `IS_BEFORE`, `IS_AFTER` для `DATE` / `DATE_TIME` | строка в формате ISO 8601 | `'2025-01-01T00:00:00Z'` |
| `IS_EMPTY`, `IS_NOT_EMPTY` | пустая строка | `''` |
| `IS` для `BOOLEAN` | `'true'` или `'false'` | `'true'` |
## Как представления отображаются в интерфейсе
Само по себе представление недоступно из боковой панели. Чтобы оно появилось там, свяжите его с [пунктом навигационного меню](/l/ru/developers/extend/apps/layout/navigation-menu-items) типа `VIEW`, который указывает на `universalIdentifier` представления. Это канонический шаблон: каждый пользовательский объект обычно поставляется с представлением по умолчанию и пунктом боковой панели, который его открывает.
@@ -38,6 +38,60 @@ export default defineView({
* Daha gelişmiş yapılandırmalar için `filters`, `filterGroups`, `groups` ve `fieldGroups` da tanımlayabilirsiniz.
* `position`, aynı nesne için birden fazla görünüm olduğunda sıralamayı kontrol eder.
## Filtreler
Bir görünüm, önceden uygulanmış filtrelerle gelebilir. Her filtrenin üç koordinatı vardır: filtrelenen **alan**, **işleç** (nasıl karşılaştırılacağı) ve **değer** (neyle karşılaştırılacağı). Üçünün de hizalı olması gerekir — bir alan türüne uygulanmayan bir işlecin kullanılması, senkronizasyon sırasında reddedilir.
```ts
import { ViewFilterOperand } from 'twenty-shared/types';
filters: [
{
universalIdentifier: '...',
fieldMetadataUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
operand: ViewFilterOperand.IS,
value: ['ACTIVE'],
},
],
```
### Alan türüne göre desteklenen işleçler
| Alan tipi | Desteklenen işleçler |
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `TEXT`, `EMAILS`, `FULL_NAME`, `ADDRESS`, `LINKS`, `PHONES`, `RAW_JSON`, `FILES`, `ACTOR`, `ARRAY` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `ACTOR.source`, `ACTOR.workspaceMemberId` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `SELECT` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `MULTI_SELECT` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RELATION` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `NUMBER` | `IS`, `IS_NOT`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RATING` | `IS`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY`, `CURRENCY.amountMicros` | `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `BOOLEAN` | `IS` |
| `UUID` | `IS` |
| `TS_VECTOR` | `VECTOR_SEARCH` |
> Benzer adlara sahip alan türleri tamamen farklı işleçler kullanabilir — buna `SELECT` ve `MULTI_SELECT` yaygın bir örnektir.
### Her işleç için değer biçimi
`value` alanı her zaman JSON olarak serileştirilebilir bir değerdir, ancak beklenen biçim işlece bağlıdır:
| İşleç ailesi | Değer biçimi | Örnek |
| ------------------------------------------------------------- | ------------------------------------- | ------------------------ |
| `SELECT` üzerinde `IS`, `IS_NOT` | seçenek anahtarları dizisi (metinler) | `['ACTIVE', 'PENDING']` |
| `MULTI_SELECT` üzerinde `CONTAINS`, `DOES_NOT_CONTAIN` | seçenek anahtarları dizisi (metinler) | `['TAG_A']` |
| `RELATION` üzerinde `IS`, `IS_NOT` | kayıt kimlikleri dizisi (uuid'ler) | `['c5a1...']` |
| metin benzeri alanlar üzerinde `CONTAINS`, `DOES_NOT_CONTAIN` | metin | `'acme'` |
| `NUMBER` üzerinde `IS`, `IS_NOT` | metin (değer) | `'5'` |
| `RATING` / `UUID` üzerinde `IS` | metin (değer) | `'5'` |
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | metin (sınır) | `'10'` |
| `DATE` / `DATE_TIME` üzerinde `IS`, `IS_BEFORE`, `IS_AFTER` | ISO 8601 dizesi | `'2025-01-01T00:00:00Z'` |
| `IS_EMPTY`, `IS_NOT_EMPTY` | boş dize | `''` |
| `BOOLEAN` üzerinde `IS` | `'true'` veya `'false'` | `'true'` |
## Görünümlerin kullanıcı arayüzünde görüntülenme şekli
Tek başına bir görünüm, kenar çubuğundan erişilebilir değildir. Orada görünmesini sağlamak için, türü `VIEW` olan ve görünümün `universalIdentifier` değerini işaret eden bir [navigasyon menüsü öğesi](/l/tr/developers/extend/apps/layout/navigation-menu-items) ile eşleştirin. Bu, standart örüntüdür: her özel nesne genellikle varsayılan bir görünüm ve bunu açan bir kenar çubuğu girdisiyle birlikte sunulur.
@@ -38,6 +38,60 @@ export default defineView({
* 你还可以声明 `filters`、`filterGroups`、`groups` 和 `fieldGroups` 以进行更高级的配置。
* 当同一对象存在多个视图时,`position` 控制其排序。
## 过滤器
视图可以附带预先应用的过滤器。 每个过滤器有三个坐标:被筛选的**字段**、**运算符**(如何比较)以及**值**(与之比较的内容)。 这三者必须全部对齐——在同步时,使用不适用于字段类型的运算符将会被拒绝。
```ts
import { ViewFilterOperand } from 'twenty-shared/types';
filters: [
{
universalIdentifier: '...',
fieldMetadataUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
operand: ViewFilterOperand.IS,
value: ['ACTIVE'],
},
],
```
### 各字段类型支持的运算符
| 字段类型 | 受支持的运算符 |
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `TEXT`, `EMAILS`, `FULL_NAME`, `ADDRESS`, `LINKS`, `PHONES`, `RAW_JSON`, `FILES`, `ACTOR`, `ARRAY` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `ACTOR.source`, `ACTOR.workspaceMemberId` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `SELECT` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `MULTI_SELECT` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RELATION` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `NUMBER` | `IS`, `IS_NOT`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RATING` | `IS`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY`, `CURRENCY.amountMicros` | `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `BOOLEAN` | `IS` |
| `UUID` | `IS` |
| `TS_VECTOR` | `VECTOR_SEARCH` |
> 名称相似的字段类型可以使用完全不同的运算符 —— `SELECT` 和 `MULTI_SELECT` 就是常见情况。
### 每个运算符对应的值结构
`value` 字段始终是可序列化为 JSON 的值,但其期望的结构取决于所使用的运算符:
| 运算符类别 | 值结构 | 示例 |
| ----------------------------------------------------- | -------------------- | ------------------------ |
| `SELECT` 上的 `IS`, `IS_NOT` | 选项键(字符串)数组 | `['ACTIVE', 'PENDING']` |
| `MULTI_SELECT` 上的 `CONTAINS`, `DOES_NOT_CONTAIN` | 选项键(字符串)数组 | `['TAG_A']` |
| `RELATION` 上的 `IS`, `IS_NOT` | 记录 IDuuid)数组 | `['c5a1...']` |
| 文本类字段上的 `CONTAINS`, `DOES_NOT_CONTAIN` | 字符串 | `'acme'` |
| `NUMBER` 上的 `IS`, `IS_NOT` | 字符串(该值) | `'5'` |
| `RATING` / `UUID` 上的 `IS` | 字符串(该值) | `'5'` |
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | 字符串(边界值) | `'10'` |
| `DATE` / `DATE_TIME` 上的 `IS`, `IS_BEFORE`, `IS_AFTER` | ISO 8601 字符串 | `'2025-01-01T00:00:00Z'` |
| `IS_EMPTY`, `IS_NOT_EMPTY` | 空字符串 | `''` |
| `BOOLEAN` 上的 `IS` | `'true'` 或 `'false'` | `'true'` |
## 视图在 UI 中如何显示
单独一个视图无法从侧边栏访问。 要让它显示在侧边栏中,请将其与类型为 `VIEW`、指向该视图 `universalIdentifier` 的[导航菜单项](/l/zh/developers/extend/apps/layout/navigation-menu-items)配对。 这是规范用法:每个自定义对象通常都会提供一个默认视图,以及一个在侧边栏中打开该视图的条目。
+1 -1
View File
@@ -9461,7 +9461,7 @@ msgstr "Teken 'n nuwe transaksie aan (maatskappy, bedrag, stadium, verwagte slui
#. js-lingui-id: FgAxTj
#: src/pages/onboarding/internal/ChooseYourPlanContent.tsx
#: src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx
#: src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
#: src/modules/auth/sign-in-up/components/WorkspaceSelectionFooter.tsx
msgid "Log out"
msgstr "Teken uit"
+1 -1
View File
@@ -9461,7 +9461,7 @@ msgstr "سجّل صفقة جديدة (الشركة، المبلغ، المرحل
#. js-lingui-id: FgAxTj
#: src/pages/onboarding/internal/ChooseYourPlanContent.tsx
#: src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx
#: src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
#: src/modules/auth/sign-in-up/components/WorkspaceSelectionFooter.tsx
msgid "Log out"
msgstr "تسجيل الخروج"
+1 -1
View File
@@ -9461,7 +9461,7 @@ msgstr "Registra una nova oportunitat (empresa, import, etapa, tancament previst
#. js-lingui-id: FgAxTj
#: src/pages/onboarding/internal/ChooseYourPlanContent.tsx
#: src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx
#: src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
#: src/modules/auth/sign-in-up/components/WorkspaceSelectionFooter.tsx
msgid "Log out"
msgstr "Tancar sessió"
+1 -1
View File
@@ -9461,7 +9461,7 @@ msgstr "Zaznamenejte nový obchod (společnost, částka, fáze, očekávané uz
#. js-lingui-id: FgAxTj
#: src/pages/onboarding/internal/ChooseYourPlanContent.tsx
#: src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx
#: src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
#: src/modules/auth/sign-in-up/components/WorkspaceSelectionFooter.tsx
msgid "Log out"
msgstr "Odhlásit se"
+1 -1
View File
@@ -9461,7 +9461,7 @@ msgstr "Registrer en ny aftale (virksomhed, beløb, fase, forventet lukkedato).
#. js-lingui-id: FgAxTj
#: src/pages/onboarding/internal/ChooseYourPlanContent.tsx
#: src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx
#: src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
#: src/modules/auth/sign-in-up/components/WorkspaceSelectionFooter.tsx
msgid "Log out"
msgstr "Log ud"
+1 -1
View File
@@ -9461,7 +9461,7 @@ msgstr "Einen neuen Deal erfassen (Unternehmen, Betrag, Phase, voraussichtlicher
#. js-lingui-id: FgAxTj
#: src/pages/onboarding/internal/ChooseYourPlanContent.tsx
#: src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx
#: src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
#: src/modules/auth/sign-in-up/components/WorkspaceSelectionFooter.tsx
msgid "Log out"
msgstr "Abmelden"
+1 -1
View File
@@ -9461,7 +9461,7 @@ msgstr "Καταχωρίστε μια νέα συμφωνία (εταιρεία,
#. js-lingui-id: FgAxTj
#: src/pages/onboarding/internal/ChooseYourPlanContent.tsx
#: src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx
#: src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
#: src/modules/auth/sign-in-up/components/WorkspaceSelectionFooter.tsx
msgid "Log out"
msgstr "Αποσύνδεση"
+1 -1
View File
@@ -9479,7 +9479,7 @@ msgstr "Log a new deal (company, amount, stage, expected close). Details: "
#. js-lingui-id: FgAxTj
#: src/pages/onboarding/internal/ChooseYourPlanContent.tsx
#: src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx
#: src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
#: src/modules/auth/sign-in-up/components/WorkspaceSelectionFooter.tsx
msgid "Log out"
msgstr "Log out"
+1 -1
View File
@@ -9461,7 +9461,7 @@ msgstr "Registra una nueva oportunidad (empresa, importe, etapa, fecha de cierre
#. js-lingui-id: FgAxTj
#: src/pages/onboarding/internal/ChooseYourPlanContent.tsx
#: src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx
#: src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
#: src/modules/auth/sign-in-up/components/WorkspaceSelectionFooter.tsx
msgid "Log out"
msgstr "Cerrar sesión"
+1 -1
View File
@@ -9461,7 +9461,7 @@ msgstr "Kirjaa uusi diili (yritys, summa, vaihe, odotettu päättymispäivä). T
#. js-lingui-id: FgAxTj
#: src/pages/onboarding/internal/ChooseYourPlanContent.tsx
#: src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx
#: src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
#: src/modules/auth/sign-in-up/components/WorkspaceSelectionFooter.tsx
msgid "Log out"
msgstr "Kirjaudu ulos"
+1 -1
View File
@@ -9461,7 +9461,7 @@ msgstr "Enregistrer une nouvelle affaire (entreprise, montant, étape, clôture
#. js-lingui-id: FgAxTj
#: src/pages/onboarding/internal/ChooseYourPlanContent.tsx
#: src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx
#: src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
#: src/modules/auth/sign-in-up/components/WorkspaceSelectionFooter.tsx
msgid "Log out"
msgstr "Déconnexion"
+1 -1
View File
@@ -9461,7 +9461,7 @@ msgstr "רשום עסקה חדשה (חברה, סכום, שלב, תאריך סג
#. js-lingui-id: FgAxTj
#: src/pages/onboarding/internal/ChooseYourPlanContent.tsx
#: src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx
#: src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
#: src/modules/auth/sign-in-up/components/WorkspaceSelectionFooter.tsx
msgid "Log out"
msgstr "התנתק"
+1 -1
View File
@@ -9461,7 +9461,7 @@ msgstr "Rögzítsen egy új ügyletet (vállalat, összeg, szakasz, várható le
#. js-lingui-id: FgAxTj
#: src/pages/onboarding/internal/ChooseYourPlanContent.tsx
#: src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx
#: src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
#: src/modules/auth/sign-in-up/components/WorkspaceSelectionFooter.tsx
msgid "Log out"
msgstr "Kijelentkezés"
+1 -1
View File
@@ -9461,7 +9461,7 @@ msgstr "Registra una nuova trattativa (azienda, importo, fase, chiusura prevista
#. js-lingui-id: FgAxTj
#: src/pages/onboarding/internal/ChooseYourPlanContent.tsx
#: src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx
#: src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
#: src/modules/auth/sign-in-up/components/WorkspaceSelectionFooter.tsx
msgid "Log out"
msgstr "Disconnetti"
+1 -1
View File
@@ -9461,7 +9461,7 @@ msgstr "新しい案件を記録(会社、金額、ステージ、予定クロ
#. js-lingui-id: FgAxTj
#: src/pages/onboarding/internal/ChooseYourPlanContent.tsx
#: src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx
#: src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
#: src/modules/auth/sign-in-up/components/WorkspaceSelectionFooter.tsx
msgid "Log out"
msgstr "ログアウト"
+1 -1
View File
@@ -9461,7 +9461,7 @@ msgstr "새 거래를 기록하세요 (회사, 금액, 단계, 예상 종결일)
#. js-lingui-id: FgAxTj
#: src/pages/onboarding/internal/ChooseYourPlanContent.tsx
#: src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx
#: src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
#: src/modules/auth/sign-in-up/components/WorkspaceSelectionFooter.tsx
msgid "Log out"
msgstr "로그아웃"
+1 -1
View File
@@ -9461,7 +9461,7 @@ msgstr "Registreer een nieuwe deal (bedrijf, bedrag, fase, verwachte sluitingsda
#. js-lingui-id: FgAxTj
#: src/pages/onboarding/internal/ChooseYourPlanContent.tsx
#: src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx
#: src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
#: src/modules/auth/sign-in-up/components/WorkspaceSelectionFooter.tsx
msgid "Log out"
msgstr "Uitloggen"
+1 -1
View File
@@ -9461,7 +9461,7 @@ msgstr "Loggfør en ny avtale (selskap, beløp, fase, forventet avslutning). Det
#. js-lingui-id: FgAxTj
#: src/pages/onboarding/internal/ChooseYourPlanContent.tsx
#: src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx
#: src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
#: src/modules/auth/sign-in-up/components/WorkspaceSelectionFooter.tsx
msgid "Log out"
msgstr "Logg ut"
+1 -1
View File
@@ -9461,7 +9461,7 @@ msgstr "Zarejestruj nową transakcję (firma, kwota, etap, oczekiwana data zamkn
#. js-lingui-id: FgAxTj
#: src/pages/onboarding/internal/ChooseYourPlanContent.tsx
#: src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx
#: src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
#: src/modules/auth/sign-in-up/components/WorkspaceSelectionFooter.tsx
msgid "Log out"
msgstr "Wyloguj się"
@@ -9456,7 +9456,7 @@ msgstr ""
#. js-lingui-id: FgAxTj
#: src/pages/onboarding/internal/ChooseYourPlanContent.tsx
#: src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx
#: src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
#: src/modules/auth/sign-in-up/components/WorkspaceSelectionFooter.tsx
msgid "Log out"
msgstr ""
+1 -1
View File
@@ -9461,7 +9461,7 @@ msgstr "Registre um novo negócio (empresa, valor, etapa, fechamento previsto).
#. js-lingui-id: FgAxTj
#: src/pages/onboarding/internal/ChooseYourPlanContent.tsx
#: src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx
#: src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
#: src/modules/auth/sign-in-up/components/WorkspaceSelectionFooter.tsx
msgid "Log out"
msgstr "Sair"
+1 -1
View File
@@ -9461,7 +9461,7 @@ msgstr "Registar um novo negócio (empresa, montante, fase, fecho previsto). Det
#. js-lingui-id: FgAxTj
#: src/pages/onboarding/internal/ChooseYourPlanContent.tsx
#: src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx
#: src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
#: src/modules/auth/sign-in-up/components/WorkspaceSelectionFooter.tsx
msgid "Log out"
msgstr "Terminar sessão"
+1 -1
View File
@@ -9461,7 +9461,7 @@ msgstr "Înregistrați o tranzacție nouă (companie, sumă, etapă, închidere
#. js-lingui-id: FgAxTj
#: src/pages/onboarding/internal/ChooseYourPlanContent.tsx
#: src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx
#: src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
#: src/modules/auth/sign-in-up/components/WorkspaceSelectionFooter.tsx
msgid "Log out"
msgstr "Deconectare"
Binary file not shown.
+1 -1
View File
@@ -9461,7 +9461,7 @@ msgstr "Евидентирајте нови договор (компанија,
#. js-lingui-id: FgAxTj
#: src/pages/onboarding/internal/ChooseYourPlanContent.tsx
#: src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx
#: src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
#: src/modules/auth/sign-in-up/components/WorkspaceSelectionFooter.tsx
msgid "Log out"
msgstr "Одјавите се"
+1 -1
View File
@@ -9461,7 +9461,7 @@ msgstr "Registrera en ny affär (företag, belopp, steg, förväntat avslut). De
#. js-lingui-id: FgAxTj
#: src/pages/onboarding/internal/ChooseYourPlanContent.tsx
#: src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx
#: src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
#: src/modules/auth/sign-in-up/components/WorkspaceSelectionFooter.tsx
msgid "Log out"
msgstr "Logga ut"
+1 -1
View File
@@ -9461,7 +9461,7 @@ msgstr "Yeni bir anlaşma kaydedin (şirket, tutar, aşama, beklenen kapanış).
#. js-lingui-id: FgAxTj
#: src/pages/onboarding/internal/ChooseYourPlanContent.tsx
#: src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx
#: src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
#: src/modules/auth/sign-in-up/components/WorkspaceSelectionFooter.tsx
msgid "Log out"
msgstr "Oturumu kapat"
+1 -1
View File
@@ -9461,7 +9461,7 @@ msgstr "Зареєструйте нову угоду (компанія, сума
#. js-lingui-id: FgAxTj
#: src/pages/onboarding/internal/ChooseYourPlanContent.tsx
#: src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx
#: src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
#: src/modules/auth/sign-in-up/components/WorkspaceSelectionFooter.tsx
msgid "Log out"
msgstr "Вийти"
+1 -1
View File
@@ -9461,7 +9461,7 @@ msgstr "Ghi nhận một thương vụ mới (công ty, số tiền, giai đoạ
#. js-lingui-id: FgAxTj
#: src/pages/onboarding/internal/ChooseYourPlanContent.tsx
#: src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx
#: src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
#: src/modules/auth/sign-in-up/components/WorkspaceSelectionFooter.tsx
msgid "Log out"
msgstr "Đăng xuất"
+1 -1
View File
@@ -9461,7 +9461,7 @@ msgstr "记录一笔新交易 (公司、金额、阶段、预计成交日期)。
#. js-lingui-id: FgAxTj
#: src/pages/onboarding/internal/ChooseYourPlanContent.tsx
#: src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx
#: src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
#: src/modules/auth/sign-in-up/components/WorkspaceSelectionFooter.tsx
msgid "Log out"
msgstr "退出登录"
+1 -1
View File
@@ -9461,7 +9461,7 @@ msgstr "記錄一筆新交易(公司、金額、階段、預計結案)。詳
#. js-lingui-id: FgAxTj
#: src/pages/onboarding/internal/ChooseYourPlanContent.tsx
#: src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx
#: src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
#: src/modules/auth/sign-in-up/components/WorkspaceSelectionFooter.tsx
msgid "Log out"
msgstr "登出"
@@ -0,0 +1,8 @@
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
export const StyledOnboardingContentContainer = styled.div`
margin-bottom: ${themeCssVariables.spacing[8]};
margin-top: ${themeCssVariables.spacing[4]};
min-width: 240px;
`;
@@ -6,7 +6,7 @@ import { Trans, useLingui } from '@lingui/react/macro';
import { FormProvider } from 'react-hook-form';
import { ClickToActionLink, UndecoratedLink } from 'twenty-ui/navigation';
import { useAuth } from '@/auth/hooks/useAuth';
import { StyledOnboardingContentContainer } from '@/auth/components/StyledOnboardingContentContainer';
import { SignInUpWithCredentials } from '@/auth/sign-in-up/components/internal/SignInUpWithCredentials';
import { SignInUpWithGoogle } from '@/auth/sign-in-up/components/internal/SignInUpWithGoogle';
import { SignInUpWithMicrosoft } from '@/auth/sign-in-up/components/internal/SignInUpWithMicrosoft';
@@ -23,7 +23,6 @@ import { isDDLLockedState } from '@/client-config/states/isDDLLockedState';
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { isNonEmptyString } from '@sniptt/guards';
import { motion } from 'framer-motion';
import { useContext } from 'react';
import {
Avatar,
@@ -35,20 +34,12 @@ import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import { type AvailableWorkspace } from '~/generated-metadata/graphql';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
const StyledContentContainer = styled(motion.div)`
margin-bottom: ${themeCssVariables.spacing[8]};
margin-top: ${themeCssVariables.spacing[4]};
min-width: 200px;
`;
const StyledWorkspaceContainer = styled.div`
background-color: ${themeCssVariables.background.secondary};
border: 1px solid ${themeCssVariables.border.color.light};
border-radius: ${themeCssVariables.border.radius.md};
display: flex;
flex-direction: column;
margin-bottom: ${themeCssVariables.spacing[8]};
margin-top: ${themeCssVariables.spacing[4]};
overflow: hidden;
width: 100%;
@@ -123,11 +114,6 @@ const StyledChevronIcon = styled.div`
display: flex;
`;
const StyledActionLinkContainer = styled.div`
display: flex;
justify-content: center;
`;
const StyledForgotPasswordLinkContainer = styled.div`
display: flex;
justify-content: center;
@@ -140,7 +126,6 @@ export const SignInUpGlobalScopeForm = () => {
const isDDLLocked = useAtomStateValue(isDDLLockedState);
const signInUpStep = useAtomStateValue(signInUpStepState);
const { buildWorkspaceUrl } = useBuildWorkspaceUrl();
const { signOut } = useAuth();
const { createWorkspace } = useSignUpInNewWorkspace();
const availableWorkspaces = useAtomStateValue(availableWorkspacesState);
@@ -169,7 +154,7 @@ export const SignInUpGlobalScopeForm = () => {
return (
<>
{signInUpStep === SignInUpStep.WorkspaceSelection && (
<>
<StyledOnboardingContentContainer>
<StyledWorkspaceContainer>
{[
...availableWorkspaces.availableWorkspacesForSignIn,
@@ -224,15 +209,10 @@ export const SignInUpGlobalScopeForm = () => {
</StyledWorkspaceItem>
)}
</StyledWorkspaceContainer>
<StyledActionLinkContainer>
<ClickToActionLink onClick={signOut}>
<Trans>Log out</Trans>
</ClickToActionLink>
</StyledActionLinkContainer>
</>
</StyledOnboardingContentContainer>
)}
{signInUpStep !== SignInUpStep.WorkspaceSelection && (
<StyledContentContainer>
<StyledOnboardingContentContainer>
{authProviders.google && (
<SignInUpWithGoogle
action="list-available-workspaces"
@@ -261,7 +241,7 @@ export const SignInUpGlobalScopeForm = () => {
</ClickToActionLink>
</StyledForgotPasswordLinkContainer>
)}
</StyledContentContainer>
</StyledOnboardingContentContainer>
)}
</>
);
@@ -1,3 +1,4 @@
import { StyledOnboardingContentContainer } from '@/auth/components/StyledOnboardingContentContainer';
import { SignInUpWithCredentials } from '@/auth/sign-in-up/components/internal/SignInUpWithCredentials';
import { SignInUpWithGoogle } from '@/auth/sign-in-up/components/internal/SignInUpWithGoogle';
import { SignInUpWithMicrosoft } from '@/auth/sign-in-up/components/internal/SignInUpWithMicrosoft';
@@ -9,20 +10,12 @@ import { useWorkspaceBypass } from '@/auth/sign-in-up/hooks/useWorkspaceBypass';
import { SignInUpStep } from '@/auth/states/signInUpStepState';
import { workspaceAuthBypassProvidersState } from '@/workspace/states/workspaceAuthBypassProvidersState';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import { styled } from '@linaria/react';
import { Trans } from '@lingui/react/macro';
import { FormProvider } from 'react-hook-form';
import { HorizontalSeparator } from 'twenty-ui/display';
import { ClickToActionLink } from 'twenty-ui/navigation';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
const StyledContentContainer = styled.div`
margin-bottom: ${themeCssVariables.spacing[8]};
margin-top: ${themeCssVariables.spacing[4]};
min-width: 200px;
`;
export const SignInUpWorkspaceScopeForm = () => {
const workspaceAuthProviders = useAtomStateValue(workspaceAuthProvidersState);
const workspaceAuthBypassProviders = useAtomStateValue(
@@ -50,7 +43,7 @@ export const SignInUpWorkspaceScopeForm = () => {
return (
<>
<StyledContentContainer>
<StyledOnboardingContentContainer>
{providers.google && <SignInUpWithGoogle action="join-workspace" />}
{providers.microsoft && (
@@ -71,7 +64,7 @@ export const SignInUpWorkspaceScopeForm = () => {
<SignInUpWithCredentials />
</FormProvider>
)}
</StyledContentContainer>
</StyledOnboardingContentContainer>
{signInUpStep === SignInUpStep.Password && (
<ClickToActionLink
onClick={handleResetPassword(form.getValues('email'))}
@@ -0,0 +1,13 @@
import { useAuth } from '@/auth/hooks/useAuth';
import { Trans } from '@lingui/react/macro';
import { ClickToActionLink } from 'twenty-ui/navigation';
export const WorkspaceSelectionFooter = () => {
const { signOut } = useAuth();
return (
<ClickToActionLink onClick={signOut}>
<Trans>Log out</Trans>
</ClickToActionLink>
);
};
@@ -1,22 +1,16 @@
/* @license Enterprise */
import { StyledOnboardingContentContainer } from '@/auth/components/StyledOnboardingContentContainer';
import { useSSO } from '@/auth/sign-in-up/hooks/useSSO';
import { guessSSOIdentityProviderIconByUrl } from '@/settings/security/utils/guessSSOIdentityProviderIconByUrl';
import { styled } from '@linaria/react';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import React from 'react';
import { isDefined } from 'twenty-shared/utils';
import { HorizontalSeparator } from 'twenty-ui/display';
import { MainButton } from 'twenty-ui/input';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
const StyledContentContainer = styled.div`
margin-bottom: ${themeCssVariables.spacing[8]};
margin-top: ${themeCssVariables.spacing[4]};
`;
export const SignInUpSSOIdentityProviderSelection = () => {
const workspaceAuthProviders = useAtomStateValue(workspaceAuthProvidersState);
@@ -24,7 +18,7 @@ export const SignInUpSSOIdentityProviderSelection = () => {
return (
<>
<StyledContentContainer>
<StyledOnboardingContentContainer>
{isDefined(workspaceAuthProviders?.sso) &&
workspaceAuthProviders?.sso.map((idp) => (
<React.Fragment key={idp.id}>
@@ -37,7 +31,7 @@ export const SignInUpSSOIdentityProviderSelection = () => {
<HorizontalSeparator visible={false} />
</React.Fragment>
))}
</StyledContentContainer>
</StyledOnboardingContentContainer>
</>
);
};
@@ -1,9 +1,9 @@
import { type FILTER_OPERANDS_MAP } from '@/object-record/record-filter/utils/getRecordFilterOperands';
import { type CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName';
import {
type FilterableAndTSVectorFieldType,
type ViewFilterOperand,
} from 'twenty-shared/types';
import { type FILTER_OPERANDS_MAP } from 'twenty-shared/utils';
// RLS-specific: references a workspace member field for dynamic "Me" comparisons
export type RLSDynamicValue = {
@@ -1,230 +1,18 @@
import { isFilterOnActorSourceSubField } from '@/object-record/object-filter-dropdown/utils/isFilterOnActorSourceSubField';
import { isFilterOnActorWorkspaceMemberSubField } from '@/object-record/object-filter-dropdown/utils/isFilterOnActorWorkspaceMemberSubField';
import { type CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName';
import {
FieldMetadataType,
ViewFilterOperand as RecordFilterOperand,
type FilterableAndTSVectorFieldType,
type FilterableFieldType,
type ViewFilterOperand as RecordFilterOperand,
} from 'twenty-shared/types';
import { assertUnreachable, isExpectedSubFieldName } from 'twenty-shared/utils';
export type GetRecordFilterOperandsParams = {
filterType: FilterableAndTSVectorFieldType;
subFieldName?: string | null | undefined;
};
const emptyOperands = [
RecordFilterOperand.IS_EMPTY,
RecordFilterOperand.IS_NOT_EMPTY,
] as const;
const relationOperands = [
RecordFilterOperand.IS,
RecordFilterOperand.IS_NOT,
] as const;
type FilterOperandMap = {
[K in FilterableAndTSVectorFieldType]: readonly RecordFilterOperand[];
};
// TODO: we would need to refactor the typing of SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS first
// with types like FieldCurrencyValue being derived from a central constant value and not being created like that
// in order to narrow down the possible subfield names for each field type
type CompositeFieldFilterOperandMap = {
[K in FilterableFieldType]: Partial<{
[S in CompositeFieldSubFieldName]: readonly RecordFilterOperand[];
}>;
};
export const FILTER_OPERANDS_MAP = {
TEXT: [
RecordFilterOperand.CONTAINS,
RecordFilterOperand.DOES_NOT_CONTAIN,
...emptyOperands,
],
EMAILS: [
RecordFilterOperand.CONTAINS,
RecordFilterOperand.DOES_NOT_CONTAIN,
...emptyOperands,
],
FULL_NAME: [
RecordFilterOperand.CONTAINS,
RecordFilterOperand.DOES_NOT_CONTAIN,
...emptyOperands,
],
ADDRESS: [
RecordFilterOperand.CONTAINS,
RecordFilterOperand.DOES_NOT_CONTAIN,
...emptyOperands,
],
LINKS: [
RecordFilterOperand.CONTAINS,
RecordFilterOperand.DOES_NOT_CONTAIN,
...emptyOperands,
],
PHONES: [
RecordFilterOperand.CONTAINS,
RecordFilterOperand.DOES_NOT_CONTAIN,
...emptyOperands,
],
CURRENCY: [
RecordFilterOperand.GREATER_THAN_OR_EQUAL,
RecordFilterOperand.LESS_THAN_OR_EQUAL,
...emptyOperands,
],
NUMBER: [
RecordFilterOperand.IS,
RecordFilterOperand.IS_NOT,
RecordFilterOperand.GREATER_THAN_OR_EQUAL,
RecordFilterOperand.LESS_THAN_OR_EQUAL,
...emptyOperands,
],
RAW_JSON: [
RecordFilterOperand.CONTAINS,
RecordFilterOperand.DOES_NOT_CONTAIN,
...emptyOperands,
],
FILES: [
RecordFilterOperand.CONTAINS,
RecordFilterOperand.DOES_NOT_CONTAIN,
...emptyOperands,
],
DATE_TIME: [
RecordFilterOperand.IS,
RecordFilterOperand.IS_RELATIVE,
RecordFilterOperand.IS_IN_PAST,
RecordFilterOperand.IS_IN_FUTURE,
RecordFilterOperand.IS_TODAY,
RecordFilterOperand.IS_BEFORE,
RecordFilterOperand.IS_AFTER,
...emptyOperands,
],
DATE: [
RecordFilterOperand.IS,
RecordFilterOperand.IS_RELATIVE,
RecordFilterOperand.IS_IN_PAST,
RecordFilterOperand.IS_IN_FUTURE,
RecordFilterOperand.IS_TODAY,
RecordFilterOperand.IS_BEFORE,
RecordFilterOperand.IS_AFTER,
...emptyOperands,
],
RATING: [
RecordFilterOperand.IS,
RecordFilterOperand.IS_NOT,
RecordFilterOperand.GREATER_THAN_OR_EQUAL,
RecordFilterOperand.LESS_THAN_OR_EQUAL,
...emptyOperands,
],
RELATION: [...relationOperands, ...emptyOperands],
MULTI_SELECT: [
RecordFilterOperand.CONTAINS,
RecordFilterOperand.DOES_NOT_CONTAIN,
...emptyOperands,
],
SELECT: [
RecordFilterOperand.IS,
RecordFilterOperand.IS_NOT,
...emptyOperands,
],
ACTOR: [
RecordFilterOperand.CONTAINS,
RecordFilterOperand.DOES_NOT_CONTAIN,
...emptyOperands,
],
ARRAY: [
RecordFilterOperand.CONTAINS,
RecordFilterOperand.DOES_NOT_CONTAIN,
...emptyOperands,
],
BOOLEAN: [RecordFilterOperand.IS],
TS_VECTOR: [RecordFilterOperand.VECTOR_SEARCH],
UUID: [RecordFilterOperand.IS, RecordFilterOperand.IS_NOT, ...emptyOperands],
} as const satisfies FilterOperandMap;
export const COMPOSITE_FIELD_FILTER_OPERANDS_MAP = {
CURRENCY: {
currencyCode: [
RecordFilterOperand.IS,
RecordFilterOperand.IS_NOT,
...emptyOperands,
],
amountMicros: [
RecordFilterOperand.GREATER_THAN_OR_EQUAL,
RecordFilterOperand.LESS_THAN_OR_EQUAL,
RecordFilterOperand.IS,
RecordFilterOperand.IS_NOT,
...emptyOperands,
],
},
} as const satisfies Partial<CompositeFieldFilterOperandMap>;
import { getFilterOperandsForFilterableFieldType } from 'twenty-shared/utils';
export const getRecordFilterOperands = ({
filterType,
subFieldName,
}: GetRecordFilterOperandsParams): readonly RecordFilterOperand[] => {
switch (filterType) {
case 'TEXT':
case 'EMAILS':
case 'FULL_NAME':
case 'ADDRESS':
case 'LINKS':
case 'PHONES':
return FILTER_OPERANDS_MAP.TEXT;
case 'CURRENCY': {
if (
isExpectedSubFieldName(
FieldMetadataType.CURRENCY,
'currencyCode',
subFieldName,
) === true
) {
return COMPOSITE_FIELD_FILTER_OPERANDS_MAP.CURRENCY.currencyCode;
} else {
return COMPOSITE_FIELD_FILTER_OPERANDS_MAP.CURRENCY.amountMicros;
}
}
case 'NUMBER':
return FILTER_OPERANDS_MAP.NUMBER;
case 'RAW_JSON':
return FILTER_OPERANDS_MAP.RAW_JSON;
case 'FILES':
return FILTER_OPERANDS_MAP.FILES;
case 'DATE_TIME':
case 'DATE':
return FILTER_OPERANDS_MAP.DATE_TIME;
case 'RATING':
return FILTER_OPERANDS_MAP.RATING;
case 'RELATION':
return FILTER_OPERANDS_MAP.RELATION;
case 'MULTI_SELECT':
return FILTER_OPERANDS_MAP.MULTI_SELECT;
case 'SELECT':
return FILTER_OPERANDS_MAP.SELECT;
case 'ACTOR': {
if (
isFilterOnActorSourceSubField(subFieldName) ||
isFilterOnActorWorkspaceMemberSubField(subFieldName)
) {
return [
RecordFilterOperand.IS,
RecordFilterOperand.IS_NOT,
...emptyOperands,
];
}
return FILTER_OPERANDS_MAP.ACTOR;
}
case 'ARRAY':
return FILTER_OPERANDS_MAP.ARRAY;
case 'BOOLEAN':
return FILTER_OPERANDS_MAP.BOOLEAN;
case 'TS_VECTOR':
return FILTER_OPERANDS_MAP.TS_VECTOR;
case 'UUID':
return FILTER_OPERANDS_MAP.UUID;
default:
assertUnreachable(filterType, `Unknown filter type ${filterType}`);
}
}: {
filterType: FilterableAndTSVectorFieldType;
subFieldName?: string | null | undefined;
}): readonly RecordFilterOperand[] => {
return getFilterOperandsForFilterableFieldType({
filterType,
subFieldName,
});
};
@@ -5,10 +5,7 @@ import {
type RecordFilter,
type RecordFilterToRecordInputOperand,
} from '@/object-record/record-filter/types/RecordFilter';
import {
FILTER_OPERANDS_MAP,
getRecordFilterOperands,
} from '@/object-record/record-filter/utils/getRecordFilterOperands';
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
import { COMPOSITE_FIELD_TYPE_SUB_FIELDS_NAMES } from 'twenty-shared/constants';
import {
compositeTypeDefinitions,
@@ -16,7 +13,11 @@ import {
ViewFilterOperand,
type FieldMetadataOptions,
} from 'twenty-shared/types';
import { assertUnreachable, parseJson } from 'twenty-shared/utils';
import {
assertUnreachable,
FILTER_OPERANDS_MAP,
parseJson,
} from 'twenty-shared/utils';
import { RelationType } from '~/generated-metadata/graphql';
import { convertCurrencyAmountToCurrencyMicros } from '~/utils/convertCurrencyToCurrencyMicros';
@@ -1,8 +1,8 @@
import { ViewFilterOperand } from 'twenty-shared/types';
import {
COMPOSITE_FIELD_FILTER_OPERANDS_MAP,
FILTER_OPERANDS_MAP,
} from '@/object-record/record-filter/utils/getRecordFilterOperands';
import { ViewFilterOperand } from 'twenty-shared/types';
} from 'twenty-shared/utils';
const defaultOperands = [
ViewFilterOperand.IS,
@@ -3,6 +3,7 @@ import { Logo } from '@/auth/components/Logo';
import { Title } from '@/auth/components/Title';
import { useAuth } from '@/auth/hooks/useAuth';
import { useHasAccessTokenPair } from '@/auth/hooks/useHasAccessTokenPair';
import { StyledOnboardingContentContainer } from '@/auth/components/StyledOnboardingContentContainer';
import { currentUserState } from '@/auth/states/currentUserState';
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
import { PASSWORD_REGEX } from '@/auth/utils/passwordRegex';
@@ -61,12 +62,6 @@ const StyledMainContainer = styled.div`
width: 100%;
`;
const StyledContentContainer = styled.div`
margin-bottom: ${themeCssVariables.spacing[8]};
margin-top: ${themeCssVariables.spacing[4]};
width: 200px;
`;
const StyledForm = styled.form`
align-items: center;
display: flex;
@@ -229,7 +224,7 @@ export const PasswordReset = () => {
/>
</AnimatedEaseIn>
<Title animate>{passwordActionLabel}</Title>
<StyledContentContainer>
<StyledOnboardingContentContainer>
{!email ? (
<SkeletonTheme
baseColor={theme.background.quaternary}
@@ -311,7 +306,7 @@ export const PasswordReset = () => {
</StyledMainButtonContainer>
</StyledForm>
)}
</StyledContentContainer>
</StyledOnboardingContentContainer>
</StyledMainContainer>
</ModalContent>
)
@@ -16,6 +16,7 @@ import { EmailVerificationSent } from '@/auth/sign-in-up/components/EmailVerific
import { FooterNote } from '@/auth/sign-in-up/components/FooterNote';
import { SignInUpGlobalScopeForm } from '@/auth/sign-in-up/components/SignInUpGlobalScopeForm';
import { SignInUpWorkspaceScopeForm } from '@/auth/sign-in-up/components/SignInUpWorkspaceScopeForm';
import { WorkspaceSelectionFooter } from '@/auth/sign-in-up/components/WorkspaceSelectionFooter';
import { SignInUpSSOIdentityProviderSelection } from '@/auth/sign-in-up/components/internal/SignInUpSSOIdentityProviderSelection';
import { SignInUpWorkspaceScopeFormEffect } from '@/auth/sign-in-up/components/internal/SignInUpWorkspaceScopeFormEffect';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
@@ -71,6 +72,9 @@ const StandardContent = ({
</AnimatedEaseIn>
<Title animate>{title}</Title>
{signInUpForm}
{signInUpStep === SignInUpStep.WorkspaceSelection && (
<WorkspaceSelectionFooter />
)}
{![
SignInUpStep.Password,
SignInUpStep.TwoFactorAuthenticationProvision,
@@ -9,6 +9,7 @@ import { extractManifestFromFile } from '@/cli/utilities/build/manifest/manifest
import { addMissingFieldOptionIds } from '@/cli/utilities/build/manifest/utils/add-missing-field-option-ids';
import { fromRoleConfigToRoleManifest } from '@/cli/utilities/build/manifest/utils/from-role-config-to-role-manifest';
import { getDefaultFieldsInObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-fields-in-object-fields';
import { validateViewFilterOperands } from '@/cli/utilities/build/manifest/utils/validate-view-filter-operands';
import { type ApplicationConfig, type LogicFunctionConfig } from '@/sdk/define';
import { type CommandMenuItemConfig } from '@/sdk/define/command-menu-items/command-menu-item-config';
import { type FrontComponentConfig } from '@/sdk/define/front-component/front-component-config';
@@ -488,6 +489,14 @@ export const buildManifest = async (
);
}
errors.push(
...validateViewFilterOperands({
views,
objects,
fields,
}),
);
const application: ApplicationManifest | undefined =
applicationConfig && resolvedDefaultRoleUniversalIdentifier
? {
@@ -0,0 +1,265 @@
import { validateViewFilterOperands } from '@/cli/utilities/build/manifest/utils/validate-view-filter-operands';
import {
type FieldManifest,
type ObjectFieldManifest,
type ObjectManifest,
type ViewManifest,
} from 'twenty-shared/application';
import {
FieldMetadataType,
ViewFilterOperand,
ViewType,
} from 'twenty-shared/types';
const buildField = <T extends FieldMetadataType>({
universalIdentifier,
type,
name = 'field',
label = 'Field',
}: {
universalIdentifier: string;
type: T;
name?: string;
label?: string;
}): FieldManifest<T> =>
({
universalIdentifier,
objectUniversalIdentifier: 'object-uid',
name,
label,
type,
}) as FieldManifest<T>;
const buildRelationField = ({
universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier,
name = 'relation',
label = 'Relation',
}: {
universalIdentifier: string;
relationTargetFieldMetadataUniversalIdentifier: string;
name?: string;
label?: string;
}): FieldManifest<FieldMetadataType.RELATION> =>
({
universalIdentifier,
objectUniversalIdentifier: 'object-uid',
name,
label,
type: FieldMetadataType.RELATION,
relationTargetFieldMetadataUniversalIdentifier,
relationTargetObjectMetadataUniversalIdentifier: 'target-object-uid',
universalSettings: {},
}) as FieldManifest<FieldMetadataType.RELATION>;
const buildView = (filters: ViewManifest['filters']): ViewManifest => ({
universalIdentifier: 'view-uid',
name: 'View',
objectUniversalIdentifier: 'object-uid',
type: ViewType.TABLE,
filters,
});
const buildObject = (fields: ObjectFieldManifest[]): ObjectManifest =>
({
universalIdentifier: 'object-uid',
nameSingular: 'company',
namePlural: 'companies',
labelSingular: 'Company',
labelPlural: 'Companies',
labelIdentifierFieldMetadataUniversalIdentifier:
fields[0]?.universalIdentifier ?? '',
fields,
}) as ObjectManifest;
describe('validateViewFilterOperands', () => {
it('should return no errors when operand is valid for the field type', () => {
const errors = validateViewFilterOperands({
views: [
buildView([
{
universalIdentifier: 'filter-uid',
fieldMetadataUniversalIdentifier: 'field-uid',
operand: ViewFilterOperand.CONTAINS,
value: 'acme',
},
]),
],
objects: [],
fields: [
buildField({
universalIdentifier: 'field-uid',
type: FieldMetadataType.TEXT,
name: 'company',
}),
],
});
expect(errors).toEqual([]);
});
it('should return an error when operand is invalid for the field type', () => {
const errors = validateViewFilterOperands({
views: [
buildView([
{
universalIdentifier: 'filter-uid',
fieldMetadataUniversalIdentifier: 'field-uid',
operand: ViewFilterOperand.CONTAINS,
value: '5',
},
]),
],
objects: [],
fields: [
buildField({
universalIdentifier: 'field-uid',
type: FieldMetadataType.NUMBER,
name: 'rating',
}),
],
});
expect(errors).toHaveLength(1);
expect(errors[0]).toContain('CONTAINS');
expect(errors[0]).toContain('rating');
expect(errors[0]).toContain('NUMBER');
});
it('should resolve relation fields against their target field type', () => {
const errors = validateViewFilterOperands({
views: [
buildView([
{
universalIdentifier: 'filter-uid',
fieldMetadataUniversalIdentifier: 'relation-field-uid',
operand: ViewFilterOperand.IS,
value: ['target-record-uid'],
},
]),
],
objects: [],
fields: [
buildRelationField({
universalIdentifier: 'relation-field-uid',
relationTargetFieldMetadataUniversalIdentifier: 'target-field-uid',
name: 'company',
}),
buildField({
universalIdentifier: 'target-field-uid',
type: FieldMetadataType.UUID,
name: 'id',
}),
],
});
expect(errors).toEqual([]);
});
it('should fallback to RELATION operands when relation target field is missing', () => {
const errors = validateViewFilterOperands({
views: [
buildView([
{
universalIdentifier: 'filter-uid',
fieldMetadataUniversalIdentifier: 'relation-field-uid',
operand: ViewFilterOperand.IS,
value: ['target-record-uid'],
},
]),
],
objects: [],
fields: [
buildRelationField({
universalIdentifier: 'relation-field-uid',
relationTargetFieldMetadataUniversalIdentifier: 'missing-target-uid',
name: 'company',
}),
],
});
expect(errors).toEqual([]);
});
it('should look up fields nested under objects', () => {
const errors = validateViewFilterOperands({
views: [
buildView([
{
universalIdentifier: 'filter-uid',
fieldMetadataUniversalIdentifier: 'field-uid',
operand: ViewFilterOperand.GREATER_THAN_OR_EQUAL,
value: '5',
},
]),
],
objects: [
buildObject([
buildField({
universalIdentifier: 'field-uid',
type: FieldMetadataType.TEXT,
name: 'company',
}),
]),
],
fields: [],
});
expect(errors).toHaveLength(1);
expect(errors[0]).toContain('GREATER_THAN_OR_EQUAL');
expect(errors[0]).toContain('TEXT');
});
it('should skip filters that reference unknown fields', () => {
const errors = validateViewFilterOperands({
views: [
buildView([
{
universalIdentifier: 'filter-uid',
fieldMetadataUniversalIdentifier: 'unknown-field-uid',
operand: ViewFilterOperand.CONTAINS,
value: 'acme',
},
]),
],
objects: [],
fields: [],
});
expect(errors).toEqual([]);
});
it('should respect CURRENCY subFieldName when checking operands', () => {
const errors = validateViewFilterOperands({
views: [
buildView([
{
universalIdentifier: 'filter-uid',
fieldMetadataUniversalIdentifier: 'amount-uid',
operand: ViewFilterOperand.CONTAINS,
value: 'USD',
subFieldName: 'currencyCode',
},
]),
],
objects: [],
fields: [
buildField({
universalIdentifier: 'amount-uid',
type: FieldMetadataType.CURRENCY,
name: 'amount',
}),
],
});
expect(errors).toHaveLength(1);
expect(errors[0]).toContain('CONTAINS');
expect(errors[0]).toContain('CURRENCY');
});
it('should return an empty array when no views are defined', () => {
expect(
validateViewFilterOperands({ views: [], objects: [], fields: [] }),
).toEqual([]);
});
});
@@ -0,0 +1,78 @@
import {
type FieldManifest,
type ObjectFieldManifest,
type ObjectManifest,
type ViewManifest,
} from 'twenty-shared/application';
import {
FieldMetadataType,
type FilterableAndTSVectorFieldType,
} from 'twenty-shared/types';
import {
FILTER_OPERANDS_MAP,
getFilterOperandsForFilterableFieldType,
isDefined,
} from 'twenty-shared/utils';
export const validateViewFilterOperands = ({
views,
objects,
fields,
}: {
views: ViewManifest[];
objects: ObjectManifest[];
fields: FieldManifest[];
}): string[] => {
const errors: string[] = [];
const fieldByUniversalIdentifier = new Map<
string,
FieldManifest | ObjectFieldManifest
>();
for (const field of fields) {
fieldByUniversalIdentifier.set(field.universalIdentifier, field);
}
for (const object of objects) {
for (const objectField of object.fields) {
fieldByUniversalIdentifier.set(
objectField.universalIdentifier,
objectField,
);
}
}
for (const view of views) {
for (const filter of view.filters ?? []) {
const referencedField = fieldByUniversalIdentifier.get(
filter.fieldMetadataUniversalIdentifier,
);
if (!isDefined(referencedField)) {
continue;
}
const effectiveFieldType: FieldMetadataType =
referencedField.type === FieldMetadataType.RELATION
? (fieldByUniversalIdentifier.get(
referencedField.relationTargetFieldMetadataUniversalIdentifier,
)?.type ?? referencedField.type)
: referencedField.type;
if (!(effectiveFieldType in FILTER_OPERANDS_MAP)) {
continue;
}
const allowedOperands = getFilterOperandsForFilterableFieldType({
filterType: effectiveFieldType as FilterableAndTSVectorFieldType,
subFieldName: filter.subFieldName,
});
if (!allowedOperands.includes(filter.operand)) {
errors.push(
`Operand "${filter.operand}" is not supported on field "${referencedField.name}" (type "${effectiveFieldType}"). Supported operands: ${allowedOperands.join(', ')}.`,
);
}
}
}
return errors;
};
@@ -0,0 +1,20 @@
import { Module } from '@nestjs/common';
import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module';
import { BackfillRelationJoinColumnIndexesCommand } from 'src/database/commands/upgrade-version-command/2-8/2-8-workspace-command-1798100000000-backfill-relation-join-column-indexes.command';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
@Module({
imports: [
ApplicationModule,
WorkspaceCacheModule,
WorkspaceIteratorModule,
WorkspaceMigrationModule,
WorkspaceSchemaManagerModule,
],
providers: [BackfillRelationJoinColumnIndexesCommand],
})
export class V2_8_UpgradeVersionCommandModule {}
@@ -0,0 +1,225 @@
import { Command } from 'nest-commander';
import { DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS } from 'twenty-shared/metadata';
import { RelationType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner';
import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { generateIndexForFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-index-for-flat-field-metadata.util';
import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { getWorkspaceSchemaContextForMigration } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/get-workspace-schema-context-for-migration.util';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
const POLYMORPHIC_STANDARD_OBJECT_NAMES_SINGULAR: ReadonlySet<string> = new Set(
DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS,
);
@RegisteredWorkspaceCommand('2.8.0', 1798100000000)
@Command({
name: 'upgrade:2-8:backfill-relation-join-column-indexes',
description:
'Backfill missing BTREE indexes on target<X>Id join columns added to polymorphic standard objects (timelineActivity, attachment, noteTarget, taskTarget) when custom objects were created before the auto-index fix. Indexes are created with CONCURRENTLY so writes are not blocked.',
})
export class BackfillRelationJoinColumnIndexesCommand extends ActiveOrSuspendedWorkspaceCommandRunner {
constructor(
protected readonly workspaceIteratorService: WorkspaceIteratorService,
private readonly applicationService: ApplicationService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly workspaceSchemaManagerService: WorkspaceSchemaManagerService,
) {
super(workspaceIteratorService);
}
override async runOnWorkspace({
workspaceId,
dataSource,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const isDryRun = options.dryRun ?? false;
if (!dataSource) {
this.logger.log(`No data source for workspace ${workspaceId}, skipping`);
return;
}
const { flatObjectMetadataMaps, flatFieldMetadataMaps, flatIndexMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatIndexMaps',
]);
const polymorphicStandardObjectIds = new Set(
Object.values(flatObjectMetadataMaps.byUniversalIdentifier)
.filter(isDefined)
.filter((flatObject) =>
POLYMORPHIC_STANDARD_OBJECT_NAMES_SINGULAR.has(
flatObject.nameSingular,
),
)
.map((flatObject) => flatObject.id),
);
if (polymorphicStandardObjectIds.size === 0) {
this.logger.log(
`No polymorphic standard objects found for workspace ${workspaceId}, skipping`,
);
return;
}
const indexedFieldIds = new Set<string>();
for (const flatIndex of Object.values(flatIndexMaps.byUniversalIdentifier)) {
if (!isDefined(flatIndex)) {
continue;
}
for (const indexField of flatIndex.flatIndexFieldMetadatas) {
indexedFieldIds.add(indexField.fieldMetadataId);
}
}
const fieldsNeedingIndex = Object.values(
flatFieldMetadataMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter(isMorphOrRelationFlatFieldMetadata)
.filter((flatField) =>
polymorphicStandardObjectIds.has(flatField.objectMetadataId),
)
.filter(
(flatField) =>
flatField.settings?.relationType === RelationType.MANY_TO_ONE,
)
.filter((flatField) => !indexedFieldIds.has(flatField.id));
if (fieldsNeedingIndex.length === 0) {
this.logger.log(
`No missing relation join column indexes for workspace ${workspaceId}, skipping`,
);
return;
}
const flatIndexBuildPlans = fieldsNeedingIndex.map((flatField) => {
const flatObjectMetadata =
findFlatEntityByIdInFlatEntityMapsOrThrow<FlatObjectMetadata>({
flatEntityId: flatField.objectMetadataId,
flatEntityMaps: flatObjectMetadataMaps,
});
const universalFlatIndexMetadata = generateIndexForFlatFieldMetadata({
flatFieldMetadata: flatField,
flatObjectMetadata,
});
const joinColumnName = computeMorphOrRelationFieldJoinColumnName({
name: flatField.name,
});
return {
flatObjectMetadata,
universalFlatIndexMetadata,
joinColumnName,
};
});
this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Found ${flatIndexBuildPlans.length} missing relation join column index(es) for workspace ${workspaceId}: ${flatIndexBuildPlans.map(({ universalFlatIndexMetadata }) => universalFlatIndexMetadata.name).join(', ')}`,
);
if (isDryRun) {
return;
}
const queryRunner = dataSource.createQueryRunner();
let isQueryRunnerConnected = false;
try {
await queryRunner.connect();
isQueryRunnerConnected = true;
for (const {
flatObjectMetadata,
universalFlatIndexMetadata,
joinColumnName,
} of flatIndexBuildPlans) {
const { schemaName, tableName } = getWorkspaceSchemaContextForMigration({
workspaceId,
objectMetadata: flatObjectMetadata,
});
await this.workspaceSchemaManagerService.indexManager.createIndex({
queryRunner,
schemaName,
tableName,
index: {
name: universalFlatIndexMetadata.name,
columns: [joinColumnName],
isUnique: universalFlatIndexMetadata.isUnique,
type: universalFlatIndexMetadata.indexType,
where: universalFlatIndexMetadata.indexWhereClause ?? undefined,
},
concurrently: true,
});
this.logger.log(
`Created index ${universalFlatIndexMetadata.name} on workspace ${workspaceId}`,
);
}
} finally {
if (isQueryRunnerConnected) {
await queryRunner.release();
}
}
const { twentyStandardFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
isSystemBuild: true,
allFlatEntityOperationByMetadataName: {
index: {
flatEntityToCreate: flatIndexBuildPlans.map(
({ universalFlatIndexMetadata }) => universalFlatIndexMetadata,
),
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
twentyStandardFlatApplication.universalIdentifier,
},
);
if (validateAndBuildResult.status === 'fail') {
this.logger.error(
`Failed to persist relation join column index metadata:\n${JSON.stringify(validateAndBuildResult, null, 2)}`,
);
throw new Error(
`Failed to persist relation join column index metadata for workspace ${workspaceId}`,
);
}
this.logger.log(
`Successfully backfilled ${flatIndexBuildPlans.length} relation join column index(es) for workspace ${workspaceId}`,
);
}
}
@@ -10,6 +10,7 @@ import { V2_3_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-
import { V2_4_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-4/2-4-upgrade-version-command.module';
import { V2_5_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-5/2-5-upgrade-version-command.module';
import { V2_7_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-7/2-7-upgrade-version-command.module';
import { V2_8_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-8/2-8-upgrade-version-command.module';
@Module({
imports: [
@@ -23,6 +24,7 @@ import { V2_7_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-
V2_4_UpgradeVersionCommandModule,
V2_5_UpgradeVersionCommandModule,
V2_7_UpgradeVersionCommandModule,
V2_8_UpgradeVersionCommandModule,
],
})
export class WorkspaceCommandProviderModule {}
@@ -38,7 +38,6 @@ export class EmailVerificationExceptionFilter implements ExceptionFilter {
userFriendlyMessage: msg`Email verification not required.`,
});
case EmailVerificationExceptionCode.INVALID_EMAIL:
case EmailVerificationExceptionCode.USER_NOT_FOUND:
throw new UserInputError(exception);
default: {
assertUnreachable(exception.code);
@@ -13,7 +13,6 @@ export enum EmailVerificationExceptionCode {
EMAIL_ALREADY_VERIFIED = 'EMAIL_ALREADY_VERIFIED',
INVALID_EMAIL = 'INVALID_EMAIL',
RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED',
USER_NOT_FOUND = 'USER_NOT_FOUND',
}
const getEmailVerificationExceptionUserFriendlyMessage = (
@@ -34,8 +33,6 @@ const getEmailVerificationExceptionUserFriendlyMessage = (
return msg`Invalid email address.`;
case EmailVerificationExceptionCode.RATE_LIMIT_EXCEEDED:
return msg`Too many requests. Please try again later.`;
case EmailVerificationExceptionCode.USER_NOT_FOUND:
return msg`No user found for this email.`;
default:
assertUnreachable(code);
}
@@ -8,7 +8,7 @@ import ms from 'ms';
import { SendEmailVerificationLinkEmail } from 'twenty-emails';
import { type APP_LOCALES } from 'twenty-shared/translations';
import { AppPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import {
@@ -138,12 +138,7 @@ export class EmailVerificationService {
},
});
if (!isDefined(user)) {
throw new EmailVerificationException(
'User not found',
EmailVerificationExceptionCode.USER_NOT_FOUND,
);
}
assertIsDefinedOrThrow(user);
if (user.isEmailVerified) {
throw new EmailVerificationException(
@@ -1643,6 +1643,11 @@ msgstr ""
msgid "Correct direction is required"
msgstr "Korrekte rigting word vereis"
#. js-lingui-id: s+lEnL
#: src/engine/twenty-orm/workspace-schema-manager/exceptions/workspace-schema-manager.exception.ts
msgid "Could not create the index because it must run outside a database transaction."
msgstr ""
#. js-lingui-id: Fep47m
#: src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util.ts
msgid "Could not find {targetMetadataName} for given {foreignKey}"
@@ -2784,6 +2789,11 @@ msgstr "Lêers"
msgid "Files field is not supported for unique fields"
msgstr "Die Files-veld word nie vir unieke velde ondersteun nie."
#. js-lingui-id: MkIaen
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-validator.service.ts
msgid "Filter operand is not supported for this field type"
msgstr ""
#. js-lingui-id: 4GaFCQ
#: src/engine/workspace-manager/workspace-migration/interceptors/utils/__tests__/build-metadata-validation-error-payload.util.spec.ts
msgid "First friendly message"
@@ -5249,6 +5259,12 @@ msgstr ""
msgid "Only field restrictions are allowed."
msgstr "Slegs veldbeperkings word toegelaat."
#. js-lingui-id: vX5t8O
#. placeholder {0}: allowedOperands.join(', ')
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-validator.service.ts
msgid "Operand \"{operand}\" is not supported on field type \"{effectiveFieldType}\". Supported operands: {0}."
msgstr ""
#. js-lingui-id: 4MyDFl
#: src/engine/workspace-manager/twenty-standard-application/utils/object-metadata/create-standard-flat-object-metadata.util.ts
#: src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-person-standard-flat-field-metadata.util.ts
@@ -1643,6 +1643,11 @@ msgstr ""
msgid "Correct direction is required"
msgstr "الاتجاه الصحيح مطلوب"
#. js-lingui-id: s+lEnL
#: src/engine/twenty-orm/workspace-schema-manager/exceptions/workspace-schema-manager.exception.ts
msgid "Could not create the index because it must run outside a database transaction."
msgstr ""
#. js-lingui-id: Fep47m
#: src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util.ts
msgid "Could not find {targetMetadataName} for given {foreignKey}"
@@ -2784,6 +2789,11 @@ msgstr "ملفات"
msgid "Files field is not supported for unique fields"
msgstr "حقل الملفات غير مدعوم للحقول الفريدة"
#. js-lingui-id: MkIaen
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-validator.service.ts
msgid "Filter operand is not supported for this field type"
msgstr ""
#. js-lingui-id: 4GaFCQ
#: src/engine/workspace-manager/workspace-migration/interceptors/utils/__tests__/build-metadata-validation-error-payload.util.spec.ts
msgid "First friendly message"
@@ -5249,6 +5259,12 @@ msgstr ""
msgid "Only field restrictions are allowed."
msgstr "يُسمح فقط بقيود الحقول."
#. js-lingui-id: vX5t8O
#. placeholder {0}: allowedOperands.join(', ')
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-validator.service.ts
msgid "Operand \"{operand}\" is not supported on field type \"{effectiveFieldType}\". Supported operands: {0}."
msgstr ""
#. js-lingui-id: 4MyDFl
#: src/engine/workspace-manager/twenty-standard-application/utils/object-metadata/create-standard-flat-object-metadata.util.ts
#: src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-person-standard-flat-field-metadata.util.ts
@@ -1643,6 +1643,11 @@ msgstr ""
msgid "Correct direction is required"
msgstr "Cal una direcció vàlida"
#. js-lingui-id: s+lEnL
#: src/engine/twenty-orm/workspace-schema-manager/exceptions/workspace-schema-manager.exception.ts
msgid "Could not create the index because it must run outside a database transaction."
msgstr ""
#. js-lingui-id: Fep47m
#: src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util.ts
msgid "Could not find {targetMetadataName} for given {foreignKey}"
@@ -2784,6 +2789,11 @@ msgstr "Arxius"
msgid "Files field is not supported for unique fields"
msgstr "El camp Files no és compatible com a camp únic."
#. js-lingui-id: MkIaen
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-validator.service.ts
msgid "Filter operand is not supported for this field type"
msgstr ""
#. js-lingui-id: 4GaFCQ
#: src/engine/workspace-manager/workspace-migration/interceptors/utils/__tests__/build-metadata-validation-error-payload.util.spec.ts
msgid "First friendly message"
@@ -5249,6 +5259,12 @@ msgstr ""
msgid "Only field restrictions are allowed."
msgstr "Només es permeten restriccions de camp."
#. js-lingui-id: vX5t8O
#. placeholder {0}: allowedOperands.join(', ')
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-validator.service.ts
msgid "Operand \"{operand}\" is not supported on field type \"{effectiveFieldType}\". Supported operands: {0}."
msgstr ""
#. js-lingui-id: 4MyDFl
#: src/engine/workspace-manager/twenty-standard-application/utils/object-metadata/create-standard-flat-object-metadata.util.ts
#: src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-person-standard-flat-field-metadata.util.ts
@@ -1643,6 +1643,11 @@ msgstr ""
msgid "Correct direction is required"
msgstr "Je vyžadován správný směr řazení"
#. js-lingui-id: s+lEnL
#: src/engine/twenty-orm/workspace-schema-manager/exceptions/workspace-schema-manager.exception.ts
msgid "Could not create the index because it must run outside a database transaction."
msgstr ""
#. js-lingui-id: Fep47m
#: src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util.ts
msgid "Could not find {targetMetadataName} for given {foreignKey}"
@@ -2784,6 +2789,11 @@ msgstr "Soubory"
msgid "Files field is not supported for unique fields"
msgstr "Pole Files není podporováno pro unikátní pole."
#. js-lingui-id: MkIaen
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-validator.service.ts
msgid "Filter operand is not supported for this field type"
msgstr ""
#. js-lingui-id: 4GaFCQ
#: src/engine/workspace-manager/workspace-migration/interceptors/utils/__tests__/build-metadata-validation-error-payload.util.spec.ts
msgid "First friendly message"
@@ -5249,6 +5259,12 @@ msgstr ""
msgid "Only field restrictions are allowed."
msgstr "Jsou povolena pouze omezení polí."
#. js-lingui-id: vX5t8O
#. placeholder {0}: allowedOperands.join(', ')
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-validator.service.ts
msgid "Operand \"{operand}\" is not supported on field type \"{effectiveFieldType}\". Supported operands: {0}."
msgstr ""
#. js-lingui-id: 4MyDFl
#: src/engine/workspace-manager/twenty-standard-application/utils/object-metadata/create-standard-flat-object-metadata.util.ts
#: src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-person-standard-flat-field-metadata.util.ts
@@ -1643,6 +1643,11 @@ msgstr ""
msgid "Correct direction is required"
msgstr "Korrekt retning er påkrævet"
#. js-lingui-id: s+lEnL
#: src/engine/twenty-orm/workspace-schema-manager/exceptions/workspace-schema-manager.exception.ts
msgid "Could not create the index because it must run outside a database transaction."
msgstr ""
#. js-lingui-id: Fep47m
#: src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util.ts
msgid "Could not find {targetMetadataName} for given {foreignKey}"
@@ -2784,6 +2789,11 @@ msgstr "Filer"
msgid "Files field is not supported for unique fields"
msgstr "Files-feltet understøttes ikke for unikke felter"
#. js-lingui-id: MkIaen
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-validator.service.ts
msgid "Filter operand is not supported for this field type"
msgstr ""
#. js-lingui-id: 4GaFCQ
#: src/engine/workspace-manager/workspace-migration/interceptors/utils/__tests__/build-metadata-validation-error-payload.util.spec.ts
msgid "First friendly message"
@@ -5249,6 +5259,12 @@ msgstr ""
msgid "Only field restrictions are allowed."
msgstr "Kun feltbegrænsninger er tilladt."
#. js-lingui-id: vX5t8O
#. placeholder {0}: allowedOperands.join(', ')
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-validator.service.ts
msgid "Operand \"{operand}\" is not supported on field type \"{effectiveFieldType}\". Supported operands: {0}."
msgstr ""
#. js-lingui-id: 4MyDFl
#: src/engine/workspace-manager/twenty-standard-application/utils/object-metadata/create-standard-flat-object-metadata.util.ts
#: src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-person-standard-flat-field-metadata.util.ts
@@ -1643,6 +1643,11 @@ msgstr ""
msgid "Correct direction is required"
msgstr "Eine gültige Richtung ist erforderlich."
#. js-lingui-id: s+lEnL
#: src/engine/twenty-orm/workspace-schema-manager/exceptions/workspace-schema-manager.exception.ts
msgid "Could not create the index because it must run outside a database transaction."
msgstr ""
#. js-lingui-id: Fep47m
#: src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util.ts
msgid "Could not find {targetMetadataName} for given {foreignKey}"
@@ -2784,6 +2789,11 @@ msgstr "Dateien"
msgid "Files field is not supported for unique fields"
msgstr "Das Dateien-Feld wird für eindeutige Felder nicht unterstützt."
#. js-lingui-id: MkIaen
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-validator.service.ts
msgid "Filter operand is not supported for this field type"
msgstr ""
#. js-lingui-id: 4GaFCQ
#: src/engine/workspace-manager/workspace-migration/interceptors/utils/__tests__/build-metadata-validation-error-payload.util.spec.ts
msgid "First friendly message"
@@ -5249,6 +5259,12 @@ msgstr ""
msgid "Only field restrictions are allowed."
msgstr "Es sind nur Feldbeschränkungen zulässig."
#. js-lingui-id: vX5t8O
#. placeholder {0}: allowedOperands.join(', ')
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-validator.service.ts
msgid "Operand \"{operand}\" is not supported on field type \"{effectiveFieldType}\". Supported operands: {0}."
msgstr ""
#. js-lingui-id: 4MyDFl
#: src/engine/workspace-manager/twenty-standard-application/utils/object-metadata/create-standard-flat-object-metadata.util.ts
#: src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-person-standard-flat-field-metadata.util.ts
@@ -1643,6 +1643,11 @@ msgstr ""
msgid "Correct direction is required"
msgstr "Απαιτείται ορθή κατεύθυνση"
#. js-lingui-id: s+lEnL
#: src/engine/twenty-orm/workspace-schema-manager/exceptions/workspace-schema-manager.exception.ts
msgid "Could not create the index because it must run outside a database transaction."
msgstr ""
#. js-lingui-id: Fep47m
#: src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util.ts
msgid "Could not find {targetMetadataName} for given {foreignKey}"
@@ -2784,6 +2789,11 @@ msgstr "Αρχεία"
msgid "Files field is not supported for unique fields"
msgstr "Το πεδίο Files δεν υποστηρίζεται για μοναδικά πεδία"
#. js-lingui-id: MkIaen
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-validator.service.ts
msgid "Filter operand is not supported for this field type"
msgstr ""
#. js-lingui-id: 4GaFCQ
#: src/engine/workspace-manager/workspace-migration/interceptors/utils/__tests__/build-metadata-validation-error-payload.util.spec.ts
msgid "First friendly message"
@@ -5249,6 +5259,12 @@ msgstr ""
msgid "Only field restrictions are allowed."
msgstr "Επιτρέπονται μόνο περιορισμοί πεδίων."
#. js-lingui-id: vX5t8O
#. placeholder {0}: allowedOperands.join(', ')
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-validator.service.ts
msgid "Operand \"{operand}\" is not supported on field type \"{effectiveFieldType}\". Supported operands: {0}."
msgstr ""
#. js-lingui-id: 4MyDFl
#: src/engine/workspace-manager/twenty-standard-application/utils/object-metadata/create-standard-flat-object-metadata.util.ts
#: src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-person-standard-flat-field-metadata.util.ts
@@ -1638,6 +1638,11 @@ msgstr "Contradicting field permissions have been detected on a relation field."
msgid "Correct direction is required"
msgstr "Correct direction is required"
#. js-lingui-id: s+lEnL
#: src/engine/twenty-orm/workspace-schema-manager/exceptions/workspace-schema-manager.exception.ts
msgid "Could not create the index because it must run outside a database transaction."
msgstr "Could not create the index because it must run outside a database transaction."
#. js-lingui-id: Fep47m
#: src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util.ts
msgid "Could not find {targetMetadataName} for given {foreignKey}"
@@ -2779,6 +2784,11 @@ msgstr "Files"
msgid "Files field is not supported for unique fields"
msgstr "Files field is not supported for unique fields"
#. js-lingui-id: MkIaen
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-validator.service.ts
msgid "Filter operand is not supported for this field type"
msgstr "Filter operand is not supported for this field type"
#. js-lingui-id: 4GaFCQ
#: src/engine/workspace-manager/workspace-migration/interceptors/utils/__tests__/build-metadata-validation-error-payload.util.spec.ts
msgid "First friendly message"
@@ -5244,6 +5254,12 @@ msgstr "Only error"
msgid "Only field restrictions are allowed."
msgstr "Only field restrictions are allowed."
#. js-lingui-id: vX5t8O
#. placeholder {0}: allowedOperands.join(', ')
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-validator.service.ts
msgid "Operand \"{operand}\" is not supported on field type \"{effectiveFieldType}\". Supported operands: {0}."
msgstr "Operand \"{operand}\" is not supported on field type \"{effectiveFieldType}\". Supported operands: {0}."
#. js-lingui-id: 4MyDFl
#: src/engine/workspace-manager/twenty-standard-application/utils/object-metadata/create-standard-flat-object-metadata.util.ts
#: src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-person-standard-flat-field-metadata.util.ts
@@ -1643,6 +1643,11 @@ msgstr ""
msgid "Correct direction is required"
msgstr "Se requiere una dirección válida"
#. js-lingui-id: s+lEnL
#: src/engine/twenty-orm/workspace-schema-manager/exceptions/workspace-schema-manager.exception.ts
msgid "Could not create the index because it must run outside a database transaction."
msgstr ""
#. js-lingui-id: Fep47m
#: src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util.ts
msgid "Could not find {targetMetadataName} for given {foreignKey}"
@@ -2784,6 +2789,11 @@ msgstr "Archivos"
msgid "Files field is not supported for unique fields"
msgstr "El campo Files no es compatible con campos únicos"
#. js-lingui-id: MkIaen
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-validator.service.ts
msgid "Filter operand is not supported for this field type"
msgstr ""
#. js-lingui-id: 4GaFCQ
#: src/engine/workspace-manager/workspace-migration/interceptors/utils/__tests__/build-metadata-validation-error-payload.util.spec.ts
msgid "First friendly message"
@@ -5249,6 +5259,12 @@ msgstr ""
msgid "Only field restrictions are allowed."
msgstr "Solo se permiten restricciones de campo."
#. js-lingui-id: vX5t8O
#. placeholder {0}: allowedOperands.join(', ')
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-validator.service.ts
msgid "Operand \"{operand}\" is not supported on field type \"{effectiveFieldType}\". Supported operands: {0}."
msgstr ""
#. js-lingui-id: 4MyDFl
#: src/engine/workspace-manager/twenty-standard-application/utils/object-metadata/create-standard-flat-object-metadata.util.ts
#: src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-person-standard-flat-field-metadata.util.ts
@@ -1643,6 +1643,11 @@ msgstr ""
msgid "Correct direction is required"
msgstr "Kelvollinen suunta vaaditaan"
#. js-lingui-id: s+lEnL
#: src/engine/twenty-orm/workspace-schema-manager/exceptions/workspace-schema-manager.exception.ts
msgid "Could not create the index because it must run outside a database transaction."
msgstr ""
#. js-lingui-id: Fep47m
#: src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util.ts
msgid "Could not find {targetMetadataName} for given {foreignKey}"
@@ -2784,6 +2789,11 @@ msgstr "Tiedostot"
msgid "Files field is not supported for unique fields"
msgstr "Files-kenttää ei tueta yksilöllisille kentille."
#. js-lingui-id: MkIaen
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-validator.service.ts
msgid "Filter operand is not supported for this field type"
msgstr ""
#. js-lingui-id: 4GaFCQ
#: src/engine/workspace-manager/workspace-migration/interceptors/utils/__tests__/build-metadata-validation-error-payload.util.spec.ts
msgid "First friendly message"
@@ -5249,6 +5259,12 @@ msgstr ""
msgid "Only field restrictions are allowed."
msgstr "Vain kenttärajoitukset ovat sallittuja."
#. js-lingui-id: vX5t8O
#. placeholder {0}: allowedOperands.join(', ')
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-validator.service.ts
msgid "Operand \"{operand}\" is not supported on field type \"{effectiveFieldType}\". Supported operands: {0}."
msgstr ""
#. js-lingui-id: 4MyDFl
#: src/engine/workspace-manager/twenty-standard-application/utils/object-metadata/create-standard-flat-object-metadata.util.ts
#: src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-person-standard-flat-field-metadata.util.ts
@@ -1643,6 +1643,11 @@ msgstr ""
msgid "Correct direction is required"
msgstr "Une direction correcte est requise"
#. js-lingui-id: s+lEnL
#: src/engine/twenty-orm/workspace-schema-manager/exceptions/workspace-schema-manager.exception.ts
msgid "Could not create the index because it must run outside a database transaction."
msgstr ""
#. js-lingui-id: Fep47m
#: src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util.ts
msgid "Could not find {targetMetadataName} for given {foreignKey}"
@@ -2784,6 +2789,11 @@ msgstr "Fichiers"
msgid "Files field is not supported for unique fields"
msgstr "Le champ Files n'est pas pris en charge pour les champs uniques"
#. js-lingui-id: MkIaen
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-validator.service.ts
msgid "Filter operand is not supported for this field type"
msgstr ""
#. js-lingui-id: 4GaFCQ
#: src/engine/workspace-manager/workspace-migration/interceptors/utils/__tests__/build-metadata-validation-error-payload.util.spec.ts
msgid "First friendly message"
@@ -5249,6 +5259,12 @@ msgstr ""
msgid "Only field restrictions are allowed."
msgstr "Seules les restrictions de champ sont autorisées."
#. js-lingui-id: vX5t8O
#. placeholder {0}: allowedOperands.join(', ')
#: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-validator.service.ts
msgid "Operand \"{operand}\" is not supported on field type \"{effectiveFieldType}\". Supported operands: {0}."
msgstr ""
#. js-lingui-id: 4MyDFl
#: src/engine/workspace-manager/twenty-standard-application/utils/object-metadata/create-standard-flat-object-metadata.util.ts
#: src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-person-standard-flat-field-metadata.util.ts
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

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