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

CaptainFact / captain-fact-frontend / 20191038404

13 Dec 2025 10:53AM UTC coverage: 3.197% (+0.3%) from 2.865%
20191038404

push

github

Betree
iterate

38 of 2113 branches covered (1.8%)

Branch coverage included in aggregate %.

0 of 243 new or added lines in 23 files covered. (0.0%)

195 existing lines in 20 files now uncovered.

125 of 2985 relevant lines covered (4.19%)

0.12 hits per line

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

0.0
/app/components/UsersActions/ActionDiff.jsx
1
import { diffWordsWithSpace } from 'diff'
2
import { startCase } from 'lodash'
3
import React, { PureComponent } from 'react'
4
import { Link } from 'react-router-dom'
5

6
import {
7
  ACTION_DELETE,
8
  ACTION_REMOVE,
9
  ACTION_RESTORE,
10
  ENTITY_SPEAKER,
11
  ENTITY_STATEMENT,
12
} from '../../constants'
13
import { speakerURL } from '../../lib/cf_routes'
14
import formatSeconds from '../../lib/seconds_formatter'
15
import { getEntityIdKey } from '../../lib/user_action_entity_id'
16
import ExternalLinkNewTab from '../Utils/ExternalLinkNewTab'
17

18
class ActionDiff extends PureComponent {
19
  render() {
NEW
20
    const allActions = this.props.allActions || [this.props.action]
×
21
    const diff = this.generateDiff(allActions, this.props.action)
×
NEW
22
    if (Object.keys(diff).length === 0) {
×
23
      return null
×
24
    }
25

26
    return (
×
27
      <div className="p-3 text-left text-sm bg-slate-800 shadow-[inset_0px_2px_4px_0px_rgba(0,0,0,0.3)] rounded-sm">
28
        {Object.entries(diff).map(([key, changes]) => (
29
          <div key={key} className="mb-3 last:mb-0">
×
30
            <div className="inline-block mr-2 font-medium min-w-[70px] text-amber-400 align-top text-xs uppercase tracking-wide">
31
              {startCase(this.formatChangeKey(key))}&nbsp;
32
            </div>
33
            <span className="inline-block max-w-full whitespace-pre-wrap break-words">
34
              {this.renderKeyDiff(key, changes)}
35
            </span>
36
          </div>
37
        ))}
38
      </div>
39
    )
40
  }
41

42
  renderKeyDiff(key, changes) {
43
    // Value completely changed, show it like prev -> new
NEW
44
    if (changes.length === 2 && changes[0].removed && changes[1].added) {
×
45
      return (
×
46
        <div>
47
          <span className="px-1 py-0.5 bg-red-900/50 line-through opacity-60">
48
            {this.formatChangeValue(changes[0].value, key)}
49
          </span>
50
          <span className="mx-1 text-slate-400">→</span>
51
          <span className="px-1 py-0.5 bg-emerald-800/50">
52
            {this.formatChangeValue(changes[1].value, key)}
53
          </span>
54
        </div>
55
      )
56
    }
57
    // Generate a real diff
58
    return changes.map((change, idx) => (
×
59
      <span
×
60
        key={idx}
61
        className={`text-slate-200 ${
62
          change.added
×
63
            ? 'px-1 py-0.5 bg-emerald-800/50'
64
            : change.removed
×
65
              ? 'px-1 py-0.5 bg-red-900/50 line-through opacity-60 whitespace-normal'
66
              : ''
67
        }`}
68
      >
69
        {this.formatChangeValue(change.value, key)}
70
      </span>
71
    ))
72
  }
73

74
  formatChangeKey(key) {
75
    return key.replace('_id', '').replace('_', ' ')
×
76
  }
77

78
  formatChangeValue(value, key) {
79
    if (key === 'speaker_id' && value) {
×
80
      return (
×
81
        <Link to={speakerURL(value)} className="text-white underline">
82
          #{value}
83
        </Link>
84
      )
85
    } else if (['is_draft', 'unlisted'].includes(key) && !value) {
×
86
      return 'No'
×
87
    } else if (typeof value === 'boolean') {
×
88
      return value ? 'Yes' : 'No'
×
89
    }
90

91
    return value
×
92
  }
93

94
  generateDiff(allActions, action) {
95
    // Filter to get only actions referencing the same entity
96
    const idKey = getEntityIdKey(action.entity)
×
97
    const entityActions = !idKey
×
98
      ? []
99
      : allActions.filter((a) => {
100
          return a.entity === action.entity && a[idKey] === action[idKey]
×
101
        })
102

103
    // Get previous state
104
    const actionIdx = entityActions.findIndex((a) => a.id === action.id)
×
NEW
105
    let prevState = {}
×
NEW
106
    if (actionIdx + 1 < entityActions.length) {
×
UNCOV
107
      prevState = this.buildReferenceEntity(entityActions.slice(actionIdx + 1))
×
108
    }
109

110
    // Build changes object like key: [diffs]
NEW
111
    const diff = {}
×
NEW
112
    const actionChanges = this.getActionChanges(action, prevState)
×
NEW
113
    for (const [key, newValue] of Object.entries(actionChanges)) {
×
NEW
114
      const valueDiff = this.diffEntry(key, prevState[key], newValue)
×
NEW
115
      diff[key] = valueDiff
×
116
    }
NEW
117
    return diff
×
118
  }
119

120
  getActionChanges(action, prevState) {
121
    if ([ACTION_DELETE, ACTION_REMOVE].includes(action.type)) {
×
NEW
122
      const result = {}
×
NEW
123
      for (const key in prevState) {
×
NEW
124
        result[key] = null
×
125
      }
NEW
126
      return result
×
127
    }
128
    if (action.type === ACTION_RESTORE) {
×
129
      return prevState
×
130
    }
131
    // Parse JSON string if needed
NEW
132
    try {
×
NEW
133
      if (typeof action.changes === 'string') {
×
NEW
134
        return JSON.parse(action.changes) || {}
×
135
      }
NEW
136
      return action.changes || {}
×
137
    } catch (e) {
138
      // If parsing fails, return empty object
NEW
139
      return {}
×
140
    }
141
  }
142

143
  completeReference(reference, actions, keysToStore) {
144
    // Let's look for the most recent entries
NEW
145
    for (const action of actions) {
×
146
      // Parse JSON string if needed
147
      let changes
NEW
148
      try {
×
NEW
149
        if (typeof action.changes === 'string') {
×
NEW
150
          changes = JSON.parse(action.changes) || {}
×
151
        } else {
NEW
152
          changes = action.changes || {}
×
153
        }
154
      } catch (e) {
155
        // If parsing fails, use empty object
NEW
156
        changes = {}
×
157
      }
NEW
158
      if (Object.keys(changes).length === 0) {
×
NEW
159
        continue
×
160
      }
NEW
161
      for (let idx = keysToStore.length - 1; idx >= 0; idx--) {
×
NEW
162
        const key = keysToStore[idx]
×
NEW
163
        if (!(key in changes)) {
×
UNCOV
164
          continue
×
165
        }
166
        // Yihaa ! Changes contains a value for key
NEW
167
        reference[key] = changes[key]
×
NEW
168
        keysToStore.splice(idx, 1)
×
NEW
169
        if (keysToStore.length === 0) {
×
NEW
170
          return reference
×
171
        }
172
      }
173
    }
NEW
174
    return reference
×
175
  }
176

177
  buildReferenceEntity(actions, base = null) {
×
NEW
178
    const entity = actions[0].entity
×
179
    if (entity === ENTITY_STATEMENT) {
×
180
      return this.buildReferenceStatement(actions, base)
×
181
    }
182
    if (entity === ENTITY_SPEAKER) {
×
183
      return this.buildReferenceSpeaker(actions, base)
×
184
    }
NEW
185
    return {}
×
186
  }
187

188
  buildReferenceStatement(actions, base = null) {
×
189
    if (!base) {
×
NEW
190
      base = { id: actions[actions.length - 1].statementId }
×
191
    }
192
    return this.completeReference(base, actions, ['text', 'time', 'speaker_id'])
×
193
  }
194

195
  buildReferenceSpeaker(actions, base = null) {
×
196
    if (!base) {
×
NEW
197
      base = { id: actions[0].speakerId }
×
198
    }
199
    return this.completeReference(base, actions, ['full_name', 'title'])
×
200
  }
201

202
  diffEntry(key, prevValue, newValue) {
203
    if (!prevValue && newValue) {
×
204
      return [{ added: true, value: this.formatValue(key, newValue) }]
×
205
    }
206

207
    // Format numbers like prevNumber -> newNumber
208
    if (typeof newValue === 'number') {
×
209
      prevValue = this.formatValue(key, prevValue)
×
210
      newValue = this.formatValue(key, newValue)
×
211

212
      // Generate diff
213
      if (prevValue) {
×
214
        return [
×
215
          { removed: true, value: prevValue },
216
          { added: true, value: newValue },
217
        ]
218
      }
219
      return [{ added: true, value: newValue }]
×
220
    }
221
    // Do a string diff
222
    return diffWordsWithSpace((prevValue || '').toString(), newValue ? newValue.toString() : '')
×
223
  }
224

225
  formatValue(key, value) {
226
    if (!value) {
×
227
      return value
×
228
    }
229
    // Format time like 0:42 -> 1:35
230
    if (key === 'time') {
×
231
      return formatSeconds(value)
×
232
    }
233
    if (key === 'source' || key === 'url') {
×
234
      return (
×
235
        <ExternalLinkNewTab className="text-neutral-200 underline" href={value}>
236
          {value}
237
        </ExternalLinkNewTab>
238
      )
239
    }
240
    return value
×
241
  }
242
}
243

244
export default ActionDiff
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