UI/UX Consistency Guidelines
This document provides comprehensive guidelines for maintaining UI/UX consistency across Waldur HomePort. Following these patterns ensures a predictable, accessible, and professional user experience.
Table of Contents
- Empty States
- Button Visibility (Hide vs Disable)
- Loading States
- Tables and Filters
- Dialogs and Confirmations
- Notifications
- Status Indicators
- Tooltips
- Typography and Content
- Accessibility
- Responsive Behavior
- Anti-Patterns
1. Empty States
Empty states are critical touchpoints that can either frustrate users or guide them toward productive actions. Never leave users at dead ends.
1.1 Empty State Types
| Type |
Purpose |
Example |
| First-use |
Onboarding opportunity |
"No projects yet. Create your first project to get started." |
| No search results |
Help refine search |
"Your search 'xyz' did not match any resources." |
| Filtered empty |
Suggest filter modification |
"No resources matching current filters" |
| User-cleared |
Task completion |
"All tasks completed!" |
| Error state |
Recovery with retry |
"Unable to load data." + Reload button |
1.2 Table Empty States
Use the NoResult component for all table empty states:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 | import { NoResult } from '@waldur/navigation/header/search/NoResult';
// Basic usage - let NoResult provide defaults
<NoResult />
// With custom title and message
<NoResult
title={translate('No projects found')}
message={translate('Create a project to start using resources.')}
/>
// With search context and clear action
<NoResult
title={getNoResultTitle({ verboseName: 'projects', hasFilter: true })}
message={getNoResultMessage({ query, verboseName: 'projects' })}
callback={clearFilters}
buttonTitle={translate('Clear filters')}
/>
// With custom action button
<NoResult
title={translate('No resources yet')}
message={translate('Deploy your first resource to get started.')}
actions={
<SubmitButton
label={translate('Deploy resource')}
onClick={handleDeploy}
/>
}
/>
|
Message hierarchy: Title → Explanation → CTA (call-to-action)
Utility functions (from src/table/utils.tsx):
1
2
3
4
5
6
7
8
9
10
11
12 | import { getNoResultTitle, getNoResultMessage } from '@waldur/table/utils';
// For filtered tables
getNoResultTitle({ verboseName: 'users', hasFilter: true });
// → "No users found matching current filters"
// For search queries
getNoResultMessage({ query: 'john', verboseName: 'users' });
// → "Your search "john" did not match any users."
// For empty tables without filters
getNoResultMessage({ verboseName: 'projects', customEmpty: translate('Start by creating a project.') });
|
1.3 Inline Empty Values
Standard: Use DASH_ESCAPE_CODE (—) for null/undefined values in displays:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | import { DASH_ESCAPE_CODE } from '@waldur/table/constants';
import { renderFieldOrDash } from '@waldur/table/utils';
// In table columns
{
title: translate('Description'),
render: ({ row }) => renderFieldOrDash(row.description),
}
// In detail views
<Field label={translate('End date')} value={renderFieldOrDash(project.end_date)} />
// Direct usage
{user.phone || DASH_ESCAPE_CODE}
|
For arrays:
| // Empty array - use descriptive message
{items.length > 0 ? items.map(renderItem) : translate('None')}
// Or use dash for consistency
{items.length > 0 ? items.join(', ') : DASH_ESCAPE_CODE}
|
1.4 Empty State Message Templates
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | // Default (no context)
translate('No {verboseName} found', { verboseName })
// With search query
translate('Your search "{query}" did not match any {verboseName}.', { query, verboseName })
// With active filters
translate('No {verboseName} found matching current filters', { verboseName })
// First use (encouraging)
translate('No {verboseName} yet. Create your first {singular} to get started.', { verboseName, singular })
// After action completion
translate('All {verboseName} have been processed.')
|
The decision to hide vs disable a button significantly impacts user experience. Use this decision matrix consistently.
2.1 Decision Matrix
| Scenario |
Action |
Rationale |
| User lacks permission (role-based) |
HIDE |
User will never be authorized in current context |
| Resource in wrong state |
DISABLE + tooltip |
Temporary; user can fix by changing state |
| Action in progress |
DISABLE + spinner |
Will become available when complete |
| Feature not applicable |
HIDE |
Doesn't apply to this resource type |
| Validation incomplete |
DISABLE + tooltip |
User can complete requirements |
| Quota exceeded |
DISABLE + tooltip |
User can request more quota |
ALWAYS provide a tooltip explaining WHY the button is disabled.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 | import { useValidators } from '@waldur/resource/actions/useValidators';
import { ActionItem } from '@waldur/resource/actions/ActionItem';
// Using useValidators hook for state-based validation
const validators = [
({ resource }) => {
if (resource.state !== 'OK') {
return translate('Resource must be in OK state');
}
},
({ resource }) => {
if (resource.runtime_state !== 'ACTIVE') {
return translate('Instance must be running');
}
},
];
const MyAction = ({ resource }) => {
const { tooltip, disabled } = useValidators(validators, resource);
return (
<ActionItem
title={translate('Restart')}
action={handleRestart}
disabled={disabled}
tooltip={tooltip} // Always provide tooltip when disabled
iconNode={<ArrowClockwiseIcon weight="bold" />}
/>
);
};
|
Visual requirements:
- Disabled buttons use design token colors (e.g.,
text-muted, btn-disabled) - NOT opacity
- Opacity is reserved for overlays only; components use solid colors for predictability, accessibility, and theming
- Keep the same width to prevent layout shift
- Use
aria-disabled for accessibility
2.3 Permission Patterns
Use hasPermission() utility consistently:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | import { hasPermission } from '@waldur/permissions/hasPermission';
import { useUser } from '@waldur/workspace/hooks';
const MyComponent = ({ project }) => {
const user = useUser();
// HIDE if user lacks permission (they can never do this)
if (!hasPermission(user, {
permission: 'resource.create',
projectId: project.uuid
})) {
return null; // Early return - hide entire component
}
return <CreateResourceButton />;
};
|
Staff-only actions:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | import { StaffOnlyIndicator } from '@waldur/customer/details/StaffOnlyIndicator';
// For staff-only actions that should still be visible
<ActionItem
title={translate('Admin action')}
action={handleAction}
staff // Shows StaffOnlyIndicator badge
/>
// Check staff status
const user = useUser();
if (!user?.is_staff) {
return null; // Hide from non-staff
}
|
3. Loading States
Consistent loading feedback prevents user confusion and maintains perceived performance.
3.1 Table Loading
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | // Table handles this automatically via the loading prop
// Spinner shown when: loading === true && rows.length === 0
// For manual control in custom components:
import { LoadingSpinner } from '@waldur/core/LoadingSpinner';
{loading && !data.length ? (
<LoadingSpinner />
) : (
<DataContent data={data} />
)}
// With existing data - show subtle indicator, don't replace content
{loading && data.length > 0 && (
<div className="text-center py-2">
<LoadingSpinnerIcon className="text-muted" />
</div>
)}
|
Use the pending prop on buttons:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | import { SubmitButton } from '@waldur/form';
import { BaseButton } from '@waldur/core/buttons/BaseButton';
// Form submit button
<SubmitButton
label={translate('Save')}
submitting={isSubmitting} // Shows spinner, disables button
/>
// Action button
<BaseButton
label={translate('Process')}
onClick={handleProcess}
pending={isProcessing} // Shows spinner, disables button
size="sm"
/>
|
Key behaviors:
- Button shows spinner and becomes disabled
- Button width stays stable (no layout shift)
- Label remains visible next to spinner
3.3 Error States
Use LoadingErred component for recoverable errors:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | import { LoadingErred } from '@waldur/core/LoadingErred';
// In data-fetching components
if (error) {
return (
<LoadingErred
loadData={refetch}
message={translate('Unable to load projects.')}
/>
);
}
// Custom error message
<LoadingErred
loadData={retry}
message={translate('Connection failed. Please check your network.')}
/>
|
Always provide a retry action - never leave users stuck.
4. Tables and Filters
4.1 Filter Visibility Rules
| Filter Position |
When Visible |
On Empty Table |
header |
Always |
Always visible |
menu |
Toggle button click |
Show toggle button |
sidebar |
When filters active OR toggled |
Show toggle button (allow discovery) |
Important: Never completely hide filters on empty tables. Users need to discover that filters exist and may be causing the empty state.
When filters return no results, show a specific empty state:
- Message: "No results match your filters"
- Actions: "Clear filters" / "View filters"
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | <NoResult
title={translate('No results match your filters')}
message={translate('Try adjusting your filters or clear them to see all items.')}
actions={
<>
<Button variant="tertiary" onClick={clearFilters}>
{translate('Clear filters')}
</Button>
<Button variant="outline" onClick={openFilters}>
{translate('View filters')}
</Button>
</>
}
/>
|
| // Table configuration
<Table
filters={<MyFilters />}
filterPosition="menu" // 'header' | 'menu' | 'sidebar'
// ...
/>
|
4.2 Filter Behavior Checklist
1
2
3
4
5
6
7
8
9
10
11
12 | import { PAGE_SIZE_COMPACT, PAGE_SIZE_FULL } from '@waldur/table/constants';
// PAGE_SIZE_COMPACT = 5 (for embedded/secondary tables)
// PAGE_SIZE_FULL = 10 (for primary tables)
// Hide pagination when items ≤ PAGE_SIZE_COMPACT
{pagination.resultCount > PAGE_SIZE_COMPACT && (
<TablePagination {...pagination} />
)}
// Show item count
// Format: "Showing 1-10 of 100"
|
5. Dialogs and Confirmations
5.1 Confirmation Dialog Pattern
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 | import { waitForConfirmation } from '@waldur/modal/actions';
// Destructive action (deletion) - name the object being deleted
const handleDelete = async () => {
try {
await waitForConfirmation(
dispatch,
translate('Delete {type}', { type: 'project' }),
translate('"{name}" will be permanently deleted. This action cannot be undone.', { name: resource.name }),
{
forDeletion: true, // Red styling, warning icon
size: 'sm',
positiveButton: translate('Delete'), // Clear action label
}
);
// User confirmed - proceed with deletion
await deleteResource(resource.uuid);
dispatch(showSuccess(translate('Resource deleted')));
} catch {
// User cancelled - do nothing
return;
}
};
// Non-destructive confirmation
await waitForConfirmation(
dispatch,
translate('Confirm action'),
translate('This will restart all services. Continue?'),
{
positiveButton: translate('Restart'),
}
);
|
Button order: Cancel (left), Confirm (right)
Clear action labels: Use "Delete", "Restart", "Submit" - not "OK" or "Yes"
| // Standard form dialog footer
<div className="d-flex gap-2 justify-content-end">
<Button variant="tertiary" onClick={closeDialog}>
{translate('Cancel')}
</Button>
<SubmitButton
label={translate('Save changes')}
submitting={isSubmitting}
disabled={!isValid}
/>
</div>
|
6. Notifications
Use the notification utilities from @waldur/store/notify:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 | import {
showSuccess,
showError,
showErrorResponse,
showInfo
} from '@waldur/store/notify';
// Success - action completed
dispatch(showSuccess(translate('Project created successfully')));
// Success with details
dispatch(showSuccess(
translate('Resource deployed'),
translate('Your resource will be ready in a few minutes.')
));
// Error - action failed
dispatch(showError(translate('Unable to save changes')));
// Error with API response details
try {
await saveData();
} catch (error) {
dispatch(showErrorResponse(error, translate('Unable to save changes.')));
}
// Info - informational only
dispatch(showInfo(translate('Changes will take effect after refresh')));
|
Configuration:
- Duration: 7000ms (7 seconds)
- Position: top-right
- Dismissible: Yes (show dismiss button)
7. Status Indicators
7.1 StateIndicator Component
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 | import { StateIndicator } from '@waldur/core/StateIndicator';
// Basic usage
<StateIndicator
label={resource.state}
variant="success"
/>
// With spinner for active/in-progress states
<StateIndicator
label={translate('Creating')}
variant="primary"
active // Shows spinner
/>
// Common variant options
<StateIndicator label="Active" variant="success" />
<StateIndicator label="Pending" variant="warning" />
<StateIndicator label="Error" variant="danger" />
<StateIndicator label="Inactive" variant="default" />
// Styling options
<StateIndicator
label="OK"
variant="success"
outline // Outlined style
pill // Rounded pill shape
hasBullet // Shows bullet indicator
size="sm" // 'sm' | 'lg'
/>
|
7.2 Variant Mapping Guidelines
| State Category |
Variant |
Examples |
| Success/Active |
success |
Active, Running, Completed, Approved |
| Warning/Pending |
warning |
Pending, Processing, Updating |
| Error/Failed |
danger |
Error, Failed, Rejected, Unavailable |
| Neutral/Default |
default |
Draft, Archived, Paused, Unknown |
| Info |
info |
New, In Review |
Custom variants (for differentiation within same category):
pink, blue, teal, indigo, purple, rose, orange, moss
8.1 Usage Guidelines
Use tooltips for:
- Disabled buttons: Explain why disabled (required)
- Icon-only buttons: Always provide tooltip describing the action (required)
- Truncated text: Show full text
- Icons without labels: Describe the element's purpose
- Complex terms: Provide definitions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 | import { Tip } from '@waldur/core/Tooltip';
// Basic tooltip
<Tip label={translate('Copy to clipboard')} id="copy-btn">
<CopyIcon />
</Tip>
// Tooltip with body (title + description)
<Tip
label={translate('Resource limits')}
body={translate('Maximum amount of resources that can be allocated.')}
id="limits-info"
>
<InfoIcon />
</Tip>
// Light theme tooltip
<Tip label={fullText} id="text-tooltip" theme="light">
<span className="ellipsis">{truncatedText}</span>
</Tip>
|
From ActionItem - use question icon for disabled action explanation:
1
2
3
4
5
6
7
8
9
10
11
12 | import { QuestionIcon } from '@phosphor-icons/react';
// When action is disabled, show question icon with tooltip
{props.disabled && props.tooltip && (
<Tip label={props.tooltip} id={`disabled-reason-${id}`}>
<QuestionIcon
size={20}
weight="bold"
className="text-muted ms-1"
/>
</Tip>
)}
|
9. Typography and Content
9.1 Text Truncation
1
2
3
4
5
6
7
8
9
10
11
12 | import { formatLongText } from '@waldur/table/utils';
// For text > 100 characters - shows tooltip with full text
{formatLongText(description)}
// CSS truncation class
<span className="ellipsis">{text}</span>
// With custom width
<span className="ellipsis d-inline-block" style={{ maxWidth: 200 }}>
{text}
</span>
|
9.2 Internationalization
All user-facing text must use translate():
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | import { translate } from '@waldur/i18n';
// Simple string
translate('Save changes')
// With placeholders
translate('Hello, {name}!', { name: user.name })
// Plural forms
translate('{count} item', '{count} items', { count })
// Never hard-code strings
// ❌ <Button>Submit</Button>
// ✅ <Button>{translate('Submit')}</Button>
|
10. Accessibility
10.1 Disabled State Accessibility
| // Use aria-disabled for screen reader support
<button
aria-disabled={isDisabled}
onClick={!isDisabled ? handleClick : undefined}
className={isDisabled ? 'text-muted' : ''}
>
{label}
</button>
// Tooltips on disabled buttons should be accessible
// The Tip component handles this automatically
|
10.2 Keyboard Navigation
- All interactive elements must be keyboard accessible
- Use proper focus management in modals
- Maintain logical tab order
10.3 Screen Reader Support
| // Loading states should be announced
<LoadingSpinnerIcon
role="status"
aria-label={translate('Loading...')}
/>
// Provide text alternatives for icons
<button aria-label={translate('Delete item')}>
<TrashIcon />
</button>
|
11. Responsive Behavior
11.1 Breakpoints
| import { GRID_BREAKPOINTS } from '@waldur/core/constants';
// GRID_BREAKPOINTS = { xs: 0, sm: 576, md: 768, lg: 992, xl: 1200, xxl: 1400 }
import { useMediaQuery } from 'react-responsive';
const isSm = useMediaQuery({ maxWidth: GRID_BREAKPOINTS.sm });
const isMd = useMediaQuery({ maxWidth: GRID_BREAKPOINTS.md });
|
11.2 Filter Position Adaptation
| // Automatically converts 'menu' to 'sidebar' on small screens
const filterPosition = isSm && originalPosition === 'menu'
? 'sidebar'
: originalPosition;
|
11.3 Interactive Element Sizing
Form controls (inputs, selects, textareas):
- Minimum height: 40px for adequate touch/click area
Buttons:
- Three standard sizes: 44px (large), 36px (default), 28px (small)
- All sizes are acceptable for desktop interfaces
- Use size appropriate to context and hierarchy
Icon-only buttons:
- Should maintain adequate click area even with small icons
- Consider padding to reach at least 28px hit area
| // Button sizes
<Button size="lg">Large (44px)</Button>
<Button>Default (36px)</Button>
<Button size="sm">Small (28px)</Button>
|
Note: The 44px minimum touch target (WCAG) is primarily for mobile/touch interfaces. Desktop applications can use smaller interactive elements.
12. Anti-Patterns
What NOT to Do
| Anti-Pattern |
Problem |
Correct Approach |
Redundant length === 0 checks |
Table already handles empty |
Trust the Table component |
Mixed null display (—, "N/A", "", "None") |
Inconsistent |
Always use DASH_ESCAPE_CODE or renderFieldOrDash |
| Hide + Disable for same scenario |
Confusing |
Follow decision matrix consistently |
| Disabled button without tooltip |
User doesn't know why |
Always provide tooltip |
| Hard-coded strings |
Not translatable |
Always use translate() |
| Hidden filters on empty tables |
Can't discover filters |
Show filter toggle |
| Empty state without CTA |
Dead end |
Always provide next action |
user.is_staff checks everywhere |
Inconsistent |
Use hasPermission() utility |
Code Examples - Bad vs Good
| // ❌ BAD: Inconsistent empty display
{user.email || 'N/A'}
{user.phone || ''}
{user.name || '—'}
// ✅ GOOD: Consistent
{renderFieldOrDash(user.email)}
{renderFieldOrDash(user.phone)}
{renderFieldOrDash(user.name)}
|
| // ❌ BAD: Disabled without explanation
<Button disabled={!canEdit}>Edit</Button>
// ✅ GOOD: Disabled with tooltip
<Tip label={canEdit ? null : translate('You need edit permission')}>
<Button disabled={!canEdit}>Edit</Button>
</Tip>
|
| // ❌ BAD: Inconsistent staff checks
if (user.is_staff) { ... }
if (hasPermission(user, { permission: 'admin.view' })) { ... }
if (user?.is_staff || user?.is_support) { ... }
// ✅ GOOD: Consistent permission checking
if (hasPermission(user, { permission: 'resource.admin', projectId })) { ... }
|
| // ❌ BAD: Empty state dead end
{items.length === 0 && <p>No items</p>}
// ✅ GOOD: Actionable empty state
{items.length === 0 && (
<NoResult
title={translate('No items yet')}
message={translate('Create your first item to get started.')}
actions={<CreateButton />}
/>
)}
|
Quick Reference
Key Imports
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37 | // Empty states
import { NoResult } from '@waldur/navigation/header/search/NoResult';
import { DASH_ESCAPE_CODE } from '@waldur/table/constants';
import { renderFieldOrDash, getNoResultTitle, getNoResultMessage } from '@waldur/table/utils';
// Buttons & Actions
import { BaseButton } from '@waldur/core/buttons/BaseButton';
import { ActionItem } from '@waldur/resource/actions/ActionItem';
import { useValidators } from '@waldur/resource/actions/useValidators';
// Loading & Errors
import { LoadingSpinner, LoadingSpinnerIcon } from '@waldur/core/LoadingSpinner';
import { LoadingErred } from '@waldur/core/LoadingErred';
// Indicators
import { StateIndicator } from '@waldur/core/StateIndicator';
import { Badge } from '@waldur/core/Badge';
// Tooltips
import { Tip } from '@waldur/core/Tooltip';
// Permissions
import { hasPermission } from '@waldur/permissions/hasPermission';
import { StaffOnlyIndicator } from '@waldur/customer/details/StaffOnlyIndicator';
// Notifications
import { showSuccess, showError, showErrorResponse, showInfo } from '@waldur/store/notify';
// Modals
import { waitForConfirmation } from '@waldur/modal/actions';
// i18n
import { translate } from '@waldur/i18n';
// Constants
import { GRID_BREAKPOINTS } from '@waldur/core/constants';
import { PAGE_SIZE_COMPACT, PAGE_SIZE_FULL } from '@waldur/table/constants';
|
Decision Trees
Should I hide or disable this button?
| Is the user PERMANENTLY unable to perform this action in current context?
├─ YES → HIDE the button
│ Examples: lacks role, feature not applicable, wrong resource type
│
└─ NO (temporary or user-fixable) → DISABLE with tooltip
Examples: wrong state, validation incomplete, quota exceeded, action in progress
|
What empty state should I show?
| Is there an active search/filter?
├─ YES (search) → "Your search '{query}' did not match any {items}"
├─ YES (filter) → "No {items} found matching current filters" + Clear button
│
└─ NO → Is this first-time use?
├─ YES → Encouraging message + Create CTA
└─ NO → "No {items} found" + relevant action
|
Top 10 Inconsistencies to Fix
Based on a codebase analysis, these are prioritized inconsistencies that should be addressed:
1. Mixed Null/Empty Display Values
Files affected: src/vmware/PortsList.tsx, src/project/manage/ProjectGeneral.tsx, src/user/hooks/HooksList.tsx, and others
Problem: Using || 'N/A' instead of renderFieldOrDash()
| // Current (inconsistent)
row.network_name || 'N/A'
project.name || 'N/A'
row.destination_url || row.email || 'N/A'
// Should be
renderFieldOrDash(row.network_name)
renderFieldOrDash(project.name)
|
2. Scattered Staff Permission Checks
Files affected: Multiple files with direct user.is_staff checks
Problem: Permission checks done inconsistently across components
| // Current (scattered)
if (user.is_staff) { ... }
// Should use centralized utility
if (hasPermission(user, { permission: 'staff.action', ... })) { ... }
|
Files affected: src/table/Table.tsx:301-323
Problem: Sidebar filters only show when filtersStorage.length > 0, preventing filter discovery
| // Current behavior
{props.filterPosition === 'sidebar' && props.filtersStorage.length > 0 && ...}
// Should show toggle button even when no filters active
|
Various components have disabled buttons that don't explain why they're disabled.
Fix: Audit all disabled props and ensure accompanying tooltip prop
5. Empty Copy Field Values
Files affected: src/proposals/manage/CallProposalsList.tsx, src/openstack/openstack-tenant/TenantPortsList.tsx
Problem: Using || '' for copy fields can result in copying empty string
| // Current
copyField: (row) => row.mac_address || ''
// Should provide fallback or hide copy when empty
copyField: (row) => row.mac_address || undefined
|
6. Inconsistent Empty State Messages
Various list components show plain text instead of using the NoResult component.
Fix: All list empty states should use NoResult with appropriate messaging
7. Mixed Boolean Permission Returns
Files affected: src/permissions/hasPermission.ts
Problem: Function returns true or undefined instead of true or false
| // Current
if (...) return true;
// Falls through to undefined
// Should be explicit
return false;
|
8. Invitation Display Inconsistencies
Files affected: src/invitations/join-organization/submission.tsx
Problem: Using || 'N/A' in user-facing messages
| // Current
organization: groupInvitation.scope_name || 'N/A'
// Should handle missing data more gracefully
|
Files affected: Various form components using || ''
Problem: Inconsistent handling of empty form values
10. Missing Error State Handling
Various data-fetching components don't show LoadingErred on fetch failure.
Fix: Audit all data-fetching components for proper error state handling
Implementation Checklist
When fixing these inconsistencies: