Compare commits

..
Author SHA1 Message Date
neo773 4250890b75 fix(messaging): preserve same-domain imports for institutional accounts
Domain equality is unreliable at universities and other institutional
domains where unrelated members share an address space. Gate
filterOutInternals and the contact-self/workspace-member filter on a
Hipolabs-backed institutional domain list so legitimate collaborators
are not silently dropped during import.
2026-05-06 17:57:57 +05:30
neo773 5e6e5aaaf2 leh lint 2026-05-06 15:07:40 +05:30
neo773 0f6c997070 fix(sdk): pin chokidar v3 to restore fsevents on darwin
v4 dropped fsevents native binding, falling back to recursive fs.watch
which opens fd-per-dir on macOS and hits EMFILE on large monorepos.
Replace internal chokidar/handler.js EventName import with local type
since v3 has no such module.
2026-05-06 14:53:30 +05:30
88988e5a55 i18n - translations (#20313)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-06 10:42:40 +02:00
martmullandGitHub 617f571400 20215 convert application variable to a syncable entity (#20269)
##  Summary

- Converts applicationVariable from a bespoke sync path to a proper
SyncableEntity,
unifying it with the workspace migration pipeline used by all other
manifest-managed
  entities (agent, skill, frontComponent, webhook, etc.)
- Removes the upsertManyApplicationVariableEntities method and its
direct-DB-mutation
approach in favor of the standard validate → build → run action handler
pipeline
- Adds universalIdentifier, deletedAt columns and makes applicationId
NOT NULL via an
  instance command migration

##  Motivation

Before this change, applicationVariable was the only manifest-managed
entity that bypassed
ApplicationManifestMigrationService.syncMetadataFromManifest(). It used
a bespoke service
method called directly from syncApplication(), creating two mental
models, two validation
styles, and two cache invalidation patterns. Now there's one unified
pipeline for all
  manifest entities.

##  What changed

###  Entity refactor:
- ApplicationVariableEntity now extends SyncableEntity (gains
universalIdentifier,
  non-nullable applicationId with CASCADE, soft-delete via deletedAt)

###  New flat entity layer (flat-application-variable/):
- Type, maps type, editable properties constant, entity-to-flat
converter, cache service,
  module

###  New migration pipeline wiring:
- Manifest converter
(fromApplicationVariableManifestToUniversalFlatApplicationVariable)
  - Validator service (FlatApplicationVariableValidatorService)
- Builder service
(WorkspaceMigrationApplicationVariableActionsBuilderService)
  - Create/Update/Delete action handlers with secret encryption hooks
- Registered in orchestrator, builder module, runner module, and all
type registries

###  Removed bespoke path:
- Deleted upsertManyApplicationVariableEntities from
ApplicationVariableEntityService
  - Removed its call from ApplicationSyncService.syncApplication()
- Kept update() (operator-set value at runtime) and getDisplayValue()
(runtime display)

###  Database migration:
- Instance command to add columns, backfill universalIdentifier, enforce
NOT NULL
  constraints, and update indexes

##  Test plan

  - npx nx typecheck twenty-server passes (0 errors)
- Unit tests pass (application-variable.service.spec.ts,
build-env-var.spec.ts)
- Install an app with applicationVariables in its manifest → variables
appear with correct
  universalIdentifier
- Update app manifest (add/remove/modify a variable) → migration
pipeline handles diff
  correctly
- Operator-set value via update endpoint persists correctly with
encryption
  - Uninstall app → variables cascade-deleted
  - app dev --once on example app syncs without errors
2026-05-06 08:23:53 +00:00
2a97e77303 fix(server): handle Redis idle disconnects in session-store client (#20143)
## Summary

The session-store node-redis client doesn't attach an `'error'` event
listener, so when Redis closes an idle connection (server-side `timeout`
setting), node-redis emits an unhandled `'error'` event and the entire
Node process crashes with `SocketClosedUnexpectedlyError`.

## Reproduction

1. Deploy twenty-server against a Redis instance with `timeout 300` (5
min idle close).
2. Don't log in (or otherwise keep the session store completely idle).
3. ~5 minutes after `Nest application successfully started`, the process
crashes:

```
node:events:487
      throw er; // Unhandled 'error' event
      ^

SocketClosedUnexpectedlyError: Socket closed unexpectedly
    at Socket.<anonymous> (/app/node_modules/@redis/client/dist/lib/client/socket.js:194:118)
    ...
Emitted 'error' event on Commander instance at:
    at RedisSocket._RedisSocket_onSocketError (/app/node_modules/@redis/client/dist/lib/client/socket.js:218:10)
```

Kubernetes restarts the pod and the loop repeats every ~5 minutes (12
restarts in 95 min in our environment).

`twenty-worker` is unaffected — BullMQ's ioredis client has its own
keep-alive and the queue keeps it busy.

## Root cause


`packages/twenty-server/src/engine/core-modules/session-storage/session-storage.module-factory.ts`
constructs the node-redis client with no error listener:

```ts
const redisClient = createClient({ url: connectionString });

redisClient.connect().catch((err) => {
  throw new Error(`Redis connection failed: ${err}`);
});
```

In Node.js, an unhandled `'error'` event on an `EventEmitter` becomes an
uncaught exception. node-redis emits `'error'` on socket close. With no
listener, the process exits 1 — even though node-redis would otherwise
reconnect on its own.

## Fix

1. Attach a `client.on('error', ...)` listener so disconnect errors are
logged. node-redis' built-in `reconnectStrategy` then takes over.
2. Set `pingInterval: 60_000` so the connection is never idle long
enough to be reaped by any reasonable Redis `timeout`. Defense in depth.

## Verification

Reproduced locally with Redis `CONFIG SET timeout 30` (30s for fast
reproduction). Without the fix: process exits 30s after boot. With the
fix: client logs the disconnect, reconnects, and the process keeps
running.

## Notes / out of scope

- `cache-storage.module-factory.ts` uses `cache-manager-redis-yet`
(which wraps node-redis under the hood). It may exhibit the same
vulnerability under sufficiently idle conditions; recommend a follow-up
to confirm and similarly harden it.
- `redis-client.service.ts` uses ioredis, which has built-in keepalive
and reconnect — no immediate crash risk, but adding error logging there
would be a nice consistency win.

## Test plan

- [ ] Existing tests still pass
- [ ] Manual: deploy with low Redis `timeout` (e.g. `30`), confirm
process survives
- [ ] Manual: kill Redis briefly, confirm twenty-server reconnects
instead of exiting

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-05 22:09:13 +00:00
bbd9720ab3 [Dashboards] [Warning] Remove gauge chart support and delete existing widgets (#20172)
## Summary

Removes gauge chart from the chart-type picker and deletes existing
gauge widgets via a workspace migration. The gauge was rendering a
hardcoded `0.7 / "Progress"` stub regardless of configuration -- never
wired to real data.

The contract stays in place. We keep
`WidgetConfigurationType.GAUGE_CHART`, the DTO, the GraphQL union
member, and the gauge folder -- so stored gauge JSON still resolves
through the schema. The render path falls through to `default: return
null`, so any un-migrated gauge widget renders as an empty cell, not a
crash.

This PR just removes existing gauge widgets if there are any (via
`upgrade:2-3:delete-gauge-widgets`). The deliberate cleanup -- deleting
the type definitions, the gauge folder, the DTO -- comes in a follow-up
PR after the migration has run.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-05 21:36:45 +00:00
6ebeedba0a i18n - docs translations (#20303)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-05 20:54:25 +02:00
a3c026f1ce i18n - translations (#20302)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-05 20:39:23 +02:00
e0563377b5 Fix unclear metadata validation errors (#20234)
https://github.com/user-attachments/assets/8f8f1122-3de1-4a9b-8bb4-a3c8d31e47ae

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-05 18:18:05 +00:00
a03c2647cf Fix unreliable SSE event stream updates during workflow form transitions (#20242)
Before - workflow run not up to date, needs refresh to see created
company in some cases


https://github.com/user-attachments/assets/28517e97-2404-4f75-8bce-cc33e3cbea20

After 


https://github.com/user-attachments/assets/60f930cb-1265-4c50-8ec5-aa4f978b1873

## Summary
- Split `SSEQuerySubscribeEffect`'s single debounced
`updateQueryListeners` into separate `syncAdditions` (leading edge, 1s
debounce) and `syncRemovals` (trailing edge, 200ms debounce) callbacks.
This prevents query unregistrations during component mount/unmount
transitions from creating gaps where events are missed, while keeping
new registrations immediate.
- Each sync path now updates `activeQueryListenersState` granularly
(append-only for additions, filter-only for removals) instead of
overwriting the entire state, eliminating a race condition where
removals could mark unregistered queries as active.
- Mount `WorkflowRunSSESubscribeEffect` inside
`WorkflowEditActionFormFiller` so the workflow-run query subscription
stays active during form steps.
- Extract `buildSortedConnectionEdges` util that builds the resulting
edge list of a cached record connection after new records are created.
Position placeholders (`'first'` / `'last'`) bypass orderBy and are
pinned to the front/back; sortable positions (numeric or undefined) are
merged into existing edges and sorted by the connection's actual
`orderBy`. This replaces the broken `length * position` insertion logic
in `triggerCreateRecordsOptimisticEffect` that treated the sortable
`position` field as a 0-1 ratio, causing new records from SSE to land at
invisible indices in the cached list. Also fixes `totalCount` increment
for batched creates, derives `pageInfo` cursors from the final array,
and gracefully skips records whose `toReference` returns null.

## Test plan
- [x] Run a workflow with a form step — verify the workflow status
updates live after form submission (no stuck "running" state)
- [x] Run the same workflow multiple times — verify company creation
events appear live on the record index page for every run, not just the
first
- [x] Click the "+" button to create a record in first position — verify
it appears immediately at the top
- [x] Verify other SSE-backed live updates (record creation, deletion,
updates) still work correctly

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-05 18:14:25 +00:00
6854dc549b i18n - website translations (#20301)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-05 20:12:54 +02:00
Abdul RahmanandGitHub cbd2a017da Improve app gallery image sizing (no cropping) (#20287)
<img width="908" height="808" alt="Screenshot 2026-05-05 at 7 38 46 PM"
src="https://github.com/user-attachments/assets/a6f0a9b7-f676-46a3-8642-48c4bd06c7f4"
/>
2026-05-05 17:28:39 +00:00
Abdullah.andGitHub 8253fb6e6d feat: improve SEO foundations and canonicalise locale URLs while adding language-switcher in Footer as planned (#20294)
#### SEO

- Heading default flipped from h1 → h2; only Hero.Heading defaults to
h1. Eliminates accidental multi-h1 pages, which was confusing search
engines about the primary topic.
- Titles and descriptions in static-website-routes.ts rewritten to be
keyword-led and unique per page.
- Added buildFaqPageJsonLd (used on /, /pricing) and
buildReleaseListJsonLd (used on /releases).
- ReleaseEntry now renders id={release} so JSON-LD @id fragments resolve
to anchors.


#### Footer language switcher
- New LocaleSwitcher.tsx (plain React popover — useState + useRef +
outside-click). Trigger renders globe icon + native language name
(Français); popover lists all enabled locales with native + English
names side-by-side.
- Intl.DisplayNames-based name resolution in locale-display-names.ts.
- Plumbed into the footer's bottom row next to copyright.

Translations have not been pulled from Crowdin yet, so French pages
currently show English copy.
2026-05-05 17:28:08 +00:00
345 changed files with 16595 additions and 1949 deletions
+2 -1
View File
@@ -34,7 +34,8 @@
"@types/qs": "6.9.16",
"@wyw-in-js/transform@npm:0.6.0": "patch:@wyw-in-js/transform@npm%3A0.7.0#~/.yarn/patches/@wyw-in-js-transform-npm-0.7.0-ba641dc99f.patch",
"@wyw-in-js/transform@npm:0.7.0": "patch:@wyw-in-js/transform@npm%3A0.7.0#~/.yarn/patches/@wyw-in-js-transform-npm-0.7.0-ba641dc99f.patch",
"@opentelemetry/api": "1.9.1"
"@opentelemetry/api": "1.9.1",
"chokidar": "^3.6.0"
},
"version": "0.2.1",
"nx": {},
@@ -19,8 +19,8 @@
"test:watch": "vitest"
},
"dependencies": {
"twenty-client-sdk": "0.9.0",
"twenty-sdk": "0.9.0"
"twenty-client-sdk": "2.2.0",
"twenty-sdk": "2.2.0"
},
"devDependencies": {
"@types/node": "^24.7.2",
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
import { useRecordId } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { isDefined } from 'twenty-shared/utils';
@@ -72,8 +72,7 @@ const CardDisplay = ({
};
const PostCardPreview = () => {
const selectedRecordIds = useSelectedRecordIds();
const recordId = selectedRecordIds.length === 1 ? selectedRecordIds[0] : null;
const recordId = useRecordId();
const [postCard, setPostCard] = useState<PostCardRecord | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -1,6 +1,11 @@
import { useEffect } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds, updateProgress, enqueueSnackbar, unmountFrontComponent } from 'twenty-sdk/front-component';
import {
enqueueSnackbar,
unmountFrontComponent,
updateProgress,
useRecordId,
} from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
@@ -10,8 +15,7 @@ const SYSTEM_PROMPT =
'text, nothing else — no greeting label, no sign-off label, just the message.';
const GeneratePostCardEffect = () => {
const selectedRecordIds = useSelectedRecordIds();
const recordId = selectedRecordIds.length === 1 ? selectedRecordIds[0] : null;
const recordId = useRecordId();
useEffect(() => {
if (recordId === null) {
@@ -1,50 +1,42 @@
import { useEffect } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds, updateProgress, enqueueSnackbar, unmountFrontComponent } from 'twenty-sdk/front-component';
import {
enqueueSnackbar,
unmountFrontComponent,
updateProgress,
useRecordId,
} from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
const SendPostCardsEffect = () => {
const selectedRecordIds = useSelectedRecordIds();
const recordId = useRecordId();
useEffect(() => {
const send = async () => {
try {
if (selectedRecordIds.length === 0) {
await enqueueSnackbar({
message: 'No postcards selected',
variant: 'error',
});
await unmountFrontComponent();
return;
}
await updateProgress(0.1);
const client = new CoreApiClient();
await updateProgress(0.3);
for (let i = 0; i < selectedRecordIds.length; i++) {
if (recordId) {
await client.mutation({
updatePostCard: {
__args: {
id: selectedRecordIds[i],
id: recordId,
data: { status: 'SENT' },
},
id: true,
},
});
await updateProgress(0.3 + (0.7 * (i + 1)) / selectedRecordIds.length);
await enqueueSnackbar({
message: `Postcard sent`,
variant: 'success',
});
}
const count = selectedRecordIds.length;
await enqueueSnackbar({
message: `${count} postcard${count > 1 ? 's' : ''} sent`,
variant: 'success',
});
await unmountFrontComponent();
} catch (error) {
const message =
@@ -56,7 +48,7 @@ const SendPostCardsEffect = () => {
};
send();
}, [selectedRecordIds]);
}, [recordId]);
return null;
};
@@ -3317,8 +3317,8 @@ __metadata:
oxlint: "npm:^0.16.0"
react: "npm:^19.0.0"
react-dom: "npm:^19.0.0"
twenty-client-sdk: "npm:0.9.0"
twenty-sdk: "npm:0.9.0"
twenty-client-sdk: "npm:2.2.0"
twenty-sdk: "npm:2.2.0"
typescript: "npm:^5.9.3"
vite-tsconfig-paths: "npm:^4.2.1"
vitest: "npm:^3.1.1"
@@ -4059,21 +4059,21 @@ __metadata:
languageName: node
linkType: hard
"twenty-client-sdk@npm:0.9.0":
version: 0.9.0
resolution: "twenty-client-sdk@npm:0.9.0"
"twenty-client-sdk@npm:2.2.0":
version: 2.2.0
resolution: "twenty-client-sdk@npm:2.2.0"
dependencies:
"@genql/cli": "npm:^3.0.3"
"@genql/runtime": "npm:^2.10.0"
esbuild: "npm:^0.25.0"
graphql: "npm:^16.8.1"
checksum: 10c0/4b42a6622a9852fc3eca50c1131b116c5602af006ee5f1d3b4f1e97721bbc8ef5c2f3b60d9dee3ca9ca8c3c7ad4b292a7b55007b779b4c042997dbc29940ba54
checksum: 10c0/90122593efa53440ae386960211a2c274b13a410942bf6f9bc35cf952ae55fe83280e29074677fd284fa3c79069fc578980db06a92a143c08e043f865dbbbd3c
languageName: node
linkType: hard
"twenty-sdk@npm:0.9.0":
version: 0.9.0
resolution: "twenty-sdk@npm:0.9.0"
"twenty-sdk@npm:2.2.0":
version: 2.2.0
resolution: "twenty-sdk@npm:2.2.0"
dependencies:
"@genql/cli": "npm:^3.0.3"
"@genql/runtime": "npm:^2.10.0"
@@ -4093,7 +4093,7 @@ __metadata:
react: "npm:^19.0.0"
react-dom: "npm:^19.0.0"
tinyglobby: "npm:^0.2.15"
twenty-client-sdk: "npm:0.9.0"
twenty-client-sdk: "npm:2.2.0"
typescript: "npm:^5.9.2"
uuid: "npm:^13.0.0"
vite: "npm:^7.0.0"
@@ -4101,7 +4101,7 @@ __metadata:
zod: "npm:^4.1.11"
bin:
twenty: dist/cli.cjs
checksum: 10c0/27f93e5edac3265f819abacc853598435718020258a95d55518562e086e42daabae784964b42005d8e4ff30d1d6f13a12a38c656e18c60bad98da3f579a8e1c3
checksum: 10c0/8978b4b0aa5ea282c8f76799347d9d1812901ec609bc2bd9270261a6bc5589ad361f6102a06900a4511a29a8378cd3811c2c0bad9f01cb8adadabca6d96dfd0b
languageName: node
linkType: hard
@@ -2872,6 +2872,7 @@ enum AllMetadataName {
fieldPermission
frontComponent
webhook
applicationVariable
connectionProvider
}
@@ -2495,7 +2495,7 @@ export interface CollectionHash {
__typename: 'CollectionHash'
}
export type AllMetadataName = 'fieldMetadata' | 'objectMetadata' | 'view' | 'viewField' | 'viewFieldGroup' | 'viewGroup' | 'viewSort' | 'rowLevelPermissionPredicate' | 'rowLevelPermissionPredicateGroup' | 'viewFilterGroup' | 'index' | 'logicFunction' | 'viewFilter' | 'role' | 'roleTarget' | 'agent' | 'skill' | 'pageLayout' | 'pageLayoutWidget' | 'pageLayoutTab' | 'commandMenuItem' | 'navigationMenuItem' | 'permissionFlag' | 'objectPermission' | 'fieldPermission' | 'frontComponent' | 'webhook' | 'connectionProvider'
export type AllMetadataName = 'fieldMetadata' | 'objectMetadata' | 'view' | 'viewField' | 'viewFieldGroup' | 'viewGroup' | 'viewSort' | 'rowLevelPermissionPredicate' | 'rowLevelPermissionPredicateGroup' | 'viewFilterGroup' | 'index' | 'logicFunction' | 'viewFilter' | 'role' | 'roleTarget' | 'agent' | 'skill' | 'pageLayout' | 'pageLayoutWidget' | 'pageLayoutTab' | 'commandMenuItem' | 'navigationMenuItem' | 'permissionFlag' | 'objectPermission' | 'fieldPermission' | 'frontComponent' | 'webhook' | 'applicationVariable' | 'connectionProvider'
export interface MinimalObjectMetadata {
id: Scalars['UUID']
@@ -8854,6 +8854,7 @@ export const enumAllMetadataName = {
fieldPermission: 'fieldPermission' as const,
frontComponent: 'frontComponent' as const,
webhook: 'webhook' as const,
applicationVariable: 'applicationVariable' as const,
connectionProvider: 'connectionProvider' as const
}
@@ -198,6 +198,14 @@ export default defineApplication({
See the [defineApplication accordion](/developers/extend/apps/building#defineentity-functions) in the Building Apps page for the full list of marketplace fields (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl`, etc.).
#### Recommended screenshot dimensions
The marketplace renders `screenshots` in a fixed `8:5` container (for example, `1600×1000 px`).
<Note>
Screenshots of any aspect ratio are displayed in full and are never cropped, but anything significantly taller or narrower than `8:5` will show empty bands on the sides.
</Note>
### Publish
```bash filename="Terminal"
@@ -15,7 +15,7 @@ Front-Komponenten können an zwei Stellen innerhalb von Twenty gerendert werden:
## Einfaches Beispiel
The quickest way to see a front component in action is to register it with a **command menu item**. Use `defineCommandMenuItem` in a separate file to make the component appear as a quick-action button in the top-right corner of the page:
Der schnellste Weg, eine Front-Komponente in Aktion zu sehen, ist, sie als **Befehlsmenüeintrag** zu registrieren. Verwende `defineCommandMenuItem` in einer separaten Datei, damit die Komponente als Schnellaktionsschaltfläche oben rechts auf der Seite erscheint:
```tsx src/front-components/hello-world.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
@@ -350,7 +350,7 @@ export default defineFrontComponent({
## defineCommandMenuItem
Use `defineCommandMenuItem` to register a front component in the command menu (Cmd+K). Wenn `isPinned` `true` ist, erscheint sie außerdem als Schnellaktionsschaltfläche oben rechts auf der Seite.
Verwende `defineCommandMenuItem`, um eine Front-Komponente im Befehlsmenü (Cmd+K) zu registrieren. Wenn `isPinned` `true` ist, erscheint sie außerdem als Schnellaktionsschaltfläche oben rechts auf der Seite.
```ts src/command-menu-items/open-dashboard.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
@@ -370,7 +370,7 @@ export default defineCommandMenuItem({
| --------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `universalIdentifier` | Ja | Stabile eindeutige ID für den Befehl |
| `label` | Ja | Vollständiges Label, das im Befehlsmenü (Cmd+K) angezeigt wird |
| `frontComponentUniversalIdentifier` | Ja | The `universalIdentifier` of the front component this command opens |
| `frontComponentUniversalIdentifier` | Ja | Der `universalIdentifier` der Front-Komponente, die dieser Befehl öffnet |
| `shortLabel` | Nein | Kürzeres Label, das auf der angehefteten Schnellaktionsschaltfläche angezeigt wird |
| `icon` | Nein | Neben dem Label angezeigter Icon-Name (z. B. 'IconBolt', 'IconSend') |
| `isPinned` | Nein | Bei `true` wird der Befehl als Schnellaktionsschaltfläche oben rechts auf der Seite angezeigt |
@@ -198,6 +198,14 @@ export default defineApplication({
Siehe das [defineApplication-Akkordeon](/l/de/developers/extend/apps/building#defineentity-functions) auf der Seite Building Apps für die vollständige Liste der Marktplatzfelder (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl` usw.).
#### Empfohlene Abmessungen für Screenshots
Der Marktplatz stellt `screenshots` in einem festen `8:5`-Container dar (zum Beispiel `1600×1000 px`).
<Note>
Screenshots mit beliebigem Seitenverhältnis werden vollständig angezeigt und niemals beschnitten, aber alles, was deutlich höher oder schmaler als `8:5` ist, zeigt an den Seiten leere Balken.
</Note>
### Veröffentlichen
```bash filename="Terminal"
@@ -15,7 +15,7 @@ Os componentes de front-end podem ser renderizados em dois locais dentro do Twen
## Exemplo básico
The quickest way to see a front component in action is to register it with a **command menu item**. Use `defineCommandMenuItem` in a separate file to make the component appear as a quick-action button in the top-right corner of the page:
A maneira mais rápida de ver um componente de front-end em ação é registrá-lo como um **item do menu de comando**. Use `defineCommandMenuItem` em um arquivo separado para fazer o componente aparecer como um botão de ação rápida no canto superior direito da página:
```tsx src/front-components/hello-world.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
@@ -350,7 +350,7 @@ export default defineFrontComponent({
## defineCommandMenuItem
Use `defineCommandMenuItem` to register a front component in the command menu (Cmd+K). Se `isPinned` for `true`, ele também aparece como um botão de ação rápida no canto superior direito da página.
Use `defineCommandMenuItem` para registrar um componente de front-end no menu de comando (Cmd+K). Se `isPinned` for `true`, ele também aparece como um botão de ação rápida no canto superior direito da página.
```ts src/command-menu-items/open-dashboard.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
@@ -370,7 +370,7 @@ export default defineCommandMenuItem({
| --------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `universalIdentifier` | Sim | ID exclusivo e estável para o comando |
| `label` | Sim | Rótulo completo exibido no menu de comandos (Cmd+K) |
| `frontComponentUniversalIdentifier` | Sim | The `universalIdentifier` of the front component this command opens |
| `frontComponentUniversalIdentifier` | Sim | O `universalIdentifier` do componente de front-end que este comando abre |
| `shortLabel` | Não | Rótulo mais curto exibido no botão fixado de ação rápida |
| `icon` | Não | Nome do ícone exibido ao lado do rótulo (por exemplo, `'IconBolt'`, `'IconSend'`) |
| `isPinned` | Não | Quando `true`, mostra o comando como um botão de ação rápida no canto superior direito da página |
@@ -198,6 +198,14 @@ export default defineApplication({
Veja o [acordeão de defineApplication](/l/pt/developers/extend/apps/building#defineentity-functions) na página Building Apps para a lista completa de campos do marketplace (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl`, etc.).
#### Dimensões recomendadas para capturas de tela
O marketplace renderiza `screenshots` em um contêiner fixo de `8:5` (por exemplo, `1600×1000 px`).
<Note>
Capturas de tela de qualquer proporção são exibidas por completo e nunca são cortadas, mas qualquer coisa significativamente mais alta ou mais estreita que `8:5` exibirá faixas vazias nas laterais.
</Note>
### Publicar
```bash filename="Terminal"
@@ -15,7 +15,7 @@ Componentele frontale pot fi afișate în două locații în cadrul Twenty:
## Exemplu de bază
The quickest way to see a front component in action is to register it with a **command menu item**. Use `defineCommandMenuItem` in a separate file to make the component appear as a quick-action button in the top-right corner of the page:
Cel mai rapid mod de a vedea o componentă front-end în acțiune este să o înregistrați ca **element din meniul de comenzi**. Folosiți `defineCommandMenuItem` într-un fișier separat pentru ca componenta să apară ca buton de acțiune rapidă în colțul din dreapta sus al paginii:
```tsx src/front-components/hello-world.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
@@ -350,7 +350,7 @@ export default defineFrontComponent({
## defineCommandMenuItem
Use `defineCommandMenuItem` to register a front component in the command menu (Cmd+K). Dacă `isPinned` este `true`, apare și ca buton de acțiune rapidă în colțul din dreapta sus al paginii.
Folosiți `defineCommandMenuItem` pentru a înregistra o componentă front-end în meniul de comenzi (Cmd+K). Dacă `isPinned` este `true`, apare și ca buton de acțiune rapidă în colțul din dreapta sus al paginii.
```ts src/command-menu-items/open-dashboard.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
@@ -370,7 +370,7 @@ export default defineCommandMenuItem({
| --------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `universalIdentifier` | Da | ID unic stabil pentru comandă |
| `label` | Da | Etichetă completă afișată în meniul de comenzi (Cmd+K) |
| `frontComponentUniversalIdentifier` | Da | The `universalIdentifier` of the front component this command opens |
| `frontComponentUniversalIdentifier` | Da | `universalIdentifier` al componentei front-end pe care această comandă o deschide |
| `shortLabel` | Nu | Etichetă mai scurtă afișată pe butonul de acțiune rapidă fixat |
| `icon` | Nu | Numele pictogramei afișat lângă etichetă (de ex. `'IconBolt'`, `'IconSend'`) |
| `isPinned` | Nu | Când este `true`, afișează comanda ca buton de acțiune rapidă în colțul din dreapta sus al paginii |
@@ -198,6 +198,14 @@ export default defineApplication({
Vezi [acordeonul defineApplication](/l/ro/developers/extend/apps/building#defineentity-functions) din pagina Building Apps pentru lista completă de câmpuri ale marketplace-ului (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl`, etc.).
#### Dimensiuni recomandate pentru capturi de ecran
marketplace-ul redă `screenshots` într-un container fix cu raport `8:5` (de exemplu, `1600×1000 px`).
<Note>
Capturile de ecran cu orice raport de aspect sunt afișate integral și nu sunt niciodată decupate, însă orice este semnificativ mai înalt sau mai îngust decât `8:5` va afișa benzi goale pe laterale.
</Note>
### Publicare
```bash filename="Terminal"
@@ -15,7 +15,7 @@ icon: window-maximize
## Простой пример
The quickest way to see a front component in action is to register it with a **command menu item**. Use `defineCommandMenuItem` in a separate file to make the component appear as a quick-action button in the top-right corner of the page:
Самый быстрый способ увидеть фронтенд-компонент в действии — зарегистрировать его в качестве **пункта меню команд**. Используйте `defineCommandMenuItem` в отдельном файле, чтобы компонент отображался как кнопка быстрого действия в правом верхнем углу страницы:
```tsx src/front-components/hello-world.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
@@ -350,7 +350,7 @@ export default defineFrontComponent({
## defineCommandMenuItem
Use `defineCommandMenuItem` to register a front component in the command menu (Cmd+K). Если `isPinned` имеет значение `true`, команда также отображается как кнопка быстрого действия в правом верхнем углу страницы.
Используйте `defineCommandMenuItem`, чтобы зарегистрировать фронтенд-компонент в меню команд (Cmd+K). Если `isPinned` имеет значение `true`, команда также отображается как кнопка быстрого действия в правом верхнем углу страницы.
```ts src/command-menu-items/open-dashboard.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
@@ -370,7 +370,7 @@ export default defineCommandMenuItem({
| --------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `universalIdentifier` | Да | Стабильный уникальный идентификатор для команды |
| `label` | Да | Полная метка, отображаемая в меню команд (Cmd+K) |
| `frontComponentUniversalIdentifier` | Да | The `universalIdentifier` of the front component this command opens |
| `frontComponentUniversalIdentifier` | Да | `universalIdentifier` фронтенд-компонента, который открывается этой командой |
| `shortLabel` | Нет | Короткая метка, отображаемая на закреплённой кнопке быстрого действия |
| `icon` | Нет | Имя значка, отображаемое рядом с меткой (например, `'IconBolt'`, `'IconSend'`) |
| `isPinned` | Нет | При значении `true` показывает команду как кнопку быстрого действия в правом верхнем углу страницы |
@@ -198,6 +198,14 @@ export default defineApplication({
См. [аккордеон defineApplication](/l/ru/developers/extend/apps/building#defineentity-functions) на странице «Создание приложений» для полного списка полей маркетплейса (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl` и т. д.).
#### Рекомендуемые размеры скриншотов
Маркетплейс отображает `screenshots` в контейнере с фиксированным соотношением сторон `8:5` (например, `1600×1000 px`).
<Note>
Скриншоты с любым соотношением сторон отображаются полностью и никогда не обрезаются, но всё, что значительно выше или уже, чем `8:5`, будет иметь пустые поля по бокам.
</Note>
### Публикация
```bash filename="Terminal"
@@ -15,7 +15,7 @@ icon: window-maximize
## Basit örnek
The quickest way to see a front component in action is to register it with a **command menu item**. Use `defineCommandMenuItem` in a separate file to make the component appear as a quick-action button in the top-right corner of the page:
Bir ön uç bileşenini çalışırken görmenin en hızlı yolu, onu bir **komut menüsü öğesi** olarak kaydetmektir. Bileşenin sayfanın sağ üst köşesinde bir hızlı işlem düğmesi olarak görünmesini sağlamak için ayrı bir dosyada `defineCommandMenuItem` kullanın:
```tsx src/front-components/hello-world.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
@@ -350,7 +350,7 @@ export default defineFrontComponent({
## defineCommandMenuItem
Use `defineCommandMenuItem` to register a front component in the command menu (Cmd+K). `isPinned` `true` ise, sayfanın sağ üst köşesinde bir hızlı işlem düğmesi olarak da görünür.
Bir ön uç bileşenini komut menüsüne (Cmd+K) kaydetmek için `defineCommandMenuItem` kullanın. `isPinned` `true` ise, sayfanın sağ üst köşesinde bir hızlı işlem düğmesi olarak da görünür.
```ts src/command-menu-items/open-dashboard.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
@@ -370,7 +370,7 @@ export default defineCommandMenuItem({
| --------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `universalIdentifier` | Evet | Komut için kalıcı benzersiz kimlik |
| `label` | Evet | Komut menüsünde (Cmd+K) gösterilen tam etiket |
| `frontComponentUniversalIdentifier` | Evet | The `universalIdentifier` of the front component this command opens |
| `frontComponentUniversalIdentifier` | Evet | Bu komutun açtığı ön uç bileşeninin `universalIdentifier` değeri |
| `shortLabel` | Hayır | Sabitlenmiş hızlı işlem düğmesinde görüntülenen daha kısa etiket |
| `icon` | Hayır | Etiketin yanında görüntülenen simge adı (örn. `'IconBolt'`, `'IconSend'`) |
| `isPinned` | Hayır | `true` olduğunda, komutu sayfanın sağ üst köşesinde bir hızlı işlem düğmesi olarak gösterir |
@@ -43,7 +43,7 @@ Uygulamanızın eşitleme yapacağı bir Twenty sunucusuna ihtiyacı vardır. Su
> **Yerel bir Twenty örneği kurmak ister misiniz?**
* **Evet (önerilir)** — `twentycrm/twenty-app-dev` Docker imajını çeker ve `2020` portunda başlatır. Önce Docker'ın çalıştığından emin olun.
* **Hayır** — Bağlanmak istediğiniz zaten bir Twenty sunucunuz varsa bunu seçin. Bunu daha sonra `yarn twenty remote add` ile bağlayabilirsiniz.
* **Hayır** — Zaten bağlanmak istediğiniz bir Twenty sunucunuz varsa bunu seçin. Bunu daha sonra `yarn twenty remote add` ile bağlayabilirsiniz.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/start-instance.png" alt="Yerel örnek başlatılsın mı?" />
@@ -58,7 +58,7 @@ Sunucu çalışır duruma geldiğinde, oturum açmak için bir tarayıcı açıl
<img src="/images/docs/developers/extends/apps/login.png" alt="Twenty oturum açma ekranı" />
</div>
Sonraki ekranda **Authorize**'a tıklayın — bu işlem CLI'ın çalışma alanınıza erişmesine izin verir.
Sonraki ekranda **Authorize**'a tıklayın — bu işlem CLI'nin çalışma alanınıza erişmesine izin verir.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/authorize.png" alt="Twenty CLI yetkilendirme ekranı" />
@@ -198,6 +198,14 @@ export default defineApplication({
Pazar yeri alanlarının tam listesi için Uygulama Oluşturma sayfasındaki [defineApplication akordeonu](/l/tr/developers/extend/apps/building#defineentity-functions)'na bakın (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl`, vb.).
#### Önerilen ekran görüntüsü boyutları
Marketplace, `screenshots`'ı sabit `8:5` oranlı bir kapsayıcıda görüntüler (örneğin, `1600×1000 px`).
<Note>
Herhangi bir en-boy oranındaki ekran görüntüleri eksiksiz görüntülenir ve asla kırpılmaz, ancak `8:5`'ten belirgin ölçüde daha uzun veya daha dar olanlarda yanlarda boş bantlar görünür.
</Note>
### Yayımla
```bash filename="Terminal"
@@ -198,6 +198,14 @@ export default defineApplication({
See the [defineApplication accordion](/l/zh/developers/extend/apps/building#defineentity-functions) in the Building Apps page for the full list of marketplace fields (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl`, etc.).
#### 建议的屏幕截图尺寸
该市场会在固定的 `8:5` 容器中渲染 `screenshots`(例如,`1600×1000 px`)。
<Note>
任意纵横比的屏幕截图都会完整显示,且绝不会被裁剪,但相对于 `8:5` 明显更高或更窄的图片,两侧会出现空白边。
</Note>
### Publish
```bash filename="Terminal"
@@ -185,6 +185,7 @@ export type AiSystemPromptSection = {
export enum AllMetadataName {
agent = 'agent',
applicationVariable = 'applicationVariable',
commandMenuItem = 'commandMenuItem',
connectionProvider = 'connectionProvider',
fieldMetadata = 'fieldMetadata',
@@ -2167,6 +2167,11 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Toepassing suksesvol opgegradeer."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,6 +2167,11 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "تمت ترقية التطبيق بنجاح."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,6 +2167,11 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Aplicació actualitzada correctament."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,6 +2167,11 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Aplikace byla úspěšně aktualizována."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,6 +2167,11 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Applikationen blev opgraderet med succes."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,6 +2167,11 @@ msgstr "Anwendung erfolgreich deinstalliert."
msgid "Application upgraded successfully."
msgstr "Anwendung erfolgreich aktualisiert."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,6 +2167,11 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Η εφαρμογή αναβαθμίστηκε με επιτυχία."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
+5
View File
@@ -2162,6 +2162,11 @@ msgstr "Application successfully uninstalled."
msgid "Application upgraded successfully."
msgstr "Application upgraded successfully."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr "application variable"
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,6 +2167,11 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Aplicación actualizada con éxito."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,6 +2167,11 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Sovellus päivitetty onnistuneesti."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,6 +2167,11 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Application mise à niveau avec succès."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
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
@@ -2167,6 +2167,11 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "היישום שודרג בהצלחה."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,6 +2167,11 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Az alkalmazás sikeresen frissítve."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,6 +2167,11 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Applicazione aggiornata con successo."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,6 +2167,11 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "アプリケーションのアップグレードに成功しました。"
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,6 +2167,11 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "응용 프로그램이 성공적으로 업그레이드되었습니다."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,6 +2167,11 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Applicatie succesvol geüpgraded."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,6 +2167,11 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Applikasjonen ble oppgradert vellykket."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,6 +2167,11 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Aplikacja została pomyślnie zaktualizowana."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2162,6 +2162,11 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr ""
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,6 +2167,11 @@ msgstr "Aplicação desinstalada com sucesso."
msgid "Application upgraded successfully."
msgstr "Aplicação atualizada com sucesso."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,6 +2167,11 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Aplicação atualizada com sucesso."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,6 +2167,11 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Aplicația a fost actualizată cu succes."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
Binary file not shown.
@@ -2167,6 +2167,11 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Апликација је успешно надограђена."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,6 +2167,11 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Applikationen uppgraderades framgångsrikt."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,6 +2167,11 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Uygulama başarıyla yükseltildi."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,6 +2167,11 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Застосунок успішно оновлено."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,6 +2167,11 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "Đã nâng cấp ứng dụng thành công."
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,6 +2167,11 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "应用程序升级成功。"
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -2167,6 +2167,11 @@ msgstr ""
msgid "Application upgraded successfully."
msgstr "應用程式升級成功。"
#. js-lingui-id: Okob8q
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "application variable"
msgstr ""
#. js-lingui-id: fL7WXr
#: src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
#: src/pages/settings/layout/components/SettingsLayoutDetailScaffold.tsx
@@ -0,0 +1,53 @@
import { type FieldFunctionOptions } from '@apollo/client/cache';
import { sortCachedObjectEdges } from '@/apollo/optimistic-effect/utils/sortCachedObjectEdges';
import { type RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge';
import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
import { type RecordGqlOperationOrderBy } from 'twenty-shared/types';
type NewEntry = {
edge: RecordGqlRefEdge;
record: RecordGqlNode;
};
type BuildSortedConnectionEdgesArgs = {
currentEdges: readonly RecordGqlRefEdge[];
newEntries: readonly NewEntry[];
orderBy: RecordGqlOperationOrderBy | undefined;
readField: FieldFunctionOptions['readField'];
};
export const buildSortedConnectionEdges = ({
currentEdges,
newEntries,
orderBy,
readField,
}: BuildSortedConnectionEdgesArgs): RecordGqlRefEdge[] => {
const firstEdges: RecordGqlRefEdge[] = [];
const lastEdges: RecordGqlRefEdge[] = [];
const sortableEdges: RecordGqlRefEdge[] = [];
for (const { edge, record } of newEntries) {
if (record.position === 'first') {
firstEdges.push(edge);
} else if (record.position === 'last') {
lastEdges.push(edge);
} else {
sortableEdges.push(edge);
}
}
let middleEdges: RecordGqlRefEdge[];
if (Array.isArray(orderBy) && orderBy.length > 0) {
middleEdges = sortCachedObjectEdges({
edges: [...currentEdges, ...sortableEdges],
orderBy,
readCacheField: readField,
});
} else {
middleEdges = [...sortableEdges, ...currentEdges];
}
return [...firstEdges, ...middleEdges, ...lastEdges];
};
@@ -1,6 +1,7 @@
import { type ApolloCache, type StoreObject } from '@apollo/client';
import { isNonEmptyString } from '@sniptt/guards';
import { buildSortedConnectionEdges } from '@/apollo/optimistic-effect/utils/buildSortedConnectionEdges';
import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { type RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge';
@@ -119,120 +120,78 @@ export const triggerCreateRecordsOptimisticEffect = ({
hasPreviousPage?: boolean;
}>('pageInfo', rootQueryCachedObjectRecordConnection);
const nextRootQueryCachedRecordEdges = rootQueryCachedRecordEdges
? [...rootQueryCachedRecordEdges]
: [];
const newEntries = recordsToCreate.flatMap<{
edge: RecordGqlRefEdge;
record: RecordGqlNode;
}>((recordToCreate) => {
if (!isNonEmptyString(recordToCreate.id)) {
return [];
}
const nextQueryCachedPageInfo = isDefined(rootQueryCachedPageInfo)
? { ...rootQueryCachedPageInfo }
: {};
if (
isDefined(rootQueryFilter) &&
shouldMatchRootQueryFilter === true &&
!isRecordMatchingFilter({
record: recordToCreate,
filter: rootQueryFilter,
objectMetadataItem,
})
) {
return [];
}
const hasAddedRecords = recordsToCreate
.map((recordToCreate) => {
if (isNonEmptyString(recordToCreate.id)) {
if (
isDefined(rootQueryFilter) &&
shouldMatchRootQueryFilter === true
) {
const recordToCreateMatchesThisRootQueryFilter =
isRecordMatchingFilter({
record: recordToCreate,
filter: rootQueryFilter,
objectMetadataItem,
});
const node = toReference(recordToCreate);
if (!recordToCreateMatchesThisRootQueryFilter) {
return false;
}
}
if (!isDefined(node)) {
return [];
}
const recordToCreateReference = toReference(recordToCreate);
const recordAlreadyInCache = rootQueryCachedRecordEdges?.some(
(cachedEdge) =>
cache.identify(node) === cache.identify(cachedEdge.node),
);
if (!recordToCreateReference) {
throw new Error(
`Failed to create reference for record with id: ${recordToCreate.id}`,
);
}
if (recordAlreadyInCache === true) {
return [];
}
const recordAlreadyInCache = rootQueryCachedRecordEdges?.some(
(cachedEdge) => {
return (
cache.identify(recordToCreateReference) ===
cache.identify(cachedEdge.node)
);
},
);
return [
{
edge: {
__typename: getEdgeTypename(objectMetadataItem.nameSingular),
node,
cursor: encodeCursor(recordToCreate),
},
record: recordToCreate,
},
];
});
if (isDefined(recordToCreateReference) && !recordAlreadyInCache) {
const cursor = encodeCursor(recordToCreate);
const edge = {
__typename: getEdgeTypename(objectMetadataItem.nameSingular),
node: recordToCreateReference,
cursor,
};
if (
!isDefined(recordToCreate.position) ||
recordToCreate.position === 'first'
) {
nextRootQueryCachedRecordEdges.unshift(edge);
nextQueryCachedPageInfo.startCursor = cursor;
} else if (recordToCreate.position === 'last') {
nextRootQueryCachedRecordEdges.push(edge);
nextQueryCachedPageInfo.endCursor = cursor;
} else if (typeof recordToCreate.position === 'number') {
let index = Math.round(
nextRootQueryCachedRecordEdges.length *
recordToCreate.position,
);
if (recordToCreate.position < 0) {
index = Math.max(
0,
nextRootQueryCachedRecordEdges.length +
Math.round(recordToCreate.position),
);
} else if (recordToCreate.position > 1) {
index = nextRootQueryCachedRecordEdges.length;
}
index = Math.max(
0,
Math.min(index, nextRootQueryCachedRecordEdges.length),
);
nextRootQueryCachedRecordEdges.splice(index, 0, edge);
if (index === 0) {
nextQueryCachedPageInfo.startCursor = cursor;
} else if (
index ===
nextRootQueryCachedRecordEdges.length - 1
) {
nextQueryCachedPageInfo.endCursor = cursor;
}
}
return true;
}
}
return false;
})
.some((hasAddedRecord) => hasAddedRecord);
if (!hasAddedRecords) {
if (newEntries.length === 0) {
return rootQueryCachedObjectRecordConnection;
}
const sortedEdges = buildSortedConnectionEdges({
currentEdges: rootQueryCachedRecordEdges ?? [],
newEntries,
orderBy: rootQueryVariables?.orderBy,
readField,
});
return {
...rootQueryCachedObjectRecordConnection,
edges: nextRootQueryCachedRecordEdges,
edges: sortedEdges,
totalCount: isDefined(rootQueryCachedRecordTotalCount)
? rootQueryCachedRecordTotalCount + 1
? rootQueryCachedRecordTotalCount + newEntries.length
: undefined,
pageInfo: nextQueryCachedPageInfo,
pageInfo: {
...(rootQueryCachedPageInfo ?? {}),
startCursor:
sortedEdges[0]?.cursor ?? rootQueryCachedPageInfo?.startCursor,
endCursor:
sortedEdges[sortedEdges.length - 1]?.cursor ??
rootQueryCachedPageInfo?.endCursor,
},
};
},
},
@@ -48,6 +48,7 @@ export const useMetadataErrorHandler = () => {
navigationMenuItem: t`navigation menu item`,
webhook: t`webhook`,
viewSort: t`view sort`,
applicationVariable: t`application variable`,
connectionProvider: t`connection provider`,
} as const satisfies Record<AllMetadataName, string>;
@@ -1,20 +1,11 @@
import { WidgetSkeletonLoader } from '@/page-layout/widgets/components/WidgetSkeletonLoader';
import { PageLayoutWidgetInvalidConfigDisplay } from '@/page-layout/widgets/components/PageLayoutWidgetInvalidConfigDisplay';
import { GraphWidgetAggregateChartRenderer } from '@/page-layout/widgets/graph/graph-widget-aggregate-chart/components/GraphWidgetAggregateChartRenderer';
import { GraphWidgetBarChartRenderer } from '@/page-layout/widgets/graph/graph-widget-bar-chart/components/GraphWidgetBarChartRenderer';
import { GraphWidgetLineChartRenderer } from '@/page-layout/widgets/graph/graph-widget-line-chart/components/GraphWidgetLineChartRenderer';
import { GraphWidgetPieChartRenderer } from '@/page-layout/widgets/graph/graph-widget-pie-chart/components/GraphWidgetPieChartRenderer';
import { useCurrentWidget } from '@/page-layout/widgets/hooks/useCurrentWidget';
import { lazy, Suspense } from 'react';
import { WidgetConfigurationType } from '~/generated-metadata/graphql';
const GraphWidgetGaugeChart = lazy(() =>
import(
'@/page-layout/widgets/graph/graph-widget-gauge-chart/components/GraphWidgetGaugeChart'
).then((module) => ({
default: module.GraphWidgetGaugeChart,
})),
);
export const GraphWidget = () => {
const widget = useCurrentWidget();
@@ -24,31 +15,6 @@ export const GraphWidget = () => {
case WidgetConfigurationType.AGGREGATE_CHART:
return <GraphWidgetAggregateChartRenderer />;
case WidgetConfigurationType.GAUGE_CHART: {
const gaugeData = {
value: 0.7,
min: 0,
max: 1,
label: 'Progress',
};
return (
<Suspense fallback={<WidgetSkeletonLoader />}>
<GraphWidgetGaugeChart
data={{
value: gaugeData.value,
min: gaugeData.min,
max: gaugeData.max,
label: gaugeData.label,
}}
displayType="percentage"
showValue
id={`gauge-chart-${widget.id}`}
/>
</Suspense>
);
}
case WidgetConfigurationType.PIE_CHART:
return <GraphWidgetPieChartRenderer />;
@@ -58,6 +24,9 @@ export const GraphWidget = () => {
case WidgetConfigurationType.LINE_CHART:
return <GraphWidgetLineChartRenderer />;
case WidgetConfigurationType.GAUGE_CHART:
return <PageLayoutWidgetInvalidConfigDisplay />;
default:
return null;
}
@@ -29,8 +29,7 @@ const StyledScreenshotsContainer = styled.div`
const StyledScreenshotImage = styled.img`
height: 100%;
object-fit: cover;
object-position: center;
object-fit: contain;
width: 100%;
`;
@@ -43,6 +42,7 @@ const StyledScreenshotThumbnails = styled.div`
const StyledThumbnail = styled.div<{ isSelected?: boolean }>`
align-items: center;
aspect-ratio: 8 / 5;
background-color: ${themeCssVariables.background.secondary};
border: 1px solid
${({ isSelected }) =>
@@ -53,7 +53,6 @@ const StyledThumbnail = styled.div<{ isSelected?: boolean }>`
cursor: pointer;
display: flex;
flex: 0 0 96px;
height: 56px;
justify-content: center;
overflow: hidden;
@@ -64,8 +63,7 @@ const StyledThumbnail = styled.div<{ isSelected?: boolean }>`
const StyledThumbnailImage = styled.img`
height: 100%;
object-fit: cover;
object-position: center;
object-fit: contain;
width: 100%;
`;
@@ -12,7 +12,6 @@ const graphTypeOptions = [
GraphType.LINE,
GraphType.PIE,
GraphType.AGGREGATE,
GraphType.GAUGE,
];
const StyledChartTypeSelectionContainer = styled.div`
@@ -41,13 +41,35 @@ export const SSEQuerySubscribeEffect = () => {
const requiredQueryListeners = useAtomStateValue(requiredQueryListenersState);
const activeQueryListeners = useAtomStateValue(activeQueryListenersState);
const updateQueryListeners = useCallback(async () => {
const handleError = useCallback(
(error: unknown) => {
if (CombinedGraphQLErrors.is(error)) {
const extensions = getGraphqlErrorExtensionsFromError(error);
if (
isGracefullyHandledEventStreamError({
subCode: extensions?.subCode,
code: extensions?.code,
})
) {
store.set(activeQueryListenersState.atom, []);
store.set(shouldDestroyEventStreamState.atom, true);
return;
}
throw new Error(`Unhandled error for event stream: ${error.message}`);
}
},
[store],
);
const syncAdditions = useCallback(async () => {
if (!isDefined(sseEventStreamId)) {
return;
}
const requiredQueryListeners = store.get(requiredQueryListenersState.atom);
const activeQueryListeners = store.get(activeQueryListenersState.atom);
const queryListenersToAdd = requiredQueryListeners.filter(
@@ -57,12 +79,9 @@ export const SSEQuerySubscribeEffect = () => {
),
);
const queryListenersToRemove = activeQueryListeners.filter(
(listener) =>
!requiredQueryListeners.some(
(requiredListener) => requiredListener.queryId === listener.queryId,
),
);
if (queryListenersToAdd.length === 0) {
return;
}
try {
for (const queryListenerToAdd of queryListenersToAdd) {
@@ -83,7 +102,46 @@ export const SSEQuerySubscribeEffect = () => {
return;
}
}
} catch (error) {
handleError(error);
return;
}
const currentActive = store.get(activeQueryListenersState.atom);
store.set(activeQueryListenersState.atom, [
...currentActive,
...queryListenersToAdd,
]);
}, [addQueryToEventStream, handleError, sseEventStreamId, store]);
const syncRemovals = useCallback(async () => {
if (!isDefined(sseEventStreamId)) {
return;
}
const freshRequiredQueryListeners = store.get(
requiredQueryListenersState.atom,
);
const activeQueryListeners = store.get(activeQueryListenersState.atom);
const queryListenersToRemove = activeQueryListeners.filter(
(listener) =>
!freshRequiredQueryListeners.some(
(requiredListener) => requiredListener.queryId === listener.queryId,
),
);
if (queryListenersToRemove.length === 0) {
return;
}
const removedQueryIds = new Set(
queryListenersToRemove.map((listener) => listener.queryId),
);
try {
for (const queryListenerToRemove of queryListenersToRemove) {
const result = await removeQueryFromEventStream({
variables: {
@@ -102,60 +160,72 @@ export const SSEQuerySubscribeEffect = () => {
}
}
} catch (error) {
if (CombinedGraphQLErrors.is(error)) {
const extensions = getGraphqlErrorExtensionsFromError(error);
handleError(error);
if (
isGracefullyHandledEventStreamError({
subCode: extensions?.subCode,
code: extensions?.code,
})
) {
store.set(activeQueryListenersState.atom, []);
store.set(shouldDestroyEventStreamState.atom, true);
return;
}
throw new Error(`Unhandled error for event stream: ${error.message}`);
}
return;
}
store.set(activeQueryListenersState.atom, requiredQueryListeners);
}, [
addQueryToEventStream,
removeQueryFromEventStream,
sseEventStreamId,
store,
]);
const currentActive = store.get(activeQueryListenersState.atom);
const debouncedUpdateQueryListeners = useDebouncedCallback(
updateQueryListeners,
1000,
{ leading: true },
);
store.set(
activeQueryListenersState.atom,
currentActive.filter(
(listener) => !removedQueryIds.has(listener.queryId),
),
);
}, [handleError, removeQueryFromEventStream, sseEventStreamId, store]);
const debouncedSyncAdditions = useDebouncedCallback(syncAdditions, 1000, {
leading: true,
});
const debouncedSyncRemovals = useDebouncedCallback(syncRemovals, 200, {
leading: false,
});
useEffect(() => {
if (!isNonEmptyString(sseEventStreamId) || !sseEventStreamReady) {
return;
}
const areRequiredQueryListenersDifferentFromActiveQueryListeners =
compareArraysOfObjectsByProperty(
requiredQueryListeners,
activeQueryListeners,
'queryId',
);
const areDifferent = compareArraysOfObjectsByProperty(
requiredQueryListeners,
activeQueryListeners,
'queryId',
);
if (areRequiredQueryListenersDifferentFromActiveQueryListeners) {
debouncedUpdateQueryListeners();
if (!areDifferent) {
return;
}
const hasAdditions = requiredQueryListeners.some(
(listener) =>
!activeQueryListeners.some(
(activeListener) => activeListener.queryId === listener.queryId,
),
);
const hasRemovals = activeQueryListeners.some(
(listener) =>
!requiredQueryListeners.some(
(requiredListener) => requiredListener.queryId === listener.queryId,
),
);
if (hasAdditions) {
debouncedSyncAdditions();
}
if (hasRemovals) {
debouncedSyncRemovals();
}
}, [
sseEventStreamId,
sseEventStreamReady,
requiredQueryListeners,
activeQueryListeners,
debouncedUpdateQueryListeners,
debouncedSyncAdditions,
debouncedSyncRemovals,
]);
return null;
@@ -6,6 +6,7 @@ import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldM
import { SidePanelFooter } from '@/ui/layout/side-panel/components/SidePanelFooter';
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
import { type WorkflowFormAction } from '@/workflow/types/Workflow';
import { WorkflowRunSSESubscribeEffect } from '@/workflow/workflow-diagram/components/WorkflowRunSSESubscribeEffect';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { useUpdateWorkflowRunStep } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowRunStep';
import { WorkflowFormFieldInput } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowFormFieldInput';
@@ -100,6 +101,7 @@ export const WorkflowEditActionFormFiller = ({
return (
<>
<WorkflowRunSSESubscribeEffect workflowRunId={workflowRunId} />
<WorkflowStepBody>
{formData.map((field) => {
if (field.type === 'RECORD') {
+11
View File
@@ -76,3 +76,14 @@ export type FunctionExecutionResult = {
stackTrace: string;
};
};
export type ChokidarFsEvent =
| 'add'
| 'addDir'
| 'change'
| 'unlink'
| 'unlinkDir'
| 'ready'
| 'raw'
| 'error'
| 'all';
@@ -1,7 +1,7 @@
import path, { relative } from 'path';
import chokidar, { type FSWatcher } from 'chokidar';
import { type EventName } from 'chokidar/handler.js';
import { ASSETS_DIR } from 'twenty-shared/application';
import { type ChokidarFsEvent } from '@/cli/types';
export type ManifestWatcherOptions = {
appPath: string;
@@ -13,7 +13,10 @@ const IGNORED_DIRECTORY_NAMES = new Set(['node_modules', 'dist', '.twenty']);
export class ManifestWatcher {
private appPath: string;
private handleChangeDetected: (filePath: string, event: EventName) => void;
private handleChangeDetected: (
filePath: string,
event: ChokidarFsEvent,
) => void;
private verbose: boolean;
private watcher: FSWatcher | null = null;
@@ -10,7 +10,7 @@ import { type ManifestBuildResult } from '@/cli/utilities/build/manifest/manifes
import { ManifestWatcher } from '@/cli/utilities/build/manifest/manifest-watcher';
import { type OrchestratorState } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state';
import type { Location } from 'esbuild';
import { type EventName } from 'chokidar/handler.js';
import { type ChokidarFsEvent } from '@/cli/types';
import { ASSETS_DIR } from 'twenty-shared/application';
import { FileFolder } from 'twenty-shared/types';
@@ -102,7 +102,10 @@ export class StartWatchersOrchestratorStep {
]);
}
private handleChangeDetected(sourcePath: string, event: EventName): void {
private handleChangeDetected(
sourcePath: string,
event: ChokidarFsEvent,
): void {
this.state.addEvent({
message: `Change detected: ${sourcePath}`,
status: 'info',
@@ -0,0 +1,33 @@
import { QueryRunner } from 'typeorm';
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
@RegisteredInstanceCommand('2.3.0', 1777966965587)
export class TransformApplicationVariableToSyncableEntityFastInstanceCommand
implements FastInstanceCommand
{
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'DROP INDEX "core"."IDX_78ae6cfe5f49a76c4bf842ad58"',
);
await queryRunner.query(
'ALTER TABLE "core"."applicationVariable" DROP CONSTRAINT "IDX_APPLICATION_VARIABLE_KEY_APPLICATION_ID_UNIQUE"',
);
await queryRunner.query(
'ALTER TABLE "core"."applicationVariable" ADD "universalIdentifier" uuid',
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'ALTER TABLE "core"."applicationVariable" DROP COLUMN "universalIdentifier"',
);
await queryRunner.query(
'ALTER TABLE "core"."applicationVariable" ADD CONSTRAINT "IDX_APPLICATION_VARIABLE_KEY_APPLICATION_ID_UNIQUE" UNIQUE ("key", "applicationId")',
);
await queryRunner.query(
'CREATE INDEX "IDX_78ae6cfe5f49a76c4bf842ad58" ON "core"."applicationVariable" ("workspaceId") ',
);
}
}
@@ -0,0 +1,54 @@
import { DataSource, QueryRunner } from 'typeorm';
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
import { SlowInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/slow-instance-command.interface';
@RegisteredInstanceCommand('2.3.0', 1777966965588, { type: 'slow' })
export class BackfillApplicationVariableUniversalIdentifierSlowInstanceCommand
implements SlowInstanceCommand
{
async runDataMigration(dataSource: DataSource): Promise<void> {
await dataSource.query(
'DELETE "core"."applicationVariable" WHERE "applicationId" IS NULL',
);
await dataSource.query(
'UPDATE "core"."applicationVariable" SET "universalIdentifier" = gen_random_uuid() WHERE "universalIdentifier" IS NULL',
);
}
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'ALTER TABLE "core"."applicationVariable" DROP CONSTRAINT "FK_51adb49e7f8df35dd23e01c4830"',
);
await queryRunner.query(
'ALTER TABLE "core"."applicationVariable" ALTER COLUMN "applicationId" SET NOT NULL',
);
await queryRunner.query(
'ALTER TABLE "core"."applicationVariable" ADD CONSTRAINT "FK_51adb49e7f8df35dd23e01c4830" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION',
);
await queryRunner.query(
'ALTER TABLE "core"."applicationVariable" ALTER COLUMN "universalIdentifier" SET NOT NULL',
);
await queryRunner.query(
'CREATE UNIQUE INDEX "IDX_44ecebdf70cbed17f89527b36b" ON "core"."applicationVariable" ("workspaceId", "universalIdentifier") ',
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'DROP INDEX "core"."IDX_44ecebdf70cbed17f89527b36b"',
);
await queryRunner.query(
'ALTER TABLE "core"."applicationVariable" ALTER COLUMN "universalIdentifier" DROP NOT NULL',
);
await queryRunner.query(
'ALTER TABLE "core"."applicationVariable" DROP CONSTRAINT "FK_51adb49e7f8df35dd23e01c4830"',
);
await queryRunner.query(
'ALTER TABLE "core"."applicationVariable" ALTER COLUMN "applicationId" DROP NOT NULL',
);
await queryRunner.query(
'ALTER TABLE "core"."applicationVariable" ADD CONSTRAINT "FK_51adb49e7f8df35dd23e01c4830" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION',
);
}
}
@@ -3,6 +3,7 @@ import { Module } from '@nestjs/common';
import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module';
import { DropMessageDirectionFieldCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-workspace-command-1777400000000-drop-message-direction-field.command';
import { BackfillImageIdentifierFieldMetadataIdCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-workspace-command-1777920000000-backfill-image-identifier-field-metadata-id.command';
import { DeleteGaugeWidgetsCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-workspace-command-1798000000000-delete-gauge-widgets.command';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
@@ -17,6 +18,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
providers: [
DropMessageDirectionFieldCommand,
BackfillImageIdentifierFieldMetadataIdCommand,
DeleteGaugeWidgetsCommand,
],
})
export class V2_3_UpgradeVersionCommandModule {}
@@ -0,0 +1,98 @@
import { Command } from 'nest-commander';
import { isDefined } from 'twenty-shared/utils';
import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner';
import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
@RegisteredWorkspaceCommand('2.3.0', 1798000000000)
@Command({
name: 'upgrade:2-3:delete-gauge-widgets',
description:
'Delete all GAUGE_CHART page layout widgets — gauge support has been removed',
})
export class DeleteGaugeWidgetsCommand extends ActiveOrSuspendedWorkspaceCommandRunner {
constructor(
protected readonly workspaceIteratorService: WorkspaceIteratorService,
private readonly applicationService: ApplicationService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
) {
super(workspaceIteratorService);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const isDryRun = options.dryRun ?? false;
const { flatPageLayoutWidgetMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatPageLayoutWidgetMaps',
]);
const gaugeWidgets = Object.values(
flatPageLayoutWidgetMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter(
(widget) =>
widget.universalConfiguration.configurationType ===
WidgetConfigurationType.GAUGE_CHART,
);
if (gaugeWidgets.length === 0) {
this.logger.log(`No gauge widgets in workspace ${workspaceId}`);
return;
}
if (isDryRun) {
this.logger.log(
`[DRY RUN] Would delete ${gaugeWidgets.length} gauge widget(s) in workspace ${workspaceId}`,
);
return;
}
const { twentyStandardFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const result =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayoutWidget: {
flatEntityToCreate: [],
flatEntityToDelete: gaugeWidgets,
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
twentyStandardFlatApplication.universalIdentifier,
},
);
if (result.status === 'fail') {
this.logger.error(
`Failed to delete gauge widgets in workspace ${workspaceId}:\n${JSON.stringify(result, null, 2)}`,
);
throw new Error(
`Failed to delete gauge widgets for workspace ${workspaceId}`,
);
}
this.logger.log(
`Deleted ${gaugeWidgets.length} gauge widget(s) for workspace ${workspaceId}`,
);
}
}
@@ -26,6 +26,8 @@ import { AddToolAndWorkflowActionTriggerSettingsFastInstanceCommand } from 'src/
import { MigrateToolTriggerSettingsSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-slow-1797000002000-migrate-tool-trigger-settings';
import { ConnectionProviderSyncableEntityFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1777896012579-connection-provider-syncable-entity';
import { RemoveUserDefaultAvatarUrlFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1777915958318-remove-user-default-avatar-url';
import { TransformApplicationVariableToSyncableEntityFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1777966965587-transform-application-variable-to-syncable-entity';
import { BackfillApplicationVariableUniversalIdentifierSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-slow-1777966965588-backfill-application-variable-universal-identifier';
export const INSTANCE_COMMANDS = [
AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand,
@@ -54,4 +56,6 @@ export const INSTANCE_COMMANDS = [
MigrateToolTriggerSettingsSlowInstanceCommand,
ConnectionProviderSyncableEntityFastInstanceCommand,
RemoveUserDefaultAvatarUrlFastInstanceCommand,
TransformApplicationVariableToSyncableEntityFastInstanceCommand,
BackfillApplicationVariableUniversalIdentifierSlowInstanceCommand,
];

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