Skip to content

Commit 8f1f2ca

Browse files
committed
FLS-1450: Add Pre-Award API integration to Form Designer
- Add PreAwardApiClient for centralized form storage - Replace file-based persistence with direct API calls - Update form selection to edit directly instead of cloning - Maintain preview functionality via runner publishing - Remove feature flag complexity for simplified architecture - Add comprehensive test suite with TypeScript support
1 parent 54256c8 commit 8f1f2ca

File tree

11 files changed

+463
-26
lines changed

11 files changed

+463
-26
lines changed

designer/client/pages/landing-page/ViewFundForms.tsx

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type Props = {
1313
type State = {
1414
configs: { Key: string; DisplayName: string }[];
1515
loading?: boolean;
16+
1617
};
1718

1819
export class ViewFundForms extends Component<Props, State> {
@@ -25,30 +26,23 @@ export class ViewFundForms extends Component<Props, State> {
2526
};
2627
}
2728

28-
componentDidMount() {
29-
formConfigurationApi.loadConfigurations().then((configs) => {
29+
async componentDidMount() {
30+
try {
31+
const configs = await formConfigurationApi.loadConfigurations();
3032
this.setState({
3133
loading: false,
3234
configs,
3335
});
34-
});
36+
} catch (error) {
37+
logger.error("ViewFundForms componentDidMount", error);
38+
this.setState({ loading: false });
39+
}
3540
}
3641

3742
selectForm = async (form) => {
3843
try {
39-
const response = await window.fetch("/api/new", {
40-
method: "POST",
41-
body: JSON.stringify({
42-
selected: {Key: form},
43-
name: "",
44-
}),
45-
headers: {
46-
Accept: "application/json",
47-
"Content-Type": "application/json",
48-
},
49-
});
50-
const responseJson = await response.json();
51-
this.props.history.push(`/designer/${responseJson.id}`);
44+
// Always go directly to edit the form from Pre-Award API
45+
this.props.history.push(`/designer/${form}`);
5246
} catch (e) {
5347
logger.error("ChooseExisting", e);
5448
}

designer/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
"static-content:dist-push": "cp -r ../designer/public/static/images dist/client/assets && cp -r ../designer/public/static/css dist/client/assets",
1111
"build": "NODE_ENV=production && NODE_OPTIONS=--openssl-legacy-provider && webpack && yarn run static-content:dist-push",
1212
"start:server": "node dist/server.js",
13-
"start:local": "NODE_ENV=development PERSISTENT_BACKEND=preview ts-node-dev --inspect=0.0.0.0:9229 --respawn --transpile-only server/index.ts"
13+
"start:local": "NODE_ENV=development PERSISTENT_BACKEND=preview ts-node-dev --inspect=0.0.0.0:9229 --respawn --transpile-only server/index.ts",
14+
"test": "lab test/cases/server/plugins/*.test.ts -T test/.transform.js -v",
15+
"unit-test": "lab test/cases/server/plugins/*.test.ts -T test/.transform.js -v -S -r console -o stdout -r html -o unit-test.html -I version -l"
1416
},
1517
"author": "Communities UK",
1618
"license": "SEE LICENSE IN LICENSE",

designer/server/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export interface Config {
3131
authCookieName: string,
3232
sslKey: string,
3333
sslCert: string,
34+
preAwardApiUrl: string,
3435
}
3536

3637
// server-side storage expiration - defaults to 20 minutes
@@ -65,6 +66,7 @@ const schema = joi.object({
6566
authCookieName: joi.string().optional(),
6667
sslKey: joi.string().optional(),
6768
sslCert: joi.string().optional(),
69+
preAwardApiUrl: joi.string().default("https://api.communities.gov.localhost:4004"),
6870
});
6971

7072
// Build config
@@ -90,6 +92,7 @@ const config = {
9092
authCookieName: process.env.AUTH_COOKIE_NAME,
9193
sslKey: process.env.SSL_KEY,
9294
sslCert: process.env.SSL_CERT,
95+
preAwardApiUrl: process.env.PRE_AWARD_API_URL || "https://api.communities.gov.localhost:4004",
9396
};
9497

9598
// Validate config
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import config from "../config";
2+
import * as Wreck from "@hapi/wreck";
3+
4+
interface FormJson {
5+
startPage: string;
6+
pages: any[];
7+
sections: any[];
8+
name: string;
9+
version?: number;
10+
conditions?: any[];
11+
lists?: any[];
12+
metadata?: any;
13+
fees?: any[];
14+
outputs?: any[];
15+
skipSummary?: boolean;
16+
[key: string]: any;
17+
}
18+
19+
export interface FormData {
20+
name: string;
21+
form_json: FormJson;
22+
}
23+
24+
export interface FormResponse {
25+
id: string;
26+
name: string;
27+
created_at: string;
28+
updated_at: string;
29+
published_at: string | null;
30+
draft_json: FormJson;
31+
published_json: FormJson;
32+
is_published: boolean;
33+
}
34+
35+
export class PreAwardApiClient {
36+
private baseUrl: string;
37+
38+
constructor() {
39+
this.baseUrl = config.preAwardApiUrl;
40+
}
41+
42+
async createOrUpdateForm(formData: FormData): Promise<FormResponse>{
43+
const payload = formData;
44+
45+
try{
46+
const { payload: responseData } = await Wreck.post(
47+
`${this.baseUrl}/forms`,
48+
{
49+
payload: JSON.stringify(payload),
50+
headers: {
51+
'Content-Type': 'application/json'
52+
}
53+
}
54+
);
55+
const parsedData = JSON.parse((responseData as Buffer).toString());
56+
return parsedData as FormResponse;
57+
}
58+
catch (error) {
59+
throw error;
60+
}
61+
}
62+
63+
async getAllForms(): Promise<FormResponse[]> {
64+
try {
65+
const { payload: responseData } = await Wreck.get(
66+
`${this.baseUrl}/forms`,
67+
{
68+
headers: {
69+
'Content-Type': 'application/json'
70+
}
71+
}
72+
);
73+
74+
const parsedData = JSON.parse((responseData as Buffer).toString());
75+
return parsedData as FormResponse[];
76+
} catch (error) {
77+
throw error;
78+
}
79+
}
80+
81+
async getFormDraft(name: string): Promise<FormJson>{
82+
try{
83+
const { payload: responseData } = await Wreck.get(
84+
`${this.baseUrl}/forms/${name}/draft`,
85+
{
86+
headers: {
87+
'Content-Type': 'application/json'
88+
}
89+
}
90+
);
91+
const parsedData = JSON.parse((responseData as Buffer).toString());
92+
return parsedData as FormJson;
93+
}
94+
catch (error) {
95+
throw error;
96+
}
97+
}
98+
}
99+
100+
export const preAwardApiClient = new PreAwardApiClient();

designer/server/plugins/DesignerRouteRegister.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import {newConfig, api, app} from "../../../digital-form-builder/designer/server/plugins/routes";
1+
import {app} from "../../../digital-form-builder/designer/server/plugins/routes";
22
import {envStore, flagg} from "flagg";
33
import {putFormWithIdRouteRegister} from "./routes/PutFormWithIdRouteRegister";
4+
import {registerNewFormWithRunner} from "./routes/newConfig";
5+
import {getFormWithId, getAllPersistedConfigurations, log} from "./routes/api";
46
import config from "../config";
57
import {jwtAuthStrategyName} from "./AuthPlugin";
68

@@ -83,15 +85,15 @@ export const designerPlugin = {
8385
// @ts-ignore
8486
app.redirectOldUrlToDesigner.options.auth = jwtAuthStrategyName
8587
// @ts-ignore
86-
newConfig.registerNewFormWithRunner.options.auth = jwtAuthStrategyName
88+
registerNewFormWithRunner.options.auth = jwtAuthStrategyName
8789
// @ts-ignore
88-
api.getFormWithId.options.auth = jwtAuthStrategyName
90+
getFormWithId.options.auth = jwtAuthStrategyName
8991
// @ts-ignore
9092
putFormWithIdRouteRegister.options.auth = jwtAuthStrategyName
9193
// @ts-ignore
92-
api.getAllPersistedConfigurations.options.auth = jwtAuthStrategyName
94+
getAllPersistedConfigurations.options.auth = jwtAuthStrategyName
9395
// @ts-ignore
94-
api.log.options.auth = jwtAuthStrategyName
96+
log.options.auth = jwtAuthStrategyName
9597
}
9698

9799
server.route(startRoute);
@@ -118,6 +120,7 @@ export const designerPlugin = {
118120
store: envStore(process.env),
119121
definitions: {
120122
featureEditPageDuplicateButton: {default: false},
123+
121124
},
122125
});
123126

@@ -128,11 +131,11 @@ export const designerPlugin = {
128131
},
129132
});
130133

