Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c3f3c4caa | ||
|
|
a278cd6d85 | ||
|
|
2d93bfa031 | ||
|
|
9286e81275 | ||
|
|
85d649e831 | ||
|
|
2f4ebf8160 | ||
|
|
d348a4c908 | ||
|
|
24be69dbb0 | ||
|
|
f470b4d5a5 | ||
|
|
77603a4102 | ||
|
|
91ce59d8e2 | ||
|
|
056e3a4cd8 | ||
|
|
9876af5587 | ||
|
|
563acc3f57 | ||
|
|
452433a9ae | ||
|
|
3bda05ea57 | ||
|
|
eda41b4eba | ||
|
|
de044f4b45 | ||
|
|
e3c79c803c | ||
|
|
59d69e2f5c | ||
|
|
4554dbe3c9 | ||
|
|
efba71a938 | ||
|
|
e6cc783a23 | ||
|
|
76e144e85a | ||
|
|
8f24cda586 | ||
|
|
c2a48a1cc2 | ||
|
|
cffd782de7 | ||
|
|
6482824a09 | ||
|
|
47386fd578 | ||
|
|
6095f872f5 | ||
|
|
1b1860b038 | ||
|
|
393f2f11f7 | ||
|
|
42875de279 | ||
|
|
db476b2f5c | ||
|
|
c03d159e06 | ||
|
|
91f7fc66db | ||
|
|
3d096e9825 | ||
|
|
26d04719d7 | ||
|
|
5d0aa872ee | ||
|
|
8c2fa796e4 | ||
|
|
12685622c9 | ||
|
|
1c9b79013c | ||
|
|
116c17ee11 | ||
|
|
64cfe8b07f | ||
|
|
6ed955e89f |
@@ -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],
|
||||
});
|
||||
|
||||
@@ -48,9 +48,9 @@ type ApplicationRegistration {
|
||||
isListed: Boolean!
|
||||
isFeatured: Boolean!
|
||||
isPreInstalled: Boolean!
|
||||
logoUrl: String
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
logo: FileOutput
|
||||
isConfigured: Boolean!
|
||||
}
|
||||
|
||||
@@ -283,11 +283,18 @@ type Role {
|
||||
rowLevelPermissionPredicateGroups: [RowLevelPermissionPredicateGroup!]
|
||||
}
|
||||
|
||||
type FileOutput {
|
||||
fileId: UUID
|
||||
label: String
|
||||
extension: String
|
||||
url: String!
|
||||
}
|
||||
|
||||
type ApplicationRegistrationSummary {
|
||||
id: UUID!
|
||||
latestAvailableVersion: String
|
||||
sourceType: ApplicationRegistrationSourceType!
|
||||
logoUrl: String
|
||||
logo: FileOutput
|
||||
}
|
||||
|
||||
type ApplicationVariable {
|
||||
@@ -691,7 +698,6 @@ type Application {
|
||||
id: UUID!
|
||||
name: String!
|
||||
description: String
|
||||
logo: String
|
||||
version: String
|
||||
universalIdentifier: String!
|
||||
packageJsonChecksum: String
|
||||
@@ -711,6 +717,7 @@ type Application {
|
||||
objects: [Object!]!
|
||||
applicationVariables: [ApplicationVariable!]!
|
||||
applicationRegistration: ApplicationRegistrationSummary
|
||||
logo: FileOutput
|
||||
}
|
||||
|
||||
type ViewField {
|
||||
@@ -1971,7 +1978,7 @@ type CreateApplicationRegistration {
|
||||
type PublicApplicationRegistration {
|
||||
id: UUID!
|
||||
name: String!
|
||||
logoUrl: String
|
||||
logo: FileOutput
|
||||
websiteUrl: String
|
||||
oAuthScopes: [String!]!
|
||||
}
|
||||
@@ -2357,6 +2364,7 @@ type MarketplaceAppDetail {
|
||||
sourceType: ApplicationRegistrationSourceType!
|
||||
sourcePackage: String
|
||||
latestAvailableVersion: String
|
||||
logo: FileOutput
|
||||
isListed: Boolean!
|
||||
isFeatured: Boolean!
|
||||
manifest: JSON
|
||||
@@ -2476,6 +2484,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 +2922,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 +2990,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 +3009,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 +3230,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 +3249,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 +4055,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 +4215,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!
|
||||
|
||||
@@ -52,9 +52,9 @@ export interface ApplicationRegistration {
|
||||
isListed: Scalars['Boolean']
|
||||
isFeatured: Scalars['Boolean']
|
||||
isPreInstalled: Scalars['Boolean']
|
||||
logoUrl?: Scalars['String']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
logo?: FileOutput
|
||||
isConfigured: Scalars['Boolean']
|
||||
__typename: 'ApplicationRegistration'
|
||||
}
|
||||
@@ -232,11 +232,19 @@ export interface Role {
|
||||
__typename: 'Role'
|
||||
}
|
||||
|
||||
export interface FileOutput {
|
||||
fileId?: Scalars['UUID']
|
||||
label?: Scalars['String']
|
||||
extension?: Scalars['String']
|
||||
url: Scalars['String']
|
||||
__typename: 'FileOutput'
|
||||
}
|
||||
|
||||
export interface ApplicationRegistrationSummary {
|
||||
id: Scalars['UUID']
|
||||
latestAvailableVersion?: Scalars['String']
|
||||
sourceType: ApplicationRegistrationSourceType
|
||||
logoUrl?: Scalars['String']
|
||||
logo?: FileOutput
|
||||
__typename: 'ApplicationRegistrationSummary'
|
||||
}
|
||||
|
||||
@@ -456,7 +464,6 @@ export interface Application {
|
||||
id: Scalars['UUID']
|
||||
name: Scalars['String']
|
||||
description?: Scalars['String']
|
||||
logo?: Scalars['String']
|
||||
version?: Scalars['String']
|
||||
universalIdentifier: Scalars['String']
|
||||
packageJsonChecksum?: Scalars['String']
|
||||
@@ -476,6 +483,7 @@ export interface Application {
|
||||
objects: Object[]
|
||||
applicationVariables: ApplicationVariable[]
|
||||
applicationRegistration?: ApplicationRegistrationSummary
|
||||
logo?: FileOutput
|
||||
__typename: 'Application'
|
||||
}
|
||||
|
||||
@@ -1611,7 +1619,7 @@ export interface CreateApplicationRegistration {
|
||||
export interface PublicApplicationRegistration {
|
||||
id: Scalars['UUID']
|
||||
name: Scalars['String']
|
||||
logoUrl?: Scalars['String']
|
||||
logo?: FileOutput
|
||||
websiteUrl?: Scalars['String']
|
||||
oAuthScopes: Scalars['String'][]
|
||||
__typename: 'PublicApplicationRegistration'
|
||||
@@ -2034,6 +2042,7 @@ export interface MarketplaceAppDetail {
|
||||
sourceType: ApplicationRegistrationSourceType
|
||||
sourcePackage?: Scalars['String']
|
||||
latestAvailableVersion?: Scalars['String']
|
||||
logo?: FileOutput
|
||||
isListed: Scalars['Boolean']
|
||||
isFeatured: Scalars['Boolean']
|
||||
manifest?: Scalars['JSON']
|
||||
@@ -2160,6 +2169,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 +2550,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 +2600,8 @@ export interface Query {
|
||||
getRoles: Role[]
|
||||
getToolIndex: ToolIndexEntry[]
|
||||
getToolInputSchema?: Scalars['JSON']
|
||||
webhooks: Webhook[]
|
||||
webhook?: Webhook
|
||||
field: Field
|
||||
fields: FieldConnection
|
||||
getViewGroups: ViewGroup[]
|
||||
@@ -2599,8 +2610,6 @@ export interface Query {
|
||||
myMessageChannels: MessageChannel[]
|
||||
myConnectedAccounts: ConnectedAccountPublicDTO[]
|
||||
myCalendarChannels: CalendarChannel[]
|
||||
webhooks: Webhook[]
|
||||
webhook?: Webhook
|
||||
minimalMetadata: MinimalMetadata
|
||||
chatThreads: AgentChatThread[]
|
||||
chatThread: AgentChatThread
|
||||
@@ -2755,6 +2764,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 +2783,6 @@ export interface Mutation {
|
||||
deleteEmailGroupChannel: MessageChannel
|
||||
deleteConnectedAccount: ConnectedAccountPublicDTO
|
||||
updateCalendarChannel: CalendarChannel
|
||||
createWebhook: Webhook
|
||||
updateWebhook: Webhook
|
||||
deleteWebhook: Webhook
|
||||
createChatThread: AgentChatThread
|
||||
sendChatMessage: SendChatMessageResult
|
||||
stopAgentChatStream: Scalars['Boolean']
|
||||
@@ -2923,9 +2932,9 @@ export interface ApplicationRegistrationGenqlSelection{
|
||||
isListed?: boolean | number
|
||||
isFeatured?: boolean | number
|
||||
isPreInstalled?: boolean | number
|
||||
logoUrl?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
logo?: FileOutputGenqlSelection
|
||||
isConfigured?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
@@ -3096,11 +3105,20 @@ export interface RoleGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface FileOutputGenqlSelection{
|
||||
fileId?: boolean | number
|
||||
label?: boolean | number
|
||||
extension?: boolean | number
|
||||
url?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ApplicationRegistrationSummaryGenqlSelection{
|
||||
id?: boolean | number
|
||||
latestAvailableVersion?: boolean | number
|
||||
sourceType?: boolean | number
|
||||
logoUrl?: boolean | number
|
||||
logo?: FileOutputGenqlSelection
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
@@ -3365,7 +3383,6 @@ export interface ApplicationGenqlSelection{
|
||||
id?: boolean | number
|
||||
name?: boolean | number
|
||||
description?: boolean | number
|
||||
logo?: boolean | number
|
||||
version?: boolean | number
|
||||
universalIdentifier?: boolean | number
|
||||
packageJsonChecksum?: boolean | number
|
||||
@@ -3385,6 +3402,7 @@ export interface ApplicationGenqlSelection{
|
||||
objects?: ObjectGenqlSelection
|
||||
applicationVariables?: ApplicationVariableGenqlSelection
|
||||
applicationRegistration?: ApplicationRegistrationSummaryGenqlSelection
|
||||
logo?: FileOutputGenqlSelection
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
@@ -4555,7 +4573,7 @@ export interface CreateApplicationRegistrationGenqlSelection{
|
||||
export interface PublicApplicationRegistrationGenqlSelection{
|
||||
id?: boolean | number
|
||||
name?: boolean | number
|
||||
logoUrl?: boolean | number
|
||||
logo?: FileOutputGenqlSelection
|
||||
websiteUrl?: boolean | number
|
||||
oAuthScopes?: boolean | number
|
||||
__typename?: boolean | number
|
||||
@@ -5030,6 +5048,7 @@ export interface MarketplaceAppDetailGenqlSelection{
|
||||
sourceType?: boolean | number
|
||||
sourcePackage?: boolean | number
|
||||
latestAvailableVersion?: boolean | number
|
||||
logo?: FileOutputGenqlSelection
|
||||
isListed?: boolean | number
|
||||
isFeatured?: boolean | number
|
||||
manifest?: boolean | number
|
||||
@@ -5166,6 +5185,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 +5574,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 +5636,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 +5652,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 +5827,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 +5846,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 +6173,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 +6235,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']}
|
||||
@@ -6409,6 +6428,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const FileOutput_possibleTypes: string[] = ['FileOutput']
|
||||
export const isFileOutput = (obj?: { __typename?: any } | null): obj is FileOutput => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isFileOutput"')
|
||||
return FileOutput_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ApplicationRegistrationSummary_possibleTypes: string[] = ['ApplicationRegistrationSummary']
|
||||
export const isApplicationRegistrationSummary = (obj?: { __typename?: any } | null): obj is ApplicationRegistrationSummary => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isApplicationRegistrationSummary"')
|
||||
@@ -7937,6 +7964,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 +8236,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"')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
|
||||
|
||||
@@ -75,11 +75,11 @@ filters: [
|
||||
|
||||
> Feldtypen mit ähnlichen Namen können völlig unterschiedliche Operanden verwenden – `SELECT` und `MULTI_SELECT` sind ein häufiges Beispiel.
|
||||
|
||||
### Wertform pro Operand
|
||||
### Wertstruktur je Operand
|
||||
|
||||
Das Feld `value` ist immer ein JSON-serialisierbarer Wert, aber seine erwartete Form hängt vom Operanden ab:
|
||||
Das Feld `value` ist immer ein JSON-serialisierbarer Wert, aber die erwartete Struktur hängt vom Operanden ab:
|
||||
|
||||
| Operandfamilie | Wertform | Beispiel |
|
||||
| Operandenfamilie | Wertstruktur | Beispiel |
|
||||
| -------------------------------------------------------- | ------------------------------------- | ------------------------ |
|
||||
| `IS`, `IS_NOT` bei `SELECT` | Array von Optionsschlüsseln (Strings) | `['ACTIVE', 'PENDING']` |
|
||||
| `CONTAINS`, `DOES_NOT_CONTAIN` bei `MULTI_SELECT` | Array von Optionsschlüsseln (Strings) | `['TAG_A']` |
|
||||
@@ -87,9 +87,9 @@ Das Feld `value` ist immer ein JSON-serialisierbarer Wert, aber seine erwartete
|
||||
| `CONTAINS`, `DOES_NOT_CONTAIN` bei textähnlichen Feldern | Zeichenkette | `'acme'` |
|
||||
| `IS`, `IS_NOT` bei `NUMBER` | String (der Wert) | `'5'` |
|
||||
| `IS` bei `RATING` / `UUID` | String (der Wert) | `'5'` |
|
||||
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | String (die Schranke) | `'10'` |
|
||||
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | String (der Grenzwert) | `'10'` |
|
||||
| `IS`, `IS_BEFORE`, `IS_AFTER` bei `DATE` / `DATE_TIME` | ISO-8601-String | `'2025-01-01T00:00:00Z'` |
|
||||
| `IS_EMPTY`, `IS_NOT_EMPTY` | Leerer String | `''` |
|
||||
| `IS_EMPTY`, `IS_NOT_EMPTY` | leerer String | `''` |
|
||||
| `IS` bei `BOOLEAN` | `'true'` oder `'false'` | `'true'` |
|
||||
|
||||
## Wie Ansichten in der UI angezeigt werden
|
||||
|
||||
@@ -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ță)
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ filters: [
|
||||
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | строка (граница) | `'10'` |
|
||||
| `IS`, `IS_BEFORE`, `IS_AFTER` для `DATE` / `DATE_TIME` | строка в формате ISO 8601 | `'2025-01-01T00:00:00Z'` |
|
||||
| `IS_EMPTY`, `IS_NOT_EMPTY` | пустая строка | `''` |
|
||||
| `IS` для `BOOLEAN` | `'true'` или `'false'` | `'истина'` |
|
||||
| `IS` для `BOOLEAN` | `'true'` или `'false'` | `'true'` |
|
||||
|
||||
## Как представления отображаются в интерфейсе
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
### Отозвать ключ (только при утечке / в экстренных случаях)
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ export default defineView({
|
||||
|
||||
## Filtreler
|
||||
|
||||
Bir görünüm, önceden uygulanmış filtrelerle birlikte gönderilebilir. Her filtrenin üç koordinatı vardır: filtrelenen **alan**, **işleç** (nasıl karşılaştırılacağı) ve **değer** (neyle karşılaştırılacağı). Üçünün de hizalı olması gerekir — bir alan türüne uygulanmayan bir işlecin kullanılması, senkronizasyon sırasında reddedilir.
|
||||
Bir görünüm, önceden uygulanmış filtrelerle gelebilir. Her filtrenin üç koordinatı vardır: filtrelenen **alan**, **işleç** (nasıl karşılaştırılacağı) ve **değer** (neyle karşılaştırılacağı). Üçünün de hizalı olması gerekir — bir alan türüne uygulanmayan bir işlecin kullanılması, senkronizasyon sırasında reddedilir.
|
||||
|
||||
```ts
|
||||
import { ViewFilterOperand } from 'twenty-shared/types';
|
||||
@@ -57,7 +57,7 @@ filters: [
|
||||
|
||||
### Alan türüne göre desteklenen işleçler
|
||||
|
||||
| Alan tipi | Desteklenen işlenenler |
|
||||
| Alan tipi | Desteklenen işleçler |
|
||||
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| `TEXT`, `EMAILS`, `FULL_NAME`, `ADDRESS`, `LINKS`, `PHONES`, `RAW_JSON`, `FILES`, `ACTOR`, `ARRAY` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
| `ACTOR.source`, `ACTOR.workspaceMemberId` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
|
||||
@@ -73,24 +73,24 @@ filters: [
|
||||
| `UUID` | `IS` |
|
||||
| `TS_VECTOR` | `VECTOR_SEARCH` |
|
||||
|
||||
> Benzer adlara sahip alan türleri tamamen farklı işlenenler kullanabilir — yaygın bir durum `SELECT` ve `MULTI_SELECT` içindir.
|
||||
> Benzer adlara sahip alan türleri tamamen farklı işleçler kullanabilir — buna `SELECT` ve `MULTI_SELECT` yaygın bir örnektir.
|
||||
|
||||
### Her işlenene göre değer şekli
|
||||
### Her işleç için değer biçimi
|
||||
|
||||
`value` alanı her zaman JSON-serileştirilebilir bir değerdir, ancak beklenen şekli işlenene bağlıdır:
|
||||
`value` alanı her zaman JSON olarak serileştirilebilir bir değerdir, ancak beklenen biçim işlece bağlıdır:
|
||||
|
||||
| İşlenen ailesi | Değer şekli | Örnek |
|
||||
| ------------------------------------------------------------- | -------------------------------------- | ------------------------ |
|
||||
| `SELECT` üzerinde `IS`, `IS_NOT` | seçenek anahtarları dizisi (stringler) | `['ACTIVE', 'PENDING']` |
|
||||
| `MULTI_SELECT` üzerinde `CONTAINS`, `DOES_NOT_CONTAIN` | seçenek anahtarları dizisi (stringler) | `['TAG_A']` |
|
||||
| `RELATION` üzerinde `IS`, `IS_NOT` | kayıt kimlikleri dizisi (uuid'ler) | `['c5a1...']` |
|
||||
| metin benzeri alanlar üzerinde `CONTAINS`, `DOES_NOT_CONTAIN` | metin | `'acme'` |
|
||||
| `NUMBER` üzerinde `IS`, `IS_NOT` | string (değer) | `'5'` |
|
||||
| `RATING` / `UUID` üzerinde `IS` | string (değer) | `'5'` |
|
||||
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | string (sınır) | `'10'` |
|
||||
| `DATE` / `DATE_TIME` üzerinde `IS`, `IS_BEFORE`, `IS_AFTER` | ISO 8601 stringi | `'2025-01-01T00:00:00Z'` |
|
||||
| `IS_EMPTY`, `IS_NOT_EMPTY` | empty string | `''` |
|
||||
| `IS` on `BOOLEAN` | `'true'` or `'false'` | `'doğru'` |
|
||||
| İşleç ailesi | Değer biçimi | Örnek |
|
||||
| ------------------------------------------------------------- | ------------------------------------- | ------------------------ |
|
||||
| `SELECT` üzerinde `IS`, `IS_NOT` | seçenek anahtarları dizisi (metinler) | `['ACTIVE', 'PENDING']` |
|
||||
| `MULTI_SELECT` üzerinde `CONTAINS`, `DOES_NOT_CONTAIN` | seçenek anahtarları dizisi (metinler) | `['TAG_A']` |
|
||||
| `RELATION` üzerinde `IS`, `IS_NOT` | kayıt kimlikleri dizisi (uuid'ler) | `['c5a1...']` |
|
||||
| metin benzeri alanlar üzerinde `CONTAINS`, `DOES_NOT_CONTAIN` | metin | `'acme'` |
|
||||
| `NUMBER` üzerinde `IS`, `IS_NOT` | metin (değer) | `'5'` |
|
||||
| `RATING` / `UUID` üzerinde `IS` | metin (değer) | `'5'` |
|
||||
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | metin (sınır) | `'10'` |
|
||||
| `DATE` / `DATE_TIME` üzerinde `IS`, `IS_BEFORE`, `IS_AFTER` | ISO 8601 dizesi | `'2025-01-01T00:00:00Z'` |
|
||||
| `IS_EMPTY`, `IS_NOT_EMPTY` | boş dize | `''` |
|
||||
| `BOOLEAN` üzerinde `IS` | `'true'` veya `'false'` | `'true'` |
|
||||
|
||||
## Görünümlerin kullanıcı arayüzünde görüntülenme şekli
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
### 吊销密钥(仅限泄露 / 紧急情况)
|
||||
|
||||
|
||||
@@ -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