|
7 | 7 | */ |
8 | 8 |
|
9 | 9 | import * as React from 'react'; |
10 | | -import { cn, Button, Input, Popover, PopoverContent, PopoverTrigger, FilterBuilder, SortBuilder, NavigationOverlay, GroupingEditor, RefreshIndicator, DataEmptyState, DataErrorState, resolveIcon, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@object-ui/components'; |
| 10 | +import { cn, Button, Input, Popover, PopoverContent, PopoverTrigger, FilterBuilder, SortBuilder, NavigationOverlay, GroupingEditor, RefreshIndicator, DataErrorState, resolveIcon, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@object-ui/components'; |
11 | 11 | import type { SortItem } from '@object-ui/components'; |
12 | | -import { Search, SlidersHorizontal, ArrowUpDown, X, EyeOff, Pencil, Group, Paintbrush, Inbox, Download, Rows4, Rows3, Rows2, Share2, Printer, Plus, Trash2, CheckSquare, AlertTriangle, ShieldAlert, RotateCw, Loader2, type LucideIcon } from 'lucide-react'; |
| 12 | +import { Search, SlidersHorizontal, ArrowUpDown, X, EyeOff, Pencil, Group, Paintbrush, Download, Rows4, Rows3, Rows2, Share2, Printer, Plus, Trash2, CheckSquare, AlertTriangle, ShieldAlert, RotateCw, Loader2 } from 'lucide-react'; |
13 | 13 | import type { FilterBuilderRowGroup } from '@object-ui/components'; |
14 | 14 | import { VALUELESS_FILTER_BUILDER_OPERATORS, isFilterValueComplete } from '@object-ui/components'; |
15 | 15 | import { ViewSwitcherDropdown } from './ViewSwitcher'; |
| 16 | +import { ListEmptyState, hasDeclaredFilter } from './ListEmptyState'; |
16 | 17 | import { ViewSettingsPopover } from './components/ViewSettingsPopover'; |
17 | 18 | import { UserFilters } from './UserFilters'; |
18 | 19 | import { |
@@ -1035,8 +1036,12 @@ export const LIST_DEFAULT_TRANSLATIONS: Record<string, string> = { |
1035 | 1036 | * Delegates to `@object-ui/i18n`'s `createSafeTranslation`. The local copy |
1036 | 1037 | * this replaced wrapped the hook in try/catch — the very thing the comment on |
1037 | 1038 | * `useListViewObjectLabel` below warns against (objectui#2879). |
| 1039 | + * |
| 1040 | + * Exported for this package's `ObjectGallery` only (objectui#12217), which |
| 1041 | + * draws `ListEmptyState`'s `list.*` copy when no `ListView` is around it; the |
| 1042 | + * package barrel does not re-export it. |
1038 | 1043 | */ |
1039 | | -const useListViewTranslation = createSafeTranslation( |
| 1044 | +export const useListViewTranslation = createSafeTranslation( |
1040 | 1045 | LIST_DEFAULT_TRANSLATIONS, |
1041 | 1046 | 'list.recordCount', |
1042 | 1047 | ); |
@@ -5674,84 +5679,34 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({ |
5674 | 5679 | /> |
5675 | 5680 | ))} |
5676 | 5681 | </div> |
5677 | | - ) : !loading && data.length === 0 && currentView === 'grid' ? ( |
5678 | | - (() => { |
5679 | | - const iconName = schema.emptyState?.icon; |
5680 | | - // objectui#5935: normalisation through the ONE seam. This site had |
5681 | | - // its own inline `split('-')` and no rename map, so `home` and every |
5682 | | - // snake_case spelling fell through to `Inbox` here while resolving |
5683 | | - // elsewhere. ⛔ The `Inbox` fallback itself stays at this call site |
5684 | | - // — an empty state always shows a glyph, and that is this surface's |
5685 | | - // decision, not the seam's (maintainer ruling 2026-09-03, option C). |
5686 | | - const ResolvedIcon: LucideIcon = resolveIcon(iconName) ?? Inbox; |
5687 | | - // Distinguish "filtered/searched to empty" from "truly empty |
5688 | | - // (first run)". A new user with no filters shouldn't be told to |
5689 | | - // "adjust your filters" — they should be invited to create. |
5690 | | - // |
5691 | | - // The VIEW's own `filter` counts as an active query too |
5692 | | - // (objectui#4155). It used to be excluded, so a view that returns |
5693 | | - // nothing *because it is filtered* — the declared `status not_in |
5694 | | - // [archived]`, or a stale stored condition — rendered the first-run |
5695 | | - // copy ("no data yet / create your first record") over an object |
5696 | | - // full of records. That reads as data loss or a permission problem |
5697 | | - // and sends triage away from the view layer, which is exactly what |
5698 | | - // this issue reported. |
5699 | | - // |
5700 | | - // …but it is not the USER's query (objectui#11687). "Your current |
5701 | | - // filters or search" is said only when the user applied one — the |
5702 | | - // search box, the user-filter chips, the filter panel. A view |
5703 | | - // emptied by its own declared filter alone gets its own message, |
5704 | | - // which names the view's filter instead of telling the user to |
5705 | | - // clear filters they never set. |
5706 | | - const hasBaseFilter = |
5707 | | - Array.isArray(authoredFilter) |
5708 | | - ? authoredFilter.length > 0 |
5709 | | - : !!authoredFilter && typeof authoredFilter === 'object' |
5710 | | - ? Object.keys(authoredFilter).length > 0 |
5711 | | - : false; |
5712 | | - const hasUserQuery = |
| 5682 | + ) : !loading && data.length === 0 && (currentView === 'grid' || currentView === 'gallery') ? ( |
| 5683 | + // objectui#12217 — the gallery draws the grid's empty state, through |
| 5684 | + // the one shared component: the authored `emptyState`, resolved |
| 5685 | + // against the display locale, otherwise the localized `list.*` copy. |
| 5686 | + // It used to hand the empty list to `ObjectGallery`, which drew a |
| 5687 | + // hard-coded English line and never read the view's `emptyState`. |
| 5688 | + <ListEmptyState |
| 5689 | + title={schema.emptyState?.title} |
| 5690 | + message={schema.emptyState?.message} |
| 5691 | + // objectui#5935: the authored name is normalised through the ONE |
| 5692 | + // icon seam. This site once had its own inline `split('-')` and no |
| 5693 | + // rename map, so `home` and every snake_case spelling fell through |
| 5694 | + // to the fallback here while resolving elsewhere. What an |
| 5695 | + // unresolvable name draws is the empty state's own decision. |
| 5696 | + icon={resolveIcon(schema.emptyState?.icon)} |
| 5697 | + hasBaseFilter={hasDeclaredFilter(authoredFilter)} |
| 5698 | + // The USER's query (objectui#11687): the search box, the |
| 5699 | + // user-filter chips, the filter panel's own conditions. The view's |
| 5700 | + // declared `filter` is `hasBaseFilter`, not this. |
| 5701 | + hasUserQuery={ |
5713 | 5702 | !!(searchTerm && searchTerm.trim()) || |
5714 | 5703 | (Array.isArray(appliedUserFilterConditions) && appliedUserFilterConditions.length > 0) || |
5715 | | - (Array.isArray(appliedFilters?.conditions) && appliedFilters.conditions.length > 0); |
5716 | | - const hasActiveQuery = hasUserQuery || hasBaseFilter; |
5717 | | - // objectui#11227 — `title` and `message` are the spec's `I18nLabel` |
5718 | | - // (`EmptyStateSchema`): a plain string or an inline locale map. They |
5719 | | - // are RESOLVED against the display locale, as the view's `label` |
5720 | | - // is. A `typeof === 'string'` test stood here, which is not a |
5721 | | - // resolution: it answered "absent" for every map an author may write, |
5722 | | - // so a localised empty state silently drew the default copy in every |
5723 | | - // locale. A string still passes through unchanged, and a map with no |
5724 | | - // usable entry still falls to the default. |
5725 | | - const title = resolveInlineI18nLabel(schema.emptyState?.title, displayLocale) |
5726 | | - ?? (hasActiveQuery ? t('list.noMatches') : t('list.firstRunTitle')); |
5727 | | - const description = resolveInlineI18nLabel(schema.emptyState?.message, displayLocale) |
5728 | | - ?? (hasUserQuery |
5729 | | - ? t('list.noMatchesMessage') |
5730 | | - : hasBaseFilter |
5731 | | - ? t('list.viewFilterNoMatchesMessage') |
5732 | | - : t('list.firstRunMessage')); |
5733 | | - return ( |
5734 | | - <DataEmptyState |
5735 | | - data-testid="empty-state" |
5736 | | - className="h-full min-h-[200px] p-8 gap-1 [&>h3]:text-lg [&>h3]:font-medium [&>h3]:text-foreground [&>p]:max-w-md" |
5737 | | - icon={<ResolvedIcon className="h-12 w-12 text-muted-foreground/50" />} |
5738 | | - iconWrapperClassName="mb-3" |
5739 | | - title={title} |
5740 | | - description={description} |
5741 | | - action={toolbarFlags.showAddRecord ? ( |
5742 | | - <Button |
5743 | | - variant="default" |
5744 | | - size="sm" |
5745 | | - data-testid="empty-state-add-record" |
5746 | | - onClick={() => props.onAddRecord?.()} |
5747 | | - > |
5748 | | - <Plus className="h-4 w-4 mr-1.5" /> |
5749 | | - {t('list.addRecord')} |
5750 | | - </Button> |
5751 | | - ) : undefined} |
5752 | | - /> |
5753 | | - ); |
5754 | | - })() |
| 5704 | + (Array.isArray(appliedFilters?.conditions) && appliedFilters.conditions.length > 0) |
| 5705 | + } |
| 5706 | + onAddRecord={toolbarFlags.showAddRecord ? () => props.onAddRecord?.() : undefined} |
| 5707 | + t={t} |
| 5708 | + displayLocale={displayLocale} |
| 5709 | + /> |
5755 | 5710 | ) : ( |
5756 | 5711 | <SchemaRenderer |
5757 | 5712 | schema={viewComponentSchema} |
|
0 commit comments