Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e27b5ec62 | ||
|
|
5751cacb4d | ||
|
|
64f9e07d5e | ||
|
|
a6aeb10484 | ||
|
|
2327c5dd30 | ||
|
|
91ce59d8e2 | ||
|
|
056e3a4cd8 | ||
|
|
9876af5587 | ||
|
|
563acc3f57 | ||
|
|
452433a9ae | ||
|
|
3bda05ea57 | ||
|
|
eda41b4eba | ||
|
|
de044f4b45 | ||
|
|
e3c79c803c | ||
|
|
59d69e2f5c | ||
|
|
4554dbe3c9 | ||
|
|
efba71a938 | ||
|
|
e6cc783a23 | ||
|
|
76e144e85a | ||
|
|
8f24cda586 | ||
|
|
c2a48a1cc2 | ||
|
|
37a1d3980f | ||
|
|
05f58b2dba | ||
|
|
4e0b69eb8d | ||
|
|
99799073e9 | ||
|
|
084fa8eaba | ||
|
|
788d120b71 | ||
|
|
3128157988 | ||
|
|
068d365731 | ||
|
|
9b05e7fff4 |
+38
-6
@@ -1,6 +1,38 @@
|
||||
/.github/ @charlesBochet @FelixMalfait @Weiko @prastoin @bosiraphael @etiennejouan @ijreilly @martmull @thomtrp
|
||||
/.github/CODEOWNERS @charlesBochet @FelixMalfait @Weiko @prastoin @bosiraphael @etiennejouan @ijreilly @martmull @thomtrp
|
||||
/.github/workflows/ @charlesBochet @FelixMalfait @Weiko @prastoin @bosiraphael @etiennejouan @ijreilly @martmull @thomtrp
|
||||
/.github/actions/ @charlesBochet @FelixMalfait @Weiko @prastoin @bosiraphael @etiennejouan @ijreilly @martmull @thomtrp
|
||||
/.github/dependabot.yml @charlesBochet @FelixMalfait @Weiko @prastoin @bosiraphael @etiennejouan @ijreilly @martmull @thomtrp
|
||||
/.yarnrc.yml @charlesBochet @FelixMalfait @Weiko @prastoin @bosiraphael @etiennejouan @ijreilly @martmull @thomtrp
|
||||
# ============================================================
|
||||
# CI/CD & GitHub infrastructure
|
||||
# ============================================================
|
||||
/.github/ @Weiko @charlesBochet @prastoin
|
||||
/.github/CODEOWNERS @Weiko @charlesBochet @prastoin
|
||||
/.github/workflows/ @Weiko @charlesBochet @prastoin
|
||||
/.github/actions/ @Weiko @charlesBochet @prastoin
|
||||
/.github/dependabot.yml @Weiko @charlesBochet @prastoin
|
||||
|
||||
# ============================================================
|
||||
# Package management & dependency lockfiles
|
||||
# ============================================================
|
||||
/.yarnrc.yml @Weiko @charlesBochet @prastoin
|
||||
/.npmrc @Weiko @charlesBochet @prastoin
|
||||
**/package.json @Weiko @charlesBochet @prastoin
|
||||
**/yarn.lock @Weiko @charlesBochet @prastoin
|
||||
|
||||
# ============================================================
|
||||
# twenty-apps: exempt from strict ownership (last match wins)
|
||||
# ============================================================
|
||||
/packages/twenty-apps/
|
||||
|
||||
# ============================================================
|
||||
# Build & tooling configs
|
||||
# ============================================================
|
||||
/nx.json @Weiko @charlesBochet @prastoin
|
||||
|
||||
# ============================================================
|
||||
# Docker (container-level compromise)
|
||||
# ============================================================
|
||||
**/Dockerfile* @Weiko @charlesBochet @prastoin
|
||||
**/docker-compose*.yml @Weiko @charlesBochet @prastoin
|
||||
**/docker-compose*.yaml @Weiko @charlesBochet @prastoin
|
||||
|
||||
# ============================================================
|
||||
# Git hooks (execute on checkout/commit/push)
|
||||
# ============================================================
|
||||
/.husky/** @Weiko @charlesBochet @prastoin
|
||||
|
||||
@@ -144,6 +144,9 @@ jobs:
|
||||
|
||||
npx nx run twenty-server:database:init:prod
|
||||
|
||||
- name: Flush cache before seeding current branch
|
||||
run: npx nx command-no-deps twenty-server -- cache:flush
|
||||
|
||||
- name: Seed current branch database with test data
|
||||
run: |
|
||||
npx nx command-no-deps twenty-server -- workspace:seed:dev
|
||||
@@ -160,7 +163,7 @@ jobs:
|
||||
- name: Wait for current branch server to be ready
|
||||
run: |
|
||||
echo "Waiting for current branch server to start..."
|
||||
timeout=300
|
||||
timeout=60
|
||||
interval=5
|
||||
elapsed=0
|
||||
|
||||
@@ -185,9 +188,10 @@ jobs:
|
||||
done
|
||||
|
||||
if [ $elapsed -ge $timeout ]; then
|
||||
echo "::warning::Timed out waiting for current branch server to serve a valid schema. Validation will skip the API diff."
|
||||
echo "❌ Timed out waiting for current branch server to serve a valid schema."
|
||||
echo "Current server log:"
|
||||
cat /tmp/current-server.log || echo "No current server log found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Download GraphQL and REST responses from current branch
|
||||
@@ -311,6 +315,9 @@ jobs:
|
||||
|
||||
npx nx run twenty-server:database:init:prod
|
||||
|
||||
- name: Flush cache before seeding main branch
|
||||
run: npx nx command-no-deps twenty-server -- cache:flush
|
||||
|
||||
- name: Seed main branch database with test data
|
||||
run: |
|
||||
npx nx command-no-deps twenty-server -- workspace:seed:dev
|
||||
@@ -352,9 +359,10 @@ jobs:
|
||||
done
|
||||
|
||||
if [ $elapsed -ge $timeout ]; then
|
||||
echo "::warning::Timed out waiting for main branch server to serve a valid schema. Validation will skip the API diff."
|
||||
echo "❌ Timed out waiting for main branch server to serve a valid schema."
|
||||
echo "Main server log:"
|
||||
cat /tmp/main-server.log || echo "No main server log found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Download GraphQL and REST responses from main branch
|
||||
@@ -450,6 +458,7 @@ jobs:
|
||||
echo "Using OpenAPITools/openapi-diff via Docker"
|
||||
|
||||
- name: Generate GraphQL Schema Diff Reports
|
||||
id: graphql-diff
|
||||
if: steps.validate-schemas.outputs.valid == 'true'
|
||||
run: |
|
||||
echo "=== INSTALLING GRAPHQL INSPECTOR CLI ==="
|
||||
@@ -463,6 +472,7 @@ jobs:
|
||||
echo "✅ No changes in GraphQL schema"
|
||||
else
|
||||
echo "⚠️ Changes detected in GraphQL schema, generating report..."
|
||||
echo "core_breaking=true" >> $GITHUB_OUTPUT
|
||||
echo "# GraphQL Schema Changes" > graphql-schema-diff.md
|
||||
echo "" >> graphql-schema-diff.md
|
||||
graphql-inspector diff main-schema-introspection.json current-schema-introspection.json >> graphql-schema-diff.md 2>&1 || {
|
||||
@@ -480,6 +490,7 @@ jobs:
|
||||
echo "✅ No changes in GraphQL metadata schema"
|
||||
else
|
||||
echo "⚠️ Changes detected in GraphQL metadata schema, generating report..."
|
||||
echo "metadata_breaking=true" >> $GITHUB_OUTPUT
|
||||
echo "# GraphQL Metadata Schema Changes" > graphql-metadata-diff.md
|
||||
echo "" >> graphql-metadata-diff.md
|
||||
graphql-inspector diff main-metadata-schema-introspection.json current-metadata-schema-introspection.json >> graphql-metadata-diff.md 2>&1 || {
|
||||
@@ -496,6 +507,7 @@ jobs:
|
||||
ls -la *-diff.md 2>/dev/null || echo "No diff files generated (no changes detected)"
|
||||
|
||||
- name: Check REST API Breaking Changes
|
||||
id: rest-diff
|
||||
if: steps.validate-schemas.outputs.valid == 'true'
|
||||
run: |
|
||||
echo "=== CHECKING REST API FOR BREAKING CHANGES ==="
|
||||
@@ -518,6 +530,7 @@ jobs:
|
||||
|
||||
if [ "$incompatible" = "true" ]; then
|
||||
echo "❌ Breaking changes detected in REST API"
|
||||
echo "breaking=true" >> $GITHUB_OUTPUT
|
||||
|
||||
# Generate breaking changes report
|
||||
echo "# REST API Breaking Changes" > rest-api-diff.md
|
||||
@@ -565,6 +578,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Check REST Metadata API Breaking Changes
|
||||
id: rest-metadata-diff
|
||||
if: steps.validate-schemas.outputs.valid == 'true'
|
||||
run: |
|
||||
echo "=== CHECKING REST METADATA API FOR BREAKING CHANGES ==="
|
||||
@@ -587,6 +601,7 @@ jobs:
|
||||
|
||||
if [ "$incompatible" = "true" ]; then
|
||||
echo "❌ Breaking changes detected in REST Metadata API"
|
||||
echo "breaking=true" >> $GITHUB_OUTPUT
|
||||
|
||||
# Generate breaking changes report (only for breaking changes)
|
||||
echo "# REST Metadata API Breaking Changes" > rest-metadata-api-diff.md
|
||||
@@ -632,6 +647,79 @@ jobs:
|
||||
echo "::warning::REST Metadata API analysis tool error - continuing workflow"
|
||||
fi
|
||||
|
||||
- name: Fail on breaking changes
|
||||
if: steps.validate-schemas.outputs.valid == 'true'
|
||||
run: |
|
||||
breaking=false
|
||||
|
||||
if [ "${{ steps.graphql-diff.outputs.core_breaking }}" = "true" ]; then
|
||||
echo "❌ GraphQL core schema has breaking changes"
|
||||
breaking=true
|
||||
if [ -f graphql-schema-diff.md ]; then
|
||||
echo ""
|
||||
cat graphql-schema-diff.md
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${{ steps.graphql-diff.outputs.metadata_breaking }}" = "true" ]; then
|
||||
echo "❌ GraphQL metadata schema has breaking changes"
|
||||
breaking=true
|
||||
if [ -f graphql-metadata-diff.md ]; then
|
||||
echo ""
|
||||
cat graphql-metadata-diff.md
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${{ steps.rest-diff.outputs.breaking }}" = "true" ]; then
|
||||
echo "❌ REST core API has breaking changes"
|
||||
breaking=true
|
||||
if [ -f rest-api-diff.json ]; then
|
||||
echo ""
|
||||
jq -r '
|
||||
(if (.missingEndpoints | length) > 0 then
|
||||
" Removed endpoints:\n" +
|
||||
(.missingEndpoints | map(" - " + (.method // "?") + " " + (.pathUrl // "?")) | join("\n"))
|
||||
else "" end),
|
||||
(if (.changedOperations | length) > 0 then
|
||||
" Changed operations:\n" +
|
||||
(.changedOperations | map(" - " + (.method // "?") + " " + (.pathUrl // "?")) | join("\n"))
|
||||
else "" end)
|
||||
' rest-api-diff.json | sed '/^$/d'
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${{ steps.rest-metadata-diff.outputs.breaking }}" = "true" ]; then
|
||||
echo "❌ REST metadata API has breaking changes"
|
||||
breaking=true
|
||||
if [ -f rest-metadata-api-diff.json ]; then
|
||||
echo ""
|
||||
jq -r '
|
||||
(if (.missingEndpoints | length) > 0 then
|
||||
" Removed endpoints:\n" +
|
||||
(.missingEndpoints | map(" - " + (.method // "?") + " " + (.pathUrl // "?")) | join("\n"))
|
||||
else "" end),
|
||||
(if (.changedOperations | length) > 0 then
|
||||
" Changed operations:\n" +
|
||||
(.changedOperations | map(" - " + (.method // "?") + " " + (.pathUrl // "?")) | join("\n"))
|
||||
else "" end)
|
||||
' rest-metadata-api-diff.json | sed '/^$/d'
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$breaking" = "true" ]; then
|
||||
echo ""
|
||||
echo "This PR introduces breaking changes to the public API."
|
||||
echo "If intentional, deprecate the old endpoint and introduce a new one."
|
||||
echo "See the breaking changes report artifact and PR comment for details."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ No breaking API changes detected"
|
||||
|
||||
- name: Upload breaking changes report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
@@ -652,5 +740,3 @@ jobs:
|
||||
if [ -f /tmp/main-server.pid ]; then
|
||||
kill $(cat /tmp/main-server.pid) || true
|
||||
fi
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PermissionFlag, defineRole } from 'twenty-sdk/define';
|
||||
import { SystemPermissionFlag, defineRole } from 'twenty-sdk/define';
|
||||
import {
|
||||
CONTENT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
@@ -36,5 +36,5 @@ export default defineRole({
|
||||
canUpdateFieldValue: true,
|
||||
},
|
||||
],
|
||||
permissionFlags: [PermissionFlag.APPLICATIONS],
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS],
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PermissionFlag, defineRole } from 'twenty-sdk/define';
|
||||
import { SystemPermissionFlag, defineRole } from 'twenty-sdk/define';
|
||||
import {
|
||||
CONTENT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
@@ -36,5 +36,5 @@ export default defineRole({
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
],
|
||||
permissionFlags: [PermissionFlag.APPLICATIONS],
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS],
|
||||
});
|
||||
|
||||
@@ -2476,6 +2476,18 @@ type ImapSmtpCaldavConnectionSuccess {
|
||||
connectedAccountId: String!
|
||||
}
|
||||
|
||||
type Webhook {
|
||||
id: UUID!
|
||||
targetUrl: String!
|
||||
operations: [String!]!
|
||||
description: String
|
||||
secret: String!
|
||||
applicationId: UUID!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
deletedAt: DateTime
|
||||
}
|
||||
|
||||
type ToolIndexEntry {
|
||||
name: String!
|
||||
description: String!
|
||||
@@ -2902,18 +2914,6 @@ type MinimalMetadata {
|
||||
collectionHashes: [CollectionHash!]!
|
||||
}
|
||||
|
||||
type Webhook {
|
||||
id: UUID!
|
||||
targetUrl: String!
|
||||
operations: [String!]!
|
||||
description: String
|
||||
secret: String!
|
||||
applicationId: UUID!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
deletedAt: DateTime
|
||||
}
|
||||
|
||||
type Query {
|
||||
navigationMenuItems: [NavigationMenuItem!]!
|
||||
navigationMenuItem(id: UUID!): NavigationMenuItem
|
||||
@@ -2982,6 +2982,8 @@ type Query {
|
||||
getRoles: [Role!]!
|
||||
getToolIndex: [ToolIndexEntry!]!
|
||||
getToolInputSchema(toolName: String!): JSON
|
||||
webhooks: [Webhook!]!
|
||||
webhook(id: UUID!): Webhook
|
||||
field(
|
||||
"""The id of the record to find."""
|
||||
id: UUID!
|
||||
@@ -2999,8 +3001,6 @@ type Query {
|
||||
myMessageChannels(connectedAccountId: UUID): [MessageChannel!]!
|
||||
myConnectedAccounts: [ConnectedAccountPublicDTO!]!
|
||||
myCalendarChannels(connectedAccountId: UUID): [CalendarChannel!]!
|
||||
webhooks: [Webhook!]!
|
||||
webhook(id: UUID!): Webhook
|
||||
minimalMetadata: MinimalMetadata!
|
||||
chatThreads: [AgentChatThread!]!
|
||||
chatThread(id: UUID!): AgentChatThread!
|
||||
@@ -3222,6 +3222,9 @@ type Mutation {
|
||||
upsertRowLevelPermissionPredicates(input: UpsertRowLevelPermissionPredicatesInput!): UpsertRowLevelPermissionPredicatesResult!
|
||||
assignRoleToAgent(agentId: UUID!, roleId: UUID!): Boolean!
|
||||
removeRoleFromAgent(agentId: UUID!): Boolean!
|
||||
createWebhook(input: CreateWebhookInput!): Webhook!
|
||||
updateWebhook(input: UpdateWebhookInput!): Webhook!
|
||||
deleteWebhook(id: UUID!): Webhook!
|
||||
createOneField(input: CreateOneFieldMetadataInput!): Field!
|
||||
updateOneField(input: UpdateOneFieldMetadataInput!): Field!
|
||||
deleteOneField(input: DeleteOneFieldInput!): Field!
|
||||
@@ -3238,9 +3241,6 @@ type Mutation {
|
||||
deleteEmailGroupChannel(id: UUID!): MessageChannel!
|
||||
deleteConnectedAccount(id: UUID!): ConnectedAccountPublicDTO!
|
||||
updateCalendarChannel(input: UpdateCalendarChannelInput!): CalendarChannel!
|
||||
createWebhook(input: CreateWebhookInput!): Webhook!
|
||||
updateWebhook(input: UpdateWebhookInput!): Webhook!
|
||||
deleteWebhook(id: UUID!): Webhook!
|
||||
createChatThread: AgentChatThread!
|
||||
sendChatMessage(threadId: UUID!, text: String!, messageId: UUID!, browsingContext: JSON, modelId: String, fileAttachments: [FileAttachmentInput!]): SendChatMessageResult!
|
||||
stopAgentChatStream(threadId: UUID!): Boolean!
|
||||
@@ -3392,7 +3392,7 @@ input UpdateViewFilterGroupInput {
|
||||
input CreateViewFilterInput {
|
||||
id: UUID
|
||||
fieldMetadataId: UUID!
|
||||
operand: ViewFilterOperand = CONTAINS
|
||||
operand: ViewFilterOperand
|
||||
value: JSON!
|
||||
viewFilterGroupId: UUID
|
||||
positionInViewFilterGroup: Float
|
||||
@@ -3500,7 +3500,7 @@ input UpsertViewWidgetViewFieldInput {
|
||||
input UpsertViewWidgetViewFilterInput {
|
||||
id: UUID
|
||||
fieldMetadataId: UUID!
|
||||
operand: ViewFilterOperand = CONTAINS
|
||||
operand: ViewFilterOperand
|
||||
value: JSON!
|
||||
viewFilterGroupId: UUID
|
||||
positionInViewFilterGroup: Float
|
||||
@@ -4047,6 +4047,29 @@ input RowLevelPermissionPredicateGroupInput {
|
||||
positionInRowLevelPermissionPredicateGroup: Float
|
||||
}
|
||||
|
||||
input CreateWebhookInput {
|
||||
id: UUID
|
||||
targetUrl: String!
|
||||
operations: [String!]!
|
||||
description: String
|
||||
secret: String
|
||||
}
|
||||
|
||||
input UpdateWebhookInput {
|
||||
"""The id of the webhook to update"""
|
||||
id: UUID!
|
||||
|
||||
"""The webhook fields to update"""
|
||||
update: UpdateWebhookInputUpdates!
|
||||
}
|
||||
|
||||
input UpdateWebhookInputUpdates {
|
||||
targetUrl: String
|
||||
operations: [String!]
|
||||
description: String
|
||||
secret: String
|
||||
}
|
||||
|
||||
input CreateOneFieldMetadataInput {
|
||||
"""The record to create"""
|
||||
field: CreateFieldInput!
|
||||
@@ -4184,29 +4207,6 @@ input UpdateCalendarChannelInputUpdates {
|
||||
isSyncEnabled: Boolean
|
||||
}
|
||||
|
||||
input CreateWebhookInput {
|
||||
id: UUID
|
||||
targetUrl: String!
|
||||
operations: [String!]!
|
||||
description: String
|
||||
secret: String
|
||||
}
|
||||
|
||||
input UpdateWebhookInput {
|
||||
"""The id of the webhook to update"""
|
||||
id: UUID!
|
||||
|
||||
"""The webhook fields to update"""
|
||||
update: UpdateWebhookInputUpdates!
|
||||
}
|
||||
|
||||
input UpdateWebhookInputUpdates {
|
||||
targetUrl: String
|
||||
operations: [String!]
|
||||
description: String
|
||||
secret: String
|
||||
}
|
||||
|
||||
input FileAttachmentInput {
|
||||
id: UUID!
|
||||
filename: String!
|
||||
|
||||
@@ -2160,6 +2160,19 @@ export interface ImapSmtpCaldavConnectionSuccess {
|
||||
__typename: 'ImapSmtpCaldavConnectionSuccess'
|
||||
}
|
||||
|
||||
export interface Webhook {
|
||||
id: Scalars['UUID']
|
||||
targetUrl: Scalars['String']
|
||||
operations: Scalars['String'][]
|
||||
description?: Scalars['String']
|
||||
secret: Scalars['String']
|
||||
applicationId: Scalars['UUID']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
deletedAt?: Scalars['DateTime']
|
||||
__typename: 'Webhook'
|
||||
}
|
||||
|
||||
export interface ToolIndexEntry {
|
||||
name: Scalars['String']
|
||||
description: Scalars['String']
|
||||
@@ -2528,19 +2541,6 @@ export interface MinimalMetadata {
|
||||
__typename: 'MinimalMetadata'
|
||||
}
|
||||
|
||||
export interface Webhook {
|
||||
id: Scalars['UUID']
|
||||
targetUrl: Scalars['String']
|
||||
operations: Scalars['String'][]
|
||||
description?: Scalars['String']
|
||||
secret: Scalars['String']
|
||||
applicationId: Scalars['UUID']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
deletedAt?: Scalars['DateTime']
|
||||
__typename: 'Webhook'
|
||||
}
|
||||
|
||||
export interface Query {
|
||||
navigationMenuItems: NavigationMenuItem[]
|
||||
navigationMenuItem?: NavigationMenuItem
|
||||
@@ -2591,6 +2591,8 @@ export interface Query {
|
||||
getRoles: Role[]
|
||||
getToolIndex: ToolIndexEntry[]
|
||||
getToolInputSchema?: Scalars['JSON']
|
||||
webhooks: Webhook[]
|
||||
webhook?: Webhook
|
||||
field: Field
|
||||
fields: FieldConnection
|
||||
getViewGroups: ViewGroup[]
|
||||
@@ -2599,8 +2601,6 @@ export interface Query {
|
||||
myMessageChannels: MessageChannel[]
|
||||
myConnectedAccounts: ConnectedAccountPublicDTO[]
|
||||
myCalendarChannels: CalendarChannel[]
|
||||
webhooks: Webhook[]
|
||||
webhook?: Webhook
|
||||
minimalMetadata: MinimalMetadata
|
||||
chatThreads: AgentChatThread[]
|
||||
chatThread: AgentChatThread
|
||||
@@ -2755,6 +2755,9 @@ export interface Mutation {
|
||||
upsertRowLevelPermissionPredicates: UpsertRowLevelPermissionPredicatesResult
|
||||
assignRoleToAgent: Scalars['Boolean']
|
||||
removeRoleFromAgent: Scalars['Boolean']
|
||||
createWebhook: Webhook
|
||||
updateWebhook: Webhook
|
||||
deleteWebhook: Webhook
|
||||
createOneField: Field
|
||||
updateOneField: Field
|
||||
deleteOneField: Field
|
||||
@@ -2771,9 +2774,6 @@ export interface Mutation {
|
||||
deleteEmailGroupChannel: MessageChannel
|
||||
deleteConnectedAccount: ConnectedAccountPublicDTO
|
||||
updateCalendarChannel: CalendarChannel
|
||||
createWebhook: Webhook
|
||||
updateWebhook: Webhook
|
||||
deleteWebhook: Webhook
|
||||
createChatThread: AgentChatThread
|
||||
sendChatMessage: SendChatMessageResult
|
||||
stopAgentChatStream: Scalars['Boolean']
|
||||
@@ -5166,6 +5166,20 @@ export interface ImapSmtpCaldavConnectionSuccessGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface WebhookGenqlSelection{
|
||||
id?: boolean | number
|
||||
targetUrl?: boolean | number
|
||||
operations?: boolean | number
|
||||
description?: boolean | number
|
||||
secret?: boolean | number
|
||||
applicationId?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
deletedAt?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ToolIndexEntryGenqlSelection{
|
||||
name?: boolean | number
|
||||
description?: boolean | number
|
||||
@@ -5541,20 +5555,6 @@ export interface MinimalMetadataGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface WebhookGenqlSelection{
|
||||
id?: boolean | number
|
||||
targetUrl?: boolean | number
|
||||
operations?: boolean | number
|
||||
description?: boolean | number
|
||||
secret?: boolean | number
|
||||
applicationId?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
deletedAt?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface QueryGenqlSelection{
|
||||
navigationMenuItems?: NavigationMenuItemGenqlSelection
|
||||
navigationMenuItem?: (NavigationMenuItemGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
@@ -5617,6 +5617,8 @@ export interface QueryGenqlSelection{
|
||||
getRoles?: RoleGenqlSelection
|
||||
getToolIndex?: ToolIndexEntryGenqlSelection
|
||||
getToolInputSchema?: { __args: {toolName: Scalars['String']} }
|
||||
webhooks?: WebhookGenqlSelection
|
||||
webhook?: (WebhookGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
field?: (FieldGenqlSelection & { __args: {
|
||||
/** The id of the record to find. */
|
||||
id: Scalars['UUID']} })
|
||||
@@ -5631,8 +5633,6 @@ export interface QueryGenqlSelection{
|
||||
myMessageChannels?: (MessageChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} })
|
||||
myConnectedAccounts?: ConnectedAccountPublicDTOGenqlSelection
|
||||
myCalendarChannels?: (CalendarChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} })
|
||||
webhooks?: WebhookGenqlSelection
|
||||
webhook?: (WebhookGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
minimalMetadata?: MinimalMetadataGenqlSelection
|
||||
chatThreads?: AgentChatThreadGenqlSelection
|
||||
chatThread?: (AgentChatThreadGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
@@ -5808,6 +5808,9 @@ export interface MutationGenqlSelection{
|
||||
upsertRowLevelPermissionPredicates?: (UpsertRowLevelPermissionPredicatesResultGenqlSelection & { __args: {input: UpsertRowLevelPermissionPredicatesInput} })
|
||||
assignRoleToAgent?: { __args: {agentId: Scalars['UUID'], roleId: Scalars['UUID']} }
|
||||
removeRoleFromAgent?: { __args: {agentId: Scalars['UUID']} }
|
||||
createWebhook?: (WebhookGenqlSelection & { __args: {input: CreateWebhookInput} })
|
||||
updateWebhook?: (WebhookGenqlSelection & { __args: {input: UpdateWebhookInput} })
|
||||
deleteWebhook?: (WebhookGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
createOneField?: (FieldGenqlSelection & { __args: {input: CreateOneFieldMetadataInput} })
|
||||
updateOneField?: (FieldGenqlSelection & { __args: {input: UpdateOneFieldMetadataInput} })
|
||||
deleteOneField?: (FieldGenqlSelection & { __args: {input: DeleteOneFieldInput} })
|
||||
@@ -5824,9 +5827,6 @@ export interface MutationGenqlSelection{
|
||||
deleteEmailGroupChannel?: (MessageChannelGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
deleteConnectedAccount?: (ConnectedAccountPublicDTOGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
updateCalendarChannel?: (CalendarChannelGenqlSelection & { __args: {input: UpdateCalendarChannelInput} })
|
||||
createWebhook?: (WebhookGenqlSelection & { __args: {input: CreateWebhookInput} })
|
||||
updateWebhook?: (WebhookGenqlSelection & { __args: {input: UpdateWebhookInput} })
|
||||
deleteWebhook?: (WebhookGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
createChatThread?: AgentChatThreadGenqlSelection
|
||||
sendChatMessage?: (SendChatMessageResultGenqlSelection & { __args: {threadId: Scalars['UUID'], text: Scalars['String'], messageId: Scalars['UUID'], browsingContext?: (Scalars['JSON'] | null), modelId?: (Scalars['String'] | null), fileAttachments?: (FileAttachmentInput[] | null)} })
|
||||
stopAgentChatStream?: { __args: {threadId: Scalars['UUID']} }
|
||||
@@ -6154,6 +6154,16 @@ export interface RowLevelPermissionPredicateInput {id?: (Scalars['UUID'] | null)
|
||||
|
||||
export interface RowLevelPermissionPredicateGroupInput {id?: (Scalars['UUID'] | null),objectMetadataId: Scalars['UUID'],parentRowLevelPermissionPredicateGroupId?: (Scalars['UUID'] | null),logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator,positionInRowLevelPermissionPredicateGroup?: (Scalars['Float'] | null)}
|
||||
|
||||
export interface CreateWebhookInput {id?: (Scalars['UUID'] | null),targetUrl: Scalars['String'],operations: Scalars['String'][],description?: (Scalars['String'] | null),secret?: (Scalars['String'] | null)}
|
||||
|
||||
export interface UpdateWebhookInput {
|
||||
/** The id of the webhook to update */
|
||||
id: Scalars['UUID'],
|
||||
/** The webhook fields to update */
|
||||
update: UpdateWebhookInputUpdates}
|
||||
|
||||
export interface UpdateWebhookInputUpdates {targetUrl?: (Scalars['String'] | null),operations?: (Scalars['String'][] | null),description?: (Scalars['String'] | null),secret?: (Scalars['String'] | null)}
|
||||
|
||||
export interface CreateOneFieldMetadataInput {
|
||||
/** The record to create */
|
||||
field: CreateFieldInput}
|
||||
@@ -6206,16 +6216,6 @@ export interface UpdateCalendarChannelInput {id: Scalars['UUID'],update: UpdateC
|
||||
|
||||
export interface UpdateCalendarChannelInputUpdates {visibility?: (CalendarChannelVisibility | null),isContactAutoCreationEnabled?: (Scalars['Boolean'] | null),contactAutoCreationPolicy?: (CalendarChannelContactAutoCreationPolicy | null),isSyncEnabled?: (Scalars['Boolean'] | null)}
|
||||
|
||||
export interface CreateWebhookInput {id?: (Scalars['UUID'] | null),targetUrl: Scalars['String'],operations: Scalars['String'][],description?: (Scalars['String'] | null),secret?: (Scalars['String'] | null)}
|
||||
|
||||
export interface UpdateWebhookInput {
|
||||
/** The id of the webhook to update */
|
||||
id: Scalars['UUID'],
|
||||
/** The webhook fields to update */
|
||||
update: UpdateWebhookInputUpdates}
|
||||
|
||||
export interface UpdateWebhookInputUpdates {targetUrl?: (Scalars['String'] | null),operations?: (Scalars['String'][] | null),description?: (Scalars['String'] | null),secret?: (Scalars['String'] | null)}
|
||||
|
||||
export interface FileAttachmentInput {id: Scalars['UUID'],filename: Scalars['String']}
|
||||
|
||||
export interface CreateSkillInput {id?: (Scalars['UUID'] | null),name: Scalars['String'],label: Scalars['String'],icon?: (Scalars['String'] | null),description?: (Scalars['String'] | null),content: Scalars['String']}
|
||||
@@ -7937,6 +7937,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const Webhook_possibleTypes: string[] = ['Webhook']
|
||||
export const isWebhook = (obj?: { __typename?: any } | null): obj is Webhook => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isWebhook"')
|
||||
return Webhook_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ToolIndexEntry_possibleTypes: string[] = ['ToolIndexEntry']
|
||||
export const isToolIndexEntry = (obj?: { __typename?: any } | null): obj is ToolIndexEntry => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isToolIndexEntry"')
|
||||
@@ -8201,14 +8209,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const Webhook_possibleTypes: string[] = ['Webhook']
|
||||
export const isWebhook = (obj?: { __typename?: any } | null): obj is Webhook => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isWebhook"')
|
||||
return Webhook_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const Query_possibleTypes: string[] = ['Query']
|
||||
export const isQuery = (obj?: { __typename?: any } | null): obj is Query => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isQuery"')
|
||||
|
||||
@@ -60,20 +60,20 @@ export default {
|
||||
226,
|
||||
262,
|
||||
263,
|
||||
292,
|
||||
301,
|
||||
293,
|
||||
302,
|
||||
303,
|
||||
304,
|
||||
306,
|
||||
305,
|
||||
307,
|
||||
308,
|
||||
309,
|
||||
310,
|
||||
311,
|
||||
312,
|
||||
315,
|
||||
317,
|
||||
313,
|
||||
316,
|
||||
318,
|
||||
327,
|
||||
334,
|
||||
341,
|
||||
@@ -4912,6 +4912,38 @@ export default {
|
||||
1
|
||||
]
|
||||
},
|
||||
"Webhook": {
|
||||
"id": [
|
||||
3
|
||||
],
|
||||
"targetUrl": [
|
||||
1
|
||||
],
|
||||
"operations": [
|
||||
1
|
||||
],
|
||||
"description": [
|
||||
1
|
||||
],
|
||||
"secret": [
|
||||
1
|
||||
],
|
||||
"applicationId": [
|
||||
3
|
||||
],
|
||||
"createdAt": [
|
||||
4
|
||||
],
|
||||
"updatedAt": [
|
||||
4
|
||||
],
|
||||
"deletedAt": [
|
||||
4
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"ToolIndexEntry": {
|
||||
"name": [
|
||||
1
|
||||
@@ -5051,7 +5083,7 @@ export default {
|
||||
1
|
||||
],
|
||||
"series": [
|
||||
277
|
||||
278
|
||||
],
|
||||
"xAxisLabel": [
|
||||
1
|
||||
@@ -5100,7 +5132,7 @@ export default {
|
||||
1
|
||||
],
|
||||
"data": [
|
||||
279
|
||||
280
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -5108,7 +5140,7 @@ export default {
|
||||
},
|
||||
"LineChartData": {
|
||||
"series": [
|
||||
280
|
||||
281
|
||||
],
|
||||
"xAxisLabel": [
|
||||
1
|
||||
@@ -5145,7 +5177,7 @@ export default {
|
||||
},
|
||||
"PieChartData": {
|
||||
"data": [
|
||||
282
|
||||
283
|
||||
],
|
||||
"showLegend": [
|
||||
6
|
||||
@@ -5239,13 +5271,13 @@ export default {
|
||||
},
|
||||
"EventLogQueryResult": {
|
||||
"records": [
|
||||
286
|
||||
287
|
||||
],
|
||||
"totalCount": [
|
||||
21
|
||||
],
|
||||
"pageInfo": [
|
||||
287
|
||||
288
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -5309,7 +5341,7 @@ export default {
|
||||
1
|
||||
],
|
||||
"parts": [
|
||||
275
|
||||
276
|
||||
],
|
||||
"processedAt": [
|
||||
4
|
||||
@@ -5323,7 +5355,7 @@ export default {
|
||||
},
|
||||
"AgentChatThread": {
|
||||
"id": [
|
||||
292
|
||||
293
|
||||
],
|
||||
"title": [
|
||||
1
|
||||
@@ -5379,7 +5411,7 @@ export default {
|
||||
},
|
||||
"AiSystemPromptPreview": {
|
||||
"sections": [
|
||||
293
|
||||
294
|
||||
],
|
||||
"estimatedTokenCount": [
|
||||
21
|
||||
@@ -5455,10 +5487,10 @@ export default {
|
||||
3
|
||||
],
|
||||
"evaluations": [
|
||||
298
|
||||
299
|
||||
],
|
||||
"messages": [
|
||||
290
|
||||
291
|
||||
],
|
||||
"createdAt": [
|
||||
4
|
||||
@@ -5475,19 +5507,19 @@ export default {
|
||||
1
|
||||
],
|
||||
"syncStatus": [
|
||||
301
|
||||
],
|
||||
"syncStage": [
|
||||
302
|
||||
],
|
||||
"visibility": [
|
||||
"syncStage": [
|
||||
303
|
||||
],
|
||||
"visibility": [
|
||||
304
|
||||
],
|
||||
"isContactAutoCreationEnabled": [
|
||||
6
|
||||
],
|
||||
"contactAutoCreationPolicy": [
|
||||
304
|
||||
305
|
||||
],
|
||||
"isSyncEnabled": [
|
||||
6
|
||||
@@ -5523,22 +5555,22 @@ export default {
|
||||
3
|
||||
],
|
||||
"visibility": [
|
||||
306
|
||||
307
|
||||
],
|
||||
"handle": [
|
||||
1
|
||||
],
|
||||
"type": [
|
||||
307
|
||||
308
|
||||
],
|
||||
"isContactAutoCreationEnabled": [
|
||||
6
|
||||
],
|
||||
"contactAutoCreationPolicy": [
|
||||
308
|
||||
309
|
||||
],
|
||||
"messageFolderImportPolicy": [
|
||||
309
|
||||
310
|
||||
],
|
||||
"excludeNonProfessionalEmails": [
|
||||
6
|
||||
@@ -5547,7 +5579,7 @@ export default {
|
||||
6
|
||||
],
|
||||
"pendingGroupEmailsAction": [
|
||||
310
|
||||
311
|
||||
],
|
||||
"isSyncEnabled": [
|
||||
6
|
||||
@@ -5556,10 +5588,10 @@ export default {
|
||||
4
|
||||
],
|
||||
"syncStatus": [
|
||||
311
|
||||
312
|
||||
],
|
||||
"syncStage": [
|
||||
312
|
||||
313
|
||||
],
|
||||
"syncStageStartedAt": [
|
||||
4
|
||||
@@ -5595,7 +5627,7 @@ export default {
|
||||
"MessageChannelSyncStage": {},
|
||||
"CreateEmailGroupChannelOutput": {
|
||||
"messageChannel": [
|
||||
305
|
||||
306
|
||||
],
|
||||
"forwardingAddress": [
|
||||
1
|
||||
@@ -5624,7 +5656,7 @@ export default {
|
||||
1
|
||||
],
|
||||
"pendingSyncAction": [
|
||||
315
|
||||
316
|
||||
],
|
||||
"messageChannelId": [
|
||||
3
|
||||
@@ -5642,7 +5674,7 @@ export default {
|
||||
"MessageFolderPendingSyncAction": {},
|
||||
"CollectionHash": {
|
||||
"collectionName": [
|
||||
317
|
||||
318
|
||||
],
|
||||
"hash": [
|
||||
1
|
||||
@@ -5709,45 +5741,13 @@ export default {
|
||||
},
|
||||
"MinimalMetadata": {
|
||||
"objectMetadataItems": [
|
||||
318
|
||||
],
|
||||
"views": [
|
||||
319
|
||||
],
|
||||
"views": [
|
||||
320
|
||||
],
|
||||
"collectionHashes": [
|
||||
316
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"Webhook": {
|
||||
"id": [
|
||||
3
|
||||
],
|
||||
"targetUrl": [
|
||||
1
|
||||
],
|
||||
"operations": [
|
||||
1
|
||||
],
|
||||
"description": [
|
||||
1
|
||||
],
|
||||
"secret": [
|
||||
1
|
||||
],
|
||||
"applicationId": [
|
||||
3
|
||||
],
|
||||
"createdAt": [
|
||||
4
|
||||
],
|
||||
"updatedAt": [
|
||||
4
|
||||
],
|
||||
"deletedAt": [
|
||||
4
|
||||
317
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -6107,7 +6107,7 @@ export default {
|
||||
29
|
||||
],
|
||||
"getToolIndex": [
|
||||
274
|
||||
275
|
||||
],
|
||||
"getToolInputSchema": [
|
||||
15,
|
||||
@@ -6118,6 +6118,18 @@ export default {
|
||||
]
|
||||
}
|
||||
],
|
||||
"webhooks": [
|
||||
274
|
||||
],
|
||||
"webhook": [
|
||||
274,
|
||||
{
|
||||
"id": [
|
||||
3,
|
||||
"UUID!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"field": [
|
||||
43,
|
||||
{
|
||||
@@ -6158,7 +6170,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"myMessageFolders": [
|
||||
314,
|
||||
315,
|
||||
{
|
||||
"messageChannelId": [
|
||||
3
|
||||
@@ -6166,7 +6178,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"myMessageChannels": [
|
||||
305,
|
||||
306,
|
||||
{
|
||||
"connectedAccountId": [
|
||||
3
|
||||
@@ -6177,33 +6189,21 @@ export default {
|
||||
269
|
||||
],
|
||||
"myCalendarChannels": [
|
||||
300,
|
||||
301,
|
||||
{
|
||||
"connectedAccountId": [
|
||||
3
|
||||
]
|
||||
}
|
||||
],
|
||||
"webhooks": [
|
||||
"minimalMetadata": [
|
||||
321
|
||||
],
|
||||
"webhook": [
|
||||
321,
|
||||
{
|
||||
"id": [
|
||||
3,
|
||||
"UUID!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"minimalMetadata": [
|
||||
320
|
||||
],
|
||||
"chatThreads": [
|
||||
291
|
||||
292
|
||||
],
|
||||
"chatThread": [
|
||||
291,
|
||||
292,
|
||||
{
|
||||
"id": [
|
||||
3,
|
||||
@@ -6212,7 +6212,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"chatMessages": [
|
||||
290,
|
||||
291,
|
||||
{
|
||||
"threadId": [
|
||||
3,
|
||||
@@ -6221,7 +6221,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"chatStreamCatchupChunks": [
|
||||
295,
|
||||
296,
|
||||
{
|
||||
"threadId": [
|
||||
3,
|
||||
@@ -6230,13 +6230,13 @@ export default {
|
||||
}
|
||||
],
|
||||
"getAiSystemPromptPreview": [
|
||||
294
|
||||
295
|
||||
],
|
||||
"skills": [
|
||||
289
|
||||
290
|
||||
],
|
||||
"skill": [
|
||||
289,
|
||||
290,
|
||||
{
|
||||
"id": [
|
||||
3,
|
||||
@@ -6245,7 +6245,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"agentTurns": [
|
||||
299,
|
||||
300,
|
||||
{
|
||||
"agentId": [
|
||||
3,
|
||||
@@ -6390,7 +6390,7 @@ export default {
|
||||
219
|
||||
],
|
||||
"eventLogs": [
|
||||
288,
|
||||
289,
|
||||
{
|
||||
"input": [
|
||||
326,
|
||||
@@ -6399,7 +6399,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"pieChartData": [
|
||||
283,
|
||||
284,
|
||||
{
|
||||
"input": [
|
||||
330,
|
||||
@@ -6408,7 +6408,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"lineChartData": [
|
||||
281,
|
||||
282,
|
||||
{
|
||||
"input": [
|
||||
331,
|
||||
@@ -6417,7 +6417,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"barChartData": [
|
||||
278,
|
||||
279,
|
||||
{
|
||||
"input": [
|
||||
332,
|
||||
@@ -6506,7 +6506,7 @@ export default {
|
||||
},
|
||||
"LogicFunctionIdInput": {
|
||||
"id": [
|
||||
292
|
||||
293
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -7616,11 +7616,38 @@ export default {
|
||||
]
|
||||
}
|
||||
],
|
||||
"createWebhook": [
|
||||
274,
|
||||
{
|
||||
"input": [
|
||||
418,
|
||||
"CreateWebhookInput!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"updateWebhook": [
|
||||
274,
|
||||
{
|
||||
"input": [
|
||||
419,
|
||||
"UpdateWebhookInput!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"deleteWebhook": [
|
||||
274,
|
||||
{
|
||||
"id": [
|
||||
3,
|
||||
"UUID!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"createOneField": [
|
||||
43,
|
||||
{
|
||||
"input": [
|
||||
418,
|
||||
421,
|
||||
"CreateOneFieldMetadataInput!"
|
||||
]
|
||||
}
|
||||
@@ -7629,7 +7656,7 @@ export default {
|
||||
43,
|
||||
{
|
||||
"input": [
|
||||
420,
|
||||
423,
|
||||
"UpdateOneFieldMetadataInput!"
|
||||
]
|
||||
}
|
||||
@@ -7638,7 +7665,7 @@ export default {
|
||||
43,
|
||||
{
|
||||
"input": [
|
||||
422,
|
||||
425,
|
||||
"DeleteOneFieldInput!"
|
||||
]
|
||||
}
|
||||
@@ -7647,7 +7674,7 @@ export default {
|
||||
65,
|
||||
{
|
||||
"input": [
|
||||
423,
|
||||
426,
|
||||
"CreateViewGroupInput!"
|
||||
]
|
||||
}
|
||||
@@ -7656,7 +7683,7 @@ export default {
|
||||
65,
|
||||
{
|
||||
"inputs": [
|
||||
423,
|
||||
426,
|
||||
"[CreateViewGroupInput!]!"
|
||||
]
|
||||
}
|
||||
@@ -7665,7 +7692,7 @@ export default {
|
||||
65,
|
||||
{
|
||||
"input": [
|
||||
424,
|
||||
427,
|
||||
"UpdateViewGroupInput!"
|
||||
]
|
||||
}
|
||||
@@ -7674,7 +7701,7 @@ export default {
|
||||
65,
|
||||
{
|
||||
"inputs": [
|
||||
424,
|
||||
427,
|
||||
"[UpdateViewGroupInput!]!"
|
||||
]
|
||||
}
|
||||
@@ -7683,7 +7710,7 @@ export default {
|
||||
65,
|
||||
{
|
||||
"input": [
|
||||
426,
|
||||
429,
|
||||
"DeleteViewGroupInput!"
|
||||
]
|
||||
}
|
||||
@@ -7692,49 +7719,49 @@ export default {
|
||||
65,
|
||||
{
|
||||
"input": [
|
||||
427,
|
||||
430,
|
||||
"DestroyViewGroupInput!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"updateMessageFolder": [
|
||||
314,
|
||||
315,
|
||||
{
|
||||
"input": [
|
||||
428,
|
||||
431,
|
||||
"UpdateMessageFolderInput!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"updateMessageFolders": [
|
||||
314,
|
||||
315,
|
||||
{
|
||||
"input": [
|
||||
430,
|
||||
433,
|
||||
"UpdateMessageFoldersInput!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"updateMessageChannel": [
|
||||
305,
|
||||
306,
|
||||
{
|
||||
"input": [
|
||||
431,
|
||||
434,
|
||||
"UpdateMessageChannelInput!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"createEmailGroupChannel": [
|
||||
313,
|
||||
314,
|
||||
{
|
||||
"input": [
|
||||
433,
|
||||
436,
|
||||
"CreateEmailGroupChannelInput!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"deleteEmailGroupChannel": [
|
||||
305,
|
||||
306,
|
||||
{
|
||||
"id": [
|
||||
3,
|
||||
@@ -7752,46 +7779,19 @@ export default {
|
||||
}
|
||||
],
|
||||
"updateCalendarChannel": [
|
||||
300,
|
||||
301,
|
||||
{
|
||||
"input": [
|
||||
434,
|
||||
437,
|
||||
"UpdateCalendarChannelInput!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"createWebhook": [
|
||||
321,
|
||||
{
|
||||
"input": [
|
||||
436,
|
||||
"CreateWebhookInput!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"updateWebhook": [
|
||||
321,
|
||||
{
|
||||
"input": [
|
||||
437,
|
||||
"UpdateWebhookInput!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"deleteWebhook": [
|
||||
321,
|
||||
{
|
||||
"id": [
|
||||
3,
|
||||
"UUID!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"createChatThread": [
|
||||
291
|
||||
292
|
||||
],
|
||||
"sendChatMessage": [
|
||||
296,
|
||||
297,
|
||||
{
|
||||
"threadId": [
|
||||
3,
|
||||
@@ -7827,7 +7827,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"renameChatThread": [
|
||||
291,
|
||||
292,
|
||||
{
|
||||
"id": [
|
||||
3,
|
||||
@@ -7840,7 +7840,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"archiveChatThread": [
|
||||
291,
|
||||
292,
|
||||
{
|
||||
"id": [
|
||||
3,
|
||||
@@ -7849,7 +7849,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"unarchiveChatThread": [
|
||||
291,
|
||||
292,
|
||||
{
|
||||
"id": [
|
||||
3,
|
||||
@@ -7876,7 +7876,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"createSkill": [
|
||||
289,
|
||||
290,
|
||||
{
|
||||
"input": [
|
||||
440,
|
||||
@@ -7885,7 +7885,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"updateSkill": [
|
||||
289,
|
||||
290,
|
||||
{
|
||||
"input": [
|
||||
441,
|
||||
@@ -7894,7 +7894,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"deleteSkill": [
|
||||
289,
|
||||
290,
|
||||
{
|
||||
"id": [
|
||||
3,
|
||||
@@ -7903,7 +7903,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"activateSkill": [
|
||||
289,
|
||||
290,
|
||||
{
|
||||
"id": [
|
||||
3,
|
||||
@@ -7912,7 +7912,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"deactivateSkill": [
|
||||
289,
|
||||
290,
|
||||
{
|
||||
"id": [
|
||||
3,
|
||||
@@ -7921,7 +7921,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"evaluateAgentTurn": [
|
||||
298,
|
||||
299,
|
||||
{
|
||||
"turnId": [
|
||||
3,
|
||||
@@ -7930,7 +7930,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"runEvaluationInput": [
|
||||
299,
|
||||
300,
|
||||
{
|
||||
"agentId": [
|
||||
3,
|
||||
@@ -8443,7 +8443,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"duplicateDashboard": [
|
||||
284,
|
||||
285,
|
||||
{
|
||||
"id": [
|
||||
3,
|
||||
@@ -8465,7 +8465,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"sendEmail": [
|
||||
285,
|
||||
286,
|
||||
{
|
||||
"input": [
|
||||
459,
|
||||
@@ -8474,7 +8474,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"startChannelSync": [
|
||||
276,
|
||||
277,
|
||||
{
|
||||
"connectedAccountId": [
|
||||
3,
|
||||
@@ -10320,9 +10320,57 @@ export default {
|
||||
1
|
||||
]
|
||||
},
|
||||
"CreateWebhookInput": {
|
||||
"id": [
|
||||
3
|
||||
],
|
||||
"targetUrl": [
|
||||
1
|
||||
],
|
||||
"operations": [
|
||||
1
|
||||
],
|
||||
"description": [
|
||||
1
|
||||
],
|
||||
"secret": [
|
||||
1
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"UpdateWebhookInput": {
|
||||
"id": [
|
||||
3
|
||||
],
|
||||
"update": [
|
||||
420
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"UpdateWebhookInputUpdates": {
|
||||
"targetUrl": [
|
||||
1
|
||||
],
|
||||
"operations": [
|
||||
1
|
||||
],
|
||||
"description": [
|
||||
1
|
||||
],
|
||||
"secret": [
|
||||
1
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"CreateOneFieldMetadataInput": {
|
||||
"field": [
|
||||
419
|
||||
422
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -10395,7 +10443,7 @@ export default {
|
||||
3
|
||||
],
|
||||
"update": [
|
||||
421
|
||||
424
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -10487,7 +10535,7 @@ export default {
|
||||
3
|
||||
],
|
||||
"update": [
|
||||
425
|
||||
428
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -10531,7 +10579,7 @@ export default {
|
||||
3
|
||||
],
|
||||
"update": [
|
||||
429
|
||||
432
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -10550,7 +10598,7 @@ export default {
|
||||
3
|
||||
],
|
||||
"update": [
|
||||
429
|
||||
432
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -10561,7 +10609,7 @@ export default {
|
||||
3
|
||||
],
|
||||
"update": [
|
||||
432
|
||||
435
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -10569,16 +10617,16 @@ export default {
|
||||
},
|
||||
"UpdateMessageChannelInputUpdates": {
|
||||
"visibility": [
|
||||
306
|
||||
307
|
||||
],
|
||||
"isContactAutoCreationEnabled": [
|
||||
6
|
||||
],
|
||||
"contactAutoCreationPolicy": [
|
||||
308
|
||||
309
|
||||
],
|
||||
"messageFolderImportPolicy": [
|
||||
309
|
||||
310
|
||||
],
|
||||
"isSyncEnabled": [
|
||||
6
|
||||
@@ -10606,7 +10654,7 @@ export default {
|
||||
3
|
||||
],
|
||||
"update": [
|
||||
435
|
||||
438
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -10614,13 +10662,13 @@ export default {
|
||||
},
|
||||
"UpdateCalendarChannelInputUpdates": {
|
||||
"visibility": [
|
||||
303
|
||||
304
|
||||
],
|
||||
"isContactAutoCreationEnabled": [
|
||||
6
|
||||
],
|
||||
"contactAutoCreationPolicy": [
|
||||
304
|
||||
305
|
||||
],
|
||||
"isSyncEnabled": [
|
||||
6
|
||||
@@ -10629,54 +10677,6 @@ export default {
|
||||
1
|
||||
]
|
||||
},
|
||||
"CreateWebhookInput": {
|
||||
"id": [
|
||||
3
|
||||
],
|
||||
"targetUrl": [
|
||||
1
|
||||
],
|
||||
"operations": [
|
||||
1
|
||||
],
|
||||
"description": [
|
||||
1
|
||||
],
|
||||
"secret": [
|
||||
1
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"UpdateWebhookInput": {
|
||||
"id": [
|
||||
3
|
||||
],
|
||||
"update": [
|
||||
438
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"UpdateWebhookInputUpdates": {
|
||||
"targetUrl": [
|
||||
1
|
||||
],
|
||||
"operations": [
|
||||
1
|
||||
],
|
||||
"description": [
|
||||
1
|
||||
],
|
||||
"secret": [
|
||||
1
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"FileAttachmentInput": {
|
||||
"id": [
|
||||
3
|
||||
@@ -10947,7 +10947,7 @@ export default {
|
||||
454
|
||||
],
|
||||
"metadataName": [
|
||||
317
|
||||
318
|
||||
],
|
||||
"universalIdentifier": [
|
||||
1
|
||||
@@ -11138,7 +11138,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"onAgentChatEvent": [
|
||||
297,
|
||||
298,
|
||||
{
|
||||
"threadId": [
|
||||
3,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Views
|
||||
description: Ship pre-configured saved views — column order, filters, groups — for objects in your app.
|
||||
icon: "list"
|
||||
icon: 'list'
|
||||
---
|
||||
|
||||
A **view** is a saved configuration for how records of an object are displayed: which fields appear, their order, whether they're visible, and any filters or groups applied. Use `defineView()` to ship pre-configured views with your app — typically a default index view for each custom object you create.
|
||||
@@ -38,6 +38,60 @@ export default defineView({
|
||||
- You can also declare `filters`, `filterGroups`, `groups`, and `fieldGroups` for advanced configurations.
|
||||
- `position` controls ordering when multiple views exist for the same object.
|
||||
|
||||
## Filters
|
||||
|
||||
A view can ship with pre-applied filters. Each filter has three coordinates: the **field** being filtered, the **operand** (how to compare), and the **value** (what to compare against). All three must line up — using an operand that doesn't apply to a field type will be rejected at sync time.
|
||||
|
||||
```ts
|
||||
import { ViewFilterOperand } from 'twenty-shared/types';
|
||||
|
||||
filters: [
|
||||
{
|
||||
universalIdentifier: '...',
|
||||
fieldMetadataUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
operand: ViewFilterOperand.IS,
|
||||
value: ['ACTIVE'],
|
||||
},
|
||||
],
|
||||
```
|
||||
|
||||
### Supported operands per field type
|
||||
|
||||
| Field type | Supported operands |
|
||||
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| `TEXT`, `EMAILS`, `FULL_NAME`, `ADDRESS`, `LINKS`, `PHONES`, `RAW_JSON`, `FILES`, `ACTOR`, `ARRAY` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `ACTOR.source`, `ACTOR.workspaceMemberId` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `SELECT` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `MULTI_SELECT` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `RELATION` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `NUMBER` | `IS`, `IS_NOT`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `RATING` | `IS`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `CURRENCY`, `CURRENCY.amountMicros` | `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `BOOLEAN` | `IS` |
|
||||
| `UUID` | `IS` |
|
||||
| `TS_VECTOR` | `VECTOR_SEARCH` |
|
||||
|
||||
> Field types with similar names can use entirely different operands — `SELECT` and `MULTI_SELECT` being a common case.
|
||||
|
||||
### Value shape per operand
|
||||
|
||||
The `value` field is always a JSON-serializable value, but its expected shape depends on the operand:
|
||||
|
||||
| Operand family | Value shape | Example |
|
||||
| ----------------------------------------------------- | ------------------------------ | ------------------------ |
|
||||
| `IS`, `IS_NOT` on `SELECT` | array of option keys (strings) | `['ACTIVE', 'PENDING']` |
|
||||
| `CONTAINS`, `DOES_NOT_CONTAIN` on `MULTI_SELECT` | array of option keys (strings) | `['TAG_A']` |
|
||||
| `IS`, `IS_NOT` on `RELATION` | array of record IDs (uuids) | `['c5a1...']` |
|
||||
| `CONTAINS`, `DOES_NOT_CONTAIN` on text-like fields | string | `'acme'` |
|
||||
| `IS`, `IS_NOT` on `NUMBER` | string (the value) | `'5'` |
|
||||
| `IS` on `RATING` / `UUID` | string (the value) | `'5'` |
|
||||
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | string (the bound) | `'10'` |
|
||||
| `IS`, `IS_BEFORE`, `IS_AFTER` on `DATE` / `DATE_TIME` | ISO 8601 string | `'2025-01-01T00:00:00Z'` |
|
||||
| `IS_EMPTY`, `IS_NOT_EMPTY` | empty string | `''` |
|
||||
| `IS` on `BOOLEAN` | `'true'` or `'false'` | `'true'` |
|
||||
|
||||
## How views show up in the UI
|
||||
|
||||
A view by itself isn't reachable from the sidebar. To make it appear there, pair it with a [navigation menu item](/developers/extend/apps/layout/navigation-menu-items) of type `VIEW` that points at the view's `universalIdentifier`. That's the canonical pattern: every custom object typically ships a default view + a sidebar entry that opens it.
|
||||
|
||||
@@ -16,10 +16,9 @@ Each key carries a `publicKey` (kept indefinitely so it can verify previously is
|
||||
|
||||
### Rotate the current key
|
||||
|
||||
- **Manual** — **Settings → Admin Panel → Signing keys → Revoke** on the current row. Revoking wipes its encrypted private material and demotes it; the next sign call automatically mints a fresh ES256 keypair as the new current. Tokens signed under any other (non-revoked) `kid` keep verifying until they expire.
|
||||
- **Enterprise (automatic)** — a daily cron (`'15 3 * * *'` UTC) issues a new current key once the existing one has been current for `SIGNING_KEY_ROTATION_DAYS` (default `90`). The previous key is *not* revoked, so tokens signed under it keep verifying. Register it once with `yarn command:prod cron:register:all`.
|
||||
Set `SIGNING_KEY_ROTATION_DAYS` to opt in: a daily cron then issues a new current key once the existing one is older than that threshold. Previous keys are *not* revoked, so tokens signed under them keep verifying. Leave the variable unset to disable auto-rotation.
|
||||
|
||||
<Note>The Enterprise cron and `SIGNING_KEY_ROTATION_DAYS` ship in v2.6+.</Note>
|
||||
<Note>Auto-rotation ships in v2.6+.</Note>
|
||||
|
||||
### Revoke a key (leak / emergency only)
|
||||
|
||||
|
||||
@@ -38,6 +38,60 @@ export default defineView({
|
||||
* Für erweiterte Konfigurationen können Sie außerdem `filters`, `filterGroups`, `groups` und `fieldGroups` deklarieren.
|
||||
* `position` steuert die Reihenfolge, wenn mehrere Ansichten für dasselbe Objekt existieren.
|
||||
|
||||
## Filter
|
||||
|
||||
Eine Ansicht kann mit vorab angewendeten Filtern ausgeliefert werden. Jeder Filter hat drei Koordinaten: das **Feld**, das gefiltert wird, der **Operand** (wie verglichen wird) und der **Wert** (womit verglichen wird). Alle drei müssen übereinstimmen — die Verwendung eines Operanden, der nicht auf einen Feldtyp anwendbar ist, wird bei der Synchronisierung zurückgewiesen.
|
||||
|
||||
```ts
|
||||
import { ViewFilterOperand } from 'twenty-shared/types';
|
||||
|
||||
filters: [
|
||||
{
|
||||
universalIdentifier: '...',
|
||||
fieldMetadataUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
operand: ViewFilterOperand.IS,
|
||||
value: ['ACTIVE'],
|
||||
},
|
||||
],
|
||||
```
|
||||
|
||||
### Unterstützte Operanden pro Feldtyp
|
||||
|
||||
| Feldtyp | Unterstützte Operanden |
|
||||
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| `TEXT`, `EMAILS`, `FULL_NAME`, `ADDRESS`, `LINKS`, `PHONES`, `RAW_JSON`, `FILES`, `ACTOR`, `ARRAY` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `ACTOR.source`, `ACTOR.workspaceMemberId` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `SELECT` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `MULTI_SELECT` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `RELATION` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `NUMBER` | `IS`, `IS_NOT`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `RATING` | `IS`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `CURRENCY`, `CURRENCY.amountMicros` | `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `BOOLEAN` | `IS` |
|
||||
| `UUID` | `IS` |
|
||||
| `TS_VECTOR` | `VECTOR_SEARCH` |
|
||||
|
||||
> Feldtypen mit ähnlichen Namen können völlig unterschiedliche Operanden verwenden – `SELECT` und `MULTI_SELECT` sind ein häufiges Beispiel.
|
||||
|
||||
### Wertstruktur je Operand
|
||||
|
||||
Das Feld `value` ist immer ein JSON-serialisierbarer Wert, aber die erwartete Struktur hängt vom Operanden ab:
|
||||
|
||||
| Operandenfamilie | Wertstruktur | Beispiel |
|
||||
| -------------------------------------------------------- | ------------------------------------- | ------------------------ |
|
||||
| `IS`, `IS_NOT` bei `SELECT` | Array von Optionsschlüsseln (Strings) | `['ACTIVE', 'PENDING']` |
|
||||
| `CONTAINS`, `DOES_NOT_CONTAIN` bei `MULTI_SELECT` | Array von Optionsschlüsseln (Strings) | `['TAG_A']` |
|
||||
| `IS`, `IS_NOT` bei `RELATION` | Array von Datensatz-IDs (UUIDs) | `['c5a1...']` |
|
||||
| `CONTAINS`, `DOES_NOT_CONTAIN` bei textähnlichen Feldern | Zeichenkette | `'acme'` |
|
||||
| `IS`, `IS_NOT` bei `NUMBER` | String (der Wert) | `'5'` |
|
||||
| `IS` bei `RATING` / `UUID` | String (der Wert) | `'5'` |
|
||||
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | String (der Grenzwert) | `'10'` |
|
||||
| `IS`, `IS_BEFORE`, `IS_AFTER` bei `DATE` / `DATE_TIME` | ISO-8601-String | `'2025-01-01T00:00:00Z'` |
|
||||
| `IS_EMPTY`, `IS_NOT_EMPTY` | leerer String | `''` |
|
||||
| `IS` bei `BOOLEAN` | `'true'` oder `'false'` | `'true'` |
|
||||
|
||||
## Wie Ansichten in der UI angezeigt werden
|
||||
|
||||
Eine Ansicht für sich ist aus der Seitenleiste nicht erreichbar. Damit sie dort erscheint, verknüpfen Sie sie mit einem [Navigationsmenüeintrag](/l/de/developers/extend/apps/layout/navigation-menu-items) des Typs `VIEW`, der auf den `universalIdentifier` der Ansicht zeigt. Das ist das kanonische Muster: Jedes benutzerdefinierte Objekt liefert typischerweise eine Standardansicht plus einen Eintrag in der Seitenleiste, der sie öffnet.
|
||||
|
||||
@@ -38,6 +38,60 @@ export default defineView({
|
||||
* Você também pode declarar `filters`, `filterGroups`, `groups` e `fieldGroups` para configurações avançadas.
|
||||
* `position` controla a ordenação quando existem várias visualizações para o mesmo objeto.
|
||||
|
||||
## Filtros
|
||||
|
||||
Uma visualização pode vir com filtros pré-aplicados. Cada filtro tem três coordenadas: o **campo** a ser filtrado, o **operador** (como comparar) e o **valor** (com o que comparar). As três precisam estar alinhadas — usar um operador que não se aplica a um tipo de campo será rejeitado no momento da sincronização.
|
||||
|
||||
```ts
|
||||
import { ViewFilterOperand } from 'twenty-shared/types';
|
||||
|
||||
filters: [
|
||||
{
|
||||
universalIdentifier: '...',
|
||||
fieldMetadataUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
operand: ViewFilterOperand.IS,
|
||||
value: ['ACTIVE'],
|
||||
},
|
||||
],
|
||||
```
|
||||
|
||||
### Operadores compatíveis por tipo de campo
|
||||
|
||||
| Tipo de campo | Operandos suportados |
|
||||
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| `TEXT`, `EMAILS`, `FULL_NAME`, `ADDRESS`, `LINKS`, `PHONES`, `RAW_JSON`, `FILES`, `ACTOR`, `ARRAY` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `ACTOR.source`, `ACTOR.workspaceMemberId` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `SELECT` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `MULTI_SELECT` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `RELATION` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `NUMBER` | `IS`, `IS_NOT`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `RATING` | `IS`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `CURRENCY`, `CURRENCY.amountMicros` | `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `BOOLEAN` | `IS` |
|
||||
| `UUID` | `IS` |
|
||||
| `TS_VECTOR` | `VECTOR_SEARCH` |
|
||||
|
||||
> Tipos de campos com nomes semelhantes podem usar operandos completamente diferentes — `SELECT` e `MULTI_SELECT` sendo um caso comum.
|
||||
|
||||
### Formato do valor por operando
|
||||
|
||||
O campo `value` é sempre um valor serializável em JSON, mas o seu formato esperado depende do operando:
|
||||
|
||||
| Família do operando | Formato do valor | Exemplo |
|
||||
| ----------------------------------------------------- | ----------------------------------- | ------------------------ |
|
||||
| `IS`, `IS_NOT` em `SELECT` | array de chaves de opções (strings) | `['ACTIVE', 'PENDING']` |
|
||||
| `CONTAINS`, `DOES_NOT_CONTAIN` em `MULTI_SELECT` | array de chaves de opções (strings) | `['TAG_A']` |
|
||||
| `IS`, `IS_NOT` em `RELATION` | array de IDs de registros (uuids) | `['c5a1...']` |
|
||||
| `CONTAINS`, `DOES_NOT_CONTAIN` em campos de texto | string | `'acme'` |
|
||||
| `IS`, `IS_NOT` em `NUMBER` | string (o valor) | `'5'` |
|
||||
| `IS` em `RATING` / `UUID` | string (o valor) | `'5'` |
|
||||
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | string (o limite) | `'10'` |
|
||||
| `IS`, `IS_BEFORE`, `IS_AFTER` em `DATE` / `DATE_TIME` | string ISO 8601 | `'2025-01-01T00:00:00Z'` |
|
||||
| `IS_EMPTY`, `IS_NOT_EMPTY` | string vazia | `''` |
|
||||
| `IS` em `BOOLEAN` | `'true'` ou `'false'` | `'true'` |
|
||||
|
||||
## Como as visualizações aparecem na UI
|
||||
|
||||
Uma visualização por si só não é acessível a partir da barra lateral. Para fazê-la aparecer lá, associe-a a um [item de menu de navegação](/l/pt/developers/extend/apps/layout/navigation-menu-items) do tipo `VIEW` que aponte para o `universalIdentifier` da visualização. Esse é o padrão canônico: cada objeto personalizado normalmente inclui uma visualização padrão + uma entrada na barra lateral que a abre.
|
||||
|
||||
@@ -38,6 +38,60 @@ export default defineView({
|
||||
* Puteți declara, de asemenea, `filters`, `filterGroups`, `groups` și `fieldGroups` pentru configurații avansate.
|
||||
* `position` controlează ordonarea atunci când există mai multe vizualizări pentru același obiect.
|
||||
|
||||
## Filtre
|
||||
|
||||
O vizualizare poate include filtre aplicate în prealabil. Fiecare filtru are trei coordonate: **câmpul** care este filtrat, **operandul** (cum se compară) și **valoarea** (față de ce se compară). Toate cele trei trebuie să se potrivească — folosirea unui operand care nu se aplică unui tip de câmp va fi respinsă în timpul sincronizării.
|
||||
|
||||
```ts
|
||||
import { ViewFilterOperand } from 'twenty-shared/types';
|
||||
|
||||
filters: [
|
||||
{
|
||||
universalIdentifier: '...',
|
||||
fieldMetadataUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
operand: ViewFilterOperand.IS,
|
||||
value: ['ACTIVE'],
|
||||
},
|
||||
],
|
||||
```
|
||||
|
||||
### Operanzi acceptați pentru fiecare tip de câmp
|
||||
|
||||
| Tipul câmpului | Operanzi acceptați |
|
||||
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| `TEXT`, `EMAILS`, `FULL_NAME`, `ADDRESS`, `LINKS`, `PHONES`, `RAW_JSON`, `FILES`, `ACTOR`, `ARRAY` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `ACTOR.source`, `ACTOR.workspaceMemberId` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `SELECT` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `MULTI_SELECT` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `RELATION` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `NUMBER` | `IS`, `IS_NOT`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `RATING` | `IS`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `CURRENCY`, `CURRENCY.amountMicros` | `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `BOOLEAN` | `IS` |
|
||||
| `UUID` | `IS` |
|
||||
| `TS_VECTOR` | `VECTOR_SEARCH` |
|
||||
|
||||
> Tipurile de câmp cu nume similare pot folosi operanzi complet diferiți — `SELECT` și `MULTI_SELECT` fiind un caz comun.
|
||||
|
||||
### Formă de valoare per operand
|
||||
|
||||
Câmpul `value` este întotdeauna o valoare serializabilă JSON, dar forma sa așteptată depinde de operand:
|
||||
|
||||
| Familie de operanzi | Formă de valoare | Exemplu |
|
||||
| ----------------------------------------------------- | ---------------------------------------------- | ------------------------ |
|
||||
| `IS`, `IS_NOT` pe `SELECT` | array de chei de opțiune (șiruri de caractere) | `['ACTIVE', 'PENDING']` |
|
||||
| `CONTAINS`, `DOES_NOT_CONTAIN` pe `MULTI_SELECT` | array de chei de opțiune (șiruri de caractere) | `['TAG_A']` |
|
||||
| `IS`, `IS_NOT` pe `RELATION` | array de ID-uri de înregistrare (uuid-uri) | `['c5a1...']` |
|
||||
| `CONTAINS`, `DOES_NOT_CONTAIN` pe câmpuri de tip text | șir | `'acme'` |
|
||||
| `IS`, `IS_NOT` pe `NUMBER` | șir de caractere (valoarea) | `'5'` |
|
||||
| `IS` pe `RATING` / `UUID` | șir de caractere (valoarea) | `'5'` |
|
||||
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | șir de caractere (limita) | `'10'` |
|
||||
| `IS`, `IS_BEFORE`, `IS_AFTER` pe `DATE` / `DATE_TIME` | șir în format ISO 8601 | `'2025-01-01T00:00:00Z'` |
|
||||
| `IS_EMPTY`, `IS_NOT_EMPTY` | șir gol | `''` |
|
||||
| `IS` pe `BOOLEAN` | `'true'` sau `'false'` | `'true'` |
|
||||
|
||||
## Cum apar vizualizările în interfața utilizatorului
|
||||
|
||||
O vizualizare, de una singură, nu este accesibilă din bara laterală. Pentru a o face să apară acolo, asociați-o cu un [element de meniu de navigare](/l/ro/developers/extend/apps/layout/navigation-menu-items) de tip `VIEW` care indică către `universalIdentifier` al vizualizării. Acesta este modelul canonic: fiecare obiect personalizat livrează, de obicei, o vizualizare implicită + o intrare în bara laterală care o deschide.
|
||||
|
||||
@@ -38,6 +38,60 @@ export default defineView({
|
||||
* Также вы можете определить `filters`, `filterGroups`, `groups` и `fieldGroups` для более продвинутых конфигураций.
|
||||
* `position` управляет порядком, когда для одного и того же объекта существует несколько представлений.
|
||||
|
||||
## Фильтры
|
||||
|
||||
Представление может поставляться с заранее примененными фильтрами. У каждого фильтра есть три координаты: **поле**, по которому выполняется фильтрация, **операнд** (как сравнивать) и **значение** (с чем сравнивать). Все три должны совпадать — использование операнда, который не подходит для типа поля, будет отклонено во время синхронизации.
|
||||
|
||||
```ts
|
||||
import { ViewFilterOperand } from 'twenty-shared/types';
|
||||
|
||||
filters: [
|
||||
{
|
||||
universalIdentifier: '...',
|
||||
fieldMetadataUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
operand: ViewFilterOperand.IS,
|
||||
value: ['ACTIVE'],
|
||||
},
|
||||
],
|
||||
```
|
||||
|
||||
### Поддерживаемые операнды по типу поля
|
||||
|
||||
| Тип поля | Поддерживаемые операнды |
|
||||
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| `TEXT`, `EMAILS`, `FULL_NAME`, `ADDRESS`, `LINKS`, `PHONES`, `RAW_JSON`, `FILES`, `ACTOR`, `ARRAY` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `ACTOR.source`, `ACTOR.workspaceMemberId` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `SELECT` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `MULTI_SELECT` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `RELATION` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `NUMBER` | `IS`, `IS_NOT`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `RATING` | `IS`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `CURRENCY`, `CURRENCY.amountMicros` | `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `BOOLEAN` | `IS` |
|
||||
| `UUID` | `IS` |
|
||||
| `TS_VECTOR` | `VECTOR_SEARCH` |
|
||||
|
||||
> Типы полей с похожими названиями могут использовать совершенно разные операнды — типичный пример: `SELECT` и `MULTI_SELECT`.
|
||||
|
||||
### Форма значения для каждого операнда
|
||||
|
||||
Поле `value` всегда содержит значение, сериализуемое в JSON, но ожидаемая форма зависит от операнда:
|
||||
|
||||
| Семейство операндов | Форма значения | Пример |
|
||||
| ------------------------------------------------------ | ------------------------------------- | ------------------------ |
|
||||
| `IS`, `IS_NOT` для `SELECT` | массив ключей опций (строки) | `['ACTIVE', 'PENDING']` |
|
||||
| `CONTAINS`, `DOES_NOT_CONTAIN` для `MULTI_SELECT` | массив ключей опций (строки) | `['TAG_A']` |
|
||||
| `IS`, `IS_NOT` для `RELATION` | массив идентификаторов записей (uuid) | `['c5a1...']` |
|
||||
| `CONTAINS`, `DOES_NOT_CONTAIN` для текстовых полей | строка | `'acme'` |
|
||||
| `IS`, `IS_NOT` для `NUMBER` | строка (значение) | `'5'` |
|
||||
| `IS` для `RATING` / `UUID` | строка (значение) | `'5'` |
|
||||
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | строка (граница) | `'10'` |
|
||||
| `IS`, `IS_BEFORE`, `IS_AFTER` для `DATE` / `DATE_TIME` | строка в формате ISO 8601 | `'2025-01-01T00:00:00Z'` |
|
||||
| `IS_EMPTY`, `IS_NOT_EMPTY` | пустая строка | `''` |
|
||||
| `IS` для `BOOLEAN` | `'true'` или `'false'` | `'true'` |
|
||||
|
||||
## Как представления отображаются в интерфейсе
|
||||
|
||||
Само по себе представление недоступно из боковой панели. Чтобы оно появилось там, свяжите его с [пунктом навигационного меню](/l/ru/developers/extend/apps/layout/navigation-menu-items) типа `VIEW`, который указывает на `universalIdentifier` представления. Это канонический шаблон: каждый пользовательский объект обычно поставляется с представлением по умолчанию и пунктом боковой панели, который его открывает.
|
||||
|
||||
@@ -38,6 +38,60 @@ export default defineView({
|
||||
* Daha gelişmiş yapılandırmalar için `filters`, `filterGroups`, `groups` ve `fieldGroups` da tanımlayabilirsiniz.
|
||||
* `position`, aynı nesne için birden fazla görünüm olduğunda sıralamayı kontrol eder.
|
||||
|
||||
## Filtreler
|
||||
|
||||
Bir görünüm, önceden uygulanmış filtrelerle gelebilir. Her filtrenin üç koordinatı vardır: filtrelenen **alan**, **işleç** (nasıl karşılaştırılacağı) ve **değer** (neyle karşılaştırılacağı). Üçünün de hizalı olması gerekir — bir alan türüne uygulanmayan bir işlecin kullanılması, senkronizasyon sırasında reddedilir.
|
||||
|
||||
```ts
|
||||
import { ViewFilterOperand } from 'twenty-shared/types';
|
||||
|
||||
filters: [
|
||||
{
|
||||
universalIdentifier: '...',
|
||||
fieldMetadataUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
operand: ViewFilterOperand.IS,
|
||||
value: ['ACTIVE'],
|
||||
},
|
||||
],
|
||||
```
|
||||
|
||||
### Alan türüne göre desteklenen işleçler
|
||||
|
||||
| Alan tipi | Desteklenen işleçler |
|
||||
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| `TEXT`, `EMAILS`, `FULL_NAME`, `ADDRESS`, `LINKS`, `PHONES`, `RAW_JSON`, `FILES`, `ACTOR`, `ARRAY` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `ACTOR.source`, `ACTOR.workspaceMemberId` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `SELECT` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `MULTI_SELECT` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `RELATION` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `NUMBER` | `IS`, `IS_NOT`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `RATING` | `IS`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `CURRENCY`, `CURRENCY.amountMicros` | `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `BOOLEAN` | `IS` |
|
||||
| `UUID` | `IS` |
|
||||
| `TS_VECTOR` | `VECTOR_SEARCH` |
|
||||
|
||||
> Benzer adlara sahip alan türleri tamamen farklı işleçler kullanabilir — buna `SELECT` ve `MULTI_SELECT` yaygın bir örnektir.
|
||||
|
||||
### Her işleç için değer biçimi
|
||||
|
||||
`value` alanı her zaman JSON olarak serileştirilebilir bir değerdir, ancak beklenen biçim işlece bağlıdır:
|
||||
|
||||
| İşleç ailesi | Değer biçimi | Örnek |
|
||||
| ------------------------------------------------------------- | ------------------------------------- | ------------------------ |
|
||||
| `SELECT` üzerinde `IS`, `IS_NOT` | seçenek anahtarları dizisi (metinler) | `['ACTIVE', 'PENDING']` |
|
||||
| `MULTI_SELECT` üzerinde `CONTAINS`, `DOES_NOT_CONTAIN` | seçenek anahtarları dizisi (metinler) | `['TAG_A']` |
|
||||
| `RELATION` üzerinde `IS`, `IS_NOT` | kayıt kimlikleri dizisi (uuid'ler) | `['c5a1...']` |
|
||||
| metin benzeri alanlar üzerinde `CONTAINS`, `DOES_NOT_CONTAIN` | metin | `'acme'` |
|
||||
| `NUMBER` üzerinde `IS`, `IS_NOT` | metin (değer) | `'5'` |
|
||||
| `RATING` / `UUID` üzerinde `IS` | metin (değer) | `'5'` |
|
||||
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | metin (sınır) | `'10'` |
|
||||
| `DATE` / `DATE_TIME` üzerinde `IS`, `IS_BEFORE`, `IS_AFTER` | ISO 8601 dizesi | `'2025-01-01T00:00:00Z'` |
|
||||
| `IS_EMPTY`, `IS_NOT_EMPTY` | boş dize | `''` |
|
||||
| `BOOLEAN` üzerinde `IS` | `'true'` veya `'false'` | `'true'` |
|
||||
|
||||
## Görünümlerin kullanıcı arayüzünde görüntülenme şekli
|
||||
|
||||
Tek başına bir görünüm, kenar çubuğundan erişilebilir değildir. Orada görünmesini sağlamak için, türü `VIEW` olan ve görünümün `universalIdentifier` değerini işaret eden bir [navigasyon menüsü öğesi](/l/tr/developers/extend/apps/layout/navigation-menu-items) ile eşleştirin. Bu, standart örüntüdür: her özel nesne genellikle varsayılan bir görünüm ve bunu açan bir kenar çubuğu girdisiyle birlikte sunulur.
|
||||
|
||||
@@ -38,6 +38,60 @@ export default defineView({
|
||||
* 你还可以声明 `filters`、`filterGroups`、`groups` 和 `fieldGroups` 以进行更高级的配置。
|
||||
* 当同一对象存在多个视图时,`position` 控制其排序。
|
||||
|
||||
## 过滤器
|
||||
|
||||
视图可以附带预先应用的过滤器。 每个过滤器有三个坐标:被筛选的**字段**、**运算符**(如何比较)以及**值**(与之比较的内容)。 这三者必须全部对齐——在同步时,使用不适用于字段类型的运算符将会被拒绝。
|
||||
|
||||
```ts
|
||||
import { ViewFilterOperand } from 'twenty-shared/types';
|
||||
|
||||
filters: [
|
||||
{
|
||||
universalIdentifier: '...',
|
||||
fieldMetadataUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
operand: ViewFilterOperand.IS,
|
||||
value: ['ACTIVE'],
|
||||
},
|
||||
],
|
||||
```
|
||||
|
||||
### 各字段类型支持的运算符
|
||||
|
||||
| 字段类型 | 受支持的运算符 |
|
||||
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| `TEXT`, `EMAILS`, `FULL_NAME`, `ADDRESS`, `LINKS`, `PHONES`, `RAW_JSON`, `FILES`, `ACTOR`, `ARRAY` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `ACTOR.source`, `ACTOR.workspaceMemberId` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `SELECT` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `MULTI_SELECT` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `RELATION` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `NUMBER` | `IS`, `IS_NOT`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `RATING` | `IS`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `CURRENCY`, `CURRENCY.amountMicros` | `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `BOOLEAN` | `IS` |
|
||||
| `UUID` | `IS` |
|
||||
| `TS_VECTOR` | `VECTOR_SEARCH` |
|
||||
|
||||
> 名称相似的字段类型可以使用完全不同的运算符 —— `SELECT` 和 `MULTI_SELECT` 就是常见情况。
|
||||
|
||||
### 每个运算符对应的值结构
|
||||
|
||||
`value` 字段始终是可序列化为 JSON 的值,但其期望的结构取决于所使用的运算符:
|
||||
|
||||
| 运算符类别 | 值结构 | 示例 |
|
||||
| ----------------------------------------------------- | -------------------- | ------------------------ |
|
||||
| `SELECT` 上的 `IS`, `IS_NOT` | 选项键(字符串)数组 | `['ACTIVE', 'PENDING']` |
|
||||
| `MULTI_SELECT` 上的 `CONTAINS`, `DOES_NOT_CONTAIN` | 选项键(字符串)数组 | `['TAG_A']` |
|
||||
| `RELATION` 上的 `IS`, `IS_NOT` | 记录 ID(uuid)数组 | `['c5a1...']` |
|
||||
| 文本类字段上的 `CONTAINS`, `DOES_NOT_CONTAIN` | 字符串 | `'acme'` |
|
||||
| `NUMBER` 上的 `IS`, `IS_NOT` | 字符串(该值) | `'5'` |
|
||||
| `RATING` / `UUID` 上的 `IS` | 字符串(该值) | `'5'` |
|
||||
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | 字符串(边界值) | `'10'` |
|
||||
| `DATE` / `DATE_TIME` 上的 `IS`, `IS_BEFORE`, `IS_AFTER` | ISO 8601 字符串 | `'2025-01-01T00:00:00Z'` |
|
||||
| `IS_EMPTY`, `IS_NOT_EMPTY` | 空字符串 | `''` |
|
||||
| `BOOLEAN` 上的 `IS` | `'true'` 或 `'false'` | `'true'` |
|
||||
|
||||
## 视图在 UI 中如何显示
|
||||
|
||||
单独一个视图无法从侧边栏访问。 要让它显示在侧边栏中,请将其与类型为 `VIEW`、指向该视图 `universalIdentifier` 的[导航菜单项](/l/zh/developers/extend/apps/layout/navigation-menu-items)配对。 这是规范用法:每个自定义对象通常都会提供一个默认视图,以及一个在侧边栏中打开该视图的条目。
|
||||
|
||||
@@ -66,10 +66,16 @@
|
||||
],
|
||||
"inputs": [
|
||||
"{projectRoot}/scripts/front-component-stories/**/*",
|
||||
"{projectRoot}/src/__stories__/example-sources/*",
|
||||
"{projectRoot}/src/__stories__/html-tag/**/*",
|
||||
"{projectRoot}/src/__stories__/host-api/**/*",
|
||||
"{projectRoot}/src/__stories__/showcase/**/*",
|
||||
"{projectRoot}/src/__stories__/shared/front-components/**/*",
|
||||
"{workspaceRoot}/packages/twenty-sdk/src/cli/utilities/build/**/*"
|
||||
],
|
||||
"outputs": ["{projectRoot}/src/__stories__/example-sources-built/*"],
|
||||
"outputs": [
|
||||
"{projectRoot}/src/__stories__/example-sources-built/*",
|
||||
"{projectRoot}/src/__stories__/example-sources-built-preact/*"
|
||||
],
|
||||
"options": {
|
||||
"command": "tsx {projectRoot}/scripts/front-component-stories/build-source-examples.ts"
|
||||
}
|
||||
|
||||
+52
-28
@@ -6,10 +6,7 @@ import { fileURLToPath } from 'node:url';
|
||||
import { getFrontComponentBuildPlugins } from 'twenty-sdk/front-component-renderer/build';
|
||||
|
||||
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const exampleSourcesDir = path.resolve(
|
||||
dirname,
|
||||
'../../src/__stories__/example-sources',
|
||||
);
|
||||
const storiesDir = path.resolve(dirname, '../../src/__stories__');
|
||||
const exampleSourcesBuiltDir = path.resolve(
|
||||
dirname,
|
||||
'../../src/__stories__/example-sources-built',
|
||||
@@ -19,6 +16,8 @@ const exampleSourcesBuiltPreactDir = path.resolve(
|
||||
'../../src/__stories__/example-sources-built-preact',
|
||||
);
|
||||
|
||||
const SOURCE_SCAN_ROOTS = ['html-tag', 'host-api', 'showcase'];
|
||||
|
||||
const rootNodeModules = path.resolve(dirname, '../../../../node_modules');
|
||||
|
||||
const twentyUiIndividualIndex = path.resolve(
|
||||
@@ -71,44 +70,69 @@ const storyAlias = {
|
||||
...twentySharedAliases,
|
||||
};
|
||||
|
||||
const STORY_COMPONENTS = [
|
||||
'static.front-component',
|
||||
'interactive.front-component',
|
||||
'lifecycle.front-component',
|
||||
'chakra-example.front-component',
|
||||
'tailwind-example.front-component',
|
||||
'emotion-example.front-component',
|
||||
'styled-components-example.front-component',
|
||||
'shadcn-example.front-component',
|
||||
'mui-example.front-component',
|
||||
'twenty-ui-example.front-component',
|
||||
'sdk-context-example.front-component',
|
||||
'form-events.front-component',
|
||||
'keyboard-events.front-component',
|
||||
'host-api-calls.front-component',
|
||||
'caret-preservation.front-component',
|
||||
'file-input.front-component',
|
||||
];
|
||||
const ENTRY_POINT_PATTERN = /\.front-component\.tsx$/;
|
||||
|
||||
const findEntryPointFiles = (directory: string): string[] => {
|
||||
const result: string[] = [];
|
||||
|
||||
if (!fs.existsSync(directory)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const dirent of fs.readdirSync(directory, { withFileTypes: true })) {
|
||||
const absolutePath = path.join(directory, dirent.name);
|
||||
|
||||
if (dirent.isDirectory()) {
|
||||
if (dirent.name === 'shared') {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.push(...findEntryPointFiles(absolutePath));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!dirent.isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ENTRY_POINT_PATTERN.test(dirent.name)) {
|
||||
result.push(absolutePath);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const resolveEntryPoints = (): Record<string, string> => {
|
||||
const files = SOURCE_SCAN_ROOTS.flatMap((root) =>
|
||||
findEntryPointFiles(path.join(storiesDir, root)),
|
||||
);
|
||||
|
||||
const entryPoints: Record<string, string> = {};
|
||||
|
||||
for (const name of STORY_COMPONENTS) {
|
||||
const filePath = path.join(exampleSourcesDir, `${name}.tsx`);
|
||||
for (const filePath of files) {
|
||||
const basename = path.basename(filePath).replace(/\.tsx$/, '');
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
if (entryPoints[basename] !== undefined) {
|
||||
throw new Error(
|
||||
`Story component source file not found: ${filePath}\n` +
|
||||
`Ensure the file exists in ${exampleSourcesDir} and the name in STORY_COMPONENTS is correct.`,
|
||||
`Duplicate front-component basename "${basename}" found at ${filePath} and ${entryPoints[basename]}`,
|
||||
);
|
||||
}
|
||||
|
||||
entryPoints[name] = filePath;
|
||||
entryPoints[basename] = filePath;
|
||||
}
|
||||
|
||||
if (Object.keys(entryPoints).length === 0) {
|
||||
throw new Error(
|
||||
`No front-component source files found under ${storiesDir} (scanned: ${SOURCE_SCAN_ROOTS.join(', ')})`,
|
||||
);
|
||||
}
|
||||
|
||||
return entryPoints;
|
||||
};
|
||||
|
||||
const STORY_COMPONENTS = Object.keys(resolveEntryPoints());
|
||||
|
||||
type BundleSizeEntry = {
|
||||
name: string;
|
||||
reactBytes: number;
|
||||
|
||||
+78
-11
@@ -107,6 +107,66 @@ const generateCommonEventsType = (
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
sourceFile.addVariableStatement({
|
||||
declarationKind: VariableDeclarationKind.Const,
|
||||
declarations: [
|
||||
{
|
||||
name: 'createSerializedEventConfig',
|
||||
initializer: (writer) => {
|
||||
writer.write(
|
||||
'(eventType: string): RemoteElementEventListenerDefinition => (',
|
||||
);
|
||||
writer.block(() => {
|
||||
writer.writeLine(
|
||||
'dispatchEvent(this: Element, eventData: SerializedEventData) {',
|
||||
);
|
||||
writer.indent(() => {
|
||||
writer.writeLine('applySerializedEventTargetProperties(');
|
||||
writer.indent(() => {
|
||||
writer.writeLine('this as unknown as Record<string, unknown>,');
|
||||
writer.writeLine('eventData,');
|
||||
});
|
||||
writer.writeLine(');');
|
||||
writer.blankLine();
|
||||
writer.writeLine('return new CustomEvent(eventType, {');
|
||||
writer.indent(() => {
|
||||
writer.writeLine('detail: eventData,');
|
||||
});
|
||||
writer.writeLine('}) as RemoteEvent<SerializedEventData>;');
|
||||
});
|
||||
writer.writeLine('},');
|
||||
});
|
||||
writer.write(')');
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
sourceFile.addVariableStatement({
|
||||
declarationKind: VariableDeclarationKind.Const,
|
||||
declarations: [
|
||||
{
|
||||
name: 'HTML_COMMON_EVENTS_CONFIG',
|
||||
initializer: (writer) => {
|
||||
writer.writeLine('Object.fromEntries(');
|
||||
writer.indent(() => {
|
||||
writer.writeLine(
|
||||
`${TYPE_NAMES.COMMON_EVENTS_ARRAY}.map((eventType) => [`,
|
||||
);
|
||||
writer.indent(() => {
|
||||
writer.writeLine('eventType,');
|
||||
writer.writeLine('createSerializedEventConfig(eventType),');
|
||||
});
|
||||
writer.writeLine(']),');
|
||||
});
|
||||
writer.write(
|
||||
`) as RemoteElementEventListenersDefinition<${TYPE_NAMES.COMMON_EVENTS}>`,
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const generateCommonPropertiesConfig = (
|
||||
@@ -246,17 +306,19 @@ const generateElementDefinition = (
|
||||
}
|
||||
}
|
||||
if (hasEvents) {
|
||||
const formattedCustomEvents = customEvents
|
||||
.map((event) => `'${event}'`)
|
||||
.join(', ');
|
||||
writer.write('events: ');
|
||||
writer.block(() => {
|
||||
if (hasCommonHtmlEvents) {
|
||||
writer.writeLine('...HTML_COMMON_EVENTS_CONFIG,');
|
||||
}
|
||||
|
||||
writer.write(
|
||||
hasCommonHtmlEvents && customEvents.length > 0
|
||||
? `events: [...${TYPE_NAMES.COMMON_EVENTS_ARRAY}, ${formattedCustomEvents}],`
|
||||
: hasCommonHtmlEvents
|
||||
? `events: [...${TYPE_NAMES.COMMON_EVENTS_ARRAY}],`
|
||||
: `events: [${formattedCustomEvents}],`,
|
||||
);
|
||||
for (const event of customEvents) {
|
||||
writer.writeLine(
|
||||
`'${event}': createSerializedEventConfig('${event}'),`,
|
||||
);
|
||||
}
|
||||
});
|
||||
writer.write(',');
|
||||
writer.newLine();
|
||||
}
|
||||
});
|
||||
@@ -332,12 +394,17 @@ export const generateRemoteElements = (
|
||||
INTERNAL_ELEMENT_CLASSES.ROOT,
|
||||
INTERNAL_ELEMENT_CLASSES.FRAGMENT,
|
||||
{ name: 'RemoteEvent', isTypeOnly: true },
|
||||
{ name: 'RemoteElementEventListenerDefinition', isTypeOnly: true },
|
||||
{ name: 'RemoteElementEventListenersDefinition', isTypeOnly: true },
|
||||
],
|
||||
});
|
||||
|
||||
sourceFile.addImportDeclaration({
|
||||
moduleSpecifier: '@/constants/SerializedEventData',
|
||||
namedImports: [{ name: 'SerializedEventData', isTypeOnly: true }],
|
||||
namedImports: [
|
||||
'applySerializedEventTargetProperties',
|
||||
{ name: 'SerializedEventData', isTypeOnly: true },
|
||||
],
|
||||
});
|
||||
|
||||
const commonPropertyNames = new Set(Object.keys(commonProperties));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
|
||||
import bundleSizes from './example-sources-built/bundle-sizes.json';
|
||||
import bundleSizes from '@/__stories__/example-sources-built/bundle-sizes.json';
|
||||
|
||||
type BundleSizeEntry = {
|
||||
name: string;
|
||||
|
||||
@@ -1,517 +0,0 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { expect, fn, userEvent, waitFor, within } from 'storybook/test';
|
||||
|
||||
import { FrontComponentRenderer } from '../host/components/FrontComponentRenderer';
|
||||
|
||||
import { getBuiltStoryComponentPathForRender } from './utils/getBuiltStoryComponentPathForRender';
|
||||
|
||||
const errorHandler = fn();
|
||||
|
||||
const createHostApiMocks = () => ({
|
||||
navigate: fn().mockResolvedValue(undefined),
|
||||
enqueueSnackbar: fn().mockResolvedValue(undefined),
|
||||
openSidePanelPage: fn().mockResolvedValue(undefined),
|
||||
closeSidePanel: fn().mockResolvedValue(undefined),
|
||||
unmountFrontComponent: fn().mockResolvedValue(undefined),
|
||||
updateProgress: fn().mockResolvedValue(undefined),
|
||||
requestAccessTokenRefresh: fn().mockResolvedValue('refreshed-token'),
|
||||
openCommandConfirmationModal: fn().mockResolvedValue(undefined),
|
||||
});
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/EventForwarding',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
args: {
|
||||
onError: errorHandler,
|
||||
applicationAccessToken: 'fake-token',
|
||||
executionContext: {
|
||||
frontComponentId: 'storybook-test',
|
||||
userId: null,
|
||||
recordId: null,
|
||||
selectedRecordIds: [],
|
||||
},
|
||||
colorScheme: 'light',
|
||||
frontComponentHostCommunicationApi: createHostApiMocks(),
|
||||
},
|
||||
beforeEach: () => {
|
||||
errorHandler.mockClear();
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof FrontComponentRenderer>;
|
||||
|
||||
const MOUNT_TIMEOUT = 30000;
|
||||
const INTERACTION_TIMEOUT = 5000;
|
||||
const HOST_API_TIMEOUT = 10000;
|
||||
|
||||
const createComponentStory = (
|
||||
name: string,
|
||||
options?: { play?: Story['play'] },
|
||||
): Story => ({
|
||||
args: {
|
||||
componentUrl: getBuiltStoryComponentPathForRender(
|
||||
`${name}.front-component`,
|
||||
),
|
||||
},
|
||||
...(options?.play ? { play: options.play } : {}),
|
||||
});
|
||||
|
||||
const createHostApiStory = (play: Story['play']): Story => ({
|
||||
...createComponentStory('host-api-calls'),
|
||||
args: {
|
||||
...createComponentStory('host-api-calls').args,
|
||||
frontComponentHostCommunicationApi: createHostApiMocks(),
|
||||
},
|
||||
play,
|
||||
});
|
||||
|
||||
export const FormTextInput: Story = createComponentStory('form-events', {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByTestId(
|
||||
'form-events-component',
|
||||
{},
|
||||
{ timeout: MOUNT_TIMEOUT },
|
||||
);
|
||||
|
||||
const textInput = await canvas.findByTestId('text-input');
|
||||
await userEvent.type(textInput, 'hello');
|
||||
|
||||
expect(
|
||||
await canvas.findByText('hello', {}, { timeout: INTERACTION_TIMEOUT }),
|
||||
).toBeVisible();
|
||||
},
|
||||
});
|
||||
|
||||
export const FormCheckbox: Story = createComponentStory('form-events', {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByTestId(
|
||||
'form-events-component',
|
||||
{},
|
||||
{ timeout: MOUNT_TIMEOUT },
|
||||
);
|
||||
|
||||
const checkbox = await canvas.findByTestId('checkbox-input');
|
||||
await userEvent.click(checkbox);
|
||||
|
||||
expect(
|
||||
await canvas.findByText('true', {}, { timeout: INTERACTION_TIMEOUT }),
|
||||
).toBeVisible();
|
||||
},
|
||||
});
|
||||
|
||||
export const FormFocusAndBlur: Story = createComponentStory('form-events', {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByTestId(
|
||||
'form-events-component',
|
||||
{},
|
||||
{ timeout: MOUNT_TIMEOUT },
|
||||
);
|
||||
|
||||
const textInput = await canvas.findByTestId('text-input');
|
||||
await userEvent.click(textInput);
|
||||
|
||||
expect(
|
||||
await canvas.findByText('focused', {}, { timeout: INTERACTION_TIMEOUT }),
|
||||
).toBeVisible();
|
||||
|
||||
await userEvent.click(await canvas.findByTestId('form-events-component'));
|
||||
|
||||
expect(
|
||||
await canvas.findByText('blurred', {}, { timeout: INTERACTION_TIMEOUT }),
|
||||
).toBeVisible();
|
||||
},
|
||||
});
|
||||
|
||||
export const FormSubmission: Story = createComponentStory('form-events', {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByTestId(
|
||||
'form-events-component',
|
||||
{},
|
||||
{ timeout: MOUNT_TIMEOUT },
|
||||
);
|
||||
|
||||
const textInput = await canvas.findByTestId('text-input');
|
||||
await userEvent.type(textInput, 'hello');
|
||||
|
||||
const checkbox = await canvas.findByTestId('checkbox-input');
|
||||
await userEvent.click(checkbox);
|
||||
|
||||
const submitButton = await canvas.findByTestId('submit-button');
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
expect(
|
||||
await canvas.findByText(
|
||||
'{"text":"hello","checkbox":true}',
|
||||
{},
|
||||
{ timeout: INTERACTION_TIMEOUT },
|
||||
),
|
||||
).toBeVisible();
|
||||
},
|
||||
});
|
||||
|
||||
export const KeyboardBasicInput: Story = createComponentStory(
|
||||
'keyboard-events',
|
||||
{
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByTestId(
|
||||
'keyboard-events-component',
|
||||
{},
|
||||
{ timeout: MOUNT_TIMEOUT },
|
||||
);
|
||||
|
||||
const input = await canvas.findByTestId('keyboard-input');
|
||||
await userEvent.click(input);
|
||||
|
||||
await userEvent.keyboard('a');
|
||||
|
||||
expect(
|
||||
await canvas.findByText('a', {}, { timeout: INTERACTION_TIMEOUT }),
|
||||
).toBeVisible();
|
||||
|
||||
expect(
|
||||
await canvas.findByText('KeyA', {}, { timeout: INTERACTION_TIMEOUT }),
|
||||
).toBeVisible();
|
||||
|
||||
expect(
|
||||
await canvas.findByText(
|
||||
/^[1-9]\d*$/,
|
||||
{},
|
||||
{ timeout: INTERACTION_TIMEOUT },
|
||||
),
|
||||
).toBeVisible();
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export const KeyboardModifiers: Story = createComponentStory(
|
||||
'keyboard-events',
|
||||
{
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByTestId(
|
||||
'keyboard-events-component',
|
||||
{},
|
||||
{ timeout: MOUNT_TIMEOUT },
|
||||
);
|
||||
|
||||
const input = await canvas.findByTestId('keyboard-input');
|
||||
await userEvent.click(input);
|
||||
|
||||
await userEvent.keyboard('{Shift>}b{/Shift}');
|
||||
|
||||
expect(
|
||||
await canvas.findByText('shift', {}, { timeout: INTERACTION_TIMEOUT }),
|
||||
).toBeVisible();
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export const HostApiNavigate: Story = createHostApiStory(
|
||||
async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const api = args.frontComponentHostCommunicationApi!;
|
||||
|
||||
await canvas.findByTestId(
|
||||
'host-api-calls-component',
|
||||
{},
|
||||
{ timeout: MOUNT_TIMEOUT },
|
||||
);
|
||||
|
||||
const navigateBtn = await canvas.findByTestId('btn-navigate');
|
||||
await userEvent.click(navigateBtn);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(api.navigate).toHaveBeenCalled();
|
||||
},
|
||||
{ timeout: HOST_API_TIMEOUT },
|
||||
);
|
||||
|
||||
expect(
|
||||
await canvas.findByText(
|
||||
'navigate:success',
|
||||
{},
|
||||
{ timeout: INTERACTION_TIMEOUT },
|
||||
),
|
||||
).toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
export const HostApiSnackbar: Story = createHostApiStory(
|
||||
async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const api = args.frontComponentHostCommunicationApi!;
|
||||
|
||||
await canvas.findByTestId(
|
||||
'host-api-calls-component',
|
||||
{},
|
||||
{ timeout: MOUNT_TIMEOUT },
|
||||
);
|
||||
|
||||
const snackbarBtn = await canvas.findByTestId('btn-snackbar');
|
||||
await userEvent.click(snackbarBtn);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(api.enqueueSnackbar).toHaveBeenCalledWith({
|
||||
message: 'Test notification',
|
||||
variant: 'success',
|
||||
});
|
||||
},
|
||||
{ timeout: HOST_API_TIMEOUT },
|
||||
);
|
||||
|
||||
expect(
|
||||
await canvas.findByText(
|
||||
'snackbar:success',
|
||||
{},
|
||||
{ timeout: INTERACTION_TIMEOUT },
|
||||
),
|
||||
).toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
export const HostApiProgress: Story = createHostApiStory(
|
||||
async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const api = args.frontComponentHostCommunicationApi!;
|
||||
|
||||
await canvas.findByTestId(
|
||||
'host-api-calls-component',
|
||||
{},
|
||||
{ timeout: MOUNT_TIMEOUT },
|
||||
);
|
||||
|
||||
const progressBtn = await canvas.findByTestId('btn-progress');
|
||||
await userEvent.click(progressBtn);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(api.updateProgress).toHaveBeenCalledWith(50);
|
||||
},
|
||||
{ timeout: HOST_API_TIMEOUT },
|
||||
);
|
||||
|
||||
expect(
|
||||
await canvas.findByText(
|
||||
'progress:success',
|
||||
{},
|
||||
{ timeout: INTERACTION_TIMEOUT },
|
||||
),
|
||||
).toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
const TYPING_TIMEOUT = 10000;
|
||||
|
||||
const expectCaretAt = async (
|
||||
element: HTMLInputElement | HTMLTextAreaElement,
|
||||
position: number,
|
||||
): Promise<void> => {
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(element.selectionStart).toBe(position);
|
||||
expect(element.selectionEnd).toBe(position);
|
||||
},
|
||||
{ timeout: TYPING_TIMEOUT },
|
||||
);
|
||||
};
|
||||
|
||||
export const InputCaretPreservedMidString: Story = createComponentStory(
|
||||
'caret-preservation',
|
||||
{
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByTestId(
|
||||
'caret-preservation-component',
|
||||
{},
|
||||
{ timeout: MOUNT_TIMEOUT },
|
||||
);
|
||||
|
||||
const input = (await canvas.findByTestId(
|
||||
'caret-text-input',
|
||||
)) as HTMLInputElement;
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(input.value).toBe('Hello world');
|
||||
},
|
||||
{ timeout: INTERACTION_TIMEOUT },
|
||||
);
|
||||
|
||||
input.focus();
|
||||
input.setSelectionRange(4, 4);
|
||||
|
||||
await userEvent.keyboard('X');
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(input.value).toBe('HellXo world');
|
||||
expect(canvas.getByTestId('caret-text-value').textContent).toBe(
|
||||
'HellXo world',
|
||||
);
|
||||
},
|
||||
{ timeout: TYPING_TIMEOUT },
|
||||
);
|
||||
|
||||
await expectCaretAt(input, 5);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export const TextareaCaretPreservedMidString: Story = createComponentStory(
|
||||
'caret-preservation',
|
||||
{
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByTestId(
|
||||
'caret-preservation-component',
|
||||
{},
|
||||
{ timeout: MOUNT_TIMEOUT },
|
||||
);
|
||||
|
||||
const textarea = (await canvas.findByTestId(
|
||||
'caret-textarea-input',
|
||||
)) as HTMLTextAreaElement;
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(textarea.value).toBe('Hello world');
|
||||
},
|
||||
{ timeout: INTERACTION_TIMEOUT },
|
||||
);
|
||||
|
||||
textarea.focus();
|
||||
textarea.setSelectionRange(4, 4);
|
||||
|
||||
await userEvent.keyboard('X');
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(textarea.value).toBe('HellXo world');
|
||||
expect(canvas.getByTestId('caret-textarea-value').textContent).toBe(
|
||||
'HellXo world',
|
||||
);
|
||||
},
|
||||
{ timeout: TYPING_TIMEOUT },
|
||||
);
|
||||
|
||||
await expectCaretAt(textarea, 5);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export const FileInputSingle: Story = createComponentStory('file-input', {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByTestId(
|
||||
'file-input-component',
|
||||
{},
|
||||
{ timeout: MOUNT_TIMEOUT },
|
||||
);
|
||||
|
||||
const input = (await canvas.findByTestId(
|
||||
'single-file-input',
|
||||
)) as HTMLInputElement;
|
||||
|
||||
const file = new File(['hello world'], 'hello.txt', {
|
||||
type: 'text/plain',
|
||||
lastModified: 1700000000000,
|
||||
});
|
||||
|
||||
await userEvent.upload(input, file);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(canvas.getByTestId('single-file-count').textContent).toBe('1');
|
||||
expect(canvas.getByTestId('single-file-name').textContent).toContain(
|
||||
'hello.txt',
|
||||
);
|
||||
expect(canvas.getByTestId('single-file-name').textContent).toContain(
|
||||
'text/plain',
|
||||
);
|
||||
},
|
||||
{ timeout: INTERACTION_TIMEOUT },
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const FileInputMultiple: Story = createComponentStory('file-input', {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByTestId(
|
||||
'file-input-component',
|
||||
{},
|
||||
{ timeout: MOUNT_TIMEOUT },
|
||||
);
|
||||
|
||||
const input = (await canvas.findByTestId(
|
||||
'multi-file-input',
|
||||
)) as HTMLInputElement;
|
||||
|
||||
const first = new File(['a'], 'one.png', { type: 'image/png' });
|
||||
const second = new File(['bb'], 'two.png', { type: 'image/png' });
|
||||
|
||||
await userEvent.upload(input, [first, second]);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(canvas.getByTestId('multi-file-count').textContent).toBe('2');
|
||||
const list = canvas.getByTestId('multi-file-list');
|
||||
expect(list.textContent).toContain('one.png');
|
||||
expect(list.textContent).toContain('two.png');
|
||||
},
|
||||
{ timeout: INTERACTION_TIMEOUT },
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const HostApiClosePanel: Story = createHostApiStory(
|
||||
async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const api = args.frontComponentHostCommunicationApi!;
|
||||
|
||||
await canvas.findByTestId(
|
||||
'host-api-calls-component',
|
||||
{},
|
||||
{ timeout: MOUNT_TIMEOUT },
|
||||
);
|
||||
|
||||
const closePanelBtn = await canvas.findByTestId('btn-close-panel');
|
||||
await userEvent.click(closePanelBtn);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(api.closeSidePanel).toHaveBeenCalled();
|
||||
},
|
||||
{ timeout: HOST_API_TIMEOUT },
|
||||
);
|
||||
|
||||
expect(
|
||||
await canvas.findByText(
|
||||
'closePanel:success',
|
||||
{},
|
||||
{ timeout: INTERACTION_TIMEOUT },
|
||||
),
|
||||
).toBeVisible();
|
||||
},
|
||||
);
|
||||
+2
-3
@@ -1,9 +1,8 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { expect, fn, userEvent, waitFor, within } from 'storybook/test';
|
||||
|
||||
import { FrontComponentRenderer } from '../host/components/FrontComponentRenderer';
|
||||
|
||||
import { getBuiltStoryComponentPathForRender } from './utils/getBuiltStoryComponentPathForRender';
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import { getBuiltStoryComponentPathForRender } from '@/__stories__/utils/getBuiltStoryComponentPathForRender';
|
||||
|
||||
const errorHandler = fn();
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { expect, fn, userEvent, within } from 'storybook/test';
|
||||
|
||||
import { FrontComponentRenderer } from '../host/components/FrontComponentRenderer';
|
||||
|
||||
import { getBuiltStoryComponentPathForRender } from './utils/getBuiltStoryComponentPathForRender';
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import { getBuiltStoryComponentPathForRender } from '@/__stories__/utils/getBuiltStoryComponentPathForRender';
|
||||
|
||||
const errorHandler = fn();
|
||||
|
||||
|
||||
-98
@@ -1,98 +0,0 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { type ChangeEvent, useState } from 'react';
|
||||
|
||||
const CARD_STYLE = {
|
||||
padding: 24,
|
||||
backgroundColor: '#eff6ff',
|
||||
border: '2px solid #3b82f6',
|
||||
borderRadius: 12,
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
gap: 16,
|
||||
maxWidth: 400,
|
||||
};
|
||||
|
||||
const HEADING_STYLE = {
|
||||
color: '#1e3a8a',
|
||||
fontWeight: 700,
|
||||
fontSize: 18,
|
||||
margin: 0,
|
||||
};
|
||||
|
||||
const LABEL_STYLE = {
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: '#374151',
|
||||
};
|
||||
|
||||
const HINT_STYLE = {
|
||||
fontSize: 13,
|
||||
color: '#6b7280',
|
||||
fontFamily: 'monospace',
|
||||
};
|
||||
|
||||
const INPUT_STYLE = {
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: 6,
|
||||
fontSize: 14,
|
||||
fontFamily: 'monospace',
|
||||
};
|
||||
|
||||
const INITIAL_VALUE = 'Hello world';
|
||||
|
||||
const CaretPreservationComponent = () => {
|
||||
const [text, setText] = useState(INITIAL_VALUE);
|
||||
const [textareaText, setTextareaText] = useState(INITIAL_VALUE);
|
||||
|
||||
return (
|
||||
<div data-testid="caret-preservation-component" style={CARD_STYLE}>
|
||||
<h2 style={HEADING_STYLE}>Caret Preservation</h2>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<label style={LABEL_STYLE}>Text input (pre-filled)</label>
|
||||
<input
|
||||
data-testid="caret-text-input"
|
||||
type="text"
|
||||
value={text}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
const detail = (event as unknown as { detail: { value?: string } })
|
||||
.detail;
|
||||
setText(detail?.value ?? '');
|
||||
}}
|
||||
style={INPUT_STYLE}
|
||||
/>
|
||||
<span data-testid="caret-text-value" style={HINT_STYLE}>
|
||||
{text}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<label style={LABEL_STYLE}>Textarea (pre-filled)</label>
|
||||
<textarea
|
||||
data-testid="caret-textarea-input"
|
||||
value={textareaText}
|
||||
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const detail = (event as unknown as { detail: { value?: string } })
|
||||
.detail;
|
||||
setTextareaText(detail?.value ?? '');
|
||||
}}
|
||||
style={INPUT_STYLE}
|
||||
rows={3}
|
||||
/>
|
||||
<span data-testid="caret-textarea-value" style={HINT_STYLE}>
|
||||
{textareaText}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'test-caret-00000000-0000-0000-0000-000000000021',
|
||||
name: 'caret-preservation-component',
|
||||
description:
|
||||
'Component verifying caret position is preserved during mid-string editing of <input>/<textarea>',
|
||||
component: CaretPreservationComponent,
|
||||
});
|
||||
-120
@@ -1,120 +0,0 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { type ChangeEvent, useState } from 'react';
|
||||
|
||||
type SelectedFile = {
|
||||
name: string;
|
||||
size: number;
|
||||
type: string;
|
||||
lastModified: number;
|
||||
};
|
||||
|
||||
const CARD_STYLE = {
|
||||
padding: 24,
|
||||
backgroundColor: '#fef3c7',
|
||||
border: '2px solid #f59e0b',
|
||||
borderRadius: 12,
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
gap: 16,
|
||||
maxWidth: 480,
|
||||
};
|
||||
|
||||
const HEADING_STYLE = {
|
||||
color: '#92400e',
|
||||
fontWeight: 700,
|
||||
fontSize: 18,
|
||||
margin: 0,
|
||||
};
|
||||
|
||||
const LABEL_STYLE = {
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: '#374151',
|
||||
};
|
||||
|
||||
const HINT_STYLE = {
|
||||
fontSize: 13,
|
||||
color: '#6b7280',
|
||||
fontFamily: 'monospace',
|
||||
};
|
||||
|
||||
const LIST_STYLE = {
|
||||
margin: 0,
|
||||
paddingLeft: 16,
|
||||
fontSize: 13,
|
||||
fontFamily: 'monospace',
|
||||
color: '#374151',
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
gap: 4,
|
||||
};
|
||||
|
||||
const FileInputComponent = () => {
|
||||
const [singleFile, setSingleFile] = useState<SelectedFile | null>(null);
|
||||
const [multiFiles, setMultiFiles] = useState<SelectedFile[]>([]);
|
||||
|
||||
return (
|
||||
<div data-testid="file-input-component" style={CARD_STYLE}>
|
||||
<h2 style={HEADING_STYLE}>File Input</h2>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<label style={LABEL_STYLE}>Single file</label>
|
||||
<input
|
||||
data-testid="single-file-input"
|
||||
type="file"
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
const detail = (
|
||||
event as unknown as { detail: { files?: SelectedFile[] } }
|
||||
).detail;
|
||||
setSingleFile(detail?.files?.[0] ?? null);
|
||||
}}
|
||||
/>
|
||||
<span data-testid="single-file-count" style={HINT_STYLE}>
|
||||
{singleFile === null ? 'none' : '1'}
|
||||
</span>
|
||||
{singleFile !== null && (
|
||||
<span data-testid="single-file-name" style={HINT_STYLE}>
|
||||
{singleFile.name} ({singleFile.size}B, {singleFile.type})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<label style={LABEL_STYLE}>Multiple files (image/*)</label>
|
||||
<input
|
||||
data-testid="multi-file-input"
|
||||
type="file"
|
||||
multiple
|
||||
accept="image/*"
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
const detail = (
|
||||
event as unknown as { detail: { files?: SelectedFile[] } }
|
||||
).detail;
|
||||
setMultiFiles(detail?.files ?? []);
|
||||
}}
|
||||
/>
|
||||
<span data-testid="multi-file-count" style={HINT_STYLE}>
|
||||
{multiFiles.length}
|
||||
</span>
|
||||
{multiFiles.length > 0 && (
|
||||
<ul data-testid="multi-file-list" style={LIST_STYLE}>
|
||||
{multiFiles.map((file) => (
|
||||
<li key={`${file.name}-${file.lastModified}`}>
|
||||
{file.name} ({file.size}B)
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'test-file-00000000-0000-0000-0000-000000000022',
|
||||
name: 'file-input-component',
|
||||
description:
|
||||
'Component verifying file input metadata is forwarded from host to worker',
|
||||
component: FileInputComponent,
|
||||
});
|
||||
-141
@@ -1,141 +0,0 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { type ChangeEvent, useState } from 'react';
|
||||
|
||||
const CARD_STYLE = {
|
||||
padding: 24,
|
||||
backgroundColor: '#f0fdf4',
|
||||
border: '2px solid #22c55e',
|
||||
borderRadius: 12,
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
gap: 16,
|
||||
maxWidth: 400,
|
||||
};
|
||||
|
||||
const HEADING_STYLE = {
|
||||
color: '#166534',
|
||||
fontWeight: 700,
|
||||
fontSize: 18,
|
||||
margin: 0,
|
||||
};
|
||||
|
||||
const LABEL_STYLE = {
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: '#374151',
|
||||
};
|
||||
|
||||
const HINT_STYLE = {
|
||||
fontSize: 13,
|
||||
color: '#6b7280',
|
||||
};
|
||||
|
||||
const INPUT_STYLE = {
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: 6,
|
||||
fontSize: 14,
|
||||
};
|
||||
|
||||
const SUBMIT_BUTTON_STYLE = {
|
||||
padding: '10px 20px',
|
||||
backgroundColor: '#16a34a',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: 6,
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
};
|
||||
|
||||
const FormEventsComponent = () => {
|
||||
const [textValue, setTextValue] = useState('');
|
||||
const [checkboxValue, setCheckboxValue] = useState(false);
|
||||
const [focusState, setFocusState] = useState('none');
|
||||
const [submittedData, setSubmittedData] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<div data-testid="form-events-component" style={CARD_STYLE}>
|
||||
<h2 style={HEADING_STYLE}>Form Events</h2>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<label style={LABEL_STYLE}>Text Input</label>
|
||||
<input
|
||||
data-testid="text-input"
|
||||
type="text"
|
||||
placeholder="Type here..."
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
const detail = (event as unknown as { detail: { value?: string } })
|
||||
.detail;
|
||||
setTextValue(detail?.value ?? '');
|
||||
}}
|
||||
onFocus={() => setFocusState('focused')}
|
||||
onBlur={() => setFocusState('blurred')}
|
||||
style={INPUT_STYLE}
|
||||
/>
|
||||
<span data-testid="text-value" style={HINT_STYLE}>
|
||||
{textValue}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<input
|
||||
data-testid="checkbox-input"
|
||||
type="checkbox"
|
||||
checked={checkboxValue}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
const detail = (
|
||||
event as unknown as { detail: { checked?: boolean } }
|
||||
).detail;
|
||||
setCheckboxValue(detail?.checked ?? false);
|
||||
}}
|
||||
/>
|
||||
<label style={LABEL_STYLE}>Check me</label>
|
||||
<span data-testid="checkbox-value" style={HINT_STYLE}>
|
||||
{String(checkboxValue)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span data-testid="focus-state" style={HINT_STYLE}>
|
||||
{focusState}
|
||||
</span>
|
||||
|
||||
<button
|
||||
data-testid="submit-button"
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setSubmittedData(
|
||||
JSON.stringify({ text: textValue, checkbox: checkboxValue }),
|
||||
)
|
||||
}
|
||||
style={SUBMIT_BUTTON_STYLE}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
|
||||
{submittedData !== null && (
|
||||
<pre
|
||||
data-testid="submitted-data"
|
||||
style={{
|
||||
fontSize: 13,
|
||||
background: '#dcfce7',
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
margin: 0,
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
{submittedData}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'test-form-00000000-0000-0000-0000-000000000020',
|
||||
name: 'form-events-component',
|
||||
description:
|
||||
'Component testing form input events (onChange, onFocus, onBlur, submit)',
|
||||
component: FormEventsComponent,
|
||||
});
|
||||
-155
@@ -1,155 +0,0 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
AppPath,
|
||||
closeSidePanel,
|
||||
enqueueSnackbar,
|
||||
navigate,
|
||||
openSidePanelPage,
|
||||
SidePanelPages,
|
||||
unmountFrontComponent,
|
||||
updateProgress,
|
||||
} from 'twenty-sdk/front-component';
|
||||
import { useState } from 'react';
|
||||
|
||||
const CARD_STYLE = {
|
||||
padding: 24,
|
||||
backgroundColor: '#faf5ff',
|
||||
border: '2px solid #a78bfa',
|
||||
borderRadius: 12,
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
gap: 10,
|
||||
maxWidth: 400,
|
||||
};
|
||||
|
||||
const HEADING_STYLE = {
|
||||
color: '#5b21b6',
|
||||
fontWeight: 700,
|
||||
fontSize: 18,
|
||||
margin: 0,
|
||||
};
|
||||
|
||||
const BUTTON_STYLE = {
|
||||
padding: '8px 16px',
|
||||
backgroundColor: '#7c3aed',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: 6,
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
fontSize: 13,
|
||||
};
|
||||
|
||||
const STATUS_STYLE = {
|
||||
fontSize: 13,
|
||||
color: '#6b7280',
|
||||
fontFamily: 'monospace',
|
||||
};
|
||||
|
||||
const HostApiCallsComponent = () => {
|
||||
const [apiStatus, setApiStatus] = useState('idle');
|
||||
|
||||
const callApi = async (name: string, apiFunction: () => Promise<void>) => {
|
||||
try {
|
||||
await apiFunction();
|
||||
setApiStatus(`${name}:success`);
|
||||
} catch (error) {
|
||||
setApiStatus(
|
||||
`${name}:error:${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-testid="host-api-calls-component" style={CARD_STYLE}>
|
||||
<h2 style={HEADING_STYLE}>Host API Calls</h2>
|
||||
|
||||
<button
|
||||
data-testid="btn-navigate"
|
||||
type="button"
|
||||
onClick={() =>
|
||||
callApi('navigate', () =>
|
||||
navigate(AppPath.RecordIndexPage, {
|
||||
objectNamePlural: 'companies',
|
||||
}),
|
||||
)
|
||||
}
|
||||
style={BUTTON_STYLE}
|
||||
>
|
||||
Navigate
|
||||
</button>
|
||||
|
||||
<button
|
||||
data-testid="btn-snackbar"
|
||||
type="button"
|
||||
onClick={() =>
|
||||
callApi('snackbar', () =>
|
||||
enqueueSnackbar({
|
||||
message: 'Test notification',
|
||||
variant: 'success',
|
||||
}),
|
||||
)
|
||||
}
|
||||
style={BUTTON_STYLE}
|
||||
>
|
||||
Snackbar
|
||||
</button>
|
||||
|
||||
<button
|
||||
data-testid="btn-side-panel"
|
||||
type="button"
|
||||
onClick={() =>
|
||||
callApi('sidePanel', () =>
|
||||
openSidePanelPage({
|
||||
page: SidePanelPages.ViewRecord,
|
||||
pageTitle: 'Test Record',
|
||||
}),
|
||||
)
|
||||
}
|
||||
style={BUTTON_STYLE}
|
||||
>
|
||||
Open Side Panel
|
||||
</button>
|
||||
|
||||
<button
|
||||
data-testid="btn-close-panel"
|
||||
type="button"
|
||||
onClick={() => callApi('closePanel', () => closeSidePanel())}
|
||||
style={BUTTON_STYLE}
|
||||
>
|
||||
Close Side Panel
|
||||
</button>
|
||||
|
||||
<button
|
||||
data-testid="btn-unmount"
|
||||
type="button"
|
||||
onClick={() => callApi('unmount', () => unmountFrontComponent())}
|
||||
style={BUTTON_STYLE}
|
||||
>
|
||||
Unmount
|
||||
</button>
|
||||
|
||||
<button
|
||||
data-testid="btn-progress"
|
||||
type="button"
|
||||
onClick={() => callApi('progress', () => updateProgress(50))}
|
||||
style={BUTTON_STYLE}
|
||||
>
|
||||
Update Progress (50)
|
||||
</button>
|
||||
|
||||
<span data-testid="api-status" style={STATUS_STYLE}>
|
||||
{apiStatus}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'test-hapi-00000000-0000-0000-0000-000000000022',
|
||||
name: 'host-api-calls-component',
|
||||
description:
|
||||
'Component testing host communication API calls (navigate, snackbar, side panel, etc.)',
|
||||
component: HostApiCallsComponent,
|
||||
});
|
||||
-125
@@ -1,125 +0,0 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { type KeyboardEvent, useState } from 'react';
|
||||
|
||||
type RemoteKeyboardEventDetail = {
|
||||
key?: string;
|
||||
code?: string;
|
||||
shiftKey?: boolean;
|
||||
ctrlKey?: boolean;
|
||||
metaKey?: boolean;
|
||||
altKey?: boolean;
|
||||
};
|
||||
|
||||
const KeyboardEventsComponent = () => {
|
||||
const [lastKey, setLastKey] = useState('');
|
||||
const [lastCode, setLastCode] = useState('');
|
||||
const [modifiers, setModifiers] = useState('');
|
||||
const [keyCount, setKeyCount] = useState(0);
|
||||
|
||||
// remote-dom serializes keyboard events into CustomEvent.detail
|
||||
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
const data = (event as unknown as { detail: RemoteKeyboardEventDetail })
|
||||
.detail;
|
||||
|
||||
setLastKey(data.key ?? '');
|
||||
setLastCode(data.code ?? '');
|
||||
setKeyCount((previousCount) => previousCount + 1);
|
||||
|
||||
const activeModifiers: string[] = [];
|
||||
if (data.shiftKey) activeModifiers.push('shift');
|
||||
if (data.ctrlKey) activeModifiers.push('ctrl');
|
||||
if (data.metaKey) activeModifiers.push('meta');
|
||||
if (data.altKey) activeModifiers.push('alt');
|
||||
setModifiers(activeModifiers.join(','));
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="keyboard-events-component"
|
||||
style={{
|
||||
padding: 24,
|
||||
backgroundColor: '#fefce8',
|
||||
border: '2px solid #eab308',
|
||||
borderRadius: 12,
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 12,
|
||||
maxWidth: 400,
|
||||
}}
|
||||
>
|
||||
<h2
|
||||
style={{
|
||||
color: '#854d0e',
|
||||
fontWeight: 700,
|
||||
fontSize: 18,
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
Keyboard Events
|
||||
</h2>
|
||||
|
||||
<input
|
||||
data-testid="keyboard-input"
|
||||
type="text"
|
||||
placeholder="Press keys here..."
|
||||
onKeyDown={handleKeyDown}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: 6,
|
||||
fontSize: 14,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<span style={{ fontSize: 13, color: '#6b7280' }}>
|
||||
Key:{' '}
|
||||
<span
|
||||
data-testid="last-key"
|
||||
style={{ fontWeight: 700, color: '#854d0e' }}
|
||||
>
|
||||
{lastKey}
|
||||
</span>
|
||||
</span>
|
||||
<span style={{ fontSize: 13, color: '#6b7280' }}>
|
||||
Code:{' '}
|
||||
<span
|
||||
data-testid="last-code"
|
||||
style={{ fontWeight: 700, color: '#854d0e' }}
|
||||
>
|
||||
{lastCode}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<span style={{ fontSize: 13, color: '#6b7280' }}>
|
||||
Modifiers:{' '}
|
||||
<span
|
||||
data-testid="modifiers"
|
||||
style={{ fontWeight: 700, color: '#854d0e' }}
|
||||
>
|
||||
{modifiers}
|
||||
</span>
|
||||
</span>
|
||||
<span style={{ fontSize: 13, color: '#6b7280' }}>
|
||||
Key count:{' '}
|
||||
<span
|
||||
data-testid="key-count"
|
||||
style={{ fontWeight: 700, color: '#854d0e' }}
|
||||
>
|
||||
{keyCount}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'test-kbd0-00000000-0000-0000-0000-000000000021',
|
||||
name: 'keyboard-events-component',
|
||||
description:
|
||||
'Component testing keyboard event serialization (key, code, modifiers)',
|
||||
component: KeyboardEventsComponent,
|
||||
});
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { expect, userEvent, waitFor, within } from 'storybook/test';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import {
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
|
||||
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
|
||||
import {
|
||||
HOST_API_TIMEOUT,
|
||||
INTERACTION_TIMEOUT,
|
||||
} from '@/__stories__/shared/test-utils/timeouts';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HostApi/ClosePanel',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof FrontComponentRenderer>;
|
||||
|
||||
export const ClosePanel: Story = runFrontComponentStory({
|
||||
frontComponentBundleName: 'host-api-side-panel-close',
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const api = args.frontComponentHostCommunicationApi;
|
||||
|
||||
if (!isDefined(api)) {
|
||||
throw new Error('frontComponentHostCommunicationApi is required');
|
||||
}
|
||||
|
||||
await expectFrontComponentMounted(canvas);
|
||||
|
||||
const subject = await canvas.findByTestId('subject');
|
||||
|
||||
await userEvent.click(subject);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(api.closeSidePanel).toHaveBeenCalled();
|
||||
},
|
||||
{ timeout: HOST_API_TIMEOUT },
|
||||
);
|
||||
|
||||
expect(
|
||||
await canvas.findByText(
|
||||
'closePanel:success',
|
||||
{},
|
||||
{ timeout: INTERACTION_TIMEOUT },
|
||||
),
|
||||
).toBeVisible();
|
||||
},
|
||||
});
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { expect, userEvent, waitFor, within } from 'storybook/test';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import {
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
|
||||
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
|
||||
import {
|
||||
HOST_API_TIMEOUT,
|
||||
INTERACTION_TIMEOUT,
|
||||
} from '@/__stories__/shared/test-utils/timeouts';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HostApi/CopyToClipboard',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof FrontComponentRenderer>;
|
||||
|
||||
export const CopyToClipboard: Story = runFrontComponentStory({
|
||||
frontComponentBundleName: 'host-api-copy-to-clipboard',
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const api = args.frontComponentHostCommunicationApi;
|
||||
|
||||
if (!isDefined(api)) {
|
||||
throw new Error('frontComponentHostCommunicationApi is required');
|
||||
}
|
||||
|
||||
await expectFrontComponentMounted(canvas);
|
||||
|
||||
const subject = await canvas.findByTestId('subject');
|
||||
|
||||
await userEvent.click(subject);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(api.copyToClipboard).toHaveBeenCalledWith('Hello clipboard');
|
||||
},
|
||||
{ timeout: HOST_API_TIMEOUT },
|
||||
);
|
||||
|
||||
expect(
|
||||
await canvas.findByText(
|
||||
'clipboard:success',
|
||||
{},
|
||||
{ timeout: INTERACTION_TIMEOUT },
|
||||
),
|
||||
).toBeVisible();
|
||||
},
|
||||
});
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { copyToClipboard } from 'twenty-sdk/front-component';
|
||||
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { BUTTON_STYLE } from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const STATUS_STYLE = {
|
||||
fontSize: 13,
|
||||
color: '#1f2937',
|
||||
fontFamily: 'monospace',
|
||||
};
|
||||
|
||||
const HostApiCopyToClipboardFrontComponent = () => {
|
||||
const [status, setStatus] = useState('idle');
|
||||
|
||||
const handleClick = async () => {
|
||||
try {
|
||||
await copyToClipboard('Hello clipboard');
|
||||
setStatus('clipboard:success');
|
||||
} catch (error) {
|
||||
setStatus(
|
||||
`clipboard:error:${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="host-api:copy-to-clipboard">
|
||||
<button
|
||||
data-testid="subject"
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
style={BUTTON_STYLE}
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
<span data-testid="api-status" style={STATUS_STYLE}>
|
||||
{status}
|
||||
</span>
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-host-clipboard-00000000-0000-0000-0000-000000000020',
|
||||
name: 'host-api-copy-to-clipboard-front-component',
|
||||
description: 'Front component covering copyToClipboard host API',
|
||||
component: HostApiCopyToClipboardFrontComponent,
|
||||
});
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { AppPath, navigate } from 'twenty-sdk/front-component';
|
||||
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { BUTTON_STYLE } from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const STATUS_STYLE = {
|
||||
fontSize: 13,
|
||||
color: '#1f2937',
|
||||
fontFamily: 'monospace',
|
||||
};
|
||||
|
||||
const HostApiNavigateFrontComponent = () => {
|
||||
const [status, setStatus] = useState('idle');
|
||||
|
||||
const handleClick = async () => {
|
||||
try {
|
||||
await navigate(AppPath.RecordIndexPage, {
|
||||
objectNamePlural: 'companies',
|
||||
});
|
||||
setStatus('navigate:success');
|
||||
} catch (error) {
|
||||
setStatus(
|
||||
`navigate:error:${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="host-api:navigate">
|
||||
<button
|
||||
data-testid="subject"
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
style={BUTTON_STYLE}
|
||||
>
|
||||
Navigate
|
||||
</button>
|
||||
<span data-testid="api-status" style={STATUS_STYLE}>
|
||||
{status}
|
||||
</span>
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-host-navigate-00000000-0000-0000-0000-000000000020',
|
||||
name: 'host-api-navigate-front-component',
|
||||
description: 'Front component covering navigate host API',
|
||||
component: HostApiNavigateFrontComponent,
|
||||
});
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { updateProgress } from 'twenty-sdk/front-component';
|
||||
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { BUTTON_STYLE } from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const STATUS_STYLE = {
|
||||
fontSize: 13,
|
||||
color: '#1f2937',
|
||||
fontFamily: 'monospace',
|
||||
};
|
||||
|
||||
const HostApiProgressFrontComponent = () => {
|
||||
const [status, setStatus] = useState('idle');
|
||||
|
||||
const handleClick = async () => {
|
||||
try {
|
||||
await updateProgress(50);
|
||||
setStatus('progress:success');
|
||||
} catch (error) {
|
||||
setStatus(
|
||||
`progress:error:${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="host-api:progress">
|
||||
<button
|
||||
data-testid="subject"
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
style={BUTTON_STYLE}
|
||||
>
|
||||
Update Progress
|
||||
</button>
|
||||
<span data-testid="api-status" style={STATUS_STYLE}>
|
||||
{status}
|
||||
</span>
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-host-progress-00000000-0000-0000-0000-000000000020',
|
||||
name: 'host-api-progress-front-component',
|
||||
description: 'Front component covering updateProgress host API',
|
||||
component: HostApiProgressFrontComponent,
|
||||
});
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { closeSidePanel } from 'twenty-sdk/front-component';
|
||||
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { BUTTON_STYLE } from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const STATUS_STYLE = {
|
||||
fontSize: 13,
|
||||
color: '#1f2937',
|
||||
fontFamily: 'monospace',
|
||||
};
|
||||
|
||||
const HostApiSidePanelCloseFrontComponent = () => {
|
||||
const [status, setStatus] = useState('idle');
|
||||
|
||||
const handleClick = async () => {
|
||||
try {
|
||||
await closeSidePanel();
|
||||
setStatus('closePanel:success');
|
||||
} catch (error) {
|
||||
setStatus(
|
||||
`closePanel:error:${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="host-api:side-panel:close">
|
||||
<button
|
||||
data-testid="subject"
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
style={BUTTON_STYLE}
|
||||
>
|
||||
Close Side Panel
|
||||
</button>
|
||||
<span data-testid="api-status" style={STATUS_STYLE}>
|
||||
{status}
|
||||
</span>
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-host-spc-00000000-0000-0000-0000-000000000020',
|
||||
name: 'host-api-side-panel-close-front-component',
|
||||
description: 'Front component covering closeSidePanel host API',
|
||||
component: HostApiSidePanelCloseFrontComponent,
|
||||
});
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { openSidePanelPage, SidePanelPages } from 'twenty-sdk/front-component';
|
||||
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { BUTTON_STYLE } from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const STATUS_STYLE = {
|
||||
fontSize: 13,
|
||||
color: '#1f2937',
|
||||
fontFamily: 'monospace',
|
||||
};
|
||||
|
||||
const HostApiSidePanelOpenFrontComponent = () => {
|
||||
const [status, setStatus] = useState('idle');
|
||||
|
||||
const handleClick = async () => {
|
||||
try {
|
||||
await openSidePanelPage({
|
||||
page: SidePanelPages.ViewRecord,
|
||||
pageTitle: 'Test Record',
|
||||
});
|
||||
setStatus('sidePanel:success');
|
||||
} catch (error) {
|
||||
setStatus(
|
||||
`sidePanel:error:${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="host-api:side-panel:open">
|
||||
<button
|
||||
data-testid="subject"
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
style={BUTTON_STYLE}
|
||||
>
|
||||
Open Side Panel
|
||||
</button>
|
||||
<span data-testid="api-status" style={STATUS_STYLE}>
|
||||
{status}
|
||||
</span>
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-host-spo-00000000-0000-0000-0000-000000000020',
|
||||
name: 'host-api-side-panel-open-front-component',
|
||||
description: 'Front component covering openSidePanelPage host API',
|
||||
component: HostApiSidePanelOpenFrontComponent,
|
||||
});
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { enqueueSnackbar } from 'twenty-sdk/front-component';
|
||||
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { BUTTON_STYLE } from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const STATUS_STYLE = {
|
||||
fontSize: 13,
|
||||
color: '#1f2937',
|
||||
fontFamily: 'monospace',
|
||||
};
|
||||
|
||||
const HostApiSnackbarFrontComponent = () => {
|
||||
const [status, setStatus] = useState('idle');
|
||||
|
||||
const handleClick = async () => {
|
||||
try {
|
||||
await enqueueSnackbar({
|
||||
message: 'Test notification',
|
||||
variant: 'success',
|
||||
});
|
||||
setStatus('snackbar:success');
|
||||
} catch (error) {
|
||||
setStatus(
|
||||
`snackbar:error:${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="host-api:snackbar">
|
||||
<button
|
||||
data-testid="subject"
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
style={BUTTON_STYLE}
|
||||
>
|
||||
Snackbar
|
||||
</button>
|
||||
<span data-testid="api-status" style={STATUS_STYLE}>
|
||||
{status}
|
||||
</span>
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-host-snackbar-00000000-0000-0000-0000-000000000020',
|
||||
name: 'host-api-snackbar-front-component',
|
||||
description: 'Front component covering enqueueSnackbar host API',
|
||||
component: HostApiSnackbarFrontComponent,
|
||||
});
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { unmountFrontComponent } from 'twenty-sdk/front-component';
|
||||
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { BUTTON_STYLE } from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const STATUS_STYLE = {
|
||||
fontSize: 13,
|
||||
color: '#1f2937',
|
||||
fontFamily: 'monospace',
|
||||
};
|
||||
|
||||
const HostApiUnmountFrontComponent = () => {
|
||||
const [status, setStatus] = useState('idle');
|
||||
|
||||
const handleClick = async () => {
|
||||
try {
|
||||
await unmountFrontComponent();
|
||||
setStatus('unmount:success');
|
||||
} catch (error) {
|
||||
setStatus(
|
||||
`unmount:error:${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="host-api:unmount">
|
||||
<button
|
||||
data-testid="subject"
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
style={BUTTON_STYLE}
|
||||
>
|
||||
Unmount
|
||||
</button>
|
||||
<span data-testid="api-status" style={STATUS_STYLE}>
|
||||
{status}
|
||||
</span>
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-host-unmount-00000000-0000-0000-0000-000000000020',
|
||||
name: 'host-api-unmount-front-component',
|
||||
description: 'Front component covering unmountFrontComponent host API',
|
||||
component: HostApiUnmountFrontComponent,
|
||||
});
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { expect, userEvent, waitFor, within } from 'storybook/test';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import {
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
|
||||
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
|
||||
import {
|
||||
HOST_API_TIMEOUT,
|
||||
INTERACTION_TIMEOUT,
|
||||
} from '@/__stories__/shared/test-utils/timeouts';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HostApi/Navigate',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof FrontComponentRenderer>;
|
||||
|
||||
export const Navigate: Story = runFrontComponentStory({
|
||||
frontComponentBundleName: 'host-api-navigate',
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const api = args.frontComponentHostCommunicationApi;
|
||||
|
||||
if (!isDefined(api)) {
|
||||
throw new Error('frontComponentHostCommunicationApi is required');
|
||||
}
|
||||
|
||||
await expectFrontComponentMounted(canvas);
|
||||
|
||||
const subject = await canvas.findByTestId('subject');
|
||||
|
||||
await userEvent.click(subject);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(api.navigate).toHaveBeenCalled();
|
||||
},
|
||||
{ timeout: HOST_API_TIMEOUT },
|
||||
);
|
||||
|
||||
expect(
|
||||
await canvas.findByText(
|
||||
'navigate:success',
|
||||
{},
|
||||
{ timeout: INTERACTION_TIMEOUT },
|
||||
),
|
||||
).toBeVisible();
|
||||
},
|
||||
});
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { expect, userEvent, waitFor, within } from 'storybook/test';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import {
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
|
||||
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
|
||||
import {
|
||||
HOST_API_TIMEOUT,
|
||||
INTERACTION_TIMEOUT,
|
||||
} from '@/__stories__/shared/test-utils/timeouts';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HostApi/Progress',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof FrontComponentRenderer>;
|
||||
|
||||
export const Progress: Story = runFrontComponentStory({
|
||||
frontComponentBundleName: 'host-api-progress',
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const api = args.frontComponentHostCommunicationApi;
|
||||
|
||||
if (!isDefined(api)) {
|
||||
throw new Error('frontComponentHostCommunicationApi is required');
|
||||
}
|
||||
|
||||
await expectFrontComponentMounted(canvas);
|
||||
|
||||
const subject = await canvas.findByTestId('subject');
|
||||
|
||||
await userEvent.click(subject);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(api.updateProgress).toHaveBeenCalledWith(50);
|
||||
},
|
||||
{ timeout: HOST_API_TIMEOUT },
|
||||
);
|
||||
|
||||
expect(
|
||||
await canvas.findByText(
|
||||
'progress:success',
|
||||
{},
|
||||
{ timeout: INTERACTION_TIMEOUT },
|
||||
),
|
||||
).toBeVisible();
|
||||
},
|
||||
});
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { expect, userEvent, waitFor, within } from 'storybook/test';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import {
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
|
||||
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
|
||||
import {
|
||||
HOST_API_TIMEOUT,
|
||||
INTERACTION_TIMEOUT,
|
||||
} from '@/__stories__/shared/test-utils/timeouts';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HostApi/Snackbar',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof FrontComponentRenderer>;
|
||||
|
||||
export const Snackbar: Story = runFrontComponentStory({
|
||||
frontComponentBundleName: 'host-api-snackbar',
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const api = args.frontComponentHostCommunicationApi;
|
||||
|
||||
if (!isDefined(api)) {
|
||||
throw new Error('frontComponentHostCommunicationApi is required');
|
||||
}
|
||||
|
||||
await expectFrontComponentMounted(canvas);
|
||||
|
||||
const subject = await canvas.findByTestId('subject');
|
||||
|
||||
await userEvent.click(subject);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(api.enqueueSnackbar).toHaveBeenCalledWith({
|
||||
message: 'Test notification',
|
||||
variant: 'success',
|
||||
});
|
||||
},
|
||||
{ timeout: HOST_API_TIMEOUT },
|
||||
);
|
||||
|
||||
expect(
|
||||
await canvas.findByText(
|
||||
'snackbar:success',
|
||||
{},
|
||||
{ timeout: INTERACTION_TIMEOUT },
|
||||
),
|
||||
).toBeVisible();
|
||||
},
|
||||
});
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
EventLog,
|
||||
useEventLog,
|
||||
} from '@/__stories__/shared/front-components/event-log';
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { FILL_RECT_STYLE } from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const IframeClickFrontComponent = () => {
|
||||
const [interactionCount, setInteractionCount] = useState(0);
|
||||
const { entries, pushEvent } = useEventLog();
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="iframe:click">
|
||||
<iframe
|
||||
data-testid="subject"
|
||||
title="probe"
|
||||
onClick={(event) => {
|
||||
setInteractionCount((previous) => previous + 1);
|
||||
pushEvent(event);
|
||||
}}
|
||||
tabIndex={0}
|
||||
style={{ ...FILL_RECT_STYLE, height: 80 }}
|
||||
/>
|
||||
<span data-testid="front-component-value">{interactionCount}</span>
|
||||
<EventLog entries={entries} />
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-iframe-c-click-00000000-0000-0000-0000-000000000020',
|
||||
name: 'iframe-click-front-component',
|
||||
description: 'Front component covering click on <iframe>',
|
||||
component: IframeClickFrontComponent,
|
||||
});
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { type Meta } from '@storybook/react-vite';
|
||||
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import {
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import {
|
||||
createHtmlTagClickStory,
|
||||
createHtmlTagFocusStory,
|
||||
} from '@/__stories__/shared/test-utils/createHtmlElementStory';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HtmlTag/Embedded/Iframe/Events',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Click = createHtmlTagClickStory({
|
||||
frontComponentBundleName: 'iframe-click',
|
||||
});
|
||||
|
||||
export const FocusBlur = createHtmlTagFocusStory({
|
||||
frontComponentBundleName: 'iframe-focus-blur',
|
||||
});
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
EventLog,
|
||||
useEventLog,
|
||||
} from '@/__stories__/shared/front-components/event-log';
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { FILL_RECT_STYLE } from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const IframeFocusBlurFrontComponent = () => {
|
||||
const [interactionCount, setInteractionCount] = useState(0);
|
||||
const { entries, pushEvent } = useEventLog();
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="iframe:focus-blur">
|
||||
<iframe
|
||||
data-testid="subject"
|
||||
title="probe"
|
||||
onFocus={(event) => {
|
||||
setInteractionCount((previous) => previous + 1);
|
||||
pushEvent(event);
|
||||
}}
|
||||
onBlur={(event) => pushEvent(event)}
|
||||
tabIndex={0}
|
||||
style={{ ...FILL_RECT_STYLE, height: 80 }}
|
||||
/>
|
||||
<span data-testid="front-component-value">{interactionCount}</span>
|
||||
<EventLog entries={entries} />
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-iframe-fb-fb-00000000-0000-0000-0000-000000000020',
|
||||
name: 'iframe-focus-blur-front-component',
|
||||
description: 'Front component covering focus-blur on <iframe>',
|
||||
component: IframeFocusBlurFrontComponent,
|
||||
});
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
EventLog,
|
||||
useEventLog,
|
||||
} from '@/__stories__/shared/front-components/event-log';
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { FILL_RECT_STYLE } from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const ImgClickFrontComponent = () => {
|
||||
const [interactionCount, setInteractionCount] = useState(0);
|
||||
const { entries, pushEvent } = useEventLog();
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="img:click">
|
||||
<img
|
||||
data-testid="subject"
|
||||
src="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='80' height='40'/>"
|
||||
alt="probe"
|
||||
onClick={(event) => {
|
||||
setInteractionCount((previous) => previous + 1);
|
||||
pushEvent(event);
|
||||
}}
|
||||
tabIndex={0}
|
||||
style={{ ...FILL_RECT_STYLE, height: 40 }}
|
||||
/>
|
||||
<span data-testid="front-component-value">{interactionCount}</span>
|
||||
<EventLog entries={entries} />
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-img-c-click-00000000-0000-0000-0000-000000000020',
|
||||
name: 'img-click-front-component',
|
||||
description: 'Front component covering click on <img>',
|
||||
component: ImgClickFrontComponent,
|
||||
});
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { type Meta } from '@storybook/react-vite';
|
||||
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import {
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import {
|
||||
createHtmlTagClickStory,
|
||||
createHtmlTagFocusStory,
|
||||
} from '@/__stories__/shared/test-utils/createHtmlElementStory';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HtmlTag/Embedded/Img/Events',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Click = createHtmlTagClickStory({
|
||||
frontComponentBundleName: 'img-click',
|
||||
});
|
||||
|
||||
export const FocusBlur = createHtmlTagFocusStory({
|
||||
frontComponentBundleName: 'img-focus-blur',
|
||||
});
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
EventLog,
|
||||
useEventLog,
|
||||
} from '@/__stories__/shared/front-components/event-log';
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { FILL_RECT_STYLE } from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const ImgFocusBlurFrontComponent = () => {
|
||||
const [interactionCount, setInteractionCount] = useState(0);
|
||||
const { entries, pushEvent } = useEventLog();
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="img:focus-blur">
|
||||
<img
|
||||
data-testid="subject"
|
||||
src="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='80' height='40'/>"
|
||||
alt="probe"
|
||||
onFocus={(event) => {
|
||||
setInteractionCount((previous) => previous + 1);
|
||||
pushEvent(event);
|
||||
}}
|
||||
onBlur={(event) => pushEvent(event)}
|
||||
tabIndex={0}
|
||||
style={{ ...FILL_RECT_STYLE, height: 40 }}
|
||||
/>
|
||||
<span data-testid="front-component-value">{interactionCount}</span>
|
||||
<EventLog entries={entries} />
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-img-fb-fb-00000000-0000-0000-0000-000000000020',
|
||||
name: 'img-focus-blur-front-component',
|
||||
description: 'Front component covering focus-blur on <img>',
|
||||
component: ImgFocusBlurFrontComponent,
|
||||
});
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
|
||||
|
||||
const ImgPropertiesFrontComponent = () => (
|
||||
<FrontComponentCard title="img:properties">
|
||||
<img
|
||||
data-testid="subject"
|
||||
id={PROPERTY_FIXTURE.id}
|
||||
className={PROPERTY_FIXTURE.className}
|
||||
title={PROPERTY_FIXTURE.title}
|
||||
role={PROPERTY_FIXTURE.role}
|
||||
aria-label={PROPERTY_FIXTURE.ariaLabel}
|
||||
tabIndex={PROPERTY_FIXTURE.tabIndex}
|
||||
src={PROPERTY_FIXTURE.src}
|
||||
alt={PROPERTY_FIXTURE.alt}
|
||||
/>
|
||||
</FrontComponentCard>
|
||||
);
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-img-props-00000000-0000-0000-0000-000000000020',
|
||||
name: 'img-properties-front-component',
|
||||
description: 'Front component covering property reflection on <img>',
|
||||
component: ImgPropertiesFrontComponent,
|
||||
});
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { type Meta } from '@storybook/react-vite';
|
||||
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import {
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import { createPropertyReflectionStory } from '@/__stories__/shared/test-utils/createPropertyReflectionStory';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HtmlTag/Embedded/Img/Properties',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Properties = createPropertyReflectionStory({
|
||||
frontComponentBundleName: 'img-properties',
|
||||
extraAttributes: {
|
||||
src: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="80" height="40"/>',
|
||||
alt: 'subject-alt',
|
||||
},
|
||||
});
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
EventLog,
|
||||
useEventLog,
|
||||
} from '@/__stories__/shared/front-components/event-log';
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { FILL_RECT_STYLE } from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const PictureClickFrontComponent = () => {
|
||||
const [interactionCount, setInteractionCount] = useState(0);
|
||||
const { entries, pushEvent } = useEventLog();
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="picture:click">
|
||||
<picture
|
||||
data-testid="subject"
|
||||
onClick={(event) => {
|
||||
setInteractionCount((previous) => previous + 1);
|
||||
pushEvent(event);
|
||||
}}
|
||||
tabIndex={0}
|
||||
style={FILL_RECT_STYLE}
|
||||
>
|
||||
<img
|
||||
src="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='80' height='40'/>"
|
||||
alt="probe"
|
||||
/>
|
||||
</picture>
|
||||
<span data-testid="front-component-value">{interactionCount}</span>
|
||||
<EventLog entries={entries} />
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier:
|
||||
'fc-picture-c-click-00000000-0000-0000-0000-000000000020',
|
||||
name: 'picture-click-front-component',
|
||||
description: 'Front component covering click on <picture>',
|
||||
component: PictureClickFrontComponent,
|
||||
});
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { type Meta } from '@storybook/react-vite';
|
||||
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import {
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import {
|
||||
createHtmlTagClickStory,
|
||||
createHtmlTagFocusStory,
|
||||
} from '@/__stories__/shared/test-utils/createHtmlElementStory';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HtmlTag/Embedded/Picture/Events',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Click = createHtmlTagClickStory({
|
||||
frontComponentBundleName: 'picture-click',
|
||||
});
|
||||
|
||||
export const FocusBlur = createHtmlTagFocusStory({
|
||||
frontComponentBundleName: 'picture-focus-blur',
|
||||
});
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
EventLog,
|
||||
useEventLog,
|
||||
} from '@/__stories__/shared/front-components/event-log';
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { FILL_RECT_STYLE } from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const PictureFocusBlurFrontComponent = () => {
|
||||
const [interactionCount, setInteractionCount] = useState(0);
|
||||
const { entries, pushEvent } = useEventLog();
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="picture:focus-blur">
|
||||
<picture
|
||||
data-testid="subject"
|
||||
onFocus={(event) => {
|
||||
setInteractionCount((previous) => previous + 1);
|
||||
pushEvent(event);
|
||||
}}
|
||||
onBlur={(event) => pushEvent(event)}
|
||||
tabIndex={0}
|
||||
style={FILL_RECT_STYLE}
|
||||
>
|
||||
<img
|
||||
src="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='80' height='40'/>"
|
||||
alt="probe"
|
||||
/>
|
||||
</picture>
|
||||
<span data-testid="front-component-value">{interactionCount}</span>
|
||||
<EventLog entries={entries} />
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-picture-fb-fb-00000000-0000-0000-0000-000000000020',
|
||||
name: 'picture-focus-blur-front-component',
|
||||
description: 'Front component covering focus-blur on <picture>',
|
||||
component: PictureFocusBlurFrontComponent,
|
||||
});
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
EventLog,
|
||||
useEventLog,
|
||||
} from '@/__stories__/shared/front-components/event-log';
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { BUTTON_STYLE } from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const ButtonClickFrontComponent = () => {
|
||||
const [clickCount, setClickCount] = useState(0);
|
||||
const { entries, pushEvent } = useEventLog();
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="button:click">
|
||||
<button
|
||||
data-testid="subject"
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
setClickCount((previous) => previous + 1);
|
||||
pushEvent(event);
|
||||
}}
|
||||
style={BUTTON_STYLE}
|
||||
>
|
||||
Click me
|
||||
</button>
|
||||
<span data-testid="front-component-value">{clickCount}</span>
|
||||
<EventLog entries={entries} />
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-btn-click-00000000-0000-0000-0000-000000000020',
|
||||
name: 'button-click-front-component',
|
||||
description: 'Front component covering click event on <button>',
|
||||
component: ButtonClickFrontComponent,
|
||||
});
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { userEvent, within } from 'storybook/test';
|
||||
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import {
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import { expectEventLogged } from '@/__stories__/shared/test-utils/matchers/expectEventLogged';
|
||||
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
|
||||
import { expectFrontComponentValue } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentValue';
|
||||
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HtmlTag/Form/Button/Events',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof FrontComponentRenderer>;
|
||||
|
||||
export const ClickEvent: Story = runFrontComponentStory({
|
||||
frontComponentBundleName: 'button-click',
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await expectFrontComponentMounted(canvas);
|
||||
|
||||
const subject = await canvas.findByTestId('subject');
|
||||
|
||||
await userEvent.click(subject);
|
||||
await expectFrontComponentValue({ canvas, expected: '1' });
|
||||
|
||||
await userEvent.click(subject);
|
||||
await expectFrontComponentValue({ canvas, expected: '2' });
|
||||
|
||||
await expectEventLogged({ canvas, matcher: { type: 'click' } });
|
||||
},
|
||||
});
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
|
||||
|
||||
const ButtonPropertiesFrontComponent = () => (
|
||||
<FrontComponentCard title="button:properties">
|
||||
<button
|
||||
data-testid="subject"
|
||||
type="button"
|
||||
id={PROPERTY_FIXTURE.id}
|
||||
className={PROPERTY_FIXTURE.className}
|
||||
title={PROPERTY_FIXTURE.title}
|
||||
role={PROPERTY_FIXTURE.role}
|
||||
aria-label={PROPERTY_FIXTURE.ariaLabel}
|
||||
tabIndex={PROPERTY_FIXTURE.tabIndex}
|
||||
>
|
||||
button
|
||||
</button>
|
||||
</FrontComponentCard>
|
||||
);
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-btn-props-00000000-0000-0000-0000-000000000020',
|
||||
name: 'button-properties-front-component',
|
||||
description: 'Front component covering property reflection on <button>',
|
||||
component: ButtonPropertiesFrontComponent,
|
||||
});
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { type Meta } from '@storybook/react-vite';
|
||||
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import {
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import { createPropertyReflectionStory } from '@/__stories__/shared/test-utils/createPropertyReflectionStory';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HtmlTag/Form/Button/Properties',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Properties = createPropertyReflectionStory({
|
||||
frontComponentBundleName: 'button-properties',
|
||||
});
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
EventLog,
|
||||
useEventLog,
|
||||
} from '@/__stories__/shared/front-components/event-log';
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
|
||||
const DatalistClickFrontComponent = () => {
|
||||
const [interactionCount, setInteractionCount] = useState(0);
|
||||
const { entries, pushEvent } = useEventLog();
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="datalist:click">
|
||||
<>
|
||||
<input list="probe-list" />
|
||||
<datalist
|
||||
id="probe-list"
|
||||
data-testid="subject"
|
||||
onClick={(event) => {
|
||||
setInteractionCount((previous) => previous + 1);
|
||||
pushEvent(event);
|
||||
}}
|
||||
>
|
||||
<option value="alpha" />
|
||||
<option value="beta" />
|
||||
</datalist>
|
||||
</>
|
||||
<span data-testid="front-component-value">{interactionCount}</span>
|
||||
<EventLog entries={entries} />
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier:
|
||||
'fc-datalist-c-click-00000000-0000-0000-0000-000000000020',
|
||||
name: 'datalist-click-front-component',
|
||||
description: 'Front component covering click on <datalist>',
|
||||
component: DatalistClickFrontComponent,
|
||||
});
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { type Meta } from '@storybook/react-vite';
|
||||
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import {
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import { createHtmlTagClickStory } from '@/__stories__/shared/test-utils/createHtmlElementStory';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HtmlTag/Form/Datalist/Events',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Click = createHtmlTagClickStory({
|
||||
frontComponentBundleName: 'datalist-click',
|
||||
});
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
EventLog,
|
||||
useEventLog,
|
||||
} from '@/__stories__/shared/front-components/event-log';
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { FILL_RECT_STYLE } from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const FieldsetClickFrontComponent = () => {
|
||||
const [interactionCount, setInteractionCount] = useState(0);
|
||||
const { entries, pushEvent } = useEventLog();
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="fieldset:click">
|
||||
<fieldset
|
||||
data-testid="subject"
|
||||
onClick={(event) => {
|
||||
setInteractionCount((previous) => previous + 1);
|
||||
pushEvent(event);
|
||||
}}
|
||||
tabIndex={0}
|
||||
style={FILL_RECT_STYLE}
|
||||
>
|
||||
fieldset
|
||||
</fieldset>
|
||||
<span data-testid="front-component-value">{interactionCount}</span>
|
||||
<EventLog entries={entries} />
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier:
|
||||
'fc-fieldset-c-click-00000000-0000-0000-0000-000000000020',
|
||||
name: 'fieldset-click-front-component',
|
||||
description: 'Front component covering click on <fieldset>',
|
||||
component: FieldsetClickFrontComponent,
|
||||
});
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { type Meta } from '@storybook/react-vite';
|
||||
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import {
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import {
|
||||
createHtmlTagClickStory,
|
||||
createHtmlTagFocusStory,
|
||||
} from '@/__stories__/shared/test-utils/createHtmlElementStory';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HtmlTag/Form/Fieldset/Events',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Click = createHtmlTagClickStory({
|
||||
frontComponentBundleName: 'fieldset-click',
|
||||
});
|
||||
|
||||
export const FocusBlur = createHtmlTagFocusStory({
|
||||
frontComponentBundleName: 'fieldset-focus-blur',
|
||||
});
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
EventLog,
|
||||
useEventLog,
|
||||
} from '@/__stories__/shared/front-components/event-log';
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { FILL_RECT_STYLE } from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const FieldsetFocusBlurFrontComponent = () => {
|
||||
const [interactionCount, setInteractionCount] = useState(0);
|
||||
const { entries, pushEvent } = useEventLog();
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="fieldset:focus-blur">
|
||||
<fieldset
|
||||
data-testid="subject"
|
||||
onFocus={(event) => {
|
||||
setInteractionCount((previous) => previous + 1);
|
||||
pushEvent(event);
|
||||
}}
|
||||
onBlur={(event) => pushEvent(event)}
|
||||
tabIndex={0}
|
||||
style={FILL_RECT_STYLE}
|
||||
>
|
||||
fieldset
|
||||
</fieldset>
|
||||
<span data-testid="front-component-value">{interactionCount}</span>
|
||||
<EventLog entries={entries} />
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-fieldset-fb-fb-00000000-0000-0000-0000-000000000020',
|
||||
name: 'fieldset-focus-blur-front-component',
|
||||
description: 'Front component covering focus-blur on <fieldset>',
|
||||
component: FieldsetFocusBlurFrontComponent,
|
||||
});
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
|
||||
|
||||
const FieldsetPropertiesFrontComponent = () => (
|
||||
<FrontComponentCard title="fieldset:properties">
|
||||
<fieldset
|
||||
data-testid="subject"
|
||||
id={PROPERTY_FIXTURE.id}
|
||||
className={PROPERTY_FIXTURE.className}
|
||||
title={PROPERTY_FIXTURE.title}
|
||||
role={PROPERTY_FIXTURE.role}
|
||||
aria-label={PROPERTY_FIXTURE.ariaLabel}
|
||||
tabIndex={PROPERTY_FIXTURE.tabIndex}
|
||||
>
|
||||
field
|
||||
</fieldset>
|
||||
</FrontComponentCard>
|
||||
);
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-fieldset-props-00000000-0000-0000-0000-000000000020',
|
||||
name: 'fieldset-properties-front-component',
|
||||
description: 'Front component covering property reflection on <fieldset>',
|
||||
component: FieldsetPropertiesFrontComponent,
|
||||
});
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { type Meta } from '@storybook/react-vite';
|
||||
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import {
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import { createPropertyReflectionStory } from '@/__stories__/shared/test-utils/createPropertyReflectionStory';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HtmlTag/Form/Fieldset/Properties',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Properties = createPropertyReflectionStory({
|
||||
frontComponentBundleName: 'fieldset-properties',
|
||||
});
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { userEvent, within } from 'storybook/test';
|
||||
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import {
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import { expectEventLogged } from '@/__stories__/shared/test-utils/matchers/expectEventLogged';
|
||||
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
|
||||
import { expectFrontComponentValue } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentValue';
|
||||
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HtmlTag/Form/Form/Events',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof FrontComponentRenderer>;
|
||||
|
||||
export const SubmitEvent: Story = runFrontComponentStory({
|
||||
frontComponentBundleName: 'form-submit',
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await expectFrontComponentMounted(canvas);
|
||||
|
||||
const submitButton = await canvas.findByTestId('submit-button');
|
||||
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
await expectFrontComponentValue({ canvas, expected: 'value-from-form' });
|
||||
await expectEventLogged({ canvas, matcher: { type: 'submit' } });
|
||||
},
|
||||
});
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
|
||||
|
||||
const FormPropertiesFrontComponent = () => (
|
||||
<FrontComponentCard title="form:properties">
|
||||
<form
|
||||
data-testid="subject"
|
||||
id={PROPERTY_FIXTURE.id}
|
||||
className={PROPERTY_FIXTURE.className}
|
||||
title={PROPERTY_FIXTURE.title}
|
||||
role={PROPERTY_FIXTURE.role}
|
||||
aria-label={PROPERTY_FIXTURE.ariaLabel}
|
||||
tabIndex={PROPERTY_FIXTURE.tabIndex}
|
||||
>
|
||||
form
|
||||
</form>
|
||||
</FrontComponentCard>
|
||||
);
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-form-props-00000000-0000-0000-0000-000000000020',
|
||||
name: 'form-properties-front-component',
|
||||
description: 'Front component covering property reflection on <form>',
|
||||
component: FormPropertiesFrontComponent,
|
||||
});
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { type Meta } from '@storybook/react-vite';
|
||||
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import {
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import { createPropertyReflectionStory } from '@/__stories__/shared/test-utils/createPropertyReflectionStory';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HtmlTag/Form/Form/Properties',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Properties = createPropertyReflectionStory({
|
||||
frontComponentBundleName: 'form-properties',
|
||||
});
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import { type FormEvent, useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
EventLog,
|
||||
useEventLog,
|
||||
} from '@/__stories__/shared/front-components/event-log';
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import {
|
||||
BUTTON_STYLE,
|
||||
INPUT_STYLE,
|
||||
} from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const FormSubmitFrontComponent = () => {
|
||||
const [fieldValue, setFieldValue] = useState('value-from-form');
|
||||
const [submittedValue, setSubmittedValue] = useState<string | null>(null);
|
||||
const { entries, pushEvent } = useEventLog();
|
||||
|
||||
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setSubmittedValue(fieldValue);
|
||||
pushEvent(event);
|
||||
};
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="form:submit">
|
||||
<form
|
||||
data-testid="subject"
|
||||
action="javascript:void(0);"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<input
|
||||
data-testid="form-field"
|
||||
type="text"
|
||||
name="field"
|
||||
value={fieldValue}
|
||||
onChange={(event) => setFieldValue(event.target.value)}
|
||||
style={INPUT_STYLE}
|
||||
/>
|
||||
<button data-testid="submit-button" type="submit" style={BUTTON_STYLE}>
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
<span data-testid="front-component-value">
|
||||
{submittedValue ?? 'none'}
|
||||
</span>
|
||||
<EventLog entries={entries} />
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-form-submit-00000000-0000-0000-0000-000000000020',
|
||||
name: 'form-submit-front-component',
|
||||
description: 'Front component covering submit event on <form>',
|
||||
component: FormSubmitFrontComponent,
|
||||
});
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import {
|
||||
INPUT_STYLE,
|
||||
LABEL_STYLE,
|
||||
SUBJECT_WRAPPER_STYLE,
|
||||
} from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const CARET_INITIAL_VALUE = 'Hello world';
|
||||
|
||||
const InputCaretFrontComponent = () => {
|
||||
const [value, setValue] = useState(CARET_INITIAL_VALUE);
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="input:text:caret">
|
||||
<div style={SUBJECT_WRAPPER_STYLE}>
|
||||
<label style={LABEL_STYLE}>Text input (pre-filled)</label>
|
||||
<input
|
||||
data-testid="subject"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
style={INPUT_STYLE}
|
||||
/>
|
||||
<span data-testid="front-component-value">{value}</span>
|
||||
</div>
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-input-caret-00000000-0000-0000-0000-000000000020',
|
||||
name: 'input-caret-front-component',
|
||||
description: 'Front component covering caret behavior on <input type="text">',
|
||||
component: InputCaretFrontComponent,
|
||||
});
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { expect, userEvent, waitFor, within } from 'storybook/test';
|
||||
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import {
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
|
||||
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
|
||||
import {
|
||||
INTERACTION_TIMEOUT,
|
||||
TYPING_TIMEOUT,
|
||||
} from '@/__stories__/shared/test-utils/timeouts';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HtmlTag/Form/Input/Caret',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof FrontComponentRenderer>;
|
||||
|
||||
export const CaretPreservedMidString: Story = runFrontComponentStory({
|
||||
frontComponentBundleName: 'input-caret',
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await expectFrontComponentMounted(canvas);
|
||||
|
||||
const subject = (await canvas.findByTestId('subject')) as HTMLInputElement;
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(subject.value).toBe('Hello world');
|
||||
},
|
||||
{ timeout: INTERACTION_TIMEOUT },
|
||||
);
|
||||
|
||||
subject.focus();
|
||||
subject.setSelectionRange(4, 4);
|
||||
|
||||
await userEvent.keyboard('X');
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(subject.value).toBe('HellXo world');
|
||||
expect(canvas.getByTestId('front-component-value').textContent).toBe(
|
||||
'HellXo world',
|
||||
);
|
||||
expect(subject.selectionStart).toBe(5);
|
||||
expect(subject.selectionEnd).toBe(5);
|
||||
},
|
||||
{ timeout: TYPING_TIMEOUT },
|
||||
);
|
||||
},
|
||||
});
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
EventLog,
|
||||
useEventLog,
|
||||
} from '@/__stories__/shared/front-components/event-log';
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import {
|
||||
LABEL_STYLE,
|
||||
ROW_STYLE,
|
||||
} from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const InputCheckboxFrontComponent = () => {
|
||||
const [checked, setChecked] = useState(false);
|
||||
const { entries, pushEvent } = useEventLog();
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="input:checkbox:checked">
|
||||
<div style={ROW_STYLE}>
|
||||
<input
|
||||
data-testid="subject"
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={(event) => {
|
||||
setChecked(event.target.checked);
|
||||
pushEvent(event);
|
||||
}}
|
||||
/>
|
||||
<label style={LABEL_STYLE}>Check me</label>
|
||||
<span data-testid="front-component-value">{String(checked)}</span>
|
||||
</div>
|
||||
<EventLog entries={entries} />
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-input-checkbox-00000000-0000-0000-0000-000000000020',
|
||||
name: 'input-checkbox-front-component',
|
||||
description: 'Front component covering <input type="checkbox">',
|
||||
component: InputCheckboxFrontComponent,
|
||||
});
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { userEvent, within } from 'storybook/test';
|
||||
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import {
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import { expectEventLogged } from '@/__stories__/shared/test-utils/matchers/expectEventLogged';
|
||||
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
|
||||
import { expectFrontComponentValue } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentValue';
|
||||
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HtmlTag/Form/Input/Checkbox',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof FrontComponentRenderer>;
|
||||
|
||||
export const CheckedRoundTrip: Story = runFrontComponentStory({
|
||||
frontComponentBundleName: 'input-checkbox',
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await expectFrontComponentMounted(canvas);
|
||||
|
||||
const subject = await canvas.findByTestId('subject');
|
||||
|
||||
await userEvent.click(subject);
|
||||
|
||||
await expectFrontComponentValue({ canvas, expected: 'true' });
|
||||
await expectEventLogged({
|
||||
canvas,
|
||||
matcher: { type: 'change', checked: true },
|
||||
});
|
||||
|
||||
await userEvent.click(subject);
|
||||
|
||||
await expectFrontComponentValue({ canvas, expected: 'false' });
|
||||
await expectEventLogged({
|
||||
canvas,
|
||||
matcher: { type: 'change', checked: false },
|
||||
});
|
||||
},
|
||||
});
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { userEvent, within } from 'storybook/test';
|
||||
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import {
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import { expectEventLogged } from '@/__stories__/shared/test-utils/matchers/expectEventLogged';
|
||||
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
|
||||
import { expectFrontComponentValue } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentValue';
|
||||
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
|
||||
import { TYPING_DELAY } from '@/__stories__/shared/test-utils/timeouts';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HtmlTag/Form/Input/Events',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof FrontComponentRenderer>;
|
||||
|
||||
export const TextValueRoundTrip: Story = runFrontComponentStory({
|
||||
frontComponentBundleName: 'input-text-value',
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await expectFrontComponentMounted(canvas);
|
||||
|
||||
const subject = await canvas.findByTestId('subject');
|
||||
|
||||
await userEvent.type(subject, 'hello', { delay: TYPING_DELAY });
|
||||
|
||||
await expectFrontComponentValue({ canvas, expected: 'hello' });
|
||||
await expectEventLogged({
|
||||
canvas,
|
||||
matcher: { type: 'change', value: 'hello' },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const FocusBlurEvents: Story = runFrontComponentStory({
|
||||
frontComponentBundleName: 'input-text-focus-blur',
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await expectFrontComponentMounted(canvas);
|
||||
|
||||
const subject = await canvas.findByTestId('subject');
|
||||
|
||||
await userEvent.click(subject);
|
||||
await expectEventLogged({ canvas, matcher: { type: 'focus' } });
|
||||
|
||||
const blurTarget = await canvas.findByTestId('blur-target');
|
||||
|
||||
await userEvent.click(blurTarget);
|
||||
await expectEventLogged({ canvas, matcher: { type: 'blur' } });
|
||||
},
|
||||
});
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
EventLog,
|
||||
useEventLog,
|
||||
} from '@/__stories__/shared/front-components/event-log';
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import {
|
||||
LABEL_STYLE,
|
||||
SUBJECT_WRAPPER_STYLE,
|
||||
} from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const InputFileMultipleFrontComponent = () => {
|
||||
const { entries, pushEvent } = useEventLog();
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="input:file:multiple">
|
||||
<div style={SUBJECT_WRAPPER_STYLE}>
|
||||
<label style={LABEL_STYLE}>Multiple files</label>
|
||||
<input
|
||||
data-testid="subject"
|
||||
type="file"
|
||||
multiple
|
||||
accept="image/*"
|
||||
onChange={(event) => pushEvent(event)}
|
||||
/>
|
||||
</div>
|
||||
<EventLog entries={entries} />
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier:
|
||||
'fc-input-file-multiple-00000000-0000-0000-0000-000000000020',
|
||||
name: 'input-file-multiple-front-component',
|
||||
description: 'Front component covering multi-file <input type="file">',
|
||||
component: InputFileMultipleFrontComponent,
|
||||
});
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
EventLog,
|
||||
useEventLog,
|
||||
} from '@/__stories__/shared/front-components/event-log';
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import {
|
||||
LABEL_STYLE,
|
||||
SUBJECT_WRAPPER_STYLE,
|
||||
} from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const InputFileSingleFrontComponent = () => {
|
||||
const { entries, pushEvent } = useEventLog();
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="input:file:single">
|
||||
<div style={SUBJECT_WRAPPER_STYLE}>
|
||||
<label style={LABEL_STYLE}>Single file</label>
|
||||
<input
|
||||
data-testid="subject"
|
||||
type="file"
|
||||
onChange={(event) => pushEvent(event)}
|
||||
/>
|
||||
</div>
|
||||
<EventLog entries={entries} />
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier:
|
||||
'fc-input-file-single-00000000-0000-0000-0000-000000000020',
|
||||
name: 'input-file-single-front-component',
|
||||
description: 'Front component covering single-file <input type="file">',
|
||||
component: InputFileSingleFrontComponent,
|
||||
});
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { userEvent, within } from 'storybook/test';
|
||||
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import {
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import { expectEventLogged } from '@/__stories__/shared/test-utils/matchers/expectEventLogged';
|
||||
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
|
||||
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HtmlTag/Form/Input/File',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof FrontComponentRenderer>;
|
||||
|
||||
export const SingleFile: Story = runFrontComponentStory({
|
||||
frontComponentBundleName: 'input-file-single',
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await expectFrontComponentMounted(canvas);
|
||||
|
||||
const subject = (await canvas.findByTestId('subject')) as HTMLInputElement;
|
||||
|
||||
const file = new File(['hello world'], 'hello.txt', {
|
||||
type: 'text/plain',
|
||||
lastModified: 1700000000000,
|
||||
});
|
||||
|
||||
await userEvent.upload(subject, file);
|
||||
|
||||
await expectEventLogged({
|
||||
canvas,
|
||||
matcher: {
|
||||
type: 'change',
|
||||
files: [{ name: 'hello.txt', type: 'text/plain' }],
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const MultipleFiles: Story = runFrontComponentStory({
|
||||
frontComponentBundleName: 'input-file-multiple',
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await expectFrontComponentMounted(canvas);
|
||||
|
||||
const subject = (await canvas.findByTestId('subject')) as HTMLInputElement;
|
||||
|
||||
const first = new File(['a'], 'one.png', { type: 'image/png' });
|
||||
const second = new File(['bb'], 'two.png', { type: 'image/png' });
|
||||
|
||||
await userEvent.upload(subject, [first, second]);
|
||||
|
||||
await expectEventLogged({
|
||||
canvas,
|
||||
matcher: {
|
||||
type: 'change',
|
||||
files: [
|
||||
{ name: 'one.png', type: 'image/png' },
|
||||
{ name: 'two.png', type: 'image/png' },
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
EventLog,
|
||||
useEventLog,
|
||||
} from '@/__stories__/shared/front-components/event-log';
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import {
|
||||
INPUT_STYLE,
|
||||
LABEL_STYLE,
|
||||
SUBJECT_WRAPPER_STYLE,
|
||||
} from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const InputKeyboardFrontComponent = () => {
|
||||
const [lastKey, setLastKey] = useState('');
|
||||
const { entries, pushEvent } = useEventLog();
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="input:text:keyboard">
|
||||
<div style={SUBJECT_WRAPPER_STYLE}>
|
||||
<label style={LABEL_STYLE}>Keyboard input</label>
|
||||
<input
|
||||
data-testid="subject"
|
||||
type="text"
|
||||
onKeyDown={(event) => {
|
||||
setLastKey(event.key);
|
||||
pushEvent(event);
|
||||
}}
|
||||
onKeyUp={(event) => pushEvent(event)}
|
||||
style={INPUT_STYLE}
|
||||
/>
|
||||
<span data-testid="front-component-value">{lastKey}</span>
|
||||
</div>
|
||||
<EventLog entries={entries} />
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-input-keyboard-00000000-0000-0000-0000-000000000020',
|
||||
name: 'input-keyboard-front-component',
|
||||
description: 'Front component covering keyboard events on <input>',
|
||||
component: InputKeyboardFrontComponent,
|
||||
});
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { userEvent, within } from 'storybook/test';
|
||||
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import {
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import { expectEventLogged } from '@/__stories__/shared/test-utils/matchers/expectEventLogged';
|
||||
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
|
||||
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HtmlTag/Form/Input/Keyboard',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof FrontComponentRenderer>;
|
||||
|
||||
export const BasicKey: Story = runFrontComponentStory({
|
||||
frontComponentBundleName: 'input-keyboard',
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await expectFrontComponentMounted(canvas);
|
||||
|
||||
const subject = await canvas.findByTestId('subject');
|
||||
|
||||
await userEvent.click(subject);
|
||||
await userEvent.keyboard('a');
|
||||
|
||||
await expectEventLogged({
|
||||
canvas,
|
||||
matcher: { type: 'keydown', key: 'a', code: 'KeyA' },
|
||||
});
|
||||
await expectEventLogged({
|
||||
canvas,
|
||||
matcher: { type: 'keyup', key: 'a', code: 'KeyA' },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const ShiftModifier: Story = runFrontComponentStory({
|
||||
frontComponentBundleName: 'input-keyboard',
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await expectFrontComponentMounted(canvas);
|
||||
|
||||
const subject = await canvas.findByTestId('subject');
|
||||
|
||||
await userEvent.click(subject);
|
||||
await userEvent.keyboard('{Shift>}b{/Shift}');
|
||||
|
||||
await expectEventLogged({
|
||||
canvas,
|
||||
matcher: { type: 'keydown', shiftKey: true },
|
||||
});
|
||||
},
|
||||
});
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
|
||||
|
||||
const noop = () => undefined;
|
||||
|
||||
const InputNumberPropertiesFrontComponent = () => (
|
||||
<FrontComponentCard title="input:number:properties">
|
||||
<input
|
||||
data-testid="subject"
|
||||
id={PROPERTY_FIXTURE.id}
|
||||
className={PROPERTY_FIXTURE.className}
|
||||
title={PROPERTY_FIXTURE.title}
|
||||
role={PROPERTY_FIXTURE.role}
|
||||
aria-label={PROPERTY_FIXTURE.ariaLabel}
|
||||
tabIndex={PROPERTY_FIXTURE.tabIndex}
|
||||
type="number"
|
||||
name={PROPERTY_FIXTURE.name}
|
||||
value={String(PROPERTY_FIXTURE.numberValue)}
|
||||
onChange={noop}
|
||||
/>
|
||||
</FrontComponentCard>
|
||||
);
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier:
|
||||
'fc-input-num-props-00000000-0000-0000-0000-000000000020',
|
||||
name: 'input-number-properties-front-component',
|
||||
description:
|
||||
'Front component covering property reflection on <input type="number">',
|
||||
component: InputNumberPropertiesFrontComponent,
|
||||
});
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { type Meta } from '@storybook/react-vite';
|
||||
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
|
||||
import {
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import { createPropertyReflectionStory } from '@/__stories__/shared/test-utils/createPropertyReflectionStory';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HtmlTag/Form/Input/Properties',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Text = createPropertyReflectionStory({
|
||||
frontComponentBundleName: 'input-text-properties',
|
||||
extraAttributes: {
|
||||
type: PROPERTY_FIXTURE.type,
|
||||
name: PROPERTY_FIXTURE.name,
|
||||
placeholder: PROPERTY_FIXTURE.placeholder,
|
||||
},
|
||||
extraProperties: { value: PROPERTY_FIXTURE.textValue },
|
||||
});
|
||||
|
||||
export const Number = createPropertyReflectionStory({
|
||||
frontComponentBundleName: 'input-number-properties',
|
||||
extraAttributes: {
|
||||
type: 'number',
|
||||
name: PROPERTY_FIXTURE.name,
|
||||
},
|
||||
extraProperties: { value: String(PROPERTY_FIXTURE.numberValue) },
|
||||
});
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
EventLog,
|
||||
useEventLog,
|
||||
} from '@/__stories__/shared/front-components/event-log';
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import {
|
||||
INPUT_STYLE,
|
||||
LABEL_STYLE,
|
||||
SUBJECT_WRAPPER_STYLE,
|
||||
} from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const InputTextFocusBlurFrontComponent = () => {
|
||||
const { entries, pushEvent } = useEventLog();
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="input:text:focus-blur">
|
||||
<div style={SUBJECT_WRAPPER_STYLE}>
|
||||
<label style={LABEL_STYLE}>Focus / Blur</label>
|
||||
<input
|
||||
data-testid="subject"
|
||||
type="text"
|
||||
onFocus={(event) => pushEvent(event)}
|
||||
onBlur={(event) => pushEvent(event)}
|
||||
style={INPUT_STYLE}
|
||||
/>
|
||||
</div>
|
||||
<input data-testid="blur-target" type="text" style={INPUT_STYLE} />
|
||||
<EventLog entries={entries} />
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-input-text-fb-00000000-0000-0000-0000-000000000020',
|
||||
name: 'input-text-focus-blur-front-component',
|
||||
description: 'Front component covering focus/blur on text <input>',
|
||||
component: InputTextFocusBlurFrontComponent,
|
||||
});
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
|
||||
|
||||
const noop = () => undefined;
|
||||
|
||||
const InputTextPropertiesFrontComponent = () => (
|
||||
<FrontComponentCard title="input:text:properties">
|
||||
<input
|
||||
data-testid="subject"
|
||||
id={PROPERTY_FIXTURE.id}
|
||||
className={PROPERTY_FIXTURE.className}
|
||||
title={PROPERTY_FIXTURE.title}
|
||||
role={PROPERTY_FIXTURE.role}
|
||||
aria-label={PROPERTY_FIXTURE.ariaLabel}
|
||||
tabIndex={PROPERTY_FIXTURE.tabIndex}
|
||||
type={PROPERTY_FIXTURE.type}
|
||||
name={PROPERTY_FIXTURE.name}
|
||||
placeholder={PROPERTY_FIXTURE.placeholder}
|
||||
value={PROPERTY_FIXTURE.textValue}
|
||||
onChange={noop}
|
||||
/>
|
||||
</FrontComponentCard>
|
||||
);
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier:
|
||||
'fc-input-text-props-00000000-0000-0000-0000-000000000020',
|
||||
name: 'input-text-properties-front-component',
|
||||
description:
|
||||
'Front component covering property reflection on <input type="text">',
|
||||
component: InputTextPropertiesFrontComponent,
|
||||
});
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
EventLog,
|
||||
useEventLog,
|
||||
} from '@/__stories__/shared/front-components/event-log';
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import {
|
||||
INPUT_STYLE,
|
||||
LABEL_STYLE,
|
||||
SUBJECT_WRAPPER_STYLE,
|
||||
} from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const InputTextValueFrontComponent = () => {
|
||||
const [value, setValue] = useState('');
|
||||
const { entries, pushEvent } = useEventLog();
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="input:text:value">
|
||||
<div style={SUBJECT_WRAPPER_STYLE}>
|
||||
<label style={LABEL_STYLE}>Text input</label>
|
||||
<input
|
||||
data-testid="subject"
|
||||
type="text"
|
||||
placeholder="Type here..."
|
||||
value={value}
|
||||
onChange={(event) => {
|
||||
setValue(event.target.value);
|
||||
pushEvent(event);
|
||||
}}
|
||||
style={INPUT_STYLE}
|
||||
/>
|
||||
<span data-testid="front-component-value">{value}</span>
|
||||
</div>
|
||||
<EventLog entries={entries} />
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier:
|
||||
'fc-input-text-value-00000000-0000-0000-0000-000000000020',
|
||||
name: 'input-text-value-front-component',
|
||||
description: 'Front component covering <input type="text"> value round-trip',
|
||||
component: InputTextValueFrontComponent,
|
||||
});
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
EventLog,
|
||||
useEventLog,
|
||||
} from '@/__stories__/shared/front-components/event-log';
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { FILL_INLINE_STYLE } from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const LabelClickFrontComponent = () => {
|
||||
const [interactionCount, setInteractionCount] = useState(0);
|
||||
const { entries, pushEvent } = useEventLog();
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="label:click">
|
||||
<label
|
||||
data-testid="subject"
|
||||
onClick={(event) => {
|
||||
setInteractionCount((previous) => previous + 1);
|
||||
pushEvent(event);
|
||||
}}
|
||||
tabIndex={0}
|
||||
style={FILL_INLINE_STYLE}
|
||||
>
|
||||
label
|
||||
</label>
|
||||
<span data-testid="front-component-value">{interactionCount}</span>
|
||||
<EventLog entries={entries} />
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-label-c-click-00000000-0000-0000-0000-000000000020',
|
||||
name: 'label-click-front-component',
|
||||
description: 'Front component covering click on <label>',
|
||||
component: LabelClickFrontComponent,
|
||||
});
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { type Meta } from '@storybook/react-vite';
|
||||
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import {
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import {
|
||||
createHtmlTagClickStory,
|
||||
createHtmlTagFocusStory,
|
||||
} from '@/__stories__/shared/test-utils/createHtmlElementStory';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HtmlTag/Form/Label/Events',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Click = createHtmlTagClickStory({
|
||||
frontComponentBundleName: 'label-click',
|
||||
});
|
||||
|
||||
export const FocusBlur = createHtmlTagFocusStory({
|
||||
frontComponentBundleName: 'label-focus-blur',
|
||||
});
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
EventLog,
|
||||
useEventLog,
|
||||
} from '@/__stories__/shared/front-components/event-log';
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { FILL_INLINE_STYLE } from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const LabelFocusBlurFrontComponent = () => {
|
||||
const [interactionCount, setInteractionCount] = useState(0);
|
||||
const { entries, pushEvent } = useEventLog();
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="label:focus-blur">
|
||||
<label
|
||||
data-testid="subject"
|
||||
onFocus={(event) => {
|
||||
setInteractionCount((previous) => previous + 1);
|
||||
pushEvent(event);
|
||||
}}
|
||||
onBlur={(event) => pushEvent(event)}
|
||||
tabIndex={0}
|
||||
style={FILL_INLINE_STYLE}
|
||||
>
|
||||
label
|
||||
</label>
|
||||
<span data-testid="front-component-value">{interactionCount}</span>
|
||||
<EventLog entries={entries} />
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-label-fb-fb-00000000-0000-0000-0000-000000000020',
|
||||
name: 'label-focus-blur-front-component',
|
||||
description: 'Front component covering focus-blur on <label>',
|
||||
component: LabelFocusBlurFrontComponent,
|
||||
});
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
|
||||
|
||||
const LabelPropertiesFrontComponent = () => (
|
||||
<FrontComponentCard title="label:properties">
|
||||
<label
|
||||
data-testid="subject"
|
||||
id={PROPERTY_FIXTURE.id}
|
||||
className={PROPERTY_FIXTURE.className}
|
||||
title={PROPERTY_FIXTURE.title}
|
||||
role={PROPERTY_FIXTURE.role}
|
||||
aria-label={PROPERTY_FIXTURE.ariaLabel}
|
||||
tabIndex={PROPERTY_FIXTURE.tabIndex}
|
||||
>
|
||||
content
|
||||
</label>
|
||||
</FrontComponentCard>
|
||||
);
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-label-props-00000000-0000-0000-0000-000000000020',
|
||||
name: 'label-properties-front-component',
|
||||
description: 'Front component covering property reflection on <label>',
|
||||
component: LabelPropertiesFrontComponent,
|
||||
});
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { type Meta } from '@storybook/react-vite';
|
||||
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import {
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import { createPropertyReflectionStory } from '@/__stories__/shared/test-utils/createPropertyReflectionStory';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HtmlTag/Form/Label/Properties',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Properties = createPropertyReflectionStory({
|
||||
frontComponentBundleName: 'label-properties',
|
||||
});
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
EventLog,
|
||||
useEventLog,
|
||||
} from '@/__stories__/shared/front-components/event-log';
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { FILL_INLINE_STYLE } from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const LegendClickFrontComponent = () => {
|
||||
const [interactionCount, setInteractionCount] = useState(0);
|
||||
const { entries, pushEvent } = useEventLog();
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="legend:click">
|
||||
<fieldset>
|
||||
<legend
|
||||
data-testid="subject"
|
||||
onClick={(event) => {
|
||||
setInteractionCount((previous) => previous + 1);
|
||||
pushEvent(event);
|
||||
}}
|
||||
tabIndex={0}
|
||||
style={FILL_INLINE_STYLE}
|
||||
>
|
||||
legend
|
||||
</legend>
|
||||
</fieldset>
|
||||
<span data-testid="front-component-value">{interactionCount}</span>
|
||||
<EventLog entries={entries} />
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-legend-c-click-00000000-0000-0000-0000-000000000020',
|
||||
name: 'legend-click-front-component',
|
||||
description: 'Front component covering click on <legend>',
|
||||
component: LegendClickFrontComponent,
|
||||
});
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { type Meta } from '@storybook/react-vite';
|
||||
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import {
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import {
|
||||
createHtmlTagClickStory,
|
||||
createHtmlTagFocusStory,
|
||||
} from '@/__stories__/shared/test-utils/createHtmlElementStory';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HtmlTag/Form/Legend/Events',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Click = createHtmlTagClickStory({
|
||||
frontComponentBundleName: 'legend-click',
|
||||
});
|
||||
|
||||
export const FocusBlur = createHtmlTagFocusStory({
|
||||
frontComponentBundleName: 'legend-focus-blur',
|
||||
});
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
EventLog,
|
||||
useEventLog,
|
||||
} from '@/__stories__/shared/front-components/event-log';
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { FILL_INLINE_STYLE } from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const LegendFocusBlurFrontComponent = () => {
|
||||
const [interactionCount, setInteractionCount] = useState(0);
|
||||
const { entries, pushEvent } = useEventLog();
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="legend:focus-blur">
|
||||
<fieldset>
|
||||
<legend
|
||||
data-testid="subject"
|
||||
onFocus={(event) => {
|
||||
setInteractionCount((previous) => previous + 1);
|
||||
pushEvent(event);
|
||||
}}
|
||||
onBlur={(event) => pushEvent(event)}
|
||||
tabIndex={0}
|
||||
style={FILL_INLINE_STYLE}
|
||||
>
|
||||
legend
|
||||
</legend>
|
||||
</fieldset>
|
||||
<span data-testid="front-component-value">{interactionCount}</span>
|
||||
<EventLog entries={entries} />
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-legend-fb-fb-00000000-0000-0000-0000-000000000020',
|
||||
name: 'legend-focus-blur-front-component',
|
||||
description: 'Front component covering focus-blur on <legend>',
|
||||
component: LegendFocusBlurFrontComponent,
|
||||
});
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
EventLog,
|
||||
useEventLog,
|
||||
} from '@/__stories__/shared/front-components/event-log';
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { FILL_INLINE_STYLE } from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const MeterClickFrontComponent = () => {
|
||||
const [interactionCount, setInteractionCount] = useState(0);
|
||||
const { entries, pushEvent } = useEventLog();
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="meter:click">
|
||||
<meter
|
||||
data-testid="subject"
|
||||
value={0.5}
|
||||
min={0}
|
||||
max={1}
|
||||
onClick={(event) => {
|
||||
setInteractionCount((previous) => previous + 1);
|
||||
pushEvent(event);
|
||||
}}
|
||||
tabIndex={0}
|
||||
style={FILL_INLINE_STYLE}
|
||||
/>
|
||||
<span data-testid="front-component-value">{interactionCount}</span>
|
||||
<EventLog entries={entries} />
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-meter-c-click-00000000-0000-0000-0000-000000000020',
|
||||
name: 'meter-click-front-component',
|
||||
description: 'Front component covering click on <meter>',
|
||||
component: MeterClickFrontComponent,
|
||||
});
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { type Meta } from '@storybook/react-vite';
|
||||
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import {
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import {
|
||||
createHtmlTagClickStory,
|
||||
createHtmlTagFocusStory,
|
||||
} from '@/__stories__/shared/test-utils/createHtmlElementStory';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HtmlTag/Form/Meter/Events',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Click = createHtmlTagClickStory({
|
||||
frontComponentBundleName: 'meter-click',
|
||||
});
|
||||
|
||||
export const FocusBlur = createHtmlTagFocusStory({
|
||||
frontComponentBundleName: 'meter-focus-blur',
|
||||
});
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
EventLog,
|
||||
useEventLog,
|
||||
} from '@/__stories__/shared/front-components/event-log';
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { FILL_INLINE_STYLE } from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const MeterFocusBlurFrontComponent = () => {
|
||||
const [interactionCount, setInteractionCount] = useState(0);
|
||||
const { entries, pushEvent } = useEventLog();
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="meter:focus-blur">
|
||||
<meter
|
||||
data-testid="subject"
|
||||
value={0.5}
|
||||
min={0}
|
||||
max={1}
|
||||
onFocus={(event) => {
|
||||
setInteractionCount((previous) => previous + 1);
|
||||
pushEvent(event);
|
||||
}}
|
||||
onBlur={(event) => pushEvent(event)}
|
||||
tabIndex={0}
|
||||
style={FILL_INLINE_STYLE}
|
||||
/>
|
||||
<span data-testid="front-component-value">{interactionCount}</span>
|
||||
<EventLog entries={entries} />
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-meter-fb-fb-00000000-0000-0000-0000-000000000020',
|
||||
name: 'meter-focus-blur-front-component',
|
||||
description: 'Front component covering focus-blur on <meter>',
|
||||
component: MeterFocusBlurFrontComponent,
|
||||
});
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
|
||||
|
||||
const MeterPropertiesFrontComponent = () => (
|
||||
<FrontComponentCard title="meter:properties">
|
||||
<meter
|
||||
data-testid="subject"
|
||||
id={PROPERTY_FIXTURE.id}
|
||||
className={PROPERTY_FIXTURE.className}
|
||||
title={PROPERTY_FIXTURE.title}
|
||||
role={PROPERTY_FIXTURE.role}
|
||||
aria-label={PROPERTY_FIXTURE.ariaLabel}
|
||||
tabIndex={PROPERTY_FIXTURE.tabIndex}
|
||||
value={0.5}
|
||||
min={0}
|
||||
max={1}
|
||||
/>
|
||||
</FrontComponentCard>
|
||||
);
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-meter-props-00000000-0000-0000-0000-000000000020',
|
||||
name: 'meter-properties-front-component',
|
||||
description: 'Front component covering property reflection on <meter>',
|
||||
component: MeterPropertiesFrontComponent,
|
||||
});
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { type Meta } from '@storybook/react-vite';
|
||||
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import {
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import { createPropertyReflectionStory } from '@/__stories__/shared/test-utils/createPropertyReflectionStory';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HtmlTag/Form/Meter/Properties',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Properties = createPropertyReflectionStory({
|
||||
frontComponentBundleName: 'meter-properties',
|
||||
extraAttributes: {
|
||||
value: '0.5',
|
||||
min: '0',
|
||||
max: '1',
|
||||
},
|
||||
});
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
EventLog,
|
||||
useEventLog,
|
||||
} from '@/__stories__/shared/front-components/event-log';
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
|
||||
const OptgroupClickFrontComponent = () => {
|
||||
const [interactionCount, setInteractionCount] = useState(0);
|
||||
const { entries, pushEvent } = useEventLog();
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="optgroup:click">
|
||||
<select defaultValue="x">
|
||||
<optgroup
|
||||
data-testid="subject"
|
||||
label="group"
|
||||
onClick={(event) => {
|
||||
setInteractionCount((previous) => previous + 1);
|
||||
pushEvent(event);
|
||||
}}
|
||||
>
|
||||
<option value="x">inside</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<span data-testid="front-component-value">{interactionCount}</span>
|
||||
<EventLog entries={entries} />
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier:
|
||||
'fc-optgroup-c-click-00000000-0000-0000-0000-000000000020',
|
||||
name: 'optgroup-click-front-component',
|
||||
description: 'Front component covering click on <optgroup>',
|
||||
component: OptgroupClickFrontComponent,
|
||||
});
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { type Meta } from '@storybook/react-vite';
|
||||
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import {
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import { createHtmlTagClickStory } from '@/__stories__/shared/test-utils/createHtmlElementStory';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HtmlTag/Form/Optgroup/Events',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Click = createHtmlTagClickStory({
|
||||
frontComponentBundleName: 'optgroup-click',
|
||||
});
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
EventLog,
|
||||
useEventLog,
|
||||
} from '@/__stories__/shared/front-components/event-log';
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
|
||||
const OptionClickFrontComponent = () => {
|
||||
const [interactionCount, setInteractionCount] = useState(0);
|
||||
const { entries, pushEvent } = useEventLog();
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="option:click">
|
||||
<select defaultValue="alpha">
|
||||
<>
|
||||
<option
|
||||
data-testid="subject"
|
||||
value="alpha"
|
||||
onClick={(event) => {
|
||||
setInteractionCount((previous) => previous + 1);
|
||||
pushEvent(event);
|
||||
}}
|
||||
>
|
||||
alpha
|
||||
</option>
|
||||
<option value="beta">beta</option>
|
||||
</>
|
||||
</select>
|
||||
<span data-testid="front-component-value">{interactionCount}</span>
|
||||
<EventLog entries={entries} />
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-option-c-click-00000000-0000-0000-0000-000000000020',
|
||||
name: 'option-click-front-component',
|
||||
description: 'Front component covering click on <option>',
|
||||
component: OptionClickFrontComponent,
|
||||
});
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { type Meta } from '@storybook/react-vite';
|
||||
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import {
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import { createHtmlTagClickStory } from '@/__stories__/shared/test-utils/createHtmlElementStory';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HtmlTag/Form/Option/Events',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Click = createHtmlTagClickStory({
|
||||
frontComponentBundleName: 'option-click',
|
||||
});
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
EventLog,
|
||||
useEventLog,
|
||||
} from '@/__stories__/shared/front-components/event-log';
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { FILL_INLINE_STYLE } from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const OutputClickFrontComponent = () => {
|
||||
const [interactionCount, setInteractionCount] = useState(0);
|
||||
const { entries, pushEvent } = useEventLog();
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="output:click">
|
||||
<output
|
||||
data-testid="subject"
|
||||
onClick={(event) => {
|
||||
setInteractionCount((previous) => previous + 1);
|
||||
pushEvent(event);
|
||||
}}
|
||||
tabIndex={0}
|
||||
style={FILL_INLINE_STYLE}
|
||||
>
|
||||
output
|
||||
</output>
|
||||
<span data-testid="front-component-value">{interactionCount}</span>
|
||||
<EventLog entries={entries} />
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-output-c-click-00000000-0000-0000-0000-000000000020',
|
||||
name: 'output-click-front-component',
|
||||
description: 'Front component covering click on <output>',
|
||||
component: OutputClickFrontComponent,
|
||||
});
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { type Meta } from '@storybook/react-vite';
|
||||
|
||||
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
|
||||
import {
|
||||
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
resetFrontComponentStoryMocks,
|
||||
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
|
||||
import {
|
||||
createHtmlTagClickStory,
|
||||
createHtmlTagFocusStory,
|
||||
} from '@/__stories__/shared/test-utils/createHtmlElementStory';
|
||||
|
||||
const meta: Meta<typeof FrontComponentRenderer> = {
|
||||
title: 'FrontComponent/HtmlTag/Form/Output/Events',
|
||||
component: FrontComponentRenderer,
|
||||
parameters: { layout: 'centered' },
|
||||
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
|
||||
beforeEach: resetFrontComponentStoryMocks,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Click = createHtmlTagClickStory({
|
||||
frontComponentBundleName: 'output-click',
|
||||
});
|
||||
|
||||
export const FocusBlur = createHtmlTagFocusStory({
|
||||
frontComponentBundleName: 'output-focus-blur',
|
||||
});
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
EventLog,
|
||||
useEventLog,
|
||||
} from '@/__stories__/shared/front-components/event-log';
|
||||
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
|
||||
import { FILL_INLINE_STYLE } from '@/__stories__/shared/front-components/styles';
|
||||
|
||||
const OutputFocusBlurFrontComponent = () => {
|
||||
const [interactionCount, setInteractionCount] = useState(0);
|
||||
const { entries, pushEvent } = useEventLog();
|
||||
|
||||
return (
|
||||
<FrontComponentCard title="output:focus-blur">
|
||||
<output
|
||||
data-testid="subject"
|
||||
onFocus={(event) => {
|
||||
setInteractionCount((previous) => previous + 1);
|
||||
pushEvent(event);
|
||||
}}
|
||||
onBlur={(event) => pushEvent(event)}
|
||||
tabIndex={0}
|
||||
style={FILL_INLINE_STYLE}
|
||||
>
|
||||
output
|
||||
</output>
|
||||
<span data-testid="front-component-value">{interactionCount}</span>
|
||||
<EventLog entries={entries} />
|
||||
</FrontComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'fc-output-fb-fb-00000000-0000-0000-0000-000000000020',
|
||||
name: 'output-focus-blur-front-component',
|
||||
description: 'Front component covering focus-blur on <output>',
|
||||
component: OutputFocusBlurFrontComponent,
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user