Compare commits

...
Author SHA1 Message Date
Charles Bochet 0b7b33fe18 fix(server): backport relationTargetFieldMetadataId column-add to 2.4 and 2.5 fast instance
Backports the column-add to two new fast instance commands (2.4 at
timestamp 1747234400000 and 2.5 at 1747234500000) so users at v2.3.x or
v2.4.x baselines can upgrade past v2.5 without hitting the 2.5 workspace
command NormalizeCompositeFieldDefaults failing on
`column ViewFilterEntity.relationTargetFieldMetadataId does not exist`.

Same fix as main #20721, scoped to v2.5.x. ADD COLUMN IF NOT EXISTS keeps
both idempotent against DBs that already received the column via the 2.3
backport or the 2.6 add.
2026-05-19 16:02:30 +02:00
Charles Bochet f1125d0894 chore: revert version constants to 2.5.0 for v2.5.3 patch release
The v2.5.x patch tags (v2.5.0, v2.5.1, v2.5.2) were cut from main *after* #20585 bumped TWENTY_CURRENT_VERSION to 2.6.0, so all three binaries report "2.6.0" in the admin-panel inferred-version view despite being tagged 2.5.x.

Restore the version constants to the values they had at v2.5.0 so v2.5.3 reports the correct minor version. Same shape as a pure revert of #20585.
2026-05-18 11:36:42 +02:00
Charles BochetandGitHub 6b3064e2ba fix(server): add relationTargetFieldMetadataId column early in upgrade sequence (#20664)
## Summary

Cross-version upgrade fails at the 2.3
`DropMessageDirectionFieldCommand` stage:

```
[QueryFailedError] column ViewFilterEntity.relationTargetFieldMetadataId does not exist
  at WorkspaceFlatViewFilterMapCacheService.computeForCache
```

(see
https://github.com/twentyhq/twenty-infra/actions/runs/25929264129/job/76219964380)

Same shape as #20584 (subFieldName), one column over.

### Root cause

1. The 2.3 `DropMessageDirectionFieldCommand` builds a workspace
migration that deletes a `fieldMetadata` (the `direction` field).
2. `WorkspaceMigrationRunnerService.run` walks the metadata cascade
graph and pulls `viewFilter` into the dependency set because
`viewFilter` is the inverse one-to-many of `fieldMetadata`.
3. That maps to cache keys → `flatViewFilterMaps` gets requested →
`WorkspaceFlatViewFilterMapCacheService.computeForCache` runs.
4. `computeForCache` does `viewFilterRepository.find({ where: {
workspaceId }, withDeleted: true })` with no `select`, so TypeORM emits
a SELECT that includes `relationTargetFieldMetadataId` — column only
added by the 2.6 fast instance command `1798000005000`, not yet run at
the 2.3 stage. 💥

### Why v2.5.0 / v2.5.1 passed

They didn't include #20527 (one-hop relation filters, May 14), which
added `relationTargetFieldMetadataId` to `ViewFilterEntity` and the 2.6
instance command. The CI base image (v1.22) seeded the DB, then the
v2.5.0/v2.5.1 container ran upgrade commands against an entity that
didn't yet know about this column.
2026-05-18 10:42:40 +02:00
Charles BochetandGitHub a321e24839 fix(server): scope workspace findOne in incrementMetadataVersion (#20660)
## Summary

Cross-version upgrade fails at the 2.1
`GateExportImportCommandMenuItemsByPermissionFlagCommand` stage:

```
[GateExportImportCommandMenuItemsByPermissionFlagCommand] Found 3 command menu item(s) to update for workspace ...
error: column WorkspaceEntity.isInternalMessagesImportEnabled does not exist
```

(see
https://github.com/twentyhq/twenty-infra/actions/runs/25929264129/job/76219964380)

### Root cause

Same class of bug as #20581 and #20583, one layer deeper in the call
graph.

1. The 2.1 workspace command emits a `commandMenuItem` migration (3
items differ from the current standard expressions).
2. After the migration commits,
`WorkspaceMigrationRunnerService.invalidateCache` walks the
related-for-validation metadata for `commandMenuItem`, which includes
`objectMetadata`. That puts `flatObjectMetadataMaps` in the keys set.
3. `getLegacyCacheInvalidationPromises` sees `flatObjectMetadataMaps` in
the keys and calls
`WorkspaceMetadataVersionService.incrementMetadataVersion(workspaceId)`.
4. `incrementMetadataVersion` did a bare `findOne` on `WorkspaceEntity`
with no `select` → TypeORM emits a SELECT for every column declared on
the entity → hits `isInternalMessagesImportEnabled` (added by #20457),
whose DB column is only created by the 2.5 fast instance command
`1778525104406-add-is-internal-messages-import-enabled`, which has not
run yet at the 2.1 stage. 💥

### Fix

The function only reads `workspace.metadataVersion`, so narrow the
`select` to `['id', 'metadataVersion']`. No behavior change.

```diff
 async incrementMetadataVersion(workspaceId: string): Promise<void> {
   const workspace = await this.workspaceRepository.findOne({
+    select: ['id', 'metadataVersion'],
     where: { id: workspaceId },
     withDeleted: true,
   });
```
2026-05-18 10:36:33 +02:00
nitinandGitHub 3717df34be fix(twenty-front): anchor body text color to theme var (#20622)
Fixes #20607.
also fixes https://github.com/twentyhq/twenty/issues/20627


Front Components rendered via Remote DOM produced black-on-dark text in
dark mode for any unstyled element. `body` already anchored `background`
to a theme var; the matching `color` rule was missing, so unstyled
subtrees fell through to browser default `#000`.

before - 

<img width="850" height="526" alt="CleanShot 2026-05-16 at 16 31 49@2x"
src="https://github.com/user-attachments/assets/ca21359c-d1d1-4367-831e-f694673757e5"
/>

<img width="840" height="1694" alt="CleanShot 2026-05-16 at 16 32 02@2x"
src="https://github.com/user-attachments/assets/ed0891b5-1b97-4499-bb52-9e31c4cabf11"
/>

after - 

<img width="828" height="514" alt="CleanShot 2026-05-16 at 16 30 50@2x"
src="https://github.com/user-attachments/assets/a22d1f1b-a79b-454c-8c05-5c7c00157b2c"
/>

<img width="852" height="1674" alt="CleanShot 2026-05-16 at 16 31 07@2x"
src="https://github.com/user-attachments/assets/9b1dc19e-ccc5-472b-9184-59fa9b8832f8"
/>
2026-05-18 10:29:22 +02:00
Shubham SinghandGitHub 140dceebd1 fix(front): use theme-aware color for side panel title (#20645)
## Summary
- Added `color: ${themeCssVariables.font.color.primary}` to
`StyledPageInfoTitleContainer` in `SidePanelPageInfoLayout.tsx`
- The "Update records" title had no explicit color, so it didn't adapt
to dark mode and was nearly invisible against the dark background
- Now correctly uses the theme-aware primary font color

## Changes
-
`packages/twenty-front/src/modules/side-panel/components/SidePanelPageInfoLayout.tsx`

Fixes #20627
2026-05-18 10:25:34 +02:00
4d2ceaf70a i18n - translations (#20661)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-05-18 10:20:30 +02:00
Félix MalfaitandGitHub 6b49a14b9f feat(auth): set 50-character maximum length on passwords (#20655)
## Summary
- Cap password length at 50 characters in the shared regex used by
sign-up, password reset, and password change (both `twenty-front` and
`twenty-server`).
- Update the user-facing validation message on sign-up and password
reset to mention both the 8 min and 50 max bounds.
- Extend the `PASSWORD_REGEX` unit test to cover the new upper bound.

The cap also prevents unbounded inputs from reaching bcrypt, which
silently truncates passwords above 72 bytes and can mask user-visible
bugs.

## Test plan
- [x] `npx jest src/modules/auth/utils/__tests__/passwordRegex.test.ts`
passes (8-char min and 50-char max).
- [ ] Sign up with a 51-character password — form rejects with "Password
must be between 8 and 50 characters".
- [ ] Sign up with an 8–50 character password — succeeds.
- [ ] Password reset rejects a 51-character password with the same
message.
- [ ] Existing users with longer passwords (if any pre-exist) can still
sign in (the regex only gates write paths: sign-up, change, reset).
2026-05-18 10:12:19 +02:00
62b347fc74 chore: sync AI model catalog from models.dev (#20620)
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 <[email protected]>
2026-05-16 08:45:07 +02:00
Charles BochetandGitHub cf4b4455d3 fix(server): normalize composite defaultValues in manifest converter (unblock app re-install on 2.5-normalized workspaces) (#20615)
## Context

The runtime create-field path and the v2.5
`NormalizeCompositeFieldDefaultsCommand` workspace upgrade both run
composite `defaultValue`s through `nullifyEmptyCompositeDefaultValue`.
The manifest install/sync path was the only write path that skipped it:
[`fromFieldManifestToUniversalFlatFieldMetadata`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-field-manifest-to-universal-flat-field-metadata.util.ts)
passed `fieldManifest.defaultValue` through verbatim.

For the SDK-emitted ACTOR system fields (`createdBy` / `updatedBy`),
`twenty-sdk` ships `{ name: "''", source: "'MANUAL'" }`. After the
runtime or the 2.5 normalize command stores them, the workspace row
holds the canonical four-key form `{ context: null, name: null, source:
"'MANUAL'", workspaceMemberId: null }`. The next install computes its TO
map from the manifest, still gets the raw two-key shape, and diffs it
against the normalized FROM. The dispatcher emits a `defaultValue`
update on each system actor field; the flat-field-metadata validator
rejects it with `FIELD_MUTATION_NOT_ALLOWED`, blocking every re-install
of any application that defines a custom object on a v2.5-normalized
workspace.


## Fix

Normalize composite `defaultValue`s inside the converter, reusing the
same `nullifyEmptyCompositeDefaultValue` helper the three other write
paths already share:

-
[`get-default-flat-field-metadata-from-create-field-input.util.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util.ts)
— `createOneObject` and `createOneField` GraphQL paths.
-
[`sanitize-raw-update-field-input.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/sanitize-raw-update-field-input.ts)
— `updateOneField` GraphQL path.
-
[`2-5-workspace-command-1778000001000-normalize-composite-field-defaults.command.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/database/commands/upgrade-version-command/2-5/2-5-workspace-command-1778000001000-normalize-composite-field-defaults.command.ts)
— the upgrade backfill that introduced the divergence.

After the fix, the four write paths agree on the canonical shape, so
re-installs are no-ops on system actor fields regardless of when the 2.5
normalize command ran. Non-composite types pass through unchanged.

## Test

New spec
`from-field-manifest-to-universal-flat-field-metadata.util.spec.ts`
covers:

- Empty-name actor defaults are normalized to the four-key canonical
shape.
- The converter is idempotent: feeding its own output back in produces
the same result (so two consecutive syncs of the same manifest never
emit a `defaultValue` update).
- When the manifest omits `defaultValue`, the converter falls back to
`generateDefaultValue` and normalizes the result.
- Non-composite defaults pass through unchanged.

```
PASS  src/engine/core-modules/application/application-manifest/converters/__tests__/from-field-manifest-to-universal-flat-field-metadata.util.spec.ts
  fromFieldManifestToUniversalFlatFieldMetadata
    composite defaultValue normalization
      ✓ normalizes empty-name actor defaults to the canonical four-key shape
      ✓ is idempotent: re-running the converter on its own output yields the same defaultValue
      ✓ falls back to the generated default and normalizes it when defaultValue is omitted
      ✓ leaves non-composite defaults untouched
Tests: 4 passed
```

## CI gap that let this through

The integration suites covering manifest install (`appDevOnce` against
the test workspace) never re-installed an existing app on a workspace
whose composite fields had already been put through the 2.5 normalize
command. They synced once, then ran assertions on the resulting state;
the second sync that would have re-triggered the `defaultValue` diff was
never exercised.

If we want to catch this class of regression at the integration level
too, we'd add a test that (1) syncs an app whose manifest includes an
ACTOR system field with the raw SDK shape, (2) invokes
`NormalizeCompositeFieldDefaultsCommand` directly on the test workspace,
(3) re-syncs the same manifest, and (4) asserts no
`FIELD_MUTATION_NOT_ALLOWED` errors. The unit-level idempotency check in
this PR is the minimal version of that same coverage. Happy to ship that
integration spec in a follow-up if it'd help.
2026-05-15 18:31:48 +02:00
268fccca29 i18n - translations (#20609)
Created by Github action

Co-authored-by: github-actions <[email protected]>
2026-05-15 16:51:02 +02:00
c938fbf4d6 feat(twenty-front): relation traversal in filter dropdown (stacked) (#20533)
**Stacked on #20527** 




https://github.com/user-attachments/assets/48995655-401a-4c35-8094-e88da8408bdd





## Summary

Surfaces the one-hop relation traversal added in #20527 through the
existing **composite sub-field dropdown pattern**. Clicking a
MANY_TO_ONE relation field in the "+ Filter" picker now opens the same
second-level dropdown that composite fields (FULL_NAME, ADDRESS,
CURRENCY, etc.) already use — populated with the target object's
filterable fields. Picking one (e.g. `Company → Name`) builds a filter
that serializes to the nested GraphQL filter the backend now accepts: `{
company: { name: { ilike: "%X%" } } }`.

No new components. The whole feature reuses
`AdvancedFilterSubFieldSelectMenu` + the existing
`subFieldNameUsedInDropdownComponentState` + the existing `MenuItem
hasSubMenu` indicator. Only the conditions that gate the sub-menu (and
the sub-menu's content for relations) were broadened.

## What landed

| File | Change |
|---|---|
| `ObjectFilterDropdownFilterSelectMenuItem` | Sub-menu chevron now
shows on MANY_TO_ONE relations (`isManyToOneRelationField` util). |
| `AdvancedFilterFieldSelectMenu` | Relation clicks open the sub-menu
alongside composite clicks. |
| `AdvancedFilterSubFieldSelectMenu` | New branch: when the sub-menu
type is `'RELATION'`, render the target object's filterable fields via
`useFilterableFieldMetadataItems(targetObjectMetadataId)`. Composite
logic untouched. |
| `objectFilterDropdownSubMenuFieldType` state | Widened to accept a
`'RELATION'` sentinel. Role-permissions sub-field menu narrows it back
out (it doesn't traverse relations). |
| `useSelectFieldUsedInAdvancedFilterDropdown` | New optional
`targetFieldMetadataItem` arg. When present, the stored RecordFilter's
`type` is the target field's type so the operand picker and value input
render the target's operands (`'TEXT'` operators when filtering
`company.name`, etc.). |
| `turnRecordFilterIntoGqlOperationFilter` (shared) | When the filter
targets a `RELATION` field with a `subFieldName`, synthesize a
field-metadata for the target, recurse to build the inner filter, then
wrap it under the relation field's name → `{ relationName: {
targetFieldName: { ...operator } } }`. |

`RecordFilter.subFieldName` stays narrowly typed as
`CompositeFieldSubFieldName` so the wide downstream consumers
(`shouldShowFilterTextInput`, composite handlers in the serializer,
etc.) don't change. The relation target field's name is stored through a
narrowly-scoped cast at the dropdown's storage point — the serializer
checks `filter.type === 'RELATION'` before interpreting it as a target
field name, so the cast can't be mis-read by composite-only code paths.

## Test plan

- [ ] Open a table view on People, click "+ Filter", click "Company" →
sub-menu opens with Company's filterable fields
- [ ] Pick "Name" → operand picker shows TEXT operators (Contains,
Equals, …)
- [ ] Type "Airbnb" → filter applies, table shows people whose company
name contains "Airbnb"
- [ ] Verify network tab: the GraphQL filter variable is `{ company: {
name: { ilike: "%Airbnb%" } } }`
- [ ] Same flow with a composite target field (e.g. `Company →
annualRecurringRevenue → amountMicros`) — should work end-to-end
(backend supports composite-within-relation; #20527 has an integration
test covering this)
- [ ] Composite fields (FULL_NAME, ADDRESS) still open their normal
sub-menu and filter correctly — no regression
- [ ] Role-permissions field-select sub-field menu is unaffected (it
bails out early on the RELATION sentinel)

## Out of scope

- ONE_TO_MANY traversal (no backend support yet)
- Aggregates (`people.count > 5`)
- Persisting relation-traversal filters into a saved view (ViewFilter
has no `relationPath` column yet; that's a separate slice)
- REST API DSL changes
- AI Tools

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 <[email protected]>
2026-05-15 16:42:47 +02:00
Charles BochetandGitHub eca92ca559 fix(server): rebuild unique phone indexes drops legacy non-empty partial WHERE clause (#20606)
## Summary

`RebuildUniquePhoneIndexesCommand` reuses each index's existing
`indexWhereClause` when recreating the physical index. For workspaces
whose unique phone indexes have a legacy clause like
`"primaryPhoneNumber" != ''` (created before PR #18024 hardened the
validator allowlist), the recreate path fails at
`validateAndReturnIndexWhereClause` because the clause isn't in
`ALLOWED_INDEX_WHERE_CLAUSES`.

Two workspaces are hitting this on the 2.5 upgrade:
- `3a797122-…` — `"companyPhonePrimaryPhoneNumber" != ''`
- `ea74716f-…` — `"phonesPrimaryPhoneNumber" != ''`

## Fix

Detect the legacy `"<col>" != ''` shape via a strict regex. When it's
there, before the existing drop+create, do three things inside the
workspace transaction:

1. **Normalize the data** that the legacy partial clause was masking —
`UPDATE "<schema>"."<table>" SET "<col>" = NULL WHERE "<col>" = ''` for
every column the index covers. Without this the next step would fail
because the new plain-unique index would see duplicate `''` values
across the rows the old partial clause was excluding.
2. **Null out `core."indexMetadata".indexWhereClause`** so the metadata
row matches what the UI would have created (`indexWhereClause: null`)
and doesn't carry the validator-rejected clause forward to any future
re-emit. Uses the same workspace `queryRunner` (Postgres lets one
connection write across schemas).
3. **Recreate** with an overridden flat index where `indexWhereClause:
null`. `createIndexInWorkspaceSchema` → `indexManager.createIndex` →
`validateAndReturnIndexWhereClause` short-circuits on null, no allowlist
check.

End state matches the shape a fresh "toggle unique in Settings UI"
creates: plain unique index, no `WHERE`, NULL semantics doing the
"exclude empty phones" work via PG's default NULL-distinct behaviour.

For indexes whose clause is already allowlisted (`"deletedAt" IS NULL`)
or null, behaviour is unchanged — just the column-list widening this
command already does.
2026-05-15 16:39:34 +02:00
170 changed files with 2245 additions and 730 deletions
@@ -23,9 +23,11 @@ jobs:
packages/twenty-sdk/**
packages/twenty-client-sdk/**
packages/twenty-shared/**
packages/twenty-server/**
!packages/twenty-sdk/package.json
!packages/twenty-client-sdk/package.json
!packages/twenty-shared/package.json
!packages/twenty-server/package.json
example-app-postcard:
needs: changed-files-check
@@ -83,6 +85,31 @@ jobs:
working-directory: packages/twenty-apps/examples/postcard
run: npx vitest run
- name: Configure remote for SDK CLI
run: |
mkdir -p ~/.twenty
cat > ~/.twenty/config.json <<EOF
{
"version": 1,
"remotes": {
"target": {
"apiUrl": "${TWENTY_API_URL}",
"apiKey": "${TWENTY_API_KEY}",
"accessToken": "${TWENTY_API_KEY}"
}
},
"defaultRemote": "target"
}
EOF
- name: Deploy postcard app (registry install path)
working-directory: packages/twenty-apps/examples/postcard
run: node ${{ github.workspace }}/packages/twenty-sdk/dist/cli.cjs deploy --remote target
- name: Install postcard app (registry install path)
working-directory: packages/twenty-apps/examples/postcard
run: node ${{ github.workspace }}/packages/twenty-sdk/dist/cli.cjs install --remote target
ci-example-app-postcard-status-check:
if: always() && !cancelled()
timeout-minutes: 5
@@ -771,6 +771,7 @@ type ViewFilter {
viewFilterGroupId: UUID
positionInViewFilterGroup: Float
subFieldName: String
relationTargetFieldMetadataId: UUID
viewId: UUID!
workspaceId: UUID!
createdAt: DateTime!
@@ -3416,6 +3417,7 @@ input CreateViewFilterInput {
viewFilterGroupId: UUID
positionInViewFilterGroup: Float
subFieldName: String
relationTargetFieldMetadataId: UUID
viewId: UUID!
}
@@ -3434,6 +3436,7 @@ input UpdateViewFilterInputUpdates {
viewFilterGroupId: UUID
positionInViewFilterGroup: Float
subFieldName: String
relationTargetFieldMetadataId: UUID
}
input DeleteViewFilterInput {
@@ -3522,6 +3525,7 @@ input UpsertViewWidgetViewFilterInput {
viewFilterGroupId: UUID
positionInViewFilterGroup: Float
subFieldName: String
relationTargetFieldMetadataId: UUID
}
input UpsertViewWidgetViewFilterGroupInput {
@@ -523,6 +523,7 @@ export interface ViewFilter {
viewFilterGroupId?: Scalars['UUID']
positionInViewFilterGroup?: Scalars['Float']
subFieldName?: Scalars['String']
relationTargetFieldMetadataId?: Scalars['UUID']
viewId: Scalars['UUID']
workspaceId: Scalars['UUID']
createdAt: Scalars['DateTime']
@@ -3451,6 +3452,7 @@ export interface ViewFilterGenqlSelection{
viewFilterGroupId?: boolean | number
positionInViewFilterGroup?: boolean | number
subFieldName?: boolean | number
relationTargetFieldMetadataId?: boolean | number
viewId?: boolean | number
workspaceId?: boolean | number
createdAt?: boolean | number
@@ -5973,7 +5975,7 @@ export interface CreateViewFilterGroupInput {id?: (Scalars['UUID'] | null),paren
export interface UpdateViewFilterGroupInput {id?: (Scalars['UUID'] | null),parentViewFilterGroupId?: (Scalars['UUID'] | null),logicalOperator?: (ViewFilterGroupLogicalOperator | null),positionInViewFilterGroup?: (Scalars['Float'] | null),viewId?: (Scalars['UUID'] | null)}
export interface CreateViewFilterInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],operand?: (ViewFilterOperand | null),value: Scalars['JSON'],viewFilterGroupId?: (Scalars['UUID'] | null),positionInViewFilterGroup?: (Scalars['Float'] | null),subFieldName?: (Scalars['String'] | null),viewId: Scalars['UUID']}
export interface CreateViewFilterInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],operand?: (ViewFilterOperand | null),value: Scalars['JSON'],viewFilterGroupId?: (Scalars['UUID'] | null),positionInViewFilterGroup?: (Scalars['Float'] | null),subFieldName?: (Scalars['String'] | null),relationTargetFieldMetadataId?: (Scalars['UUID'] | null),viewId: Scalars['UUID']}
export interface UpdateViewFilterInput {
/** The id of the view filter to update */
@@ -5981,7 +5983,7 @@ id: Scalars['UUID'],
/** The view filter to update */
update: UpdateViewFilterInputUpdates}
export interface UpdateViewFilterInputUpdates {fieldMetadataId?: (Scalars['UUID'] | null),operand?: (ViewFilterOperand | null),value?: (Scalars['JSON'] | null),viewFilterGroupId?: (Scalars['UUID'] | null),positionInViewFilterGroup?: (Scalars['Float'] | null),subFieldName?: (Scalars['String'] | null)}
export interface UpdateViewFilterInputUpdates {fieldMetadataId?: (Scalars['UUID'] | null),operand?: (ViewFilterOperand | null),value?: (Scalars['JSON'] | null),viewFilterGroupId?: (Scalars['UUID'] | null),positionInViewFilterGroup?: (Scalars['Float'] | null),subFieldName?: (Scalars['String'] | null),relationTargetFieldMetadataId?: (Scalars['UUID'] | null)}
export interface DeleteViewFilterInput {
/** The id of the view filter to delete. */
@@ -6013,7 +6015,7 @@ viewFieldId?: (Scalars['UUID'] | null),
/** The field metadata id. Used to create a new view field when viewFieldId is not provided. */
fieldMetadataId?: (Scalars['UUID'] | null),isVisible: Scalars['Boolean'],position: Scalars['Float'],size?: (Scalars['Float'] | null)}
export interface UpsertViewWidgetViewFilterInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],operand?: (ViewFilterOperand | null),value: Scalars['JSON'],viewFilterGroupId?: (Scalars['UUID'] | null),positionInViewFilterGroup?: (Scalars['Float'] | null),subFieldName?: (Scalars['String'] | null)}
export interface UpsertViewWidgetViewFilterInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],operand?: (ViewFilterOperand | null),value: Scalars['JSON'],viewFilterGroupId?: (Scalars['UUID'] | null),positionInViewFilterGroup?: (Scalars['Float'] | null),subFieldName?: (Scalars['String'] | null),relationTargetFieldMetadataId?: (Scalars['UUID'] | null)}
export interface UpsertViewWidgetViewFilterGroupInput {id?: (Scalars['UUID'] | null),parentViewFilterGroupId?: (Scalars['UUID'] | null),logicalOperator?: (ViewFilterGroupLogicalOperator | null),positionInViewFilterGroup?: (Scalars['Float'] | null)}
@@ -1521,6 +1521,9 @@ export default {
"subFieldName": [
1
],
"relationTargetFieldMetadataId": [
3
],
"viewId": [
3
],
@@ -8929,6 +8932,9 @@ export default {
"subFieldName": [
1
],
"relationTargetFieldMetadataId": [
3
],
"viewId": [
3
],
@@ -8966,6 +8972,9 @@ export default {
"subFieldName": [
1
],
"relationTargetFieldMetadataId": [
3
],
"__typename": [
1
]
@@ -9154,6 +9163,9 @@ export default {
"subFieldName": [
1
],
"relationTargetFieldMetadataId": [
3
],
"__typename": [
1
]
File diff suppressed because one or more lines are too long
+1
View File
@@ -4,6 +4,7 @@ body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: var(--t-background-tertiary);
color: var(--t-font-color-primary);
}
html {
+4 -4
View File
@@ -1963,7 +1963,7 @@ msgid "And"
msgstr "En"
#. js-lingui-id: KYvJeB
#: src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
#: src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx
msgid "Any {fieldLabel} field"
msgstr "Enige {fieldLabel}-veld"
@@ -11801,11 +11801,11 @@ msgstr "Wagwoord is gestel"
msgid "Password has been updated"
msgstr "Wagwoord is opgedateer"
#. js-lingui-id: 1oqxe6
#. js-lingui-id: BfLK2u
#: src/pages/auth/PasswordReset.tsx
#: src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts
msgid "Password must be min. 8 characters"
msgstr "Wagwoord moet minstens 8 karakters wees"
msgid "Password must be between 8 and 50 characters"
msgstr ""
#. js-lingui-id: mi6Rel
#: src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
+4 -4
View File
@@ -1963,7 +1963,7 @@ msgid "And"
msgstr "و"
#. js-lingui-id: KYvJeB
#: src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
#: src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx
msgid "Any {fieldLabel} field"
msgstr "أي حقل {fieldLabel}"
@@ -11801,11 +11801,11 @@ msgstr "تم تعيين كلمة المرور"
msgid "Password has been updated"
msgstr "تم تحديث كلمة السر"
#. js-lingui-id: 1oqxe6
#. js-lingui-id: BfLK2u
#: src/pages/auth/PasswordReset.tsx
#: src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts
msgid "Password must be min. 8 characters"
msgstr "يجب أن تكون كلمة المرور 8 أحرف على الأقل"
msgid "Password must be between 8 and 50 characters"
msgstr ""
#. js-lingui-id: mi6Rel
#: src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
+4 -4
View File
@@ -1963,7 +1963,7 @@ msgid "And"
msgstr "I"
#. js-lingui-id: KYvJeB
#: src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
#: src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx
msgid "Any {fieldLabel} field"
msgstr "Qualsevol camp {fieldLabel}"
@@ -11801,11 +11801,11 @@ msgstr "La contrasenya s'ha establert"
msgid "Password has been updated"
msgstr "La contrasenya s'ha actualitzat"
#. js-lingui-id: 1oqxe6
#. js-lingui-id: BfLK2u
#: src/pages/auth/PasswordReset.tsx
#: src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts
msgid "Password must be min. 8 characters"
msgstr "La contrasenya ha de tenir almenys 8 caràcters"
msgid "Password must be between 8 and 50 characters"
msgstr ""
#. js-lingui-id: mi6Rel
#: src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
+4 -4
View File
@@ -1963,7 +1963,7 @@ msgid "And"
msgstr "A"
#. js-lingui-id: KYvJeB
#: src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
#: src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx
msgid "Any {fieldLabel} field"
msgstr "Libovolné pole {fieldLabel}"
@@ -11801,11 +11801,11 @@ msgstr "Heslo bylo nastaveno"
msgid "Password has been updated"
msgstr "Heslo bylo aktualizováno"
#. js-lingui-id: 1oqxe6
#. js-lingui-id: BfLK2u
#: src/pages/auth/PasswordReset.tsx
#: src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts
msgid "Password must be min. 8 characters"
msgstr "Heslo musí mít alespoň 8 znaků"
msgid "Password must be between 8 and 50 characters"
msgstr ""
#. js-lingui-id: mi6Rel
#: src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
+4 -4
View File
@@ -1963,7 +1963,7 @@ msgid "And"
msgstr "Og"
#. js-lingui-id: KYvJeB
#: src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
#: src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx
msgid "Any {fieldLabel} field"
msgstr "Ethvert {fieldLabel}-felt"
@@ -11801,11 +11801,11 @@ msgstr "Adgangskoden er blevet sat"
msgid "Password has been updated"
msgstr "Password er blevet opdateret"
#. js-lingui-id: 1oqxe6
#. js-lingui-id: BfLK2u
#: src/pages/auth/PasswordReset.tsx
#: src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts
msgid "Password must be min. 8 characters"
msgstr "Adgangskode skal være min. 8 tegn"
msgid "Password must be between 8 and 50 characters"
msgstr ""
#. js-lingui-id: mi6Rel
#: src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
+4 -4
View File
@@ -1963,7 +1963,7 @@ msgid "And"
msgstr "Und"
#. js-lingui-id: KYvJeB
#: src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
#: src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx
msgid "Any {fieldLabel} field"
msgstr "Beliebiges {fieldLabel}-Feld"
@@ -11801,11 +11801,11 @@ msgstr "Das Passwort wurde aktualisiert"
msgid "Password has been updated"
msgstr "Das Passwort wurde aktualisiert"
#. js-lingui-id: 1oqxe6
#. js-lingui-id: BfLK2u
#: src/pages/auth/PasswordReset.tsx
#: src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts
msgid "Password must be min. 8 characters"
msgstr "Passwort muss mind. 8 Zeichen lang sein"
msgid "Password must be between 8 and 50 characters"
msgstr ""
#. js-lingui-id: mi6Rel
#: src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
+4 -4
View File
@@ -1963,7 +1963,7 @@ msgid "And"
msgstr "Και"
#. js-lingui-id: KYvJeB
#: src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
#: src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx
msgid "Any {fieldLabel} field"
msgstr "Οποιοδήποτε πεδίο {fieldLabel}"
@@ -11801,11 +11801,11 @@ msgstr "Ο κωδικός πρόσβασης έχει ρυθμιστεί"
msgid "Password has been updated"
msgstr "Ο κωδικός έχει ενημερωθεί"
#. js-lingui-id: 1oqxe6
#. js-lingui-id: BfLK2u
#: src/pages/auth/PasswordReset.tsx
#: src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts
msgid "Password must be min. 8 characters"
msgstr "Ο κωδικός πρόσβασης πρέπει να είναι τουλάχιστον 8 χαρακτήρες"
msgid "Password must be between 8 and 50 characters"
msgstr ""
#. js-lingui-id: mi6Rel
#: src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
+4 -4
View File
@@ -1958,7 +1958,7 @@ msgid "And"
msgstr "And"
#. js-lingui-id: KYvJeB
#: src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
#: src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx
msgid "Any {fieldLabel} field"
msgstr "Any {fieldLabel} field"
@@ -11819,11 +11819,11 @@ msgstr "Password has been set"
msgid "Password has been updated"
msgstr "Password has been updated"
#. js-lingui-id: 1oqxe6
#. js-lingui-id: BfLK2u
#: src/pages/auth/PasswordReset.tsx
#: src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts
msgid "Password must be min. 8 characters"
msgstr "Password must be min. 8 characters"
msgid "Password must be between 8 and 50 characters"
msgstr "Password must be between 8 and 50 characters"
#. js-lingui-id: mi6Rel
#: src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
+4 -4
View File
@@ -1963,7 +1963,7 @@ msgid "And"
msgstr "Y"
#. js-lingui-id: KYvJeB
#: src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
#: src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx
msgid "Any {fieldLabel} field"
msgstr "Cualquier campo {fieldLabel}"
@@ -11801,11 +11801,11 @@ msgstr "La contraseña ha sido establecida"
msgid "Password has been updated"
msgstr "La contraseña ha sido actualizada"
#. js-lingui-id: 1oqxe6
#. js-lingui-id: BfLK2u
#: src/pages/auth/PasswordReset.tsx
#: src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts
msgid "Password must be min. 8 characters"
msgstr "La contraseña debe tener al menos 8 caracteres"
msgid "Password must be between 8 and 50 characters"
msgstr ""
#. js-lingui-id: mi6Rel
#: src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
+4 -4
View File
@@ -1963,7 +1963,7 @@ msgid "And"
msgstr "Ja"
#. js-lingui-id: KYvJeB
#: src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
#: src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx
msgid "Any {fieldLabel} field"
msgstr "Mikä tahansa {fieldLabel}-kenttä"
@@ -11801,11 +11801,11 @@ msgstr "Salasana on asetettu"
msgid "Password has been updated"
msgstr "Salasana on päivitetty"
#. js-lingui-id: 1oqxe6
#. js-lingui-id: BfLK2u
#: src/pages/auth/PasswordReset.tsx
#: src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts
msgid "Password must be min. 8 characters"
msgstr "Salasanassa on oltava vähintään 8 merkkiä"
msgid "Password must be between 8 and 50 characters"
msgstr ""
#. js-lingui-id: mi6Rel
#: src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
+4 -4
View File
@@ -1963,7 +1963,7 @@ msgid "And"
msgstr "Et"
#. js-lingui-id: KYvJeB
#: src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
#: src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx
msgid "Any {fieldLabel} field"
msgstr "Tout champ {fieldLabel}"
@@ -11801,11 +11801,11 @@ msgstr "Le mot de passe a été défini"
msgid "Password has been updated"
msgstr "Le mot de passe a été mis à jour"
#. js-lingui-id: 1oqxe6
#. js-lingui-id: BfLK2u
#: src/pages/auth/PasswordReset.tsx
#: src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts
msgid "Password must be min. 8 characters"
msgstr "Le mot de passe doit comporter au minimum 8 caractères"
msgid "Password must be between 8 and 50 characters"
msgstr ""
#. js-lingui-id: mi6Rel
#: src/modules/auth/sign-in-up/hooks/useHandleResetPassword.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
File diff suppressed because one or more lines are too long
+4 -4
View File
@@ -1963,7 +1963,7 @@ msgid "And"
msgstr "וגם"
#. js-lingui-id: KYvJeB
#: src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
#: src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx
msgid "Any {fieldLabel} field"
msgstr "כל שדה {fieldLabel}"
@@ -11801,11 +11801,11 @@ msgstr "הסיסמה עודכנה"
msgid "Password has been updated"
msgstr "הסיסמה עודכנה"
#. js-lingui-id: 1oqxe6
#. js-lingui-id: BfLK2u
#: src/pages/auth/PasswordReset.tsx
#: src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts
msgid "Password must be min. 8 characters"
msgstr "הסיסמה חייבת להיות באורך מינימום 8 תווים"
msgid "Password must be between 8 and 50 characters"
msgstr ""
#. js-lingui-id: mi6Rel
#: src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
+4 -4
View File
@@ -1963,7 +1963,7 @@ msgid "And"
msgstr "És"
#. js-lingui-id: KYvJeB
#: src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
#: src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx
msgid "Any {fieldLabel} field"
msgstr "Bármely {fieldLabel} mező"
@@ -11801,11 +11801,11 @@ msgstr "A jelszót beállították"
msgid "Password has been updated"
msgstr "A jelszót frissítették"
#. js-lingui-id: 1oqxe6
#. js-lingui-id: BfLK2u
#: src/pages/auth/PasswordReset.tsx
#: src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts
msgid "Password must be min. 8 characters"
msgstr "A jelszónak legalább 8 karakter hosszúnak kell lennie"
msgid "Password must be between 8 and 50 characters"
msgstr ""
#. js-lingui-id: mi6Rel
#: src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
+4 -4
View File
@@ -1963,7 +1963,7 @@ msgid "And"
msgstr "E"
#. js-lingui-id: KYvJeB
#: src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
#: src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx
msgid "Any {fieldLabel} field"
msgstr "Qualsiasi campo {fieldLabel}"
@@ -11801,11 +11801,11 @@ msgstr "La password è stata impostata"
msgid "Password has been updated"
msgstr "La password è stata aggiornata"
#. js-lingui-id: 1oqxe6
#. js-lingui-id: BfLK2u
#: src/pages/auth/PasswordReset.tsx
#: src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts
msgid "Password must be min. 8 characters"
msgstr "La password deve essere di almeno 8 caratteri"
msgid "Password must be between 8 and 50 characters"
msgstr ""
#. js-lingui-id: mi6Rel
#: src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
+4 -4
View File
@@ -1963,7 +1963,7 @@ msgid "And"
msgstr "および"
#. js-lingui-id: KYvJeB
#: src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
#: src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx
msgid "Any {fieldLabel} field"
msgstr "任意の {fieldLabel} フィールド"
@@ -11801,11 +11801,11 @@ msgstr "パスワードが設定されました"
msgid "Password has been updated"
msgstr "パスワードが更新されました"
#. js-lingui-id: 1oqxe6
#. js-lingui-id: BfLK2u
#: src/pages/auth/PasswordReset.tsx
#: src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts
msgid "Password must be min. 8 characters"
msgstr "パスワードは8文字以上である必要があります"
msgid "Password must be between 8 and 50 characters"
msgstr ""
#. js-lingui-id: mi6Rel
#: src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
+4 -4
View File
@@ -1963,7 +1963,7 @@ msgid "And"
msgstr "및"
#. js-lingui-id: KYvJeB
#: src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
#: src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx
msgid "Any {fieldLabel} field"
msgstr "모든 {fieldLabel} 필드"
@@ -11801,11 +11801,11 @@ msgstr "비밀번호가 설정되었습니다"
msgid "Password has been updated"
msgstr "비밀번호가 업데이트되었습니다"
#. js-lingui-id: 1oqxe6
#. js-lingui-id: BfLK2u
#: src/pages/auth/PasswordReset.tsx
#: src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts
msgid "Password must be min. 8 characters"
msgstr "비밀번호는 최소 8자여야 합니다"
msgid "Password must be between 8 and 50 characters"
msgstr ""
#. js-lingui-id: mi6Rel
#: src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
+4 -4
View File
@@ -1963,7 +1963,7 @@ msgid "And"
msgstr "En"
#. js-lingui-id: KYvJeB
#: src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
#: src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx
msgid "Any {fieldLabel} field"
msgstr "Elk {fieldLabel}-veld"
@@ -11801,11 +11801,11 @@ msgstr "Wachtwoord is ingesteld"
msgid "Password has been updated"
msgstr "Wachtwoord is geüpdated"
#. js-lingui-id: 1oqxe6
#. js-lingui-id: BfLK2u
#: src/pages/auth/PasswordReset.tsx
#: src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts
msgid "Password must be min. 8 characters"
msgstr "Wachtwoord moet min. 8 tekens bevatten"
msgid "Password must be between 8 and 50 characters"
msgstr ""
#. js-lingui-id: mi6Rel
#: src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
+4 -4
View File
@@ -1963,7 +1963,7 @@ msgid "And"
msgstr "Og"
#. js-lingui-id: KYvJeB
#: src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
#: src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx
msgid "Any {fieldLabel} field"
msgstr "Ethvert {fieldLabel}-felt"
@@ -11801,11 +11801,11 @@ msgstr "Passordet har blitt satt"
msgid "Password has been updated"
msgstr "Passordet har blitt oppdatert"
#. js-lingui-id: 1oqxe6
#. js-lingui-id: BfLK2u
#: src/pages/auth/PasswordReset.tsx
#: src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts
msgid "Password must be min. 8 characters"
msgstr "Passord må være minst 8 tegn"
msgid "Password must be between 8 and 50 characters"
msgstr ""
#. js-lingui-id: mi6Rel
#: src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
+4 -4
View File
@@ -1963,7 +1963,7 @@ msgid "And"
msgstr "I"
#. js-lingui-id: KYvJeB
#: src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
#: src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx
msgid "Any {fieldLabel} field"
msgstr "Dowolne pole {fieldLabel}"
@@ -11801,11 +11801,11 @@ msgstr "Hasło zostało ustawione"
msgid "Password has been updated"
msgstr "Hasło zostało zaktualizowane"
#. js-lingui-id: 1oqxe6
#. js-lingui-id: BfLK2u
#: src/pages/auth/PasswordReset.tsx
#: src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts
msgid "Password must be min. 8 characters"
msgstr "Hasło musi mieć min. 8 znaków"
msgid "Password must be between 8 and 50 characters"
msgstr ""
#. js-lingui-id: mi6Rel
#: src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
@@ -1958,7 +1958,7 @@ msgid "And"
msgstr ""
#. js-lingui-id: KYvJeB
#: src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
#: src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx
msgid "Any {fieldLabel} field"
msgstr ""
@@ -11796,10 +11796,10 @@ msgstr ""
msgid "Password has been updated"
msgstr ""
#. js-lingui-id: 1oqxe6
#. js-lingui-id: BfLK2u
#: src/pages/auth/PasswordReset.tsx
#: src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts
msgid "Password must be min. 8 characters"
msgid "Password must be between 8 and 50 characters"
msgstr ""
#. js-lingui-id: mi6Rel
+4 -4
View File
@@ -1963,7 +1963,7 @@ msgid "And"
msgstr "E"
#. js-lingui-id: KYvJeB
#: src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
#: src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx
msgid "Any {fieldLabel} field"
msgstr "Qualquer campo {fieldLabel}"
@@ -11801,11 +11801,11 @@ msgstr "A senha foi definida"
msgid "Password has been updated"
msgstr "A senha foi atualizada"
#. js-lingui-id: 1oqxe6
#. js-lingui-id: BfLK2u
#: src/pages/auth/PasswordReset.tsx
#: src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts
msgid "Password must be min. 8 characters"
msgstr "A senha deve ter no mínimo 8 caracteres"
msgid "Password must be between 8 and 50 characters"
msgstr ""
#. js-lingui-id: mi6Rel
#: src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
+4 -4
View File
@@ -1963,7 +1963,7 @@ msgid "And"
msgstr "E"
#. js-lingui-id: KYvJeB
#: src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
#: src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx
msgid "Any {fieldLabel} field"
msgstr "Qualquer campo {fieldLabel}"
@@ -11801,11 +11801,11 @@ msgstr "A palavra-passe foi definida"
msgid "Password has been updated"
msgstr "A palavra-passe foi atualizada"
#. js-lingui-id: 1oqxe6
#. js-lingui-id: BfLK2u
#: src/pages/auth/PasswordReset.tsx
#: src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts
msgid "Password must be min. 8 characters"
msgstr "A palavra-passe deve ter no mínimo 8 caracteres"
msgid "Password must be between 8 and 50 characters"
msgstr ""
#. js-lingui-id: mi6Rel
#: src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
+4 -4
View File
@@ -1963,7 +1963,7 @@ msgid "And"
msgstr "Și"
#. js-lingui-id: KYvJeB
#: src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
#: src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx
msgid "Any {fieldLabel} field"
msgstr "Orice câmp {fieldLabel}"
@@ -11801,11 +11801,11 @@ msgstr "Parola a fost setată"
msgid "Password has been updated"
msgstr "Parola a fost actualizată"
#. js-lingui-id: 1oqxe6
#. js-lingui-id: BfLK2u
#: src/pages/auth/PasswordReset.tsx
#: src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts
msgid "Password must be min. 8 characters"
msgstr "Parola trebuie să aibă minimum 8 caractere"
msgid "Password must be between 8 and 50 characters"
msgstr ""
#. js-lingui-id: mi6Rel
#: src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
Binary file not shown.
+4 -4
View File
@@ -1963,7 +1963,7 @@ msgid "And"
msgstr "И"
#. js-lingui-id: KYvJeB
#: src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
#: src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx
msgid "Any {fieldLabel} field"
msgstr "Било које поље {fieldLabel}"
@@ -11801,11 +11801,11 @@ msgstr "Лозинка је постављена"
msgid "Password has been updated"
msgstr "Лозинка је ажурирана"
#. js-lingui-id: 1oqxe6
#. js-lingui-id: BfLK2u
#: src/pages/auth/PasswordReset.tsx
#: src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts
msgid "Password must be min. 8 characters"
msgstr "Лозинка мора имати најмање 8 знакова"
msgid "Password must be between 8 and 50 characters"
msgstr ""
#. js-lingui-id: mi6Rel
#: src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
+4 -4
View File
@@ -1963,7 +1963,7 @@ msgid "And"
msgstr "Och"
#. js-lingui-id: KYvJeB
#: src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
#: src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx
msgid "Any {fieldLabel} field"
msgstr "Vilket {fieldLabel}-fält som helst"
@@ -11803,11 +11803,11 @@ msgstr "Lösenordet har ställts in"
msgid "Password has been updated"
msgstr "Lösenordet har uppdaterats"
#. js-lingui-id: 1oqxe6
#. js-lingui-id: BfLK2u
#: src/pages/auth/PasswordReset.tsx
#: src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts
msgid "Password must be min. 8 characters"
msgstr "Lösenord måste vara minst 8 tecken"
msgid "Password must be between 8 and 50 characters"
msgstr ""
#. js-lingui-id: mi6Rel
#: src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
+4 -4
View File
@@ -1963,7 +1963,7 @@ msgid "And"
msgstr "Ve"
#. js-lingui-id: KYvJeB
#: src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
#: src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx
msgid "Any {fieldLabel} field"
msgstr "Herhangi bir {fieldLabel} alanı"
@@ -11801,11 +11801,11 @@ msgstr "Parola ayarlandı"
msgid "Password has been updated"
msgstr "Parola güncellendi"
#. js-lingui-id: 1oqxe6
#. js-lingui-id: BfLK2u
#: src/pages/auth/PasswordReset.tsx
#: src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts
msgid "Password must be min. 8 characters"
msgstr "Parola en az 8 karakter olmalıdır"
msgid "Password must be between 8 and 50 characters"
msgstr ""
#. js-lingui-id: mi6Rel
#: src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
+4 -4
View File
@@ -1963,7 +1963,7 @@ msgid "And"
msgstr "Та"
#. js-lingui-id: KYvJeB
#: src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
#: src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx
msgid "Any {fieldLabel} field"
msgstr "Будь-яке поле {fieldLabel}"
@@ -11801,11 +11801,11 @@ msgstr "Пароль було встановлено"
msgid "Password has been updated"
msgstr "Пароль було оновлено"
#. js-lingui-id: 1oqxe6
#. js-lingui-id: BfLK2u
#: src/pages/auth/PasswordReset.tsx
#: src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts
msgid "Password must be min. 8 characters"
msgstr "Пароль має містити щонайменше 8 символів"
msgid "Password must be between 8 and 50 characters"
msgstr ""
#. js-lingui-id: mi6Rel
#: src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
+4 -4
View File
@@ -1963,7 +1963,7 @@ msgid "And"
msgstr "Và"
#. js-lingui-id: KYvJeB
#: src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
#: src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx
msgid "Any {fieldLabel} field"
msgstr "Bất kỳ trường {fieldLabel} nào"
@@ -11801,11 +11801,11 @@ msgstr "Mật khẩu đã được thiết lập"
msgid "Password has been updated"
msgstr "Mật khẩu đã được cập nhật"
#. js-lingui-id: 1oqxe6
#. js-lingui-id: BfLK2u
#: src/pages/auth/PasswordReset.tsx
#: src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts
msgid "Password must be min. 8 characters"
msgstr "Mật khẩu phải có tối thiểu 8 ký tự"
msgid "Password must be between 8 and 50 characters"
msgstr ""
#. js-lingui-id: mi6Rel
#: src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
+4 -4
View File
@@ -1963,7 +1963,7 @@ msgid "And"
msgstr "且"
#. js-lingui-id: KYvJeB
#: src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
#: src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx
msgid "Any {fieldLabel} field"
msgstr "任意 {fieldLabel} 字段"
@@ -11801,11 +11801,11 @@ msgstr "密码已更新"
msgid "Password has been updated"
msgstr "密码已更新"
#. js-lingui-id: 1oqxe6
#. js-lingui-id: BfLK2u
#: src/pages/auth/PasswordReset.tsx
#: src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts
msgid "Password must be min. 8 characters"
msgstr "密码长度至少为 8 个字符"
msgid "Password must be between 8 and 50 characters"
msgstr ""
#. js-lingui-id: mi6Rel
#: src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
+4 -4
View File
@@ -1963,7 +1963,7 @@ msgid "And"
msgstr "和"
#. js-lingui-id: KYvJeB
#: src/modules/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu.tsx
#: src/modules/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu.tsx
msgid "Any {fieldLabel} field"
msgstr "任何 {fieldLabel} 欄位"
@@ -11801,11 +11801,11 @@ msgstr "密碼已設定"
msgid "Password has been updated"
msgstr "密碼已更新"
#. js-lingui-id: 1oqxe6
#. js-lingui-id: BfLK2u
#: src/pages/auth/PasswordReset.tsx
#: src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts
msgid "Password must be min. 8 characters"
msgstr "密碼至少需 8 個字元"
msgid "Password must be between 8 and 50 characters"
msgstr ""
#. js-lingui-id: mi6Rel
#: src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
@@ -26,7 +26,10 @@ const makeValidationSchema = (signInUpStep: SignInUpStep) =>
signInUpStep === SignInUpStep.Password
? z
.string()
.regex(PASSWORD_REGEX, t`Password must be min. 8 characters`)
.regex(
PASSWORD_REGEX,
t`Password must be between 8 and 50 characters`,
)
: z.string().optional(),
captchaToken: z.string().default(''),
})
@@ -8,4 +8,12 @@ describe('PASSWORD_REGEX', () => {
expect(PASSWORD_REGEX.test(validPassword)).toBe(true);
expect(PASSWORD_REGEX.test(invalidPassword)).toBe(false);
});
it('should match passwords with at most 50 characters', () => {
const validPassword = 'a'.repeat(50);
const invalidPassword = 'a'.repeat(51);
expect(PASSWORD_REGEX.test(validPassword)).toBe(true);
expect(PASSWORD_REGEX.test(invalidPassword)).toBe(false);
});
});
@@ -1 +1 @@
export const PASSWORD_REGEX = /^.{8,}$/;
export const PASSWORD_REGEX = /^.{8,50}$/;
@@ -89,12 +89,17 @@ export const buildHeadlessCommandContextApi = ({
? (currentWorkspaceMember?.timeZone ?? systemTimeZone)
: systemTimeZone;
const flattenedFieldMetadataItems = objectMetadataItems.flatMap(
(objectMetadataItem) => objectMetadataItem.fields,
);
const graphqlFilter = isDefined(objectMetadataItem)
? computeContextStoreFilters({
contextStoreTargetedRecordsRule: targetedRecordsRule,
contextStoreFilters: filters,
contextStoreFilterGroups: filterGroups,
objectMetadataItem,
flattenedFieldMetadataItems,
filterValueDependencies: {
currentWorkspaceMemberId: currentWorkspaceMember?.id,
timeZone: userTimezone,
@@ -72,6 +72,7 @@ export const useFindManyRecordsSelectedInContextStore = ({
contextStoreFilters,
contextStoreFilterGroups,
objectMetadataItem,
flattenedFieldMetadataItems: allFieldMetadataItems,
filterValueDependencies,
contextStoreAnyFieldFilterValue,
});
@@ -29,6 +29,7 @@ describe('computeContextStoreFilters', () => {
contextStoreFilters: [],
contextStoreFilterGroups: [],
objectMetadataItem: personObjectMetadataItem,
flattenedFieldMetadataItems: personObjectMetadataItem.fields,
filterValueDependencies: mockFilterValueDependencies,
contextStoreAnyFieldFilterValue: '',
});
@@ -73,6 +74,7 @@ describe('computeContextStoreFilters', () => {
contextStoreFilters,
contextStoreFilterGroups: [],
objectMetadataItem: personObjectMetadataItem,
flattenedFieldMetadataItems: personObjectMetadataItem.fields,
filterValueDependencies: mockFilterValueDependencies,
contextStoreAnyFieldFilterValue: '',
});
@@ -1,7 +1,9 @@
import { type ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets';
import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables';
import {
type RecordFilterValueDependencies,
@@ -17,6 +19,7 @@ type ComputeContextStoreFiltersProps = {
contextStoreFilters: RecordFilter[];
contextStoreFilterGroups: RecordFilterGroup[];
objectMetadataItem: EnrichedObjectMetadataItem;
flattenedFieldMetadataItems: FieldMetadataItem[];
filterValueDependencies: RecordFilterValueDependencies;
contextStoreAnyFieldFilterValue: string;
};
@@ -26,6 +29,7 @@ export const computeContextStoreFilters = ({
contextStoreFilters,
contextStoreFilterGroups,
objectMetadataItem,
flattenedFieldMetadataItems,
filterValueDependencies,
contextStoreAnyFieldFilterValue,
}: ComputeContextStoreFiltersProps) => {
@@ -37,12 +41,18 @@ export const computeContextStoreFilters = ({
fields: objectMetadataItem.fields,
});
const fields = augmentFieldsWithRelationTargets({
baseFields: objectMetadataItem?.fields ?? [],
recordFilters: contextStoreFilters,
allFieldMetadataItems: flattenedFieldMetadataItems,
});
if (contextStoreTargetedRecordsRule.mode === 'exclusion') {
queryFilter = makeAndFilterVariables([
recordGqlFilterForAnyFieldFilter,
computeRecordGqlOperationFilter({
filterValueDependencies,
fields: objectMetadataItem?.fields ?? [],
fields,
recordFilters: contextStoreFilters,
recordFilterGroups: contextStoreFilterGroups,
}),
@@ -71,7 +81,7 @@ export const computeContextStoreFilters = ({
},
computeRecordGqlOperationFilter({
filterValueDependencies,
fields: objectMetadataItem?.fields ?? [],
fields,
recordFilters: contextStoreFilters,
recordFilterGroups: contextStoreFilterGroups,
}),
@@ -1,6 +1,10 @@
import { SORTABLE_FIELD_METADATA_TYPES } from '@/object-metadata/constants/SortableFieldMetadataTypes';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
import { isManyToOneRelationField } from '@/object-metadata/utils/isManyToOneRelationField';
import {
type FieldMetadataType,
type RelationType,
} from '~/generated-metadata/graphql';
type SortableFieldInput = {
isSystem?: boolean | null;
@@ -17,13 +21,9 @@ export const filterSortableFieldMetadataItems = (field: SortableFieldInput) => {
field.type,
);
const isRelationFieldSortable =
field.type === FieldMetadataType.RELATION &&
field.relation?.type === RelationType.MANY_TO_ONE;
return (
!isHiddenSystemField(field) &&
isFieldActive &&
(isFieldTypeSortable || isRelationFieldSortable)
(isFieldTypeSortable || isManyToOneRelationField(field))
);
};
@@ -0,0 +1,12 @@
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
type FieldWithRelation = {
type: FieldMetadataType;
relation?: { type: RelationType } | null;
};
export const isManyToOneRelationField = <T extends FieldWithRelation>(
field: T,
): field is T & { relation: NonNullable<T['relation']> } =>
field.type === FieldMetadataType.RELATION &&
field.relation?.type === RelationType.MANY_TO_ONE;
@@ -1,13 +1,6 @@
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { isDefined } from 'twenty-shared/utils';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { useAdvancedFilterFieldSelectDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterFieldSelectDropdown';
import { useSelectFieldUsedInAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown';
import { useApplyAdvancedFilterCompositeSubField } from '@/object-record/advanced-filter/hooks/useApplyAdvancedFilterCompositeSubField';
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState';
@@ -20,21 +13,26 @@ import { type CompositeFieldSubFieldName } from '@/settings/data-model/types/Com
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { IconChevronLeft, useIcons } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
type AdvancedFilterSubFieldSelectMenuProps = {
type AdvancedFilterCompositeSubFieldSelectMenuProps = {
recordFilterId: string;
};
export const AdvancedFilterSubFieldSelectMenu = ({
export const AdvancedFilterCompositeSubFieldSelectMenu = ({
recordFilterId,
}: AdvancedFilterSubFieldSelectMenuProps) => {
}: AdvancedFilterCompositeSubFieldSelectMenuProps) => {
const { getIcon } = useIcons();
const fieldMetadataItemUsedInDropdown = useAtomComponentSelectorValue(
@@ -54,21 +52,20 @@ export const AdvancedFilterSubFieldSelectMenu = ({
const { closeAdvancedFilterFieldSelectDropdown } =
useAdvancedFilterFieldSelectDropdown(recordFilterId);
const { selectFieldUsedInAdvancedFilterDropdown } =
useSelectFieldUsedInAdvancedFilterDropdown();
const { applyAdvancedFilterCompositeSubField } =
useApplyAdvancedFilterCompositeSubField();
const handleSelectFilter = (
selectedFieldMetadataItem: FieldMetadataItem | null | undefined,
subFieldName?: CompositeFieldSubFieldName | null | undefined,
) => {
if (!isDefined(selectedFieldMetadataItem)) {
return;
}
selectFieldUsedInAdvancedFilterDropdown({
fieldMetadataItemId: selectedFieldMetadataItem.id,
const handleSelectFilter = ({
fieldMetadataItem,
subFieldName,
}: {
fieldMetadataItem: FieldMetadataItem;
subFieldName?: CompositeFieldSubFieldName | null;
}) => {
applyAdvancedFilterCompositeSubField({
sourceFieldMetadataItem: fieldMetadataItem,
subFieldName: subFieldName ?? null,
recordFilterId,
subFieldName,
});
closeAdvancedFilterFieldSelectDropdown();
@@ -132,36 +129,42 @@ export const AdvancedFilterSubFieldSelectMenu = ({
selectableItemIdArray={selectableItemIdArray}
selectableListInstanceId={advancedFilterFieldSelectDropdownId}
>
{compositeFieldTypeIsFilterableByAnySubField && (
<SelectableListItem
itemId="-1"
key={`select-filter-${-1}`}
onEnter={() => {
handleSelectFilter(fieldMetadataItemUsedInDropdown);
}}
>
<MenuItem
{compositeFieldTypeIsFilterableByAnySubField &&
isDefined(fieldMetadataItemUsedInDropdown) && (
<SelectableListItem
itemId="-1"
key={`select-filter-${-1}`}
testId={`select-filter-${-1}`}
focused={selectedItemId === '-1'}
onClick={() => {
handleSelectFilter(fieldMetadataItemUsedInDropdown);
onEnter={() => {
handleSelectFilter({
fieldMetadataItem: fieldMetadataItemUsedInDropdown,
});
}}
LeftIcon={getIcon(fieldMetadataItemUsedInDropdown.icon)}
text={t`Any ${fieldLabel} field`}
/>
</SelectableListItem>
)}
>
<MenuItem
key={`select-filter-${-1}`}
testId={`select-filter-${-1}`}
focused={selectedItemId === '-1'}
onClick={() => {
handleSelectFilter({
fieldMetadataItem: fieldMetadataItemUsedInDropdown,
});
}}
LeftIcon={getIcon(fieldMetadataItemUsedInDropdown.icon)}
text={t`Any ${fieldLabel} field`}
/>
</SelectableListItem>
)}
{subFieldsAreFilterable &&
isDefined(fieldMetadataItemUsedInDropdown) &&
subFieldNames.map((subFieldName, index) => (
<SelectableListItem
itemId={subFieldName}
key={`select-filter-${index}`}
onEnter={() => {
handleSelectFilter(
fieldMetadataItemUsedInDropdown,
handleSelectFilter({
fieldMetadataItem: fieldMetadataItemUsedInDropdown,
subFieldName,
);
});
}}
>
<MenuItem
@@ -169,12 +172,10 @@ export const AdvancedFilterSubFieldSelectMenu = ({
key={`select-filter-${index}`}
testId={`select-filter-${index}`}
onClick={() => {
if (isDefined(fieldMetadataItemUsedInDropdown)) {
handleSelectFilter(
fieldMetadataItemUsedInDropdown,
subFieldName,
);
}
handleSelectFilter({
fieldMetadataItem: fieldMetadataItemUsedInDropdown,
subFieldName,
});
}}
text={getCompositeSubFieldLabel(
objectFilterDropdownSubMenuFieldType,
@@ -182,7 +183,7 @@ export const AdvancedFilterSubFieldSelectMenu = ({
)}
LeftIcon={getIcon(
ICON_NAME_BY_SUB_FIELD[subFieldName] ??
fieldMetadataItemUsedInDropdown?.icon,
fieldMetadataItemUsedInDropdown.icon,
)}
/>
</SelectableListItem>
@@ -1,7 +1,9 @@
import { AdvancedFilterCompositeSubFieldSelectMenu } from '@/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu';
import { AdvancedFilterFieldSelectMenu } from '@/object-record/advanced-filter/components/AdvancedFilterFieldSelectMenu';
import { AdvancedFilterSubFieldSelectMenu } from '@/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu';
import { AdvancedFilterRelationTargetFieldSelectMenu } from '@/object-record/advanced-filter/components/AdvancedFilterRelationTargetFieldSelectMenu';
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
import { objectFilterDropdownIsSelectingRelationTargetFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingRelationTargetFieldComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
type AdvancedFilterFieldSelectDropdownContentProps = {
recordFilterId: string;
@@ -10,16 +12,31 @@ type AdvancedFilterFieldSelectDropdownContentProps = {
export const AdvancedFilterFieldSelectDropdownContent = ({
recordFilterId,
}: AdvancedFilterFieldSelectDropdownContentProps) => {
const [objectFilterDropdownIsSelectingCompositeField] = useAtomComponentState(
objectFilterDropdownIsSelectingCompositeFieldComponentState,
);
const objectFilterDropdownIsSelectingCompositeField =
useAtomComponentStateValue(
objectFilterDropdownIsSelectingCompositeFieldComponentState,
);
const shouldShowCompositeSelectionSubMenu =
objectFilterDropdownIsSelectingCompositeField;
const objectFilterDropdownIsSelectingRelationTargetField =
useAtomComponentStateValue(
objectFilterDropdownIsSelectingRelationTargetFieldComponentState,
);
return shouldShowCompositeSelectionSubMenu ? (
<AdvancedFilterSubFieldSelectMenu recordFilterId={recordFilterId} />
) : (
<AdvancedFilterFieldSelectMenu recordFilterId={recordFilterId} />
);
if (objectFilterDropdownIsSelectingRelationTargetField) {
return (
<AdvancedFilterRelationTargetFieldSelectMenu
recordFilterId={recordFilterId}
/>
);
}
if (objectFilterDropdownIsSelectingCompositeField) {
return (
<AdvancedFilterCompositeSubFieldSelectMenu
recordFilterId={recordFilterId}
/>
);
}
return <AdvancedFilterFieldSelectMenu recordFilterId={recordFilterId} />;
};
@@ -11,17 +11,20 @@ import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { AdvancedFilterFieldSelectSearchInput } from '@/object-record/advanced-filter/components/AdvancedFilterFieldSelectSearchInput';
import { useAdvancedFilterFieldSelectDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterFieldSelectDropdown';
import { useSelectFieldUsedInAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown';
import { useApplyAdvancedFilterSourceField } from '@/object-record/advanced-filter/hooks/useApplyAdvancedFilterSourceField';
import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/context/AdvancedFilterContext';
import { ObjectFilterDropdownFilterSelectMenuItem } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem';
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
import { objectFilterDropdownIsSelectingRelationTargetFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingRelationTargetFieldComponentState';
import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState';
import { isCompositeFilterableFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFilterableFieldType';
import { isManyToOneRelationField } from '@/object-metadata/utils/isManyToOneRelationField';
import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector';
import { useFilterableFieldMetadataItems } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItems';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuSectionLabel } from '@/ui/layout/dropdown/components/DropdownMenuSectionLabel';
import { usePushFocusForLeafFieldValuePicker } from '@/object-record/advanced-filter/hooks/usePushFocusForLeafFieldValuePicker';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
@@ -87,8 +90,8 @@ export const AdvancedFilterFieldSelectMenu = ({
advancedFilterFieldSelectDropdownId,
);
const { selectFieldUsedInAdvancedFilterDropdown } =
useSelectFieldUsedInAdvancedFilterDropdown();
const { applyAdvancedFilterSourceField } =
useApplyAdvancedFilterSourceField();
const [, setObjectFilterDropdownSubMenuFieldType] = useAtomComponentState(
objectFilterDropdownSubMenuFieldTypeComponentState,
@@ -99,10 +102,18 @@ export const AdvancedFilterFieldSelectMenu = ({
objectFilterDropdownIsSelectingCompositeFieldComponentState,
);
const [, setObjectFilterDropdownIsSelectingRelationTargetField] =
useAtomComponentState(
objectFilterDropdownIsSelectingRelationTargetFieldComponentState,
);
const setFieldMetadataItemIdUsedInDropdown = useSetAtomComponentState(
fieldMetadataItemIdUsedInDropdownComponentState,
);
const { pushFocusForLeafFieldValuePicker } =
usePushFocusForLeafFieldValuePicker();
const handleFieldMetadataItemSelect = (
selectedFieldMetadataItem: FieldMetadataItem,
) => {
@@ -112,19 +123,41 @@ export const AdvancedFilterFieldSelectMenu = ({
selectedFieldMetadataItem.type,
);
selectFieldUsedInAdvancedFilterDropdown({
fieldMetadataItemId: selectedFieldMetadataItem.id,
const isRelationTraversalField = isManyToOneRelationField(
selectedFieldMetadataItem,
);
const compositeSubMenuFieldType =
!isRelationTraversalField && isCompositeFilterableFieldType(filterType)
? filterType
: null;
// For sub-menu paths (composite or relation traversal) we only stage
// the source field in the dropdown state and open the sub-menu —
// upsertRecordFilter happens in the sub-menu's hook once the user
// makes the final choice. Otherwise navigating back from the sub-menu
// would leave an orphan partial filter in the chip list.
if (isRelationTraversalField) {
setFieldMetadataItemIdUsedInDropdown(selectedFieldMetadataItem.id);
setObjectFilterDropdownIsSelectingRelationTargetField(true);
return;
}
if (compositeSubMenuFieldType !== null) {
setFieldMetadataItemIdUsedInDropdown(selectedFieldMetadataItem.id);
setObjectFilterDropdownSubMenuFieldType(compositeSubMenuFieldType);
setObjectFilterDropdownIsSelectingCompositeField(true);
return;
}
applyAdvancedFilterSourceField({
sourceFieldMetadataItem: selectedFieldMetadataItem,
recordFilterId,
});
if (isCompositeFilterableFieldType(filterType)) {
setObjectFilterDropdownSubMenuFieldType(filterType);
pushFocusForLeafFieldValuePicker(selectedFieldMetadataItem);
setFieldMetadataItemIdUsedInDropdown(selectedFieldMetadataItem.id);
setObjectFilterDropdownIsSelectingCompositeField(true);
} else {
closeAdvancedFilterFieldSelectDropdown();
}
closeAdvancedFilterFieldSelectDropdown();
};
const shouldShowVisibleFields = visibleFieldMetadataItems.length > 0;
@@ -0,0 +1,146 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { isManyToOneRelationField } from '@/object-metadata/utils/isManyToOneRelationField';
import { useAdvancedFilterFieldSelectDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterFieldSelectDropdown';
import { useApplyAdvancedFilterRelationTargetField } from '@/object-record/advanced-filter/hooks/useApplyAdvancedFilterRelationTargetField';
import { usePushFocusForLeafFieldValuePicker } from '@/object-record/advanced-filter/hooks/usePushFocusForLeafFieldValuePicker';
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { objectFilterDropdownIsSelectingRelationTargetFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingRelationTargetFieldComponentState';
import { useFilterableFieldMetadataItems } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItems';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { isDefined } from 'twenty-shared/utils';
import { IconChevronLeft, useIcons } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
type AdvancedFilterRelationTargetFieldSelectMenuProps = {
recordFilterId: string;
};
export const AdvancedFilterRelationTargetFieldSelectMenu = ({
recordFilterId,
}: AdvancedFilterRelationTargetFieldSelectMenuProps) => {
const { getIcon } = useIcons();
const sourceFieldMetadataItem = useAtomComponentSelectorValue(
fieldMetadataItemUsedInDropdownComponentSelector,
);
const setObjectFilterDropdownIsSelectingRelationTargetField =
useSetAtomComponentState(
objectFilterDropdownIsSelectingRelationTargetFieldComponentState,
);
const { closeAdvancedFilterFieldSelectDropdown } =
useAdvancedFilterFieldSelectDropdown(recordFilterId);
const { applyAdvancedFilterRelationTargetField } =
useApplyAdvancedFilterRelationTargetField();
const { pushFocusForLeafFieldValuePicker } =
usePushFocusForLeafFieldValuePicker();
const { advancedFilterFieldSelectDropdownId } =
useAdvancedFilterFieldSelectDropdown(recordFilterId);
const selectedItemId = useAtomComponentStateValue(
selectedItemIdComponentState,
advancedFilterFieldSelectDropdownId,
);
const targetObjectMetadataId =
isDefined(sourceFieldMetadataItem) &&
isManyToOneRelationField(sourceFieldMetadataItem)
? sourceFieldMetadataItem.relation.targetObjectMetadata.id
: null;
const { filterableFieldMetadataItems: allTargetFields } =
useFilterableFieldMetadataItems(targetObjectMetadataId ?? '');
// The backend supports a single hop only. Exclude many-to-one relations
// from the target list so the user can't compose multi-hop traversals
// (e.g. Person → Company → ParentCompany) that the dispatcher would
// collapse back to a filter-by-id on the intermediate relation.
const relationTargetFields = allTargetFields.filter(
(field) => !isManyToOneRelationField(field),
);
if (
!isDefined(sourceFieldMetadataItem) ||
!isManyToOneRelationField(sourceFieldMetadataItem)
) {
return null;
}
const handleSubMenuBack = () => {
setObjectFilterDropdownIsSelectingRelationTargetField(false);
};
const handleSelectTargetField = (
relationTargetFieldMetadataItem: FieldMetadataItem,
) => {
applyAdvancedFilterRelationTargetField({
sourceFieldMetadataItem,
relationTargetFieldMetadataItem,
recordFilterId,
});
pushFocusForLeafFieldValuePicker(relationTargetFieldMetadataItem);
setObjectFilterDropdownIsSelectingRelationTargetField(false);
closeAdvancedFilterFieldSelectDropdown();
};
const selectableItemIdArray = relationTargetFields.map((field) => field.id);
return (
<DropdownContent widthInPixels={GenericDropdownContentWidth.ExtraLarge}>
<DropdownMenuHeader
StartComponent={
<DropdownMenuHeaderLeftComponent
onClick={handleSubMenuBack}
Icon={IconChevronLeft}
/>
}
>
{sourceFieldMetadataItem.label}
</DropdownMenuHeader>
<DropdownMenuItemsContainer>
<SelectableList
focusId={advancedFilterFieldSelectDropdownId}
selectableItemIdArray={selectableItemIdArray}
selectableListInstanceId={advancedFilterFieldSelectDropdownId}
>
{relationTargetFields.map((targetField, index) => (
<SelectableListItem
itemId={targetField.id}
key={`select-filter-relation-${index}`}
onEnter={() => {
handleSelectTargetField(targetField);
}}
>
<MenuItem
focused={selectedItemId === targetField.id}
key={`select-filter-relation-${index}`}
testId={`select-filter-relation-${index}`}
onClick={() => {
handleSelectTargetField(targetField);
}}
text={targetField.label}
LeftIcon={getIcon(targetField.icon)}
/>
</SelectableListItem>
))}
</SelectableList>
</DropdownMenuItemsContainer>
</DropdownContent>
);
};
@@ -6,6 +6,7 @@ import { shouldShowFilterTextInput } from '@/object-record/advanced-filter/utils
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState';
import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState';
import { relationTargetFieldMetadataIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/relationTargetFieldMetadataIdUsedInDropdownComponentState';
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
import { configurableViewFilterOperands } from '@/object-record/object-filter-dropdown/utils/configurableViewFilterOperands';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
@@ -55,6 +56,12 @@ export const AdvancedFilterValueInput = ({
dropdownId,
);
const setRelationTargetFieldMetadataIdUsedInDropdown =
useSetAtomComponentState(
relationTargetFieldMetadataIdUsedInDropdownComponentState,
dropdownId,
);
const setObjectFilterDropdownCurrentRecordFilter = useSetAtomComponentState(
objectFilterDropdownCurrentRecordFilterComponentState,
dropdownId,
@@ -74,6 +81,9 @@ export const AdvancedFilterValueInput = ({
const handleFilterValueDropdownOpen = () => {
setObjectFilterDropdownCurrentRecordFilter(recordFilter);
setFieldMetadataItemIdUsedInDropdown(recordFilter.fieldMetadataId);
setRelationTargetFieldMetadataIdUsedInDropdown(
recordFilter.relationTargetFieldMetadataId ?? null,
);
};
const filterType = recordFilter.type;
@@ -1,31 +1,30 @@
import { useGetFieldMetadataItemByIdOrThrow } from '@/object-metadata/hooks/useGetFieldMetadataItemById';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { useGetInitialFilterValue } from '@/object-record/object-filter-dropdown/hooks/useGetInitialFilterValue';
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState';
import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState';
import { relationTargetFieldMetadataIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/relationTargetFieldMetadataIdUsedInDropdownComponentState';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { getDefaultSubFieldNameForCompositeFilterableFieldType } from '@/object-record/record-filter/utils/getDefaultSubFieldNameForCompositeFilterableFieldType';
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
import { isCompositeTypeNonFilterableByAnySubField } from '@/object-record/record-filter/utils/isCompositeTypeNonFilterableByAnySubField';
import { type CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { getFilterTypeFromFieldType, isDefined } from 'twenty-shared/utils';
type SelectFilterParams = {
fieldMetadataItemId: string;
type ApplyAdvancedFilterCompositeSubFieldParams = {
sourceFieldMetadataItem: FieldMetadataItem;
subFieldName: CompositeFieldSubFieldName | null;
recordFilterId: string;
subFieldName?: CompositeFieldSubFieldName | null | undefined;
};
export const useSelectFieldUsedInAdvancedFilterDropdown = () => {
// Creates an advanced filter from a composite source field together with
// the user's chosen sub-field (e.g. address city, name first name). The
// filter type is still driven by the source field's composite type.
export const useApplyAdvancedFilterCompositeSubField = () => {
const setSelectedOperandInDropdown = useSetAtomComponentState(
selectedOperandInDropdownComponentState,
);
@@ -38,54 +37,34 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => {
objectFilterDropdownSearchInputComponentState,
);
const currentRecordFilters = useAtomComponentStateValue(
currentRecordFiltersComponentState,
);
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const { getFieldMetadataItemByIdOrThrow } =
useGetFieldMetadataItemByIdOrThrow();
const setSubFieldNameUsedInDropdown = useSetAtomComponentState(
subFieldNameUsedInDropdownComponentState,
);
const setRelationTargetFieldMetadataIdUsedInDropdown =
useSetAtomComponentState(
relationTargetFieldMetadataIdUsedInDropdownComponentState,
);
const setObjectFilterDropdownCurrentRecordFilter = useSetAtomComponentState(
objectFilterDropdownCurrentRecordFilterComponentState,
);
const currentRecordFilters = useAtomComponentStateValue(
currentRecordFiltersComponentState,
);
const { upsertRecordFilter } = useUpsertRecordFilter();
const { getInitialFilterValue } = useGetInitialFilterValue();
const selectFieldUsedInAdvancedFilterDropdown = ({
fieldMetadataItemId,
recordFilterId,
const applyAdvancedFilterCompositeSubField = ({
sourceFieldMetadataItem,
subFieldName,
}: SelectFilterParams) => {
setFieldMetadataItemIdUsedInDropdown(fieldMetadataItemId);
recordFilterId,
}: ApplyAdvancedFilterCompositeSubFieldParams) => {
setFieldMetadataItemIdUsedInDropdown(sourceFieldMetadataItem.id);
const { fieldMetadataItem } =
getFieldMetadataItemByIdOrThrow(fieldMetadataItemId);
if (!isDefined(fieldMetadataItem)) {
return;
}
if (
fieldMetadataItem.type === 'RELATION' ||
fieldMetadataItem.type === 'SELECT'
) {
pushFocusItemToFocusStack({
focusId: fieldMetadataItem.id,
component: {
type: FocusComponentType.DROPDOWN,
instanceId: fieldMetadataItem.id,
},
});
}
const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type);
const filterType = getFilterTypeFromFieldType(sourceFieldMetadataItem.type);
const firstOperand = getRecordFilterOperands({
filterType,
@@ -109,30 +88,9 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => {
(recordFilter) => recordFilter.id === recordFilterId,
);
const isCompositeFilterOnAnySubField =
isCompositeFieldType(filterType) && !isDefined(subFieldName);
const compositeFilterNonFilterableByAnySubField =
isCompositeTypeNonFilterableByAnySubField(filterType);
let subFieldNameForNonFilterableWithAny:
| CompositeFieldSubFieldName
| undefined
| null = subFieldName;
if (
isCompositeFilterOnAnySubField &&
compositeFilterNonFilterableByAnySubField
) {
subFieldNameForNonFilterableWithAny =
getDefaultSubFieldNameForCompositeFilterableFieldType(filterType);
}
const subFieldNameToUse =
subFieldName ?? subFieldNameForNonFilterableWithAny;
const newAdvancedFilter = {
id: recordFilterId,
fieldMetadataId: fieldMetadataItem.id,
fieldMetadataId: sourceFieldMetadataItem.id,
displayValue,
operand: firstOperand,
value,
@@ -140,11 +98,13 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => {
positionInRecordFilterGroup:
existingRecordFilter?.positionInRecordFilterGroup,
type: filterType,
label: fieldMetadataItem.label,
subFieldName: subFieldNameToUse,
label: sourceFieldMetadataItem.label,
subFieldName,
relationTargetFieldMetadataId: null,
} satisfies RecordFilter;
setSubFieldNameUsedInDropdown(subFieldNameToUse);
setSubFieldNameUsedInDropdown(subFieldName);
setRelationTargetFieldMetadataIdUsedInDropdown(null);
setObjectFilterDropdownSearchInput('');
@@ -153,6 +113,6 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => {
};
return {
selectFieldUsedInAdvancedFilterDropdown,
applyAdvancedFilterCompositeSubField,
};
};
@@ -0,0 +1,122 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { useGetInitialFilterValue } from '@/object-record/object-filter-dropdown/hooks/useGetInitialFilterValue';
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState';
import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState';
import { relationTargetFieldMetadataIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/relationTargetFieldMetadataIdUsedInDropdownComponentState';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { getFilterTypeFromFieldType, isDefined } from 'twenty-shared/utils';
type ApplyAdvancedFilterRelationTargetFieldParams = {
sourceFieldMetadataItem: FieldMetadataItem;
relationTargetFieldMetadataItem: FieldMetadataItem;
recordFilterId: string;
};
// Creates an advanced filter that traverses a relation: the source is the
// relation field, the filter operates on a target field of the related
// object. Filter type comes from the target field; the label combines
// both labels so the chip reads "Source → Target".
export const useApplyAdvancedFilterRelationTargetField = () => {
const setSelectedOperandInDropdown = useSetAtomComponentState(
selectedOperandInDropdownComponentState,
);
const setFieldMetadataItemIdUsedInDropdown = useSetAtomComponentState(
fieldMetadataItemIdUsedInDropdownComponentState,
);
const setObjectFilterDropdownSearchInput = useSetAtomComponentState(
objectFilterDropdownSearchInputComponentState,
);
const setSubFieldNameUsedInDropdown = useSetAtomComponentState(
subFieldNameUsedInDropdownComponentState,
);
const setRelationTargetFieldMetadataIdUsedInDropdown =
useSetAtomComponentState(
relationTargetFieldMetadataIdUsedInDropdownComponentState,
);
const setObjectFilterDropdownCurrentRecordFilter = useSetAtomComponentState(
objectFilterDropdownCurrentRecordFilterComponentState,
);
const currentRecordFilters = useAtomComponentStateValue(
currentRecordFiltersComponentState,
);
const { upsertRecordFilter } = useUpsertRecordFilter();
const { getInitialFilterValue } = useGetInitialFilterValue();
const applyAdvancedFilterRelationTargetField = ({
sourceFieldMetadataItem,
relationTargetFieldMetadataItem,
recordFilterId,
}: ApplyAdvancedFilterRelationTargetFieldParams) => {
setFieldMetadataItemIdUsedInDropdown(sourceFieldMetadataItem.id);
const filterType = getFilterTypeFromFieldType(
relationTargetFieldMetadataItem.type,
);
const firstOperand = getRecordFilterOperands({
filterType,
subFieldName: null,
})?.[0];
if (!isDefined(firstOperand)) {
throw new Error(
`No valid operand found for relation-traversal filter type: ${filterType}`,
);
}
setSelectedOperandInDropdown(firstOperand);
const { value, displayValue } = getInitialFilterValue(
filterType,
firstOperand,
);
const existingRecordFilter = currentRecordFilters.find(
(recordFilter) => recordFilter.id === recordFilterId,
);
const newAdvancedFilter = {
id: recordFilterId,
fieldMetadataId: sourceFieldMetadataItem.id,
displayValue,
operand: firstOperand,
value,
recordFilterGroupId: existingRecordFilter?.recordFilterGroupId,
positionInRecordFilterGroup:
existingRecordFilter?.positionInRecordFilterGroup,
type: filterType,
label: `${sourceFieldMetadataItem.label}${relationTargetFieldMetadataItem.label}`,
subFieldName: null,
relationTargetFieldMetadataId: relationTargetFieldMetadataItem.id,
} satisfies RecordFilter;
setSubFieldNameUsedInDropdown(null);
setRelationTargetFieldMetadataIdUsedInDropdown(
relationTargetFieldMetadataItem.id,
);
setObjectFilterDropdownSearchInput('');
setObjectFilterDropdownCurrentRecordFilter(newAdvancedFilter);
upsertRecordFilter(newAdvancedFilter);
};
return {
applyAdvancedFilterRelationTargetField,
};
};
@@ -0,0 +1,115 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { useGetInitialFilterValue } from '@/object-record/object-filter-dropdown/hooks/useGetInitialFilterValue';
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState';
import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState';
import { relationTargetFieldMetadataIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/relationTargetFieldMetadataIdUsedInDropdownComponentState';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { getFilterTypeFromFieldType, isDefined } from 'twenty-shared/utils';
type ApplyAdvancedFilterSourceFieldParams = {
sourceFieldMetadataItem: FieldMetadataItem;
recordFilterId: string;
};
// Creates a leaf advanced filter from a source field selection. Composite
// and many-to-one relation sources are handled by their own specialized
// hooks (useApplyAdvancedFilterCompositeSubField,
// useApplyAdvancedFilterRelationTargetField) — callers branch to those
// before reaching this hook.
export const useApplyAdvancedFilterSourceField = () => {
const setSelectedOperandInDropdown = useSetAtomComponentState(
selectedOperandInDropdownComponentState,
);
const setFieldMetadataItemIdUsedInDropdown = useSetAtomComponentState(
fieldMetadataItemIdUsedInDropdownComponentState,
);
const setObjectFilterDropdownSearchInput = useSetAtomComponentState(
objectFilterDropdownSearchInputComponentState,
);
const setSubFieldNameUsedInDropdown = useSetAtomComponentState(
subFieldNameUsedInDropdownComponentState,
);
const setRelationTargetFieldMetadataIdUsedInDropdown =
useSetAtomComponentState(
relationTargetFieldMetadataIdUsedInDropdownComponentState,
);
const setObjectFilterDropdownCurrentRecordFilter = useSetAtomComponentState(
objectFilterDropdownCurrentRecordFilterComponentState,
);
const currentRecordFilters = useAtomComponentStateValue(
currentRecordFiltersComponentState,
);
const { upsertRecordFilter } = useUpsertRecordFilter();
const { getInitialFilterValue } = useGetInitialFilterValue();
const applyAdvancedFilterSourceField = ({
sourceFieldMetadataItem,
recordFilterId,
}: ApplyAdvancedFilterSourceFieldParams) => {
setFieldMetadataItemIdUsedInDropdown(sourceFieldMetadataItem.id);
const filterType = getFilterTypeFromFieldType(sourceFieldMetadataItem.type);
const firstOperand = getRecordFilterOperands({
filterType,
subFieldName: null,
})?.[0];
if (!isDefined(firstOperand)) {
throw new Error(`No valid operand found for filter type: ${filterType}`);
}
setSelectedOperandInDropdown(firstOperand);
const { value, displayValue } = getInitialFilterValue(
filterType,
firstOperand,
);
const existingRecordFilter = currentRecordFilters.find(
(recordFilter) => recordFilter.id === recordFilterId,
);
const newAdvancedFilter = {
id: recordFilterId,
fieldMetadataId: sourceFieldMetadataItem.id,
displayValue,
operand: firstOperand,
value,
recordFilterGroupId: existingRecordFilter?.recordFilterGroupId,
positionInRecordFilterGroup:
existingRecordFilter?.positionInRecordFilterGroup,
type: filterType,
label: sourceFieldMetadataItem.label,
subFieldName: null,
relationTargetFieldMetadataId: null,
} satisfies RecordFilter;
setSubFieldNameUsedInDropdown(null);
setRelationTargetFieldMetadataIdUsedInDropdown(null);
setObjectFilterDropdownSearchInput('');
setObjectFilterDropdownCurrentRecordFilter(newAdvancedFilter);
upsertRecordFilter(newAdvancedFilter);
};
return {
applyAdvancedFilterSourceField,
};
};
@@ -0,0 +1,32 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
// RELATION and SELECT leaf fields open a value picker keyed by the field
// id once the user finishes choosing the field — push it on the focus
// stack so the picker's keyboard hotkeys are active when it opens. Other
// field types use input-style pickers that don't rely on the focus stack.
export const usePushFocusForLeafFieldValuePicker = () => {
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const pushFocusForLeafFieldValuePicker = (
fieldMetadataItem: FieldMetadataItem,
) => {
if (
fieldMetadataItem.type !== 'RELATION' &&
fieldMetadataItem.type !== 'SELECT'
) {
return;
}
pushFocusItemToFocusStack({
focusId: fieldMetadataItem.id,
component: {
type: FocusComponentType.DROPDOWN,
instanceId: fieldMetadataItem.id,
},
});
};
return { pushFocusForLeafFieldValuePicker };
};
@@ -1,6 +1,7 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { FILTER_FIELD_LIST_ID } from '@/object-record/object-filter-dropdown/constants/FilterFieldListId';
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
import { isManyToOneRelationField } from '@/object-metadata/utils/isManyToOneRelationField';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { isSelectedItemIdComponentFamilyState } from '@/ui/layout/selectable-list/states/isSelectedItemIdComponentFamilyState';
import { useAtomComponentFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateValue';
@@ -27,9 +28,9 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({
const Icon = getIcon(fieldMetadataItemToSelect.icon);
const shouldShowSubMenu = isCompositeFieldType(
fieldMetadataItemToSelect.type,
);
const shouldShowSubMenu =
isCompositeFieldType(fieldMetadataItemToSelect.type) ||
isManyToOneRelationField(fieldMetadataItemToSelect);
const handleClick = () => {
resetSelectedItem();
@@ -1,7 +1,10 @@
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { getFieldMetadataItemById } from '@/object-metadata/utils/getFieldMetadataItemById';
import { DATE_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/DateFilterTypes';
import { DATE_PICKER_DROPDOWN_CONTENT_WIDTH } from '@/object-record/object-filter-dropdown/constants/DatePickerDropdownContentWidth';
import { useApplyObjectFilterDropdownOperand } from '@/object-record/object-filter-dropdown/hooks/useApplyObjectFilterDropdownOperand';
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { relationTargetFieldMetadataIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/relationTargetFieldMetadataIdUsedInDropdownComponentState';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
import { getOperandLabel } from '@/object-record/object-filter-dropdown/utils/getOperandLabel';
@@ -13,6 +16,7 @@ import { DropdownMenuInnerSelect } from '@/ui/layout/dropdown/components/Dropdow
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { getFilterTypeFromFieldType, isDefined } from 'twenty-shared/utils';
import { type SelectOption } from 'twenty-ui/input';
@@ -32,11 +36,34 @@ export const ObjectFilterDropdownInnerSelectOperandDropdown = () => {
subFieldNameUsedInDropdownComponentState,
);
const operandsForFilterType = isDefined(fieldMetadataItemUsedInDropdown)
const relationTargetFieldMetadataIdUsedInDropdown =
useAtomComponentStateValue(
relationTargetFieldMetadataIdUsedInDropdownComponentState,
);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
// The target field may have been deleted from the workspace since the
// filter was saved — return null and let the parent skip rendering
// rather than throwing.
const relationTargetFieldMetadataItem = isDefined(
relationTargetFieldMetadataIdUsedInDropdown,
)
? getFieldMetadataItemById({
fieldMetadataId: relationTargetFieldMetadataIdUsedInDropdown,
objectMetadataItems,
}).fieldMetadataItem
: null;
const effectiveFieldMetadataItem = isDefined(
relationTargetFieldMetadataIdUsedInDropdown,
)
? relationTargetFieldMetadataItem
: fieldMetadataItemUsedInDropdown;
const operandsForFilterType = isDefined(effectiveFieldMetadataItem)
? getRecordFilterOperands({
filterType: getFilterTypeFromFieldType(
fieldMetadataItemUsedInDropdown.type,
),
filterType: getFilterTypeFromFieldType(effectiveFieldMetadataItem.type),
subFieldName: subFieldNameUsedInDropdown,
})
: [];
@@ -70,13 +97,13 @@ export const ObjectFilterDropdownInnerSelectOperandDropdown = () => {
if (
!isDefined(selectedOperandInDropdown) ||
!isDefined(fieldMetadataItemUsedInDropdown)
!isDefined(effectiveFieldMetadataItem)
) {
return null;
}
const filterType = getFilterTypeFromFieldType(
fieldMetadataItemUsedInDropdown.type,
effectiveFieldMetadataItem.type,
);
const isDateFilter = DATE_FILTER_TYPES.includes(filterType);
@@ -5,6 +5,7 @@ import { objectFilterDropdownAnyFieldSearchIsSelectedComponentState } from '@/ob
import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState';
import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState';
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
import { objectFilterDropdownIsSelectingRelationTargetFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingRelationTargetFieldComponentState';
import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
@@ -45,6 +46,12 @@ export const useResetFilterDropdown = (componentInstanceId?: string) => {
componentInstanceId,
);
const objectFilterDropdownIsSelectingRelationTargetField =
useAtomComponentStateCallbackState(
objectFilterDropdownIsSelectingRelationTargetFieldComponentState,
componentInstanceId,
);
const objectFilterDropdownCurrentRecordFilter =
useAtomComponentStateCallbackState(
objectFilterDropdownCurrentRecordFilterComponentState,
@@ -56,6 +63,7 @@ export const useResetFilterDropdown = (componentInstanceId?: string) => {
store.set(selectedOperandInDropdown, null);
store.set(objectFilterDropdownFilterIsSelected, false);
store.set(objectFilterDropdownIsSelectingCompositeField, false);
store.set(objectFilterDropdownIsSelectingRelationTargetField, false);
store.set(fieldMetadataItemIdUsedInDropdown, null);
store.set(objectFilterDropdownCurrentRecordFilter, null);
store.set(objectFilterDropdownAnyFieldSearchIsSelected, false);
@@ -64,6 +72,7 @@ export const useResetFilterDropdown = (componentInstanceId?: string) => {
selectedOperandInDropdown,
objectFilterDropdownFilterIsSelected,
objectFilterDropdownIsSelectingCompositeField,
objectFilterDropdownIsSelectingRelationTargetField,
fieldMetadataItemIdUsedInDropdown,
objectFilterDropdownCurrentRecordFilter,
objectFilterDropdownAnyFieldSearchIsSelected,
@@ -0,0 +1,9 @@
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
export const objectFilterDropdownIsSelectingRelationTargetFieldComponentState =
createAtomComponentState<boolean>({
key: 'objectFilterDropdownIsSelectingRelationTargetFieldComponentState',
defaultValue: false,
componentInstanceContext: ObjectFilterDropdownComponentInstanceContext,
});
@@ -1,9 +1,9 @@
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
import { type CompositeFilterableFieldType } from '@/object-record/record-filter/types/CompositeFilterableFieldType';
import { type ObjectFilterDropdownSubMenuFieldType } from '@/object-record/record-filter/types/ObjectFilterDropdownSubMenuFieldType';
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
export const objectFilterDropdownSubMenuFieldTypeComponentState =
createAtomComponentState<CompositeFilterableFieldType | null>({
createAtomComponentState<ObjectFilterDropdownSubMenuFieldType | null>({
key: 'objectFilterDropdownSubMenuFieldTypeComponentState',
defaultValue: null,
componentInstanceContext: ObjectFilterDropdownComponentInstanceContext,
@@ -0,0 +1,9 @@
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
export const relationTargetFieldMetadataIdUsedInDropdownComponentState =
createAtomComponentState<string | null>({
key: 'relationTargetFieldMetadataIdUsedInDropdownComponentState',
defaultValue: null,
componentInstanceContext: ObjectFilterDropdownComponentInstanceContext,
});
@@ -1,3 +1,4 @@
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
import { useRecordCalendarContextOrThrow } from '@/object-record/record-calendar/contexts/RecordCalendarContext';
import { useRecordCalendarMonthDaysRange } from '@/object-record/record-calendar/month/hooks/useRecordCalendarMonthDaysRange';
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
@@ -6,8 +7,10 @@ import { anyFieldFilterValueComponentState } from '@/object-record/record-filter
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets';
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { t } from '@lingui/core/macro';
import { type Temporal } from 'temporal-polyfill';
@@ -46,6 +49,10 @@ export const useRecordCalendarQueryDateRangeFilter = (
const { filterValueDependencies } = useFilterValueDependencies();
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
);
const anyFieldFilterValue = useAtomComponentStateValue(
anyFieldFilterValueComponentState,
viewBarInstanceId,
@@ -94,15 +101,21 @@ export const useRecordCalendarQueryDateRangeFilter = (
displayValue: `${lastDayOfLastWeek.toString()}`,
};
const calendarRecordFilters = [
...currentRecordFilters,
dateRangeFilterAfter,
dateRangeFilterBefore,
];
const dateRangeFilter = computeRecordGqlOperationFilter({
filterValueDependencies,
recordFilters: [
...currentRecordFilters,
dateRangeFilterAfter,
dateRangeFilterBefore,
],
recordFilters: calendarRecordFilters,
recordFilterGroups: currentRecordFilterGroups,
fields: objectMetadataItem.fields,
fields: augmentFieldsWithRelationTargets({
baseFields: objectMetadataItem.fields,
recordFilters: calendarRecordFilters,
allFieldMetadataItems: flattenedFieldMetadataItems,
}),
});
const { recordGqlOperationFilter: anyFieldFilter } =
@@ -156,7 +156,8 @@ export const useGetRecordFilterDisplayValue = () => {
}
const { fieldMetadataItem } = getFieldMetadataItemByIdOrThrow(
recordFilter.fieldMetadataId,
recordFilter.relationTargetFieldMetadataId ??
recordFilter.fieldMetadataId,
);
const fieldMetadataItemOptions = fieldMetadataItem.options;
@@ -0,0 +1,3 @@
import { type CompositeFilterableFieldType } from '@/object-record/record-filter/types/CompositeFilterableFieldType';
export type ObjectFilterDropdownSubMenuFieldType = CompositeFilterableFieldType;
@@ -24,7 +24,7 @@ export type RecordFilter = {
positionInRecordFilterGroup?: number | null;
label: string;
subFieldName?: CompositeFieldSubFieldName | null | undefined;
// RLS-specific: when set, filter compares against current user's field value
relationTargetFieldMetadataId?: string | null;
rlsDynamicValue?: RLSDynamicValue | null;
};
@@ -0,0 +1,41 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { isDefined } from 'twenty-shared/utils';
// Relation-traversal filters reference target fields that live on a
// different object than the one being queried, so they aren't in the
// source object's own field list. The shared GraphQL filter dispatcher
// looks the target up by id in `fieldMetadataItems` and drops the filter
// when it can't find it — this helper merges the resolved target fields
// in from the workspace-wide flat list so callers don't silently lose
// relation-traversal filters.
export const augmentFieldsWithRelationTargets = ({
baseFields,
recordFilters,
allFieldMetadataItems,
}: {
baseFields: FieldMetadataItem[];
recordFilters: Pick<RecordFilter, 'relationTargetFieldMetadataId'>[];
allFieldMetadataItems: FieldMetadataItem[];
}): FieldMetadataItem[] => {
const targetFieldIds = new Set(
recordFilters
.map((filter) => filter.relationTargetFieldMetadataId)
.filter(isDefined),
);
if (targetFieldIds.size === 0) {
return baseFields;
}
const baseFieldIds = new Set(baseFields.map((field) => field.id));
const additionalTargetFields = allFieldMetadataItems.filter(
(field) => targetFieldIds.has(field.id) && !baseFieldIds.has(field.id),
);
if (additionalTargetFields.length === 0) {
return baseFields;
}
return [...baseFields, ...additionalTargetFields];
};
@@ -6,11 +6,13 @@ import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/s
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { useEffect } from 'react';
@@ -52,11 +54,16 @@ export const RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect =
const { filterValueDependencies } = useFilterValueDependencies();
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
);
const computedFilter = computeContextStoreFilters({
contextStoreTargetedRecordsRule,
contextStoreFilters,
contextStoreFilterGroups,
objectMetadataItem,
flattenedFieldMetadataItems,
filterValueDependencies,
contextStoreAnyFieldFilterValue,
});
@@ -7,6 +7,7 @@ import { contextStoreFilterGroupsComponentState } from '@/context-store/states/c
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/object-options-dropdown/constants/ExportTableDataDefaultPageSize';
@@ -18,6 +19,7 @@ import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState';
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { ViewType } from '@/views/types/ViewType';
import { isDefined } from 'twenty-shared/utils';
@@ -87,6 +89,10 @@ export const useRecordIndexLazyFetchRecords = ({
const { filterValueDependencies } = useFilterValueDependencies();
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
);
const findManyRecordsParams = useFindManyRecordIndexTableParams(
objectMetadataItem.nameSingular,
recordIndexId,
@@ -101,6 +107,7 @@ export const useRecordIndexLazyFetchRecords = ({
contextStoreFilters,
contextStoreFilterGroups,
objectMetadataItem,
flattenedFieldMetadataItems,
filterValueDependencies,
contextStoreAnyFieldFilterValue,
});
@@ -1,14 +1,17 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets';
import { useCurrentRecordGroupDefinition } from '@/object-record/record-group/hooks/useCurrentRecordGroupDefinition';
import { useRecordGroupFilter } from '@/object-record/record-group/hooks/useRecordGroupFilter';
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import {
combineFilters,
computeRecordGqlOperationFilter,
@@ -47,8 +50,16 @@ export const useFindManyRecordIndexTableParams = (
const { filterValueDependencies } = useFilterValueDependencies();
const flattenedFieldMetadataItems = useAtomStateValue(
flattenedFieldMetadataItemsSelector,
);
const currentFilters = computeRecordGqlOperationFilter({
fields: objectMetadataItem?.fields ?? [],
fields: augmentFieldsWithRelationTargets({
baseFields: objectMetadataItem?.fields ?? [],
recordFilters: currentRecordFilters,
allFieldMetadataItems: flattenedFieldMetadataItems,
}),
recordFilterGroups: currentRecordFilterGroups,
recordFilters: currentRecordFilters,
filterValueDependencies,

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