From 4c7e8d40bf2434a45acfd1967edafd29cb7b959e Mon Sep 17 00:00:00 2001 From: hlomzik Date: Thu, 3 Jul 2025 19:47:23 +0100 Subject: [PATCH 1/6] fix: BROS-136: Don't allow to create TimeSeries regions in View All --- .../src/components/TimeSeries/TimeSeriesVisualizer.jsx | 5 ++++- web/libs/editor/src/tags/object/TimeSeries/Channel.jsx | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/web/libs/editor/src/components/TimeSeries/TimeSeriesVisualizer.jsx b/web/libs/editor/src/components/TimeSeries/TimeSeriesVisualizer.jsx index 56b10b981980..c5c17a5a3c60 100644 --- a/web/libs/editor/src/components/TimeSeries/TimeSeriesVisualizer.jsx +++ b/web/libs/editor/src/components/TimeSeries/TimeSeriesVisualizer.jsx @@ -108,10 +108,13 @@ class TimeSeriesVisualizerD3 extends React.Component { const x = d3.mouse(d3.event.sourceEvent.target)[0]; const newRegion = this.newRegion; + // double click handler to create instant region // when 2nd click happens during 300ms after 1st click and in the same place if (newRegion && Math.abs(newRegion.x - x) < 4) { clearTimeout(this.newRegionTimer); - parent?.regionChanged(newRegion.range, ranges.length, newRegion.states); + if (!readonly) { + parent?.regionChanged(newRegion.range, ranges.length, newRegion.states); + } this.newRegion = null; this.newRegionTimer = null; } else if (statesSelected) { diff --git a/web/libs/editor/src/tags/object/TimeSeries/Channel.jsx b/web/libs/editor/src/tags/object/TimeSeries/Channel.jsx index 188fb4c92373..248d0559f4f8 100644 --- a/web/libs/editor/src/tags/object/TimeSeries/Channel.jsx +++ b/web/libs/editor/src/tags/object/TimeSeries/Channel.jsx @@ -210,10 +210,13 @@ class ChannelD3 extends React.Component { const x = d3.mouse(d3.event.sourceEvent.target)[0]; const newRegion = this.newRegion; + // double click handler to create instant region // when 2nd click happens during 300ms after 1st click and in the same place if (newRegion && Math.abs(newRegion.x - x) < 4) { clearTimeout(this.newRegionTimer); - parent?.regionChanged(newRegion.range, ranges.length, newRegion.states); + if (!readonly) { + parent?.regionChanged(newRegion.range, ranges.length, newRegion.states); + } this.newRegion = null; this.newRegionTimer = null; } else if (statesSelected) { From e8edeff24e2eac94d190f3bcb82d08028995cfb8 Mon Sep 17 00:00:00 2001 From: hlomzik Date: Thu, 3 Jul 2025 19:49:31 +0100 Subject: [PATCH 2/6] fix: BROS-136: Don't allow to create Audio regions in View All --- web/libs/editor/src/tags/object/AudioUltra/model.js | 3 +++ web/libs/editor/src/tags/object/AudioUltra/view.tsx | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/web/libs/editor/src/tags/object/AudioUltra/model.js b/web/libs/editor/src/tags/object/AudioUltra/model.js index 5a311a75beb7..84079a034b3a 100644 --- a/web/libs/editor/src/tags/object/AudioUltra/model.js +++ b/web/libs/editor/src/tags/object/AudioUltra/model.js @@ -175,6 +175,9 @@ export const AudioModel = types.compose( // use label to generate a unique key to ensure that adding/deleting can trigger changes return labels ? labels.join(",") : ""; }, + get readonly() { + return self.annotation.isReadOnly(); + }, })) ////// Sync actions .actions((self) => ({ diff --git a/web/libs/editor/src/tags/object/AudioUltra/view.tsx b/web/libs/editor/src/tags/object/AudioUltra/view.tsx index 7235e2258793..f137bd3f9cdb 100644 --- a/web/libs/editor/src/tags/object/AudioUltra/view.tsx +++ b/web/libs/editor/src/tags/object/AudioUltra/view.tsx @@ -71,8 +71,6 @@ const AudioUltraView: FC = ({ item, children, settings = {}, ch onError: item.onError, regions: { createable: !item.readonly, - updateable: !item.readonly, - deleteable: !item.readonly, }, timeline: { backgroundColor: isDarkMode ? "rgb(38, 37, 34)" : "rgba(255,255,255,0.8)", From 6f25f788a23ac40428a24029d39feea84a0ea308 Mon Sep 17 00:00:00 2001 From: hlomzik Date: Thu, 3 Jul 2025 19:50:11 +0100 Subject: [PATCH 3/6] fix: BROS-136: Don't allow to create Paragraphs regions in View All --- web/libs/editor/src/tags/object/Paragraphs/HtxParagraphs.jsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/web/libs/editor/src/tags/object/Paragraphs/HtxParagraphs.jsx b/web/libs/editor/src/tags/object/Paragraphs/HtxParagraphs.jsx index be18db1fde6e..61d694a9497e 100644 --- a/web/libs/editor/src/tags/object/Paragraphs/HtxParagraphs.jsx +++ b/web/libs/editor/src/tags/object/Paragraphs/HtxParagraphs.jsx @@ -303,6 +303,10 @@ class HtxParagraphsView extends Component { if (!states || states.length === 0 || ev.ctrlKey || ev.metaKey) return this._selectRegions(ev.ctrlKey || ev.metaKey); + if (item.annotation.isReadOnly()) { + return; + } + const selectedRanges = this.captureDocumentSelection(); if (selectedRanges.length === 0) { From 6ccb2fcf3a437f035e7f6c351bc71f09763c6337 Mon Sep 17 00:00:00 2001 From: hlomzik Date: Thu, 3 Jul 2025 19:50:46 +0100 Subject: [PATCH 4/6] Fix linting and remove unused method --- .../TimeSeries/TimeSeriesVisualizer.jsx | 2 +- .../src/tags/object/AudioUltra/model.js | 28 ------------------- .../src/tags/object/TimeSeries/Channel.jsx | 8 ++++-- 3 files changed, 6 insertions(+), 32 deletions(-) diff --git a/web/libs/editor/src/components/TimeSeries/TimeSeriesVisualizer.jsx b/web/libs/editor/src/components/TimeSeries/TimeSeriesVisualizer.jsx index c5c17a5a3c60..fa35d063e6a2 100644 --- a/web/libs/editor/src/components/TimeSeries/TimeSeriesVisualizer.jsx +++ b/web/libs/editor/src/components/TimeSeries/TimeSeriesVisualizer.jsx @@ -97,7 +97,7 @@ class TimeSeriesVisualizerD3 extends React.Component { } = this.props; const activeStates = parent?.activeStates(); - const statesSelected = activeStates && activeStates.length; + const statesSelected = activeStates?.length; const readonly = parent?.annotation?.isReadOnly(); // skip if event fired by .move() - prevent recursion and bugs diff --git a/web/libs/editor/src/tags/object/AudioUltra/model.js b/web/libs/editor/src/tags/object/AudioUltra/model.js index 84079a034b3a..f9abc2bc525d 100644 --- a/web/libs/editor/src/tags/object/AudioUltra/model.js +++ b/web/libs/editor/src/tags/object/AudioUltra/model.js @@ -396,34 +396,6 @@ export const AudioModel = types.compose( self.playBackRate = val; }, - createRegion(wsRegion, states) { - let bgColor = self.selectedregionbg; - const st = states.find((s) => s.type === "labels"); - - if (st) bgColor = Utils.Colors.convertToRGBA(st.getSelectedColor(), 0.3); - - const r = AudioRegionModel.create({ - id: wsRegion.id ? wsRegion.id : guidGenerator(), - pid: wsRegion.pid ? wsRegion.pid : guidGenerator(), - parentID: wsRegion.parent_id === null ? "" : wsRegion.parent_id, - start: wsRegion.start, - end: wsRegion.end, - score: wsRegion.score, - readonly: wsRegion.readonly, - regionbg: self.regionbg, - selectedregionbg: bgColor, - normalization: wsRegion.normalization, - states, - }); - - r.setWSRegion(wsRegion); - - self.regions.push(r); - self.annotation.addRegion(r); - - return r; - }, - addRegion(wsRegion) { // area id is assigned to WS region during deserealization const find_r = self.annotation.areas.get(wsRegion.id); diff --git a/web/libs/editor/src/tags/object/TimeSeries/Channel.jsx b/web/libs/editor/src/tags/object/TimeSeries/Channel.jsx index 248d0559f4f8..f02391b6f614 100644 --- a/web/libs/editor/src/tags/object/TimeSeries/Channel.jsx +++ b/web/libs/editor/src/tags/object/TimeSeries/Channel.jsx @@ -199,7 +199,7 @@ class ChannelD3 extends React.Component { } = this.props; const activeStates = parent?.activeStates(); - const statesSelected = activeStates && activeStates.length; + const statesSelected = activeStates?.length; const readonly = parent?.annotation?.isReadOnly(); // skip if event fired by .move() - prevent recursion and bugs @@ -367,7 +367,7 @@ class ChannelD3 extends React.Component { const block = this.gCreator; const getRegion = this.getRegion; const x = this.x; - const brush = (this.brushCreator = d3 + const brush = d3 .brushX() .extent([ [0, 0], @@ -384,7 +384,9 @@ class ChannelD3 extends React.Component { // replacing default filter to allow ctrl-click action .filter(() => { return !d3.event.button; - })); + }); + + this.brushCreator = brush; this.gCreator.call(this.brushCreator); } From 6a8f1f8d6f2ade32535a9ca5c4a56615df3e7340 Mon Sep 17 00:00:00 2001 From: hlomzik Date: Fri, 4 Jul 2025 03:39:33 +0100 Subject: [PATCH 5/6] fix: BROS-136: Don't allow to create Timeline regions in View All Also don't allow to change interpolation of VideoRectangle regions --- web/libs/editor/src/components/Timeline/Timeline.tsx | 4 +++- web/libs/editor/src/components/Timeline/Types.ts | 2 ++ .../src/components/Timeline/Views/Frames/Controls.tsx | 6 +++--- web/libs/editor/src/tags/object/Video/HtxVideo.jsx | 1 + web/libs/editor/src/tags/object/Video/Video.js | 3 +++ 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/web/libs/editor/src/components/Timeline/Timeline.tsx b/web/libs/editor/src/components/Timeline/Timeline.tsx index 497c98b127c6..afeb44487aef 100644 --- a/web/libs/editor/src/components/Timeline/Timeline.tsx +++ b/web/libs/editor/src/components/Timeline/Timeline.tsx @@ -32,6 +32,7 @@ const TimelineComponent: FC = ({ speed, className, formatPosition, + readonly = false, ...props }) => { const View = Views[mode]; @@ -106,8 +107,9 @@ const TimelineComponent: FC = ({ seekOffset, settings: View.settings, visibleWidth: seekVisibleWidth, + readonly, }), - [position, seekOffset, seekVisibleWidth, length, regions, step, playing, View.settings, data], + [position, seekOffset, seekVisibleWidth, length, regions, step, playing, View.settings, data, readonly], ); useEffect(() => { diff --git a/web/libs/editor/src/components/Timeline/Types.ts b/web/libs/editor/src/components/Timeline/Types.ts index f1fb2d7d7b66..31ced333d2a7 100644 --- a/web/libs/editor/src/components/Timeline/Types.ts +++ b/web/libs/editor/src/components/Timeline/Types.ts @@ -32,6 +32,7 @@ export interface TimelineProps { controlsOnTop?: boolean; controls?: TimelineControls; customControls?: TimelineCustomControls[]; + readonly?: boolean; onReady?: (data: Record) => void; onPlay?: () => void; onPause?: () => void; @@ -121,6 +122,7 @@ export interface TimelineContextValue { settings?: TimelineSettings; changeSetting?: (key: string, value: any) => void; data?: any; + readonly?: boolean; } export interface TimelineMinimapProps { diff --git a/web/libs/editor/src/components/Timeline/Views/Frames/Controls.tsx b/web/libs/editor/src/components/Timeline/Views/Frames/Controls.tsx index 998680e98026..2500efc01829 100644 --- a/web/libs/editor/src/components/Timeline/Views/Frames/Controls.tsx +++ b/web/libs/editor/src/components/Timeline/Views/Frames/Controls.tsx @@ -10,7 +10,7 @@ type DataType = { }; export const Controls: FC> = ({ onAction }) => { - const { position, regions } = useContext(TimelineContext); + const { position, regions, readonly } = useContext(TimelineContext); const hasSelectedRegion = regions.some(({ selected, timeline }) => selected && !timeline); const closestKeypoint = useMemo(() => { const region = regions.find((r) => r.selected && !r.timeline); @@ -69,11 +69,11 @@ export const Controls: FC> = ({ onActio return ( <> - + {keypointIcon} - + {interpolationIcon} diff --git a/web/libs/editor/src/tags/object/Video/HtxVideo.jsx b/web/libs/editor/src/tags/object/Video/HtxVideo.jsx index e6abcf3d2324..e82ddb3f5f54 100644 --- a/web/libs/editor/src/tags/object/Video/HtxVideo.jsx +++ b/web/libs/editor/src/tags/object/Video/HtxVideo.jsx @@ -556,6 +556,7 @@ const HtxVideoView = ({ item, store }) => { disableView={!supportsTimelineRegions && !supportsRegions} framerate={item.framerate} controls={{ FramesControl: true }} + readonly={item.annotation?.isReadOnly()} customControls={[ { position: "left", diff --git a/web/libs/editor/src/tags/object/Video/Video.js b/web/libs/editor/src/tags/object/Video/Video.js index 9533b6ed6adb..5855670d668b 100644 --- a/web/libs/editor/src/tags/object/Video/Video.js +++ b/web/libs/editor/src/tags/object/Video/Video.js @@ -304,6 +304,9 @@ const Model = types * @returns {Object} created region */ startDrawing({ frame, region: id }) { + // don't create or edit regions in read-only mode + if (self.annotation.isReadOnly()) return null; + if (id) { const region = self.annotation.regions.find((r) => r.cleanId === id); const range = region?.ranges?.[0]; From f06a519c5785a4ff23bcd180b3ef0dfcae5fd283 Mon Sep 17 00:00:00 2001 From: hlomzik Date: Fri, 4 Jul 2025 03:44:41 +0100 Subject: [PATCH 6/6] Fix linting --- .../src/tags/object/AudioUltra/model.js | 24 +++++++++---------- .../editor/src/tags/object/Video/Video.js | 10 ++++---- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/web/libs/editor/src/tags/object/AudioUltra/model.js b/web/libs/editor/src/tags/object/AudioUltra/model.js index f9abc2bc525d..cd2efebc9a0d 100644 --- a/web/libs/editor/src/tags/object/AudioUltra/model.js +++ b/web/libs/editor/src/tags/object/AudioUltra/model.js @@ -2,13 +2,11 @@ import { observe } from "mobx"; import { getEnv, getRoot, getType, types } from "mobx-state-tree"; import { createRef } from "react"; import { customTypes } from "../../../core/CustomTypes"; -import { guidGenerator } from "../../../core/Helpers.ts"; import { AnnotationMixin } from "../../../mixins/AnnotationMixin"; import IsReadyMixin from "../../../mixins/IsReadyMixin"; import ProcessAttrsMixin from "../../../mixins/ProcessAttrs"; import { SyncableMixin } from "../../../mixins/Syncable"; import { AudioRegionModel } from "../../../regions/AudioRegion"; -import Utils from "../../../utils"; import { FF_LSDV_E_278, isFF } from "../../../utils/feature-flags"; import { isDefined } from "../../../utils/utilities"; import ObjectBase from "../Base"; @@ -155,13 +153,13 @@ export const AudioModel = types.compose( activeStates() { const states = self.states(); - return states && states.filter((s) => getType(s).name === "LabelsModel" && s.isSelected); + return states?.filter((s) => getType(s).name === "LabelsModel" && s.isSelected); }, get activeState() { const states = self.states(); - return states && states.filter((s) => getType(s).name === "LabelsModel" && s.isSelected)[0]; + return states?.filter((s) => getType(s).name === "LabelsModel" && s.isSelected)[0]; }, get activeLabel() { @@ -221,9 +219,9 @@ export const AudioModel = types.compose( ////// Incoming registerSyncHandlers() { - ["play", "pause", "seek"].forEach((event) => { + for (const event of ["play", "pause", "seek"]) { self.syncHandlers.set(event, self.handleSync); - }); + } self.syncHandlers.set("speed", self.handleSyncSpeed); }, @@ -290,13 +288,13 @@ export const AudioModel = types.compose( const selectedColor = activeState?.selectedColor; const labels = activeState?.selectedValues(); - selectedRegions.forEach((r) => { + for (const r of selectedRegions) { r.update({ color: selectedColor, labels: labels ?? [] }); const region = r.isRegion ? self.updateRegion(r) : self.addRegion(r); self.annotation.selectArea(region); - }); + } if (selectedRegions.length) { self.requestWSUpdate(); @@ -343,7 +341,7 @@ export const AudioModel = types.compose( (target) => target.type === "paragraphs" && target.contextscroll, ); - syncedParagraphs.forEach((paragraph) => { + for (const paragraph of syncedParagraphs) { const segments = Object.values(paragraph.regionsStartEnd).map(({ start, end }) => ({ start, end, @@ -353,7 +351,7 @@ export const AudioModel = types.compose( })); self._ws.addRegions(segments); - }); + } }, handleNewRegions() { @@ -383,7 +381,7 @@ export const AudioModel = types.compose( }, onHotKey(e) { - e && e.preventDefault(); + e?.preventDefault(); self._ws.togglePlay(); return false; }, @@ -459,9 +457,9 @@ export const AudioModel = types.compose( }, clearRegionMappings() { - self.regs.forEach((r) => { + for (const r of self.regs) { r.setWSRegion(null); - }); + } }, onLoad(ws) { diff --git a/web/libs/editor/src/tags/object/Video/Video.js b/web/libs/editor/src/tags/object/Video/Video.js index 5855670d668b..de266d06fa59 100644 --- a/web/libs/editor/src/tags/object/Video/Video.js +++ b/web/libs/editor/src/tags/object/Video/Video.js @@ -140,7 +140,7 @@ const Model = types // normalize framerate — should be string with number of frames per second const framerate = Number(parseValue(self.framerate, self.store.task?.dataObj)); - if (!framerate || isNaN(framerate)) self.framerate = "24"; + if (!framerate || Number.isNaN(framerate)) self.framerate = "24"; else if (framerate < 1) self.framerate = String(1 / framerate); else self.framerate = String(framerate); }, @@ -178,9 +178,9 @@ const Model = types ////// Incoming registerSyncHandlers() { - ["play", "pause", "seek"].forEach((event) => { + for (const event of ["play", "pause", "seek"]) { self.syncHandlers.set(event, self.handleSync); - }); + } self.syncHandlers.set("speed", self.handleSyncSpeed); }, @@ -260,9 +260,9 @@ const Model = types const area = self.annotation.createResult({ sequence }, {}, control, self); // add labels - self.activeStates().forEach((tag) => { + for (const tag of self.activeStates()) { area.setValue(tag); - }); + } return area; },