diff --git a/.vscode/launch.json b/.vscode/launch.json index 96d8043c23..b80e833e9b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -85,7 +85,6 @@ "SERVER_SECRET": "secret", "REKONI_URL": "http://localhost:4004", "MODEL_JSON": "${workspaceRoot}/models/all/bundle/model.json", - "REGION":"pg", "ELASTIC_INDEX_NAME": "local_storage_index", "STATS_URL":"http://host.docker.internal:4900", "ACCOUNTS_URL": "http://localhost:3000", diff --git a/dev/tool/src/clean.ts b/dev/tool/src/clean.ts index d049f81491..da7d4f6c4a 100644 --- a/dev/tool/src/clean.ts +++ b/dev/tool/src/clean.ts @@ -1152,7 +1152,9 @@ function isPersonAccount (tx: TxCUD): boolean { } async function update (h: Hierarchy, db: Db, doc: T, update: DocumentUpdate): Promise { - await db.collection(h.getDomain(doc._class)).updateOne({ _id: doc._id }, { $set: { ...update, '%hash%': null } }) + await db + .collection(h.getDomain(doc._class)) + .updateOne({ _id: doc._id }, { $set: { ...update, '%hash%': Date.now().toString(16) } }) } async function updateId ( @@ -1173,12 +1175,14 @@ async function updateId ( const newId = generateId() // update txes - await db.collection(DOMAIN_TX).updateMany({ objectId: doc._id }, { $set: { objectId: newId, '%hash%': null } }) + await db + .collection(DOMAIN_TX) + .updateMany({ objectId: doc._id }, { $set: { objectId: newId, '%hash%': Date.now().toString(16) } }) // update nested txes await db .collection(DOMAIN_TX) - .updateMany({ 'tx.objectId': doc._id }, { $set: { 'tx.objectId': newId, '%hash%': null } }) + .updateMany({ 'tx.objectId': doc._id }, { $set: { 'tx.objectId': newId, '%hash%': Date.now().toString(16) } }) // we have generated ids for calendar, let's update in if (h.isDerived(doc._class, core.class.Account)) { @@ -1232,7 +1236,7 @@ async function updateId ( await db.collection(domain).insertOne({ ...raw, _id: newId as any, - '%hash%': null + '%hash%': Date.now().toString(16) }) await db.collection(domain).deleteOne({ _id: doc._id }) } diff --git a/models/activity/src/migration.ts b/models/activity/src/migration.ts index 898984596f..34c1ff8b34 100644 --- a/models/activity/src/migration.ts +++ b/models/activity/src/migration.ts @@ -217,12 +217,6 @@ export const activityOperation: MigrateOperation = { state: 'migrate-activity-markup', func: migrateActivityMarkup }, - { - state: 'fix-rename-backups', - func: async (client: MigrationClient): Promise => { - await client.update(DOMAIN_ACTIVITY, { '%hash%': { $exists: true } }, { $set: { '%hash%': null } }) - } - }, { state: 'move-reactions', func: async (client: MigrationClient): Promise => { diff --git a/models/attachment/src/migration.ts b/models/attachment/src/migration.ts index 73b55dd12b..df8c59ae94 100644 --- a/models/attachment/src/migration.ts +++ b/models/attachment/src/migration.ts @@ -24,12 +24,6 @@ import attachment, { attachmentId, DOMAIN_ATTACHMENT } from '.' export const attachmentOperation: MigrateOperation = { async migrate (client: MigrationClient): Promise { await tryMigrate(client, attachmentId, [ - { - state: 'fix-rename-backups', - func: async (client: MigrationClient): Promise => { - await client.update(DOMAIN_ATTACHMENT, { '%hash%': { $exists: true } }, { $set: { '%hash%': null } }) - } - }, { state: 'fix-attachedTo', func: async (client: MigrationClient): Promise => { diff --git a/models/chunter/src/migration.ts b/models/chunter/src/migration.ts index 65438a982b..70d2c2151c 100644 --- a/models/chunter/src/migration.ts +++ b/models/chunter/src/migration.ts @@ -362,12 +362,6 @@ export const chunterOperation: MigrateOperation = { 'attributeUpdates.attrKey': 'members' }) } - }, - { - state: 'fix-rename-backups', - func: async (client: MigrationClient): Promise => { - await client.update(DOMAIN_CHUNTER, { '%hash%': { $exists: true } }, { $set: { '%hash%': null } }) - } } ]) }, diff --git a/models/contact/src/migration.ts b/models/contact/src/migration.ts index a109668aa8..d78df3c00e 100644 --- a/models/contact/src/migration.ts +++ b/models/contact/src/migration.ts @@ -26,7 +26,7 @@ import activity, { DOMAIN_ACTIVITY } from '@hcengineering/model-activity' import core, { DOMAIN_SPACE } from '@hcengineering/model-core' import { DOMAIN_VIEW } from '@hcengineering/model-view' -import contact, { contactId, DOMAIN_CHANNEL, DOMAIN_CONTACT } from './index' +import contact, { contactId, DOMAIN_CONTACT } from './index' async function createEmployeeEmail (client: TxOperations): Promise { const employees = await client.findAll(contact.mixin.Employee, {}) @@ -300,13 +300,6 @@ export const contactOperation: MigrateOperation = { { state: 'create-person-spaces-v1', func: createPersonSpaces - }, - { - state: 'fix-rename-backups', - func: async (client: MigrationClient): Promise => { - await client.update(DOMAIN_CONTACT, { '%hash%': { $exists: true } }, { $set: { '%hash%': null } }) - await client.update(DOMAIN_CHANNEL, { '%hash%': { $exists: true } }, { $set: { '%hash%': null } }) - } } ]) }, diff --git a/models/core/src/migration.ts b/models/core/src/migration.ts index ccebe20b86..f922129f28 100644 --- a/models/core/src/migration.ts +++ b/models/core/src/migration.ts @@ -424,10 +424,12 @@ export const coreOperation: MigrateOperation = { func: migrateCollaborativeContentToStorage }, { - state: 'fix-rename-backups', + state: 'fix-backups-hash-timestamp', func: async (client: MigrationClient): Promise => { - await client.update(DOMAIN_TX, { '%hash%': { $exists: true } }, { $set: { '%hash%': null } }) - await client.update(DOMAIN_SPACE, { '%hash%': { $exists: true } }, { $set: { '%hash%': null } }) + const now = Date.now().toString(16) + for (const d of client.hierarchy.domains()) { + await client.update(d, { '%hash%': { $in: [null, ''] } }, { $set: { '%hash%': now } }) + } } }, { diff --git a/models/document/src/migration.ts b/models/document/src/migration.ts index 6bc4efb4a4..79dcd9fd4a 100644 --- a/models/document/src/migration.ts +++ b/models/document/src/migration.ts @@ -335,12 +335,6 @@ export const documentOperation: MigrateOperation = { state: 'renameFields', func: renameFields }, - { - state: 'fix-rename-backups', - func: async (client: MigrationClient): Promise => { - await client.update(DOMAIN_DOCUMENT, { '%hash%': { $exists: true } }, { $set: { '%hash%': null } }) - } - }, { state: 'renameFieldsRevert', func: renameFieldsRevert diff --git a/models/drive/src/migration.ts b/models/drive/src/migration.ts index 6ea69d90d2..e2762c8b13 100644 --- a/models/drive/src/migration.ts +++ b/models/drive/src/migration.ts @@ -132,12 +132,6 @@ export const driveOperation: MigrateOperation = { { state: 'renameFields', func: renameFields - }, - { - state: 'fix-rename-backups', - func: async (client: MigrationClient): Promise => { - await client.update(DOMAIN_DRIVE, { '%hash%': { $exists: true } }, { $set: { '%hash%': null } }) - } } ]) }, diff --git a/models/notification/src/migration.ts b/models/notification/src/migration.ts index 56fe3affe2..972d3f77a2 100644 --- a/models/notification/src/migration.ts +++ b/models/notification/src/migration.ts @@ -393,12 +393,6 @@ export const notificationOperation: MigrateOperation = { ) } }, - { - state: 'fix-rename-backups', - func: async (client: MigrationClient): Promise => { - await client.update(DOMAIN_DOC_NOTIFY, { '%hash%': { $exists: true } }, { $set: { '%hash%': null } }) - } - }, { state: 'remove-update-txes-docnotify-ctx-v2', func: async (client) => { diff --git a/models/task/src/migration.ts b/models/task/src/migration.ts index ffb516d8de..03c06f7c69 100644 --- a/models/task/src/migration.ts +++ b/models/task/src/migration.ts @@ -569,12 +569,6 @@ export const taskOperation: MigrateOperation = { await migrateSpace(client, task.space.Sequence, core.space.Workspace, [DOMAIN_KANBAN]) } }, - { - state: 'fix-rename-backups', - func: async (client: MigrationClient): Promise => { - await client.update(DOMAIN_TASK, { '%hash%': { $exists: true } }, { $set: { '%hash%': null } }) - } - }, { state: 'migrateRanks', func: migrateRanks diff --git a/models/time/src/migration.ts b/models/time/src/migration.ts index 481e28dfe0..91e282515c 100644 --- a/models/time/src/migration.ts +++ b/models/time/src/migration.ts @@ -172,12 +172,6 @@ export const timeOperation: MigrateOperation = { func: async (client) => { await fillProps(client) } - }, - { - state: 'fix-rename-backups', - func: async (client: MigrationClient): Promise => { - await client.update(DOMAIN_TIME, { '%hash%': { $exists: true } }, { $set: { '%hash%': null } }) - } } ]) }, diff --git a/models/view/src/migration.ts b/models/view/src/migration.ts index 572a60d8d7..f5587c0547 100644 --- a/models/view/src/migration.ts +++ b/models/view/src/migration.ts @@ -86,12 +86,6 @@ export const viewOperation: MigrateOperation = { { state: 'remove-done-state-filter', func: removeDoneStateFilter - }, - { - state: 'fix-rename-backups', - func: async (client: MigrationClient): Promise => { - await client.update(DOMAIN_VIEW, { '%hash%': { $exists: true } }, { $set: { '%hash%': null } }) - } } ]) }, diff --git a/packages/api-client/src/index.ts b/packages/api-client/src/index.ts index fbd40e2faf..2dd51ba8f5 100644 --- a/packages/api-client/src/index.ts +++ b/packages/api-client/src/index.ts @@ -14,5 +14,6 @@ // export * from './client' +export * from './markup/types' export * from './socket' export * from './types' diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index fc7e458ee3..8d56516f27 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -236,7 +236,7 @@ export async function createClient ( let hierarchy = new Hierarchy() let model = new ModelDb(hierarchy) - let lastTx: number + let lastTx: number = 0 function txHandler (...tx: Tx[]): void { if (tx == null || tx.length === 0) { @@ -295,6 +295,10 @@ export async function createClient ( } // We need to look for last {transactionThreshold} transactions and if it is more since lastTx one we receive, we need to perform full refresh. + if (lastTx === 0) { + await oldOnConnect?.(ClientConnectEvent.Refresh, data) + return + } const atxes = await ctx.with('find-atx', {}, () => conn.findAll( core.class.Tx, diff --git a/packages/presentation/src/components/DrawingBoard.svelte b/packages/presentation/src/components/DrawingBoard.svelte index 11395bf3b7..1a3aa40b29 100644 --- a/packages/presentation/src/components/DrawingBoard.svelte +++ b/packages/presentation/src/components/DrawingBoard.svelte @@ -16,7 +16,14 @@ import { Analytics } from '@hcengineering/analytics' import { resizeObserver } from '@hcengineering/ui' import { onDestroy } from 'svelte' - import { drawing, type DrawingCmd, type DrawingData, type DrawingTool, type DrawTextCmd } from '../drawing' + import { + drawing, + makeCommandId, + type DrawingCmd, + type DrawingData, + type DrawingTool, + type DrawTextCmd + } from '../drawing' import DrawingBoardToolbar from './DrawingBoardToolbar.svelte' export let active = false @@ -38,7 +45,8 @@ let oldReadonly: boolean let oldDrawings: DrawingData[] let modified = false - let changingCmdIndex: number | undefined + let changingCmdId: string | undefined + let cmdEditor: HTMLDivElement | undefined $: updateToolbarPosition(readonly, board, toolbar) $: updateEditableState(drawings, readonly) @@ -63,14 +71,15 @@ commands = [] } else { // Edit current content as a new drawing - commands = [...commands] + commands = commands.map((cmd) => ({ ...cmd, id: cmd.id ?? makeCommandId() })) } modified = false } } else { commands = undefined } - changingCmdIndex = undefined + changingCmdId = undefined + cmdEditor = undefined oldDrawings = drawings oldReadonly = readonly } @@ -105,33 +114,40 @@ function addCommand (cmd: DrawingCmd): void { if (commands !== undefined) { commands = [...commands, cmd] - changingCmdIndex = undefined + changingCmdId = undefined + cmdEditor = undefined modified = true } } - function showCommandProps (index: number): void { - changingCmdIndex = index - const anyCmd = commands?.[index] - if (anyCmd?.type === 'text') { - const cmd = anyCmd as DrawTextCmd - penColor = cmd.color - fontSize = cmd.fontSize + function showCommandProps (id: string): void { + changingCmdId = id + for (const cmd of commands ?? []) { + if (cmd.id === id) { + if (cmd.type === 'text') { + const textCmd = cmd as DrawTextCmd + penColor = textCmd.color + fontSize = textCmd.fontSize + } + break + } } } - function changeCommand (index: number, cmd: DrawingCmd): void { + function changeCommand (cmd: DrawingCmd): void { if (commands !== undefined) { - commands = commands.map((c, i) => (i === index ? cmd : c)) - changingCmdIndex = undefined + commands = commands.map((c) => (c.id === cmd.id ? cmd : c)) + changingCmdId = undefined + cmdEditor = undefined modified = true } } - function deleteCommand (index: number): void { + function deleteCommand (id: string): void { if (commands !== undefined) { - commands = commands.filter((_, i) => i !== index) - changingCmdIndex = undefined + commands = commands.filter((c) => c.id !== id) + changingCmdId = undefined + cmdEditor = undefined modified = true } } @@ -159,19 +175,23 @@ penWidth, eraserWidth, fontSize, - changingCmdIndex, + changingCmdId, cmdAdded: addCommand, cmdChanging: showCommandProps, cmdChanged: changeCommand, cmdUnchanged: () => { - changingCmdIndex = undefined + changingCmdId = undefined }, - cmdDeleted: deleteCommand + cmdDeleted: deleteCommand, + editorCreated: (editor) => { + cmdEditor = editor + } }} > {#if !readonly} c !== penColor) localStorage.setItem(storageKey.colors, JSON.stringify(penColors)) selectColor(penColors[0]) + focusEditor() break } case 'reset-colors': { penColors = defaultColors localStorage.removeItem(storageKey.colors) selectColor(penColors[0]) + focusEditor() break } case undefined: { @@ -115,6 +118,7 @@ penColors = [...penColors, penColor] localStorage.setItem(storageKey.colors, JSON.stringify(penColors)) } + focusEditor() } function selectColor (color: string): void { @@ -148,6 +152,15 @@ function updateFontSize (): void { localStorage.setItem(storageKey.fontSize, fontSize.toString()) + focusEditor() + } + + function focusEditor (): void { + setTimeout(() => { + if (cmdEditor !== undefined) { + cmdEditor.focus() + } + }, 100) } @@ -243,6 +256,7 @@ tool = 'pen' } selectColor(color) + focusEditor() }} >
diff --git a/packages/presentation/src/drawing.ts b/packages/presentation/src/drawing.ts index 8d0ca8eac9..59e20f1232 100644 --- a/packages/presentation/src/drawing.ts +++ b/packages/presentation/src/drawing.ts @@ -30,16 +30,18 @@ export interface DrawingProps { eraserWidth?: number fontSize?: number defaultCursor?: string - changingCmdIndex?: number + changingCmdId?: string cmdAdded?: (cmd: DrawingCmd) => void - cmdChanging?: (index: number) => void - cmdUnchanged?: (index: number) => void - cmdChanged?: (index: number, cmd: DrawingCmd) => void - cmdDeleted?: (index: number) => void + cmdChanging?: (id: string) => void + cmdUnchanged?: (id: string) => void + cmdChanged?: (cmd: DrawingCmd) => void + cmdDeleted?: (id: string) => void + editorCreated?: (editor: HTMLDivElement) => void panned?: (offset: Point) => void } export interface DrawingCmd { + id: string type: 'line' | 'text' } @@ -71,6 +73,10 @@ function avgPoint (p1: Point, p2: Point): Point { const maxTextLength = 500 +export const makeCommandId = (): string => { + return crypto.randomUUID().toString() +} + const crossSvg = ` ` @@ -294,13 +300,15 @@ export function drawing ( draw.eraserWidth = props.eraserWidth ?? draw.eraserWidth draw.fontSize = props.fontSize ?? draw.fontSize draw.offset = props.offset ?? draw.offset + updateCanvasCursor() + updateCanvasTouchAction() interface LiveTextBox { pos: Point box: HTMLDivElement editor: HTMLDivElement - cmdIndex: number + cmdId: string } let liveTextBox: LiveTextBox | undefined @@ -328,6 +336,61 @@ export function drawing ( }) resizeObserver.observe(canvas) + let touchId: number | undefined + + function findTouch (touches: TouchList, id: number | undefined = touchId): Touch | undefined { + for (let i = 0; i < touches.length; i++) { + const touch = touches[i] + if (touch.identifier === id) { + return touch + } + } + } + + function touchToNodePoint (touch: Touch, node: HTMLElement): Point { + const rect = node.getBoundingClientRect() + return { + x: touch.clientX - rect.left, + y: touch.clientY - rect.top + } + } + + function pointerToNodePoint (e: PointerEvent): Point { + return { x: e.offsetX, y: e.offsetY } + } + + canvas.ontouchstart = (e) => { + if (readonly) { + return + } + const touch = e.changedTouches[0] + touchId = touch.identifier + drawStart(touchToNodePoint(touch, canvas)) + } + + canvas.ontouchmove = (e) => { + if (readonly) { + return + } + const touch = findTouch(e.changedTouches) + if (touch !== undefined) { + drawContinue(touchToNodePoint(touch, canvas)) + } + } + + canvas.ontouchend = (e) => { + if (readonly) { + return + } + const touch = findTouch(e.changedTouches) + if (touch !== undefined) { + drawEnd(touchToNodePoint(touch, canvas)) + } + touchId = undefined + } + + canvas.ontouchcancel = canvas.ontouchend + canvas.onpointerdown = (e) => { if (readonly) { return @@ -337,16 +400,7 @@ export function drawing ( } e.preventDefault() canvas.setPointerCapture(e.pointerId) - - const x = e.offsetX - const y = e.offsetY - - draw.on = true - draw.points = [] - prevPos = { x, y } - if (draw.isDrawingTool()) { - draw.addPoint(x, y) - } + drawStart(pointerToNodePoint(e)) } canvas.onpointermove = (e) => { @@ -354,35 +408,7 @@ export function drawing ( return } e.preventDefault() - - const x = e.offsetX - const y = e.offsetY - - if (draw.isDrawingTool()) { - const w = draw.cursorWidth() - canvasCursor.style.left = `${x - w / 2}px` - canvasCursor.style.top = `${y - w / 2}px` - if (draw.on) { - if (Math.hypot(prevPos.x - x, prevPos.y - y) < draw.minLineLength) { - return - } - draw.drawLive(x, y) - prevPos = { x, y } - } - } - - if (draw.on && draw.tool === 'pan') { - requestAnimationFrame(() => { - draw.offset.x += x - prevPos.x - draw.offset.y += y - prevPos.y - replayCommands() - prevPos = { x, y } - }) - } - - if (draw.on && draw.tool === 'text') { - prevPos = { x, y } - } + drawContinue(pointerToNodePoint(e)) } canvas.onpointerup = (e) => { @@ -391,24 +417,11 @@ export function drawing ( } e.preventDefault() canvas.releasePointerCapture(e.pointerId) - if (draw.on) { - if (draw.isDrawingTool()) { - draw.drawLive(e.offsetX, e.offsetY, true) - storeLineCommand() - } else if (draw.tool === 'pan') { - props.panned?.(draw.offset) - } else if (draw.tool === 'text') { - if (liveTextBox !== undefined) { - storeTextCommand() - } else { - const cmdIndex = findTextCommand(prevPos) - props.cmdChanging?.(cmdIndex) - } - } - draw.on = false - } + drawEnd(pointerToNodePoint(e)) } + canvas.onpointercancel = canvas.onpointerup + canvas.onpointerenter = () => { if (!readonly && draw.isDrawingTool()) { canvasCursor.style.visibility = 'visible' @@ -421,26 +434,86 @@ export function drawing ( } } - function findTextCommand (mousePos: Point): number { + function drawStart (p: Point): void { + draw.on = true + draw.points = [] + prevPos = p + if (draw.isDrawingTool()) { + draw.addPoint(p.x, p.y) + } + } + + function drawContinue (p: Point): void { + if (draw.isDrawingTool()) { + const w = draw.cursorWidth() + canvasCursor.style.left = `${p.x - w / 2}px` + canvasCursor.style.top = `${p.y - w / 2}px` + if (draw.on) { + if (Math.hypot(prevPos.x - p.x, prevPos.y - p.y) < draw.minLineLength) { + return + } + draw.drawLive(p.x, p.y) + prevPos = p + } + } + + if (draw.on && draw.tool === 'pan') { + requestAnimationFrame(() => { + draw.offset.x += p.x - prevPos.x + draw.offset.y += p.y - prevPos.y + replayCommands() + prevPos = p + }) + } + + if (draw.on && draw.tool === 'text') { + prevPos = p + } + } + + function drawEnd (p: Point): void { + if (draw.on) { + if (draw.isDrawingTool()) { + draw.drawLive(p.x, p.y, true) + storeLineCommand() + } else if (draw.tool === 'pan') { + props.panned?.(draw.offset) + } else if (draw.tool === 'text') { + if (liveTextBox !== undefined) { + storeTextCommand() + closeLiveTextBox() + } else { + const cmd = findTextCommand(prevPos) + props.cmdChanging?.(cmd?.id ?? '') + } + } + draw.on = false + } + } + + function findTextCommand (mousePos: Point): DrawTextCmd | undefined { const pos = draw.mouseToCanvasPoint(mousePos) for (let i = commands.length - 1; i >= 0; i--) { const anyCmd = commands[i] if (anyCmd.type === 'text') { const cmd = anyCmd as DrawTextCmd if (draw.isPointInText(pos, cmd)) { - return i + return cmd } } } - return -1 + return undefined } - function makeLiveTextBox (cmdIndex: number): void { + function makeLiveTextBox (cmdId: string): void { let pos = prevPos let existingCmd: DrawTextCmd | undefined - if (cmdIndex >= 0 && commands[cmdIndex]?.type === 'text') { - existingCmd = commands[cmdIndex] as DrawTextCmd - pos = draw.canvasToMousePoint(existingCmd.pos) + for (const cmd of commands) { + if (cmd.id === cmdId && cmd.type === 'text') { + existingCmd = cmd as DrawTextCmd + pos = draw.canvasToMousePoint(existingCmd.pos) + break + } } const padding = 6 @@ -455,6 +528,7 @@ export function drawing ( box.style.borderRadius = 'var(--small-BorderRadius)' box.style.padding = `${padding}px` box.style.background = 'var(--theme-popup-header)' + box.style.touchAction = 'none' box.addEventListener('mousedown', (e) => { e.stopPropagation() }) @@ -513,19 +587,18 @@ export function drawing ( if (e.key === 'Escape') { e.preventDefault() if (liveTextBox !== undefined) { - const cmdIndex = liveTextBox.cmdIndex - if (cmdIndex >= 0) { - // reset changingCmdIndex in clients - setTimeout(() => { - props.cmdUnchanged?.(cmdIndex) - }, 0) - } + const cmdId = liveTextBox.cmdId + // reset changingCmdId in clients + setTimeout(() => { + props.cmdUnchanged?.(cmdId) + }, 0) } closeLiveTextBox() replayCommands() } else if (e.key === 'Enter' && e.ctrlKey) { e.preventDefault() storeTextCommand() + closeLiveTextBox() } }) box.appendChild(editor) @@ -562,44 +635,43 @@ export function drawing ( return handle } + const moveTextBox = (dx: number, dy: number): void => { + let newX = box.offsetLeft + dx + let newY = box.offsetTop + dy + // For screenshots the canvas always has the same size as the underlying image + // and we should not be able to drag the text box outside of the screenshot + if (props.autoSize !== true) { + newX = Math.max(0, newX) + newY = Math.max(0, newY) + if (newX + box.offsetWidth > node.clientWidth) { + newX = node.clientWidth - box.offsetWidth + } + if (newY + box.offsetHeight > node.clientHeight) { + newY = node.clientHeight - box.offsetHeight + } + } + box.style.left = `${newX}px` + box.style.top = `${newY}px` + if (liveTextBox !== undefined) { + liveTextBox.pos.x = newX + padding + liveTextBox.pos.y = newY + padding + } + } + const dragHandle = makeHandle() dragHandle.style.left = `-${handleSize / 2}px` dragHandle.style.cursor = 'grab' + dragHandle.style.touchAction = 'none' dragHandle.addEventListener('pointerdown', (e) => { e.preventDefault() dragHandle.style.cursor = 'grabbing' dragHandle.setPointerCapture(e.pointerId) - const x = e.clientX - const y = e.clientY - const dragStart = { x, y } + let prevPos = { x: e.clientX, y: e.clientY } const pointerMove = (e: PointerEvent): void => { e.preventDefault() - const x = e.clientX - const y = e.clientY - const dx = x - dragStart.x - const dy = y - dragStart.y - dragStart.x = x - dragStart.y = y - let newX = box.offsetLeft + dx - let newY = box.offsetTop + dy - // For screenshots the canvas always has the same size as the underlying image - // and we should not be able to drag the text box outside of the screenshot - if (props.autoSize !== true) { - newX = Math.max(0, newX) - newY = Math.max(0, newY) - if (newX + box.offsetWidth > node.clientWidth) { - newX = node.clientWidth - box.offsetWidth - } - if (newY + box.offsetHeight > node.clientHeight) { - newY = node.clientHeight - box.offsetHeight - } - } - box.style.left = `${newX}px` - box.style.top = `${newY}px` - if (liveTextBox !== undefined) { - liveTextBox.pos.x = newX + padding - liveTextBox.pos.y = newY + padding - } + const p = { x: e.clientX, y: e.clientY } + moveTextBox(p.x - prevPos.x, p.y - prevPos.y) + prevPos = p } const pointerUp = (e: PointerEvent): void => { setTimeout(() => { @@ -610,9 +682,37 @@ export function drawing ( dragHandle.releasePointerCapture(e.pointerId) dragHandle.removeEventListener('pointermove', pointerMove) dragHandle.removeEventListener('pointerup', pointerUp) + dragHandle.removeEventListener('pointercancel', pointerUp) } dragHandle.addEventListener('pointermove', pointerMove) dragHandle.addEventListener('pointerup', pointerUp) + dragHandle.addEventListener('pointercancel', pointerUp) + }) + dragHandle.addEventListener('touchstart', (e) => { + dragHandle.style.cursor = 'grabbing' + const touch = e.changedTouches[0] + const touchId = touch.identifier + let prevPos = touchToNodePoint(touch, dragHandle) + const touchMove = (e: TouchEvent): void => { + const touch = findTouch(e.changedTouches, touchId) + if (touch !== undefined) { + const p = touchToNodePoint(touch, dragHandle) + moveTextBox(p.x - prevPos.x, p.y - prevPos.y) + prevPos = p + } + } + const touchEnd = (e: TouchEvent): void => { + setTimeout(() => { + editor.focus() + }, 100) + dragHandle.style.cursor = 'grab' + dragHandle.removeEventListener('touchmove', touchMove) + dragHandle.removeEventListener('touchend', touchEnd) + dragHandle.removeEventListener('touchcancel', touchEnd) + } + dragHandle.addEventListener('touchmove', touchMove) + dragHandle.addEventListener('touchend', touchEnd) + dragHandle.addEventListener('touchcancel', touchEnd) }) box.appendChild(dragHandle) @@ -622,20 +722,21 @@ export function drawing ( deleteButton.innerHTML = crossSvg deleteButton.addEventListener('click', () => { node.removeChild(box) - if (liveTextBox?.cmdIndex !== undefined) { - props.cmdDeleted?.(liveTextBox.cmdIndex) + if (liveTextBox?.cmdId !== undefined) { + props.cmdDeleted?.(liveTextBox.cmdId) } liveTextBox = undefined }) box.appendChild(deleteButton) node.appendChild(box) - liveTextBox = { box, editor, pos, cmdIndex } + liveTextBox = { box, editor, pos, cmdId } updateLiveTextBox() setTimeout(() => { editor.focus() }, 100) selectAll() + props.editorCreated?.(editor) } function updateLiveTextBox (): void { @@ -658,7 +759,9 @@ export function drawing ( if (liveTextBox !== undefined) { const text = (liveTextBox.editor.innerText ?? '').trim() if (text !== '') { + const cmdId = liveTextBox.cmdId const cmd: DrawTextCmd = { + id: cmdId === '' ? makeCommandId() : cmdId, type: 'text', text, pos: draw.mouseToCanvasPoint(liveTextBox.pos), @@ -666,10 +769,9 @@ export function drawing ( fontFace: draw.fontFace, color: draw.penColor } - const cmdIndex = liveTextBox.cmdIndex const notify = (): void => { - if (cmdIndex >= 0) { - props.cmdChanged?.(cmdIndex, cmd) + if (cmdId !== '') { + props.cmdChanged?.(cmd) } else { props.cmdAdded?.(cmd) } @@ -680,7 +782,7 @@ export function drawing ( notify() } } else { - props.cmdUnchanged?.(liveTextBox.cmdIndex) + props.cmdUnchanged?.(liveTextBox.cmdId) } } } @@ -689,6 +791,7 @@ export function drawing ( if (draw.points.length > 0) { const erasing = draw.tool === 'erase' const cmd: DrawLineCmd = { + id: makeCommandId(), type: 'line', lineWidth: erasing ? draw.eraserWidth : draw.penWidth, erasing, @@ -726,13 +829,17 @@ export function drawing ( } } + function updateCanvasTouchAction (): void { + canvas.style.touchAction = readonly ? 'unset' : 'none' + } + function replayCommands (): void { draw.ctx.reset() - for (let i = 0; i < commands.length; i++) { - if (liveTextBox?.cmdIndex === i) { + for (const cmd of commands) { + if (cmd.id !== undefined && liveTextBox?.cmdId === cmd.id) { continue } - draw.drawCommand(commands[i]) + draw.drawCommand(cmd) } } @@ -774,9 +881,10 @@ export function drawing ( } if (props.readonly !== readonly) { readonly = props.readonly ?? false + updateCanvasTouchAction() updateCursor = true } - if (props.changingCmdIndex === undefined) { + if (props.changingCmdId === undefined) { if (liveTextBox !== undefined) { storeTextCommand(true) closeLiveTextBox() @@ -784,9 +892,9 @@ export function drawing ( } } else { if (liveTextBox === undefined) { - makeLiveTextBox(props.changingCmdIndex) + makeLiveTextBox(props.changingCmdId) replay = true - } else if (liveTextBox.cmdIndex !== props.changingCmdIndex) { + } else if (liveTextBox.cmdId !== props.changingCmdId) { storeTextCommand(true) closeLiveTextBox() replay = true diff --git a/packages/query/src/index.ts b/packages/query/src/index.ts index 14ab8d8f67..d24b6ea13c 100644 --- a/packages/query/src/index.ts +++ b/packages/query/src/index.ts @@ -364,7 +364,8 @@ export class LiveQuery implements WithTx, Client { total: 0, options: options as FindOptions, callbacks: new Map(), - refresh: reduceCalls(() => this.doRefresh(q)) + refresh: reduceCalls(() => this.doRefresh(q)), + refreshId: 0 } if (callback !== undefined) { q.callbacks.set(callback.callbackId, callback.callback as unknown as Callback) @@ -787,8 +788,9 @@ export class LiveQuery implements WithTx, Client { } private async doRefresh (q: Query): Promise { + const qid = ++q.refreshId const res = await this.client.findAll(q._class, q.query, q.options) - if (!deepEqual(res, q.result) || (res.total !== q.total && q.options?.total === true)) { + if (q.refreshId === qid && (!deepEqual(res, q.result) || (res.total !== q.total && q.options?.total === true))) { q.result = new ResultArray(res, this.getHierarchy()) q.total = res.total await this.callback(q) diff --git a/packages/query/src/types.ts b/packages/query/src/types.ts index 3628782acd..5dec89ed02 100644 --- a/packages/query/src/types.ts +++ b/packages/query/src/types.ts @@ -12,6 +12,6 @@ export interface Query { options?: FindOptions total: number callbacks: Map - refresh: () => Promise + refreshId: number } diff --git a/plugins/recruit-resources/src/components/CreateVacancy.svelte b/plugins/recruit-resources/src/components/CreateVacancy.svelte index 40a08eade5..a3c546c8c3 100644 --- a/plugins/recruit-resources/src/components/CreateVacancy.svelte +++ b/plugins/recruit-resources/src/components/CreateVacancy.svelte @@ -103,7 +103,7 @@ type: typeId as Ref } export function canClose (): boolean { - return name === '' && typeId !== undefined + return name.trim() === '' && typeId !== undefined } const client = getClient() @@ -185,7 +185,7 @@ const resId: Ref = generateId() const identifier = `${project?.identifier}-${number}` const data: AttachedData = { - title: template.title + ` (${name})`, + title: template.title + ` (${name.trim()})`, description: null, assignee: template.assignee, component: template.component, @@ -240,7 +240,7 @@ const incResult = await client.update(sequence, { $inc: { sequence: 1 } }, true) const data: Data = { ...vacancyData, - name, + name: name.trim(), description: template?.shortDescription ?? '', fullDescription: null, private: false, @@ -336,7 +336,7 @@ { dispatch('close') diff --git a/plugins/recruit-resources/src/components/EditVacancy.svelte b/plugins/recruit-resources/src/components/EditVacancy.svelte index 8ccc9d8907..b43f63d082 100644 --- a/plugins/recruit-resources/src/components/EditVacancy.svelte +++ b/plugins/recruit-resources/src/components/EditVacancy.svelte @@ -101,9 +101,13 @@ const updates: Partial> = {} const trimmedName = rawName.trim() + const trimmedNameOld = object.name?.trim() - if (trimmedName.length > 0 && trimmedName !== object.name?.trim()) { + if (trimmedName.length > 0 && (trimmedName !== trimmedNameOld || trimmedNameOld !== object.name)) { updates.name = trimmedName + rawName = trimmedName + } else { + rawName = object.name } if (rawDesc !== object.description) { diff --git a/plugins/text-editor-resources/src/components/DrawingBoardEditor.svelte b/plugins/text-editor-resources/src/components/DrawingBoardEditor.svelte index 99ab7799e4..53dd84f654 100644 --- a/plugins/text-editor-resources/src/components/DrawingBoardEditor.svelte +++ b/plugins/text-editor-resources/src/components/DrawingBoardEditor.svelte @@ -13,7 +13,14 @@ // limitations under the License. -->