• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

scriptype / writ-cms / 28798924418

06 Jul 2026 02:26PM UTC coverage: 53.397% (-0.6%) from 53.975%
28798924418

push

github

scriptype
Implement delete attachment in updatePost

a delete button for each attachment &
a cancel button for each new selected file.

Deletions are processed together with the updatePost request. So a
deletedAttachments field is added into payload.data. Probably a separate
endpoint for attachments would be better. But it's a future thing.

To keep the file input updated after cancelling files, some manual DOM
plumbing is done.

Deletions are optimistically reflected on the UI after the prompt is
confirmed (e.g. the attachment disappears from screen). But then after
hitting save and before the post is done updating, the attachment
becomes visible again. That's because internal states are cleared too
eagerly, before waiting for request's resolve. Maybe fix that sometime

650 of 1221 branches covered (53.24%)

Branch coverage included in aggregate %.

0 of 45 new or added lines in 3 files covered. (0.0%)

1 existing line in 1 file now uncovered.

2242 of 4195 relevant lines covered (53.44%)

1557.27 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/cms/api/models/post.js
1
const { writeFile, mkdir, rename, rm } = require('fs/promises')
×
2
const { join, dirname, basename, relative } = require('path')
×
3
const matter = require('gray-matter')
×
4
const _ = require('lodash')
×
5
const { ['default']: filenamify } = require('filenamify')
×
6
const { unusedFilename } = require('unused-filename')
×
7
const { contentRootPath } = require('../helpers')
×
8

9
const replaceFilename = (oldAbsolutePath, newAbsolutePath) => {
×
10
  return join(
×
11
    dirname(oldAbsolutePath),
12
    basename(newAbsolutePath)
13
  )
14
}
15

16
const createPostModel = ({ getSettings, getContentModel }) => {
×
17
  const createPost = async (data, attachments) => {
×
18
    const opts = {
×
19
      taxonomyPath: data.taxonomyPath || [],
×
20
      title: data.title || 'Untitled',
×
21
      content: data.content || '',
×
22
      excerpt: data.excerpt || '',
×
23
      extension: data.extension || '.md',
×
24
      deletedAttachments: data.deletedAttachments || [],
×
25
      metadata: _(data)
26
        .omit(['taxonomyPath', 'title', 'content', 'excerpt', 'extension', 'deletedAttachments'])
27
        .pickBy(value => (!!value || value === 0))
×
28
        .value()
29
    }
30

31
    const { rootDirectory, contentDirectory } = getSettings()
×
32
    const root = await contentRootPath(rootDirectory, contentDirectory)
×
33

34
    const sanitizedTitle = filenamify(opts.title)
×
35

36
    const path = join(...[root].concat(opts.taxonomyPath).concat(sanitizedTitle))
×
37
    const unusedPath = await unusedFilename(path)
×
38

39
    const shouldOverrideTitle = (sanitizedTitle !== opts.title) || (unusedPath !== path)
×
40
    const metadataWithTitle = shouldOverrideTitle ?
×
41
      {
42
        ...opts.metadata,
43
        title: `${opts.title}`
44
      } : opts.metadata
45

46
    const fileContent = matter.stringify({
×
47
      data: metadataWithTitle,
48
      content: opts.content,
49
      excerpt: opts.excerpt
50
    })
51
    try {
×
52
      await mkdir(unusedPath, { recursive: true })
×
53
    } catch {}
54
    return Promise.all([
×
55
      writeFile(`${join(unusedPath, 'post')}${opts.extension}`, fileContent),
56
      ...attachments.map(file => {
57
        const dest = join(unusedPath, file.originalname)
×
58
        return writeFile(dest, file.buffer)
×
59
      })
60
    ])
61
  }
62

63
  const updatePost = async (path, data, attachments = []) => {
×
64
    if (!path) {
×
65
      throw new Error('path is required')
×
66
    }
67

68
    const opts = {
×
69
      title: data.title || 'Untitled',
×
70
      content: data.content || '',
×
71
      excerpt: data.excerpt || '',
×
72
      extension: data.extension || '',
×
73
      deletedAttachments: data.deletedAttachments || [],
×
74
      metadata: _(data)
75
        .omit(['title', 'content', 'excerpt', 'extension', 'deletedAttachments'])
76
        .pickBy(value => (!!value || value === 0))
×
77
        .value()
78
    }
79

80
    const collectionName = path.split('/')[0]
×
81
    const collection = getContentModel().subtree.collections.find(c => c.name === collectionName)
×
82
    const post = collection.subtree.posts.find(p => p.path === path)
×
83

84
    const isFoldered = !post.extension
×
85
    const isTitleDifferent = opts.title !== post.title
×
86
    let absolutePath = post.absolutePath
×
87
    let isPathDifferentThanTitle
88

89
    if (isTitleDifferent) {
×
90
      const sanitizedTitle = filenamify(opts.title)
×
91
      const sanitizedPath = replaceFilename(post.absolutePath, sanitizedTitle)
×
92
      const unusedPath = await unusedFilename(sanitizedPath)
×
93

94
      if (isFoldered) {
×
95
        await rename(post.absolutePath, unusedPath)
×
96
      } else {
97
        await rm(post.absolutePath)
×
98
      }
99

100
      isPathDifferentThanTitle = (sanitizedTitle !== opts.title) || (unusedPath !== sanitizedPath)
×
101
      absolutePath = isFoldered ? unusedPath : `${unusedPath}${opts.extension || post.extension}`
×
102
    }
103

104
    const metadataWithTitle = isPathDifferentThanTitle ? {
×
105
      ...opts.metadata,
106
      title: `${opts.title}`
107
    } : opts.metadata
108

109
    const fileContent = matter.stringify({
×
110
      data: metadataWithTitle,
111
      content: opts.content,
112
      excerpt: opts.excerpt
113
    })
114

115
    const targetPath = isFoldered ?
×
116
      join(absolutePath, post.indexFile.name) :
117
      absolutePath
118

NEW
119
    const deleteAttachments = () => {
×
NEW
120
      return opts.deletedAttachments.map(fileName => {
×
NEW
121
        const filePath = join(absolutePath, fileName)
×
NEW
122
        return rm(filePath)
×
123
      })
124
    }
125

NEW
126
    const uploadAttachments = () => {
×
NEW
127
      return attachments.map(file => {
×
128
        const dest = join(absolutePath, file.originalname)
×
129
        return writeFile(dest, file.buffer)
×
130
      })
131
    }
132

NEW
133
    await Promise.all([
×
134
      writeFile(targetPath, fileContent),
135
      (async () => {
NEW
136
        await Promise.all(deleteAttachments())
×
NEW
137
        await Promise.all(uploadAttachments())
×
138
      })()
139
    ])
140

141
    const { rootDirectory, contentDirectory } = getSettings()
×
142
    const root = await contentRootPath(rootDirectory, contentDirectory)
×
143

144
    return {
×
145
      path: relative(root, absolutePath)
146
    }
147
  }
148

149
  return {
×
150
    create: createPost,
151
    update: updatePost
152
  }
153
}
154

155
module.exports = createPostModel
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc