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:
+11
@@ -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',
|
||||
});
|
||||
+11
@@ -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
-13
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
-9
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
+11
@@ -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',
|
||||
});
|
||||
-9
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
+11
@@ -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',
|
||||
});
|
||||
-9
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
+11
@@ -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',
|
||||
});
|
||||
-9
@@ -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';
|
||||
|
||||
|
||||
+4
-2
@@ -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',
|
||||
|
||||
+2
-2
@@ -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',
|
||||
|
||||
+5
-2
@@ -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',
|
||||
|
||||
+5
-2
@@ -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',
|
||||
|
||||
+2
-2
@@ -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',
|
||||
|
||||
+5
-2
@@ -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
-2
@@ -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',
|
||||
|
||||
+6
-2
@@ -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',
|
||||
|
||||
+5
-2
@@ -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',
|
||||
|
||||
+31
@@ -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) {
|
||||
|
||||
+241
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
+156
@@ -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.`;
|
||||
});
|
||||
};
|
||||
+56
@@ -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),
|
||||
);
|
||||
});
|
||||
});
|
||||
+33
@@ -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;
|
||||
+167
@@ -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,
|
||||
|
||||
-92
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user