refactor(core): doc property (#8465)

doc property upgraded to use orm.

The visibility of the property are simplified to three types: `always show`, `always hide`, `hide when empty`, and the default is `always show`.

![CleanShot 2024-10-14 at 15 34 52](https://github.com/user-attachments/assets/748b8b80-061f-4d6a-8579-52e59df717c2)

Added a sidebar view to manage properties
![CleanShot 2024-10-14 at 15 35 58](https://github.com/user-attachments/assets/bffa9b1a-a1a5-4708-b2e8-4963120f3af9)

new property ui in workspace settings
![CleanShot 2024-10-14 at 15 36 44](https://github.com/user-attachments/assets/572d8dcc-9b3d-462a-9bcc-5f5fa8e622da)

Property lists can be collapsed
![CleanShot 2024-10-14 at 15 37 59](https://github.com/user-attachments/assets/2b20be1a-8141-478a-8fe7-405aff6d04fd)
This commit is contained in:
EYHN
2024-10-15 10:17:11 +00:00
parent 13b24eb823
commit 24e0c5797c
88 changed files with 3151 additions and 3617 deletions
@@ -1,41 +0,0 @@
import { expect, test } from 'vitest';
import { generateFractionalIndexingKeyBetween } from '../fractional-indexing';
function gen(a: string | null, b: string | null) {
const result = generateFractionalIndexingKeyBetween(a, b);
expect(
a === null || b === null || (a < result && result < b),
`${a} ${b} ${result}`
).toBe(true);
return result;
}
test('fractional-indexing', () => {
for (let i = 0; i < 100; i++) {
const set = new Set<string>();
let a = null;
let b = null;
for (let i = 0; i < 100; i++) {
const s1 = gen(a, b);
expect(a === null || b === null || (a < s1 && s1 < b)).toBe(true);
const s2 = gen(a, b);
expect(a === null || b === null || (a < s2 && s2 < b)).toBe(true);
if (set.has(s1) || set.has(s2) || s1 === s2) {
throw new Error('Duplicate key, ' + set.size + ', ' + s1 + ', ' + s2);
break;
}
set.add(s1);
set.add(s2);
if (s1 < s2) {
a = s1;
b = s2;
} else {
a = s2;
b = s1;
}
}
}
});
@@ -1,213 +0,0 @@
import { generateKeyBetween } from 'fractional-indexing';
export interface SortableProvider<T, K extends string | number> {
getItems(): T[];
getItemId(item: T): K;
getItemOrder(item: T): string;
setItemOrder(item: T, order: string): void;
}
// Using fractional-indexing managing orders of items in a list
/**
* @deprecated
*/
export function createFractionalIndexingSortableHelper<
T,
K extends string | number,
>(provider: SortableProvider<T, K>) {
function getOrderedItems() {
return provider.getItems().sort((a, b) => {
const oa = provider.getItemOrder(a);
const ob = provider.getItemOrder(b);
return oa > ob ? 1 : oa < ob ? -1 : 0;
});
}
function getLargestOrder() {
const lastItem = getOrderedItems().at(-1);
return lastItem ? provider.getItemOrder(lastItem) : null;
}
function getSmallestOrder() {
const firstItem = getOrderedItems().at(0);
return firstItem ? provider.getItemOrder(firstItem) : null;
}
/**
* Get a new order at the end of the list
*/
function getNewItemOrder() {
return generateKeyBetween(getLargestOrder(), null);
}
/**
* Move item from one position to another
*
* in the most common sorting case, moving over will visually place the dragging item to the target position
* the original item in the target position will either move up or down, depending on the direction of the drag
*
* @param fromId
* @param toId
*/
function move(fromId: K, toId: K) {
const items = getOrderedItems();
const from = items.findIndex(i => provider.getItemId(i) === fromId);
const to = items.findIndex(i => provider.getItemId(i) === toId);
const fromItem = items[from];
const toItem = items[to];
const toNextItem = items[from < to ? to + 1 : to - 1];
const toOrder = toItem ? provider.getItemOrder(toItem) : null;
const toNextOrder = toNextItem ? provider.getItemOrder(toNextItem) : null;
const args: [string | null, string | null] =
from < to ? [toOrder, toNextOrder] : [toNextOrder, toOrder];
provider.setItemOrder(fromItem, generateKeyBetween(...args));
}
function moveTo(fromId: K, toId: K, position: 'before' | 'after') {
const items = getOrderedItems();
const from = items.findIndex(i => provider.getItemId(i) === fromId);
const to = items.findIndex(i => provider.getItemId(i) === toId);
const fromItem = items[from] as T | undefined;
if (fromItem === undefined) return;
const toItem = items[to] as T | undefined;
const toItemPrev = items[to - 1] as T | undefined;
const toItemNext = items[to + 1] as T | undefined;
const toItemOrder = toItem ? provider.getItemOrder(toItem) : null;
const toItemPrevOrder = toItemPrev
? provider.getItemOrder(toItemPrev)
: null;
const toItemNextOrder = toItemNext
? provider.getItemOrder(toItemNext)
: null;
if (position === 'before') {
provider.setItemOrder(
fromItem,
generateKeyBetween(toItemPrevOrder, toItemOrder)
);
} else {
provider.setItemOrder(
fromItem,
generateKeyBetween(toItemOrder, toItemNextOrder)
);
}
}
/**
* Cases example:
* Imagine we have the following items, | a | b | c |
* 1. insertBefore('b', undefined). before is not provided, which means insert b after c
* | a | c |
* ▴
* b
* result: | a | c | b |
*
* 2. insertBefore('b', 'a'). insert b before a
* | a | c |
* ▴
* b
*
* result: | b | a | c |
*/
function insertBefore(
id: string | number,
beforeId: string | number | undefined
) {
const items = getOrderedItems();
// assert id is in the list
const item = items.find(i => provider.getItemId(i) === id);
if (!item) return;
const beforeItemIndex = items.findIndex(
i => provider.getItemId(i) === beforeId
);
const beforeItem = beforeItemIndex !== -1 ? items[beforeItemIndex] : null;
const beforeItemPrev = beforeItem ? items[beforeItemIndex - 1] : null;
const beforeOrder = beforeItem ? provider.getItemOrder(beforeItem) : null;
const beforePrevOrder = beforeItemPrev
? provider.getItemOrder(beforeItemPrev)
: null;
provider.setItemOrder(
item,
generateKeyBetween(beforePrevOrder, beforeOrder)
);
}
return {
getOrderedItems,
getLargestOrder,
getSmallestOrder,
getNewItemOrder,
move,
moveTo,
insertBefore,
};
}
/**
* generate a key between a and b, the result key is always satisfied with a < result < b.
* the key always has a random suffix, so there is no need to worry about collision.
*
* make sure a and b are generated by this function.
*/
export function generateFractionalIndexingKeyBetween(
a: string | null,
b: string | null
) {
const randomSize = 32;
function randomString(length: number = randomSize) {
const values = new Uint8Array(length);
crypto.getRandomValues(values);
const chars =
'123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(values[i] % chars.length);
}
return result;
}
if (a !== null && b !== null && a >= b) {
throw new Error('a should be smaller than b');
}
// get the subkey in full key
// e.g.
// a0xxxx -> a
// a0x0xxxx -> a0x
function subkey(key: string | null) {
if (key === null) {
return null;
}
if (key.length <= randomSize + 1) {
// no subkey
return key;
}
const splitAt = key.substring(0, key.length - randomSize - 1);
return splitAt;
}
const aSubkey = subkey(a);
const bSubkey = subkey(b);
if (aSubkey === null && bSubkey === null) {
// generate a new key
return generateKeyBetween(null, null) + '0' + randomString();
} else if (aSubkey === null && bSubkey !== null) {
// generate a key before b
return generateKeyBetween(null, bSubkey) + '0' + randomString();
} else if (bSubkey === null && aSubkey !== null) {
// generate a key after a
return generateKeyBetween(aSubkey, null) + '0' + randomString();
} else if (aSubkey !== null && bSubkey !== null) {
// generate a key between a and b
if (aSubkey === bSubkey && a !== null && b !== null) {
// conflict, if the subkeys are the same, generate a key between fullkeys
return generateKeyBetween(a, b) + '0' + randomString();
} else {
return generateKeyBetween(aSubkey, bSubkey) + '0' + randomString();
}
}
throw new Error('Never reach here');
}
@@ -1,7 +1,6 @@
export * from './create-emotion-cache';
export * from './event';
export * from './extract-emoji-icon';
export * from './fractional-indexing';
export * from './popup';
export * from './string2color';
export * from './toast';
@@ -0,0 +1,17 @@
export const generateUniqueNameInSequence = (
baseName: string,
existingNames: string[]
): string => {
if (!existingNames.includes(baseName)) {
return baseName;
}
const numericSuffix = baseName.match(/(\d+)$/);
if (numericSuffix) {
const currentNumber = parseInt(numericSuffix[1], 10);
const newName = baseName.replace(/\d+$/, (currentNumber + 1).toString());
return generateUniqueNameInSequence(newName, existingNames);
}
return generateUniqueNameInSequence(`${baseName} 2`, existingNames);
};