{"record":{"id":"0f359641054a0298","repo":"TryGhost/Ghost","slug":"a-view-with-this-name-already-exists","errorCode":null,"errorMessage":"A view with this name already exists","messagePattern":"A view with this name already exists","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"apps/admin/src/members/member-views.ts","lineNumber":91,"sourceCode":"}\n\nexport function buildViewsForSave(allViews: SharedView[], name: string, filter: string, originalView?: MemberView): SharedView[] {\n    const nextView = buildMemberView(name, filter);\n\n    if (originalView) {\n        const matchingIndexes = findMatchingSharedViewIndexes(allViews, originalView);\n\n        if (matchingIndexes.length === 0) {\n            throw new Error(VIEW_UPDATE_NOT_FOUND_ERROR);\n        }\n\n        if (matchingIndexes.length > 1) {\n            throw new Error(VIEW_UPDATE_AMBIGUOUS_ERROR);\n        }\n\n        const targetIndex = matchingIndexes[0];\n        if (hasSharedViewNameConflict(allViews, nextView, targetIndex)) {\n            throw new Error(VIEW_EXISTS_ERROR);\n        }\n\n        return allViews.map((view, index) => {\n            return index === targetIndex ? nextView : view;\n        });\n    }\n\n    if (hasSharedViewNameConflict(allViews, nextView)) {\n        throw new Error(VIEW_EXISTS_ERROR);\n    }\n\n    return [...allViews, nextView];\n}\n\nexport function buildViewsForDelete(allViews: SharedView[], view: MemberView): SharedView[] {\n    const matchingIndexes = findMatchingSharedViewIndexes(allViews, view);\n\n    if (matchingIndexes.length === 0) {","sourceCodeStart":73,"sourceCodeEnd":109,"githubUrl":"https://github.com/TryGhost/Ghost/blob/47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe/apps/admin/src/members/member-views.ts#L73-L109","documentation":"Thrown by buildViewsForSave (member-views.ts) when saving a member view whose name — case-insensitively trimmed and compared — collides with another saved view on the same 'members' route. There are two throw sites: line 91 (renaming an existing view to a name another view already has, with the target index excluded) and line 100 (creating a new view whose name duplicates an existing one). The check is normalizeSharedViewName (trim + lower-case).","triggerScenarios":"On create: hasSharedViewNameConflict(allViews, nextView) returns true — another view on route 'members' has the same trimmed/lowercased name. On update: hasSharedViewNameConflict(allViews, nextView, targetIndex) returns true — a view OTHER than the one being updated shares the normalized name. Examples: saving 'My View' when 'my view' exists; editing a view and renaming it to 'Drafts' while another 'drafts' view exists.","commonSituations":"User types a name that differs only by case or whitespace from an existing saved view; shared_views setting was imported/migrated containing near-duplicate names; two browser tabs editing views concurrently so the second save collides; copy-paste of a view name.","solutions":["Pre-check for conflict before calling buildViewsForSave using hasSharedViewNameConflict, and prompt the user to pick a distinct name.","If the duplicate is unintended, audit the stored shared_views setting for near-duplicate names (same after trim + lower-case) and deduplicate.","For concurrent-edit races, re-read the latest shared_views from the API immediately before saving and retry.","Strip leading/trailing whitespace and normalize case in the UI before submitting so the user sees the canonical form."],"exampleFix":"// before\nconst next = buildViewsForSave(allViews, name, filter, originalView); // throws on duplicate name\n\n// after — detect the conflict first and surface a user-facing message\nimport {hasSharedViewNameConflict} from './shared-views';\nconst nextView = {name: name.trim(), route: 'members' as const, filter: {filter}};\nif (hasSharedViewNameConflict(allViews, nextView, originalViewIndex)) {\n    setFieldError('name', 'A view with this name already exists');\n    return;\n}\nconst next = buildViewsForSave(allViews, name, filter, originalView);","handlingStrategy":"validation","validationCode":"// Pre-check for a name conflict before saving\nimport {hasSharedViewNameConflict} from './shared-views';\nimport type {SharedView} from './shared-views';\n\nfunction wouldConflict(allViews: SharedView[], name: string, excludeIndex?: number): boolean {\n    return hasSharedViewNameConflict(allViews, {name, route: 'members'}, excludeIndex);\n}","typeGuard":"// (No custom error class; detect by message.)\nfunction isViewExistsError(e: unknown): boolean {\n    return e instanceof Error && e.message === 'A view with this name already exists';\n}","tryCatchPattern":"try {\n    const next = buildViewsForSave(allViews, name, filter, originalView);\n} catch (e) {\n    if (e instanceof Error && e.message === 'A view with this name already exists') {\n        setFieldError('name', e.message);\n        return;\n    }\n    throw e;\n}","preventionTips":["Trim and case-normalise the view name in the UI before submit so the user sees the canonical form.","Re-read the latest shared_views from the API immediately before saving to avoid concurrent-edit collisions.","Pre-check with hasSharedViewNameConflict to give a inline field error before the save attempt.","Audit migrated/imported shared_views for near-duplicate names."],"tags":["members","views","validation","state","ghost"],"backgroundTag":null,"analyzedSha":"47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe","analyzedAt":"2026-08-13T01:25:26.651Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}