diff --git a/.changeset/25915-app-no-access-shared-state.md b/.changeset/25915-app-no-access-shared-state.md new file mode 100644 index 00000000000..671c99b9011 --- /dev/null +++ b/.changeset/25915-app-no-access-shared-state.md @@ -0,0 +1,10 @@ +--- +'@object-ui/app-shell': patch +--- + +fix(app-shell): the "nothing in this app is available to you" screen is drawn by the shared `DataEmptyState` + +Clause-②: no (one block swaps its own markup for the shared components; no exported type change. If a shared component needs a new prop, the thread reports first and the seat amends this line) +Metadata-protocol: none + +The screen an app's root shows when the app serves the caller no navigation, or names only objects the caller may not read (`AppNoAccessEmptyState`), now uses `DataEmptyState` from `@object-ui/components` instead of the `Empty` primitive with a hand-copied icon square. It is announced to assistive technology as `role="status"`. The text, the Open Setup link, the test ids and the `data-reason` attribute (now on the status node) are unchanged; the title takes the shared component's smaller size. diff --git a/ui/packages/app-shell/src/console/AppNoAccessEmptyState.tsx b/ui/packages/app-shell/src/console/AppNoAccessEmptyState.tsx index 9ed5c2061b5..75f26245eb3 100644 --- a/ui/packages/app-shell/src/console/AppNoAccessEmptyState.tsx +++ b/ui/packages/app-shell/src/console/AppNoAccessEmptyState.tsx @@ -27,10 +27,12 @@ import { Link } from 'react-router-dom'; import { Lock } from 'lucide-react'; -// The three primitives `RouteNotFound` and the app-verdict screens in -// `AppContent.tsx` already compose, so this screen looks like its siblings and -// adds nothing to the eager `ui-components` chunk. -import { Button, Empty, EmptyDescription, EmptyTitle } from '@object-ui/components'; +// The shared empty state (objectstack#25915): it draws the muted icon square +// this screen used to hand-copy, and it is `role="status"`, so "nothing here +// for you" is announced instead of being an unlabelled block. `AppContent.tsx` +// already reaches it through `PreviewDraftEmptyState`, so the eager graph +// gains no new module. +import { Button, DataEmptyState } from '@object-ui/components'; import { useObjectLabel, useObjectTranslation } from '@object-ui/i18n'; import { resolveKeyedI18nLabel } from '../utils/index.js'; @@ -56,44 +58,44 @@ export function AppNoAccessEmptyState({ app, setupPath, reason = 'no-navigation' // The same label the header's app switcher shows for this app. const label = appLabel({ name: app.name, label: resolveKeyedI18nLabel(app.label, t) }); + const description = + reason === 'no-readable-object' + ? setupPath + ? t('empty.appNoReadableObjectGrantDescription', { + defaultValue: 'Your account cannot read any of the records in this app yet. You can grant access in Setup.', + }) + : t('empty.appNoReadableObjectDescription', { + defaultValue: + 'Your account cannot read any of the records in this app yet. Ask an administrator to assign you a permission set.', + }) + : setupPath + ? t('empty.appNothingAvailableGrantDescription', { + defaultValue: 'This app has no pages your account can open. You can grant access in Setup.', + }) + : t('empty.appNothingAvailableDescription', { + defaultValue: 'This app has no pages your account can open. Ask an administrator to grant you access.', + }); + return (
- - - - {t('empty.appNothingAvailable', { - app: label, - defaultValue: 'Nothing in {{app}} is available to you yet', - })} - - - {reason === 'no-readable-object' - ? setupPath - ? t('empty.appNoReadableObjectGrantDescription', { - defaultValue: 'Your account cannot read any of the records in this app yet. You can grant access in Setup.', - }) - : t('empty.appNoReadableObjectDescription', { - defaultValue: - 'Your account cannot read any of the records in this app yet. Ask an administrator to assign you a permission set.', - }) - : setupPath - ? t('empty.appNothingAvailableGrantDescription', { - defaultValue: 'This app has no pages your account can open. You can grant access in Setup.', - }) - : t('empty.appNothingAvailableDescription', { - defaultValue: 'This app has no pages your account can open. Ask an administrator to grant you access.', - })} - - {setupPath && ( - - )} - +
); } diff --git a/ui/packages/app-shell/src/console/__tests__/AppNoAccessEmptyState.sharedState-25915.test.tsx b/ui/packages/app-shell/src/console/__tests__/AppNoAccessEmptyState.sharedState-25915.test.tsx new file mode 100644 index 00000000000..6097b97a121 --- /dev/null +++ b/ui/packages/app-shell/src/console/__tests__/AppNoAccessEmptyState.sharedState-25915.test.tsx @@ -0,0 +1,76 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * The app root's "nothing here for you" screen is drawn by the shared + * `DataEmptyState` (objectstack#25915), so it is announced as + * `role="status"`. Before, it composed the bare `Empty` primitive around a + * hand-copied icon square, an unlabelled block to assistive technology. + * + * The routing that decides WHEN this screen shows is pinned by the + * AppContent landing suites; this file pins only what the screen is. + * + * The `t` mock returns each call site's `defaultValue`, which the i18n + * call-site gate holds byte-equal to the `en` pack value. + */ + +import '@testing-library/jest-dom/vitest'; +import { describe, expect, it, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import type { ComponentProps } from 'react'; +import { MemoryRouter } from 'react-router-dom'; + +vi.mock('@object-ui/i18n', async (importOriginal) => ({ + ...(await importOriginal()), + useObjectTranslation: () => ({ + t: (_k: string, o?: { defaultValue?: string }) => o?.defaultValue ?? _k, + }), + useObjectLabel: () => ({ + appLabel: (app: { name: string; label?: string }) => app.label ?? app.name, + }), +})); + +import { AppNoAccessEmptyState } from '../AppNoAccessEmptyState'; + +function renderState(props: Partial> = {}) { + return render( + + + , + ); +} + +describe('AppNoAccessEmptyState draws the shared empty state (objectstack#25915)', () => { + it('is a role="status" DataEmptyState inside the test-id wrapper', () => { + renderState(); + + const wrapper = screen.getByTestId('app-no-access-empty-state'); + const status = screen.getByRole('status'); + expect(wrapper).toContainElement(status); + expect(status).toHaveAttribute('data-slot', 'data-empty-state'); + expect(status).toHaveTextContent('Nothing in {{app}} is available to you yet'); + expect(status).toHaveTextContent('Ask an administrator to grant you access.'); + }); + + it.each([ + ['no-navigation', undefined], + ['no-readable-object', 'no-readable-object' as const], + ])('carries data-reason=%s on the status node', (expected, reason) => { + renderState(reason ? { reason } : {}); + + expect(screen.getByRole('status')).toHaveAttribute('data-reason', expected); + }); + + it('offers Open Setup as the action only when a setup path is given', () => { + renderState({ setupPath: '/setup/permissions' }); + + const link = screen.getByTestId('app-no-access-open-setup'); + expect(screen.getByRole('status')).toContainElement(link); + expect(link).toHaveAttribute('href', '/setup/permissions'); + }); +});