Merge remote-tracking branch 'origin/develop' into staging

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2025-01-08 16:38:57 +07:00
commit cc2c0c31b2
No known key found for this signature in database
GPG Key ID: BD80F68D68D8F7F2
14 changed files with 244 additions and 59 deletions

View File

@ -207,4 +207,11 @@
<path d="M0.5 7.99988L3 10.4999L3.705 9.79488L2.415 8.49988H6.5C6.77614 8.49988 7 8.27602 7 7.99988C7 7.72374 6.77614 7.49988 6.5 7.49988H2.415L3.705 6.20488L3 5.49988L0.5 7.99988Z"/> <path d="M0.5 7.99988L3 10.4999L3.705 9.79488L2.415 8.49988H6.5C6.77614 8.49988 7 8.27602 7 7.99988C7 7.72374 6.77614 7.49988 6.5 7.49988H2.415L3.705 6.20488L3 5.49988L0.5 7.99988Z"/>
<path d="M12.295 6.20488L13.585 7.49988H9.5C9.22386 7.49988 9 7.72374 9 7.99988C9 8.27602 9.22386 8.49988 9.5 8.49988H13.585L12.295 9.79488L13 10.4999L15.5 7.99988L13 5.49988L12.295 6.20488Z"/> <path d="M12.295 6.20488L13.585 7.49988H9.5C9.22386 7.49988 9 7.72374 9 7.99988C9 8.27602 9.22386 8.49988 9.5 8.49988H13.585L12.295 9.79488L13 10.4999L15.5 7.99988L13 5.49988L12.295 6.20488Z"/>
</symbol> </symbol>
<symbol id="union" viewBox="0 0 32 32" fill="none">
<path d="M17 3H6C4.34315 3 3 4.34315 3 6V17C3 18.6569 4.34315 20 6 20H12V26C12 27.6569 13.3431 29 15 29H26C27.6569 29 29 27.6569 29 26V15C29 13.3431 27.6569 12 26 12H20V6C20 4.34315 18.6569 3 17 3Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
</symbol>
<symbol id="divide" viewBox="0 0 32 32" fill="none">
<path d="M3 6C3 4.34315 4.34315 3 6 3H17C18.6569 3 20 4.34315 20 6V10.5556V17C20 18.6569 18.6569 20 17 20H10.5556H6C4.34314 20 3 18.6569 3 17V6Z" stroke="currentColor" stroke-width="2"/>
<path d="M12 15C12 13.3431 13.3431 12 15 12H26C27.6569 12 29 13.3431 29 15V26C29 27.6569 27.6569 29 26 29H15C13.3431 29 12 27.6569 12 26V15Z" stroke="currentColor" stroke-width="2"/>
</symbol>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -46,9 +46,12 @@
"AddRowAfter": "Add after", "AddRowAfter": "Add after",
"DeleteRow": "Delete", "DeleteRow": "Delete",
"DeleteTable": "Delete", "DeleteTable": "Delete",
"MergeCells": "Merge cells",
"SplitCells": "Split cells",
"Duplicate": "Duplicate", "Duplicate": "Duplicate",
"CategoryRow": "Rows", "CategoryRow": "Rows",
"CategoryColumn": "Columns", "CategoryColumn": "Columns",
"CategoryCell": "Cells",
"Table": "Table", "Table": "Table",
"InsertTable": "Insert table", "InsertTable": "Insert table",
"TableOptions": "Customize table", "TableOptions": "Customize table",

View File

@ -46,9 +46,12 @@
"AddRowAfter": "Добавить после", "AddRowAfter": "Добавить после",
"DeleteRow": "Удалить", "DeleteRow": "Удалить",
"DeleteTable": "Удалить", "DeleteTable": "Удалить",
"MergeCells": "Объединить ячейки",
"SplitCells": "Разделить ячейки",
"Duplicate": "Дублировать", "Duplicate": "Дублировать",
"CategoryRow": "Строки", "CategoryRow": "Строки",
"CategoryColumn": "Колонки", "CategoryColumn": "Колонки",
"CategoryCell": "Ячейки",
"Table": "Таблица", "Table": "Таблица",
"InsertTable": "Добавить таблицу", "InsertTable": "Добавить таблицу",
"TableOptions": "Настроить таблицу", "TableOptions": "Настроить таблицу",

View File

