mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-02 18:20:39 +08:00
728e02cab7
#### PR Dependency Tree * **PR #14452** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved null-safety, dependency tracking, upload validation, and error logging for more reliable uploads, clipboard, calendar linking, telemetry, PDF/theme printing, and preview/zoom behavior. * Tightened handling of all-day calendar events (missing date now reported). * **Deprecations** * Removed deprecated RadioButton and RadioButtonGroup; use RadioGroup. * **Chores** * Unified and upgraded linting/config, reorganized imports, and standardized binary handling for more consistent builds and tooling. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
228 lines
6.4 KiB
TypeScript
228 lines
6.4 KiB
TypeScript
import type { Doc } from 'yjs';
|
|
import { Map as YMap } from 'yjs';
|
|
|
|
import { diffKeyedRecords, type KeyedRecord } from './diff';
|
|
import { formatValue, toPlain } from './plain';
|
|
|
|
const DELETE_FLAG_KEY = '$$DELETED';
|
|
|
|
type TableExtract = {
|
|
recordsByKey: Map<string, KeyedRecord>;
|
|
duplicateKeys: string[];
|
|
};
|
|
|
|
function getRecordKey(record: YMap<any>, keyField: string): string | null {
|
|
const keyRaw = record.get(keyField);
|
|
if (typeof keyRaw === 'string') {
|
|
return keyRaw;
|
|
}
|
|
if (typeof keyRaw?.toString === 'function') {
|
|
return keyRaw.toString();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function isDeletedRecord(record: YMap<any>): boolean {
|
|
return record.get(DELETE_FLAG_KEY) === true || record.size === 0;
|
|
}
|
|
|
|
export function extractYjsTable(doc: Doc, keyField: string): TableExtract {
|
|
const recordsByKey = new Map<string, KeyedRecord>();
|
|
const duplicateKeys: string[] = [];
|
|
|
|
for (const sharedKey of doc.share.keys()) {
|
|
let record: unknown;
|
|
try {
|
|
record = doc.getMap(sharedKey);
|
|
} catch {
|
|
// Not a YMap shared type; ignore.
|
|
continue;
|
|
}
|
|
if (!(record instanceof YMap)) {
|
|
continue;
|
|
}
|
|
|
|
const key = getRecordKey(record, keyField);
|
|
if (!key) {
|
|
continue;
|
|
}
|
|
|
|
if (isDeletedRecord(record)) {
|
|
continue;
|
|
}
|
|
|
|
if (recordsByKey.has(key)) {
|
|
duplicateKeys.push(key);
|
|
}
|
|
|
|
const recordPlain = toPlain(record) as unknown;
|
|
if (
|
|
recordPlain &&
|
|
typeof recordPlain === 'object' &&
|
|
!Array.isArray(recordPlain)
|
|
) {
|
|
recordsByKey.set(key, recordPlain as KeyedRecord);
|
|
}
|
|
}
|
|
|
|
return { recordsByKey, duplicateKeys };
|
|
}
|
|
|
|
function folderLabel(id: string, record: KeyedRecord) {
|
|
const type = record.type;
|
|
const data = record.data;
|
|
if (type === 'folder') {
|
|
const name = typeof data === 'string' && data.trim() ? `"${data}"` : '';
|
|
return `folder:${id}${name ? ` ${name}` : ''}`;
|
|
}
|
|
if (typeof type === 'string' && typeof data === 'string') {
|
|
return `${type}:${data} (id=${id})`;
|
|
}
|
|
return `id=${id}`;
|
|
}
|
|
|
|
function favoriteLabel(key: string) {
|
|
return key;
|
|
}
|
|
|
|
export function printFolderPairDiff(opts: {
|
|
fromLabel: string;
|
|
toLabel: string;
|
|
fromTable: TableExtract;
|
|
toTable: TableExtract;
|
|
}) {
|
|
const diff = diffKeyedRecords(
|
|
opts.fromTable.recordsByKey,
|
|
opts.toTable.recordsByKey
|
|
);
|
|
|
|
console.log(`\n=== ${opts.fromLabel} -> ${opts.toLabel} ===`);
|
|
console.log(
|
|
`Rows: ${opts.fromTable.recordsByKey.size} -> ${opts.toTable.recordsByKey.size} (+${diff.added.length} / -${diff.removed.length} / ~${diff.changed.length})`
|
|
);
|
|
|
|
if (opts.fromTable.duplicateKeys.length) {
|
|
console.log(
|
|
`! Warning: duplicate keys in FROM: ${Array.from(new Set(opts.fromTable.duplicateKeys)).sort().join(', ')}`
|
|
);
|
|
}
|
|
if (opts.toTable.duplicateKeys.length) {
|
|
console.log(
|
|
`! Warning: duplicate keys in TO: ${Array.from(new Set(opts.toTable.duplicateKeys)).sort().join(', ')}`
|
|
);
|
|
}
|
|
|
|
if (diff.added.length) {
|
|
console.log(`\n+ Added (${diff.added.length})`);
|
|
for (const { id, record } of diff.added) {
|
|
const parentId = record.parentId;
|
|
const index = record.index;
|
|
console.log(
|
|
` + ${folderLabel(id, record)} (parentId=${formatValue('parentId', parentId ?? null)}, index=${formatValue('index', index ?? null)})`
|
|
);
|
|
}
|
|
}
|
|
|
|
if (diff.removed.length) {
|
|
console.log(`\n- Removed (${diff.removed.length})`);
|
|
for (const { id, record } of diff.removed) {
|
|
const parentId = record.parentId;
|
|
const index = record.index;
|
|
console.log(
|
|
` - ${folderLabel(id, record)} (parentId=${formatValue('parentId', parentId ?? null)}, index=${formatValue('index', index ?? null)})`
|
|
);
|
|
}
|
|
}
|
|
|
|
if (diff.changed.length) {
|
|
console.log(`\n~ Changed (${diff.changed.length})`);
|
|
for (const change of diff.changed) {
|
|
const fromName =
|
|
change.fromRecord.type === 'folder' ? change.fromRecord.data : null;
|
|
const toName =
|
|
change.toRecord.type === 'folder' ? change.toRecord.data : null;
|
|
let header = ` ~ ${folderLabel(change.id, change.toRecord)}`;
|
|
if (
|
|
typeof fromName === 'string' &&
|
|
typeof toName === 'string' &&
|
|
fromName !== toName
|
|
) {
|
|
header += ` ("${fromName}" -> "${toName}")`;
|
|
}
|
|
console.log(header);
|
|
|
|
for (const field of change.fields) {
|
|
console.log(
|
|
` - ${field.key}: ${formatValue(field.key, field.from)} -> ${formatValue(field.key, field.to)}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!diff.added.length && !diff.removed.length && !diff.changed.length) {
|
|
console.log('\n(no changes)');
|
|
}
|
|
}
|
|
|
|
export function printFavoritePairDiff(opts: {
|
|
fromLabel: string;
|
|
toLabel: string;
|
|
fromTable: TableExtract;
|
|
toTable: TableExtract;
|
|
}) {
|
|
const diff = diffKeyedRecords(
|
|
opts.fromTable.recordsByKey,
|
|
opts.toTable.recordsByKey
|
|
);
|
|
|
|
console.log(`\n=== ${opts.fromLabel} -> ${opts.toLabel} ===`);
|
|
console.log(
|
|
`Rows: ${opts.fromTable.recordsByKey.size} -> ${opts.toTable.recordsByKey.size} (+${diff.added.length} / -${diff.removed.length} / ~${diff.changed.length})`
|
|
);
|
|
|
|
if (opts.fromTable.duplicateKeys.length) {
|
|
console.log(
|
|
`! Warning: duplicate keys in FROM: ${Array.from(new Set(opts.fromTable.duplicateKeys)).sort().join(', ')}`
|
|
);
|
|
}
|
|
if (opts.toTable.duplicateKeys.length) {
|
|
console.log(
|
|
`! Warning: duplicate keys in TO: ${Array.from(new Set(opts.toTable.duplicateKeys)).sort().join(', ')}`
|
|
);
|
|
}
|
|
|
|
if (diff.added.length) {
|
|
console.log(`\n+ Added (${diff.added.length})`);
|
|
for (const { id, record } of diff.added) {
|
|
console.log(
|
|
` + ${favoriteLabel(id)} (index=${formatValue('index', record.index ?? null)})`
|
|
);
|
|
}
|
|
}
|
|
|
|
if (diff.removed.length) {
|
|
console.log(`\n- Removed (${diff.removed.length})`);
|
|
for (const { id, record } of diff.removed) {
|
|
console.log(
|
|
` - ${favoriteLabel(id)} (index=${formatValue('index', record.index ?? null)})`
|
|
);
|
|
}
|
|
}
|
|
|
|
if (diff.changed.length) {
|
|
console.log(`\n~ Changed (${diff.changed.length})`);
|
|
for (const change of diff.changed) {
|
|
console.log(` ~ ${favoriteLabel(change.id)}`);
|
|
for (const field of change.fields) {
|
|
console.log(
|
|
` - ${field.key}: ${formatValue(field.key, field.from)} -> ${formatValue(field.key, field.to)}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!diff.added.length && !diff.removed.length && !diff.changed.length) {
|
|
console.log('\n(no changes)');
|
|
}
|
|
}
|