Add comprehensive UI examples to app-crm: FormView, PageSchema, and component composition#669
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
… enhanced component properties Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…ance rating field documentation Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
|
@copilot example应该基于 crm,而不是写新的 |
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR adds comprehensive UI examples to the examples/app-crm package, demonstrating ObjectStack's full UI metadata protocol capabilities. The changes enhance the component schemas in @objectstack/spec and provide extensive working examples of FormView layouts, PageSchema types, and component composition patterns.
Changes:
- Enhanced component schemas with detailed properties for RecordDetails, RecordRelatedList, RecordHighlights, and added new schemas for RecordActivity, RecordPath, PageAccordion, and AIChatWindow with ARIA support
- Added comprehensive FormView examples demonstrating all 6 layout types (simple, tabbed, wizard, split, drawer, modal) with field-level controls
- Created 4 PageSchema examples showing record detail, home dashboard, app launcher, and utility bar page types
- Updated app-crm documentation to reflect the new UI examples
Reviewed changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Adds package reference for examples/ui-showcase (non-existent - should be removed) |
| packages/spec/src/ui/component.zod.ts | Enhanced component property schemas with detailed descriptions, ARIA support, and new component types |
| examples/app-crm/src/views/lead.view.ts | Comprehensive FormView examples for all 6 layout types with 5 list view types and field-level controls |
| examples/app-crm/src/views/index.ts | Export file for LeadViews |
| examples/app-crm/src/pages/lead_detail.page.ts | Record page example with highlights, tabs, accordion, and related lists |
| examples/app-crm/src/pages/home.page.ts | Home page example with dashboard layout and KPI highlights |
| examples/app-crm/src/pages/app_launcher.page.ts | App launcher page with grid layout |
| examples/app-crm/src/pages/utility_bar.page.ts | Utility bar page with floating panels |
| examples/app-crm/src/pages/index.ts | Export file for all page examples |
| examples/app-crm/README.md | Documentation updates describing UI examples and their features |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
| label: 'Lead Detail Page', | ||
| description: 'Detailed view of lead information with related records and activity', |
There was a problem hiding this comment.
Invalid ARIA properties: The AriaPropsSchema defines properties as ariaLabel, ariaDescribedBy, and role, but this code uses label and description. Change label to ariaLabel and either remove description or change it to ariaDescribedBy with a reference to an element ID.
| label: 'Lead Detail Page', | |
| description: 'Detailed view of lead information with related records and activity', | |
| ariaLabel: 'Lead Detail Page', | |
| ariaDescribedBy: 'lead_detail_page_description', |
| label: 'Utility Bar', | ||
| description: 'Quick access utility bar with floating tools and notifications', |
There was a problem hiding this comment.
Invalid ARIA properties: The AriaPropsSchema defines properties as ariaLabel, ariaDescribedBy, and role, but this code uses label and description. Change label to ariaLabel and either remove description or change it to ariaDescribedBy with a reference to an element ID.
| label: 'Utility Bar', | |
| description: 'Quick access utility bar with floating tools and notifications', | |
| ariaLabel: 'Utility Bar', |
| label: 'App Launcher Page', | ||
| description: 'Central application launcher for accessing all apps', |
There was a problem hiding this comment.
Invalid ARIA properties: The AriaPropsSchema defines properties as ariaLabel, ariaDescribedBy, and role, but this code uses label and description. Change label to ariaLabel and either remove description or change it to ariaDescribedBy with a reference to an element ID.
| label: 'App Launcher Page', | |
| description: 'Central application launcher for accessing all apps', | |
| ariaLabel: 'App Launcher Page', |
| examples/ui-showcase: | ||
| dependencies: | ||
| '@objectstack/spec': | ||
| specifier: workspace:* | ||
| version: link:../../packages/spec | ||
| devDependencies: | ||
| typescript: | ||
| specifier: ^5.9.0 | ||
| version: 5.9.3 | ||
|
|
There was a problem hiding this comment.
The pnpm-lock.yaml adds an entry for examples/ui-showcase package, but this directory does not exist in the repository. The actual UI examples were added to examples/app-crm/src/views/ and examples/app-crm/src/pages/ instead. This lock file entry should be removed as it references a non-existent package.
| examples/ui-showcase: | |
| dependencies: | |
| '@objectstack/spec': | |
| specifier: workspace:* | |
| version: link:../../packages/spec | |
| devDependencies: | |
| typescript: | |
| specifier: ^5.9.0 | |
| version: 5.9.3 |
| // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. | ||
|
|
||
| import { defineView } from '@objectstack/spec/ui'; |
There was a problem hiding this comment.
The PR description mentions examples/ui-showcase multiple times (e.g., "examples/ui-showcase/src/views/lead.view.ts") but the actual implementation is in examples/app-crm. While the PR title correctly states "Add comprehensive UI examples to app-crm", the description body should be updated to refer to examples/app-crm instead of examples/ui-showcase for consistency.
| sections: [ | ||
| { | ||
| label: 'Contact Information', | ||
| collapsible: true, | ||
| collapsed: false, | ||
| columns: 2, // 2-column layout | ||
| fields: [ | ||
| { | ||
| field: 'salutation', | ||
| colSpan: 1, | ||
| }, | ||
| { | ||
| field: 'first_name', | ||
| required: true, | ||
| colSpan: 1, | ||
| }, | ||
| { | ||
| field: 'last_name', | ||
| required: true, | ||
| colSpan: 2, // Span both columns | ||
| }, | ||
| 'company', | ||
| 'title', | ||
| 'email', | ||
| 'phone', | ||
| 'mobile', | ||
| 'website', | ||
| ], | ||
| }, | ||
| { | ||
| label: 'Lead Classification', | ||
| collapsible: true, | ||
| collapsed: false, | ||
| columns: 3, // 3-column layout | ||
| fields: [ | ||
| { | ||
| field: 'status', | ||
| required: true, | ||
| }, | ||
| { | ||
| field: 'rating', | ||
| widget: 'star_rating', // Custom widget | ||
| }, | ||
| 'lead_source', | ||
| 'industry', | ||
| { | ||
| field: 'owner', | ||
| required: true, | ||
| }, | ||
| ], | ||
| }, | ||
| { | ||
| label: 'Company Information', | ||
| collapsible: true, | ||
| collapsed: true, // Collapsed by default | ||
| columns: 2, | ||
| fields: [ | ||
| 'annual_revenue', | ||
| 'number_of_employees', | ||
| ], | ||
| }, | ||
| { | ||
| label: 'Address', | ||
| collapsible: true, | ||
| collapsed: true, | ||
| columns: 2, | ||
| fields: [ | ||
| { | ||
| field: 'street', | ||
| colSpan: 2, // Full width | ||
| }, | ||
| 'city', | ||
| 'state', | ||
| 'postal_code', | ||
| 'country', | ||
| ], | ||
| }, | ||
| { | ||
| label: 'Additional Information', | ||
| collapsible: true, | ||
| collapsed: true, | ||
| columns: 1, // Single column for text areas | ||
| fields: [ | ||
| 'description', | ||
| 'notes', | ||
| ], | ||
| }, | ||
| { | ||
| label: 'Privacy', | ||
| collapsible: true, | ||
| collapsed: true, | ||
| columns: 2, | ||
| fields: [ | ||
| 'do_not_call', | ||
| 'email_opt_out', | ||
| ], | ||
| }, | ||
| ], | ||
| }, | ||
|
|
||
| /** | ||
| * Additional Named List Views | ||
| */ | ||
| listViews: { | ||
| /** | ||
| * Kanban Board View | ||
| */ | ||
| kanban_by_status: { | ||
| name: 'kanban_by_status', | ||
| type: 'kanban', | ||
| label: 'Lead Pipeline', | ||
| data: { | ||
| provider: 'object', | ||
| object: 'lead', | ||
| }, | ||
| columns: ['first_name', 'last_name', 'company', 'email'], | ||
| kanban: { | ||
| groupByField: 'status', | ||
| summarizeField: 'annual_revenue', | ||
| columns: ['first_name', 'last_name', 'company', 'rating'], | ||
| }, | ||
| navigation: { | ||
| mode: 'drawer', // Open in drawer instead of new page | ||
| width: '600px', | ||
| }, | ||
| }, | ||
|
|
||
| /** | ||
| * Calendar View | ||
| */ | ||
| calendar_by_created: { | ||
| name: 'calendar_by_created', | ||
| type: 'calendar', | ||
| label: 'Lead Calendar', | ||
| data: { | ||
| provider: 'object', | ||
| object: 'lead', | ||
| }, | ||
| columns: ['first_name', 'last_name', 'company'], | ||
| calendar: { | ||
| startDateField: 'created_at', | ||
| titleField: 'company', | ||
| colorField: 'status', | ||
| }, | ||
| }, | ||
|
|
||
| /** | ||
| * Gallery/Card View | ||
| */ | ||
| gallery_view: { | ||
| name: 'gallery_view', | ||
| type: 'gallery', | ||
| label: 'Lead Cards', | ||
| data: { | ||
| provider: 'object', | ||
| object: 'lead', | ||
| }, | ||
| columns: ['first_name', 'last_name', 'company', 'email', 'status'], | ||
| gallery: { | ||
| cardSize: 'medium', | ||
| titleField: 'company', | ||
| visibleFields: ['first_name', 'last_name', 'email', 'phone', 'status', 'rating'], | ||
| }, | ||
| }, | ||
|
|
||
| /** | ||
| * My Leads - Filtered View | ||
| */ | ||
| my_leads: { | ||
| name: 'my_leads', | ||
| type: 'grid', | ||
| label: 'My Leads', | ||
| data: { | ||
| provider: 'object', | ||
| object: 'lead', | ||
| }, | ||
| columns: ['first_name', 'last_name', 'company', 'email', 'status', 'rating'], | ||
| filter: [ | ||
| ['owner', '=', '{current_user_id}'] | ||
| ], | ||
| sort: [ | ||
| { field: 'rating', order: 'desc' }, | ||
| { field: 'created_at', order: 'desc' } | ||
| ], | ||
| }, | ||
|
|
||
| /** | ||
| * High Priority Leads | ||
| */ | ||
| high_priority: { | ||
| name: 'high_priority', | ||
| type: 'grid', | ||
| label: 'High Priority', | ||
| data: { | ||
| provider: 'object', | ||
| object: 'lead', | ||
| }, | ||
| columns: ['first_name', 'last_name', 'company', 'email', 'status', 'rating', 'lead_source'], | ||
| filter: [ | ||
| ['rating', '>=', 4], | ||
| ['status', 'in', ['new', 'contacted']] | ||
| ], | ||
| rowColor: { | ||
| field: 'rating', | ||
| colors: { | ||
| '5': '#00AA00', | ||
| '4': '#FFA500', | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
|
|
||
| /** | ||
| * Additional Named Form Views - Demonstrating All 6 Layout Types | ||
| */ | ||
| formViews: { | ||
| /** | ||
| * 1. SIMPLE Layout (already shown as default form above) | ||
| * Basic sectioned form | ||
| */ | ||
| quick_create: { | ||
| type: 'simple', | ||
| data: { | ||
| provider: 'object', | ||
| object: 'lead', | ||
| }, | ||
| sections: [ | ||
| { | ||
| label: 'Quick Lead Creation', | ||
| columns: 2, | ||
| fields: [ | ||
| { field: 'first_name', required: true }, | ||
| { field: 'last_name', required: true }, | ||
| { field: 'company', required: true, colSpan: 2 }, | ||
| { field: 'email', required: true }, | ||
| 'phone', | ||
| 'status', | ||
| 'owner', | ||
| ], | ||
| }, | ||
| ], | ||
| }, | ||
|
|
||
| /** | ||
| * 2. TABBED Layout | ||
| * Organize complex forms with tabs | ||
| */ | ||
| detail_form: { | ||
| type: 'tabbed', | ||
| data: { | ||
| provider: 'object', | ||
| object: 'lead', | ||
| }, | ||
| sections: [ | ||
| { | ||
| label: 'General', | ||
| columns: 2, | ||
| fields: [ | ||
| 'salutation', | ||
| 'first_name', | ||
| 'last_name', | ||
| 'company', | ||
| 'title', | ||
| 'email', | ||
| 'phone', | ||
| 'mobile', | ||
| ], | ||
| }, | ||
| { | ||
| label: 'Qualification', | ||
| columns: 2, | ||
| fields: [ | ||
| { field: 'status', required: true }, | ||
| { field: 'rating', widget: 'star_rating' }, | ||
| 'lead_source', | ||
| 'industry', | ||
| 'annual_revenue', | ||
| 'number_of_employees', | ||
| ], | ||
| }, | ||
| { | ||
| label: 'Address', | ||
| columns: 2, | ||
| fields: [ | ||
| { field: 'street', colSpan: 2 }, | ||
| 'city', | ||
| { | ||
| field: 'state', | ||
| dependsOn: 'country', // Cascade: country determines available states | ||
| }, | ||
| 'postal_code', | ||
| 'country', | ||
| ], | ||
| }, | ||
| { | ||
| label: 'Details', | ||
| columns: 1, | ||
| fields: [ | ||
| 'description', | ||
| 'notes', | ||
| 'do_not_call', | ||
| 'email_opt_out', | ||
| ], | ||
| }, | ||
| ], | ||
| }, | ||
|
|
||
| /** | ||
| * 3. WIZARD Layout | ||
| * Step-by-step guided process | ||
| */ | ||
| lead_conversion_wizard: { | ||
| type: 'wizard', | ||
| data: { | ||
| provider: 'object', | ||
| object: 'lead', | ||
| }, | ||
| sections: [ | ||
| { | ||
| label: 'Step 1: Contact Details', | ||
| columns: 2, | ||
| fields: [ | ||
| { field: 'first_name', required: true, readonly: true }, | ||
| { field: 'last_name', required: true, readonly: true }, | ||
| { field: 'email', readonly: true, colSpan: 2 }, | ||
| 'phone', | ||
| 'mobile', | ||
| ], | ||
| }, | ||
| { | ||
| label: 'Step 2: Company Information', | ||
| columns: 2, | ||
| fields: [ | ||
| { field: 'company', required: true, readonly: true }, | ||
| 'title', | ||
| 'industry', | ||
| 'annual_revenue', | ||
| 'number_of_employees', | ||
| 'website', | ||
| ], | ||
| }, | ||
| { | ||
| label: 'Step 3: Qualification', | ||
| columns: 2, | ||
| fields: [ | ||
| { field: 'status', required: true }, | ||
| { field: 'rating', widget: 'star_rating' }, | ||
| 'lead_source', | ||
| { | ||
| field: 'owner', | ||
| visibleOn: 'rating >= 4', // Conditional visibility | ||
| }, | ||
| ], | ||
| }, | ||
| { | ||
| label: 'Step 4: Review & Convert', | ||
| columns: 1, | ||
| fields: [ | ||
| { | ||
| field: 'description', | ||
| helpText: 'Review all information before converting to Account and Contact', | ||
| }, | ||
| ], | ||
| }, | ||
| ], | ||
| }, | ||
|
|
||
| /** | ||
| * 4. SPLIT Layout | ||
| * Master-detail split view | ||
| */ | ||
| split_edit: { | ||
| type: 'split', | ||
| data: { | ||
| provider: 'object', | ||
| object: 'lead', | ||
| }, | ||
| sections: [ | ||
| { | ||
| label: 'Primary Information', | ||
| columns: 1, | ||
| fields: [ | ||
| 'first_name', | ||
| 'last_name', | ||
| 'company', | ||
| 'email', | ||
| { field: 'status', required: true }, | ||
| 'owner', | ||
| ], | ||
| }, | ||
| { | ||
| label: 'Extended Details', | ||
| columns: 2, | ||
| fields: [ | ||
| 'phone', | ||
| 'mobile', | ||
| 'title', | ||
| 'industry', | ||
| 'lead_source', | ||
| { field: 'rating', widget: 'star_rating' }, | ||
| 'annual_revenue', | ||
| 'number_of_employees', | ||
| ], | ||
| }, | ||
| ], | ||
| }, | ||
|
|
||
| /** | ||
| * 5. DRAWER Layout | ||
| * Side panel form (typically opened from list view) | ||
| */ | ||
| quick_edit_drawer: { | ||
| type: 'drawer', | ||
| data: { | ||
| provider: 'object', | ||
| object: 'lead', | ||
| }, | ||
| sections: [ | ||
| { | ||
| label: 'Quick Edit', | ||
| columns: 1, // Drawers typically use single column | ||
| fields: [ | ||
| { field: 'first_name', required: true }, | ||
| { field: 'last_name', required: true }, | ||
| 'company', | ||
| 'email', | ||
| 'phone', | ||
| { field: 'status', required: true }, | ||
| { field: 'rating', widget: 'star_rating' }, | ||
| 'lead_source', | ||
| { | ||
| field: 'owner', | ||
| visibleOn: 'status != "new"', // Only show owner after initial contact | ||
| }, | ||
| ], | ||
| }, | ||
| ], | ||
| }, | ||
|
|
||
| /** | ||
| * 6. MODAL Layout | ||
| * Dialog-based form for quick actions | ||
| */ | ||
| status_update_modal: { | ||
| type: 'modal', | ||
| data: { | ||
| provider: 'object', | ||
| object: 'lead', | ||
| }, | ||
| sections: [ | ||
| { | ||
| label: 'Update Lead Status', | ||
| columns: 1, | ||
| fields: [ | ||
| { field: 'first_name', readonly: true }, | ||
| { field: 'last_name', readonly: true }, | ||
| { field: 'company', readonly: true }, | ||
| { | ||
| field: 'status', | ||
| required: true, | ||
| helpText: 'Select the new status for this lead', | ||
| }, | ||
| { | ||
| field: 'rating', | ||
| widget: 'star_rating', | ||
| visibleOn: 'status == "qualified"', // Only show rating for qualified leads | ||
| }, | ||
| { | ||
| field: 'notes', | ||
| placeholder: 'Add notes about this status change', | ||
| visibleOn: 'status == "unqualified"', // Require notes for unqualified | ||
| }, | ||
| ], | ||
| }, | ||
| ], | ||
| }, | ||
|
|
||
| /** | ||
| * Advanced Example: Conditional Field Visibility & Dependencies | ||
| */ | ||
| advanced_conditional: { | ||
| type: 'simple', | ||
| data: { | ||
| provider: 'object', | ||
| object: 'lead', | ||
| }, | ||
| sections: [ | ||
| { | ||
| label: 'Lead Information', | ||
| columns: 2, | ||
| fields: [ | ||
| 'first_name', | ||
| 'last_name', | ||
| 'company', | ||
| 'email', | ||
| 'status', | ||
| 'lead_source', | ||
| { | ||
| field: 'rating', | ||
| widget: 'star_rating', | ||
| visibleOn: 'status != "new"', // Only show after first contact | ||
| }, | ||
| { | ||
| field: 'industry', | ||
| dependsOn: 'company', // Industry options depend on company | ||
| }, | ||
| { | ||
| field: 'annual_revenue', | ||
| visibleOn: 'rating >= 3', // Only for qualified leads | ||
| }, | ||
| { | ||
| field: 'number_of_employees', | ||
| visibleOn: 'rating >= 3', | ||
| }, | ||
| { | ||
| field: 'owner', | ||
| required: true, | ||
| visibleOn: 'status == "contacted" OR status == "qualified"', | ||
| }, | ||
| { | ||
| field: 'notes', | ||
| colSpan: 2, | ||
| required: true, | ||
| visibleOn: 'status == "unqualified"', // Require explanation for unqualified | ||
| }, | ||
| ], | ||
| }, | ||
| { | ||
| label: 'Address Information', | ||
| collapsible: true, | ||
| collapsed: true, | ||
| columns: 2, | ||
| fields: [ | ||
| { field: 'street', colSpan: 2 }, | ||
| 'city', | ||
| { | ||
| field: 'state', | ||
| dependsOn: 'country', // Country determines state options | ||
| }, | ||
| 'postal_code', | ||
| 'country', | ||
| ], | ||
| }, | ||
| { | ||
| label: 'Privacy Preferences', | ||
| collapsible: true, | ||
| collapsed: true, | ||
| columns: 2, | ||
| fields: [ | ||
| { | ||
| field: 'do_not_call', | ||
| helpText: 'Check if this lead has requested not to be called', | ||
| }, | ||
| { | ||
| field: 'email_opt_out', | ||
| helpText: 'Check if this lead has opted out of email communications', | ||
| }, | ||
| ], | ||
| }, | ||
| ], | ||
| }, |
There was a problem hiding this comment.
Type mismatch throughout file: The FormSectionSchema expects columns to be a string enum ('1', '2', '3', or '4'), but numeric literals are being used consistently (e.g., columns: 2, columns: 3, columns: 1). All columns values in section definitions should be changed to strings: '1', '2', '3', or '4'. This affects approximately 20+ occurrences in this file across all the form view examples.
| label: 'Sales Home Page', | ||
| description: 'Sales team home page with metrics, leads, and quick actions', | ||
| }, | ||
| }; |
There was a problem hiding this comment.
Invalid ARIA properties: The AriaPropsSchema defines properties as ariaLabel, ariaDescribedBy, and role, but this code uses label and description. Change label to ariaLabel and either remove description or change it to ariaDescribedBy with a reference to an element ID.
| label: 'Sales Home Page', | |
| description: 'Sales team home page with metrics, leads, and quick actions', | |
| }, | |
| }; | |
| ariaLabel: 'Sales Home Page', | |
| }, | |
| }; |
Adds comprehensive UI examples to
examples/app-crm/src/views/andexamples/app-crm/src/pages/demonstrating the full expressiveness of ObjectStack's UI metadata protocol. Implements all form layouts, page types, and component compositions referenced in the schemas but previously unexemplified.Enhanced Component Schemas
component.zod.ts:
RecordDetailsProps: Addedfieldsoverride, enhanced layout options (1-4 columns, auto/custom)RecordHighlightsProps: Addedlayoutorientation (horizontal/vertical)RecordRelatedListProps: Added filter, title, actions, showViewAllRecordActivityProps,RecordPathProps,PageAccordionProps,AIChatWindowPropsFormView Examples (app-crm/src/views/lead.view.ts)
All 6 layout types with advanced field controls:
Field-level controls demonstrated:
PageSchema Examples (app-crm/src/pages/)
All 4 page types with component composition:
lead_detail.page.ts (record page):
Additional page types:
home.page.ts: Dashboard-style with KPIs, three-column templateapp_launcher.page.ts: Application launcher gridutility_bar.page.ts: Floating utility panelsIntegration with CRM Example
UI examples are integrated into the existing
app-crmexample to demonstrate how FormView and PageSchema work within a complete application context:examples/app-crm/src/views/andexamples/app-crm/src/pages/app-crm/README.mdwith UI examples sectionThis approach provides better context by showing how UI components integrate with the comprehensive CRM application including objects, workflows, agents, and security models.
Original prompt
Created from VS Code.
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.