diff --git a/.eslintignore b/.eslintignore index 42871b1461..42a9f2783a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -6,3 +6,4 @@ storybook-static affine-out _next lib +.eslintrc.js diff --git a/.eslintrc.js b/.eslintrc.js index c09b044b12..4e46209668 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,3 +1,5 @@ +const { readdirSync, statSync } = require('fs'); + const createPattern = packageName => [ { group: ['**/dist', '**/dist/**'], @@ -21,22 +23,14 @@ const createPattern = packageName => [ }, ]; -const allPackages = [ - 'cli', - 'component', - 'debug', - 'env', - 'graphql', - 'hooks', - 'i18n', - 'jotai', - 'native', - 'plugin-infra', - 'templates', - 'theme', - 'workspace', - 'y-indexeddb', -]; +const pkgs = readdirSync('./packages').filter(pkg => { + return statSync(`./packages/${pkg}`).isDirectory(); +}); +const apps = readdirSync('./apps').filter(pkg => { + return statSync(`./apps/${pkg}`).isDirectory(); +}); + +const allPackages = pkgs.concat(apps); /** * @type {import('eslint').Linter.Config} @@ -67,6 +61,7 @@ const config = { }, ecmaVersion: 'latest', sourceType: 'module', + project: './tsconfig.eslint.json', }, plugins: [ 'react', @@ -83,7 +78,7 @@ const config = { 'no-cond-assign': 'off', 'react/prop-types': 'off', '@typescript-eslint/consistent-type-imports': 'error', - '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-non-null-assertion': 'error', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/no-unused-vars': [ @@ -136,6 +131,12 @@ const config = { '@typescript-eslint/no-var-requires': 0, }, }, + { + files: ['**/__tests__/**/*', '**/*.stories.tsx'], + rules: { + '@typescript-eslint/no-non-null-assertion': 0, + }, + }, ...allPackages.map(pkg => ({ files: [`packages/${pkg}/src/**/*.ts`, `packages/${pkg}/src/**/*.tsx`], rules: { diff --git a/apps/electron/layers/main/src/db/base-db-adapter.ts b/apps/electron/layers/main/src/db/base-db-adapter.ts index 485214a602..60a6693621 100644 --- a/apps/electron/layers/main/src/db/base-db-adapter.ts +++ b/apps/electron/layers/main/src/db/base-db-adapter.ts @@ -10,9 +10,7 @@ export abstract class BaseSQLiteAdapter { db: SqliteConnection | null = null; abstract role: string; - constructor(public readonly path: string) { - logger.info(`[SQLiteAdapter]`, 'path:', path); - } + constructor(public readonly path: string) {} async connectIfNeeded() { if (!this.db) { diff --git a/apps/electron/layers/main/src/db/ensure-db.ts b/apps/electron/layers/main/src/db/ensure-db.ts index eaa5f17af3..36a9e2851a 100644 --- a/apps/electron/layers/main/src/db/ensure-db.ts +++ b/apps/electron/layers/main/src/db/ensure-db.ts @@ -100,6 +100,7 @@ function getWorkspaceDB$(id: string) { ) ); } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return db$Map.get(id)!; } diff --git a/apps/electron/layers/main/src/db/secondary-db.ts b/apps/electron/layers/main/src/db/secondary-db.ts index 35b487337e..66a0ee746d 100644 --- a/apps/electron/layers/main/src/db/secondary-db.ts +++ b/apps/electron/layers/main/src/db/secondary-db.ts @@ -107,7 +107,6 @@ export class SecondaryWorkspaceSQLiteDB extends BaseSQLiteAdapter { return; } this.firstConnected = true; - const { db } = this; const onUpstreamUpdate = (update: Uint8Array, origin: YOrigin) => { if (origin === 'renderer') { @@ -118,8 +117,8 @@ export class SecondaryWorkspaceSQLiteDB extends BaseSQLiteAdapter { const onSelfUpdate = (update: Uint8Array, origin: YOrigin) => { // for self update from upstream, we need to push it to external DB - if (origin === 'upstream') { - this.addUpdateToUpdateQueue(db!, update); + if (origin === 'upstream' && this.db) { + this.addUpdateToUpdateQueue(this.db, update); } if (origin === 'self') { diff --git a/apps/electron/layers/main/src/updater/electron-updater.ts b/apps/electron/layers/main/src/updater/electron-updater.ts index f1e50f97d4..8f0e864621 100644 --- a/apps/electron/layers/main/src/updater/electron-updater.ts +++ b/apps/electron/layers/main/src/updater/electron-updater.ts @@ -67,7 +67,7 @@ export const registerUpdater = async () => { // register events for checkForUpdatesAndNotify _autoUpdater.on('update-available', info => { if (allowAutoUpdate) { - _autoUpdater!.downloadUpdate(); + _autoUpdater?.downloadUpdate(); logger.info('Update available, downloading...', info); } updaterSubjects.updateAvailable.next({ diff --git a/apps/server/src/utils/nestjs.ts b/apps/server/src/utils/nestjs.ts index f1f18beefe..27129a2dee 100644 --- a/apps/server/src/utils/nestjs.ts +++ b/apps/server/src/utils/nestjs.ts @@ -14,7 +14,7 @@ export function getRequestResponseFromContext(context: ExecutionContext) { }>(); return { req: gqlContext.req, - res: gqlContext.req.res!, + res: gqlContext.req.res, }; } case 'http': { @@ -37,7 +37,7 @@ export function getRequestResponseFromHost(host: ArgumentsHost) { }>(); return { req: gqlContext.req, - res: gqlContext.req.res!, + res: gqlContext.req.res, }; } case 'http': { diff --git a/packages/component/src/components/app-sidebar/app-updater-button/index.tsx b/packages/component/src/components/app-sidebar/app-updater-button/index.tsx index ba343afbb4..b58e51ea50 100644 --- a/packages/component/src/components/app-sidebar/app-updater-button/index.tsx +++ b/packages/component/src/components/app-sidebar/app-updater-button/index.tsx @@ -4,7 +4,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { CloseIcon, NewIcon, ResetIcon } from '@blocksuite/icons'; import clsx from 'clsx'; import { atom, useAtomValue, useSetAtom } from 'jotai'; -import { startTransition } from 'react'; +import { startTransition, useCallback } from 'react'; import * as styles from './index.css'; import { @@ -50,53 +50,67 @@ export function AppUpdaterButton({ className, style }: AddPageButtonProps) { const downloadProgress = useAtomValue(downloadProgressAtom); const setChangelogCheckAtom = useSetAtom(changelogCheckedAtom); - const onDismissCurrentChangelog = () => { + const onDismissCurrentChangelog = useCallback(() => { + if (!currentVersion) { + return; + } startTransition(() => setChangelogCheckAtom(mapping => { return { ...mapping, - [currentVersion!]: true, + [currentVersion]: true, }; }) ); - }; + }, [currentVersion, setChangelogCheckAtom]); + const onClickUpdate = useCallback(() => { + if (updateReady) { + window.apis?.updater.quitAndInstall(); + } else if (updateAvailable) { + if (updateAvailable.allowAutoUpdate) { + // wait for download to finish + } else { + window.open( + `https://github.com/toeverything/AFFiNE/releases/tag/v${currentVersion}`, + '_blank' + ); + } + } else if (currentChangelogUnread) { + window.open(config.changelogUrl, '_blank'); + onDismissCurrentChangelog(); + } else { + throw new Unreachable(); + } + }, [ + currentChangelogUnread, + currentVersion, + onDismissCurrentChangelog, + updateAvailable, + updateReady, + ]); if (!updateAvailable && !currentChangelogUnread) { return null; } + const updateAvailableNode = updateAvailable + ? updateAvailable.allowAutoUpdate + ? renderUpdateAvailableAllowAutoUpdate() + : renderUpdateAvailableNotAllowAutoUpdate() + : null; + const whatsNew = + !updateAvailable && currentChangelogUnread ? renderWhatsNew() : null; + return ( diff --git a/packages/component/src/components/page-list/operation-menu-items/export.tsx b/packages/component/src/components/page-list/operation-menu-items/export.tsx index b827cf16b6..42606ea9ea 100644 --- a/packages/component/src/components/page-list/operation-menu-items/export.tsx +++ b/packages/component/src/components/page-list/operation-menu-items/export.tsx @@ -8,7 +8,7 @@ import { ExportToMarkdownIcon, ExportToPdfIcon, } from '@blocksuite/icons'; -import { useRef } from 'react'; +import { useCallback, useRef } from 'react'; import { Menu, MenuItem } from '../../..'; import type { CommonMenuItemProps } from './types'; @@ -18,35 +18,39 @@ const ExportToPdfMenuItem = ({ }: CommonMenuItemProps<{ type: 'pdf' }>) => { const t = useAFFiNEI18N(); const contentParserRef = useRef(); - return ( - <> - {globalThis.currentEditor!.mode === 'page' && ( - { - if (!contentParserRef.current) { - contentParserRef.current = new ContentParser( - globalThis.currentEditor!.page - ); - } - const result = await window.apis?.export.savePDFFileAs( - ( - globalThis.currentEditor!.page.root as PageBlockModel - ).title.toString() - ); - if (result !== undefined) { - return; - } - contentParserRef.current.exportPdf(); - onSelect?.({ type: 'pdf' }); - }} - icon={} - > - {t['Export to PDF']()} - - )} - - ); + const { currentEditor } = globalThis; + const onClickDownloadPDF = useCallback(() => { + if (!currentEditor) { + return; + } + const contentParser = + contentParserRef.current ?? + (contentParserRef.current = new ContentParser(currentEditor.page)); + + window.apis?.export + .savePDFFileAs( + (currentEditor.page.root as PageBlockModel).title.toString() + ) + .then(result => { + if (result !== undefined) { + return; + } + contentParser.exportPdf(); + onSelect?.({ type: 'pdf' }); + }); + }, [currentEditor, onSelect]); + if (currentEditor && currentEditor.mode === 'page') { + return ( + } + > + {t['Export to PDF']()} + + ); + } + return null; }; const ExportToHtmlMenuItem = ({ @@ -54,19 +58,22 @@ const ExportToHtmlMenuItem = ({ }: CommonMenuItemProps<{ type: 'html' }>) => { const t = useAFFiNEI18N(); const contentParserRef = useRef(); + const { currentEditor } = globalThis; + const onClickExportHtml = useCallback(() => { + if (!currentEditor) { + return; + } + if (!contentParserRef.current) { + contentParserRef.current = new ContentParser(currentEditor.page); + } + contentParserRef.current.exportHtml(); + onSelect?.({ type: 'html' }); + }, [onSelect, currentEditor]); return ( <> { - if (!contentParserRef.current) { - contentParserRef.current = new ContentParser( - globalThis.currentEditor!.page - ); - } - contentParserRef.current.exportHtml(); - onSelect?.({ type: 'html' }); - }} + onClick={onClickExportHtml} icon={} > {t['Export to HTML']()} @@ -108,19 +115,22 @@ const ExportToMarkdownMenuItem = ({ }: CommonMenuItemProps<{ type: 'markdown' }>) => { const t = useAFFiNEI18N(); const contentParserRef = useRef(); + const { currentEditor } = globalThis; + const onClickExportMarkdown = useCallback(() => { + if (!currentEditor) { + return; + } + if (!contentParserRef.current) { + contentParserRef.current = new ContentParser(currentEditor.page); + } + contentParserRef.current.exportMarkdown(); + onSelect?.({ type: 'markdown' }); + }, [onSelect, currentEditor]); return ( <> { - if (!contentParserRef.current) { - contentParserRef.current = new ContentParser( - globalThis.currentEditor!.page - ); - } - contentParserRef.current.exportMarkdown(); - onSelect?.({ type: 'markdown' }); - }} + onClick={onClickExportMarkdown} icon={} > {t['Export to Markdown']()} diff --git a/packages/component/src/ui/tree-view/tree-view.tsx b/packages/component/src/ui/tree-view/tree-view.tsx index 8137f15d0d..67ca39388d 100644 --- a/packages/component/src/ui/tree-view/tree-view.tsx +++ b/packages/component/src/ui/tree-view/tree-view.tsx @@ -12,7 +12,7 @@ import { useCallback, useState } from 'react'; import useCollapsed from './hooks/use-collapsed'; import useSelectWithKeyboard from './hooks/use-select-with-keyboard'; import { TreeNode, TreeNodeWithDnd } from './tree-node'; -import type { TreeViewProps } from './types'; +import type { Node, TreeViewProps } from './types'; import { findNode } from './utils'; export const TreeView = ({ data, @@ -58,6 +58,39 @@ export const TreeView = ({ setDraggingId(e.active.id as string); }, []); if (enableDnd) { + const treeNodes = data.map((node, index) => ( + + )); + const draggingNode = (function () { + let draggingNode: Node | undefined; + if (draggingId) { + draggingNode = findNode(draggingId, data); + } + if (draggingNode) { + return ( + {}} + {...otherProps} + /> + ); + } + return null; + })(); return ( ({ onDragMove={onDragMove} onDragEnd={onDragEnd} > - {data.map((node, index) => ( - - ))} - - - {draggingId && ( - {}} - {...otherProps} - /> - )} - + {treeNodes} + {draggingNode} ); } diff --git a/packages/hooks/src/use-affine-ipc-renderer.ts b/packages/hooks/src/use-affine-ipc-renderer.ts index 5cf9457f23..0cf2176b3e 100644 --- a/packages/hooks/src/use-affine-ipc-renderer.ts +++ b/packages/hooks/src/use-affine-ipc-renderer.ts @@ -46,14 +46,15 @@ export function useAffineListener( if (!fnRef.current) { fnRef.current = listener; } + const ipcListener = fnRef.current ?? (fnRef.current = listener); useEffect(() => { if (once) { - window.affine.ipcRenderer.once(channel, fnRef.current!); + window.affine.ipcRenderer.once(channel, ipcListener); } else { - window.affine.ipcRenderer.on(channel, fnRef.current!); + window.affine.ipcRenderer.on(channel, ipcListener); } return () => { - window.affine.ipcRenderer.removeListener(channel, fnRef.current!); + window.affine.ipcRenderer.removeListener(channel, ipcListener); }; - }, [channel, once]); + }, [channel, once, ipcListener]); } diff --git a/packages/workspace/src/providers/broad-cast-channel/index.ts b/packages/workspace/src/providers/broad-cast-channel/index.ts index f3085d223b..c9c830b3ed 100644 --- a/packages/workspace/src/providers/broad-cast-channel/index.ts +++ b/packages/workspace/src/providers/broad-cast-channel/index.ts @@ -33,7 +33,7 @@ export const createBroadCastChannelProvider = ( case 'doc:diff': { const [, diff, clientId] = event.data; const update = Y.encodeStateAsUpdate(doc, diff); - broadcastChannel!.postMessage(['doc:update', update, clientId]); + broadcastChannel?.postMessage(['doc:update', update, clientId]); break; } case 'doc:update': { @@ -47,7 +47,7 @@ export const createBroadCastChannelProvider = ( const [, clientId] = event.data; const clients = getClients(awareness); const update = encodeAwarenessUpdate(awareness, clients); - broadcastChannel!.postMessage(['awareness:update', update, clientId]); + broadcastChannel?.postMessage(['awareness:update', update, clientId]); break; } case 'awareness:update': { diff --git a/packages/workspace/src/providers/index.ts b/packages/workspace/src/providers/index.ts index 8d863cd2d5..bddd5c58b8 100644 --- a/packages/workspace/src/providers/index.ts +++ b/packages/workspace/src/providers/index.ts @@ -162,8 +162,7 @@ const sqliteOrigin = Symbol('sqlite-provider-origin'); const createSQLiteProvider = ( blockSuiteWorkspace: BlockSuiteWorkspace ): SQLiteProvider => { - const apis = window.apis!; - const events = window.events!; + const { apis, events } = window; // make sure it is being used in Electron with APIs assertExists(apis); assertExists(events); @@ -216,7 +215,7 @@ const createSQLiteProvider = ( const createSQLiteDBDownloadProvider = ( blockSuiteWorkspace: BlockSuiteWorkspace ): SQLiteDBDownloadProvider => { - const apis = window.apis!; + const { apis } = window; let disconnected = false; let _resolve: () => void; @@ -273,7 +272,7 @@ const createSQLiteDBDownloadProvider = ( return; } - return window.apis?.db.addBlob( + return apis?.db.addBlob( blockSuiteWorkspace.id, k, new Uint8Array(await blob.arrayBuffer()) diff --git a/packages/workspace/src/utils.ts b/packages/workspace/src/utils.ts index 043a571750..cfc379b1e0 100644 --- a/packages/workspace/src/utils.ts +++ b/packages/workspace/src/utils.ts @@ -65,9 +65,10 @@ export function createEmptyBlockSuiteWorkspace( const blobStorages: StoreOptions['blobStorages'] = []; if (flavour === WorkspaceFlavour.AFFINE) { - blobStorages.push(id => - createAffineBlobStorage(id, config!.workspaceApis!) - ); + if (config && config.workspaceApis) { + const workspaceApis = config.workspaceApis; + blobStorages.push(id => createAffineBlobStorage(id, workspaceApis)); + } } else { if (typeof window !== 'undefined') { blobStorages.push(createIndexeddbStorage); diff --git a/plugins/bookmark-block/src/blocksuite/ui.tsx b/plugins/bookmark-block/src/blocksuite/ui.tsx index 28f4066294..174d533dd2 100644 --- a/plugins/bookmark-block/src/blocksuite/ui.tsx +++ b/plugins/bookmark-block/src/blocksuite/ui.tsx @@ -47,8 +47,8 @@ const handleEnter = ({ null >; const vEditor = getVirgoByModel(blockRange.models[0]); - const linkInfo = vEditor! - .getDeltasByVRange({ + const linkInfo = vEditor + ?.getDeltasByVRange({ index: blockRange.startOffset, length: 0, }) @@ -70,7 +70,7 @@ const handleEnter = ({ currentBlockIndex + 1 ); - vEditor!.deleteText({ + vEditor?.deleteText({ index, length, }); diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json new file mode 100644 index 0000000000..e7030777d2 --- /dev/null +++ b/tsconfig.eslint.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "allowJs": true + }, + "include": ["."], + "exclude": [ + "target", + "node_modules", + "dist", + "lib", + ".coverage", + ".yarn", + "test-results" + ] +}