init: the first public commit for AFFiNE

This commit is contained in:
DarkSky
2022-07-22 15:49:21 +08:00
commit e3e3741393
1451 changed files with 108124 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
export const AFFINE_MAIN_SERVER = '/api/v1';
// export const AFFINE_MAIN_SERVER = 'http://localhost:3001/api/v1';
export const AUTHING_APP_ID_CN = '61961a7b8b90af37e7c422e0';
export const AUTHING_APP_ID_US = '626941ad32dea590043903d8';
export const AUTHING_APP_HOST_US = 'https://affine.us.authing.co';
export const USER_AUTH_LOCAL_KEY = '_authing_token';
export const LOGOUT_LOCAL_STORAGE = [USER_AUTH_LOCAL_KEY, '_authing_user'];
export const LOGOUT_COOKIES = ['authing_session'];

View File

@@ -0,0 +1,3 @@
import copy from 'copy-to-clipboard';
export const copyToClipboard = copy;

14
libs/utils/src/date.ts Normal file
View File

@@ -0,0 +1,14 @@
/**
* Get the current time in ISO format, the return value example is 2021-11-17T15:45:56, the return value can be directly used to create a Date object
*/
export function getDateIsoStringWithTimezone(dateMilliseconds?: number) {
if (dateMilliseconds === undefined || dateMilliseconds === null) {
dateMilliseconds = Date.now();
}
//offset in milliseconds
const timezoneOffset = new Date().getTimezoneOffset() * 60000;
const date_iso_string = new Date(
dateMilliseconds - timezoneOffset
).toISOString();
return date_iso_string.slice(0, -5);
}

124
libs/utils/src/di/di.ts Normal file
View File

@@ -0,0 +1,124 @@
type Token = any;
interface DependencyConfig {
token: Token;
lazy?: boolean;
}
export interface Value<T> {
value: T;
}
export interface DependencyCallOrConstructProps {
getDependency: <R, T = any>(
token: T | (new (...p: any[]) => T)
) => R extends Value<infer A> ? A : T;
}
interface DependencyCallOrConstruct {
new (props: DependencyCallOrConstructProps): any;
(props: DependencyCallOrConstructProps): any;
}
export interface RegisterDependencyConfig {
type: 'value' | 'function' | 'class';
value: DependencyCallOrConstruct | any;
dependencies?: DependencyConfig[];
/**
* The unique identifier of the injection, if not passed, the value is equal to value
*/
token: Token;
}
export class DiContainer {
private dependency_config = new Map<Token, RegisterDependencyConfig>();
private dependency_map = new Map<Token, any>();
private dependency_status = new Map<Token, 'loading' | 'loaded'>();
private get_create_params(
config: RegisterDependencyConfig
): DependencyCallOrConstructProps {
const get_dependency: DependencyCallOrConstructProps['getDependency'] =
token => {
const found = config.dependencies?.find(
dep => dep.token === token
);
if (!found) {
return null;
}
if (found.lazy) {
this.load(found.token);
}
return this.dependency_map.get(token);
};
return {
getDependency: get_dependency,
};
}
private load(token: Token) {
if (this.dependency_map.has(token)) {
return;
}
if (this.dependency_status.get(token) === 'loading') {
throw new Error('Circular dependency', token);
}
this.dependency_status.set(token, 'loading');
const config = this.dependency_config.get(token);
if (!config) {
throw new Error('No dependencies found', token);
}
switch (config.type) {
case 'value': {
this.dependency_map.set(token, config.value);
break;
}
case 'class': {
const deps = config.dependencies || [];
deps.forEach(dep => {
if (!dep.lazy && !this.dependency_map.has(dep.token)) {
this.load(dep.token);
}
});
this.dependency_map.set(
token,
new config.value(this.get_create_params(config))
);
break;
}
case 'function': {
const deps = config.dependencies || [];
deps.forEach(dep => {
if (!dep.lazy && !this.dependency_map.has(dep.token)) {
this.load(dep.token);
}
});
this.dependency_map.set(
token,
config.value(this.get_create_params(config))
);
break;
}
default: {
console.error(
'Unknown type of dependency',
config.token,
config.type
);
}
}
this.dependency_status.set(token, 'loaded');
}
register(dependencyConfigs: RegisterDependencyConfig[]) {
dependencyConfigs.forEach(config => {
this.dependency_config.set(config.token, config);
});
}
getDependency: DependencyCallOrConstructProps['getDependency'] = token => {
this.load(token);
return this.dependency_map.get(token);
};
}

