fix
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
const DB_NAME = 'papercrate_desk';
|
||||
const DB_VERSION = 1;
|
||||
const LAYOUT_STORE = 'layouts';
|
||||
|
||||
const currentDbPromise = { value: null };
|
||||
|
||||
const openDatabase = () => {
|
||||
if (currentDbPromise.value) {
|
||||
return currentDbPromise.value;
|
||||
}
|
||||
|
||||
currentDbPromise.value = new Promise((resolve, reject) => {
|
||||
if (typeof indexedDB === 'undefined') {
|
||||
reject(new Error('IndexedDB not available'));
|
||||
return;
|
||||
}
|
||||
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result;
|
||||
if (!db.objectStoreNames.contains(LAYOUT_STORE)) {
|
||||
const store = db.createObjectStore(LAYOUT_STORE, {
|
||||
keyPath: ['tenantId', 'viewId', 'documentId'],
|
||||
});
|
||||
store.createIndex('tenantViewIdx', ['tenantId', 'viewId'], { unique: false });
|
||||
store.createIndex('tenantIdx', 'tenantId', { unique: false });
|
||||
store.createIndex('updatedIdx', 'updatedAt', { unique: false });
|
||||
}
|
||||
};
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve(request.result);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(request.error || new Error('Failed to open IndexedDB'));
|
||||
};
|
||||
});
|
||||
|
||||
return currentDbPromise.value;
|
||||
};
|
||||
|
||||
export const fetchLayoutRecords = async ({ tenantId, viewId }) => {
|
||||
if (!tenantId || !viewId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const db = await openDatabase();
|
||||
const transaction = db.transaction(LAYOUT_STORE, 'readonly');
|
||||
const store = transaction.objectStore(LAYOUT_STORE);
|
||||
const index = store.index('tenantViewIdx');
|
||||
const request = index.getAll([tenantId, viewId]);
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
request.onsuccess = () => resolve(request.result || []);
|
||||
request.onerror = () => reject(request.error || new Error('Failed to fetch layout records'));
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('[desk] Failed to read layout records', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const upsertLayoutRecords = async ({ tenantId, viewId, entries }) => {
|
||||
if (!tenantId || !viewId || !Array.isArray(entries) || !entries.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const db = await openDatabase();
|
||||
const transaction = db.transaction(LAYOUT_STORE, 'readwrite');
|
||||
const store = transaction.objectStore(LAYOUT_STORE);
|
||||
const timestamp = Date.now();
|
||||
|
||||
entries.forEach((entry) => {
|
||||
if (!entry || !entry.documentId) {
|
||||
return;
|
||||
}
|
||||
store.put({
|
||||
tenantId,
|
||||
viewId,
|
||||
documentId: entry.documentId,
|
||||
centerX: Number(entry.centerX) || 0,
|
||||
centerY: Number(entry.centerY) || 0,
|
||||
rotation: Number(entry.rotation) || 0,
|
||||
zIndex: Number(entry.zIndex) || 0,
|
||||
updatedAt: entry.updatedAt || timestamp,
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
transaction.oncomplete = resolve;
|
||||
transaction.onerror = () => reject(transaction.error || new Error('Failed to persist layout records'));
|
||||
transaction.onabort = () => reject(transaction.error || new Error('Layout transaction aborted'));
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('[desk] Failed to upsert layout records', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteTenantLayouts = async (tenantId) => {
|
||||
if (!tenantId) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const db = await openDatabase();
|
||||
const transaction = db.transaction(LAYOUT_STORE, 'readwrite');
|
||||
const store = transaction.objectStore(LAYOUT_STORE);
|
||||
const index = store.index('tenantIdx');
|
||||
const request = index.openCursor(tenantId);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
request.onsuccess = (event) => {
|
||||
const cursor = event.target.result;
|
||||
if (cursor) {
|
||||
cursor.delete();
|
||||
cursor.continue();
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
request.onerror = () => reject(request.error || new Error('Failed to delete tenant layouts'));
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('[desk] Failed to clean tenant layouts', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const closeDeskDatabase = () => {
|
||||
if (!currentDbPromise.value) {
|
||||
return;
|
||||
}
|
||||
currentDbPromise.value = currentDbPromise.value.then((db) => {
|
||||
try {
|
||||
db.close();
|
||||
} catch (error) {
|
||||
console.warn('[desk] Failed to close IndexedDB', error);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
const TAG_MIME_TYPES = ['application/x-papercrate-tag', 'text/papercrate-tag'];
|
||||
const TAG_TEXT_MIME_TYPE = 'text/plain';
|
||||
|
||||
const serializePayload = (payload) => {
|
||||
try {
|
||||
return JSON.stringify(payload);
|
||||
} catch (error) {
|
||||
console.warn('[tagTransfer] Failed to serialize payload', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const createTagTransferPayload = (tag, sourceDocId = null) => {
|
||||
if (!tag || !tag.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: tag.id,
|
||||
label: tag.label || '',
|
||||
sourceDocId: sourceDocId ?? null,
|
||||
};
|
||||
};
|
||||
|
||||
export const writeTagTransferData = (dataTransfer, tag, sourceDocId = null) => {
|
||||
if (!dataTransfer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = createTagTransferPayload(tag, sourceDocId);
|
||||
if (!payload) {
|
||||
return;
|
||||
}
|
||||
|
||||
const serialized = serializePayload(payload);
|
||||
if (!serialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
dataTransfer.setData(TAG_MIME_TYPES[0], serialized);
|
||||
dataTransfer.setData(TAG_MIME_TYPES[1], serialized);
|
||||
if (payload.label) {
|
||||
dataTransfer.setData(TAG_TEXT_MIME_TYPE, payload.label);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[tagTransfer] Failed to write drag data', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const readTagTransferData = (dataTransfer) => {
|
||||
if (!dataTransfer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (let index = 0; index < TAG_MIME_TYPES.length; index += 1) {
|
||||
const type = TAG_MIME_TYPES[index];
|
||||
try {
|
||||
const raw = dataTransfer.getData(type);
|
||||
if (raw) {
|
||||
return raw;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[tagTransfer] Failed to read drag data for type', type, error);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const parseTagTransferPayload = (input) => {
|
||||
const dataTransfer = input && 'dataTransfer' in input ? input.dataTransfer : input;
|
||||
const raw = readTagTransferData(dataTransfer);
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch (error) {
|
||||
console.warn('[tagTransfer] Failed to parse drag payload', error);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const isTagTransferEvent = (event) => {
|
||||
const types = event?.dataTransfer?.types;
|
||||
if (!types) {
|
||||
return false;
|
||||
}
|
||||
const typeList = Array.isArray(types) ? types : Array.from(types);
|
||||
return TAG_MIME_TYPES.some((type) => typeList.includes(type));
|
||||
};
|
||||
|
||||
export { TAG_MIME_TYPES };
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
const defaultFilter = (event) => {
|
||||
if (!event) {
|
||||
return false;
|
||||
}
|
||||
const { type, button, pointerType, isPrimary } = event;
|
||||
const isPointerUp = type === 'pointerup';
|
||||
const buttonValid =
|
||||
button == null || button === 0 || (isPointerUp && (button === -1 || button === 0));
|
||||
if (!buttonValid) {
|
||||
return false;
|
||||
}
|
||||
if (pointerType === 'touch' && isPrimary === false) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const usePointerTap = ({
|
||||
onSingle,
|
||||
onDouble,
|
||||
delay = 240,
|
||||
filter = defaultFilter,
|
||||
} = {}) => {
|
||||
const timerRef = useRef(null);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return useCallback(
|
||||
(event, metadata = undefined) => {
|
||||
if (!filter(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof event.persist === 'function') {
|
||||
event.persist();
|
||||
}
|
||||
|
||||
const context = {
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
pointerType: event.pointerType,
|
||||
event,
|
||||
data: metadata,
|
||||
};
|
||||
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = null;
|
||||
if (typeof onDouble === 'function') {
|
||||
onDouble(context);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
timerRef.current = setTimeout(() => {
|
||||
timerRef.current = null;
|
||||
if (typeof onSingle === 'function') {
|
||||
onSingle(context);
|
||||
}
|
||||
}, delay);
|
||||
},
|
||||
[delay, filter, onDouble, onSingle],
|
||||
);
|
||||
};
|
||||
|
||||
export default usePointerTap;
|
||||
Reference in New Issue
Block a user