@ -41,5 +41,7 @@ loadMetadata(textEditor.icon, {
Download: `${icons}#download`, Download: `${icons}#download`,
Note: `${icons}#note`, Note: `${icons}#note`,
Comment: `${icons}#comment`, Comment: `${icons}#comment`,
SelectTable: `${icons}#move` SelectTable: `${icons}#move`,
MergeCells: `${icons}#union`,
SplitCells: `${icons}#divide`
}) })

View File

@ -175,7 +175,7 @@
} }
&__col { &__col {
right: -1.5rem; right: calc(var(--table-offscreen-spacing) - 1.5rem);
top: 0; top: 0;
bottom: 0; bottom: 0;
margin: 1.25rem 0; margin: 1.25rem 0;
@ -188,7 +188,7 @@
&__row { &__row {
bottom: -0.25rem; bottom: -0.25rem;
left: var(--table-offscreen-spacing); left: var(--table-offscreen-spacing);
right: 0; right: var(--table-offscreen-spacing);
.table-button { .table-button {
height: 1.25rem; height: 1.25rem;

View File

@ -13,25 +13,76 @@
// limitations under the License. // limitations under the License.
// //
import type { Node as ProseMirrorNode } from '@tiptap/pm/model' import { Fragment, type Node, type Node as ProseMirrorNode } from '@tiptap/pm/model'
import type { Transaction } from '@tiptap/pm/state' import type { Transaction } from '@tiptap/pm/state'
import { TableMap } from '@tiptap/pm/tables' import { type CellSelection, TableMap } from '@tiptap/pm/tables'
import type { TableNodeLocation } from '../types' import type { TableNodeLocation } from '../types'
import { type Editor } from '@tiptap/core'
type TableRow = Array<ProseMirrorNode | null> type TableRow = Array<ProseMirrorNode | null>
type TableRows = TableRow[] type TableRows = TableRow[]
export function moveColumn (table: TableNodeLocation, from: number, to: number, tr: Transaction): Transaction { export function moveSelectedColumns (
const cols = transpose(tableToCells(table)) editor: Editor,
moveRowInplace(cols, from, to) table: TableNodeLocation,
tableFromCells(table, transpose(cols), tr) selection: CellSelection,
to: number,
tr: Transaction
): Transaction {
const tableMap = TableMap.get(table.node)
let columnStart = -1
let columnEnd = -1
selection.forEachCell((node, pos) => {
const cell = tableMap.findCell(pos - table.pos - 1)
for (let i = cell.left; i < cell.right; i++) {
columnStart = columnStart >= 0 ? Math.min(cell.left, columnStart) : cell.left
columnEnd = columnEnd >= 0 ? Math.max(cell.right, columnEnd) : cell.right
}
})
if (to < 0 || to > tableMap.width || (to >= columnStart && to < columnEnd)) return tr
const rows = tableToCells(table)
for (const row of rows) {
const range = row.splice(columnStart, columnEnd - columnStart)
const offset = to > columnStart ? to - (columnEnd - columnStart - 1) : to
row.splice(offset, 0, ...range)
}
tableFromCells(editor, table, rows, tr)
return tr return tr
} }
export function moveRow (table: TableNodeLocation, from: number, to: number, tr: Transaction): Transaction { export function moveSelectedRows (
editor: Editor,
table: TableNodeLocation,
selection: CellSelection,
to: number,
tr: Transaction
): Transaction {
const tableMap = TableMap.get(table.node)
let rowStart = -1
let rowEnd = -1
selection.forEachCell((node, pos) => {
const cell = tableMap.findCell(pos - table.pos - 1)
for (let i = cell.top; i < cell.bottom; i++) {
rowStart = rowStart >= 0 ? Math.min(cell.top, rowStart) : cell.top
rowEnd = rowEnd >= 0 ? Math.max(cell.bottom, rowEnd) : cell.bottom
}
})
if (to < 0 || to > tableMap.height || (to >= rowStart && to < rowEnd)) return tr
const rows = tableToCells(table) const rows = tableToCells(table)
moveRowInplace(rows, from, to) const range = rows.splice(rowStart, rowEnd - rowStart)
tableFromCells(table, rows, tr) const offset = to > rowStart ? to - (rowEnd - rowStart - 1) : to
rows.splice(offset, 0, ...range)
tableFromCells(editor, table, rows, tr)
return tr return tr
} }
@ -78,23 +129,17 @@ export function duplicateColumns (table: TableNodeLocation, columnIndices: numbe
return tr return tr
} }
function moveRowInplace (rows: TableRows, from: number, to: number): void {
rows.splice(to, 0, rows.splice(from, 1)[0])
}
function transpose (rows: TableRows): TableRows {
return rows[0].map((_, colIdx) => rows.map((row) => row[colIdx]))
}
function tableToCells (table: TableNodeLocation): TableRows { function tableToCells (table: TableNodeLocation): TableRows {
const { map, width, height } = TableMap.get(table.node) const { map, width, height } = TableMap.get(table.node)
const visitedCells = new Set<number>()
const rows = [] const rows = []
for (let row = 0; row < height; row++) { for (let row = 0; row < height; row++) {
const cells = [] const cells = []
for (let col = 0; col < width; col++) { for (let col = 0; col < width; col++) {
const pos = map[row * width + col] const pos = map[row * width + col]
cells.push(table.node.nodeAt(pos)) cells.push(!visitedCells.has(pos) ? table.node.nodeAt(pos) : null)
visitedCells.add(pos)
} }
rows.push(cells) rows.push(cells)
} }
@ -102,23 +147,11 @@ function tableToCells (table: TableNodeLocation): TableRows {
return rows return rows
} }
function tableFromCells (table: TableNodeLocation, rows: TableRows, tr: Transaction): void { function tableFromCells (editor: Editor, table: TableNodeLocation, rows: TableRows, tr: Transaction): void {
const { map, width, height } = TableMap.get(table.node) const schema = editor.schema.nodes
const mapStart = tr.mapping.maps.length const newRowNodes = rows.map((row) =>
schema.tableRow.create(null, row.filter((cell) => cell !== null) as readonly Node[])
for (let row = 0; row < height; row++) { )
for (let col = 0; col < width; col++) { const newTableNode = table.node.copy(Fragment.from(newRowNodes))
const pos = map[row * width + col] tr.replaceWith(table.pos, table.pos + table.node.nodeSize, newTableNode)
const oldCell = table.node.nodeAt(pos)
const newCell = rows[row][col]
if (oldCell !== null && newCell !== null && oldCell !== newCell) {
const start = tr.mapping.slice(mapStart).map(table.start + pos)
const end = start + oldCell.nodeSize
tr.replaceWith(start, end, newCell)
}
}
}
} }

View File

@ -15,14 +15,14 @@
import { type Editor } from '@tiptap/core' import { type Editor } from '@tiptap/core'
import { type EditorState } from '@tiptap/pm/state' import { type EditorState } from '@tiptap/pm/state'
import { TableMap } from '@tiptap/pm/tables' import { CellSelection, TableMap } from '@tiptap/pm/tables'
import { Decoration } from '@tiptap/pm/view' import { Decoration } from '@tiptap/pm/view'
import textEditor from '@hcengineering/text-editor' import textEditor from '@hcengineering/text-editor'
import { type TableNodeLocation } from '../types' import { type TableNodeLocation } from '../types'
import { findTable, getSelectedColumns, isColumnSelected, selectColumn } from '../utils' import { findTable, getSelectedColumns, isColumnSelected, selectColumn } from '../utils'
import { duplicateColumns, moveColumn } from './actions' import { duplicateColumns, moveSelectedColumns } from './actions'
import DeleteCol from '../../../icons/table/DeleteCol.svelte' import DeleteCol from '../../../icons/table/DeleteCol.svelte'
import Duplicate from '../../../icons/table/Duplicate.svelte' import Duplicate from '../../../icons/table/Duplicate.svelte'
import { createCellsHandle, type OptionItem } from './cellsHandle' import { createCellsHandle, type OptionItem } from './cellsHandle'
@ -120,8 +120,10 @@ const handleMouseDown = (
if (col !== dropIndex) { if (col !== dropIndex) {
let tr = editor.state.tr let tr = editor.state.tr
tr = selectColumn(table, dropIndex, tr) const selection = editor.state.selection
tr = moveColumn(table, col, dropIndex, tr) if (selection instanceof CellSelection) {
tr = moveSelectedColumns(editor, table, selection, dropIndex, tr)
}
editor.view.dispatch(tr) editor.view.dispatch(tr)
} }
window.removeEventListener('mouseup', handleFinish) window.removeEventListener('mouseup', handleFinish)

View File

@ -15,14 +15,14 @@
import { type Editor } from '@tiptap/core' import { type Editor } from '@tiptap/core'
import { type EditorState } from '@tiptap/pm/state' import { type EditorState } from '@tiptap/pm/state'
import { TableMap } from '@tiptap/pm/tables' import { CellSelection, TableMap } from '@tiptap/pm/tables'
import { Decoration } from '@tiptap/pm/view' import { Decoration } from '@tiptap/pm/view'
import textEditor from '@hcengineering/text-editor' import textEditor from '@hcengineering/text-editor'
import { type TableNodeLocation } from '../types' import { type TableNodeLocation } from '../types'
import { findTable, getSelectedRows, isRowSelected, selectRow } from '../utils' import { findTable, getSelectedRows, isRowSelected, selectRow } from '../utils'
import { duplicateRows, moveRow } from './actions' import { duplicateRows, moveSelectedRows } from './actions'
import DeleteRow from '../../../icons/table/DeleteRow.svelte' import DeleteRow from '../../../icons/table/DeleteRow.svelte'
import Duplicate from '../../../icons/table/Duplicate.svelte' import Duplicate from '../../../icons/table/Duplicate.svelte'
import { createCellsHandle, type OptionItem } from './cellsHandle' import { createCellsHandle, type OptionItem } from './cellsHandle'
@ -120,8 +120,10 @@ const handleMouseDown = (
if (row !== dropIndex) { if (row !== dropIndex) {
let tr = editor.state.tr let tr = editor.state.tr
tr = selectRow(table, dropIndex, tr) const selection = editor.state.selection
tr = moveRow(table, row, dropIndex, tr) if (selection instanceof CellSelection) {
tr = moveSelectedRows(editor, table, selection, dropIndex, tr)
}
editor.view.dispatch(tr) editor.view.dispatch(tr)
} }
window.removeEventListener('mouseup', handleFinish) window.removeEventListener('mouseup', handleFinish)

View File

@ -60,10 +60,17 @@ function getTableCellBorders (
const { width, height } = tableMap const { width, height } = tableMap
const cellIndex = tableMap.map.indexOf(cell) const cellIndex = tableMap.map.indexOf(cell)
const rect = tableMap.findCell(cell)
const cellW = rect.right - rect.left
const cellH = rect.bottom - rect.top
const testRight = cellW
const testBottom = width * cellH
const topCell = cellIndex >= width ? tableMap.map[cellIndex - width] : undefined const topCell = cellIndex >= width ? tableMap.map[cellIndex - width] : undefined
const bottomCell = cellIndex < width * height - width ? tableMap.map[cellIndex + width] : undefined const bottomCell = cellIndex < width * height - testBottom ? tableMap.map[cellIndex + testBottom] : undefined
const leftCell = cellIndex % width !== 0 ? tableMap.map[cellIndex - 1] : undefined const leftCell = cellIndex % width > 0 ? tableMap.map[cellIndex - 1] : undefined
const rightCell = cellIndex % width !== width - 1 ? tableMap.map[cellIndex + 1] : undefined const rightCell = cellIndex % width < width - testRight ? tableMap.map[cellIndex + testRight] : undefined
return { return {
top: topCell === undefined || !selection.includes(topCell), top: topCell === undefined || !selection.includes(topCell),

View File

@ -116,6 +116,24 @@ export async function openTableOptions (editor: Editor, event: MouseEvent): Prom
label: textEditor.string.CategoryRow label: textEditor.string.CategoryRow
} }
}, },
{
id: '#mergeCells',
icon: textEditor.icon.MergeCells,
label: textEditor.string.MergeCells,
action: () => editor.commands.mergeCells(),
category: {
label: textEditor.string.CategoryCell
}
},
{
id: '#splitCell',
icon: textEditor.icon.SplitCells,
label: textEditor.string.SplitCells,
action: () => editor.commands.splitCell(),
category: {
label: textEditor.string.CategoryCell
}
},
{ {
id: '#deleteTable', id: '#deleteTable',
icon: DeleteTable, icon: DeleteTable,

View File

@ -18,17 +18,19 @@ import TiptapTableCell from '@tiptap/extension-table-cell'
import { Plugin, PluginKey, type Selection } from '@tiptap/pm/state' import { Plugin, PluginKey, type Selection } from '@tiptap/pm/state'
import { DecorationSet } from '@tiptap/pm/view' import { DecorationSet } from '@tiptap/pm/view'
import { findTable } from './utils' import { CellSelection, type Rect, TableMap } from '@tiptap/pm/tables'
import { columnHandlerDecoration } from './decorations/columnHandlerDecoration' import { columnHandlerDecoration } from './decorations/columnHandlerDecoration'
import { columnInsertDecoration } from './decorations/columnInsertDecoration' import { columnInsertDecoration } from './decorations/columnInsertDecoration'
import { rowHandlerDecoration } from './decorations/rowHandlerDecoration'
import { rowInsertDecoration } from './decorations/rowInsertDecoration' import { rowInsertDecoration } from './decorations/rowInsertDecoration'
import { tableDragMarkerDecoration } from './decorations/tableDragMarkerDecoration' import { tableDragMarkerDecoration } from './decorations/tableDragMarkerDecoration'
import { tableSelectionDecoration } from './decorations/tableSelectionDecoration' import { tableSelectionDecoration } from './decorations/tableSelectionDecoration'
import { rowHandlerDecoration } from './decorations/rowHandlerDecoration' import { findTable } from './utils'
import { type Node } from '@tiptap/pm/model'
export const TableCell = TiptapTableCell.extend({ export const TableCell = TiptapTableCell.extend({
addProseMirrorPlugins () { addProseMirrorPlugins () {
return [tableCellDecorationPlugin(this.editor)] return [tableCellDecorationPlugin(this.editor), tableSelectionNormalizer()]
} }
}) })
@ -78,3 +80,80 @@ const tableCellDecorationPlugin = (editor: Editor): Plugin<TableCellDecorationPl
} }
}) })
} }
const tableSelectionNormalizer = (): Plugin<any> => {
return new Plugin({
appendTransaction: (transactions, oldState, newState) => {
const selection = newState.selection
if (selection.eq(oldState.selection) || !(selection instanceof CellSelection)) return
const table = findTable(newState.selection)
if (table === undefined) return
const tableMap = TableMap.get(table.node)
let rect: Rect | undefined
const walkCell = (pos: number): void => {
const cell = tableMap.findCell(pos)
if (cell === undefined) return
if (rect === undefined) {
rect = { ...cell }
} else {
rect.left = Math.min(rect.left, cell.left)
rect.top = Math.min(rect.top, cell.top)
rect.right = Math.max(rect.right, cell.right)
rect.bottom = Math.max(rect.bottom, cell.bottom)
}
}
selection.forEachCell((_node, pos) => {
walkCell(pos - table.pos - 1)
})
if (rect === undefined) return
const rectSelection: number[] = []
for (let row = rect.top; row < rect.bottom; row++) {
for (let col = rect.left; col < rect.right; col++) {
rectSelection.push(tableMap.map[row * tableMap.width + col])
}
}
rectSelection.forEach((pos) => {
walkCell(pos)
})
if (rect === undefined) return
// Original promemirror implementation of TableMap.positionAt skips rowspawn cells, which leads to unpredictable selection behaviour
const firstCellOffset = cellPositionAt(tableMap, rect.bottom - 1, rect.right - 1, table.node)
const lastCellOffset = cellPositionAt(tableMap, rect.top, rect.left, table.node)
const firstCellPos = newState.doc.resolve(table.start + firstCellOffset)
const lastCellPos = newState.doc.resolve(table.start + lastCellOffset)
const reverseOrder = selection.$anchorCell.pos > selection.$headCell.pos
const $head = reverseOrder ? lastCellPos : firstCellPos
const $anchor = reverseOrder ? firstCellPos : lastCellPos
const newSelection = new CellSelection($anchor, $head)
if (newSelection.eq(newState.selection)) return
return newState.tr.setSelection(new CellSelection($anchor, $head))
}
})
}
function cellPositionAt (tableMap: TableMap, row: number, col: number, table: Node): number {
for (let i = 0, rowStart = 0; ; i++) {
const rowEnd = rowStart + table.child(i).nodeSize
if (i === row) {
const index = col + row * tableMap.width
const rowEndIndex = (row + 1) * tableMap.width
return index === rowEndIndex ? rowEnd - 1 : tableMap.map[index]
}
rowStart = rowEnd
}
}

View File

@ -84,11 +84,14 @@ export default plugin(textEditorId, {
DeleteColumn: '' as IntlString, DeleteColumn: '' as IntlString,
AddRowBefore: '' as IntlString, AddRowBefore: '' as IntlString,
AddRowAfter: '' as IntlString, AddRowAfter: '' as IntlString,
MergeCells: '' as IntlString,
SplitCells: '' as IntlString,
DeleteRow: '' as IntlString, DeleteRow: '' as IntlString,
DeleteTable: '' as IntlString, DeleteTable: '' as IntlString,
Duplicate: '' as IntlString, Duplicate: '' as IntlString,
CategoryRow: '' as IntlString, CategoryRow: '' as IntlString,
CategoryColumn: '' as IntlString, CategoryColumn: '' as IntlString,
CategoryCell: '' as IntlString,
Table: '' as IntlString, Table: '' as IntlString,
TableOptions: '' as IntlString, TableOptions: '' as IntlString,
SelectTable: '' as IntlString, SelectTable: '' as IntlString,
@ -125,6 +128,8 @@ export default plugin(textEditorId, {
Download: '' as Asset, Download: '' as Asset,
Note: '' as Asset, Note: '' as Asset,
Comment: '' as Asset, Comment: '' as Asset,
SelectTable: '' as Asset SelectTable: '' as Asset,
MergeCells: '' as Asset,
SplitCells: '' as Asset
} }
}) })

