diff --git a/__test__/projects/GitHubProjectDataSource.test.ts b/__test__/projects/GitHubProjectDataSource.test.ts index 6dcbda37..c98b3a8e 100644 --- a/__test__/projects/GitHubProjectDataSource.test.ts +++ b/__test__/projects/GitHubProjectDataSource.test.ts @@ -87,7 +87,8 @@ test("It maps projects including branches and tags", async () => { id: "openapi.yml", name: "openapi.yml", url: "/api/blob/acme/foo-openapi/openapi.yml?ref=12345678", - editURL: "https://github.com/acme/foo-openapi/edit/main/openapi.yml" + editURL: "https://github.com/acme/foo-openapi/edit/main/openapi.yml", + isDefault: false }], url: "https://github.com/acme/foo-openapi/tree/main", isDefault: true @@ -98,7 +99,8 @@ test("It maps projects including branches and tags", async () => { id: "openapi.yml", name: "openapi.yml", url: "/api/blob/acme/foo-openapi/openapi.yml?ref=12345678", - editURL: "https://github.com/acme/foo-openapi/edit/1.0/openapi.yml" + editURL: "https://github.com/acme/foo-openapi/edit/1.0/openapi.yml", + isDefault: false }], url: "https://github.com/acme/foo-openapi/tree/1.0", isDefault: false @@ -195,17 +197,20 @@ test("It supports multiple OpenAPI specifications on a branch", async () => { id: "foo-service.yml", name: "foo-service.yml", url: "/api/blob/acme/foo-openapi/foo-service.yml?ref=12345678", - editURL: "https://github.com/acme/foo-openapi/edit/main/foo-service.yml" + editURL: "https://github.com/acme/foo-openapi/edit/main/foo-service.yml", + isDefault: false }, { id: "bar-service.yml", name: "bar-service.yml", url: "/api/blob/acme/foo-openapi/bar-service.yml?ref=12345678", - editURL: "https://github.com/acme/foo-openapi/edit/main/bar-service.yml" + editURL: "https://github.com/acme/foo-openapi/edit/main/bar-service.yml", + isDefault: false }, { id: "baz-service.yml", name: "baz-service.yml", url: "/api/blob/acme/foo-openapi/baz-service.yml?ref=12345678", - editURL: "https://github.com/acme/foo-openapi/edit/main/baz-service.yml" + editURL: "https://github.com/acme/foo-openapi/edit/main/baz-service.yml", + isDefault: false }], url: "https://github.com/acme/foo-openapi/tree/main", isDefault: true @@ -216,7 +221,8 @@ test("It supports multiple OpenAPI specifications on a branch", async () => { id: "openapi.yml", name: "openapi.yml", url: "/api/blob/acme/foo-openapi/openapi.yml?ref=12345678", - editURL: "https://github.com/acme/foo-openapi/edit/1.0/openapi.yml" + editURL: "https://github.com/acme/foo-openapi/edit/1.0/openapi.yml", + isDefault: false }], url: "https://github.com/acme/foo-openapi/tree/1.0", isDefault: false @@ -749,11 +755,13 @@ test("It adds remote versions from the project configuration", async () => { specifications: [{ id: "huey", name: "Huey", - url: `/api/remotes/${base64RemoteConfigEncoder.encode({ url: "https://example.com/huey.yml" })}` + url: `/api/remotes/${base64RemoteConfigEncoder.encode({ url: "https://example.com/huey.yml" })}`, + isDefault: false }, { id: "dewey", name: "Dewey", - url: `/api/remotes/${base64RemoteConfigEncoder.encode({ url: "https://example.com/dewey.yml" })}` + url: `/api/remotes/${base64RemoteConfigEncoder.encode({ url: "https://example.com/dewey.yml" })}`, + isDefault: false }] }, { id: "bobby", @@ -762,7 +770,8 @@ test("It adds remote versions from the project configuration", async () => { specifications: [{ id: "louie", name: "Louie", - url: `/api/remotes/${base64RemoteConfigEncoder.encode({ url: "https://example.com/louie.yml" })}` + url: `/api/remotes/${base64RemoteConfigEncoder.encode({ url: "https://example.com/louie.yml" })}`, + isDefault: false }] }]) }) @@ -816,7 +825,8 @@ test("It modifies ID of remote version if the ID already exists", async () => { id: "openapi.yml", name: "openapi.yml", url: "/api/blob/acme/foo-openapi/openapi.yml?ref=12345678", - editURL: "https://github.com/acme/foo-openapi/edit/bar/openapi.yml" + editURL: "https://github.com/acme/foo-openapi/edit/bar/openapi.yml", + isDefault: false }] }, { id: "bar1", @@ -825,7 +835,8 @@ test("It modifies ID of remote version if the ID already exists", async () => { specifications: [{ id: "baz", name: "Baz", - url: `/api/remotes/${base64RemoteConfigEncoder.encode({ url: "https://example.com/baz.yml" })}` + url: `/api/remotes/${base64RemoteConfigEncoder.encode({ url: "https://example.com/baz.yml" })}`, + isDefault: false }] }, { id: "bar2", @@ -834,7 +845,8 @@ test("It modifies ID of remote version if the ID already exists", async () => { specifications: [{ id: "hello", name: "Hello", - url: `/api/remotes/${base64RemoteConfigEncoder.encode({ url: "https://example.com/hello.yml" })}` + url: `/api/remotes/${base64RemoteConfigEncoder.encode({ url: "https://example.com/hello.yml" })}`, + isDefault: false }] }]) }) @@ -877,7 +889,8 @@ test("It lets users specify the ID of a remote version", async () => { specifications: [{ id: "baz", name: "Baz", - url: `/api/remotes/${base64RemoteConfigEncoder.encode({ url: "https://example.com/baz.yml" })}` + url: `/api/remotes/${base64RemoteConfigEncoder.encode({ url: "https://example.com/baz.yml" })}`, + isDefault: false }] }]) }) @@ -920,7 +933,168 @@ test("It lets users specify the ID of a remote specification", async () => { specifications: [{ id: "some-spec", name: "Baz", - url: `/api/remotes/${base64RemoteConfigEncoder.encode({ url: "https://example.com/baz.yml" })}` + url: `/api/remotes/${base64RemoteConfigEncoder.encode({ url: "https://example.com/baz.yml" })}`, + isDefault: false }] }]) }) + +test("It sets isDefault on the correct specification based on defaultSpecificationName in config", async () => { + const sut = new GitHubProjectDataSource({ + repositoryNameSuffix: "-openapi", + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + configYml: { + text: ` + defaultSpecificationName: bar-service.yml + remoteVersions: + - name: Bar + specifications: + - id: some-spec + name: Baz + url: https://example.com/baz.yml + ` + }, + branches: [{ + id: "12345678", + name: "main", + files: [ + { name: "foo-service.yml" }, + { name: "bar-service.yml" }, + { name: "baz-service.yml" } + ] + }], + tags: [] + }] + } + }, + encryptionService: noopEncryptionService, + remoteConfigEncoder: base64RemoteConfigEncoder + }) + const projects = await sut.getProjects() + const specs = projects[0].versions[0].specifications + expect(specs.find(s => s.name === "bar-service.yml")!.isDefault).toBe(true) + expect(specs.find(s => s.name === "foo-service.yml")!.isDefault).toBe(false) + expect(specs.find(s => s.name === "baz-service.yml")!.isDefault).toBe(false) + expect(projects[0].versions[1].specifications.find(s => s.name === "Baz")!.isDefault).toBe(false) +}) + +test("It sets a remote specification as the default if specified", async () => { + const sut = new GitHubProjectDataSource({ + repositoryNameSuffix: "-openapi", + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + configYaml: { + text: ` + defaultSpecificationName: Baz + remoteVersions: + - name: Bar + specifications: + - id: some-spec + name: Baz + url: https://example.com/baz.yml + - id: another-spec + name: Qux + url: https://example.com/qux.yml + ` + }, + branches: [], + tags: [] + }] + } + }, + encryptionService: noopEncryptionService, + remoteConfigEncoder: base64RemoteConfigEncoder + }) + const projects = await sut.getProjects() + const remoteSpecs = projects[0].versions[0].specifications + expect(remoteSpecs.find(s => s.id === "some-spec")!.isDefault).toBe(true) + expect(remoteSpecs.find(s => s.id === "another-spec")!.isDefault).toBe(false) +}) + + +test("It sets isDefault to false for all specifications if defaultSpecificationName is not set", async () => { + const sut = new GitHubProjectDataSource({ + repositoryNameSuffix: "-openapi", + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + configYml: { + text: `` + }, + branches: [{ + id: "12345678", + name: "main", + files: [ + { name: "foo-service.yml" }, + { name: "bar-service.yml" }, + { name: "baz-service.yml" } + ] + }], + tags: [] + }] + } + }, + encryptionService: noopEncryptionService, + remoteConfigEncoder: base64RemoteConfigEncoder + }) + const projects = await sut.getProjects() + const specs = projects[0].versions[0].specifications + expect(specs.every(s => s.isDefault === false)).toBe(true) +}) + +test("It silently ignores defaultSpecificationName if no matching spec is found", async () => { + const sut = new GitHubProjectDataSource({ + repositoryNameSuffix: "-openapi", + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + configYml: { + text: `defaultSpecificationName: non-existent.yml` + }, + branches: [{ + id: "12345678", + name: "main", + files: [ + { name: "foo-service.yml" }, + { name: "bar-service.yml" }, + { name: "baz-service.yml" } + ] + }], + tags: [] + }] + } + }, + encryptionService: noopEncryptionService, + remoteConfigEncoder: base64RemoteConfigEncoder + }) + const projects = await sut.getProjects() + const specs = projects[0].versions[0].specifications + expect(specs.every(s => s.isDefault === false)).toBe(true) +}) diff --git a/src/features/projects/data/GitHubProjectDataSource.ts b/src/features/projects/data/GitHubProjectDataSource.ts index dc6de577..acc33ed5 100644 --- a/src/features/projects/data/GitHubProjectDataSource.ts +++ b/src/features/projects/data/GitHubProjectDataSource.ts @@ -65,6 +65,7 @@ export default class GitHubProjectDataSource implements IProjectDataSource { ).filter(version => { return version.specifications.length > 0 }) + .map(version => this.setDefaultSpecification(version, config?.defaultSpecificationName)); const defaultName = repository.name.replace(new RegExp(this.repositoryNameSuffix + "$"), "") return { id: `${repository.owner}-${defaultName}`, @@ -130,7 +131,8 @@ export default class GitHubProjectDataSource implements IProjectDataSource { path: file.name, ref: ref.id }), - editURL: `https://github.com/${ownerName}/${repositoryName}/edit/${ref.name}/${file.name}` + editURL: `https://github.com/${ownerName}/${repositoryName}/edit/${ref.name}/${file.name}`, + isDefault: false // initial value } }) return { @@ -187,7 +189,8 @@ export default class GitHubProjectDataSource implements IProjectDataSource { return { id: this.makeURLSafeID((e.id || e.name).toLowerCase()), name: e.name, - url: `/api/remotes/${encodedRemoteConfig}` + url: `/api/remotes/${encodedRemoteConfig}`, + isDefault: false // initial value }; }) versions.push({ @@ -246,4 +249,14 @@ export default class GitHubProjectDataSource implements IProjectDataSource { return undefined } } + + private setDefaultSpecification(version: Version, defaultSpecificationName?: string): Version { + return { + ...version, + specifications: version.specifications.map(spec => ({ + ...spec, + isDefault: spec.name === defaultSpecificationName + })) + } + } } diff --git a/src/features/projects/data/useProjectSelection.ts b/src/features/projects/data/useProjectSelection.ts index 2ff26e96..de8327b4 100644 --- a/src/features/projects/data/useProjectSelection.ts +++ b/src/features/projects/data/useProjectSelection.ts @@ -4,7 +4,13 @@ import NProgress from "nprogress" import { useRouter, usePathname } from "next/navigation" import { useContext } from "react" import { ProjectsContext } from "@/common" -import { Project, ProjectNavigator, getProjectSelectionFromPath } from "../domain" +import { + Project, + ProjectNavigator, + getProjectSelectionFromPath, + getDefaultSpecification + +} from "../domain" export default function useProjectSelection() { const router = useRouter() @@ -29,7 +35,7 @@ export default function useProjectSelection() { }, selectProject: (project: Project) => { const version = project.versions[0] - const specification = version.specifications[0] + const specification = getDefaultSpecification(version) NProgress.start() projectNavigator.navigate( project.owner, diff --git a/src/features/projects/domain/IProjectConfig.ts b/src/features/projects/domain/IProjectConfig.ts index 08e27f7c..34cf660c 100644 --- a/src/features/projects/domain/IProjectConfig.ts +++ b/src/features/projects/domain/IProjectConfig.ts @@ -20,6 +20,7 @@ export const ProjectConfigRemoteVersionSchema = z.object({ export const IProjectConfigSchema = z.object({ name: z.coerce.string().optional(), image: z.string().optional(), + defaultSpecificationName: z.string().optional(), remoteVersions: ProjectConfigRemoteVersionSchema.array().optional() }) diff --git a/src/features/projects/domain/OpenApiSpecification.ts b/src/features/projects/domain/OpenApiSpecification.ts index 32ed1012..b0c3bfa5 100644 --- a/src/features/projects/domain/OpenApiSpecification.ts +++ b/src/features/projects/domain/OpenApiSpecification.ts @@ -4,7 +4,8 @@ export const OpenApiSpecificationSchema = z.object({ id: z.string(), name: z.string(), url: z.string(), - editURL: z.string().optional() + editURL: z.string().optional(), + isDefault: z.boolean() }) type OpenApiSpecification = z.infer diff --git a/src/features/projects/domain/ProjectNavigator.ts b/src/features/projects/domain/ProjectNavigator.ts index aa87c239..e3d03217 100644 --- a/src/features/projects/domain/ProjectNavigator.ts +++ b/src/features/projects/domain/ProjectNavigator.ts @@ -1,4 +1,5 @@ import Project from "./Project" +import { getDefaultSpecification } from "./Version" interface IPathnameReader { readonly pathname: string @@ -36,8 +37,8 @@ export default class ProjectNavigator { if (candidateSpecification) { this.router.push(`/${project.owner}/${project.name}/${newVersion.id}/${candidateSpecification.id}`) } else { - const firstSpecification = newVersion.specifications[0] - this.router.push(`/${project.owner}/${project.name}/${newVersion.id}/${firstSpecification.id}`) + const defaultOrFirstSpecification = getDefaultSpecification(newVersion) + this.router.push(`/${project.owner}/${project.name}/${newVersion.id}/${defaultOrFirstSpecification.id}`) } } diff --git a/src/features/projects/domain/Version.ts b/src/features/projects/domain/Version.ts index 2eee9225..f6b69989 100644 --- a/src/features/projects/domain/Version.ts +++ b/src/features/projects/domain/Version.ts @@ -12,3 +12,7 @@ export const VersionSchema = z.object({ type Version = z.infer export default Version + +export function getDefaultSpecification(version: Version) { + return version.specifications.find((spec) => spec.isDefault) || version.specifications[0] +} diff --git a/src/features/projects/domain/getProjectSelectionFromPath.ts b/src/features/projects/domain/getProjectSelectionFromPath.ts index b18c405c..dfa5497e 100644 --- a/src/features/projects/domain/getProjectSelectionFromPath.ts +++ b/src/features/projects/domain/getProjectSelectionFromPath.ts @@ -1,5 +1,5 @@ import Project from "./Project" -import Version from "./Version" +import Version, { getDefaultSpecification } from "./Version" import OpenApiSpecification from "./OpenApiSpecification" export default function getProjectSelectionFromPath({ @@ -61,7 +61,7 @@ export default function getProjectSelectionFromPath({ if (specificationId && !didMoveSpecificationIdToVersionId) { specification = version.specifications.find(e => e.id == specificationId) } else if (version.specifications.length > 0) { - specification = version.specifications[0] + specification = getDefaultSpecification(version) } return { project, version, specification } } diff --git a/src/features/projects/domain/index.ts b/src/features/projects/domain/index.ts index 2b3f10cf..2bf08445 100644 --- a/src/features/projects/domain/index.ts +++ b/src/features/projects/domain/index.ts @@ -16,3 +16,4 @@ export { default as ProjectNavigator } from "./ProjectNavigator" export { default as ProjectRepository } from "./ProjectRepository" export { default as updateWindowTitle } from "./updateWindowTitle" export type { default as Version } from "./Version" +export { getDefaultSpecification } from "./Version"