Compare commits

..
Author SHA1 Message Date
prastoin dfef76cd20 feat(server): extract hooks function 2026-02-26 12:16:00 +01:00
prastoin 7c632c4884 test(server): edge case 2026-02-26 11:52:25 +01:00
prastoin d2694a0865 mime type and legeacy write file 2026-02-26 11:26:21 +01:00
prastoin fdfdc44891 feat(server): basic tarball installation 2026-02-26 11:26:19 +01:00
Paul RastoinandGitHub 3aedce9af7 Fix remaining non v4 uuid universal identifier (#18263) 2026-02-26 11:24:01 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8e8ecfb8a3 Bump @sentry/react from 10.27.0 to 10.40.0 (#18252)
Bumps [@sentry/react](https://github.com/getsentry/sentry-javascript)
from 10.27.0 to 10.40.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/getsentry/sentry-javascript/releases"><code>@​sentry/react</code>'s
releases</a>.</em></p>
<blockquote>
<h2>10.40.0</h2>
<h3>Important Changes</h3>
<ul>
<li>
<p><strong>feat(tanstackstart-react): Add global sentry exception
middlewares (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/19330">#19330</a>)</strong></p>
<p>The <code>sentryGlobalRequestMiddleware</code> and
<code>sentryGlobalFunctionMiddleware</code> global middlewares capture
unhandled exceptions thrown in TanStack Start API routes and server
functions. Add them as the first entries in the
<code>requestMiddleware</code> and <code>functionMiddleware</code>
arrays of <code>createStart()</code>:</p>
<pre lang="ts"><code>import { createStart } from
'@tanstack/react-start/server';
import { sentryGlobalRequestMiddleware, sentryGlobalFunctionMiddleware }
from '@sentry/tanstackstart-react';
<p>export default createStart({
requestMiddleware: [sentryGlobalRequestMiddleware, myRequestMiddleware],
functionMiddleware: [sentryGlobalFunctionMiddleware,
myFunctionMiddleware],
});
</code></pre></p>
</li>
<li>
<p><strong>feat(tanstackstart-react)!: Export Vite plugin from
<code>@sentry/tanstackstart-react/vite</code> subpath (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/19182">#19182</a>)</strong></p>
<p>The <code>sentryTanstackStart</code> Vite plugin is now exported from
a dedicated subpath. Update your import:</p>
<pre lang="diff"><code>- import { sentryTanstackStart } from
'@sentry/tanstackstart-react';
+ import { sentryTanstackStart } from
'@sentry/tanstackstart-react/vite';
</code></pre>
</li>
<li>
<p><strong>fix(node-core): Reduce bundle size by removing apm-js-collab
and requiring pino &gt;= 9.10 (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/18631">#18631</a>)</strong></p>
<p>In order to keep receiving pino logs, you need to update your pino
version to &gt;= 9.10, the reason for the support bump is to reduce the
bundle size of the node-core SDK in frameworks that cannot tree-shake
the apm-js-collab dependency.</p>
</li>
<li>
<p><strong>fix(browser): Ensure user id is consistently added to
sessions (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/19341">#19341</a>)</strong></p>
<p>Previously, the SDK inconsistently set the user id on sessions,
meaning sessions were often lacking proper coupling to the user set for
example via <code>Sentry.setUser()</code>.
Additionally, the SDK incorrectly skipped starting a new session for the
first soft navigation after the pageload.
This patch fixes these issues. As a result, metrics around sessions,
like &quot;Crash Free Sessions&quot; or &quot;Crash Free Users&quot;
might change.
This could also trigger alerts, depending on your set thresholds and
conditions.
We apologize for any inconvenience caused!</p>
<p>While we're at it, if you're using Sentry in a Single Page App or
meta framework, you might want to give the new <code>'page'</code>
session lifecycle a try!
This new mode no longer creates a session per soft navigation but
continues the initial session until the next hard page refresh.
Check out the <a
href="https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/integrations/browsersession/">docs</a>
to learn more!</p>
</li>
<li>
<p><strong>ref!(gatsby): Drop Gatsby v2 support (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/19467">#19467</a>)</strong></p>
<p>We drop support for Gatsby v2 (which still relies on webpack 4) for a
critical security update in <a
href="https://github.com/getsentry/sentry-javascript-bundler-plugins/releases/tag/5.0.0">https://github.com/getsentry/sentry-javascript-bundler-plugins/releases/tag/5.0.0</a></p>
</li>
</ul>
<h3>Other Changes</h3>
<ul>
<li>feat(astro): Add support for Astro on CF Workers (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/19265">#19265</a>)</li>
<li>feat(cloudflare): Instrument async KV API (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/19404">#19404</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md"><code>@​sentry/react</code>'s
changelog</a>.</em></p>
<blockquote>
<h2>10.40.0</h2>
<h3>Important Changes</h3>
<ul>
<li>
<p><strong>feat(tanstackstart-react): Add global sentry exception
middlewares (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/19330">#19330</a>)</strong></p>
<p>The <code>sentryGlobalRequestMiddleware</code> and
<code>sentryGlobalFunctionMiddleware</code> global middlewares capture
unhandled exceptions thrown in TanStack Start API routes and server
functions. Add them as the first entries in the
<code>requestMiddleware</code> and <code>functionMiddleware</code>
arrays of <code>createStart()</code>:</p>
<pre lang="ts"><code>import { createStart } from
'@tanstack/react-start/server';
import { sentryGlobalRequestMiddleware, sentryGlobalFunctionMiddleware }
from '@sentry/tanstackstart-react/server';
<p>export default createStart({
requestMiddleware: [sentryGlobalRequestMiddleware, myRequestMiddleware],
functionMiddleware: [sentryGlobalFunctionMiddleware,
myFunctionMiddleware],
});
</code></pre></p>
</li>
<li>
<p><strong>feat(tanstackstart-react)!: Export Vite plugin from
<code>@sentry/tanstackstart-react/vite</code> subpath (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/19182">#19182</a>)</strong></p>
<p>The <code>sentryTanstackStart</code> Vite plugin is now exported from
a dedicated subpath. Update your import:</p>
<pre lang="diff"><code>- import { sentryTanstackStart } from
'@sentry/tanstackstart-react';
+ import { sentryTanstackStart } from
'@sentry/tanstackstart-react/vite';
</code></pre>
</li>
<li>
<p><strong>fix(node-core): Reduce bundle size by removing apm-js-collab
and requiring pino &gt;= 9.10 (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/18631">#18631</a>)</strong></p>
<p>In order to keep receiving pino logs, you need to update your pino
version to &gt;= 9.10, the reason for the support bump is to reduce the
bundle size of the node-core SDK in frameworks that cannot tree-shake
the apm-js-collab dependency.</p>
</li>
<li>
<p><strong>fix(browser): Ensure user id is consistently added to
sessions (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/19341">#19341</a>)</strong></p>
<p>Previously, the SDK inconsistently set the user id on sessions,
meaning sessions were often lacking proper coupling to the user set for
example via <code>Sentry.setUser()</code>.
Additionally, the SDK incorrectly skipped starting a new session for the
first soft navigation after the pageload.
This patch fixes these issues. As a result, metrics around sessions,
like &quot;Crash Free Sessions&quot; or &quot;Crash Free Users&quot;
might change.
This could also trigger alerts, depending on your set thresholds and
conditions.
We apologize for any inconvenience caused!</p>
<p>While we're at it, if you're using Sentry in a Single Page App or
meta framework, you might want to give the new <code>'page'</code>
session lifecycle a try!
This new mode no longer creates a session per soft navigation but
continues the initial session until the next hard page refresh.
Check out the <a
href="https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/integrations/browsersession/">docs</a>
to learn more!</p>
</li>
<li>
<p><strong>ref!(gatsby): Drop Gatsby v2 support (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/19467">#19467</a>)</strong></p>
<p>We drop support for Gatsby v2 (which still relies on webpack 4) for a
critical security update in <a
href="https://github.com/getsentry/sentry-javascript-bundler-plugins/releases/tag/5.0.0">https://github.com/getsentry/sentry-javascript-bundler-plugins/releases/tag/5.0.0</a></p>
</li>
</ul>
<h3>Other Changes</h3>
<ul>
<li>feat(astro): Add support for Astro on CF Workers (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/19265">#19265</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/663fd5e7e3c1808d4a636f001d768845f167668e"><code>663fd5e</code></a>
Increase bundler-tests timeout to 30s</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/8033ea380f0526cc863c6d50347fd5747ae5df32"><code>8033ea3</code></a>
release: 10.40.0</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/eb3c4d2489a77753377f7e3a320f18cd853ebf6a"><code>eb3c4d2</code></a>
Merge pull request <a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/19488">#19488</a>
from getsentry/prepare-release/10.40.0</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/9a10630c6b7524d053b96cfaafa14751b0611f33"><code>9a10630</code></a>
meta(changelog): Update changelog for 10.40.0</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/39d1ef77849223f7742999c808f7f23da0c42adf"><code>39d1ef7</code></a>
fix(deps): Bump to latest version of each minimatch major (<a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/19486">#19486</a>)</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/e8ed6d262f7f43cef8b04265794db83ab013f95c"><code>e8ed6d2</code></a>
test(nextjs): Deactivate canary test for cf-workers (<a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/19483">#19483</a>)</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/6eb320eb3e01985720238c8f08e3ac114502059b"><code>6eb320e</code></a>
chore(deps): Bump Sentry CLI to latest v2 (<a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/19477">#19477</a>)</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/8fc81d2cd4048fb41b49e773d4829d9fb799f16c"><code>8fc81d2</code></a>
fix: Bump bundler plugins to v5 (<a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/19468">#19468</a>)</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/365f7fab4e33d69363d4eb6d99e5f87e48672fba"><code>365f7fa</code></a>
chore(ci): Adapt max turns of triage issue agent (<a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/19473">#19473</a>)</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/11e5412d42f6126e5415d67d1418ffdb17f5caa6"><code>11e5412</code></a>
feat(tanstackstart-react)!: Export Vite plugin from
<code>@​sentry/tanstackstart-rea</code>...</li>
<li>Additional commits viewable in <a
href="https://github.com/getsentry/sentry-javascript/compare/10.27.0...10.40.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@sentry/react&package-manager=npm_and_yarn&previous-version=10.27.0&new-version=10.40.0)](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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-26 11:16:24 +01:00
martmullandGitHub 120096346a Add define post isntall logic function (#18248)
As title
2026-02-26 11:09:21 +01:00
Paul RastoinandGitHub fde8168a85 Centralized universal identifier validation on create (#18258) 2026-02-26 10:48:24 +01:00
Charles BochetandGitHub 2674589b44 Remove any recoil reference from project (#18250)
## Remove all Recoil references and replace with Jotai

### Summary

- Removed every occurrence of Recoil from the entire codebase, replacing
with Jotai equivalents where applicable
- Updated `README.md` tech stack: `Recoil` → `Jotai`
- Rewrote documentation code examples to use
`createAtomState`/`useAtomState` instead of `atom`/`useRecoilState`, and
removed `RecoilRoot` wrappers
- Cleaned up source code comment and Cursor rules that referenced Recoil
- Applied changes across all 13 locale translations (ar, cs, de, es, fr,
it, ja, ko, pt, ro, ru, tr, zh)
2026-02-26 10:28:40 +01:00
Charles BochetandGitHub f325509d96 Fix Jotai state leak in frontend tests causing unbounded memory growth (#18249)
## Summary

The global `jotaiStore` singleton was shared across all tests without
reset, leaking state between test suites and causing unbounded heap
growth within each Jest worker.

- `getJestMetadataAndApolloMocksWrapper` now creates a **fresh Jotai
store per wrapper** (equivalent of RecoilRoot isolation), so each test
gets clean state
- Added `workerIdleMemoryLimit: '512MB'` to recycle workers that still
accumulate memory
- Set `maxWorkers: 3` for CI

## Performance

Measured on 48 test suites (command-menu + views + object-record hooks),
single worker:

| Metric | Before | After |
|---|---|---|
| Peak heap | **1519 MB** | **~530 MB** (-65%) |
| Final heap | **1519 MB** | **437 MB** (-71%) |
| Growth pattern | Monotonic (never freed) | Bounded sawtooth (recycled
at 512MB) |
2026-02-26 01:20:25 +01:00
a9649b06d5 followup 18044 (#18213)
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-02-26 00:55:57 +01:00
Charles BochetandGitHub 0b4bf97f35 Fix twenty-sdk typecheck race condition (#18246)
## Fix SDK first-run build failure when generated API client is missing

### Problem

When running `twenty app:dev` for the first time, the build pipeline
crashes because:

1. The esbuild front-component watcher tries to resolve
`twenty-sdk/generated`, but the generated API client doesn't exist yet —
it's only created later in the pipeline after syncing with the server.
2. The typecheck plugin (`tsc --noEmit`) runs as a blocking esbuild
`onStart` hook, so even with stub files, type errors on the empty client
classes (e.g. `Property 'query' does not exist on type 'CoreApiClient'`)
cause the build to fail before the real client can be generated.

This creates a chicken-and-egg problem: the watchers need the generated
client to build, but the client is generated after the watchers produce
their output.

### Solution

**1. Stub generated client on startup**

Added `ensureGeneratedClientStub` to `ClientService` that writes minimal
placeholder files (`CoreApiClient`, `MetadataApiClient`) into
`node_modules/twenty-sdk/generated/` if the directory doesn't already
exist. This is called in `DevModeOrchestrator.start()` before any
watchers are created, so the `twenty-sdk/generated` import always
resolves.

**2. Skip typecheck on first sync round**

Made the esbuild typecheck plugin accept a `shouldSkipTypecheck`
callback. The orchestrator starts with `skipTypecheck = true` and passes
`() => this.skipTypecheck` through the watcher chain. After the real API
client is generated, the flag is flipped to `false`, so subsequent
rebuilds enforce full type checking with the real generated types.
2026-02-25 23:47:08 +01:00
4ea8245387 i18n - docs translations (#18245)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-25 23:42:24 +01:00
0fa8063054 Handle ObjectMetadataItem, FieldMetadataItem and NavigationMenuItem with SSE events (#18235)
This PR allows to handle ObjectMetadataItem, FieldMetadataItem and
NavigationMenuItem SSE events.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-25 23:18:00 +01:00
Abdullah.andGitHub 546114d07f fix: next-mdx-remote related dependabot alerts (#18244)
Resolves [Dependabot Alert
485](https://github.com/twentyhq/twenty/security/dependabot/485) and
[Dependabot Alert
486](https://github.com/twentyhq/twenty/security/dependabot/486).

Bumped up `next-mdx-remote` to v6.0.0 and `remark-gfm` to v4.0.1 for
compatibility - no breaking changes for our use-case.
2026-02-25 23:12:23 +01:00
92824c6533 i18n - translations (#18243)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-02-25 22:34:27 +01:00
Charles BochetandGitHub bc4ae35bc4 Rework atom naming (#18240)
## Summary

- **Enhanced the `matching-state-variable` ESLint rule** to enforce
consistent naming for `ComponentState`, `FamilyState`, and
`ComponentFamilyState` hooks — previously it only covered `useAtomState`
and `useAtomStateValue`
- **The rule now checks 12 hooks** across three categories: value hooks
(`useAtomComponentStateValue`, `useAtomFamilyStateValue`, etc.), state
hooks (`useAtomComponentState`, `useAtomComponentFamilyState`), and
setter hooks (`useSetAtomState`, `useSetAtomComponentState`,
`useSetAtomFamilyState`, `useSetAtomComponentFamilyState`)
- **Fixed all 225 resulting lint violations** across 151 files, renaming
variables to match their state atom names (e.g. `currentViewId` →
`contextStoreCurrentViewId`, `selectedRecord` → `recordStore`). Cases
where the same state is accessed with different family keys/instance IDs
are suppressed with `eslint-disable-next-line`.

## Naming convention

| Hook | State argument | Valid | Invalid |
|------|---------------|-------|---------|
| `useAtomStateValue` | `fooState` | `const foo = ...` | `const bar =
...` |
| `useAtomComponentStateValue` | `fooComponentState` | `const foo = ...`
| `const bar = ...` |
| `useAtomFamilyStateValue` | `fooFamilyState` | `const foo = ...` |
`const bar = ...` |
| `useAtomComponentFamilyStateValue` | `fooComponentFamilyState` |
`const foo = ...` | `const bar = ...` |
| `useAtomState` | `fooState` | `const [foo, setFoo] = ...` | `const
[bar, setBar] = ...` |
| `useAtomComponentState` | `fooComponentState` | `const [foo, setFoo] =
...` | `const [bar, setBar] = ...` |
| `useAtomComponentFamilyState` | `fooComponentFamilyState` | `const
[foo, setFoo] = ...` | `const [bar, setBar] = ...` |
| `useSetAtomState` | `fooState` | `const setFoo = ...` | `const setBar
= ...` |
| `useSetAtomComponentState` | `fooComponentState` | `const setFoo =
...` | `const setBar = ...` |
| `useSetAtomFamilyState` | `fooFamilyState` | `const setFoo = ...` |
`const setBar = ...` |
| `useSetAtomComponentFamilyState` | `fooComponentFamilyState` | `const
setFoo = ...` | `const setBar = ...` |
2026-02-25 22:28:25 +01:00
EtienneandGitHub 1b805a36f3 Fix page layout widget creation with front component (#18242) 2026-02-25 22:07:51 +01:00
Baptiste DevessierandGitHub f7ea1d9500 Update doc for front components (#18196) 2026-02-25 22:04:37 +01:00
Abdullah.andGitHub cbb0f212cf fix: fast-xml-parser related dependabot alerts (#18241)
Resolves [Dependabot Alert
459](https://github.com/twentyhq/twenty/security/dependabot/459) and
[Dependabot Alert
483](https://github.com/twentyhq/twenty/security/dependabot/483).

Upgraded AWS SDKs pulling in fast-xml-parser as transitive dependency.
2026-02-25 22:01:34 +01:00
WeikoandGitHub 0af980a783 introduce metadata api client to twenty sdk (#18233)
Logic function: hello-world.ts
```typescript
import { CoreApiClient } from 'twenty-sdk/generated/core';
import { MetadataApiClient } from 'twenty-sdk/generated/metadata';

const handler = async () => {
  const coreClient = new CoreApiClient();
  const metadataClient = new MetadataApiClient();

  // Query the core /graphql endpoint — fetch some people
  const coreResult = await coreClient.query({
    people: {
      edges: {
        node: {
          id: true,
          name: {
            firstName: true,
            lastName: true,
          },
        },
      },
    },
  });

  // Query the metadata /metadata endpoint — fetch current workspace
  const metadataResult = await metadataClient.query({
    currentWorkspace: {
      id: true,
      displayName: true,
    },
  });

  return {
    coreResponse: coreResult,
    metadataResponse: metadataResult,
  };
};
```
With route trigger should now produce:
<img width="582" height="238" alt="Screenshot 2026-02-25 at 17 14 29"
src="https://github.com/user-attachments/assets/8c597113-7552-4d32-845a-352083d84ac7"
/>

```json
{
  "coreResponse": {
    "people": {
      "edges": [
        {
          "node": {
            "id": "20202020-b000-4485-94de-70c2a98daef2",
            "name": {
              "firstName": "Jeffery",
              "lastName": "Griffin"
            }
          }
        },
        {
          "node": {
            "id": "20202020-b003-415a-9051-133248495f7f",
            "name": {
              "firstName": "Terry",
              "lastName": "Melendez"
            }
          }
        },
        {
          "node": {
            "id": "20202020-b00e-4bc1-87c8-00aeb49c10f8",
            "name": {
              "firstName": "Lee",
              "lastName": "Jones"
            }
          }
        },
        {
          "node": {
            "id": "20202020-b012-44c1-9fdc-90f110962d07",
            "name": {
              "firstName": "Sarah",
              "lastName": "Hernandez"
            }
          }
        },
      ]
    }
  },
...
  "metadataResponse": {
    "currentWorkspace": {
      "id": "20202020-1c25-4d02-bf25-6aeccf7ea419",
      "displayName": "Apple"
    }
  }
}
2026-02-25 21:42:05 +01:00
Charles BochetandGitHub e01b641a05 Introduce npx nx mock:generate twenty-front (#18237)
## Add codegen script for frontend test mock data

### Summary

- Adds a new `npx nx mock:generate twenty-front` and
`generate-mock-data.ts` script that fetches object metadata from a
running server's `/metadata` endpoint, authenticates with default seeds,
and writes the result to a generated TypeScript file
(`src/testing/mock-data/generated/mock-metadata-query-result.ts`). This
replaces hand-maintained mock metadata with server-sourced data,
ensuring tests always reflect the real schema.
- Updates all frontend tests to be compatible with the newly generated
metadata, fixing hard-coded GraphQL queries, Zod validation schemas,
snapshot expectations, and Apollo mock mismatches.

### What changed

**New files**
- `scripts/generate-mock-data.ts` — codegen script that authenticates
against the server, queries `/metadata` for all object metadata (with
explicit `__typename` at every level), and writes a typed `.ts` file.
- `project.json` — added `mock:generate` Nx target (`dotenv npx tsx
scripts/generate-mock-data.ts`).

**Schema validation updates**
- `objectMetadataItemSchema.ts` — added `universalIdentifier`, made
`duplicateCriteria` nullable.
- `fieldMetadataItemSchema.ts` — added `universalIdentifier`, `morphId`,
`morphRelations`, restructured relation schema.
- `indexMetadataItemSchema.ts` — added optional `isCustom` field.
2026-02-25 21:40:05 +01:00
nitinandGitHub 50be97422d Fix dropdown scroll by restoring scroll container nesting order (#18239) 2026-02-25 19:55:08 +01:00
618 changed files with 31314 additions and 24496 deletions
+4 -4
View File
@@ -36,18 +36,18 @@ export const userByIdState = createAtomFamilyState<User | null, string>({
## Jotai Hooks
```typescript
// useAtomState - read and write (like useRecoilState)
// useAtomState - read and write
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
// useAtomStateValue - read only (like useRecoilValue)
// useAtomStateValue - read only
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
// useSetAtomState - write only (like useSetRecoilState)
// useSetAtomState - write only
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
```
## Provider
Jotai works without a Provider by default (unlike Recoil's RecoilRoot). For scoped stores or testing, use `Provider` from `jotai`.
Jotai works without a Provider by default. For scoped stores or testing, use `Provider` from `jotai`.
## Local State Guidelines
```typescript
+2 -2
View File
@@ -27,7 +27,7 @@ jobs:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
runs-on: depot-ubuntu-24.04
strategy:
matrix:
task: [lint, typecheck, test, validate]
@@ -52,7 +52,7 @@ jobs:
ci-zapier-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
runs-on: depot-ubuntu-24.04
needs: [changed-files-check, zapier-test]
steps:
- name: Fail job if any needs failed
+1 -1
View File
@@ -109,7 +109,7 @@ Below are a few features we have implemented to date:
- [TypeScript](https://www.typescriptlang.org/)
- [Nx](https://nx.dev/)
- [NestJS](https://nestjs.com/), with [BullMQ](https://bullmq.io/), [PostgreSQL](https://www.postgresql.org/), [Redis](https://redis.io/)
- [React](https://reactjs.org/), with [Recoil](https://recoiljs.org/), [Emotion](https://emotion.sh/) and [Lingui](https://lingui.dev/)
- [React](https://reactjs.org/), with [Jotai](https://jotai.org/), [Emotion](https://emotion.sh/) and [Lingui](https://lingui.dev/)
+1 -1
View File
@@ -126,7 +126,7 @@
"configurations": {
"ci": {
"ci": true,
"maxWorkers": 3
"maxWorkers": 1
},
"coverage": {
"coverageReporters": ["lcov", "text"]
+1 -1
View File
@@ -48,7 +48,7 @@
"react-responsive": "^9.0.2",
"react-router-dom": "^6.4.4",
"react-tooltip": "^5.13.1",
"remark-gfm": "^3.0.1",
"remark-gfm": "^4.0.1",
"rxjs": "^7.2.0",
"semver": "^7.5.4",
"slash": "^5.1.0",
+6 -2
View File
@@ -41,7 +41,7 @@ yarn twenty auth:login
yarn twenty entity:add
# Start dev mode: watches, builds, and syncs local changes to your workspace
# (also auto-generates a typed API client in node_modules/twenty-sdk/generated)
# (also auto-generates typed API clients — CoreApiClient and MetadataApiClient — in node_modules/twenty-sdk/generated)
yarn twenty app:dev
# Watch your application's function logs
@@ -50,6 +50,9 @@ yarn twenty function:logs
# Execute a function with a JSON payload
yarn twenty function:execute -n my-function -p '{"key": "value"}'
# Execute the pre-install function
yarn twenty function:execute --preInstall
# Execute the post-install function
yarn twenty function:execute --postInstall
@@ -92,6 +95,7 @@ In interactive mode, you can pick from:
**Core files (always created):**
- `application-config.ts` — Application metadata configuration
- `roles/default-role.ts` — Default role for logic functions
- `logic-functions/pre-install.ts` — Pre-install logic function (runs before app installation)
- `logic-functions/post-install.ts` — Post-install logic function (runs after app installation)
- TypeScript configuration, ESLint, package.json, .gitignore
- A prewired `twenty` script that delegates to the `twenty` CLI from twenty-sdk
@@ -110,7 +114,7 @@ In interactive mode, you can pick from:
- Use `yarn twenty auth:login` to authenticate with your Twenty workspace.
- Explore the generated project and add your first entity with `yarn twenty entity:add` (logic functions, front components, objects, roles, views, navigation menu items, skills).
- Use `yarn twenty app:dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
- Types are autogenerated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated`.
- Two typed API clients are autogenerated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated`: `CoreApiClient` (for workspace data via `/graphql`) and `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`).
## Publish your application
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "create-twenty-app",
"version": "0.6.1",
"version": "0.6.2",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",
@@ -3,6 +3,9 @@
- Documentation: https://docs.twenty.com/developers/extend/capabilities/apps
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-sdk/src/cli/__tests__/apps/rich-app
## UUID requirement
- All generated UUIDs must be valid UUID v4.
## Common Pitfalls
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
@@ -375,6 +375,18 @@ describe('copyBaseApplicationProject', () => {
),
),
).toBe(true);
// Install functions should always exist
expect(
await fs.pathExists(
join(srcPath, 'logic-functions', 'pre-install.ts'),
),
).toBe(true);
expect(
await fs.pathExists(
join(srcPath, 'logic-functions', 'post-install.ts'),
),
).toBe(true);
});
});
@@ -398,6 +410,18 @@ describe('copyBaseApplicationProject', () => {
await fs.pathExists(join(srcPath, 'roles', DEFAULT_ROLE_FILE_NAME)),
).toBe(true);
// Install functions should always exist (not gated by exampleOptions)
expect(
await fs.pathExists(
join(srcPath, 'logic-functions', 'pre-install.ts'),
),
).toBe(true);
expect(
await fs.pathExists(
join(srcPath, 'logic-functions', 'post-install.ts'),
),
).toBe(true);
// Example files should not exist
expect(
await fs.pathExists(join(srcPath, 'objects', 'example-object.ts')),
@@ -676,4 +700,124 @@ describe('copyBaseApplicationProject', () => {
expect(content).toContain('position: 0');
});
});
describe('pre-install logic function', () => {
it('should create pre-install.ts with definePreInstallLogicFunction and typed payload', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
exampleOptions: NO_EXAMPLES,
});
const preInstallPath = join(
testAppDirectory,
'src',
'logic-functions',
'pre-install.ts',
);
expect(await fs.pathExists(preInstallPath)).toBe(true);
const content = await fs.readFile(preInstallPath, 'utf8');
expect(content).toContain(
"import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk'",
);
expect(content).toContain(
'export default definePreInstallLogicFunction({',
);
expect(content).toContain("name: 'pre-install'");
expect(content).toContain('timeoutSeconds: 300');
expect(content).toContain(
'const handler = async (payload: InstallLogicFunctionPayload): Promise<void>',
);
expect(content).toContain('payload.previousVersion');
// Verify it has a universalIdentifier (UUID format)
expect(content).toMatch(
/universalIdentifier: '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'/,
);
});
it('should always create pre-install.ts regardless of example options', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
exampleOptions: NO_EXAMPLES,
});
const preInstallPath = join(
testAppDirectory,
'src',
'logic-functions',
'pre-install.ts',
);
expect(await fs.pathExists(preInstallPath)).toBe(true);
});
});
describe('post-install logic function', () => {
it('should create post-install.ts with definePostInstallLogicFunction and typed payload', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
exampleOptions: NO_EXAMPLES,
});
const postInstallPath = join(
testAppDirectory,
'src',
'logic-functions',
'post-install.ts',
);
expect(await fs.pathExists(postInstallPath)).toBe(true);
const content = await fs.readFile(postInstallPath, 'utf8');
expect(content).toContain(
"import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk'",
);
expect(content).toContain(
'export default definePostInstallLogicFunction({',
);
expect(content).toContain("name: 'post-install'");
expect(content).toContain('timeoutSeconds: 300');
expect(content).toContain(
'const handler = async (payload: InstallLogicFunctionPayload): Promise<void>',
);
expect(content).toContain('payload.previousVersion');
// Verify it has a universalIdentifier (UUID format)
expect(content).toMatch(
/universalIdentifier: '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'/,
);
});
it('should always create post-install.ts regardless of example options', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
exampleOptions: NO_EXAMPLES,
});
const postInstallPath = join(
testAppDirectory,
'src',
'logic-functions',
'post-install.ts',
);
expect(await fs.pathExists(postInstallPath)).toBe(true);
});
});
});
@@ -97,6 +97,12 @@ export const copyBaseApplicationProject = async ({
});
}
await createDefaultPreInstallFunction({
appDirectory: sourceFolderPath,
fileFolder: 'logic-functions',
fileName: 'pre-install.ts',
});
await createDefaultPostInstallFunction({
appDirectory: sourceFolderPath,
fileFolder: 'logic-functions',
@@ -268,6 +274,36 @@ export default defineLogicFunction({
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createDefaultPreInstallFunction = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const universalIdentifier = v4();
const content = `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: '${universalIdentifier}',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
handler,
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createDefaultPostInstallFunction = async ({
appDirectory,
fileFolder,
@@ -279,16 +315,14 @@ const createDefaultPostInstallFunction = async ({
}) => {
const universalIdentifier = v4();
const content = `import { defineLogicFunction } from 'twenty-sdk';
const content = `import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '${universalIdentifier}';
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: '${universalIdentifier}',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
@@ -477,14 +511,12 @@ const createApplicationConfig = async ({
}) => {
const content = `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: '${v4()}',
displayName: '${displayName}',
description: '${description ?? ''}',
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
`;
@@ -7,9 +7,9 @@ This document outlines the best practices you should follow when working on the
## State management
React and Recoil handle state management in the codebase.
React and Jotai handle state management in the codebase.
### Use `useRecoilState` to store state
### Use Jotai atoms to store state
It's good practice to create as many atoms as you need to store your state.
@@ -20,13 +20,16 @@ It's better to use extra atoms than trying to be too concise with props drilling
</Warning>
```tsx
export const myAtomState = atom({
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
export const myAtomState = createAtomState<string>({
key: 'myAtomState',
default: 'default value',
defaultValue: 'default value',
});
export const MyComponent = () => {
const [myAtom, setMyAtom] = useRecoilState(myAtomState);
const [myAtom, setMyAtom] = useAtomState(myAtomState);
return (
<div>
@@ -43,7 +46,7 @@ export const MyComponent = () => {
Avoid using `useRef` to store state.
If you want to store state, you should use `useState` or `useRecoilState`.
If you want to store state, you should use `useState` or Jotai atoms with `useAtomState`.
See [how to manage re-renders](#managing-re-renders) if you feel like you need `useRef` to prevent some re-renders from happening.
@@ -83,8 +86,8 @@ You can apply the same for data fetching logic, with Apollo hooks.
// ❌ Bad, will cause re-renders even if data is not changing,
// because useEffect needs to be re-evaluated
export const PageComponent = () => {
const [data, setData] = useRecoilState(dataState);
const [someDependency] = useRecoilState(someDependencyState);
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
@@ -96,9 +99,7 @@ export const PageComponent = () => {
};
export const App = () => (
<RecoilRoot>
<PageComponent />
</RecoilRoot>
<PageComponent />
);
```
@@ -106,14 +107,14 @@ export const App = () => (
// ✅ Good, will not cause re-renders if data is not changing,
// because useEffect is re-evaluated in another sibling component
export const PageComponent = () => {
const [data, setData] = useRecoilState(dataState);
const [data, setData] = useAtomState(dataState);
return <div>{data}</div>;
};
export const PageData = () => {
const [data, setData] = useRecoilState(dataState);
const [someDependency] = useRecoilState(someDependencyState);
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
@@ -125,16 +126,16 @@ export const PageData = () => {
};
export const App = () => (
<RecoilRoot>
<>
<PageData />
<PageComponent />
</RecoilRoot>
</>
);
```
### Use recoil family states and recoil family selectors
### Use atom family states and selectors
Recoil family states and selectors are a great way to avoid re-renders.
Atom family states and selectors are a great way to avoid re-renders.
They are useful when you need to store a list of items.
@@ -83,9 +83,9 @@ See [Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks) for more de
### States
Contains the state management logic. [RecoilJS](https://recoiljs.org) handles this.
Contains the state management logic. [Jotai](https://jotai.org) handles this.
- Selectors: See [RecoilJS Selectors](https://recoiljs.org/docs/basic-tutorial/selectors) for more details.
- Selectors: Derived atoms (using `createAtomSelector`) compute values from other atoms and are automatically memoized.
React's built-in state management still handles state within a component.
@@ -52,7 +52,7 @@ The project has a clean and simple stack, with minimal boilerplate code.
- [React](https://react.dev/)
- [Apollo](https://www.apollographql.com/docs/)
- [GraphQL Codegen](https://the-guild.dev/graphql/codegen)
- [Recoil](https://recoiljs.org/docs/introduction/core-concepts)
- [Jotai](https://jotai.org/)
- [TypeScript](https://www.typescriptlang.org/)
**Testing**
@@ -76,7 +76,7 @@ To avoid unnecessary [re-renders](/developers/contribute/capabilities/frontend-d
### State Management
[Recoil](https://recoiljs.org/docs/introduction/core-concepts) handles state management.
[Jotai](https://jotai.org/) handles state management.
See [best practices](/developers/contribute/capabilities/frontend-development/best-practices-front#state-management) for more information on state management.
@@ -159,7 +159,7 @@ export enum PageHotkeyScope {
}
```
Internally, the currently selected scope is stored in a Recoil state that is shared across the application :
Internally, the currently selected scope is stored in a Jotai atom that is shared across the application :
```tsx
export const currentHotkeyScopeState = createState<HotkeyScope>({
@@ -168,10 +168,10 @@ export const currentHotkeyScopeState = createState<HotkeyScope>({
});
```
But this Recoil state should never be handled manually ! We'll see how to use it in the next section.
But this atom should never be handled manually ! We'll see how to use it in the next section.
## How is it working internally?
We made a thin wrapper on top of [react-hotkeys-hook](https://react-hotkeys-hook.vercel.app/docs/intro) that makes it more performant and avoids unnecessary re-renders.
We also create a Recoil state to handle the hotkey scope state and make it available everywhere in the application.
We also create a Jotai atom to handle the hotkey scope state and make it available everywhere in the application.
@@ -60,6 +60,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
@@ -79,7 +82,7 @@ When you run `npx create-twenty-app@latest my-twenty-app`, the scaffolder:
- Copies a minimal base application into `my-twenty-app/`
- Adds a local `twenty-sdk` dependency and Yarn 4 configuration
- Creates config files and scripts wired to the `twenty` CLI
- Generates core files (application config, default function role, post-install function) plus example files based on the scaffolding mode
- Generates core files (application config, default function role, pre-install and post-install functions) plus example files based on the scaffolding mode
A freshly scaffolded app with the default `--exhaustive` mode looks like this:
@@ -106,6 +109,7 @@ my-twenty-app/
│ └── example-field.ts # Example standalone field definition
├── logic-functions/
│ ├── 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 # Example front component
@@ -117,7 +121,7 @@ my-twenty-app/
└── example-skill.ts # Example AI agent skill definition
```
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.
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`). With `--interactive`, you choose which example files to include.
At a high level:
@@ -138,6 +142,8 @@ The SDK detects entities by parsing your TypeScript files for **`export default
|-----------------|-------------|
| `defineObject()` | Custom object definitions |
| `defineLogicFunction()` | Logic function definitions |
| `definePreInstallLogicFunction()` | Pre-install logic function (runs before installation) |
| `definePostInstallLogicFunction()` | Post-install logic function (runs after installation) |
| `defineFrontComponent()` | Front component definitions |
| `defineRole()` | Role definitions |
| `defineField()` | Field extensions for existing objects |
@@ -163,7 +169,7 @@ export default defineObject({
Later commands will add more files and folders:
- `yarn twenty app:dev` will auto-generate a typed API client in `node_modules/twenty-sdk/generated` (typed Twenty client + workspace types).
- `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 entity:add` will add entity definition files under `src/` for your custom objects, functions, front components, roles, skills, and more.
## Authentication
@@ -212,6 +218,8 @@ The SDK provides helper functions for defining your app entities. As described i
| `defineApplication()` | Configure application metadata (required, one per app) |
| `defineObject()` | Define custom objects with fields |
| `defineLogicFunction()` | Define logic functions with handlers |
| `definePreInstallLogicFunction()` | Define a pre-install logic function (one per app) |
| `definePostInstallLogicFunction()` | Define a post-install logic function (one per app) |
| `defineFrontComponent()` | Define front components for custom UI |
| `defineRole()` | Configure role permissions and object access |
| `defineField()` | Extend existing objects with additional fields |
@@ -318,6 +326,7 @@ Every app has a single `application-config.ts` file that describes:
- **Who the app is**: identifiers, display name, and description.
- **How its functions run**: which role they use for permissions.
- **(Optional) variables**: keyvalue pairs exposed to your functions as environment variables.
- **(Optional) pre-install function**: a logic function that runs before the app is installed.
- **(Optional) post-install function**: a logic function that runs after the app is installed.
Use `defineApplication()` to define your application configuration:
@@ -326,7 +335,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',
@@ -342,7 +350,6 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -350,7 +357,7 @@ Notes:
- `universalIdentifier` fields are deterministic IDs you own; generate them once and keep them stable across syncs.
- `applicationVariables` become environment variables for your functions (for example, `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`).
- `defaultRoleUniversalIdentifier` must match the role file (see below).
- `postInstallLogicFunctionUniversalIdentifier` (optional) points to a logic function that runs automatically after the app is installed. See [Post-install functions](#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).
#### Roles and permissions
@@ -423,10 +430,10 @@ Each function file uses `defineLogicFunction()` to export a configuration with a
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
import { CoreApiClient, type Person } from 'twenty-sdk/generated';
const handler = async (params: RoutePayload) => {
const client = new Twenty(); // generated typed client
const client = new CoreApiClient();
const name = 'name' in params.queryStringParameters
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
@@ -483,6 +490,43 @@ Notes:
- The `triggers` array is optional. Functions without triggers can be used as utility functions called by other functions.
- You can mix multiple trigger types in a single function.
### 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
```
Key points:
- 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`.
### 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.
@@ -491,16 +535,14 @@ When you scaffold a new app with `create-twenty-app`, a post-install function is
```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,
@@ -508,17 +550,6 @@ export default defineLogicFunction({
});
```
The function is wired into your app by referencing its universal identifier in `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"
@@ -526,8 +557,10 @@ yarn twenty function:execute --postInstall
```
Key points:
- 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.
- 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()`.
- 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`.
@@ -635,10 +668,10 @@ To mark a logic function as a tool, set `isTool: true` and provide a `toolInputS
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/generated';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new Twenty();
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
@@ -696,7 +729,7 @@ Key points:
Front components let you build custom React components that render within Twenty's UI. Use `defineFrontComponent()` to define components with built-in validation:
```typescript
// src/my-widget.front-component.tsx
// src/front-components/my-widget.tsx
import { defineFrontComponent } from 'twenty-sdk';
const MyWidget = () => {
@@ -718,14 +751,13 @@ export default defineFrontComponent({
Key points:
- Front components are React components that render in isolated contexts within Twenty.
- Use the `*.front-component.tsx` file suffix for automatic detection.
- The `component` field references your React component.
- Components are built and synced automatically during `yarn twenty app:dev`.
You can create new front components in two ways:
- **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new front component.
- **Manual**: Create a new `*.front-component.tsx` file and use `defineFrontComponent()`.
- **Manual**: Create a new `.tsx` file and use `defineFrontComponent()`, following the same pattern.
### Skills
@@ -761,18 +793,24 @@ 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 client
### Generated typed clients
The typed client is auto-generated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated` based on your workspace schema. Use it in your functions:
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
```typescript
import Twenty from '~/generated';
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';
const client = new Twenty();
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
```
The client is re-generated automatically by `yarn twenty app:dev` whenever your objects or fields change.
Both clients are re-generated automatically by `yarn twenty app:dev` whenever your objects or fields change.
#### Runtime credentials in logic functions
@@ -788,17 +826,17 @@ Notes:
#### Uploading files
The generated `Twenty` client includes an `uploadFile` method for attaching files to file-type fields on your workspace objects. Because standard GraphQL clients do not support multipart file uploads natively, the client provides this dedicated method that implements the [GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec) under the hood.
The generated `MetadataApiClient` includes an `uploadFile` method for attaching files to file-type fields on your workspace objects. Because standard GraphQL clients do not support multipart file uploads natively, the client provides this dedicated method that implements the [GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec) under the hood.
```typescript
import Twenty from '~/generated';
import { MetadataApiClient } from 'twenty-sdk/generated';
import * as fs from 'fs';
const client = new Twenty();
const metadataClient = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
const uploadedFile = await client.uploadFile(
const uploadedFile = await metadataClient.uploadFile(
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type (defaults to 'application/octet-stream')
@@ -828,7 +866,7 @@ uploadFile(
| `fieldMetadataUniversalIdentifier` | `string` | The `universalIdentifier` of the file-type field on your object |
Key points:
- The method sends the file to the **metadata endpoint** (not the main GraphQL endpoint), where the upload mutation is resolved.
- The `uploadFile` method is available on `MetadataApiClient` because the upload mutation is resolved by the `/metadata` endpoint.
- It uses the field's `universalIdentifier` (not its workspace-specific ID), so your upload code works across any workspace where your app is installed — consistent with how apps reference fields everywhere else.
- The returned `url` is a signed URL you can use to access the uploaded file.
@@ -6,9 +6,9 @@ title: أفضل الممارسات
## إدارة الحالة
تقوم React و Recoil بإدارة الحالة في قاعدة الشيفرة.
تقوم React و Jotai بإدارة الحالة في قاعدة الشيفرة.
### استخدم `useRecoilState` لتخزين الحالة
### استخدم `useAtomState` لتخزين الحالة
من الجيد إنشاء أكبر عدد ممكن من الذرات لتخزين الحالة الخاصة بك.
@@ -19,13 +19,16 @@ title: أفضل الممارسات
</Warning>
```tsx
export const myAtomState = atom({
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
export const myAtomState = createAtomState<string>({
key: 'myAtomState',
default: 'default value',
defaultValue: 'default value',
});
export const MyComponent = () => {
const [myAtom, setMyAtom] = useRecoilState(myAtomState);
const [myAtom, setMyAtom] = useAtomState(myAtomState);
return (
<div>
@@ -42,7 +45,7 @@ export const MyComponent = () => {
تجنب استخدام `useRef` لتخزين الحالة.
If you want to store state, you should use `useState` or `useRecoilState`.
If you want to store state, you should use `useState` or `useAtomState`.
انظر [كيفية إدارة إعادة العرض](#managing-re-renders) إذا شعرت أنك بحاجة إلى `useRef` لمنع بعض إعادة العرض من الحدوث.
@@ -82,8 +85,8 @@ If you feel like you need to add a `useEffect` in your root component, you shoul
// ❌ سيّئ، سيتسبب في إعادة التصيير حتى إذا لم تتغير البيانات،
// لأن useEffect يحتاج إلى إعادة التقييم
export const PageComponent = () => {
const [data, setData] = useRecoilState(dataState);
const [someDependency] = useRecoilState(someDependencyState);
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
@@ -95,9 +98,7 @@ export const PageComponent = () => {
};
export const App = () => (
<RecoilRoot>
<PageComponent />
</RecoilRoot>
<PageComponent />
);
```
@@ -105,14 +106,14 @@ export const App = () => (
// ✅ جيّد، لن يتسبب في إعادة التصيير إذا لم تتغير البيانات،
// لأن useEffect يُعاد تقييمه في مكوّن شقيق آخر
export const PageComponent = () => {
const [data, setData] = useRecoilState(dataState);
const [data, setData] = useAtomState(dataState);
return <div>{data}</div>;
};
export const PageData = () => {
const [data, setData] = useRecoilState(dataState);
const [someDependency] = useRecoilState(someDependencyState);
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
@@ -124,16 +125,16 @@ export const PageData = () => {
};
export const App = () => (
<RecoilRoot>
<>
<PageData />
<PageComponent />
</RecoilRoot>
</>
);
```
### استخدم حالات عائلة Recoil ومحددات عائلة Recoil
### استخدم حالات عائلة Jotai ومحددات عائلة Jotai
حالات عائلة Recoil والمحددات تعتبر طريقة رائعة لتجنب إعادة العرض.
حالات عائلة Jotai والمحددات تعتبر طريقة رائعة لتجنب إعادة العرض.
إنها مفيدة عندما تحتاج إلى تخزين قائمة من العناصر.
@@ -82,9 +82,9 @@ module1
### الحالات
تشمل منطق إدارة الحالة. [RecoilJS](https://recoiljs.org) يتولّى ذلك.
تشمل منطق إدارة الحالة. [Jotai](https://jotai.org) يتولّى ذلك.
* المحددات: انظر [RecoilJS Selectors](https://recoiljs.org/docs/basic-tutorial/selectors) لمزيد من التفاصيل.
* المحددات: الذرات المشتقة (باستخدام `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)"
* "[Recoil](https://recoiljs.org/docs/introduction/core-concepts)"
* "[Jotai](https://jotai.org/)"
* "[TypeScript](https://www.typescriptlang.org/)"
**الاختبار**
@@ -73,7 +73,7 @@ To avoid unnecessary [re-renders](/l/ar/developers/contribute/capabilities/front
### "إدارة الحالة"
"[Recoil](https://recoiljs.org/docs/introduction/core-concepts) يتعامل مع إدارة الحالة."
"[Jotai](https://jotai.org/) يتعامل مع إدارة الحالة."
"راجع [أفضل الممارسات](/l/ar/developers/contribute/capabilities/frontend-development/best-practices-front#state-management) لمزيد من المعلومات حول إدارة الحالة."
@@ -160,7 +160,7 @@ export enum PageHotkeyScope {
}
```
داخليًا، يتم تخزين النطاق المحدد حاليًا في حالة Recoil مشتركة عبر التطبيق:
داخليًا، يتم تخزين النطاق المحدد حاليًا في حالة Jotai مشتركة عبر التطبيق:
```tsx
export const currentHotkeyScopeState = createState<HotkeyScope>({
@@ -169,10 +169,10 @@ export const currentHotkeyScopeState = createState<HotkeyScope>({
});
```
لكن لا يجب التعامل مع هذه الحالة Recoil يدويًا! سنرى كيف يمكن استخدامها في القسم التالي.
لكن لا يجب التعامل مع هذه الحالة Jotai يدويًا! سنرى كيف يمكن استخدامها في القسم التالي.
## كيف يعمل داخليًا؟
قمنا بإنشاء غلاف رقيق فوق [react-hotkeys-hook](https://react-hotkeys-hook.vercel.app/docs/intro) والذي يجعله أكثر كفاءة ويتجنب عمليات إعادة التقديم غير الضرورية.
ونقوم أيضًا بإنشاء حالة Recoil للتعامل مع حالة نطاق المفتاح وجعلها متاحة في جميع أنحاء التطبيق.
ونقوم أيضًا بإنشاء حالة Jotai للتعامل مع حالة نطاق المفتاح وجعلها متاحة في جميع أنحاء التطبيق.
@@ -165,7 +165,7 @@ export default defineObject({
ستضيف الأوامر اللاحقة مزيدًا من الملفات والمجلدات:
* `yarn twenty app:dev` سيولّد تلقائيًا عميل API مضبوط الأنواع في `node_modules/twenty-sdk/generated` (عميل Twenty مضبوط الأنواع + أنواع مساحة العمل).
* سيقوم `yarn twenty app:dev` بتوليد عميلين API مضبوطي الأنواع تلقائيًا في `node_modules/twenty-sdk/generated`: `CoreApiClient` (لبيانات مساحة العمل عبر `/graphql`) و`MetadataApiClient` (لتكوين مساحة العمل وتحميل الملفات عبر `/metadata`).
* `yarn twenty entity:add` سيضيف ملفات تعريف الكيانات ضمن `src/` لكائناتك المخصّصة، والوظائف، ومكوّنات الواجهة الأمامية، والأدوار، والمهارات، وغير ذلك.
## المصادقة
@@ -426,10 +426,10 @@ export default defineRole({
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
import { CoreApiClient, type Person } from 'twenty-sdk/generated';
const handler = async (params: RoutePayload) => {
const client = new Twenty(); // generated typed client
const client = new CoreApiClient();
const name = 'name' in params.queryStringParameters
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
@@ -644,10 +644,10 @@ const handler = async (event: RoutePayload) => {
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/generated';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new Twenty();
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
@@ -705,7 +705,7 @@ export default defineLogicFunction({
تتيح لك المكوّنات الأمامية إنشاء مكوّنات React مخصّصة تُعرَض داخل واجهة مستخدم Twenty. استخدم `defineFrontComponent()` لتعريف مكوّنات مع تحقّق مدمج:
```typescript
// src/my-widget.front-component.tsx
// src/front-components/my-widget.tsx
import { defineFrontComponent } from 'twenty-sdk';
const MyWidget = () => {
@@ -728,14 +728,13 @@ export default defineFrontComponent({
النقاط الرئيسية:
* المكوّنات الأمامية هي مكوّنات React تُعرَض ضمن سياقات معزولة داخل Twenty.
* استخدم لاحقة الملف `*.front-component.tsx` للاكتشاف التلقائي.
* يشير الحقل `component` إلى مكوّن React الخاص بك.
* يتم بناء المكوّنات ومزامنتها تلقائيًا أثناء `yarn twenty app:dev`.
يمكنك إنشاء مكوّنات أمامية جديدة بطريقتين:
* **مُنشأ بالقالب**: شغّل `yarn twenty entity:add` واختر خيار إضافة مكوّن أمامي جديد.
* **يدوي**: أنشئ ملفًا جديدًا `*.front-component.tsx` واستخدم `defineFrontComponent()`.
* **Manual**: Create a new `.tsx` file and use `defineFrontComponent()`, following the same pattern.
### المهارات
@@ -772,18 +771,24 @@ export default defineSkill({
* **مُنشأ بالقالب**: شغِّل `yarn twenty entity:add` واختر خيار إضافة مهارة جديدة.
* **يدوي**: أنشئ ملفًا جديدًا واستخدم `defineSkill()` مع اتباع النمط نفسه.
### عميل مُولَّد مضبوط الأنواع
### عملاء مُولَّدون مضبوطو الأنواع
يُولَّد العميل مضبوط الأنواع تلقائيًا بواسطة `yarn twenty app:dev` ويُخزَّن في `node_modules/twenty-sdk/generated` استنادًا إلى مخطط مساحة العمل لديك. استخدمه في وظائفك:
يتم توليد عميلين مضبوطي الأنواع تلقائيًا بواسطة `yarn twenty app:dev` وتخزينهما في `node_modules/twenty-sdk/generated` استنادًا إلى مخطط مساحة العمل لديك:
* **`CoreApiClient`** — يُجري استعلامات إلى نقطة النهاية `/graphql` للحصول على بيانات مساحة العمل
* **`MetadataApiClient`** — queries the `/metadata` endpoint for workspace configuration and file uploads
```typescript
import Twenty from '~/generated';
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';
const client = new Twenty();
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
```
يُعاد توليد العميل تلقائيًا بواسطة `yarn twenty app:dev` كلما تغيّرت كائناتك أو حقولك.
Both clients are re-generated automatically by `yarn twenty app:dev` whenever your objects or fields change.
#### بيانات الاعتماد وقت التشغيل في الوظائف المنطقية
@@ -800,17 +805,17 @@ const { me } = await client.query({ me: { id: true, displayName: true } });
#### رفع الملفات
يتضمن العميل `Twenty` المُولَّد طريقة `uploadFile` لإرفاق الملفات بالحقول من نوع ملف ضمن كائنات مساحة العمل الخاصة بك. نظرًا لأن عملاء GraphQL القياسيون لا يدعمون تحميل الملفات متعددة الأجزاء افتراضيًا، يوفر العميل هذه الطريقة المخصصة التي تطبق [مواصفة طلب GraphQL متعدد الأجزاء](https://github.com/jaydenseric/graphql-multipart-request-spec) في الخلفية.
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) في الخلفية.
```typescript
import Twenty from '~/generated';
import { MetadataApiClient } from 'twenty-sdk/generated';
import * as fs from 'fs';
const client = new Twenty();
const metadataClient = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
const uploadedFile = await client.uploadFile(
const uploadedFile = await metadataClient.uploadFile(
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type (defaults to 'application/octet-stream')
@@ -841,7 +846,7 @@ uploadFile(
النقاط الرئيسية:
* ترسل هذه الطريقة الملف إلى **نقطة نهاية البيانات الوصفية** (وليست نقطة النهاية الرئيسية لـ GraphQL)، حيث تُنفَّذ عملية الرفع.
* The `uploadFile` method is available on `MetadataApiClient` because the upload mutation is resolved by the `/metadata` endpoint.
* تستخدم `universalIdentifier` الخاص بالحقل (وليس المعرّف الخاص بمساحة العمل)، ليعمل كود الرفع لديك عبر أي مساحة عمل مُثبَّت فيها تطبيقك — بما يتماشى مع كيفية إشارة التطبيقات إلى الحقول في كل مكان آخر.
* العنوان `url` المُعاد هو عنوان URL موقّع يمكنك استخدامه للوصول إلى الملف المرفوع.
@@ -14,7 +14,6 @@ image: /images/user-guide/github/github-header.png
<Tab title="استخدام">
```jsx
import { RecoilRoot } from "recoil";
import React, { useState } from "react";
import { IconPicker } from "@/ui/input/components/IconPicker";
@@ -27,14 +26,12 @@ export const MyComponent = () => {
};
return (
<RecoilRoot>
<IconPicker
disabled={false}
onChange={handleIconChange}
selectedIconKey={selectedIcon}
variant="primary"
/>
</RecoilRoot>
);
};
```
@@ -13,7 +13,6 @@ image: /images/user-guide/what-is-twenty/20.png
<Tab title="استخدام">
```jsx
import { RecoilRoot } from 'recoil';
import { IconTwentyStar } from 'twenty-ui/display';
import { Select } from '@/ui/input/components/Select';
@@ -21,7 +20,6 @@ import { Select } from '@/ui/input/components/Select';
export const MyComponent = () => {
return (
<RecoilRoot>
<Select
className
disabled={false}
@@ -32,7 +30,6 @@ export const MyComponent = () => {
]}
value="option1"
/>
</RecoilRoot>
);
};
@@ -16,7 +16,6 @@ image: /images/user-guide/notes/notes_header.png
<Tab title="27332A2E2F2745">
```jsx
import { RecoilRoot } from "recoil";
import { TextInput } from "@/ui/input/components/TextInput";
export const MyComponent = () => {
@@ -29,7 +28,6 @@ export const MyComponent = () => {
};
return (
<RecoilRoot>
<TextInput
className
label="اسم المستخدم"
@@ -40,7 +38,6 @@ export const MyComponent = () => {
onKeyDown={handleKeyDown}
RightIcon={null}
/>
</RecoilRoot>
);
};
},{
@@ -81,12 +78,10 @@ export const MyComponent = () => {
<Tab title="الاستخدام">
```jsx
import { RecoilRoot } from "recoil";
import { AutosizeTextInput } from "@/ui/input/components/AutosizeTextInput";
export const MyComponent = () => {
return (
<RecoilRoot>
<AutosizeTextInput
onValidate={() => console.log("تم تشغيل الدالة onValidate")}
minRows={1}
@@ -96,7 +91,6 @@ export const MyComponent = () => {
buttonTitle
value="المهمة: "
/>
</RecoilRoot>
);
};},{
```
@@ -6,9 +6,9 @@ Tento dokument popisuje osvědčené postupy, které byste měli dodržovat při
## Správa stavu
React a Recoil zajišťují správu stavu v kódu.
React a Jotai zajišťují správu stavu v kódu.
### Použijte `useRecoilState` k ukládání stavu
### Použijte `useAtomState` k ukládání stavu
Je dobrým zvykem vytvořit tolik atomů, kolik potřebujete ke správě stavu.
@@ -19,13 +19,16 @@ Je lepší použít více atomů než se snažit být příliš stručný s prop
</Warning>
```tsx
export const myAtomState = atom({
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
export const myAtomState = createAtomState<string>({
key: 'myAtomState',
default: 'default value',
defaultValue: 'default value',
});
export const MyComponent = () => {
const [myAtom, setMyAtom] = useRecoilState(myAtomState);
const [myAtom, setMyAtom] = useAtomState(myAtomState);
return (
<div>
@@ -42,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 `useRecoilState`.
If you want to store state, you should use `useState` or `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 +85,8 @@ Stejný postup můžete aplikovat na logiku získávání dat pomocí Apollo hoo
// ❌ Špatně, způsobí překreslení i když se data nemění,
// protože useEffect je třeba přehodnotit
export const PageComponent = () => {
const [data, setData] = useRecoilState(dataState);
const [someDependency] = useRecoilState(someDependencyState);
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
@@ -95,9 +98,7 @@ export const PageComponent = () => {
};
export const App = () => (
<RecoilRoot>
<PageComponent />
</RecoilRoot>
<PageComponent />
);},{
```
@@ -105,14 +106,14 @@ export const App = () => (
// ✅ Dobře, nezpůsobí překreslení, pokud se data nemění,
// protože useEffect je přehodnoceno v další sourozené komponentě
export const PageComponent = () => {
const [data, setData] = useRecoilState(dataState);
const [data, setData] = useAtomState(dataState);
return <div>{data}</div>;
};
export const PageData = () => {
const [data, setData] = useRecoilState(dataState);
const [someDependency] = useRecoilState(someDependencyState);
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
@@ -124,14 +125,14 @@ export const PageData = () => {
};
export const App = () => (
<RecoilRoot>
<>
<PageData />
<PageComponent />
</RecoilRoot>
</>
);
```
### Použijte Recoil family states a Recoil family selectors
### Použijte atom family states a atom family selectors
Stavy rodiny třísek a selektory jsou skvělý způsob, jak se vyhnout překreslování.
@@ -82,9 +82,9 @@ Více podrobností naleznete v [Hooks](https://react.dev/learn/reusing-logic-wit
### Stavy
Obsahuje logiku správy stavů. To řeší [RecoilJS](https://recoiljs.org).
Obsahuje logiku správy stavů. To řeší [Jotai](https://jotai.org).
* Selektory: Více podrobností naleznete v [RecoilJS Selectors](https://recoiljs.org/docs/basic-tutorial/selectors).
* Selektory: Odvozené atomy (pomocí `createAtomSelector`) počítají hodnoty z jiných atomů a jsou automaticky memoizovány.
Vestavěná správa stavů v Reactu stále spravuje stav uvnitř komponenty.
@@ -53,7 +53,7 @@ Projekt má čistý a jednoduchý stack s minimálním počtem šablonových kó
* [React](https://react.dev/)
* [Apollo](https://www.apollographql.com/docs/)
* [GraphQL Codegen](https://the-guild.dev/graphql/codegen)
* [Recoil](https://recoiljs.org/docs/introduction/core-concepts)
* [Jotai](https://jotai.org/)
* [TypeScript](https://www.typescriptlang.org/)
**Testování**
@@ -77,7 +77,7 @@ To avoid unnecessary [re-renders](/l/cs/developers/contribute/capabilities/front
### Správa stavu
[Recoil](https://recoiljs.org/docs/introduction/core-concepts) zajišťuje správu stavu.
[Jotai](https://jotai.org/) zajišťuje správu stavu.
Podívejte se na [osvědčené postupy](/l/cs/developers/contribute/capabilities/frontend-development/best-practices-front#state-management) pro více informací o správě stavu.
@@ -160,7 +160,7 @@ export enum PageHotkeyScope {
}
```
Interně je aktuálně vybraný rozsah uložen v Recoil stavu, který je sdílen napříč aplikací :
Interně je aktuálně vybraný rozsah uložen v Jotai stavu, který je sdílen napříč aplikací :
```tsx
export const currentHotkeyScopeState = createState<HotkeyScope>({
@@ -169,10 +169,10 @@ export const currentHotkeyScopeState = createState<HotkeyScope>({
});
```
Ale tento Recoil stav by neměl být nikdy řízen ručně! Ukážeme si, jak jej používat v příští sekci.
Ale tento Jotai stav by neměl být nikdy řízen 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 Recoil stav, abychom mohli řídit stav rozsahu klávesových zkratek a učinit jej dostupným kdekoli v aplikaci.
Také jsme vytvořili Jotai stav, abychom mohli řídit stav rozsahu klávesových zkratek a učinit jej dostupným kdekoli v aplikaci.
@@ -165,7 +165,7 @@ export default defineObject({
Pozdější příkazy přidají další soubory a složky:
* `yarn twenty app:dev` automaticky vygeneruje typovaného klienta API v `node_modules/twenty-sdk/generated` (typovaný klient Twenty + typy pracovního prostoru).
* `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 entity:add` přidá soubory s definicemi entit do `src/` pro vaše vlastní objekty, funkce, frontové komponenty, role, dovednosti a další.
## Ověření
@@ -426,10 +426,10 @@ Každý soubor funkce používá `defineLogicFunction()` k exportu konfigurace s
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
import { CoreApiClient, type Person } from 'twenty-sdk/generated';
const handler = async (params: RoutePayload) => {
const client = new Twenty(); // generated typed client
const client = new CoreApiClient();
const name = 'name' in params.queryStringParameters
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
@@ -644,10 +644,10 @@ Chcete-li označit logickou funkci jako nástroj, nastavte `isTool: true` a posk
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/generated';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new Twenty();
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
@@ -705,7 +705,7 @@ Hlavní body:
Frontendové komponenty vám umožňují vytvářet vlastní React komponenty, které se vykreslují v rozhraní Twenty. K definování komponent s vestavěnou validací použijte `defineFrontComponent()`:
```typescript
// src/my-widget.front-component.tsx
// src/front-components/my-widget.tsx
import { defineFrontComponent } from 'twenty-sdk';
const MyWidget = () => {
@@ -728,14 +728,13 @@ export default defineFrontComponent({
Hlavní body:
* Frontendové komponenty jsou React komponenty, které se vykreslují v izolovaných kontextech v rámci Twenty.
* Pro automatickou detekci použijte příponu souboru `*.front-component.tsx`.
* Pole `component` odkazuje na vaši React komponentu.
* Komponenty se během `yarn twenty app:dev` automaticky sestaví a synchronizují.
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.
* **Ruční**: Vytvořte nový soubor `*.front-component.tsx` a použijte `defineFrontComponent()`.
* **Manual**: Create a new `.tsx` file and use `defineFrontComponent()`, following the same pattern.
### Dovednosti
@@ -772,18 +771,24 @@ 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.
### Generovaný typovaný klient
### Generated typed clients
Typovaný klient je automaticky generován pomocí `yarn twenty app:dev` a ukládá se do `node_modules/twenty-sdk/generated` podle schématu vašeho pracovního prostoru. Použijte jej ve svých funkcích:
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
```typescript
import Twenty from '~/generated';
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';
const client = new Twenty();
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
```
Klient se automaticky znovu generuje pomocí `yarn twenty app:dev` kdykoli se změní vaše objekty nebo pole.
Both clients are re-generated automatically by `yarn twenty app:dev` whenever your objects or fields change.
#### Běhové přihlašovací údaje v logických funkcích
@@ -800,17 +805,17 @@ Poznámky:
#### Nahrávání souborů
Vygenerovaný klient `Twenty` 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).
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).
```typescript
import Twenty from '~/generated';
import { MetadataApiClient } from 'twenty-sdk/generated';
import * as fs from 'fs';
const client = new Twenty();
const metadataClient = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
const uploadedFile = await client.uploadFile(
const uploadedFile = await metadataClient.uploadFile(
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type (defaults to 'application/octet-stream')
@@ -841,7 +846,7 @@ uploadFile(
Hlavní body:
* Metoda odešle soubor na **koncový bod metadat** (nikoli na hlavní koncový bod GraphQL), kde se provede mutace nahrání.
* The `uploadFile` method is available on `MetadataApiClient` because the upload mutation is resolved by the `/metadata` endpoint.
* 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.
@@ -14,7 +14,6 @@ Rozbalovací výběr ikon, který uživatelům umožňuje vybrat ikonu ze seznam
<Tab title="Použití">
```jsx
import { RecoilRoot } from "recoil";
import React, { useState } from "react";
import { IconPicker } from "@/ui/input/components/IconPicker";
@@ -27,14 +26,12 @@ export const MyComponent = () => {
};
return (
<RecoilRoot>
<IconPicker
disabled={false}
onChange={handleIconChange}
selectedIconKey={selectedIcon}
variant="primary"
/>
</RecoilRoot>
);
};
```
@@ -13,7 +13,6 @@ Umožňuje uživatelům vybrat hodnotu z nabídky předdefinovaných možností.
<Tab title="Použití">
```jsx
import { RecoilRoot } from 'recoil';
import { IconTwentyStar } from 'twenty-ui/display';
import { Select } from '@/ui/input/components/Select';
@@ -21,7 +20,6 @@ import { Select } from '@/ui/input/components/Select';
export const MyComponent = () => {
return (
<RecoilRoot>
<Select
className
disabled={false}
@@ -32,7 +30,6 @@ export const MyComponent = () => {
]}
value="option1"
/>
</RecoilRoot>
);
};
@@ -16,7 +16,6 @@ Umožňuje uživatelům zadávat a upravovat text.
<Tab title="Použití">
```jsx
import { RecoilRoot } from "recoil";
import { TextInput } from "@/ui/input/components/TextInput";
export const MyComponent = () => {
@@ -29,7 +28,6 @@ export const MyComponent = () => {
};
return (
<RecoilRoot>
<TextInput
className
label="Username"
@@ -40,7 +38,6 @@ export const MyComponent = () => {
onKeyDown={handleKeyDown}
RightIcon={null}
/>
</RecoilRoot>
);
};
@@ -81,12 +78,10 @@ Textová vstupní komponenta, která automaticky přizpůsobuje svou výšku na
<Tab title="Použití">
```jsx
import { RecoilRoot } from "recoil";
import { AutosizeTextInput } from "@/ui/input/components/AutosizeTextInput";
export const MyComponent = () => {
return (
<RecoilRoot>
<AutosizeTextInput
onValidate={() => console.log("onValidate function fired")}
minRows={1}
@@ -96,7 +91,6 @@ export const MyComponent = () => {
buttonTitle
value="Task: "
/>
</RecoilRoot>
);
};
```
@@ -6,9 +6,9 @@ Dieses Dokument beschreibt die besten Praktiken, die Sie beim Arbeiten am Fronte
## Zustandsverwaltung
React und Recoil übernehmen die Zustandsverwaltung im Code.
React und Jotai übernehmen die Zustandsverwaltung im Code.
### Verwenden Sie `useRecoilState`, um den Zustand zu speichern.
### Verwenden Sie `useAtomState`, um den Zustand zu speichern.
Es ist eine gute Praxis, so viele Atome zu erstellen, wie Sie benötigen, um Ihren Zustand zu speichern.
@@ -19,13 +19,16 @@ Es ist besser, zusätzliche Atome zu verwenden, als zu versuchen, mit Prop Drill
</Warning>
```tsx
export const myAtomState = atom({
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
export const myAtomState = createAtomState<string>({
key: 'myAtomState',
default: 'default value',
defaultValue: 'default value',
});
export const MyComponent = () => {
const [myAtom, setMyAtom] = useRecoilState(myAtomState);
const [myAtom, setMyAtom] = useAtomState(myAtomState);
return (
<div>
@@ -42,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 `useRecoilState`.
If you want to store state, you should use `useState` or `useAtomState`.
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 +85,8 @@ Dasselbe können Sie auch für die Datenabruflogik mit Apollo-Hooks anwenden.
// ❌ Bad, will cause re-renders even if data is not changing,
// because useEffect needs to be re-evaluated
export const PageComponent = () => {
const [data, setData] = useRecoilState(dataState);
const [someDependency] = useRecoilState(someDependencyState);
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
@@ -95,9 +98,7 @@ export const PageComponent = () => {
};
export const App = () => (
<RecoilRoot>
<PageComponent />
</RecoilRoot>
<PageComponent />
);
```
@@ -105,14 +106,14 @@ export const App = () => (
// ✅ Good, will not cause re-renders if data is not changing,
// because useEffect is re-evaluated in another sibling component
export const PageComponent = () => {
const [data, setData] = useRecoilState(dataState);
const [data, setData] = useAtomState(dataState);
return <div>{data}</div>;
};
export const PageData = () => {
const [data, setData] = useRecoilState(dataState);
const [someDependency] = useRecoilState(someDependencyState);
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
@@ -124,16 +125,16 @@ export const PageData = () => {
};
export const App = () => (
<RecoilRoot>
<>
<PageData />
<PageComponent />
</RecoilRoot>
</>
);
```
### Verwenden Sie Recoil-Familienzustände und Recoil-Familienselektoren
### Verwenden Sie Jotai-Familienzustände und Jotai-Familienselektoren
Recoil-Familienzustände und -Selektoren sind eine großartige Möglichkeit, Re-Renders zu vermeiden.
Jotai-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.
@@ -82,9 +82,9 @@ Weitere Details finden Sie unter [Hooks](https://react.dev/learn/reusing-logic-w
### Zustände
Enthält die State-Management-Logik. [RecoilJS](https://recoiljs.org) übernimmt dies.
Enthält die State-Management-Logik. [Jotai](https://jotai.org) übernimmt dies.
* Selektoren: Weitere Einzelheiten finden Sie unter [RecoilJS Selektoren](https://recoiljs.org/docs/basic-tutorial/selectors).
* Selektoren: Abgeleitete Atome (mit `createAtomSelector`) berechnen Werte aus anderen Atomen und werden automatisch memoisiert.
Die integrierte Zustandsverwaltung von React verwaltet weiterhin den Zustand innerhalb einer Komponente.
@@ -53,7 +53,7 @@ Das Projekt hat einen sauberen und einfachen Stack mit minimalem Boilerplate-Cod
* [React](https://react.dev/)
* [Apollo](https://www.apollographql.com/docs/)
* [GraphQL Codegen](https://the-guild.dev/graphql/codegen)
* [Recoil](https://recoiljs.org/docs/introduction/core-concepts)
* [Jotai](https://jotai.org/)
* [TypeScript](https://www.typescriptlang.org/)
**Tests**
@@ -77,7 +77,7 @@ To avoid unnecessary [re-renders](/l/de/developers/contribute/capabilities/front
### Zustandsverwaltung
[Recoil](https://recoiljs.org/docs/introduction/core-concepts) übernimmt die Zustandsverwaltung.
[Jotai](https://jotai.org/) übernimmt die Zustandsverwaltung.
Siehe [Best Practices](/l/de/developers/contribute/capabilities/frontend-development/best-practices-front#state-management) für mehr Informationen zur Zustandsverwaltung.
@@ -160,7 +160,7 @@ export enum PageHotkeyScope {
}
```
Intern wird der aktuell ausgewählte Bereich in einem Recoil-State gespeichert, der in der gesamten Anwendung geteilt wird:
Intern wird der aktuell ausgewählte Bereich in einem Jotai-State gespeichert, der in der gesamten Anwendung geteilt wird:
```tsx
export const currentHotkeyScopeState = createState<HotkeyScope>({
@@ -169,10 +169,10 @@ export const currentHotkeyScopeState = createState<HotkeyScope>({
});
```
Aber dieser Recoil-State sollte niemals manuell bearbeitet werden! Wir werden im nächsten Abschnitt sehen, wie man es verwendet.
Aber dieser Jotai-State 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 Recoil-State erstellt, um den Tastenkombinationsbereich zu verwalten und in der gesamten Anwendung verfügbar zu machen.
Wir haben auch einen Jotai-State erstellt, um den Tastenkombinationsbereich zu verwalten und in der gesamten Anwendung verfügbar zu machen.
@@ -165,7 +165,7 @@ export default defineObject({
Spätere Befehle fügen weitere Dateien und Ordner hinzu:
* `yarn twenty app:dev` generiert automatisch einen typisierten API-Client in `node_modules/twenty-sdk/generated` (typisierter Twenty-Client + Arbeitsbereichs-Typen).
* `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 entity:add` fügt unter `src/` Entitätsdefinitionsdateien für Ihre benutzerdefinierten Objekte, Funktionen, Frontend-Komponenten, Rollen, Skills und mehr hinzu.
## Authentifizierung
@@ -426,10 +426,10 @@ Jede Funktionsdatei verwendet `defineLogicFunction()`, um eine Konfiguration mit
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
import { CoreApiClient, type Person } from 'twenty-sdk/generated';
const handler = async (params: RoutePayload) => {
const client = new Twenty(); // generated typed client
const client = new CoreApiClient();
const name = 'name' in params.queryStringParameters
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
@@ -644,10 +644,10 @@ Um eine Logikfunktion als Tool zu markieren, setzen Sie `isTool: true` und geben
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/generated';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new Twenty();
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
@@ -705,7 +705,7 @@ Hauptpunkte:
Frontend-Komponenten ermöglichen es Ihnen, benutzerdefinierte React-Komponenten zu erstellen, die innerhalb der Twenty-UI gerendert werden. Verwenden Sie `defineFrontComponent()`, um Komponenten mit eingebauter Validierung zu definieren:
```typescript
// src/my-widget.front-component.tsx
// src/front-components/my-widget.tsx
import { defineFrontComponent } from 'twenty-sdk';
const MyWidget = () => {
@@ -728,14 +728,13 @@ export default defineFrontComponent({
Hauptpunkte:
* Frontend-Komponenten sind React-Komponenten, die in isolierten Kontexten innerhalb von Twenty gerendert werden.
* Verwenden Sie die Dateiendung `*.front-component.tsx` für die automatische Erkennung.
* Das Feld `component` verweist auf Ihre React-Komponente.
* Komponenten werden während `yarn twenty app:dev` automatisch gebaut und synchronisiert.
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.
* **Manuell**: Erstellen Sie eine neue `*.front-component.tsx`-Datei und verwenden Sie `defineFrontComponent()`.
* **Manual**: Create a new `.tsx` file and use `defineFrontComponent()`, following the same pattern.
### Fähigkeiten
@@ -772,18 +771,24 @@ 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.
### Generierter typisierter Client
### Generated typed clients
Der typisierte Client wird von `yarn twenty app:dev` automatisch generiert und basierend auf Ihrem Arbeitsbereichs-Schema in `node_modules/twenty-sdk/generated` gespeichert. Verwenden Sie ihn in Ihren Funktionen:
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
```typescript
import Twenty from '~/generated';
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';
const client = new Twenty();
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
```
Der Client wird von `yarn twenty app:dev` automatisch neu generiert, sobald sich Ihre Objekte oder Felder ändern.
Both clients are re-generated automatically by `yarn twenty app:dev` whenever your objects or fields change.
#### Laufzeit-Anmeldedaten in Logikfunktionen
@@ -800,17 +805,17 @@ Notizen:
#### Dateien hochladen
Der generierte `Twenty`-Client 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.
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.
```typescript
import Twenty from '~/generated';
import { MetadataApiClient } from 'twenty-sdk/generated';
import * as fs from 'fs';
const client = new Twenty();
const metadataClient = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
const uploadedFile = await client.uploadFile(
const uploadedFile = await metadataClient.uploadFile(
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type (defaults to 'application/octet-stream')
@@ -841,7 +846,7 @@ uploadFile(
Hauptpunkte:
* Die Methode sendet die Datei an den **metadata endpoint** (nicht den main GraphQL endpoint), wo die Upload-Mutation ausgeführt wird.
* The `uploadFile` method is available on `MetadataApiClient` because the upload mutation is resolved by the `/metadata` endpoint.
* 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.
@@ -14,7 +14,6 @@ Eine Dropdown-basierte Symbolauswahl, mit der Benutzer ein Symbol aus einer List
<Tab title="&#x22;Verwendung&#x22;">
```jsx
import { RecoilRoot } from "recoil";
import React, { useState } from "react";
import { IconPicker } from "@/ui/input/components/IconPicker";
@@ -27,14 +26,12 @@ export const MyComponent = () => {
};
return (
<RecoilRoot>
<IconPicker
disabled={false}
onChange={handleIconChange}
selectedIconKey={selectedIcon}
variant="primary"
/>
</RecoilRoot>
);
};
```
@@ -13,7 +13,6 @@ Ermöglicht es Benutzern, einen Wert aus einer Liste vordefinierter Optionen aus
<Tab title="&#x22;Verwendung&#x22;">
```jsx
import { RecoilRoot } from 'recoil';
import { IconTwentyStar } from 'twenty-ui/display';
import { Select } from '@/ui/input/components/Select';
@@ -21,7 +20,6 @@ import { Select } from '@/ui/input/components/Select';
export const MyComponent = () => {
return (
<RecoilRoot>
<Select
className
disabled={false}
@@ -32,7 +30,6 @@ export const MyComponent = () => {
]}
value="option1"
/>
</RecoilRoot>
);
};
@@ -16,7 +16,6 @@ Ermöglicht es den Benutzern, Text einzugeben und zu bearbeiten.
<Tab title="Verwendung">
```jsx
import { RecoilRoot } from "recoil";
import { TextInput } from "@/ui/input/components/TextInput";
export const MyComponent = () => {
@@ -29,7 +28,6 @@ export const MyComponent = () => {
};
return (
<RecoilRoot>
<TextInput
className
label="Username"
@@ -40,7 +38,6 @@ export const MyComponent = () => {
onKeyDown={handleKeyDown}
RightIcon={null}
/>
</RecoilRoot>
);
};
@@ -81,12 +78,10 @@ Textkomponente, die ihre Höhe automatisch anhand des Inhalts anpasst.
<Tab title="Verwendung">
```jsx
import { RecoilRoot } from "recoil";
import { AutosizeTextInput } from "@/ui/input/components/AutosizeTextInput";
export const MyComponent = () => {
return (
<RecoilRoot>
<AutosizeTextInput
onValidate={() => console.log("onValidate function fired")}
minRows={1}
@@ -96,7 +91,6 @@ export const MyComponent = () => {
buttonTitle
value="Task: "
/>
</RecoilRoot>
);
};
```
@@ -6,9 +6,9 @@ Este documento describe las mejores prácticas que debes seguir al trabajar en e
## Gestión de estado
React y Recoil manejan la gestión de estado en la base de código.
React y Jotai manejan la gestión de estado en la base de código.
### Usa `useRecoilState` para almacenar el estado
### Usa `useAtomState` para almacenar el estado
Es buena práctica crear tantos átomos como necesites para almacenar tu estado.
@@ -17,13 +17,16 @@ Es buena práctica crear tantos átomos como necesites para almacenar tu estado.
</Warning>
```tsx
export const myAtomState = atom({
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
export const myAtomState = createAtomState<string>({
key: 'myAtomState',
default: 'default value',
defaultValue: 'default value',
});
export const MyComponent = () => {
const [myAtom, setMyAtom] = useRecoilState(myAtomState);
const [myAtom, setMyAtom] = useAtomState(myAtomState);
return (
<div>
@@ -40,7 +43,7 @@ export const MyComponent = () => {
Evita usar `useRef` para almacenar el estado.
If you want to store state, you should use `useState` or `useRecoilState`.
If you want to store state, you should use `useState` or `useAtomState`.
Consulta [cómo gestionar las re-renderizaciones](#managing-re-renders) si sientes que necesitas `useRef` para evitar algunas re-renderizaciones.
@@ -80,8 +83,8 @@ Puedes aplicar lo mismo para la lógica de obtención de datos, con hooks de Apo
// ❌ Malo, provocará re-renderizados incluso si los datos no cambian,
// porque useEffect necesita volver a evaluarse
export const PageComponent = () => {
const [data, setData] = useRecoilState(dataState);
const [someDependency] = useRecoilState(someDependencyState);
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
@@ -93,9 +96,7 @@ export const PageComponent = () => {
};
export const App = () => (
<RecoilRoot>
<PageComponent />
</RecoilRoot>
<PageComponent />
);
```
@@ -103,14 +104,14 @@ export const App = () => (
// ✅ Bueno, no provocará re-renderizados si los datos no cambian,
// porque useEffect se vuelve a evaluar en otro componente hermano
export const PageComponent = () => {
const [data, setData] = useRecoilState(dataState);
const [data, setData] = useAtomState(dataState);
return <div>{data}</div>;
};
export const PageData = () => {
const [data, setData] = useRecoilState(dataState);
const [someDependency] = useRecoilState(someDependencyState);
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
@@ -122,16 +123,16 @@ export const PageData = () => {
};
export const App = () => (
<RecoilRoot>
<>
<PageData />
<PageComponent />
</RecoilRoot>
</>
);
```
### Usa estados de familia de recoil y selectores de familia de recoil
### Usa estados de familia de jotai y selectores de familia de jotai
Los estados y selectores de familia de recoil son una gran manera de evitar re-renderizaciones.
Los estados y selectores de familia de jotai son una gran manera de evitar re-renderizaciones.
Son útiles cuando necesitas almacenar una lista de elementos.
@@ -82,9 +82,9 @@ Ver [Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks) para más d
### Estados
Contiene la lógica de gestión de estado. [RecoilJS](https://recoiljs.org) maneja esto.
Contiene la lógica de gestión de estado. [Jotai](https://jotai.org) maneja esto.
* Selectores: Ver [RecoilJS Selectors](https://recoiljs.org/docs/basic-tutorial/selectors) para más detalles.
* Selectores: Los átomos derivados (usando `createAtomSelector`) calculan valores a partir de otros átomos y se memorizan automáticamente.
La gestión de estado incorporada de React todavía maneja el estado dentro de un componente.
@@ -53,7 +53,7 @@ El proyecto tiene un stack limpio y sencillo, con un código boilerplate mínimo
* [React](https://react.dev/)
* [Apollo](https://www.apollographql.com/docs/)
* [GraphQL Codegen](https://the-guild.dev/graphql/codegen)
* [Recoil](https://recoiljs.org/docs/introduction/core-concepts)
* [Jotai](https://jotai.org/)
* [TypeScript](https://www.typescriptlang.org/)
**Pruebas**
@@ -77,7 +77,7 @@ To avoid unnecessary [re-renders](/l/es/developers/contribute/capabilities/front
### Gestión del Estado
[Recoil](https://recoiljs.org/docs/introduction/core-concepts) maneja la gestión del estado.
[Jotai](https://jotai.org/) maneja la gestión del estado.
Ver [mejores prácticas](/l/es/developers/contribute/capabilities/frontend-development/best-practices-front#state-management) para más información sobre la gestión del estado.
@@ -160,7 +160,7 @@ export enum PageHotkeyScope {
}
```
Internamente, el ámbito seleccionado se almacena en un estado de Recoil que se comparte en toda la aplicación:
Internamente, el ámbito seleccionado se almacena en un estado de Jotai que se comparte en toda la aplicación:
```tsx
export const currentHotkeyScopeState = createState<HotkeyScope>({
@@ -169,10 +169,10 @@ export const currentHotkeyScopeState = createState<HotkeyScope>({
});
```
¡Pero este estado de Recoil nunca debe manejarse manualmente! Veremos cómo usarlo en la siguiente sección.
¡Pero este estado de Jotai nunca debe manejarse manualmente! Veremos cómo usarlo en la siguiente sección.
## ¿Cómo funciona internamente?
Hicimos un contenedor delgado sobre [react-hotkeys-hook](https://react-hotkeys-hook.vercel.app/docs/intro) que lo hace más eficiente y evita renders innecesarios.
También creamos un estado de Recoil para manejar el estado del ámbito de atajos de teclado y hacerlo disponible en toda la aplicación.
También creamos un estado de Jotai para manejar el estado del ámbito de atajos de teclado y hacerlo disponible en toda la aplicación.
@@ -12,7 +12,6 @@ Un selector de íconos basado en un menú desplegable que permite a los usuarios
<Tabs>
<Tab title="Uso">
```jsx
import { RecoilRoot } from "recoil";
import React, { useState } from "react";
import { IconPicker } from "@/ui/input/components/IconPicker";
@@ -25,14 +24,12 @@ Un selector de íconos basado en un menú desplegable que permite a los usuarios
};
return (
<RecoilRoot>
<IconPicker
disabled={false}
onChange={handleIconChange}
selectedIconKey={selectedIcon}
variant="primary"
/>
</RecoilRoot>
);
};
```
@@ -12,7 +12,6 @@ Permite a los usuarios seleccionar un valor de una lista de opciones predefinida
<Tabs>
<Tab title="Uso">
```jsx
import { RecoilRoot } from 'recoil';
import { IconTwentyStar } from 'twenty-ui/display';
import { Select } from '@/ui/input/components/Select';
@@ -20,7 +19,6 @@ Permite a los usuarios seleccionar un valor de una lista de opciones predefinida
export const MyComponent = () => {
return (
<RecoilRoot>
<Select
className
disabled={false}
@@ -31,7 +29,6 @@ Permite a los usuarios seleccionar un valor de una lista de opciones predefinida
]}
value="option1"
/>
</RecoilRoot>
);
};
@@ -14,7 +14,6 @@ image: '"/images/user-guide/notes/notes_header.png"'
<Tabs>
<Tab title="&#x22;Uso&#x22;">
```jsx
import { RecoilRoot } from "recoil";
import { TextInput } from "@/ui/input/components/TextInput";
export const MyComponent = () => {
@@ -27,7 +26,6 @@ image: '"/images/user-guide/notes/notes_header.png"'
};
return (
<RecoilRoot>
<TextInput
className
label="Nombre de usuario"
@@ -38,7 +36,6 @@ image: '"/images/user-guide/notes/notes_header.png"'
onKeyDown={handleKeyDown}
RightIcon={null}
/>
</RecoilRoot>
);
};
},{
@@ -68,12 +65,10 @@ image: '"/images/user-guide/notes/notes_header.png"'
<Tabs>
<Tab title="&#x22;Uso&#x22;">
```jsx
import { RecoilRoot } from "recoil";
import { AutosizeTextInput } from "@/ui/input/components/AutosizeTextInput";
export const MyComponent = () => {
return (
<RecoilRoot>
<AutosizeTextInput
onValidate={() => console.log("Función onValidate ejecutada")}
minRows={1}
@@ -83,7 +78,6 @@ image: '"/images/user-guide/notes/notes_header.png"'
buttonTitle
value="Tarea: "
/>
</RecoilRoot>
);
};
```
@@ -6,9 +6,9 @@ Ce document décrit les meilleures pratiques à suivre lors de votre travail sur
## Gestion de l'état
React et Recoil gèrent la gestion de l'état dans la base de code.
React et Jotai gèrent la gestion de l'état dans la base de code.
### Utilisez `useRecoilState` pour stocker l'état
### Utilisez `useAtomState` pour stocker l'état
C'est une bonne pratique de créer autant d'atomes que nécessaire pour stocker votre état.
@@ -17,13 +17,16 @@ C'est une bonne pratique de créer autant d'atomes que nécessaire pour stocker
</Warning>
```tsx
export const myAtomState = atom({
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
export const myAtomState = createAtomState<string>({
key: 'myAtomState',
default: 'default value',
defaultValue: 'default value',
});
export const MyComponent = () => {
const [myAtom, setMyAtom] = useRecoilState(myAtomState);
const [myAtom, setMyAtom] = useAtomState(myAtomState);
return (
<div>
@@ -40,7 +43,7 @@ export const MyComponent = () => {
Évitez d'utiliser `useRef` pour stocker l'état.
If you want to store state, you should use `useState` or `useRecoilState`.
If you want to store state, you should use `useState` or `useAtomState`.
Consultez [comment gérer les re-rendus](#managing-re-renders) si vous avez l'impression d'avoir besoin de `useRef` pour empêcher certains re-rendus.
@@ -80,8 +83,8 @@ Vous pouvez appliquer la même chose à la logique de récupération de données
// ❌ Mauvais, provoquera de nouveaux rendus même si les données ne changent pas,
// car useEffect doit être réévalué
export const PageComponent = () => {
const [data, setData] = useRecoilState(dataState);
const [someDependency] = useRecoilState(someDependencyState);
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
@@ -93,9 +96,7 @@ export const PageComponent = () => {
};
export const App = () => (
<RecoilRoot>
<PageComponent />
</RecoilRoot>
<PageComponent />
);
```
@@ -103,14 +104,14 @@ export const App = () => (
// ✅ Bon, ne provoquera pas de nouveaux rendus si les données ne changent pas,
// car useEffect est réévalué dans un autre composant frère
export const PageComponent = () => {
const [data, setData] = useRecoilState(dataState);
const [data, setData] = useAtomState(dataState);
return <div>{data}</div>;
};
export const PageData = () => {
const [data, setData] = useRecoilState(dataState);
const [someDependency] = useRecoilState(someDependencyState);
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
@@ -122,16 +123,16 @@ export const PageData = () => {
};
export const App = () => (
<RecoilRoot>
<>
<PageData />
<PageComponent />
</RecoilRoot>
</>
);
```
### Utilisez les états de famille de recoil et les sélecteurs de famille de recoil
### Utilisez les états de famille de jotai et les sélecteurs de famille de jotai
Les états et sélecteurs de famille recoil sont un excellent moyen d'éviter les re-rendus.
Les états et sélecteurs de famille jotai sont un excellent moyen d'éviter les re-rendus.
Ils sont utiles lorsque vous devez stocker une liste d'articles.
@@ -82,9 +82,9 @@ Voir [Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks) pour plus
### États
Contient la logique de gestion des états. [RecoilJS](https://recoiljs.org) gère cela.
Contient la logique de gestion des états. [Jotai](https://jotai.org) gère cela.
* Sélecteurs : Voir [RecoilJS Selectors](https://recoiljs.org/docs/basic-tutorial/selectors) pour plus de détails.
* Sélecteurs : Les atomes dérivés (via `createAtomSelector`) calculent des valeurs à partir d'autres atomes et sont automatiquement mémoïsés.
La gestion de l'état intégrée de React gère toujours l'état au sein d'un composant.
@@ -53,7 +53,7 @@ Le projet a une stack simple et propre, avec un code boilerplate minimal.
* [React](https://react.dev/)
* [Apollo](https://www.apollographql.com/docs/)
* [GraphQL Codegen](https://the-guild.dev/graphql/codegen)
* [Recoil](https://recoiljs.org/docs/introduction/core-concepts)
* [Jotai](https://jotai.org/)
* [TypeScript](https://www.typescriptlang.org/)
**Tests**
@@ -77,7 +77,7 @@ To avoid unnecessary [re-renders](/l/fr/developers/contribute/capabilities/front
### Gestion de l'État
[Recoil](https://recoiljs.org/docs/introduction/core-concepts) gère la gestion de l'état.
[Jotai](https://jotai.org/) gère la gestion de l'état.
Voir [les meilleures pratiques](/l/fr/developers/contribute/capabilities/frontend-development/best-practices-front#state-management) pour plus d'informations sur la gestion de l'état.
@@ -160,7 +160,7 @@ export enum PageHotkeyScope {
}
```
En interne, le périmètre sélectionné est stocké dans un état Recoil qui est partagé dans toute l'application :
En interne, le périmètre sélectionné est stocké dans un état Jotai qui est partagé dans toute l'application :
```tsx
export const currentHotkeyScopeState = createState<HotkeyScope>({
@@ -169,10 +169,10 @@ export const currentHotkeyScopeState = createState<HotkeyScope>({
});
```
Mais cet état Recoil ne doit jamais être manipulé manuellement ! Nous verrons comment l'utiliser dans la prochaine section.
Mais cet état Jotai ne doit jamais être manipulé manuellement ! Nous verrons comment l'utiliser dans la prochaine section.
## Comment cela fonctionne-t-il en interne ?
Nous avons créé un léger emballage au-dessus de [react-hotkeys-hook](https://react-hotkeys-hook.vercel.app/docs/intro) qui le rend plus performant et évite les rendus inutiles.
Nous créons également un état Recoil pour gérer l'état du périmètre des raccourcis et le rendre disponible partout dans l'application.
Nous créons également un état Jotai pour gérer l'état du périmètre des raccourcis et le rendre disponible partout dans l'application.
@@ -12,7 +12,6 @@ Un sélecteur d'icônes basé sur un menu déroulant qui permet aux utilisateurs
<Tabs>
<Tab title="Utilisation">
```jsx
import { RecoilRoot } from "recoil";
import React, { useState } from "react";
import { IconPicker } from "@/ui/input/components/IconPicker";
@@ -25,14 +24,12 @@ Un sélecteur d'icônes basé sur un menu déroulant qui permet aux utilisateurs
};
return (
<RecoilRoot>
<IconPicker
disabled={false}
onChange={handleIconChange}
selectedIconKey={selectedIcon}
variant="primary"
/>
</RecoilRoot>
);
};
```
@@ -12,7 +12,6 @@ Permet aux utilisateurs de choisir une valeur parmi une liste d'options prédéf
<Tabs>
<Tab title="Utilisation">
```jsx
import { RecoilRoot } from 'recoil';
import { IconTwentyStar } from 'twenty-ui/display';
import { Select } from '@/ui/input/components/Select';
@@ -20,7 +19,6 @@ Permet aux utilisateurs de choisir une valeur parmi une liste d'options prédéf
export const MyComponent = () => {
return (
<RecoilRoot>
<Select
className
disabled={false}
@@ -31,7 +29,6 @@ Permet aux utilisateurs de choisir une valeur parmi une liste d'options prédéf
]}
value="option1"
/>
</RecoilRoot>
);
};
@@ -14,7 +14,6 @@ Permet aux utilisateurs de saisir et de modifier du texte.
<Tabs>
<Tab title="Utilisation">
```jsx
import { RecoilRoot } from "recoil";
import { TextInput } from "@/ui/input/components/TextInput";
export const MyComponent = () => {
@@ -27,7 +26,6 @@ Permet aux utilisateurs de saisir et de modifier du texte.
};
return (
<RecoilRoot>
<TextInput
className
label="Username"
@@ -38,7 +36,6 @@ Permet aux utilisateurs de saisir et de modifier du texte.
onKeyDown={handleKeyDown}
RightIcon={null}
/>
</RecoilRoot>
);
};
@@ -68,12 +65,10 @@ Composant d'entrée de texte qui ajuste automatiquement sa hauteur en fonction d
<Tabs>
<Tab title="Utilisation">
```jsx
import { RecoilRoot } from "recoil";
import { AutosizeTextInput } from "@/ui/input/components/AutosizeTextInput";
export const MyComponent = () => {
return (
<RecoilRoot>
<AutosizeTextInput
onValidate={() => console.log("onValidate function fired")}
minRows={1}
@@ -83,7 +78,6 @@ Composant d'entrée de texte qui ajuste automatiquement sa hauteur en fonction d
buttonTitle
value="Task: "
/>
</RecoilRoot>
);
};
```
@@ -6,9 +6,9 @@ Questo documento descrive le migliori pratiche da seguire quando si lavora sul f
## Gestione dello Stato
React e Recoil gestiscono la gestione dello stato nella base di codice.
React e Jotai gestiscono la gestione dello stato nella base di codice.
### Usa `useRecoilState` per memorizzare lo stato
### Usa `useAtomState` per memorizzare lo stato
È buona pratica creare tanti atomi quanti servono per memorizzare il tuo stato.
@@ -19,13 +19,16 @@ React e Recoil gestiscono la gestione dello stato nella base di codice.
</Warning>
```tsx
export const myAtomState = atom({
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
export const myAtomState = createAtomState<string>({
key: 'myAtomState',
default: 'default value',
defaultValue: 'default value',
});
export const MyComponent = () => {
const [myAtom, setMyAtom] = useRecoilState(myAtomState);
const [myAtom, setMyAtom] = useAtomState(myAtomState);
return (
<div>
@@ -42,7 +45,7 @@ export const MyComponent = () => {
Evita di usare `useRef` per memorizzare lo stato.
Se vuoi memorizzare lo stato, dovresti usare `useState` o `useRecoilState`.
Se vuoi memorizzare lo stato, dovresti usare `useState` o `useAtomState`.
Consulta [come gestire i re-render](#managing-re-renders) se senti che hai bisogno di `useRef` per evitare alcuni re-render.
@@ -82,8 +85,8 @@ Puoi applicare lo stesso per la logica di recupero dati, con i hook di Apollo.
// ❌ Sconsigliato, causerà re-render anche se i dati non cambiano,
// perché useEffect deve essere ri-eseguito
export const PageComponent = () => {
const [data, setData] = useRecoilState(dataState);
const [someDependency] = useRecoilState(someDependencyState);
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
@@ -95,9 +98,7 @@ export const PageComponent = () => {
};
export const App = () => (
<RecoilRoot>
<PageComponent />
</RecoilRoot>
<PageComponent />
);
```
@@ -105,14 +106,14 @@ export const App = () => (
// ✅ Consigliato, non causerà re-render se i dati non cambiano,
// perché useEffect viene ri-eseguito in un altro componente fratello
export const PageComponent = () => {
const [data, setData] = useRecoilState(dataState);
const [data, setData] = useAtomState(dataState);
return <div>{data}</div>;
};
export const PageData = () => {
const [data, setData] = useRecoilState(dataState);
const [someDependency] = useRecoilState(someDependencyState);
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
@@ -124,16 +125,16 @@ export const PageData = () => {
};
export const App = () => (
<RecoilRoot>
<>
<PageData />
<PageComponent />
</RecoilRoot>
</>
);
```
### Usa stati di famiglia recoil e selettori di famiglia recoil
### Usa stati di famiglia jotai e selettori di famiglia jotai
Gli stati e i selettori di famiglia Recoil sono un ottimo modo per evitare re-render.
Gli stati e i selettori di famiglia Jotai sono un ottimo modo per evitare re-render.
Sono utili quando hai bisogno di memorizzare una lista di elementi.
@@ -82,9 +82,9 @@ Vedi [Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks) per ulteri
### Stati
Contiene la logica di gestione degli stati. [RecoilJS](https://recoiljs.org) se ne occupa.
Contiene la logica di gestione degli stati. [Jotai](https://jotai.org) se ne occupa.
* Selettori: Vedi [RecoilJS Selectors](https://recoiljs.org/docs/basic-tutorial/selectors) per ulteriori dettagli.
* Selettori: Gli atomi derivati (tramite `createAtomSelector`) calcolano valori da altri atomi e sono automaticamente memorizzati.
La gestione degli stati integrata di React si occupa ancora dello stato all'interno di un componente.
@@ -53,7 +53,7 @@ Il progetto ha una struttura chiara e semplice, con un codice boilerplate minimo
* [React](https://react.dev/)
* [Apollo](https://www.apollographql.com/docs/)
* [GraphQL Codegen](https://the-guild.dev/graphql/codegen)
* [Recoil](https://recoiljs.org/docs/introduction/core-concepts)
* [Jotai](https://jotai.org/)
* [TypeScript](https://www.typescriptlang.org/)
**Testing**
@@ -77,7 +77,7 @@ Per evitare [re-render](/l/it/developers/contribute/capabilities/frontend-develo
### Gestione dello stato
[Recoil](https://recoiljs.org/docs/introduction/core-concepts) gestisce lo stato.
[Jotai](https://jotai.org/) gestisce lo stato.
Vedi [best practices](/l/it/developers/contribute/capabilities/frontend-development/best-practices-front#state-management) per ulteriori informazioni sulla gestione dello stato.
@@ -160,7 +160,7 @@ export enum PageHotkeyScope {
}
```
Internamente, l'ambito selezionato attualmente viene memorizzato in uno stato Recoil condiviso in tutta l'applicazione:
Internamente, l'ambito selezionato attualmente viene memorizzato in uno stato Jotai condiviso in tutta l'applicazione:
```tsx
export const currentHotkeyScopeState = createState<HotkeyScope>({
@@ -169,10 +169,10 @@ export const currentHotkeyScopeState = createState<HotkeyScope>({
});
```
Ma questo stato Recoil non dovrebbe mai essere gestito manualmente! Vedremo come usarlo nella sezione successiva.
Ma questo stato Jotai 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 Recoil per gestire lo stato dell'ambito del tasto di scelta rapida e renderlo disponibile ovunque nell'applicazione.
Abbiamo anche creato uno stato Jotai per gestire lo stato dell'ambito del tasto di scelta rapida e renderlo disponibile ovunque nell'applicazione.
@@ -165,7 +165,7 @@ export default defineObject({
Comandi successivi aggiungeranno altri file e cartelle:
* `yarn twenty app:dev` genererà automaticamente un client API tipizzato in `node_modules/twenty-sdk/generated` (client Twenty tipizzato + tipi dell'area di lavoro).
* `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 entity:add` will add entity definition files under `src/` for your custom objects, functions, front components, roles, skills, and more.
## Autenticazione
@@ -426,10 +426,10 @@ Ogni file di funzione usa `defineLogicFunction()` per esportare una configurazio
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
import { CoreApiClient, type Person } from 'twenty-sdk/generated';
const handler = async (params: RoutePayload) => {
const client = new Twenty(); // generated typed client
const client = new CoreApiClient();
const name = 'name' in params.queryStringParameters
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
@@ -644,10 +644,10 @@ Per contrassegnare una funzione logica come strumento, imposta `isTool: true` e
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/generated';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new Twenty();
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
@@ -705,7 +705,7 @@ Punti chiave:
I componenti front-end ti consentono di creare componenti React personalizzati che vengono renderizzati all'interno dell'interfaccia di Twenty. Usa `defineFrontComponent()` per definire componenti con convalida integrata:
```typescript
// src/my-widget.front-component.tsx
// src/front-components/my-widget.tsx
import { defineFrontComponent } from 'twenty-sdk';
const MyWidget = () => {
@@ -728,14 +728,13 @@ export default defineFrontComponent({
Punti chiave:
* I componenti front-end sono componenti React che eseguono il rendering in contesti isolati all'interno di Twenty.
* Usa il suffisso di file `*.front-component.tsx` per il rilevamento automatico.
* Il campo `component` fa riferimento al tuo componente React.
* I componenti vengono compilati e sincronizzati automaticamente durante `yarn twenty app:dev`.
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.
* **Manuale**: Crea un nuovo file `*.front-component.tsx` e usa `defineFrontComponent()`.
* **Manual**: Create a new `.tsx` file and use `defineFrontComponent()`, following the same pattern.
### Abilità
@@ -772,18 +771,24 @@ 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.
### Client tipizzato generato
### Generated typed clients
Il client tipizzato è generato automaticamente da `yarn twenty app:dev` e salvato in `node_modules/twenty-sdk/generated` in base allo schema della tua area di lavoro. Usalo nelle tue funzioni:
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
```typescript
import Twenty from '~/generated';
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';
const client = new Twenty();
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
```
Il client viene rigenerato automaticamente da `yarn twenty app:dev` ogni volta che i tuoi oggetti o campi cambiano.
Both clients are re-generated automatically by `yarn twenty app:dev` whenever your objects or fields change.
#### Credenziali di runtime nelle funzioni logiche
@@ -800,17 +805,17 @@ Note:
#### Caricamento dei file
Il client `Twenty` 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.
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.
```typescript
import Twenty from '~/generated';
import { MetadataApiClient } from 'twenty-sdk/generated';
import * as fs from 'fs';
const client = new Twenty();
const metadataClient = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
const uploadedFile = await client.uploadFile(
const uploadedFile = await metadataClient.uploadFile(
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type (defaults to 'application/octet-stream')
@@ -841,7 +846,7 @@ uploadFile(
Punti chiave:
* Il metodo invia il file al **metadata endpoint** (non all'endpoint GraphQL principale), dove la mutation di upload viene risolta.
* The `uploadFile` method is available on `MetadataApiClient` because the upload mutation is resolved by the `/metadata` endpoint.
* 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.
@@ -14,7 +14,6 @@ Un selettore di icone basato su menu a tendina che consente agli utenti di scegl
<Tab title="Utilizzo">
```jsx
import { RecoilRoot } from "recoil";
import React, { useState } from "react";
import { IconPicker } from "@/ui/input/components/IconPicker";
@@ -27,14 +26,12 @@ export const MyComponent = () => {
};
return (
<RecoilRoot>
<IconPicker
disabled={false}
onChange={handleIconChange}
selectedIconKey={selectedIcon}
variant="primary"
/>
</RecoilRoot>
);
};
```
@@ -13,7 +13,6 @@ Permette agli utenti di scegliere un valore da un elenco di opzioni predefinite.
<Tab title="Utilizzo">
```jsx
import { RecoilRoot } from 'recoil';
import { IconTwentyStar } from 'twenty-ui/display';
import { Select } from '@/ui/input/components/Select';
@@ -21,7 +20,6 @@ import { Select } from '@/ui/input/components/Select';
export const MyComponent = () => {
return (
<RecoilRoot>
<Select
className
disabled={false}
@@ -32,7 +30,6 @@ export const MyComponent = () => {
]}
value="option1"
/>
</RecoilRoot>
);
};
@@ -16,7 +16,6 @@ Consente agli utenti di inserire e modificare il testo.
<Tab title="Utilizzo">
```jsx
import { RecoilRoot } from "recoil";
import { TextInput } from "@/ui/input/components/TextInput";
export const MyComponent = () => {
@@ -29,7 +28,6 @@ export const MyComponent = () => {
};
return (
<RecoilRoot>
<TextInput
className
label="Username"
@@ -40,7 +38,6 @@ export const MyComponent = () => {
onKeyDown={handleKeyDown}
RightIcon={null}
/>
</RecoilRoot>
);
};
@@ -81,12 +78,10 @@ Componente di input testo che regola automaticamente la sua altezza in base al c
<Tab title="Utilizzo">
```jsx
import { RecoilRoot } from "recoil";
import { AutosizeTextInput } from "@/ui/input/components/AutosizeTextInput";
export const MyComponent = () => {
return (
<RecoilRoot>
<AutosizeTextInput
onValidate={() => console.log("onValidate function fired")}
minRows={1}
@@ -96,7 +91,6 @@ export const MyComponent = () => {
buttonTitle
value="Task: "
/>
</RecoilRoot>
);
};
```
@@ -6,9 +6,9 @@ title: ベストプラクティス',
## 状態管理
React と Recoil はコードベース内の状態管理を行います。
React と Jotai はコードベース内の状態管理を行います。
### 状態を保存するために `useRecoilState` を使用する
### 状態を保存するために `useAtomState` を使用する
状態を保存するために必要なだけ多くのアトムを作成するのがよいです。
@@ -17,13 +17,16 @@ React と Recoil はコードベース内の状態管理を行います。
</Warning>
```tsx
export const myAtomState = atom({
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
export const myAtomState = createAtomState<string>({
key: 'myAtomState',
default: 'default value',
defaultValue: 'default value',
});
export const MyComponent = () => {
const [myAtom, setMyAtom] = useRecoilState(myAtomState);
const [myAtom, setMyAtom] = useAtomState(myAtomState);
return (
<div>
@@ -40,7 +43,7 @@ export const MyComponent = () => {
状態の保存に `useRef` を使用するのは避けてください。
If you want to store state, you should use `useState` or `useRecoilState`.
If you want to store state, you should use `useState` or `useAtomState`.
いくつかの再レンダリングを防ぐために `useRef` が必要だと感じた場合は、[再レンダリングの管理方法](#managing-re-renders)を参照してください。
@@ -80,8 +83,8 @@ Apollo フックを使用してデータ取得ロジックにも同じことを
// ❌ 悪い例: データが変化していなくても再レンダーを引き起こす
// useEffect を再評価する必要があるため
export const PageComponent = () => {
const [data, setData] = useRecoilState(dataState);
const [someDependency] = useRecoilState(someDependencyState);
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
@@ -93,9 +96,7 @@ export const PageComponent = () => {
};
export const App = () => (
<RecoilRoot>
<PageComponent />
</RecoilRoot>
<PageComponent />
);
```
@@ -103,14 +104,14 @@ export const App = () => (
// ✅ 良い例: データが変化していなければ再レンダーは発生しない
// useEffect が別の兄弟コンポーネントで再評価されるため
export const PageComponent = () => {
const [data, setData] = useRecoilState(dataState);
const [data, setData] = useAtomState(dataState);
return <div>{data}</div>;
};
export const PageData = () => {
const [data, setData] = useRecoilState(dataState);
const [someDependency] = useRecoilState(someDependencyState);
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
@@ -122,16 +123,16 @@ export const PageData = () => {
};
export const App = () => (
<RecoilRoot>
<>
<PageData />
<PageComponent />
</RecoilRoot>
</>
);
```
### Recoilファミリー状態とファミリーセレクターを使用する
### Jotaiファミリー状態とファミリーセレクターを使用する
Recoil ファミリー状態とセレクターは、再レンダリングを回避するための優れた方法です。
Jotai ファミリー状態とセレクターは、再レンダリングを回避するための優れた方法です。
アイテムのリストを保存する必要があるときに有用です。
@@ -82,9 +82,9 @@ module1
### ステート
ステート管理のロジックを含みます。 [RecoilJS](https://recoiljs.org) がこれを管理します。
ステート管理のロジックを含みます。 [Jotai](https://jotai.org) がこれを管理します。
* セレクター: 詳細は[RecoilJSセレクター](https://recoiljs.org/docs/basic-tutorial/selectors)を参照してください
* セレクター: 派生アトム(`createAtomSelector`を使用)は他のアトムから値を計算し、自動的にメモ化されます
Reactの組み込みステート管理は依然としてコンポーネント内のステートを処理します。
@@ -53,7 +53,7 @@ npx nx run twenty-front:storybook:coverage # (needs yarn storybook:serve:dev to
* [React](https://react.dev/)
* [Apollo](https://www.apollographql.com/docs/)
* [GraphQL Codegen](https://the-guild.dev/graphql/codegen)
* [Recoil](https://recoiljs.org/docs/introduction/core-concepts)
* [Jotai](https://jotai.org/)
* [TypeScript](https://www.typescriptlang.org/)
**テスト**
@@ -77,7 +77,7 @@ To avoid unnecessary [re-renders](/l/ja/developers/contribute/capabilities/front
### 状態管理
[Recoil](https://recoiljs.org/docs/introduction/core-concepts)は状態管理を処理します。
[Jotai](https://jotai.org/)は状態管理を処理します。
状態管理に関する詳細な情報は[ベストプラクティス](/l/ja/developers/contribute/capabilities/frontend-development/best-practices-front#state-management)を参照してください。
@@ -160,7 +160,7 @@ export enum PageHotkeyScope {
}
```
内部的には、現在選択されているスコープはアプリケーション全体で共有されるRecoilステートに格納されています:
内部的には、現在選択されているスコープはアプリケーション全体で共有されるJotaiステートに格納されています:
```tsx
export const currentHotkeyScopeState = createState<HotkeyScope>({
@@ -169,10 +169,10 @@ export const currentHotkeyScopeState = createState<HotkeyScope>({
});
```
しかし、このRecoilステートは手動で処理しないでください! 次のセクションでその使用方法を見ていきます。 次のセクションでその使用方法を見ていきます。 次のセクションでその使用方法を見ていきます。
しかし、このJotaiステートは手動で処理しないでください! 次のセクションでその使用方法を見ていきます。 次のセクションでその使用方法を見ていきます。 次のセクションでその使用方法を見ていきます。
## 内部的にはどう機能しているのか?
[react-hotkeys-hook](https://react-hotkeys-hook.vercel.app/docs/intro)の上に薄いラッパーを作成し、より効率的にし、不必要な再レンダリングを避けます。
また、ホットキースコープの状態を処理し、アプリケーション全体で利用できるRecoilステートを作成しました。
また、ホットキースコープの状態を処理し、アプリケーション全体で利用できるJotaiステートを作成しました。
@@ -12,7 +12,6 @@ image: /images/user-guide/github/github-header.png
<Tabs>
<Tab title="使用方法">
```jsx
import { RecoilRoot } from "recoil";
import React, { useState } from "react";
import { IconPicker } from "@/ui/input/components/IconPicker";
@@ -25,14 +24,12 @@ image: /images/user-guide/github/github-header.png
};
return (
<RecoilRoot>
<IconPicker
disabled={false}
onChange={handleIconChange}
selectedIconKey={selectedIcon}
variant="primary"
/>
</RecoilRoot>
);
};
```
@@ -12,7 +12,6 @@ image: /images/user-guide/what-is-twenty/20.png
<Tabs>
<Tab title="使用方法">
```jsx
import { RecoilRoot } from 'recoil';
import { IconTwentyStar } from 'twenty-ui/display';
import { Select } from '@/ui/input/components/Select';
@@ -20,7 +19,6 @@ image: /images/user-guide/what-is-twenty/20.png
export const MyComponent = () => {
return (
<RecoilRoot>
<Select
className
disabled={false}
@@ -31,7 +29,6 @@ image: /images/user-guide/what-is-twenty/20.png
]}
value="option1"
/>
</RecoilRoot>
);
};
@@ -14,7 +14,6 @@ image: /images/user-guide/notes/notes_header.png
<Tabs>
<Tab title="使用方法">
```jsx
import { RecoilRoot } from "recoil";
import { TextInput } from "@/ui/input/components/TextInput";
export const MyComponent = () => {
@@ -27,7 +26,6 @@ image: /images/user-guide/notes/notes_header.png
};
return (
<RecoilRoot>
<TextInput
className
label="ユーザー名"
@@ -38,7 +36,6 @@ image: /images/user-guide/notes/notes_header.png
onKeyDown={handleKeyDown}
RightIcon={null}
/>
</RecoilRoot>
);
};
@@ -68,12 +65,10 @@ image: /images/user-guide/notes/notes_header.png
<Tabs>
<Tab title="使用方法">
```jsx
import { RecoilRoot } from "recoil";
import { AutosizeTextInput } from "@/ui/input/components/AutosizeTextInput";
export const MyComponent = () => {
return (
<RecoilRoot>
<AutosizeTextInput
onValidate={() => console.log("onValidate function fired")}
minRows={1}
@@ -83,7 +78,6 @@ image: /images/user-guide/notes/notes_header.png
buttonTitle
value="Task: "
/>
</RecoilRoot>
);
};
```
@@ -6,9 +6,9 @@ title: 모범 사례
## 상태 관리
React와 Recoil은 코드베이스에서 상태 관리를 처리합니다.
React와 Jotai는 코드베이스에서 상태 관리를 처리합니다.
### `useRecoilState`로 상태 저장하기
### `useAtomState`로 상태 저장하기
상태를 저장하는 데 필요한 만큼의 atom을 만드는 것이 좋은 습관입니다.
@@ -17,13 +17,16 @@ React와 Recoil은 코드베이스에서 상태 관리를 처리합니다.
</Warning>
```tsx
export const myAtomState = atom({
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
export const myAtomState = createAtomState<string>({
key: 'myAtomState',
default: 'default value',
defaultValue: 'default value',
});
export const MyComponent = () => {
const [myAtom, setMyAtom] = useRecoilState(myAtomState);
const [myAtom, setMyAtom] = useAtomState(myAtomState);
return (
<div>
@@ -40,7 +43,7 @@ export const MyComponent = () => {
상태 저장에 `useRef`를 사용하지 않도록 주의하십시오.
If you want to store state, you should use `useState` or `useRecoilState`.
If you want to store state, you should use `useState` or `useAtomState`.
일부 리렌더링을 방지하기 위해 `useRef`가 필요하다고 느낄 경우 [리렌더링 관리 방법](#managing-re-renders)을 참조하십시오.
@@ -80,8 +83,8 @@ Apollo 훅을 사용하여 데이터 페칭 로직에 동일한 규칙을 적용
// ❌ Bad, will cause re-renders even if data is not changing,
// because useEffect needs to be re-evaluated
export const PageComponent = () => {
const [data, setData] = useRecoilState(dataState);
const [someDependency] = useRecoilState(someDependencyState);
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
@@ -93,9 +96,7 @@ export const PageComponent = () => {
};
export const App = () => (
<RecoilRoot>
<PageComponent />
</RecoilRoot>
<PageComponent />
);
```
@@ -103,14 +104,14 @@ export const App = () => (
// ✅ Good, will not cause re-renders if data is not changing,
// because useEffect is re-evaluated in another sibling component
export const PageComponent = () => {
const [data, setData] = useRecoilState(dataState);
const [data, setData] = useAtomState(dataState);
return <div>{data}</div>;
};
export const PageData = () => {
const [data, setData] = useRecoilState(dataState);
const [someDependency] = useRecoilState(someDependencyState);
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
@@ -122,16 +123,16 @@ export const PageData = () => {
};
export const App = () => (
<RecoilRoot>
<>
<PageData />
<PageComponent />
</RecoilRoot>
</>
);
```
### Recoil 가족 상태 및 Recoil 가족 선택자 사용하기
### Jotai 가족 상태 및 Jotai 가족 선택자 사용하기
Recoil 가족 상태와 선택자는 리렌더링을 피하기 위한 훌륭한 방법입니다.
Jotai 가족 상태와 선택자는 리렌더링을 피하기 위한 훌륭한 방법입니다.
항목 목록을 저장해야 할 때 유용합니다.
@@ -82,9 +82,9 @@ module1
### 상태
상태 관리 로직이 포함되어 있습니다. [RecoilJS](https://recoiljs.org)가 이것을 처리합니다.
상태 관리 로직이 포함되어 있습니다. [Jotai](https://jotai.org)가 이것을 처리합니다.
* 셀렉터: 자세한 내용은 [RecoilJS 셀렉터](https://recoiljs.org/docs/basic-tutorial/selectors)를 참조하세요.
* 셀렉터: 파생 아톰(`createAtomSelector` 사용)은 다른 아톰에서 값을 계산하며 자동으로 메모이제이션됩니다.
React의 내장 상태 관리는 구성 요소 내에서의 상태를 여전히 처리합니다.
@@ -53,7 +53,7 @@ npx nx run twenty-front:storybook:coverage # (needs yarn storybook:serve:dev to
* [React](https://react.dev/)
* [Apollo](https://www.apollographql.com/docs/)
* [GraphQL Codegen](https://the-guild.dev/graphql/codegen)
* [Recoil](https://recoiljs.org/docs/introduction/core-concepts)
* [Jotai](https://jotai.org/)
* [TypeScript](https://www.typescriptlang.org/)
**테스트**
@@ -77,7 +77,7 @@ To avoid unnecessary [re-renders](/l/ko/developers/contribute/capabilities/front
### 상태 관리
[Recoil](https://recoiljs.org/docs/introduction/core-concepts)이 상태 관리를 처리합니다.
[Jotai](https://jotai.org/)이 상태 관리를 처리합니다.
상태 관리에 대한 자세한 정보는 [최고의 관례](/l/ko/developers/contribute/capabilities/frontend-development/best-practices-front#state-management)를 참조하십시오.
@@ -160,7 +160,7 @@ export enum PageHotkeyScope {
}
```
내부적으로, 현재 선택한 스코프는 애플리케이션 전반에 걸쳐 공유되는 Recoil 상태에 저장됩니다:
내부적으로, 현재 선택한 스코프는 애플리케이션 전반에 걸쳐 공유되는 Jotai 상태에 저장됩니다:
```tsx
export const currentHotkeyScopeState = createState<HotkeyScope>({
@@ -169,10 +169,10 @@ export const currentHotkeyScopeState = createState<HotkeyScope>({
});
```
하지만 이 Recoil 상태는 수동으로 처리해서는 안 됩니다! 다음 섹션에서 사용하는 방법을 배웁니다.
하지만 이 Jotai 상태는 수동으로 처리해서는 안 됩니다! 다음 섹션에서 사용하는 방법을 배웁니다.
## 내부적으로 어떻게 작동합니까?
[react-hotkeys-hook](https://react-hotkeys-hook.vercel.app/docs/intro) 위에 얇은 래퍼를 만들어 성능을 높이고 불필요한 재랜더링을 방지합니다.
또한 핫키 스코프 상태를 처리하고 애플리케이션 전반에서 사용할 수 있도록 한 Recoil 상태를 만듭니다.
또한 핫키 스코프 상태를 처리하고 애플리케이션 전반에서 사용할 수 있도록 한 Jotai 상태를 만듭니다.
@@ -12,7 +12,6 @@ image: /images/user-guide/github/github-header.png
<Tabs>
<Tab title="사용법">
```jsx
import { RecoilRoot } from "recoil";
import React, { useState } from "react";
import { IconPicker } from "@/ui/input/components/IconPicker";
@@ -25,14 +24,12 @@ image: /images/user-guide/github/github-header.png
};
return (
<RecoilRoot>
<IconPicker
disabled={false}
onChange={handleIconChange}
selectedIconKey={selectedIcon}
variant="primary"
/>
</RecoilRoot>
);
};
```
@@ -12,7 +12,6 @@ image: /images/user-guide/what-is-twenty/20.png
<Tabs>
<Tab title="사용법">
```jsx
import { RecoilRoot } from 'recoil';
import { IconTwentyStar } from 'twenty-ui/display';
import { Select } from '@/ui/input/components/Select';
@@ -20,7 +19,6 @@ image: /images/user-guide/what-is-twenty/20.png
export const MyComponent = () => {
return (
<RecoilRoot>
<Select
className
disabled={false}
@@ -31,7 +29,6 @@ image: /images/user-guide/what-is-twenty/20.png
]}
value="option1"
/>
</RecoilRoot>
);
};
@@ -14,7 +14,6 @@ image: /images/user-guide/notes/notes_header.png
<Tabs>
<Tab title="사용법">
```jsx
import { RecoilRoot } from "recoil";
import { TextInput } from "@/ui/input/components/TextInput";
export const MyComponent = () => {
@@ -27,7 +26,6 @@ image: /images/user-guide/notes/notes_header.png
};
return (
<RecoilRoot>
<TextInput
className
label="사용자 이름"
@@ -38,7 +36,6 @@ image: /images/user-guide/notes/notes_header.png
onKeyDown={handleKeyDown}
RightIcon={null}
/>
</RecoilRoot>
);
};
```
@@ -67,12 +64,10 @@ image: /images/user-guide/notes/notes_header.png
<Tabs>
<Tab title="사용법">
```jsx
import { RecoilRoot } from "recoil";
import { AutosizeTextInput } from "@/ui/input/components/AutosizeTextInput";
export const MyComponent = () => {
return (
<RecoilRoot>
<AutosizeTextInput
onValidate={() => console.log("onValidate 함수 실행됨")}
minRows={1}
@@ -82,7 +77,6 @@ image: /images/user-guide/notes/notes_header.png
buttonTitle
value="작업: "
/>
</RecoilRoot>
);
};
```
@@ -6,9 +6,9 @@ Este documento descreve as melhores práticas que você deve seguir ao trabalhar
## Gerenciamento de Estado
React e Recoil lidam com o gerenciamento de estado na base de código.
React e Jotai lidam com o gerenciamento de estado na base de código.
### Use `useRecoilState` para armazenar o estado
### Use `useAtomState` para armazenar o estado
É uma boa prática criar tantos átomos quanto necessário para armazenar seu estado.
@@ -19,13 +19,16 @@ React e Recoil lidam com o gerenciamento de estado na base de código.
</Warning>
```tsx
export const myAtomState = atom({
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
export const myAtomState = createAtomState<string>({
key: 'myAtomState',
default: 'default value',
defaultValue: 'default value',
});
export const MyComponent = () => {
const [myAtom, setMyAtom] = useRecoilState(myAtomState);
const [myAtom, setMyAtom] = useAtomState(myAtomState);
return (
<div>
@@ -42,7 +45,7 @@ export const MyComponent = () => {
Evite usar `useRef` para armazenar estado.
If you want to store state, you should use `useState` or `useRecoilState`.
If you want to store state, you should use `useState` or `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 +85,8 @@ Você pode aplicar o mesmo para lógica de busca de dados, com hooks do Apollo.
// ❌ Bad, will cause re-renders even if data is not changing,
// because useEffect needs to be re-evaluated
export const PageComponent = () => {
const [data, setData] = useRecoilState(dataState);
const [someDependency] = useRecoilState(someDependencyState);
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
@@ -95,9 +98,7 @@ export const PageComponent = () => {
};
export const App = () => (
<RecoilRoot>
<PageComponent />
</RecoilRoot>
<PageComponent />
);
```
@@ -105,14 +106,14 @@ export const App = () => (
// ✅ Good, will not cause re-renders if data is not changing,
// because useEffect is re-evaluated in another sibling component
export const PageComponent = () => {
const [data, setData] = useRecoilState(dataState);
const [data, setData] = useAtomState(dataState);
return <div>{data}</div>;
};
export const PageData = () => {
const [data, setData] = useRecoilState(dataState);
const [someDependency] = useRecoilState(someDependencyState);
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
@@ -124,16 +125,16 @@ export const PageData = () => {
};
export const App = () => (
<RecoilRoot>
<>
<PageData />
<PageComponent />
</RecoilRoot>
</>
);
```
### Use estados de família Recoil e seletores de família Recoil
### Use estados de família Jotai e seletores de família Jotai
Estados de família Recoil e seletores são uma ótima maneira de evitar re-renderizações.
Estados de família Jotai e seletores são uma ótima maneira de evitar re-renderizações.
Eles são úteis quando você precisa armazenar uma lista de itens.
@@ -82,9 +82,9 @@ Veja [Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks) para mais
### Estados
Contém a lógica de gerenciamento de estado. [RecoilJS](https://recoiljs.org) lida com isso.
Contém a lógica de gerenciamento de estado. [Jotai](https://jotai.org) lida com isso.
* Seletores: Veja [RecoilJS Selectors](https://recoiljs.org/docs/basic-tutorial/selectors) para mais detalhes.
* Seletores: Átomos derivados (usando `createAtomSelector`) calculam valores a partir de outros átomos e são automaticamente memorizados.
O gerenciamento de estado embutido do React ainda lida com o estado dentro de um componente.
@@ -53,7 +53,7 @@ O projeto possui uma pilha limpa e simples, com pouco código boilerplate.
* [React](https://react.dev/)
* [Apollo](https://www.apollographql.com/docs/)
* [GraphQL Codegen](https://the-guild.dev/graphql/codegen)
* [Recoil](https://recoiljs.org/docs/introduction/core-concepts)
* [Jotai](https://jotai.org/)
* [TypeScript](https://www.typescriptlang.org/)
**Testes**
@@ -77,7 +77,7 @@ To avoid unnecessary [re-renders](/l/pt/developers/contribute/capabilities/front
### Gerenciamento de Estado
[Recoil](https://recoiljs.org/docs/introduction/core-concepts) gerencia o estado.
[Jotai](https://jotai.org/) gerencia o estado.
Veja [melhores práticas](/l/pt/developers/contribute/capabilities/frontend-development/best-practices-front#state-management) para mais informações sobre gerenciamento de estado.
@@ -160,7 +160,7 @@ export enum PageHotkeyScope {
}
```
Internamente, o escopo atualmente selecionado é armazenado em um estado Recoil que é compartilhado por toda a aplicação :
Internamente, o escopo atualmente selecionado é armazenado em um estado 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 Recoil nunca deve ser manipulado manualmente! Veremos como usá-lo na próxima seção.
Mas esse estado Jotai 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 Recoil para gerenciar o estado do escopo da tecla de atalho e torná-lo disponível em toda a aplicação.
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.
@@ -165,7 +165,7 @@ export default defineObject({
Comandos posteriores adicionarão mais arquivos e pastas:
* `yarn twenty app:dev` vai gerar automaticamente um cliente de API tipado em `node_modules/twenty-sdk/generated` (cliente Twenty tipado + tipos do espaço de trabalho).
* `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 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
@@ -426,10 +426,10 @@ Cada arquivo de função usa `defineLogicFunction()` para exportar uma configura
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
import { CoreApiClient, type Person } from 'twenty-sdk/generated';
const handler = async (params: RoutePayload) => {
const client = new Twenty(); // generated typed client
const client = new CoreApiClient();
const name = 'name' in params.queryStringParameters
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
@@ -644,10 +644,10 @@ Para marcar uma função lógica como ferramenta, defina `isTool: true` e forne
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/generated';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new Twenty();
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
@@ -705,7 +705,7 @@ Pontos-chave:
Componentes de front-end permitem criar componentes React personalizados que são renderizados na UI do Twenty. Use `defineFrontComponent()` para definir componentes com validação integrada:
```typescript
// src/my-widget.front-component.tsx
// src/front-components/my-widget.tsx
import { defineFrontComponent } from 'twenty-sdk';
const MyWidget = () => {
@@ -728,14 +728,13 @@ export default defineFrontComponent({
Pontos-chave:
* Componentes de front-end são componentes React que renderizam em contextos isolados dentro do Twenty.
* Use o sufixo de arquivo `*.front-component.tsx` para detecção automática.
* O campo `component` faz referência ao seu componente React.
* Os componentes são compilados e sincronizados automaticamente durante `yarn twenty app:dev`.
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**: Crie um novo arquivo `*.front-component.tsx` e use `defineFrontComponent()`.
* **Manual**: Create a new `.tsx` file and use `defineFrontComponent()`, following the same pattern.
### Habilidades
@@ -772,18 +771,24 @@ 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.
### Cliente tipado gerado
### Generated typed clients
O cliente tipado é gerado automaticamente pelo `yarn twenty app:dev` e armazenado em `node_modules/twenty-sdk/generated` com base no esquema do seu espaço de trabalho. Use-o em suas funções:
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
```typescript
import Twenty from '~/generated';
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';
const client = new Twenty();
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
```
O cliente é regenerado automaticamente pelo `yarn twenty app:dev` sempre que seus objetos ou campos forem alterados.
Both clients are re-generated automatically by `yarn twenty app:dev` whenever your objects or fields change.
#### Credenciais em tempo de execução em funções de lógica
@@ -800,17 +805,17 @@ Notas:
#### Carregamento de ficheiros
O cliente `Twenty` 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).
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).
```typescript
import Twenty from '~/generated';
import { MetadataApiClient } from 'twenty-sdk/generated';
import * as fs from 'fs';
const client = new Twenty();
const metadataClient = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
const uploadedFile = await client.uploadFile(
const uploadedFile = await metadataClient.uploadFile(
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type (defaults to 'application/octet-stream')
@@ -841,7 +846,7 @@ uploadFile(
Pontos-chave:
* O método envia o arquivo para o **endpoint de metadados** (não para o endpoint GraphQL principal), onde a mutação de upload é resolvida.
* The `uploadFile` method is available on `MetadataApiClient` because the upload mutation is resolved by the `/metadata` endpoint.
* 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.
@@ -14,7 +14,6 @@ Um seletor de ícones baseado em lista suspensa que permite aos usuários seleci
<Tab title="Uso">
```jsx
import { RecoilRoot } from "recoil";
import React, { useState } from "react";
import { IconPicker } from "@/ui/input/components/IconPicker";
@@ -27,14 +26,12 @@ export const MyComponent = () => {
};
return (
<RecoilRoot>
<IconPicker
disabled={false}
onChange={handleIconChange}
selectedIconKey={selectedIcon}
variant="primary"
/>
</RecoilRoot>
);
};
```
@@ -13,7 +13,6 @@ Permite aos utilizadores escolher um valor a partir de uma lista de opções pr
<Tab title="Uso">
```jsx
import { RecoilRoot } from 'recoil';
import { IconTwentyStar } from 'twenty-ui/display';
import { Select } from '@/ui/input/components/Select';
@@ -21,7 +20,6 @@ import { Select } from '@/ui/input/components/Select';
export const MyComponent = () => {
return (
<RecoilRoot>
<Select
className
disabled={false}
@@ -32,7 +30,6 @@ export const MyComponent = () => {
]}
value="option1"
/>
</RecoilRoot>
);
};
```
@@ -16,7 +16,6 @@ Permite aos usuários inserir e editar texto.
<Tab title="Uso">
```jsx
import { RecoilRoot } from "recoil";
import { TextInput } from "@/ui/input/components/TextInput";
export const MyComponent = () => {
@@ -29,7 +28,6 @@ export const MyComponent = () => {
};
return (
<RecoilRoot>
<TextInput
className
label="Username"
@@ -40,7 +38,6 @@ export const MyComponent = () => {
onKeyDown={handleKeyDown}
RightIcon={null}
/>
</RecoilRoot>
);
};
@@ -81,12 +78,10 @@ Componente de entrada de texto que ajusta automaticamente sua altura com base no
<Tab title="Uso">
```jsx
import { RecoilRoot } from "recoil";
import { AutosizeTextInput } from "@/ui/input/components/AutosizeTextInput";
export const MyComponent = () => {
return (
<RecoilRoot>
<AutosizeTextInput
onValidate={() => console.log("onValidate function fired")}
minRows={1}
@@ -96,7 +91,6 @@ export const MyComponent = () => {
buttonTitle
value="Task: "
/>
</RecoilRoot>
);
};
```
@@ -6,9 +6,9 @@ Acest document prezintă cele mai bune practici pe care ar trebui să le urmați
## Managementul stării
React și Recoil se ocupă de managementul stării în cod.
React și Jotai se ocupă de managementul stării în cod.
### Folosiți `useRecoilState` pentru a stoca starea
### Folosiți `useAtomState` pentru a stoca starea
Este o bună practică să creezi atâția atomi câți ai nevoie pentru a-ți stoca starea.
@@ -19,13 +19,16 @@ Este mai bine să folosești atomi suplimentari decât să încerci să fii prea
</Warning>
```tsx
export const myAtomState = atom({
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
export const myAtomState = createAtomState<string>({
key: 'myAtomState',
default: 'default value',
defaultValue: 'default value',
});
export const MyComponent = () => {
const [myAtom, setMyAtom] = useRecoilState(myAtomState);
const [myAtom, setMyAtom] = useAtomState(myAtomState);
return (
<div>
@@ -42,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 `useRecoilState`.
Dacă doriți să stocați starea, ar trebui să folosiți `useState` sau `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.
@@ -82,8 +85,8 @@ Puteți aplica același principiu pentru logica de interogare de date, folosind
// ❌ Greșit, va provoca re-randări chiar dacă datele nu se schimbă,
// deoarece useEffect trebuie re-evaluat
export const PageComponent = () => {
const [data, setData] = useRecoilState(dataState);
const [someDependency] = useRecoilState(someDependencyState);
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
@@ -95,9 +98,7 @@ export const PageComponent = () => {
};
export const App = () => (
<RecoilRoot>
<PageComponent />
</RecoilRoot>
<PageComponent />
);
```
@@ -105,14 +106,14 @@ export const App = () => (
// ✅ Bun, nu va provoca re-randări dacă datele nu se schimbă,
// deoarece useEffect este re-evaluat într-o altă componentă la același nivel
export const PageComponent = () => {
const [data, setData] = useRecoilState(dataState);
const [data, setData] = useAtomState(dataState);
return <div>{data}</div>;
};
export const PageData = () => {
const [data, setData] = useRecoilState(dataState);
const [someDependency] = useRecoilState(someDependencyState);
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
@@ -124,16 +125,16 @@ export const PageData = () => {
};
export const App = () => (
<RecoilRoot>
<>
<PageData />
<PageComponent />
</RecoilRoot>
</>
);
```
### Folosiți stări de familie și selectoare de familie cu recoil
### Folosiți stări de familie și selectoare de familie cu jotai
Stările și selectoarele de familie cu recoil sunt o metodă excelentă de a evita re-render-urile.
Stările și selectoarele de familie cu jotai sunt o metodă excelentă de a evita re-render-urile.
Sunt utile când trebuie să stocați o listă de elemente.
@@ -82,9 +82,9 @@ Vezi [Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks) pentru mai
### Stări
Conține logica de gestionare a stării. [RecoilJS](https://recoiljs.org) gestionează aceasta.
Conține logica de gestionare a stării. [Jotai](https://jotai.org) gestionează aceasta.
* Selectori: Vezi [RecoilJS Selectors](https://recoiljs.org/docs/basic-tutorial/selectors) pentru mai multe detalii.
* Selectori: Atomii derivați (folosind `createAtomSelector`) calculează valori din alți atomi și sunt memorați automat.
Managementul de stare încorporat în React gestionează încă starea în cadrul unei componente.
@@ -53,7 +53,7 @@ Proiectul are un stack curat și simplu, cu cod boilerplate minim.
* [React](https://react.dev/)
* [Apollo](https://www.apollographql.com/docs/)
* [GraphQL Codegen](https://the-guild.dev/graphql/codegen)
* [Recoil](https://recoiljs.org/docs/introduction/core-concepts)
* [Jotai](https://jotai.org/)
* [TypeScript](https://www.typescriptlang.org/)
**Testare**
@@ -77,7 +77,7 @@ Pentru a evita [re-redări](/l/ro/developers/contribute/capabilities/frontend-de
### Managementul stării
[Recoil](https://recoiljs.org/docs/introduction/core-concepts) se ocupă de managementul stării.
[Jotai](https://jotai.org/) se ocupă de managementul stării.
Consultați [cele mai bune practici](/l/ro/developers/contribute/capabilities/frontend-development/best-practices-front#state-management) pentru mai multe informații despre managementul stării.
@@ -160,7 +160,7 @@ export enum PageHotkeyScope {
}
```
Intern, domeniul selectat în prezent este stocat într-o stare Recoil care este partajată în toată aplicația:
Intern, domeniul selectat în prezent este stocat într-o stare 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 Recoil nu ar trebui să fie gestionată manual! Vom vedea cum să o folosim în secțiunea următoare.
Însă această stare Jotai nu ar trebui 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 Recoil pentru a gestiona starea domeniului comenzilor rapide și să fie disponibilă oriunde în aplicație.
De asemenea, creăm o stare Jotai pentru a gestiona starea domeniului comenzilor rapide și să fie disponibilă oriunde în aplicație.
@@ -165,7 +165,7 @@ export default defineObject({
Comenzile ulterioare vor adăuga mai multe fișiere și foldere:
* `yarn twenty app:dev` va genera automat un client API tipizat în `node_modules/twenty-sdk/generated` (client Twenty tipizat + tipuri ale spațiului de lucru).
* `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 entity:add` va adăuga fișiere de definire a entităților în `src/` pentru obiectele, funcțiile, componentele front-end, rolurile, abilitățile și altele.
## Autentificare
@@ -426,10 +426,10 @@ Fiecare fișier de funcție folosește `defineLogicFunction()` pentru a exporta
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
import { CoreApiClient, type Person } from 'twenty-sdk/generated';
const handler = async (params: RoutePayload) => {
const client = new Twenty(); // generated typed client
const client = new CoreApiClient();
const name = 'name' in params.queryStringParameters
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
@@ -644,10 +644,10 @@ Pentru a marca o funcție logică drept instrument, setați `isTool: true` și f
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/generated';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new Twenty();
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
@@ -705,7 +705,7 @@ Puncte cheie:
Componentele Front vă permit să construiți componente React personalizate care sunt randate în interfața Twenty. Utilizați `defineFrontComponent()` pentru a defini componente cu validare încorporată:
```typescript
// src/my-widget.front-component.tsx
// src/front-components/my-widget.tsx
import { defineFrontComponent } from 'twenty-sdk';
const MyWidget = () => {
@@ -728,14 +728,13 @@ export default defineFrontComponent({
Puncte cheie:
* Componentele Front sunt componente React care sunt randate în contexte izolate în cadrul Twenty.
* Folosiți sufixul de fișier `*.front-component.tsx` pentru detectare automată.
* Câmpul `component` face referire la componenta React.
* Componentele sunt construite și sincronizate automat în timpul `yarn twenty app:dev`.
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**: Creați un fișier nou `*.front-component.tsx` și folosiți `defineFrontComponent()`.
* **Manual**: Create a new `.tsx` file and use `defineFrontComponent()`, following the same pattern.
### Abilități
@@ -772,18 +771,24 @@ Puteți crea abilități noi în două moduri:
* **Generat**: Rulați `yarn twenty entity:add` și alegeți opțiunea de a adăuga o abilitate nouă.
* **Manual**: Creați un fișier nou și folosiți `defineSkill()`, urmând același model.
### Client tipizat generat
### Generated typed clients
Clientul tipizat este generat automat de `yarn twenty app:dev` și stocat în `node_modules/twenty-sdk/generated`, pe baza schemei spațiului tău de lucru. Folosiți-l în funcțiile dvs.:
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
```typescript
import Twenty from '~/generated';
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';
const client = new Twenty();
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
```
Clientul este regenerat automat de `yarn twenty app:dev` ori de câte ori obiectele sau câmpurile tale se schimbă.
Both clients are re-generated automatically by `yarn twenty app:dev` whenever your objects or fields change.
#### Acreditări la runtime în funcțiile de logică
@@ -800,17 +805,17 @@ Notițe:
#### Încărcarea fișierelor
Clientul `Twenty` generat include o metodă `uploadFile` pentru atașarea fișierelor la câmpuri de tip fișier ale obiectelor din spațiul 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).
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).
```typescript
import Twenty from '~/generated';
import { MetadataApiClient } from 'twenty-sdk/generated';
import * as fs from 'fs';
const client = new Twenty();
const metadataClient = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
const uploadedFile = await client.uploadFile(
const uploadedFile = await metadataClient.uploadFile(
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type (defaults to 'application/octet-stream')
@@ -841,7 +846,7 @@ uploadFile(
Puncte cheie:
* Metoda trimite fișierul către **metadata endpoint** (nu către endpoint-ul principal GraphQL), unde mutația de încărcare este rezolvată.
* The `uploadFile` method is available on `MetadataApiClient` because the upload mutation is resolved by the `/metadata` endpoint.
* 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.
@@ -14,7 +14,6 @@ Un selector de iconițe bazat pe listă derulantă care permite utilizatorilor s
<Tab title="Utilizare">
```jsx
import { RecoilRoot } from "recoil";
import React, { useState } from "react";
import { IconPicker } from "@/ui/input/components/IconPicker";
@@ -27,14 +26,12 @@ export const MyComponent = () => {
};
return (
<RecoilRoot>
<IconPicker
disabled={false}
onChange={handleIconChange}
selectedIconKey={selectedIcon}
variant="primary"
/>
</RecoilRoot>
);
};
```
@@ -13,7 +13,6 @@ Permite utilizatorilor să aleagă o valoare dintr-o listă de opțiuni predefin
<Tab title="Utilizare">
```jsx
import { RecoilRoot } from 'recoil';
import { IconTwentyStar } from 'twenty-ui/display';
import { Select } from '@/ui/input/components/Select';
@@ -21,7 +20,6 @@ import { Select } from '@/ui/input/components/Select';
export const MyComponent = () => {
return (
<RecoilRoot>
<Select
className
disabled={false}
@@ -32,7 +30,6 @@ export const MyComponent = () => {
]}
value="option1"
/>
</RecoilRoot>
);
};
@@ -16,7 +16,6 @@ Permite utilizatorilor să introducă și să editeze text.
<Tab title="Utilizare">
```jsx
import { RecoilRoot } from "recoil";
import { TextInput } from "@/ui/input/components/TextInput";
export const MyComponent = () => {
@@ -29,7 +28,6 @@ export const MyComponent = () => {
};
return (
<RecoilRoot>
<TextInput
className
label="Username"
@@ -40,7 +38,6 @@ export const MyComponent = () => {
onKeyDown={handleKeyDown}
RightIcon={null}
/>
</RecoilRoot>
);
};
@@ -81,12 +78,10 @@ Componenta de intrare text care își ajustează automat înălțimea în funcț
<Tab title="Utilizare">
```jsx
import { RecoilRoot } from "recoil";
import { AutosizeTextInput } from "@/ui/input/components/AutosizeTextInput";
export const MyComponent = () => {
return (
<RecoilRoot>
<AutosizeTextInput
onValidate={() => console.log("onValidate function fired")}
minRows={1}
@@ -96,7 +91,6 @@ export const MyComponent = () => {
buttonTitle
value="Task: "
/>
</RecoilRoot>
);
};
```
@@ -6,9 +6,9 @@ title: Лучшие практики
## Управление состоянием
React и Recoil отвечают за управление состоянием в коде.
React и Jotai отвечают за управление состоянием в коде.
### Используйте `useRecoilState` для хранения состояния
### Используйте `useAtomState` для хранения состояния
Полезно создавать столько атомов, сколько вам нужно для хранения состояния.
@@ -19,13 +19,16 @@ React и Recoil отвечают за управление состоянием
</Warning>
```tsx
export const myAtomState = atom({
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
export const myAtomState = createAtomState<string>({
key: 'myAtomState',
default: 'default value',
defaultValue: 'default value',
});
export const MyComponent = () => {
const [myAtom, setMyAtom] = useRecoilState(myAtomState);
const [myAtom, setMyAtom] = useAtomState(myAtomState);
return (
<div>
@@ -42,7 +45,7 @@ export const MyComponent = () => {
Избегайте использования `useRef` для хранения состояния.
Если вы хотите сохранить состояние, вам следует использовать `useState` или `useRecoilState`.
Если вы хотите сохранить состояние, вам следует использовать `useState` или `useAtomState`.
Смотрите [как управлять повторными рендерами](#managing-re-renders), если вы считаете, что вам нужен `useRef`, чтобы предотвратить их.
@@ -82,8 +85,8 @@ export const MyComponent = () => {
// ❌ Bad, will cause re-renders even if data is not changing,
// because useEffect needs to be re-evaluated
export const PageComponent = () => {
const [data, setData] = useRecoilState(dataState);
const [someDependency] = useRecoilState(someDependencyState);
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
@@ -95,9 +98,7 @@ export const PageComponent = () => {
};
export const App = () => (
<RecoilRoot>
<PageComponent />
</RecoilRoot>
<PageComponent />
);
```
@@ -105,14 +106,14 @@ export const App = () => (
// ✅ Good, will not cause re-renders if data is not changing,
// because useEffect is re-evaluated in another sibling component
export const PageComponent = () => {
const [data, setData] = useRecoilState(dataState);
const [data, setData] = useAtomState(dataState);
return <div>{data}</div>;
};
export const PageData = () => {
const [data, setData] = useRecoilState(dataState);
const [someDependency] = useRecoilState(someDependencyState);
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
@@ -124,16 +125,16 @@ export const PageData = () => {
};
export const App = () => (
<RecoilRoot>
<>
<PageData />
<PageComponent />
</RecoilRoot>
</>
);
```
### Используйте состояния семейства Recoil и селекторы семейства Recoil
### Используйте состояния семейства Jotai и селекторы семейства Jotai
Состояния семейства Recoil и селекторы — отличный способ избежать повторных рендеров.
Состояния семейства Jotai и селекторы — отличный способ избежать повторных рендеров.
Они полезны, когда нужно хранить список элементов.
@@ -82,9 +82,9 @@ module1
### Состояния
Содержит логику управления состоянием. [RecoilJS](https://recoiljs.org) этим управляет.
Содержит логику управления состоянием. [Jotai](https://jotai.org) этим управляет.
* Селекторы: См. [RecoilJS Selectors](https://recoiljs.org/docs/basic-tutorial/selectors) для более подробной информации.
* Селекторы: Производные атомы (с использованием `createAtomSelector`) вычисляют значения из других атомов и автоматически мемоизируются.
Встроенное управление состоянием в React все еще управляет состоянием внутри компонента.
@@ -53,7 +53,7 @@ npx nx run twenty-front:storybook:coverage # (требуется yarn storybook:
* [React](https://react.dev/)
* [Apollo](https://www.apollographql.com/docs/)
* [GraphQL Codegen](https://the-guild.dev/graphql/codegen)
* [Recoil](https://recoiljs.org/docs/introduction/core-concepts)
* [Jotai](https://jotai.org/)
* [TypeScript](https://www.typescriptlang.org/)
**Тестирование**
@@ -77,7 +77,7 @@ npx nx run twenty-front:storybook:coverage # (требуется yarn storybook:
### Управление состоянием
[Recoil](https://recoiljs.org/docs/introduction/core-concepts) обрабатывает управление состоянием.
[Jotai](https://jotai.org/) обрабатывает управление состоянием.
[лучшие практики](/l/ru/developers/contribute/capabilities/frontend-development/best-practices-front#state-management) для получения дополнительной информации об управлении состоянием.
@@ -160,7 +160,7 @@ export enum PageHotkeyScope {
}
```
Внутренне текущая выбранная область хранится в состоянии Recoil, которое используется по всему приложению:
Внутренне текущая выбранная область хранится в состоянии Jotai, которое используется по всему приложению:
```tsx
export const currentHotkeyScopeState = createState<HotkeyScope>({
@@ -169,10 +169,10 @@ export const currentHotkeyScopeState = createState<HotkeyScope>({
});
```
Но это состояние Recoil никогда не следует обрабатывать вручную! Мы увидим, как использовать это в следующем разделе.
Но это состояние Jotai никогда не следует обрабатывать вручную! Мы увидим, как использовать это в следующем разделе.
## Как это работает внутренне?
Мы сделали тонкую обертку поверх [react-hotkeys-hook](https://react-hotkeys-hook.vercel.app/docs/intro), которая делает его более производительным и избегает ненужных повторных рендеров.
Мы также создаем состояние Recoil, чтобы управлять состоянием горячей клавиши и сделать его доступным везде в приложении.
Мы также создаем состояние Jotai, чтобы управлять состоянием горячей клавиши и сделать его доступным везде в приложении.
@@ -165,7 +165,7 @@ export default defineObject({
Позднее команды добавят больше файлов и папок:
* `yarn twenty app:dev` автоматически сгенерирует типизированный клиент API в `node_modules/twenty-sdk/generated` (типизированный клиент Twenty + типы рабочего пространства).
* `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 entity:add` добавит файлы определений сущностей в `src/` для ваших пользовательских объектов, функций, фронтенд-компонентов, ролей, навыков и многого другого.
## Аутентификация
@@ -426,10 +426,10 @@ export default defineRole({
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
import { CoreApiClient, type Person } from 'twenty-sdk/generated';
const handler = async (params: RoutePayload) => {
const client = new Twenty(); // generated typed client
const client = new CoreApiClient();
const name = 'name' in params.queryStringParameters
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
@@ -644,10 +644,10 @@ const handler = async (event: RoutePayload) => {
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/generated';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new Twenty();
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
@@ -705,7 +705,7 @@ export default defineLogicFunction({
Фронт-компоненты позволяют создавать пользовательские компоненты React, которые рендерятся внутри интерфейса Twenty. Используйте `defineFrontComponent()` для определения компонентов со встроенной валидацией:
```typescript
// src/my-widget.front-component.tsx
// src/front-components/my-widget.tsx
import { defineFrontComponent } from 'twenty-sdk';
const MyWidget = () => {
@@ -728,14 +728,13 @@ export default defineFrontComponent({
Основные моменты:
* Фронт-компоненты — это компоненты React, которые рендерятся в изолированных контекстах внутри Twenty.
* Используйте суффикс файла `*.front-component.tsx` для автоматического обнаружения.
* Поле `component` ссылается на ваш компонент React.
* Компоненты автоматически собираются и синхронизируются во время `yarn twenty app:dev`.
Вы можете создать новые фронт-компоненты двумя способами:
* **Сгенерировано**: Запустите `yarn twenty entity:add` и выберите опцию добавления нового фронтенд-компонента.
* **Вручную**: Создайте новый файл `*.front-component.tsx` и используйте `defineFrontComponent()`.
* **Manual**: Create a new `.tsx` file and use `defineFrontComponent()`, following the same pattern.
### Навыки
@@ -772,18 +771,24 @@ export default defineSkill({
* **Сгенерировано**: Запустите `yarn twenty entity:add` и выберите опцию добавления нового навыка.
* **Вручную**: Создайте новый файл и используйте `defineSkill()`, следуя тому же шаблону.
### Сгенерированный типизированный клиент
### Generated typed clients
Типизированный клиент автоматически генерируется с помощью `yarn twenty app:dev` и сохраняется в `node_modules/twenty-sdk/generated` на основе схемы вашего рабочего пространства. Используйте его в своих функциях:
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
```typescript
import Twenty from '~/generated';
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';
const client = new Twenty();
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
```
Клиент автоматически перегенерируется с помощью `yarn twenty app:dev` при изменении ваших объектов или полей.
Both clients are re-generated automatically by `yarn twenty app:dev` whenever your objects or fields change.
#### Учётные данные времени выполнения в логических функциях
@@ -800,17 +805,17 @@ const { me } = await client.query({ me: { id: true, displayName: true } });
#### Загрузка файлов
Сгенерированный клиент `Twenty` включает метод `uploadFile` для прикрепления файлов к полям типа «файл» в объектах вашего рабочего пространства. Поскольку стандартные клиенты GraphQL изначально не поддерживают многочастовую загрузку файлов, клиент предоставляет специальный метод, который под капотом реализует [спецификацию многочастных запросов GraphQL](https://github.com/jaydenseric/graphql-multipart-request-spec).
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).
```typescript
import Twenty from '~/generated';
import { MetadataApiClient } from 'twenty-sdk/generated';
import * as fs from 'fs';
const client = new Twenty();
const metadataClient = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
const uploadedFile = await client.uploadFile(
const uploadedFile = await metadataClient.uploadFile(
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type (defaults to 'application/octet-stream')
@@ -841,7 +846,7 @@ uploadFile(
Основные моменты:
* Метод отправляет файл на **metadata endpoint** (не на основной GraphQL endpoint), где обрабатывается мутация загрузки.
* The `uploadFile` method is available on `MetadataApiClient` because the upload mutation is resolved by the `/metadata` endpoint.
* Он использует `universalIdentifier` поля (а не его идентификатор, специфичный для рабочего пространства), поэтому ваш код загрузки будет работать в любом рабочем пространстве, где установлено ваше приложение — в соответствии с тем, как приложения ссылаются на поля повсюду.
* Возвращаемый `url` — это подписанный URL, который можно использовать для доступа к загруженному файлу.
@@ -14,7 +14,6 @@ image: /images/user-guide/github/github-header.png
<Tab title="Использование">
```jsx
import { RecoilRoot } from "recoil";
import React, { useState } from "react";
import { IconPicker } from "@/ui/input/components/IconPicker";
@@ -27,14 +26,12 @@ export const MyComponent = () => {
};
return (
<RecoilRoot>
<IconPicker
disabled={false}
onChange={handleIconChange}
selectedIconKey={selectedIcon}
variant="primary"
/>
</RecoilRoot>
);
};
```
@@ -13,7 +13,6 @@ image: /images/user-guide/what-is-twenty/20.png
<Tab title="Использование">
```jsx
import { RecoilRoot } from 'recoil';
import { IconTwentyStar } from 'twenty-ui/display';
import { Select } from '@/ui/input/components/Select';
@@ -21,7 +20,6 @@ import { Select } from '@/ui/input/components/Select';
export const MyComponent = () => {
return (
<RecoilRoot>
<Select
className
disabled={false}
@@ -32,7 +30,6 @@ export const MyComponent = () => {
]}
value="option1"
/>
</RecoilRoot>
);
};
@@ -16,7 +16,6 @@ image: /images/user-guide/notes/notes_header.png
<Tab title="Использование">
```jsx
import { RecoilRoot } from "recoil";
import { TextInput } from "@/ui/input/components/TextInput";
export const MyComponent = () => {
@@ -29,7 +28,6 @@ export const MyComponent = () => {
};
return (
<RecoilRoot>
<TextInput
className
label="Username"
@@ -40,7 +38,6 @@ export const MyComponent = () => {
onKeyDown={handleKeyDown}
RightIcon={null}
/>
</RecoilRoot>
);
};
@@ -81,12 +78,10 @@ export const MyComponent = () => {
<Tab title="Использование">
```jsx
import { RecoilRoot } from "recoil";
import { AutosizeTextInput } from "@/ui/input/components/AutosizeTextInput";
export const MyComponent = () => {
return (
<RecoilRoot>
<AutosizeTextInput
onValidate={() => console.log("onValidate function fired")}
minRows={1}
@@ -96,7 +91,6 @@ export const MyComponent = () => {
buttonTitle
value="Task: "
/>
</RecoilRoot>
);
};
```
@@ -6,9 +6,9 @@ Bu belge, ön yüz üzerinde çalışırken takip etmeniz gereken en iyi uygulam
## Durum Yönetimi
React ve Recoil, kod tabanında durumu yönetir.
React ve Jotai, kod tabanında durumu yönetir.
### Durumu depolamak için `useRecoilState` kullanın.
### Durumu depolamak için `useAtomState` kullanın.
Durumunuzu depolamak için ihtiyaç duyduğunuz kadar atom oluşturmak iyi bir uygulamadır.
@@ -19,13 +19,16 @@ Prop drilling ile gereğinden fazla sade olmaya çalışmaktansa, fazladan atom
</Warning>
```tsx
export const myAtomState = atom({
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
export const myAtomState = createAtomState<string>({
key: 'myAtomState',
default: 'default value',
defaultValue: 'default value',
});
export const MyComponent = () => {
const [myAtom, setMyAtom] = useRecoilState(myAtomState);
const [myAtom, setMyAtom] = useAtomState(myAtomState);
return (
<div>
@@ -42,7 +45,7 @@ export const MyComponent = () => {
Durum saklamak için `useRef` kullanmaktan kaçının.
Durum saklamak istiyorsanız, `useState` veya `useRecoilState` kullanmalısınız.
Durum saklamak istiyorsanız, `useState` veya `useAtomState` 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.
@@ -82,8 +85,8 @@ Aynısını Apollo kancaları ile veri çekme mantığı için de uygulayabilirs
// ❌ Kötü, veri değişmese bile yeniden render'a neden olacak,
// çünkü useEffect'in yeniden değerlendirilmesi gerekiyor
export const PageComponent = () => {
const [data, setData] = useRecoilState(dataState);
const [someDependency] = useRecoilState(someDependencyState);
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
@@ -95,9 +98,7 @@ export const PageComponent = () => {
};
export const App = () => (
<RecoilRoot>
<PageComponent />
</RecoilRoot>
<PageComponent />
);
```
@@ -105,14 +106,14 @@ export const App = () => (
// ✅ İyi, veri değişmiyorsa yeniden render'a neden olmaz,
// çünkü useEffect başka bir kardeş bileşende yeniden değerlendirilir
export const PageComponent = () => {
const [data, setData] = useRecoilState(dataState);
const [data, setData] = useAtomState(dataState);
return <div>{data}</div>;
};
export const PageData = () => {
const [data, setData] = useRecoilState(dataState);
const [someDependency] = useRecoilState(someDependencyState);
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
@@ -124,16 +125,16 @@ export const PageData = () => {
};
export const App = () => (
<RecoilRoot>
<>
<PageData />
<PageComponent />
</RecoilRoot>
</>
);
```
### Recoil aile durumlarını ve recoil aile seçimcilerini kullanın.
### Jotai aile durumlarını ve jotai aile seçimcilerini kullanın.
Recoil aile durumları ve seçimcileri, yeniden render'ları önlemenin harika bir yoludur.
Jotai aile durumları ve seçimcileri, yeniden render'ları önlemenin harika bir yoludur.
Bir öğe listesini saklamanız gerektiğinde kullanılabilirdir.

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