View File

@ -553,6 +553,13 @@ export interface Session {
query: DocumentQuery<T>, query: DocumentQuery<T>,
options?: FindOptions<T> options?: FindOptions<T>
) => Promise<void> ) => Promise<void>
findAllRaw: <T extends Doc>(
ctx: MeasureContext,
pipeline: Pipeline,
_class: Ref<Class<T>>,
query: DocumentQuery<T>,
options?: FindOptions<T>
) => Promise<FindResult<T>>
searchFulltext: (ctx: ClientSessionCtx, query: SearchQuery, options: SearchOptions) => Promise<void> searchFulltext: (ctx: ClientSessionCtx, query: SearchQuery, options: SearchOptions) => Promise<void>
tx: (ctx: ClientSessionCtx, tx: Tx) => Promise<void> tx: (ctx: ClientSessionCtx, tx: Tx) => Promise<void>
loadChunk: (ctx: ClientSessionCtx, domain: Domain, idx?: number) => Promise<void> loadChunk: (ctx: ClientSessionCtx, domain: Domain, idx?: number) => Promise<void>

View File

@ -716,21 +716,38 @@ class TSessionManager implements SessionManager {
const user = pipeline.context.modelDb.getAccountByEmail(session.getUser()) const user = pipeline.context.modelDb.getAccountByEmail(session.getUser())
if (user === undefined) return if (user === undefined) return
const status = (await pipeline.findAll(ctx, core.class.UserStatus, { user: user._id }, { limit: 1 }))[0] const clientCtx: ClientSessionCtx = {
requestId: undefined,
pipeline,
sendResponse: async (msg) => {
// No response
},
ctx,
sendError: async (msg, error: Status) => {
// Assume no error send
},
sendPong: () => {}
}
const status = (
await session.findAllRaw(ctx, pipeline, core.class.UserStatus, { user: user._id }, { limit: 1 })
)[0]
const txFactory = new TxFactory(user._id, true) const txFactory = new TxFactory(user._id, true)
if (status === undefined) { if (status === undefined) {
const tx = txFactory.createTxCreateDoc(core.class.UserStatus, core.space.Space, { const tx = txFactory.createTxCreateDoc(core.class.UserStatus, core.space.Space, {
online, online,
user: user._id user: user._id
}) })
await pipeline.tx(ctx, [tx]) await session.tx(clientCtx, tx)
} else if (status.online !== online) { } else if (status.online !== online) {
const tx = txFactory.createTxUpdateDoc(status._class, status.space, status._id, { const tx = txFactory.createTxUpdateDoc(status._class, status.space, status._id, {
online online
}) })
await pipeline.tx(ctx, [tx]) await session.tx(clientCtx, tx)
} }
} catch {} } catch (err: any) {
ctx.error('failed to set status', { err })
Analytics.handleError(err)
}
} }
async close (ctx: MeasureContext, ws: ConnectionSocket, wsid: string): Promise<void> { async close (ctx: MeasureContext, ws: ConnectionSocket, wsid: string): Promise<void> {