collavre 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/assets/stylesheets/collavre/comments_popup.css +62 -0
- data/app/javascript/controllers/comment_controller.js +2 -0
- data/app/javascript/lib/utils/markdown.js +2 -0
- data/app/javascript/lib/utils/table_download.js +163 -0
- data/app/views/collavre/comments/_comments_popup.html.erb +3 -1
- data/config/locales/comments.en.yml +3 -0
- data/config/locales/comments.ko.yml +3 -0
- data/lib/collavre/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 617bf6c7eda577396e21384c92f4655c44eb0f433ef2cfeba05f6c47ab00f2c9
|
|
4
|
+
data.tar.gz: b3cd87f98abf7a7cd2e1c1876a33d8eab6c993993933d23c021c59c6663cb525
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bcdda91582a8b87d3ba5f890f22a0df72c9aed567860c14e06eb85dca5980c2adc735a29a0fa1bbcd9aea3e8b8bdf79d8d88aa6ee88b7231afc9dbdc11edc6d8
|
|
7
|
+
data.tar.gz: 289e1667b14dd0b578c46d7623fc751489ddb4c8d9dfd09f6024d4355fe69a4b5e375275c3395c541c6f97c26e7ce953b378c1e44d2fd8eda03e5346fa319b8c
|
|
@@ -352,6 +352,68 @@ body.chat-fullscreen {
|
|
|
352
352
|
margin: 0;
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
+
/* Table styling */
|
|
356
|
+
.comment-content table {
|
|
357
|
+
border-collapse: collapse;
|
|
358
|
+
width: 100%;
|
|
359
|
+
font-size: 0.85em;
|
|
360
|
+
margin: 0;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.comment-content table th,
|
|
364
|
+
.comment-content table td {
|
|
365
|
+
border: 1px solid var(--color-border);
|
|
366
|
+
padding: 0.3em 0.6em;
|
|
367
|
+
text-align: left;
|
|
368
|
+
white-space: nowrap;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.comment-content table th {
|
|
372
|
+
background: var(--color-section-bg);
|
|
373
|
+
font-weight: 600;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.comment-content table tr:hover td {
|
|
377
|
+
background: color-mix(in srgb, var(--color-section-bg) 40%, transparent);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/* Table download wrapper */
|
|
381
|
+
.table-download-wrapper {
|
|
382
|
+
position: relative;
|
|
383
|
+
overflow-x: auto;
|
|
384
|
+
margin: 0.4em 0;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.table-download-toolbar {
|
|
388
|
+
display: flex;
|
|
389
|
+
justify-content: flex-end;
|
|
390
|
+
gap: 0.3em;
|
|
391
|
+
padding: 0.15em 0;
|
|
392
|
+
opacity: 0;
|
|
393
|
+
transition: opacity 0.15s ease;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.table-download-wrapper:hover .table-download-toolbar {
|
|
397
|
+
opacity: 1;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.table-download-btn {
|
|
401
|
+
background: none;
|
|
402
|
+
border: 1px solid var(--color-border);
|
|
403
|
+
border-radius: var(--radius-2);
|
|
404
|
+
color: var(--color-muted);
|
|
405
|
+
font-size: 0.75em;
|
|
406
|
+
padding: 0.15em 0.5em;
|
|
407
|
+
cursor: pointer;
|
|
408
|
+
line-height: 1.4;
|
|
409
|
+
transition: color 0.15s ease, border-color 0.15s ease;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.table-download-btn:hover {
|
|
413
|
+
color: var(--color-active);
|
|
414
|
+
border-color: var(--color-active);
|
|
415
|
+
}
|
|
416
|
+
|
|
355
417
|
/* Quote indicator in form */
|
|
356
418
|
.comment-quote-indicator {
|
|
357
419
|
display: flex;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
2
|
import { renderCommentMarkdown } from '../lib/utils/markdown'
|
|
3
|
+
import { addTableDownloadButtons } from '../lib/utils/table_download'
|
|
3
4
|
import CommonPopup from '../lib/common_popup'
|
|
4
5
|
|
|
5
6
|
// Global tracker: persists streaming state across Turbo replacements
|
|
@@ -110,6 +111,7 @@ export default class extends Controller {
|
|
|
110
111
|
this._resetStreamingTimeout()
|
|
111
112
|
} else {
|
|
112
113
|
contentElement.innerHTML = renderCommentMarkdown(text)
|
|
114
|
+
addTableDownloadButtons(contentElement)
|
|
113
115
|
contentElement.classList.remove('streaming')
|
|
114
116
|
if (this._isStreaming) this._cleanupStreaming()
|
|
115
117
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { marked } from 'marked'
|
|
2
2
|
import DOMPurify from 'dompurify'
|
|
3
|
+
import { addTableDownloadButtons } from './table_download'
|
|
3
4
|
import hljs from 'highlight.js/lib/core'
|
|
4
5
|
|
|
5
6
|
// Register only commonly used languages to keep the bundle small
|
|
@@ -129,5 +130,6 @@ export function renderMarkdownInContainer(container) {
|
|
|
129
130
|
if (element.dataset.rendered === 'true') return
|
|
130
131
|
element.innerHTML = renderCommentMarkdown(element.textContent)
|
|
131
132
|
element.dataset.rendered = 'true'
|
|
133
|
+
addTableDownloadButtons(element)
|
|
132
134
|
})
|
|
133
135
|
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table Download Utility
|
|
3
|
+
*
|
|
4
|
+
* Detects rendered <table> elements within comment content and adds
|
|
5
|
+
* download buttons (CSV / Excel) above each table.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function getI18n(commentEl) {
|
|
9
|
+
const popup = commentEl.closest('#comments-popup')
|
|
10
|
+
return {
|
|
11
|
+
csv: popup?.dataset?.tableDownloadCsvText || 'CSV',
|
|
12
|
+
excel: popup?.dataset?.tableDownloadExcelText || 'Excel',
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseTable(table) {
|
|
17
|
+
const rows = []
|
|
18
|
+
table.querySelectorAll('tr').forEach((tr) => {
|
|
19
|
+
const cells = []
|
|
20
|
+
tr.querySelectorAll('th, td').forEach((cell) => {
|
|
21
|
+
cells.push(cell.textContent.trim())
|
|
22
|
+
})
|
|
23
|
+
rows.push(cells)
|
|
24
|
+
})
|
|
25
|
+
return rows
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function escapeCsvCell(value) {
|
|
29
|
+
if (/[",\n\r]/.test(value)) {
|
|
30
|
+
return `"${value.replace(/"/g, '""')}"`
|
|
31
|
+
}
|
|
32
|
+
return value
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function downloadBlob(blob, filename) {
|
|
36
|
+
const url = URL.createObjectURL(blob)
|
|
37
|
+
const a = document.createElement('a')
|
|
38
|
+
a.href = url
|
|
39
|
+
a.download = filename
|
|
40
|
+
document.body.appendChild(a)
|
|
41
|
+
a.click()
|
|
42
|
+
document.body.removeChild(a)
|
|
43
|
+
URL.revokeObjectURL(url)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function downloadCsv(rows, filename) {
|
|
47
|
+
const csv = rows.map((row) => row.map(escapeCsvCell).join(',')).join('\r\n')
|
|
48
|
+
// BOM for Excel UTF-8 compatibility
|
|
49
|
+
const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8' })
|
|
50
|
+
downloadBlob(blob, filename)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function downloadExcel(rows, filename) {
|
|
54
|
+
const escapeXml = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
|
55
|
+
|
|
56
|
+
const headerRow = rows[0] || []
|
|
57
|
+
const dataRows = rows.slice(1)
|
|
58
|
+
|
|
59
|
+
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
60
|
+
xml += '<?mso-application progid="Excel.Sheet"?>\n'
|
|
61
|
+
xml += '<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"\n'
|
|
62
|
+
xml += ' xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">\n'
|
|
63
|
+
xml += ' <Styles>\n'
|
|
64
|
+
xml += ' <Style ss:ID="header"><Font ss:Bold="1"/></Style>\n'
|
|
65
|
+
xml += ' </Styles>\n'
|
|
66
|
+
xml += ' <Worksheet ss:Name="Sheet1">\n'
|
|
67
|
+
xml += ' <Table>\n'
|
|
68
|
+
|
|
69
|
+
// Header row
|
|
70
|
+
if (headerRow.length) {
|
|
71
|
+
xml += ' <Row>\n'
|
|
72
|
+
headerRow.forEach((cell) => {
|
|
73
|
+
xml += ` <Cell ss:StyleID="header"><Data ss:Type="String">${escapeXml(cell)}</Data></Cell>\n`
|
|
74
|
+
})
|
|
75
|
+
xml += ' </Row>\n'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Data rows
|
|
79
|
+
dataRows.forEach((row) => {
|
|
80
|
+
xml += ' <Row>\n'
|
|
81
|
+
row.forEach((cell) => {
|
|
82
|
+
const type = isNaN(cell) || cell === '' ? 'String' : 'Number'
|
|
83
|
+
xml += ` <Cell><Data ss:Type="${type}">${escapeXml(cell)}</Data></Cell>\n`
|
|
84
|
+
})
|
|
85
|
+
xml += ' </Row>\n'
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
xml += ' </Table>\n'
|
|
89
|
+
xml += ' </Worksheet>\n'
|
|
90
|
+
xml += '</Workbook>'
|
|
91
|
+
|
|
92
|
+
const blob = new Blob([xml], { type: 'application/vnd.ms-excel;charset=utf-8' })
|
|
93
|
+
downloadBlob(blob, filename)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function generateFilename(table, index) {
|
|
97
|
+
// Try to use the first header cell as a hint for the filename
|
|
98
|
+
const firstHeader = table.querySelector('th')
|
|
99
|
+
if (firstHeader) {
|
|
100
|
+
const hint = firstHeader.textContent.trim().slice(0, 30).replace(/[^a-zA-Z0-9가-힣_-]/g, '_')
|
|
101
|
+
if (hint) return `table_${hint}`
|
|
102
|
+
}
|
|
103
|
+
return `table_${index + 1}`
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function createToolbar(table, index, i18n) {
|
|
107
|
+
const toolbar = document.createElement('div')
|
|
108
|
+
toolbar.className = 'table-download-toolbar'
|
|
109
|
+
|
|
110
|
+
const csvBtn = document.createElement('button')
|
|
111
|
+
csvBtn.type = 'button'
|
|
112
|
+
csvBtn.className = 'table-download-btn'
|
|
113
|
+
csvBtn.textContent = `\u2913 ${i18n.csv}`
|
|
114
|
+
csvBtn.addEventListener('click', (e) => {
|
|
115
|
+
e.preventDefault()
|
|
116
|
+
e.stopPropagation()
|
|
117
|
+
const rows = parseTable(table)
|
|
118
|
+
const filename = generateFilename(table, index)
|
|
119
|
+
downloadCsv(rows, `${filename}.csv`)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const excelBtn = document.createElement('button')
|
|
123
|
+
excelBtn.type = 'button'
|
|
124
|
+
excelBtn.className = 'table-download-btn'
|
|
125
|
+
excelBtn.textContent = `\u2913 ${i18n.excel}`
|
|
126
|
+
excelBtn.addEventListener('click', (e) => {
|
|
127
|
+
e.preventDefault()
|
|
128
|
+
e.stopPropagation()
|
|
129
|
+
const rows = parseTable(table)
|
|
130
|
+
const filename = generateFilename(table, index)
|
|
131
|
+
downloadExcel(rows, `${filename}.xls`)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
toolbar.appendChild(csvBtn)
|
|
135
|
+
toolbar.appendChild(excelBtn)
|
|
136
|
+
return toolbar
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Scans the given content element for <table> tags and adds download
|
|
141
|
+
* toolbars. Call this after markdown has been rendered into HTML.
|
|
142
|
+
*/
|
|
143
|
+
export function addTableDownloadButtons(contentElement) {
|
|
144
|
+
if (!contentElement) return
|
|
145
|
+
|
|
146
|
+
const tables = contentElement.querySelectorAll('table')
|
|
147
|
+
if (!tables.length) return
|
|
148
|
+
|
|
149
|
+
const i18n = getI18n(contentElement)
|
|
150
|
+
|
|
151
|
+
tables.forEach((table, index) => {
|
|
152
|
+
// Skip if already wrapped
|
|
153
|
+
if (table.parentElement?.classList.contains('table-download-wrapper')) return
|
|
154
|
+
|
|
155
|
+
const wrapper = document.createElement('div')
|
|
156
|
+
wrapper.className = 'table-download-wrapper'
|
|
157
|
+
|
|
158
|
+
const toolbar = createToolbar(table, index, i18n)
|
|
159
|
+
table.parentNode.insertBefore(wrapper, table)
|
|
160
|
+
wrapper.appendChild(toolbar)
|
|
161
|
+
wrapper.appendChild(table)
|
|
162
|
+
})
|
|
163
|
+
}
|
|
@@ -59,7 +59,9 @@
|
|
|
59
59
|
data-review-type-review-label="<%= t('collavre.comments.review_type_review') %>"
|
|
60
60
|
data-review-type-question-label="<%= t('collavre.comments.review_type_question') %>"
|
|
61
61
|
data-remove-from-history-label="<%= t('collavre.comments.remove_from_history') %>"
|
|
62
|
-
data-inbox-reply-button="<%= t('collavre.comments.inbox_reply_button') %>"
|
|
62
|
+
data-inbox-reply-button="<%= t('collavre.comments.inbox_reply_button') %>"
|
|
63
|
+
data-table-download-csv-text="<%= t('collavre.comments.table_download.csv') %>"
|
|
64
|
+
data-table-download-excel-text="<%= t('collavre.comments.table_download.excel') %>">
|
|
63
65
|
<div class="resize-handle resize-handle-left" data-comments--popup-target="leftHandle"></div>
|
|
64
66
|
<div class="resize-handle resize-handle-right" data-comments--popup-target="rightHandle"></div>
|
|
65
67
|
<div class="comments-popup-header" data-comments--popup-target="header">
|
data/lib/collavre/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: collavre
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.15.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Collavre
|
|
@@ -321,6 +321,7 @@ files:
|
|
|
321
321
|
- app/javascript/lib/touch_drag.js
|
|
322
322
|
- app/javascript/lib/turbo_stream_actions.js
|
|
323
323
|
- app/javascript/lib/utils/markdown.js
|
|
324
|
+
- app/javascript/lib/utils/table_download.js
|
|
324
325
|
- app/javascript/modules/__tests__/creative_progress.test.js
|
|
325
326
|
- app/javascript/modules/command_args_form.js
|
|
326
327
|
- app/javascript/modules/command_menu.js
|