Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request 'fix: prevent [object Object] display for links in sheets' (#148) from fix/object-object-links into main

scott 41528b41 044332b9

+96 -1
+12
src/sheets/formulas.ts
··· 1764 1764 value = value.getTime(); 1765 1765 } 1766 1766 1767 + // Safety net: extract text from objects to prevent [object Object] display 1768 + if (typeof value === 'object' && value !== null && !(value instanceof Date)) { 1769 + if ('text' in value) value = (value as { text: string }).text; 1770 + else if ('hyperlink' in value) value = (value as { hyperlink: string }).hyperlink; 1771 + else if ('richText' in value) { 1772 + const parts = (value as { richText: Array<{ text: string }> }).richText; 1773 + value = Array.isArray(parts) ? parts.map(p => p.text || '').join('') : ''; 1774 + } else { 1775 + value = JSON.stringify(value); 1776 + } 1777 + } 1778 + 1767 1779 if (!format || format === 'auto') { 1768 1780 if (typeof value === 'number') { 1769 1781 return Number.isInteger(value) ? value.toString() : value.toFixed(2);
+7 -1
src/sheets/main.ts
··· 654 654 cell = new Y.Map(); 655 655 cells.set(id, cell); 656 656 } 657 - // Date objects don't survive Yjs serialization — store as timestamp 657 + // Ensure only primitives are stored — objects cause [object Object] display 658 658 let v = data.v; 659 659 if (v instanceof Date) v = v.getTime(); 660 + else if (typeof v === 'object' && v !== null) { 661 + if ('text' in v) v = v.text; 662 + else if ('hyperlink' in v) v = v.hyperlink; 663 + else if ('richText' in v && Array.isArray(v.richText)) v = v.richText.map((p: { text?: string }) => p.text || '').join(''); 664 + else v = String(v); 665 + } 660 666 if (v !== undefined) cell.set('v', v); 661 667 if (data.f !== undefined) cell.set('f', data.f); 662 668 if (data.s !== undefined) cell.set('s', JSON.stringify(data.s));
+10
src/sheets/xlsx-import.ts
··· 219 219 // Store dates as numeric timestamps — Date objects don't survive Yjs serialization 220 220 data.v = cell.value.getTime(); 221 221 if (!data.s.format) data.s.format = 'date'; 222 + } else if (typeof cell.value === 'object' && cell.value !== null && 'hyperlink' in cell.value) { 223 + // ExcelJS hyperlink cells: { text: 'Label', hyperlink: 'https://...' } 224 + data.v = (cell.value as { text?: string; hyperlink: string }).text || (cell.value as { hyperlink: string }).hyperlink; 225 + } else if (typeof cell.value === 'object' && cell.value !== null && 'richText' in cell.value) { 226 + // ExcelJS rich text cells: { richText: [{ text: '...' }, ...] } 227 + const parts = (cell.value as { richText: Array<{ text: string }> }).richText; 228 + data.v = Array.isArray(parts) ? parts.map(p => p.text || '').join('') : ''; 229 + } else if (typeof cell.value === 'object') { 230 + // Any other object — coerce to string to prevent [object Object] 231 + data.v = String(cell.value); 222 232 } else { 223 233 data.v = cell.value; 224 234 }
+21
tests/formulas.test.ts
··· 247 247 it('passes through errors', () => { 248 248 expect(formatCell('#ERROR!', 'auto')).toBe('#ERROR!'); 249 249 }); 250 + 251 + it('extracts text from hyperlink objects instead of [object Object]', () => { 252 + expect(formatCell({ text: 'Click here', hyperlink: 'https://example.com' }, 'auto')).toBe('Click here'); 253 + expect(formatCell({ hyperlink: 'https://example.com' }, 'auto')).toBe('https://example.com'); 254 + }); 255 + 256 + it('extracts text from richText objects instead of [object Object]', () => { 257 + expect(formatCell({ richText: [{ text: 'Hello ' }, { text: 'World' }] }, 'auto')).toBe('Hello World'); 258 + expect(formatCell({ richText: [] }, 'auto')).toBe(''); 259 + }); 260 + 261 + it('JSON-stringifies unknown objects instead of [object Object]', () => { 262 + const result = formatCell({ foo: 'bar' }, 'auto'); 263 + expect(result).toBe('{"foo":"bar"}'); 264 + expect(result).not.toContain('[object Object]'); 265 + }); 266 + 267 + it('handles object values with format types', () => { 268 + expect(formatCell({ text: '42.50' }, 'text')).toBe('42.50'); 269 + expect(formatCell({ text: 'Link' }, 'auto')).toBe('Link'); 270 + }); 250 271 }); 251 272 252 273 describe('error handling', () => {
+46
tests/xlsx-import.test.ts
··· 248 248 expect(mapExcelFormat('dd-mm-yyyy')).toBe('date'); 249 249 }); 250 250 }); 251 + 252 + describe('.xlsx Import — hyperlink cells', () => { 253 + async function createXlsxWithHyperlink() { 254 + const workbook = new ExcelJS.Workbook(); 255 + const ws = workbook.addWorksheet('Sheet1'); 256 + ws.getCell('A1').value = { text: 'Example', hyperlink: 'https://example.com' } as any; 257 + ws.getCell('A2').value = 'plain text'; 258 + return await workbook.xlsx.writeBuffer(); 259 + } 260 + 261 + it('extracts text from hyperlink cell values', async () => { 262 + const buf = await createXlsxWithHyperlink(); 263 + const result = await parseXlsxWithLib(buf); 264 + expect(result.cells.get('A1').v).toBe('Example'); 265 + expect(String(result.cells.get('A1').v)).not.toContain('[object Object]'); 266 + }); 267 + 268 + it('preserves plain text cells alongside hyperlinks', async () => { 269 + const buf = await createXlsxWithHyperlink(); 270 + const result = await parseXlsxWithLib(buf); 271 + expect(result.cells.get('A2').v).toBe('plain text'); 272 + }); 273 + }); 274 + 275 + describe('.xlsx Import — rich text cells', () => { 276 + async function createXlsxWithRichText() { 277 + const workbook = new ExcelJS.Workbook(); 278 + const ws = workbook.addWorksheet('Sheet1'); 279 + ws.getCell('A1').value = { richText: [{ text: 'Hello ' }, { text: 'World' }] } as any; 280 + ws.getCell('A2').value = { richText: [{ text: 'Single' }] } as any; 281 + return await workbook.xlsx.writeBuffer(); 282 + } 283 + 284 + it('concatenates richText parts into a single string', async () => { 285 + const buf = await createXlsxWithRichText(); 286 + const result = await parseXlsxWithLib(buf); 287 + expect(result.cells.get('A1').v).toBe('Hello World'); 288 + expect(String(result.cells.get('A1').v)).not.toContain('[object Object]'); 289 + }); 290 + 291 + it('handles single-part richText', async () => { 292 + const buf = await createXlsxWithRichText(); 293 + const result = await parseXlsxWithLib(buf); 294 + expect(result.cells.get('A2').v).toBe('Single'); 295 + }); 296 + });