View File

@@ -0,0 +1,6 @@
export { DiContainer } from './di';
export type {
Value,
DependencyCallOrConstructProps,
RegisterDependencyConfig,
} from './di';

112
libs/utils/src/dom.ts Normal file
View File

@@ -0,0 +1,112 @@
/**
* Get the position information of the dom element based on ClientRect
* @param {HTMLElement} dom
* @param {HTMLElement=} parent
* @returns
*/
export function getBoundingOffsetRect(
dom: HTMLElement | Element,
parent?: HTMLElement
): DOMRect {
const dom_rect = dom.getBoundingClientRect();
if (!parent) {
return dom_rect;
}
const parent_rect = parent.getBoundingClientRect();
const x = dom_rect.x - parent_rect.x;
const y = dom_rect.y - parent_rect.y;
const rect = {
width: dom_rect.width,
height: dom_rect.height,
x,
y,
left: x,
top: y,
right: x + dom_rect.width,
bottom: y + dom_rect.height,
};
return {
...rect,
// eslint-disable-next-line @typescript-eslint/naming-convention
toJSON() {
return rect;
},
};
}
/**
*
* get block`s id from element or node
* @export
* @param {(HTMLElement | Node)} node
* @return {*}
*/
export function getBlockIdByDom(node: HTMLElement | Node): string | undefined {
let element;
if (node instanceof HTMLElement) {
element = node;
}
if (node instanceof Node) {
element = node.parentElement;
}
return element
?.closest('[data-block-id]')
?.attributes.getNamedItem('data-block-id')?.value;
}
export function removeDomPaddingFromDomRect(
dom: HTMLElement | Element,
rect: DOMRect
): DOMRect {
const client_rect = {
x: rect.x,
y: rect.y,
left: rect.left,
top: rect.top,
bottom: rect.bottom,
right: rect.right,
width: rect.width,
height: rect.height,
};
const computed_style = window.getComputedStyle(dom);
const padding_left = parseInt(computed_style.paddingLeft);
const padding_right = parseInt(computed_style.paddingRight);
const padding_top = parseInt(computed_style.paddingTop);
const padding_bottom = parseInt(computed_style.paddingBottom);
if (client_rect) {
client_rect.left += padding_left;
client_rect.right -= padding_right;
client_rect.top += padding_top;
client_rect.bottom -= padding_bottom;
client_rect.x = client_rect.left;
client_rect.y = client_rect.top;
client_rect.width = client_rect.right - client_rect.left;
client_rect.height = client_rect.bottom - client_rect.top;
}
return {
...client_rect,
// eslint-disable-next-line @typescript-eslint/naming-convention
toJSON() {
return client_rect;
},
};
}
export const withUnit = (text: string | number, unit = 'px') => {
return `${text}${unit}`;
};
export const withPx = (text: string | number) => withUnit(text, 'px');
export const ownerDocument = (node: Node | null | undefined): Document => {
return (node && node.ownerDocument) || document;
};
export const getBlockIdByNode = (node: Node) => {
return (
node.parentElement
?.closest('[data-block-id]')
?.attributes.getNamedItem('data-block-id')?.value ?? undefined
);
};

1
libs/utils/src/env.ts Normal file
View File

@@ -0,0 +1 @@
export const isDev = process.env['NODE_ENV'] === 'development';

View File

