From a7bcafd808bc877e672ff090214530ef32cd0050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 26 Oct 2023 14:19:23 +0200 Subject: [PATCH 01/16] Create action.yml --- .github/actions/deploy/action.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/actions/deploy/action.yml diff --git a/.github/actions/deploy/action.yml b/.github/actions/deploy/action.yml new file mode 100644 index 00000000..87e2a2b2 --- /dev/null +++ b/.github/actions/deploy/action.yml @@ -0,0 +1,24 @@ +name: Installs HMAC secret +inputs: + heroku-app: + required: true + source-branch: + required: true +runs: + using: composite + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + - name: Install 1Password CLI + uses: 1password/install-cli-action@v1 + - name: Get Heroku API Key + run: | + API_KEY=$(op read "op://Shape Docs GitHub Actions/Heroku API Key/password") + echo "HEROKU_API_KEY=$(echo $API_KEY)" >> $GITHUB_ENV + shell: bash + - name: Add Heroku Git Remote + run: heroku git:remote -a ${{ inputs.heroku-app }} + shell: bash + - name: Push to Heroku + run: git push heroku ${{ inputs.source-branch }}:main + shell: bash From a60220fd1183d26d8b3ca4c7cef794313ba93d68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 26 Oct 2023 14:20:29 +0200 Subject: [PATCH 02/16] Create deploy.yml --- .github/workflows/deploy.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..cec162f5 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,21 @@ +name: Deploy to Staging +on: + workflow_dispatch: {} + push: + branches: + - develop +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true +env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN_SHAPE_DOCS }} +jobs: + deploy: + name: Deploy + runs-on: ubuntu-latest + steps: + - name: Deploy + uses: ./.github/actions/deploy + with: + heroku-app: shape-docs-staging + source-branch: develop From 18dcba0a087c0ff9b2173fd0fc54049befdc8b2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 26 Oct 2023 14:22:11 +0200 Subject: [PATCH 03/16] Update action.yml --- .github/actions/deploy/action.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/actions/deploy/action.yml b/.github/actions/deploy/action.yml index 87e2a2b2..c8957898 100644 --- a/.github/actions/deploy/action.yml +++ b/.github/actions/deploy/action.yml @@ -7,8 +7,6 @@ inputs: runs: using: composite steps: - - name: Checkout Repository - uses: actions/checkout@v4 - name: Install 1Password CLI uses: 1password/install-cli-action@v1 - name: Get Heroku API Key From dee29c39dacd11e18940ab2bc12e54a202e9756f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 26 Oct 2023 14:22:25 +0200 Subject: [PATCH 04/16] Update deploy.yml --- .github/workflows/deploy.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index cec162f5..b5866201 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -14,6 +14,8 @@ jobs: name: Deploy runs-on: ubuntu-latest steps: + - name: Checkout Repository + uses: actions/checkout@v4 - name: Deploy uses: ./.github/actions/deploy with: From 18004631b848d82c5174cd0f9949c978499eb948 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 26 Oct 2023 14:33:37 +0200 Subject: [PATCH 05/16] Update action.yml --- .github/actions/deploy/action.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/actions/deploy/action.yml b/.github/actions/deploy/action.yml index c8957898..157fe13c 100644 --- a/.github/actions/deploy/action.yml +++ b/.github/actions/deploy/action.yml @@ -12,11 +12,15 @@ runs: - name: Get Heroku API Key run: | API_KEY=$(op read "op://Shape Docs GitHub Actions/Heroku API Key/password") + echo "::add-mask::$API_KEY" echo "HEROKU_API_KEY=$(echo $API_KEY)" >> $GITHUB_ENV shell: bash - name: Add Heroku Git Remote run: heroku git:remote -a ${{ inputs.heroku-app }} shell: bash - name: Push to Heroku - run: git push heroku ${{ inputs.source-branch }}:main + run: | + git config --global user.name "GitHub Actions" + git config --global user.email "heroku@shape.dk" + git push heroku ${{ inputs.source-branch }}:main shell: bash From 1505da2ed41f0e039da8d49c26a38a1c8c80b8c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 26 Oct 2023 14:43:33 +0200 Subject: [PATCH 06/16] Update action.yml --- .github/actions/deploy/action.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/actions/deploy/action.yml b/.github/actions/deploy/action.yml index 157fe13c..adda6a67 100644 --- a/.github/actions/deploy/action.yml +++ b/.github/actions/deploy/action.yml @@ -15,6 +15,12 @@ runs: echo "::add-mask::$API_KEY" echo "HEROKU_API_KEY=$(echo $API_KEY)" >> $GITHUB_ENV shell: bash + - name: Install SSH Key + run: | + op read --out-file ~/.ssh/heroku "op://Shape Docs GitHub Actions/Heroku SSH Key/ssh-key" + op read --out-file ~/.ssh/heroku.pub "op://Shape Docs GitHub Actions/Heroku SSH Key/ssh-key.pub" + ssh-add ~/.ssh/heroku + shell: bash - name: Add Heroku Git Remote run: heroku git:remote -a ${{ inputs.heroku-app }} shell: bash From beb2b7e8fc06966794a5889843ae99fa82aa1384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 26 Oct 2023 14:48:16 +0200 Subject: [PATCH 07/16] Delete .github/actions/deploy/action.yml --- .github/actions/deploy/action.yml | 32 ------------------------------- 1 file changed, 32 deletions(-) delete mode 100644 .github/actions/deploy/action.yml diff --git a/.github/actions/deploy/action.yml b/.github/actions/deploy/action.yml deleted file mode 100644 index adda6a67..00000000 --- a/.github/actions/deploy/action.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Installs HMAC secret -inputs: - heroku-app: - required: true - source-branch: - required: true -runs: - using: composite - steps: - - name: Install 1Password CLI - uses: 1password/install-cli-action@v1 - - name: Get Heroku API Key - run: | - API_KEY=$(op read "op://Shape Docs GitHub Actions/Heroku API Key/password") - echo "::add-mask::$API_KEY" - echo "HEROKU_API_KEY=$(echo $API_KEY)" >> $GITHUB_ENV - shell: bash - - name: Install SSH Key - run: | - op read --out-file ~/.ssh/heroku "op://Shape Docs GitHub Actions/Heroku SSH Key/ssh-key" - op read --out-file ~/.ssh/heroku.pub "op://Shape Docs GitHub Actions/Heroku SSH Key/ssh-key.pub" - ssh-add ~/.ssh/heroku - shell: bash - - name: Add Heroku Git Remote - run: heroku git:remote -a ${{ inputs.heroku-app }} - shell: bash - - name: Push to Heroku - run: | - git config --global user.name "GitHub Actions" - git config --global user.email "heroku@shape.dk" - git push heroku ${{ inputs.source-branch }}:main - shell: bash From f055af4ab685a74c58be39cdfc8f56054dcb0bad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 26 Oct 2023 14:48:30 +0200 Subject: [PATCH 08/16] Delete .github/workflows/deploy.yml --- .github/workflows/deploy.yml | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index b5866201..00000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Deploy to Staging -on: - workflow_dispatch: {} - push: - branches: - - develop -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true -env: - OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN_SHAPE_DOCS }} -jobs: - deploy: - name: Deploy - runs-on: ubuntu-latest - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - name: Deploy - uses: ./.github/actions/deploy - with: - heroku-app: shape-docs-staging - source-branch: develop From b2751d1ed5bbc9af56ce1b6b6f458f7bc660528a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 26 Oct 2023 16:52:21 +0200 Subject: [PATCH 09/16] Prioritizes project selection --- __test__/projects/ProjectPageState.test.ts | 48 +++++++++++ .../projects/domain/ProjectPageState.ts | 86 +++++++++++-------- 2 files changed, 98 insertions(+), 36 deletions(-) diff --git a/__test__/projects/ProjectPageState.test.ts b/__test__/projects/ProjectPageState.test.ts index e91d462c..aa08d51e 100644 --- a/__test__/projects/ProjectPageState.test.ts +++ b/__test__/projects/ProjectPageState.test.ts @@ -302,3 +302,51 @@ test("It errors when the selected version has no specifications", async () => { expect(sut.state).toEqual(ProjectPageState.SPECIFICATION_NOT_FOUND) }) +test("It enters loading state, even when attempting to load a project that does not exist", async () => { + const sut = getProjectPageState({ + selectedProjectId: "doesnotexist", + selectedVersionId: "bar", + selectedSpecificationId: "baz", + isLoading: true, + projects: [{ + id: "foo", + name: "foo", + displayName: "foo", + versions: [{ + id: "bar", + name: "bar", + isDefault: false, + specifications: [] + }] + }] + }) + expect(sut.state).toEqual(ProjectPageState.LOADING) +}) + +test("It selects project, version, and specification when they exist even when still loading", async () => { + const sut = getProjectPageState({ + selectedProjectId: "foo", + selectedVersionId: "bar", + selectedSpecificationId: "baz", + isLoading: true, + projects: [{ + id: "foo", + name: "foo", + displayName: "foo", + versions: [{ + id: "bar", + name: "bar", + isDefault: false, + specifications: [{ + id: "baz", + name: "baz.yml", + url: "https://example.com/baz.yml" + }] + }] + }] + }) + expect(sut.state).toEqual(ProjectPageState.HAS_SELECTION) + expect(sut.selection!.project.id).toEqual("foo") + expect(sut.selection!.version.id).toEqual("bar") + expect(sut.selection!.specification.id).toEqual("baz") +}) diff --git a/src/features/projects/domain/ProjectPageState.ts b/src/features/projects/domain/ProjectPageState.ts index 4e9f134f..674b25ca 100644 --- a/src/features/projects/domain/ProjectPageState.ts +++ b/src/features/projects/domain/ProjectPageState.ts @@ -36,56 +36,70 @@ export function getProjectPageState({ selectedVersionId, selectedSpecificationId }: GetProjectPageStateProps): ProjectPageStateContainer { - if (isLoading) { - return { state: ProjectPageState.LOADING } - } - if (error) { - return { state: ProjectPageState.ERROR, error } - } - projects = projects || [] // If no project is selected and the user only has a single project then we select that. + projects = projects || [] if (!selectedProjectId && projects.length == 1) { selectedProjectId = projects[0].id } - if (!selectedProjectId) { + const { project, version, specification } = getSelection( + projects, + selectedProjectId, + selectedVersionId, + selectedSpecificationId + ) + if (project && version && specification) { + return { + state: ProjectPageState.HAS_SELECTION, + selection: { project, version, specification } + } + } else if (isLoading) { + return { state: ProjectPageState.LOADING } + } else if (error) { + return { state: ProjectPageState.ERROR, error } + } else if (!selectedProjectId) { return { state: ProjectPageState.NO_PROJECT_SELECTED } + } else if (!project) { + return { state: ProjectPageState.PROJECT_NOT_FOUND } + } else if (!version) { + return { state: ProjectPageState.VERSION_NOT_FOUND } + } else { + return { state: ProjectPageState.SPECIFICATION_NOT_FOUND } } - const project = projects.find(e => e.id == selectedProjectId) +} + +function getSelection( + projects: Project[], + projectId?: string, + versionId?: string, + specificationId?: string +): { + project?: Project, + version?: Version, + specification?: OpenApiSpecification +} { + if (!projectId) { + return {} + } + let project = projects.find(e => e.id == projectId) if (!project) { - return { state: ProjectPageState.PROJECT_NOT_FOUND } + return {} } - // Find selected version or default to first version if none is selected. - let version: Version - if (selectedVersionId) { - const selectedVersion = project.versions.find(e => e.id == selectedVersionId) - if (selectedVersion) { - version = selectedVersion - } else { - return { state: ProjectPageState.VERSION_NOT_FOUND } - } + let version: Version | undefined + if (versionId) { + version = project.versions.find(e => e.id == versionId) } else if (project.versions.length > 0) { version = project.versions[0] - } else { - return { state: ProjectPageState.VERSION_NOT_FOUND } } - // Find selected specification or default to first specification if none is selected. - let specification: OpenApiSpecification - if (selectedSpecificationId) { - const selectedSpecification = version.specifications.find(e => e.id == selectedSpecificationId) - if (selectedSpecification) { - specification = selectedSpecification - } else { - return { state: ProjectPageState.SPECIFICATION_NOT_FOUND } - } + if (!version) { + return { project } + } + let specification: OpenApiSpecification | undefined + if (specificationId) { + specification = version.specifications.find(e => e.id == specificationId) } else if (version.specifications.length > 0) { specification = version.specifications[0] - } else { - return { state: ProjectPageState.SPECIFICATION_NOT_FOUND } - } - return { - state: ProjectPageState.HAS_SELECTION, - selection: { project, version, specification } } + return { project, version, specification } } export default ProjectPageState From d30fe2f4f01ba7fc15b0e254dcfade190a829044 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 26 Oct 2023 16:57:59 +0200 Subject: [PATCH 10/16] Considers page to be loading as long as client is loading --- src/features/projects/view/client/ProjectsPage.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/features/projects/view/client/ProjectsPage.tsx b/src/features/projects/view/client/ProjectsPage.tsx index 71a9cf5e..219f7722 100644 --- a/src/features/projects/view/client/ProjectsPage.tsx +++ b/src/features/projects/view/client/ProjectsPage.tsx @@ -32,9 +32,8 @@ export default function ProjectsPage({ const { projects: clientProjects, error, isLoading: isClientLoading } = useProjects() const [forceCloseSidebar, setForceCloseSidebar] = useState(false) const projects = isClientLoading ? (serverProjects || []) : clientProjects - const isLoading = serverProjects === undefined && isClientLoading const stateContainer = getProjectPageState({ - isLoading, + isLoading: isClientLoading, error, projects, selectedProjectId: projectId, @@ -74,7 +73,7 @@ export default function ProjectsPage({ forceClose={forceCloseSidebar} sidebar={ Date: Thu, 26 Oct 2023 16:59:05 +0200 Subject: [PATCH 11/16] Fixes linting error --- src/features/projects/domain/ProjectPageState.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/projects/domain/ProjectPageState.ts b/src/features/projects/domain/ProjectPageState.ts index 674b25ca..17c71dbf 100644 --- a/src/features/projects/domain/ProjectPageState.ts +++ b/src/features/projects/domain/ProjectPageState.ts @@ -80,7 +80,7 @@ function getSelection( if (!projectId) { return {} } - let project = projects.find(e => e.id == projectId) + const project = projects.find(e => e.id == projectId) if (!project) { return {} } From 2208bad4f7cfedea7e15c1b2ed764ee592ebf683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 26 Oct 2023 16:59:09 +0200 Subject: [PATCH 12/16] Fixes linting warning --- src/features/sidebar/view/client/SidebarContainer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/sidebar/view/client/SidebarContainer.tsx b/src/features/sidebar/view/client/SidebarContainer.tsx index d282fe47..048b2c48 100644 --- a/src/features/sidebar/view/client/SidebarContainer.tsx +++ b/src/features/sidebar/view/client/SidebarContainer.tsx @@ -34,7 +34,7 @@ const SidebarContainer = ({ if (forceClose) { setOpen(false) } - }, [forceClose]) + }, [forceClose, setOpen]) return ( Date: Thu, 26 Oct 2023 17:56:58 +0200 Subject: [PATCH 13/16] Refactors project selection logic --- __test__/projects/ProjectPageState.test.ts | 224 +++++------------ __test__/projects/projectNavigator.test.ts | 229 +++++++++++------- __test__/projects/updateWindowTitle.test.ts | 24 +- .../projects/domain/ProjectPageSelection.ts | 11 - .../projects/domain/ProjectPageState.ts | 105 -------- src/features/projects/domain/getSelection.ts | 47 ++++ .../projects/domain/projectNavigator.ts | 30 ++- .../projects/domain/updateWindowTitle.ts | 30 ++- src/features/projects/view/MainContent.tsx | 35 +++ .../projects/view/ProjectsPageContent.tsx | 27 --- .../projects/view/client/ProjectsPage.tsx | 77 +++--- 11 files changed, 376 insertions(+), 463 deletions(-) delete mode 100644 src/features/projects/domain/ProjectPageSelection.ts delete mode 100644 src/features/projects/domain/ProjectPageState.ts create mode 100644 src/features/projects/domain/getSelection.ts create mode 100644 src/features/projects/view/MainContent.tsx delete mode 100644 src/features/projects/view/ProjectsPageContent.tsx diff --git a/__test__/projects/ProjectPageState.test.ts b/__test__/projects/ProjectPageState.test.ts index aa08d51e..25c116de 100644 --- a/__test__/projects/ProjectPageState.test.ts +++ b/__test__/projects/ProjectPageState.test.ts @@ -1,41 +1,7 @@ -import { - getProjectPageState, - ProjectPageState -} from "../../src/features/projects/domain/ProjectPageState" +import getSelection from "../../src/features/projects/domain/getSelection" -test("It enters the loading state", async () => { - const sut = getProjectPageState({ isLoading: true }) - expect(sut.state).toEqual(ProjectPageState.LOADING) -}) - -test("It enters the error state", async () => { - const sut = getProjectPageState({ - isLoading: false, - error: new Error("foo") - }) - expect(sut.state).toEqual(ProjectPageState.ERROR) - expect(sut.error).toEqual(new Error("foo")) -}) - -test("It gracefully errors when no project has been selected", async () => { - const sut = getProjectPageState({ - projects: [{ - id: "foo", - name: "foo", - displayName: "foo", - versions: [] - }, { - id: "bar", - name: "bar", - displayName: "bar", - versions: [] - }] - }) - expect(sut.state).toEqual(ProjectPageState.NO_PROJECT_SELECTED) -}) - -test("It selects the first project when there is only one project", async () => { - const sut = getProjectPageState({ +test("It selects the first project when there is only one project", () => { + const sut = getSelection({ projects: [{ id: "foo", name: "foo", @@ -52,15 +18,14 @@ test("It selects the first project when there is only one project", async () => }] }] }) - expect(sut.state).toEqual(ProjectPageState.HAS_SELECTION) - expect(sut.selection!.project.id).toEqual("foo") - expect(sut.selection!.version.id).toEqual("bar") - expect(sut.selection!.specification.id).toEqual("hello") + expect(sut.project!.id).toEqual("foo") + expect(sut.version!.id).toEqual("bar") + expect(sut.specification!.id).toEqual("hello") }) -test("It selects the first version and specification of the specified project", async () => { - const sut = getProjectPageState({ - selectedProjectId: "bar", +test("It selects the first version and specification of the specified project", () => { + const sut = getSelection({ + projectId: "bar", projects: [{ id: "foo", name: "foo", @@ -91,16 +56,15 @@ test("It selects the first version and specification of the specified project", }] }] }) - expect(sut.state).toEqual(ProjectPageState.HAS_SELECTION) - expect(sut.selection!.project.id).toEqual("bar") - expect(sut.selection!.version.id).toEqual("baz1") - expect(sut.selection!.specification.id).toEqual("hello1") + expect(sut.project!.id).toEqual("bar") + expect(sut.version!.id).toEqual("baz1") + expect(sut.specification!.id).toEqual("hello1") }) -test("It selects the first specification of the specified project and version", async () => { - const sut = getProjectPageState({ - selectedProjectId: "bar", - selectedVersionId: "baz2", +test("It selects the first specification of the specified project and version", () => { + const sut = getSelection({ + projectId: "bar", + versionId: "baz2", projects: [{ id: "foo", name: "foo", @@ -127,16 +91,15 @@ test("It selects the first specification of the specified project and version", }] }] }) - expect(sut.state).toEqual(ProjectPageState.HAS_SELECTION) - expect(sut.selection!.project.id).toEqual("bar") - expect(sut.selection!.version.id).toEqual("baz2") - expect(sut.selection!.specification.id).toEqual("hello1") + expect(sut.project!.id).toEqual("bar") + expect(sut.version!.id).toEqual("baz2") + expect(sut.specification!.id).toEqual("hello1") }) -test("It selects the specification of the specified version", async () => { - const sut = getProjectPageState({ - selectedProjectId: "bar", - selectedVersionId: "baz2", +test("It selects the specification of the specified version", () => { + const sut = getSelection({ + projectId: "bar", + versionId: "baz2", projects: [{ id: "foo", name: "foo", @@ -167,17 +130,16 @@ test("It selects the specification of the specified version", async () => { }] }] }) - expect(sut.state).toEqual(ProjectPageState.HAS_SELECTION) - expect(sut.selection!.project.id).toEqual("bar") - expect(sut.selection!.version.id).toEqual("baz2") - expect(sut.selection!.specification.id).toEqual("hello1") + expect(sut.project!.id).toEqual("bar") + expect(sut.version!.id).toEqual("baz2") + expect(sut.specification!.id).toEqual("hello1") }) -test("It selects the specified project, version, and specification", async () => { - const sut = getProjectPageState({ - selectedProjectId: "bar", - selectedVersionId: "baz2", - selectedSpecificationId: "hello2", +test("It selects the specified project, version, and specification", () => { + const sut = getSelection({ + projectId: "bar", + versionId: "baz2", + specificationId: "hello2", projects: [{ id: "foo", name: "foo", @@ -208,15 +170,14 @@ test("It selects the specified project, version, and specification", async () => }] }] }) - expect(sut.state).toEqual(ProjectPageState.HAS_SELECTION) - expect(sut.selection!.project.id).toEqual("bar") - expect(sut.selection!.version.id).toEqual("baz2") - expect(sut.selection!.specification.id).toEqual("hello2") + expect(sut.project!.id).toEqual("bar") + expect(sut.version!.id).toEqual("baz2") + expect(sut.specification!.id).toEqual("hello2") }) -test("It errors when the selected project cannot be found", async () => { - const sut = getProjectPageState({ - selectedProjectId: "foo", +test("It returns a undefined project, version, and specification when the selected project cannot be found", () => { + const sut = getSelection({ + projectId: "foo", projects: [{ id: "bar", name: "bar", @@ -224,13 +185,15 @@ test("It errors when the selected project cannot be found", async () => { versions: [] }] }) - expect(sut.state).toEqual(ProjectPageState.PROJECT_NOT_FOUND) + expect(sut.project).toBeUndefined() + expect(sut.version).toBeUndefined() + expect(sut.specification).toBeUndefined() }) -test("It errors when the selected version cannot be found", async () => { - const sut = getProjectPageState({ - selectedProjectId: "foo", - selectedVersionId: "bar", +test("It returns a undefined version and specification when the selected version cannot be found", () => { + const sut = getSelection({ + projectId: "foo", + versionId: "bar", projects: [{ id: "foo", name: "foo", @@ -243,14 +206,16 @@ test("It errors when the selected version cannot be found", async () => { }] }] }) - expect(sut.state).toEqual(ProjectPageState.VERSION_NOT_FOUND) + expect(sut.project!.id).toEqual("foo") + expect(sut.version).toBeUndefined() + expect(sut.specification).toBeUndefined() }) -test("It errors when the selected specification cannot be found", async () => { - const sut = getProjectPageState({ - selectedProjectId: "foo", - selectedVersionId: "bar", - selectedSpecificationId: "baz", +test("It returns a undefined specification when the selected specification cannot be found", () => { + const sut = getSelection({ + projectId: "foo", + versionId: "bar", + specificationId: "baz", projects: [{ id: "foo", name: "foo", @@ -267,86 +232,7 @@ test("It errors when the selected specification cannot be found", async () => { }] }] }) - expect(sut.state).toEqual(ProjectPageState.SPECIFICATION_NOT_FOUND) -}) - -test("It errors when the selected project has no versions", async () => { - const sut = getProjectPageState({ - selectedProjectId: "foo", - projects: [{ - id: "foo", - name: "foo", - displayName: "foo", - versions: [] - }] - }) - expect(sut.state).toEqual(ProjectPageState.VERSION_NOT_FOUND) -}) - -test("It errors when the selected version has no specifications", async () => { - const sut = getProjectPageState({ - selectedProjectId: "foo", - selectedVersionId: "bar", - projects: [{ - id: "foo", - name: "foo", - displayName: "foo", - versions: [{ - id: "bar", - name: "bar", - isDefault: false, - specifications: [] - }] - }] - }) - expect(sut.state).toEqual(ProjectPageState.SPECIFICATION_NOT_FOUND) -}) - -test("It enters loading state, even when attempting to load a project that does not exist", async () => { - const sut = getProjectPageState({ - selectedProjectId: "doesnotexist", - selectedVersionId: "bar", - selectedSpecificationId: "baz", - isLoading: true, - projects: [{ - id: "foo", - name: "foo", - displayName: "foo", - versions: [{ - id: "bar", - name: "bar", - isDefault: false, - specifications: [] - }] - }] - }) - expect(sut.state).toEqual(ProjectPageState.LOADING) -}) - -test("It selects project, version, and specification when they exist even when still loading", async () => { - const sut = getProjectPageState({ - selectedProjectId: "foo", - selectedVersionId: "bar", - selectedSpecificationId: "baz", - isLoading: true, - projects: [{ - id: "foo", - name: "foo", - displayName: "foo", - versions: [{ - id: "bar", - name: "bar", - isDefault: false, - specifications: [{ - id: "baz", - name: "baz.yml", - url: "https://example.com/baz.yml" - }] - }] - }] - }) - expect(sut.state).toEqual(ProjectPageState.HAS_SELECTION) - expect(sut.selection!.project.id).toEqual("foo") - expect(sut.selection!.version.id).toEqual("bar") - expect(sut.selection!.specification.id).toEqual("baz") + expect(sut.project!.id).toEqual("foo") + expect(sut.version!.id).toEqual("bar") + expect(sut.specification).toBeUndefined() }) diff --git a/__test__/projects/projectNavigator.test.ts b/__test__/projects/projectNavigator.test.ts index 86b9b514..a4f69daa 100644 --- a/__test__/projects/projectNavigator.test.ts +++ b/__test__/projects/projectNavigator.test.ts @@ -1,4 +1,3 @@ -import ProjectPageSelection from "../../src/features/projects/domain/ProjectPageSelection" import projectNavigator from "../../src/features/projects/domain/projectNavigator" test("It navigates to the correct path", async () => { @@ -7,122 +6,176 @@ test("It navigates to the correct path", async () => { push: (path: string) => { pushedPath = path }, - replace: (_path: string) => {} + replace: () => {} } projectNavigator.navigate(router, "foo", "bar", "hello.yml") expect(pushedPath).toEqual("/foo/bar/hello.yml") }) test("It navigates to first specification when changing version", async () => { - const selection: ProjectPageSelection = { - project: { - id: "foo", - name: "foo", - displayName: "foo", - versions: [{ - id: "bar", - name: "bar", - isDefault: false, - specifications: [{ - id: "baz.yml", - name: "baz.yml", - url: "https://example.com/baz.yml" - }] - }, { - id: "hello", - name: "hello", - isDefault: false, - specifications: [{ - id: "world.yml", - name: "world.yml", - url: "https://example.com/world.yml" - }] - }] - }, - version: { + const project = { + id: "foo", + name: "foo", + displayName: "foo", + versions: [{ id: "bar", name: "bar", isDefault: false, - specifications: [] - }, - specification: { - id: "baz.yml", - name: "baz.yml", - url: "https://example.com/baz.yml" - } + specifications: [{ + id: "baz.yml", + name: "baz.yml", + url: "https://example.com/baz.yml" + }] + }, { + id: "hello", + name: "hello", + isDefault: false, + specifications: [{ + id: "world.yml", + name: "world.yml", + url: "https://example.com/world.yml" + }] + }] } let pushedPath: string | undefined const router = { push: (path: string) => { pushedPath = path }, - replace: (_path: string) => {} + replace: () => {} } - projectNavigator.navigateToVersion(router, selection, "hello") + projectNavigator.navigateToVersion(router, project, "hello", "baz.yml") expect(pushedPath).toEqual("/foo/hello/world.yml") }) test("It finds a specification with the same name when changing version", async () => { - const selection: ProjectPageSelection = { - project: { - id: "foo", - name: "foo", - displayName: "foo", - versions: [{ - id: "bar", - name: "bar", - isDefault: false, - specifications: [{ - id: "hello.yml", - name: "hello.yml", - url: "https://example.com/hello.yml" - }, { - id: "earth.yml", - name: "earth.yml", - url: "https://example.com/earth.yml" - }] - }, { - id: "baz", - name: "baz", - isDefault: false, - specifications: [{ - id: "moon.yml", - name: "moon.yml", - url: "https://example.com/moon.yml" - }, { - id: "saturn.yml", - name: "saturn.yml", - url: "https://example.com/saturn.yml" - }, { - id: "earth.yml", - name: "earth.yml", - url: "https://example.com/earth.yml" - }, { - id: "jupiter.yml", - name: "jupiter.yml", - url: "https://example.com/jupiter.yml" - }] - }] - }, - version: { + const project = { + id: "foo", + name: "foo", + displayName: "foo", + versions: [{ id: "bar", name: "bar", isDefault: false, - specifications: [] - }, - specification: { - id: "earth.yml", - name: "earth.yml", - url: "https://example.com/earth.yml" - } + specifications: [{ + id: "hello.yml", + name: "hello.yml", + url: "https://example.com/hello.yml" + }, { + id: "earth.yml", + name: "earth.yml", + url: "https://example.com/earth.yml" + }] + }, { + id: "baz", + name: "baz", + isDefault: false, + specifications: [{ + id: "moon.yml", + name: "moon.yml", + url: "https://example.com/moon.yml" + }, { + id: "saturn.yml", + name: "saturn.yml", + url: "https://example.com/saturn.yml" + }, { + id: "earth.yml", + name: "earth.yml", + url: "https://example.com/earth.yml" + }, { + id: "jupiter.yml", + name: "jupiter.yml", + url: "https://example.com/jupiter.yml" + }] + }] } let pushedPath: string | undefined const router = { push: (path: string) => { pushedPath = path }, - replace: (_path: string) => {} + replace: () => {} } - projectNavigator.navigateToVersion(router, selection, "baz") + projectNavigator.navigateToVersion(router, project, "baz", "earth.yml") expect(pushedPath).toEqual("/foo/baz/earth.yml") }) + +test("It skips navigating when URL matches selection", async () => { + let didNavigate = false + const router = { + push: () => {}, + replace: () => { + didNavigate = true + } + } + projectNavigator.navigateIfNeeded(router, { + projectId: "foo", + versionId: "bar", + specificationId: "baz" + }, { + projectId: "foo", + versionId: "bar", + specificationId: "baz" + }) + expect(didNavigate).toBeFalsy() +}) + +test("It navigates when project ID in URL does not match ID of selected project", async () => { + let didNavigate = false + const router = { + push: () => {}, + replace: () => { + didNavigate = true + } + } + projectNavigator.navigateIfNeeded(router, { + projectId: "foo", + versionId: "bar", + specificationId: "baz" + }, { + projectId: "hello", + versionId: "bar", + specificationId: "baz" + }) + expect(didNavigate).toBeTruthy() +}) + +test("It navigates when version ID in URL does not match ID of selected version", async () => { + let didNavigate = false + const router = { + push: () => {}, + replace: () => { + didNavigate = true + } + } + projectNavigator.navigateIfNeeded(router, { + projectId: "foo", + versionId: "bar", + specificationId: "baz" + }, { + projectId: "foo", + versionId: "hello", + specificationId: "baz" + }) + expect(didNavigate).toBeTruthy() +}) + +test("It navigates when specification ID in URL does not match ID of selected specification", async () => { + let didNavigate = false + const router = { + push: () => {}, + replace: () => { + didNavigate = true + } + } + projectNavigator.navigateIfNeeded(router, { + projectId: "foo", + versionId: "bar", + specificationId: "baz" + }, { + projectId: "foo", + versionId: "bar", + specificationId: "hello" + }) + expect(didNavigate).toBeTruthy() +}) diff --git a/__test__/projects/updateWindowTitle.test.ts b/__test__/projects/updateWindowTitle.test.ts index 13464868..8fe93ef1 100644 --- a/__test__/projects/updateWindowTitle.test.ts +++ b/__test__/projects/updateWindowTitle.test.ts @@ -2,13 +2,18 @@ import updateWindowTitle from "../../src/features/projects/domain/updateWindowTi test("It uses default title when there is no selection", async () => { const store: { title: string } = { title: "" } - updateWindowTitle(store, "Shape Docs") + updateWindowTitle({ + storage: store, + defaultTitle: "Shape Docs" + }) expect(store.title).toEqual("Shape Docs") }) test("It leaves out specification when the specification has a generic name", async () => { const store: { title: string } = { title: "" } - updateWindowTitle(store, "Shape Docs", { + updateWindowTitle({ + storage: store, + defaultTitle: "Shape Docs", project: { id: "foo", name: "foo", @@ -40,7 +45,9 @@ test("It leaves out specification when the specification has a generic name", as test("It leaves out version when it is the defualt version", async () => { const store: { title: string } = { title: "" } - updateWindowTitle(store, "Shape Docs", { + updateWindowTitle({ + storage: store, + defaultTitle: "Shape Docs", project: { id: "foo", name: "foo", @@ -68,7 +75,9 @@ test("It leaves out version when it is the defualt version", async () => { test("It adds version when it is not the defualt version", async () => { const store: { title: string } = { title: "" } - updateWindowTitle(store, "Shape Docs", { + updateWindowTitle({ + storage: store, + defaultTitle: "Shape Docs", project: { id: "foo", name: "foo", @@ -93,10 +102,3 @@ test("It adds version when it is not the defualt version", async () => { }) expect(store.title).toEqual("foo / bar") }) - -// } else if (selection.version.isDefault) { -// storage.title = selection.project.displayName -// } else { -// storage.title = `${selection.project.displayName} / ${selection.version.name}` -// } -// } diff --git a/src/features/projects/domain/ProjectPageSelection.ts b/src/features/projects/domain/ProjectPageSelection.ts deleted file mode 100644 index 0da2e3f3..00000000 --- a/src/features/projects/domain/ProjectPageSelection.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Project from "../domain/Project" -import Version from "../domain/Version" -import OpenApiSpecification from "../domain/OpenApiSpecification" - -type ProjectPageSelection = { - readonly project: Project - readonly version: Version - readonly specification: OpenApiSpecification -} - -export default ProjectPageSelection diff --git a/src/features/projects/domain/ProjectPageState.ts b/src/features/projects/domain/ProjectPageState.ts deleted file mode 100644 index 17c71dbf..00000000 --- a/src/features/projects/domain/ProjectPageState.ts +++ /dev/null @@ -1,105 +0,0 @@ -import Project from "./Project" -import Version from "./Version" -import OpenApiSpecification from "./OpenApiSpecification" -import ProjectPageSelection from "./ProjectPageSelection" - -export enum ProjectPageState { - LOADING, - ERROR, - HAS_SELECTION, - NO_PROJECT_SELECTED, - PROJECT_NOT_FOUND, - VERSION_NOT_FOUND, - SPECIFICATION_NOT_FOUND -} - -export type ProjectPageStateContainer = { - readonly state: ProjectPageState - readonly selection?: ProjectPageSelection - readonly error?: Error -} - -type GetProjectPageStateProps = { - isLoading?: boolean - error?: Error - projects?: Project[] - selectedProjectId?: string - selectedVersionId?: string - selectedSpecificationId?: string -} - -export function getProjectPageState({ - isLoading, - error, - projects, - selectedProjectId, - selectedVersionId, - selectedSpecificationId -}: GetProjectPageStateProps): ProjectPageStateContainer { - // If no project is selected and the user only has a single project then we select that. - projects = projects || [] - if (!selectedProjectId && projects.length == 1) { - selectedProjectId = projects[0].id - } - const { project, version, specification } = getSelection( - projects, - selectedProjectId, - selectedVersionId, - selectedSpecificationId - ) - if (project && version && specification) { - return { - state: ProjectPageState.HAS_SELECTION, - selection: { project, version, specification } - } - } else if (isLoading) { - return { state: ProjectPageState.LOADING } - } else if (error) { - return { state: ProjectPageState.ERROR, error } - } else if (!selectedProjectId) { - return { state: ProjectPageState.NO_PROJECT_SELECTED } - } else if (!project) { - return { state: ProjectPageState.PROJECT_NOT_FOUND } - } else if (!version) { - return { state: ProjectPageState.VERSION_NOT_FOUND } - } else { - return { state: ProjectPageState.SPECIFICATION_NOT_FOUND } - } -} - -function getSelection( - projects: Project[], - projectId?: string, - versionId?: string, - specificationId?: string -): { - project?: Project, - version?: Version, - specification?: OpenApiSpecification -} { - if (!projectId) { - return {} - } - const project = projects.find(e => e.id == projectId) - if (!project) { - return {} - } - let version: Version | undefined - if (versionId) { - version = project.versions.find(e => e.id == versionId) - } else if (project.versions.length > 0) { - version = project.versions[0] - } - if (!version) { - return { project } - } - let specification: OpenApiSpecification | undefined - if (specificationId) { - specification = version.specifications.find(e => e.id == specificationId) - } else if (version.specifications.length > 0) { - specification = version.specifications[0] - } - return { project, version, specification } -} - -export default ProjectPageState diff --git a/src/features/projects/domain/getSelection.ts b/src/features/projects/domain/getSelection.ts new file mode 100644 index 00000000..a9aa2caf --- /dev/null +++ b/src/features/projects/domain/getSelection.ts @@ -0,0 +1,47 @@ +import Project from "./Project" +import Version from "./Version" +import OpenApiSpecification from "./OpenApiSpecification" + +export default function getSelection({ + projects, + projectId, + versionId, + specificationId, +}: { + projects: Project[], + projectId?: string, + versionId?: string, + specificationId?: string +}): { + project?: Project, + version?: Version, + specification?: OpenApiSpecification +} { + // If no project is selected and the user only has a single project then we select that. + if (!projectId && projects.length == 1) { + projectId = projects[0].id + } + if (!projectId) { + return {} + } + const project = projects.find(e => e.id == projectId) + if (!project) { + return {} + } + let version: Version | undefined + if (versionId) { + version = project.versions.find(e => e.id == versionId) + } else if (project.versions.length > 0) { + version = project.versions[0] + } + if (!version) { + return { project } + } + let specification: OpenApiSpecification | undefined + if (specificationId) { + specification = version.specifications.find(e => e.id == specificationId) + } else if (version.specifications.length > 0) { + specification = version.specifications[0] + } + return { project, version, specification } +} \ No newline at end of file diff --git a/src/features/projects/domain/projectNavigator.ts b/src/features/projects/domain/projectNavigator.ts index 091096eb..6aa1ce8c 100644 --- a/src/features/projects/domain/projectNavigator.ts +++ b/src/features/projects/domain/projectNavigator.ts @@ -1,4 +1,4 @@ -import ProjectPageSelection from "./ProjectPageSelection" +import Project from "./Project" export interface IProjectRouter { push(path: string): void @@ -8,24 +8,25 @@ export interface IProjectRouter { const projectNavigator = { navigateToVersion( router: IProjectRouter, - selection: ProjectPageSelection, + project: Project, versionId: string, + preferredSpecificationName: string ) { // Let's see if we can find a specification with the same name. - const newVersion = selection.project.versions.find(e => { + const newVersion = project.versions.find(e => { return e.id == versionId }) if (!newVersion) { return } const candidateSpecification = newVersion.specifications.find(e => { - return e.name == selection.specification.name + return e.name == preferredSpecificationName }) if (candidateSpecification) { - router.push(`/${selection.project.id}/${newVersion.id}/${candidateSpecification.id}`) + router.push(`/${project.id}/${newVersion.id}/${candidateSpecification.id}`) } else { const firstSpecification = newVersion.specifications[0] - router.push(`/${selection.project.id}/${newVersion.id}/${firstSpecification.id}`) + router.push(`/${project.id}/${newVersion.id}/${firstSpecification.id}`) } }, navigate( @@ -43,14 +44,21 @@ const projectNavigator = { versionId?: string, specificationId?: string }, - selection: ProjectPageSelection + selection: { + projectId?: string, + versionId?: string, + specificationId?: string + } ) { + if (!selection.projectId || !selection.versionId || !selection.specificationId) { + return + } if ( - urlComponents.projectId != selection.project.id || - urlComponents.versionId != selection.version.id || - urlComponents.specificationId != selection.specification.id + urlComponents.projectId != selection.projectId || + urlComponents.versionId != selection.versionId || + urlComponents.specificationId != selection.specificationId ) { - const path = `/${selection.project.id}/${selection.version.id}/${selection.specification.id}` + const path = `/${selection.projectId}/${selection.versionId}/${selection.specificationId}` router.replace(path) } } diff --git a/src/features/projects/domain/updateWindowTitle.ts b/src/features/projects/domain/updateWindowTitle.ts index 747229bc..3f239c03 100644 --- a/src/features/projects/domain/updateWindowTitle.ts +++ b/src/features/projects/domain/updateWindowTitle.ts @@ -1,20 +1,30 @@ -import ProjectPageSelection from "./ProjectPageSelection" +import Project from "./Project" +import Version from "./Version" +import OpenApiSpecification from "./OpenApiSpecification" -export default function updateWindowTitle( +export default function updateWindowTitle({ + storage, + defaultTitle, + project, + version, + specification, +}: { storage: { title: string }, defaultTitle: string, - selection?: ProjectPageSelection -) { - if (!selection) { + project?: Project, + version?: Version, + specification?: OpenApiSpecification +}) { + if (!project || !version || !specification) { storage.title = defaultTitle return } - if (!isSpecificationNameGeneric(selection.specification.name)) { - storage.title = `${selection.project.displayName} / ${selection.version.name} / ${selection.specification.name}` - } else if (!selection.version.isDefault) { - storage.title = `${selection.project.displayName} / ${selection.version.name}` + if (!isSpecificationNameGeneric(specification.name)) { + storage.title = `${project.displayName} / ${version.name} / ${specification.name}` + } else if (!version.isDefault) { + storage.title = `${project.displayName} / ${version.name}` } else { - storage.title = selection.project.displayName + storage.title = project.displayName } } diff --git a/src/features/projects/view/MainContent.tsx b/src/features/projects/view/MainContent.tsx new file mode 100644 index 00000000..ee65cc05 --- /dev/null +++ b/src/features/projects/view/MainContent.tsx @@ -0,0 +1,35 @@ +import Project from "../domain/Project" +import Version from "../domain/Version" +import OpenApiSpecification from "../domain/OpenApiSpecification" +import ProjectErrorContent from "./ProjectErrorContent" +import DocumentationViewer from "./docs/DocumentationViewer" + +const MainContent = ({ + isLoading, + error, + project, + version, + specification +}: { + isLoading: boolean, + error?: Error, + project?: Project, + version?: Version, + specification?: OpenApiSpecification +}) => { + if (project && version && specification) { + return + } else if (isLoading) { + return <> + } else if (error) { + return + } else if (!project) { + return + } else if (!version) { + return + } else { + return + } +} + +export default MainContent \ No newline at end of file diff --git a/src/features/projects/view/ProjectsPageContent.tsx b/src/features/projects/view/ProjectsPageContent.tsx deleted file mode 100644 index 70c6f598..00000000 --- a/src/features/projects/view/ProjectsPageContent.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { ProjectPageStateContainer, ProjectPageState } from "../domain/ProjectPageState" -import ProjectErrorContent from "./ProjectErrorContent" -import DocumentationViewer from "./docs/DocumentationViewer" - -const ProjectsPageContent = ({ - stateContainer -}: { - stateContainer: ProjectPageStateContainer -}) => { - switch (stateContainer.state) { - case ProjectPageState.LOADING: - case ProjectPageState.NO_PROJECT_SELECTED: - return <> - case ProjectPageState.ERROR: - return - case ProjectPageState.HAS_SELECTION: - return - case ProjectPageState.PROJECT_NOT_FOUND: - return - case ProjectPageState.VERSION_NOT_FOUND: - return - case ProjectPageState.SPECIFICATION_NOT_FOUND: - return - } -} - -export default ProjectsPageContent \ No newline at end of file diff --git a/src/features/projects/view/client/ProjectsPage.tsx b/src/features/projects/view/client/ProjectsPage.tsx index 219f7722..62c1d332 100644 --- a/src/features/projects/view/client/ProjectsPage.tsx +++ b/src/features/projects/view/client/ProjectsPage.tsx @@ -7,10 +7,10 @@ import useMediaQuery from "@mui/material/useMediaQuery" import SidebarContainer from "@/features/sidebar/view/client/SidebarContainer" import Project from "../../domain/Project" import ProjectList from "../ProjectList" -import ProjectsPageContent from "../ProjectsPageContent" -import TrailingToolbarItem from "../TrailingToolbarItem" +import MainContent from "../MainContent" import MobileToolbar from "../MobileToolbar" -import { getProjectPageState } from "../../domain/ProjectPageState" +import TrailingToolbarItem from "../TrailingToolbarItem" +import getSelection from "../../domain/getSelection" import projectNavigator from "../../domain/projectNavigator" import updateWindowTitle from "../../domain/updateWindowTitle" import useProjects from "../../data/useProjects" @@ -32,29 +32,31 @@ export default function ProjectsPage({ const { projects: clientProjects, error, isLoading: isClientLoading } = useProjects() const [forceCloseSidebar, setForceCloseSidebar] = useState(false) const projects = isClientLoading ? (serverProjects || []) : clientProjects - const stateContainer = getProjectPageState({ - isLoading: isClientLoading, - error, + const { project, version, specification } = getSelection({ projects, - selectedProjectId: projectId, - selectedVersionId: versionId, - selectedSpecificationId: specificationId + projectId, + versionId, + specificationId }) useEffect(() => { - updateWindowTitle( - document, - process.env.NEXT_PUBLIC_SHAPE_DOCS_TITLE, - stateContainer.selection - ) - }, [stateContainer.selection]) + updateWindowTitle({ + storage: document, + defaultTitle: process.env.NEXT_PUBLIC_SHAPE_DOCS_TITLE, + project, + version, + specification + }) + }, [project, version, specification]) useEffect(() => { - if (!stateContainer.selection) { - return - } // Ensure the URL reflects the current selection of project, version, and specification. const urlSelection = { projectId, versionId, specificationId } - projectNavigator.navigateIfNeeded(router, urlSelection, stateContainer.selection) - }, [router, projectId, versionId, specificationId, stateContainer.selection]) + const selection = { + projectId: project?.id, + versionId: version?.id, + specificationId: specification?.id + } + projectNavigator.navigateIfNeeded(router, urlSelection, selection) + }, [router, projectId, versionId, specificationId, project, version, specification]) const selectProject = (project: Project) => { setForceCloseSidebar(!isDesktopLayout) const version = project.versions[0] @@ -62,14 +64,18 @@ export default function ProjectsPage({ projectNavigator.navigate(router, project.id, version.id, specification.id) } const selectVersion = (versionId: string) => { - projectNavigator.navigateToVersion(router, stateContainer.selection!, versionId) + projectNavigator.navigateToVersion(router, project!, versionId, specification!.name) } const selectSpecification = (specificationId: string) => { projectNavigator.navigate(router, projectId!, versionId!, specificationId) } return ( } - toolbarTrailingItem={stateContainer.selection && + toolbarTrailingItem={project && version && specification && } - mobileToolbar={stateContainer.selection && + mobileToolbar={project && version && specification && } > - + {/* If the user has not selected any project then we do not render any content */} + {projectId && + + } ) } From 22bdfa82c84447e6e643bd0812ec97d60ab448ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 26 Oct 2023 17:59:10 +0200 Subject: [PATCH 14/16] Fixes filename --- .../projects/{ProjectPageState.test.ts => getSelection.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename __test__/projects/{ProjectPageState.test.ts => getSelection.test.ts} (100%) diff --git a/__test__/projects/ProjectPageState.test.ts b/__test__/projects/getSelection.test.ts similarity index 100% rename from __test__/projects/ProjectPageState.test.ts rename to __test__/projects/getSelection.test.ts From dc75cfb85ab1f050850f2b42ff40e4b46ff9b758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 26 Oct 2023 18:01:16 +0200 Subject: [PATCH 15/16] Fixes layout of error message --- .../{ProjectErrorContent.tsx => ErrorMessage.tsx} | 11 ++++------- src/features/projects/view/MainContent.tsx | 10 +++++----- 2 files changed, 9 insertions(+), 12 deletions(-) rename src/features/projects/view/{ProjectErrorContent.tsx => ErrorMessage.tsx} (54%) diff --git a/src/features/projects/view/ProjectErrorContent.tsx b/src/features/projects/view/ErrorMessage.tsx similarity index 54% rename from src/features/projects/view/ProjectErrorContent.tsx rename to src/features/projects/view/ErrorMessage.tsx index 1228c830..8f4a6510 100644 --- a/src/features/projects/view/ProjectErrorContent.tsx +++ b/src/features/projects/view/ErrorMessage.tsx @@ -1,16 +1,13 @@ import { Box, Typography } from "@mui/material" -const ProjectErrorContent = ({ text }: { text: string }) => { +const ErrorMessage = ({ text }: { text: string }) => { return ( {text} @@ -19,4 +16,4 @@ const ProjectErrorContent = ({ text }: { text: string }) => { ) } -export default ProjectErrorContent +export default ErrorMessage diff --git a/src/features/projects/view/MainContent.tsx b/src/features/projects/view/MainContent.tsx index ee65cc05..67d7070f 100644 --- a/src/features/projects/view/MainContent.tsx +++ b/src/features/projects/view/MainContent.tsx @@ -1,7 +1,7 @@ import Project from "../domain/Project" import Version from "../domain/Version" import OpenApiSpecification from "../domain/OpenApiSpecification" -import ProjectErrorContent from "./ProjectErrorContent" +import ErrorMessage from "./ErrorMessage" import DocumentationViewer from "./docs/DocumentationViewer" const MainContent = ({ @@ -22,13 +22,13 @@ const MainContent = ({ } else if (isLoading) { return <> } else if (error) { - return + return } else if (!project) { - return + return } else if (!version) { - return + return } else { - return + return } } From 1d65ca338d12321df27b1bce466221c877b56230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 26 Oct 2023 18:11:30 +0200 Subject: [PATCH 16/16] Shows loading indicator --- src/features/projects/view/DelayedLoading.tsx | 30 +++++++++++++++++++ src/features/projects/view/MainContent.tsx | 3 +- 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 src/features/projects/view/DelayedLoading.tsx diff --git a/src/features/projects/view/DelayedLoading.tsx b/src/features/projects/view/DelayedLoading.tsx new file mode 100644 index 00000000..098b3c84 --- /dev/null +++ b/src/features/projects/view/DelayedLoading.tsx @@ -0,0 +1,30 @@ +import { useState, useEffect } from "react" +import { Box, CircularProgress } from "@mui/material" + +const DelayedLoading = () => { + const [isVisible, setVisible] = useState(false) + useEffect(() => { + const timer = setTimeout(() => { + setVisible(true) + }, 1000) + return () => clearTimeout(timer) + }, [setVisible]) + return ( + + {isVisible && + + } + + ) +} + +export default DelayedLoading diff --git a/src/features/projects/view/MainContent.tsx b/src/features/projects/view/MainContent.tsx index 67d7070f..cea689a8 100644 --- a/src/features/projects/view/MainContent.tsx +++ b/src/features/projects/view/MainContent.tsx @@ -1,6 +1,7 @@ import Project from "../domain/Project" import Version from "../domain/Version" import OpenApiSpecification from "../domain/OpenApiSpecification" +import DelayedLoading from "./DelayedLoading" import ErrorMessage from "./ErrorMessage" import DocumentationViewer from "./docs/DocumentationViewer" @@ -20,7 +21,7 @@ const MainContent = ({ if (project && version && specification) { return } else if (isLoading) { - return <> + return } else if (error) { return } else if (!project) {