Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .changeset/26163-map-style-runtime-fallback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
'@object-ui/plugin-map': patch
'@object-ui/react': patch
'@object-ui/app-shell': patch
'@object-ui/i18n': patch
---

feat(plugin-map): a map view with no style of its own draws the deployment's default map style before the public demo style

Clause-②: no
Metadata-protocol: none

An operator who sets `OS_CONSOLE_MAP_STYLE_URL` now gets that style on every map view that names none. A map draws the first style it finds, in this order:

1. the view's own style: `mapStyle`, then `map.style`;
2. the deployment's default style, the runtime-config `mapStyleUrl`;
3. the public MapLibre demo style.

- **`@object-ui/react`:** `HostMapStyleProvider` and `useHostMapStyle()` let a host hand the deployment's default map style to the map renderer. Outside a provider, the hook answers `undefined` and the map keeps the demo style.
- **`@object-ui/app-shell`:** the runtime config reads the top-level `mapStyleUrl` into `AppShellRuntimeConfig.mapStyleUrl`, re-read from each payload, and `ConsoleShell` supplies it through `HostMapStyleProvider`.
- **`@object-ui/plugin-map`:** `ObjectMap` reads the host's style after the view's own. When the deployment's style fails to load, the notice above the record list says the deployment's default map style could not be loaded and asks the reader to have an administrator check it. It no longer blames the public default tiles, which that map never drew.
- **`@object-ui/i18n`:** one new key, `element.map.styleFailedDeployment`, in all ten packs.

What to do: nothing. A deployment that sets no `OS_CONSOLE_MAP_STYLE_URL` draws the demo style as before, and a view's own `mapStyle` still wins.
19 changes: 18 additions & 1 deletion ui/content/docs/plugins/plugin-map.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ to `onMarkerClick` is not `===` the authored object.
descriptionField?: string, // Field for marker description
zoom?: number, // Zoom level (1-20) — opts out of the auto-fit
center?: [number, number], // Center coordinates [lat, lng] — opts out of the auto-fit
style?: string // MapLibre style URL/spec (overrides the demo default)
style?: string // MapLibre style URL/spec (overrides the deployment and demo defaults)
}
```

Expand All @@ -171,6 +171,23 @@ warns `[ObjectMap] Invalid map configuration`, naming the same key. A typo that
leaves the block with no coordinate binding, as `latitudeFieId` does, draws the
refusal described under "An unconfigured map refuses" rather than a map.

## Which style draws

A map draws the first style it finds, in this order:

1. the view's own style: the node-level `mapStyle`, then `map.style`;
2. the deployment's default style, which the host hands down through
`HostMapStyleProvider` from `@object-ui/react`. The Console supplies the
operator's `OS_CONSOLE_MAP_STYLE_URL`, served as `mapStyleUrl` on
`GET /api/v1/runtime/config`;
3. the public MapLibre demo style (`https://demotiles.maplibre.org/style.json`),
which is rate-limited and not meant for production traffic.

A host that provides no style, such as an embedded renderer or a Console whose
operator set none, keeps the demo style. When the style that drew fails to load,
the map is replaced by its record list, and the notice names that style: the
view's own, the deployment's or the public default.

## Configuration

### An unconfigured map refuses
Expand Down
13 changes: 11 additions & 2 deletions ui/packages/app-shell/src/console/ConsoleShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { Suspense, useEffect, useRef, useState, type ReactNode } from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import { AuthGuard, useAuth, createAuthenticatedFetch } from '@object-ui/auth';
import { useObjectTranslation } from '@object-ui/i18n';
import { SchemaRendererProvider, ActionProvider, NotificationProvider } from '@object-ui/react';
import { SchemaRendererProvider, ActionProvider, NotificationProvider, HostMapStyleProvider } from '@object-ui/react';
import { NotificationAlerts, NotificationSnackbar } from '@object-ui/components';
import { presentNotificationToast } from '../chrome/notificationToast.js';
import { useActionModal } from '../hooks/useActionModal.js';
Expand Down Expand Up @@ -54,6 +54,7 @@ import { ConsoleElementApiOpen } from './ConsoleElementApiOpen.js';
import { ImpersonationBanner } from '../layout/ImpersonationBanner.js';
import { ReadRateBanner } from '../layout/ReadRateBanner.js';
import { StorageUsageBanner } from '../layout/StorageUsageBanner.js';
import { getRuntimeConfig } from '../runtime-config.js';

