diff --git a/src/services/BoardApi.js b/src/services/BoardApi.js index a6551b56d..29d8e72d6 100644 --- a/src/services/BoardApi.js +++ b/src/services/BoardApi.js @@ -7,6 +7,10 @@ import axios from '@nextcloud/axios' import { generateOcsUrl, generateUrl } from '@nextcloud/router' import '../models/index.js' +export const formatCsvList = (items, getValue) => items + .map(item => getValue(item).replaceAll('"', '""')) + .join(', ') + /** * This class handles all the api communication with the Deck backend. */ @@ -201,23 +205,9 @@ export class BoardApi { } else if (field === 'stackId') { row += '"' + stack.title.replaceAll('"', '""') + '"' + '\t' } else if (field === 'labels') { - row += '"' - card[field].forEach(label => { - row += label.title.replaceAll('"', '""') + ', ' - }) - if (card[field].length > 0) { - row = row.slice(0, -1) - } - row += '"' + '\t' + row += '"' + formatCsvList(card[field], label => label.title) + '"' + '\t' } else if (field === 'assignedUsers') { - row += '"' - card[field].forEach(assignedUsers => { - row += assignedUsers.participant.displayname.replaceAll('"', '""') + ', ' - }) - if (card[field].length > 0) { - row = row.slice(0, -1) - } - row += '"' + '\t' + row += '"' + formatCsvList(card[field], assignment => assignment.participant.displayname) + '"' + '\t' } else if (field === 'description' || field === 'title') { row += '"' + card[field].replaceAll('"', '""') + '"' + '\t' } else { diff --git a/src/services/BoardApi.spec.js b/src/services/BoardApi.spec.js new file mode 100644 index 000000000..b3fc83592 --- /dev/null +++ b/src/services/BoardApi.spec.js @@ -0,0 +1,32 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { formatCsvList } from './BoardApi.js' + +jest.mock('@nextcloud/axios', () => ({})) + +jest.mock('@nextcloud/router', () => ({ + generateOcsUrl: jest.fn(url => url), + generateUrl: jest.fn(url => url), +})) + +describe('formatCsvList', () => { + it('joins values without a trailing separator', () => { + const labels = [{ title: 'Backend' }, { title: 'Urgent' }] + + expect(formatCsvList(labels, label => label.title)).toBe('Backend, Urgent') + }) + + it('escapes double quotes in values', () => { + const assignments = [{ participant: { displayname: 'Jane "JJ" Doe' } }] + + expect(formatCsvList(assignments, assignment => assignment.participant.displayname)) + .toBe('Jane ""JJ"" Doe') + }) + + it('returns an empty string for an empty list', () => { + expect(formatCsvList([], label => label.title)).toBe('') + }) +})