@@ -0,0 +1,177 @@
import * as React from 'react';
const changedArray = (a: Array<unknown> = [], b: Array<unknown> = []) =>
a.length !== b.length ||
a.some((item, index) => !Object.is(item, b[index]));
interface FallbackProps {
error: Error;
resetErrorBoundary: (...args: Array<unknown>) => void;
}
interface ErrorBoundaryPropsWithComponent {
onResetKeysChange?: (
prevResetKeys: Array<unknown> | undefined,
resetKeys: Array<unknown> | undefined
) => void;
onReset?: (...args: Array<unknown>) => void;
onError?: (error: Error, info: { componentStack: string }) => void;
resetKeys?: Array<unknown>;
fallback?: never;
FallbackComponent: React.ComponentType<FallbackProps>;
fallbackRender?: never;
}
declare function FallbackRender(
props: FallbackProps
): React.ReactElement<
unknown,
string | React.FunctionComponent | typeof React.Component
> | null;
interface ErrorBoundaryPropsWithRender {
onResetKeysChange?: (
prevResetKeys: Array<unknown> | undefined,
resetKeys: Array<unknown> | undefined
) => void;
onReset?: (...args: Array<unknown>) => void;
onError?: (error: Error, info: { componentStack: string }) => void;
resetKeys?: Array<unknown>;
fallback?: never;
FallbackComponent?: never;
fallbackRender: typeof FallbackRender;
}
interface ErrorBoundaryPropsWithFallback {
onResetKeysChange?: (
prevResetKeys: Array<unknown> | undefined,
resetKeys: Array<unknown> | undefined
) => void;
onReset?: (...args: Array<unknown>) => void;
onError?: (error: Error, info: { componentStack: string }) => void;
resetKeys?: Array<unknown>;
fallback: React.ReactElement<
unknown,
string | React.FunctionComponent | typeof React.Component
> | null;
FallbackComponent?: never;
fallbackRender?: never;
}
type ErrorBoundaryProps =
| ErrorBoundaryPropsWithFallback
| ErrorBoundaryPropsWithComponent
| ErrorBoundaryPropsWithRender;
type ErrorBoundaryState = { error: Error | null };
const initialState: ErrorBoundaryState = { error: null };
class ErrorBoundary extends React.Component<
React.PropsWithRef<React.PropsWithChildren<ErrorBoundaryProps>>,
ErrorBoundaryState
> {
// eslint-disable-next-line @typescript-eslint/naming-convention
static getDerivedStateFromError(error: Error) {
return { error };
}
override state = initialState;
resetErrorBoundary = (...args: Array<unknown>) => {
this.props.onReset?.(...args);
this.reset();
};
reset() {
this.setState(initialState);
}
override componentDidCatch(error: Error, info: React.ErrorInfo) {
this.props.onError?.(error, info);
}
override componentDidUpdate(
prevProps: ErrorBoundaryProps,
prevState: ErrorBoundaryState
) {
const { error } = this.state;
const { resetKeys } = this.props;
// There's an edge case where if the thing that triggered the error
// happens to *also* be in the resetKeys array, we'd end up resetting
// the error boundary immediately. This would likely trigger a second
// error to be thrown.
// So we make sure that we don't check the resetKeys on the first call
// of cDU after the error is set
if (
error !== null &&
prevState.error !== null &&
changedArray(prevProps.resetKeys, resetKeys)
) {
this.props.onResetKeysChange?.(prevProps.resetKeys, resetKeys);
this.reset();
}
}
override render() {
const { error } = this.state;
const { fallbackRender, FallbackComponent, fallback } = this.props;
if (error !== null) {
const props = {
error,
resetErrorBoundary: this.resetErrorBoundary,
};
if (React.isValidElement(fallback)) {
return fallback;
} else if (typeof fallbackRender === 'function') {
return fallbackRender(props);
} else if (FallbackComponent) {
return <FallbackComponent {...props} />;
} else {
throw new Error(
'react-error-boundary requires either a fallback, fallbackRender, or FallbackComponent prop'
);
}
}
return this.props.children;
}
}
function withErrorBoundary<P>(
Component: React.ComponentType<P>,
errorBoundaryProps: ErrorBoundaryProps
): React.ComponentType<P> {
const Wrapped: React.ComponentType<P> = props => {
return (
<ErrorBoundary {...errorBoundaryProps}>
<Component {...props} />
</ErrorBoundary>
);
};
// Format for display in DevTools
const name = Component.displayName || Component.name || 'Unknown';
Wrapped.displayName = `withErrorBoundary(${name})`;
return Wrapped;
}
function useErrorHandler(givenError?: unknown): (error: unknown) => void {
const [error, setError] = React.useState<unknown>(null);
if (givenError != null) throw givenError;
if (error != null) throw error;
return setError;
}
export { ErrorBoundary, withErrorBoundary, useErrorHandler };
export type {
FallbackProps,
ErrorBoundaryPropsWithComponent,
ErrorBoundaryPropsWithRender,
ErrorBoundaryPropsWithFallback,
ErrorBoundaryProps,
};

