Skip to content

Commit fe19a5a

Browse files
author
Developer
committed
Refactor collaborative code
1 parent 9524a65 commit fe19a5a

File tree

9 files changed

+915
-793
lines changed

9 files changed

+915
-793
lines changed

app/assets/builds/application.js

Lines changed: 214 additions & 151 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/assets/builds/application.js.map

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { createConsumer } from "@rails/actioncable"
2+
3+
export class ActionCableManager {
4+
constructor(controller) {
5+
this.controller = controller
6+
this.subscription = null
7+
}
8+
9+
connect() {
10+
this.subscription = createConsumer().subscriptions.create("CollaborativeStreamsChannel", {
11+
received: (data) => this.handleMessage(data),
12+
connected: () => console.log("Connected to CollaborativeStreamsChannel"),
13+
disconnected: () => console.log("Disconnected from CollaborativeStreamsChannel")
14+
})
15+
}
16+
17+
disconnect() {
18+
if (this.subscription) {
19+
this.subscription.unsubscribe()
20+
this.subscription = null
21+
}
22+
}
23+
24+
perform(action, data) {
25+
if (this.subscription) {
26+
this.subscription.perform(action, data)
27+
}
28+
}
29+
30+
handleMessage(data) {
31+
switch(data.action) {
32+
case 'active_users_list':
33+
this.controller.collaborationManager.setActiveUsers(data.users)
34+
break
35+
case 'user_joined':
36+
this.controller.collaborationManager.addUser(data.user_id, data.user_name, data.user_color)
37+
break
38+
case 'user_left':
39+
this.controller.collaborationManager.removeUser(data.user_id)
40+
break
41+
case 'cell_locked':
42+
this.controller.cellRenderer.showCellLocked(data.cell_id, data.user_id, data.user_name, data.user_color)
43+
break
44+
case 'cell_unlocked':
45+
this.controller.cellRenderer.hideCellLocked(data.cell_id)
46+
break
47+
case 'cell_updated':
48+
this.controller.cellRenderer.updateCell(data.cell_id, data.field, data.value, data.stream_id)
49+
break
50+
}
51+
}
52+
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
export class CellEditor {
2+
constructor(controller) {
3+
this.controller = controller
4+
this.autosaveTimeout = null
5+
}
6+
7+
handleCellClick(event) {
8+
const cell = event.currentTarget
9+
const cellId = cell.dataset.cellId
10+
const isLocked = cell.dataset.locked === 'true'
11+
const lockedBy = cell.dataset.lockedBy
12+
const field = cell.dataset.field
13+
const fieldType = cell.dataset.fieldType
14+
15+
// Prevent editing if locked by another user
16+
if (isLocked && lockedBy !== this.controller.currentUser) {
17+
this.controller.messageDisplayManager.showLockedMessage(cell)
18+
return
19+
}
20+
21+
// Request lock
22+
this.controller.actionCableManager.perform('lock_cell', { cell_id: cellId })
23+
24+
// Handle select fields differently
25+
if (fieldType === 'select') {
26+
this.controller.cellRenderer.showSelectDropdown(cell)
27+
} else {
28+
// For status field and other fields with special formatting,
29+
// replace content with plain text for editing
30+
if (field === 'status') {
31+
const currentValue = cell.dataset.originalValue || cell.textContent.trim()
32+
cell.textContent = currentValue
33+
}
34+
35+
// Make cell editable
36+
cell.contentEditable = true
37+
cell.focus()
38+
39+
// Select all text
40+
const range = document.createRange()
41+
range.selectNodeContents(cell)
42+
const sel = window.getSelection()
43+
sel.removeAllRanges()
44+
sel.addRange(range)
45+
}
46+
47+
// Set edit timeout (30 seconds)
48+
this.controller.editTimeoutManager.setEditTimeout(cellId)
49+
}
50+
51+
handleCellBlur(event) {
52+
const cell = event.currentTarget
53+
const cellId = cell.dataset.cellId
54+
const field = cell.dataset.field
55+
56+
// Clear any pending autosave to prevent duplicate saves
57+
if (this.autosaveTimeout) {
58+
clearTimeout(this.autosaveTimeout)
59+
this.autosaveTimeout = null
60+
}
61+
62+
// Check if we should skip saving (e.g., Escape was pressed)
63+
if (cell.dataset.skipSave !== 'true') {
64+
// Save changes
65+
this.saveCell(cell)
66+
// Note: saveCell will handle unlocking
67+
} else {
68+
// Just unlock without saving
69+
this.controller.actionCableManager.perform('unlock_cell', { cell_id: cellId })
70+
delete cell.dataset.skipSave
71+
}
72+
73+
// Make cell non-editable
74+
cell.contentEditable = false
75+
76+
// For status field, restore the formatted display with current value
77+
if (field === 'status') {
78+
this.controller.cellRenderer.formatStatusCell(cell)
79+
}
80+
81+
// Clear edit timeout
82+
this.controller.editTimeoutManager.clearEditTimeout(cellId)
83+
}
84+
85+
handleCellInput(event) {
86+
const cell = event.currentTarget
87+
const cellId = cell.dataset.cellId
88+
89+
// Reset edit timeout on input
90+
this.controller.editTimeoutManager.clearEditTimeout(cellId)
91+
this.controller.editTimeoutManager.setEditTimeout(cellId)
92+
93+
// Debounced autosave
94+
clearTimeout(this.autosaveTimeout)
95+
this.autosaveTimeout = setTimeout(() => {
96+
this.saveCell(cell)
97+
}, 1000)
98+
}
99+
100+
handleCellKeydown(event) {
101+
const cell = event.currentTarget
102+
103+
if (event.key === 'Enter' && !event.shiftKey) {
104+
event.preventDefault()
105+
cell.blur()
106+
} else if (event.key === 'Escape') {
107+
event.preventDefault()
108+
// Restore original value
109+
const originalValue = cell.dataset.originalValue || ''
110+
cell.textContent = originalValue
111+
// Don't save changes
112+
cell.dataset.skipSave = 'true'
113+
cell.blur()
114+
} else if (event.key === 'Tab') {
115+
event.preventDefault()
116+
// Save current cell
117+
cell.blur()
118+
119+
// Find next/previous editable cell
120+
const allCells = this.controller.cellTargets
121+
const currentIndex = allCells.indexOf(cell)
122+
let nextIndex
123+
124+
if (event.shiftKey) {
125+
// Shift+Tab: go to previous cell
126+
nextIndex = currentIndex - 1
127+
if (nextIndex < 0) nextIndex = allCells.length - 1
128+
} else {
129+
// Tab: go to next cell
130+
nextIndex = currentIndex + 1
131+
if (nextIndex >= allCells.length) nextIndex = 0
132+
}
133+
134+
// Click on the next cell to edit it
135+
if (allCells[nextIndex]) {
136+
allCells[nextIndex].click()
137+
}
138+
}
139+
}
140+
141+
saveCell(cell) {
142+
const cellId = cell.dataset.cellId
143+
const streamId = cell.dataset.streamId
144+
const field = cell.dataset.field
145+
const value = cell.textContent.trim()
146+
const originalValue = cell.dataset.originalValue || ''
147+
148+
console.log('saveCell called for field:', field, 'with value:', value)
149+
150+
// Validate required data
151+
if (!cellId || !streamId || !field) {
152+
console.error('Missing required data for cell save:', { cellId, streamId, field })
153+
return
154+
}
155+
156+
// Validate cell is still in correct position
157+
const td = cell.closest('td')
158+
const tr = td ? td.closest('tr') : null
159+
if (!td || !tr) {
160+
console.error('Cell is not in a valid table structure!', { cellId })
161+
return
162+
}
163+
164+
// Only save if value changed
165+
if (value !== originalValue) {
166+
console.log('Saving cell:', { cellId, streamId, field, value, originalValue })
167+
168+
this.controller.actionCableManager.perform('update_cell', {
169+
cell_id: cellId,
170+
stream_id: streamId,
171+
field: field,
172+
value: value
173+
})
174+
175+
// Update original value
176+
cell.dataset.originalValue = value
177+
} else {
178+
console.log('No change detected, not saving:', { value, originalValue })
179+
}
180+
181+
// Unlock cell
182+
this.controller.actionCableManager.perform('unlock_cell', { cell_id: cellId })
183+
}
184+
}

0 commit comments

Comments
 (0)