Repository: DEFRA/forms-manager
Pull request #464: Feat/468297  reorder pages
Author: login: whitewaterdesign
Created: 2025-03-11T07:46:32Z | Merged: 2025-03-12T11:03:32Z

## Re-order pages endpoint

API for https://dev.azure.com/defragovuk/DEFRA-CDP/_workitems/edit/468297

Endpoint for updating the ordering of pages in a draft definition:

POST /forms/:id/definition/draft/pages/order

[
  "7ec759c3-1bcf-4f82-aa98-8a1682250e00",
  "27dfab66-0ebc-4b83-bddc-b5fe56b12682",
  "31cc5395-80f0-43ec-b02c-345c11aecaed"
]

Ids not included in the list (namely Summary page) are ignored and given a sort order of 0.  

Updates a whole form definition in one go as an interim solution.  If we have conflicts we can look into this again.


Diff:
diff --git a/src/api/forms/service/definition.js b/src/api/forms/service/definition.js
index fd2924ff..5150e69b 100644
--- a/src/api/forms/service/definition.js
+++ b/src/api/forms/service/definition.js
@@ -3,6 +3,7 @@ import Boom from '@hapi/boom'
 import { makeFormLiveErrorMessages } from '~/src/api/forms/constants.js'
 import * as formDefinition from '~/src/api/forms/repositories/form-definition-repository.js'
 import * as formMetadata from '~/src/api/forms/repositories/form-metadata-repository.js'
+import { reorderPages } from '~/src/api/forms/service/helpers/definition.js'
 import { getForm } from '~/src/api/forms/service/index.js'
 import {
   DRAFT,
@@ -234,6 +235,68 @@ export async function createDraftFromLive(formId, author) {
   }
 }
 
