Skip to content

Commit

Permalink
Move metadata request into app pinia store
Browse files Browse the repository at this point in the history
  • Loading branch information
david-mears-2 committed Sep 10, 2024
1 parent a76a480 commit 385cccd
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 79 deletions.
42 changes: 18 additions & 24 deletions components/ParameterForm.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<template>
<div>
<CForm
v-if="props.metadata && formData"
v-if="appStore.metadata && formData"
class="inputs"
role="form"
:data-test-navigate-to="navigateToData"
@submit.prevent="submitForm"
>
<div
v-for="(parameter) in props.metadata.parameters"
v-for="(parameter) in appStore.metadata.parameters"
:key="parameter.id"
class="field-container"
>
Expand Down Expand Up @@ -76,28 +76,25 @@
<CIcon v-else icon="cilArrowRight" />
</CButton>
</CForm>
<CAlert v-else-if="props.metadataFetchStatus === 'error'" color="warning">
Failed to retrieve metadata from R API. {{ metadataFetchError }}
<CAlert v-else-if="appStore.metadataFetchStatus === 'error'" color="warning">
Failed to retrieve metadata from R API. {{ appStore.metadataFetchError }}
</CAlert>
<CSpinner v-else-if="props.metadataFetchStatus === 'pending'" />
<CSpinner v-else-if="appStore.metadataFetchStatus === 'pending'" />
</div>
</template>

