Prevent conditional availability variables from being used at runtime (#21110)

Fixes https://github.com/twentyhq/twenty/issues/21094

Conditional availability variables (`objectMetadataItem`,
`numberOfSelectedRecords`, `objectPermissions`, operators like
`everyEquals`/`none`, etc.) are compile-time-only constructs used in
`conditionalAvailabilityExpression`. They were previously exported from
`twenty-sdk/front-component`, which let developers mistakenly import
them into runtime component code where they have no value.

- Move conditional availability variables from
`twenty-sdk/front-component` to `twenty-sdk/define`.
- Add a build-time manifest validation
(validate-conditional-availability-usage) that fails the build if these
variables are imported/used outside of
`conditionalAvailabilityExpression`.
- Update the github-connector example app to register commands via
dedicated *.command-menu-item.ts files instead of inline command config
in front components.
- Update docs (all locales) and test mocks to reflect the new import
paths.
This commit is contained in:
Raphaël Bosi
2026-06-02 13:22:38 +02:00
committed by GitHub
parent 2375c2f59c
commit b2539f5b6a
52 changed files with 872 additions and 249 deletions
@@ -0,0 +1,11 @@
import { defineCommandMenuItem, objectMetadataItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '7d3f2b9e-5c0a-4e8b-ad6e-2f9c3b4d5e6a',
frontComponentUniversalIdentifier: '6c2f1a8d-4b9e-4f7a-9c5d-1e8b2a3d4c5f',
label: 'Contributor Stats',
icon: 'IconChartBar',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'contributor',
});
@@ -0,0 +1,11 @@
import { defineCommandMenuItem, objectMetadataItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '4640992f-c2c9-4bba-b5df-9c8f05dc9e80',
frontComponentUniversalIdentifier: '08f40f82-24ed-4f3e-8c99-695151e90e38',
label: 'Fetch Contributors',
icon: 'IconUsers',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'contributor',
});
@@ -1,10 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
enqueueSnackbar,
objectMetadataItem,
useRecordId,
} from 'twenty-sdk/front-component';
import { enqueueSnackbar, useRecordId } from 'twenty-sdk/front-component';
import { THEME } from 'src/modules/github/contributor/components/theme';
import { callAppRoute } from 'src/modules/shared/call-app-route';
@@ -682,12 +678,4 @@ export default defineFrontComponent({
description:
'Displays time-bucketed charts of PRs authored (merged only), merged and reviewed by a contributor over the last week, month, 3 months or year.',
component: ContributorStats,
command: {
universalIdentifier: '7d3f2b9e-5c0a-4e8b-ad6e-2f9c3b4d5e6a',
label: 'Contributor Stats',
icon: 'IconChartBar',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'contributor',
},
});
@@ -2,7 +2,6 @@ import { useEffect, useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
enqueueSnackbar,
objectMetadataItem,
unmountFrontComponent,
updateProgress,
} from 'twenty-sdk/front-component';
@@ -95,12 +94,4 @@ export default defineFrontComponent({
'Fetches contributors from every configured GitHub repo (GITHUB_REPOS) and upserts them as Contributor records.',
isHeadless: true,
component: FetchContributors,
command: {
universalIdentifier: '4640992f-c2c9-4bba-b5df-9c8f05dc9e80',
label: 'Fetch Contributors',
icon: 'IconUsers',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'contributor',
},
});
@@ -0,0 +1,11 @@
import { defineCommandMenuItem, objectMetadataItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'c34f56aa-ff65-43a4-9db2-774945dbcc53',
frontComponentUniversalIdentifier: '9430e4fc-9ecb-428d-9bde-2babeb1f452f',
label: 'Fetch Issues',
icon: 'IconBug',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'issue',
});
@@ -2,7 +2,6 @@ import { useEffect, useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
enqueueSnackbar,
objectMetadataItem,
unmountFrontComponent,
updateProgress,
} from 'twenty-sdk/front-component';
@@ -98,12 +97,4 @@ export default defineFrontComponent({
description: 'Fetches issues from GitHub repos',
isHeadless: true,
component: FetchIssues,
command: {
universalIdentifier: 'c34f56aa-ff65-43a4-9db2-774945dbcc53',
label: 'Fetch Issues',
icon: 'IconBug',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'issue',
},
});
@@ -0,0 +1,11 @@
import { defineCommandMenuItem, objectMetadataItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '719cfe1c-d570-4c8c-89e6-88671c6ba1ea',
frontComponentUniversalIdentifier: '7c397b0c-8b19-4fac-924a-8f6aa1dece78',
label: 'Fetch Project Items',
icon: 'IconLayoutKanban',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'projectItem',
});
@@ -2,7 +2,6 @@ import { useEffect, useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
enqueueSnackbar,
objectMetadataItem,
unmountFrontComponent,
updateProgress,
} from 'twenty-sdk/front-component';
@@ -97,12 +96,4 @@ export default defineFrontComponent({
description: 'Fetches project items from GitHub Projects V2',
isHeadless: true,
component: FetchProjectItems,
command: {
universalIdentifier: '719cfe1c-d570-4c8c-89e6-88671c6ba1ea',
label: 'Fetch Project Items',
icon: 'IconLayoutKanban',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'projectItem',
},
});
@@ -0,0 +1,11 @@
import { defineCommandMenuItem, objectMetadataItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '0b24e6d6-da0c-4c0e-8d88-5bfd7f3cd75a',
frontComponentUniversalIdentifier: '5e2ba8df-1db5-4964-94d3-44cccfd791a0',
label: 'Fetch Pull Requests',
icon: 'IconGitPullRequest',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'pullRequest',
});
@@ -2,7 +2,6 @@ import { useEffect, useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
enqueueSnackbar,
objectMetadataItem,
unmountFrontComponent,
updateProgress,
} from 'twenty-sdk/front-component';
@@ -101,12 +100,4 @@ export default defineFrontComponent({
description: 'Fetches pull requests and reviews from GitHub repos',
isHeadless: true,
component: FetchPrs,
command: {
universalIdentifier: '0b24e6d6-da0c-4c0e-8d88-5bfd7f3cd75a',
label: 'Fetch Pull Requests',
icon: 'IconGitPullRequest',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'pullRequest',
},
});
@@ -84,11 +84,11 @@ export default defineCommandMenuItem({
The `conditionalAvailabilityExpression` field lets you control when a command is visible based on the current page context. Import typed variables and operators from `twenty-sdk` to build expressions:
```ts src/command-menu-items/bulk-update.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
import {
defineCommandMenuItem,
objectPermissions,
everyEquals,
} from 'twenty-sdk/front-component';
} from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '...',
@@ -423,8 +423,8 @@ export default defineFrontComponent({
Use `useSelectedRecordIds()` to handle multiple selected records. This is useful for bulk operations:
```tsx src/front-components/bulk-export.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds, numberOfSelectedRecords } from 'twenty-sdk/front-component';
import { defineFrontComponent, numberOfSelectedRecords } from 'twenty-sdk/define';
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-sdk/clients';
@@ -306,14 +306,14 @@ export default defineFrontComponent({
يتيح لك الحقل `conditionalAvailabilityExpression` التحكّم في وقت ظهور الأمر بناءً على سياق الصفحة الحالي. استورد متغيّرات ومشغّلات مضبوطة الأنواع من `twenty-sdk` لبناء التعابير:
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import {
defineFrontComponent,
pageType,
numberOfSelectedRecords,
objectPermissions,
everyEquals,
isDefined,
} from 'twenty-sdk/front-component';
} from 'twenty-sdk/define';
export default defineFrontComponent({
universalIdentifier: '...',
@@ -306,14 +306,14 @@ Přidání pole `command` do `defineFrontComponent` zaregistruje komponentu v p
Pole `conditionalAvailabilityExpression` vám umožní řídit viditelnost příkazu na základě aktuálního kontextu stránky. Pro sestavení výrazů importujte typované proměnné a operátory z `twenty-sdk`:
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import {
defineFrontComponent,
pageType,
numberOfSelectedRecords,
objectPermissions,
everyEquals,
isDefined,
} from 'twenty-sdk/front-component';
} from 'twenty-sdk/define';
export default defineFrontComponent({
universalIdentifier: '...',
@@ -298,8 +298,8 @@ export default defineFrontComponent({
Verwenden Sie `useSelectedRecordIds()`, um mehrere ausgewählte Datensätze zu verwalten. Dies ist nützlich für Stapelvorgänge:
```tsx src/front-components/bulk-export.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds, numberOfSelectedRecords } from 'twenty-sdk/front-component';
import { defineFrontComponent, numberOfSelectedRecords } from 'twenty-sdk/define';
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-sdk/clients';
@@ -383,11 +383,11 @@ export default defineCommandMenuItem({
Mit dem Feld `conditionalAvailabilityExpression` können Sie basierend auf dem aktuellen Seitenkontext steuern, wann ein Befehl sichtbar ist. Importieren Sie typisierte Variablen und Operatoren aus `twenty-sdk`, um Ausdrücke zu erstellen:
```ts src/command-menu-items/bulk-update.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
import {
defineCommandMenuItem,
objectPermissions,
everyEquals,
} from 'twenty-sdk/front-component';
} from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '...',
@@ -84,11 +84,11 @@ export default defineCommandMenuItem({
Mit dem Feld `conditionalAvailabilityExpression` können Sie basierend auf dem aktuellen Seitenkontext steuern, wann ein Befehl sichtbar ist. Importieren Sie typisierte Variablen und Operatoren aus `twenty-sdk`, um Ausdrücke zu erstellen:
```ts src/command-menu-items/bulk-update.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
import {
defineCommandMenuItem,
objectPermissions,
everyEquals,
} from 'twenty-sdk/front-component';
} from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '...',
@@ -423,8 +423,8 @@ export default defineFrontComponent({
Verwenden Sie `useSelectedRecordIds()`, um mehrere ausgewählte Datensätze zu verwalten. Dies ist nützlich für Stapelvorgänge:
```tsx src/front-components/bulk-export.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds, numberOfSelectedRecords } from 'twenty-sdk/front-component';
import { defineFrontComponent, numberOfSelectedRecords } from 'twenty-sdk/define';
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-sdk/clients';
@@ -306,14 +306,14 @@ Aggiungere un campo `command` a `defineFrontComponent` registra il componente ne
Il campo `conditionalAvailabilityExpression` consente di controllare quando un comando è visibile in base al contesto della pagina corrente. Importa variabili tipizzate e operatori da `twenty-sdk` per costruire espressioni:
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import {
defineFrontComponent,
pageType,
numberOfSelectedRecords,
objectPermissions,
everyEquals,
isDefined,
} from 'twenty-sdk/front-component';
} from 'twenty-sdk/define';
export default defineFrontComponent({
universalIdentifier: '...',
@@ -298,8 +298,8 @@ export default defineFrontComponent({
Use `useSelectedRecordIds()` para lidar com vários registros selecionados. Isso é útil para operações em lote:
```tsx src/front-components/bulk-export.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds, numberOfSelectedRecords } from 'twenty-sdk/front-component';
import { defineFrontComponent, numberOfSelectedRecords } from 'twenty-sdk/define';
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-sdk/clients';
@@ -383,11 +383,11 @@ export default defineCommandMenuItem({
O campo `conditionalAvailabilityExpression` permite controlar quando um comando é visível com base no contexto da página atual. Importe variáveis tipadas e operadores de `twenty-sdk` para construir expressões:
```ts src/command-menu-items/bulk-update.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
import {
defineCommandMenuItem,
objectPermissions,
everyEquals,
} from 'twenty-sdk/front-component';
} from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '...',
@@ -84,11 +84,11 @@ export default defineCommandMenuItem({
O campo `conditionalAvailabilityExpression` permite controlar quando um comando é visível com base no contexto da página atual. Importe variáveis tipadas e operadores de `twenty-sdk` para construir expressões:
```ts src/command-menu-items/bulk-update.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
import {
defineCommandMenuItem,
objectPermissions,
everyEquals,
} from 'twenty-sdk/front-component';
} from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '...',
@@ -423,8 +423,8 @@ export default defineFrontComponent({
Use `useSelectedRecordIds()` para lidar com vários registros selecionados. Isso é útil para operações em lote:
```tsx src/front-components/bulk-export.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds, numberOfSelectedRecords } from 'twenty-sdk/front-component';
import { defineFrontComponent, numberOfSelectedRecords } from 'twenty-sdk/define';
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-sdk/clients';
@@ -298,8 +298,8 @@ export default defineFrontComponent({
Folosiți `useSelectedRecordIds()` pentru a gestiona mai multe înregistrări selectate. Acest lucru este util pentru operațiuni în masă:
```tsx src/front-components/bulk-export.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds, numberOfSelectedRecords } from 'twenty-sdk/front-component';
import { defineFrontComponent, numberOfSelectedRecords } from 'twenty-sdk/define';
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-sdk/clients';
@@ -383,11 +383,11 @@ export default defineCommandMenuItem({
Câmpul `conditionalAvailabilityExpression` vă permite să controlați când este vizibilă o comandă în funcție de contextul paginii curente. Importați variabile tipizate și operatori din `twenty-sdk` pentru a construi expresii:
```ts src/command-menu-items/bulk-update.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
import {
defineCommandMenuItem,
objectPermissions,
everyEquals,
} from 'twenty-sdk/front-component';
} from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '...',
@@ -84,11 +84,11 @@ export default defineCommandMenuItem({
Câmpul `conditionalAvailabilityExpression` vă permite să controlați când este vizibilă o comandă în funcție de contextul paginii curente. Importați variabile tipizate și operatori din `twenty-sdk` pentru a construi expresii:
```ts src/command-menu-items/bulk-update.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
import {
defineCommandMenuItem,
objectPermissions,
everyEquals,
} from 'twenty-sdk/front-component';
} from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '...',
@@ -423,8 +423,8 @@ export default defineFrontComponent({
Folosiți `useSelectedRecordIds()` pentru a gestiona mai multe înregistrări selectate. Acest lucru este util pentru operațiuni în masă:
```tsx src/front-components/bulk-export.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds, numberOfSelectedRecords } from 'twenty-sdk/front-component';
import { defineFrontComponent, numberOfSelectedRecords } from 'twenty-sdk/define';
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-sdk/clients';
@@ -298,8 +298,8 @@ export default defineFrontComponent({
Используйте `useSelectedRecordIds()` для обработки нескольких выбранных записей. Это полезно для массовых операций:
```tsx src/front-components/bulk-export.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds, numberOfSelectedRecords } from 'twenty-sdk/front-component';
import { defineFrontComponent, numberOfSelectedRecords } from 'twenty-sdk/define';
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-sdk/clients';
@@ -383,11 +383,11 @@ export default defineCommandMenuItem({
Поле `conditionalAvailabilityExpression` позволяет управлять видимостью команды в зависимости от текущего контекста страницы. Импортируйте типизированные переменные и операторы из `twenty-sdk`, чтобы составлять выражения:
```ts src/command-menu-items/bulk-update.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
import {
defineCommandMenuItem,
objectPermissions,
everyEquals,
} from 'twenty-sdk/front-component';
} from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '...',
@@ -84,11 +84,11 @@ export default defineCommandMenuItem({
Поле `conditionalAvailabilityExpression` позволяет управлять видимостью команды в зависимости от текущего контекста страницы. Импортируйте типизированные переменные и операторы из `twenty-sdk`, чтобы составлять выражения:
```ts src/command-menu-items/bulk-update.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
import {
defineCommandMenuItem,
objectPermissions,
everyEquals,
} from 'twenty-sdk/front-component';
} from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '...',
@@ -423,8 +423,8 @@ export default defineFrontComponent({
Используйте `useSelectedRecordIds()` для обработки нескольких выбранных записей. Это полезно для массовых операций:
```tsx src/front-components/bulk-export.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds, numberOfSelectedRecords } from 'twenty-sdk/front-component';
import { defineFrontComponent, numberOfSelectedRecords } from 'twenty-sdk/define';
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-sdk/clients';
@@ -298,8 +298,8 @@ export default defineFrontComponent({
Birden çok seçili kaydı yönetmek için `useSelectedRecordIds()` kullanın. Bu, toplu işlemler için kullanışlıdır:
```tsx src/front-components/bulk-export.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds, numberOfSelectedRecords } from 'twenty-sdk/front-component';
import { defineFrontComponent, numberOfSelectedRecords } from 'twenty-sdk/define';
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-sdk/clients';
@@ -383,11 +383,11 @@ export default defineCommandMenuItem({
`conditionalAvailabilityExpression` alanı, geçerli sayfa bağlamına göre bir komutun ne zaman görünür olacağını kontrol etmenizi sağlar. İfadeler oluşturmak için `twenty-sdk`'den türlendirilmiş değişkenleri ve operatörleri içe aktarın:
```ts src/command-menu-items/bulk-update.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
import {
defineCommandMenuItem,
objectPermissions,
everyEquals,
} from 'twenty-sdk/front-component';
} from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '...',
@@ -84,11 +84,11 @@ export default defineCommandMenuItem({
`conditionalAvailabilityExpression` alanı, geçerli sayfa bağlamına göre bir komutun ne zaman görünür olacağını kontrol etmenizi sağlar. İfadeler oluşturmak için `twenty-sdk`'den türlendirilmiş değişkenleri ve operatörleri içe aktarın:
```ts src/command-menu-items/bulk-update.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
import {
defineCommandMenuItem,
objectPermissions,
everyEquals,
} from 'twenty-sdk/front-component';
} from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '...',
@@ -423,8 +423,8 @@ export default defineFrontComponent({
Birden çok seçili kaydı yönetmek için `useSelectedRecordIds()` kullanın. Bu, toplu işlemler için kullanışlıdır:
```tsx src/front-components/bulk-export.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds, numberOfSelectedRecords } from 'twenty-sdk/front-component';
import { defineFrontComponent, numberOfSelectedRecords } from 'twenty-sdk/define';
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-sdk/clients';
@@ -298,8 +298,8 @@ export default defineFrontComponent({
使用 `useSelectedRecordIds()` 来处理多个已选记录。 这对于批量操作很有用:
```tsx src/front-components/bulk-export.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds, numberOfSelectedRecords } from 'twenty-sdk/front-component';
import { defineFrontComponent, numberOfSelectedRecords } from 'twenty-sdk/define';
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-sdk/clients';
@@ -383,11 +383,11 @@ export default defineCommandMenuItem({
通过 `conditionalAvailabilityExpression` 字段,您可以基于当前页面上下文控制命令何时可见。 从 `twenty-sdk` 导入带类型的变量和运算符来构建表达式:
```ts src/command-menu-items/bulk-update.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
import {
defineCommandMenuItem,
objectPermissions,
everyEquals,
} from 'twenty-sdk/front-component';
} from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '...',
@@ -84,11 +84,11 @@ export default defineCommandMenuItem({
通过 `conditionalAvailabilityExpression` 字段,您可以基于当前页面上下文控制命令何时可见。 从 `twenty-sdk` 导入带类型的变量和运算符来构建表达式:
```ts src/command-menu-items/bulk-update.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
import {
defineCommandMenuItem,
objectPermissions,
everyEquals,
} from 'twenty-sdk/front-component';
} from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '...',
@@ -423,8 +423,8 @@ export default defineFrontComponent({
使用 `useSelectedRecordIds()` 来处理多个已选记录。 这对于批量操作很有用:
```tsx src/front-components/bulk-export.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds, numberOfSelectedRecords } from 'twenty-sdk/front-component';
import { defineFrontComponent, numberOfSelectedRecords } from 'twenty-sdk/define';
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-sdk/clients';
@@ -1,5 +1,7 @@
import { defineCommandMenuItem } from '@/sdk/define';
import { numberOfSelectedRecords } from '@/sdk/front-component';
import {
defineCommandMenuItem,
numberOfSelectedRecords,
} from '@/sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'comparison-operator-cmd',
@@ -1,10 +1,10 @@
import { defineCommandMenuItem } from '@/sdk/define';
import {
defineCommandMenuItem,
none,
numberOfSelectedRecords,
objectPermissions,
selectedRecords,
} from '@/sdk/front-component';
} from '@/sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'complex-soft-delete-cmd',
@@ -1,5 +1,8 @@
import { defineCommandMenuItem } from '@/sdk/define';
import { someDefined, selectedRecords } from '@/sdk/front-component';
import {
defineCommandMenuItem,
selectedRecords,
someDefined,
} from '@/sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'custom-function-cmd',
@@ -1,5 +1,8 @@
import { defineCommandMenuItem } from '@/sdk/define';
import { featureFlags, objectPermissions } from '@/sdk/front-component';
import {
defineCommandMenuItem,
featureFlags,
objectPermissions,
} from '@/sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'feature-flag-gated-cmd',
@@ -1,9 +1,9 @@
import { defineCommandMenuItem } from '@/sdk/define';
import {
defineCommandMenuItem,
favoriteRecordIds,
objectMetadataItem,
pageType,
} from '@/sdk/front-component';
} from '@/sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'parenthesized-expression-cmd',
@@ -1,5 +1,8 @@
import { defineCommandMenuItem } from '@/sdk/define';
import { isInSidePanel, objectPermissions } from '@/sdk/front-component';
import {
defineCommandMenuItem,
isInSidePanel,
objectPermissions,
} from '@/sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'permissions-check-cmd',
@@ -1,5 +1,4 @@
import { defineCommandMenuItem } from '@/sdk/define';
import { pageType } from '@/sdk/front-component';
import { defineCommandMenuItem, pageType } from '@/sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'simple-boolean-cmd',
@@ -1,5 +1,9 @@
import { defineCommandMenuItem } from '@/sdk/define';
import { everyEquals, pageType, selectedRecords } from '@/sdk/front-component';
import {
defineCommandMenuItem,
everyEquals,
pageType,
selectedRecords,
} from '@/sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'string-comparison-cmd',
@@ -1,5 +1,8 @@
import { defineCommandMenuItem } from '@/sdk/define';
import { pageType, targetObjectWritePermissions } from '@/sdk/front-component';
import {
defineCommandMenuItem,
pageType,
targetObjectWritePermissions,
} from '@/sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'target-permissions-cmd',
@@ -27,7 +27,38 @@ exports[`stub-twenty-sdk-define plugin > matches the recorded export partition 1
"ViewSortDirection",
"ViewType",
"ViewVisibility",
"canAccessFullAdminPanel",
"canImpersonate",
"every",
"everyDefined",
"everyEquals",
"favoriteRecordIds",
"featureFlags",
"generateDefaultFieldUniversalIdentifier",
"hasAnySoftDeleteFilterOnView",
"includes",
"includesEvery",
"isDashboardPageLayoutInEditMode",
"isDefined",
"isInSidePanel",
"isLayoutCustomizationModeEnabled",
"isNonEmptyString",
"isSelectAll",
"none",
"noneDefined",
"noneEquals",
"numberOfSelectedRecords",
"objectMetadataItem",
"objectMetadataLabel",
"objectPermissions",
"pageType",
"selectedRecords",
"some",
"someDefined",
"someEquals",
"someNonEmptyString",
"targetObjectReadPermissions",
"targetObjectWritePermissions",
"validateFields",
],
"factories": [
@@ -9,6 +9,7 @@ import { extractManifestFromFile } from '@/cli/utilities/build/manifest/manifest
import { addMissingFieldOptionIds } from '@/cli/utilities/build/manifest/utils/add-missing-field-option-ids';
import { fromRoleConfigToRoleManifest } from '@/cli/utilities/build/manifest/utils/from-role-config-to-role-manifest';
import { getDefaultFieldsInObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-fields-in-object-fields';
import { validateConditionalAvailabilityUsage } from '@/cli/utilities/build/manifest/utils/validate-conditional-availability-usage';
import { validateViewFilterOperands } from '@/cli/utilities/build/manifest/utils/validate-view-filter-operands';
import { type ApplicationConfig, type LogicFunctionConfig } from '@/sdk/define';
import { type CommandMenuItemConfig } from '@/sdk/define/command-menu-items/command-menu-item-config';
@@ -126,6 +127,10 @@ export const buildManifest = async (
const fileContent = await readFile(filePath, 'utf-8');
const relativePath = relative(appPath, filePath);
errors.push(
...validateConditionalAvailabilityUsage(fileContent, relativePath),
);
const targetFunctionName = extractDefineEntity(fileContent);
if (!targetFunctionName) {
@@ -0,0 +1,241 @@
import { validateConditionalAvailabilityUsage } from '@/cli/utilities/build/manifest/utils/validate-conditional-availability-usage';
const validate = (source: string, filename = 'src/example.front-component.tsx') =>
validateConditionalAvailabilityUsage(source, filename);
describe('validateConditionalAvailabilityUsage', () => {
describe('valid usage', () => {
it('should allow a variable inside a bare conditionalAvailabilityExpression', () => {
const source = `
import { defineCommandMenuItem } from 'twenty-sdk/define';
import { objectMetadataItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'x',
label: 'X',
frontComponentUniversalIdentifier: 'y',
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'pullRequest',
});
`;
expect(validate(source)).toEqual([]);
});
it('should allow a variable inside a nested command.conditionalAvailabilityExpression', () => {
const source = `
import { defineFrontComponent } from 'twenty-sdk/define';
import { objectMetadataItem } from 'twenty-sdk/define';
export default defineFrontComponent({
universalIdentifier: 'x',
component: () => null,
command: {
universalIdentifier: 'z',
label: 'Z',
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'issue',
},
});
`;
expect(validate(source)).toEqual([]);
});
it('should allow operators inside the expression', () => {
const source = `
import { defineCommandMenuItem } from 'twenty-sdk/define';
import { objectPermissions, everyEquals } from 'twenty-sdk/define';
export default defineCommandMenuItem({
conditionalAvailabilityExpression: everyEquals(
objectPermissions,
'canUpdateObjectRecords',
true,
),
});
`;
expect(validate(source)).toEqual([]);
});
it('should not flag same-named helpers imported from twenty-shared/utils', () => {
const source = `
import { isDefined } from 'twenty-shared/utils';
export const useThing = (value: unknown) => {
if (isDefined(value)) {
return value;
}
return null;
};
`;
expect(validate(source, 'src/use-thing.ts')).toEqual([]);
});
it('should not flag Array.prototype.includes property access', () => {
const source = `
import { includes } from 'twenty-sdk/define';
import { defineCommandMenuItem } from 'twenty-sdk/define';
const items: string[] = [];
export const hasItem = items.includes('a');
export default defineCommandMenuItem({
conditionalAvailabilityExpression: includes(items, 'a'),
});
`;
expect(validate(source, 'src/has-item.ts')).toEqual([]);
});
it('should trust files that define an expression (runtime placeholder is the backstop for in-file misuse)', () => {
const source = `
import { defineCommandMenuItem } from 'twenty-sdk/define';
import { numberOfSelectedRecords } from 'twenty-sdk/define';
export default defineCommandMenuItem({
label: numberOfSelectedRecords,
conditionalAvailabilityExpression: numberOfSelectedRecords >= 2,
});
`;
expect(validate(source)).toEqual([]);
});
it('should trust an expression declared with a string-literal property key', () => {
const source = `
import { defineCommandMenuItem } from 'twenty-sdk/define';
import { objectMetadataItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
'conditionalAvailabilityExpression':
objectMetadataItem.nameSingular === 'pullRequest',
});
`;
expect(validate(source)).toEqual([]);
});
it('should not flag a type-only import declaration', () => {
const source = `
import type { objectMetadataItem } from 'twenty-sdk/define';
export type Thing = typeof objectMetadataItem;
`;
expect(validate(source, 'src/thing.ts')).toEqual([]);
});
it('should not flag an inline type-only import specifier', () => {
const source = `
import { type objectMetadataItem } from 'twenty-sdk/define';
export type Thing = typeof objectMetadataItem;
`;
expect(validate(source, 'src/thing.ts')).toEqual([]);
});
});
describe('invalid usage', () => {
it('should flag a variable used in a component body', () => {
const source = `
import { objectMetadataItem } from 'twenty-sdk/define';
export const Component = () => {
const name = objectMetadataItem.nameSingular;
return name;
};
`;
const errors = validate(source);
expect(errors).toHaveLength(1);
expect(errors[0]).toContain('objectMetadataItem');
expect(errors[0]).toContain('conditionalAvailabilityExpression');
});
it('should flag a variable imported in a file with no expression', () => {
const source = `
import { defineCommandMenuItem } from 'twenty-sdk/define';
import { numberOfSelectedRecords } from 'twenty-sdk/define';
export default defineCommandMenuItem({
label: numberOfSelectedRecords,
});
`;
const errors = validate(source);
expect(errors).toHaveLength(1);
expect(errors[0]).toContain('numberOfSelectedRecords');
});
it('should flag an operator called at runtime', () => {
const source = `
import { isDefined } from 'twenty-sdk/define';
export const check = (value: unknown) => isDefined(value);
`;
const errors = validate(source, 'src/check.ts');
expect(errors).toHaveLength(1);
expect(errors[0]).toContain('isDefined');
});
it('should flag a variable imported from the twenty-sdk root entry', () => {
const source = `
import { objectMetadataItem } from 'twenty-sdk';
export const label = objectMetadataItem.labelSingular;
`;
expect(validate(source, 'src/label.ts')).toHaveLength(1);
});
it('should flag misuse when conditionalAvailabilityExpression appears only in a comment', () => {
const source = `
import { objectMetadataItem } from 'twenty-sdk/define';
// TODO: move this into a conditionalAvailabilityExpression later
export const Component = () => objectMetadataItem.nameSingular;
`;
const errors = validate(source);
expect(errors).toHaveLength(1);
expect(errors[0]).toContain('objectMetadataItem');
});
it('should flag misuse when conditionalAvailabilityExpression appears only in a string literal', () => {
const source = `
import { objectMetadataItem } from 'twenty-sdk/define';
export const doc = 'conditionalAvailabilityExpression';
export const Component = () => objectMetadataItem.nameSingular;
`;
const errors = validate(source);
expect(errors).toHaveLength(1);
expect(errors[0]).toContain('objectMetadataItem');
});
it('should flag namespace-import runtime use with no expression', () => {
const source = `
import * as sdk from 'twenty-sdk/define';
export const label = sdk.objectMetadataItem.labelSingular;
`;
const errors = validate(source, 'src/label.ts');
expect(errors).toHaveLength(1);
expect(errors[0]).toContain('objectMetadataItem');
});
});
});
@@ -0,0 +1,156 @@
import { CONDITIONAL_AVAILABILITY_VARIABLE_NAMES } from '@/sdk/define/conditional-availability/conditional-availability-variable-names';
import { isDefined } from 'twenty-shared/utils';
import ts from 'typescript';
const CONDITIONAL_AVAILABILITY_EXPRESSION_PROPERTY =
'conditionalAvailabilityExpression';
const EXPRESSION_VARIABLE_MODULE_SPECIFIERS = ['twenty-sdk/define', 'twenty-sdk'];
const EXPRESSION_VARIABLE_NAMES = new Set<string>(
CONDITIONAL_AVAILABILITY_VARIABLE_NAMES,
);
type ImportedExpressionVariable = {
importedName: string;
node: ts.Node;
};
const collectImportedExpressionVariables = (
sourceFile: ts.SourceFile,
): ImportedExpressionVariable[] => {
const imported: ImportedExpressionVariable[] = [];
for (const statement of sourceFile.statements) {
if (
!ts.isImportDeclaration(statement) ||
!ts.isStringLiteral(statement.moduleSpecifier) ||
!EXPRESSION_VARIABLE_MODULE_SPECIFIERS.includes(
statement.moduleSpecifier.text,
)
) {
continue;
}
const importClause = statement.importClause;
if (
!isDefined(importClause) ||
importClause.phaseModifier === ts.SyntaxKind.TypeKeyword
) {
continue;
}
const namedBindings = importClause.namedBindings;
if (!isDefined(namedBindings)) {
continue;
}
if (ts.isNamespaceImport(namedBindings)) {
const namespaceName = namedBindings.name.text;
const collectNamespaceMemberUsages = (node: ts.Node): void => {
if (
ts.isPropertyAccessExpression(node) &&
ts.isIdentifier(node.expression) &&
node.expression.text === namespaceName &&
EXPRESSION_VARIABLE_NAMES.has(node.name.text)
) {
imported.push({ importedName: node.name.text, node });
}
ts.forEachChild(node, collectNamespaceMemberUsages);
};
collectNamespaceMemberUsages(sourceFile);
continue;
}
if (!ts.isNamedImports(namedBindings)) {
continue;
}
for (const element of namedBindings.elements) {
if (element.isTypeOnly) {
continue;
}
const importedName = (element.propertyName ?? element.name).text;
if (EXPRESSION_VARIABLE_NAMES.has(importedName)) {
imported.push({ importedName, node: element });
}
}
}
return imported;
};
const hasConditionalAvailabilityExpressionProperty = (
sourceFile: ts.SourceFile,
): boolean => {
let found = false;
const findExpressionProperty = (node: ts.Node): void => {
if (found) {
return;
}
if (
(ts.isPropertyAssignment(node) ||
ts.isShorthandPropertyAssignment(node)) &&
(ts.isIdentifier(node.name) || ts.isStringLiteral(node.name)) &&
node.name.text === CONDITIONAL_AVAILABILITY_EXPRESSION_PROPERTY
) {
found = true;
return;
}
ts.forEachChild(node, findExpressionProperty);
};
findExpressionProperty(sourceFile);
return found;
};
export const validateConditionalAvailabilityUsage = (
fileContent: string,
relativePath: string,
): string[] => {
if (
!CONDITIONAL_AVAILABILITY_VARIABLE_NAMES.some((name) =>
fileContent.includes(name),
)
) {
return [];
}
const sourceFile = ts.createSourceFile(
relativePath,
fileContent,
ts.ScriptTarget.Latest,
true,
relativePath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
);
const importedExpressionVariables =
collectImportedExpressionVariables(sourceFile);
if (importedExpressionVariables.length === 0) {
return [];
}
if (hasConditionalAvailabilityExpressionProperty(sourceFile)) {
return [];
}
return importedExpressionVariables.map(({ importedName, node }) => {
const { line, character } = sourceFile.getLineAndCharacterOfPosition(
node.getStart(sourceFile),
);
return `${relativePath}:${line + 1}:${character + 1} - "${importedName}" is a conditional-availability expression variable from twenty-sdk but this file has no ${CONDITIONAL_AVAILABILITY_EXPRESSION_PROPERTY}. These variables can only be used inside a command's ${CONDITIONAL_AVAILABILITY_EXPRESSION_PROPERTY}, not at runtime.`;
});
};
@@ -0,0 +1,56 @@
import { CONDITIONAL_AVAILABILITY_VARIABLE_NAMES } from '@/sdk/define/conditional-availability/conditional-availability-variable-names';
import * as conditionalAvailabilityVariables from '@/sdk/define/conditional-availability/conditional-availability-variables';
describe('conditional-availability-variables', () => {
it('should not throw when the module is imported', () => {
expect(conditionalAvailabilityVariables.objectMetadataItem).toBeDefined();
});
it('should throw on property access of a context variable', () => {
expect(
() =>
(
conditionalAvailabilityVariables.objectMetadataItem as unknown as {
nameSingular: string;
}
).nameSingular,
).toThrow('conditionalAvailabilityExpression');
});
it('should throw on primitive coercion of a context variable', () => {
expect(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
void (conditionalAvailabilityVariables.numberOfSelectedRecords >= 2);
}).toThrow('conditionalAvailabilityExpression');
});
it('should throw when an operator is called at runtime', () => {
expect(() => conditionalAvailabilityVariables.isDefined('value')).toThrow(
'conditionalAvailabilityExpression',
);
});
it('should mention the variable name in the error', () => {
expect(() =>
conditionalAvailabilityVariables.everyEquals([], 'prop', true),
).toThrow('everyEquals');
});
it('should report its own export name in the runtime error for every variable', () => {
for (const [name, value] of Object.entries(
conditionalAvailabilityVariables,
)) {
expect(() => (value as unknown as () => void)()).toThrow(name);
}
});
// Drift guard: every exported variable must be listed in the shared constant
// so the build-time guard can never miss a newly added variable.
it('should keep CONDITIONAL_AVAILABILITY_VARIABLE_NAMES in sync with the exports', () => {
const exportedNames = Object.keys(conditionalAvailabilityVariables);
expect(new Set(CONDITIONAL_AVAILABILITY_VARIABLE_NAMES)).toEqual(
new Set(exportedNames),
);
});
});
@@ -0,0 +1,33 @@
export const CONDITIONAL_AVAILABILITY_VARIABLE_NAMES = [
'pageType',
'isInSidePanel',
'isDashboardPageLayoutInEditMode',
'isLayoutCustomizationModeEnabled',
'favoriteRecordIds',
'isSelectAll',
'hasAnySoftDeleteFilterOnView',
'numberOfSelectedRecords',
'objectPermissions',
'selectedRecords',
'featureFlags',
'targetObjectReadPermissions',
'targetObjectWritePermissions',
'canImpersonate',
'canAccessFullAdminPanel',
'objectMetadataItem',
'objectMetadataLabel',
'isDefined',
'isNonEmptyString',
'includes',
'every',
'everyDefined',
'everyEquals',
'some',
'someDefined',
'someEquals',
'none',
'noneDefined',
'noneEquals',
'someNonEmptyString',
'includesEvery',
] as const;
@@ -0,0 +1,167 @@
import { type CommandMenuContextApi } from 'twenty-shared/types';
const isPromiseUnwrapProbe = (property: string | symbol): boolean =>
property === 'then';
const isSymbolProbe = (property: string | symbol): boolean =>
typeof property === 'symbol';
const createExpressionOnlyPlaceholder = (name: string): unknown => {
const fail = (): never => {
throw new Error(
`"${name}" from twenty-sdk can only be used inside a command's conditionalAvailabilityExpression, not at runtime.`,
);
};
const handler: ProxyHandler<() => void> = {
get: (_target, property) => {
if (isPromiseUnwrapProbe(property) || isSymbolProbe(property)) {
return undefined;
}
return fail();
},
apply: fail,
construct: fail,
has: fail,
};
return new Proxy(function expressionOnlyPlaceholder() {}, handler);
};
export const pageType = createExpressionOnlyPlaceholder(
'pageType',
) as CommandMenuContextApi['pageType'];
export const isInSidePanel = createExpressionOnlyPlaceholder(
'isInSidePanel',
) as CommandMenuContextApi['isInSidePanel'];
export const isDashboardPageLayoutInEditMode = createExpressionOnlyPlaceholder(
'isDashboardPageLayoutInEditMode',
) as CommandMenuContextApi['isDashboardPageLayoutInEditMode'];
export const isLayoutCustomizationModeEnabled = createExpressionOnlyPlaceholder(
'isLayoutCustomizationModeEnabled',
) as CommandMenuContextApi['isLayoutCustomizationModeEnabled'];
export const favoriteRecordIds = createExpressionOnlyPlaceholder(
'favoriteRecordIds',
) as CommandMenuContextApi['favoriteRecordIds'];
export const isSelectAll = createExpressionOnlyPlaceholder(
'isSelectAll',
) as CommandMenuContextApi['isSelectAll'];
export const hasAnySoftDeleteFilterOnView = createExpressionOnlyPlaceholder(
'hasAnySoftDeleteFilterOnView',
) as CommandMenuContextApi['hasAnySoftDeleteFilterOnView'];
export const numberOfSelectedRecords = createExpressionOnlyPlaceholder(
'numberOfSelectedRecords',
) as CommandMenuContextApi['numberOfSelectedRecords'];
export const objectPermissions = createExpressionOnlyPlaceholder(
'objectPermissions',
) as CommandMenuContextApi['objectPermissions'];
export const selectedRecords = createExpressionOnlyPlaceholder(
'selectedRecords',
) as CommandMenuContextApi['selectedRecords'];
export const featureFlags = createExpressionOnlyPlaceholder(
'featureFlags',
) as CommandMenuContextApi['featureFlags'];
export const targetObjectReadPermissions = createExpressionOnlyPlaceholder(
'targetObjectReadPermissions',
) as CommandMenuContextApi['targetObjectReadPermissions'];
export const targetObjectWritePermissions = createExpressionOnlyPlaceholder(
'targetObjectWritePermissions',
) as CommandMenuContextApi['targetObjectWritePermissions'];
export const canImpersonate = createExpressionOnlyPlaceholder(
'canImpersonate',
) as CommandMenuContextApi['canImpersonate'];
export const canAccessFullAdminPanel = createExpressionOnlyPlaceholder(
'canAccessFullAdminPanel',
) as CommandMenuContextApi['canAccessFullAdminPanel'];
export const objectMetadataItem = createExpressionOnlyPlaceholder(
'objectMetadataItem',
) as CommandMenuContextApi['objectMetadataItem'];
export const objectMetadataLabel = createExpressionOnlyPlaceholder(
'objectMetadataLabel',
) as CommandMenuContextApi['objectMetadataLabel'];
export const isDefined = createExpressionOnlyPlaceholder('isDefined') as (
value: unknown,
) => boolean;
export const isNonEmptyString = createExpressionOnlyPlaceholder(
'isNonEmptyString',
) as (value: unknown) => boolean;
export const includes = createExpressionOnlyPlaceholder('includes') as (
array: unknown,
value: unknown,
) => boolean;
export const every = createExpressionOnlyPlaceholder('every') as (
array: unknown,
prop: string,
) => boolean;
export const everyDefined = createExpressionOnlyPlaceholder('everyDefined') as (
array: unknown,
prop: string,
) => boolean;
export const everyEquals = createExpressionOnlyPlaceholder('everyEquals') as (
array: unknown,
prop: string,
value: unknown,
) => boolean;
export const some = createExpressionOnlyPlaceholder('some') as (
array: unknown,
prop: string,
) => boolean;
export const someDefined = createExpressionOnlyPlaceholder('someDefined') as (
array: unknown,
prop: string,
) => boolean;
export const someEquals = createExpressionOnlyPlaceholder('someEquals') as (
array: unknown,
prop: string,
value: unknown,
) => boolean;
export const none = createExpressionOnlyPlaceholder('none') as (
array: unknown,
prop: string,
) => boolean;
export const noneDefined = createExpressionOnlyPlaceholder('noneDefined') as (
array: unknown,
prop: string,
) => boolean;
export const noneEquals = createExpressionOnlyPlaceholder('noneEquals') as (
array: unknown,
prop: string,
value: unknown,
) => boolean;
export const someNonEmptyString = createExpressionOnlyPlaceholder(
'someNonEmptyString',
) as (array: unknown, prop: string) => boolean;
export const includesEvery = createExpressionOnlyPlaceholder(
'includesEvery',
) as (array: unknown, prop: string, value: unknown) => boolean;
@@ -31,6 +31,40 @@ export { defineCommandMenuItem } from '@/sdk/define/command-menu-items/define-co
export type { CommandMenuItemConfig } from '@/sdk/define/command-menu-items/command-menu-item-config';
export type { CommandMenuItemManifest } from 'twenty-shared/application';
export {
pageType,
isInSidePanel,
isDashboardPageLayoutInEditMode,
isLayoutCustomizationModeEnabled,
favoriteRecordIds,
isSelectAll,
hasAnySoftDeleteFilterOnView,
numberOfSelectedRecords,
objectPermissions,
selectedRecords,
featureFlags,
targetObjectReadPermissions,
targetObjectWritePermissions,
canImpersonate,
canAccessFullAdminPanel,
isDefined,
isNonEmptyString,
includes,
every,
everyDefined,
everyEquals,
some,
someDefined,
someEquals,
none,
noneDefined,
noneEquals,
someNonEmptyString,
includesEvery,
objectMetadataItem,
objectMetadataLabel,
} from '@/sdk/define/conditional-availability/conditional-availability-variables';
export { defineFrontComponent } from '@/sdk/define/front-component/define-front-component';
export type {
FrontComponentConfig,
@@ -1,92 +0,0 @@
import { type CommandMenuContextApi } from 'twenty-shared/types';
export const pageType = null as unknown as CommandMenuContextApi['pageType'];
export const isInSidePanel =
null as unknown as CommandMenuContextApi['isInSidePanel'];
export const isDashboardPageLayoutInEditMode =
null as unknown as CommandMenuContextApi['isDashboardPageLayoutInEditMode'];
export const isLayoutCustomizationModeEnabled =
null as unknown as CommandMenuContextApi['isLayoutCustomizationModeEnabled'];
export const favoriteRecordIds =
null as unknown as CommandMenuContextApi['favoriteRecordIds'];
export const isSelectAll =
null as unknown as CommandMenuContextApi['isSelectAll'];
export const hasAnySoftDeleteFilterOnView =
null as unknown as CommandMenuContextApi['hasAnySoftDeleteFilterOnView'];
export const numberOfSelectedRecords =
null as unknown as CommandMenuContextApi['numberOfSelectedRecords'];
export const objectPermissions =
null as unknown as CommandMenuContextApi['objectPermissions'];
export const selectedRecords =
null as unknown as CommandMenuContextApi['selectedRecords'];
export const featureFlags =
null as unknown as CommandMenuContextApi['featureFlags'];
export const targetObjectReadPermissions =
null as unknown as CommandMenuContextApi['targetObjectReadPermissions'];
export const targetObjectWritePermissions =
null as unknown as CommandMenuContextApi['targetObjectWritePermissions'];
export const canImpersonate =
null as unknown as CommandMenuContextApi['canImpersonate'];
export const canAccessFullAdminPanel =
null as unknown as CommandMenuContextApi['canAccessFullAdminPanel'];
export const objectMetadataItem =
null as unknown as CommandMenuContextApi['objectMetadataItem'];
export const objectMetadataLabel =
null as unknown as CommandMenuContextApi['objectMetadataLabel'];
export const isDefined = null as unknown as (value: unknown) => boolean;
export const isNonEmptyString = null as unknown as (value: unknown) => boolean;
export const includes = null as unknown as (
array: unknown,
value: unknown,
) => boolean;
export const every = null as unknown as (
array: unknown,
prop: string,
) => boolean;
export const everyDefined = null as unknown as (
array: unknown,
prop: string,
) => boolean;
export const everyEquals = null as unknown as (
array: unknown,
prop: string,
value: unknown,
) => boolean;
export const some = null as unknown as (
array: unknown,
prop: string,
) => boolean;
export const someDefined = null as unknown as (
array: unknown,
prop: string,
) => boolean;
export const someEquals = null as unknown as (
array: unknown,
prop: string,
value: unknown,
) => boolean;
export const none = null as unknown as (
array: unknown,
prop: string,
) => boolean;
export const noneDefined = null as unknown as (
array: unknown,
prop: string,
) => boolean;
export const noneEquals = null as unknown as (
array: unknown,
prop: string,
value: unknown,
) => boolean;
export const someNonEmptyString = null as unknown as (
array: unknown,
prop: string,
) => boolean;
export const includesEvery = null as unknown as (
array: unknown,
prop: string,
value: unknown,
) => boolean;
@@ -1,35 +1,3 @@
export {
pageType,
isInSidePanel,
isDashboardPageLayoutInEditMode,
isLayoutCustomizationModeEnabled,
favoriteRecordIds,
isSelectAll,
hasAnySoftDeleteFilterOnView,
numberOfSelectedRecords,
objectPermissions,
selectedRecords,
featureFlags,
targetObjectReadPermissions,
targetObjectWritePermissions,
canImpersonate,
canAccessFullAdminPanel,
isDefined,
isNonEmptyString,
includes,
every,
everyDefined,
everyEquals,
some,
someDefined,
someEquals,
none,
noneDefined,
noneEquals,
someNonEmptyString,
includesEvery,
objectMetadataItem,
} from './conditional-availability/conditional-availability-variables';
export { closeSidePanel } from './functions/closeSidePanel';
export { copyToClipboard } from './functions/copyToClipboard';
export { getApplicationVariable } from './functions/getApplicationVariable';