+/**
+ * Based on a list of Page ids will reorder the pages
+ * @param {string} formId
+ * @param {string[]} orderList
+ * @param {FormMetadataAuthor} author
+ */
+export async function reorderDraftFormDefinitionPages(
+  formId,
+  orderList,
+  author
+) {
+  logger.info(
+    `Reordering pages on Form Definition (draft) for form ID ${formId}`
+  )
+
+  const form = await getFormDefinition(formId)
+
+  if (!orderList.length) {
+    return form
+  }
+
+  const session = client.startSession()
+
+  try {
+    const newForm = await session.withTransaction(async () => {
+      const reorderedForm = reorderPages(form, orderList)
+
+      // Update the form definition
+      await formDefinition.upsert(formId, reorderedForm, session)
+
+      logger.info(`Updating form metadata (draft) for form ID ${formId}`)
+
+      // Update the `updatedAt/By` fields of the draft state
+      const now = new Date()
+      await formMetadata.update(
+        formId,
+        {
+          $set: partialAuditFields(now, author)
+        },
+        session
+      )
+
+      return reorderedForm
+    })
+
+    logger.info(
+      `Reordered pages on Form Definition (draft) for form ID ${formId}`
+    )
+
+    return newForm
+  } catch (err) {
+    logger.error(
+      err,
+      `Reordering pages on form definition (draft) for form ID ${formId} failed`
+    )
+
+    throw err
+  } finally {
+    await session.endSession()
+  }
+}
+
 /**
  * @import { FormDefinition, FormMetadataAuthor, FormMetadata, FilterOptions, QueryOptions } from '@defra/forms-model'
  * @import { ClientSession } from 'mongodb'
diff --git a/src/api/forms/service/definition.test.js b/src/api/forms/service/definition.test.js
index 8ee30925..b7704139 100644
--- a/src/api/forms/service/definition.test.js
+++ b/src/api/forms/service/definition.test.js
@@ -2,6 +2,11 @@ import Boom from '@hapi/boom'
 import { ObjectId } from 'mongodb'
 import { pino } from 'pino'
 
+import {
+  buildDefinition,
+  buildQuestionPage,
+  buildSummaryPage
+} from '~/src/api/forms/__stubs__/definition.js'
 import { makeFormLiveErrorMessages } from '~/src/api/forms/constants.js'
 import { InvalidFormDefinitionError } from '~/src/api/forms/errors.js'
 import * as formDefinition from '~/src/api/forms/repositories/form-definition-repository.js'
@@ -19,6 +24,7 @@ import {
   createLiveFromDraft,
   getFormDefinition,
   listForms,
+  reorderDraftFormDefinitionPages,
   updateDraftFormDefinition
 } from '~/src/api/forms/service/definition.js'
 import {
@@ -48,9 +54,24 @@ describe('Forms service', () => {
   const id = '661e4ca5039739ef2902b214'
   const slug = 'test-form'
   const dateUsedInFakeTime = new Date('2020-01-01')
+  const defaultAudit = {
+    'draft.updatedAt': dateUsedInFakeTime,
+    'draft.updatedBy': author,
+    updatedAt: dateUsedInFakeTime,
+    updatedBy: author
+  }
 
   let definition = emptyFormWithSummary()
 
+  const dbMetadataSpy = jest.spyOn(formMetadata, 'update')
+
+  const expectMetadataUpdate = () => {
+    expect(dbMetadataSpy).toHaveBeenCalled()
+    const [formId, updateFilter] = dbMetadataSpy.mock.calls[0]
+    expect(formId).toBe(id)
+    expect(updateFilter.$set).toEqual(defaultAudit)
+  }
+
   beforeAll(async () => {
     await prepareDb(pino())
   })
@@ -964,6 +985,73 @@ describe('Forms service', () => {
       )
     })
   })
+
+  describe('reorderDraftFormDefinitionPages', () => {
+    const pageOneId = 'e6511b1c-c813-43d7-92c4-d84ba35d5f62'
+    const pageTwoId = 'e3a1cb1e-8c9e-41d7-8ba7-719829bce84a'
+    const summaryPageId = 'b90e6453-d4c1-46a4-a233-3dbee566c79e'
+
+    const pageOne = buildQuestionPage({
+      id: pageOneId,
+      title: 'Page One'
+    })
+    const pageTwo = buildQuestionPage({
+      id: pageTwoId,
+      title: 'Page Two'
+    })
+    const summaryPage = buildSummaryPage({
+      id: summaryPageId
+    })
+
+    const definition = buildDefinition({
+      pages: [pageTwo, pageOne, summaryPage]
+    })
+
+    beforeEach(() => {
+      jest.mocked(formDefinition.get).mockResolvedValueOnce(definition)
+    })
+
+    it('should reorder the pages', async () => {
+      const expectedDefinition = buildDefinition({
+        pages: [pageOne, pageTwo, summaryPage]
+      })
+      const orderList = [pageOneId, pageOneId]
+      const result = await reorderDraftFormDefinitionPages(
+        id,
+        orderList,
+        author
+      )
+
+      const [, upsertedDefinition] = jest.mocked(formDefinition.upsert).mock
+        .calls[0]
+      expect(upsertedDefinition).toEqual(expectedDefinition)
+      expect(result).toEqual(expectedDefinition)
+      expectMetadataUpdate()
+    })
+
+    it('should not do any updates if no order list is sent', async () => {
+      const returnedDefinition = await reorderDraftFormDefinitionPages(
+        id,
+        [],
+        author
+      )
+      expect(returnedDefinition).toEqual(definition)
+      expect(formDefinition.upsert).not.toHaveBeenCalled()
+      expect(formMetadata.update).not.toHaveBeenCalled()
+    })
+
+    it('should surface errors', async () => {
+      const boomInternal = Boom.internal('Something went wrong')
+      jest.mocked(formDefinition.upsert).mockRejectedValueOnce(boomInternal)
+      await expect(
+        reorderDraftFormDefinitionPages(
+          id,
+          ['5a1c2ef7-ed4e-4ec7-9119-226fc3063bda'],
+          author
+        )
+      ).rejects.toThrow(boomInternal)
+    })
+  })
 })
 
 /**
diff --git a/src/api/forms/service/helpers/definition.js b/src/api/forms/service/helpers/definition.js
new file mode 100644
index 00000000..a646e861
--- /dev/null
+++ b/src/api/forms/service/helpers/definition.js
@@ -0,0 +1,57 @@
+/**
+ * @typedef {{
+ *     [k: string]: number
+ * }} PageOrderHelper
+ */
+
+/**
+ * Creates an object of keys and their desired position based on the pageOrder (pure function)
+ * For ['a','b''c'] will return { a: -2, b: -1, c: -0 }
+ * @param {string[]} pageOrder
+ * @returns {PageOrderHelper}
+ */
+export function createOrder(pageOrder) {
+  return pageOrder
+    .slice()
+    .reverse()
+    .reduce((pageOrderChart, id, idx) => {
+      return {
+        ...pageOrderChart,
+        [id]: -idx
+      }
+    }, {})
+}
+
+/**
+ * Re-orders the pages in the definition
+ * @param {FormDefinition} formDefinition
+ * @param {string[]} pageOrder
+ */
+export function reorderPages(formDefinition, pageOrder) {
+  const order = createOrder(pageOrder)
+
+  return {
+    ...formDefinition,
+    pages: formDefinition.pages.toSorted((pageA, pageB) => {
+      const aId = pageA.id
+      const bId = pageB.id
+
+      const aPosition = aId !== undefined && aId in order ? order[aId] : 0
+      const bPosition = bId !== undefined && bId in order ? order[bId] : 0
+
+      if (aPosition < bPosition) {
+        return -1
+      }
+
+      if (aPosition > bPosition) {
+        return 1
+      }
+
+      return 0
+    })
+  }
+}
+
+/**
+ * @import { FormDefinition, Page } from '@defra/forms-model'
+ */
diff --git a/src/api/forms/service/helpers/definition.test.js b/src/api/forms/service/helpers/definition.test.js
new file mode 100644
index 00000000..404dff53
--- /dev/null
+++ b/src/api/forms/service/helpers/definition.test.js
@@ -0,0 +1,90 @@
+import {
+  buildDefinition,
+  buildQuestionPage,
+  buildSummaryPage
+} from '~/src/api/forms/__stubs__/definition.js'
+import {
+  createOrder,
+  reorderPages
+} from '~/src/api/forms/service/helpers/definition.js'
+
+describe('page helpers', () => {
+  const pageOneId = '181c5e0a-9c68-44f9-a731-9699d915e5a3'
+  const pageTwoId = '37734ec9-508d-416a-9fc5-0fbb5fdd6abc'
+  const pageThreeId = '082ad4bb-fd75-457d-bf6a-c721cd7f26cc'
+  const summaryPageId = 'c5118caa-b133-4672-a616-c385a05426e8'
+
+  describe('createOrder', () => {
+    it('should create an object with orders', () => {
+      const orderDictionary = {
+        '181c5e0a-9c68-44f9-a731-9699d915e5a3': -2,
+        '37734ec9-508d-416a-9fc5-0fbb5fdd6abc': -1,
+        '082ad4bb-fd75-457d-bf6a-c721cd7f26cc': -0
+      }
+
+      expect(
+        createOrder([
+          '181c5e0a-9c68-44f9-a731-9699d915e5a3',
+          '37734ec9-508d-416a-9fc5-0fbb5fdd6abc',
+          '082ad4bb-fd75-457d-bf6a-c721cd7f26cc'
+        ])
+      ).toEqual(orderDictionary)
+    })
+  })
+
+  describe('reorderPages', () => {
+    const pageOne = buildQuestionPage({
+      title: 'Page One',
+      id: pageOneId
+    })
+    const pageTwo = buildQuestionPage({
+      title: 'Page Two',
+      id: pageTwoId
+    })
+    const pageThree = buildQuestionPage({
+      title: 'Page Three',
+      id: pageThreeId
+    })
+    const summaryPage = buildSummaryPage({
+      title: 'Summary Page',
+      id: summaryPageId
+    })
+
+    it('should reorder pages', () => {
+      const definition = buildDefinition({
+        pages: [pageTwo, pageThree, pageOne, summaryPage]
+      })
+      const expectedDefinition = buildDefinition({
+        ...definition,
+        pages: [pageOne, pageTwo, pageThree, summaryPage]
+      })
+      const pageOrder = [pageOneId, pageTwoId, pageThreeId]
+
+      expect(reorderPages(definition, pageOrder)).toEqual(expectedDefinition)
+    })
+
+    it('should do nothing if there are no pages', () => {
+      const definition = buildDefinition({
+        pages: []
+      })
+      expect(reorderPages(definition, [])).toEqual(definition)
+    })
+
+    it('should handle undefined and summary pages', () => {
+      const summaryWithoutId = buildSummaryPage({
+        ...summaryPage,
+        id: undefined
+      })
+      const pageWithoutId = buildQuestionPage({
+        id: undefined
+      })
+      const page2WithoutId = buildQuestionPage({
+        id: undefined
+      })
+      const definition = buildDefinition({
+        pages: [pageWithoutId, page2WithoutId, summaryWithoutId]
+      })
+      expect(reorderPages(definition, [])).toEqual(definition)
+    })
+  })
+})
diff --git a/src/api/types.js b/src/api/types.js
index 7009b0bc..1d215d9d 100644
--- a/src/api/types.js
+++ b/src/api/types.js
@@ -11,6 +11,7 @@
  * @typedef {Request<{ Server: { db: Db }, Params: FormByIdInput, Payload: Partial<FormMetadataInput> }>} RequestFormMetadataUpdateById
  * @typedef {Request<{ Server: { db: Db }, Query: QueryOptions }>} RequestListForms
  * @typedef {Request<{ Server: { db: Db }, Params: FormByIdInput & {version: 'v1'|'v2'}, }>} MigrateDraftFormRequest
