-
Notifications
You must be signed in to change notification settings - Fork 0
Implement 10 missing field widgets per @objectstack/spec UI protocol #253
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements 10 missing field widget components to improve ObjectUI's coverage of the @objectstack/spec UI protocol from 59% to 91% (29/32 field types). The new widgets provide input controls for colors, ratings, code, avatars, addresses, geolocation, signatures, and QR codes, plus a master-detail relationship editor.
Changes:
- Added 10 new field widget components following the
FieldWidgetProps<T>interface pattern with readonly/editable mode support - Updated component registry to include both direct type registrations (
'color') and prefixed variants ('field:color') - Replaced placeholder
LookupFieldregistration formaster_detailtype with dedicatedMasterDetailFieldimplementation - Added basic test coverage for all new widgets
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 22 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/fields/src/widgets/ColorField.tsx | Color picker with hex input/preview and native color input |
| packages/fields/src/widgets/SliderField.tsx | Range slider with configurable min/max/step via field metadata |
| packages/fields/src/widgets/RatingField.tsx | Interactive star rating widget with hover preview |
| packages/fields/src/widgets/CodeField.tsx | Monospace code editor using Textarea with language configuration |
| packages/fields/src/widgets/AvatarField.tsx | Profile picture uploader with base64 encoding and file validation |
| packages/fields/src/widgets/AddressField.tsx | Structured address input with street/city/state/zip/country fields |
| packages/fields/src/widgets/GeolocationField.tsx | Lat/long input with browser geolocation API and map integration |
| packages/fields/src/widgets/SignatureField.tsx | Canvas-based signature capture with touch/mouse support |
| packages/fields/src/widgets/QRCodeField.tsx | QR code generator/display using external API service |
| packages/fields/src/widgets/MasterDetailField.tsx | One-to-many relationship editor with add/remove/view actions |
| packages/fields/src/new-widgets.test.tsx | Basic render tests for all new widgets in readonly and editable modes |
| packages/fields/src/index.tsx | Component registry updates and exports for all new widgets |
| const getQRCodeUrl = (text: string): string => { | ||
| // Using a simple QR code API service (you might want to replace this with a library) | ||
| return `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(text)}`; | ||
| }; |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The QRCodeField uses an external third-party API (api.qrserver.com) without HTTPS enforcement or error handling. This introduces several concerns:
- Security: The URL uses 'https://' but there's no guarantee the service will remain available or secure
- Privacy: Sensitive data in QR codes would be sent to an external service
- Reliability: No fallback if the API is unavailable
- Performance: Network latency for external API calls
Consider using a client-side QR code library instead (e.g., 'qrcode.react' or 'qrcode') to keep data processing local and ensure reliability.
| // This would typically navigate to the detail view | ||
| console.log('View detail:', item); |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The handleView function only logs to console instead of implementing actual navigation or detail view functionality. This placeholder implementation should either be properly implemented (e.g., through an event callback prop) or clearly documented as requiring custom integration by the consumer.
| // This would typically navigate to the detail view | |
| console.log('View detail:', item); | |
| /** | |
| * Default "view" behavior: | |
| * - If the item includes a `url` property, open it in a new tab. | |
| * - Otherwise, no-op. | |
| * | |
| * Consumers that need custom navigation or detail views should either: | |
| * - Provide a `url` field on each item, or | |
| * - Wrap this field widget and override the click behavior externally. | |
| */ | |
| const url = (item as any).url; | |
| if (typeof window !== 'undefined' && typeof url === 'string' && url.length > 0) { | |
| window.open(url, '_blank', 'noopener,noreferrer'); | |
| } |
| const openInMaps = () => { | ||
| if (!location.latitude || !location.longitude) return; | ||
| const url = `https://www.google.com/maps?q=${location.latitude},${location.longitude}`; | ||
| window.open(url, '_blank'); |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The openInMaps function uses window.open with '_blank' target without setting 'noopener,noreferrer' for security. This can expose the parent window to potential security vulnerabilities through the window.opener reference. Consider using window.open(url, '_blank', 'noopener,noreferrer') to prevent the opened page from accessing the parent window.
| window.open(url, '_blank'); | |
| window.open(url, '_blank', 'noopener,noreferrer'); |
| // Convert to base64 or upload to server | ||
| const reader = new FileReader(); | ||
| reader.onloadend = () => { | ||
| onChange(reader.result as string); | ||
| }; | ||
| reader.readAsDataURL(file); | ||
| }; |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Converting large images (up to 5MB) to base64 can significantly increase memory usage and data size (base64 encoding increases size by ~33%). Additionally, storing base64 images in component state and potentially in a database can cause performance issues. Consider:
- Adding a warning about base64 limitations in the documentation
- Optionally supporting direct file upload to a storage service
- Reducing the max file size or adding compression before encoding
- Implementing image resizing before base64 conversion to optimize for avatar use cases (typically 256x256 or similar)
| console.error('Geolocation is not supported by this browser'); | ||
| return; | ||
| } | ||
|
|
||
| setIsLoading(true); | ||
| navigator.geolocation.getCurrentPosition( | ||
| (position) => { | ||
| onChange({ | ||
| latitude: position.coords.latitude, | ||
| longitude: position.coords.longitude, | ||
| accuracy: position.coords.accuracy, | ||
| }); | ||
| setIsLoading(false); | ||
| }, | ||
| (error) => { | ||
| console.error('Error getting location:', error.message); | ||
| setIsLoading(false); |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Error handling uses console.error without providing user feedback. When geolocation fails (browser doesn't support it or user denies permission), users won't see a visible error message. Consider displaying the error in the UI or leveraging the errorMessage prop available in FieldWidgetProps.
| * Signature field widget - provides a signature pad for capturing signatures | ||
| * Outputs signature as a base64 PNG image | ||
| */ | ||
| export function SignatureField({ value, onChange, readonly }: FieldWidgetProps<string>) { |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The SignatureField doesn't use the 'field' parameter at all, unlike other widgets that typically access field.placeholder, field.label, or custom properties. While this might be intentional for this specific widget, it's inconsistent with the pattern seen in other widgets. Consider whether the field prop should be used for configuration (e.g., canvas dimensions, stroke color/width) or mark it as optional in the destructuring if it's genuinely not needed.
| */ | ||
|
|
||
| import { describe, it, expect } from 'vitest'; | ||
| import { render, screen } from '@testing-library/react'; |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused import screen.
| import { render, screen } from '@testing-library/react'; | |
| import { render } from '@testing-library/react'; |
| {value && <AvatarImage src={value} alt={field.label} />} | ||
| <AvatarFallback>{getInitials()}</AvatarFallback> | ||
| </Avatar> | ||
| {!readonly && isHovered && value && ( |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This negation always evaluates to true.
| {!readonly && isHovered && value && ( | |
| {isHovered && value && ( |
| variant="outline" | ||
| size="sm" | ||
| onClick={getCurrentLocation} | ||
| disabled={readonly || isLoading} |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This use of variable 'readonly' always evaluates to false.
| disabled={readonly || isLoading} | |
| disabled={isLoading} |
| {!readonly && ( | ||
| <Button | ||
| type="button" | ||
| variant="outline" | ||
| size="sm" | ||
| onClick={handleAdd} | ||
| className="w-full" | ||
| > | ||
| <Plus className="w-4 h-4 mr-2" /> | ||
| Add Related Record | ||
| </Button> | ||
| )} |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This negation always evaluates to true.
| {!readonly && ( | |
| <Button | |
| type="button" | |
| variant="outline" | |
| size="sm" | |
| onClick={handleAdd} | |
| className="w-full" | |
| > | |
| <Plus className="w-4 h-4 mr-2" /> | |
| Add Related Record | |
| </Button> | |
| )} | |
| <Button | |
| type="button" | |
| variant="outline" | |
| size="sm" | |
| onClick={handleAdd} | |
| className="w-full" | |
| > | |
| <Plus className="w-4 h-4 mr-2" /> | |
| Add Related Record | |
| </Button> |
📦 Bundle Size Report
Size Limits
|
|
✅ All checks passed!
|
ObjectUI field widget coverage was at 59% (19/32 types) relative to @objectstack/spec v0.3.3. This adds the 10 missing widget implementations to achieve 91% coverage.
Added Field Widgets
Input Controls
ColorField- Color picker with hex input/previewSliderField- Range slider (configurable min/max/step)RatingField- Star rating (configurable max stars)CodeField- Monospace code editorComplex Inputs
AvatarField- Profile picture uploader with base64 encodingAddressField- Structured address (street/city/state/zip/country)GeolocationField- Lat/long with browser geolocation API integrationSignatureField- Canvas-based signature captureSpecialized
QRCodeField- QR code generator/displayMasterDetailField- One-to-many relationship editorImplementation
All widgets follow the
FieldWidgetProps<T>interface pattern with readonly/editable mode support:Registered in ComponentRegistry with both direct type (
'color') and prefixed variant ('field:color') for consistency with existing patterns.Coverage
Field widget implementation: 19/32 → 29/32 types (91%)
Remaining 3 types are aliases handled by existing widgets (richtext/html → RichTextField).
Original prompt
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.