<script lang="ts" setup>
import type { FetchError } from "ofetch";
import { CIcon } from "@coreui/icons-vue";
import { ParameterType } from "@/types/apiResponseTypes";
import type { Metadata, NewScenarioData, Parameter } from "@/types/apiResponseTypes";
import type { AsyncDataRequestStatus } from "#app";
import type { Parameter } from "@/types/parameterTypes";
import { TypeOfParameter } from "@/types/parameterTypes";
import type { NewScenarioData } from "@/types/apiResponseTypes";
const props = defineProps<{
metadata: Metadata | undefined
metadataFetchStatus: AsyncDataRequestStatus
metadataFetchError: FetchError | null
}>();
const appStore = useAppStore();
const navigateToData = ref("");
// This is only a temporary piece of code, used until we implement numeric inputs.
const allParametersOfImplementedTypes = computed(() => props.metadata?.parameters.filter(({ parameterType }) => parameterType !== ParameterType.Numeric));
const allParametersOfImplementedTypes = computed(() => appStore.metadata?.parameters.filter(({ parameterType }) => parameterType !== TypeOfParameter.Numeric));
const formData = ref(
// Create a new object with keys set to the id values of the metadata.parameters array of objects, and all values set to default values.
Expand All @@ -107,23 +104,20 @@ const formData = ref(
}, {} as { [key: string]: string | number }),
);
const appStore = useAppStore();
const navigateToData = ref("");
const optionsAreTerse = (parameter: Parameter) => {
const eachOptionIsASingleWord = parameter.options.every((option) => {
const optionsAreTerse = (param: Parameter) => {
const eachOptionIsASingleWord = param.options.every((option) => {
return !option.label.includes(" ");
});
return parameter.options.length <= 5 && eachOptionIsASingleWord;
return param.options.length <= 5 && eachOptionIsASingleWord;
};
const renderAsSelect = (parameter: Parameter) => {
return parameter.parameterType === ParameterType.Select || parameter.parameterType === ParameterType.GlobeSelect;
const renderAsRadios = (param: Parameter) => {
return param.parameterType === TypeOfParameter.Select && optionsAreTerse(param);
};
const renderAsRadios = (parameter: Parameter) => {
return parameter.parameterType === ParameterType.Select && optionsAreTerse(parameter);
const renderAsSelect = (param: Parameter) => {
return !renderAsRadios(param) && [TypeOfParameter.Select, TypeOfParameter.GlobeSelect].includes(param.parameterType);
};
const formSubmitting = ref(false);
Expand Down
2 changes: 2 additions & 0 deletions layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ const setScreenSize = () => {
}
};
appStore.loadMetadata();
onMounted(() => {
setScreenSize();
appStore.loadVersionData();
Expand Down
25 changes: 1 addition & 24 deletions pages/scenarios/new.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,12 @@
<div class="overlay">
<h3>Simulate a new scenario</h3>
<p>Select the parameters for your next scenario.</p>
<ParameterForm
:metadata="metadata"
:metadata-fetch-status="metadataFetchStatus"
:metadata-fetch-error="metadataFetchError"
/>
<ParameterForm />
</div>
<p>{{ globeParameter?.id }} globe select to go here in future PR</p>
</div>
</template>

<script lang="ts" setup>
import type { FetchError } from "ofetch";
import type { AsyncDataRequestStatus } from "#app";
import { type Metadata, ParameterType } from "@/types/apiResponseTypes";
const { data: metadata, status: metadataFetchStatus, error: metadataFetchError } = useFetch("/api/metadata") as {
data: Ref<Metadata>
status: Ref<AsyncDataRequestStatus>
error: Ref<FetchError | null>
};
const globeParameter = computed(() => {
if (metadata.value) {
return metadata.value.parameters.filter(parameter => parameter.parameterType === ParameterType.GlobeSelect)[0];
} else {
return undefined;
}
});
definePageMeta({
hideBreadcrumbs: true,
});
Expand Down
24 changes: 21 additions & 3 deletions stores/appStore.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,38 @@
import { defineStore } from "pinia";
import type { FetchError } from "ofetch";
import type { AsyncDataRequestStatus } from "#app";
import type { AppState } from "@/types/storeTypes";
import type { VersionData } from "@/types/apiResponseTypes";
import type { Metadata, VersionData } from "@/types/apiResponseTypes";
import { TypeOfParameter } from "@/types/parameterTypes";

export const useAppStore = defineStore("app", {
state: (): AppState => ({
largeScreen: true,
versions: undefined,
metadata: undefined,
metadataFetchError: undefined,
metadataFetchStatus: undefined,
}),
getters: {
globeParameter: state => state.metadata?.parameters.find(param => param.parameterType === TypeOfParameter.GlobeSelect),
},
actions: {
async loadMetadata() {
const { data: metadata, status: metadataFetchStatus, error: metadataFetchError } = await useFetch("/api/metadata") as {
data: Ref<Metadata>
status: Ref<AsyncDataRequestStatus>
error: Ref<FetchError | undefined>
};
this.metadata = metadata.value;
this.metadataFetchStatus = metadataFetchStatus.value;
this.metadataFetchError = metadataFetchError.value;
},
// This function is designed to be called e.g. in onMounted lifecycle hooks, not
// setup scripts (hence the use of $fetch rather than useFetch), since version data
// does not need to be immediately available as soon as the page loads. We therefore
// don't need to make the render of the page wait for this data to be fetched.
// Just in case this mission-non-critical fetch takes a long time.
loadVersionData(): void {
// No need for 'await' here - would needlessly block other execution.
// Just in case this mission-non-critical fetch takes a long time.
$fetch("/api/versions", {
onResponse: ({ response }) => {
const data = response._data;
Expand Down
39 changes: 30 additions & 9 deletions tests/unit/components/ParameterForm.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
import { mountSuspended, registerEndpoint } from "@nuxt/test-utils/runtime";
import { FetchError } from "ofetch";
import { flushPromises } from "@vue/test-utils";

import { mockPinia } from "@/tests/unit/mocks/mockPinia";
import { formDataToObject } from "@/server/utils/helpers";
import type { Metadata } from "@/types/apiResponseTypes";
import ParameterForm from "@/components/ParameterForm.vue";
Expand Down Expand Up @@ -57,8 +57,13 @@ const metadata = { modelVersion: "0.0.0", parameters: [...selectParameters, glob
describe("parameter form", () => {
it("renders the correct parameter labels, inputs, options, and default values", async () => {
const component = await mountSuspended(ParameterForm, {
props: { metadata, metadataFetchStatus: "success", metadataFetchError: null },
global: { stubs },
global: {
stubs,
plugins: [mockPinia({
metadata,
metadataFetchStatus: "success",
})],
},
});

expect(component.text()).toContain("Region");
Expand Down Expand Up @@ -121,8 +126,13 @@ describe("parameter form", () => {
});

const component = await mountSuspended(ParameterForm, {
props: { metadata, metadataFetchStatus: "success", metadataFetchError: null },
global: { stubs },
global: {
stubs,
plugins: [mockPinia({
metadata,
metadataFetchStatus: "success",
})],
},
});

const buttonEl = component.find("button[type='submit']");
Expand All @@ -143,8 +153,14 @@ describe("parameter form", () => {
const error = new FetchError("There was a bee-related issue.");

const component = await mountSuspended(ParameterForm, {
props: { metadata: undefined, metadataFetchStatus: "error", metadataFetchError: error },
global: { stubs },
global: {
stubs,
plugins: [mockPinia({
metadata: undefined,
metadataFetchStatus: "error",
metadataFetchError: error,
})],
},
});

expect(component.findComponent({ name: "CAlert" }).exists()).toBe(true);
Expand All @@ -154,8 +170,13 @@ describe("parameter form", () => {

it("displays CSpinner when metadataFetchStatus is 'pending'", async () => {
const component = await mountSuspended(ParameterForm, {
props: { metadata: undefined, metadataFetchStatus: "pending", metadataFetchError: null },
global: { stubs },
global: {
stubs,
plugins: [mockPinia({
metadata: undefined,
metadataFetchStatus: "pending",
})],
},
});

expect(component.findComponent({ name: "CSpinner" }).exists()).toBe(true);
Expand Down
1 change: 0 additions & 1 deletion tests/unit/components/defaultLayout.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { mountSuspended, registerEndpoint } from "@nuxt/test-utils/runtime";
import { waitFor } from "@testing-library/vue";

import DefaultLayout from "@/layouts/default.vue";

const stubs = {
Expand Down
4 changes: 4 additions & 0 deletions tests/unit/mocks/mockPinia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ export const mockPinia = (appState: Partial<AppState> = {}) => {
const initialState = {
app: {
largeScreen: true,
versions: undefined,
metadata: undefined,
metadataFetchError: undefined,
metadataFetchStatus: undefined,
...appState,
},
};
Expand Down
46 changes: 45 additions & 1 deletion tests/unit/stores/appStore.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,33 @@ describe("app store", () => {
};
});

registerEndpoint("/api/metadata", () => {
return {
parameters: [
{ id: "country", parameterType: "globeSelect" },
{ id: "settings", parameterType: "select" },
],
results: {
costs: [
{ id: "total", label: "Total" },
],
},
modelVersion: "1.2.3",
};
});

describe("actions", () => {
it("initialises correctly", async () => {
const store = useAppStore();
expect(store.versions).toBeUndefined();
expect(store.metadata).toBeUndefined();
expect(store.largeScreen).toBe(true);
});

it("can retrieve the version numbers", async () => {
const store = useAppStore();
store.loadVersionData();

// The fetch should eventually complete, and the version numbers should be updated.
await waitFor(() => {
expect(store.versions).toEqual({
daedalusModel: "1.2.3",
Expand All @@ -37,5 +52,34 @@ describe("app store", () => {
});
});
});

it("can retrieve the metadata", async () => {
const store = useAppStore();
await store.loadMetadata();

await waitFor(() => {
expect(store.metadata).toEqual({
parameters: [
{ id: "country", parameterType: "globeSelect" },
{ id: "settings", parameterType: "select" },
],
results: {
costs: [
{ id: "total", label: "Total" },
],
},
modelVersion: "1.2.3",
});
});
});

it("can get the globe parameter", async () => {
const store = useAppStore();
await store.loadMetadata();

await waitFor(() => {
expect(store.globeParameter).toEqual({ id: "country", parameterType: "globeSelect" });
});
});
});
});
21 changes: 5 additions & 16 deletions types/apiResponseTypes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Types for responses from our API endpoints.
import type { Parameter } from "./parameterTypes";

export interface ApiError {
error: string
Expand All @@ -21,27 +22,15 @@ export interface VersionData {
export interface VersionDataResponse extends ApiResponse<VersionData> { }

// Metadata
export interface ParameterOption {
id: string
interface DisplayInfo {
label: string
}

export enum ParameterType {
Select = "select",
GlobeSelect = "globeSelect",
Numeric = "numeric",
}
export interface Parameter {
id: string
label: string
parameterType: ParameterType
defaultOption: string | null
ordered: boolean
options: Array<ParameterOption>
value: number
description: string | null
}
export interface Metadata {
modelVersion: string
parameters: Array<Parameter>
results: Record<string, Array<DisplayInfo>>
}

export interface MetadataResponse extends ApiResponse<Metadata> { }
Expand Down
Loading

0 comments on commit 385cccd

Please sign in to comment.