+ * @typedef {Request<{ Server: { db: Db }, Params: FormByIdInput, Payload: string[] }>} SortDraftFormPagesRequest
  */
 
 /**
diff --git a/src/models/forms.js b/src/models/forms.js
index 43aaffdd..949afcba 100644
--- a/src/models/forms.js
+++ b/src/models/forms.js
@@ -69,3 +69,8 @@ export const migrateDefinitionParamSchema = Joi.object()
     version: Joi.string().allow('v1', 'v2').required()
   })
   .required()
+
+export const sortIdsSchema = Joi.array()
+  .items(Joi.string().uuid().required())
+  .min(1)
+  .required()
diff --git a/src/models/forms.test.js b/src/models/forms.test.js
new file mode 100644
index 00000000..90732fc6
--- /dev/null
+++ b/src/models/forms.test.js
@@ -0,0 +1,36 @@
+import { ValidationError } from 'joi'
+
+import { sortIdsSchema } from '~/src/models/forms.js'
+
+describe('forms model', () => {
+  describe('sortIdsSchema', () => {
+    it('should accept a list of uuids', () => {
+      const payload = [
+        '49e31f0d-63e4-4da4-9525-1494a7d7edc5',
+        '90c9d202-5594-4953-b77f-3a65022186c6',
+        '0cc7b818-a64f-4159-ba86-ef01f9e46e32'
+      ]
+
+      const { value, error } = sortIdsSchema.validate(payload)
+      expect(error).toBeUndefined()
+      expect(value).toEqual(payload)
+    })
+
+    it('should not accept an invalid list', () => {
+      expect(sortIdsSchema.validate([]).error).toEqual(
+        new ValidationError(
+          '"value" does not contain 1 required value(s)',
+          [],
+          []
+        )
+      )
+      expect(sortIdsSchema.validate(['not-a-valid-uuid']).error).toEqual(
+        new ValidationError(
+          '"[0]" must be a valid GUID',
+          [],
+          ['not-a-valid-uuid']
+        )
+      )
+    })
+  })
+})
diff --git a/src/routes/forms.js b/src/routes/forms.js
index ae4474a6..2b7fe074 100644
--- a/src/routes/forms.js
+++ b/src/routes/forms.js
@@ -16,6 +16,7 @@ import {
   createLiveFromDraft,
   getFormDefinition,
   listForms,
+  reorderDraftFormDefinitionPages,
   updateDraftFormDefinition
 } from '~/src/api/forms/service/definition.js'
 import {
@@ -41,6 +42,7 @@ import {
   pageByIdSchema,
   patchPageSchema,
   prependQuerySchema,
+  sortIdsSchema,
   updateFormDefinitionSchema
 } from '~/src/models/forms.js'
 
@@ -261,6 +263,24 @@ export default [
       }
     }
   },
+  {
+    method: 'POST',
+    path: '/forms/{id}/definition/draft/pages/order',
+    /**
+     * @param {SortDraftFormPagesRequest} request
+     */
+    handler(request) {
+      const { auth, params, payload } = request
+      const author = getAuthor(auth.credentials.user)
+      return reorderDraftFormDefinitionPages(params.id, payload, author)
+    },
+    options: {
+      validate: {
+        params: formByIdSchema,
+        payload: sortIdsSchema
+      }
+    }
+  },
   {
     method: 'PATCH',
     path: '/forms/{id}/definition/draft/pages/{pageId}',
@@ -438,6 +458,6 @@ export default [
 /**
  * @import { FormMetadata } from '@defra/forms-model'
  * @import { ServerRoute } from '@hapi/hapi'
- * @import { RequestFormById, RequestFormBySlug, RequestFormDefinition, RequestFormMetadataCreate, RequestFormMetadataUpdateById, RequestListForms, RequestPage, RequestComponent, PatchPageRequest, RequestUpdateComponent, MigrateDraftFormRequest } from '~/src/api/types.js'
+ * @import { RequestFormById, RequestFormBySlug, RequestFormDefinition, RequestFormMetadataCreate, RequestFormMetadataUpdateById, RequestListForms, RequestPage, RequestComponent, PatchPageRequest, RequestUpdateComponent, MigrateDraftFormRequest, SortDraftFormPagesRequest } from '~/src/api/types.js'
  * @import { ExtendedResponseToolkit } from '~/src/plugins/query-handler/types.js'
  */
diff --git a/src/routes/forms.test.js b/src/routes/forms.test.js
index 840b48d7..9a595838 100644
--- a/src/routes/forms.test.js
+++ b/src/routes/forms.test.js
@@ -2,7 +2,9 @@ import { organisations } from '@defra/forms-model'
 import Boom from '@hapi/boom'
 
 import {
+  buildDefinition,
   buildQuestionPage,
+  buildSummaryPage,
   buildTextFieldComponent
 } from '~/src/api/forms/__stubs__/definition.js'
 import { FormAlreadyExistsError } from '~/src/api/forms/errors.js'
@@ -15,7 +17,8 @@ import {
   createDraftFromLive,
   createLiveFromDraft,
   getFormDefinition,
-  listForms
+  listForms,
+  reorderDraftFormDefinitionPages
 } from '~/src/api/forms/service/definition.js'
 import {
   createForm,
@@ -632,6 +635,39 @@ describe('Forms route', () => {
       ])
     })
 
+    test('Testing POST /forms/{id}/definition/draft/pages/order reorders the pages in the db', async () => {
+      const pageOneId = '5113a8ab-b297-46b5-b732-7fe42660c4db'
+      const pageTwoId = 'd3dc6af2-3235-4455-80f7-941f0bf69c4f'
+
+      const expectedDefinition = buildDefinition({
+        pages: [
+          buildQuestionPage({
+            id: pageOneId,
+            title: 'Page one'
+          }),
+          buildQuestionPage({
+            id: pageOneId,
+            title: 'Page two'
+          }),
+          buildSummaryPage()
+        ]
+      })
+      jest
+        .mocked(reorderDraftFormDefinitionPages)
+        .mockResolvedValue(expectedDefinition)
+
+      const response = await server.inject({
+        method: 'POST',
+        url: `/forms/${id}/definition/draft/pages/order`,
+        payload: [pageOneId, pageTwoId],
+        auth
+      })
+
+      expect(response.result).toEqual(expectedDefinition)
+      expect(response.statusCode).toEqual(okStatusCode)
+      expect(response.headers['content-type']).toContain(jsonContentType)
+    })
+
     test('Testing PUT /forms/{id}/definition/draft/pages/{pageId}/components/{componentId} updates a component on a page', async () => {
       const updatedComponent = buildTextFieldComponent({
         id: componentId,
@@ -1008,6 +1044,28 @@ describe('Forms route', () => {
       })
     })
 
+    test('Testing POST /forms/{id}/definition/draft/pages/order with invalid payload returns validation errors', async () => {
+      const response = await server.inject({
+        method: 'POST',
+        url: `/forms/${id}/definition/draft/pages/order/`,
+        payload: ['not-a-valid-uuid'],
+        auth
+      })
+
+      expect(response.statusCode).toEqual(badRequestStatusCode)
+      expect(response.headers['content-type']).toContain(jsonContentType)
+      expect(response.result).toEqual({
+        error: 'Bad Request',
+        message:
+          '"[0]" must be a valid GUID. "value" does not contain 1 required value(s)',
+        statusCode: 400,
+        validation: {
+          keys: ['0', ''],
+          source: 'payload'
+        }
+      })
+    })
+
     test('Testing POST /forms/{id}/definition/draft/pages/{pageId} with invalid payload returns validation errors', async () => {
       const invalidPatchPayload = {
         id: 1234


Reviews:
user: login: whitewaterdesign
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T14:20:56Z
user: login: jbarnsley10
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T09:08:12Z
user: login: jbarnsley10
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T09:12:36Z
user: login: jbarnsley10
body: Looks good. I'd question the complexity of your reorderPages method though
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T09:13:57Z
user: login: whitewaterdesign
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T09:15:10Z
user: login: alexluckett
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T10:55:48Z

Review comments:
diff_hunk: @@ -0,0 +1,56 @@
+/**
+ * @typedef {{
+ *     [k: string]: number
+ * }} PageOrderHelper
+ */
+
+/**
+ * Creates an object of keys and their desired position based on the pageOrder
+ * @param {string[]} pageOrder
+ * @returns {PageOrderHelper}
+ */
+export function createOrder(pageOrder) {
+  return pageOrder
+    .slice()
+    .reverse()
+    .reduce((pageOrderChart, id, idx) => {
+      return {
+        ...pageOrderChart,
+        [id]: -idx
+      }
+    }, {})
+}
path: src/api/forms/service/helpers/definition.js
user: login: whitewaterdesign
body: gives the relative sort order of each ID -

For three items in the list


```
['0','1','2'] => {
'0': -2,
'1': -1,
'2': 0
}
```
created_at: 2025-03-11T14:20:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,56 @@
+/**
+ * @typedef {{
+ *     [k: string]: number
+ * }} PageOrderHelper
+ */
+
+/**
+ * Creates an object of keys and their desired position based on the pageOrder
+ * @param {string[]} pageOrder
+ * @returns {PageOrderHelper}
+ */
+export function createOrder(pageOrder) {
+  return pageOrder
+    .slice()
path: src/api/forms/service/helpers/definition.js
user: login: jbarnsley10
body: Do you need the slice here (presumably just cloning the array)?
created_at: 2025-03-12T09:08:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,56 @@
+/**
+ * @typedef {{
+ *     [k: string]: number
+ * }} PageOrderHelper
+ */
+
+/**
+ * Creates an object of keys and their desired position based on the pageOrder
+ * @param {string[]} pageOrder
+ * @returns {PageOrderHelper}
+ */
+export function createOrder(pageOrder) {
+  return pageOrder
+    .slice()
+    .reverse()
+    .reduce((pageOrderChart, id, idx) => {
+      return {
+        ...pageOrderChart,
+        [id]: -idx
+      }
+    }, {})
+}
+
+/**
+ * Re-orders the pages in the definition
+ * @param {FormDefinition} formDefinition
+ * @param {string[]} pageOrder
+ */
path: src/api/forms/service/helpers/definition.js
user: login: jbarnsley10
body: Although this looks good, in tandem with method orderPages(), it seems to be a lot more code and more difficult to understand than a simple 'pulling items out of an array in order' as was used in the designer:
/**
 * Orders a list of pages based on a list of ids
 * @param {Page[]} orderablePages
 * @param {string} pageOrder
 */
export function orderPages(orderablePages, pageOrder) {
  const pageIdsInOrder = pageOrder.split(',')
  const pagesInOrder = /** @type {Page[]} */ ([])
  pageIdsInOrder.forEach((pid) => {
    const foundPage = orderablePages.find((page) => page.id === pid)
    if (foundPage) {
      pagesInOrder.push(foundPage)
    }
  })
  return pagesInOrder
}
created_at: 2025-03-12T09:12:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,56 @@
+/**
+ * @typedef {{
+ *     [k: string]: number
+ * }} PageOrderHelper
+ */
+
+/**
+ * Creates an object of keys and their desired position based on the pageOrder
+ * @param {string[]} pageOrder
+ * @returns {PageOrderHelper}
+ */
+export function createOrder(pageOrder) {
+  return pageOrder
+    .slice()
path: src/api/forms/service/helpers/definition.js
user: login: whitewaterdesign
body: yeah, makes a copy of the array
created_at: 2025-03-12T09:15:10Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-03-12T11:03:19Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=DEFRA_forms-manager&pullRequest=464) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=DEFRA_forms-manager&pullRequest=464&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=DEFRA_forms-manager&pullRequest=464&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=DEFRA_forms-manager&pullRequest=464&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [100.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=DEFRA_forms-manager&pullRequest=464&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=DEFRA_forms-manager&pullRequest=464&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=DEFRA_forms-manager&pullRequest=464)


author_association: NONE