Skip to content

Implement downloading and copying of attendee names and IDs#72

Draft
MattyTheHacker wants to merge 3 commits intomainfrom
add-attendee-download
Draft

Implement downloading and copying of attendee names and IDs#72
MattyTheHacker wants to merge 3 commits intomainfrom
add-attendee-download

Conversation

@MattyTheHacker
Copy link
Copy Markdown
Member

@MattyTheHacker MattyTheHacker commented Apr 30, 2026

Testing complete, appears to work

@MattyTheHacker MattyTheHacker self-assigned this Apr 30, 2026
Copilot AI review requested due to automatic review settings April 30, 2026 12:16
@MattyTheHacker MattyTheHacker added the enhancement New feature or request label Apr 30, 2026
@MattyTheHacker MattyTheHacker enabled auto-merge (squash) April 30, 2026 12:16
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an admin UI affordance on the Users page to export the current user list as a CSV for offline use/reporting.

Changes:

  • Added a downloadCSV helper that serializes data.users into a CSV and triggers a browser download.
  • Added a “Download CSV” button to the Users management panel header.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread web/src/routes/users/+page.svelte
Comment thread web/src/routes/users/+page.svelte
@MattyTheHacker MattyTheHacker requested a review from Copilot April 30, 2026 12:34
@MattyTheHacker MattyTheHacker changed the title Implement downloading of attendee names and IDs Implement downloading and copying of attendee names and IDs Apr 30, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +51 to +60
const copyCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`);
const csvContent = [headers.join(","), ...rows].join("\n");
navigator.clipboard.writeText(csvContent);
};

const downloadCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`);
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same CSV escaping/injection concerns apply here as in copyCSV: u.studentID is not escaped and formula-like prefixes (=, +, -, @) should be neutralized to avoid CSV injection when opened in spreadsheet applications.

Suggested change
const copyCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`);
const csvContent = [headers.join(","), ...rows].join("\n");
navigator.clipboard.writeText(csvContent);
};
const downloadCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`);
const escapeCSVField = (value: string) => {
const neutralizedValue = /^[=+\-@]/.test(value) ? `'${value}` : value;
return `"${neutralizedValue.replace(/"/g, '""')}"`;
};
const copyCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map((u) => `${escapeCSVField(u.studentID)},${escapeCSVField(u.name)}`);
const csvContent = [headers.join(","), ...rows].join("\n");
navigator.clipboard.writeText(csvContent);
};
const downloadCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map((u) => `${escapeCSVField(u.studentID)},${escapeCSVField(u.name)}`);

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +55
const copyCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`);
const csvContent = [headers.join(","), ...rows].join("\n");
navigator.clipboard.writeText(csvContent);
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

navigator.clipboard.writeText returns a Promise and can fail (permission/insecure context). As written, failures are ignored and the UI provides no feedback. Make copyCSV async, await the call, and handle errors (e.g., set $error or show a message).

Suggested change
const copyCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`);
const csvContent = [headers.join(","), ...rows].join("\n");
navigator.clipboard.writeText(csvContent);
const copyCSV = async () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`);
const csvContent = [headers.join(","), ...rows].join("\n");
try {
await navigator.clipboard.writeText(csvContent);
} catch (e) {
$error = e instanceof Error ? e : new Error("Failed to copy CSV to clipboard");
}

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +60
const copyCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`);
const csvContent = [headers.join(","), ...rows].join("\n");
navigator.clipboard.writeText(csvContent);
};

const downloadCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`);
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CSV generation currently only escapes quotes in u.name, but not u.studentID, and it does not mitigate CSV formula injection (values starting with =, +, -, @) which can execute formulas when opened in spreadsheet tools. Consider a shared CSV-escaping helper applied to both fields, including formula-injection hardening.

Suggested change
const copyCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`);
const csvContent = [headers.join(","), ...rows].join("\n");
navigator.clipboard.writeText(csvContent);
};
const downloadCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`);
const escapeCSVField = (value: string) => {
const hardenedValue = /^[=+\-@]/.test(value) ? `'${value}` : value;
return `"${hardenedValue.replace(/"/g, '""')}"`;
};
const copyCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map(
(u) => `${escapeCSVField(u.studentID)},${escapeCSVField(u.name)}`,
);
const csvContent = [headers.join(","), ...rows].join("\n");
navigator.clipboard.writeText(csvContent);
};
const downloadCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map(
(u) => `${escapeCSVField(u.studentID)},${escapeCSVField(u.name)}`,
);

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +61
const copyCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`);
const csvContent = [headers.join(","), ...rows].join("\n");
navigator.clipboard.writeText(csvContent);
};

const downloadCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`);
const csvContent = [headers.join(","), ...rows].join("\n");
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

downloadCSV duplicates the header/row building logic from copyCSV. Extracting a single helper to build the CSV string will avoid the two paths drifting (especially once escaping/sanitization is improved).

Suggested change
const copyCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`);
const csvContent = [headers.join(","), ...rows].join("\n");
navigator.clipboard.writeText(csvContent);
};
const downloadCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`);
const csvContent = [headers.join(","), ...rows].join("\n");
const buildUsersCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`);
return [headers.join(","), ...rows].join("\n");
};
const copyCSV = () => {
const csvContent = buildUsersCSV();
navigator.clipboard.writeText(csvContent);
};
const downloadCSV = () => {
const csvContent = buildUsersCSV();

Copilot uses AI. Check for mistakes.
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

URL.revokeObjectURL(url) is called immediately after link.click(). In some browsers this can revoke the blob URL before the download has started, causing intermittent download failures. Revoke the URL asynchronously (e.g., in a setTimeout/next tick) after the click.

Suggested change
URL.revokeObjectURL(url);
setTimeout(() => URL.revokeObjectURL(url), 0);

Copilot uses AI. Check for mistakes.
@MattyTheHacker MattyTheHacker marked this pull request as draft April 30, 2026 12:38
auto-merge was automatically disabled April 30, 2026 12:38

Pull request was converted to draft

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add ability to easily download / copy a list of all registered users and their student ID

2 participants