diff --git a/.claude/skills/indexing-diagnostics/SKILL.md b/.claude/skills/indexing-diagnostics/SKILL.md index 1f26b4c448b..34d9c6b4538 100644 --- a/.claude/skills/indexing-diagnostics/SKILL.md +++ b/.claude/skills/indexing-diagnostics/SKILL.md @@ -2743,7 +2743,7 @@ The per-realm reindex endpoints (`POST _reindex`, `POST _full-rein -d '{ "user": "@user:server", "realms": ["/"] }' ``` - The response carries a map of `{ : }`. **That** is the token you pass on `Authorization` when calling the reindex endpoint in the next section. It's _not_ the token the prerender tab uses — that one comes from the `prerenderer-reproduce` log. + The response carries a map of `{ : }`, covering the realms the user has a permission row of their own for. Add `?include-public=true` to the URL to also get tokens for realms only world-readable to them. **That** is the token you pass on `Authorization` when calling the reindex endpoint in the next section. It's _not_ the token the prerender tab uses — that one comes from the `prerenderer-reproduce` log. Three different JWTs float around in this area, so always be explicit about which one you mean: diff --git a/packages/boxel-cli/src/commands/realm/list.ts b/packages/boxel-cli/src/commands/realm/list.ts index bae81a8b2dd..f79afba5b03 100644 --- a/packages/boxel-cli/src/commands/realm/list.ts +++ b/packages/boxel-cli/src/commands/realm/list.ts @@ -66,7 +66,7 @@ export async function listRealms( let realmServerUrl = active.profile.realmServerUrl.replace(/\/$/, ''); let response = await pm.authedRealmServerFetch( - `${realmServerUrl}/_realm-auth`, + `${realmServerUrl}/_realm-auth?include-public=true`, { method: 'POST', headers: { diff --git a/packages/boxel-cli/src/lib/auth.ts b/packages/boxel-cli/src/lib/auth.ts index d66d1ac6e5e..4c774a00d0e 100644 --- a/packages/boxel-cli/src/lib/auth.ts +++ b/packages/boxel-cli/src/lib/auth.ts @@ -292,7 +292,9 @@ export async function getRealmTokens( realmServerUrl: string, serverToken: string, ): Promise { - let url = `${realmServerUrl.replace(/\/$/, '')}/_realm-auth`; + // `include-public=true`: a token for every realm the user may read, public + // ones included, since authed realm fetches resolve their token from this. + let url = `${realmServerUrl.replace(/\/$/, '')}/_realm-auth?include-public=true`; let response = await fetch(url, { method: 'POST', diff --git a/packages/host/app/components/operator-mode/workspace-chooser/index.gts b/packages/host/app/components/operator-mode/workspace-chooser/index.gts index 86196a5e81a..41fc5a18ae8 100644 --- a/packages/host/app/components/operator-mode/workspace-chooser/index.gts +++ b/packages/host/app/components/operator-mode/workspace-chooser/index.gts @@ -25,15 +25,30 @@ import type { Icon } from '@cardstack/boxel-ui/icons'; import { logger, ri } from '@cardstack/runtime-common'; import config from '@cardstack/host/config/environment'; +import { catalogRealmURL } from '@cardstack/host/lib/utils'; import type MatrixService from '@cardstack/host/services/matrix-service'; import type RealmService from '@cardstack/host/services/realm'; import type RealmServerService from '@cardstack/host/services/realm-server'; import AddWorkspace from './add-workspace'; import ArchivedWorkspace from './archived-workspace'; +import ManageCatalogs from './manage-catalogs'; import Workspace from './workspace'; import WorkspaceLoadingIndicator from './workspace-loading-indicator'; +// Moves `first` to the front of `urls` when it's there, keeping the rest in +// order. +export function leadWith( + urls: T[], + first: string | null, +): T[] { + let index = first ? urls.indexOf(first as T) : -1; + if (index <= 0) { + return urls; + } + return [urls[index], ...urls.slice(0, index), ...urls.slice(index + 1)]; +} + interface SortOption { label: string; icon: Icon; @@ -152,13 +167,6 @@ export default class WorkspaceChooser extends Component { return this.selectedSortOption.value; } - private get displayCatalogWorkspaces() { - return ( - this.realmServer.displayedCatalogRealmIdentifiers && - this.realmServer.displayedCatalogRealmIdentifiers.length > 0 - ); - } - private get communityRealmIdentifiers() { let realmURLs = this.realmServer.displayedCatalogRealmIdentifiers ?? []; if (config.environment !== 'production') { @@ -212,9 +220,13 @@ export default class WorkspaceChooser extends Component { return this.filterByHosted(this.realmServer.userRealmIdentifiers); } + // Newest first, except the Boxel catalog, which always leads the section. private get filteredCatalogRealmIdentifiers() { return this.filterByHosted( - this.sortByCreatedAtDesc(this.communityRealmIdentifiers), + leadWith( + this.sortByCreatedAtDesc(this.communityRealmIdentifiers), + catalogRealmURL, + ), ); } @@ -262,13 +274,12 @@ export default class WorkspaceChooser extends Component { } private get catalogEmptyMessage(): string | null { - if ( - this.sortOrder === 'hosted-only' && - this.filteredCatalogRealmIdentifiers.length === 0 - ) { - return 'No matching results'; + if (this.filteredCatalogRealmIdentifiers.length > 0) { + return null; } - return null; + return this.sortOrder === 'hosted-only' + ? 'No matching results' + : 'No catalogs yet. Add public realms with the gear.'; } private get favoritesEmptyMessage(): string | null { @@ -386,7 +397,7 @@ export default class WorkspaceChooser extends Component { } private get renderedCatalogCount() { - if (!this.displayCatalogWorkspaces || this.catalogEmptyMessage) { + if (this.catalogEmptyMessage) { return 0; } return this.filteredCatalogRealmIdentifiers.length; @@ -695,36 +706,35 @@ export default class WorkspaceChooser extends Component { {{/if}} - {{#if this.displayCatalogWorkspaces}} -
-
- - Catalogs -
- {{#if this.catalogEmptyMessage}} - {{this.catalogEmptyMessage}} - {{else}} -
- {{#each - this.filteredCatalogRealmIdentifiers - as |realmIdentifier i| - }} - {{#let (add this.catalogNavBase i) as |navIndex|}} - - {{/let}} - {{/each}} -
- {{/if}} +
+
+ + Catalogs +
- {{/if}} + {{#if this.catalogEmptyMessage}} + {{this.catalogEmptyMessage}} + {{else}} +
+ {{#each + this.filteredCatalogRealmIdentifiers + as |realmIdentifier i| + }} + {{#let (add this.catalogNavBase i) as |navIndex|}} + + {{/let}} + {{/each}} +
+ {{/if}} +