From 900d4f09a4692c5bb98424881402989a6bbefee3 Mon Sep 17 00:00:00 2001 From: iandebruin98 Date: Fri, 25 Oct 2024 16:46:35 +0200 Subject: [PATCH 1/5] feat: Expand duplicate function --- .../pages/projects/[project]/duplicate.tsx | 88 +++++++- apps/api-server/src/routes/api/project.js | 191 ++++++++++++++++++ 2 files changed, 275 insertions(+), 4 deletions(-) diff --git a/apps/admin-server/src/pages/projects/[project]/duplicate.tsx b/apps/admin-server/src/pages/projects/[project]/duplicate.tsx index 22657cb32..994aba44b 100644 --- a/apps/admin-server/src/pages/projects/[project]/duplicate.tsx +++ b/apps/admin-server/src/pages/projects/[project]/duplicate.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { PageLayout } from '../../../components/ui/page-layout'; import { Button } from '../../../components/ui/button'; import { useRouter } from 'next/router'; @@ -29,6 +29,7 @@ export default function ProjectDuplicate() { const router = useRouter(); const { project } = router.query; const { data, isLoading } = useProject(); + const [errors, setErrors] = useState([]); const defaults = useCallback( () => ({ @@ -48,6 +49,27 @@ export default function ProjectDuplicate() { if (!data) return null; + async function fetchData( url: string) { + let response = await fetch(url) || []; + if (response.ok) { + let data = await response.json(); + + data = Array.isArray(data) ? data.map((item) => { + if (item.deletedAt) { + return null; + } + delete item.projectId; + + item.originalId = item.id; + delete item.id; + + return item; + }) : []; + + return data; + } + } + async function duplicate(values: z.infer) { const duplicateData = { areaId: data.areaId, @@ -56,17 +78,66 @@ export default function ProjectDuplicate() { hostStatus: data.hostStatus, name: values.name, title: data.title, + widgets: [], + tags: [], + statuses: [], + resources: [], + resourceSettings: false, }; - await fetch(`/api/openstad/api/project`, { + const widgets = await fetchData(`/api/openstad/api/project/${data.id}/widgets`); + duplicateData.widgets = widgets; + + const tags = await fetchData(`/api/openstad/api/project/${data.id}/tag`); + duplicateData.tags = tags; + + const statuses = await fetchData(`/api/openstad/api/project/${data.id}/status`); + duplicateData.statuses = statuses; + + const resources = await fetchData(`/api/openstad/api/project/${data.id}/resource?includeTags=1&includeStatus=1`); + duplicateData.resources = resources; + + duplicateData.resourceSettings = duplicateData?.config?.resources || {}; + + if (Array.isArray(resources) && resources.length > 0) { + // Set the canAddNewResources to true to prevent the API from returning an error + duplicateData.config = duplicateData.config || {}; + duplicateData.config.resources = duplicateData.config.resources || {}; + duplicateData.config.resources.canAddNewResources = true; + + // Set min and max for title, description and summary to prevent the API from returning an error + duplicateData.config.resources.titleMaxLength = 10000; + duplicateData.config.resources.titleMinLength = 1; + duplicateData.config.resources.summaryMaxLength = 10000; + duplicateData.config.resources.summaryMinLength = 1; + duplicateData.config.resources.descriptionMaxLength = 10000; + duplicateData.config.resources.descriptionMinLength = 1; + } + + const response = await fetch(`/api/openstad/api/project`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(duplicateData), }); - toast.success('Er is een kopie van het project aangemaakt.') - router.push('/projects'); + + if (response.ok) { + const newId = await response.json(); + toast.success('Er is een kopie van het project aangemaakt. Je wordt nu doorgestuurd naar de projecten pagina.', { + duration: 5000 + }); + + setTimeout(() => { + if (newId) { + router.push(`/projects/${newId}`); + } + }, 4000); + } else { + const errorData = await response.json(); + setErrors(errorData.errors || ['Er is een fout opgetreden bij het dupliceren van het project.']); + toast.error('Er is een fout opgetreden bij het dupliceren van het project.'); + } } return ( @@ -118,6 +189,15 @@ export default function ProjectDuplicate() { Opslaan + {errors.length > 0 && ( +
+
    + {errors.map((error, index) => ( +
  • {error}
  • + ))} +
+
+ )}
diff --git a/apps/api-server/src/routes/api/project.js b/apps/api-server/src/routes/api/project.js index a7bafcf78..b12108445 100755 --- a/apps/api-server/src/routes/api/project.js +++ b/apps/api-server/src/routes/api/project.js @@ -21,6 +21,7 @@ const widgetDefinitions = getWidgetSettings(); const createError = require('http-errors'); let router = express.Router({mergeParams: true}); +const {Op} = require("sequelize"); async function getProject(req, res, next, include = []) { const projectId = req.params.projectId; @@ -143,11 +144,24 @@ router.route('/') .post(auth.can('Project', 'create')) .post(removeProtocolFromUrl) .post(async function (req, res, next) { + req.widgets = req.body.widgets || []; + req.tags = req.body.tags || []; + req.statuses = req.body.statuses || []; + req.resources = req.body.resources || []; + req.resourceSettings = req.body.resourceSettings || {}; + + delete req.body.widgets; + delete req.body.tags; + delete req.body.statuses; + delete req.body.resources; + delete req.body.resourceSettings; + // create an oauth client if nessecary let project = { config: req.body.config || {} }; try { + project.name = project?.name || req.body?.name || ''; let providers = await authSettings.providers({ project, useOnlyDefinedOnProject: true }); let providersDone = []; for (let provider of providers) { @@ -179,6 +193,7 @@ router.route('/') .create({ emailConfig: {}, ...req.body }) .then(result => { req.results = result; + req.projectId = result.id; return checkHostStatus({id: result.id}); }) @@ -188,6 +203,162 @@ router.route('/') }) .catch(next) }) + .post(async function (req, res, next) { + const errors = []; + + try { + req.query.nomail = true; + + // Create tags + const tagMap = {}; + for (const tag of req.tags) { + try { + const newTag = await db.Tag.create({ ...tag, projectId: req.projectId }); + tagMap[tag.originalId] = newTag.id; + } catch (error) { + errors.push({ step: 'Create tags', error: error.message }); + } + } + + // Create statuses + const statusMap = {}; + for (const status of req.statuses) { + try { + const newStatus = await db.Status.create({ ...status, projectId: req.projectId }); + statusMap[status.originalId] = newStatus.id; + } catch (error) { + errors.push({ step: 'Create statuses', error: error.message }); + } + } + + // Create new widgets and store their new IDs + const widgetMap = {}; + const newWidgets = []; + for (const widget of req.widgets) { + try { + const newWidget = await db.Widget.create({ ...widget, projectId: req.projectId }); + widgetMap[widget.originalId] = newWidget.id; + newWidgets.push(newWidget); + } catch (error) { + errors.push({ step: 'Create widgets', error: error.message }); + } + } + + // Create resources + const resourceMap = {}; + for (const resource of req.resources) { + try { + const updateWidgetIds = (singleResource) => { + for (const key in singleResource) { + if (typeof singleResource[key] === 'object' && singleResource[key] !== null) { + updateWidgetIds(singleResource[key]); + } else if (key === "widgetId") { + singleResource[key] = widgetMap[singleResource[key]]; + } + } + return singleResource; + } + + const updatedResource = updateWidgetIds(resource); + + const newResource = await db.Resource.create({ ...updatedResource, projectId: req.projectId }); + resourceMap[resource.originalId] = newResource.id; + + if (resource.tags) { + const validTagIds = await getValidTags(req.projectId, resource.tags.map(tag => tagMap[tag.id])); + await newResource.setTags(validTagIds); + } + if (resource.statuses) { + const validStatusIds = await getValidStatuses(req.projectId, resource.statuses.map(status => statusMap[status.id])); + await newResource.setStatuses(validStatusIds); + } + } catch (error) { + errors.push({ step: 'Create resources', error: error.message }); + } + } + + // Update widget IDs in widget config + const updateWidgetIds = (obj) => { + for (const key in obj) { + if (typeof obj[key] === 'object' && obj[key] !== null) { + updateWidgetIds(obj[key]); + } else { + if (key === 'projectId') { + obj[key] = req.projectId; + } + if (key === 'resourceId') { + obj[key] = resourceMap[obj[key]]; + } + if (key.includes('tag') || key.includes('Tag')) { + if (obj[key]) { + let tagValue = typeof obj[key] === 'number' ? obj[key].toString() : obj[key]; + if (typeof tagValue === 'string' && tagValue !== '') { + tagValue = tagValue.split(',').map(id => tagMap[id] || id).join(','); + obj[key] = tagValue; + } + } + } + + if (key.includes('status') || key.includes('Status')) { + if (obj[key]) { + let statusValue = typeof obj[key] === 'number' ? obj[key].toString() : obj[key]; + if (typeof statusValue === 'string' && statusValue !== '') { + statusValue = statusValue.split(',').map(id => statusMap[id] || id).join(','); + obj[key] = statusValue; + } + } + } + if (key === 'choiceguideWidgetId') { + obj[key] = widgetMap[obj[key]]; + } + } + } + }; + + // Update widget function + async function updateWidget(widgetId, updatedData) { + try { + await db.Widget.update(updatedData, { + where: { id: widgetId } + }); + } catch (error) { + errors.push({ step: 'Update widget', error: error.message }); + } + } + + // Update widget IDs in new widgets + for (const widget of newWidgets) { + try { + const updatedData = { config: widget.config }; + updateWidgetIds(updatedData); + await updateWidget(widget.id, updatedData); + } catch (error) { + errors.push({ step: 'Update widget IDs in new widgets', error: error.message }); + } + } + + // Revert the config resource settings + try { + const project = await db.Project.findOne({ where: { id: req.projectId } }); + const newConfig = project?.config || {}; + newConfig.resources = req.resourceSettings || {}; + + await project.update({ config: newConfig }); + } catch (error) { + errors.push({ step: 'Revert config resource settings', error: error.message }); + } + + if (errors.length > 0) { + return res.status(500).json({ errors }); + } + + res.json(req.projectId); + next(); + } catch (error) { + errors.push({ step: 'Overall', error: error.message }); + res.status(500).json({ errors }); + } + }) .post(async function (req, res, next) { let publisher = await messageStreaming.getPublisher(); if (publisher) { @@ -493,5 +664,25 @@ router.route('/:projectId(\\d+)/widget-css/:widgetType') res.send(css); }); +async function getValidStatuses(projectId, statuses) { + const uniqueIds = Array.from(new Set(statuses)); + + const statusesOfProject = await db.Status.findAll({ + where: { projectId, id: { [Op.in]: uniqueIds } }, + }); + + return statusesOfProject; +} + +async function getValidTags(projectId, tags) { + const uniqueIds = Array.from(new Set(tags)); + const validTags = await db.Tag.findAll({ + where: { + id: uniqueIds, + projectId: projectId, + }, + }); + return validTags.map(tag => tag.id); +} module.exports = router; From eb9604c7a4f25fda601025f360cad737520106d2 Mon Sep 17 00:00:00 2001 From: iandebruin98 Date: Fri, 25 Oct 2024 16:47:00 +0200 Subject: [PATCH 2/5] fix: Use right url --- apps/admin-server/src/pages/projects/[project]/duplicate.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/admin-server/src/pages/projects/[project]/duplicate.tsx b/apps/admin-server/src/pages/projects/[project]/duplicate.tsx index 994aba44b..de6b02442 100644 --- a/apps/admin-server/src/pages/projects/[project]/duplicate.tsx +++ b/apps/admin-server/src/pages/projects/[project]/duplicate.tsx @@ -130,7 +130,7 @@ export default function ProjectDuplicate() { setTimeout(() => { if (newId) { - router.push(`/projects/${newId}`); + router.push(`/projects/${newId}/widgets`); } }, 4000); } else { From 24b7fce879accfb120ada25812ed6ae296a66c15 Mon Sep 17 00:00:00 2001 From: iandebruin98 Date: Mon, 28 Oct 2024 09:33:35 +0100 Subject: [PATCH 3/5] fix: Replace ternary with guard clause --- .../src/pages/projects/[project]/duplicate.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/apps/admin-server/src/pages/projects/[project]/duplicate.tsx b/apps/admin-server/src/pages/projects/[project]/duplicate.tsx index de6b02442..0bdfca841 100644 --- a/apps/admin-server/src/pages/projects/[project]/duplicate.tsx +++ b/apps/admin-server/src/pages/projects/[project]/duplicate.tsx @@ -54,19 +54,18 @@ export default function ProjectDuplicate() { if (response.ok) { let data = await response.json(); - data = Array.isArray(data) ? data.map((item) => { + if (!Array.isArray(data)) { + return data; + } + return data.map((item) => { if (item.deletedAt) { return null; } delete item.projectId; - item.originalId = item.id; delete item.id; - return item; - }) : []; - - return data; + }) } } From 51c935847e40a6e6591bf48d00d59cbb9e45f455 Mon Sep 17 00:00:00 2001 From: iandebruin98 Date: Mon, 28 Oct 2024 10:06:16 +0100 Subject: [PATCH 4/5] fix: Refactor project duplication by dividing everything into a separate function --- apps/api-server/src/routes/api/project.js | 283 ++++++++++++---------- 1 file changed, 151 insertions(+), 132 deletions(-) diff --git a/apps/api-server/src/routes/api/project.js b/apps/api-server/src/routes/api/project.js index b12108445..a5e012a4f 100755 --- a/apps/api-server/src/routes/api/project.js +++ b/apps/api-server/src/routes/api/project.js @@ -58,6 +58,151 @@ async function getProject(req, res, next, include = []) { } } +// Function to create tags +async function createTags(req, tagMap, errors) { + for (const tag of req.tags) { + try { + const newTag = await db.Tag.create({ ...tag, projectId: req.projectId }); + tagMap[tag.originalId] = newTag.id; + } catch (error) { + errors.push({ step: 'Create tags', error: error.message }); + } + } +} + +// Function to create statuses +async function createStatuses(req, statusMap, errors) { + for (const status of req.statuses) { + try { + const newStatus = await db.Status.create({ ...status, projectId: req.projectId }); + statusMap[status.originalId] = newStatus.id; + } catch (error) { + errors.push({ step: 'Create statuses', error: error.message }); + } + } +} + +// Function to create widgets +async function createWidgets(req, widgetMap, newWidgets, errors) { + for (const widget of req.widgets) { + try { + const newWidget = await db.Widget.create({ ...widget, projectId: req.projectId }); + widgetMap[widget.originalId] = newWidget.id; + newWidgets.push(newWidget); + } catch (error) { + errors.push({ step: 'Create widgets', error: error.message }); + } + } +} + +// Function to create resources +async function createResources(req, resourceMap, widgetMap, tagMap, statusMap, errors) { + for (const resource of req.resources) { + try { + const updateWidgetIds = (singleResource) => { + for (const key in singleResource) { + if (typeof singleResource[key] === 'object' && singleResource[key] !== null) { + updateWidgetIds(singleResource[key]); + } else if (key === "widgetId") { + singleResource[key] = widgetMap[singleResource[key]]; + } + } + return singleResource; + } + + const updatedResource = updateWidgetIds(resource); + + const newResource = await db.Resource.create({ ...updatedResource, projectId: req.projectId }); + resourceMap[resource.originalId] = newResource.id; + + if (resource.tags) { + const validTagIds = await getValidTags(req.projectId, resource.tags.map(tag => tagMap[tag.id])); + await newResource.setTags(validTagIds); + } + if (resource.statuses) { + const validStatusIds = await getValidStatuses(req.projectId, resource.statuses.map(status => statusMap[status.id])); + await newResource.setStatuses(validStatusIds); + } + } catch (error) { + errors.push({ step: 'Create resources', error: error.message }); + } + } +} + +// Function to update widget IDs in an object +function updateWidgetIds(obj, widgetMap, resourceMap, tagMap, statusMap, projectId) { + for (const key in obj) { + if (typeof obj[key] === 'object' && obj[key] !== null) { + updateWidgetIds(obj[key], widgetMap, resourceMap, tagMap, statusMap, projectId); + } else { + if (key === 'projectId') { + obj[key] = projectId; + } + if (key === 'resourceId') { + obj[key] = resourceMap[obj[key]]; + } + if (key.includes('tag') || key.includes('Tag')) { + if (obj[key]) { + let tagValue = typeof obj[key] === 'number' ? obj[key].toString() : obj[key]; + if (typeof tagValue === 'string' && tagValue !== '') { + tagValue = tagValue.split(',').map(id => tagMap[id] || id).join(','); + obj[key] = tagValue; + } + } + } + if (key.includes('status') || key.includes('Status')) { + if (obj[key]) { + let statusValue = typeof obj[key] === 'number' ? obj[key].toString() : obj[key]; + if (typeof statusValue === 'string' && statusValue !== '') { + statusValue = statusValue.split(',').map(id => statusMap[id] || id).join(','); + obj[key] = statusValue; + } + } + } + if (key === 'choiceguideWidgetId') { + obj[key] = widgetMap[obj[key]]; + } + } + } +} + +// Function to update widget +async function updateWidget(widgetId, updatedData, errors) { + try { + await db.Widget.update(updatedData, { + where: { id: widgetId } + }); + } catch (error) { + errors.push({ step: 'Update widget', error: error.message }); + } +} + +// Function to update widget IDs in new widgets +async function updateWidgetIdsInNewWidgets(newWidgets, widgetMap, resourceMap, tagMap, statusMap, projectId, errors) { + for (const widget of newWidgets) { + try { + const updatedData = { config: widget.config }; + updateWidgetIds(updatedData, widgetMap, resourceMap, tagMap, statusMap, projectId); + await updateWidget(widget.id, updatedData, errors); + } catch (error) { + errors.push({ step: 'Update widget IDs in new widgets', error: error.message }); + } + } +} + +// Function to revert the config resource settings +async function revertConfigResourceSettings(req, errors) { + try { + const project = await db.Project.findOne({ where: { id: req.projectId } }); + const newConfig = project?.config || {}; + newConfig.resources = req.resourceSettings || {}; + + await project.update({ config: newConfig }); + } catch (error) { + errors.push({ step: 'Revert config resource settings', error: error.message }); + } +} + // scopes // ------ router @@ -209,144 +354,18 @@ router.route('/') try { req.query.nomail = true; - // Create tags const tagMap = {}; - for (const tag of req.tags) { - try { - const newTag = await db.Tag.create({ ...tag, projectId: req.projectId }); - tagMap[tag.originalId] = newTag.id; - } catch (error) { - errors.push({ step: 'Create tags', error: error.message }); - } - } - - // Create statuses const statusMap = {}; - for (const status of req.statuses) { - try { - const newStatus = await db.Status.create({ ...status, projectId: req.projectId }); - statusMap[status.originalId] = newStatus.id; - } catch (error) { - errors.push({ step: 'Create statuses', error: error.message }); - } - } - - // Create new widgets and store their new IDs const widgetMap = {}; const newWidgets = []; - for (const widget of req.widgets) { - try { - const newWidget = await db.Widget.create({ ...widget, projectId: req.projectId }); - widgetMap[widget.originalId] = newWidget.id; - newWidgets.push(newWidget); - } catch (error) { - errors.push({ step: 'Create widgets', error: error.message }); - } - } - - // Create resources const resourceMap = {}; - for (const resource of req.resources) { - try { - const updateWidgetIds = (singleResource) => { - for (const key in singleResource) { - if (typeof singleResource[key] === 'object' && singleResource[key] !== null) { - updateWidgetIds(singleResource[key]); - } else if (key === "widgetId") { - singleResource[key] = widgetMap[singleResource[key]]; - } - } - return singleResource; - } - - const updatedResource = updateWidgetIds(resource); - - const newResource = await db.Resource.create({ ...updatedResource, projectId: req.projectId }); - resourceMap[resource.originalId] = newResource.id; - - if (resource.tags) { - const validTagIds = await getValidTags(req.projectId, resource.tags.map(tag => tagMap[tag.id])); - await newResource.setTags(validTagIds); - } - if (resource.statuses) { - const validStatusIds = await getValidStatuses(req.projectId, resource.statuses.map(status => statusMap[status.id])); - await newResource.setStatuses(validStatusIds); - } - } catch (error) { - errors.push({ step: 'Create resources', error: error.message }); - } - } - - // Update widget IDs in widget config - const updateWidgetIds = (obj) => { - for (const key in obj) { - if (typeof obj[key] === 'object' && obj[key] !== null) { - updateWidgetIds(obj[key]); - } else { - if (key === 'projectId') { - obj[key] = req.projectId; - } - if (key === 'resourceId') { - obj[key] = resourceMap[obj[key]]; - } - if (key.includes('tag') || key.includes('Tag')) { - if (obj[key]) { - let tagValue = typeof obj[key] === 'number' ? obj[key].toString() : obj[key]; - if (typeof tagValue === 'string' && tagValue !== '') { - tagValue = tagValue.split(',').map(id => tagMap[id] || id).join(','); - obj[key] = tagValue; - } - } - } - - if (key.includes('status') || key.includes('Status')) { - if (obj[key]) { - let statusValue = typeof obj[key] === 'number' ? obj[key].toString() : obj[key]; - if (typeof statusValue === 'string' && statusValue !== '') { - statusValue = statusValue.split(',').map(id => statusMap[id] || id).join(','); - obj[key] = statusValue; - } - } - } - if (key === 'choiceguideWidgetId') { - obj[key] = widgetMap[obj[key]]; - } - } - } - }; - - // Update widget function - async function updateWidget(widgetId, updatedData) { - try { - await db.Widget.update(updatedData, { - where: { id: widgetId } - }); - } catch (error) { - errors.push({ step: 'Update widget', error: error.message }); - } - } - // Update widget IDs in new widgets - for (const widget of newWidgets) { - try { - const updatedData = { config: widget.config }; - updateWidgetIds(updatedData); - await updateWidget(widget.id, updatedData); - } catch (error) { - errors.push({ step: 'Update widget IDs in new widgets', error: error.message }); - } - } - - // Revert the config resource settings - try { - const project = await db.Project.findOne({ where: { id: req.projectId } }); - const newConfig = project?.config || {}; - newConfig.resources = req.resourceSettings || {}; - - await project.update({ config: newConfig }); - } catch (error) { - errors.push({ step: 'Revert config resource settings', error: error.message }); - } + await createTags(req, tagMap, errors); + await createStatuses(req, statusMap, errors); + await createWidgets(req, widgetMap, newWidgets, errors); + await createResources(req, resourceMap, widgetMap, tagMap, statusMap, errors); + await updateWidgetIdsInNewWidgets(newWidgets, widgetMap, resourceMap, tagMap, statusMap, req.projectId, errors); + await revertConfigResourceSettings(req, errors); if (errors.length > 0) { return res.status(500).json({ errors }); From e5b14c913a32d314a58b52d4257e7ee20ea1a602 Mon Sep 17 00:00:00 2001 From: iandebruin98 Date: Mon, 28 Oct 2024 10:26:00 +0100 Subject: [PATCH 5/5] fix: Refactor `fetchData` function to ensure it returns an array --- .../pages/projects/[project]/duplicate.tsx | 47 +++++++++++++------ 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/apps/admin-server/src/pages/projects/[project]/duplicate.tsx b/apps/admin-server/src/pages/projects/[project]/duplicate.tsx index 0bdfca841..edf496f65 100644 --- a/apps/admin-server/src/pages/projects/[project]/duplicate.tsx +++ b/apps/admin-server/src/pages/projects/[project]/duplicate.tsx @@ -51,26 +51,43 @@ export default function ProjectDuplicate() { async function fetchData( url: string) { let response = await fetch(url) || []; - if (response.ok) { - let data = await response.json(); - if (!Array.isArray(data)) { - return data; - } - return data.map((item) => { - if (item.deletedAt) { - return null; - } - delete item.projectId; - item.originalId = item.id; - delete item.id; - return item; - }) + if (!response.ok) { + return []; + } + + let data = await response.json(); + + if (!Array.isArray(data)) { + return []; } + return data.map((item) => { + if (item.deletedAt) { + return null; + } + delete item.projectId; + item.originalId = item.id; + delete item.id; + return item; + }) } + type DuplicateData = { + areaId: number; + config: any; + emailConfig: any; + hostStatus: any; + name: string; + title: string; + widgets: any[]; + tags: any[]; + statuses: any[]; + resources: any[]; + resourceSettings: boolean; + }; + async function duplicate(values: z.infer) { - const duplicateData = { + const duplicateData: DuplicateData = { areaId: data.areaId, config: data.config, emailConfig: data.emailConfig,