Portal Community

Step 1 — Define Your Interaction Type Key

Custom type keys must be namespaced to avoid collision with built-in types. Use a reverse-domain prefix:

// Your custom type key
export const SPECIAL_REVIEW_TYPE = 'com.acme/special-review';

Step 2 — Define Payload and Response Types

// TypeScript types for your custom interaction
export interface SpecialReviewPayload {
  entityId: string;
  entityType: string;
  reviewCriteria: string[];
  attachmentUrls: string[];
}

export interface SpecialReviewResponseData {
  decision: 'approved-with-changes' | 'approved' | 'declined';
  requiredChanges?: string[];
  reviewNotes: string;
}

Step 3 — Implement the Renderer Component

import { InteractionComponentProps } from 'edge-interact-core';
import { useState } from 'react';

export function SpecialReviewComponent({
  interaction,
  respond
}: InteractionComponentProps<SpecialReviewPayload>) {
  const { payload } = interaction;
  const [decision, setDecision] = useState<string>('');
  const [notes, setNotes] = useState('');
  const [changes, setChanges] = useState<string[]>([]);
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleSubmit = async () => {
    if (!decision) return;
    setIsSubmitting(true);
    try {
      await respond(decision, {
        decision,
        requiredChanges: decision === 'approved-with-changes' ? changes : undefined,
        reviewNotes: notes
      } as SpecialReviewResponseData);
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <div className="special-review-component">
      <h3>{interaction.title}</h3>
      <p>Entity: {payload.entityType} — {payload.entityId}</p>
      <ul>
        {payload.reviewCriteria.map(c => <li key={c}>{c}</li>)}
      </ul>
      {/* ... render attachments, decision selector, notes input ... */}
      <div>
        <button onClick={() => setDecision('approved')}>Approve</button>
        <button onClick={() => setDecision('approved-with-changes')}>Approve with Changes</button>
        <button onClick={() => setDecision('declined')}>Decline</button>
      </div>
      <textarea value={notes} onChange={e => setNotes(e.target.value)}
        placeholder="Review notes..." />
      <button onClick={handleSubmit} disabled={!decision || isSubmitting}>
        {isSubmitting ? 'Submitting...' : 'Submit Review'}
      </button>
    </div>
  );
}

Step 4 — Register the Renderer

// Register before the app renders (e.g., in main.tsx or App.tsx)
import { registerInteractionRenderer } from 'edge-interact-ui';
import { SpecialReviewComponent } from './interactions/SpecialReviewComponent';
import { SPECIAL_REVIEW_TYPE } from './interactions/types';

// Register at module load time — not inside a component
registerInteractionRenderer(SPECIAL_REVIEW_TYPE, SpecialReviewComponent);

Step 5 — Register a Server-Side Validator

// Register custom response validator on the server
builder.Services.AddInteractionTypeValidator<SpecialReviewValidator>(SPECIAL_REVIEW_TYPE);

Custom Type Checklist

ItemStatus
Namespaced type key (reverse-domain prefix)Required
TypeScript payload interfaceRequired
TypeScript response data interfaceRequired
React renderer component (receives interaction + respond)Required
registerInteractionRenderer(typeKey, Component)Required (client)
Server-side response validatorStrongly recommended