131-
server.route(newConfig.registerNewFormWithRunner);
132-
server.route(api.getFormWithId);
134+
server.route(registerNewFormWithRunner);
135+
server.route(getFormWithId);
133136
server.route(putFormWithIdRouteRegister);
134-
server.route(api.getAllPersistedConfigurations);
135-
server.route(api.log);
137+
server.route(getAllPersistedConfigurations);
138+
server.route(log);
136139
},
137140
},
138141
};

designer/server/plugins/routes/PutFormWithIdRouteRegister.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {ServerRoute} from "@hapi/hapi";
22
import {AdapterSchema} from "@communitiesuk/model";
33
import {publish} from "../../../../digital-form-builder/designer/server/lib/publish";
4+
import {preAwardApiClient} from "../../lib/preAwardApiClient";
5+
import config from "../../config";
46

57

68
export const putFormWithIdRouteRegister: ServerRoute = {
@@ -31,6 +33,10 @@ export const putFormWithIdRouteRegister: ServerRoute = {
3133
`${id}`,
3234
JSON.stringify(value)
3335
);
36+
// Save to Pre-Award API
37+
const formData = { name: id, form_json: value };
38+
await preAwardApiClient.createOrUpdateForm(formData);
39+
// Publish to runner for preview
3440
await publish(id, value);
3541
return h.response({ok: true}).code(204);
3642
} catch (err) {
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { api as originalApi } from "../../../../digital-form-builder/designer/server/plugins/routes";
2+
import { preAwardApiClient } from "../../lib/preAwardApiClient";
3+
import config from "../../config";
4+
import { ServerRoute, ResponseObject } from "@hapi/hapi";
5+
6+
// Extend the original getFormWithId with Pre-Award API support
7+
export const getFormWithId: ServerRoute = {
8+
...originalApi.getFormWithId,
9+
options: {
10+
...originalApi.getFormWithId.options || {},
11+
handler: async (request, h) => {
12+
const { id } = request.params;
13+
const formJson = await preAwardApiClient.getFormDraft(id);
14+
return h.response(formJson).type("application/json");
15+
},
16+
},
17+
};
18+
19+
// Extend the original putFormWithId with Pre-Award API support
20+
export const putFormWithId: ServerRoute = {
21+
...originalApi.putFormWithId,
22+
options: {
23+
...originalApi.putFormWithId.options || {},
24+
handler: async (request, h) => {
25+
const { id } = request.params;
26+
const { Schema } = await import("../../../../digital-form-builder/model/src");
27+
const { value, error } = Schema.validate(request.payload, {
28+
abortEarly: false,
29+
});
30+
31+
if (error) {
32+
throw new Error("Schema validation failed, reason: " + error.message);
33+
}
34+
const formData = { name: id, form_json: value };
35+
await preAwardApiClient.createOrUpdateForm(formData);
36+
37+
return h.response({ ok: true }).code(204);
38+
},
39+
},
40+
};
41+
42+
// Extend the original getAllPersistedConfigurations with Pre-Award API support
43+
export const getAllPersistedConfigurations: ServerRoute = {
44+
...originalApi.getAllPersistedConfigurations,
45+
options: {
46+
...originalApi.getAllPersistedConfigurations.options || {},
47+
handler: async (request, h): Promise<ResponseObject | undefined> => {
48+
const forms = await preAwardApiClient.getAllForms();
49+
const response = forms.map(form => ({
50+
Key: form.name,
51+
DisplayName: form.name,
52+
LastModified: form.updated_at
53+
}));
54+
return h.response(response).type("application/json");
55+
},
56+
},
57+
};
58+
59+
// Use original log route as-is
60+
export const log = originalApi.log;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { newConfig as originalNewConfig } from "../../../../digital-form-builder/designer/server/plugins/routes";
2+
import { preAwardApiClient } from "../../lib/preAwardApiClient";
3+
import config from "../../config";
4+
import { ServerRoute } from "@hapi/hapi";
5+
import { HapiRequest } from "../../../../digital-form-builder/designer/server/types";
6+
import { nanoid } from "nanoid";
7+
import newFormJson from "../../../../digital-form-builder/designer/new-form.json";
8+
9+
// Extend the original registerNewFormWithRunner with Pre-Award API support
10+
export const registerNewFormWithRunner: ServerRoute = {
11+
...originalNewConfig.registerNewFormWithRunner,
12+
options: {
13+
...originalNewConfig.registerNewFormWithRunner.options,
14+
handler: async (request: HapiRequest, h) => {
15+
const { selected, name } = request.payload as any;
16+
17+
if (name && name !== "" && !name.match(/^[a-zA-Z0-9 _-]+$/)) {
18+
return h
19+
.response("Form name should not contain special characters")
20+
.type("application/json")
21+
.code(400);
22+
}
23+
24+
const newName = name === "" ? nanoid(10) : name;
25+
26+
if (selected.Key === "New") {
27+
const formData = { name: newName, form_json: newFormJson };
28+
await preAwardApiClient.createOrUpdateForm(formData);
29+
} else {
30+
const existingForm = await preAwardApiClient.getFormDraft(selected.Key);
31+
const formData = { name: newName, form_json: existingForm };
32+
await preAwardApiClient.createOrUpdateForm(formData);
33+
}
34+
35+
const response = JSON.stringify({
36+
id: `${newName}`,
37+
previewUrl: config.previewUrl,
38+
});
39+
return h.response(response).type("application/json").code(200);
40+
},
41+
},
42+
};

designer/test/.transform.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const Babel = require("@babel/core");
2+
3+
module.exports = [
4+
{
5+
ext: ".ts",
6+
transform: (content, filename) => {
7+
const result = Babel.transformSync(content, {
8+
filename,
9+
presets: [
10+
["@babel/preset-env", { targets: { node: "current" } }],
11+
"@babel/preset-typescript"
12+
]
13+
});
14+
return result.code;
15+
}
16+
}
17+
];

0 commit comments

Comments
 (0)