View File

@@ -0,0 +1,17 @@
import { log } from './logger';
interface CreateNoopWithMessageProps {
module?: string;
message: string;
level?: 'info' | 'warn' | 'error';
}
export function createNoopWithMessage(
props: CreateNoopWithMessageProps
): (...p: any[]) => any {
const { message, level = 'info', module } = props;
const module_log = module ? log.get(module) : log;
return () => {
module_log[level](message);
};
}

35
libs/utils/src/index.ts Normal file
View File

@@ -0,0 +1,35 @@
export * from './types';
export * from './constants';
export * from './error-boundary';
export * from './date';
export * from './utils';
export * from './dom';
// eslint-disable-next-line no-restricted-imports
export * from './lodash';
export * from './rect';
export * from './keyboard';
export * from './useragent';
export { isDev } from './env';
export { log } from './logger';
export { createNoopWithMessage } from './function';
export { DiContainer } from './di';
export type {
Value as DiValue,
DependencyCallOrConstructProps,
RegisterDependencyConfig,
} from './di';
export { copyToClipboard } from './copy-to-clipboard';

View File

@@ -0,0 +1,20 @@
import { KeyboardEvent } from 'react';
class KeyboardHelper {
public eventHelper: EventHelper;
constructor() {
this.eventHelper = new EventHelper();
}
}
class EventHelper {
isSelectAll(event: KeyboardEvent) {
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyA') {
return true;
}
return false;
}
}
export const keyboardHelper = new KeyboardHelper();

24
libs/utils/src/lodash.ts Normal file
View File

@@ -0,0 +1,24 @@
// eslint-disable-next-line no-restricted-imports
export {
debounce,
throttle,
cloneDeep,
isEqual,
sum,
has,
forEach,
upperFirst,
curry,
isFunction,
isString,
isPlainObject,
noop,
lowerFirst,
last,
first,
without,
difference,
escape,
countBy,
maxBy,
} from 'lodash-es';

4
libs/utils/src/logger.ts Normal file
View File

@@ -0,0 +1,4 @@
import Logger from 'js-logger';
// eslint-disable-next-line react-hooks/rules-of-hooks
Logger.useDefaults();
export const log = Logger;

172
libs/utils/src/rect.ts Normal file
View File

