Compare commits

..
Author SHA1 Message Date
Sonarly Claude Code 6c0a0f18da Missing plan metadata in subscription causes billing page crash
https://sonarly.com/issue/7805?type=bug

The `useCurrentPlan` hook throws "Current plan not found" when a workspace's Stripe subscription metadata lacks a `plan` key, crashing the entire billing settings page.

Fix: ## Classification: Category D — Data Integrity Issue

Legacy subscriptions created before the Sept 2025 billing refactoring (`43e0cd5d05`) have `metadata: {}` or `metadata: { workspaceId: '...' }` — no `plan` key. The server already handles this gracefully in `getPlanKeyFromSubscription` by defaulting to `BillingPlanKey.PRO`. The frontend hook was introduced in the same refactoring without this fallback.

## Fix

Extract the plan key resolution into a single variable with a `?? BillingPlanKey.PRO` fallback, matching server-side behavior. `findOrThrow` is preserved so it still throws for genuinely unknown plan key strings in metadata.

```typescript file=packages/twenty-front/src/modules/billing/hooks/useCurrentPlan.ts lines=14-28
  const planKeyFromMetadata =
    (currentWorkspace.currentBillingSubscription?.metadata?.['plan'] as
      | BillingPlanKey
      | undefined) ?? BillingPlanKey.PRO;

  const currentPlan = findOrThrow(
    listPlans(),
    (plan) => plan.planKey === planKeyFromMetadata,
    new Error('Current plan not found'),
  );

  const oppositPlan =
    planKeyFromMetadata === BillingPlanKey.ENTERPRISE
      ? BillingPlanKey.PRO
      : BillingPlanKey.ENTERPRISE;
```

**Why this is correct and not a banned pattern:** This is not silencing an error — it applies the same business rule that already exists server-side: a subscription without a `plan` metadata key is treated as `PRO`. The fallback is deliberate and mirrors the canonical server logic. `findOrThrow` still guards against a completely unrecognized plan key value in the metadata.

**Secondary benefit:** The original code read `metadata?.['plan']` twice (once in `findOrThrow`, once in the `oppositPlan` ternary), with both being independent raw accesses. Now both use the single resolved `planKeyFromMetadata`, making the logic consistent.

**Note for the team:** Consider a backfill script/command under `billing/commands/` to populate `metadata.plan` for all active subscriptions that are missing it, so the fallback becomes unreachable over time.
2026-03-02 14:14:57 +00:00
WeikoandGitHub 37bcb35391 Migrate pagelayout position frontend (#18229)
## Context
Part 1 of migrating gridPosition in favor of typed position
FE should now always send both values to the BE and use both.

Next steps: 
- Update the backend to enforce and validate the new position field + DB
migrations gridPositon -> position (type: GRID)
- Cleanup frontend usage
- Cleanup backend
2026-03-02 14:42:30 +01:00
Thomas TrompetteandGitHub 78a0197643 Prevent deletion of il-else branches (#18294)
If-else branches cannot be recreated once deleted. Only else-if branches
can. On if-else branches removal, we now remplace the node by an empty
node instead of only deleting

Also fixing nested if-else.
2026-03-02 13:52:32 +01:00
ff3326a53b i18n - translations (#18323)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-03-02 13:46:36 +01:00
martmullandGitHub 5e92fb4fc6 Do not console.log while consoleListener (#18322)
It can occur infinite loops

see
https://twenty-v7.sentry.io/issues/7269592888/?environment=prod&environment=prod-eu&project=4507072499810304&query=is%3Aunresolved%20!issue.type%3A%5Bperformance_consecutive_db_queries%2Cperformance_consecutive_http%2Cperformance_file_io_main_thread%2Cperformance_db_main_thread%2Cperformance_n_plus_one_db_queries%2Cperformance_n_plus_one_api_calls%2Cperformance_p95_endpoint_regression%2Cperformance_slow_db_query%2Cperformance_render_blocking_asset_span%2Cperformance_uncompressed_assets%2Cperformance_http_overhead%2Cperformance_large_http_payload%5D%20timesSeen%3A%3E10&referrer=issue-stream&sort=freq
2026-03-02 12:04:12 +00:00
Félix MalfaitandGitHub 1a8be234de OAuth security hardening: RFC compliance, PKCE binding, rate limiting (#18305)
## Summary

Follow-up to #18267. Hardens the OAuth implementation with security
fixes identified during audit:

**P0 — Critical:**
- Bind authorization codes to `client_id` in context to prevent auth
code injection (RFC 6749 §4.1.3)
- Store PKCE `code_challenge` directly in auth code context instead of a
separate `CodeChallenge` token — cryptographically binds the challenge
to its code
- Enforce `code_verifier` when `code_challenge` was used during
authorization
- Hash authorization codes (SHA-256) before storage to prevent exposure
if DB is compromised
- Add `Cache-Control: no-store` + `Pragma: no-cache` headers on token
responses (RFC 6749 §5.1)
- Add rate limiting on `/oauth/token` endpoint (20 req/min per client
via existing `ThrottlerService`)

**P1 — High:**
- Return HTTP 401 for `invalid_client` errors instead of 400 (RFC 6749
§5.2)
- Verify refresh tokens belong to the presenting client (cross-client
token theft prevention)
- Limit fields exposed by public `findApplicationRegistrationByClientId`
query to only what the frontend needs (`id`, `name`, `logoUrl`,
`websiteUrl`, `oAuthScopes`)
- Require `API_KEYS_AND_WEBHOOKS` permission for
`createApplicationRegistration` mutation

**P2/P3 — Medium/Low:**
- Add error handling and loading states to frontend Authorize page
- Rename redirect URL param from `authorizationCode` to `code` (RFC
standard)
- Add unit tests for `validateRedirectUri` utility (8 test cases)

## Test plan

- [ ] Existing OAuth integration tests updated for all changes (hashed
codes, context-based PKCE, client binding, 401 status codes, cache
headers)
- [ ] New test: auth code rejected when presented by a different client
- [ ] New test: refresh token rejected when presented by a different
client
- [ ] New test: `code_verifier` required when PKCE was used in
authorization
- [ ] New test: `Cache-Control: no-store` header present on responses
- [ ] New unit tests for `validateRedirectUri` (HTTPS, localhost,
fragments, invalid URIs)
- [ ] Verify frontend authorize page shows errors gracefully


Made with [Cursor](https://cursor.com)
2026-03-02 12:21:26 +01:00
martmullGitHubcubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
d021f7e369 Fix self host application (#18292)
- Fixes self host application
- add new telemetry information
- add serverId to identify a server instance
- remove .twenty from git tracking
- tree-shake "twenty-sdk" usage in built logic functions and front
components
- fix "twenty-sdk" version usage
- fix twenty-zapier cli

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-03-02 12:06:05 +01:00
neo773andGitHub 1b67ba6a75 Draft emails fix onblur on text input and callout banner component overflow (#18310)
Before

<img width="396" height="759" alt="SCR-20260301-daft"
src="https://github.com/user-attachments/assets/c3fb3a19-3456-424d-9fd2-dd13ed0d2ad5"
/>

After

<img width="391" height="752" alt="SCR-20260301-daio"
src="https://github.com/user-attachments/assets/80a64991-6e69-4ddf-b968-bdc788af02cd"
/>
2026-03-02 12:04:30 +01:00
5afc46ebd3 i18n - translations (#18321)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-03-02 12:03:13 +01:00
a06abb1d60 Fields widget rename group (#18169)
## Rename


https://github.com/user-attachments/assets/b151683a-d1ae-447f-9d9f-95a14b50608b

## Delete



https://github.com/user-attachments/assets/8da73a33-1c57-4771-b712-527b8080117d

---------

Co-authored-by: Weiko <[email protected]>
2026-03-02 10:32:31 +00:00
Félix Malfait 2a5b2746c9 Fix preview-env-dispatch: repository_dispatch requires contents:write
The `repository_dispatch` API endpoint requires `contents: write`
permission on the GITHUB_TOKEN, not `actions: write`. Our security
hardening PR inadvertently changed this to `contents: read`, breaking
the self-dispatch to the keepalive workflow.

Made-with: Cursor
2026-03-02 11:25:14 +01:00
Félix MalfaitandGitHub 0223975bbd Harden GitHub Actions: fix injections, isolate privileged operations to ci-privileged repo (#18318)
## Summary

- Fix expression injection vulnerabilities in composite actions
(`restore-cache`, `nx-affected`) and workflow files (`claude.yml`)
- Reduce overly broad permissions in `ci-utils.yaml` (Danger.js) and
`ci-breaking-changes.yaml`
- Restructure `preview-env-dispatch.yaml`: auto-trigger for members,
opt-in for contributor PRs via `preview-app` label (safe because
keepalive has no write tokens)
- Isolate all write-access operations (PR comments, cross-repo posting)
to a new dedicated
[`twentyhq/ci-privileged`](https://github.com/twentyhq/ci-privileged)
repo via `repository_dispatch`, so that workflows in twenty that execute
contributor code never have write tokens
- Create `post-ci-comments.yaml` (`workflow_run` bridge) to dispatch
breaking changes results to ci-privileged, solving the [fork PR comment
issue](https://github.com/twentyhq/twenty/pull/13713#issuecomment-3168999083)
- Delete 5 unused secrets and broken `i18n-qa-report` workflow
- Remove `TWENTY_DISPATCH_TOKEN` from twenty (moved to ci-privileged as
`CORE_TEAM_ISSUES_COMMENT_TOKEN`)
- Use `toJSON()` for all `client-payload` values to prevent JSON
injection

## Security model after this PR

| Workflow | Executes fork code? | Write tokens available? |
|----------|---------------------|------------------------|
| preview-env-keepalive | Yes | None (contents: read only) |
| preview-env-dispatch | No (base branch) | CI_PRIVILEGED_DISPATCH_TOKEN
only |
| ci-breaking-changes | Yes | None (contents: read only) |
| post-ci-comments (workflow_run) | No (default branch) |
CI_PRIVILEGED_DISPATCH_TOKEN only |
| claude.yml | No (base branch) | CI_PRIVILEGED_DISPATCH_TOKEN,
CLAUDE_CODE_OAUTH_TOKEN |
| ci-utils (Danger.js) | No (base branch) | GITHUB_TOKEN (scoped) |

All actual write tokens (`TWENTY_PR_COMMENT_TOKEN`,
`CORE_TEAM_ISSUES_COMMENT_TOKEN`) live in `twentyhq/ci-privileged` with
strict CODEOWNERS review and branch protection.

## Test plan

- [ ] Verify preview environment comments still appear on member PRs
- [ ] Verify adding `preview-app` label triggers preview for contributor
PRs
- [ ] Verify breaking changes reports still post on PRs (including fork
PRs)
- [ ] Verify Claude cross-repo responses still post on core-team-issues
- [ ] Confirm ci-privileged branch protection is enforced
2026-03-02 10:57:14 +01:00
Félix MalfaitandGitHub 8d47d8ae38 Fix E2E tests broken by redesigned navigation menu (#18315)
## Summary
- **Settings selector**: The Settings navigation item is now rendered as
a `<button>` (via `NavigationDrawerItem` with `onClick`) instead of an
`<a>` link (with `to`). Updated `leftMenu.ts` POM and
`create-kanban-view.spec.ts` to use `getByRole('button', { name:
'Settings' })`.
- **create-record URL field**: The Linkedin field interaction was
missing an initial label click to trigger the hover portal rendering.
Added `recordFieldList.getByText('Linkedin').first().click()` before the
value click, matching the pattern used by the working Emails field.

## Test plan
- [ ] E2E `signup_invite_email.spec.ts` passes (uses
`leftMenu.goToSettings()`)
- [ ] E2E `create-kanban-view.spec.ts` passes (uses Settings click
directly)
- [ ] E2E `create-record.spec.ts` passes (Linkedin URL field
interaction)
- [ ] Existing passing E2E tests remain green


Made with [Cursor](https://cursor.com)
2026-03-02 10:52:10 +01:00
Félix MalfaitandGitHub 68d2297338 Fix expression injection in cross-repo GitHub Actions workflow (#18316)
## Summary

- Fixes a script injection vulnerability in the `claude-cross-repo`
job's `actions/github-script` step where `${{ steps.prompt.outputs.repo
}}` and `${{ steps.prompt.outputs.issue_number }}` were interpolated
directly into JavaScript string literals. A crafted dispatch payload
could inject arbitrary JavaScript with access to
`secrets.TWENTY_DISPATCH_TOKEN`.
- Values are now passed via `env:` and accessed through `process.env`,
which treats them as data rather than code.

## Context

Motivated by the [hackerbot-claw
campaign](https://www.stepsecurity.io/blog/hackerbot-claw-github-actions-exploitation)
which exploited similar `${{ }}` expression injection patterns in
workflows at Microsoft, DataDog, and CNCF projects.

The broader analysis found that our workflow is **not vulnerable** to
the primary attack vector (Pwn Request via `pull_request_target` +
untrusted checkout), and `claude-code-action` already gates on write
access internally. This expression injection in the cross-repo dispatch
job was the only concrete vulnerability identified.

## Test plan

- [ ] Verify the `claude-cross-repo` job still posts comments back to
the source issue after a dispatch run
- [ ] Confirm `TARGET_REPO` and `TARGET_ISSUE` env vars are correctly
resolved from step outputs


Made with [Cursor](https://cursor.com)
2026-03-02 09:20:03 +01:00
Charles BochetandGitHub 1db2a40961 Migrate twenty ui to linaria (#18307)
## Migrate twenty-ui from Emotion to Linaria

Completes the migration of all `twenty-ui` components from Emotion
(runtime CSS-in-JS) to Linaria (zero-runtime, CSS extracted at build
time).

- Replaced `@emotion/styled` with `@linaria/react` across ~170 files
- Removed all Emotion dependencies from `twenty-ui`
- Introduced a CSS custom properties-based theme system:
`themeCssVariables` where every leaf is a `var(--t-xxx)` reference,
injected onto `document.documentElement` by
`ThemeCssVariableInjectorEffect`
- No more `theme` prop threading — styled components reference
`themeCssVariables.x.y` directly at build time
- Updated `twenty-front` consumers to remove `theme={theme}` prop
passing

**Before / After:**
```tsx
// Emotion
color: ${({ theme }) => theme.font.color.primary};
padding: ${({ theme }) => theme.spacing(4)};

// Linaria
color: ${themeCssVariables.font.color.primary};
padding: ${themeCssVariables.spacing[4]};
```

### Theme architecture

Two build-time utilities produce the theme system:

- **`buildThemeReferencingRootCssVariables`** — walks the theme object
and builds a nested mirror where every leaf is a `var(--t-xxx)` string
(evaluated at build time by wyw-in-js)
- **`prepareThemeForRootCssVariableInjection`** — walks the runtime
theme and collects flat `[--css-variable-name, value]` pairs, injected
onto `document.documentElement` by `ThemeCssVariableInjectorEffect`

Both share naming conventions (`camelToKebab`, `SPACING_VALUES`,
`formatSpacingKey`) and are unit tested.

### Spacing cleanup

Spacing scale now uses integers 0–32 (generated via loop), with `0.5`
and `1.5` as the only fractional exceptions. All other fractional
spacing usages (`0.25`, `0.75`, `1.25`, `2.5`, `3.5`) were replaced with
literal pixel values across ~20 twenty-front files.

### Framer Motion integration

Linaria doesn't support `styled(motion.div)` — wrapping a motion element
with `styled()` causes the component body to be stripped at build time.
Instead, we define the styled component first, then wrap it with
`motion.create()`:

```tsx
const StyledBarBase = styled.div`
  background-color: ${themeCssVariables.font.color.primary};
  height: 100%;
`;

const StyledBar = motion.create(StyledBarBase);
```

### Block interpolations

Linaria doesn't support interpolations that return multiple CSS
declarations (Linaria wraps the entire block in a single `var()`,
producing invalid CSS). These were split into individual property
interpolations:

```tsx
// Emotion — single interpolation returning multiple declarations
border-left: ${({ divider, theme }) => {
  const border = `1px solid ${theme.border.color.light}`;
  return divider ? `border-${divider}: ${border}` : '';
}}

// Linaria — one interpolation per property
border-left: ${({ divider }) =>
  divider === 'left' ? `1px solid ${themeCssVariables.border.color.light}` : 'none'};
border-right: ${({ divider }) =>
  divider === 'right' ? `1px solid ${themeCssVariables.border.color.light}` : 'none'};
```

### Dynamic styles via CSS variables

When a component needs to compute styles from multiple props with
complex branching logic (e.g. `Button` combining `variant`, `accent`,
`inverted`, `disabled`, `focus`, `position`), Linaria's prop
interpolations become unwieldy. In those cases we use a
`computeDynamicStyles` function that returns a `CSSProperties` object
injected via `style={}`, referenced from the static CSS with `var()`:

```tsx
const StyledButton = styled.button`
  background: var(--btn-bg);
  border-color: var(--btn-border-color);
  &:hover { background: var(--btn-hover-bg); }
`;

const dynamicStyles = useMemo(() => {
  const s = computeButtonDynamicStyles(variant, accent, ...);
  return { '--btn-bg': s.background, '--btn-hover-bg': s.hoverBackground } as CSSProperties;
}, [variant, accent, ...]);

return <StyledButton style={dynamicStyles} />;
```

### CSS var + unit concatenation

CSS custom properties can't be concatenated with unit suffixes directly
(`var(--x)px` is invalid). Values that need units use `calc()`:

```tsx
// Broken
transition: background ${themeCssVariables.animation.duration.instant}s ease;

// Fixed
transition: background calc(${themeCssVariables.animation.duration.instant} * 1s) ease;
```
2026-03-01 15:13:42 +01:00
Charles BochetandGitHub 159bb9d70a A few fixes on table performance (#18304)
## RecordTable Performance Investigation & Optimization (WIP)

Investigates what makes the RecordTable slow (14 components, ~12 hooks
per cell) and starts applying fixes.

### Key Findings (2,000 cells benchmark)

![Cell Render
Benchmark](https://github.com/twentyhq/twenty/blob/table-perf/packages/twenty-front/src/modules/object-record/record-table/__perf__/cell-render-benchmark.png?raw=true)

![State Access
Benchmark](https://github.com/twentyhq/twenty/blob/table-perf/packages/twenty-front/src/modules/object-record/record-table/__perf__/state-access-benchmark.png?raw=true)

- **Jotai atoms are the dominant cost**: 10 atom reads/cell = +312%.
Full sim with atoms = +476%.
- **Derived atoms are 3x cheaper** than individual reads (12 sources:
+93% vs +294%).
- **Component depth is expensive**: 4-level nesting = +82%, 14 wrappers
= +109%.
- **Styling engines are comparable**: Linaria vs Emotion is within
noise.
- **Context reads and useState are nearly free** vs baseline.

### Optimizations Applied

1. **Static focus providers** — replaced per-cell `useState(false)` with
static context. Eliminates 400 useState instances.
2. **Delegated onMouseMove** — single handler on table body instead of
400 per-cell handlers.
3. **Hoisted `useObjectMetadataItems()`** — moved from per-cell to
table-level context. Eliminates 400 global atom reads.

### Tooling

- Perf page at `/__perf__/table`: 17 cell render + 13 state access
benchmarks
- Render profiler: `window.__RECORD_TABLE_PROFILE = true`
- Full plan in `__perf__/PERFORMANCE_PLAN.md`

### Remaining Phases (not high priority to-be-honest)

| Phase | What | Status |
|-------|------|--------|
| 1 | Separate display from interaction | Partial |
| 2 | Flatten hierarchy (14 → ~5 components/cell) | TODO |
| 3 | Reduce atom reads per cell | Partial |
| 4 | CSS-only hover/focus | TODO |
| 5 | Event delegation, lazy Draggable | TODO |
2026-02-28 15:12:44 +01:00
b341704a0e i18n - translations (#18306)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-02-28 14:24:11 +01:00
d5d0f5d994 i18n - translations (#18302)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-02-28 14:11:52 +01:00
Félix MalfaitGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
012d819557 OAuth Client — Unified ApplicationRegistration, OAuth server, and frontend (#18267)
## Summary

Consolidates three separate PRs (#18260, #18261, #18262) into a single
unified branch with all review feedback addressed:

### New features
- **ApplicationRegistration entity** — server-level registration for
OAuth apps with encrypted server variables
- **OAuth 2.0 server** — authorization code, client credentials, refresh
token grants with PKCE support
- **OAuth discovery endpoint** —
`.well-known/oauth-authorization-server` metadata
- **Frontend UI** — app registration details page with credential
management, redirect URI editing, and server variable configuration
- **CLI integration** — `twenty dev` auto-registers apps and stores
OAuth credentials locally
- **Authorize consent screen** — OAuth consent page at `/authorize`
showing requested scopes

### Review feedback addressed

**Renames (PR #18260):**
- `appRegistration` → `applicationRegistration` (entity, tables, files,
imports, GraphQL types)
- `appRegistrationVariable` → `applicationRegistrationVariable`
- `clientId` → `oAuthClientId`, `clientSecretHash` →
`oAuthClientSecretHash`, `redirectUris` → `oAuthRedirectUris`, `scopes`
→ `oAuthScopes`

**Security fixes (PR #18261):**
- Fixed redirect URI validation bypass when `oAuthRedirectUris` is an
empty array
- Fixed workspace isolation in `clientCredentialsGrant` — now uses
`find()` with explicit handling for multiple installations
- Added error logging in refresh token `catch` block instead of silently
swallowing

**Code quality (PR #18262):**
- Split `VersionDistributionEntry` into its own file (one export per
file)
- Split GraphQL queries and mutations into individual files with a
shared fragment
- Removed unused `OAuth` entry from `AuthProviderEnum`
- Added loading state to `handleRotateSecret`
- Removed 27 narration-style comments from test files
- Added proper guards (`PublicEndpointGuard`, `NoPermissionGuard`) to
controllers and resolvers

## Test plan

- [ ] Verify `twenty dev` registers an app and stores OAuth credentials
- [ ] Test OAuth authorization code flow end-to-end (authorize → token →
API call)
- [ ] Test client credentials grant
- [ ] Verify redirect URI validation rejects requests when no URIs are
registered
- [ ] Verify app registration detail page renders correctly
- [ ] Test secret rotation with loading state
- [ ] Verify server variable editing and saving
- [ ] Run `npx nx database:reset twenty-server` to validate migration

Closes #18260, #18261, #18262


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-02-28 14:07:49 +01:00
Abdul RahmanGitHubCopilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>DevessierCharles Bochet
9fe2a07c55 Navbar customization v2 (#18026)
Adds color support for navigation menu items.

---------

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: Devessier <[email protected]>
Co-authored-by: Charles Bochet <[email protected]>
2026-02-28 11:48:28 +01:00
63f17eec2c i18n - translations (#18300)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-02-27 23:43:42 +01:00
e806d36099 Navbar with AI chats (#18161)
## Summary
Add Home/Chat tabs and a dedicated threads list in the navigation
drawer.

## Changes
- **Navbar tabs:** Tabs in the drawer to switch between Home and Chat
(with “New chat” button). Shown on desktop when expanded and on mobile
below the workspace selector.
- **Navbar threads list:** New `NavigationDrawerAIChatThreadsList` for
the Chat tab with date groups (Today / Yesterday / Older), thread rows
as `NavigationDrawerItem` (IconComment, title, timestamp). Shared
`useAIChatThreadClick` hook used by navbar and command menu; navbar
passes `resetNavigationStack: true`.
- **NavigationDrawerItem:** New `alwaysShowRightOptions` prop so the
timestamp is always visible (no hover-only).

---------

Co-authored-by: Etienne <[email protected]>
Co-authored-by: Félix Malfait <[email protected]>
Co-authored-by: Charles Bochet <[email protected]>
2026-02-27 23:37:14 +01:00
9f5a8735c9 i18n - docs translations (#18280)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-02-27 23:12:39 +01:00
c0e6aa1c0b i18n - translations (#18295)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-02-27 23:12:15 +01:00
Charles BochetandGitHub 9342b16aad Fix more tests 2 (#18293)
## Summary
- Migrate more hand-written test mocks to auto-generated data from a
real Twenty instance
- Add generators for views, billing plans, API keys; extend record
generator for workspace members, favorites, connected accounts, calendar
events
- Remove 9 hand-written mock files replaced by generated equivalents
- Update 16 test/story files to use generated data
- Fix WorkflowEditActionEmailBase story assertion to match configured
recipient email

## Test plan
- [x] Lint, typecheck, unit tests pass
- [ ] Storybook tests pass in CI
2026-02-27 23:11:56 +01:00
Baptiste DevessierandGitHub cfad24da48 Fix empty user id clickhouse (#18238)
- Fixes:
- Make Workspace User select work; previously, it didn't work as we were
not fetching the workspace users correctly
  - Send Object Events with valid record id and object id

## Audit logs demo


https://github.com/user-attachments/assets/92437037-d253-4810-a138-7c709550755d
2026-02-27 16:58:44 +01:00
Charles BochetandGitHub 86fbf69e95 Fix more tests (#18287)
Improve mock in front tests
2026-02-27 14:06:18 +01:00
9667b3f369 i18n - translations (#18291)
Created by Github action

Co-authored-by: github-actions <[email protected]>
2026-02-27 14:05:24 +01:00
Abdullah.andGitHub 76c7639eb3 fix: upgrade storybook to latest to resolve dependabot alert (#18285)
Resolves [Dependabot Alert
509](https://github.com/twentyhq/twenty/security/dependabot/509).

Upgraded storybook and related packages to latest, also fixed a failing
test to match what the DOM really contains.
2026-02-27 10:58:54 +01:00
Abdullah.andGitHub 4ed09a3feb Upgrade blocknote dependencies from 0.31.1 to 0.47.0. (#18207)
This PR pgrades all BlockNote packages (@blocknote/core,
@blocknote/react, @blocknote/mantine, @blocknote/server-util,
@blocknote/xl-docx-exporter, @blocknote/xl-pdf-exporter) to 0.47.0 and
adapts the codebase to the new API.

### Changes
- Dependency upgrades: Bumped all BlockNote packages to 0.47.0, added
required Mantine v8 peer dependencies, removed unnecessary prosemirror
resolutions
- Formatting toolbar: Replaced the manual reimplementation of
FormattingToolbarController (which handled visibility, positioning,
portal rendering, text-alignment-based placement, and a
dangerouslySetInnerHTML transition trick) with BlockNote's built-in
FormattingToolbarController. The toolbar buttons themselves are
unchanged.
- Side menu: Replaced manual drag handle menu positioning and rendering
(DashboardBlockDragHandleMenu, DashboardBlockColorPicker, and their
floating configs) with BlockNote's built-in SideMenuController,
DragHandleButton, and DragHandleMenu components. Deleted 4 files that
became dead code.
- Extension API migration: Replaced deprecated editor.suggestionMenus
and editor.formattingToolbar APIs with the new extension system
(SuggestionMenu, useExtensionState, editor.getExtension())
- Slash menu fixes: Filtered out BlockNote's new default "File" item
(added in 0.47) to avoid duplicates with our custom one; added icon
mappings for new block types (Toggle List, Divider, Toggle Headings,
Headings 4-6)
- Server-side: Switched @blocknote/server-util to dynamic import() to
handle ESM-only transitive dependencies in CJS context
2026-02-27 09:16:49 +01:00
Thomas TrompetteandGitHub def5ea5764 Fix ai agent node prompt and variables (#18275)
- prompt stored on workflow lvl so input variables can be resolved and
it can evolves with versions
- make ai agent node output available as variables
2026-02-26 17:17:48 +01:00
81698ff32c i18n - docs translations (#18274)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
Co-authored-by: Charles Bochet <[email protected]>
2026-02-26 16:52:43 +01:00
c0c51f2ef5 i18n - translations (#18278)
Created by Github action

Co-authored-by: github-actions <[email protected]>
2026-02-26 16:48:08 +01:00
WeikoandGitHub 3bbaff801a Add Twenty app settings custom app (#18273)
## Context
This PR adds the ability to define a front-component as a custom tab for
the application settings, allowing app creators to inject some
logic/rendering the their app settings.


## Example
```typescript
// packages/twenty-apps/my-test-app/src/front-components/settings-custom-tab.tsx
import { defineFrontComponent } from 'twenty-sdk';

export const SETTINGS_CUSTOM_TAB_UNIVERSAL_IDENTIFIER =
  'a42a88a8-21ce-4d22-bc44-d5146da64726';

export const SettingsCustomTab = () => {
  return (
    <div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
      <h2>My Test App Settings</h2>
      <p>This is a custom settings tab provided by My Test App.</p>
      <p>Application creators can customize this component freely.</p>
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: SETTINGS_CUSTOM_TAB_UNIVERSAL_IDENTIFIER,
  name: 'settings-custom-tab',
  description: 'Custom settings tab for the application',
  component: SettingsCustomTab,
});
```

```typescript
// packages/twenty-apps/my-test-app/src/application-config.ts
export default defineApplication({
  universalIdentifier: '52870ce6-e584-4bd4-bc1a-6c63c508982e',
  displayName: 'My test app',
  description: '',
  defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
  settingsCustomTabFrontComponentUniversalIdentifier:
    SETTINGS_CUSTOM_TAB_UNIVERSAL_IDENTIFIER,
});
```
<img width="786" height="757" alt="Screenshot 2026-02-26 at 14 54 09"
src="https://github.com/user-attachments/assets/fcba70db-35da-48f1-bd65-359e894a691d"
/>
2026-02-26 16:26:49 +01:00
Abdullah.andGitHub 2d2fc06265 fix: basic-ftp related dependabot alert (#18269)
Resolves [Dependabot Alert
507](https://github.com/twentyhq/twenty/security/dependabot/507).

Fixes critical SLA breach in 6 days.
2026-02-26 14:50:49 +01:00
Charles BochetandGitHub 8a7a19f312 Improve test tooling (#18259)
## Summary

Unifies test mocking tooling across Jest and Storybook, replaces
handcrafted mock data with auto-generated server-fetched data, and
restructures the mock data generation script for maintainability.

### Mock data generation

- Split `generate-mock-data.ts` into three focused modules under
`scripts/mock-data/`:
  - `utils.ts` — shared authentication, GraphQL client, and file writer
  - `generate-metadata.ts` — fetches object metadata from `/metadata`
- `generate-record-data.ts` — fetches record data from `/graphql` using
metadata-driven dynamic queries
- The orchestrator (`generate-mock-data.ts`) authenticates once and
passes the token to both generators
- Company records are now fetched from the actual server (limited to 10
records) instead of being handcrafted
- Generated files are organized under `generated/metadata/objects/` and
`generated/data/companies/`

### Unified test utilities

- Consolidated Jest and MSW mocking into shared utilities that compose
production code (`prefillRecord`, `getRecordNodeFromRecord`,
`getRecordConnectionFromRecords`) with mock metadata
- Renamed `generateEmptyJestRecordNode` → `generateMockRecordNode` and
moved to `testing/utils/`
- Extracted `generateMockRecordConnection` into its own file
- Removed `sanitizeInputForPrefill` workaround (no longer needed with
correctly shaped generated data)
2026-02-26 14:31:54 +01:00
EtienneandGitHub 2f0103faa5 OAuth Client - Add OAuth propagator (#18266)
For OAuth Server without wildcard redirect URL

https://www.benanderson.co.uk/2023/07/28/dynamic-redirect-uris-oauth/
2026-02-26 13:44:54 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
08bfbfda45 Bump @emotion/styled from 11.13.0 to 11.14.1 (#18253)
Bumps [@emotion/styled](https://github.com/emotion-js/emotion) from
11.13.0 to 11.14.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/emotion-js/emotion/releases"><code>@​emotion/styled</code>'s
releases</a>.</em></p>
<blockquote>
<h2><code>@​emotion/styled</code><a
href="https://github.com/11"><code>@​11</code></a>.14.1</h2>
<h3>Patch Changes</h3>
<ul>
<li><a
href="https://redirect.github.com/emotion-js/emotion/pull/3334">#3334</a>
<a
href="https://github.com/emotion-js/emotion/commit/0facbe47bd9099ae4ed22dc201822d910ac3dec5"><code>0facbe4</code></a>
Thanks <a
href="https://github.com/ZachRiegel"><code>@​ZachRiegel</code></a>! -
Renamed default-exported variable in <code>@emotion/styled</code> to aid
inferred import names in auto-import completions in IDEs</li>
</ul>
<h2><code>@​emotion/styled</code><a
href="https://github.com/11"><code>@​11</code></a>.14.0</h2>
<h3>Minor Changes</h3>
<ul>
<li><a
href="https://redirect.github.com/emotion-js/emotion/pull/3284">#3284</a>
<a
href="https://github.com/emotion-js/emotion/commit/a19d019bd418ebc3b9cba0e58f58b36ac2862a42"><code>a19d019</code></a>
Thanks <a
href="https://github.com/Andarist"><code>@​Andarist</code></a>! - Source
code has been migrated to TypeScript. From now on type declarations will
be emitted based on that, instead of being hand-written.</li>
</ul>
<h3>Patch Changes</h3>
<ul>
<li>Updated dependencies [<a
href="https://github.com/emotion-js/emotion/commit/e1bf17ee87ec51da1412eb5291460ea95a39d27a"><code>e1bf17e</code></a>]:
<ul>
<li><code>@​emotion/use-insertion-effect-with-fallbacks</code><a
href="https://github.com/1"><code>@​1</code></a>.2.0</li>
</ul>
</li>
</ul>
<h2><code>@​emotion/styled</code><a
href="https://github.com/11"><code>@​11</code></a>.13.5</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/emotion-js/emotion/pull/3270">#3270</a>
<a
href="https://github.com/emotion-js/emotion/commit/77d930dc708015ff6fd34a1084bb343b02d732fa"><code>77d930d</code></a>
Thanks <a
href="https://github.com/emmatown"><code>@​emmatown</code></a>! - Fix
inconsistent hashes using development vs production
bundles/<code>exports</code> conditions when using
<code>@emotion/babel-plugin</code> with <code>sourceMap: true</code>
(the default). This is particularly visible when using Emotion with the
Next.js Pages router where the <code>development</code> condition is
used when bundling code but not when importing external code with
Node.js.</p>
</li>
<li>
<p>Updated dependencies [<a
href="https://github.com/emotion-js/emotion/commit/77d930dc708015ff6fd34a1084bb343b02d732fa"><code>77d930d</code></a>]:</p>
<ul>
<li><code>@​emotion/serialize</code><a
href="https://github.com/1"><code>@​1</code></a>.3.3</li>
<li><code>@​emotion/utils</code><a
href="https://github.com/1"><code>@​1</code></a>.4.2</li>
<li><code>@​emotion/babel-plugin</code><a
href="https://github.com/11"><code>@​11</code></a>.13.5</li>
</ul>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/emotion-js/emotion/commit/49229553967b6050c92d9602eb577bdc48167e91"><code>4922955</code></a>
Version Packages (<a
href="https://redirect.github.com/emotion-js/emotion/issues/3335">#3335</a>)</li>
<li><a
href="https://github.com/emotion-js/emotion/commit/0facbe47bd9099ae4ed22dc201822d910ac3dec5"><code>0facbe4</code></a>
Renamed default-exported variable in <code>@emotion/styled</code> to aid
inferred import...</li>
<li><a
href="https://github.com/emotion-js/emotion/commit/cce67ec6b2fc94261028b4f4778aae8c3d6c5fd6"><code>cce67ec</code></a>
Bump parcel (<a
href="https://redirect.github.com/emotion-js/emotion/issues/3258">#3258</a>)</li>
<li><a
href="https://github.com/emotion-js/emotion/commit/3c19ce5997f73960679e546af47801205631dfde"><code>3c19ce5</code></a>
Version Packages (<a
href="https://redirect.github.com/emotion-js/emotion/issues/3280">#3280</a>)</li>
<li><a
href="https://github.com/emotion-js/emotion/commit/a19d019bd418ebc3b9cba0e58f58b36ac2862a42"><code>a19d019</code></a>
Convert <code>@emotion/styled</code>'s source code to TypeScript (<a
href="https://redirect.github.com/emotion-js/emotion/issues/3284">#3284</a>)</li>
<li><a
href="https://github.com/emotion-js/emotion/commit/5974e33fcb5e7aee177408684ac6fe8b38b3e353"><code>5974e33</code></a>
Fix JSX namespace <a
href="https://github.com/ts-ignores"><code>@​ts-ignores</code></a> (<a
href="https://redirect.github.com/emotion-js/emotion/issues/3282">#3282</a>)</li>
<li><a
href="https://github.com/emotion-js/emotion/commit/fc4d7bd744c205f55513dcd4e4e5134198c219de"><code>fc4d7bd</code></a>
Convert <code>@emotion/react</code>'s source code to TypeScript (<a
href="https://redirect.github.com/emotion-js/emotion/issues/3281">#3281</a>)</li>
<li><a
href="https://github.com/emotion-js/emotion/commit/8dc1a6dd19d2dc9ce435ef0aff85ccf5647f5d2e"><code>8dc1a6d</code></a>
Convert <code>@emotion/cache</code>'s source code to TypeScript (<a
href="https://redirect.github.com/emotion-js/emotion/issues/3277">#3277</a>)</li>
<li><a
href="https://github.com/emotion-js/emotion/commit/282b61d2ad4e39ea65af88351a894a903c2d42c4"><code>282b61d</code></a>
Convert <code>@emotion/css-prettifier</code>'s source code to TypeScript
(<a
href="https://redirect.github.com/emotion-js/emotion/issues/3278">#3278</a>)</li>
<li><a
href="https://github.com/emotion-js/emotion/commit/e1bf17ee87ec51da1412eb5291460ea95a39d27a"><code>e1bf17e</code></a>
Convert <code>@emotion/use-insertion-effect-with-fallbacks</code>'s
source code to TypeS...</li>
<li>Additional commits viewable in <a
href="https://github.com/emotion-js/emotion/compare/@emotion/[email protected]...@emotion/[email protected]">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-26 12:59:59 +01:00
Raphaël BosiandGitHub c025a0c2b8 Add events and properties to video, audio and iFrame (#18257)
- Add media-specific events
- Extend `iFrame` with missing properties
- Enrich `SerializedEventData` with media-related target fields so
serialized events carry the media element state.
- Refactor the `remote-elements` code generator to support per-element
custom events
2026-02-26 12:14:51 +01:00
Paul RastoinandGitHub 5e19361494 Allow uuidv5 for universal identifier (#18265)
Twenty apps are using v5
2026-02-26 12:13:38 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f11d76d6cf Bump @babel/preset-react from 7.26.3 to 7.28.5 (#18254)
Bumps
[@babel/preset-react](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-react)
from 7.26.3 to 7.28.5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/babel/babel/releases"><code>@​babel/preset-react</code>'s
releases</a>.</em></p>
<blockquote>
<h2>v7.28.5 (2025-10-23)</h2>
<p>Thank you <a
href="https://github.com/CO0Ki3"><code>@​CO0Ki3</code></a>, <a
href="https://github.com/Olexandr88"><code>@​Olexandr88</code></a>, and
<a href="https://github.com/youthfulhps"><code>@​youthfulhps</code></a>
for your first PRs!</p>
<h4>👓 Spec Compliance</h4>
<ul>
<li><code>babel-parser</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17446">#17446</a>
Allow <code>Runtime Errors for Function Call Assignment Targets</code>
(<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
<li><code>babel-helper-validator-identifier</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17501">#17501</a>
fix: update identifier to unicode 17 (<a
href="https://github.com/fisker"><code>@​fisker</code></a>)</li>
</ul>
</li>
</ul>
<h4>🐛 Bug Fix</h4>
<ul>
<li><code>babel-plugin-proposal-destructuring-private</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17534">#17534</a>
Allow mixing private destructuring and rest (<a
href="https://github.com/CO0Ki3"><code>@​CO0Ki3</code></a>)</li>
</ul>
</li>
<li><code>babel-parser</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17521">#17521</a>
Improve <code>@babel/parser</code> error typing (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
<li><a
href="https://redirect.github.com/babel/babel/pull/17491">#17491</a>
fix: improve ts-only declaration parsing (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-plugin-proposal-discard-binding</code>,
<code>babel-plugin-transform-destructuring</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17519">#17519</a>
fix: <code>rest</code> correctly returns plain array (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
<li><code>babel-helper-create-class-features-plugin</code>,
<code>babel-helper-member-expression-to-functions</code>,
<code>babel-plugin-transform-block-scoping</code>,
<code>babel-plugin-transform-optional-chaining</code>,
<code>babel-traverse</code>, <code>babel-types</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17503">#17503</a> Fix
<code>JSXIdentifier</code> handling in
<code>isReferencedIdentifier</code> (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-traverse</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17504">#17504</a>
fix: ensure scope.push register in anonymous fn (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
</ul>
<h4>🏠 Internal</h4>
<ul>
<li><code>babel-types</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17494">#17494</a>
Type checking babel-types scripts (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
</ul>
<h4>🏃‍♀️ Performance</h4>
<ul>
<li><code>babel-core</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17490">#17490</a>
Faster finding of locations in <code>buildCodeFrameError</code> (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
</ul>
<h4>Committers: 8</h4>
<ul>
<li>Babel Bot (<a
href="https://github.com/babel-bot"><code>@​babel-bot</code></a>)</li>
<li>Byeongho Yoo (<a
href="https://github.com/youthfulhps"><code>@​youthfulhps</code></a>)</li>
<li>Huáng Jùnliàng (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
<li>Hyeon Dokko (<a
href="https://github.com/CO0Ki3"><code>@​CO0Ki3</code></a>)</li>
<li>Nicolò Ribaudo (<a
href="https://github.com/nicolo-ribaudo"><code>@​nicolo-ribaudo</code></a>)</li>
<li><a
href="https://github.com/Olexandr88"><code>@​Olexandr88</code></a></li>
<li><a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a></li>
<li>fisker Cheung (<a
href="https://github.com/fisker"><code>@​fisker</code></a>)</li>
</ul>
<h2>v7.28.4 (2025-09-05)</h2>
<p>Thanks <a
href="https://github.com/gwillen"><code>@​gwillen</code></a> and <a
href="https://github.com/mrginglymus"><code>@​mrginglymus</code></a> for
your first PRs!</p>
<h4>🏠 Internal</h4>
<ul>
<li><code>babel-core</code>,
<code>babel-helper-check-duplicate-nodes</code>,
<code>babel-traverse</code>, <code>babel-types</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17493">#17493</a>
Update Jest to v30.1.1 (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-plugin-transform-regenerator</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17455">#17455</a>
chore: Clean up <code>transform-regenerator</code> (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/babel/babel/blob/main/CHANGELOG.md"><code>@​babel/preset-react</code>'s
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<blockquote>
<p><strong>Tags:</strong></p>
<ul>
<li>💥 [Breaking Change]</li>
<li>👓 [Spec Compliance]</li>
<li>🚀 [New Feature]</li>
<li>🐛 [Bug Fix]</li>
<li>📝 [Documentation]</li>
<li>🏠 [Internal]</li>
<li>💅 [Polish]</li>
</ul>
</blockquote>
<p><em>Note: Gaps between patch versions are faulty, broken or test
releases.</em></p>
<p>This file contains the changelog starting from v8.0.0-alpha.0.</p>
<ul>
<li>See <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v7.15.0-v7.28.5.md">CHANGELOG
- v7.15.0 to v7.28.5</a> for v7.15.0 to v7.28.5 changes (the last common
release between the v8 and v7 release lines was v7.28.5).</li>
<li>See <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v7.0.0-v7.14.9.md">CHANGELOG
- v7.0.0 to v7.14.9</a> for v7.0.0 to v7.14.9 changes.</li>
<li>See <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v7-prereleases.md">CHANGELOG
- v7 prereleases</a> for v7.0.0-alpha.1 to v7.0.0-rc.4 changes.</li>
<li>See <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v4.md">CHANGELOG
- v4</a>, <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v5.md">CHANGELOG
- v5</a>, and <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v6.md">CHANGELOG
- v6</a> for v4.x-v6.x changes.</li>
<li>See <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-6to5.md">CHANGELOG
- 6to5</a> for the pre-4.0.0 version changelog.</li>
<li>See <a
href="https://github.com/babel/babel/blob/main/packages/babel-parser/CHANGELOG.md">Babylon's
CHANGELOG</a> for the Babylon pre-7.0.0-beta.29 version changelog.</li>
<li>See <a
href="https://github.com/babel/babel-eslint/releases"><code>babel-eslint</code>'s
releases</a> for the changelog before <code>@babel/eslint-parser</code>
7.8.0.</li>
<li>See <a
href="https://github.com/babel/eslint-plugin-babel/releases"><code>eslint-plugin-babel</code>'s
releases</a> for the changelog before <code>@babel/eslint-plugin</code>
7.8.0.</li>
</ul>
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<h2>v8.0.0-rc.2 (2026-02-15)</h2>
<h4>💥 Breaking Change</h4>
<ul>
<li>Other
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17766">#17766</a>
Remove unused code for old ESLint versions (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
<li><code>babel-code-frame</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17772">#17772</a>
Remove deprecated default export from <code>@babel/code-frame</code> (<a
href="https://github.com/fisker"><code>@​fisker</code></a>)</li>
</ul>
</li>
</ul>
<h4>🐛 Bug Fix</h4>
<ul>
<li><code>babel-helpers</code>,
<code>babel-plugin-transform-async-generator-functions</code>,
<code>babel-runtime-corejs3</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17797">#17797</a>
fix: Properly handle <code>await</code> in <code>finally</code> (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
<li><code>babel-parser</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17796">#17796</a>
Support ESLint 10 (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-preset-env</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17787">#17787</a>
Fix: preset-env include/exclude should accept bugfix plugins (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-generator</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17781">#17781</a>
fix: preserve trailing comma in optional call args (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
<li><a
href="https://redirect.github.com/babel/babel/pull/17774">#17774</a> Fix
<code>undefined</code> indentation when exactly 64 indents (<a
href="https://github.com/YoussefHenna"><code>@​YoussefHenna</code></a>)</li>
</ul>
</li>
<li><code>babel-standalone</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17770">#17770</a>
fix: ensure <code>targets.esmodules</code> is validated (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
</ul>
<h4>💅 Polish</h4>
<ul>
<li><code>babel-core</code></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/babel/babel/commit/61647ae2397c82c3c71f077b5ab109106a5cac0f"><code>61647ae</code></a>
v7.28.5</li>
<li><a
href="https://github.com/babel/babel/commit/42cb285b59fc99a8102d69bef6223b75617e9f46"><code>42cb285</code></a>
Improve <code>@babel/core</code> types (<a
href="https://github.com/babel/babel/tree/HEAD/packages/babel-preset-react/issues/17404">#17404</a>)</li>
<li><a
href="https://github.com/babel/babel/commit/eebd3a06021c13d335b5b0bd79734df3abbea678"><code>eebd3a0</code></a>
v7.27.1</li>
<li><a
href="https://github.com/babel/babel/commit/fdc0fb59e119ee0b38bced63867a344a5b4bc2f3"><code>fdc0fb5</code></a>
[Babel 8] Bump nodejs requirements to <code>^20.19.0 || &gt;=
22.12.0</code> (<a
href="https://github.com/babel/babel/tree/HEAD/packages/babel-preset-react/issues/17204">#17204</a>)</li>
<li><a
href="https://github.com/babel/babel/commit/cd24cc07ef6558b7f6510f9177f6393c91b0549f"><code>cd24cc0</code></a>
chore: Update TS 5.7 (<a
href="https://github.com/babel/babel/tree/HEAD/packages/babel-preset-react/issues/17053">#17053</a>)</li>
<li>See full diff in <a
href="https://github.com/babel/babel/commits/v7.28.5/packages/babel-preset-react">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by [GitHub Actions](<a
href="https://www.npmjs.com/~GitHub">https://www.npmjs.com/~GitHub</a>
Actions), a new releaser for <code>@​babel/preset-react</code> since
your current version.</p>
</details>
<br />


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

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

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

---

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

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-26 11:57:26 +01:00
1059 changed files with 101653 additions and 36123 deletions
+7 -1
View File
@@ -19,4 +19,10 @@ runs:
uses: nrwl/nx-set-shas@v4
- name: Run affected command
shell: bash
run: npx nx affected --nxBail --configuration=${{ inputs.configuration }} -t=${{ inputs.tasks }} --parallel=${{ inputs.parallel }} --exclude='*,!tag:${{ inputs.tag }}' ${{ inputs.args }}
env:
NX_CONFIGURATION: ${{ inputs.configuration }}
NX_TASKS: ${{ inputs.tasks }}
NX_PARALLEL: ${{ inputs.parallel }}
NX_TAG: ${{ inputs.tag }}
NX_ARGS: ${{ inputs.args }}
run: npx nx affected --nxBail --configuration="$NX_CONFIGURATION" -t="$NX_TASKS" --parallel="$NX_PARALLEL" --exclude="*,!tag:$NX_TAG" $NX_ARGS
+4 -1
View File
@@ -19,8 +19,11 @@ runs:
- name: Cache primary key builder
id: cache-primary-key-builder
shell: bash
env:
CACHE_KEY: ${{ inputs.key }}
REF_NAME: ${{ github.ref_name }}
run: |
echo "CACHE_PRIMARY_KEY_PREFIX=v4-${{ inputs.key }}-${{ github.ref_name }}" >> "${GITHUB_OUTPUT}"
echo "CACHE_PRIMARY_KEY_PREFIX=v4-${CACHE_KEY}-${REF_NAME}" >> "${GITHUB_OUTPUT}"
- name: Restore cache
uses: actions/cache/restore@v4
id: restore-cache
+8 -188
View File
@@ -16,8 +16,6 @@ env:
permissions:
contents: read
pull-requests: write
checks: write
jobs:
changed-files-check:
@@ -584,182 +582,16 @@ jobs:
echo "::warning::REST Metadata API analysis tool error - continuing workflow"
fi
- name: Comment API Changes on PR
- name: Upload breaking changes report
if: always()
uses: actions/github-script@v7
uses: actions/upload-artifact@v4
with:
script: |
const fs = require('fs');
let hasChanges = false;
let comment = '';
try {
if (fs.existsSync('graphql-schema-diff.md')) {
const graphqlDiff = fs.readFileSync('graphql-schema-diff.md', 'utf8');
if (graphqlDiff.trim()) {
if (!hasChanges) {
comment = '## 📊 API Changes Report\n\n';
hasChanges = true;
}
comment += '### GraphQL Schema Changes\n' + graphqlDiff + '\n\n';
}
}
if (fs.existsSync('graphql-metadata-diff.md')) {
const graphqlMetadataDiff = fs.readFileSync('graphql-metadata-diff.md', 'utf8');
if (graphqlMetadataDiff.trim()) {
if (!hasChanges) {
comment = '## 📊 API Changes Report\n\n';
hasChanges = true;
}
comment += '### GraphQL Metadata Schema Changes\n' + graphqlMetadataDiff + '\n\n';
}
}
if (fs.existsSync('rest-api-diff.md')) {
const restDiff = fs.readFileSync('rest-api-diff.md', 'utf8');
if (restDiff.trim()) {
if (!hasChanges) {
comment = '## 📊 API Changes Report\n\n';
hasChanges = true;
}
comment += restDiff + '\n\n';
}
}
if (fs.existsSync('rest-metadata-api-diff.md')) {
const metadataDiff = fs.readFileSync('rest-metadata-api-diff.md', 'utf8');
if (metadataDiff.trim()) {
if (!hasChanges) {
comment = '## 📊 API Changes Report\n\n';
hasChanges = true;
}
comment += metadataDiff + '\n\n';
}
}
// Only post comment if there are changes
if (hasChanges) {
// Add branch state information only if there were conflicts
const branchState = process.env.BRANCH_STATE || 'unknown';
let branchStateNote = '';
if (branchState === 'conflicts') {
branchStateNote = '\n\n⚠️ **Note**: Could not merge with `main` due to conflicts. This comparison shows changes between the current branch and `main` as separate states.\n';
}
// Check if there are any breaking changes detected
let hasBreakingChanges = false;
let breakingChangeNote = '';
// Check for breaking changes in any of the diff files
if (fs.existsSync('rest-api-diff.md')) {
const restDiff = fs.readFileSync('rest-api-diff.md', 'utf8');
if (restDiff.includes('Breaking Changes') || restDiff.includes('🚨') ||
restDiff.includes('Removed Endpoints') || restDiff.includes('Changed Operations')) {
hasBreakingChanges = true;
}
}
if (fs.existsSync('rest-metadata-api-diff.md')) {
const metadataDiff = fs.readFileSync('rest-metadata-api-diff.md', 'utf8');
if (metadataDiff.includes('Breaking Changes') || metadataDiff.includes('🚨') ||
metadataDiff.includes('Removed Endpoints') || metadataDiff.includes('Changed Operations')) {
hasBreakingChanges = true;
}
}
// Also check GraphQL changes for breaking changes indicators
if (fs.existsSync('graphql-schema-diff.md')) {
const graphqlDiff = fs.readFileSync('graphql-schema-diff.md', 'utf8');
if (graphqlDiff.includes('Breaking changes') || graphqlDiff.includes('BREAKING')) {
hasBreakingChanges = true;
}
}
if (fs.existsSync('graphql-metadata-diff.md')) {
const graphqlMetadataDiff = fs.readFileSync('graphql-metadata-diff.md', 'utf8');
if (graphqlMetadataDiff.includes('Breaking changes') || graphqlMetadataDiff.includes('BREAKING')) {
hasBreakingChanges = true;
}
}
// Check PR title for "breaking"
const prTitle = ${{ toJSON(github.event.pull_request.title) }};
const titleContainsBreaking = prTitle.toLowerCase().includes('breaking');
if (hasBreakingChanges) {
if (titleContainsBreaking) {
breakingChangeNote = '\n\n## ✅ Breaking Change Protocol\n\n' +
'**This PR title contains "breaking" and breaking changes were detected - the CI will fail as expected.**\n\n' +
'📝 **Action Required**: Please add `BREAKING CHANGE:` to your commit message to trigger a major version bump.\n\n' +
'Example:\n```\nfeat: add new API endpoint\n\nBREAKING CHANGE: removed deprecated field from User schema\n```';
} else {
breakingChangeNote = '\n\n## ⚠️ Breaking Change Protocol\n\n' +
'**Breaking changes detected but PR title does not contain "breaking" - CI will pass but action needed.**\n\n' +
'🔄 **Options**:\n' +
'1. **If this IS a breaking change**: Add "breaking" to your PR title and add `BREAKING CHANGE:` to your commit message\n' +
'2. **If this is NOT a breaking change**: The API diff tool may have false positives - please review carefully\n\n' +
'For breaking changes, add to commit message:\n```\nfeat: add new API endpoint\n\nBREAKING CHANGE: removed deprecated field from User schema\n```';
}
}
const COMMENT_MARKER = '<!-- API_CHANGES_REPORT -->';
const commentBody = COMMENT_MARKER + '\n' + comment + branchStateNote + '\n⚠️ **Please review these API changes carefully before merging.**' + breakingChangeNote;
// Get all comments to find existing API changes comment
const {data: comments} = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
// Find our existing comment
const botComment = comments.find(comment => comment.body.includes(COMMENT_MARKER));
if (botComment) {
// Update existing comment
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: commentBody
});
console.log('Updated existing API changes comment');
} else {
// Create new comment
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody
});
console.log('Created new API changes comment');
}
} else {
console.log('No API changes detected - skipping PR comment');
// Check if there's an existing comment to remove
const {data: comments} = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const COMMENT_MARKER = '<!-- API_CHANGES_REPORT -->';
const botComment = comments.find(comment => comment.body.includes(COMMENT_MARKER));
if (botComment) {
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
});
console.log('Deleted existing API changes comment (no changes detected)');
}
}
} catch (error) {
console.log('Could not post comment:', error);
}
name: breaking-changes-report
path: |
*-diff.md
*-diff.json
if-no-files-found: ignore
retention-days: 3
- name: Cleanup servers
if: always()
@@ -771,16 +603,4 @@ jobs:
kill $(cat /tmp/main-server.pid) || true
fi
# - name: Upload API specifications and diffs
# if: always()
# uses: actions/upload-artifact@v4
# with:
# name: api-specifications-and-diffs
# path: |
# /tmp/main-server.log
# /tmp/current-server.log
# *-api.json
# *-schema-introspection.json
# *-diff.md
# *-diff.json
-3
View File
@@ -9,10 +9,7 @@ on:
types: [opened, synchronize, reopened, closed]
permissions:
actions: write
checks: write
contents: write
issues: write
pull-requests: write
statuses: write
+74 -6
View File
@@ -1,12 +1,10 @@
name: CI Zapier
on:
push:
branches:
- main
pull_request:
merge_group:
permissions:
contents: read
@@ -14,6 +12,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
env:
SERVER_SETUP_CACHE_KEY: server-setup
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
@@ -23,14 +24,81 @@ jobs:
packages/twenty-server/**
!packages/twenty-zapier/package.json
!packages/twenty-zapier/CHANGELOG.md
zapier-test:
server-setup:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: depot-ubuntu-24.04-8
services:
postgres:
image: twentycrm/twenty-postgres-spilo
env:
PGUSER_SUPERUSER: postgres
PGPASSWORD_SUPERUSER: postgres
ALLOW_NOSSL: 'true'
SPILO_PROVIDER: 'local'
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build twenty-shared
run: npx nx build twenty-shared
- name: Server / Write .env
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Server / Build
run: npx nx build twenty-server
- name: Create and setup database
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
npx nx run twenty-server:database:reset
- name: Server / Start
run: |
npx nx start twenty-server &
echo "Waiting for server to be ready..."
timeout 60 bash -c 'until curl -s http://localhost:3000/health; do sleep 2; done'
- name: Start worker
run: |
npx nx run twenty-server:worker &
echo "Worker started"
- name: Zapier / Build
run: npx nx build twenty-zapier
- name: Zapier / Run Tests
uses: ./.github/actions/nx-affected
with:
tag: scope:zapier
tasks: test
zapier-test:
needs: server-setup
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: depot-ubuntu-24.04
strategy:
matrix:
task: [lint, typecheck, test, validate]
task: [lint, typecheck, validate]
steps:
- name: Cancel Previous Runs
uses: styfle/[email protected]
+7 -14
View File
@@ -87,7 +87,7 @@ jobs:
exit 0
fi
ISSUE_NUMBER="${{ github.event.issue.number || github.event.pull_request.number }}"
ENCODED_BRANCH=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$BRANCH', safe=''))")
ENCODED_BRANCH=$(python3 -c "import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "$BRANCH")
PR_URL="https://github.com/${{ github.repository }}/compare/main...${ENCODED_BRANCH}?quick_pull=1"
BODY="⚠️ Claude ran out of turns before creating a PR. Work has been pushed to [\`$BRANCH\`](https://github.com/${{ github.repository }}/tree/$ENCODED_BRANCH).\n\n[**Create PR →**]($PR_URL)"
if [ -n "$ISSUE_NUMBER" ]; then
@@ -157,18 +157,11 @@ jobs:
"PG_DATABASE_URL": "postgres://postgres:postgres@localhost:5432/default"
}
}
- name: Post response to source issue
- name: Dispatch response to ci-privileged
if: always()
uses: actions/github-script@v7
uses: peter-evans/repository-dispatch@v2
with:
github-token: ${{ secrets.TWENTY_DISPATCH_TOKEN }}
script: |
const [owner, repo] = '${{ steps.prompt.outputs.repo }}'.split('/');
const issueNumber = parseInt('${{ steps.prompt.outputs.issue_number }}', 10);
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: `Claude finished processing this request. [See workflow run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`
});
token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
repository: twentyhq/ci-privileged
event-type: claude-cross-repo-response
client-payload: '{"repo": ${{ toJSON(steps.prompt.outputs.repo) }}, "issue_number": ${{ toJSON(steps.prompt.outputs.issue_number) }}, "run_id": ${{ toJSON(github.run_id) }}, "run_url": ${{ toJSON(format('{0}/{1}/actions/runs/{2}', github.server_url, github.repository, github.run_id)) }}}'
-118
View File
@@ -1,118 +0,0 @@
# Weekly translation QA report using Crowdin's native QA checks
name: 'Weekly Translation QA Report'
permissions:
contents: write
pull-requests: write
on:
schedule:
- cron: '0 9 * * 1' # Every Monday at 9am UTC
workflow_dispatch: # Allow manual trigger
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
qa_report:
name: Generate QA Report
runs-on: depot-ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build twenty-shared
run: npx nx build twenty-shared
- name: Generate QA report from Crowdin
id: generate_report
run: |
npx ts-node packages/twenty-utils/translation-qa-report.ts || true
if [ -f TRANSLATION_QA_REPORT.md ]; then
echo "report_generated=true" >> $GITHUB_OUTPUT
# Count critical issues (exclude spellcheck)
CRITICAL=$(grep -oP '⚠️\s+\K\d+' TRANSLATION_QA_REPORT.md 2>/dev/null || echo "0")
echo "critical_issues=$CRITICAL" >> $GITHUB_OUTPUT
else
echo "report_generated=false" >> $GITHUB_OUTPUT
echo "critical_issues=0" >> $GITHUB_OUTPUT
fi
env:
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: Create QA branch and commit report
if: steps.generate_report.outputs.report_generated == 'true'
run: |
git config --global user.name 'github-actions'
git config --global user.email '[email protected]'
BRANCH_NAME="i18n-qa-report-$(date +%Y-%m-%d)"
git checkout -B $BRANCH_NAME
git add TRANSLATION_QA_REPORT.md
if ! git diff --staged --quiet --exit-code; then
git commit -m "docs: weekly translation QA report"
git push origin HEAD:$BRANCH_NAME --force
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
else
echo "No changes to commit"
echo "BRANCH_NAME=" >> $GITHUB_ENV
fi
- name: Create pull request
if: steps.generate_report.outputs.report_generated == 'true' && env.BRANCH_NAME != ''
run: |
CRITICAL="${{ steps.generate_report.outputs.critical_issues }}"
BODY=$(cat <<EOF
## Weekly Translation QA Report
**Critical issues (excluding spellcheck): $CRITICAL**
📊 **View in Crowdin**: https://twenty.crowdin.com/u/projects/1/all?filter=qa-issue
### For AI-Assisted Fixing
Open this PR in Cursor and say:
> "Fix the translation QA issues using the Crowdin API"
The AI can help fix:
- ✅ Variables mismatch (missing/wrong placeholders)
- ✅ Escaped Unicode sequences
- ⚠️ Tags mismatch
- ⚠️ Empty translations
### Available Scripts
\`\`\`bash
# View QA report
CROWDIN_PERSONAL_TOKEN=xxx npx ts-node packages/twenty-utils/translation-qa-report.ts
# Fix encoding issues automatically
CROWDIN_PERSONAL_TOKEN=xxx npx ts-node packages/twenty-utils/fix-crowdin-translations.ts
\`\`\`
---
*Close without merging after issues are addressed*
EOF
)
EXISTING_PR=$(gh pr list --head $BRANCH_NAME --json number --jq '.[0].number' 2>/dev/null || echo "")
if [ -n "$EXISTING_PR" ]; then
gh pr edit $EXISTING_PR --body "$BODY"
else
gh pr create \
--base main \
--head $BRANCH_NAME \
--title "i18n: Translation QA Report ($CRITICAL critical issues)" \
--body "$BODY" || true
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+71
View File
@@ -0,0 +1,71 @@
name: Post CI Comments
on:
workflow_run:
workflows: ['GraphQL and OpenAPI Breaking Changes Detection']
types: [completed]
permissions:
actions: read
jobs:
dispatch-breaking-changes:
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Get PR number from workflow run
id: pr-info
uses: actions/github-script@v7
with:
script: |
const runId = context.payload.workflow_run.id;
const headSha = context.payload.workflow_run.head_sha;
const headBranch = context.payload.workflow_run.head_branch;
const headRepo = context.payload.workflow_run.head_repository;
// workflow_run.pull_requests is empty for fork PRs,
// so fall back to searching by head SHA
let pullRequests = context.payload.workflow_run.pull_requests;
let prNumber;
if (pullRequests && pullRequests.length > 0) {
prNumber = pullRequests[0].number;
} else {
core.info(`pull_requests is empty (likely a fork PR), searching by SHA ${headSha}`);
const owner = context.repo.owner;
const repo = context.repo.repo;
const headLabel = `${headRepo.owner.login}:${headBranch}`;
const { data: prs } = await github.rest.pulls.list({
owner,
repo,
state: 'open',
head: headLabel,
per_page: 1,
});
if (prs.length > 0) {
prNumber = prs[0].number;
}
}
if (!prNumber) {
core.info('No pull request found for this workflow run');
core.setOutput('has_pr', 'false');
return;
}
core.setOutput('pr_number', prNumber);
core.setOutput('run_id', runId);
core.setOutput('has_pr', 'true');
core.info(`PR #${prNumber}, Run ID: ${runId}`);
- name: Dispatch to ci-privileged
if: steps.pr-info.outputs.has_pr == 'true'
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
repository: twentyhq/ci-privileged
event-type: breaking-changes-report
client-payload: '{"pr_number": ${{ toJSON(steps.pr-info.outputs.pr_number) }}, "run_id": ${{ toJSON(steps.pr-info.outputs.run_id) }}, "repo": ${{ toJSON(github.repository) }}, "branch_state": ${{ toJSON(github.event.workflow_run.head_branch) }}}'
+21 -6
View File
@@ -2,13 +2,8 @@ name: 'Preview Environment Dispatch'
permissions:
contents: write
actions: write
pull-requests: read
on:
# Using pull_request_target instead of pull_request to have access to secrets for external contributors
# Security note: This is safe because we're only using the repository-dispatch action with limited scope
# and not checking out or running any code from the external contributor's PR
pull_request_target:
types: [opened, synchronize, reopened, labeled]
paths:
@@ -24,7 +19,19 @@ concurrency:
jobs:
trigger-preview:
if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.action == 'reopened' || (github.event.action == 'labeled' && github.event.label.name == 'preview-app')
if: |
(github.event.action == 'labeled' && github.event.label.name == 'preview-app') ||
(
(
github.event.pull_request.author_association == 'MEMBER' ||
github.event.pull_request.author_association == 'OWNER' ||
github.event.pull_request.author_association == 'COLLABORATOR'
) && (
github.event.action == 'opened' ||
github.event.action == 'synchronize' ||
github.event.action == 'reopened'
)
)
timeout-minutes: 5
runs-on: depot-ubuntu-24.04
steps:
@@ -35,3 +42,11 @@ jobs:
repository: ${{ github.repository }}
event-type: preview-environment
client-payload: '{"pr_number": "${{ github.event.pull_request.number }}", "pr_head_sha": "${{ github.event.pull_request.head.sha }}", "repo_full_name": "${{ github.repository }}"}'
- name: Dispatch to ci-privileged for PR comment
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
repository: twentyhq/ci-privileged
event-type: preview-env-url
client-payload: '{"pr_number": ${{ toJSON(github.event.pull_request.number) }}, "keepalive_dispatch_time": ${{ toJSON(github.event.pull_request.updated_at) }}, "repo": ${{ toJSON(github.repository) }}}'
+19 -47
View File
@@ -2,7 +2,6 @@ name: 'Preview Environment Keep Alive'
permissions:
contents: read
pull-requests: write
on:
repository_dispatch:
@@ -55,14 +54,15 @@ jobs:
port: 3000
- name: Start services with correct SERVER_URL
env:
TUNNEL_URL: ${{ steps.expose-tunnel.outputs.tunnel-url }}
run: |
cd packages/twenty-docker/
# Update the SERVER_URL with the tunnel URL
echo "Setting SERVER_URL to ${{ steps.expose-tunnel.outputs.tunnel-url }}"
echo "Setting SERVER_URL to $TUNNEL_URL"
sed -i '/SERVER_URL=/d' .env
echo "" >> .env
echo "SERVER_URL=${{ steps.expose-tunnel.outputs.tunnel-url }}" >> .env
echo "SERVER_URL=$TUNNEL_URL" >> .env
# Start the services
echo "Docker compose up..."
@@ -99,54 +99,26 @@ jobs:
fi
working-directory: ./
- name: Output tunnel URL to logs
- name: Output tunnel URL
env:
TUNNEL_URL: ${{ steps.expose-tunnel.outputs.tunnel-url }}
run: |
echo "✅ Preview Environment Ready!"
echo "🔗 Preview URL: ${{ steps.expose-tunnel.outputs.tunnel-url }}"
echo "🔗 Preview URL: $TUNNEL_URL"
echo "⏱️ This environment will be available for 5 hours"
echo "## 🚀 Preview Environment Ready!" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "Preview URL: $TUNNEL_URL" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "This environment will automatically shut down after 5 hours." >> "$GITHUB_STEP_SUMMARY"
echo "$TUNNEL_URL" > tunnel-url.txt
- name: Post comment on PR
uses: actions/github-script@v6
- name: Upload tunnel URL artifact
uses: actions/upload-artifact@v4
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const COMMENT_MARKER = '<!-- PR_PREVIEW_ENV -->';
const commentBody = `${COMMENT_MARKER}
🚀 **Preview Environment Ready!**
Your preview environment is available at: ${{ steps.expose-tunnel.outputs.tunnel-url }}
This environment will automatically shut down when the PR is closed or after 5 hours.`;
// Get all comments
const {data: comments} = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: ${{ github.event.client_payload.pr_number }},
});
// Find our comment
const botComment = comments.find(comment => comment.body.includes(COMMENT_MARKER));
if (botComment) {
// Update existing comment
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: commentBody
});
console.log('Updated existing comment');
} else {
// Create new comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: ${{ github.event.client_payload.pr_number }},
body: commentBody
});
console.log('Created new comment');
}
name: tunnel-url
path: tunnel-url.txt
retention-days: 1
- name: Keep tunnel alive for 5 hours
run: timeout 300m sleep 18000 # Stop on whichever we reach first (300m or 5hour sleep)
+283
View File
@@ -0,0 +1,283 @@
# npm-Based App Distribution for Twenty
*Technical Design Document -- February 2026*
## Overview
Add npm registry support for distributing Twenty apps (public and private), with per-AppRegistration registry overrides, direct tarball upload as escape hatch, and version upgrade detection.
## Assumptions
- The marketplace install flow (currently a TODO in the resolver and frontend) will be implemented separately. This plan provides the infrastructure that install flow will call into.
- The existing `app:dev` flow (individual file uploads via CLI) remains unchanged.
- The existing GitHub-based marketplace discovery remains as a curated fallback.
## Architecture
```
Developer
/ | \
npm publish | twenty app:push
/ | \
npmjs.com Private Reg Server REST Upload
| | |
v v v
[Discovery Layer] [Direct Upload]
npm search API |
GitHub curated list |
| |
v |
MarketplaceService |
(sourcePackage in DTO) |
| |
v v
AppPackageResolverService <--------+
.npmrc generation
yarn add / tarball extract
|
v
ApplicationSyncService (existing)
WorkspaceMigrationRunnerService
|
v
AppRegistration + Application entities
```
## Phase 1: Entity and Config Changes
### 1a. Extend AppRegistrationEntity
Add four columns to `application-registration.entity.ts`:
- **`sourcePackage`** (text, nullable) -- npm package name, e.g. `"twenty-app-fireflies"` or `"@myorg/twenty-app-crm"`. Null for tarball-only or OAuth-only apps.
- **`tarballFileId`** (uuid, nullable) -- FK to a FileEntity storing a directly-uploaded `.tar.gz`. Null when the app comes from npm.
- **`registryUrl`** (text, nullable) -- per-registration npm registry override. Null means "use the server default `APP_REGISTRY_URL`." This is how a single server can pull public apps from npmjs.com while pulling `@mycompany/*` apps from GitHub Packages.
- **`latestAvailableVersion`** (text, nullable) -- cached latest version from the registry, updated periodically. Compared against `Application.version` to surface upgrade availability.
**Source resolution priority:**
1. `sourcePackage` is set → resolve from npm via `yarn add`
2. `tarballFileId` is set → extract from file storage
3. Neither → OAuth-only app, no server-side code
### 1b. Extend ApplicationEntity.sourceType
Widen the `sourceType` union from `'local'` to `'local' | 'npm' | 'tarball'`:
- `'local'` -- existing behavior (CLI `app:dev`, individual file uploads, workspace-custom)
- `'npm'` -- installed from an npm registry via `yarn add`
- `'tarball'` -- installed from a directly-uploaded tarball
This lets the system distinguish how an app was installed, which matters for upgrade logic (npm apps can check the registry for newer versions; tarball apps cannot).
### 1c. Add server-wide config variables
Add a new `APP_REGISTRY_CONFIG` group to ConfigVariablesGroup:
- **`APP_REGISTRY_URL`** (string, default `https://registry.npmjs.org`) -- default npm registry URL
- **`APP_REGISTRY_TOKEN`** (string, optional, sensitive) -- auth token for the default registry
### 1d. Generate migration
TypeORM migration adding `sourcePackage`, `tarballFileId`, `registryUrl`, `latestAvailableVersion` to `core.applicationRegistration`.
## Phase 2: App Package Resolver Service
### 2a. Create AppPackageResolverService
New service with core method:
```
resolvePackage(appRegistration, options?: { targetVersion? }) → ResolvedPackage | null
```
Returns `{ manifestPath, packageJsonPath, filesDir }` or null for OAuth-only apps.
**Resolution logic:**
```
if sourcePackage:
1. Determine registry: appRegistration.registryUrl ?? APP_REGISTRY_URL
2. Determine auth token for the resolved registry
3. Generate temporary .npmrc in an isolated working directory
4. Run: yarn add <sourcePackage>@<targetVersion ?? latest>
5. Read manifest from node_modules/<sourcePackage>/.twenty/output/manifest.json
6. Return paths
if tarballFileId:
1. Download tarball from FileStorageService
2. Extract to temporary directory
3. Read manifest from extracted files
4. Return paths
else:
return null (OAuth-only)
```
### 2b. Isolated working directories
Each resolution runs in a temporary directory under `{os.tmpdir()}/twenty-app-resolver/{uuid}/`. This avoids contaminating the server's own `node_modules` and isolates apps from each other. Cleaned up after files are copied to storage.
### 2c. .npmrc generation
For scoped packages (`@scope/twenty-app-*`):
```
@scope:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=TOKEN
```
For unscoped packages with a non-default registry:
```
registry=https://my-verdaccio.internal:4873
//my-verdaccio.internal:4873/:_authToken=TOKEN
```
### 2d. Post-resolution file transfer
After resolving, copies files into the app's storage path using the existing FileStorageService layout:
```
{workspaceId}/{applicationUniversalIdentifier}/
built-logic-function/...
built-front-component/...
dependencies/package.json
dependencies/yarn.lock
public-asset/...
source/...
```
This reuses the same FileFolder enum paths that `app:dev` uses, so downstream ApplicationSyncService works unchanged.
## Phase 3: Marketplace Discovery via npm
### 3a. Update MarketplaceService
Add npm-based discovery alongside the existing GitHub path:
- Query npm search API: `GET {registryUrl}/-/v1/search?text=keywords:twenty-app&size=250`
- Map each result to MarketplaceAppDTO using package.json metadata
**Merge strategy:**
1. Fetch from npm search API (apps with `keywords: ["twenty-app"]`)
2. Fetch from GitHub (existing curated list)
3. Merge by `universalIdentifier` -- GitHub entries override npm entries (allowing curation)
4. Cache merged result with existing 1-hour TTL
### 3b. Add sourcePackage to MarketplaceAppDTO
The DTO needs a `sourcePackage: string | null` field so the install flow knows which npm package to resolve. For npm-discovered apps, this is the package name. For GitHub-only apps, this is null.
## Phase 4: Version Upgrade Support
### 4a. Create AppUpgradeService
**Periodic version check (npm-sourced apps only):**
- Fetches `{registryUrl}/{sourcePackage}/latest` from the npm registry
- Stores result in `AppRegistration.latestAvailableVersion`
- Frontend compares against `Application.version` to show "Update available"
**Upgrade trigger:**
1. Resolve the new version via AppPackageResolverService
2. Sync via existing ApplicationSyncService (triggers workspace migration for schema changes)
3. Update Application.version
**Rollback strategy:** If sync fails (e.g., migration validation error), re-resolve the previous version and re-sync. This is possible because npm retains all published versions.
### 4b. Version check scheduling
Lightweight cron or check-on-access pattern. Iterates over AppRegistrations where `sourcePackage IS NOT NULL` and calls `checkForUpdates()`. Frequency: once per hour, matching the existing marketplace cache TTL.
## Phase 5: SDK CLI Commands
### 5a. Finalize `twenty app:build`
Ensure `.twenty/output/` is npm-publishable. The build step generates a `package.json` in the output directory:
```json
{
"name": "twenty-app-fireflies",
"version": "1.2.0",
"keywords": ["twenty-app"],
"twenty": {
"universalIdentifier": "a4df0c0f-c65e-44e5-8436-24814182d4ac"
},
"files": ["manifest.json", "built-logic-function", "built-front-component", "public-asset"]
}
```
The developer then publishes with standard `npm publish` -- no custom command needed.
### 5b. `twenty app:pack` (new command)
```
twenty app:pack [appPath]
```
- Runs `app:build` if `.twenty/output/` doesn't exist or is stale
- Uses existing TarballService to create `{name}-{version}.tar.gz`
- Outputs the file path for manual distribution
### 5c. `twenty app:push` (new command)
```
twenty app:push [appPath] --server <url> --token <token>
```
- Runs `app:pack` to produce the tarball
- Reads universalIdentifier from manifest to find or create the AppRegistration
- Uploads via `POST /api/app-registrations/upload-tarball`
- Reports success with the registration ID
- Reuses `twenty auth:login` credentials if `--server` is not specified
## Phase 6: Server Tarball Upload Endpoint
### 6a. REST controller
```
POST /api/app-registrations/upload-tarball
Content-Type: multipart/form-data
Body: tarball file + optional universalIdentifier
```
**Validation:**
- Max file size: 50MB
- Must be a valid `.tar.gz`
- Extracted contents must contain `manifest.json` with a valid `universalIdentifier`
- The `universalIdentifier` must not conflict with an existing registration owned by a different user
**Flow:**
1. Extract tarball to temp directory
2. Validate manifest structure
3. Find or create AppRegistration by universalIdentifier
4. Store tarball in FileStorageService under `FileFolder.AppTarball`
5. Set `tarballFileId` on the AppRegistration
6. Return the AppRegistration entity
### 6b. Add FileFolder.AppTarball
New enum value `AppTarball = 'app-tarball'` in FileFolder.
## Key Design Decisions
| Decision | Rationale |
|---|---|
| Per-AppRegistration registry override | `registryUrl` on the entity allows mixing registries. Public apps from npmjs.com, private from GitHub Packages/Verdaccio. Server-wide `APP_REGISTRY_URL` is the fallback. |
| npm publish is standard | No custom publish infra. Free versioning, README, `npm audit`, download stats, proven auth model. |
| Tarball as escape hatch | Air-gapped environments, CI pipelines, one-off installs. Cannot auto-upgrade. |
| sourceType distinction | `'npm' \| 'tarball' \| 'local'` lets the system know which upgrade path is available. Only npm apps can check for newer versions. |
| Backward compatible | `app:dev` flow unchanged. GitHub marketplace unchanged. All new fields nullable. |
| Upgrade rollback | Re-resolve previous version from npm on failure. Safe because npm never deletes published versions. |
## Edge Cases
- **npm unreachable**: Timeout after 30s, throw clear error. App remains at current installed version.
- **Package name conflicts**: The `universalIdentifier` in the `twenty` field of `package.json` is the source of truth, not the npm package name. Two packages with the same universalIdentifier conflict at the AppRegistration level (unique index).
- **Scoped vs unscoped packages**: Both work. Scoped packages naturally route to a private registry via `.npmrc` scope mapping.
- **Multiple workspaces, same server**: AppRegistration is server-level (core schema). Application is workspace-level. One AppRegistration can be installed in multiple workspaces at different versions.
Binary file not shown.
+7 -9
View File
@@ -83,11 +83,11 @@
"@sentry/types": "^8",
"@storybook-community/storybook-addon-cookie": "^5.0.0",
"@storybook/addon-coverage": "^3.0.0",
"@storybook/addon-docs": "^10.1.11",
"@storybook/addon-links": "^10.1.11",
"@storybook/addon-vitest": "^10.1.11",
"@storybook/addon-docs": "^10.2.13",
"@storybook/addon-links": "^10.2.13",
"@storybook/addon-vitest": "^10.2.13",
"@storybook/icons": "^2.0.1",
"@storybook/react-vite": "^10.1.11",
"@storybook/react-vite": "^10.2.13",
"@storybook/test-runner": "^0.24.2",
"@stylistic/eslint-plugin": "^1.5.0",
"@swc-node/register": "1.11.1",
@@ -159,7 +159,7 @@
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.4",
"eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-plugin-storybook": "^10.1.11",
"eslint-plugin-storybook": "^10.2.13",
"eslint-plugin-unicorn": "^56.0.1",
"eslint-plugin-unused-imports": "^3.0.0",
"http-server": "^14.1.1",
@@ -175,9 +175,9 @@
"raw-loader": "^4.0.2",
"rimraf": "^5.0.5",
"source-map-support": "^0.5.20",
"storybook": "^10.1.11",
"storybook": "^10.2.13",
"storybook-addon-mock-date": "2.0.0",
"storybook-addon-pseudo-states": "^10.1.11",
"storybook-addon-pseudo-states": "^10.2.13",
"supertest": "^6.1.3",
"ts-jest": "^29.1.1",
"ts-loader": "^9.2.3",
@@ -201,8 +201,6 @@
"type-fest": "4.10.1",
"typescript": "5.9.2",
"graphql-redis-subscriptions/ioredis": "^5.6.0",
"prosemirror-view": "1.40.0",
"prosemirror-transform": "1.10.4",
"@lingui/core": "5.1.2",
"@types/qs": "6.9.16"
},
@@ -15,6 +15,7 @@ const jestConfig = {
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'^package.json$': '<rootDir>/package.json',
},
moduleFileExtensions: ['ts', 'js'],
extensionsToTreatAsEsm: ['.ts'],
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "create-twenty-app",
"version": "0.6.2",
"version": "0.6.3",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",
@@ -4,8 +4,8 @@ import { copyBaseApplicationProject } from '@/utils/app-template';
import * as fs from 'fs-extra';
import { tmpdir } from 'os';
import { join } from 'path';
import createTwentyAppPackageJson from 'package.json';
// Mock fs-extra's copy function to skip copying base template (not available during tests)
jest.mock('fs-extra', () => {
const actual = jest.requireActual('fs-extra');
return {
@@ -41,7 +41,6 @@ describe('copyBaseApplicationProject', () => {
let testAppDirectory: string;
beforeEach(async () => {
// Create a unique temp directory for each test
testAppDirectory = join(
tmpdir(),
`test-twenty-app-${Date.now()}-${Math.random().toString(36).slice(2)}`,
@@ -51,7 +50,6 @@ describe('copyBaseApplicationProject', () => {
});
afterEach(async () => {
// Clean up temp directory after each test
if (testAppDirectory && (await fs.pathExists(testAppDirectory))) {
await fs.remove(testAppDirectory);
}
@@ -66,15 +64,12 @@ describe('copyBaseApplicationProject', () => {
exampleOptions: ALL_EXAMPLES,
});
// Verify src/ folder exists
const srcAppPath = join(testAppDirectory, 'src');
expect(await fs.pathExists(srcAppPath)).toBe(true);
// Verify application-config.ts exists in src/
const appConfigPath = join(srcAppPath, APPLICATION_FILE_NAME);
expect(await fs.pathExists(appConfigPath)).toBe(true);
// Verify default-role.ts exists in src/
const roleConfigPath = join(srcAppPath, 'roles', DEFAULT_ROLE_FILE_NAME);
expect(await fs.pathExists(roleConfigPath)).toBe(true);
});
@@ -94,7 +89,9 @@ describe('copyBaseApplicationProject', () => {
const packageJson = await fs.readJson(packageJsonPath);
expect(packageJson.name).toBe('my-test-app');
expect(packageJson.version).toBe('0.1.0');
expect(packageJson.dependencies['twenty-sdk']).toBe('latest');
expect(packageJson.devDependencies['twenty-sdk']).toBe(
createTwentyAppPackageJson.version,
);
expect(packageJson.scripts['twenty']).toBe('twenty');
});
@@ -143,27 +140,22 @@ describe('copyBaseApplicationProject', () => {
const appConfigPath = join(testAppDirectory, 'src', APPLICATION_FILE_NAME);
const appConfigContent = await fs.readFile(appConfigPath, 'utf8');
// Verify it uses defineApplication
expect(appConfigContent).toContain(
"import { defineApplication } from 'twenty-sdk'",
);
expect(appConfigContent).toContain('export default defineApplication({');
// Verify it imports the role identifier
expect(appConfigContent).toContain(
"import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role'",
);
// Verify display name and description
expect(appConfigContent).toContain("displayName: 'My Test App'");
expect(appConfigContent).toContain("description: 'A test application'");
// Verify it has a universalIdentifier (UUID format)
expect(appConfigContent).toMatch(
/universalIdentifier: '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'/,
);
// Verify it references the role
expect(appConfigContent).toContain(
'defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER',
);
@@ -186,29 +178,24 @@ describe('copyBaseApplicationProject', () => {
);
const roleConfigContent = await fs.readFile(roleConfigPath, 'utf8');
// Verify it uses defineRole
expect(roleConfigContent).toContain(
"import { defineRole } from 'twenty-sdk'",
);
expect(roleConfigContent).toContain('export default defineRole({');
// Verify it exports the universal identifier constant
expect(roleConfigContent).toContain(
'export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER',
);
// Verify role label includes app name
expect(roleConfigContent).toContain(
"label: 'My Test App default function role'",
);
// Verify default permissions
expect(roleConfigContent).toContain('canReadAllObjectRecords: true');
expect(roleConfigContent).toContain('canUpdateAllObjectRecords: true');
expect(roleConfigContent).toContain('canSoftDeleteAllObjectRecords: true');
expect(roleConfigContent).toContain('canDestroyAllObjectRecords: false');
// Verify it has a universalIdentifier (UUID format)
expect(roleConfigContent).toMatch(
/universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER/,
);
@@ -223,7 +210,6 @@ describe('copyBaseApplicationProject', () => {
exampleOptions: ALL_EXAMPLES,
});
// Verify fs.copy was called with correct destination
expect(fs.copy).toHaveBeenCalledTimes(1);
expect(fs.copy).toHaveBeenCalledWith(
expect.stringContaining('base-application'),
@@ -247,7 +233,6 @@ describe('copyBaseApplicationProject', () => {
});
it('should generate unique UUIDs for each application', async () => {
// Create first app
const firstAppDir = join(testAppDirectory, 'app1');
await fs.ensureDir(firstAppDir);
await copyBaseApplicationProject({
@@ -258,7 +243,6 @@ describe('copyBaseApplicationProject', () => {
exampleOptions: ALL_EXAMPLES,
});
// Create second app
const secondAppDir = join(testAppDirectory, 'app2');
await fs.ensureDir(secondAppDir);
await copyBaseApplicationProject({
@@ -269,7 +253,6 @@ describe('copyBaseApplicationProject', () => {
exampleOptions: ALL_EXAMPLES,
});
// Read both app configs
const firstAppConfig = await fs.readFile(
join(firstAppDir, 'src', APPLICATION_FILE_NAME),
'utf8',
@@ -279,7 +262,6 @@ describe('copyBaseApplicationProject', () => {
'utf8',
);
// Extract UUIDs using regex
const uuidRegex =
/universalIdentifier: '([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'/;
const firstUuid = firstAppConfig.match(uuidRegex)?.[1];
@@ -291,7 +273,6 @@ describe('copyBaseApplicationProject', () => {
});
it('should generate unique role UUIDs for each application', async () => {
// Create first app
const firstAppDir = join(testAppDirectory, 'app1');
await fs.ensureDir(firstAppDir);
await copyBaseApplicationProject({
@@ -302,7 +283,6 @@ describe('copyBaseApplicationProject', () => {
exampleOptions: ALL_EXAMPLES,
});
// Create second app
const secondAppDir = join(testAppDirectory, 'app2');
await fs.ensureDir(secondAppDir);
await copyBaseApplicationProject({
@@ -323,7 +303,6 @@ describe('copyBaseApplicationProject', () => {
'utf8',
);
// Extract UUIDs using regex
const uuidRegex =
/DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =\s*'([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'/;
const firstUuid = firstRoleConfig.match(uuidRegex)?.[1];
@@ -402,7 +381,6 @@ describe('copyBaseApplicationProject', () => {
const srcPath = join(testAppDirectory, 'src');
// Core files should exist
expect(await fs.pathExists(join(srcPath, APPLICATION_FILE_NAME))).toBe(
true,
);
@@ -422,7 +400,6 @@ describe('copyBaseApplicationProject', () => {
),
).toBe(true);
// Example files should not exist
expect(
await fs.pathExists(join(srcPath, 'objects', 'example-object.ts')),
).toBe(false);
@@ -4,6 +4,7 @@ import { v4 } from 'uuid';
import { ASSETS_DIR } from 'twenty-shared/application';
import { type ExampleOptions } from '@/types/scaffolding-options';
import createTwentyAppPackageJson from 'package.json';
const SRC_FOLDER = 'src';
@@ -146,8 +147,7 @@ generated
# dev
/dist/
.twenty/*
!.twenty/output/
.twenty
# production
/build
@@ -546,10 +546,9 @@ const createPackageJson = async ({
lint: 'eslint',
'lint:fix': 'eslint --fix',
},
dependencies: {
'twenty-sdk': 'latest',
},
dependencies: {},
devDependencies: {
'twenty-sdk': createTwentyAppPackageJson.version,
typescript: '^5.9.3',
'@types/node': '^24.7.2',
'@types/react': '^18.2.0',
+2 -1
View File
@@ -11,7 +11,8 @@
"noEmit": true,
"types": ["jest", "node"],
"paths": {
"@/*": ["./src/*"]
"@/*": ["./src/*"],
"package.json": ["./package.json"]
},
"jsx": "react"
},
@@ -1,2 +1,37 @@
.yarn/install-state.gz
.env
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn
# codegen
generated
# testing
/coverage
# dev
/dist/
.twenty
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# typescript
*.tsbuildinfo
@@ -2,25 +2,6 @@
Used to manage billing and telemetry of self-hosted instances
## Requirements
- twenty-cli `npm install -g twenty-cli`
- an `apiKey`. Go to `https://twenty.com/settings/api-webhooks` to generate one
## Install to your Twenty workspace
```bash
twenty auth login
twenty app sync
```
## Environment Variables
This application requires the following environment variables to be set:
- `TWENTY_API_URL`: The Twenty instance API URL where selfHostingUser records will be created
- `TWENTY_API_KEY`: API key for authentication (generate at `/settings/api-webhooks`)
## Features
### Telemetry Webhook
@@ -9,26 +9,13 @@
},
"packageManager": "[email protected]",
"scripts": {
"auth:login": "twenty auth login",
"auth:logout": "twenty auth logout",
"auth:status": "twenty auth status",
"auth:switch": "twenty auth switch",
"auth:list": "twenty auth list",
"app:dev": "twenty app dev",
"app:sync": "twenty app sync",
"entity:add": "twenty entity add",
"function:logs": "twenty function logs",
"function:execute": "twenty function execute",
"app:uninstall": "twenty app uninstall",
"help": "twenty help",
"twenty": "twenty",
"lint": "eslint",
"lint:fix": "eslint --fix"
},
"dependencies": {
"twenty-sdk": "0.3.1"
},
"devDependencies": {
"@types/node": "^24.7.2"
"@types/node": "^24.7.2",
"twenty-sdk": "0.6.2"
},
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/src/constants/schemas/appManifest.schema.json",
"universalIdentifier": "a7070f46-3158-4b40-828f-8e6b1febc233"
@@ -1,19 +0,0 @@
import { defineApp } from 'twenty-sdk';
export default defineApp({
universalIdentifier: '94f7db30-59e5-4b09-a5fe-64cd3d4a65b0',
displayName: 'Self Hosting',
description: 'Used to manage billing and telemetry of self-hosted instances',
applicationVariables: {
TWENTY_API_KEY: {
universalIdentifier: 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d',
description: 'Twenty API key for creating selfHostingUser records',
isSecret: true,
},
TWENTY_API_URL: {
universalIdentifier: 'b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e',
description: 'Twenty API URL (e.g., https://api.twenty.com)',
isSecret: false,
},
},
});
@@ -1,18 +0,0 @@
import { FieldType, defineObject } from 'twenty-sdk';
export default defineObject({
universalIdentifier: '06f3fb53-599e-4c6b-9df6-8f731973afd7',
nameSingular: 'selfHostingUser',
namePlural: 'selfHostingUsers',
labelSingular: 'Self Hosting User',
labelPlural: 'Self Hosting Users',
fields: [
{
type: FieldType.EMAILS,
name: 'email',
label: 'Email',
description: 'The email of the self hosting user',
universalIdentifier: 'a4b7892c-431a-4d44-973e-a5481652704f',
},
],
});
@@ -1,132 +0,0 @@
import { defineFunction } from 'twenty-sdk';
import { createClient } from '../../generated';
// TODO: import from twenty-sdk when 0.4.0 is deployed
type ServerlessFunctionEvent<TBody = object> = {
headers: Record<string, string | undefined>;
queryStringParameters: Record<string, string | undefined>;
pathParameters: Record<string, string | undefined>;
body: TBody | null;
isBase64Encoded: boolean;
requestContext: {
http: {
method: string;
path: string;
};
};
};
type TelemetryEventPayload = {
action: string;
timestamp: string;
version: string;
payload: {
userId: string | null;
workspaceId: string | null;
payload?: {
events?: Array<{
userId?: string;
userEmail?: string;
userFirstName?: string;
userLastName?: string;
locale?: string;
serverUrl?: string;
}>;
};
};
};
export const main = async (
params: ServerlessFunctionEvent<TelemetryEventPayload>,
): Promise<{ success: boolean; message: string; error?: string }> => {
try {
const { action, payload } = params.body || {};
if (action !== 'user_signup') {
return {
success: true,
message: `Event type '${action}' ignored`,
};
}
const userEmail =
payload?.payload?.events?.[0]?.userEmail ||
payload?.payload?.events?.[0]?.userId;
if (!userEmail) {
return {
success: false,
message: 'No email found in telemetry event',
error: 'Missing userEmail in payload',
};
}
if (
userEmail.toLowerCase().includes('example') ||
userEmail.toLowerCase().includes('test')
) {
return {
success: true,
message: `Email '${userEmail}' ignored (contains test/example data)`,
};
}
const client = createClient({
url: `${process.env.TWENTY_API_URL}/graphql`,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.TWENTY_API_KEY}`,
},
});
// Create or update selfHostingUser record
const result = await client.mutation({
createSelfHostingUser: {
__args: {
data: {
name:
payload?.payload?.events?.[0]?.userFirstName +
' ' +
payload?.payload?.events?.[0]?.userLastName,
email: {
primaryEmail: userEmail,
additionalEmails: null,
},
},
upsert: true,
},
id: true,
email: {
primaryEmail: true,
},
},
});
return {
success: true,
message: `Self hosting user created/updated: ${result.createSelfHostingUser?.id}`,
};
} catch (error) {
return {
success: false,
message: 'Failed to process telemetry event',
error: error instanceof Error ? error.message : String(error),
};
}
};
export default defineFunction({
universalIdentifier: '10104201-622b-4a5e-9f27-8f2af19b2a3c',
name: 'telemetry-webhook',
timeoutSeconds: 5,
handler: main,
triggers: [
{
universalIdentifier: '7c8e3f5a-9b4c-4d1e-8f2a-1b3c4d5e6f7a',
type: 'route',
path: '/webhook/telemetry',
httpMethod: 'POST',
isAuthRequired: false,
},
],
});
@@ -0,0 +1,10 @@
import { defineApplication } from 'twenty-sdk';
import { UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers.constant';
export default defineApplication({
universalIdentifier: '94f7db30-59e5-4b09-a5fe-64cd3d4a65b0',
displayName: 'Self Hosting',
description: 'Used to manage billing and telemetry of self-hosted instances',
defaultRoleUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.roles.defaultRole.universalIdentifier,
});
@@ -0,0 +1,120 @@
export const UNIVERSAL_IDENTIFIERS = {
objects: {
selfHostingUser: {
universalIdentifier: '06f3fb53-599e-4c6b-9df6-8f731973afd7',
fields: {
name: { universalIdentifier: '682cccbf-9f37-4290-a94c-902c771f61e4' },
email: { universalIdentifier: 'a4b7892c-431a-4d44-973e-a5481652704f' },
personId: {
universalIdentifier: 'b453a43c-1512-48ca-8604-db750ad3ffb8',
},
domain: {
universalIdentifier: '1dfa7d4e-c8f5-4639-b58e-3392a8789f76',
},
userWorkspaceId: {
universalIdentifier: '297a7d6b-e407-4b2d-8c03-8964bc1b7805',
},
userId: {
universalIdentifier: '5c7ba3ce-1473-4e3d-8e7c-31816fcb87d8',
},
locale: {
universalIdentifier: '7b39df37-a22e-4f38-ae77-91cf3ee7c076',
},
serverUrl: {
universalIdentifier: 'f2516b77-2912-4cbb-8838-46ac5a5465d9',
},
serverId: {
universalIdentifier: 'e68a2b15-786d-4e9d-a74d-6d6d577ae721',
},
numberOfEmailsWithSameDomain: {
universalIdentifier: '0bf05db0-6771-4400-91ca-1579ec11e76e',
},
isEnriched: {
universalIdentifier: 'fefe9fd6-23ae-4046-b60b-64d17e9ff7ed',
},
triedToBeEnriched: {
universalIdentifier: 'd32c8cc3-8855-453d-bb7d-9c9c0b3f2128',
},
isPersonalEmail: {
universalIdentifier: 'f4568391-9474-4ed8-8cbb-e36d86e0f5f9',
},
isTwenty: {
universalIdentifier: 'b1acef1f-7c10-47a9-899e-aaca45b36e04',
},
personCity: {
universalIdentifier: 'ca733484-e595-4257-9ca9-9a7802fb8bcb',
},
personCountry: {
universalIdentifier: '18c06357-1b50-4d5b-82cf-1f71f286fbe4',
},
personJobFunction: {
universalIdentifier: '26e7e2c7-ea83-41e0-8c07-1fc2549a3fb4',
},
personJobTitle: {
universalIdentifier: '177908e9-1ca6-4762-9518-0df966d3e9fc',
},
personLinkedIn: {
universalIdentifier: '3515683f-7f9f-4b6d-9b16-614824d277b7',
},
personSeniority: {
universalIdentifier: '8b63855a-5915-4d6a-a6ed-ef7d8f8e5dd1',
},
companyAlexaRank: {
universalIdentifier: '7c61335b-cd4b-4eae-8b02-0db746913e36',
},
companyAnnualRevenue: {
universalIdentifier: 'a2367973-aa12-42c2-9577-fe868f61b83b',
},
companyAnnualRevenuePrinted: {
universalIdentifier: 'bc02b6af-8f48-4fde-920d-1fd3e2a8557b',
},
companyDescription: {
universalIdentifier: 'a9bb622e-56b6-42ba-8b03-17a47d707409',
},
companyEmployees: {
universalIdentifier: '8e1dbc58-d444-470f-b8fe-9eed8da4b59e',
},
companyFoundedYear: {
universalIdentifier: '3cf95527-5064-43ab-bf5e-421eb45fac5f',
},
companyFundingLatestStage: {
universalIdentifier: 'a7dcd92a-6811-490b-a8dd-fad1c19091a1',
},
companyFundingTotalAmount: {
universalIdentifier: '6fca8a11-b49a-4081-a7c9-9646f43ad7aa',
},
companyFundingTotalAmountPrinted: {
universalIdentifier: '0078f0f0-2262-4c74-aaf8-4061c6c8a1f3',
},
companyIndustries: {
universalIdentifier: '6b971b9c-6ef5-4497-989e-f9a7c72720cf',
},
companyIndustry: {
universalIdentifier: 'ab84e651-d35b-4e02-8d69-1740af3e22f7',
},
companyLinkedIn: {
universalIdentifier: '4c44b956-f880-434f-b4cd-854b82076e56',
},
companyName: {
universalIdentifier: '1a25412b-f9ce-4406-ac53-f20d1ab8c5ea',
},
companyTags: {
universalIdentifier: 'ceb64d0b-1203-4c6d-af00-39b668f5f891',
},
companyTech: {
universalIdentifier: '11dd57c3-06bb-4722-bb65-96d0a899ca91',
},
},
},
},
roles: {
defaultRole: {
universalIdentifier: '66972e19-9fdb-4336-87ce-442a17fd179c',
},
},
views: {
selfHostingUserView: {
universalIdentifier: 'e903f0ee-52cb-4537-aca8-8940e30b023d',
},
},
};
@@ -0,0 +1,29 @@
import {
defineField,
FieldType,
RelationType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
import { UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers.constant';
export const SELF_HOSTING_USER_ID_UNIVERSAL_IDENTIFIER =
'9507f244-fdea-47d5-a734-725d4dae43da';
export default defineField({
universalIdentifier: SELF_HOSTING_USER_ID_UNIVERSAL_IDENTIFIER,
name: 'selfHostingUsers',
label: 'Self hosting users',
description: 'Self hosting user related to the person',
type: FieldType.RELATION,
relationTargetFieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personId
.universalIdentifier,
relationTargetObjectMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.universalIdentifier,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
isNullable: true,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
});
@@ -0,0 +1,94 @@
import {
defineLogicFunction,
type DatabaseEventPayload,
type ObjectRecordCreateEvent,
type ObjectRecordUpdateEvent,
} from 'twenty-sdk';
import { SELF_HOSTING_USER_NAME_SINGULAR } from 'src/objects/selfHostingUser.object';
import { type SelfHostingUser } from 'twenty-sdk/generated/core';
import { CoreApiClient } from 'twenty-sdk/generated';
const handler = async (
params: DatabaseEventPayload<
| ObjectRecordCreateEvent<SelfHostingUser>
| ObjectRecordUpdateEvent<SelfHostingUser>
>,
) => {
const [object, action] = params.name.split('.');
if (object !== SELF_HOSTING_USER_NAME_SINGULAR) {
return;
}
if (!['created', 'updated'].includes(action)) {
return;
}
const email = params.properties.after.email?.primaryEmail;
if (!email) {
return;
}
const client = new CoreApiClient();
const { people } = await client.query({
people: {
edges: { node: { id: true } },
__args: {
filter: {
emails: {
primaryEmail: { eq: email },
},
},
},
},
});
let personId = people?.edges[0]?.node?.id;
if (!personId) {
const { createPerson } = await client.mutation({
createPerson: {
__args: {
data: {
name: {
firstName: params.properties.after.name?.firstName,
lastName: params.properties.after.name?.lastName,
},
emails: {
primaryEmail: email,
},
},
},
id: true,
},
});
personId = createPerson?.id;
}
await client.mutation({
updateSelfHostingUser: {
__args: {
id: params.properties.after.id,
data: {
personId,
},
},
id: true,
},
});
};
export default defineLogicFunction({
universalIdentifier: '87f0293a-997a-4c7b-85e2-e77462ccf0c5',
name: 'match-telemetry-event-with-people',
description:
'Matches self hosting users with existing people based on email address',
timeoutSeconds: 10,
handler,
databaseEventTriggerSettings: {
eventName: `${SELF_HOSTING_USER_NAME_SINGULAR}.*`,
},
});
@@ -0,0 +1,136 @@
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-sdk/generated';
import { type TelemetryEvent } from 'src/logic-functions/types/telemetry-event.type';
export const main = async (
params: RoutePayload<TelemetryEvent>,
): Promise<{
success: boolean;
message: string;
error?: string;
}> => {
try {
const {
action,
workspaceId,
userWorkspaceId,
userId,
userEmail,
userFirstName,
userLastName,
locale,
serverUrl,
serverId,
} = params.body || {};
if (action !== 'user_signup') {
return {
success: true,
message: `Event type '${action}' ignored`,
};
}
if (!userEmail) {
return {
success: true,
message: 'No email found in telemetry event',
};
}
if (
userEmail.toLowerCase().includes('example') ||
userEmail.toLowerCase().includes('test')
) {
return {
success: true,
message: `Email '${userEmail}' ignored (contains test/example data)`,
};
}
const client = new CoreApiClient();
let existingSelfHostingUserId: string | undefined = undefined;
try {
const { selfHostingUser: existingSelfHostingUser } = await client.query({
selfHostingUser: {
__args: {
filter: {
email: { primaryEmail: { eq: userEmail } },
},
},
id: true,
},
});
existingSelfHostingUserId = existingSelfHostingUser?.id;
} catch {
//
}
if (existingSelfHostingUserId) {
await client.mutation({
updateSelfHostingUser: {
__args: {
id: existingSelfHostingUserId,
data: {
name: { firstName: userFirstName, lastName: userLastName },
email: { primaryEmail: userEmail, additionalEmails: null },
userWorkspaceId,
userId,
locale,
serverUrl,
serverId,
},
},
id: true,
},
});
return {
success: true,
message: `Self hosting user ${existingSelfHostingUserId} updated`,
};
}
const { createSelfHostingUser } = await client.mutation({
createSelfHostingUser: {
__args: {
data: {
name: { firstName: userFirstName, lastName: userLastName },
email: { primaryEmail: userEmail, additionalEmails: null },
workspaceId,
userWorkspaceId,
userId,
locale,
serverUrl,
serverId,
},
},
id: true,
},
});
return {
success: true,
message: `Self hosting user ${createSelfHostingUser?.id} created`,
};
} catch (error) {
return {
success: false,
message: 'Failed to process telemetry event',
error: error instanceof Error ? error.message : String(error),
};
}
};
export default defineLogicFunction({
universalIdentifier: '10104201-622b-4a5e-9f27-8f2af19b2a3c',
name: 'telemetry-webhook',
timeoutSeconds: 10,
handler: main,
httpRouteTriggerSettings: {
path: '/webhook/telemetry',
httpMethod: 'POST',
isAuthRequired: false,
},
});
@@ -0,0 +1,12 @@
export type TelemetryEvent = {
action: string;
workspaceId?: string;
userWorkspaceId?: string;
userId: string;
userEmail?: string;
userFirstName?: string;
userLastName?: string;
locale?: string;
serverUrl: string;
serverId: string;
};
@@ -0,0 +1,11 @@
import { defineNavigationMenuItem } from 'twenty-sdk';
import { UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers.constant';
export default defineNavigationMenuItem({
universalIdentifier: 'fe3aaca4-9eda-4565-b215-5d268fbf8164',
name: 'Self host user',
icon: 'IconList',
position: 1,
viewUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.views.selfHostingUserView.universalIdentifier,
});
@@ -0,0 +1,354 @@
import {
defineObject,
FieldType,
RelationType,
OnDeleteAction,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
import { UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers.constant';
import { SELF_HOSTING_USER_ID_UNIVERSAL_IDENTIFIER } from 'src/fields/self-hosting-user-id';
export const SELF_HOSTING_USER_NAME_SINGULAR = 'selfHostingUser';
export default defineObject({
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.universalIdentifier,
nameSingular: SELF_HOSTING_USER_NAME_SINGULAR,
namePlural: 'selfHostingUsers',
labelSingular: 'Self Hosting User',
labelPlural: 'Self Hosting Users',
fields: [
{
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personId
.universalIdentifier,
name: 'person',
label: 'Person',
description: 'Person matching with the self hosting user',
type: FieldType.RELATION,
relationTargetFieldMetadataUniversalIdentifier:
SELF_HOSTING_USER_ID_UNIVERSAL_IDENTIFIER,
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
isNullable: true,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'personId',
},
},
{
type: FieldType.FULL_NAME,
name: 'name',
label: 'Name',
description: 'Name of the self hosting user',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.name
.universalIdentifier,
},
{
type: FieldType.EMAILS,
name: 'email',
label: 'Email',
description: 'The email of the self hosting user',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.email
.universalIdentifier,
},
{
type: FieldType.LINKS,
name: 'domain',
label: 'Domain',
description:
'Domain extracted from the email address (e.g. domain.com / https://domain.com/)',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.domain
.universalIdentifier,
},
{
type: FieldType.UUID,
name: 'userWorkspaceId',
label: 'User workspace Id',
description: 'User workspace id of the self hosting user',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.userWorkspaceId
.universalIdentifier,
},
{
type: FieldType.UUID,
name: 'userId',
label: 'User Id',
description: 'User id of the self hosting user',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.userId
.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'locale',
label: 'Locale',
description: 'Locale of the self hosting user',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.locale
.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'serverUrl',
label: 'Server url',
description: 'Server url of the self hosting user',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.serverUrl
.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'serverId',
label: 'Server id',
description: 'Server id of the self hosting user',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.serverId
.universalIdentifier,
},
{
type: FieldType.NUMBER,
name: 'numberOfEmailsWithSameDomain',
label: 'Number of Emails with Same Domain',
description:
'Aggregated count of self hosting users sharing the same business domain',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
.numberOfEmailsWithSameDomain.universalIdentifier,
},
{
type: FieldType.BOOLEAN,
name: 'isEnriched',
label: 'Is Enriched',
description: 'Whether the record has been enriched',
defaultValue: false,
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.isEnriched
.universalIdentifier,
},
{
type: FieldType.BOOLEAN,
name: 'triedToBeEnriched',
label: 'Tried to Be Enriched',
description: 'Whether an enrichment attempt has been made',
defaultValue: false,
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.triedToBeEnriched
.universalIdentifier,
},
{
type: FieldType.BOOLEAN,
name: 'isPersonalEmail',
label: 'Is Personal Email',
description: 'Whether the email is a personal email address',
defaultValue: true,
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.isPersonalEmail
.universalIdentifier,
},
{
type: FieldType.BOOLEAN,
name: 'isTwenty',
label: 'Is Twenty',
description: 'Whether the user is from Twenty',
defaultValue: false,
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.isTwenty
.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'personCity',
label: 'Person City',
description: 'City of the person',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personCity
.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'personCountry',
label: 'Person Country',
description: 'Country of the person',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personCountry
.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'personJobFunction',
label: 'Person Job Function',
description: 'Job function of the person',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personJobFunction
.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'personJobTitle',
label: 'Person Job Title',
description: 'Job title of the person',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personJobTitle
.universalIdentifier,
},
{
type: FieldType.LINKS,
name: 'personLinkedIn',
label: 'Person LinkedIn',
description: 'LinkedIn profile of the person',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personLinkedIn
.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'personSeniority',
label: 'Person Seniority',
description: 'Seniority level of the person',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personSeniority
.universalIdentifier,
},
{
type: FieldType.NUMBER,
name: 'companyAlexaRank',
label: 'Company Alexa Rank',
description: 'Alexa rank of the company',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyAlexaRank
.universalIdentifier,
},
{
type: FieldType.CURRENCY,
name: 'companyAnnualRevenue',
label: 'Company Annual Revenue',
description: 'Annual revenue of the company',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
.companyAnnualRevenue.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'companyAnnualRevenuePrinted',
label: 'Company Annual Revenue Printed',
description: 'Formatted annual revenue of the company',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
.companyAnnualRevenuePrinted.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'companyDescription',
label: 'Company Description',
description: 'Description of the company',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyDescription
.universalIdentifier,
},
{
type: FieldType.NUMBER,
name: 'companyEmployees',
label: 'Company Employees',
description: 'Number of employees at the company',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyEmployees
.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'companyFoundedYear',
label: 'Company Founded Year',
description: 'Year the company was founded',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyFoundedYear
.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'companyFundingLatestStage',
label: 'Company Funding Latest Stage',
description: 'Latest funding stage of the company',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
.companyFundingLatestStage.universalIdentifier,
},
{
type: FieldType.NUMBER,
name: 'companyFundingTotalAmount',
label: 'Company Funding Total Amount',
description: 'Total funding amount of the company',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
.companyFundingTotalAmount.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'companyFundingTotalAmountPrinted',
label: 'Company Funding Total Amount Printed',
description: 'Formatted total funding amount of the company',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
.companyFundingTotalAmountPrinted.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'companyIndustries',
label: 'Company Industries',
description: 'Industries the company operates in',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyIndustries
.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'companyIndustry',
label: 'Company Industry',
description: 'Primary industry of the company',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyIndustry
.universalIdentifier,
},
{
type: FieldType.LINKS,
name: 'companyLinkedIn',
label: 'Company LinkedIn',
description: 'LinkedIn page of the company',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyLinkedIn
.universalIdentifier,
},
{
type: FieldType.TEXT,
name: 'companyName',
label: 'Company Name',
description: 'Name of the company',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyName
.universalIdentifier,
},
{
type: FieldType.ARRAY,
name: 'companyTags',
label: 'Company Tags',
description: 'Tags associated with the company',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyTags
.universalIdentifier,
},
{
type: FieldType.ARRAY,
name: 'companyTech',
label: 'Company Tech',
description: 'Technologies used by the company',
universalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyTech
.universalIdentifier,
},
],
});
@@ -0,0 +1,13 @@
import { defineRole } from 'twenty-sdk';
import { UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers.constant';
export default defineRole({
universalIdentifier:
UNIVERSAL_IDENTIFIERS.roles.defaultRole.universalIdentifier,
label: 'default role',
description: 'Add a description for your role',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: false,
});
@@ -0,0 +1,308 @@
import { defineView } from 'twenty-sdk';
import { UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers.constant';
export default defineView({
universalIdentifier:
UNIVERSAL_IDENTIFIERS.views.selfHostingUserView.universalIdentifier,
name: 'Self hosting users',
objectUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.universalIdentifier,
icon: 'IconList',
position: 0,
fields: [
{
universalIdentifier: '243a2401-cd13-440c-8dcd-649e26df36bc',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.name
.universalIdentifier,
position: 0,
isVisible: true,
size: 150,
},
{
universalIdentifier: 'dfa75ef8-d40d-416f-9f1c-3e86edfa9fce',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.email
.universalIdentifier,
position: 1,
isVisible: true,
size: 150,
},
{
universalIdentifier: '15cc9215-eb48-4487-a92e-a25d8e99702f',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.domain
.universalIdentifier,
position: 2,
isVisible: true,
size: 200,
},
{
universalIdentifier: '0f9e4f63-3664-443a-9f06-8a6cc04c1d90',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personId
.universalIdentifier,
position: 2.1,
isVisible: true,
size: 200,
},
{
universalIdentifier: 'dcf88ae8-e71d-452f-b51e-d88cbc6dd273',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.userWorkspaceId
.universalIdentifier,
position: 3,
isVisible: true,
},
{
universalIdentifier: 'aad70516-936b-41d1-b6c6-961a22299761',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.userId
.universalIdentifier,
position: 4,
isVisible: true,
},
{
universalIdentifier: '8c210eb0-bdda-476e-9f98-42f909872f2a',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.locale
.universalIdentifier,
position: 5,
isVisible: true,
},
{
universalIdentifier: '367abe85-11c4-440f-80a2-663edd6b4231',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.serverUrl
.universalIdentifier,
position: 6,
isVisible: true,
},
{
universalIdentifier: '32c199d6-ebf3-434b-81b4-e2b59a0518b7',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.serverId
.universalIdentifier,
position: 6.1,
isVisible: true,
},
{
universalIdentifier: '924ee786-ab93-44be-9d21-941ff9ffe1ac',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
.numberOfEmailsWithSameDomain.universalIdentifier,
position: 7,
isVisible: true,
},
{
universalIdentifier: '2feadf3d-e251-4356-add8-7fa70dea5401',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.isEnriched
.universalIdentifier,
position: 8,
isVisible: true,
},
{
universalIdentifier: 'de252ae6-c723-4bf7-96cf-d93f5a539f36',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.triedToBeEnriched
.universalIdentifier,
position: 9,
isVisible: true,
},
{
universalIdentifier: 'b121e8e6-b3eb-4f6c-b67e-c7c6d19e1bc5',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.isPersonalEmail
.universalIdentifier,
position: 10,
isVisible: true,
},
{
universalIdentifier: '0ada0bcc-8d6b-4df6-bcc1-78ba14cb04e6',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.isTwenty
.universalIdentifier,
position: 11,
isVisible: true,
},
{
universalIdentifier: 'ec7c8d51-ea63-41bd-9eb1-995835b94218',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personCity
.universalIdentifier,
position: 12,
isVisible: true,
},
{
universalIdentifier: '7522dd84-0d23-48e7-85dd-f0a8d9e275f8',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personCountry
.universalIdentifier,
position: 13,
isVisible: true,
},
{
universalIdentifier: '54191cb9-4d5c-466e-affb-d9ba4adeff87',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personJobFunction
.universalIdentifier,
position: 14,
isVisible: true,
size: 200,
},
{
universalIdentifier: 'ace75fc7-fb20-4e53-a9a2-6a7529befaf0',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personJobTitle
.universalIdentifier,
position: 15,
isVisible: true,
size: 180,
},
{
universalIdentifier: 'a0b42d61-4553-42eb-aca4-327b9bf9f30e',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personLinkedIn
.universalIdentifier,
position: 16,
isVisible: true,
},
{
universalIdentifier: '74bc7dd2-fe53-4ff4-8778-2768f3439571',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personSeniority
.universalIdentifier,
position: 17,
isVisible: true,
size: 180,
},
{
universalIdentifier: '61b34f41-8d56-472d-ab1e-414703c6ca12',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyAlexaRank
.universalIdentifier,
position: 18,
isVisible: true,
},
{
universalIdentifier: '5bb7d36b-6a73-4832-b41e-f67130a4708f',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
.companyAnnualRevenue.universalIdentifier,
position: 19,
isVisible: true,
},
{
universalIdentifier: 'ae6f23ce-006c-41dd-82a1-e9fe7b65bce3',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
.companyAnnualRevenuePrinted.universalIdentifier,
position: 20,
isVisible: true,
size: 250,
},
{
universalIdentifier: 'dd2a4728-a743-43bb-b096-9e7bd5125e56',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyDescription
.universalIdentifier,
position: 21,
isVisible: true,
size: 200,
},
{
universalIdentifier: 'ecca02c9-db2e-41e2-b571-b5db75054b56',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyEmployees
.universalIdentifier,
position: 22,
isVisible: true,
},
{
universalIdentifier: '2e76775b-f8b8-4184-8cd3-72d2b93edaa2',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyFoundedYear
.universalIdentifier,
position: 23,
isVisible: true,
size: 200,
},
{
universalIdentifier: 'a7eb002c-6f0c-48ba-a9eb-247c498ad9bd',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
.companyFundingLatestStage.universalIdentifier,
position: 24,
isVisible: true,
size: 240,
},
{
universalIdentifier: '61be97f6-20da-4b2b-861d-32345e0f9953',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
.companyFundingTotalAmount.universalIdentifier,
position: 25,
isVisible: true,
},
{
universalIdentifier: '55720810-3120-4e76-bcf2-2da9517edbbc',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
.companyFundingTotalAmountPrinted.universalIdentifier,
position: 26,
isVisible: true,
size: 280,
},
{
universalIdentifier: '01e31752-cbc1-499a-8ecf-504dd402d7e2',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyIndustries
.universalIdentifier,
position: 27,
isVisible: true,
size: 200,
},
{
universalIdentifier: '2e1c8b8b-469b-483e-8348-1fe3d1764e17',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyIndustry
.universalIdentifier,
position: 28,
isVisible: true,
size: 180,
},
{
universalIdentifier: '976cc8ae-6cf8-4c30-8da4-5bf61e799893',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyLinkedIn
.universalIdentifier,
position: 29,
isVisible: true,
},
{
universalIdentifier: '5f0776b3-2849-4b9b-82f0-baa38c6d889d',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyName
.universalIdentifier,
position: 30,
isVisible: true,
},
{
universalIdentifier: '86f0397a-2924-4e5c-a610-3c9ad7bb4923',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyTags
.universalIdentifier,
position: 31,
isVisible: true,
},
{
universalIdentifier: '562084f4-1242-4e60-868b-1d9b268a35b0',
fieldMetadataUniversalIdentifier:
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyTech
.universalIdentifier,
position: 32,
isVisible: true,
},
],
});
@@ -5,13 +5,14 @@
"declaration": true,
"outDir": "./dist",
"rootDir": ".",
"jsx": "react-jsx",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"importHelpers": true,
"allowUnreachableCode": false,
"strictNullChecks": true,
"strict": true,
"alwaysStrict": true,
"noImplicitAny": true,
"strictBindCallApply": false,
@@ -26,10 +27,5 @@
"~/*": ["./*"]
}
},
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"**/*.spec.ts"
]
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
}
File diff suppressed because it is too large Load Diff
@@ -8,7 +8,7 @@ title: أفضل الممارسات
تقوم React و Jotai بإدارة الحالة في قاعدة الشيفرة.
### استخدم `useAtomState` لتخزين الحالة
### استخدم ذرات Jotai لتخزين الحالة
من الجيد إنشاء أكبر عدد ممكن من الذرات لتخزين الحالة الخاصة بك.
@@ -45,7 +45,7 @@ export const MyComponent = () => {
تجنب استخدام `useRef` لتخزين الحالة.
If you want to store state, you should use `useState` or `useAtomState`.
إذا كنت ترغب في تخزين الحالة، يجب أن تستخدم `useState` أو ذرات Jotai مع `useAtomState`.
انظر [كيفية إدارة إعادة العرض](#managing-re-renders) إذا شعرت أنك بحاجة إلى `useRef` لمنع بعض إعادة العرض من الحدوث.
@@ -82,7 +82,7 @@ If you feel like you need to add a `useEffect` in your root component, you shoul
يمكنك تطبيق نفس الشيء على منطق جلب البيانات، مع الخُطافات Apollo.
```tsx
// ❌ سيّئ، سيتسبب في إعادة التصيير حتى إذا لم تتغير البيانات،
// ❌ سيئ، سيتسبب في إعادة التصيير حتى إذا لم تتغير البيانات،
// لأن useEffect يحتاج إلى إعادة التقييم
export const PageComponent = () => {
const [data, setData] = useAtomState(dataState);
@@ -132,9 +132,9 @@ export const App = () => (
);
```
### استخدم حالات عائلة Jotai ومحددات عائلة Jotai
### استخدم حالات عائلة الذرات والمحددات
حالات عائلة Jotai والمحددات تعتبر طريقة رائعة لتجنب إعادة العرض.
تُعد حالات عائلة الذرات والمحددات طريقة رائعة لتجنّب عمليات إعادة التصيير.
إنها مفيدة عندما تحتاج إلى تخزين قائمة من العناصر.
@@ -84,7 +84,7 @@ module1
تشمل منطق إدارة الحالة. [Jotai](https://jotai.org) يتولّى ذلك.
* المحددات: الذرات المشتقة (باستخدام `createAtomSelector`) تحسب القيم من ذرات أخرى ويتم تخزينها تلقائيًا.
* المحددات: الذرات المشتقة (باستخدام `createAtomSelector`) تحسب قيماً من ذرات أخرى وتخزن نتائجها في الذاكرة مؤقتاً تلقائياً.
لا تزال إدارة الحالة المدمجة في React تتولّى الحالة داخل المكوّن.
@@ -49,7 +49,7 @@ title: أوامر الواجهة الأمامية
* "[React](https://react.dev/)"
* "[Apollo](https://www.apollographql.com/docs/)"
* "[GraphQL Codegen](https://the-guild.dev/graphql/codegen)"
* "[Jotai](https://jotai.org/)"
* [Jotai](https://jotai.org/)
* "[TypeScript](https://www.typescriptlang.org/)"
**الاختبار**
@@ -73,7 +73,7 @@ To avoid unnecessary [re-renders](/l/ar/developers/contribute/capabilities/front
### "إدارة الحالة"
"[Jotai](https://jotai.org/) يتعامل مع إدارة الحالة."
[Jotai](https://jotai.org/) يتعامل مع إدارة الحالة.
"راجع [أفضل الممارسات](/l/ar/developers/contribute/capabilities/frontend-development/best-practices-front#state-management) لمزيد من المعلومات حول إدارة الحالة."
@@ -160,7 +160,7 @@ export enum PageHotkeyScope {
}
```
داخليًا، يتم تخزين النطاق المحدد حاليًا في حالة Jotai مشتركة عبر التطبيق:
داخليًا، يتم تخزين النطاق المحدد حاليًا في ذرة Jotai مشتركة عبر التطبيق:
```tsx
export const currentHotkeyScopeState = createState<HotkeyScope>({
@@ -169,10 +169,10 @@ export const currentHotkeyScopeState = createState<HotkeyScope>({
});
```
لكن لا يجب التعامل مع هذه الحالة Jotai يدويًا! سنرى كيف يمكن استخدامها في القسم التالي.
لكن لا يجب التعامل مع هذه الذرة يدويًا! سنرى كيف يمكن استخدامها في القسم التالي.
## كيف يعمل داخليًا؟
قمنا بإنشاء غلاف رقيق فوق [react-hotkeys-hook](https://react-hotkeys-hook.vercel.app/docs/intro) والذي يجعله أكثر كفاءة ويتجنب عمليات إعادة التقديم غير الضرورية.
ونقوم أيضًا بإنشاء حالة Jotai للتعامل مع حالة نطاق المفتاح وجعلها متاحة في جميع أنحاء التطبيق.
ونقوم أيضًا بإنشاء ذرة Jotai للتعامل مع حالة نطاق مفاتيح الاختصار وجعلها متاحة في جميع أنحاء التطبيق.
@@ -52,22 +52,25 @@ npx create-twenty-app@latest my-app --interactive
من هنا يمكنك:
```bash filename="Terminal"
# Add a new entity to your application (guided)
# أضف كيانًا جديدًا إلى تطبيقك (موجّه)
yarn twenty entity:add
# Watch your application's function logs
# راقب سجلات وظائف تطبيقك
yarn twenty function:logs
# Execute a function by name
# نفّذ وظيفة بالاسم
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Execute the post-install function
# نفّذ دالة ما قبل التثبيت
yarn twenty function:execute --preInstall
# نفّذ دالة ما بعد التثبيت
yarn twenty function:execute --postInstall
# Uninstall the application from the current workspace
# أزل تثبيت التطبيق من مساحة العمل الحالية
yarn twenty app:uninstall
# Display commands' help
# اعرض مساعدة الأوامر
yarn twenty help
```
@@ -80,7 +83,7 @@ yarn twenty help
* ينسخ تطبيقًا أساسيًا مصغّرًا إلى `my-twenty-app/`
* يضيف اعتمادًا محليًا `twenty-sdk` وتهيئة Yarn 4
* ينشئ ملفات ضبط ونصوصًا مرتبطة بـ `twenty` CLI
* يُنشئ الملفات الأساسية (تهيئة التطبيق، دور الدالة الافتراضي، دالة ما بعد التثبيت) بالإضافة إلى ملفات الأمثلة بحسب وضع الإنشاء
* يُنشئ الملفات الأساسية (تهيئة التطبيق، دور الدالة الافتراضي، دالتا ما قبل التثبيت وما بعد التثبيت) بالإضافة إلى ملفات أمثلة استنادًا إلى وضع الإنشاء.
يبدو التطبيق المُنشأ حديثًا باستخدام الوضع الافتراضي `--exhaustive` كما يلي:
@@ -107,6 +110,7 @@ my-twenty-app/
│ └── example-field.ts # تعريف حقل مستقل — مثال
├── logic-functions/
│ ├── hello-world.ts # دالة منطقية — مثال
│ ├── pre-install.ts # دالة منطقية لما قبل التثبيت
│ └── post-install.ts # دالة منطقية لما بعد التثبيت
├── front-components/
│ └── hello-world.tsx # مكوّن واجهة أمامية — مثال
@@ -118,7 +122,7 @@ my-twenty-app/
└── example-skill.ts # تعريف مهارة لوكيل الذكاء الاصطناعي — مثال
```
مع `--minimal`، سيتم إنشاء الملفات الأساسية فقط (`application-config.ts` و`roles/default-role.ts` و`logic-functions/post-install.ts`). مع `--interactive`، تختار ملفات الأمثلة التي تريد تضمينها.
مع `--minimal`، سيتم إنشاء الملفات الأساسية فقط (`application-config.ts`، `roles/default-role.ts`، `logic-functions/pre-install.ts`، و`logic-functions/post-install.ts`). مع `--interactive`، تختار ملفات الأمثلة التي تريد تضمينها.
بشكل عام:
@@ -135,16 +139,18 @@ my-twenty-app/
يكتشف SDK الكيانات عبر تحليل ملفات TypeScript الخاصة بك بحثًا عن استدعاءات **`export default define<Entity>({...})`**. يحتوي كل نوع كيان على دالة مساعدة مقابلة يتم تصديرها من `twenty-sdk`:
| دالة مساعدة | نوع الكيان |
| ---------------------------- | ------------------------------------ |
| `defineObject()` | تعريفات كائنات مخصصة |
| `defineLogicFunction()` | تعريفات الوظائف المنطقية |
| `defineFrontComponent()` | Front component definitions |
| `defineRole()` | تعريفات الأدوار |
| `defineField()` | امتدادات الحقول للكائنات الموجودة |
| `defineView()` | تعريفات العروض المحفوظة |
| `defineNavigationMenuItem()` | تعريفات عناصر قائمة التنقل |
| `defineSkill()` | تعريفات مهارات وكيل الذكاء الاصطناعي |
| دالة مساعدة | نوع الكيان |
| ---------------------------------- | ---------------------------------------------- |
| `defineObject()` | تعريفات كائنات مخصصة |
| `defineLogicFunction()` | تعريفات الوظائف المنطقية |
| `definePreInstallLogicFunction()` | دالة منطقية لما قبل التثبيت (تعمل قبل التثبيت) |
| `definePostInstallLogicFunction()` | دالة منطقية لما بعد التثبيت (تعمل بعد التثبيت) |
| `defineFrontComponent()` | Front component definitions |
| `defineRole()` | تعريفات الأدوار |
| `defineField()` | امتدادات الحقول للكائنات الموجودة |
| `defineView()` | تعريفات العروض المحفوظة |
| `defineNavigationMenuItem()` | تعريفات عناصر قائمة التنقل |
| `defineSkill()` | تعريفات مهارات وكيل الذكاء الاصطناعي |
<Note>
**تسمية الملفات مرنة.** يعتمد اكتشاف الكيانات على بنية الشجرة المجردة (AST) — إذ يقوم SDK بفحص ملفات المصدر لديك بحثًا عن النمط `export default define<Entity>({...})`. يمكنك تنظيم ملفاتك ومجلداتك كيفما تشاء. التجميع حسب نوع الكيان (مثلًا، `logic-functions/` و`roles/`) هو مجرد عرف لتنظيم الشيفرة، وليس مطلبًا إلزاميًا.
@@ -209,17 +215,19 @@ yarn twenty auth:status
يوفّر SDK دوالًا مساعدة لتعريف كيانات تطبيقك. كما هو موضح في [اكتشاف الكيانات](#entity-detection)، يجب استخدام `export default define<Entity>({...})` كي يتم اكتشاف كياناتك:
| دالة | الغرض |
| ---------------------------- | ---------------------------------------------------- |
| `defineApplication()` | تهيئة بيانات التعريف للتطبيق (مطلوب، واحد لكل تطبيق) |
| `defineObject()` | تعريف كائنات مخصصة مع حقول |
| `defineLogicFunction()` | تعريف وظائف منطقية مع معالجات |
| `defineFrontComponent()` | عرِّف مكوّنات أمامية لواجهة مستخدم مخصّصة |
| `defineRole()` | تهيئة صلاحيات الدور والوصول إلى الكائنات |
| `defineField()` | وسّع الكائنات الموجودة بحقول إضافية |
| `defineView()` | تعريف العروض المحفوظة للكائنات |
| `defineNavigationMenuItem()` | تعريف روابط التنقل في الشريط الجانبي |
| `defineSkill()` | عرّف مهارات وكيل الذكاء الاصطناعي |
| دالة | الغرض |
| ---------------------------------- | ---------------------------------------------------- |
| `defineApplication()` | تهيئة بيانات التعريف للتطبيق (مطلوب، واحد لكل تطبيق) |
| `defineObject()` | تعريف كائنات مخصصة مع حقول |
| `defineLogicFunction()` | تعريف وظائف منطقية مع معالجات |
| `definePreInstallLogicFunction()` | تعريف دالة منطقية لما قبل التثبيت (واحدة لكل تطبيق) |
| `definePostInstallLogicFunction()` | تعريف دالة منطقية لما بعد التثبيت (واحدة لكل تطبيق) |
| `defineFrontComponent()` | عرِّف مكوّنات أمامية لواجهة مستخدم مخصّصة |
| `defineRole()` | تهيئة صلاحيات الدور والوصول إلى الكائنات |
| `defineField()` | وسّع الكائنات الموجودة بحقول إضافية |
| `defineView()` | تعريف العروض المحفوظة للكائنات |
| `defineNavigationMenuItem()` | تعريف روابط التنقل في الشريط الجانبي |
| `defineSkill()` | عرّف مهارات وكيل الذكاء الاصطناعي |
تتحقق هذه الدوال من تكوينك وقت البناء وتوفّر إكمالًا تلقائيًا في بيئة التطوير وأمان الأنواع.
@@ -319,6 +327,7 @@ export default defineObject({
* **هوية التطبيق**: المعرفات، اسم العرض، والوصف.
* **كيفية تشغيل وظائفه**: الدور الذي تستخدمه للأذونات.
* **متغيرات (اختياري)**: أزواج مفتاح-قيمة تُعرض لوظائفك كمتغيرات بيئة.
* **(اختياري) دالة ما قبل التثبيت**: دالة منطقية تعمل قبل تثبيت التطبيق.
* **(Optional) post-install function**: a logic function that runs after the app is installed.
Use `defineApplication()` to define your application configuration:
@@ -327,23 +336,21 @@ Use `defineApplication()` to define your application configuration:
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
displayName: 'My Twenty App',
description: 'My first Twenty app',
displayName: 'تطبيق Twenty الخاص بي',
description: 'أول تطبيق لي لـ Twenty',
icon: 'IconWorld',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
description: 'Default recipient name for postcards',
description: 'الاسم الافتراضي للمستلم للبطاقات البريدية',
value: 'Jane Doe',
isSecret: false,
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -352,7 +359,7 @@ export default defineApplication({
* حقول `universalIdentifier` هي معرّفات حتمية تخصك؛ أنشئها مرة واحدة واحتفظ بها ثابتة عبر عمليات المزامنة.
* `applicationVariables` تصبح متغيرات بيئة لوظائفك (على سبيل المثال، `DEFAULT_RECIPIENT_NAME` متاح كـ `process.env.DEFAULT_RECIPIENT_NAME`).
* `defaultRoleUniversalIdentifier` يجب أن يطابق ملف الدور (انظر أدناه).
* `postInstallLogicFunctionUniversalIdentifier` (optional) points to a logic function that runs automatically after the app is installed. See [Post-install functions](#post-install-functions).
* يتم اكتشاف دوال ما قبل التثبيت وما بعد التثبيت تلقائيًا أثناء إنشاء ملف البيان. راجع [دوال ما قبل التثبيت](#pre-install-functions) و[دوال ما بعد التثبيت](#post-install-functions).
#### الأدوار والصلاحيات
@@ -491,6 +498,44 @@ export default defineLogicFunction({
* المصفوفة `triggers` اختيارية. يمكن استخدام الوظائف بدون مشغلات كوظائف مساعدة تُستدعى بواسطة وظائف أخرى.
* يمكنك مزج أنواع متعددة من المشغلات في وظيفة واحدة.
### دوال ما قبل التثبيت
دالة ما قبل التثبيت هي دالة منطقية تعمل تلقائيًا قبل تثبيت تطبيقك على مساحة عمل. يفيد ذلك في مهام التحقق، وفحص المتطلبات المسبقة، أو تجهيز حالة مساحة العمل قبل متابعة التثبيت الرئيسي.
عند إنشاء هيكل تطبيق جديد باستخدام `create-twenty-app`، يتم إنشاء دالة ما قبل التثبيت لك في `src/logic-functions/pre-install.ts`:
```typescript
// src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: '<generated-uuid>',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
handler,
});
```
يمكنك أيضًا تنفيذ دالة ما قبل التثبيت يدويًا في أي وقت باستخدام CLI:
```bash filename="Terminal"
yarn twenty function:execute --preInstall
```
النقاط الرئيسية:
* تستخدم دوال ما قبل التثبيت `definePreInstallLogicFunction()` — وهو إصدار متخصص يستبعد إعدادات المُشغِّل (`cronTriggerSettings` و`databaseEventTriggerSettings` و`httpRouteTriggerSettings` و`isTool`).
* يتلقى المُعالج `InstallLogicFunctionPayload` يحوي `{ previousVersion: string }` — إصدار التطبيق الذي كان مُثبّتًا سابقًا (أو سلسلة فارغة للتثبيتات الجديدة).
* يُسمح بدالة ما قبل التثبيت واحدة فقط لكل تطبيق. سيُنتج إنشاء ملف البيان خطأً إذا تم اكتشاف أكثر من واحدة.
* يتم تعيين `universalIdentifier` للدالة تلقائيًا كـ `preInstallLogicFunctionUniversalIdentifier` في بيان التطبيق أثناء الإنشاء — لست بحاجة إلى الإشارة إليه في `defineApplication()`.
* تم ضبط المهلة الافتراضية على 300 ثانية (5 دقائق) للسماح بمهام التحضير الأطول.
* لا تحتاج دوال ما قبل التثبيت إلى مُشغِّلات — إذ يستدعيها النظام الأساسي قبل التثبيت أو يدويًا عبر `function:execute --preInstall`.
### Post-install functions
A post-install function is a logic function that runs automatically after your app is installed on a workspace. This is useful for one-time setup tasks such as seeding default data, creating initial records, or configuring workspace settings.
@@ -499,16 +544,14 @@ A post-install function is a logic function that runs automatically after your a
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
export default definePostInstallLogicFunction({
universalIdentifier: '<generated-uuid>',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
@@ -516,17 +559,6 @@ export default defineLogicFunction({
});
```
يتم ربط الدالة بتطبيقك من خلال الإشارة إلى المعرِّف العالمي الخاص بها في `application-config.ts`:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
يمكنك أيضًا تنفيذ دالة ما بعد التثبيت يدويًا في أي وقت باستخدام CLI:
```bash filename="Terminal"
@@ -535,8 +567,10 @@ yarn twenty function:execute --postInstall
النقاط الرئيسية:
* دوال ما بعد التثبيت هي دوال منطقية قياسية — فهي تستخدم `defineLogicFunction()` مثل أي دالة أخرى.
* حقل `postInstallLogicFunctionUniversalIdentifier` في `defineApplication()` اختياري. إذا تم تجاهله، لن يتم تشغيل أي دالة بعد التثبيت.
* تستخدم دوال ما بعد التثبيت `definePostInstallLogicFunction()` — وهو إصدار متخصص يستبعد إعدادات المُشغِّل (`cronTriggerSettings` و`databaseEventTriggerSettings` و`httpRouteTriggerSettings` و`isTool`).
* يتلقى المُعالج `InstallLogicFunctionPayload` يحوي `{ previousVersion: string }` — إصدار التطبيق الذي كان مُثبّتًا سابقًا (أو سلسلة فارغة للتثبيتات الجديدة).
* يُسمح بدالة ما بعد التثبيت واحدة فقط لكل تطبيق. سيُنتج إنشاء ملف البيان خطأً إذا تم اكتشاف أكثر من واحدة.
* يتم تعيين `universalIdentifier` للدالة تلقائيًا كـ `postInstallLogicFunctionUniversalIdentifier` في بيان التطبيق أثناء الإنشاء — لست بحاجة إلى الإشارة إليه في `defineApplication()`.
* تم تعيين مهلة افتراضية إلى 300 ثانية (5 دقائق) للسماح بمهام الإعداد الأطول مثل تهيئة البيانات.
* لا تحتاج دوال ما بعد التثبيت إلى مُشغِّلات — حيث يستدعيها النظام الأساسي أثناء التثبيت أو يدويًا عبر `function:execute --postInstall`.
@@ -734,7 +768,7 @@ export default defineFrontComponent({
يمكنك إنشاء مكوّنات أمامية جديدة بطريقتين:
* **مُنشأ بالقالب**: شغّل `yarn twenty entity:add` واختر خيار إضافة مكوّن أمامي جديد.
* **Manual**: Create a new `.tsx` file and use `defineFrontComponent()`, following the same pattern.
* **يدوي**: أنشئ ملفًا جديدًا `.tsx` واستخدم `defineFrontComponent()` مع اتباع النمط نفسه.
### المهارات
@@ -776,7 +810,7 @@ export default defineSkill({
يتم توليد عميلين مضبوطي الأنواع تلقائيًا بواسطة `yarn twenty app:dev` وتخزينهما في `node_modules/twenty-sdk/generated` استنادًا إلى مخطط مساحة العمل لديك:
* **`CoreApiClient`** — يُجري استعلامات إلى نقطة النهاية `/graphql` للحصول على بيانات مساحة العمل
* **`MetadataApiClient`** — queries the `/metadata` endpoint for workspace configuration and file uploads
* **`MetadataApiClient`** — يستعلم عن نقطة النهاية `/metadata` لتكوين مساحة العمل وتحميل الملفات.
```typescript
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';
@@ -788,7 +822,7 @@ const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
```
Both clients are re-generated automatically by `yarn twenty app:dev` whenever your objects or fields change.
يُعاد توليد كلا العميلين تلقائيًا بواسطة `yarn twenty app:dev` كلما تغيّرت كائناتك أو حقولك.
#### بيانات الاعتماد وقت التشغيل في الوظائف المنطقية
@@ -805,7 +839,7 @@ Both clients are re-generated automatically by `yarn twenty app:dev` whenever yo
#### رفع الملفات
The generated `MetadataApiClient` includes an `uploadFile` method for attaching files to file-type fields on your workspace objects. نظرًا لأن عملاء GraphQL القياسيون لا يدعمون تحميل الملفات متعددة الأجزاء افتراضيًا، يوفر العميل هذه الطريقة المخصصة التي تطبق [مواصفة طلب GraphQL متعدد الأجزاء](https://github.com/jaydenseric/graphql-multipart-request-spec) في الخلفية.
يتضمن `MetadataApiClient` المُولَّد طريقة `uploadFile` لإرفاق الملفات بالحقول من نوع ملف ضمن كائنات مساحة العمل الخاصة بك. نظرًا لأن عملاء GraphQL القياسيون لا يدعمون تحميل الملفات متعددة الأجزاء افتراضيًا، يوفر العميل هذه الطريقة المخصصة التي تطبق [مواصفة طلب GraphQL متعدد الأجزاء](https://github.com/jaydenseric/graphql-multipart-request-spec) في الخلفية.
```typescript
import { MetadataApiClient } from 'twenty-sdk/generated';
@@ -846,7 +880,7 @@ uploadFile(
النقاط الرئيسية:
* The `uploadFile` method is available on `MetadataApiClient` because the upload mutation is resolved by the `/metadata` endpoint.
* تتوفر طريقة `uploadFile` على `MetadataApiClient` لأن عملية الـ mutation الخاصة بالرفع تُعالَج عبر نقطة النهاية `/metadata`.
* تستخدم `universalIdentifier` الخاص بالحقل (وليس المعرّف الخاص بمساحة العمل)، ليعمل كود الرفع لديك عبر أي مساحة عمل مُثبَّت فيها تطبيقك — بما يتماشى مع كيفية إشارة التطبيقات إلى الحقول في كل مكان آخر.
* العنوان `url` المُعاد هو عنوان URL موقّع يمكنك استخدامه للوصول إلى الملف المرفوع.
@@ -26,12 +26,12 @@ export const MyComponent = () => {
};
return (
<IconPicker
disabled={false}
onChange={handleIconChange}
selectedIconKey={selectedIcon}
variant="primary"
/>
<IconPicker
disabled={false}
onChange={handleIconChange}
selectedIconKey={selectedIcon}
variant="primary"
/>
);
};
```
@@ -20,16 +20,16 @@ import { Select } from '@/ui/input/components/Select';
export const MyComponent = () => {
return (
<Select
className
disabled={false}
label="Select an option"
options={[
{ value: 'option1', label: 'Option A', Icon: IconTwentyStar },
{ value: 'option2', label: 'Option B', Icon: IconTwentyStar },
]}
value="option1"
/>
<Select
className
disabled={false}
label="Select an option"
options={[
{ value: 'option1', label: 'Option A', Icon: IconTwentyStar },
{ value: 'option2', label: 'Option B', Icon: IconTwentyStar },
]}
value="option1"
/>
);
};
@@ -20,27 +20,27 @@ import { TextInput } from "@/ui/input/components/TextInput";
export const MyComponent = () => {
const handleChange = (text) => {
console.log("تم تغيير الإدخال:", text);
console.log("Input changed:", text);
};
const handleKeyDown = (event) => {
console.log("تم ضغط المفتاح:", event.key);
console.log("Key pressed:", event.key);
};
return (
<TextInput
className
label="اسم المستخدم"
onChange={handleChange}
fullWidth={false}
disableHotkeys={false}
error="اسم مستخدم غير صالح"
onKeyDown={handleKeyDown}
RightIcon={null}
/>
<TextInput
className
label="Username"
onChange={handleChange}
fullWidth={false}
disableHotkeys={false}
error="Invalid username"
onKeyDown={handleKeyDown}
RightIcon={null}
/>
);
};
},{
```
@@ -82,17 +82,17 @@ import { AutosizeTextInput } from "@/ui/input/components/AutosizeTextInput";
export const MyComponent = () => {
return (
<AutosizeTextInput
onValidate={() => console.log("تم تشغيل الدالة onValidate")}
minRows={1}
placeholder="اكتب تعليقًا"
onFocus={() => console.log("تم تشغيل الدالة onFocus")}
variant="icon"
buttonTitle
value="المهمة: "
/>
<AutosizeTextInput
onValidate={() => console.log("onValidate function fired")}
minRows={1}
placeholder="Write a comment"
onFocus={() => console.log("onFocus function fired")}
variant="icon"
buttonTitle
value="Task: "
/>
);
};},{
};
```
@@ -8,7 +8,7 @@ Tento dokument popisuje osvědčené postupy, které byste měli dodržovat při
React a Jotai zajišťují správu stavu v kódu.
### Použijte `useAtomState` k ukládání stavu
### Použijte atomy Jotai k ukládání stavu
Je dobrým zvykem vytvořit tolik atomů, kolik potřebujete ke správě stavu.
@@ -45,7 +45,7 @@ export const MyComponent = () => {
Vyhněte se používání `useRef` k ukládání stavu.
If you want to store state, you should use `useState` or `useAtomState`.
Pokud chcete ukládat stav, měli byste použít `useState` nebo atomy Jotai s `useAtomState`.
Podívejte se, jak spravovat překreslení, pokud máte pocit, že potřebujete `useRef`, abyste zabránili některým překreslením.
@@ -82,8 +82,8 @@ If you feel like you need to add a `useEffect` in your root component, you shoul
Stejný postup můžete aplikovat na logiku získávání dat pomocí Apollo hooks.
```tsx
// ❌ Špatně, způsobí překreslení i když se data nemění,
// protože useEffect je třeba přehodnotit
// ❌ Špatně, způsobí překreslení, i když se data nemění,
// protože useEffect je třeba znovu vyhodnotit
export const PageComponent = () => {
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
@@ -99,12 +99,12 @@ export const PageComponent = () => {
export const App = () => (
<PageComponent />
);},{
);
```
```tsx
// ✅ Dobře, nezpůsobí překreslení, pokud se data nemění,
// protože useEffect je přehodnoceno v další sourozené komponentě
// protože useEffect je znovu vyhodnocen v jiné sourozenecké komponentě
export const PageComponent = () => {
const [data, setData] = useAtomState(dataState);
@@ -132,9 +132,9 @@ export const App = () => (
);
```
### Použijte atom family states a atom family selectors
### Použijte rodiny atomů a selektory
Stavy rodiny třísek a selektory jsou skvělý způsob, jak se vyhnout překreslování.
Rodiny atomů a selektory jsou skvělým způsobem, jak se vyhnout překreslování.
Jsou užitečné, když potřebujete uložit seznam položek.
@@ -84,7 +84,7 @@ Více podrobností naleznete v [Hooks](https://react.dev/learn/reusing-logic-wit
Obsahuje logiku správy stavů. To řeší [Jotai](https://jotai.org).
* Selektory: Odvozené atomy (pomocí `createAtomSelector`) počítají hodnoty z jiných atomů a jsou automaticky memoizovány.
* Selektory: Odvozené atomy (pomocí `createAtomSelector`) odvozují hodnoty z jiných atomů a jsou automaticky memoizovány.
Vestavěná správa stavů v Reactu stále spravuje stav uvnitř komponenty.
@@ -160,7 +160,7 @@ export enum PageHotkeyScope {
}
```
Interně je aktuálně vybraný rozsah uložen v Jotai stavu, který je sdílen napříč aplikací :
Interně je aktuálně vybraný rozsah uložen v Jotai atomu, který je sdílen napříč aplikací :
```tsx
export const currentHotkeyScopeState = createState<HotkeyScope>({
@@ -169,10 +169,10 @@ export const currentHotkeyScopeState = createState<HotkeyScope>({
});
```
Ale tento Jotai stav by neměl být nikdy řízen ručně! Ukážeme si, jak jej používat v příští sekci.
Ale tento atom by se nikdy neměl spravovat ručně ! Ukážeme si, jak jej používat v příští sekci.
## Jak to funguje interně?
Vytvořili jsme tenkou vrstvu nad [react-hotkeys-hook](https://react-hotkeys-hook.vercel.app/docs/intro), která je výkonnější a vyhýbá se zbytečným překreslením.
Také jsme vytvořili Jotai stav, abychom mohli řídit stav rozsahu klávesových zkratek a učinit jej dostupným kdekoli v aplikaci.
Také vytváříme Jotai atom, abychom mohli řídit stav rozsahu klávesových zkratek a učinit jej dostupným kdekoli v aplikaci.
@@ -61,6 +61,9 @@ yarn twenty function:logs
# Spusťte funkci podle názvu
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Spusťte předinstalační funkci
yarn twenty function:execute --preInstall
# Spusťte postinstalační funkci
yarn twenty function:execute --postInstall
@@ -80,7 +83,7 @@ Když spustíte `npx create-twenty-app@latest my-twenty-app`, scaffolder:
* Zkopíruje minimální základní aplikaci do `my-twenty-app/`
* Přidá lokální závislost `twenty-sdk` a konfiguraci pro Yarn 4
* Vytvoří konfigurační soubory a skripty napojené na `twenty` CLI
* Vygeneruje základní soubory (konfigurace aplikace, výchozí role funkcí, postinstalační funkce) a k nim ukázkové soubory podle zvoleného režimu generování kostry
* Vygeneruje základní soubory (konfigurace aplikace, výchozí role funkcí, předinstalační a postinstalační funkce) a k nim ukázkové soubory podle zvoleného režimu generování kostry
Čerstvě vygenerovaná aplikace s výchozím režimem `--exhaustive` vypadá takto:
@@ -107,6 +110,7 @@ my-twenty-app/
│ └── example-field.ts # Ukázková samostatná definice pole
├── logic-functions/
│ ├── hello-world.ts # Ukázková logická funkce
│ ├── pre-install.ts # Předinstalační logická funkce
│ └── post-install.ts # Postinstalační logická funkce
├── front-components/
│ └── hello-world.tsx # Ukázková front-endová komponenta
@@ -118,7 +122,7 @@ my-twenty-app/
└── example-skill.ts # Ukázková definice dovednosti agenta AI
```
S volbou `--minimal` se vytvoří pouze základní soubory (`application-config.ts`, `roles/default-role.ts` a `logic-functions/post-install.ts`). S volbou `--interactive` si vyberete, které ukázkové soubory chcete zahrnout.
S volbou `--minimal` se vytvoří pouze základní soubory (`application-config.ts`, `roles/default-role.ts`, `logic-functions/pre-install.ts` a `logic-functions/post-install.ts`). S volbou `--interactive` si vyberete, které ukázkové soubory chcete zahrnout.
V kostce:
@@ -135,16 +139,18 @@ V kostce:
SDK detekuje entity analýzou vašich souborů TypeScript a hledá volání **`export default define<Entity>({...})`**. Každý typ entity má odpovídající pomocnou funkci exportovanou z `twenty-sdk`:
| Pomocná funkce | Typ entity |
| ---------------------------- | ------------------------------------- |
| `defineObject()` | Definice vlastních objektů |
| `defineLogicFunction()` | Definice logických funkcí |
| `defineFrontComponent()` | Definice frontendových komponent |
| `defineRole()` | Definice rolí |
| `defineField()` | Rozšíření polí u existujících objektů |
| `defineView()` | Definice uložených zobrazení |
| `defineNavigationMenuItem()` | Definice položek navigační nabídky |
| `defineSkill()` | Definice dovedností agenta AI |
| Pomocná funkce | Typ entity |
| ---------------------------------- | --------------------------------------------------------- |
| `defineObject()` | Definice vlastních objektů |
| `defineLogicFunction()` | Definice logických funkcí |
| `definePreInstallLogicFunction()` | Předinstalační logická funkce (spouští se před instalací) |
| `definePostInstallLogicFunction()` | Postinstalační logická funkce (spouští se po instalaci) |
| `defineFrontComponent()` | Definice frontendových komponent |
| `defineRole()` | Definice rolí |
| `defineField()` | Rozšíření polí u existujících objektů |
| `defineView()` | Definice uložených zobrazení |
| `defineNavigationMenuItem()` | Definice položek navigační nabídky |
| `defineSkill()` | Definice dovedností agenta AI |
<Note>
**Pojmenování souborů je flexibilní.** Detekce entit je založená na AST — SDK prochází vaše zdrojové soubory a hledá vzor `export default define<Entity>({...})`. Soubory a složky můžete organizovat, jak chcete. Seskupování podle typu entity (např. `logic-functions/`, `roles/`) je pouze konvence pro organizaci kódu, nikoli požadavek.
@@ -165,7 +171,7 @@ export default defineObject({
Pozdější příkazy přidají další soubory a složky:
* `yarn twenty app:dev` will auto-generate two typed API clients in `node_modules/twenty-sdk/generated`: `CoreApiClient` (for workspace data via `/graphql`) and `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`).
* `yarn twenty app:dev` automaticky vygeneruje dva typované API klienty v `node_modules/twenty-sdk/generated`: `CoreApiClient` (pro data pracovního prostoru přes `/graphql`) a `MetadataApiClient` (pro konfiguraci pracovního prostoru a nahrávání souborů přes `/metadata`).
* `yarn twenty entity:add` přidá soubory s definicemi entit do `src/` pro vaše vlastní objekty, funkce, frontové komponenty, role, dovednosti a další.
## Ověření
@@ -209,17 +215,19 @@ twenty-sdk poskytuje typované stavební bloky a pomocné funkce, které použí
SDK poskytuje pomocné funkce pro definování entit vaší aplikace. Jak je popsáno v [Detekce entit](#entity-detection), musíte použít `export default define<Entity>({...})`, aby byly vaše entity detekovány:
| Funkce | Účel |
| ---------------------------- | ----------------------------------------------------------------- |
| `defineApplication()` | Nakonfigurujte metadata aplikace (povinné, jedno na aplikaci) |
| `defineObject()` | Definice vlastních objektů s poli |
| `defineLogicFunction()` | Definice logických funkcí s obslužnými funkcemi |
| `defineFrontComponent()` | Definujte frontendové komponenty pro vlastní uživatelské rozhraní |
| `defineRole()` | Konfigurace oprávnění rolí a přístupu k objektům |
| `defineField()` | Rozšiřte existující objekty o další pole |
| `defineView()` | Definujte uložená zobrazepro objekty |
| `defineNavigationMenuItem()` | Definujte odkazy postranní navigace |
| `defineSkill()` | Definuje dovednosti agenta AI |
| Funkce | Účel |
| ---------------------------------- | ----------------------------------------------------------------- |
| `defineApplication()` | Nakonfigurujte metadata aplikace (povinné, jedno na aplikaci) |
| `defineObject()` | Definice vlastních objektů s poli |
| `defineLogicFunction()` | Definice logických funkcí s obslužnými funkcemi |
| `definePreInstallLogicFunction()` | Definujte předinstalační logickou funkci (jedna na aplikaci) |
| `definePostInstallLogicFunction()` | Definujte postinstalační logickou funkci (jedna na aplikaci) |
| `defineFrontComponent()` | Definujte frontendové komponenty pro vlastní uživatelské rozhraní |
| `defineRole()` | Konfigurace oprávnění rolí a přístupu k objektům |
| `defineField()` | Rozšiřte existující objekty o další pole |
| `defineView()` | Definujte uložená zobrazení pro objekty |
| `defineNavigationMenuItem()` | Definujte odkazy postranní navigace |
| `defineSkill()` | Definuje dovednosti agenta AI |
Tyto funkce validují vaši konfiguraci v době sestavení a poskytují automatické doplňování v IDE a typovou bezpečnost.
@@ -319,6 +327,7 @@ Každá aplikace má jeden soubor `application-config.ts`, který popisuje:
* **Identitu aplikace**: identifikátory, zobrazovaný název a popis.
* **Jak běží její funkce**: kterou roli používají pro oprávnění.
* **(Volitelné) proměnné**: dvojice klíč–hodnota zpřístupněné vašim funkcím jako proměnné prostředí.
* **(Volitelná) předinstalační funkce**: logická funkce, která se spouští před instalací aplikace.
* **(Volitelná) postinstalační funkce**: logická funkce, která se spouští po instalaci aplikace.
Use `defineApplication()` to define your application configuration:
@@ -327,7 +336,6 @@ Use `defineApplication()` to define your application configuration:
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -343,7 +351,6 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -352,7 +359,7 @@ Poznámky:
* Pole `universalIdentifier` jsou deterministická ID, která vlastníte; vygenerujte je jednou a udržujte je stabilní napříč synchronizacemi.
* `applicationVariables` se stanou proměnnými prostředí pro vaše funkce (například `DEFAULT_RECIPIENT_NAME` je dostupné jako `process.env.DEFAULT_RECIPIENT_NAME`).
* `defaultRoleUniversalIdentifier` se musí shodovat se souborem role (viz níže).
* `postInstallLogicFunctionUniversalIdentifier` (volitelné) odkazuje na logickou funkci, která se automaticky spustí po instalaci aplikace. Viz [Postinstalační funkce](#post-install-functions).
* Předinstalační a postinstalační funkce jsou při sestavování manifestu automaticky detekovány. Viz [Předinstalační funkce](#pre-install-functions) a [Postinstalační funkce](#post-install-functions).
#### Role a oprávnění
@@ -491,6 +498,44 @@ Poznámky:
* Pole `triggers` je volitelné. Funkce bez spouštěčů lze použít jako pomocné funkce volané jinými funkcemi.
* V jedné funkci můžete kombinovat více typů spouštěčů.
### Předinstalační funkce
Předinstalační funkce je logická funkce, která se automaticky spouští před instalací vaší aplikace v pracovním prostoru. To je užitečné pro validační úlohy, kontrolu předpokladů nebo přípravu stavu pracovního prostoru před zahájením hlavní instalace.
Když vygenerujete kostru nové aplikace pomocí `create-twenty-app`, vytvoří se pro vás předinstalační funkce v `src/logic-functions/pre-install.ts`:
```typescript
// src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: '<generated-uuid>',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
handler,
});
```
Předinstalační funkci můžete také kdykoli spustit ručně pomocí CLI:
```bash filename="Terminal"
yarn twenty function:execute --preInstall
```
Hlavní body:
* Předinstalační funkce používají `definePreInstallLogicFunction()` — specializovanou variantu, která vynechává nastavení spouštěčů (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
* Obslužná funkce (handler) obdrží `InstallLogicFunctionPayload` s `{ previousVersion: string }` — verzi aplikace, která byla dříve nainstalována (nebo prázdný řetězec při čisté instalaci).
* Na jednu aplikaci je povolena pouze jedna předinstalační funkce. Sestavení manifestu skončí chybou, pokud je zjištěna více než jedna.
* Identifikátor `universalIdentifier` funkce se během sestavení automaticky nastaví v manifestu aplikace jako `preInstallLogicFunctionUniversalIdentifier` — není potřeba jej uvádět v `defineApplication()`.
* Výchozí časový limit je nastaven na 300 sekund (5 minut), aby umožnil delší přípravné úlohy.
* Předinstalační funkce nepotřebují spouštěče — platforma je vyvolává před instalací nebo je lze spustit ručně pomocí `function:execute --preInstall`.
### Postinstalační funkce
Postinstalační funkce je logická funkce, která se automaticky spouští po instalaci vaší aplikace do pracovního prostoru. To je užitečné pro jednorázové úlohy nastavení, jako je naplnění výchozími daty, vytvoření počátečních záznamů nebo konfigurace nastavení pracovního prostoru.
@@ -499,16 +544,14 @@ Když vygenerujete kostru nové aplikace pomocí `create-twenty-app`, vytvoří
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
export default definePostInstallLogicFunction({
universalIdentifier: '<generated-uuid>',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
@@ -516,17 +559,6 @@ export default defineLogicFunction({
});
```
Funkce je připojena do vaší aplikace odkazem na její univerzální identifikátor v `application-config.ts`:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
Postinstalační funkci můžete také kdykoli spustit ručně pomocí CLI:
```bash filename="Terminal"
@@ -535,8 +567,10 @@ yarn twenty function:execute --postInstall
Hlavní body:
* Postinstalační funkce jsou standardní logické funkce — používají `defineLogicFunction()` stejně jako jakákoli jiná funkce.
* Pole `postInstallLogicFunctionUniversalIdentifier` v `defineApplication()` je volitelné. Pokud je vynecháno, po instalaci se nespustí žádná funkce.
* Postinstalační funkce používají `definePostInstallLogicFunction()` — specializovanou variantu, která vynechává nastavení spouštěčů (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
* Obslužná funkce (handler) obdrží `InstallLogicFunctionPayload` s `{ previousVersion: string }` — verzi aplikace, která byla dříve nainstalována (nebo prázdný řetězec při čisté instalaci).
* Na jednu aplikaci je povolena pouze jedna postinstalační funkce. Sestavení manifestu skončí chybou, pokud je zjištěna více než jedna.
* Identifikátor `universalIdentifier` funkce se během sestavení automaticky nastaví v manifestu aplikace jako `postInstallLogicFunctionUniversalIdentifier` — není potřeba jej uvádět v `defineApplication()`.
* Výchozí časový limit je nastaven na 300 sekund (5 minut), aby umožnil delší úlohy nastavení, jako je naplnění daty.
* Postinstalační funkce nepotřebují spouštěče — jsou spouštěny platformou během instalace nebo ručně pomocí `function:execute --postInstall`.
@@ -734,7 +768,7 @@ Hlavní body:
Nové frontendové komponenty můžete vytvořit dvěma způsoby:
* **Vygenerované**: Spusťte `yarn twenty entity:add` a zvolte možnost přidat novou frontendovou komponentu.
* **Manual**: Create a new `.tsx` file and use `defineFrontComponent()`, following the same pattern.
* **Ruční**: Vytvořte nový soubor `.tsx` a použijte `defineFrontComponent()`, podle stejného vzoru.
### Dovednosti
@@ -771,12 +805,12 @@ Nové dovednosti můžete vytvářet dvěma způsoby:
* **Vygenerované**: Spusťte `yarn twenty entity:add` a zvolte možnost přidat novou dovednost.
* **Ruční**: Vytvořte nový soubor a použijte `defineSkill()` podle stejného vzoru.
### Generated typed clients
### Generované typované klienty
Two typed clients are auto-generated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated` based on your workspace schema:
Dva typované klienty jsou automaticky generovány pomocí `yarn twenty app:dev` a ukládají se do `node_modules/twenty-sdk/generated` podle schématu vašeho pracovního prostoru:
* **`CoreApiClient`** — queries the `/graphql` endpoint for workspace data
* **`MetadataApiClient`** — queries the `/metadata` endpoint for workspace configuration and file uploads
* **`CoreApiClient`** — provádí dotazy na endpoint `/graphql` za účelem získání dat pracovního prostoru
* **`MetadataApiClient`** — odesílá dotazy na endpoint `/metadata` pro konfiguraci pracovního prostoru a nahrávání souborů
```typescript
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';
@@ -788,7 +822,7 @@ const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
```
Both clients are re-generated automatically by `yarn twenty app:dev` whenever your objects or fields change.
Oba klienti se automaticky znovu generují pomocí `yarn twenty app:dev` kdykoli se změní vaše objekty nebo pole.
#### Běhové přihlašovací údaje v logických funkcích
@@ -805,7 +839,7 @@ Poznámky:
#### Nahrávání souborů
The generated `MetadataApiClient` includes an `uploadFile` method for attaching files to file-type fields on your workspace objects. Protože standardní klienti GraphQL nativně nepodporují nahrávání souborů pomocí multipart, klient poskytuje tuto speciální metodu, která interně implementuje [specifikaci multipart požadavků GraphQL](https://github.com/jaydenseric/graphql-multipart-request-spec).
Vygenerovaný `MetadataApiClient` obsahuje metodu `uploadFile` pro připojování souborů k polím typu souboru u objektů ve vašem pracovním prostoru. Protože standardní klienti GraphQL nativně nepodporují nahrávání souborů pomocí multipart, klient poskytuje tuto speciální metodu, která interně implementuje [specifikaci multipart požadavků GraphQL](https://github.com/jaydenseric/graphql-multipart-request-spec).
```typescript
import { MetadataApiClient } from 'twenty-sdk/generated';
@@ -846,7 +880,7 @@ uploadFile(
Hlavní body:
* The `uploadFile` method is available on `MetadataApiClient` because the upload mutation is resolved by the `/metadata` endpoint.
* Metoda `uploadFile` je k dispozici v `MetadataApiClient`, protože mutaci nahrávání obsluhuje endpoint `/metadata`.
* Používá `universalIdentifier` pole (nikoli jeho ID specifické pro pracovní prostor), takže váš kód pro nahrávání funguje ve všech pracovních prostorech, kde je vaše aplikace nainstalována — v souladu s tím, jak aplikace odkazují na pole všude jinde.
* Vrácená hodnota `url` je podepsaná adresa URL, kterou můžete použít k přístupu k nahranému souboru.
@@ -26,12 +26,12 @@ export const MyComponent = () => {
};
return (
<IconPicker
disabled={false}
onChange={handleIconChange}
selectedIconKey={selectedIcon}
variant="primary"
/>
<IconPicker
disabled={false}
onChange={handleIconChange}
selectedIconKey={selectedIcon}
variant="primary"
/>
);
};
```
@@ -20,16 +20,16 @@ import { Select } from '@/ui/input/components/Select';
export const MyComponent = () => {
return (
<Select
className
disabled={false}
label="Vyberte možnost"
options={[
{ value: 'option1', label: 'Možnost A', Icon: IconTwentyStar },
{ value: 'option2', label: 'Možnost B', Icon: IconTwentyStar },
]}
value="option1"
/>
<Select
className
disabled={false}
label="Vyberte možnost"
options={[
{ value: 'option1', label: 'Možnost A', Icon: IconTwentyStar },
{ value: 'option2', label: 'Možnost B', Icon: IconTwentyStar },
]}
value="option1"
/>
);
};
@@ -20,27 +20,27 @@ import { TextInput } from "@/ui/input/components/TextInput";
export const MyComponent = () => {
const handleChange = (text) => {
console.log("Input changed:", text);
console.log("Změněn vstup:", text);
};
const handleKeyDown = (event) => {
console.log("Key pressed:", event.key);
console.log("Stisknutá klávesa:", event.key);
};
return (
<TextInput
className
label="Username"
onChange={handleChange}
fullWidth={false}
disableHotkeys={false}
error="Invalid username"
onKeyDown={handleKeyDown}
RightIcon={null}
/>
<TextInput
className
label="Uživatelské jméno"
onChange={handleChange}
fullWidth={false}
disableHotkeys={false}
error="Neplatné uživatelské jméno"
onKeyDown={handleKeyDown}
RightIcon={null}
/>
);
};
},{
```
@@ -82,15 +82,15 @@ import { AutosizeTextInput } from "@/ui/input/components/AutosizeTextInput";
export const MyComponent = () => {
return (
<AutosizeTextInput
onValidate={() => console.log("onValidate function fired")}
minRows={1}
placeholder="Write a comment"
onFocus={() => console.log("onFocus function fired")}
variant="icon"
buttonTitle
value="Task: "
/>
<AutosizeTextInput
onValidate={() => console.log("Funkce onValidate spuštěna")}
minRows={1}
placeholder="Napište komentář"
onFocus={() => console.log("Funkce onFocus spuštěna")}
variant="icon"
buttonTitle
value="Úkol: "
/>
);
};
```
@@ -8,7 +8,7 @@ Dieses Dokument beschreibt die besten Praktiken, die Sie beim Arbeiten am Fronte
React und Jotai übernehmen die Zustandsverwaltung im Code.
### Verwenden Sie `useAtomState`, um den Zustand zu speichern.
### Verwenden Sie Jotai-Atome, um den Zustand zu speichern
Es ist eine gute Praxis, so viele Atome zu erstellen, wie Sie benötigen, um Ihren Zustand zu speichern.
@@ -45,7 +45,7 @@ export const MyComponent = () => {
Vermeiden Sie die Verwendung von `useRef`, um den Zustand zu speichern.
If you want to store state, you should use `useState` or `useAtomState`.
Wenn Sie den Zustand speichern möchten, sollten Sie `useState` oder Jotai-Atome mit `useAtomState` verwenden.
Sehen Sie sich [an, wie Re-Renderings verwaltet werden können](#managing-re-renders), falls Sie das Gefühl haben, dass Sie `useRef` benötigen, um einige Re-Renderings zu verhindern.
@@ -82,8 +82,8 @@ If you feel like you need to add a `useEffect` in your root component, you shoul
Dasselbe können Sie auch für die Datenabruflogik mit Apollo-Hooks anwenden.
```tsx
// ❌ Bad, will cause re-renders even if data is not changing,
// because useEffect needs to be re-evaluated
// ❌ Schlecht, verursacht Re-Renders, auch wenn sich die Daten nicht ändern,
// weil useEffect neu ausgewertet werden muss
export const PageComponent = () => {
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
@@ -103,8 +103,8 @@ export const App = () => (
```
```tsx
// ✅ Good, will not cause re-renders if data is not changing,
// because useEffect is re-evaluated in another sibling component
// ✅ Gut, verursacht keine Re-Renders, wenn sich die Daten nicht ändern,
// weil useEffect in einer anderen Geschwisterkomponente neu ausgewertet wird
export const PageComponent = () => {
const [data, setData] = useAtomState(dataState);
@@ -132,9 +132,9 @@ export const App = () => (
);
```
### Verwenden Sie Jotai-Familienzustände und Jotai-Familienselektoren
### Verwenden Sie Atom-Familienzustände und Selektoren
Jotai-Familienzustände und -Selektoren sind eine großartige Möglichkeit, Re-Renders zu vermeiden.
Atom-Familienzustände und Selektoren sind eine großartige Möglichkeit, Re-Renders zu vermeiden.
Sie sind nützlich, wenn Sie eine Liste von Elementen speichern müssen.
@@ -160,7 +160,7 @@ export enum PageHotkeyScope {
}
```
Intern wird der aktuell ausgewählte Bereich in einem Jotai-State gespeichert, der in der gesamten Anwendung geteilt wird:
Intern wird der aktuell ausgewählte Bereich in einem Jotai-Atom gespeichert, das in der gesamten Anwendung geteilt wird:
```tsx
export const currentHotkeyScopeState = createState<HotkeyScope>({
@@ -169,10 +169,10 @@ export const currentHotkeyScopeState = createState<HotkeyScope>({
});
```
Aber dieser Jotai-State sollte niemals manuell bearbeitet werden! Wir werden im nächsten Abschnitt sehen, wie man es verwendet.
Aber dieses Atom sollte niemals manuell bearbeitet werden! Wir werden im nächsten Abschnitt sehen, wie man es verwendet.
## Wie funktioniert es intern?
Wir haben eine dünne Schicht über [react-hotkeys-hook](https://react-hotkeys-hook.vercel.app/docs/intro) erstellt, die es leistungsfähiger macht und unnötige Neu-Renderings vermeidet.
Wir haben auch einen Jotai-State erstellt, um den Tastenkombinationsbereich zu verwalten und in der gesamten Anwendung verfügbar zu machen.
Wir erstellen außerdem ein Jotai-Atom, um den Zustand des Tastenkombinationsbereichs zu verwalten und in der gesamten Anwendung verfügbar zu machen.
@@ -61,6 +61,9 @@ yarn twenty function:logs
# Eine Funktion anhand ihres Namens ausführen
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Die Pre-Installationsfunktion ausführen
yarn twenty function:execute --preInstall
# Die Post-Installationsfunktion ausführen
yarn twenty function:execute --postInstall
@@ -68,7 +71,7 @@ yarn twenty function:execute --postInstall
yarn twenty app:uninstall
# Hilfe zu Befehlen anzeigen
yarn twenty help
yarn twenty help},{
```
Siehe auch: die CLI-Referenzseiten für [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) und [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
@@ -80,7 +83,7 @@ Wenn Sie `npx create-twenty-app@latest my-twenty-app` ausführen, erledigt der S
* Kopiert eine minimale Basisanwendung nach `my-twenty-app/`
* Fügt eine lokale `twenty-sdk`-Abhängigkeit und die Yarn-4-Konfiguration hinzu
* Erstellt Konfigurationsdateien und Skripte, die an die `twenty`-CLI angebunden sind
* Erzeugt Kerndateien (Anwendungskonfiguration, Standardrolle für Logikfunktionen, Post-Installationsfunktion) sowie Beispieldateien entsprechend dem Scaffolding-Modus
* Erzeugt Kerndateien (Anwendungskonfiguration, Standardrolle für Logikfunktionen, Pre-Installations- und Post-Installationsfunktionen) sowie Beispieldateien entsprechend dem Scaffolding-Modus
Eine frisch erstellte App mit dem Standardmodus `--exhaustive` sieht so aus:
@@ -107,6 +110,7 @@ my-twenty-app/
│ └── example-field.ts # Beispiel für eine eigenständige Felddefinition
├── logic-functions/
│ ├── hello-world.ts # Beispiel für eine Logikfunktion
│ ├── pre-install.ts # Pre-Installations-Logikfunktion
│ └── post-install.ts # Post-Installations-Logikfunktion
├── front-components/
│ └── hello-world.tsx # Beispiel für eine Frontend-Komponente
@@ -118,7 +122,7 @@ my-twenty-app/
└── example-skill.ts # Beispiel für eine Skill-Definition eines KI-Agenten
```
Mit `--minimal` werden nur die Kerndateien erstellt (`application-config.ts`, `roles/default-role.ts` und `logic-functions/post-install.ts`). Mit `--interactive` wählst du aus, welche Beispieldateien enthalten sein sollen.
Mit `--minimal` werden nur die Kerndateien erstellt (`application-config.ts`, `roles/default-role.ts`, `logic-functions/pre-install.ts` und `logic-functions/post-install.ts`). Mit `--interactive` wählst du aus, welche Beispieldateien enthalten sein sollen.
Auf hoher Ebene:
@@ -135,16 +139,18 @@ Auf hoher Ebene:
Das SDK erkennt Entitäten, indem es Ihre TypeScript-Dateien nach Aufrufen von **`export default define<Entity>({...})`** parst. Für jeden Entitätstyp gibt es eine entsprechende Hilfsfunktion, die aus `twenty-sdk` exportiert wird:
| Hilfsfunktion | Entitätstyp |
| ---------------------------- | ----------------------------------------- |
| `defineObject()` | Benutzerdefinierte Objektdefinitionen |
| `defineLogicFunction()` | Definitionen von Logikfunktionen |
| `defineFrontComponent()` | Definitionen von Frontend-Komponenten |
| `defineRole()` | Rollendefinitionen |
| `defineField()` | Felderweiterungen für bestehende Objekte |
| `defineView()` | Gespeicherte View-Definitionen |
| `defineNavigationMenuItem()` | Definitionen von Navigationsmenüeinträgen |
| `defineSkill()` | Skill-Definitionen für KI-Agenten |
| Hilfsfunktion | Entitätstyp |
| ---------------------------------- | ------------------------------------------------------------------------ |
| `defineObject()` | Benutzerdefinierte Objektdefinitionen |
| `defineLogicFunction()` | Definitionen von Logikfunktionen |
| `definePreInstallLogicFunction()` | Pre-Installations-Logikfunktion (wird vor der Installation ausgeführt) |
| `definePostInstallLogicFunction()` | Post-Installations-Logikfunktion (wird nach der Installation ausgeführt) |
| `defineFrontComponent()` | Definitionen von Frontend-Komponenten |
| `defineRole()` | Rollendefinitionen |
| `defineField()` | Felderweiterungen für bestehende Objekte |
| `defineView()` | Gespeicherte View-Definitionen |
| `defineNavigationMenuItem()` | Definitionen von Navigationsmenüeinträgen |
| `defineSkill()` | Skill-Definitionen für KI-Agenten |
<Note>
**Dateibenennung ist flexibel.** Die Entitätserkennung ist AST-basiert — das SDK durchsucht Ihre Quelldateien nach dem Muster `export default define<Entity>({...})`. Sie können Ihre Dateien und Ordner nach Belieben organisieren. Die Gruppierung nach Entitätstyp (z. B. `logic-functions/`, `roles/`) ist lediglich eine Konvention zur Codeorganisation, keine Voraussetzung.
@@ -165,7 +171,7 @@ export default defineObject({
Spätere Befehle fügen weitere Dateien und Ordner hinzu:
* `yarn twenty app:dev` will auto-generate two typed API clients in `node_modules/twenty-sdk/generated`: `CoreApiClient` (for workspace data via `/graphql`) and `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`).
* `yarn twenty app:dev` generiert automatisch zwei typisierte API-Clients in `node_modules/twenty-sdk/generated`: `CoreApiClient` (für Arbeitsbereichsdaten über `/graphql`) und `MetadataApiClient` (für Arbeitsbereichskonfiguration und Datei-Uploads über `/metadata`).
* `yarn twenty entity:add` fügt unter `src/` Entitätsdefinitionsdateien für Ihre benutzerdefinierten Objekte, Funktionen, Frontend-Komponenten, Rollen, Skills und mehr hinzu.
## Authentifizierung
@@ -209,17 +215,19 @@ Das twenty-sdk stellt typisierte Bausteine und Hilfsfunktionen bereit, die Sie i
Das SDK stellt Hilfsfunktionen bereit, um die Entitäten Ihrer App zu definieren. Wie in [Entitätserkennung](#entity-detection) beschrieben, müssen Sie `export default define<Entity>({...})` verwenden, damit Ihre Entitäten erkannt werden:
| Funktion | Zweck |
| ---------------------------- | -------------------------------------------------------------- |
| `defineApplication()` | Anwendungsmetadaten konfigurieren (erforderlich, eine pro App) |
| `defineObject()` | Benutzerdefinierte Objekte mit Feldern definieren |
| `defineLogicFunction()` | Logikfunktionen mit Handlern definieren |
| `defineFrontComponent()` | Frontend-Komponenten für benutzerdefinierte UI definieren |
| `defineRole()` | Rollenberechtigungen und Objektzugriff konfigurieren |
| `defineField()` | Bestehende Objekte mit zusätzlichen Feldern erweitern |
| `defineView()` | Gespeicherte Views für Objekte definieren |
| `defineNavigationMenuItem()` | Seitenleisten-Navigationslinks definieren |
| `defineSkill()` | Definieren Sie Skills für KI-Agenten |
| Funktion | Zweck |
| ---------------------------------- | --------------------------------------------------------------- |
| `defineApplication()` | Anwendungsmetadaten konfigurieren (erforderlich, eine pro App) |
| `defineObject()` | Benutzerdefinierte Objekte mit Feldern definieren |
| `defineLogicFunction()` | Logikfunktionen mit Handlern definieren |
| `definePreInstallLogicFunction()` | Eine Pre-Installations-Logikfunktion definieren (eine pro App) |
| `definePostInstallLogicFunction()` | Eine Post-Installations-Logikfunktion definieren (eine pro App) |
| `defineFrontComponent()` | Frontend-Komponenten für benutzerdefinierte UI definieren |
| `defineRole()` | Rollenberechtigungen und Objektzugriff konfigurieren |
| `defineField()` | Bestehende Objekte mit zusätzlichen Feldern erweitern |
| `defineView()` | Gespeicherte Views für Objekte definieren |
| `defineNavigationMenuItem()` | Seitenleisten-Navigationslinks definieren |
| `defineSkill()` | Definieren Sie Skills für KI-Agenten |
Diese Funktionen validieren Ihre Konfiguration zur Build-Zeit und bieten IDE-Autovervollständigung sowie Typsicherheit.
@@ -319,6 +327,7 @@ Jede App hat eine einzelne Datei `application-config.ts`, die Folgendes beschrei
* **Was die App ist**: Bezeichner, Anzeigename und Beschreibung.
* **Wie ihre Funktionen ausgeführt werden**: welche Rolle sie für Berechtigungen verwenden.
* **(Optional) Variablen**: SchlüsselWert-Paare, die Ihren Funktionen als Umgebungsvariablen zur Verfügung gestellt werden.
* **(Optional) Pre-Installationsfunktion**: eine Logikfunktion, die vor der Installation der App ausgeführt wird.
* **(Optional) Post-Installationsfunktion**: eine Logikfunktion, die nach der Installation der App ausgeführt wird.
Verwenden Sie `defineApplication()`, um Ihre Anwendungskonfiguration zu definieren:
@@ -327,7 +336,6 @@ Verwenden Sie `defineApplication()`, um Ihre Anwendungskonfiguration zu definier
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -343,7 +351,6 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -352,7 +359,7 @@ Notizen:
* `universalIdentifier`-Felder sind deterministische IDs, die Sie besitzen; generieren Sie sie einmal und halten Sie sie über Synchronisierungen hinweg stabil.
* `applicationVariables` werden zu Umgebungsvariablen für Ihre Funktionen (zum Beispiel ist `DEFAULT_RECIPIENT_NAME` als `process.env.DEFAULT_RECIPIENT_NAME` verfügbar).
* `defaultRoleUniversalIdentifier` muss mit der Rollendatei übereinstimmen (siehe unten).
* `postInstallLogicFunctionUniversalIdentifier` (optional) verweist auf eine Logikfunktion, die nach der Installation der App automatisch ausgeführt wird. Siehe [Post-Installationsfunktionen](#post-install-functions).
* Pre-Installations- und Post-Installationsfunktionen werden während des Manifest-Builds automatisch erkannt. Siehe [Pre-Installationsfunktionen](#pre-install-functions) und [Post-Installationsfunktionen](#post-install-functions).
#### Rollen und Berechtigungen
@@ -491,6 +498,44 @@ Notizen:
* Das Array `triggers` ist optional. Funktionen ohne Trigger können als von anderen Funktionen aufgerufene Utility-Funktionen verwendet werden.
* Sie können mehrere Trigger-Typen in einer Funktion kombinieren.
### Pre-Installationsfunktionen
Eine Pre-Installationsfunktion ist eine Logikfunktion, die automatisch ausgeführt wird, bevor deine App in einem Arbeitsbereich installiert wird. Dies ist nützlich für Validierungsaufgaben, Überprüfungen von Voraussetzungen oder die Vorbereitung des Status des Arbeitsbereichs, bevor die Hauptinstallation fortgesetzt wird.
Wenn du mit `create-twenty-app` eine neue App erstellst, wird für dich eine Pre-Installationsfunktion unter `src/logic-functions/pre-install.ts` erzeugt:
```typescript
// src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: '<generated-uuid>',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
handler,
});
```
Du kannst die Pre-Installationsfunktion auch jederzeit manuell über die CLI ausführen:
```bash filename="Terminal"
yarn twenty function:execute --preInstall
```
Hauptpunkte:
* Pre-Installationsfunktionen verwenden `definePreInstallLogicFunction()` — eine spezialisierte Variante, die Trigger-Einstellungen (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`) weglässt.
* Der Handler erhält ein `InstallLogicFunctionPayload` mit `{ previousVersion: string }` — die Version der App, die zuvor installiert war (oder eine leere Zeichenkette bei Neuinstallationen).
* Pro Anwendung ist nur eine Pre-Installationsfunktion zulässig. Der Manifest-Build schlägt fehl, wenn mehr als eine erkannt wird.
* Der `universalIdentifier` der Funktion wird während des Builds im Anwendungsmanifest automatisch als `preInstallLogicFunctionUniversalIdentifier` gesetzt — du musst ihn nicht in `defineApplication()` referenzieren.
* Das standardmäßige Timeout ist auf 300 Sekunden (5 Minuten) festgelegt, um längere Vorbereitungsvorgänge zu ermöglichen.
* Pre-Installationsfunktionen benötigen keine Trigger — sie werden von der Plattform vor der Installation oder manuell über `function:execute --preInstall` aufgerufen.
### Post-Installationsfunktionen
Eine Post-Installationsfunktion ist eine Logikfunktion, die automatisch ausgeführt wird, nachdem Ihre App in einem Arbeitsbereich installiert wurde. Dies ist nützlich für einmalige Einrichtungsvorgänge wie das Befüllen mit Standarddaten, das Erstellen erster Datensätze oder das Konfigurieren von Arbeitsbereichseinstellungen.
@@ -499,16 +544,14 @@ Wenn du mit `create-twenty-app` eine neue App erstellst, wird für dich eine Pos
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
export default definePostInstallLogicFunction({
universalIdentifier: '<generated-uuid>',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
@@ -516,17 +559,6 @@ export default defineLogicFunction({
});
```
Die Funktion wird in deine App eingebunden, indem ihr universeller Bezeichner in `application-config.ts` referenziert wird:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
Du kannst die Post-Installationsfunktion auch jederzeit manuell über die CLI ausführen:
```bash filename="Terminal"
@@ -535,8 +567,10 @@ yarn twenty function:execute --postInstall
Hauptpunkte:
* Post-Installationsfunktionen sind Standard-Logikfunktionen — sie verwenden `defineLogicFunction()` wie jede andere Funktion.
* Das Feld `postInstallLogicFunctionUniversalIdentifier` in `defineApplication()` ist optional. Wenn es weggelassen wird, wird nach der Installation keine Funktion ausgeführt.
* Post-Installationsfunktionen verwenden `definePostInstallLogicFunction()` — eine spezialisierte Variante, die Trigger-Einstellungen (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`) weglässt.
* Der Handler erhält ein `InstallLogicFunctionPayload` mit `{ previousVersion: string }` — die Version der App, die zuvor installiert war (oder eine leere Zeichenkette bei Neuinstallationen).
* Pro Anwendung ist nur eine Post-Installationsfunktion zulässig. Der Manifest-Build schlägt fehl, wenn mehr als eine erkannt wird.
* Der `universalIdentifier` der Funktion wird während des Builds im Anwendungsmanifest automatisch als `postInstallLogicFunctionUniversalIdentifier` gesetzt — du musst ihn nicht in `defineApplication()` referenzieren.
* Das standardmäßige Timeout ist auf 300 Sekunden (5 Minuten) festgelegt, um längere Einrichtungsvorgänge wie Daten-Seeding zu ermöglichen.
* Post-Installationsfunktionen benötigen keine Trigger — sie werden von der Plattform während der Installation oder manuell über `function:execute --postInstall` aufgerufen.
@@ -734,7 +768,7 @@ Hauptpunkte:
Sie können neue Frontend-Komponenten auf zwei Arten erstellen:
* **Generiert**: Führen Sie `yarn twenty entity:add` aus und wählen Sie die Option zum Hinzufügen einer neuen Frontend-Komponente.
* **Manual**: Create a new `.tsx` file and use `defineFrontComponent()`, following the same pattern.
* **Manuell**: Erstellen Sie eine neue `.tsx`-Datei und verwenden Sie `defineFrontComponent()` nach demselben Muster.
### Fähigkeiten
@@ -771,12 +805,12 @@ Sie können neue Skills auf zwei Arten erstellen:
* **Generiert**: Führen Sie `yarn twenty entity:add` aus und wählen Sie die Option zum Hinzufügen eines neuen Skills.
* **Manuell**: Erstellen Sie eine neue Datei und verwenden Sie `defineSkill()` nach demselben Muster.
### Generated typed clients
### Generierte typisierte Clients
Two typed clients are auto-generated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated` based on your workspace schema:
Zwei typisierte Clients werden von `yarn twenty app:dev` automatisch generiert und basierend auf Ihrem Arbeitsbereichs-Schema in `node_modules/twenty-sdk/generated` gespeichert:
* **`CoreApiClient`** — queries the `/graphql` endpoint for workspace data
* **`MetadataApiClient`** — queries the `/metadata` endpoint for workspace configuration and file uploads
* **`CoreApiClient`** — fragt den `/graphql`-Endpunkt nach Arbeitsbereichsdaten ab
* **`MetadataApiClient`** — ruft über den Endpunkt `/metadata` die Arbeitsbereichskonfiguration und Datei-Uploads ab.
```typescript
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';
@@ -788,7 +822,7 @@ const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
```
Both clients are re-generated automatically by `yarn twenty app:dev` whenever your objects or fields change.
Beide Clients werden von `yarn twenty app:dev` automatisch neu generiert, sobald sich Ihre Objekte oder Felder ändern.
#### Laufzeit-Anmeldedaten in Logikfunktionen
@@ -805,7 +839,7 @@ Notizen:
#### Dateien hochladen
The generated `MetadataApiClient` includes an `uploadFile` method for attaching files to file-type fields on your workspace objects. Da Standard-GraphQL-Clients Multipart-Datei-Uploads nicht nativ unterstützen, stellt der Client diese dedizierte Methode bereit, die unter der Haube die [GraphQL-Multipart-Anfragespezifikation](https://github.com/jaydenseric/graphql-multipart-request-spec) implementiert.
Der generierte `MetadataApiClient` enthält eine Methode `uploadFile`, um Dateien an Felder des Typs Datei in Ihren Arbeitsbereichsobjekten anzuhängen. Da Standard-GraphQL-Clients Multipart-Datei-Uploads nicht nativ unterstützen, stellt der Client diese dedizierte Methode bereit, die unter der Haube die [GraphQL-Multipart-Anfragespezifikation](https://github.com/jaydenseric/graphql-multipart-request-spec) implementiert.
```typescript
import { MetadataApiClient } from 'twenty-sdk/generated';
@@ -846,7 +880,7 @@ uploadFile(
Hauptpunkte:
* The `uploadFile` method is available on `MetadataApiClient` because the upload mutation is resolved by the `/metadata` endpoint.
* Die Methode `uploadFile` ist auf dem `MetadataApiClient` verfügbar, weil die Upload-Mutation vom Endpunkt `/metadata` aufgelöst wird.
* Sie verwendet den `universalIdentifier` des Feldes (nicht dessen arbeitsbereichsspezifische ID), sodass Ihr Upload-Code in jedem Arbeitsbereich funktioniert, in dem Ihre App installiert ist — im Einklang damit, wie Apps Felder überall sonst referenzieren.
* Die zurückgegebene `url` ist eine signierte URL, mit der Sie auf die hochgeladene Datei zugreifen können.
File diff suppressed because one or more lines are too long
@@ -20,16 +20,16 @@ import { Select } from '@/ui/input/components/Select';
export const MyComponent = () => {
return (
<Select
className
disabled={false}
label="Select an option"
options={[
{ value: 'option1', label: 'Option A', Icon: IconTwentyStar },
{ value: 'option2', label: 'Option B', Icon: IconTwentyStar },
]}
value="option1"
/>
<Select
className
disabled={false}
label="Option auswählen"
options={[
{ value: 'option1', label: 'Option A', Icon: IconTwentyStar },
{ value: 'option2', label: 'Option B', Icon: IconTwentyStar },
]}
value="option1"
/>
);
};
@@ -20,27 +20,27 @@ import { TextInput } from "@/ui/input/components/TextInput";
export const MyComponent = () => {
const handleChange = (text) => {
console.log("Input changed:", text);
console.log("Eingabe geändert:", text);
};
const handleKeyDown = (event) => {
console.log("Key pressed:", event.key);
console.log("Taste gedrückt:", event.key);
};
return (
<TextInput
className
label="Username"
onChange={handleChange}
fullWidth={false}
disableHotkeys={false}
error="Invalid username"
onKeyDown={handleKeyDown}
RightIcon={null}
/>
<TextInput
className
label="Benutzername"
onChange={handleChange}
fullWidth={false}
disableHotkeys={false}
error="Ungültiger Benutzername"
onKeyDown={handleKeyDown}
RightIcon={null}
/>
);
};
},{
```
@@ -82,15 +82,15 @@ import { AutosizeTextInput } from "@/ui/input/components/AutosizeTextInput";
export const MyComponent = () => {
return (
<AutosizeTextInput
onValidate={() => console.log("onValidate function fired")}
minRows={1}
placeholder="Write a comment"
onFocus={() => console.log("onFocus function fired")}
variant="icon"
buttonTitle
value="Task: "
/>
<AutosizeTextInput
onValidate={() => console.log("onValidate-Funktion ausgelöst")}
minRows={1}
placeholder="Kommentar schreiben"
onFocus={() => console.log("onFocus-Funktion ausgelöst")}
variant="icon"
buttonTitle
value="Aufgabe: "
/>
);
};
```
@@ -6,9 +6,9 @@ Questo documento descrive le migliori pratiche da seguire quando si lavora sul f
## Gestione dello Stato
React e Jotai gestiscono la gestione dello stato nella base di codice.
React e Jotai gestiscono lo stato nella base di codice.
### Usa `useAtomState` per memorizzare lo stato
### Usa gli atomi di Jotai per memorizzare lo stato
È buona pratica creare tanti atomi quanti servono per memorizzare il tuo stato.
@@ -45,7 +45,7 @@ export const MyComponent = () => {
Evita di usare `useRef` per memorizzare lo stato.
Se vuoi memorizzare lo stato, dovresti usare `useState` o `useAtomState`.
Se vuoi memorizzare lo stato, dovresti usare `useState` o gli atomi di Jotai con `useAtomState`.
Consulta [come gestire i re-render](#managing-re-renders) se senti che hai bisogno di `useRef` per evitare alcuni re-render.
@@ -132,9 +132,9 @@ export const App = () => (
);
```
### Usa stati di famiglia jotai e selettori di famiglia jotai
### Usa gli stati e i selettori della famiglia di atomi
Gli stati e i selettori di famiglia Jotai sono un ottimo modo per evitare re-render.
Gli stati e i selettori della famiglia di atomi sono un ottimo modo per evitare re-render.
Sono utili quando hai bisogno di memorizzare una lista di elementi.
@@ -84,7 +84,7 @@ Vedi [Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks) per ulteri
Contiene la logica di gestione degli stati. [Jotai](https://jotai.org) se ne occupa.
* Selettori: Gli atomi derivati (tramite `createAtomSelector`) calcolano valori da altri atomi e sono automaticamente memorizzati.
* Selettori: Gli atomi derivati (utilizzando `createAtomSelector`) calcolano valori da altri atomi e vengono memoizzati automaticamente.
La gestione degli stati integrata di React si occupa ancora dello stato all'interno di un componente.
@@ -160,7 +160,7 @@ export enum PageHotkeyScope {
}
```
Internamente, l'ambito selezionato attualmente viene memorizzato in uno stato Jotai condiviso in tutta l'applicazione:
Internamente, l'ambito selezionato attualmente viene memorizzato in un atomo Jotai condiviso in tutta l'applicazione:
```tsx
export const currentHotkeyScopeState = createState<HotkeyScope>({
@@ -169,10 +169,10 @@ export const currentHotkeyScopeState = createState<HotkeyScope>({
});
```
Ma questo stato Jotai non dovrebbe mai essere gestito manualmente! Vedremo come usarlo nella sezione successiva.
Ma questo atomo non dovrebbe mai essere gestito manualmente! Vedremo come usarlo nella sezione successiva.
## Come funziona internamente?
Abbiamo creato un sottile wrapper su [react-hotkeys-hook](https://react-hotkeys-hook.vercel.app/docs/intro) che lo rende più performante ed evita rendering non necessari.
Abbiamo anche creato uno stato Jotai per gestire lo stato dell'ambito del tasto di scelta rapida e renderlo disponibile ovunque nell'applicazione.
Creiamo anche un atomo Jotai per gestire lo stato dell'ambito del tasto di scelta rapida e renderlo disponibile ovunque nell'applicazione.
@@ -61,6 +61,9 @@ yarn twenty function:logs
# Esegui una funzione per nome
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Esegui la funzione di pre-installazione
yarn twenty function:execute --preInstall
# Esegui la funzione post-installazione
yarn twenty function:execute --postInstall
@@ -80,7 +83,7 @@ Quando esegui `npx create-twenty-app@latest my-twenty-app`, lo scaffolder:
* Copia un'applicazione base minimale in `my-twenty-app/`
* Aggiunge una dipendenza locale `twenty-sdk` e la configurazione di Yarn 4
* Crea file di configurazione e script collegati alla CLI `twenty`
* Genera i file principali (configurazione dell'applicazione, ruolo predefinito per le funzioni logiche, funzione di post-installazione) più i file di esempio in base alla modalità di scaffolding
* Genera i file principali (configurazione dell'applicazione, ruolo predefinito per le funzioni logiche, funzioni di pre-installazione e post-installazione) più i file di esempio in base alla modalità di scaffolding
Un'app appena creata con la modalità predefinita `--exhaustive` si presenta così:
@@ -96,29 +99,30 @@ my-twenty-app/
eslint.config.mjs
tsconfig.json
README.md
public/ # Public assets folder (images, fonts, etc.)
public/ # Cartella delle risorse pubbliche (immagini, font, ecc.)
src/
├── application-config.ts # Required - main application configuration
├── application-config.ts # Obbligatorio - configurazione principale dell'applicazione
├── roles/
│ └── default-role.ts # Default role for logic functions
│ └── default-role.ts # Ruolo predefinito per le funzioni logiche
├── objects/
│ └── example-object.ts # Example custom object definition
│ └── example-object.ts # Definizione di oggetto personalizzato di esempio
├── fields/
│ └── example-field.ts # Example standalone field definition
│ └── example-field.ts # Definizione di campo autonomo di esempio
├── logic-functions/
│ ├── hello-world.ts # Example logic function
── post-install.ts # Post-install logic function
│ ├── hello-world.ts # Funzione logica di esempio
── pre-install.ts # Funzione logica di pre-installazione
│ └── post-install.ts # Funzione logica di post-installazione
├── front-components/
│ └── hello-world.tsx # Example front component
│ └── hello-world.tsx # Componente front-end di esempio
├── views/
│ └── example-view.ts # Example saved view definition
│ └── example-view.ts # Definizione di vista salvata di esempio
├── navigation-menu-items/
│ └── example-navigation-menu-item.ts # Example sidebar navigation link
│ └── example-navigation-menu-item.ts # Link di navigazione della barra laterale di esempio
└── skills/
└── example-skill.ts # Example AI agent skill definition
└── example-skill.ts # Definizione di skill per agente IA di esempio
```
Con `--minimal`, vengono creati solo i file principali (`application-config.ts`, `roles/default-role.ts` e `logic-functions/post-install.ts`). Con `--interactive`, scegli quali file di esempio includere.
Con `--minimal`, vengono creati solo i file principali (`application-config.ts`, `roles/default-role.ts`, `logic-functions/pre-install.ts` e `logic-functions/post-install.ts`). Con `--interactive`, scegli quali file di esempio includere.
A livello generale:
@@ -135,16 +139,18 @@ A livello generale:
L'SDK rileva le entità analizzando i tuoi file TypeScript alla ricerca di chiamate **`export default define<Entity>({...})`**. Ogni tipo di entità ha una corrispondente funzione helper esportata da `twenty-sdk`:
| Funzione helper | Tipo di entità |
| ---------------------------- | ---------------------------------------------- |
| `defineObject()` | Definizioni di oggetti personalizzati |
| `defineLogicFunction()` | Definizioni di funzioni logiche |
| `defineFrontComponent()` | Definizioni dei componenti front-end |
| `defineRole()` | Definizioni di ruoli |
| `defineField()` | Estensioni di campo per oggetti esistenti |
| `defineView()` | Definizioni di viste salvate |
| `defineNavigationMenuItem()` | Definizioni delle voci del menu di navigazione |
| `defineSkill()` | AI agent skill definitions |
| Funzione helper | Tipo di entità |
| ---------------------------------- | ------------------------------------------------------------------------------ |
| `defineObject()` | Definizioni di oggetti personalizzati |
| `defineLogicFunction()` | Definizioni di funzioni logiche |
| `definePreInstallLogicFunction()` | Funzione logica di pre-installazione (viene eseguita prima dell'installazione) |
| `definePostInstallLogicFunction()` | Funzione logica di post-installazione (viene eseguita dopo l'installazione) |
| `defineFrontComponent()` | Definizioni dei componenti front-end |
| `defineRole()` | Definizioni di ruoli |
| `defineField()` | Estensioni di campo per oggetti esistenti |
| `defineView()` | Definizioni di viste salvate |
| `defineNavigationMenuItem()` | Definizioni delle voci del menu di navigazione |
| `defineSkill()` | AI agent skill definitions |
<Note>
**La denominazione dei file è flessibile.** Il rilevamento delle entità è basato sull'AST — l'SDK esegue la scansione dei file sorgente alla ricerca del pattern `export default define<Entity>({...})`. Puoi organizzare file e cartelle come preferisci. Raggruppare per tipo di entità (ad es., `logic-functions/`, `roles/`) è solo una convenzione per l'organizzazione del codice, non un requisito.
@@ -165,7 +171,7 @@ export default defineObject({
Comandi successivi aggiungeranno altri file e cartelle:
* `yarn twenty app:dev` will auto-generate two typed API clients in `node_modules/twenty-sdk/generated`: `CoreApiClient` (for workspace data via `/graphql`) and `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`).
* `yarn twenty app:dev` genererà automaticamente due client API tipizzati in `node_modules/twenty-sdk/generated`: `CoreApiClient` (per i dati dell'area di lavoro tramite `/graphql`) e `MetadataApiClient` (per la configurazione dell'area di lavoro e il caricamento di file tramite `/metadata`).
* `yarn twenty entity:add` will add entity definition files under `src/` for your custom objects, functions, front components, roles, skills, and more.
## Autenticazione
@@ -209,17 +215,19 @@ Il pacchetto twenty-sdk fornisce blocchi tipizzati e funzioni helper da usare ne
L'SDK fornisce funzioni helper per definire le entità della tua app. Come descritto in [Rilevamento delle entità](#entity-detection), devi usare `export default define<Entity>({...})` affinché le tue entità vengano rilevate:
| Funzione | Scopo |
| ---------------------------- | ----------------------------------------------------------------------- |
| `defineApplication()` | Configura i metadati dell'applicazione (obbligatorio, uno per app) |
| `defineObject()` | Definisci oggetti personalizzati con campi |
| `defineLogicFunction()` | Definisci funzioni logiche con handler |
| `defineFrontComponent()` | Definisci componenti front-end per un'interfaccia utente personalizzata |
| `defineRole()` | Configura i permessi dei ruoli e l'accesso agli oggetti |
| `defineField()` | Estendi gli oggetti esistenti con campi aggiuntivi |
| `defineView()` | Definisce viste salvate per gli oggetti |
| `defineNavigationMenuItem()` | Definisce i link di navigazione della barra laterale |
| `defineSkill()` | Define AI agent skills |
| Funzione | Scopo |
| ---------------------------------- | -------------------------------------------------------------------------- |
| `defineApplication()` | Configura i metadati dell'applicazione (obbligatorio, uno per app) |
| `defineObject()` | Definisci oggetti personalizzati con campi |
| `defineLogicFunction()` | Definisci funzioni logiche con handler |
| `definePreInstallLogicFunction()` | Definisce una funzione logica di pre-installazione (una per applicazione) |
| `definePostInstallLogicFunction()` | Definisce una funzione logica di post-installazione (una per applicazione) |
| `defineFrontComponent()` | Definisci componenti front-end per un'interfaccia utente personalizzata |
| `defineRole()` | Configura i permessi dei ruoli e l'accesso agli oggetti |
| `defineField()` | Estendi gli oggetti esistenti con campi aggiuntivi |
| `defineView()` | Definisce viste salvate per gli oggetti |
| `defineNavigationMenuItem()` | Definisce i link di navigazione della barra laterale |
| `defineSkill()` | Define AI agent skills |
Queste funzioni convalidano la configurazione in fase di build e offrono il completamento automatico nell'IDE e la sicurezza dei tipi.
@@ -319,6 +327,7 @@ Ogni app ha un singolo file `application-config.ts` che descrive:
* **Identità dell'app**: identificatori, nome visualizzato e descrizione.
* **Come vengono eseguite le sue funzioni**: quale ruolo usano per i permessi.
* **Variabili (opzionali)**: coppie chiavevalore esposte alle funzioni come variabili d'ambiente.
* **(Opzionale) funzione di pre-installazione**: una funzione logica che viene eseguita prima che l'app venga installata.
* **(Opzionale) funzione post-installazione**: una funzione logica che viene eseguita dopo l'installazione dell'app.
Usa `defineApplication()` per definire la configurazione della tua applicazione:
@@ -327,7 +336,6 @@ Usa `defineApplication()` per definire la configurazione della tua applicazione:
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -343,7 +351,6 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -352,7 +359,7 @@ Note:
* I campi `universalIdentifier` sono ID deterministici sotto il tuo controllo; generali una volta e mantienili stabili tra le sincronizzazioni.
* `applicationVariables` diventano variabili d'ambiente per le tue funzioni (ad esempio, `DEFAULT_RECIPIENT_NAME` è disponibile come `process.env.DEFAULT_RECIPIENT_NAME`).
* `defaultRoleUniversalIdentifier` deve corrispondere al file del ruolo (vedi sotto).
* `postInstallLogicFunctionUniversalIdentifier` (opzionale) fa riferimento a una funzione logica che viene eseguita automaticamente dopo l'installazione dell'app. Vedi [Funzioni post-installazione](#post-install-functions).
* Le funzioni di pre-installazione e post-installazione vengono rilevate automaticamente durante la build del manifesto. Vedi [Funzioni di pre-installazione](#pre-install-functions) e [Funzioni di post-installazione](#post-install-functions).
#### Ruoli e permessi
@@ -491,6 +498,44 @@ Note:
* L'array `triggers` è facoltativo. Le funzioni senza trigger possono essere utilizzate come funzioni di utilità richiamate da altre funzioni.
* Puoi combinare più tipi di trigger in un'unica funzione.
### Funzioni di pre-installazione
Una funzione di pre-installazione è una funzione logica che viene eseguita automaticamente prima che la tua app venga installata in uno spazio di lavoro. È utile per attività di convalida, controlli dei prerequisiti o per preparare lo stato dello spazio di lavoro prima che proceda l'installazione principale.
Quando esegui lo scaffolding di una nuova app con `create-twenty-app`, viene generata una funzione di pre-installazione in `src/logic-functions/pre-install.ts`:
```typescript
// src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: '<generated-uuid>',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
handler,
});
```
Puoi anche eseguire manualmente la funzione di pre-installazione in qualsiasi momento utilizzando la CLI:
```bash filename="Terminal"
yarn twenty function:execute --preInstall
```
Punti chiave:
* Le funzioni di pre-installazione utilizzano `definePreInstallLogicFunction()` — una variante specializzata che omette le impostazioni dei trigger (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
* L'handler riceve un `InstallLogicFunctionPayload` con `{ previousVersion: string }` — la versione dell'app precedentemente installata (oppure una stringa vuota per nuove installazioni).
* È consentita una sola funzione di pre-installazione per applicazione. La build del manifesto genererà un errore se ne viene rilevata più di una.
* L'`universalIdentifier` della funzione viene impostato automaticamente come `preInstallLogicFunctionUniversalIdentifier` nel manifesto dell'applicazione durante la build — non è necessario farvi riferimento in `defineApplication()`.
* Il timeout predefinito è impostato a 300 secondi (5 minuti) per consentire attività di preparazione più lunghe.
* Le funzioni di pre-installazione non necessitano di trigger — vengono invocate dalla piattaforma prima dell'installazione o manualmente tramite `function:execute --preInstall`.
### Funzioni post-installazione
Una funzione post-installazione è una funzione logica che viene eseguita automaticamente dopo che la tua app è stata installata in uno spazio di lavoro. Questo è utile per attività di configurazione una tantum come il popolamento di dati predefiniti, la creazione di record iniziali o la configurazione delle impostazioni dello spazio di lavoro.
@@ -499,16 +544,14 @@ Quando esegui lo scaffolding di una nuova app con `create-twenty-app`, viene gen
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
export default definePostInstallLogicFunction({
universalIdentifier: '<generated-uuid>',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
@@ -516,17 +559,6 @@ export default defineLogicFunction({
});
```
La funzione viene collegata alla tua app facendo riferimento al suo identificatore universale in `application-config.ts`:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
Puoi anche eseguire manualmente la funzione di post-installazione in qualsiasi momento utilizzando la CLI:
```bash filename="Terminal"
@@ -535,8 +567,10 @@ yarn twenty function:execute --postInstall
Punti chiave:
* Le funzioni di post-installazione sono funzioni logiche standard — usano `defineLogicFunction()` come qualsiasi altra funzione.
* Il campo `postInstallLogicFunctionUniversalIdentifier` in `defineApplication()` è facoltativo. Se omesso, nessuna funzione viene eseguita dopo l'installazione.
* Le funzioni di post-installazione utilizzano `definePostInstallLogicFunction()` — una variante specializzata che omette le impostazioni dei trigger (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
* L'handler riceve un `InstallLogicFunctionPayload` con `{ previousVersion: string }` — la versione dell'app precedentemente installata (oppure una stringa vuota per nuove installazioni).
* È consentita una sola funzione di post-installazione per applicazione. La build del manifesto genererà un errore se ne viene rilevata più di una.
* L'`universalIdentifier` della funzione viene impostato automaticamente come `postInstallLogicFunctionUniversalIdentifier` nel manifesto dell'applicazione durante la build — non è necessario farvi riferimento in `defineApplication()`.
* Il timeout predefinito è impostato a 300 secondi (5 minuti) per consentire attività di configurazione più lunghe, come il popolamento dei dati.
* Le funzioni di post-installazione non necessitano di trigger — vengono invocate dalla piattaforma durante l'installazione o manualmente tramite `function:execute --postInstall`.
@@ -734,7 +768,7 @@ Punti chiave:
Puoi creare nuovi componenti front-end in due modi:
* **Generata dallo scaffolder**: Esegui `yarn twenty entity:add` e scegli l'opzione per aggiungere un nuovo componente front-end.
* **Manual**: Create a new `.tsx` file and use `defineFrontComponent()`, following the same pattern.
* **Manuale**: Crea un nuovo file `.tsx` e usa `defineFrontComponent()`, seguendo lo stesso schema.
### Abilità
@@ -771,12 +805,12 @@ You can create new skills in two ways:
* **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new skill.
* **Manual**: Create a new file and use `defineSkill()`, following the same pattern.
### Generated typed clients
### Client tipizzati generati
Two typed clients are auto-generated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated` based on your workspace schema:
Due client tipizzati sono generati automaticamente da `yarn twenty app:dev` e salvati in `node_modules/twenty-sdk/generated` in base allo schema della tua area di lavoro:
* **`CoreApiClient`** — queries the `/graphql` endpoint for workspace data
* **`MetadataApiClient`** — queries the `/metadata` endpoint for workspace configuration and file uploads
* **`CoreApiClient`** — interroga l'endpoint `/graphql` per i dati dell'area di lavoro
* **`MetadataApiClient`** — interroga l'endpoint `/metadata` per la configurazione dello spazio di lavoro e il caricamento dei file
```typescript
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';
@@ -788,7 +822,7 @@ const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
```
Both clients are re-generated automatically by `yarn twenty app:dev` whenever your objects or fields change.
Entrambi i client vengono rigenerati automaticamente da `yarn twenty app:dev` ogni volta che i tuoi oggetti o campi cambiano.
#### Credenziali di runtime nelle funzioni logiche
@@ -805,7 +839,7 @@ Note:
#### Caricamento dei file
The generated `MetadataApiClient` includes an `uploadFile` method for attaching files to file-type fields on your workspace objects. Poiché i client GraphQL standard non supportano nativamente il caricamento di file multipart, il client fornisce questo metodo dedicato che implementa la [specifica della richiesta GraphQL multipart](https://github.com/jaydenseric/graphql-multipart-request-spec) dietro le quinte.
Il `MetadataApiClient` generato include un metodo `uploadFile` per allegare file ai campi di tipo file sugli oggetti del tuo spazio di lavoro. Poiché i client GraphQL standard non supportano nativamente il caricamento di file multipart, il client fornisce questo metodo dedicato che implementa la [specifica della richiesta GraphQL multipart](https://github.com/jaydenseric/graphql-multipart-request-spec) dietro le quinte.
```typescript
import { MetadataApiClient } from 'twenty-sdk/generated';
@@ -846,7 +880,7 @@ uploadFile(
Punti chiave:
* The `uploadFile` method is available on `MetadataApiClient` because the upload mutation is resolved by the `/metadata` endpoint.
* Il metodo `uploadFile` è disponibile su `MetadataApiClient` perché la mutazione di upload viene risolta dall'endpoint `/metadata`.
* Usa l'`universalIdentifier` del campo (non il suo ID specifico dello spazio di lavoro), quindi il tuo codice di upload funziona in qualsiasi spazio di lavoro in cui la tua app è installata — coerentemente con il modo in cui le app fanno riferimento ai campi altrove.
* L'`url` restituito è un URL firmato che puoi usare per accedere al file caricato.
@@ -26,12 +26,12 @@ export const MyComponent = () => {
};
return (
<IconPicker
disabled={false}
onChange={handleIconChange}
selectedIconKey={selectedIcon}
variant="primary"
/>
<IconPicker
disabled={false}
onChange={handleIconChange}
selectedIconKey={selectedIcon}
variant="primary"
/>
);
};
```
@@ -20,16 +20,16 @@ import { Select } from '@/ui/input/components/Select';
export const MyComponent = () => {
return (
<Select
className
disabled={false}
label="Seleziona un'opzione"
options={[
{ value: 'option1', label: 'Opzione A', Icon: IconTwentyStar },
{ value: 'option2', label: 'Opzione B', Icon: IconTwentyStar },
]}
value="option1"
/>
<Select
className
disabled={false}
label="Seleziona un'opzione"
options={[
{ value: 'option1', label: 'Opzione A', Icon: IconTwentyStar },
{ value: 'option2', label: 'Opzione B', Icon: IconTwentyStar },
]}
value="option1"
/>
);
};
@@ -28,16 +28,16 @@ export const MyComponent = () => {
};
return (
<TextInput
className
label="Username"
onChange={handleChange}
fullWidth={false}
disableHotkeys={false}
error="Invalid username"
onKeyDown={handleKeyDown}
RightIcon={null}
/>
<TextInput
className
label="Username"
onChange={handleChange}
fullWidth={false}
disableHotkeys={false}
error="Invalid username"
onKeyDown={handleKeyDown}
RightIcon={null}
/>
);
};
@@ -82,15 +82,15 @@ import { AutosizeTextInput } from "@/ui/input/components/AutosizeTextInput";
export const MyComponent = () => {
return (
<AutosizeTextInput
onValidate={() => console.log("onValidate function fired")}
minRows={1}
placeholder="Write a comment"
onFocus={() => console.log("onFocus function fired")}
variant="icon"
buttonTitle
value="Task: "
/>
<AutosizeTextInput
onValidate={() => console.log("onValidate function fired")}
minRows={1}
placeholder="Write a comment"
onFocus={() => console.log("onFocus function fired")}
variant="icon"
buttonTitle
value="Task: "
/>
);
};
```
@@ -8,7 +8,7 @@ Este documento descreve as melhores práticas que você deve seguir ao trabalhar
React e Jotai lidam com o gerenciamento de estado na base de código.
### Use `useAtomState` para armazenar o estado
### Use átomos do Jotai para armazenar o estado
É uma boa prática criar tantos átomos quanto necessário para armazenar seu estado.
@@ -45,7 +45,7 @@ export const MyComponent = () => {
Evite usar `useRef` para armazenar estado.
If you want to store state, you should use `useState` or `useAtomState`.
Se quiser armazenar o estado, você deve usar `useState` ou átomos do Jotai com `useAtomState`.
Veja [como gerenciar re-renderizações](#managing-re-renders) se você sentir que precisa de `useRef` para evitar que algumas re-renderizações aconteçam.
@@ -82,8 +82,8 @@ If you feel like you need to add a `useEffect` in your root component, you shoul
Você pode aplicar o mesmo para lógica de busca de dados, com hooks do Apollo.
```tsx
// ❌ Bad, will cause re-renders even if data is not changing,
// because useEffect needs to be re-evaluated
// ❌ Ruim, causará re-renderizações mesmo se os dados não estiverem mudando,
// porque o useEffect precisa ser reavaliado
export const PageComponent = () => {
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
@@ -103,8 +103,8 @@ export const App = () => (
```
```tsx
// ✅ Good, will not cause re-renders if data is not changing,
// because useEffect is re-evaluated in another sibling component
// ✅ Bom, não causará re-renderizações se os dados não estiverem mudando,
// porque o useEffect é reavaliado em outro componente irmão
export const PageComponent = () => {
const [data, setData] = useAtomState(dataState);
@@ -132,9 +132,9 @@ export const App = () => (
);
```
### Use estados de família Jotai e seletores de família Jotai
### Use famílias de átomos e seletores de família
Estados de família Jotai e seletores são uma ótima maneira de evitar re-renderizações.
Famílias de átomos e seletores são uma ótima maneira de evitar re-renderizações.
Eles são úteis quando você precisa armazenar uma lista de itens.
@@ -84,7 +84,7 @@ Veja [Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks) para mais
Contém a lógica de gerenciamento de estado. [Jotai](https://jotai.org) lida com isso.
* Seletores: Átomos derivados (usando `createAtomSelector`) calculam valores a partir de outros átomos e são automaticamente memorizados.
* Seletores: Átomos derivados (usando `createAtomSelector`) calculam valores a partir de outros átomos e são automaticamente memoizados.
O gerenciamento de estado embutido do React ainda lida com o estado dentro de um componente.
@@ -160,7 +160,7 @@ export enum PageHotkeyScope {
}
```
Internamente, o escopo atualmente selecionado é armazenado em um estado Jotai que é compartilhado por toda a aplicação :
Internamente, o escopo atualmente selecionado é armazenado em um átomo Jotai que é compartilhado por toda a aplicação :
```tsx
export const currentHotkeyScopeState = createState<HotkeyScope>({
@@ -169,10 +169,10 @@ export const currentHotkeyScopeState = createState<HotkeyScope>({
});
```
Mas esse estado Jotai nunca deve ser manipulado manualmente! Veremos como usá-lo na próxima seção.
Mas esse átomo nunca deve ser manipulado manualmente! Veremos como usá-lo na próxima seção.
## Como funciona internamente?
Criamos um wrapper leve em cima de [react-hotkeys-hook](https://react-hotkeys-hook.vercel.app/docs/intro) que o torna mais eficiente e evita renderizações desnecessárias.
Também criamos um estado Jotai para gerenciar o estado do escopo da tecla de atalho e torná-lo disponível em toda a aplicação.
Também criamos um átomo Jotai para gerenciar o estado do escopo da tecla de atalho e torná-lo disponível em toda a aplicação.
@@ -52,22 +52,25 @@ npx create-twenty-app@latest my-app --interactive
A partir daqui você pode:
```bash filename="Terminal"
# Adicionar uma nova entidade à sua aplicação (assistido)
# Add a new entity to your application (guided)
yarn twenty entity:add
# Acompanhar os logs das funções da sua aplicação
# Watch your application's function logs
yarn twenty function:logs
# Executar uma função pelo nome
yarn twenty function:execute -n my-function -p '{\"name\": \"test\"}'
# Execute a function by name
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Executar a função de pós-instalação
# Execute the pre-install function
yarn twenty function:execute --preInstall
# Execute the post-install function
yarn twenty function:execute --postInstall
# Desinstalar a aplicação do espaço de trabalho atual
# Uninstall the application from the current workspace
yarn twenty app:uninstall
# Exibir a ajuda dos comandos
# Display commands' help
yarn twenty help
```
@@ -80,7 +83,7 @@ Ao executar `npx create-twenty-app@latest my-twenty-app`, o gerador:
* Copia um aplicativo base mínimo para `my-twenty-app/`
* Adiciona uma dependência local `twenty-sdk` e a configuração do Yarn 4
* Cria arquivos de configuração e scripts conectados à CLI `twenty`
* Gera arquivos principais (configuração da aplicação, papel padrão para funções de lógica, função de pós-instalação) além de arquivos de exemplo com base no modo de geração de estrutura
* Generates core files (application config, default function role, pre-install and post-install functions) plus example files based on the scaffolding mode
Um app recém-criado com o modo padrão `--exhaustive` fica assim:
@@ -96,29 +99,30 @@ my-twenty-app/
eslint.config.mjs
tsconfig.json
README.md
public/ # Pasta de recursos públicos (imagens, fontes, etc.)
public/ # Public assets folder (images, fonts, etc.)
src/
├── application-config.ts # Obrigatório - configuração principal da aplicação
├── application-config.ts # Required - main application configuration
├── roles/
│ └── default-role.ts # Papel padrão para funções de lógica
│ └── default-role.ts # Default role for logic functions
├── objects/
│ └── example-object.ts # Exemplo de definição de objeto personalizado
│ └── example-object.ts # Example custom object definition
├── fields/
│ └── example-field.ts # Exemplo de definição de campo independente
│ └── example-field.ts # Example standalone field definition
├── logic-functions/
│ ├── hello-world.ts # Exemplo de função de lógica
── post-install.ts # Função de lógica de pós-instalação
│ ├── hello-world.ts # Example logic function
── pre-install.ts # Pre-install logic function
│ └── post-install.ts # Post-install logic function
├── front-components/
│ └── hello-world.tsx # Exemplo de componente de front-end
│ └── hello-world.tsx # Example front component
├── views/
│ └── example-view.ts # Exemplo de definição de visualização salva
│ └── example-view.ts # Example saved view definition
├── navigation-menu-items/
│ └── example-navigation-menu-item.ts # Exemplo de link de navegação da barra lateral
│ └── example-navigation-menu-item.ts # Example sidebar navigation link
└── skills/
└── example-skill.ts # Exemplo de definição de habilidade de agente de IA
└── example-skill.ts # Example AI agent skill definition
```
Com `--minimal`, apenas os arquivos principais são criados (`application-config.ts`, `roles/default-role.ts` e `logic-functions/post-install.ts`). Com `--interactive`, você escolhe quais arquivos de exemplo incluir.
With `--minimal`, only the core files are created (`application-config.ts`, `roles/default-role.ts`, `logic-functions/pre-install.ts`, and `logic-functions/post-install.ts`). Com `--interactive`, você escolhe quais arquivos de exemplo incluir.
Em alto nível:
@@ -135,16 +139,18 @@ Em alto nível:
O SDK detecta entidades analisando seus arquivos TypeScript em busca de chamadas **`export default define<Entity>({...})`**. Cada tipo de entidade tem uma função utilitária correspondente exportada de `twenty-sdk`:
| Função utilitária | Tipo de entidade |
| ---------------------------- | ------------------------------------------- |
| `defineObject()` | Definições de objetos personalizados |
| `defineLogicFunction()` | Definições de funções de lógica |
| `defineFrontComponent()` | Definições de componentes de front-end |
| `defineRole()` | Definições de papéis |
| `defineField()` | Extensões de campos para objetos existentes |
| `defineView()` | Definições de visualizações salvas |
| `defineNavigationMenuItem()` | Definições de itens do menu de navegação |
| `defineSkill()` | Definições de habilidades de agente de IA |
| Função utilitária | Tipo de entidade |
| ---------------------------------- | ----------------------------------------------------- |
| `defineObject()` | Definições de objetos personalizados |
| `defineLogicFunction()` | Definições de funções de lógica |
| `definePreInstallLogicFunction()` | Pre-install logic function (runs before installation) |
| `definePostInstallLogicFunction()` | Post-install logic function (runs after installation) |
| `defineFrontComponent()` | Definições de componentes de front-end |
| `defineRole()` | Definições de papéis |
| `defineField()` | Extensões de campos para objetos existentes |
| `defineView()` | Definições de visualizações salvas |
| `defineNavigationMenuItem()` | Definições de itens do menu de navegação |
| `defineSkill()` | Definições de habilidades de agente de IA |
<Note>
**A nomeação de arquivos é flexível.** A detecção de entidades é baseada em AST — o SDK varre seus arquivos fonte em busca do padrão `export default define<Entity>({...})`. Você pode organizar seus arquivos e pastas como quiser. Agrupar por tipo de entidade (por exemplo, `logic-functions/`, `roles/`) é apenas uma convenção para organização do código, não um requisito.
@@ -165,7 +171,7 @@ export default defineObject({
Comandos posteriores adicionarão mais arquivos e pastas:
* `yarn twenty app:dev` will auto-generate two typed API clients in `node_modules/twenty-sdk/generated`: `CoreApiClient` (for workspace data via `/graphql`) and `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`).
* `yarn twenty app:dev` irá gerar automaticamente dois clientes de API tipados em `node_modules/twenty-sdk/generated`: `CoreApiClient` (para dados do espaço de trabalho via `/graphql`) e `MetadataApiClient` (para configuração do espaço de trabalho e envio de arquivos via `/metadata`).
* `yarn twenty entity:add` adicionará arquivos de definição de entidade em `src/` para seus objetos, funções, componentes de front-end, papéis e habilidades personalizados, entre outros.
## Autenticação
@@ -209,17 +215,19 @@ O twenty-sdk fornece blocos de construção tipados e funções utilitárias que
O SDK fornece funções utilitárias para definir as entidades do seu app. Conforme descrito em [Detecção de entidades](#entity-detection), você deve usar `export default define<Entity>({...})` para que suas entidades sejam detectadas:
| Função | Finalidade |
| ---------------------------- | ------------------------------------------------------------ |
| `defineApplication()` | Configurar metadados do aplicativo (obrigatório, um por app) |
| `defineObject()` | Define objetos personalizados com campos |
| `defineLogicFunction()` | Defina funções de lógica com handlers |
| `defineFrontComponent()` | Definir componentes de front-end para UI personalizada |
| `defineRole()` | Configura permissões de papéis e acesso a objetos |
| `defineField()` | Estender objetos existentes com campos adicionais |
| `defineView()` | Define visualizações salvas para objetos |
| `defineNavigationMenuItem()` | Define links de navegação da barra lateral |
| `defineSkill()` | Define habilidades de agente de IA |
| Função | Finalidade |
| ---------------------------------- | ------------------------------------------------------------ |
| `defineApplication()` | Configurar metadados do aplicativo (obrigatório, um por app) |
| `defineObject()` | Define objetos personalizados com campos |
| `defineLogicFunction()` | Defina funções de lógica com handlers |
| `definePreInstallLogicFunction()` | Define a pre-install logic function (one per app) |
| `definePostInstallLogicFunction()` | Define a post-install logic function (one per app) |
| `defineFrontComponent()` | Definir componentes de front-end para UI personalizada |
| `defineRole()` | Configura permissões de papéis e acesso a objetos |
| `defineField()` | Estender objetos existentes com campos adicionais |
| `defineView()` | Define visualizações salvas para objetos |
| `defineNavigationMenuItem()` | Define links de navegação da barra lateral |
| `defineSkill()` | Define habilidades de agente de IA |
Essas funções validam sua configuração em tempo de compilação e oferecem autocompletar na IDE e segurança de tipos.
@@ -319,6 +327,7 @@ Todo aplicativo tem um único arquivo `application-config.ts` que descreve:
* **O que é o aplicativo**: identificadores, nome de exibição e descrição.
* **Como suas funções são executadas**: qual papel usam para permissões.
* **Variáveis (opcional)**: pares chavevalor expostos às suas funções como variáveis de ambiente.
* **(Optional) pre-install function**: a logic function that runs before the app is installed.
* **(Opcional) função de pós-instalação**: uma função de lógica que é executada após a instalação da aplicação.
Use `defineApplication()` to define your application configuration:
@@ -327,23 +336,21 @@ Use `defineApplication()` to define your application configuration:
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
displayName: 'My Twenty App',
description: 'My first Twenty app',
displayName: 'Meu aplicativo Twenty',
description: 'Meu primeiro aplicativo Twenty',
icon: 'IconWorld',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
description: 'Default recipient name for postcards',
description: 'Nome padrão do destinatário para cartões-postais',
value: 'Jane Doe',
isSecret: false,
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -352,7 +359,7 @@ Notas:
* `universalIdentifier` são IDs determinísticos que você controla; gere-os uma vez e mantenha-os estáveis entre sincronizações.
* `applicationVariables` tornam-se variáveis de ambiente para suas funções (por exemplo, `DEFAULT_RECIPIENT_NAME` fica disponível como `process.env.DEFAULT_RECIPIENT_NAME`).
* `defaultRoleUniversalIdentifier` deve corresponder ao arquivo do papel (veja abaixo).
* `postInstallLogicFunctionUniversalIdentifier` (opcional) aponta para uma função de lógica que é executada automaticamente após a instalação da aplicação. Consulte [Funções de pós-instalação](#post-install-functions).
* Pre-install and post-install functions are automatically detected during the manifest build. See [Pre-install functions](#pre-install-functions) and [Post-install functions](#post-install-functions).
#### Papéis e permissões
@@ -491,6 +498,44 @@ Notas:
* O array `triggers` é opcional. Funções sem gatilhos podem ser usadas como funções utilitárias chamadas por outras funções.
* Você pode misturar vários tipos de gatilho em uma única função.
### Pre-install functions
A pre-install function is a logic function that runs automatically before your app is installed on a workspace. This is useful for validation tasks, prerequisite checks, or preparing workspace state before the main installation proceeds.
When you scaffold a new app with `create-twenty-app`, a pre-install function is generated for you at `src/logic-functions/pre-install.ts`:
```typescript
// src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: '<generated-uuid>',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
handler,
});
```
You can also manually execute the pre-install function at any time using the CLI:
```bash filename="Terminal"
yarn twenty function:execute --preInstall
```
Pontos-chave:
* Pre-install functions use `definePreInstallLogicFunction()` — a specialized variant that omits trigger settings (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
* The handler receives an `InstallLogicFunctionPayload` with `{ previousVersion: string }` — the version of the app that was previously installed (or an empty string for fresh installs).
* Only one pre-install function is allowed per application. The manifest build will error if more than one is detected.
* The function's `universalIdentifier` is automatically set as `preInstallLogicFunctionUniversalIdentifier` on the application manifest during the build — you do not need to reference it in `defineApplication()`.
* The default timeout is set to 300 seconds (5 minutes) to allow for longer preparation tasks.
* Pre-install functions do not need triggers — they are invoked by the platform before installation or manually via `function:execute --preInstall`.
### Funções de pós-instalação
Uma função de pós-instalação é uma função de lógica que é executada automaticamente após a sua aplicação ser instalada em um espaço de trabalho. Isso é útil para tarefas de configuração únicas, como preencher dados padrão, criar registros iniciais ou configurar as configurações do espaço de trabalho.
@@ -499,16 +544,14 @@ Ao criar a estrutura de um novo app com `create-twenty-app`, uma função de pó
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
export default definePostInstallLogicFunction({
universalIdentifier: '<generated-uuid>',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
@@ -516,17 +559,6 @@ export default defineLogicFunction({
});
```
A função é conectada ao seu app referenciando seu identificador universal em `application-config.ts`:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
Você também pode executar manualmente a função de pós-instalação a qualquer momento usando a CLI:
```bash filename="Terminal"
@@ -535,8 +567,10 @@ yarn twenty function:execute --postInstall
Pontos-chave:
* As funções de pós-instalação são funções de lógica padrão — elas usam `defineLogicFunction()` como qualquer outra função.
* O campo `postInstallLogicFunctionUniversalIdentifier` em `defineApplication()` é opcional. Se omitido, nenhuma função é executada após a instalação.
* Post-install functions use `definePostInstallLogicFunction()` — a specialized variant that omits trigger settings (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
* The handler receives an `InstallLogicFunctionPayload` with `{ previousVersion: string }` — the version of the app that was previously installed (or an empty string for fresh installs).
* Only one post-install function is allowed per application. The manifest build will error if more than one is detected.
* The function's `universalIdentifier` is automatically set as `postInstallLogicFunctionUniversalIdentifier` on the application manifest during the build — you do not need to reference it in `defineApplication()`.
* O tempo limite padrão é definido como 300 segundos (5 minutos) para permitir tarefas de configuração mais longas, como o pré-carregamento de dados.
* As funções de pós-instalação não precisam de gatilhos — elas são invocadas pela plataforma durante a instalação ou manualmente via `function:execute --postInstall`.
@@ -723,6 +757,7 @@ export default defineFrontComponent({
description: 'A custom widget component',
component: MyWidget,
});
```
Pontos-chave:
@@ -734,7 +769,7 @@ Pontos-chave:
Você pode criar novos componentes de front-end de duas formas:
* **Gerado automaticamente**: Execute `yarn twenty entity:add` e escolha a opção para adicionar um novo componente de front-end.
* **Manual**: Create a new `.tsx` file and use `defineFrontComponent()`, following the same pattern.
* **Manual**: Crie um novo ficheiro `.tsx` e use `defineFrontComponent()`, seguindo o mesmo padrão.
### Habilidades
@@ -771,12 +806,12 @@ Você pode criar novas habilidades de duas formas:
* **Gerado automaticamente**: Execute `yarn twenty entity:add` e escolha a opção para adicionar uma nova habilidade.
* **Manual**: Crie um novo arquivo e use `defineSkill()`, seguindo o mesmo padrão.
### Generated typed clients
### Clientes tipados gerados
Two typed clients are auto-generated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated` based on your workspace schema:
Dois clientes tipados são gerados automaticamente pelo `yarn twenty app:dev` e armazenados em `node_modules/twenty-sdk/generated` com base no esquema do seu espaço de trabalho:
* **`CoreApiClient`** — queries the `/graphql` endpoint for workspace data
* **`MetadataApiClient`** — queries the `/metadata` endpoint for workspace configuration and file uploads
* **`CoreApiClient`** — consulta o endpoint `/graphql` para dados do espaço de trabalho
* **`MetadataApiClient`** — consulta o endpoint `/metadata` para obter a configuração do espaço de trabalho e o carregamento de ficheiros
```typescript
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';
@@ -786,9 +821,10 @@ const { me } = await client.query({ me: { id: true, displayName: true } });
const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
```
Both clients are re-generated automatically by `yarn twenty app:dev` whenever your objects or fields change.
Ambos os clientes são regenerados automaticamente pelo `yarn twenty app:dev` sempre que seus objetos ou campos forem alterados.
#### Credenciais em tempo de execução em funções de lógica
@@ -805,7 +841,7 @@ Notas:
#### Carregamento de ficheiros
The generated `MetadataApiClient` includes an `uploadFile` method for attaching files to file-type fields on your workspace objects. Como os clientes GraphQL padrão não suportam nativamente o carregamento de ficheiros multipart, o cliente fornece este método dedicado que implementa, nos bastidores, a [especificação de pedidos multipart do GraphQL](https://github.com/jaydenseric/graphql-multipart-request-spec).
O `MetadataApiClient` gerado inclui um método `uploadFile` para anexar ficheiros a campos do tipo ficheiro nos objetos do seu espaço de trabalho. Como os clientes GraphQL padrão não suportam nativamente o carregamento de ficheiros multipart, o cliente fornece este método dedicado que implementa, nos bastidores, a [especificação de pedidos multipart do GraphQL](https://github.com/jaydenseric/graphql-multipart-request-spec).
```typescript
import { MetadataApiClient } from 'twenty-sdk/generated';
@@ -824,6 +860,7 @@ const uploadedFile = await metadataClient.uploadFile(
console.log(uploadedFile);
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
```
A assinatura do método:
@@ -846,7 +883,7 @@ uploadFile(
Pontos-chave:
* The `uploadFile` method is available on `MetadataApiClient` because the upload mutation is resolved by the `/metadata` endpoint.
* O método `uploadFile` está disponível no `MetadataApiClient` porque a mutação de upload é resolvida pelo endpoint `/metadata`.
* Ele usa o `universalIdentifier` do campo (não o ID específico do espaço de trabalho), de modo que seu código de upload funcione em qualquer espaço de trabalho onde seu app esteja instalado — consistente com a forma como os apps referenciam campos em qualquer outro lugar.
* A `url` retornada é um URL assinado que você pode usar para acessar o arquivo enviado.
@@ -17,21 +17,21 @@ Um seletor de ícones baseado em lista suspensa que permite aos usuários seleci
import React, { useState } from "react";
import { IconPicker } from "@/ui/input/components/IconPicker";
export const MyComponent = () => {
export const MeuComponente = () => {
const [selectedIcon, setSelectedIcon] = useState("");
const handleIconChange = ({ iconKey, Icon }) => {
console.log("Selected Icon:", iconKey);
console.log("Ícone Selecionado:", iconKey);
setSelectedIcon(iconKey);
};
return (
<IconPicker
disabled={false}
onChange={handleIconChange}
selectedIconKey={selectedIcon}
variant="primary"
/>
<IconPicker
disabled={false}
onChange={handleIconChange}
selectedIconKey={selectedIcon}
variant="primary"
/>
);
};
```
@@ -20,18 +20,19 @@ import { Select } from '@/ui/input/components/Select';
export const MyComponent = () => {
return (
<Select
className
disabled={false}
label="Selecione uma opção"
options={[
{ value: 'option1', label: 'Opção A', Icon: IconTwentyStar },
{ value: 'option2', label: 'Opção B', Icon: IconTwentyStar },
]}
value="option1"
/>
<Select
className
disabled={false}
label="Selecione uma opção"
options={[
{ value: 'option1', label: 'Opção A', Icon: IconTwentyStar },
{ value: 'option2', label: 'Opção B', Icon: IconTwentyStar },
]}
value="option1"
/>
);
};
```
</Tab>
@@ -20,24 +20,24 @@ import { TextInput } from "@/ui/input/components/TextInput";
export const MyComponent = () => {
const handleChange = (text) => {
console.log("Input changed:", text);
console.log("Entrada alterada:", text);
};
const handleKeyDown = (event) => {
console.log("Key pressed:", event.key);
console.log("Tecla pressionada:", event.key);
};
return (
<TextInput
className
label="Username"
onChange={handleChange}
fullWidth={false}
disableHotkeys={false}
error="Invalid username"
onKeyDown={handleKeyDown}
RightIcon={null}
/>
<TextInput
className
label="Nome de usuário"
onChange={handleChange}
fullWidth={false}
disableHotkeys={false}
error="Nome de usuário inválido"
onKeyDown={handleKeyDown}
RightIcon={null}
/>
);
};
@@ -82,15 +82,15 @@ import { AutosizeTextInput } from "@/ui/input/components/AutosizeTextInput";
export const MyComponent = () => {
return (
<AutosizeTextInput
onValidate={() => console.log("onValidate function fired")}
minRows={1}
placeholder="Write a comment"
onFocus={() => console.log("onFocus function fired")}
variant="icon"
buttonTitle
value="Task: "
/>
<AutosizeTextInput
onValidate={() => console.log("função onValidate executada")}
minRows={1}
placeholder="Escreva um comentário"
onFocus={() => console.log("função onFocus executada")}
variant="icon"
buttonTitle
value="Tarefa: "
/>
);
};
```
@@ -8,7 +8,7 @@ Acest document prezintă cele mai bune practici pe care ar trebui să le urmați
React și Jotai se ocupă de managementul stării în cod.
### Folosiți `useAtomState` pentru a stoca starea
### Folosiți atomi Jotai pentru a stoca starea
Este o bună practică să creezi atâția atomi câți ai nevoie pentru a-ți stoca starea.
@@ -45,7 +45,7 @@ export const MyComponent = () => {
Evitați utilizarea `useRef` pentru a stoca starea.
Dacă doriți să stocați starea, ar trebui să folosiți `useState` sau `useAtomState`.
Dacă doriți să stocați starea, ar trebui să folosiți `useState` sau atomii Jotai cu `useAtomState`.
Vezi [cum să gestionezi re-randările](#managing-re-renders) dacă ți se pare că ai nevoie de `useRef` pentru a preveni apariția unor re-randări.
@@ -132,9 +132,9 @@ export const App = () => (
);
```
### Folosiți stări de familie și selectoare de familie cu jotai
### Folosiți stări de familie de atomi și selectoare
Stările și selectoarele de familie cu jotai sunt o metodă excelentă de a evita re-render-urile.
Stările de familie de atomi și selectoarele sunt o metodă excelentă de a evita re-randările.
Sunt utile când trebuie să stocați o listă de elemente.
@@ -84,7 +84,7 @@ Vezi [Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks) pentru mai
Conține logica de gestionare a stării. [Jotai](https://jotai.org) gestionează aceasta.
* Selectori: Atomii derivați (folosind `createAtomSelector`) calculează valori din alți atomi și sunt memorați automat.
* Selectori: Atomi derivați (folosind `createAtomSelector`) calculează valori din alți atomi și sunt memoizați automat.
Managementul de stare încorporat în React gestionează încă starea în cadrul unei componente.
@@ -160,7 +160,7 @@ export enum PageHotkeyScope {
}
```
Intern, domeniul selectat în prezent este stocat într-o stare Jotai care este partajată în toată aplicația:
Intern, domeniul selectat în prezent este stocat într-un atom Jotai care este partajat în toată aplicația:
```tsx
export const currentHotkeyScopeState = createState<HotkeyScope>({
@@ -169,10 +169,10 @@ export const currentHotkeyScopeState = createState<HotkeyScope>({
});
```
Însă această stare Jotai nu ar trebui să fie gestionată manual! Vom vedea cum să o folosim în secțiunea următoare.
Însă acest atom nu ar trebui niciodată să fie gestionat manual! Vom vedea cum să o folosim în secțiunea următoare.
## Cum funcționează intern?
Am făcut un wrapper subțire peste [react-hotkeys-hook](https://react-hotkeys-hook.vercel.app/docs/intro) care îl face mai performant și evită re-rendările inutile.
De asemenea, creăm o stare Jotai pentru a gestiona starea domeniului comenzilor rapide și să fie disponibilă oriunde în aplicație.
De asemenea, creăm un atom Jotai pentru a gestiona starea domeniului comenzilor rapide și pentru a o face disponibilă oriunde în aplicație.
@@ -61,6 +61,9 @@ yarn twenty function:logs
# Execută o funcție după nume
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Execută funcția de pre-instalare
yarn twenty function:execute --preInstall
# Execută funcția post-instalare
yarn twenty function:execute --postInstall
@@ -68,7 +71,7 @@ yarn twenty function:execute --postInstall
yarn twenty app:uninstall
# Afișează ajutorul pentru comenzi
yarn twenty help
yarn twenty help},{
```
Consultați și: paginile de referință CLI pentru [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) și [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
@@ -80,7 +83,7 @@ Când rulați `npx create-twenty-app@latest my-twenty-app`, generatorul:
* Copiază o aplicație de bază minimală în `my-twenty-app/`
* Adaugă o dependență locală `twenty-sdk` și configurația Yarn 4
* Creează fișiere de configurare și scripturi conectate la CLI-ul `twenty`
* Generează fișierele de bază (configurația aplicației, rolul implicit al funcțiilor, funcția post-instalare) plus fișiere de exemplu în funcție de modul de generare a scheletului
* Generează fișierele de bază (configurația aplicației, rolul implicit al funcțiilor, funcțiile de pre-instalare și post-instalare) plus fișiere de exemplu în funcție de modul de generare a scheletului.
O aplicație proaspăt generată cu modul implicit `--exhaustive` arată astfel:
@@ -107,6 +110,7 @@ my-twenty-app/
│ └── example-field.ts # Exemplu de definiție de câmp independent
├── logic-functions/
│ ├── hello-world.ts # Exemplu de funcție logică
│ ├── pre-install.ts # Funcție logică de pre-instalare
│ └── post-install.ts # Funcție logică post-instalare
├── front-components/
│ └── hello-world.tsx # Exemplu de componentă de interfață
@@ -118,7 +122,7 @@ my-twenty-app/
└── example-skill.ts # Exemplu de definiție a unei abilități a agentului AI
```
Cu `--minimal`, sunt create doar fișierele de bază (`application-config.ts`, `roles/default-role.ts` și `logic-functions/post-install.ts`). Cu `--interactive`, alegi ce fișiere de exemplu să incluzi.
Cu `--minimal`, sunt create doar fișierele de bază (`application-config.ts`, `roles/default-role.ts`, `logic-functions/pre-install.ts` și `logic-functions/post-install.ts`). Cu `--interactive`, alegi ce fișiere de exemplu să incluzi.
Pe scurt:
@@ -135,16 +139,18 @@ Pe scurt:
SDK-ul detectează entitățile analizând fișierele TypeScript pentru apeluri **`export default define<Entity>({...})`**. Fiecare tip de entitate are o funcție ajutătoare corespunzătoare, exportată din `twenty-sdk`:
| Funcție ajutătoare | Tipul entității |
| ---------------------------- | ---------------------------------------------- |
| `defineObject()` | Definiții de obiecte personalizate |
| `defineLogicFunction()` | Definiții de funcții de logică |
| `defineFrontComponent()` | Definiții ale componentelor de interfață |
| `defineRole()` | Definiții de rol |
| `defineField()` | Extensii de câmp pentru obiectele existente |
| `defineView()` | Definiții pentru vizualizări salvate |
| `defineNavigationMenuItem()` | Definiții pentru elemente de meniu de navigare |
| `defineSkill()` | Definiții ale abilităților agentului AI |
| Funcție ajutătoare | Tipul entității |
| ---------------------------------- | -------------------------------------------------------------- |
| `defineObject()` | Definiții de obiecte personalizate |
| `defineLogicFunction()` | Definiții de funcții de logică |
| `definePreInstallLogicFunction()` | Funcție logică de pre-instalare (rulează înainte de instalare) |
| `definePostInstallLogicFunction()` | Funcție logică post-instalare (rulează după instalare) |
| `defineFrontComponent()` | Definiții ale componentelor de interfață |
| `defineRole()` | Definiții de rol |
| `defineField()` | Extensii de câmp pentru obiectele existente |
| `defineView()` | Definiții pentru vizualizări salvate |
| `defineNavigationMenuItem()` | Definiții pentru elemente de meniu de navigare |
| `defineSkill()` | Definiții ale abilităților agentului AI |
<Note>
**Denumirea fișierelor este flexibilă.** Detectarea entităților se bazează pe AST — SDK-ul scanează fișierele sursă pentru tiparul `export default define<Entity>({...})`. Puteți organiza fișierele și folderele cum doriți. Gruparea după tipul de entitate (de exemplu, `logic-functions/`, `roles/`) este doar o convenție pentru organizarea codului, nu o cerință.
@@ -209,17 +215,19 @@ Biblioteca twenty-sdk oferă blocuri de bază tipizate și funcții ajutătoare
SDK-ul oferă funcții ajutătoare pentru definirea entităților aplicației. După cum este descris în [Detectarea entităților](#entity-detection), trebuie să folosiți `export default define<Entity>({...})` pentru ca entitățile să fie detectate:
| Funcție | Scop |
| ---------------------------- | ---------------------------------------------------------------------- |
| `defineApplication()` | Configurați metadatele aplicației (obligatoriu, una per aplicație) |
| `defineObject()` | Definiți obiecte personalizate cu câmpuri |
| `defineLogicFunction()` | Definiți funcții de logică cu handleri |
| `defineFrontComponent()` | Definiți componente Front pentru interfața de utilizator personalizată |
| `defineRole()` | Configurați permisiunile rolurilor și accesul la obiecte |
| `defineField()` | Extindeți obiectele existente cu câmpuri suplimentare |
| `defineView()` | Definește vizualizări salvate pentru obiecte |
| `defineNavigationMenuItem()` | Definește linkuri de navigare în bara laterală |
| `defineSkill()` | Definiți abilități pentru agentul AI |
| Funcție | Scop |
| ---------------------------------- | ---------------------------------------------------------------------- |
| `defineApplication()` | Configurați metadatele aplicației (obligatoriu, una per aplicație) |
| `defineObject()` | Definiți obiecte personalizate cu câmpuri |
| `defineLogicFunction()` | Definiți funcții de logică cu handleri |
| `definePreInstallLogicFunction()` | Definește o funcție logică de pre-instalare (una per aplicație) |
| `definePostInstallLogicFunction()` | Definește o funcție logică post-instalare (una per aplicație) |
| `defineFrontComponent()` | Definiți componente Front pentru interfața de utilizator personalizată |
| `defineRole()` | Configurați permisiunile rolurilor și accesul la obiecte |
| `defineField()` | Extindeți obiectele existente cu câmpuri suplimentare |
| `defineView()` | Definește vizualizări salvate pentru obiecte |
| `defineNavigationMenuItem()` | Definește linkuri de navigare în bara laterală |
| `defineSkill()` | Definiți abilități pentru agentul AI |
Aceste funcții validează configurația în timpul build-ului și oferă completare automată în IDE și siguranța tipurilor.
@@ -319,6 +327,7 @@ Fiecare aplicație are un singur fișier `application-config.ts` care descrie:
* **Cine este aplicația**: identificatori, nume de afișare și descriere.
* **Cum rulează funcțiile**: ce rol folosesc pentru permisiuni.
* **(Opțional) variabile**: perechi cheievaloare expuse funcțiilor ca variabile de mediu.
* **(Opțional) funcție de pre-instalare**: o funcție logică care rulează înainte ca aplicația să fie instalată.
* **(Opțional) funcție post-instalare**: o funcție logică care rulează după instalarea aplicației.
Folosiți `defineApplication()` pentru a defini configurația aplicației:
@@ -327,7 +336,6 @@ Folosiți `defineApplication()` pentru a defini configurația aplicației:
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -343,7 +351,6 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -352,7 +359,7 @@ Notițe:
* Câmpurile `universalIdentifier` sunt ID-uri deterministe pe care le dețineți; generați-le o singură dată și păstrați-le stabile între sincronizări.
* `applicationVariables` devin variabile de mediu pentru funcțiile dvs. (de exemplu, `DEFAULT_RECIPIENT_NAME` este disponibil ca `process.env.DEFAULT_RECIPIENT_NAME`).
* `defaultRoleUniversalIdentifier` trebuie să corespundă fișierului de rol (vedeți mai jos).
* `postInstallLogicFunctionUniversalIdentifier` (opțional) indică o funcție logică care rulează automat după instalarea aplicației. Vezi [Funcții post-instalare](#post-install-functions).
* Funcțiile de pre-instalare și post-instalare sunt detectate automat în timpul construirii manifestului. Vezi [Funcții de pre-instalare](#pre-install-functions) și [Funcții post-instalare](#post-install-functions).
#### Roluri și permisiuni
@@ -491,6 +498,44 @@ Notițe:
* Matricea `triggers` este opțională. Funcțiile fără declanșatoare pot fi folosite ca funcții utilitare apelate de alte funcții.
* Puteți combina mai multe tipuri de declanșatoare într-o singură funcție.
### Funcții de pre-instalare
O funcție de pre-instalare este o funcție logică ce rulează automat înainte ca aplicația ta să fie instalată într-un spațiu de lucru. Aceasta este utilă pentru sarcini de validare, verificări ale condițiilor prealabile sau pregătirea stării spațiului de lucru înainte ca instalarea principală să continue.
Când creezi scheletul unei aplicații noi cu `create-twenty-app`, ți se generează o funcție de pre-instalare la `src/logic-functions/pre-install.ts`:
```typescript
// src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: '<generated-uuid>',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
handler,
});
```
Poți, de asemenea, să execuți manual funcția de pre-instalare oricând folosind CLI:
```bash filename="Terminal"
yarn twenty function:execute --preInstall
```
Puncte cheie:
* Funcțiile de pre-instalare folosesc `definePreInstallLogicFunction()` — o variantă specializată care omite setările de declanșare (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
* Handlerul primește un `InstallLogicFunctionPayload` cu `{ previousVersion: string }` — versiunea aplicației care a fost instalată anterior (sau un șir gol pentru instalări noi).
* Este permisă o singură funcție de pre-instalare per aplicație. Construirea manifestului va genera o eroare dacă este detectată mai mult de una.
* Proprietatea `universalIdentifier` a funcției este setată automat ca `preInstallLogicFunctionUniversalIdentifier` în manifestul aplicației în timpul build-ului — nu este nevoie să o referi în `defineApplication()`.
* Timpul de expirare implicit este setat la 300 de secunde (5 minute) pentru a permite sarcini de pregătire mai lungi.
* Funcțiile de pre-instalare nu au nevoie de declanșatoare — sunt invocate de platformă înainte de instalare sau manual prin `function:execute --preInstall`.
### Funcții post-instalare
O funcție post-instalare este o funcție logică care rulează automat după instalarea aplicației într-un spațiu de lucru. Aceasta este utilă pentru sarcini de configurare unice, cum ar fi popularea cu date implicite, crearea înregistrărilor inițiale sau configurarea setărilor spațiului de lucru.
@@ -499,16 +544,14 @@ Când creezi scheletul unei aplicații noi cu `create-twenty-app`, este generat
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
export default definePostInstallLogicFunction({
universalIdentifier: '<generated-uuid>',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
@@ -516,17 +559,6 @@ export default defineLogicFunction({
});
```
Funcția este integrată în aplicația ta prin referirea la identificatorul său universal în `application-config.ts`:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
Poți, de asemenea, să execuți manual funcția post-instalare oricând folosind CLI:
```bash filename="Terminal"
@@ -535,8 +567,10 @@ yarn twenty function:execute --postInstall
Puncte cheie:
* Funcțiile post-instalare sunt funcții logice standard — folosesc `defineLogicFunction()` la fel ca orice altă funcție.
* Câmpul `postInstallLogicFunctionUniversalIdentifier` din `defineApplication()` este opțional. Dacă este omis, nu rulează nicio funcție după instalare.
* Funcțiile de post-instalare folosesc `definePostInstallLogicFunction()` — o variantă specializată care omite setările de declanșare (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
* Handlerul primește un `InstallLogicFunctionPayload` cu `{ previousVersion: string }` — versiunea aplicației care a fost instalată anterior (sau un șir gol pentru instalări noi).
* Este permisă o singură funcție de post-instalare per aplicație. Construirea manifestului va genera o eroare dacă este detectată mai mult de una.
* Proprietatea `universalIdentifier` a funcției este setată automat ca `postInstallLogicFunctionUniversalIdentifier` în manifestul aplicației în timpul build-ului — nu este nevoie să o referi în `defineApplication()`.
* Timpul de expirare implicit este setat la 300 de secunde (5 minute) pentru a permite sarcini de configurare mai lungi, cum ar fi popularea datelor.
* Funcțiile post-instalare nu au nevoie de declanșatoare — sunt invocate de platformă în timpul instalării sau manual prin `function:execute --postInstall`.
@@ -734,7 +768,7 @@ Puncte cheie:
Puteți crea componente Front noi în două moduri:
* **Generat**: Rulați `yarn twenty entity:add` și alegeți opțiunea de a adăuga o componentă frontend nouă.
* **Manual**: Create a new `.tsx` file and use `defineFrontComponent()`, following the same pattern.
* **Manual**: Creați un fișier nou `.tsx` și folosiți `defineFrontComponent()`, urmând același model.
### Abilități
@@ -776,7 +810,7 @@ Puteți crea abilități noi în două moduri:
Two typed clients are auto-generated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated` based on your workspace schema:
* **`CoreApiClient`** — queries the `/graphql` endpoint for workspace data
* **`MetadataApiClient`** — queries the `/metadata` endpoint for workspace configuration and file uploads
* **`MetadataApiClient`** — interoghează endpointul `/metadata` pentru configurarea spațiului de lucru și încărcarea fișierelor
```typescript
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';
@@ -788,7 +822,7 @@ const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
```
Both clients are re-generated automatically by `yarn twenty app:dev` whenever your objects or fields change.
Ambii clienți sunt regenerați automat de `yarn twenty app:dev` ori de câte ori obiectele sau câmpurile tale se schimbă.
#### Acreditări la runtime în funcțiile de logică
@@ -805,7 +839,7 @@ Notițe:
#### Încărcarea fișierelor
The generated `MetadataApiClient` includes an `uploadFile` method for attaching files to file-type fields on your workspace objects. Deoarece clienții GraphQL standard nu acceptă în mod nativ încărcarea fișierelor multipart, clientul oferă această metodă dedicată care implementează, sub capotă, [specificația cererilor GraphQL multipart](https://github.com/jaydenseric/graphql-multipart-request-spec).
Clientul `MetadataApiClient` generat include o metodă `uploadFile` pentru atașarea fișierelor la câmpuri de tip fișier ale obiectelor din spațiul tău de lucru. Deoarece clienții GraphQL standard nu acceptă în mod nativ încărcarea fișierelor multipart, clientul oferă această metodă dedicată care implementează, sub capotă, [specificația cererilor GraphQL multipart](https://github.com/jaydenseric/graphql-multipart-request-spec).
```typescript
import { MetadataApiClient } from 'twenty-sdk/generated';
@@ -846,7 +880,7 @@ uploadFile(
Puncte cheie:
* The `uploadFile` method is available on `MetadataApiClient` because the upload mutation is resolved by the `/metadata` endpoint.
* Metoda `uploadFile` este disponibilă pe `MetadataApiClient` deoarece mutația de încărcare este rezolvată de endpointul `/metadata`.
* Folosește `universalIdentifier` al câmpului (nu ID-ul specific spațiului de lucru), astfel încât codul tău de încărcare funcționează în orice spațiu de lucru în care aplicația ta este instalată — în concordanță cu modul în care aplicațiile fac referire la câmpuri în rest.
* `url` returnat este un URL semnat pe care îl poți folosi pentru a accesa fișierul încărcat.
@@ -26,12 +26,12 @@ export const MyComponent = () => {
};
return (
<IconPicker
disabled={false}
onChange={handleIconChange}
selectedIconKey={selectedIcon}
variant="primary"
/>
<IconPicker
disabled={false}
onChange={handleIconChange}
selectedIconKey={selectedIcon}
variant="primary"
/>
);
};
```
@@ -20,16 +20,16 @@ import { Select } from '@/ui/input/components/Select';
export const MyComponent = () => {
return (
<Select
className
disabled={false}
label="Select an option"
options={[
{ value: 'option1', label: 'Option A', Icon: IconTwentyStar },
{ value: 'option2', label: 'Option B', Icon: IconTwentyStar },
]}
value="option1"
/>
<Select
className
disabled={false}
label="Select an option"
options={[
{ value: 'option1', label: 'Option A', Icon: IconTwentyStar },
{ value: 'option2', label: 'Option B', Icon: IconTwentyStar },
]}
value="option1"
/>
);
};
@@ -20,24 +20,24 @@ import { TextInput } from "@/ui/input/components/TextInput";
export const MyComponent = () => {
const handleChange = (text) => {
console.log("Input changed:", text);
console.log("Textul introdus s-a schimbat:", text);
};
const handleKeyDown = (event) => {
console.log("Key pressed:", event.key);
console.log("Tastă apăsată:", event.key);
};
return (
<TextInput
className
label="Username"
onChange={handleChange}
fullWidth={false}
disableHotkeys={false}
error="Invalid username"
onKeyDown={handleKeyDown}
RightIcon={null}
/>
<TextInput
className
label="Nume de utilizator"
onChange={handleChange}
fullWidth={false}
disableHotkeys={false}
error="Nume de utilizator nevalid"
onKeyDown={handleKeyDown}
RightIcon={null}
/>
);
};
@@ -82,15 +82,15 @@ import { AutosizeTextInput } from "@/ui/input/components/AutosizeTextInput";
export const MyComponent = () => {
return (
<AutosizeTextInput
onValidate={() => console.log("onValidate function fired")}
minRows={1}
placeholder="Write a comment"
onFocus={() => console.log("onFocus function fired")}
variant="icon"
buttonTitle
value="Task: "
/>
<AutosizeTextInput
onValidate={() => console.log("funcția onValidate a fost declanșată")}
minRows={1}
placeholder="Scrie un comentariu"
onFocus={() => console.log("funcția onFocus a fost declanșată")}
variant="icon"
buttonTitle
value="Sarcină: "
/>
);
};
```
@@ -8,7 +8,7 @@ title: Лучшие практики
React и Jotai отвечают за управление состоянием в коде.
### Используйте `useAtomState` для хранения состояния
### Используйте атомы Jotai для хранения состояния
Полезно создавать столько атомов, сколько вам нужно для хранения состояния.
@@ -45,7 +45,7 @@ export const MyComponent = () => {
Избегайте использования `useRef` для хранения состояния.
Если вы хотите сохранить состояние, вам следует использовать `useState` или `useAtomState`.
Если вы хотите сохранять состояние, используйте `useState` или атомы Jotai с `useAtomState`.
Смотрите [как управлять повторными рендерами](#managing-re-renders), если вы считаете, что вам нужен `useRef`, чтобы предотвратить их.
@@ -82,8 +82,8 @@ export const MyComponent = () => {
Вы можете применять то же самое для логики получения данных, с хуками Apollo.
```tsx
// ❌ Bad, will cause re-renders even if data is not changing,
// because useEffect needs to be re-evaluated
// ❌ Плохо, вызовет повторные рендеры, даже если данные не меняются,
// потому что useEffect нужно повторно вычислять
export const PageComponent = () => {
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
@@ -100,11 +100,12 @@ export const PageComponent = () => {
export const App = () => (
<PageComponent />
);
```
```tsx
// ✅ Good, will not cause re-renders if data is not changing,
// because useEffect is re-evaluated in another sibling component
// ✅ Хорошо, не вызовет повторные рендеры, если данные не меняются,
// потому что useEffect пересчитывается в другом соседнем компоненте
export const PageComponent = () => {
const [data, setData] = useAtomState(dataState);
@@ -130,11 +131,12 @@ export const App = () => (
<PageComponent />
</>
);
```
### Используйте состояния семейства Jotai и селекторы семейства Jotai
### Используйте состояния семейства атомов и селекторы
Состояния семейства Jotai и селекторы — отличный способ избежать повторных рендеров.
Состояния семейства атомов и селекторы — отличный способ избежать повторных рендеров.
Они полезны, когда нужно хранить список элементов.
@@ -77,7 +77,7 @@ npx nx run twenty-front:storybook:coverage # (требуется yarn storybook:
### Управление состоянием
[Jotai](https://jotai.org/) обрабатывает управление состоянием.
[Jotai](https://jotai.org/) отвечает за управление состоянием.
[лучшие практики](/l/ru/developers/contribute/capabilities/frontend-development/best-practices-front#state-management) для получения дополнительной информации об управлении состоянием.
@@ -160,7 +160,7 @@ export enum PageHotkeyScope {
}
```
Внутренне текущая выбранная область хранится в состоянии Jotai, которое используется по всему приложению:
Внутренне текущая выбранная область действия хранится в атоме Jotai, который используется по всему приложению:
```tsx
export const currentHotkeyScopeState = createState<HotkeyScope>({
@@ -169,10 +169,10 @@ export const currentHotkeyScopeState = createState<HotkeyScope>({
});
```
Но это состояние Jotai никогда не следует обрабатывать вручную! Мы увидим, как использовать это в следующем разделе.
Но этот атом никогда не следует обрабатывать вручную! Мы увидим, как использовать это в следующем разделе.
## Как это работает внутренне?
Мы сделали тонкую обертку поверх [react-hotkeys-hook](https://react-hotkeys-hook.vercel.app/docs/intro), которая делает его более производительным и избегает ненужных повторных рендеров.
Мы также создаем состояние Jotai, чтобы управлять состоянием горячей клавиши и сделать его доступным везде в приложении.
Мы также создаем атом Jotai, чтобы управлять состоянием области действия горячих клавиш и сделать его доступным везде в приложении.
@@ -59,7 +59,10 @@ yarn twenty entity:add
yarn twenty function:logs
# Выполнить функцию по имени
yarn twenty function:execute -n my-function -p '{\"name\": \"test\"}'
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Выполнить предустановочную функцию
yarn twenty function:execute --preInstall
# Выполнить послеустановочную функцию
yarn twenty function:execute --postInstall
@@ -80,7 +83,7 @@ yarn twenty help
* Копирует минимальное базовое приложение в `my-twenty-app/`
* Добавляет локальную зависимость `twenty-sdk` и конфигурацию Yarn 4
* Создаёт файлы конфигурации и скрипты, подключённые к CLI `twenty`
* Генерирует основные файлы (конфигурацию приложения, роль функций по умолчанию, постустановочную функцию), а также примерные файлы в зависимости от выбранного режима создания каркаса
* Генерирует основные файлы (конфигурацию приложения, роль функций по умолчанию, предустановочную и послеустановочную функции), а также примерные файлы в зависимости от выбранного режима создания каркаса
Сгенерированное с помощью каркаса приложение с режимом по умолчанию `--exhaustive` выглядит так:
@@ -107,7 +110,8 @@ my-twenty-app/
│ └── example-field.ts # Пример определения отдельного поля
├── logic-functions/
│ ├── hello-world.ts # Пример логической функции
── post-install.ts # Постустановочная логическая функция
── pre-install.ts # Предустановочная логическая функция
│ └── post-install.ts # Послеустановочная логическая функция
├── front-components/
│ └── hello-world.tsx # Пример фронтенд-компонента
├── views/
@@ -118,7 +122,7 @@ my-twenty-app/
└── example-skill.ts # Пример определения навыка агента ИИ
```
С `--minimal` создаются только основные файлы (`application-config.ts`, `roles/default-role.ts` и `logic-functions/post-install.ts`). С `--interactive` вы выбираете, какие примерные файлы включить.
С `--minimal` создаются только основные файлы (`application-config.ts`, `roles/default-role.ts`, `logic-functions/pre-install.ts` и `logic-functions/post-install.ts`). С `--interactive` вы выбираете, какие примерные файлы включить.
В общих чертах:
@@ -135,16 +139,18 @@ my-twenty-app/
SDK обнаруживает сущности, разбирая ваши файлы TypeScript в поисках вызовов **`export default define<Entity>({...})`**. Для каждого типа сущности существует соответствующая вспомогательная функция, экспортируемая из `twenty-sdk`:
| Вспомогательная функция | Тип сущности |
| ---------------------------- | ------------------------------------------ |
| `defineObject()` | Определения пользовательских объектов |
| `defineLogicFunction()` | Определения логических функций |
| `defineFrontComponent()` | Определения компонентов фронтенда |
| `defineRole()` | Определения ролей |
| `defineField()` | Расширения полей для существующих объектов |
| `defineView()` | Определения сохранённых представлений |
| `defineNavigationMenuItem()` | Определения пунктов меню навигации |
| `defineSkill()` | Определения навыков агента ИИ |
| Вспомогательная функция | Тип сущности |
| ---------------------------------- | ------------------------------------------------------------------ |
| `defineObject()` | Определения пользовательских объектов |
| `defineLogicFunction()` | Определения логических функций |
| `definePreInstallLogicFunction()` | Предустановочная логическая функция (запускается до установки) |
| `definePostInstallLogicFunction()` | Послеустановочная логическая функция (запускается после установки) |
| `defineFrontComponent()` | Определения компонентов фронтенда |
| `defineRole()` | Определения ролей |
| `defineField()` | Расширения полей для существующих объектов |
| `defineView()` | Определения сохранённых представлений |
| `defineNavigationMenuItem()` | Определения пунктов меню навигации |
| `defineSkill()` | Определения навыков агента ИИ |
<Note>
**Имена файлов заданы гибко.** Обнаружение сущностей основано на AST — SDK сканирует ваши исходные файлы в поисках шаблона `export default define<Entity>({...})`. Вы можете организовывать файлы и папки как угодно. Группировка по типу сущности (например, `logic-functions/`, `roles/`) — это лишь соглашение для организации кода, а не требование.
@@ -165,7 +171,7 @@ export default defineObject({
Позднее команды добавят больше файлов и папок:
* `yarn twenty app:dev` will auto-generate two typed API clients in `node_modules/twenty-sdk/generated`: `CoreApiClient` (for workspace data via `/graphql`) and `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`).
* `yarn twenty app:dev` автоматически сгенерирует два типизированных клиента API в `node_modules/twenty-sdk/generated`: `CoreApiClient` (для данных рабочего пространства через `/graphql`) и `MetadataApiClient` (для конфигурации рабочего пространства и загрузки файлов через `/metadata`).
* `yarn twenty entity:add` добавит файлы определений сущностей в `src/` для ваших пользовательских объектов, функций, фронтенд-компонентов, ролей, навыков и многого другого.
## Аутентификация
@@ -209,17 +215,19 @@ yarn twenty auth:status
SDK предоставляет вспомогательные функции для определения сущностей вашего приложения. Как описано в [Обнаружение сущностей](#entity-detection), вы должны использовать `export default define<Entity>({...})`, чтобы ваши сущности были обнаружены:
| Функция | Назначение |
| ---------------------------- | ---------------------------------------------------------------------- |
| `defineApplication()` | Настройка метаданных приложения (обязательно, по одному на приложение) |
| `defineObject()` | Определяет пользовательские объекты с полями |
| `defineLogicFunction()` | Определение логических функций с обработчиками |
| `defineFrontComponent()` | Определение фронт-компонентов для настраиваемого интерфейса |
| `defineRole()` | Настраивает права роли и доступ к объектам |
| `defineField()` | Расширение существующих объектов дополнительными полями |
| `defineView()` | Определяйте сохранённые представления для объектов |
| `defineNavigationMenuItem()` | Определяйте ссылки боковой панели навигации |
| `defineSkill()` | Определение навыков агента ИИ |
| Функция | Назначение |
| ---------------------------------- | ------------------------------------------------------------------------ |
| `defineApplication()` | Настройка метаданных приложения (обязательно, по одному на приложение) |
| `defineObject()` | Определяет пользовательские объекты с полями |
| `defineLogicFunction()` | Определение логических функций с обработчиками |
| `definePreInstallLogicFunction()` | Определяет предустановочную логическую функцию (по одной на приложение) |
| `definePostInstallLogicFunction()` | Определяет послеустановочную логическую функцию (по одной на приложение) |
| `defineFrontComponent()` | Определение фронт-компонентов для настраиваемого интерфейса |
| `defineRole()` | Настраивает права роли и доступ к объектам |
| `defineField()` | Расширение существующих объектов дополнительными полями |
| `defineView()` | Определяйте сохранённые представления для объектов |
| `defineNavigationMenuItem()` | Определяйте ссылки боковой панели навигации |
| `defineSkill()` | Определение навыков агента ИИ |
Эти функции проверяют вашу конфигурацию на этапе сборки и обеспечивают автодополнение в IDE и безопасность типов.
@@ -319,6 +327,7 @@ export default defineObject({
* **Что это за приложение**: идентификаторы, отображаемое имя и описание.
* **Как запускаются его функции**: какую роль они используют для прав доступа.
* **(Необязательно) переменные**: пары ключ-значение, предоставляемые вашим функциям как переменные окружения.
* **(Необязательно) предустановочная функция**: логическая функция, которая запускается до установки приложения.
* **(Необязательно) послеустановочная функция**: функция логики, которая запускается после установки приложения.
Используйте `defineApplication()` для определения конфигурации вашего приложения:
@@ -327,7 +336,6 @@ export default defineObject({
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -343,7 +351,6 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -352,7 +359,7 @@ export default defineApplication({
* `universalIdentifier` — это детерминированные идентификаторы, которыми вы управляете; сгенерируйте их один раз и сохраняйте стабильными между синхронизациями.
* `applicationVariables` становятся переменными окружения для ваших функций (например, `DEFAULT_RECIPIENT_NAME` доступна как `process.env.DEFAULT_RECIPIENT_NAME`).
* `defaultRoleUniversalIdentifier` должен соответствовать файлу роли (см. ниже).
* `postInstallLogicFunctionUniversalIdentifier` (необязательно) указывает на логическую функцию, которая автоматически выполняется после установки приложения. См. [Послеустановочные функции](#post-install-functions).
* Предустановочные и послеустановочные функции автоматически обнаруживаются во время сборки манифеста. См. [Предустановочные функции](#pre-install-functions) и [Послеустановочные функции](#post-install-functions).
#### Роли и разрешения
@@ -491,6 +498,44 @@ export default defineLogicFunction({
* Массив `triggers` необязателен. Функции без триггеров можно использовать как вспомогательные, вызываемые другими функциями.
* Вы можете сочетать несколько типов триггеров в одной функции.
### Предустановочные функции
Предустановочная функция — это логическая функция, которая автоматически выполняется до установки вашего приложения в рабочем пространстве. Это полезно для задач валидации, проверки предварительных условий или подготовки состояния рабочего пространства перед основной установкой.
Когда вы создаёте каркас нового приложения с помощью `create-twenty-app`, для вас генерируется предустановочная функция по пути `src/logic-functions/pre-install.ts`:
```typescript
// src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: '<generated-uuid>',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
handler,
});
```
Вы также можете вручную выполнить предустановочную функцию в любое время с помощью CLI:
```bash filename="Terminal"
yarn twenty function:execute --preInstall
```
Основные моменты:
* Предустановочные функции используют `definePreInstallLogicFunction()` — специализированный вариант, который опускает настройки триггеров (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
* Обработчик получает `InstallLogicFunctionPayload` с `{ previousVersion: string }` — версией приложения, которая была установлена ранее (или пустой строкой для новых установок).
* Для каждого приложения допускается только одна предустановочная функция. Сборка манифеста завершится ошибкой, если будет обнаружено более одной такой функции.
* Параметр `universalIdentifier` функции автоматически устанавливается как `preInstallLogicFunctionUniversalIdentifier` в манифесте приложения во время сборки — вам не нужно ссылаться на него в `defineApplication()`.
* Тайм-аут по умолчанию установлен на 300 секунд (5 минут), чтобы обеспечить выполнение более длительных задач подготовки.
* Предустановочным функциям не нужны триггеры — платформа вызывает их перед установкой или вручную через `function:execute --preInstall`.
### Послеустановочные функции
Послеустановочная функция — это функция логики, которая автоматически выполняется после установки вашего приложения в рабочем пространстве. Это полезно для одноразовых задач настройки, таких как инициализация данных по умолчанию, создание начальных записей или настройка параметров рабочего пространства.
@@ -499,16 +544,14 @@ export default defineLogicFunction({
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
export default definePostInstallLogicFunction({
universalIdentifier: '<generated-uuid>',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
@@ -516,17 +559,6 @@ export default defineLogicFunction({
});
```
Функция подключается к вашему приложению посредством ссылки на её универсальный идентификатор в `application-config.ts`:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
Вы также можете вручную выполнить постустановочную функцию в любое время с помощью CLI:
```bash filename="Terminal"
@@ -535,8 +567,10 @@ yarn twenty function:execute --postInstall
Основные моменты:
* Постустановочные функции — это стандартные логические функции: они используют `defineLogicFunction()` как и любые другие функции.
* Поле `postInstallLogicFunctionUniversalIdentifier` в `defineApplication()` является необязательным. Если его опустить, после установки никакая функция выполняться не будет.
* Послеустановочные функции используют `definePostInstallLogicFunction()` — специализированный вариант, который опускает настройки триггеров (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
* Обработчик получает `InstallLogicFunctionPayload` с `{ previousVersion: string }` — версией приложения, которая была установлена ранее (или пустой строкой для новых установок).
* Для каждого приложения допускается только одна послеустановочная функция. Сборка манифеста завершится ошибкой, если будет обнаружено более одной такой функции.
* Параметр `universalIdentifier` функции автоматически устанавливается как `postInstallLogicFunctionUniversalIdentifier` в манифесте приложения во время сборки — вам не нужно ссылаться на него в `defineApplication()`.
* Тайм-аут по умолчанию установлен на 300 секунд (5 минут), чтобы позволить выполнять более длительные задачи настройки, такие как инициализация данных.
* Постустановочным функциям не нужны триггеры — платформа вызывает их во время установки или вручную через `function:execute --postInstall`.
@@ -685,7 +719,7 @@ export default defineLogicFunction({
},
required: ['companyName'],
},
});
});},{
```
Основные моменты:
@@ -734,7 +768,7 @@ export default defineFrontComponent({
Вы можете создать новые фронт-компоненты двумя способами:
* **Сгенерировано**: Запустите `yarn twenty entity:add` и выберите опцию добавления нового фронтенд-компонента.
* **Manual**: Create a new `.tsx` file and use `defineFrontComponent()`, following the same pattern.
* **Вручную**: Создайте новый файл `.tsx` и используйте `defineFrontComponent()`, следуя тому же шаблону.
### Навыки
@@ -771,12 +805,12 @@ export default defineSkill({
* **Сгенерировано**: Запустите `yarn twenty entity:add` и выберите опцию добавления нового навыка.
* **Вручную**: Создайте новый файл и используйте `defineSkill()`, следуя тому же шаблону.
### Generated typed clients
### Сгенерированные типизированные клиенты
Two typed clients are auto-generated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated` based on your workspace schema:
Два типизированных клиента автоматически генерируются с помощью `yarn twenty app:dev` и сохраняются в `node_modules/twenty-sdk/generated` на основе схемы вашего рабочего пространства:
* **`CoreApiClient`** — queries the `/graphql` endpoint for workspace data
* **`MetadataApiClient`** — queries the `/metadata` endpoint for workspace configuration and file uploads
* **`CoreApiClient`** — выполняет запросы к конечной точке `/graphql` для получения данных рабочего пространства
* **`MetadataApiClient`** — выполняет запросы к эндпоинту `/metadata` для получения конфигурации рабочего пространства и загрузки файлов.
```typescript
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';
@@ -788,7 +822,7 @@ const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
```
Both clients are re-generated automatically by `yarn twenty app:dev` whenever your objects or fields change.
Оба клиента автоматически перегенерируются с помощью `yarn twenty app:dev` при изменении ваших объектов или полей.
#### Учётные данные времени выполнения в логических функциях
@@ -805,7 +839,7 @@ Both clients are re-generated automatically by `yarn twenty app:dev` whenever yo
#### Загрузка файлов
The generated `MetadataApiClient` includes an `uploadFile` method for attaching files to file-type fields on your workspace objects. Поскольку стандартные клиенты GraphQL изначально не поддерживают многочастовую загрузку файлов, клиент предоставляет специальный метод, который под капотом реализует [спецификацию многочастных запросов GraphQL](https://github.com/jaydenseric/graphql-multipart-request-spec).
Сгенерированный `MetadataApiClient` включает метод `uploadFile` для прикрепления файлов к полям типа «файл» в объектах вашего рабочего пространства. Поскольку стандартные клиенты GraphQL изначально не поддерживают многочастовую загрузку файлов, клиент предоставляет специальный метод, который под капотом реализует [спецификацию многочастных запросов GraphQL](https://github.com/jaydenseric/graphql-multipart-request-spec).
```typescript
import { MetadataApiClient } from 'twenty-sdk/generated';
@@ -846,7 +880,7 @@ uploadFile(
Основные моменты:
* The `uploadFile` method is available on `MetadataApiClient` because the upload mutation is resolved by the `/metadata` endpoint.
* Метод `uploadFile` доступен в `MetadataApiClient`, потому что мутация загрузки обрабатывается эндпоинтом `/metadata`.
* Он использует `universalIdentifier` поля (а не его идентификатор, специфичный для рабочего пространства), поэтому ваш код загрузки будет работать в любом рабочем пространстве, где установлено ваше приложение — в соответствии с тем, как приложения ссылаются на поля повсюду.
* Возвращаемый `url` — это подписанный URL, который можно использовать для доступа к загруженному файлу.
@@ -21,17 +21,17 @@ export const MyComponent = () => {
const [selectedIcon, setSelectedIcon] = useState("");
const handleIconChange = ({ iconKey, Icon }) => {
console.log("Selected Icon:", iconKey);
console.log("Выбранная иконка:", iconKey);
setSelectedIcon(iconKey);
};
return (
<IconPicker
disabled={false}
onChange={handleIconChange}
selectedIconKey={selectedIcon}
variant="primary"
/>
<IconPicker
disabled={false}
onChange={handleIconChange}
selectedIconKey={selectedIcon}
variant="primary"
/>
);
};
```
@@ -20,16 +20,16 @@ import { Select } from '@/ui/input/components/Select';
export const MyComponent = () => {
return (
<Select
className
disabled={false}
label="Выберите вариант"
options={[
{ value: 'option1', label: 'Вариант A', Icon: IconTwentyStar },
{ value: 'option2', label: 'Вариант B', Icon: IconTwentyStar },
]}
value="option1"
/>
<Select
className
disabled={false}
label="Выберите вариант"
options={[
{ value: 'option1', label: 'Вариант A', Icon: IconTwentyStar },
{ value: 'option2', label: 'Вариант B', Icon: IconTwentyStar },
]}
value="option1"
/>
);
};
@@ -20,24 +20,24 @@ import { TextInput } from "@/ui/input/components/TextInput";
export const MyComponent = () => {
const handleChange = (text) => {
console.log("Input changed:", text);
console.log("Ввод изменён:", text);
};
const handleKeyDown = (event) => {
console.log("Key pressed:", event.key);
console.log("Нажата клавиша:", event.key);
};
return (
<TextInput
className
label="Username"
onChange={handleChange}
fullWidth={false}
disableHotkeys={false}
error="Invalid username"
onKeyDown={handleKeyDown}
RightIcon={null}
/>
<TextInput
className
label="Имя пользователя"
onChange={handleChange}
fullWidth={false}
disableHotkeys={false}
error="Недопустимое имя пользователя"
onKeyDown={handleKeyDown}
RightIcon={null}
/>
);
};
@@ -82,15 +82,15 @@ import { AutosizeTextInput } from "@/ui/input/components/AutosizeTextInput";
export const MyComponent = () => {
return (
<AutosizeTextInput
onValidate={() => console.log("onValidate function fired")}
minRows={1}
placeholder="Write a comment"
onFocus={() => console.log("onFocus function fired")}
variant="icon"
buttonTitle
value="Task: "
/>
<AutosizeTextInput
onValidate={() => console.log("Функция onValidate вызвана")}
minRows={1}
placeholder="Напишите комментарий"
onFocus={() => console.log("Функция onFocus вызвана")}
variant="icon"
buttonTitle
value="Задача: "
/>
);
};
```
@@ -8,7 +8,7 @@ Bu belge, ön yüz üzerinde çalışırken takip etmeniz gereken en iyi uygulam
React ve Jotai, kod tabanında durumu yönetir.
### Durumu depolamak için `useAtomState` kullanın.
### Durumu depolamak için Jotai atomlarını kullanın
Durumunuzu depolamak için ihtiyaç duyduğunuz kadar atom oluşturmak iyi bir uygulamadır.
@@ -45,7 +45,7 @@ export const MyComponent = () => {
Durum saklamak için `useRef` kullanmaktan kaçının.
Durum saklamak istiyorsanız, `useState` veya `useAtomState` kullanmalısınız.
Durum saklamak istiyorsanız, `useState` veya `useAtomState` ile Jotai atomlarını kullanmalısınız.
Bazı yeniden render edilmelerin olmasını önlemek için `useRef`'e ihtiyacınız varmış gibi hissediyorsanız, [yeniden render yönetimi](#managing-re-renders) konusuna bakın.
@@ -132,9 +132,9 @@ export const App = () => (
);
```
### Jotai aile durumlarını ve jotai aile seçimcilerini kullanın.
### Atom aile durumlarını ve seçimcilerini kullanın
Jotai aile durumları ve seçimcileri, yeniden render'ları önlemenin harika bir yoludur.
Atom aile durumları ve seçimcileri, yeniden render'ları önlemenin harika bir yoludur.
Bir öğe listesini saklamanız gerektiğinde kullanılabilirdir.
@@ -84,7 +84,7 @@ Daha fazla bilgi için [Kancalar](https://react.dev/learn/reusing-logic-with-cus
Durum yönetim mantığını içerir. [Jotai](https://jotai.org) bunu ele almaktadır.
* Seçiciler: Türetilmiş atomlar (`createAtomSelector` kullanılarak) diğer atomlardan değer hesaplar ve otomatik olarak önbelleğe alınır.
* Seçiciler: Türetilmiş atomlar (`createAtomSelector` kullanılarak) diğer atomlardan değerleri hesaplar ve otomatik olarak önbelleğe alınır.
React'ın yerleşik durum yönetimi, bir bileşen içinde hala durumu ele alır.
@@ -77,7 +77,7 @@ Gereksiz [yeniden renderların](/l/tr/developers/contribute/capabilities/fronten
### Durum Yönetimi
Durum yönetimini [Jotai](https://jotai.org/) halleder.
[Jotai](https://jotai.org/) durum yönetimini ele alır.
Durum yönetimi hakkında daha fazla bilgi için [en iyi uygulamalar](/l/tr/developers/contribute/capabilities/frontend-development/best-practices-front#state-management) bölümüne bakın.
@@ -160,7 +160,7 @@ export enum PageHotkeyScope {
}
```
Dahili olarak, şu anda seçili olan kapsam, uygulama genelinde paylaşılan bir Jotai durumunda saklanır:
Dahili olarak, şu anda seçili olan kapsam, uygulama genelinde paylaşılan bir Jotai atomunda saklanır:
```tsx
export const currentHotkeyScopeState = createState<HotkeyScope>({
@@ -169,10 +169,10 @@ export const currentHotkeyScopeState = createState<HotkeyScope>({
});
```
Ancak bu Jotai durumu asla el ile yönetilmemelidir! Bunu bir sonraki bölümde nasıl kullanacağımızı göreceğiz.
Ancak bu atom asla el ile yönetilmemelidir! Bunu bir sonraki bölümde nasıl kullanacağımızı göreceğiz.
## İçsel olarak nasıl çalışıyor?
Gereksiz yeniden render etme işlemlerinden kaçınarak daha performanslı hale getiren [react-hotkeys-hook](https://react-hotkeys-hook.vercel.app/docs/intro) üzerine ince bir sarmalayıcı yaptık.
Ayrıca, kısayol kapsamı durumunu yönetmek ve uygulamanın her yerinde kullanılabilir hale getirmek için bir Jotai durumu oluşturuyoruz.
Ayrıca, kısayol kapsamı durumunu yönetmek ve uygulamanın her yerinde kullanılabilir hale getirmek için bir Jotai atomu oluşturuyoruz.
@@ -61,6 +61,9 @@ yarn twenty function:logs
# Execute a function by name
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Execute the pre-install function
yarn twenty function:execute --preInstall
# Execute the post-install function
yarn twenty function:execute --postInstall
@@ -80,7 +83,7 @@ Ayrıca bkz.: [create-twenty-app](https://www.npmjs.com/package/create-twenty-ap
* Minimal bir temel uygulamayı `my-twenty-app/` içine kopyalar
* Yerel bir `twenty-sdk` bağımlılığı ve Yarn 4 yapılandırması ekler
* `twenty` CLI ile bağlantılı yapılandırma dosyaları ve betikler oluşturur
* İskelet oluşturma moduna bağlı olarak çekirdek dosyaları (uygulama yapılandırması, varsayılan işlev rolü, kurulum sonrası işlev) ile örnek dosyaları üretir
* İskelet oluşturma moduna bağlı olarak çekirdek dosyaları (uygulama yapılandırması, varsayılan işlev rolü, kurulum öncesi ve kurulum sonrası işlevler) ile örnek dosyaları üretir
Varsayılan `--exhaustive` moduyla yeni oluşturulmuş bir uygulama şu şekilde görünür:
@@ -107,6 +110,7 @@ my-twenty-app/
│ └── example-field.ts # Örnek bağımsız alan tanımı
├── logic-functions/
│ ├── hello-world.ts # Örnek mantık fonksiyonu
│ ├── pre-install.ts # Kurulum öncesi mantık fonksiyonu
│ └── post-install.ts # Kurulum sonrası mantık fonksiyonu
├── front-components/
│ └── hello-world.tsx # Örnek ön bileşen
@@ -118,7 +122,7 @@ my-twenty-app/
└── example-skill.ts # Örnek yapay zekâ ajanı yetenek tanımı
```
`--minimal` ile yalnızca çekirdek dosyalar oluşturulur (`application-config.ts`, `roles/default-role.ts` ve `logic-functions/post-install.ts`). `--interactive` ile hangi örnek dosyaların dahil edileceğini siz seçersiniz.
`--minimal` ile yalnızca çekirdek dosyalar oluşturulur (`application-config.ts`, `roles/default-role.ts`, `logic-functions/pre-install.ts` ve `logic-functions/post-install.ts`). `--interactive` ile hangi örnek dosyaların dahil edileceğini siz seçersiniz.
Genel hatlarıyla:
@@ -135,16 +139,18 @@ Genel hatlarıyla:
SDK, TypeScript dosyalarınızı **`export default define<Entity>({...})`** çağrılarını arayarak ayrıştırıp varlıkları algılar. Her varlık türünün, `twenty-sdk` tarafından dışa aktarılan karşılık gelen bir yardımcı fonksiyonu vardır:
| Yardımcı fonksiyon | Varlık türü |
| ---------------------------- | ---------------------------------------- |
| `defineObject()` | Özel nesne tanımları |
| `defineLogicFunction()` | Mantık fonksiyon tanımları |
| `defineFrontComponent()` | Front component definitions |
| `defineRole()` | Rol tanımları |
| `defineField()` | Mevcut nesneler için alan genişletmeleri |
| `defineView()` | Kaydedilmiş görünüm tanımları |
| `defineNavigationMenuItem()` | Gezinme menüsü öğesi tanımları |
| `defineSkill()` | Yapay zekâ ajanı yetenek tanımları |
| Yardımcı fonksiyon | Varlık türü |
| ---------------------------------- | -------------------------------------------------------- |
| `defineObject()` | Özel nesne tanımları |
| `defineLogicFunction()` | Mantık fonksiyon tanımları |
| `definePreInstallLogicFunction()` | Kurulum öncesi mantık işlevi (kurulumdan önce çalışır) |
| `definePostInstallLogicFunction()` | Kurulum sonrası mantık işlevi (kurulumdan sonra çalışır) |
| `defineFrontComponent()` | Front component definitions |
| `defineRole()` | Rol tanımları |
| `defineField()` | Mevcut nesneler için alan genişletmeleri |
| `defineView()` | Kaydedilmiş görünüm tanımları |
| `defineNavigationMenuItem()` | Gezinme menüsü öğesi tanımları |
| `defineSkill()` | Yapay zekâ ajanı yetenek tanımları |
<Note>
**Dosya adlandırma esnektir.** Varlık algılama AST tabanlıdır — SDK, kaynak dosyalarınızı `export default define<Entity>({...})` desenini bulmak için tarar. Dosyalarınızı ve klasörlerinizi dilediğiniz gibi düzenleyebilirsiniz. Varlık türüne göre gruplama (örn. `logic-functions/`, `roles/`) bir gereklilik değil, yalnızca kod organizasyonu için bir gelenektir.
@@ -165,7 +171,7 @@ export default defineObject({
İlerideki komutlar daha fazla dosya ve klasör ekleyecektir:
* `yarn twenty app:dev` will auto-generate two typed API clients in `node_modules/twenty-sdk/generated`: `CoreApiClient` (for workspace data via `/graphql`) and `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`).
* `yarn twenty app:dev`, `node_modules/twenty-sdk/generated` içinde iki tiplendirilmiş API istemcisini otomatik olarak oluşturur: `CoreApiClient` (`/graphql` üzerinden çalışma alanı verileri için) ve `MetadataApiClient` (çalışma alanı yapılandırması ve `/metadata` üzerinden dosya yüklemeleri için).
* `yarn twenty entity:add`, özel nesneleriniz, fonksiyonlarınız, ön bileşenleriniz, rolleriniz, yetenekleriniz ve daha fazlası için `src/` altında varlık tanım dosyaları ekler.
## Kimlik Doğrulama
@@ -209,17 +215,19 @@ twenty-sdk, uygulamanız içinde kullandığınız türlendirilmiş yapı taşla
SDK, uygulama varlıklarınızı tanımlamak için yardımcı fonksiyonlar sağlar. [Varlık algılama](#entity-detection) bölümünde açıklandığı gibi, varlıklarınızın algılanması için `export default define<Entity>({...})` kullanmalısınız:
| Fonksiyon | Amaç |
| ---------------------------- | ------------------------------------------------------------------------- |
| `defineApplication()` | Uygulama meta verilerini yapılandırın (zorunlu, uygulama başına bir adet) |
| `defineObject()` | Alanlara sahip özel nesneler tanımlayın |
| `defineLogicFunction()` | İşleyicilerle mantık fonksiyonları tanımlayın |
| `defineFrontComponent()` | Özel kullanıcı arayüzü için ön uç bileşenlerini tanımlayın |
| `defineRole()` | Rol izinlerini ve nesne erişimini yapılandırın |
| `defineField()` | Mevcut nesneleri ek alanlarla genişletin |
| `defineView()` | Nesneler için kaydedilmiş görünümler tanımlayın |
| `defineNavigationMenuItem()` | Kenar çubuğu gezinme bağlantılarını tanımlayın |
| `defineSkill()` | Yapay zekâ ajanı yeteneklerini tanımlayın |
| Fonksiyon | Amaç |
| ---------------------------------- | ------------------------------------------------------------------------- |
| `defineApplication()` | Uygulama meta verilerini yapılandırın (zorunlu, uygulama başına bir adet) |
| `defineObject()` | Alanlara sahip özel nesneler tanımlayın |
| `defineLogicFunction()` | İşleyicilerle mantık fonksiyonları tanımlayın |
| `definePreInstallLogicFunction()` | Bir kurulum öncesi mantık işlevi tanımlayın (uygulama başına bir adet) |
| `definePostInstallLogicFunction()` | Bir kurulum sonrası mantık işlevi tanımlayın (uygulama başına bir adet) |
| `defineFrontComponent()` | Özel kullanıcı arayüzü için ön uç bileşenlerini tanımlayın |
| `defineRole()` | Rol izinlerini ve nesne erişimini yapılandırın |
| `defineField()` | Mevcut nesneleri ek alanlarla genişletin |
| `defineView()` | Nesneler için kaydedilmiş görünümler tanımlayın |
| `defineNavigationMenuItem()` | Kenar çubuğu gezinme bağlantılarını tanımlayın |
| `defineSkill()` | Yapay zekâ ajanı yeteneklerini tanımlayın |
Bu fonksiyonlar, derleme zamanında yapılandırmanızı doğrular ve IDE otomatik tamamlama ile tür güvenliği sağlar.
@@ -319,6 +327,7 @@ Her uygulamanın aşağıdakileri açıklayan tek bir `application-config.ts` do
* **Uygulamanın kim olduğu**: tanımlayıcılar, görünen ad ve açıklama.
* **Fonksiyonlarının nasıl çalıştığı**: izinler için hangi rolü kullandıkları.
* **(İsteğe bağlı) değişkenler**: fonksiyonlarınıza ortam değişkenleri olarak sunulan anahtardeğer çiftleri.
* **(İsteğe bağlı) kurulum öncesi işlev**: uygulama yüklenmeden önce çalışan bir mantık işlevi.
* **(İsteğe bağlı) kurulum sonrası işlev**: uygulama yüklendikten sonra çalışan bir mantık işlevi.
Uygulama yapılandırmanızı tanımlamak için `defineApplication()` kullanın:
@@ -327,7 +336,6 @@ Uygulama yapılandırmanızı tanımlamak için `defineApplication()` kullanın:
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -343,7 +351,6 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -352,7 +359,7 @@ Notlar:
* `universalIdentifier` alanları size ait belirleyici kimliklerdir; bunları bir kez oluşturun ve eşitlemeler boyunca kararlı tutun.
* `applicationVariables`, fonksiyonlarınız için ortam değişkenlerine dönüşür (örneğin, `DEFAULT_RECIPIENT_NAME` değeri `process.env.DEFAULT_RECIPIENT_NAME` olarak kullanılabilir).
* `defaultRoleUniversalIdentifier`, rol dosyasıyla eşleşmelidir (aşağıya bakın).
* `postInstallLogicFunctionUniversalIdentifier` (isteğe bağlı), uygulama yüklendikten sonra otomatik olarak çalışan bir mantık işlevine işaret eder. Bkz. [Kurulum sonrası işlevler](#post-install-functions).
* Kurulum öncesi ve kurulum sonrası işlevler, manifest oluşturma sırasında otomatik olarak algılanır. Bkz. [Kurulum öncesi işlevler](#pre-install-functions) ve [Kurulum sonrası işlevler](#post-install-functions).
#### Roller ve izinler
@@ -491,6 +498,44 @@ Notlar:
* `triggers` dizisi isteğe bağlıdır. Tetikleyicisi olmayan fonksiyonlar, diğer fonksiyonlar tarafından çağrılan yardımcı fonksiyonlar olarak kullanılabilir.
* Tek bir fonksiyonda birden çok tetikleyici türünü birleştirebilirsiniz.
### Kurulum öncesi işlevler
Kurulum öncesi işlev, uygulamanız bir çalışma alanına yüklenmeden önce otomatik olarak çalışan bir mantık işlevidir. Bu, doğrulama görevleri, önkoşul kontrolleri veya ana kurulum başlamadan önce çalışma alanı durumunun hazırlanması için yararlıdır.
`create-twenty-app` ile yeni bir uygulama iskeleti oluşturduğunuzda, `src/logic-functions/pre-install.ts` konumunda sizin için bir kurulum öncesi işlev oluşturulur:
```typescript
// src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: '<generated-uuid>',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
handler,
});
```
Ayrıca kurulum öncesi işlevi istediğiniz zaman CLI kullanarak manuel olarak çalıştırabilirsiniz:
```bash filename="Terminal"
yarn twenty function:execute --preInstall
```
Önemli noktalar:
* Kurulum öncesi işlevler `definePreInstallLogicFunction()` kullanır — tetikleyici ayarlarını atlayan (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`) özel bir varyanttır.
* İşleyici, `{ previousVersion: string }` içeren bir `InstallLogicFunctionPayload` alır — daha önce yüklü olan uygulamanın sürümü (veya yeni kurulumlar için boş bir dize).
* Uygulama başına yalnızca bir kurulum öncesi işlevine izin verilir. Birden fazla tespit edilirse manifest oluşturma hataya düşer.
* İşlevin `universalIdentifier` değeri, oluşturma sırasında uygulama manifestinde otomatik olarak `preInstallLogicFunctionUniversalIdentifier` olarak ayarlanır — `defineApplication()` içinde buna atıfta bulunmanıza gerek yoktur.
* Varsayılan zaman aşımı, daha uzun hazırlık görevlerine izin vermek için 300 saniye (5 dakika) olarak ayarlanmıştır.
* Kurulum öncesi işlevlerin tetikleyicilere ihtiyacı yoktur — kurulumdan önce platform tarafından veya `function:execute --preInstall` aracılığıyla manuel olarak çağrılırlar.
### Kurulum sonrası işlevler
Kurulum sonrası işlev, uygulamanız bir çalışma alanına yüklendikten sonra otomatik olarak çalışan bir mantık işlevidir. Bu, varsayılan verileri tohumlama, ilk kayıtları oluşturma veya çalışma alanı ayarlarını yapılandırma gibi tek seferlik kurulum görevleri için yararlıdır.
@@ -499,16 +544,14 @@ Kurulum sonrası işlev, uygulamanız bir çalışma alanına yüklendikten sonr
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
export default definePostInstallLogicFunction({
universalIdentifier: '<generated-uuid>',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
@@ -516,17 +559,6 @@ export default defineLogicFunction({
});
```
İşlev, `application-config.ts` içinde evrensel tanımlayıcısına başvurularak uygulamanıza bağlanır:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
Ayrıca kurulum sonrası işlevi istediğiniz zaman CLI kullanarak manuel olarak çalıştırabilirsiniz:
```bash filename="Terminal"
@@ -535,8 +567,10 @@ yarn twenty function:execute --postInstall
Önemli noktalar:
* Kurulum sonrası işlevleri standart mantık işlevleridir — diğer herhangi bir işlev gibi `defineLogicFunction()` kullanırlar.
* `defineApplication()` içindeki `postInstallLogicFunctionUniversalIdentifier` alanı isteğe bağlıdır. Atlanırsa, kurulumdan sonra hiçbir işlev çalıştırılmaz.
* Kurulum sonrası işlevler `definePostInstallLogicFunction()` kullanır — tetikleyici ayarlarını atlayan (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`) özel bir varyanttır.
* İşleyici, `{ previousVersion: string }` içeren bir `InstallLogicFunctionPayload` alır — daha önce yüklü olan uygulamanın sürümü (veya yeni kurulumlar için boş bir dize).
* Uygulama başına yalnızca bir kurulum sonrası işlevine izin verilir. Birden fazla tespit edilirse manifest oluşturma hataya düşer.
* İşlevin `universalIdentifier` değeri, oluşturma sırasında uygulama manifestinde otomatik olarak `postInstallLogicFunctionUniversalIdentifier` olarak ayarlanır — `defineApplication()` içinde buna atıfta bulunmanıza gerek yoktur.
* Varsayılan zaman aşımı, veri tohumlama gibi daha uzun kurulum görevlerine izin vermek için 300 saniye (5 dakika) olarak ayarlanmıştır.
* Kurulum sonrası işlevlerin tetikleyicilere ihtiyacı yoktur — kurulum sırasında platform tarafından veya `function:execute --postInstall` aracılığıyla manuel olarak çağrılırlar.
@@ -734,7 +768,7 @@ export default defineFrontComponent({
Yeni ön uç bileşenlerini iki şekilde oluşturabilirsiniz:
* **Şablondan**: `yarn twenty entity:add` çalıştırın ve yeni bir ön uç bileşeni ekleme seçeneğini seçin.
* **Manual**: Create a new `.tsx` file and use `defineFrontComponent()`, following the same pattern.
* **Manuel**: Aynı deseni izleyerek yeni bir `.tsx` dosyası oluşturun ve `defineFrontComponent()` kullanın.
### Beceriler
@@ -771,12 +805,12 @@ Yeni yetenekleri iki şekilde oluşturabilirsiniz:
* **Şablondan**: `yarn twenty entity:add` komutunu çalıştırın ve yeni bir yetenek ekleme seçeneğini seçin.
* **Manuel**: Yeni bir dosya oluşturun ve aynı deseni izleyerek `defineSkill()` kullanın.
### Generated typed clients
### Oluşturulan tiplendirilmiş istemciler
Two typed clients are auto-generated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated` based on your workspace schema:
Çalışma alanı şemanıza göre `yarn twenty app:dev` tarafından iki tiplendirilmiş istemci otomatik olarak oluşturulur ve `node_modules/twenty-sdk/generated` içine kaydedilir:
* **`CoreApiClient`** — queries the `/graphql` endpoint for workspace data
* **`MetadataApiClient`** — queries the `/metadata` endpoint for workspace configuration and file uploads
* **`CoreApiClient`** — çalışma alanı verileri için `/graphql` uç noktasını sorgular
* **`MetadataApiClient`** — çalışma alanı yapılandırması ve dosya yüklemeleri için `/metadata` uç noktasını sorgular.
```typescript
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';
@@ -788,7 +822,7 @@ const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
```
Both clients are re-generated automatically by `yarn twenty app:dev` whenever your objects or fields change.
Her iki istemci de, nesneleriniz veya alanlarınız değiştiğinde, `yarn twenty app:dev` tarafından otomatik olarak yeniden oluşturulur.
#### Mantık fonksiyonlarında çalışma zamanı kimlik bilgileri
@@ -805,7 +839,7 @@ Notlar:
#### Dosya yükleme
The generated `MetadataApiClient` includes an `uploadFile` method for attaching files to file-type fields on your workspace objects. Standart GraphQL istemcileri çok parçalı dosya yüklemelerini yerel olarak desteklemediğinden, istemci arka planda [GraphQL çok parçalı istek belirtimi](https://github.com/jaydenseric/graphql-multipart-request-spec) uygulayan bu özel yöntemi sağlar.
Oluşturulan `MetadataApiClient`, çalışma alanı nesnelerinizdeki dosya türündeki alanlara dosya eklemek için bir `uploadFile` yöntemi içerir. Standart GraphQL istemcileri çok parçalı dosya yüklemelerini yerel olarak desteklemediğinden, istemci arka planda [GraphQL çok parçalı istek belirtimi](https://github.com/jaydenseric/graphql-multipart-request-spec) uygulayan bu özel yöntemi sağlar.
```typescript
import { MetadataApiClient } from 'twenty-sdk/generated';
@@ -846,7 +880,7 @@ uploadFile(
Önemli noktalar:
* The `uploadFile` method is available on `MetadataApiClient` because the upload mutation is resolved by the `/metadata` endpoint.
* `uploadFile` yöntemi, yükleme mutasyonu `/metadata` uç noktası tarafından çözümlendiği için `MetadataApiClient` üzerinde mevcuttur.
* Alan için `universalIdentifier` kullanılır (çalışma alanına özgü kimliği değil), böylece yükleme kodunuz uygulamanızın yüklü olduğu herhangi bir çalışma alanında çalışır — uygulamaların başka her yerde alanlara nasıl atıfta bulunduğuyla tutarlıdır.
* Döndürülen `url`, yüklenen dosyaya erişmek için kullanabileceğiniz imzalı bir URL'dir.
@@ -21,17 +21,17 @@ export const MyComponent = () => {
const [selectedIcon, setSelectedIcon] = useState("");
const handleIconChange = ({ iconKey, Icon }) => {
console.log("Selected Icon:", iconKey);
console.log("Seçilen Simge:", iconKey);
setSelectedIcon(iconKey);
};
return (
<IconPicker
disabled={false}
onChange={handleIconChange}
selectedIconKey={selectedIcon}
variant="primary"
/>
<IconPicker
disabled={false}
onChange={handleIconChange}
selectedIconKey={selectedIcon}
variant="primary"
/>
);
};
```
@@ -20,16 +20,16 @@ import { Select } from '@/ui/input/components/Select';
export const MyComponent = () => {
return (
<Select
className
disabled={false}
label="Bir seçenek seçin"
options={[
{ value: 'option1', label: 'Seçenek A', Icon: IconTwentyStar },
{ value: 'option2', label: 'Seçenek B', Icon: IconTwentyStar },
]}
value="option1"
/>
<Select
className
disabled={false}
label="Bir seçenek seçin"
options={[
{ value: 'option1', label: 'Seçenek A', Icon: IconTwentyStar },
{ value: 'option2', label: 'Seçenek B', Icon: IconTwentyStar },
]}
value="option1"
/>
);
};
@@ -28,16 +28,16 @@ export const MyComponent = () => {
};
return (
<TextInput
className
label="Kullanıcı adı"
onChange={handleChange}
fullWidth={false}
disableHotkeys={false}
error="Geçersiz kullanıcı adı"
onKeyDown={handleKeyDown}
RightIcon={null}
/>
<TextInput
className
label="Kullanıcı adı"
onChange={handleChange}
fullWidth={false}
disableHotkeys={false}
error="Geçersiz kullanıcı adı"
onKeyDown={handleKeyDown}
RightIcon={null}
/>
);
};
},{
@@ -82,15 +82,15 @@ import { AutosizeTextInput } from "@/ui/input/components/AutosizeTextInput";
export const MyComponent = () => {
return (
<AutosizeTextInput
onValidate={() => console.log("onValidate fonksiyonu tetiklendi")}
minRows={1}
placeholder="Bir yorum yazın"
onFocus={() => console.log("onFocus fonksiyonu tetiklendi")}
variant="icon"
buttonTitle
value="Görev: "
/>
<AutosizeTextInput
onValidate={() => console.log("onValidate fonksiyonu tetiklendi")}
minRows={1}
placeholder="Bir yorum yazın"
onFocus={() => console.log("onFocus fonksiyonu tetiklendi")}
variant="icon"
buttonTitle
value="Görev: "
/>
);
};
```
@@ -8,7 +8,7 @@ title: 最佳实践
React 和 Jotai 在代码库中处理状态管理。
### 使用 `useAtomState` 来存储状态
### 使用 Jotai 原子来存储状态
根据需要创建足够多的原子来存储你的状态,是一种良好实践。
@@ -45,7 +45,7 @@ export const MyComponent = () => {
避免使用 `useRef` 来存储状态。
如果想存储状态,应该使用 `useState` 或 `useAtomState`。
如果想存储状态,应该使用 `useState` 或配合 `useAtomState` 的 Jotai 原子
如果您觉得需要通过使用 `useRef` 来防止一些再渲染,请了解[如何管理再渲染](#managing-re-renders)。
@@ -132,9 +132,9 @@ export const App = () => (
);
```
### 使用 Jotai 状态族和 Jotai 选择器
### 使用原子族状态和选择器
Jotai 状态和选择器是避免渲染的好方法。
原子族状态和选择器是避免重新渲染的好方法。
当您需要存储项目列表时,它们很有用。
@@ -84,7 +84,7 @@ module1
包含状态管理逻辑。 [Jotai](https://jotai.org) 处理这部分。
* 选择器:派生原子(使用 `createAtomSelector`从其他原子计算值,并自动进行记忆化。
* 选择器:派生 Atom(使用 `createAtomSelector`根据其他 Atom 计算值,并自动记忆化。
React 内置状态管理仍然在组件内处理状态。
@@ -160,7 +160,7 @@ export enum PageHotkeyScope {
}
```
在内部,当前选择的范围存储在整个应用程序共享的 Jotai 状态中:
在内部,当前选择的范围存储在整个应用程序共享的 Jotai 原子中:
```tsx
export const currentHotkeyScopeState = createState<HotkeyScope>({
@@ -169,10 +169,10 @@ export const currentHotkeyScopeState = createState<HotkeyScope>({
});
```
但这个 Jotai 状态不应该手动处理! 我们将在下一节中学习如何使用它。
但这个原子不应该手动处理! 我们将在下一节中学习如何使用它。
## 内部是如何运作的?
我们在 [react-hotkeys-hook](https://react-hotkeys-hook.vercel.app/docs/intro) 之上制作了一个薄包装,使其性能更高并避免不必要的重新渲染。
我们还创建了一个 Jotai 状态来处理快捷键范围状态,并使其在整个应用程序中可用。
我们还创建了一个 Jotai 原子来处理快捷键范围状态,并使其在整个应用程序中可用。
@@ -59,7 +59,10 @@ yarn twenty entity:add
yarn twenty function:logs
# 按名称执行一个函数
yarn twenty function:execute -n my-function -p '{\"name\": \"test\"}'
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# 执行安装前函数
yarn twenty function:execute --preInstall
# 执行安装后函数
yarn twenty function:execute --postInstall
@@ -80,7 +83,7 @@ yarn twenty help
* 将一个最小的基础应用复制到 `my-twenty-app/` 中
* 添加本地 `twenty-sdk` 依赖和 Yarn 4 配置
* 创建与 `twenty` CLI 关联的配置文件和脚本
* Generates core files (application config, default function role, post-install function) plus example files based on the scaffolding mode
* 生成核心文件(应用配置、默认函数角色、安装前/安装后函数),并基于脚手架模式生成示例文件
A freshly scaffolded app with the default `--exhaustive` mode looks like this:
@@ -96,29 +99,30 @@ my-twenty-app/
eslint.config.mjs
tsconfig.json
README.md
public/ # Public assets folder (images, fonts, etc.)
public/ # 公共资源文件夹(图片、字体等)
src/
├── application-config.ts # Required - main application configuration
├── application-config.ts # 必需 - 主应用配置
├── roles/
│ └── default-role.ts # Default role for logic functions
│ └── default-role.ts # 逻辑函数的默认角色
├── objects/
│ └── example-object.ts # Example custom object definition
│ └── example-object.ts # 示例自定义对象定义
├── fields/
│ └── example-field.ts # Example standalone field definition
│ └── example-field.ts # 示例独立字段定义
├── logic-functions/
│ ├── hello-world.ts # Example logic function
── post-install.ts # Post-install logic function
│ ├── hello-world.ts # 示例逻辑函数
── pre-install.ts # 安装前逻辑函数
│ └── post-install.ts # 安装后逻辑函数
├── front-components/
│ └── hello-world.tsx # Example front component
│ └── hello-world.tsx # 示例前端组件
├── views/
│ └── example-view.ts # Example saved view definition
│ └── example-view.ts # 示例已保存视图定义
├── navigation-menu-items/
│ └── example-navigation-menu-item.ts # Example sidebar navigation link
│ └── example-navigation-menu-item.ts # 示例侧边栏导航链接
└── skills/
└── example-skill.ts # Example AI agent skill definition
└── example-skill.ts # 示例 AI 代理技能定义
```
With `--minimal`, only the core files are created (`application-config.ts`, `roles/default-role.ts`, and `logic-functions/post-install.ts`). With `--interactive`, you choose which example files to include.
使用 `--minimal` 时,只会创建核心文件(`application-config.ts``roles/default-role.ts`、`logic-functions/pre-install.ts` 和 `logic-functions/post-install.ts`)。 With `--interactive`, you choose which example files to include.
总体来说:
@@ -135,16 +139,18 @@ With `--minimal`, only the core files are created (`application-config.ts`, `rol
该 SDK 通过在你的 TypeScript 文件中解析 **`export default define<Entity>({...})`** 调用来检测实体。 每种实体类型都有一个从 `twenty-sdk` 导出的对应辅助函数:
| 辅助函数 | 实体类型 |
| ---------------------------- | -------------------------------- |
| `defineObject()` | 自定义对象定义 |
| `defineLogicFunction()` | 逻辑函数定义 |
| `defineFrontComponent()` | 前端组件定义 |
| `defineRole()` | 角色定义 |
| `defineField()` | 现有对象的字段扩展 |
| `defineView()` | Saved view definitions |
| `defineNavigationMenuItem()` | Navigation menu item definitions |
| `defineSkill()` | AI agent skill definitions |
| 辅助函数 | 实体类型 |
| ---------------------------------- | -------------------------------- |
| `defineObject()` | 自定义对象定义 |
| `defineLogicFunction()` | 逻辑函数定义 |
| `definePreInstallLogicFunction()` | 安装前逻辑函数(在安装之前运行) |
| `definePostInstallLogicFunction()` | 安装后逻辑函数(在安装之后运行) |
| `defineFrontComponent()` | 前端组件定义 |
| `defineRole()` | 角色定义 |
| `defineField()` | 现有对象的字段扩展 |
| `defineView()` | Saved view definitions |
| `defineNavigationMenuItem()` | Navigation menu item definitions |
| `defineSkill()` | AI agent skill definitions |
<Note>
**文件命名是灵活的。** 实体检测基于 AST — SDK 会扫描你的源文件以查找 `export default define<Entity>({...})` 模式。 你可以按照自己的喜好组织文件和文件夹。 按实体类型分组(例如 `logic-functions/`、`roles/`)只是代码组织的一种约定,并非必需。
@@ -165,7 +171,7 @@ export default defineObject({
后续命令将添加更多文件和文件夹:
* `yarn twenty app:dev` will auto-generate two typed API clients in `node_modules/twenty-sdk/generated`: `CoreApiClient` (for workspace data via `/graphql`) and `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`).
* `yarn twenty app:dev` 会在 `node_modules/twenty-sdk/generated` 中自动生成两个类型化 API 客户端:`CoreApiClient`(通过 `/graphql` 获取工作区数据)和 `MetadataApiClient`(通过 `/metadata` 处理工作区配置和文件上传)。
* `yarn twenty entity:add` will add entity definition files under `src/` for your custom objects, functions, front components, roles, skills, and more.
## 身份验证
@@ -209,17 +215,19 @@ twenty-sdk 提供你在应用中使用的类型化构件和辅助函数。 以
该 SDK 提供辅助函数用于定义你的应用实体。 如 [实体检测](#entity-detection) 中所述,你必须使用 `export default define<Entity>({...})` 才能让你的实体被检测到:
| 函数 | 目的 |
| ---------------------------- | ------------------------------- |
| `defineApplication()` | 配置应用元数据(必需,每个应用一个) |
| `defineObject()` | 定义带字段的自定义对象 |
| `defineLogicFunction()` | 定义带处理程序的逻辑函数 |
| `defineFrontComponent()` | 为自定义 UI 定义前端组件 |
| `defineRole()` | 配置角色权限和对象访问 |
| `defineField()` | 为现有对象扩展额外字段 |
| `defineView()` | Define saved views for objects |
| `defineNavigationMenuItem()` | Define sidebar navigation links |
| `defineSkill()` | Define AI agent skills |
| 函数 | 目的 |
| ---------------------------------- | ------------------------------- |
| `defineApplication()` | 配置应用元数据(必需,每个应用一个) |
| `defineObject()` | 定义带字段的自定义对象 |
| `defineLogicFunction()` | 定义带处理程序的逻辑函数 |
| `definePreInstallLogicFunction()` | 定义一个安装前逻辑函数(每个应用一个) |
| `definePostInstallLogicFunction()` | 定义一个安装后逻辑函数(每个应用一个) |
| `defineFrontComponent()` | 为自定义 UI 定义前端组件 |
| `defineRole()` | 配置角色权限和对象访问 |
| `defineField()` | 为现有对象扩展额外字段 |
| `defineView()` | Define saved views for objects |
| `defineNavigationMenuItem()` | Define sidebar navigation links |
| `defineSkill()` | Define AI agent skills |
这些函数会在构建时校验你的配置,并提供 IDE 自动补全和类型安全。
@@ -319,6 +327,7 @@ export default defineObject({
* **应用的身份**:标识符、显示名称和描述。
* **函数如何运行**:它们用于权限的角色。
* **(可选)变量**:以环境变量形式提供给函数的键值对。
* **(可选)安装前函数**:在应用安装之前运行的逻辑函数。
* **(可选)安装后函数**:在应用安装后运行的逻辑函数。
使用 `defineApplication()` 定义你的应用配置:
@@ -327,7 +336,6 @@ export default defineObject({
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -343,7 +351,6 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -352,7 +359,7 @@ export default defineApplication({
* `universalIdentifier` 字段是你拥有的确定性 ID;生成一次并在多次同步中保持稳定。
* `applicationVariables` 会变成函数可用的环境变量(例如,`DEFAULT_RECIPIENT_NAME` 可作为 `process.env.DEFAULT_RECIPIENT_NAME` 使用)。
* `defaultRoleUniversalIdentifier` 必须与角色文件一致(见下文)。
* `postInstallLogicFunctionUniversalIdentifier`(可选)指向一个在应用安装后自动运行的逻辑函数。 参见 [安装后函数](#post-install-functions)。
* 清单构建期间会自动检测安装前和安装后函数。 参见 [安装前函数](#pre-install-functions) 和 [安装后函数](#post-install-functions)。
#### 角色和权限
@@ -491,6 +498,44 @@ export default defineLogicFunction({
* `triggers` 数组是可选的。 没有触发器的函数可作为实用函数,被其他函数调用。
* 你可以在单个函数中混用多种触发器类型。
### 安装前函数
安装前函数是在你的应用安装到工作区之前自动运行的逻辑函数。 这对于执行验证任务、先决条件检查,或在主安装开始前准备工作区状态很有用。
当你使用 `create-twenty-app` 脚手架创建一个新应用时,会在 `src/logic-functions/pre-install.ts` 为你生成一个安装前函数:
```typescript
// src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: '<generated-uuid>',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
handler,
});
```
你也可以随时使用 CLI 手动执行安装前函数:
```bash filename="Terminal"
yarn twenty function:execute --preInstall
```
关键点:
* 安装前函数使用 `definePreInstallLogicFunction()` —— 这是一个省略触发器设置(`cronTriggerSettings`、`databaseEventTriggerSettings`、`httpRouteTriggerSettings`、`isTool`)的专用变体。
* 处理器会接收一个 `InstallLogicFunctionPayload`,其包含 `{ previousVersion: string }` —— 即之前安装的应用版本(全新安装则为空字符串)。
* 每个应用仅允许一个安装前函数。 如果检测到多个,清单构建将报错。
* 在构建期间,函数的 `universalIdentifier` 会自动设置为应用清单上的 `preInstallLogicFunctionUniversalIdentifier` —— 你无需在 `defineApplication()` 中引用它。
* 默认超时时间设置为 300 秒(5 分钟),以便支持更长的准备任务。
* 安装前函数不需要触发器——它们会在安装前由平台调用,或通过 `function:execute --preInstall` 手动调用。
### 安装后函数
安装后函数是在你的应用安装到工作区后自动运行的逻辑函数。 这对于一次性设置任务很有用,例如填充默认数据、创建初始记录或配置工作区设置。
@@ -499,16 +544,14 @@ export default defineLogicFunction({
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
export default definePostInstallLogicFunction({
universalIdentifier: '<generated-uuid>',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
@@ -516,17 +559,6 @@ export default defineLogicFunction({
});
```
通过在 `application-config.ts` 中引用其通用标识符,可将该函数接入你的应用:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
You can also manually execute the post-install function at any time using the CLI:
```bash filename="Terminal"
@@ -535,8 +567,10 @@ yarn twenty function:execute --postInstall
关键点:
* Post-install functions are standard logic functions — they use `defineLogicFunction()` like any other function.
* The `postInstallLogicFunctionUniversalIdentifier` field in `defineApplication()` is optional. If omitted, no function runs after installation.
* 安装后函数使用 `definePostInstallLogicFunction()` —— 这是一个省略触发器设置(`cronTriggerSettings`、`databaseEventTriggerSettings`、`httpRouteTriggerSettings`、`isTool`)的专用变体。
* 处理器会接收一个 `InstallLogicFunctionPayload`,其包含 `{ previousVersion: string }` —— 即之前安装的应用版本(全新安装则为空字符串)。
* 每个应用仅允许一个安装后函数。 如果检测到多个,清单构建将报错。
* 在构建期间,函数的 `universalIdentifier` 会自动设置为应用清单上的 `postInstallLogicFunctionUniversalIdentifier` —— 你无需在 `defineApplication()` 中引用它。
* The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding.
* Post-install functions do not need triggers — they are invoked by the platform during installation or manually via `function:execute --postInstall`.
@@ -734,7 +768,7 @@ export default defineFrontComponent({
你可以通过两种方式创建新的前端组件:
* **脚手架生成**:运行 `yarn twenty entity:add` 并选择添加新前端组件的选项。
* **Manual**: Create a new `.tsx` file and use `defineFrontComponent()`, following the same pattern.
* **手动**:创建一个新的 `.tsx` 文件,并使用 `defineFrontComponent()`,遵循相同的模式。
### 技能
@@ -771,12 +805,12 @@ You can create new skills in two ways:
* **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new skill.
* **Manual**: Create a new file and use `defineSkill()`, following the same pattern.
### Generated typed clients
### 生成的类型化客户端
Two typed clients are auto-generated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated` based on your workspace schema:
两个类型化客户端由 `yarn twenty app:dev` 自动生成(基于你的工作区架构),并存放在 `node_modules/twenty-sdk/generated`
* **`CoreApiClient`** — queries the `/graphql` endpoint for workspace data
* **`MetadataApiClient`** — queries the `/metadata` endpoint for workspace configuration and file uploads
* **`CoreApiClient`** — 查询 `/graphql` 端点以获取工作区数据
* **`MetadataApiClient`** — 查询 `/metadata` 端点以获取工作区配置并处理文件上传
```typescript
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';
@@ -788,7 +822,7 @@ const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
```
Both clients are re-generated automatically by `yarn twenty app:dev` whenever your objects or fields change.
每当你的对象或字段发生变化时,`yarn twenty app:dev` 都会自动重新生成这两个客户端。
#### 逻辑函数中的运行时凭据
@@ -805,7 +839,7 @@ Both clients are re-generated automatically by `yarn twenty app:dev` whenever yo
#### 上传文件
The generated `MetadataApiClient` includes an `uploadFile` method for attaching files to file-type fields on your workspace objects. 由于标准 GraphQL 客户端不原生支持多部分文件上传,该客户端提供了一个专用方法,在底层实现了 [GraphQL 多部分请求规范](https://github.com/jaydenseric/graphql-multipart-request-spec)。
生成的 `MetadataApiClient` 包含一个 `uploadFile` 方法,用于将文件附加到你的工作区对象的文件类型字段。 由于标准 GraphQL 客户端不原生支持多部分文件上传,该客户端提供了一个专用方法,在底层实现了 [GraphQL 多部分请求规范](https://github.com/jaydenseric/graphql-multipart-request-spec)。
```typescript
import { MetadataApiClient } from 'twenty-sdk/generated';
@@ -846,7 +880,7 @@ uploadFile(
关键点:
* The `uploadFile` method is available on `MetadataApiClient` because the upload mutation is resolved by the `/metadata` endpoint.
* `uploadFile` 方法可在 `MetadataApiClient` 上使用,因为上传 mutation 由 `/metadata` 端点解析。
* 它使用该字段的 `universalIdentifier`(而不是其工作区特定的 ID),因此你的上传代码可以在安装了你的应用的任何工作区中使用——这与应用在其他地方引用字段的方式保持一致。
* 返回的 `url` 是一个签名 URL,你可以用它来访问已上传的文件。
@@ -26,12 +26,12 @@ export const MyComponent = () => {
};
return (
<IconPicker
disabled={false}
onChange={handleIconChange}
selectedIconKey={selectedIcon}
variant="primary"
/>
<IconPicker
disabled={false}
onChange={handleIconChange}
selectedIconKey={selectedIcon}
variant="primary"
/>
);
};
```
@@ -20,16 +20,16 @@ import { Select } from '@/ui/input/components/Select';
export const MyComponent = () => {
return (
<Select
className
disabled={false}
label="选择一个选项"
options={[
{ value: 'option1', label: '选项 A', Icon: IconTwentyStar },
{ value: 'option2', label: '选项 B', Icon: IconTwentyStar },
]}
value="option1"
/>
<Select
className
disabled={false}
label="选择一个选项"
options={[
{ value: 'option1', label: '选项 A', Icon: IconTwentyStar },
{ value: 'option2', label: '选项 B', Icon: IconTwentyStar },
]}
value="option1"
/>
);
};
@@ -28,18 +28,19 @@ export const MyComponent = () => {
};
return (
<TextInput
className
label="用户名"
onChange={handleChange}
fullWidth={false}
disableHotkeys={false}
error="用户名无效"
onKeyDown={handleKeyDown}
RightIcon={null}
/>
<TextInput
className
label="用户名"
onChange={handleChange}
fullWidth={false}
disableHotkeys={false}
error="用户名无效"
onKeyDown={handleKeyDown}
RightIcon={null}
/>
);
};
},{
```
@@ -81,15 +82,15 @@ import { AutosizeTextInput } from "@/ui/input/components/AutosizeTextInput";
export const MyComponent = () => {
return (
<AutosizeTextInput
onValidate={() => console.log("onValidate 函数已触发")}
minRows={1}
placeholder="写一条评论"
onFocus={() => console.log("onFocus 函数已触发")}
variant="icon"
buttonTitle
value="任务: "
/>
<AutosizeTextInput
onValidate={() => console.log("onValidate 函数已触发")}
minRows={1}
placeholder="写一条评论"
onFocus={() => console.log("onFocus 函数已触发")}
variant="icon"
buttonTitle
value="任务: "
/>
);
};
```

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