diff --git a/.changeset/12079-empty-nav-landing.md b/.changeset/12079-empty-nav-landing.md new file mode 100644 index 0000000000..af99a036c4 --- /dev/null +++ b/.changeset/12079-empty-nav-landing.md @@ -0,0 +1,17 @@ +--- +'@object-ui/app-shell': patch +'@object-ui/i18n': patch +--- + +An app that serves the signed-in user no navigation now opens on an app-level empty state, not on the Studio home (objectui#12079). + +The server filters an app's navigation per user. When every group is gated on a capability the user does not hold, the app arrives with `navigation: []`. The console's app index route found no landing page and rendered `StudioHomePage`, Studio's metadata overview, inside an app that had nothing to do with Studio. This was measured on HotCLM for its dev admin (objectstack-ai/hotclm#87). + +The app root now shows "Nothing in APP is available to you yet", with the app's own label. A user the console treats as a workspace administrator (`useWorkspaceAdminStatus`, the same check the console's no-app screen and Home's administration links use) gets an "Open Setup" link, at the URL the `/setup` deep link resolves. Everyone else is asked to contact an administrator. + +Unchanged: + +- An app with a landing page still opens on it. +- The Studio app still opens on its own home. Its navigation is full but holds only `component` items, which the landing resolver does not walk, so it reaches the same fallback with a non-empty navigation. + +New `@object-ui/i18n` keys, in all ten language packs: `empty.appNothingAvailable`, `empty.appNothingAvailableDescription`, `empty.appNothingAvailableGrantDescription` and `empty.openSetup`. diff --git a/packages/app-shell/src/console/AppContent.tsx b/packages/app-shell/src/console/AppContent.tsx index 008630853d..adafa25a59 100644 --- a/packages/app-shell/src/console/AppContent.tsx +++ b/packages/app-shell/src/console/AppContent.tsx @@ -49,6 +49,9 @@ import { KeyboardShortcutsDialog } from '../chrome/KeyboardShortcutsDialog.js'; import { OnboardingWalkthrough } from '../chrome/OnboardingWalkthrough.js'; import { RouteFader } from '../chrome/RouteFader.js'; import { NavigationSyncEffect } from '../hooks/useNavigationSync.js'; +import { AppNoAccessEmptyState } from './AppNoAccessEmptyState.js'; +import { appServesNoNavigation } from './appServesNoNavigation.js'; +import { resolveSetupAppPath } from './ConsoleShell.js'; // Route-based code splitting — lazy-load less-frequently-used routes const RecordDetailView = lazy(() => import('../views/RecordDetailView.js').then(m => ({ default: m.RecordDetailView }))); @@ -1075,13 +1078,30 @@ export function AppContent({ extraRoutes, extraRoutesNoApp }: AppContentProps = { - // When the app declares a landing target (home page or - // first nav route) honour it; otherwise — e.g. the - // metadata-admin "Studio" app whose nav is built from - // domains and has no single landing — render the rich - // overview instead of a blank ``. + // When the app declares a landing target (first nav + // route) honour it. const landing = resolveLandingRoute(activeApp, { currentUserId: user?.id ?? null }); - return landing ? : ; + if (landing) return ; + // objectui#12079 — an app that serves this caller NO + // navigation (every group gated on a capability they do + // not hold) is an app they have no access in yet. Say so, + // and offer Setup to a caller who can grant access: the + // same `isWorkspaceAdmin` read the no-app empty state + // above gates its Setup button on, and the same Setup + // URL the `/setup` deep link resolves. + if (appServesNoNavigation(activeApp)) { + return ( + + ); + } + // A navigation with no landing: the metadata-admin + // "Studio" app, whose nav is built from `component` + // items the landing resolver does not walk. Its root is + // the rich overview, not a blank ``. + return ; })()} /> {/* Metadata admin routes — declared BEFORE the generic diff --git a/packages/app-shell/src/console/AppNoAccessEmptyState.tsx b/packages/app-shell/src/console/AppNoAccessEmptyState.tsx new file mode 100644 index 0000000000..268337fae0 --- /dev/null +++ b/packages/app-shell/src/console/AppNoAccessEmptyState.tsx @@ -0,0 +1,78 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * The app root's answer when the app serves this caller NO navigation + * (objectui#12079). + * + * The server filters an app's navigation per caller before it reaches the + * console (`requiredPermissions`, `requiresService` and the doc audience, with + * emptied groups dropped). An app whose every group is capability-gated + * therefore arrives as `navigation: []` for a caller who holds none of those + * capabilities. The sidebar is empty, and the app index route has no landing + * to resolve. Until objectui#12079 that route fell back to `StudioHomePage`, + * the Studio app's own overview, so the caller saw metadata counters and + * "New Object" quick actions inside an app that had nothing to do with Studio, + * and nothing told them what to do next. + * + * This is an app-level empty state instead. It names the app and says the + * caller has no access in it yet. A caller who can grant access gets a link to + * Setup; everyone else is told to ask an administrator. + */ + +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'; +import { useObjectLabel, useObjectTranslation } from '@object-ui/i18n'; +import { resolveKeyedI18nLabel } from '../utils/index.js'; + +export interface AppNoAccessEmptyStateProps { + /** The active app, as the console's metadata list carries it. */ + app: { name: string; label?: string | { key: string; defaultValue?: string } }; + /** + * Where Setup lives, for a caller who can grant access. `null` for everyone + * else, which hides the link and asks them to contact an administrator. + */ + setupPath: string | null; +} + +export function AppNoAccessEmptyState({ app, setupPath }: AppNoAccessEmptyStateProps) { + const { t } = useObjectTranslation(); + const { appLabel } = useObjectLabel(); + // The same label the header's app switcher shows for this app. + const label = appLabel({ name: app.name, label: resolveKeyedI18nLabel(app.label, t) }); + + return ( +
+ + + + {t('empty.appNothingAvailable', { + app: label, + defaultValue: 'Nothing in {{app}} is available to you yet', + })} + + + {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/packages/app-shell/src/console/__tests__/AppContent.emptyNavigationLanding-12079.test.tsx b/packages/app-shell/src/console/__tests__/AppContent.emptyNavigationLanding-12079.test.tsx new file mode 100644 index 0000000000..cff693fa43 --- /dev/null +++ b/packages/app-shell/src/console/__tests__/AppContent.emptyNavigationLanding-12079.test.tsx @@ -0,0 +1,274 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * An app that serves the caller no navigation lands on an app-level empty + * state, never on the Studio home (objectui#12079). + * + * Measured on objectstack-ai/hotclm#87: `GET /api/v1/meta/app/clm` answered + * `navigation: []` for the dev admin, because every HotCLM group is gated on a + * `clm_*.access` capability that account does not hold. The app index route + * found no landing and rendered `StudioHomePage`: metadata counters and + * "New Object" quick actions inside a contracts app, with an empty sidebar and + * no next step. + * + * What is pinned here: + * - empty navigation renders the empty state, not `StudioHomePage`; + * - the Setup link is offered only to a caller the console already treats + * as able to administer (`useWorkspaceAdminStatus`), at the URL the + * `/setup` deep link resolves (`resolveSetupAppPath`); + * - control: an app with a landing still navigates to it; + * - control: the Studio app, whose navigation holds only `component` items + * and so has no landing either, still renders its own home. + * + * NOTE ON SCOPE: routing only. `ConsoleLayout` and the lazily imported pages + * are stubbed, as in `AppContent.inaccessibleAppStrand.test.tsx`. + */ + +import '@testing-library/jest-dom/vitest'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { MemoryRouter, Routes, Route, useLocation } from 'react-router-dom'; + +// AGENTS.md §测试纪律 — the lazy page modules are stubbed so no unbounded +// `import()` races a bounded `findBy` window. +vi.mock('../../views/metadata-admin', () => ({ + MetadataDirectoryPage: () =>
, + StudioHomePage: () =>
studio
, + MetadataResourceListPage: () =>
, + MetadataResourceEditPage: () =>
, + MetadataResourceHistoryPage: () =>
, + MetadataDiagnosticsPage: () =>
, +})); + +vi.mock('@object-ui/plugin-designer', async (importOriginal) => ({ + ...(await importOriginal()), + CreateAppPage: () =>
, + EditAppPage: () =>
, + DashboardDesignPage: () =>
, +})); + +vi.mock('../../layout/ConsoleLayout', () => ({ + ConsoleLayout: ({ activeAppName, children }: { activeAppName?: string; children?: React.ReactNode }) => ( +
+ {children} +
+ ), +})); +vi.mock('../../chrome/CommandPalette', () => ({ CommandPalette: () => null })); +vi.mock('../../chrome/KeyboardShortcutsDialog', () => ({ KeyboardShortcutsDialog: () => null })); +vi.mock('../../chrome/OnboardingWalkthrough', () => ({ OnboardingWalkthrough: () => null })); +vi.mock('../../views/ObjectView', () => ({ + ObjectView: () =>
, +})); + +// Interpolates `{{name}}` from the options, so the title can be read for the +// app label it was handed. +vi.mock('@object-ui/i18n', async (importOriginal) => ({ + ...(await importOriginal>()), + useObjectTranslation: () => ({ + t: (key: string, options?: Record) => + String(options?.defaultValue ?? key).replace(/\{\{(\w+)\}\}/g, (_m, name: string) => String(options?.[name] ?? '')), + }), + useObjectLabel: () => ({ + objectLabel: ({ label }: { label?: string }) => label, + objectPluralLabel: ({ label }: { label?: string }) => label, + appLabel: ({ name, label }: { name: string; label?: string }) => label ?? name, + }), +})); + +/** The viewer under test: a `member` by default, a workspace admin where stated. */ +let isWorkspaceAdmin = false; +vi.mock('@object-ui/auth', async (importOriginal) => ({ + ...(await importOriginal>()), + useAuth: () => ({ + user: { id: 'u_1', name: 'Dev Admin', email: 'admin@objectos.ai', role: 'member' }, + getAuthConfig: async () => ({ features: {} }), + activeOrganization: { id: 'org_1', name: 'Org' }, + }), + useWorkspaceAdminStatus: () => ({ isAdmin: isWorkspaceAdmin, isResolved: true }), +})); + +const dataSourceStub = { + onConnectionStateChange: () => () => {}, + getConnectionState: () => 'connected', +}; +vi.mock('../../providers/AdapterProvider', async (importOriginal) => ({ + ...(await importOriginal>()), + useAdapter: () => dataSourceStub, +})); + +/** The app list as the server hands it to this session. */ +let metadataApps: unknown[] = []; +vi.mock('../../providers/MetadataProvider', async (importOriginal) => ({ + ...(await importOriginal>()), + useMetadata: () => ({ + apps: metadataApps, + objects: [], + loading: false, + ensureType: undefined, + error: null, + refresh: vi.fn(async () => {}), + }), +})); + +const actionRunnerStub = { registerHandler: vi.fn(), getContext: () => ({}) }; +vi.mock('@object-ui/react', async (importOriginal) => ({ + ...(await importOriginal>()), + useActionRunner: () => ({ execute: vi.fn(), runner: actionRunnerStub }), + useGlobalUndo: () => {}, + useMutationInvalidationBridge: () => {}, +})); + +import { AppContent } from '../AppContent'; +import { appServesNoNavigation } from '../appServesNoNavigation'; + +function LocationProbe() { + const location = useLocation(); + return
{location.pathname}
; +} + +function renderConsoleAt(initialUrl: string) { + return render( + + + + } /> + + , + ); +} + +/** HotCLM as the server served it to the dev admin on hotclm#87. */ +const CLM_EMPTY = { name: 'clm', label: 'HotCLM', _packageId: 'app.objectstack.hotclm', navigation: [] }; +const SETUP = { + name: 'setup', + label: 'Setup', + _packageId: 'com.objectstack.setup', + navigation: [{ id: 'nav_users', type: 'object', objectName: 'sys_user', label: 'Users' }], +}; + +describe('AppContent — an app with empty navigation lands on an empty state (objectui#12079)', () => { + beforeEach(() => { + isWorkspaceAdmin = false; + metadataApps = []; + }); + + it('renders the app-level empty state, not the Studio home, and names the app', async () => { + metadataApps = [CLM_EMPTY]; + renderConsoleAt('/apps/clm'); + + const emptyState = await screen.findByTestId('app-no-access-empty-state'); + expect(screen.queryByTestId('studio-home-page')).not.toBeInTheDocument(); + expect(emptyState).toHaveTextContent('Nothing in HotCLM is available to you yet'); + // The app root, not a redirect somewhere else. + expect(screen.getByTestId('pathname').textContent).toBe('/apps/clm'); + expect(screen.getByTestId('console-layout')).toHaveAttribute('data-active-app', 'clm'); + }); + + it('asks a caller who cannot grant access to contact an administrator, with no Setup link', async () => { + metadataApps = [CLM_EMPTY]; + renderConsoleAt('/apps/clm'); + + const emptyState = await screen.findByTestId('app-no-access-empty-state'); + expect(emptyState).toHaveTextContent('Ask an administrator to grant you access.'); + expect(screen.queryByTestId('app-no-access-open-setup')).not.toBeInTheDocument(); + }); + + it('links a caller who can grant access to Setup, at the URL the /setup deep link resolves', async () => { + isWorkspaceAdmin = true; + metadataApps = [CLM_EMPTY, SETUP]; + renderConsoleAt('/apps/clm'); + + const emptyState = await screen.findByTestId('app-no-access-empty-state'); + expect(emptyState).toHaveTextContent('You can grant access in Setup.'); + const link = screen.getByTestId('app-no-access-open-setup'); + expect(link).toHaveTextContent('Open Setup'); + // `resolveSetupAppPath` — the Setup app's canonical `/apps/PACKAGE_ID` URL. + expect(link).toHaveAttribute('href', '/apps/com.objectstack.setup'); + expect(screen.queryByTestId('studio-home-page')).not.toBeInTheDocument(); + }); + + it('reads the admin verdict, not whether Setup is in the app list', async () => { + // A member whose list happens to carry Setup still gets no link: the + // decision is `useWorkspaceAdminStatus`, the one the console's no-app + // empty state and Home's administration nav already read. + metadataApps = [CLM_EMPTY, SETUP]; + renderConsoleAt('/apps/clm'); + + await screen.findByTestId('app-no-access-empty-state'); + expect(screen.queryByTestId('app-no-access-open-setup')).not.toBeInTheDocument(); + }); + + it('control: an app with a landing page still navigates to it', async () => { + metadataApps = [ + { + ...CLM_EMPTY, + navigation: [ + { + id: 'group_contracts', + type: 'group', + label: 'Contracts', + children: [{ id: 'nav_contracts', type: 'object', objectName: 'contract', label: 'Contracts' }], + }, + ], + }, + ]; + renderConsoleAt('/apps/clm'); + + expect(await screen.findByTestId('object-view')).toBeInTheDocument(); + expect(screen.getByTestId('pathname').textContent).toBe('/apps/clm/contract'); + expect(screen.queryByTestId('app-no-access-empty-state')).not.toBeInTheDocument(); + }); + + it('control: the Studio app, whose navigation has no landing, still renders its own home', async () => { + // Studio's navigation is all `component` items, which the landing resolver + // does not walk, so it reaches the same fallback with a FULL navigation. + metadataApps = [ + { + name: 'studio', + label: 'Studio', + navigation: [ + { + id: 'group_overview', + type: 'group', + label: 'Overview', + children: [{ id: 'nav_metadata_directory', type: 'component', componentRef: 'metadata:directory', label: 'All Metadata Types' }], + }, + ], + }, + ]; + renderConsoleAt('/apps/studio'); + + expect(await screen.findByTestId('studio-home-page')).toBeInTheDocument(); + expect(screen.queryByTestId('app-no-access-empty-state')).not.toBeInTheDocument(); + }); +}); + +describe('appServesNoNavigation (objectui#12079)', () => { + it('is true for no navigation at all, an empty array, and only childless groups or separators', () => { + expect(appServesNoNavigation({})).toBe(true); + expect(appServesNoNavigation({ navigation: [] })).toBe(true); + expect( + appServesNoNavigation({ + navigation: [ + { id: 'g', type: 'group', label: 'Contracts', children: [] }, + { id: 's', type: 'separator' }, + ] as never, + }), + ).toBe(true); + expect(appServesNoNavigation({ navigation: [], areas: [{ navigation: [] }] })).toBe(true); + }); + + it('is false when the top level or any area serves an item', () => { + expect( + appServesNoNavigation({ navigation: [{ id: 'u', type: 'url', url: 'https://example.com', label: 'Docs' }] as never }), + ).toBe(false); + expect( + appServesNoNavigation({ + navigation: [], + areas: [{ navigation: [{ id: 'o', type: 'object', objectName: 'contract', label: 'Contracts' }] as never }], + }), + ).toBe(false); + }); +}); diff --git a/packages/app-shell/src/console/appServesNoNavigation.ts b/packages/app-shell/src/console/appServesNoNavigation.ts new file mode 100644 index 0000000000..5a23908a39 --- /dev/null +++ b/packages/app-shell/src/console/appServesNoNavigation.ts @@ -0,0 +1,37 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Whether an app serves the caller nothing to navigate to (objectui#12079). + * + * Kept apart from `AppNoAccessEmptyState.tsx` so that file exports only a + * component; the app index route in `AppContent.tsx` asks this question first + * and renders the empty state on a yes. + */ + +import { hasVisibleNavigationItems } from '@object-ui/layout'; +import type { NavigationItem } from '@object-ui/types'; + +/** The two trees an app's sidebar draws from: the top level and every area. */ +interface AppNavigationLike { + navigation?: NavigationItem[] | null; + areas?: Array<{ navigation?: NavigationItem[] | null } | null> | null; +} + +/** + * Whether the app serves this caller nothing to navigate to: no item in its + * top-level `navigation` and none in any `areas[].navigation`. + * + * The question is structural. It reuses the sidebar's own predicate, + * `hasVisibleNavigationItems`, with no guards, so a `separator` and a `group` + * with no children count as nothing, exactly as the sidebar draws them. The + * sidebar wires an action dispatcher, so an `action` item counts as content. + * + * ⛔ This is NOT "no landing route". The Studio app serves a full navigation + * made only of `component` items, which the landing resolver does not walk, so + * it has no landing either. Its root keeps rendering `StudioHomePage`. + */ +export function appServesNoNavigation(app: AppNavigationLike | null | undefined): boolean { + const options = { hasActionHandler: true }; + if (hasVisibleNavigationItems(app?.navigation ?? [], options)) return false; + return !(app?.areas ?? []).some((area) => hasVisibleNavigationItems(area?.navigation ?? [], options)); +} diff --git a/packages/i18n/src/locales/ar.ts b/packages/i18n/src/locales/ar.ts index 16ccd4eb59..3d9300d513 100644 --- a/packages/i18n/src/locales/ar.ts +++ b/packages/i18n/src/locales/ar.ts @@ -2975,6 +2975,10 @@ const ar = { appAccessDenied: "ليس لديك حق الوصول إلى هذا التطبيق", appAccessDeniedDescription: "هذا التطبيق موجود، لكن حسابك غير مخوَّل بفتحه. اطلب من المسؤول منحك حق الوصول.", appAccessDeniedHome: "العودة إلى الرئيسية", + appNothingAvailable: "لا يتوفر لك أي شيء في {{app}} بعد", + appNothingAvailableDescription: "لا يحتوي هذا التطبيق على صفحات يمكن لحسابك فتحها. اطلب من المسؤول منحك حق الوصول.", + appNothingAvailableGrantDescription: "لا يحتوي هذا التطبيق على صفحات يمكن لحسابك فتحها. يمكنك منح حق الوصول من الإعداد.", + openSetup: "فتح الإعداد", createFirstApp: "إنشاء أول تطبيق", systemSettings: "إعدادات النظام", back: "رجوع", diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts index 8de519a2c3..117043b1a1 100644 --- a/packages/i18n/src/locales/de.ts +++ b/packages/i18n/src/locales/de.ts @@ -2756,6 +2756,10 @@ const de = { appAccessDenied: "Sie haben keinen Zugriff auf diese App", appAccessDeniedDescription: "Diese App existiert, aber Ihr Konto ist nicht berechtigt, sie zu öffnen. Bitten Sie einen Administrator um Zugriff.", appAccessDeniedHome: "Zurück zur Startseite", + appNothingAvailable: "In {{app}} ist für Sie noch nichts verfügbar", + appNothingAvailableDescription: "Diese App enthält keine Seiten, die Ihr Konto öffnen kann. Bitten Sie einen Administrator um Zugriff.", + appNothingAvailableGrantDescription: "Diese App enthält keine Seiten, die Ihr Konto öffnen kann. Sie können den Zugriff in der Einrichtung gewähren.", + openSetup: "Einrichtung öffnen", createFirstApp: "Erste App erstellen", systemSettings: "Systemeinstellungen", back: "Zurück", diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts index e31441f3a5..2feb87625a 100644 --- a/packages/i18n/src/locales/en.ts +++ b/packages/i18n/src/locales/en.ts @@ -3262,6 +3262,11 @@ const en = { appAccessDenied: "You don't have access to this app", appAccessDeniedDescription: 'This app exists, but your account is not authorized to open it. Ask an administrator to grant you access.', appAccessDeniedHome: 'Back to home', + // objectui#12079 — the app root when the app serves this caller no navigation (`AppNoAccessEmptyState`). + appNothingAvailable: 'Nothing in {{app}} is available to you yet', + appNothingAvailableDescription: 'This app has no pages your account can open. Ask an administrator to grant you access.', + appNothingAvailableGrantDescription: 'This app has no pages your account can open. You can grant access in Setup.', + openSetup: 'Open Setup', createFirstApp: 'Create Your First App', systemSettings: 'System Settings', back: 'Back', diff --git a/packages/i18n/src/locales/es.ts b/packages/i18n/src/locales/es.ts index e434204e4a..2a74baf4f4 100644 --- a/packages/i18n/src/locales/es.ts +++ b/packages/i18n/src/locales/es.ts @@ -2809,6 +2809,10 @@ const es = { appAccessDenied: "No tiene acceso a esta aplicación", appAccessDeniedDescription: "Esta aplicación existe, pero su cuenta no está autorizada para abrirla. Solicite acceso a un administrador.", appAccessDeniedHome: "Volver al inicio", + appNothingAvailable: "Aún no hay nada disponible para usted en {{app}}", + appNothingAvailableDescription: "Esta aplicación no tiene páginas que su cuenta pueda abrir. Solicite acceso a un administrador.", + appNothingAvailableGrantDescription: "Esta aplicación no tiene páginas que su cuenta pueda abrir. Puede conceder el acceso en la configuración.", + openSetup: "Abrir la configuración", createFirstApp: "Crear primera aplicación", systemSettings: "Configuración del sistema", back: "Atrás", diff --git a/packages/i18n/src/locales/fr.ts b/packages/i18n/src/locales/fr.ts index ecfffb6c5b..f264776ba9 100644 --- a/packages/i18n/src/locales/fr.ts +++ b/packages/i18n/src/locales/fr.ts @@ -2806,6 +2806,10 @@ const fr = { appAccessDenied: "Vous n'avez pas accès à cette application", appAccessDeniedDescription: "Cette application existe, mais votre compte n'est pas autorisé à l'ouvrir. Demandez l'accès à un administrateur.", appAccessDeniedHome: "Retour à l'accueil", + appNothingAvailable: "Rien dans {{app}} ne vous est encore accessible", + appNothingAvailableDescription: "Cette application ne contient aucune page que votre compte peut ouvrir. Demandez l'accès à un administrateur.", + appNothingAvailableGrantDescription: "Cette application ne contient aucune page que votre compte peut ouvrir. Vous pouvez accorder l'accès dans la configuration.", + openSetup: "Ouvrir la configuration", createFirstApp: "Créer la première application", systemSettings: "Paramètres système", back: "Retour", diff --git a/packages/i18n/src/locales/ja.ts b/packages/i18n/src/locales/ja.ts index ca8a353895..f71d76f18b 100644 --- a/packages/i18n/src/locales/ja.ts +++ b/packages/i18n/src/locales/ja.ts @@ -2758,6 +2758,10 @@ const ja = { appAccessDenied: "このアプリへのアクセス権限がありません", appAccessDeniedDescription: "このアプリは存在しますが、お使いのアカウントには開く権限がありません。管理者にアクセス権の付与を依頼してください。", appAccessDeniedHome: "ホームに戻る", + appNothingAvailable: "{{app}} で利用できるものはまだありません", + appNothingAvailableDescription: "このアプリには、お使いのアカウントで開けるページがありません。管理者にアクセス権の付与を依頼してください。", + appNothingAvailableGrantDescription: "このアプリには、お使いのアカウントで開けるページがありません。設定でアクセス権を付与できます。", + openSetup: "設定を開く", createFirstApp: "最初のアプリを作成", systemSettings: "システム設定", back: "戻る", diff --git a/packages/i18n/src/locales/ko.ts b/packages/i18n/src/locales/ko.ts index 93a4fe9f5b..7c417035b5 100644 --- a/packages/i18n/src/locales/ko.ts +++ b/packages/i18n/src/locales/ko.ts @@ -2755,6 +2755,10 @@ const ko = { appAccessDenied: "이 앱에 접근할 권한이 없습니다", appAccessDeniedDescription: "이 앱은 존재하지만 계정에 여는 권한이 없습니다. 관리자에게 접근 권한을 요청하세요.", appAccessDeniedHome: "홈으로 돌아가기", + appNothingAvailable: "{{app}}에서 아직 사용할 수 있는 항목이 없습니다", + appNothingAvailableDescription: "이 앱에는 계정으로 열 수 있는 페이지가 없습니다. 관리자에게 접근 권한을 요청하세요.", + appNothingAvailableGrantDescription: "이 앱에는 계정으로 열 수 있는 페이지가 없습니다. 설정에서 접근 권한을 부여할 수 있습니다.", + openSetup: "설정 열기", createFirstApp: "첫 번째 앱 만들기", systemSettings: "시스템 설정", back: "뒤로", diff --git a/packages/i18n/src/locales/pt.ts b/packages/i18n/src/locales/pt.ts index 9df49af97d..880c06faff 100644 --- a/packages/i18n/src/locales/pt.ts +++ b/packages/i18n/src/locales/pt.ts @@ -2804,6 +2804,10 @@ const pt = { appAccessDenied: "Você não tem acesso a este aplicativo", appAccessDeniedDescription: "Este aplicativo existe, mas sua conta não tem autorização para abri-lo. Peça acesso a um administrador.", appAccessDeniedHome: "Voltar ao início", + appNothingAvailable: "Ainda não há nada disponível para você em {{app}}", + appNothingAvailableDescription: "Este aplicativo não tem páginas que sua conta possa abrir. Peça acesso a um administrador.", + appNothingAvailableGrantDescription: "Este aplicativo não tem páginas que sua conta possa abrir. Você pode conceder acesso na configuração.", + openSetup: "Abrir a configuração", createFirstApp: "Criar primeiro aplicativo", systemSettings: "Configurações do sistema", back: "Voltar", diff --git a/packages/i18n/src/locales/ru.ts b/packages/i18n/src/locales/ru.ts index 4b18efcdd3..265c3961a8 100644 --- a/packages/i18n/src/locales/ru.ts +++ b/packages/i18n/src/locales/ru.ts @@ -2878,6 +2878,10 @@ const ru = { appAccessDenied: "У вас нет доступа к этому приложению", appAccessDeniedDescription: "Это приложение существует, но у вашей учётной записи нет прав на его открытие. Обратитесь к администратору за доступом.", appAccessDeniedHome: "На главную", + appNothingAvailable: "В {{app}} пока нет ничего доступного для вас", + appNothingAvailableDescription: "В этом приложении нет страниц, которые может открыть ваша учётная запись. Обратитесь к администратору за доступом.", + appNothingAvailableGrantDescription: "В этом приложении нет страниц, которые может открыть ваша учётная запись. Доступ можно предоставить в разделе настройки.", + openSetup: "Открыть настройку", createFirstApp: "Создать приложение", systemSettings: "Системные настройки", back: "Назад", diff --git a/packages/i18n/src/locales/zh.ts b/packages/i18n/src/locales/zh.ts index b89bcf7f11..56a24c91f6 100644 --- a/packages/i18n/src/locales/zh.ts +++ b/packages/i18n/src/locales/zh.ts @@ -2852,6 +2852,10 @@ const zh = { appAccessDenied: '你没有访问此应用的权限', appAccessDeniedDescription: '此应用存在,但你的账号未获授权打开它。请联系管理员为你开通访问权限。', appAccessDeniedHome: '返回首页', + appNothingAvailable: '{{app}} 中暂时没有你可以使用的内容', + appNothingAvailableDescription: '此应用中没有你的账号可以打开的页面。请联系管理员为你开通访问权限。', + appNothingAvailableGrantDescription: '此应用中没有你的账号可以打开的页面。你可以在 Setup 中授予访问权限。', + openSetup: '打开 Setup', createFirstApp: '创建您的第一个应用', systemSettings: '系统设置', back: '返回',