// The console's every pre-React / pre-auth gate (Suspense fallback, adapter
// not ready, org/auth loading) renders this. It used to be a bare, unbranded
Expand Down Expand Up @@ -162,7 +163,15 @@ export function ConsoleShell({ children }: { children: ReactNode }) {
console route and several arrive through `ComponentRegistry`, where no
prop could be passed. */
<HostNavigationBridge>
<ConsoleShellProviders>{children}</ConsoleShellProviders>
{/* objectstack#26163 — the deployment's default map style (the
operator's `OS_CONSOLE_MAP_STYLE_URL`, served as runtime-config
`mapStyleUrl`), for a map view that declares no style of its own.
The map arrives through `ComponentRegistry`, lazily, so a context is
the only way to reach it; absent, the map keeps the demo style.
`initRuntimeConfig()` settles before the console's first render. */}
<HostMapStyleProvider styleUrl={getRuntimeConfig().mapStyleUrl}>
<ConsoleShellProviders>{children}</ConsoleShellProviders>
</HostMapStyleProvider>
</HostNavigationBridge>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* `ConsoleShell` SUPPLIES the deployment's default map style (objectstack#26163).
*
* The runtime serves the operator's `OS_CONSOLE_MAP_STYLE_URL` as the top-level
* `mapStyleUrl` of `GET /api/v1/runtime/config` (objectstack#26001), and the map
* renderer reads it through `useHostMapStyle()` from `@object-ui/react`. The map
* is a lazily registered plugin, so nothing but this context reaches it; this
* file pins that the console actually mounts the provider with the served value,
* which no test of the renderer or of the config reader can see.
*/

import { describe, it, expect, vi, afterEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { useHostMapStyle } from '@object-ui/react';
import { ConsoleShell } from '../ConsoleShell';
import { initRuntimeConfig, resetRuntimeConfigForTesting } from '../../runtime-config';

// Same two stubs the sibling ConsoleShell tests use: sonner is a global toast
// surface, and the ADR-0069 remediation gate needs an AuthProvider above it.
vi.mock('sonner', () => ({
toast: Object.assign(vi.fn(), {
info: vi.fn(), success: vi.fn(), warning: vi.fn(), error: vi.fn(),
dismiss: vi.fn(), custom: vi.fn(), loading: vi.fn(), promise: vi.fn(),
}),
Toaster: () => null,
}));
vi.mock('../RemediationOverlay', () => ({ RemediationOverlay: () => null }));

const STYLE = 'https://tiles.intranet.example/styles/basic/style.json';

/** A stand-in for the map renderer below the shell. */
function MapStub() {
const style = useHostMapStyle();
return <div data-testid="host-map-style">{style ?? 'none'}</div>;
}

async function boot(payload: Record<string, unknown>) {
vi.stubGlobal('fetch', vi.fn(async () => ({ ok: true, json: async () => payload })) as any);
await initRuntimeConfig();
render(
<MemoryRouter>
<ConsoleShell>
<MapStub />
</ConsoleShell>
</MemoryRouter>,
);
}

afterEach(() => {
resetRuntimeConfigForTesting();
vi.unstubAllGlobals();
});

describe('ConsoleShell — the console supplies the deployment map style (objectstack#26163)', () => {
it('hands the served mapStyleUrl to every renderer below it', async () => {
await boot({ mapStyleUrl: STYLE });
await waitFor(() => expect(screen.getByTestId('host-map-style').textContent).toBe(STYLE));
});

it('hands nothing when the runtime serves no mapStyleUrl', async () => {
await boot({ singleEnvironment: true });
await waitFor(() => expect(screen.getByTestId('host-map-style').textContent).toBe('none'));
});
});
37 changes: 37 additions & 0 deletions ui/packages/app-shell/src/runtime-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,43 @@ describe('runtime-config hideSingleAppSwitcher (objectui#12913)', () => {
});
});

describe('runtime-config mapStyleUrl (objectstack#26163)', () => {
const STYLE = 'https://tiles.intranet.example/styles/basic/style.json';

it('is absent before init', () => {
resetRuntimeConfigForTesting();
expect(getRuntimeConfig().mapStyleUrl).toBeUndefined();
});

it('takes the top-level mapStyleUrl the runtime serves', async () => {
mockPayload({ mapStyleUrl: STYLE });
await initRuntimeConfig();
expect(getRuntimeConfig().mapStyleUrl).toBe(STYLE);
});

it.each([
['absent (the operator set no OS_CONSOLE_MAP_STYLE_URL)', { singleEnvironment: true }],
['empty', { mapStyleUrl: '' }],
['whitespace', { mapStyleUrl: ' ' }],
['not a string', { mapStyleUrl: 42 }],
['nested under branding, where it is not read', { branding: { mapStyleUrl: STYLE } }],
])('is absent when the key is %s', async (_label, payload) => {
mockPayload(payload);
await initRuntimeConfig();
expect(getRuntimeConfig().mapStyleUrl).toBeUndefined();
});

it('withdraws it when a later fetch no longer serves it', async () => {
mockPayload({ mapStyleUrl: STYLE });
await initRuntimeConfig();
expect(getRuntimeConfig().mapStyleUrl).toBe(STYLE);

mockPayload({ singleEnvironment: false });
await initRuntimeConfig();
expect(getRuntimeConfig().mapStyleUrl).toBeUndefined();
});
});

describe('runtime-config platform stage', () => {
it('defaults to preview before init (badge shows out of the box)', () => {
resetRuntimeConfigForTesting();
Expand Down
20 changes: 20 additions & 0 deletions ui/packages/app-shell/src/runtime-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,20 @@ export interface AppShellRuntimeConfig {
* `singleEnvironment`: a later payload without it withdraws it.
*/
hideSingleAppSwitcher?: boolean;
/**
* The deployment's default map style (objectstack#26163): the operator's
* `OS_CONSOLE_MAP_STYLE_URL`, served by `RuntimeConfigPlugin` as the
* top-level `mapStyleUrl` (objectstack#26001) and absent when the operator
* set none. A map view with no style of its own draws it before the public
* demo style; `ConsoleShell` hands it to the map renderer through
* `HostMapStyleProvider` from `@object-ui/react`.
*
* Re-derived from each payload in full, like `hideSingleAppSwitcher`: a
* later payload without the key withdraws it. Only a non-empty string is
* taken; the producer already refuses to boot on a value that is not an
* absolute `https:` URL, so nothing is re-validated here.
*/
mapStyleUrl?: string;
}

const defaults: AppShellRuntimeConfig = {
Expand Down Expand Up @@ -441,6 +455,12 @@ export async function initRuntimeConfig(baseUrl: string = ''): Promise<void> {
// objectui#12913 — a host's request, so only a real `true` turns it on;
// `'true'`, `1` and an absent key all keep the switcher.
hideSingleAppSwitcher: body.hideSingleAppSwitcher === true,
// objectstack#26163 — the operator's map style, or none. Absent, empty
// and non-string all withdraw it, so the map falls back to the demo style.
mapStyleUrl:
typeof body.mapStyleUrl === 'string' && body.mapStyleUrl.trim()
? body.mapStyleUrl.trim()
: undefined,
defaultOrgId: body.defaultOrgId ?? current.defaultOrgId ?? null,
defaultEnvironmentId: body.defaultEnvironmentId ?? current.defaultEnvironmentId ?? null,
features: body.features
Expand Down
1 change: 1 addition & 0 deletions ui/packages/i18n/src/locales/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5101,6 +5101,7 @@ const ar = {
invalidCoordinatesExcluded: "السجلات المستبعدة من الخريطة بسبب إحداثيات مفقودة أو غير صالحة: {{excluded}}",
styleFailedConfigured: "تعذّر تحميل الخريطة ({{reason}}). السجلات مدرجة أدناه؛ تحقّق من عنوان URL لنمط الخريطة المحدد في طريقة العرض هذه.",
styleFailedDefault: "تعذّر تحميل الخريطة ({{reason}}). السجلات مدرجة أدناه؛ تعذّر تحميل مربعات الخريطة العامة الافتراضية هنا، لذا اطلب من المسؤول تعيين نمط خريطة لطريقة العرض هذه.",
styleFailedDeployment: "تعذّر تحميل الخريطة ({{reason}}). السجلات مدرجة أدناه؛ تعذّر هنا تحميل نمط الخريطة الافتراضي المعيّن لهذا النشر، لذا اطلب من المسؤول التحقق منه.",
styleFailedUnknownReason: "تعذّر تحميل نمط الخريطة أو مربعاتها",
webgl2Unavailable: "تعذّر تحميل الخريطة (هذا المتصفح لا يوفّر WebGL2 الذي تحتاجه الخريطة). السجلات مدرجة أدناه؛ فعّل تسريع الأجهزة أو استخدم متصفحًا يدعم WebGL2 لعرض الخريطة.",
retry: "إعادة محاولة تحميل الخريطة",
Expand Down
1 change: 1 addition & 0 deletions ui/packages/i18n/src/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4639,6 +4639,7 @@ const de = {
invalidCoordinatesExcluded: "Wegen fehlender oder ungültiger Koordinaten nicht auf der Karte angezeigte Datensätze: {{excluded}}",
styleFailedConfigured: "Die Karte konnte nicht geladen werden ({{reason}}). Die Datensätze sind unten aufgelistet; prüfen Sie die für diese Ansicht festgelegte Kartenstil-URL.",
styleFailedDefault: "Die Karte konnte nicht geladen werden ({{reason}}). Die Datensätze sind unten aufgelistet; die öffentlichen Standard-Kartenkacheln konnten hier nicht geladen werden. Bitten Sie Ihren Administrator, für diese Ansicht einen Kartenstil festzulegen.",
styleFailedDeployment: "Die Karte konnte nicht geladen werden ({{reason}}). Die Datensätze sind unten aufgelistet; der für diese Bereitstellung festgelegte Standard-Kartenstil konnte hier nicht geladen werden. Bitten Sie Ihren Administrator, ihn zu überprüfen.",
styleFailedUnknownReason: "Kartenstil oder Kacheln konnten nicht geladen werden",
webgl2Unavailable: "Die Karte konnte nicht geladen werden (dieser Browser unterstützt kein WebGL2, das die Karte benötigt). Die Datensätze sind unten aufgelistet; aktivieren Sie die Hardwarebeschleunigung oder verwenden Sie einen Browser mit WebGL2, um die Karte zu sehen.",
retry: "Karte erneut laden",
Expand Down
8 changes: 5 additions & 3 deletions ui/packages/i18n/src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5437,13 +5437,15 @@ const en = {
},
map: {
sourceUnresolved: 'This map’s data source could not be resolved',
// objectstack#25889 — the map view's own notices. Both style failures
// fill `{{reason}}` with MapLibre's message, or `styleFailedUnknownReason`
// when it gave none; the excluded count reads "Label: N" so no key needs
// objectstack#25889 — the map view's own notices. The three style
// failures (the view's own style, the deployment's default style —
// objectstack#26163 — and the public default) fill `{{reason}}` with
// MapLibre's message, or `styleFailedUnknownReason` when it gave none; the excluded count reads "Label: N" so no key needs
// a plural family.
invalidCoordinatesExcluded: 'Records excluded from the map for missing or invalid coordinates: {{excluded}}',
styleFailedConfigured: 'Map failed to load ({{reason}}). The records are listed below; check the map style URL set on this view.',
styleFailedDefault: 'Map failed to load ({{reason}}). The records are listed below; the public default map tiles could not be loaded here, so ask your administrator to set a map style for this view.',
styleFailedDeployment: 'Map failed to load ({{reason}}). The records are listed below; the default map style set for this deployment could not be loaded here, so ask your administrator to check it.',
styleFailedUnknownReason: 'the map style or tiles could not be loaded',
webgl2Unavailable: 'Map failed to load (this browser does not provide WebGL2, which the map needs). The records are listed below; turn on hardware acceleration or use a browser with WebGL2 to see the map.',
retry: 'Try the map again',
Expand Down
1 change: 1 addition & 0 deletions ui/packages/i18n/src/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4754,6 +4754,7 @@ const es = {
invalidCoordinatesExcluded: "Registros excluidos del mapa por coordenadas ausentes o no válidas: {{excluded}}",
styleFailedConfigured: "No se pudo cargar el mapa ({{reason}}). Los registros aparecen a continuación; revise la URL del estilo de mapa configurada en esta vista.",
styleFailedDefault: "No se pudo cargar el mapa ({{reason}}). Los registros aparecen a continuación; aquí no se pudieron cargar los mosaicos de mapa públicos predeterminados, así que pida a su administrador que configure un estilo de mapa para esta vista.",
styleFailedDeployment: "No se pudo cargar el mapa ({{reason}}). Los registros aparecen a continuación; aquí no se pudo cargar el estilo de mapa predeterminado configurado para esta implementación, así que pida a su administrador que lo revise.",
styleFailedUnknownReason: "no se pudieron cargar el estilo o los mosaicos del mapa",
webgl2Unavailable: "No se pudo cargar el mapa (este navegador no admite WebGL2, que el mapa necesita). Los registros aparecen a continuación; active la aceleración por hardware o use un navegador con WebGL2 para ver el mapa.",
retry: "Reintentar el mapa",
Expand Down
1 change: 1 addition & 0 deletions ui/packages/i18n/src/locales/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4750,6 +4750,7 @@ const fr = {
invalidCoordinatesExcluded: "Enregistrements exclus de la carte (coordonnées manquantes ou invalides) : {{excluded}}",
styleFailedConfigured: "Échec du chargement de la carte ({{reason}}). Les enregistrements sont listés ci-dessous ; vérifiez l’URL du style de carte défini sur cette vue.",
styleFailedDefault: "Échec du chargement de la carte ({{reason}}). Les enregistrements sont listés ci-dessous ; les tuiles de carte publiques par défaut n’ont pas pu être chargées ici, demandez à votre administrateur de définir un style de carte pour cette vue.",
styleFailedDeployment: "Échec du chargement de la carte ({{reason}}). Les enregistrements sont listés ci-dessous ; le style de carte par défaut défini pour ce déploiement n’a pas pu être chargé ici, demandez à votre administrateur de le vérifier.",
styleFailedUnknownReason: "le style ou les tuiles de la carte n’ont pas pu être chargés",
webgl2Unavailable: "Échec du chargement de la carte (ce navigateur ne prend pas en charge WebGL2, nécessaire à la carte). Les enregistrements sont listés ci-dessous ; activez l’accélération matérielle ou utilisez un navigateur compatible WebGL2 pour afficher la carte.",
retry: "Réessayer la carte",
Expand Down
Loading
Loading