@@ -0,0 +1,172 @@
export class Point {
public x: number;
public y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
public equals({ x, y }: Point) {
return this.x === x && this.y === y;
}
public xDistance({ x }: Point) {
return this.x - x;
}
public yDistance({ y }: Point) {
return this.y - y;
}
public xDistanceAbsolute(point: Point) {
return Math.abs(this.xDistance(point));
}
public yDistanceAbsolute(point: Point) {
return Math.abs(this.yDistance(point));
}
public distance(point: Point) {
return Math.sqrt(
Math.pow(this.xDistance(point), 2) +
Math.pow(this.yDistance(point), 2)
);
}
}
export class Rect {
private _left: number;
private _top: number;
private _right: number;
private _bottom: number;
constructor(left: number, top: number, right: number, bottom: number) {
const [physicTop, physicBottom] =
top <= bottom ? [top, bottom] : [bottom, top];
const [physicLeft, physicRight] =
left <= right ? [left, right] : [right, left];
this._top = physicTop;
this._right = physicRight;
this._left = physicLeft;
this._bottom = physicBottom;
}
get width() {
return Math.abs(this._left - this._right);
}
get height() {
return Math.abs(this._bottom - this._top);
}
get top() {
return this._top;
}
get left() {
return this._left;
}
get bottom() {
return this._bottom;
}
get right() {
return this._right;
}
equals({ top, left, bottom, right }: Rect) {
return (
top === this._top &&
bottom === this._bottom &&
left === this._left &&
right === this._right
);
}
isContainPoint({ x, y }: Point) {
return (
y >= this._top &&
y <= this._bottom &&
x >= this.left &&
x <= this._right
);
}
isContain({ top, left, bottom, right }: Rect) {
return (
top >= this._top &&
top <= this._bottom &&
bottom >= this._top &&
bottom <= this._bottom &&
left >= this._left &&
left <= this.right &&
right >= this.left &&
right <= this._right
);
}
isIntersect(rect: Rect) {
const { left: x1, top: y1, width: w1, height: h1 } = rect;
const { left: x2, top: y2, width: w2, height: h2 } = this;
const maxX = x1 + w1 >= x2 + w2 ? x1 + w1 : x2 + w2;
const maxY = y1 + h1 >= y2 + h2 ? y1 + h1 : y2 + h2;
const minX = x1 <= x2 ? x1 : x2;
const minY = y1 <= y2 ? y1 : y2;
if (maxX - minX <= w1 + w2 && maxY - minY <= h1 + h2) {
return true;
} else {
return false;
}
}
isPointDown({ y }: Point) {
return this.bottom < y;
}
isPointUp({ y }: Point) {
return y < this.top;
}
isPointLeft({ x }: Point) {
return x < this.left;
}
isPointRight({ x }: Point) {
return x > this.right;
}
fromNewLeft(left: number) {
this._left = left;
return new Rect(left, this.top, this.right, this.bottom);
}
// eslint-disable-next-line @typescript-eslint/naming-convention
static fromLTRB(left: number, top: number, right: number, bottom: number) {
return new Rect(left, top, right, bottom);
}
// eslint-disable-next-line @typescript-eslint/naming-convention
static fromLWTH(left: number, width: number, top: number, height: number) {
return new Rect(left, top, left + width, top + height);
}
// eslint-disable-next-line @typescript-eslint/naming-convention
static fromPoints(startPoint: Point, endPoint: Point) {
const { y: top, x: left } = startPoint;
const { y: bottom, x: right } = endPoint;
return Rect.fromLTRB(left, top, right, bottom);
}
}
export const pointsToRect = (startPoint: Point, endPoint: Point) => {
const { y: top, x: left } = startPoint;
const { y: bottom, x: right } = endPoint;
return Rect.fromLTRB(left, top, right, bottom);
};
export const domToRect = (dom: HTMLElement) => {
const { top, width, left, height } = dom.getBoundingClientRect();
return Rect.fromLWTH(left, width, top, height);
};

View File

@@ -0,0 +1,2 @@
export * from './user-workspace';
export * from './tools';

View File

@@ -0,0 +1,32 @@
/**
* get all value`s type of a object
*
* @example
* ```ts
* // except: type Values = 1 | '2'
* type Values = ValueOf<{a: 1, b: '2'}>
* ```
*/
export type ValueOf<X extends Record<string, unknown>> = X[keyof X];
/**
* get type of a array by index
*
* @example
* ```ts
* // except: type FirstType = string;
* type FirstType = First<[string], 0>
* ```
*/
export type IndexOf<X extends unknown[], I extends number> = X[I];
/**
* get first type of a array
*
* @example
* ```ts
* // except: type FirstType = string;
* type FirstType = First<[string]>
* ```
*/
export type First<X extends unknown[]> = IndexOf<X, 0>;

View File

@@ -0,0 +1,8 @@
/** user model for Affine */
export type UserInfo = {
id: string;
nickname: string;
username: string;
email: string;
photo: string;
};

