diff --git a/.vscode/settings.json b/.vscode/settings.json index 99f6e23..1fe5207 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,5 +9,8 @@ "[markdown]": { "editor.defaultFormatter": "DavidAnson.vscode-markdownlint" }, - "git.branchProtection": ["main"] + "git.branchProtection": ["main"], + "[nunjucks]": { + "editor.defaultFormatter": "okitavera.vscode-nunjucks-formatter" + } } diff --git a/src/routes/__test__/prototype-routes.test.ts b/src/routes/__test__/prototype-routes.test.ts index 4dcd40f..137741a 100644 --- a/src/routes/__test__/prototype-routes.test.ts +++ b/src/routes/__test__/prototype-routes.test.ts @@ -1299,7 +1299,7 @@ describe('renderResultsPage', () => { await renderResultsPage(request, response); expect(response.statusCode).toBe(200); - expect(response._getRenderView()).toBe('results.njk'); + expect(response._getRenderView()).toBe('results/results.njk'); const data = response._getRenderData() as ResultsTemplatePayload; expect(data.enableSuggestions).toBe(true); expect(data.isOwner).toBe(true); @@ -1320,7 +1320,7 @@ describe('renderResultsPage', () => { await renderResultsPage(request, response); expect(response.statusCode).toBe(200); - expect(response._getRenderView()).toBe('results.njk'); + expect(response._getRenderView()).toBe('results/results.njk'); const data = response._getRenderData() as ResultsTemplatePayload; expect(data.allUsers).toEqual([]); expect(data.allWorkspaces).toHaveLength(1); diff --git a/src/routes/presenters/history-page.presenter.ts b/src/routes/presenters/history-page.presenter.ts new file mode 100644 index 0000000..94b2ff9 --- /dev/null +++ b/src/routes/presenters/history-page.presenter.ts @@ -0,0 +1,133 @@ +export interface HistoryPageVM { + filterCreatedByItems: { selected: boolean; text: string; value: string }[]; + filterOnlyCreatedItems: { + selected: boolean; + text: string; + value: string; + }[]; + + filterSharingItems: { selected: boolean; text: string; value: string }[]; + hasPrototypes: boolean; + paginationItems?: object[]; + paginationNextHref?: string; + + paginationPreviousHref?: string; + + perPageItems: { checked: boolean; text: string; value: string }[]; + showPagination: boolean; + summaryText: string; + workspaceItems: { selected: boolean; text: string; value: string }[]; +} + +export function buildHistoryPageVM(input: { + countPrototypes: number; + createdBy: string; + onlyCreated: boolean; + paginationItems?: object[]; + paginationNextHref?: string; + paginationPreviousHref?: string; + perPage: number; + sharing: string; + totalPrototypes: number; + workspaceItems: { selected: boolean; text: string; value: string }[]; +}): HistoryPageVM { + const { + countPrototypes, + createdBy, + onlyCreated, + paginationItems, + paginationNextHref, + paginationPreviousHref, + perPage, + sharing, + totalPrototypes, + workspaceItems, + } = input; + + // Build summary text + let summaryText = ''; + if (totalPrototypes === 0) { + summaryText = 'There are no prototypes.'; + } else if (totalPrototypes === countPrototypes) { + summaryText = `Showing all ${String(totalPrototypes)} prototype${totalPrototypes === 1 ? '' : 's'}.`; + } else { + summaryText = `Showing ${String(countPrototypes)} prototype${countPrototypes === 1 ? '' : 's'} out of ${String(totalPrototypes)}.`; + } + + return { + filterCreatedByItems: [ + { + selected: createdBy === 'anyone', + text: 'Created by anyone', + value: 'anyone', + }, + { + selected: createdBy === 'self', + text: 'Created by you', + value: 'self', + }, + { + selected: createdBy === 'others', + text: 'Created by others', + value: 'others', + }, + ], + filterOnlyCreatedItems: [ + { + selected: !onlyCreated, + text: 'All prototypes', + value: 'false', + }, + { + selected: onlyCreated, + text: 'Exclude updates to existing prototypes', + value: 'true', + }, + ], + + filterSharingItems: [ + { + selected: sharing === 'all', + text: 'All prototypes', + value: 'all', + }, + { + selected: sharing === 'public', + text: 'Prototypes shared publicly', + value: 'public', + }, + { + selected: sharing === 'users', + text: 'Prototypes shared with specific users', + value: 'users', + }, + { + selected: sharing === 'workspace', + text: 'Prototypes within a shared workspace', + value: 'workspace', + }, + { + selected: sharing === 'private', + text: 'Prototypes that are not shared', + value: 'private', + }, + ], + + hasPrototypes: totalPrototypes > 0, + + paginationItems, + + paginationNextHref, + + paginationPreviousHref, + + perPageItems: [10, 25, 50].map((n) => ({ + checked: perPage === n, + text: String(n), + value: String(n), + })), + showPagination: !!paginationItems?.length, + summaryText, + workspaceItems, + }; +} diff --git a/src/routes/presenters/results-page-history.presenter.ts b/src/routes/presenters/results-page-history.presenter.ts new file mode 100644 index 0000000..daed4dd --- /dev/null +++ b/src/routes/presenters/results-page-history.presenter.ts @@ -0,0 +1,76 @@ +import moment from 'moment'; + +import type { IPrototypeData } from '../../types/schemas/prototype-schema'; +import type { IUser } from '../../types/schemas/user-schema'; + +export interface HistoryRowVM { + html: string; +} + +export interface HistoryVM { + additionalCount: number; + additionalLabel: string; + hasMultiple: boolean; + pluralSuffix: string; + rows: HistoryRowVM[][]; + totalCount: number; +} + +export async function buildHistoryVM( + current: IPrototypeData, + previous: IPrototypeData[], + totalCount: number, + getUserById: (id: string) => Promise, + viewingUserId: string +): Promise { + async function byAndWhen(userId: string, timestamp: Date): Promise { + const creator = + userId === viewingUserId + ? 'you' + : ((await getUserById(userId))?.name ?? 'an unknown user'); + + return `${creator.replace(/\s/g, ' ')} ${moment(timestamp) + .fromNow() + .replace(/\s/g, ' ')}`; + } + + const rows: HistoryRowVM[][] = [ + [ + { + html: `${current.changesMade} by ${await byAndWhen( + current.creatorUserId, + current.createdAt + )} (this version).`, + }, + ], + ]; + + for (const pv of previous) { + rows.push([ + { + html: `${pv.changesMade} by ${await byAndWhen( + pv.creatorUserId, + pv.createdAt + )}.`, + }, + ]); + } + + const additionalCount = totalCount - previous.length; + + const pluralSuffix = additionalCount === 1 ? '' : 's'; + + const additionalLabel = + additionalCount > 0 + ? `Plus ${String(additionalCount)} more previous version${pluralSuffix} (total ${String(totalCount)}).` + : ''; + + return { + additionalCount, + additionalLabel, + hasMultiple: rows.length > 1, + pluralSuffix, + rows, + totalCount, + }; +} diff --git a/src/routes/presenters/results-page-overview.presenter.ts b/src/routes/presenters/results-page-overview.presenter.ts new file mode 100644 index 0000000..697ed5e --- /dev/null +++ b/src/routes/presenters/results-page-overview.presenter.ts @@ -0,0 +1,51 @@ +import { ITemplateData } from '../../types/schemas/prototype-schema'; + +export interface OverviewVM { + explanation?: string; + hasSuggestions: boolean; + noSuggestions: boolean; + + promptType: 'json' | 'text'; + showJsonEditor: boolean; + suggestions: SuggestionVM[]; + + switchPromptButtonText: string; +} + +export interface SuggestionVM { + text: string; + visible: boolean; +} + +export function buildOverviewVM( + json: ITemplateData, + generatedFrom: 'json' | 'text', + enableSuggestions: boolean +): OverviewVM { + const promptType = generatedFrom === 'json' ? 'json' : 'text'; + + const showJsonEditor = promptType === 'json'; + + const switchPromptButtonText = + promptType === 'json' ? 'Switch to text' : 'Switch to JSON'; + + const rawSuggestions = json.suggestions ?? []; + const suggestions: SuggestionVM[] = enableSuggestions + ? rawSuggestions.slice(0, 3).map((s) => ({ + text: s, + visible: true, + })) + : []; + + return { + explanation: promptType === 'text' ? json.explanation : undefined, + hasSuggestions: suggestions.length > 0, + noSuggestions: suggestions.length === 0, + + promptType, + showJsonEditor, + suggestions, + + switchPromptButtonText, + }; +} diff --git a/src/routes/presenters/results-page-sharing.presenter.ts b/src/routes/presenters/results-page-sharing.presenter.ts new file mode 100644 index 0000000..c96fc6b --- /dev/null +++ b/src/routes/presenters/results-page-sharing.presenter.ts @@ -0,0 +1,74 @@ +import type { IPrototypeData } from '../../types/schemas/prototype-schema'; +import type { IUser } from '../../types/schemas/user-schema'; + +export interface SharedUserRowVM { + nameAndEmail: string; + userId: string; +} + +export interface SharingVM { + hasSharedUsers: boolean; + isOwner: boolean; + ownerRows: { colspan?: number; html?: string; text?: string }[][]; + publicSharing: { + mode: 'none' | 'password' | 'public'; + password: string; + }; + sharedUsers: SharedUserRowVM[]; + showOwnerWarning: boolean; + viewerRows: { text: string }[][]; + workspaces: WorkspaceOptionVM[]; +} + +export interface WorkspaceOptionVM { + selected: boolean; + text: string; + value: string; +} + +export function buildSharingVM( + prototype: IPrototypeData, + isOwner: boolean, + workspaceOptions: WorkspaceOptionVM[], + sharedWithUsers: IUser[] +): SharingVM { + let mode: 'none' | 'password' | 'public' = 'none'; + if (prototype.livePrototypePublic) { + if (prototype.livePrototypePublicPassword) mode = 'password'; + else mode = 'public'; + } + const sharedUsers: SharedUserRowVM[] = sharedWithUsers.map((u) => ({ + nameAndEmail: `${u.name} (${u.email})`, + userId: u.id, + })); + const hasSharedUsers = sharedUsers.length > 0; + const ownerRows: { colspan?: number; html?: string; text?: string }[][] = + []; + if (hasSharedUsers) { + for (const u of sharedUsers) { + ownerRows.push([ + { text: u.nameAndEmail }, + { + html: ``, + }, + ]); + } + } else { + ownerRows.push([{ colspan: 2, text: 'Not shared with any users' }]); + } + const viewerRows = sharedUsers.map((u) => [{ text: u.nameAndEmail }]); + + return { + hasSharedUsers, + isOwner, + ownerRows, + publicSharing: { + mode, + password: prototype.livePrototypePublicPassword, + }, + sharedUsers, + showOwnerWarning: !isOwner, + viewerRows, + workspaces: workspaceOptions, + }; +} diff --git a/src/routes/presenters/results-page-structure.presenter.ts b/src/routes/presenters/results-page-structure.presenter.ts new file mode 100644 index 0000000..d4ee805 --- /dev/null +++ b/src/routes/presenters/results-page-structure.presenter.ts @@ -0,0 +1,129 @@ +import { + ITemplateField, + ITemplateFieldBranchingChoice, + ITemplateFieldNonBranching, +} from '../../types/schemas/prototype-schema'; + +export interface BranchingOptionVM { + label: string; + next: 'finish' | number; +} + +export interface StructureListItemVM { + answerType: string; + branchingOptions?: BranchingOptionVM[]; + index: number; + maxAge?: number; + minAge?: number; + nextJumpTarget?: 'finish' | number; + options?: string[]; + questionText: string; + showNextJump?: boolean; +} + +export interface StructureVM { + list: StructureListItemVM[]; + mermaid: string; +} + +export function buildStructureVM(questions: ITemplateField[]): StructureVM { + const total = questions.length; + + const list: StructureListItemVM[] = questions.map((q, i) => { + const index = i + 1; + + // Branching options + const branchingOptions: BranchingOptionVM[] | undefined = + isBranchingChoice(q) && q.options_branching + ? q.options_branching.map((opt) => ({ + label: opt.text_value, + next: + typeof opt.next_question_value === 'number' && + opt.next_question_value > 0 + ? opt.next_question_value + : 'finish', + })) + : undefined; + + let showNextJump = false; + let nextJumpTarget: 'finish' | number | undefined = undefined; + if (isNonBranching(q) && typeof q.next_question_value === 'number') { + const nv = q.next_question_value; + const isFinish = nv === -1; + const isSequential = nv === index + 1; + const notLast = index < total; + + if (isFinish || (notLast && !isSequential)) { + showNextJump = true; + nextJumpTarget = isFinish ? 'finish' : nv; + } + } + + return { + answerType: q.answer_type, + branchingOptions, + index, + maxAge: + q.answer_type === 'date_of_birth' + ? q.date_of_birth_maximum_age + : undefined, + minAge: + q.answer_type === 'date_of_birth' + ? q.date_of_birth_minimum_age + : undefined, + nextJumpTarget, + options: q.options, + questionText: q.question_text, + showNextJump, + }; + }); + + const mermaid = buildMermaid(questions); + return { list, mermaid }; +} +function buildMermaid(questions: ITemplateField[]): string { + const lines: string[] = ['flowchart TD']; + + questions.forEach((q, idx) => { + const i = String(idx + 1); + lines.push(`Q${i}["${escapeForMermaid(q.question_text)}"]`); + + if (isBranchingChoice(q) && q.options_branching?.length) { + q.options_branching.forEach((opt) => { + const label = escapeForMermaid(opt.text_value); + const next = + typeof opt.next_question_value === 'number' && + opt.next_question_value > 0 + ? `Q${String(opt.next_question_value)}` + : 'Finish'; + lines.push(`Q${i} -->|${label}| ${next}`); + }); + } else if (isNonBranching(q)) { + const nv = q.next_question_value; + if (typeof nv === 'number' && nv > 0) { + lines.push(`Q${i} --> Q${nv.toString()}`); + } else { + lines.push(`Q${i} --> Finish`); + } + } else { + lines.push(`Q${i} --> Finish`); + } + }); + + lines.push('Finish(["End"])'); + return lines.join('\n'); +} + +function escapeForMermaid(text: string): string { + return text.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); +} + +function isBranchingChoice( + f: ITemplateField +): f is ITemplateFieldBranchingChoice { + return f.answer_type === 'branching_choice'; +} + +function isNonBranching(f: ITemplateField): f is ITemplateFieldNonBranching { + return f.answer_type !== 'branching_choice'; +} diff --git a/src/routes/prototype-routes.ts b/src/routes/prototype-routes.ts index 0c580e6..21dfb28 100644 --- a/src/routes/prototype-routes.ts +++ b/src/routes/prototype-routes.ts @@ -62,6 +62,11 @@ import { } from '../utils'; import { buildZipOfForm } from '../zip-generator'; import { verifyLivePrototype, verifyPrototype, verifyUser } from './middleware'; +import { buildHistoryPageVM } from './presenters/history-page.presenter'; +import { buildHistoryVM } from './presenters/results-page-history.presenter'; +import { buildOverviewVM } from './presenters/results-page-overview.presenter'; +import { buildSharingVM } from './presenters/results-page-sharing.presenter'; +import { buildStructureVM } from './presenters/results-page-structure.presenter'; // Create an Express router const prototypeRouter = express.Router(); @@ -363,11 +368,24 @@ export async function renderHistoryPage( text: option.toString(), value: option.toString(), })); - + const totalPrototypes = await countPrototypesByUserId(user.id, false); + const historyPageVM = buildHistoryPageVM({ + countPrototypes, + createdBy, + onlyCreated, + paginationItems, + paginationNextHref, + paginationPreviousHref, + perPage, + sharing, + totalPrototypes, + workspaceItems, + }); res.render('history.njk', { countPrototypes: countPrototypes, createdBy: createdBy, header: header, + historyPageVM, itemsPerPage: perPage.toString(), onlyCreated: onlyCreated, paginationItems: paginationItems, @@ -377,7 +395,7 @@ export async function renderHistoryPage( prototypeRows: prototypeRows, sharing: sharing, showPagination: showPagination, - totalPrototypes: await countPrototypesByUserId(user.id, false), + totalPrototypes: totalPrototypes, workspaceItems: workspaceItems, }); } @@ -968,8 +986,33 @@ export async function renderResultsPage( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion workspace: (await getWorkspaceById(prototypeData.workspaceId))!, }; + const structureVM = buildStructureVM(prototypeData.json.questions); + const overviewVM = buildOverviewVM( + prototypeData.json, + prototypeData.generatedFrom, + getEnvironmentVariables().SUGGESTIONS_ENABLED + ); + const historyVM = await buildHistoryVM( + prototypeData, + previousPrototypes, + totalCountPreviousPrototypes, + getUserById, + user.id + ); + const sharingVM = buildSharingVM( + prototypeData, + isOwner, + allWorkspaces, + sharedWithUsers + ); - res.render('results.njk', data); + res.render('results/results.njk', { + ...data, + historyVM, + overviewVM, + sharingVM, + structureVM, + }); } prototypeRouter.get( '/prototype/:id', diff --git a/src/types/schemas/prototype-schema.ts b/src/types/schemas/prototype-schema.ts index d41f59f..11cd9a6 100644 --- a/src/types/schemas/prototype-schema.ts +++ b/src/types/schemas/prototype-schema.ts @@ -28,6 +28,17 @@ export type ITemplateField = | ITemplateFieldBranchingChoice | ITemplateFieldNonBranching; +// For "branching_choice", next_question_value is always undefined +export interface ITemplateFieldBranchingChoice extends ITemplateFieldBase { + answer_type: 'branching_choice'; + next_question_value: undefined; +} +// For all other answer_types, next_question_value is required +export interface ITemplateFieldNonBranching extends ITemplateFieldBase { + answer_type: Exclude; + next_question_value: number; +} + interface ITemplateFieldBase { date_of_birth_maximum_age?: number; date_of_birth_minimum_age?: number; @@ -39,19 +50,6 @@ interface ITemplateFieldBase { required: boolean; required_error_text?: string; } - -// For "branching_choice", next_question_value is always undefined -interface ITemplateFieldBranchingChoice extends ITemplateFieldBase { - answer_type: 'branching_choice'; - next_question_value: undefined; -} - -// For all other answer_types, next_question_value is required -interface ITemplateFieldNonBranching extends ITemplateFieldBase { - answer_type: Exclude; - next_question_value: number; -} - export const PrototypeDesignSystems = ['GOV.UK', 'HMRC'] as const; export type PrototypeDesignSystemsType = (typeof PrototypeDesignSystems)[number]; diff --git a/views/history.njk b/views/history.njk index 085286d..f17eb63 100644 --- a/views/history.njk +++ b/views/history.njk @@ -5,171 +5,61 @@ {% endblock %} {% block content %} -
-
-

Your prototypes

-

- {% if totalPrototypes == 0 %} - There are no prototypes. - {% elif totalPrototypes == countPrototypes %} - Showing all {{ totalPrototypes }} prototype{{ 's' if totalPrototypes != 1 else '' }}. - {% else %} - Showing {{ countPrototypes }} prototype{{ 's' if countPrototypes != 1 else '' }} out of {{ totalPrototypes }}. - {% endif %} -

-
- {{ govukButton({ - text: "Create a prototype", - href: "/create" - }) }} - {% if totalPrototypes > 0 %} - {{ govukButton({ - text: "Reset filters", - href: "/history", - classes: "govuk-button--secondary" - }) }} -
-
-
- {{ govukSelect({ - id: "onlyCreated", - name: "onlyCreated", - label: { - text: "Filter by type", - isPageHeading: false - }, - classes: "govuk-!-width-full", - items: [ - { - value: "false", - text: "All prototypes", - selected: onlyCreated === false - }, - { - value: "true", - text: "Exclude updates to existing prototypes", - selected: onlyCreated === true - } - ] - }) }} -
-
- {{ govukSelect({ - id: "createdBy", - name: "createdBy", - label: { - text: "Filter by creator", - isPageHeading: false - }, - classes: "govuk-!-width-full", - items: [ - { - value: "anyone", - text: "Created by anyone", - selected: createdBy === "anyone" - }, - { - value: "self", - text: "Created by you", - selected: createdBy === "self" - }, - { - value: "others", - text: "Created by others", - selected: createdBy === "others" - } - ] - }) }} -
-
-
-
- {{ govukSelect({ - id: "workspace", - name: "workspace", - label: { - text: "Filter by workspace", - isPageHeading: false - }, - classes: "govuk-!-width-full", - items: workspaceItems - }) }} -
-
- {{ govukSelect({ - id: "sharing", - name: "sharing", - label: { - text: "Filter by sharing", - isPageHeading: false - }, - classes: "govuk-!-width-full", - items: [ - { - value: "all", - text: "All prototypes", - selected: sharing === "all" - }, - { - value: "public", - text: "Prototypes shared publicly", - selected: sharing === "public" - }, - { - value: "users", - text: "Prototypes shared with specific users", - selected: sharing === "users" - }, - { - value: "workspace", - text: "Prototypes within a shared workspace", - selected: sharing === "workspace" - }, - { - value: "private", - text: "Prototypes that are not shared", - selected: sharing === "private" - } - ] - }) }} -
-
- {{ govukTable({ - firstCellIsHeader: true, - head: header, - rows: prototypeRows - }) }} - {% if showPagination %} - {{ govukPagination({ - classes: "justify-self-center", - previous: { - href: paginationPreviousHref - }, - next: { - href: paginationNextHref - }, - items: paginationItems - }) }} - {% endif %} - {% if countPrototypes > 0 %} - {{ govukRadios({ - classes: "govuk-radios--inline govuk-radios--small", - id: "itemsPerPage", - name: "itemsPerPage", - fieldset: { - legend: { - text: "Items per page", - isPageHeading: false - } - }, - items: perPageItems - }) }} - {% endif %} - {% else %} -
- {% endif %} -
+

+ {{ historyPageVM.summaryText }} +

+ +
+ {{ govukButton({ text: "Create a prototype", href: "/create" }) }} + {% if historyPageVM.hasPrototypes %} + {{ govukButton({ text: "Reset filters", href: "/history", classes: "govuk-button--secondary" }) }} + {% endif %}
+ + {% if historyPageVM.hasPrototypes %} +
+
+ {{ govukSelect({ + id: "onlyCreated", + name: "onlyCreated", + label: { text: "Filter by type" }, + items: historyPageVM.filterOnlyCreatedItems + }) }} +
+ +
+ {{ govukSelect({ + id: "createdBy", + name: "createdBy", + label: { text: "Filter by creator" }, + items: historyPageVM.filterCreatedByItems + }) }} +
+
+ + {{ govukTable({ head: header, rows: prototypeRows }) }} + + {% if historyPageVM.showPagination %} + {{ govukPagination({ + previous: { href: historyPageVM.paginationPreviousHref }, + next: { href: historyPageVM.paginationNextHref }, + items: historyPageVM.paginationItems + }) }} + {% endif %} + + {{ govukRadios({ + classes: "govuk-radios--inline govuk-radios--small", + id: "itemsPerPage", + name: "itemsPerPage", + fieldset: { + legend: { + text: "Items per page", + isPageHeading: false + } + }, + items: perPageItems + }) }} + {% endif %} {% endblock %} {% block bodyEnd %} @@ -178,13 +68,12 @@ - - - - - {% if isOwner %} - - {% endif %} - {% if enableSuggestions %} - - {% endif %} -{% endblock %} diff --git a/views/results/_history.njk b/views/results/_history.njk new file mode 100644 index 0000000..67fe68e --- /dev/null +++ b/views/results/_history.njk @@ -0,0 +1,18 @@ +

View previous versions

+ +{% if historyVM.hasMultiple %} + {{ govukTable({ + head: [], + rows: historyVM.rows + }) }} + + {% if historyVM.additionalLabel %} +

+ {{ historyVM.additionalLabel }} +

+ {% endif %} +{% else %} +

+ {{ historyVM.rows[0][0].html | safe }} +

+{% endif %} \ No newline at end of file diff --git a/views/results/_overview.njk b/views/results/_overview.njk new file mode 100644 index 0000000..52513e3 --- /dev/null +++ b/views/results/_overview.njk @@ -0,0 +1,102 @@ +
+ + + + + +

+ +

+ + + + {% if overviewVM.showJsonEditor %} + {% set textareaClasses = "display-none" %} + {% else %} + {% set textareaClasses = "" %} + {% endif %} + + {{ govukTextarea({ + name: "prompt", + id: "textPrompt", + rows: 6, + classes: textareaClasses, + disabled: overviewVM.showJsonEditor + }) }} + + {{ govukSelect({ + classes: "govuk-!-width-full", + id: "designSystem", + name: "designSystem", + label: { + text: "Choose a design system", + classes: "govuk-label--m" + }, + items: [ + { value: "GOV.UK", text: "GOV.UK", selected: designSystem == "GOV.UK" }, + { value: "HMRC", text: "HMRC", selected: designSystem == "HMRC" } + ] + }) }} + +
+ {{ govukButton({ + id: "startPrototypeButton", + text: "Update prototype", + type: "submit", + preventDoubleClick: true + }) }} + {{ govukButton({ + id: "switchPromptTypeButton", + type: "button", + text: overviewVM.switchPromptButtonText, + classes: "govuk-button--secondary" + }) }} + +
+
+ +{% if enableSuggestions %} +

+ Use a suggested prompt (refresh) +

+ +
+ {% for s in overviewVM.suggestions %} + + {% endfor %} +
+ +

+ No suggestions generated. +

+{% endif %} + +{% if overviewVM.explanation %} + {{ govukDetails({ + summaryText: "View AI explanation", + text: overviewVM.explanation + }) }} +{% endif %} + +{{ govukWarningText({ + text: "Always check the prototypes generated by this tool, as they may contain inaccurate information.", + iconFallbackText: "Warning" +}) }} \ No newline at end of file diff --git a/views/results/_sharing.njk b/views/results/_sharing.njk new file mode 100644 index 0000000..34fe6b1 --- /dev/null +++ b/views/results/_sharing.njk @@ -0,0 +1,130 @@ +
+

Share this prototype

+ + + + +
\ No newline at end of file diff --git a/views/results/_structure.njk b/views/results/_structure.njk new file mode 100644 index 0000000..ce07f44 --- /dev/null +++ b/views/results/_structure.njk @@ -0,0 +1,72 @@ +

View the prototype structure

+ +{{ govukButton({ + id: "switchStructureTypeButton", + type: "button", + text: "Graph view", + classes: "govuk-button--secondary govuk-!-margin-bottom-2" +}) }} + +
+
+
    + {% for item in structureVM.list %} +
  1. + {{ item.answerType }}: {{ item.questionText }} + + {% if item.branchingOptions %} +
      + {% for opt in item.branchingOptions %} +
    • + {{ opt.label }} + {% if opt.next != 'finish' %} + → question {{ opt.next }} + {% else %} + → finish + {% endif %} +
    • + {% endfor %} +
    + {% endif %} + + {% if item.showNextJump %} +
      +
    • + {% if item.nextJumpTarget == 'finish' %} + finish + {% else %} + question {{ item.nextJumpTarget }} + {% endif %} +
    • +
    + {% endif %} + + {% if item.options %} +
      + {% for o in item.options %} +
    • {{ o }}
    • + {% endfor %} +
    + {% endif %} + + {% if item.minAge %} +
      +
    • Minimum age: {{ item.minAge }} years
    • +
    + {% endif %} + {% if item.maxAge %} +
      +
    • Maximum age: {{ item.maxAge }} years
    • +
    + {% endif %} +
  2. + {% endfor %} +
+
+ + +
\ No newline at end of file diff --git a/views/results/results.njk b/views/results/results.njk new file mode 100644 index 0000000..6c3e8d8 --- /dev/null +++ b/views/results/results.njk @@ -0,0 +1,449 @@ +{% extends "template.njk" %} + +{% block pageTitle %} + Your prototype: {{ prototypeTitle }} – Gov Prototype by Prompt +{% endblock %} + +{% block beforeContent %} + {{ govukBreadcrumbs({ + items: [ + { + text: "Home", + href: "/" + }, + { + text: "Your prototypes", + href: "/history" + } + ], + labelText: "Breadcrumb-main" + }) }} +{% endblock %} + +{% block content %} + +
+
+

Your prototype: {{ prototypeTitle }}

+ {{ govukErrorSummary({ + classes: "govuk-!-margin-bottom-7 display-none", + titleText: "There was a problem", + descriptionHtml: "Check the form below." + }) }} +
+ {{ govukButton({ + text: "Download", + href: ["/prototype/", prototypeId, "/download"] | join + }) }} + {{ govukButton({ + id: "toggleDemoButton", + text: "Hide demo", + classes: "govuk-button--secondary", + attributes: { + "aria-expanded": "true", + "aria-controls": "demoColumn" + } + }) }} + {{ govukButton({ + id: "resetDemoButton", + text: "Reset demo", + classes: "govuk-button--secondary" + }) }} + Open in a new tab +
+
+
+ {% set overviewHtml %} + {% include "./_overview.njk" %} + {% endset -%} + + {% set structureHtml %} + {% include "./_structure.njk" %} + {% endset -%} + + {% set historyHtml %} + {% include "./_history.njk" %} + {% endset -%} + + {% set sharingHtml %} + {% include "./_sharing.njk" %} + {% endset -%} + + {{ govukTabs({ + items: [ + { + label: "Overview", + id: "overviewPanel", + panel: { + html: overviewHtml + } + }, + { + label: "Structure", + id: "structurePanel", + panel: { + html: structureHtml + } + }, + { + label: "History", + id: "historyPanel", + panel: { + html: historyHtml + } + }, + { + label: "Sharing", + id: "sharingPanel", + panel: { + html: sharingHtml + } + } + ] + }) }} +
+ +
+ + +
+
+{% endblock %} + +{% block bodyEnd %} + {# Run JavaScript at end of the , to avoid blocking the initial render. #} + {{ super() }} + + + + + + {% if isOwner %} + + {% endif %} + {% if enableSuggestions %} + + {% endif %} +{% endblock %} \ No newline at end of file