Skip to content

Commit 88a344d

Browse files
authored
feat: drag and drop editor
* feat: drag and drop ui * fix: update rendered list when items are loaded from the database * chore: refactor drag and drop container & wrapper, move hover logic into container * feat: feedback for drag and drop * chore: refactor drag and drop * chore: track parent of element * feat: reorder parts in the database and core * chore: useCallback * chore: render placeholder value when property is missing instead of not updating on navigation * feat: reorder segments * chore: track hoverState on the target container * fix: state updates * feat: move parts between segments
1 parent 2fd30d9 commit 88a344d

22 files changed

Lines changed: 1067 additions & 67 deletions

backend/src/background/api/api.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {
22
ApplicationSettings,
33
CoreConnectionInfo,
44
MutationPartCreate,
5+
MutationPartMove,
56
MutationPartUpdate,
67
MutationPieceCreate,
78
MutationPieceUpdate,
@@ -49,15 +50,19 @@ export interface BackendApi {
4950
getSegments: (rundownId: string) => Promise<Segment[]>
5051
addNewSegment: (segment: MutationSegmentCreate) => Promise<Segment>
5152
updateSegment: (segment: MutationSegmentUpdate) => Promise<Segment>
53+
reorderSegments: (part: MutationSegmentUpdate, targetIndex: number) => Promise<Segment[]>
5254
deleteSegment: (segmentId: string) => Promise<void>
5355

5456
getParts: (rundownId: string) => Promise<Part[]>
5557
addNewPart: (part: MutationPartCreate) => Promise<Part>
58+
movePart: (payload: MutationPartMove) => Promise<Part>
5659
updatePart: (part: MutationPartUpdate) => Promise<Part>
60+
reorderParts: (part: MutationPartUpdate, targetIndex: number) => Promise<Part[]>
5761
deletePart: (partId: string) => Promise<void>
5862

5963
getPieces: (rundownId: string) => Promise<Piece[]>
6064
addNewPiece: (piece: MutationPieceCreate) => Promise<Piece>
6165
updatePiece: (piece: MutationPieceUpdate) => Promise<Piece>
6266
deletePiece: (pieceId: string) => Promise<void>
67+
clonePiecesFromPartToPart: (fromPartId: string, toPartId: string) => Promise<Piece[]>
6368
}

backend/src/background/api/parts.ts

Lines changed: 193 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ import { v4 as uuid } from 'uuid'
1515
import { coreHandler } from '../coreHandler'
1616
import { getMutatedPiecesFromPart } from './pieces'
1717
import { mutations as rundownMutations } from './rundowns'
18-
import { mutations as segmentsMutations } from './segments'
19-
import { stringifyError } from '../util'
18+
import { mutations as segmentsMutations, sendSegmentDiffToCore } from './segments'
19+
import { spliceReorder, stringifyError } from '../util'
2020
import { mutations as settingsMutations } from './settings'
21+
import { mutations as piecesMutations } from './pieces'
2122

2223
async function mutatePart(part: Part): Promise<MutatedPart> {
2324
return {
@@ -69,6 +70,15 @@ async function sendPartDiffToCore(oldPart: Part, newPart: Part) {
6970
export const mutations = {
7071
async create(payload: MutationPartCreate): Promise<{ result?: Part; error?: Error }> {
7172
const partTypes: string[] | undefined = (await settingsMutations.read()).result?.partTypes
73+
const segmentParts: Part | Part[] | undefined = (
74+
await mutations.read({ segmentId: payload.segmentId })
75+
).result
76+
77+
const partsLength: number = Array.isArray(segmentParts)
78+
? segmentParts.length
79+
: segmentParts
80+
? 1
81+
: 0
7282

7383
const id = payload.id || uuid()
7484
const document: Partial<MutationPartCreate> = {
@@ -77,7 +87,8 @@ export const mutations = {
7787
// fallback Type to avoid errors in core
7888
type: partTypes?.[0],
7989
...payload.payload
80-
}
90+
},
91+
rank: payload.rank ?? partsLength
8192
}
8293
delete document.playlistId
8394
delete document.rundownId
@@ -106,6 +117,46 @@ export const mutations = {
106117
return { error: e as Error }
107118
}
108119
},
120+
async move(
121+
sourcePart: Part,
122+
targetPart: Part,
123+
targetIndex: number
124+
): Promise<{ result?: Part; error?: Error }> {
125+
try {
126+
const addNewPart = await mutations.create({
127+
...sourcePart,
128+
rundownId: targetPart.rundownId,
129+
playlistId: targetPart.playlistId,
130+
segmentId: targetPart.segmentId,
131+
rank: undefined,
132+
id: uuid(),
133+
payload: {
134+
script: sourcePart.payload.script,
135+
type: sourcePart.payload.type,
136+
duration: sourcePart.payload.duration
137+
}
138+
})
139+
if (!addNewPart.result) {
140+
console.error(addNewPart.error)
141+
throw new Error('Could not create new part while cloning.')
142+
}
143+
const clonePieces = await piecesMutations.cloneFromPartToPart({
144+
fromPartId: sourcePart.id,
145+
toPartId: addNewPart.result.id
146+
})
147+
const reorderParts = await mutations.reorder({ part: addNewPart.result, targetIndex })
148+
const removePart = await mutations.delete({ id: sourcePart.id })
149+
150+
if (clonePieces.error && reorderParts.error && removePart.error) {
151+
throw new Error('Cloning the part failed')
152+
}
153+
154+
return mutations.readOne(addNewPart.result.id)
155+
} catch (e) {
156+
console.error(e)
157+
return { error: e as Error }
158+
}
159+
},
109160
async readOne(id: string): Promise<{ result?: Part; error?: Error }> {
110161
try {
111162
const stmt = db.prepare(`
@@ -141,22 +192,32 @@ export const mutations = {
141192
}
142193

143194
let query = `
144-
SELECT *
145-
FROM parts
146-
`
147-
const args: string[] = []
195+
SELECT *
196+
FROM parts
197+
`
198+
const args: (string | number)[] = []
199+
const conditions: string[] = []
200+
148201
if (payload.id) {
149-
query += `\nWHERE id = ?`
202+
conditions.push(`id = ?`)
150203
args.push(payload.id)
151204
}
152205
if (payload.rundownId) {
153-
query += `\nWHERE rundownId = ?`
206+
conditions.push(`rundownId = ?`)
154207
args.push(payload.rundownId)
155208
}
156209
if (payload.segmentId) {
157-
query += `\nWHERE segmentId = ?`
210+
conditions.push(`segmentId = ?`)
158211
args.push(payload.segmentId)
159212
}
213+
if (payload.rank !== null && payload.rank !== undefined) {
214+
conditions.push(`JSON_EXTRACT(document, '$.rank') = ?`)
215+
args.push(payload.rank)
216+
}
217+
218+
if (conditions.length > 0) {
219+
query += `\nWHERE ${conditions.join(' AND ')}` // Join conditions with AND
220+
}
160221

161222
try {
162223
const stmt = db.prepare(query)
@@ -208,6 +269,70 @@ export const mutations = {
208269
return { error: e as Error }
209270
}
210271
},
272+
async reorder({
273+
part,
274+
targetIndex
275+
}: {
276+
part: MutationPartUpdate
277+
targetIndex: number
278+
}): Promise<{ result?: Part | Part[]; error?: Error }> {
279+
try {
280+
const { result, error } = await this.read({
281+
segmentId: part.segmentId,
282+
rundownId: part.rundownId
283+
})
284+
285+
if (error) throw error
286+
if (result && (!('length' in result) || result?.length < 2))
287+
throw new Error('An error occurred when getting parts from the database during reorder.')
288+
289+
const safeTargetIndex: number = Math.max(
290+
0,
291+
Math.min((result as Part[]).length - 1, targetIndex)
292+
)
293+
294+
const partsInRankOrder = (result as Part[]).sort((partA, partB) => partA.rank - partB.rank)
295+
const reorderedParts = spliceReorder(partsInRankOrder, part.rank, safeTargetIndex)
296+
297+
db.exec('BEGIN;')
298+
try {
299+
const updateStmt = db.prepare(`
300+
UPDATE parts
301+
SET playlistId = ?, segmentId = ?, document = (SELECT json_patch(parts.document, json(?)) FROM parts WHERE id = ?)
302+
WHERE id = ?;
303+
`)
304+
305+
reorderedParts.forEach((part, index) => {
306+
updateStmt.run(
307+
part.playlistId || null,
308+
part.segmentId || null,
309+
// update rank based on array order
310+
JSON.stringify({ ...part, rank: index }),
311+
part.id,
312+
part.id
313+
)
314+
})
315+
316+
db.exec('COMMIT;')
317+
} catch (transactionError) {
318+
console.error(transactionError)
319+
db.exec('ROLLBACK;')
320+
throw transactionError
321+
}
322+
323+
const { result: updatedParts, error: updatedPartserror } = await this.read({
324+
segmentId: part.segmentId,
325+
rundownId: part.rundownId
326+
})
327+
328+
if (updatedPartserror) throw updatedPartserror
329+
330+
return { result: updatedParts }
331+
} catch (e) {
332+
console.error(e)
333+
return { error: e as Error }
334+
}
335+
},
211336
async delete(payload: MutationPartDelete): Promise<{ error?: Error }> {
212337
try {
213338
db.exec('BEGIN TRANSACTION')
@@ -246,6 +371,37 @@ export async function init(): Promise<void> {
246371
}
247372

248373
return error || result
374+
} else if (operation.type === IpcOperationType.Move) {
375+
// TODO: Maybe this should be handled inside Sofie?
376+
try {
377+
const { sourcePart, targetPart, targetIndex } = operation.payload
378+
const { result: document, error: sourceError } = await mutations.readOne(sourcePart.id)
379+
if (sourceError) throw sourceError
380+
const { result: target, error: targetError } = await mutations.readOne(targetPart.id)
381+
if (targetError) throw targetError
382+
383+
if (document && target) {
384+
const { result, error } = await mutations.move(sourcePart, targetPart, targetIndex)
385+
if (error) throw error
386+
387+
const { result: sourceSegment } = await segmentsMutations.readOne(document.segmentId)
388+
const { result: targetSegment } = await segmentsMutations.readOne(target.segmentId)
389+
390+
if (result && sourceSegment && targetSegment) {
391+
try {
392+
await sendSegmentDiffToCore(sourceSegment, sourceSegment)
393+
await sendSegmentDiffToCore(targetSegment, targetSegment)
394+
} catch (error) {
395+
console.error(error)
396+
event.sender.send('error', stringifyError(error, true))
397+
}
398+
} else throw new Error('Cannot find segments while cloning')
399+
return result
400+
}
401+
} catch (e) {
402+
console.error(e)
403+
return e as Error
404+
}
249405
} else if (operation.type === IpcOperationType.Read) {
250406
const { result, error } = await mutations.read(operation.payload)
251407

@@ -264,6 +420,33 @@ export async function init(): Promise<void> {
264420
}
265421

266422
return error || result
423+
} else if (operation.type === IpcOperationType.Reorder) {
424+
const { result: sourceDocument } = await mutations.read({ id: operation.payload.part.id })
425+
const { result: reorderedParts, error } = await mutations.reorder(operation.payload)
426+
427+
if (
428+
!error &&
429+
sourceDocument &&
430+
!Array.isArray(sourceDocument) &&
431+
Array.isArray(reorderedParts)
432+
) {
433+
const { result: rundown } = await rundownMutations.read({ id: sourceDocument.rundownId })
434+
if (rundown && !Array.isArray(rundown) && rundown.sync) {
435+
try {
436+
const { result: segment, error: segmentError } = await segmentsMutations.readOne(
437+
sourceDocument.segmentId
438+
)
439+
// We need to update the entire segment, because otherwise core also reorders the parts in some cases.
440+
if (segment && !segmentError) {
441+
await sendSegmentDiffToCore(segment, segment)
442+
return reorderedParts
443+
}
444+
} catch (error) {
445+
console.error(error)
446+
event.sender.send('error', stringifyError(error, true))
447+
}
448+
}
449+
}
267450
} else if (operation.type === IpcOperationType.Delete) {
268451
const { result: document } = await mutations.read({ id: operation.payload.id })
269452
const { error } = await mutations.delete(operation.payload)

backend/src/background/api/pieces.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { db } from '../db'
1414
import { v4 as uuid } from 'uuid'
1515
import { sendPartUpdateToCore } from './parts'
1616
import { stringifyError } from '../util'
17+
import { mutations as partsMutations } from './parts'
1718

1819
export const mutations = {
1920
async create(payload: MutationPieceCreate): Promise<{ result?: Piece; error?: Error }> {
@@ -47,6 +48,7 @@ export const mutations = {
4748

4849
return this.readOne(id)
4950
} catch (e) {
51+
console.error(e)
5052
return { error: e as Error }
5153
}
5254
},
@@ -170,6 +172,53 @@ export const mutations = {
170172
} catch (e) {
171173
return { error: e as Error }
172174
}
175+
},
176+
async cloneFromPartToPart({
177+
fromPartId,
178+
toPartId
179+
}: {
180+
fromPartId: string
181+
toPartId: string
182+
}): Promise<{ result?: Piece[]; error?: Error }> {
183+
try {
184+
const { result: fromPart } = await partsMutations.readOne(fromPartId)
185+
const { result: toPart } = await partsMutations.readOne(toPartId)
186+
187+
if (!fromPart || !toPart) {
188+
throw new Error('Either the source or target Part was not found')
189+
}
190+
191+
const { result: sourcePieces } = await mutations.read({ partId: fromPartId })
192+
if (sourcePieces && Array.isArray(sourcePieces)) {
193+
await Promise.all(
194+
sourcePieces.map(async (piece) => {
195+
return await mutations.create({
196+
playlistId: toPart.playlistId,
197+
rundownId: toPart.rundownId,
198+
segmentId: toPart.segmentId,
199+
partId: toPart.id,
200+
name: piece.name,
201+
start: piece.start,
202+
duration: piece.duration,
203+
pieceType: piece.pieceType,
204+
payload: piece.payload
205+
})
206+
})
207+
)
208+
209+
const { result: resultPieces } = await mutations.read({ partId: toPartId })
210+
if (resultPieces) {
211+
return { result: Array.isArray(resultPieces) ? resultPieces : [resultPieces] }
212+
} else {
213+
throw new Error("Couldn't retrieve cloned pieces after creation.")
214+
}
215+
} else {
216+
throw new Error('Pre-conditions for cloning were not met.')
217+
}
218+
} catch (e) {
219+
console.error(e)
220+
return { error: e as Error }
221+
}
173222
}
174223
}
175224

@@ -219,6 +268,19 @@ export async function init(): Promise<void> {
219268
}
220269

221270
return error || true
271+
} else if (operation.type === IpcOperationType.CloneSet) {
272+
const { result, error } = await mutations.cloneFromPartToPart(operation.payload)
273+
274+
if (result) {
275+
try {
276+
await sendPartUpdateToCore(operation.payload.toPartId)
277+
} catch (error) {
278+
console.error(error)
279+
event.sender.send('error', stringifyError(error, true))
280+
}
281+
}
282+
283+
return error || result
222284
}
223285
})
224286
}

backend/src/background/api/rundowns.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export async function mutateRundown(rundown: Rundown): Promise<MutatedRundown> {
3131
}
3232
}
3333

34-
async function sendRundownDiffToCore(oldDocument: Rundown, newDocument: Rundown) {
34+
export async function sendRundownDiffToCore(oldDocument: Rundown, newDocument: Rundown) {
3535
if (oldDocument.sync && !newDocument.sync) {
3636
console.log('delete rundown', oldDocument, newDocument)
3737
return coreHandler.core.coreMethods.dataRundownDelete(oldDocument.id)

0 commit comments

Comments
 (0)