From d9ad6663742638e4b793fe35d37003aaba315333 Mon Sep 17 00:00:00 2001 From: cylewaitforit Date: Mon, 20 Apr 2026 21:57:21 -0500 Subject: [PATCH 1/5] docs(ui): add stories for Org page --- app/pages/org/[org].stories.ts | 107 ++++++++++++ app/storybook/mocks/handlers/registry-org.ts | 166 +++++++++++++++++++ 2 files changed, 273 insertions(+) create mode 100644 app/pages/org/[org].stories.ts create mode 100644 app/storybook/mocks/handlers/registry-org.ts diff --git a/app/pages/org/[org].stories.ts b/app/pages/org/[org].stories.ts new file mode 100644 index 0000000000..9a6d1e0906 --- /dev/null +++ b/app/pages/org/[org].stories.ts @@ -0,0 +1,107 @@ +import Org from './[org].vue' +import type { Meta, StoryObj } from '@storybook-vue/nuxt' +import { pageDecorator } from '../../../.storybook/decorators' +import { + mockOrgPackagesSuccess, + mockOrgPackagesSingle, + mockOrgPackagesEmpty, + mockOrgPackagesNotFound, + mockOrgPackagesLoading, +} from '../../storybook/mocks/handlers/registry-org' + +const meta: Meta = { + component: Org, + parameters: { + layout: 'fullscreen', + }, + decorators: [pageDecorator], +} + +export default meta +type Story = StoryObj + +/** + * Default org page showing the @npmx organization with multiple packages. + * Displays package list with filtering, sorting, and view mode controls. + * The MSW handler mocks both the org packages endpoint and Algolia search. + */ +export const Default: Story = { + parameters: { + msw: { handlers: mockOrgPackagesSuccess }, + }, + render: () => ({ + components: { Org }, + setup() { + useRouter().replace('/org/npmx') + }, + template: '', + }), +} + +/** + * Organization with only a single package. + * Shows the org page layout with minimal content. + */ +export const SinglePackage: Story = { + parameters: { + msw: { handlers: mockOrgPackagesSingle }, + }, + render: () => ({ + components: { Org }, + setup() { + useRouter().replace('/org/single-org') + }, + template: '', + }), +} + +/** + * Empty organization with zero packages. + * Shows the "This organization has no packages" message. + */ +export const EmptyOrg: Story = { + parameters: { + msw: { handlers: mockOrgPackagesEmpty }, + }, + render: () => ({ + components: { Org }, + setup() { + useRouter().replace('/org/empty-org') + }, + template: '', + }), +} + +/** + * Organization not found (404 error). + * The org endpoint returns a 404 error and the page displays an error state. + */ +export const NotFound: Story = { + parameters: { + msw: { handlers: mockOrgPackagesNotFound }, + }, + render: () => ({ + components: { Org }, + setup() { + useRouter().replace('/org/nonexistent-org') + }, + template: '', + }), +} + +/** + * Loading state when the API request is pending. + * MSW handlers delay responses indefinitely to show the loading spinner. + */ +export const Loading: Story = { + parameters: { + msw: { handlers: mockOrgPackagesLoading }, + }, + render: () => ({ + components: { Org }, + setup() { + useRouter().replace('/org/npmx') + }, + template: '', + }), +} diff --git a/app/storybook/mocks/handlers/registry-org.ts b/app/storybook/mocks/handlers/registry-org.ts new file mode 100644 index 0000000000..f7c841958a --- /dev/null +++ b/app/storybook/mocks/handlers/registry-org.ts @@ -0,0 +1,166 @@ +import { http, HttpResponse } from 'msw' + +/** + * Helper to create mock AlgoliaHit objects (mimics Algolia API response format) + */ +function createMockAlgoliaHit( + name: string, + overrides: { + description?: string + version?: string + downloadsLast30Days?: number + keywords?: string[] + modified?: number + license?: string + } = {}, +) { + return { + objectID: name, + name, + version: overrides.version || '1.2.3', + description: overrides.description || `Mock package ${name}`, + modified: overrides.modified || new Date('2026-01-22T10:07:07.000Z').getTime(), + homepage: `https://github.com/org/${name.replace('@', '').replace('/', '-')}`, + repository: { + url: `https://github.com/org/${name.replace('@', '').replace('/', '-')}`, + type: 'git', + }, + owners: [ + { + name: 'Patak Dog', + email: 'patak@patak.dog', + }, + ], + downloadsLast30Days: overrides.downloadsLast30Days || 100000, + downloadsRatio: 1, + popular: (overrides.downloadsLast30Days || 100000) > 50000, + keywords: overrides.keywords || [], + deprecated: false, + isDeprecated: false, + license: overrides.license || 'MIT', + isSecurityHeld: false, + } +} + +/** + * Mock handler: Org with multiple packages (default success scenario) + */ +export const mockOrgPackagesSuccess = [ + // Return the org package list + http.get('/api/registry/org/:org/packages', ({ params }) => { + const org = params.org as string + const packages = [ + `@${org}/xmpn`, + `@${org}/schema`, + `@${org}/i18n`, + `@${org}/noodle`, + `@${org}/tester`, + `${org}`, + ] + + return HttpResponse.json({ + packages, + count: packages.length, + }) + }), + + // Mock Algolia getObjects endpoint for package metadata + http.post('https://*.algolia.net/1/indexes/*/objects', async ({ request }) => { + const body = (await request.json()) as any + const requests = body?.requests || [] + + // Return AlgoliaHit objects for each requested package + const results = requests.map((req: any) => { + const packageName = req.objectID + const orgMatch = packageName.match(/@([\w-]+)\//) || [packageName, packageName] + const org = orgMatch[1] + const packageShortName = packageName.replace(`@${org}/`, '').replace(org, '') + + return createMockAlgoliaHit(packageName, { + description: `${org.charAt(0).toUpperCase() + org.slice(1)} ${packageShortName} - mocked package`, + downloadsLast30Days: 88477, + keywords: [org, packageShortName], + modified: new Date('2026-01-22T10:07:07.000Z').getTime(), + }) + }) + + return HttpResponse.json({ results }) + }), +] + +/** + * Mock handler: Org with single package + */ +export const mockOrgPackagesSingle = [ + http.get('/api/registry/org/:org/packages', ({ params }) => { + const org = params.org as string + const packageName = `@${org}/only-package` + + return HttpResponse.json({ + packages: [packageName], + count: 1, + }) + }), + + http.post('https://*.algolia.net/1/indexes/*/objects', async ({ request }) => { + const body = (await request.json()) as any + const requests = body?.requests || [] + + // Return AlgoliaHit objects for each requested package + const results = requests.map((req: any) => { + const packageName = req.objectID + + return createMockAlgoliaHit(packageName, { + description: 'The only package in this organization', + downloadsLast30Days: 5308, // 1234 weekly + keywords: ['single', 'lonely'], + modified: new Date('2026-01-22T10:07:07.000Z').getTime(), + }) + }) + + return HttpResponse.json({ results }) + }), +] + +/** + * Mock handler: Empty org (no packages) + */ +export const mockOrgPackagesEmpty = [ + http.get('/api/registry/org/:org/packages', () => { + return HttpResponse.json({ + packages: [], + count: 0, + }) + }), +] + +/** + * Mock handler: Org not found (404 error) + */ +export const mockOrgPackagesNotFound = [ + http.get('/api/registry/org/:org/packages', () => { + return HttpResponse.json( + { + error: 'Not Found', + message: 'Organization not found', + }, + { status: 404 }, + ) + }), +] + +/** + * Mock handler: Loading state (requests never resolve) + */ +export const mockOrgPackagesLoading = [ + http.get('/api/registry/org/:org/packages', async () => { + // Delay indefinitely to show loading state + await new Promise(() => {}) + return HttpResponse.json({ packages: [], count: 0 }) + }), + http.post('https://*.algolia.net/1/indexes/*/objects', async () => { + // Delay indefinitely to show loading state + await new Promise(() => {}) + return HttpResponse.json({ results: [] }) + }), +] From 172d0cb5da128d5fcb4167af8630df1eb84b66e7 Mon Sep 17 00:00:00 2001 From: cylewaitforit Date: Mon, 20 Apr 2026 22:04:03 -0500 Subject: [PATCH 2/5] fix: if ready --- app/pages/org/[org].stories.ts | 50 +++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/app/pages/org/[org].stories.ts b/app/pages/org/[org].stories.ts index 9a6d1e0906..0911a6e83c 100644 --- a/app/pages/org/[org].stories.ts +++ b/app/pages/org/[org].stories.ts @@ -32,9 +32,15 @@ export const Default: Story = { render: () => ({ components: { Org }, setup() { - useRouter().replace('/org/npmx') + const isReady = ref(false) + useRouter() + .replace('/org/npmx') + .then(() => { + isReady.value = true + }) + return { isReady } }, - template: '', + template: '', }), } @@ -49,9 +55,15 @@ export const SinglePackage: Story = { render: () => ({ components: { Org }, setup() { - useRouter().replace('/org/single-org') + const isReady = ref(false) + useRouter() + .replace('/org/single-org') + .then(() => { + isReady.value = true + }) + return { isReady } }, - template: '', + template: '', }), } @@ -66,9 +78,15 @@ export const EmptyOrg: Story = { render: () => ({ components: { Org }, setup() { - useRouter().replace('/org/empty-org') + const isReady = ref(false) + useRouter() + .replace('/org/empty-org') + .then(() => { + isReady.value = true + }) + return { isReady } }, - template: '', + template: '', }), } @@ -83,9 +101,15 @@ export const NotFound: Story = { render: () => ({ components: { Org }, setup() { - useRouter().replace('/org/nonexistent-org') + const isReady = ref(false) + useRouter() + .replace('/org/nonexistent-org') + .then(() => { + isReady.value = true + }) + return { isReady } }, - template: '', + template: '', }), } @@ -100,8 +124,14 @@ export const Loading: Story = { render: () => ({ components: { Org }, setup() { - useRouter().replace('/org/npmx') + const isReady = ref(false) + useRouter() + .replace('/org/npmx') + .then(() => { + isReady.value = true + }) + return { isReady } }, - template: '', + template: '', }), } From 8ccd663309ab83d179b742be4f5cc7f9947026fa Mon Sep 17 00:00:00 2001 From: cylewaitforit Date: Tue, 21 Apr 2026 19:04:48 -0500 Subject: [PATCH 3/5] fix: orgName --- app/pages/org/[org].vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/pages/org/[org].vue b/app/pages/org/[org].vue index f648dae769..0d142532cc 100644 --- a/app/pages/org/[org].vue +++ b/app/pages/org/[org].vue @@ -10,7 +10,7 @@ definePageMeta({ const route = useRoute('org') const router = useRouter() -const orgName = computed(() => route.params.org.toLowerCase()) +const orgName = computed(() => (route.params.org ?? '').toLowerCase()) const { isConnected } = useConnector() From 2f1a9b7de8e96ce018d41b4abdbf3bb806e91a8f Mon Sep 17 00:00:00 2001 From: cylewaitforit Date: Tue, 21 Apr 2026 19:56:08 -0500 Subject: [PATCH 4/5] fix: try router push --- app/pages/org/[org].stories.ts | 50 +++++++--------------------------- 1 file changed, 10 insertions(+), 40 deletions(-) diff --git a/app/pages/org/[org].stories.ts b/app/pages/org/[org].stories.ts index 0911a6e83c..fe8f772966 100644 --- a/app/pages/org/[org].stories.ts +++ b/app/pages/org/[org].stories.ts @@ -32,15 +32,9 @@ export const Default: Story = { render: () => ({ components: { Org }, setup() { - const isReady = ref(false) - useRouter() - .replace('/org/npmx') - .then(() => { - isReady.value = true - }) - return { isReady } + useRouter.push('/org/npmx') }, - template: '', + template: '', }), } @@ -55,15 +49,9 @@ export const SinglePackage: Story = { render: () => ({ components: { Org }, setup() { - const isReady = ref(false) - useRouter() - .replace('/org/single-org') - .then(() => { - isReady.value = true - }) - return { isReady } + useRouter().push('/org/single-org') }, - template: '', + template: '', }), } @@ -78,15 +66,9 @@ export const EmptyOrg: Story = { render: () => ({ components: { Org }, setup() { - const isReady = ref(false) - useRouter() - .replace('/org/empty-org') - .then(() => { - isReady.value = true - }) - return { isReady } + useRouter().push('/org/empty-org') }, - template: '', + template: '', }), } @@ -101,15 +83,9 @@ export const NotFound: Story = { render: () => ({ components: { Org }, setup() { - const isReady = ref(false) - useRouter() - .replace('/org/nonexistent-org') - .then(() => { - isReady.value = true - }) - return { isReady } + useRouter().push('/org/nonexistent-org') }, - template: '', + template: '', }), } @@ -124,14 +100,8 @@ export const Loading: Story = { render: () => ({ components: { Org }, setup() { - const isReady = ref(false) - useRouter() - .replace('/org/npmx') - .then(() => { - isReady.value = true - }) - return { isReady } + useRouter().push('/org/npmx') }, - template: '', + template: '', }), } From 6f593bb86683ce956c0c01e39a47c30368e592e5 Mon Sep 17 00:00:00 2001 From: cylewaitforit Date: Tue, 21 Apr 2026 20:05:00 -0500 Subject: [PATCH 5/5] fix: back to replace --- app/pages/org/[org].stories.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/pages/org/[org].stories.ts b/app/pages/org/[org].stories.ts index fe8f772966..9a6d1e0906 100644 --- a/app/pages/org/[org].stories.ts +++ b/app/pages/org/[org].stories.ts @@ -32,7 +32,7 @@ export const Default: Story = { render: () => ({ components: { Org }, setup() { - useRouter.push('/org/npmx') + useRouter().replace('/org/npmx') }, template: '', }), @@ -49,7 +49,7 @@ export const SinglePackage: Story = { render: () => ({ components: { Org }, setup() { - useRouter().push('/org/single-org') + useRouter().replace('/org/single-org') }, template: '', }), @@ -66,7 +66,7 @@ export const EmptyOrg: Story = { render: () => ({ components: { Org }, setup() { - useRouter().push('/org/empty-org') + useRouter().replace('/org/empty-org') }, template: '', }), @@ -83,7 +83,7 @@ export const NotFound: Story = { render: () => ({ components: { Org }, setup() { - useRouter().push('/org/nonexistent-org') + useRouter().replace('/org/nonexistent-org') }, template: '', }), @@ -100,7 +100,7 @@ export const Loading: Story = { render: () => ({ components: { Org }, setup() { - useRouter().push('/org/npmx') + useRouter().replace('/org/npmx') }, template: '', }),