Compare commits
27
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f37be206aa | ||
|
|
bac1fd8f8b | ||
|
|
85d649e831 | ||
|
|
8c2524937e | ||
|
|
2f4ebf8160 | ||
|
|
8119bba07d | ||
|
|
77603a4102 | ||
|
|
91ce59d8e2 | ||
|
|
d134c27f10 | ||
|
|
c76dd7ba07 | ||
|
|
056e3a4cd8 | ||
|
|
9876af5587 | ||
|
|
bbe866f3f8 | ||
|
|
563acc3f57 | ||
|
|
452433a9ae | ||
|
|
3bda05ea57 | ||
|
|
0e01703e25 | ||
|
|
5bef586960 | ||
|
|
eda41b4eba | ||
|
|
de044f4b45 | ||
|
|
e3c79c803c | ||
|
|
59d69e2f5c | ||
|
|
4554dbe3c9 | ||
|
|
efba71a938 | ||
|
|
e6cc783a23 | ||
|
|
76e144e85a | ||
|
|
8f24cda586 |
@@ -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,7 +1,6 @@
|
||||
name: 'Preview Environment Dispatch'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
@@ -11,7 +10,6 @@ on:
|
||||
- packages/twenty-server/**
|
||||
- packages/twenty-front/**
|
||||
- .github/workflows/preview-env-dispatch.yaml
|
||||
- .github/workflows/preview-env-keepalive.yaml
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -35,28 +33,15 @@ jobs:
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger preview environment workflow
|
||||
- name: Dispatch preview-env to ci-privileged
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
run: |
|
||||
gh api repos/"$REPOSITORY"/dispatches \
|
||||
gh api repos/twentyhq/ci-privileged/dispatches \
|
||||
-f event_type=preview-environment \
|
||||
-f "client_payload[pr_number]=$PR_NUMBER" \
|
||||
-f "client_payload[pr_head_sha]=$PR_HEAD_SHA" \
|
||||
-f "client_payload[repo_full_name]=$REPOSITORY"
|
||||
|
||||
- name: Dispatch to ci-privileged for PR comment
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
KEEPALIVE_DISPATCH_TIME: ${{ github.event.pull_request.updated_at }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
run: |
|
||||
gh api repos/twentyhq/ci-privileged/dispatches \
|
||||
-f event_type=preview-env-url \
|
||||
-f "client_payload[pr_number]=$PR_NUMBER" \
|
||||
-f "client_payload[keepalive_dispatch_time]=$KEEPALIVE_DISPATCH_TIME" \
|
||||
-f "client_payload[repo]=$REPOSITORY"
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
name: 'Preview Environment Keep Alive'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [preview-environment]
|
||||
|
||||
jobs:
|
||||
preview-environment:
|
||||
timeout-minutes: 310
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
ref: ${{ github.event.client_payload.pr_head_sha }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Run compose setup
|
||||
run: |
|
||||
echo "Patching docker-compose.yml..."
|
||||
# change image to localbuild using yq
|
||||
yq eval 'del(.services.server.image)' -i packages/twenty-docker/docker-compose.yml
|
||||
yq eval '.services.server.build.context = "../../"' -i packages/twenty-docker/docker-compose.yml
|
||||
yq eval '.services.server.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i packages/twenty-docker/docker-compose.yml
|
||||
yq eval '.services.server.build.target = "twenty"' -i packages/twenty-docker/docker-compose.yml
|
||||
|
||||
yq eval 'del(.services.worker.image)' -i packages/twenty-docker/docker-compose.yml
|
||||
yq eval '.services.worker.build.context = "../../"' -i packages/twenty-docker/docker-compose.yml
|
||||
yq eval '.services.worker.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i packages/twenty-docker/docker-compose.yml
|
||||
yq eval '.services.worker.build.target = "twenty"' -i packages/twenty-docker/docker-compose.yml
|
||||
|
||||
echo "Adding SIGN_IN_PREFILLED environment variable to server service..."
|
||||
yq eval '.services.server.environment.SIGN_IN_PREFILLED = "${SIGN_IN_PREFILLED}"' -i packages/twenty-docker/docker-compose.yml
|
||||
|
||||
echo "Setting up .env file..."
|
||||
cp packages/twenty-docker/.env.example packages/twenty-docker/.env
|
||||
|
||||
echo "Generating secrets..."
|
||||
echo "" >> packages/twenty-docker/.env
|
||||
echo "# === Randomly generated secrets ===" >> packages/twenty-docker/.env
|
||||
echo "APP_SECRET=$(openssl rand -base64 32)" >> packages/twenty-docker/.env
|
||||
echo "PG_DATABASE_PASSWORD=$(openssl rand -hex 16)" >> packages/twenty-docker/.env
|
||||
echo "SIGN_IN_PREFILLED=true" >> packages/twenty-docker/.env
|
||||
echo "Docker compose build..."
|
||||
cd packages/twenty-docker/
|
||||
docker compose build
|
||||
working-directory: ./
|
||||
|
||||
- name: Create Tunnel
|
||||
id: expose-tunnel
|
||||
env:
|
||||
CLOUDFLARED_VERSION: '2026.3.0'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Install cloudflared (pinned for reproducibility)
|
||||
sudo curl -fsSL -o /usr/local/bin/cloudflared \
|
||||
"https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64"
|
||||
sudo chmod +x /usr/local/bin/cloudflared
|
||||
cloudflared --version
|
||||
|
||||
# Start an account-less "quick tunnel" pointing at the server container.
|
||||
# Cloudflare prints the assigned https://*.trycloudflare.com URL into the log.
|
||||
log_file="$RUNNER_TEMP/cloudflared.log"
|
||||
: > "$log_file"
|
||||
|
||||
cloudflared tunnel \
|
||||
--url http://localhost:3000 \
|
||||
--no-autoupdate \
|
||||
--logfile "$log_file" \
|
||||
--loglevel info \
|
||||
> "$RUNNER_TEMP/cloudflared.stdout" 2>&1 &
|
||||
|
||||
pid=$!
|
||||
echo "$pid" > "$RUNNER_TEMP/cloudflared.pid"
|
||||
echo "cloudflared PID: $pid"
|
||||
|
||||
# Wait up to 2 minutes for the URL to appear; fail fast if cloudflared exits.
|
||||
url=''
|
||||
for _ in $(seq 1 60); do
|
||||
url=$(grep -oE 'https://[a-zA-Z0-9-]+\.trycloudflare\.com' "$log_file" 2>/dev/null | head -n1 || true)
|
||||
[ -n "$url" ] && break
|
||||
if ! kill -0 "$pid" 2>/dev/null; then
|
||||
echo "cloudflared exited before producing a URL"
|
||||
cat "$log_file" || true
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if [ -z "$url" ]; then
|
||||
echo "Timed out waiting for tunnel URL"
|
||||
cat "$log_file" || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Tunnel URL: $url"
|
||||
echo "tunnel-url=$url" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Start services with correct SERVER_URL
|
||||
env:
|
||||
TUNNEL_URL: ${{ steps.expose-tunnel.outputs.tunnel-url }}
|
||||
run: |
|
||||
cd packages/twenty-docker/
|
||||
|
||||
echo "Setting SERVER_URL to $TUNNEL_URL"
|
||||
sed -i '/SERVER_URL=/d' .env
|
||||
echo "" >> .env
|
||||
echo "SERVER_URL=$TUNNEL_URL" >> .env
|
||||
|
||||
# Start the services
|
||||
echo "Docker compose up..."
|
||||
docker compose up -d || {
|
||||
echo "Docker compose failed to start"
|
||||
docker compose logs
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "Waiting for services to be ready..."
|
||||
count=0
|
||||
while [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-db-1) = "healthy" ] || [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-server-1) = "healthy" ]; do
|
||||
sleep 5
|
||||
count=$((count+1))
|
||||
if [ $count -gt 60 ]; then
|
||||
echo "Timeout waiting for services to be ready"
|
||||
docker compose logs
|
||||
exit 1
|
||||
fi
|
||||
echo "Still waiting for services... ($count/60)"
|
||||
done
|
||||
|
||||
echo "All services are up and running!"
|
||||
working-directory: ./
|
||||
|
||||
- name: Seed Dev Workspace
|
||||
run: |
|
||||
cd packages/twenty-docker/
|
||||
echo "Seeding light dev workspace (Apple only)..."
|
||||
if ! docker compose exec -T server yarn command:prod workspace:seed:dev --light; then
|
||||
echo "❌ Seeding light dev workspace failed. Dumping server logs..."
|
||||
docker compose logs server
|
||||
exit 1
|
||||
fi
|
||||
working-directory: ./
|
||||
|
||||
- name: Output tunnel URL
|
||||
env:
|
||||
TUNNEL_URL: ${{ steps.expose-tunnel.outputs.tunnel-url }}
|
||||
run: |
|
||||
echo "✅ Preview Environment Ready!"
|
||||
echo "🔗 Preview URL: $TUNNEL_URL"
|
||||
echo "⏱️ This environment will be available for 5 hours"
|
||||
echo "## 🚀 Preview Environment Ready!" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "Preview URL: $TUNNEL_URL" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "This environment will automatically shut down after 5 hours." >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "$TUNNEL_URL" > tunnel-url.txt
|
||||
|
||||
- name: Upload tunnel URL artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: tunnel-url
|
||||
path: tunnel-url.txt
|
||||
retention-days: 1
|
||||
|
||||
- name: Keep tunnel alive for 5 hours
|
||||
run: timeout 300m sleep 18000 # Stop on whichever we reach first (300m or 5hour sleep)
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: |
|
||||
if [ -f "$RUNNER_TEMP/cloudflared.pid" ]; then
|
||||
kill "$(cat "$RUNNER_TEMP/cloudflared.pid")" 2>/dev/null || true
|
||||
fi
|
||||
cd packages/twenty-docker/
|
||||
docker compose down -v
|
||||
working-directory: ./
|
||||
@@ -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!
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -16,10 +16,9 @@ Jeder Schlüssel enthält einen `publicKey` (unbefristet aufbewahrt, damit er zu
|
||||
|
||||
### Aktuellen Schlüssel rotieren
|
||||
|
||||
* **Manuell** — **Settings → Admin Panel → Signing keys → Revoke** in der aktuellen Zeile. Das Widerrufen löscht das verschlüsselte private Material und stuft es herab; der nächste Signieraufruf erstellt automatisch ein neues ES256-Schlüsselpaar als neuen aktuellen Schlüssel. Tokens, die unter einem anderen (nicht widerrufenen) `kid` signiert wurden, werden weiterhin verifiziert, bis sie ablaufen.
|
||||
* **Enterprise (automatisch)** — ein täglicher Cron-Job (`'15 3 * * *'` UTC) erstellt einen neuen aktuellen Schlüssel, sobald der vorhandene Schlüssel seit `SIGNING_KEY_ROTATION_DAYS` (Standard `90`) aktuell ist. Der vorherige Schlüssel wird *nicht* widerrufen, daher werden Tokens, die darunter signiert wurden, weiterhin verifiziert. Registriere ihn einmalig mit `yarn command:prod cron:register:all`.
|
||||
Setzen Sie `SIGNING_KEY_ROTATION_DAYS`, um die Funktion zu aktivieren: Ein täglicher Cronjob erstellt dann einen neuen aktuellen Schlüssel, sobald der bestehende Schlüssel älter als dieser Schwellenwert ist. Frühere Schlüssel werden *nicht* widerrufen, daher werden Tokens, die darunter signiert wurden, weiterhin verifiziert. Lassen Sie die Variable ungesetzt, um die automatische Rotation zu deaktivieren.
|
||||
|
||||
<Note>Der Enterprise-Cron-Job und `SIGNING_KEY_ROTATION_DAYS` sind ab v2.6+ enthalten.</Note>
|
||||
<Note>Die automatische Rotation ist ab Version v2.6 verfügbar.</Note>
|
||||
|
||||
### Einen Schlüssel widerrufen (nur bei Leak / Notfall)
|
||||
|
||||
|
||||
@@ -16,10 +16,9 @@ Cada chave carrega uma `publicKey` (mantida indefinidamente para que possa verif
|
||||
|
||||
### Rotacionar a chave atual
|
||||
|
||||
* **Manual** — **Settings → Admin Panel → Signing keys → Revoke** na linha atual. A revogação apaga o material privado criptografado e o rebaixa; a próxima chamada de assinatura gera automaticamente um novo par de chaves ES256 como o novo atual. Tokens assinados sob qualquer outro `kid` (não revogado) continuam sendo verificados até expirarem.
|
||||
* **Enterprise (automático)** — um cron diário (`'15 3 * * *'` UTC) emite uma nova chave atual assim que a existente tiver sido atual por `SIGNING_KEY_ROTATION_DAYS` (padrão `90`). A chave anterior *não* é revogada, então tokens assinados sob ela continuam sendo verificados. Registre-o uma vez com `yarn command:prod cron:register:all`.
|
||||
Defina `SIGNING_KEY_ROTATION_DAYS` para ativar: um cron diário então gera uma nova chave e a define como atual quando a existente for mais antiga do que esse limite. Chaves anteriores *não* são revogadas, então tokens assinados sob elas continuam sendo verificados. Deixe a variável não definida para desativar a rotação automática.
|
||||
|
||||
<Note>O cron do Enterprise e `SIGNING_KEY_ROTATION_DAYS` são disponibilizados a partir da v2.6+.</Note>
|
||||
<Note>A rotação automática está disponível a partir da v2.6+.</Note>
|
||||
|
||||
### Revogar uma chave (apenas vazamento / emergência)
|
||||
|
||||
|
||||
@@ -16,10 +16,9 @@ Fiecare cheie are un `publicKey` (păstrat pe termen nelimitat pentru a putea ve
|
||||
|
||||
### Rotește cheia curentă
|
||||
|
||||
* **Manual** — **Settings → Admin Panel → Signing keys → Revoke** pe rândul curent. Revocarea șterge materialul privat criptat și o retrogradează; următorul apel de semnare generează automat o nouă pereche de chei ES256 ca noua cheie curentă. Token-urile semnate sub orice alt `kid` (nerevocat) continuă să fie verificate până la expirare.
|
||||
* **Enterprise (automat)** — un cron zilnic (`'15 3 * * *'` UTC) emite o nouă cheie curentă după ce cea existentă a fost curentă timp de `SIGNING_KEY_ROTATION_DAYS` (implicit `90`). Cheia anterioară *nu* este revocată, astfel încât token-urile semnate cu ea continuă să fie verificate. Înregistrează-l o singură dată cu `yarn command:prod cron:register:all`.
|
||||
Setați `SIGNING_KEY_ROTATION_DAYS` pentru a activa rotația automată: un job cron zilnic va emite o nouă cheie curentă odată ce cea existentă este mai veche decât acest prag. Cheile anterioare *nu* sunt revocate, astfel încât token-urile semnate cu ele continuă să fie verificate. Lăsați variabila nesetată pentru a dezactiva rotația automată.
|
||||
|
||||
<Note>Cron-ul Enterprise și `SIGNING_KEY_ROTATION_DAYS` sunt disponibile în v2.6+.</Note>
|
||||
<Note>Rotația automată este disponibilă începând cu versiunea v2.6+.</Note>
|
||||
|
||||
### Revocă o cheie (numai în caz de scurgere / urgență)
|
||||
|
||||
|
||||
@@ -16,10 +16,9 @@ icon: rotate
|
||||
|
||||
### Выполнить ротацию текущего ключа
|
||||
|
||||
* **Вручную** — **Settings → Admin Panel → Signing keys → Revoke** в текущей строке. Отзыв стирает его зашифрованный закрытый материал и понижает его; следующий вызов подписи автоматически создает новую пару ключей ES256 как новый текущий ключ. Токены, подписанные любым другим (не отозванным) `kid`, продолжают успешно проходить проверку до истечения срока действия.
|
||||
* **Enterprise (автоматически)** — ежедневный cron (`'15 3 * * *'` UTC) выпускает новый текущий ключ, как только существующий ключ находится в статусе текущего в течение `SIGNING_KEY_ROTATION_DAYS` (по умолчанию `90`). Предыдущий ключ *не* отзывается, поэтому токены, подписанные им, продолжают успешно проходить проверку. Зарегистрируйте его один раз с помощью `yarn command:prod cron:register:all`.
|
||||
Установите `SIGNING_KEY_ROTATION_DAYS`, чтобы включить эту опцию: ежедневный cron затем будет выпускать новый текущий ключ, как только существующий ключ станет старше этого порогового значения. Предыдущие ключи *не* отзываются, поэтому токены, подписанные ими, продолжают успешно проходить проверку. Оставьте переменную не заданной, чтобы отключить автоматическую ротацию.
|
||||
|
||||
<Note>Enterprise-cron и `SIGNING_KEY_ROTATION_DAYS` поставляются начиная с v2.6+.</Note>
|
||||
<Note>Автоматическая ротация доступна, начиная с версии v2.6+.</Note>
|
||||
|
||||
### Отозвать ключ (только при утечке / в экстренных случаях)
|
||||
|
||||
|
||||
@@ -16,10 +16,9 @@ Her anahtar bir `publicKey` (önceden verilmiş belirteçleri doğrulayabilmesi
|
||||
|
||||
### Geçerli anahtarı döndür
|
||||
|
||||
* **El ile** — geçerli satırda **Settings → Admin Panel → Signing keys → Revoke**. İptal etmek, şifrelenmiş özel materyalini siler ve onu geçerli olmaktan çıkarır; bir sonraki imzalama çağrısı, yeni geçerli olarak otomatik olarak yeni bir ES256 anahtar çifti oluşturur. Herhangi başka bir (iptal edilmemiş) `kid` ile imzalanan belirteçler, süreleri dolana kadar doğrulanmaya devam eder.
|
||||
* **Kurumsal (otomatik)** — günlük bir cron (`'15 3 * * *'` UTC), mevcut anahtar `SIGNING_KEY_ROTATION_DAYS` (varsayılan `90`) kadar süredir geçerli olduğunda yeni bir geçerli anahtar verir. Önceki anahtar *iptal edilmez*, bu nedenle onunla imzalanan belirteçler doğrulanmaya devam eder. `yarn command:prod cron:register:all` ile bir kez kaydedin.
|
||||
Etkinleştirmek için `SIGNING_KEY_ROTATION_DAYS` değerini ayarlayın: günlük bir cron görevi, mevcut anahtar bu eşikten daha eski olduğunda yeni bir güncel anahtar oluşturur. Önceki anahtarlar *iptal edilmez*, bu nedenle onlarla imzalanmış belirteçler doğrulanmaya devam eder. Otomatik döndürmeyi devre dışı bırakmak için değişkeni ayarlamadan bırakın.
|
||||
|
||||
<Note>Kurumsal cron ve `SIGNING_KEY_ROTATION_DAYS` v2.6+ ile birlikte gelir.</Note>
|
||||
<Note>Otomatik döndürme v2.6+ ile birlikte gelir.</Note>
|
||||
|
||||
### Bir anahtarı iptal et (yalnızca sızıntı / acil durum için)
|
||||
|
||||
|
||||
@@ -16,10 +16,9 @@ Twenty 具有两个相互独立的密钥族:
|
||||
|
||||
### 轮换当前密钥
|
||||
|
||||
* **手动** — 在当前行上执行 **Settings → Admin Panel → Signing keys → Revoke**。 吊销操作会清除其加密的私有材料并将其降级;下一次签名调用会自动生成一个新的 ES256 密钥对,作为新的当前密钥。 在任何其他(未被吊销的)`kid` 下签名的令牌会一直验证,直到过期。
|
||||
* **Enterprise(自动)** — 每日 cron(`'15 3 * * *'` UTC)会在现有密钥已作为当前密钥使用 `SIGNING_KEY_ROTATION_DAYS`(默认 `90`)后签发一个新的当前密钥。 先前的密钥*不会*被吊销,因此在其下签名的令牌仍然可以通过验证。 使用 `yarn command:prod cron:register:all` 注册一次。
|
||||
将 `SIGNING_KEY_ROTATION_DAYS` 设为启用该功能:然后每日的 cron 任务会在现有密钥超过该阈值后签发新的当前密钥。 先前的密钥*不会*被吊销,因此在其下签名的令牌仍然可以通过验证。 将变量保持未设置以禁用自动轮换。
|
||||
|
||||
<Note>Enterprise 的 cron 和 `SIGNING_KEY_ROTATION_DAYS` 从 v2.6+ 版本开始提供。</Note>
|
||||
<Note>自动轮换在 v2.6+ 中提供。</Note>
|
||||
|
||||
### 吊销密钥(仅限泄露 / 紧急情况)
|
||||
|
||||
|
||||
@@ -1,4 +1,38 @@
|
||||
import { Locator, Page } from '@playwright/test';
|
||||
import {
|
||||
expect,
|
||||
type Locator,
|
||||
type Page,
|
||||
type Response,
|
||||
} from '@playwright/test';
|
||||
|
||||
type CreateObjectParams = {
|
||||
singularLabel: string;
|
||||
pluralLabel: string;
|
||||
};
|
||||
|
||||
type CreateObjectMetadataResponseBody = {
|
||||
data?: {
|
||||
createOneObject?: {
|
||||
namePlural?: string;
|
||||
};
|
||||
};
|
||||
errors?: unknown[];
|
||||
};
|
||||
|
||||
const isCreateObjectMetadataResponse = (response: Response) => {
|
||||
if (!response.url().endsWith('/metadata')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return (
|
||||
response.request().postDataJSON().operationName ===
|
||||
'CreateOneObjectMetadataItem'
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export class DataModelSection {
|
||||
private readonly searchObjectInput: Locator;
|
||||
@@ -186,4 +220,48 @@ export class DataModelSection {
|
||||
async clickSaveButton() {
|
||||
await this.saveButton.click();
|
||||
}
|
||||
|
||||
async createObject({ singularLabel, pluralLabel }: CreateObjectParams) {
|
||||
await Promise.all([
|
||||
this.page.waitForURL(/\/settings\/objects\/new/),
|
||||
this.clickAddObjectButton(),
|
||||
]);
|
||||
|
||||
await this.typeObjectSingularName(singularLabel);
|
||||
await this.typeObjectPluralName(pluralLabel);
|
||||
await this.objectPluralNameInput.blur();
|
||||
|
||||
await expect(this.saveButton).toBeEnabled();
|
||||
|
||||
const createObjectResponsePromise = this.page.waitForResponse(
|
||||
isCreateObjectMetadataResponse,
|
||||
);
|
||||
|
||||
await this.clickSaveButton();
|
||||
|
||||
const createObjectResponse = await createObjectResponsePromise;
|
||||
const createObjectResponseBody =
|
||||
(await createObjectResponse.json()) as CreateObjectMetadataResponseBody;
|
||||
|
||||
if (!createObjectResponse.ok() || createObjectResponseBody.errors?.length) {
|
||||
throw new Error(
|
||||
`Object creation failed: ${JSON.stringify(createObjectResponseBody)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const createdObjectNamePlural =
|
||||
createObjectResponseBody.data?.createOneObject?.namePlural;
|
||||
|
||||
if (createdObjectNamePlural === undefined) {
|
||||
throw new Error(
|
||||
`Object creation response is missing createOneObject.namePlural: ${JSON.stringify(
|
||||
createObjectResponseBody,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
await this.page.waitForURL(
|
||||
new RegExp(`/settings/objects/${createdObjectNamePlural}(?:[?#]|$)`),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import { Page } from '@playwright/test';
|
||||
import { type Page } from '@playwright/test';
|
||||
|
||||
const MAC = process.platform === 'darwin';
|
||||
async function isMacBrowser(page: Page) {
|
||||
return page.evaluate(() => {
|
||||
const navigatorWithUserAgentData = navigator as Navigator & {
|
||||
userAgentData?: { platform?: string };
|
||||
};
|
||||
const platform =
|
||||
navigatorWithUserAgentData.userAgentData?.platform ?? navigator.platform;
|
||||
|
||||
return /mac|iphone|ipad|ipod/i.test(platform);
|
||||
});
|
||||
}
|
||||
|
||||
async function keyDownCtrlOrMeta(page: Page) {
|
||||
if (MAC) {
|
||||
if (await isMacBrowser(page)) {
|
||||
await page.keyboard.down('Meta');
|
||||
} else {
|
||||
await page.keyboard.down('Control');
|
||||
@@ -11,7 +21,7 @@ async function keyDownCtrlOrMeta(page: Page) {
|
||||
}
|
||||
|
||||
async function keyUpCtrlOrMeta(page: Page) {
|
||||
if (MAC) {
|
||||
if (await isMacBrowser(page)) {
|
||||
await page.keyboard.up('Meta');
|
||||
} else {
|
||||
await page.keyboard.up('Control');
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import { type Page } from '@playwright/test';
|
||||
|
||||
import { expect, test } from '../lib/fixtures/screenshot';
|
||||
import { DataModelSection } from '../lib/pom/settings/dataModelSection';
|
||||
import { withCtrlOrMeta } from '../lib/utils/keyboardShortcuts';
|
||||
|
||||
const sidePanel = (page: Page) => page.locator('[data-side-panel]');
|
||||
const commandMenuSearchInput = (page: Page) =>
|
||||
page.getByTestId('side-panel-focus');
|
||||
|
||||
async function openCommandMenuWithShortcut(page: Page) {
|
||||
await expect(page.getByTestId('page-header-side-panel-button')).toBeVisible();
|
||||
|
||||
await withCtrlOrMeta(page, () => page.keyboard.press('k'));
|
||||
|
||||
await expect(commandMenuSearchInput(page)).toBeVisible();
|
||||
await expect(commandMenuSearchInput(page)).toBeFocused();
|
||||
}
|
||||
|
||||
async function openCommandMenuWithHeaderButton(page: Page) {
|
||||
const sidePanelButton = page.getByTestId('page-header-side-panel-button');
|
||||
|
||||
await expect(sidePanelButton).toBeVisible();
|
||||
await sidePanelButton.click();
|
||||
|
||||
await expect(commandMenuSearchInput(page)).toBeVisible();
|
||||
await expect(commandMenuSearchInput(page)).toBeFocused();
|
||||
}
|
||||
|
||||
async function searchCommandMenu(page: Page, query: string) {
|
||||
await commandMenuSearchInput(page).fill(query);
|
||||
await expect(commandMenuSearchInput(page)).toHaveValue(query);
|
||||
}
|
||||
|
||||
async function selectVisibleCommandMenuItem(page: Page, label: string) {
|
||||
await expect(sidePanel(page).getByText(label, { exact: true })).toBeVisible();
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
}
|
||||
|
||||
async function expectCreateRecordForm(page: Page, objectLabelSingular: string) {
|
||||
await expect(
|
||||
sidePanel(page).getByText(`New ${objectLabelSingular}`, { exact: true }),
|
||||
).toBeVisible();
|
||||
}
|
||||
|
||||
async function fillCreateRecordName(page: Page, name: string) {
|
||||
const nameInput = sidePanel(page).getByRole('textbox').first();
|
||||
|
||||
await expect(nameInput).toBeVisible();
|
||||
await nameInput.fill(name);
|
||||
}
|
||||
|
||||
async function expectCreateRecordSubmitButton(page: Page) {
|
||||
const createButton = sidePanel(page)
|
||||
.getByRole('button', { name: /Create/ })
|
||||
.last();
|
||||
|
||||
await expect(createButton).toBeVisible();
|
||||
await expect(createButton.locator('svg')).toHaveCount(1);
|
||||
await expect(createButton).toContainText(/(Ctrl\s*⏎|⌘⏎)/);
|
||||
}
|
||||
|
||||
async function submitCreateRecordFormWithShortcut(page: Page) {
|
||||
await withCtrlOrMeta(page, () => page.keyboard.press('Enter'));
|
||||
}
|
||||
|
||||
test.describe.serial('Dynamic create record commands', () => {
|
||||
test.setTimeout(90_000);
|
||||
|
||||
const suffix = `A${Date.now().toString(36).replace(/[0-9]/g, '')}`;
|
||||
const customObjectSingularLabel = `Codex Vessel ${suffix}`;
|
||||
const customObjectPluralLabel = `Codex Vessels ${suffix}`;
|
||||
const customRecordName = `E2E Record ${suffix}`;
|
||||
|
||||
test('custom object command works from keyboard shortcut', async ({
|
||||
page,
|
||||
}) => {
|
||||
const dataModelSection = new DataModelSection(page);
|
||||
|
||||
await page.goto('/settings/objects');
|
||||
await dataModelSection.createObject({
|
||||
singularLabel: customObjectSingularLabel,
|
||||
pluralLabel: customObjectPluralLabel,
|
||||
});
|
||||
|
||||
await page.goto('/objects/people');
|
||||
await openCommandMenuWithShortcut(page);
|
||||
await searchCommandMenu(page, `create ${customObjectSingularLabel}`);
|
||||
await selectVisibleCommandMenuItem(
|
||||
page,
|
||||
`Create ${customObjectSingularLabel}`,
|
||||
);
|
||||
|
||||
await expectCreateRecordForm(page, customObjectSingularLabel);
|
||||
await fillCreateRecordName(page, customRecordName);
|
||||
await expectCreateRecordSubmitButton(page);
|
||||
await submitCreateRecordFormWithShortcut(page);
|
||||
|
||||
await expect(
|
||||
sidePanel(page).getByText(customRecordName, { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
sidePanel(page).getByText(`New ${customObjectSingularLabel}`, {
|
||||
exact: true,
|
||||
}),
|
||||
).not.toBeVisible();
|
||||
|
||||
await page.getByTestId('page-header-side-panel-button').click();
|
||||
await expect(commandMenuSearchInput(page)).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('custom object command works from header command menu button', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('/objects/people');
|
||||
await openCommandMenuWithHeaderButton(page);
|
||||
await searchCommandMenu(page, `create ${customObjectSingularLabel}`);
|
||||
await selectVisibleCommandMenuItem(
|
||||
page,
|
||||
`Create ${customObjectSingularLabel}`,
|
||||
);
|
||||
|
||||
await expectCreateRecordForm(page, customObjectSingularLabel);
|
||||
|
||||
await page.getByTestId('page-header-side-panel-button').click();
|
||||
await expect(commandMenuSearchInput(page)).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('same-object create command is not duplicated', async ({ page }) => {
|
||||
await page.goto('/objects/companies');
|
||||
await openCommandMenuWithHeaderButton(page);
|
||||
await searchCommandMenu(page, 'create company');
|
||||
|
||||
await expect(
|
||||
sidePanel(page).getByText('Create Company', { exact: true }),
|
||||
).toHaveCount(1);
|
||||
|
||||
await searchCommandMenu(page, 'create person');
|
||||
await selectVisibleCommandMenuItem(page, 'Create Person');
|
||||
|
||||
await expectCreateRecordForm(page, 'Person');
|
||||
});
|
||||
});
|
||||
@@ -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',
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user