View File

@@ -0,0 +1,74 @@
const get_ua = () => {
const ua = navigator.userAgent;
const uas = ua.toLowerCase();
const mobile =
/mobile|tablet|ip(ad|hone|od)|android/i.test(ua) ||
(uas.indexOf('safari') > -1 &&
/Mac OS/i.test(ua) &&
/Macintosh/i.test(ua));
const android =
(mobile &&
(uas.indexOf('android') > -1 || uas.indexOf('linux') > -1)) ||
uas.indexOf('adr') > -1;
const ios = mobile && !android && /Mac OS/i.test(ua);
const iphone = ios && uas.indexOf('iphone') > -1;
const ipad = ios && !iphone;
const wx = /MicroMessenger/i.test(ua);
const chrome = /CriOS/i.test(ua) || /Chrome/i.test(ua);
const tiktok = mobile && /aweme/i.test(ua);
const weibo = mobile && /Weibo/i.test(ua);
const safari =
ios &&
!chrome &&
!wx &&
!weibo &&
!tiktok &&
/Safari|Macintosh/i.test(ua);
const firefox = /Firefox/.test(ua);
const win = /windows|win32|win64|wow32|wow64/.test(uas);
const linux = /linux/.test(uas);
return {
ua,
mobile,
android,
ios,
wx,
chrome,
iphone,
ipad,
safari,
tiktok,
weibo,
win,
linux,
firefox,
};
};
class UaHelper {
private ua_map;
public isLinux = false;
public isMacOs = false;
public isSafari = false;
public isWindows = false;
public isFireFox = false;
constructor() {
this.ua_map = get_ua();
this.init_ua_flags();
}
public checkUseragent(isUseragent: keyof ReturnType<typeof get_ua>) {
return Boolean(this.ua_map[isUseragent]);
}
private init_ua_flags() {
this.isLinux = this.checkUseragent('linux');
this.isMacOs = this.checkUseragent('ios');
this.isSafari = this.checkUseragent('safari');
this.isWindows = this.checkUseragent('win');
this.isFireFox = this.checkUseragent('firefox');
}
}
export const uaHelper = new UaHelper();

78
libs/utils/src/utils.ts Normal file
View File

@@ -0,0 +1,78 @@
import type { UserInfo } from './types';
export function getUserDisplayName(user?: UserInfo) {
const { nickname, username, email } = user || {};
return nickname || username || email || 'Unknown User';
}
/**
* Get workspace_id from URL
* @returns workspace_id
*/
export function getWorkspaceId() {
const path = window.location.pathname.match(/\/(\w+)\//);
return path ? path[1] : undefined;
}
/**
* Get page_id from URL
* @returns page_id
*/
export function getPageId() {
const path = window.location.pathname.match(/\/(\w+)\/(\w+)/);
return path ? path[2] : undefined;
}
export async function sleep(delay?: number) {
return new Promise(res => {
window.setTimeout(() => {
res(true);
}, delay);
});
}
export function isInternalPageUrl(url: string) {
if (!url) return false;
return (
url.startsWith(window.location.origin) &&
/^https?:\/\/(localhost:4200|(nightly|app)\.affine\.pro|.*?\.ligo-virgo\.pages\.dev)/.test(
url
)
);
}
export function getRelativeUrlForInternalPageUrl(url: string) {
if (url?.startsWith(window.location.origin)) {
return url.slice(window.location.origin.length);
}
return url;
}
/**
* Generate a fragile object that will throw error at any operation.
*/
export const genErrorObj = (
...args: ConstructorParameters<ErrorConstructor>
): unknown =>
new Proxy(
{},
{
get(target, prop, receiver) {
console.error('You are trying to access a property', prop);
throw new Error(...args);
},
set(target, prop, val, receiver) {
console.error('You are trying to set a property', prop, val);
throw new Error(...args);
},
apply(target, thisArg, argumentsList) {
console.error(
'You are trying to call a function',
thisArg,
argumentsList
);
throw new Error(...args);
},
}
);