Browse Source

Merge branch 'master' into feat/mcp-server

Gabriel Horner 1 month ago
parent
commit
b8cae9c4f3
48 changed files with 2472 additions and 396 deletions
  1. 10 4
      deps/db/src/logseq/db/common/view.cljs
  2. 3 3
      deps/graph-parser/src/logseq/graph_parser/exporter.cljs
  3. 9 2
      deps/outliner/src/logseq/outliner/core.cljs
  4. 1 1
      deps/outliner/src/logseq/outliner/db_pipeline.cljs
  5. 1 1
      deps/outliner/src/logseq/outliner/pipeline.cljs
  6. 0 2
      gulpfile.js
  7. 10 9
      libs/src/LSPlugin.core.ts
  8. 1 1
      libs/src/LSPlugin.ts
  9. 1 1
      package.json
  10. 2 0
      packages/ui/@/components/ui/alert.tsx
  11. 5 0
      packages/ui/examples/index.html
  12. 49 8
      packages/ui/examples/index.tsx
  13. 2 0
      packages/ui/package.json
  14. 26 0
      packages/ui/src/amplify/core.ts
  15. 8 0
      packages/ui/src/amplify/index.ts
  16. 114 0
      packages/ui/src/amplify/lang.ts
  17. 710 0
      packages/ui/src/amplify/ui.tsx
  18. 40 0
      packages/ui/src/i18n.ts
  19. 11 0
      packages/ui/src/ui.ts
  20. 1183 0
      packages/ui/yarn.lock
  21. 2 2
      resources/css/shui.css
  22. 0 1
      resources/index.html
  23. 0 0
      resources/js/lsplugin.core.js
  24. 1 2
      resources/mobile/index.html
  25. 73 72
      src/main/frontend/components/block.cljs
  26. 1 1
      src/main/frontend/components/journal.cljs
  27. 5 3
      src/main/frontend/components/query.cljs
  28. 1 0
      src/main/frontend/components/query/view.cljs
  29. 5 2
      src/main/frontend/components/theme.cljs
  30. 0 56
      src/main/frontend/components/user/config.js
  31. 34 53
      src/main/frontend/components/user/login.cljs
  32. 17 105
      src/main/frontend/components/user/login.css
  33. 34 23
      src/main/frontend/components/views.cljs
  34. 29 7
      src/main/frontend/db/async/util.cljs
  35. 2 1
      src/main/frontend/db/react.cljs
  36. 1 1
      src/main/frontend/fs.cljs
  37. 2 0
      src/main/frontend/handler.cljs
  38. 15 0
      src/main/frontend/handler/user.cljs
  39. 22 7
      src/main/frontend/mobile/index.css
  40. 8 7
      src/main/frontend/worker/db/migrate.cljs
  41. 15 8
      src/main/frontend/worker/db/validate.cljs
  42. 10 9
      src/main/frontend/worker/pipeline.cljs
  43. 6 0
      src/main/frontend/worker/rtc/exception.cljs
  44. 0 1
      src/main/mobile/components/search.cljs
  45. 1 0
      src/main/mobile/components/settings.cljs
  46. 0 1
      tailwind.all.css
  47. 2 1
      tailwind.config.js
  48. 0 1
      tailwind.mobile.css

+ 10 - 4
deps/db/src/logseq/db/common/view.cljs

@@ -421,11 +421,17 @@
      (common-util/distinct-by :label))))
 
 (defn- get-query-properties
-  [entities]
-  (distinct (mapcat keys entities)))
+  [query entities]
+  (let [properties (when (and (coll? query) (= :find (first query)))
+                     (let [expr (second query)]
+                       (when (= 'pull (first expr))
+                         (last expr))))]
+    (if (and (seq properties) (not= properties ['*]))
+      properties
+      (distinct (mapcat keys entities)))))
 
 (defn ^:api ^:large-vars/cleanup-todo get-view-data
-  [db view-id {:keys [journals? _view-for-id view-feature-type group-by-property-ident input query-entity-ids filters sorting]
+  [db view-id {:keys [journals? _view-for-id view-feature-type group-by-property-ident input query-entity-ids query filters sorting]
                :as opts}]
   ;; TODO: create a view for journals maybe?
   (cond
@@ -537,4 +543,4 @@
         (= feat-type :linked-references)
         (merge (select-keys entities-result [:ref-pages-count :ref-matched-children-ids]))
         query?
-        (assoc :properties (get-query-properties entities-result))))))
+        (assoc :properties (get-query-properties query entities-result))))))

+ 3 - 3
deps/graph-parser/src/logseq/graph_parser/exporter.cljs

@@ -1791,7 +1791,7 @@
         (split-pages-and-properties-tx pages-tx old-properties existing-pages (:import-state options) @(:upstream-properties tx-options))
         ;; _ (when (seq property-pages-tx) (cljs.pprint/pprint {:property-pages-tx property-pages-tx}))
         ;; Necessary to transact new property entities first so that block+page properties can be transacted next
-        main-props-tx-report (ldb/transact! conn property-pages-tx {::new-graph? true ::path file})
+        main-props-tx-report (d/transact! conn property-pages-tx {::new-graph? true ::path file})
         _ (save-from-tx property-pages-tx options)
 
         classes-tx @(:classes-tx tx-options)
@@ -1817,13 +1817,13 @@
         ;;                        [:whiteboard-pages :pages-index :page-properties-tx :property-page-properties-tx :pages-tx' :classes-tx :blocks-index :blocks-tx]
         ;;                        [whiteboard-pages pages-index page-properties-tx property-page-properties-tx pages-tx' classes-tx blocks-index blocks-tx]))
         ;; _ (when (not (seq whiteboard-pages)) (cljs.pprint/pprint {#_:property-pages-tx #_property-pages-tx :pages-tx pages-tx :tx tx'}))
-        main-tx-report (ldb/transact! conn tx' {::new-graph? true ::path file})
+        main-tx-report (d/transact! conn tx' {::new-graph? true ::path file})
         _ (save-from-tx tx' options)
 
         upstream-properties-tx
         (build-upstream-properties-tx @conn @(:upstream-properties tx-options) (:import-state options) log-fn)
         ;; _ (when (seq upstream-properties-tx) (cljs.pprint/pprint {:upstream-properties-tx upstream-properties-tx}))
-        upstream-tx-report (when (seq upstream-properties-tx) (ldb/transact! conn upstream-properties-tx {::new-graph? true ::path file}))
+        upstream-tx-report (when (seq upstream-properties-tx) (d/transact! conn upstream-properties-tx {::new-graph? true ::path file}))
         _ (save-from-tx upstream-properties-tx options)]
 
     ;; Return all tx-reports that occurred in this fn as UI needs to know what changed

+ 9 - 2
deps/outliner/src/logseq/outliner/core.cljs

@@ -242,12 +242,19 @@
   ;; Notice: should check `page?` for block from the current db
   (if (ldb/page? (d/entity db (:db/id block)))
     block
-    (let [page-class? (fn [t]
+    (let [tags' (cond
+                  (qualified-keyword? tags)
+                  [(d/entity db tags)]
+                  (every? qualified-keyword? tags)
+                  (map #(d/entity db %) tags)
+                  :else
+                  tags)
+          page-class? (fn [t]
                         (and (map? t) (contains? db-class/page-classes
                                                  (or (:db/ident t)
                                                      (when-let [id (:block/uuid t)]
                                                        (:db/ident (d/entity db [:block/uuid id])))))))
-          page-classes (filter page-class? tags)]
+          page-classes (filter page-class? tags')]
       (if (seq page-classes)
         (-> block
             (update :block/tags

+ 1 - 1
deps/outliner/src/logseq/outliner/db_pipeline.cljs

@@ -12,7 +12,7 @@
   "Modified copy of frontend.worker.pipeline/invoke-hooks that handles new DB graphs but
    doesn't handle updating DB graphs well yet e.g. doesn't handle :block/tx-id"
   [conn tx-report]
-  (when-not (:pipeline-replace? (:tx-meta tx-report))
+  (when-not (:transact-new-graph-refs? (:tx-meta tx-report))
     ;; TODO: Handle block edits with separate :block/refs rebuild as deleting property values is buggy
     (outliner-pipeline/transact-new-db-graph-refs conn tx-report)))
 

+ 1 - 1
deps/outliner/src/logseq/outliner/pipeline.cljs

@@ -147,5 +147,5 @@
   (let [{:keys [blocks]} (ds-report/get-blocks-and-pages tx-report)
         refs-tx-report (when-let [refs-tx (and (seq blocks) (rebuild-block-refs-tx tx-report blocks))]
                          (ldb/transact! conn refs-tx (-> (:tx-meta tx-report)
-                                                         (assoc :pipeline-replace? true))))]
+                                                         (assoc :transact-new-graph-refs? true))))]
     refs-tx-report))

+ 0 - 2
gulpfile.js

@@ -79,7 +79,6 @@ const common = {
         'node_modules/marked/marked.min.js',
         'node_modules/@highlightjs/cdn-assets/highlight.min.js',
         'node_modules/@isomorphic-git/lightning-fs/dist/lightning-fs.min.js',
-        'packages/amplify/dist/amplify.js',
         'packages/ui/dist/ui/ui.js',
         'node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3.wasm',
         'node_modules/react/umd/react.production.min.js',
@@ -130,7 +129,6 @@ const common = {
         'node_modules/prop-types/prop-types.min.js',
         'node_modules/interactjs/dist/interact.min.js',
         'node_modules/photoswipe/dist/umd/*.js',
-        'packages/amplify/dist/amplify.js',
         'packages/ui/dist/ui/ui.js',
         'node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3.wasm',
       ]).pipe(gulp.dest(path.join(outputPath, 'mobile', 'js'))),

+ 10 - 9
libs/src/LSPlugin.core.ts

@@ -573,21 +573,22 @@ class PluginLocal extends EventEmitter<
     const { repo, version } = this._options
     const localRoot = (this._localRoot = this.isWebPlugin ? `${repo || url}/${version}` : safetyPathNormalize(url))
     const logseq: Partial<LSPluginPkgConfig> = pkg.logseq || {}
-    const validateEntry = (main) => main && /\.(js|html)$/.test(main)
+    // const validateEntry = (main) => main && /\.(js|html)$/.test(main)
 
-    // Entry from main
+    // entry from main
     const entry = logseq.entry || logseq.main || pkg.main
 
-    if (validateEntry(entry)) {
-      // Theme has no main
-      this._options.entry = this._resolveResourceFullUrl(entry, localRoot)
-
+    if (logseq.devEntry) {
       // development mode entry
       this._options.devEntry = logseq.devEntry
+      this._options.entry = logseq.devEntry
+    } else {
+      // theme has no main
+      this._options.entry = this._resolveResourceFullUrl(entry, localRoot)
+    }
 
-      if (logseq.mode) {
-        this._options.mode = logseq.mode
-      }
+    if (logseq.mode) {
+      this._options.mode = logseq.mode
     }
 
     const title = logseq.title || pkg.title

+ 1 - 1
libs/src/LSPlugin.ts

@@ -77,7 +77,7 @@ export interface LSPluginPkgConfig {
   /**
    * Alternative entrypoint for development.
    */
-  devEntry: unknown
+  devEntry: string
   /**
    * For legacy themes, do not use.
    */

+ 1 - 1
package.json

@@ -105,7 +105,7 @@
         "tldraw:build": "yarn --cwd packages/tldraw install",
         "amplify:build": "yarn --cwd packages/amplify install",
         "ui:build": "yarn --cwd packages/ui install",
-        "postinstall": "yarn tldraw:build && yarn amplify:build && yarn ui:build"
+        "postinstall": "yarn tldraw:build && yarn ui:build"
     },
     "dependencies": {
         "@capacitor-community/safe-area": "7.0.0-alpha.1",

+ 2 - 0
packages/ui/@/components/ui/alert.tsx

@@ -12,6 +12,8 @@ const alertVariants = cva(
         default: 'bg-background text-foreground',
         destructive:
           'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
+        warning:
+          'border-yellow-600 text-yellow-600 dark:text-yellow-500/70 [&>svg]:text-yellow-600',
       },
     },
     defaultVariants: {

+ 5 - 0
packages/ui/examples/index.html

@@ -9,6 +9,11 @@
     <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
     <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
     <script src="./index.tsx" type="module"></script>
+    <style>
+      a {
+        cursor: pointer;
+      }
+    </style>
 </head>
 <body>
 <div id="app"></div>

+ 49 - 8
packages/ui/examples/index.tsx

@@ -2,22 +2,63 @@ import '../src/index.css'
 import { setupGlobals } from '../src/ui'
 import * as React from 'react'
 import * as ReactDOM from 'react-dom'
+import { init, t } from '../src/amplify/core'
 
 // @ts-ignore
-import { Button } from '@/components/ui/button'
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { LoginForm, ResetPasswordForm, SignupForm, ConfirmWithCodeForm } from '../src/amplify/ui'
+import { AuthFormRootContext } from '../src/amplify/core'
 
 // bootstrap
 setupGlobals()
+init()
 
 function App() {
+  const [errors, setErrors] = React.useState<string | null>(null)
+  const [currentTab, setCurrentTab] = React.useState<'login' | 'reset' | 'signup' | 'confirm-code' | any>('login')
+  const onSessionCallback = React.useCallback((session: any) => {
+    console.log('==>>session callback:', session)
+  }, [])
+
+  React.useEffect(() => {
+    setErrors(null)
+  }, [currentTab])
+
+  let content = null
+  // support passing object with type field
+  let _currentTab = currentTab?.type ? currentTab.type : currentTab
+  let _currentTabProps = currentTab?.props || {}
+
+  switch (_currentTab) {
+    case 'login':
+      content = <LoginForm/>
+      break
+    case 'reset':
+      content = <ResetPasswordForm/>
+      break
+    case 'signup':
+      content = <SignupForm/>
+      break
+    case 'confirm-code':
+      content = <ConfirmWithCodeForm {..._currentTabProps}/>
+      break
+  }
+
   return (
-    <main className={'p-8'}>
-      <h1 className={'text-red-500 mb-8'}>
-        Hello, Logseq UI :)
-      </h1>
-      <Button asChild>
-        <a href={'https://google.com'} target={'_blank'}>go to google.com</a>
-      </Button>
+    <main className={'h-screen flex flex-col justify-center items-center gap-4'}>
+      <AuthFormRootContext.Provider value={{
+        errors, setErrors, setCurrentTab,
+        onSessionCallback
+      }}>
+        <Card className={'sm:w-96'}>
+          <CardHeader>
+            <CardTitle className={'capitalize'}>{t(_currentTab)?.replace('-', ' ')}</CardTitle>
+          </CardHeader>
+          <CardContent>
+            {content}
+          </CardContent>
+        </Card>
+      </AuthFormRootContext.Provider>
     </main>
   )
 }

+ 2 - 0
packages/ui/package.json

@@ -34,6 +34,7 @@
     "@radix-ui/react-toggle-group": "^1.1.7",
     "@radix-ui/react-tooltip": "^1.2.4",
     "@silk-hq/components": "^0.9.10",
+    "aws-amplify": "^6.15.6",
     "class-variance-authority": "^0.7.1",
     "clsx": "^2.0.0",
     "cmdk": "^0.2.0",
@@ -70,6 +71,7 @@
     "@types/prop-types": "^15",
     "@types/react": "17",
     "@types/react-dom": "17",
+    "buffer": "^5.5.0",
     "parcel": "2.8.3",
     "postcss": "^8.4.31",
     "postcss-loader": "^7.3.3",

+ 26 - 0
packages/ui/src/amplify/core.ts

@@ -0,0 +1,26 @@
+import { Amplify } from 'aws-amplify'
+import { createContext, useContext } from 'react'
+import { translate, setNSDicts, setLocale } from '../i18n'
+
+export const AuthFormRootContext = createContext<any>(null)
+export const useAuthFormState = () => {
+  return useContext(AuthFormRootContext)
+}
+
+export function t(key: string, ...args: any) {
+  return translate('amplify', key, ...args)
+}
+
+export function init({ lang, authCognito }: any) {
+  // Load default language
+  setNSDicts('amplify', require('./lang').default)
+  if (lang) setLocale(lang)
+  Amplify.configure({
+    Auth: {
+      Cognito: {
+        ...authCognito,
+        loginWith: { email: true }
+      }
+    }
+  })
+}

+ 8 - 0
packages/ui/src/amplify/index.ts

@@ -0,0 +1,8 @@
+import * as Auth from 'aws-amplify/auth'
+import { init } from './core'
+import { LSAuthenticator } from './ui'
+
+export {
+  init, Auth,
+  LSAuthenticator
+}

+ 114 - 0
packages/ui/src/amplify/lang.ts

@@ -0,0 +1,114 @@
+export default {
+  'en': {
+    'signup': 'Sign Up',
+    'reset-password': 'Reset Password',
+    'confirm-code': 'Confirm Code',
+    'CODE_ON_THE_WAY_TIP': 'Your code is on the way. To log in, enter the code we sent you. It may take a minute to arrive.',
+    'PW_POLICY_TIP': '1. at least 8 characters.\n' +
+      '2. must have lowercase characters.\n' +
+      '3. must have uppercase characters.\n' +
+      '4. must have symbol characters.',
+  },
+  'zh-cn': {
+    'login': '登录',
+    'signup': '注册',
+    'reset-password': '重置密码',
+    'confirm-code': '确认验证码',
+    'PW_POLICY_TIP': '1. 密码长度至少8个字符\n' +
+      '2. 密码必须包含小写字母\n' +
+      '3. 密码必须包含大写字母\n' +
+      '4. 密码必须包含特殊字符',
+    'CODE_ON_THE_WAY_TIP': '验证码已发送。请输入我们发送给您的验证码以登录。可能需要一分钟才能收到。',
+    'Sign in to your account': '登录到您的账户',
+    'Email': '电子邮箱',
+    'Password': '密码',
+    'Sign in': '登录',
+    'Confirm': '确认',
+    'Don\'t have an account?': '还没有账户?',
+    'Sign up': '注册',
+    'or': '或 ',
+    'Forgot your password?': '忘记密码?',
+    'Create account': '创建您的账户',
+    'Username': '用户名',
+    'Confirm Password': '确认密码',
+    'New Password': '新密码',
+    'By signing up, you agree to our': '注册即表示您同意我们的 ',
+    'Terms of Service': '服务条款',
+    'Privacy Policy': '隐私政策',
+    'Already have an account?': '已经有账户?',
+    'Reset password': '重置您的密码',
+    'Enter the code sent to your email': '输入发送到您邮箱的验证码',
+    'Send code': '发送验证码',
+    'Resend code': '重新发送验证码',
+    'Back to login': '返回登录',
+    'Enter your email': '请输入您的电子邮箱'
+  },
+  'zh-hant': {
+    'login': '登入',
+    'signup': '註冊',
+    'reset-password': '重置密碼',
+    'confirm-code': '確認驗證碼',
+    'CODE_ON_THE_WAY_TIP': '驗證碼已發送。請輸入我們發送給您的驗證碼以登入。可能需要一分鐘才能收到。',
+    'PW_POLICY_TIP': '1. 密碼長度至少8個字符\n' +
+      '2. 密碼必須包含小寫字母\n' +
+      '3. 密碼必須包含大寫字母\n' +
+      '4. 密碼必須包含特殊字符',
+    'Sign in to your account': '登入到您的帳戶',
+    'Email': '電子郵箱',
+    'Password': '密碼',
+    'Sign in': '登入',
+    'Confirm': '確認',
+    'Don\'t have an account?': '還沒有帳戶?',
+    'Sign up': '註冊',
+    'or': '或 ',
+    'Forgot your password?': '忘記密碼?',
+    'Create account': '創建您的帳戶',
+    'Username': '用戶名',
+    'Confirm Password': '確認密碼',
+    'New Password': '新密碼',
+    'By signing up, you agree to our': '註冊即表示您同意我們的 ',
+    'Terms of Service': '服務條款',
+    'Privacy Policy': '隱私政策',
+    'Already have an account?': '已經有帳戶?',
+    'Reset password': '重置您的密碼',
+    'Enter the code sent to your email': '輸入發送到您郵箱的驗證碼',
+    'Send code': '發送驗證碼',
+    'Resend code': '重新發送驗證碼',
+    'Back to login': '返回登入',
+    'Enter your email': '請輸入您的電子郵箱'
+  },
+  'ja': {
+    'login': 'ログイン',
+    'signup': 'サインアップ',
+    'reset-password': 'パスワードをリセットする',
+    'confirm-code': 'コードを確認する',
+    'CODE_ON_THE_WAY_TIP': 'コードが送信されました。ログインするには、送信したコードを入力してください。届くまでに1分ほどかかる場合があります。',
+    'PW_POLICY_TIP': '1. パスワードは8文字以上であること。\n' +
+      '2. パスワードには小文字を含める必要があります。\n' +
+      '3. パスワードには大文字を含める必要があります。\n' +
+      '4. パスワードには記号を含める必要があります。',
+    'Sign in to your account': 'アカウントにサインイン',
+    'Email': 'メール',
+    'Password': 'パスワード',
+    'Sign in': 'サインイン',
+    'Confirm': '確認',
+    'Don\'t have an account?': 'アカウントをお持ちでないですか?',
+    'Sign up': 'サインアップ',
+    'or': 'または ',
+    'Forgot your password?': 'パスワードをお忘れですか?',
+    'Create account': 'アカウントを作成する',
+    'Username': 'ユーザー名',
+    'New Password': '新しいパスワード',
+    'Confirm Password': 'パスワードを確認する',
+    'By signing up, you agree to our': 'サインアップすることで、あなたは私たちの ',
+    'Terms of Service': '利用規約',
+    'Privacy Policy': 'プライバシーポリシー',
+    'Already have an account? ': 'すでにアカウントをお持ちですか?',
+    'Reset password': 'パスワードをリセットする',
+    'Enter the code sent to your email': 'メールに送信されたコードを入力してください',
+    'Send code': 'コードを送信',
+    'Resend code': 'コードを再送信',
+    'Back to login': 'ログインに戻る',
+    'Enter your email': 'メールアドレスを入力してください'
+  }
+}

+ 710 - 0
packages/ui/src/amplify/ui.tsx

@@ -0,0 +1,710 @@
+import { Button } from '@/components/ui/button'
+import { Input, InputProps } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+import { cn } from '@/lib/utils'
+import { FormHTMLAttributes, useEffect, useState } from 'react'
+import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
+import { AlertCircleIcon, Loader2Icon, LucideEye, LucideEyeClosed, LucideX } from 'lucide-react'
+import { AuthFormRootContext, t, useAuthFormState } from './core'
+import * as Auth from 'aws-amplify/auth'
+import { Skeleton } from '@/components/ui/skeleton'
+import * as React from 'react'
+
+function ErrorTip({ error, removeError }: {
+  error: string | { variant?: 'warning' | 'destructive', title?: string, message: string | any },
+  removeError?: () => void
+}) {
+  if (!error) return null
+  if (typeof error === 'string') {
+    error = { message: error }
+  }
+
+  return (
+    <Alert variant={error.variant || 'destructive'} className={'relative'}>
+      <AlertCircleIcon size={18}/>
+      {error.title && <AlertTitle>{error.title}</AlertTitle>}
+      <AlertDescription>
+        <p>
+          {(typeof error.message === 'string' ? error.message : JSON.stringify(error.message))?.split('\n')
+            .map((line: string, idx: number) => {
+              return <span key={idx}>{line}<br/></span>
+            })}
+        </p>
+      </AlertDescription>
+      <a className={'close absolute right-0 top-0 opacity-50 hover:opacity-80 p-2'}
+         onClick={() => removeError?.()}>
+        <LucideX size={16}/>
+      </a>
+    </Alert>
+  )
+}
+
+function InputRow(
+  props: InputProps & { label: string | React.ReactNode },
+) {
+  const { errors, setErrors } = useAuthFormState()
+  const { label, type, ...rest } = props
+  const isPassword = type === 'password'
+  const error = props.name && errors?.[props.name]
+  const [localType, setLocalType] = useState<string>(type || 'text')
+  const [showPassword, setShowPassword] = useState<boolean>(false)
+  const removeError = () => {
+    if (props.name && errors?.[props.name]) {
+      const newErrors = { ...errors }
+      delete newErrors[props.name]
+      setErrors(newErrors)
+    }
+  }
+
+  return (
+    <div className={'relative w-full flex flex-col gap-3 pb-1'}>
+      <Label htmlFor={props.id}>
+        {label}
+      </Label>
+      <Input type={localType} {...rest as any} />
+
+      {isPassword && (
+        <a className={'absolute px-2 right-1 top-7 py-3  flex items-center opacity-50 hover:opacity-80 select-none'}
+           onClick={() => {
+             setShowPassword(!showPassword)
+             setLocalType(showPassword ? 'password' : 'text')
+           }}
+        >
+          {showPassword ? <LucideEye size={14}/> : <LucideEyeClosed size={14}/>}
+        </a>
+      )}
+
+      {error &&
+        <div className={'pt-1'}>
+          <ErrorTip error={error} removeError={removeError}/>
+        </div>
+      }
+    </div>
+  )
+}
+
+function FormGroup(props: FormHTMLAttributes<any>) {
+  const { className, children, ...reset } = props
+  return (
+    <form className={cn('relative flex flex-col justify-center items-center gap-4 w-full', className)}
+          {...reset}>
+      {children}
+    </form>
+  )
+}
+
+// 1. Password must be at least 8 characters
+// 2. Password must have lowercase characters
+// 3. Password must have uppercase characters
+// 4. Password must have symbol characters
+function validatePasswordPolicy(password: string) {
+  if (!password ||
+    password.length < 8 ||
+    !/[a-z]/.test(password) ||
+    !/[A-Z]/.test(password) ||
+    !/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?~`]/.test(password)
+  ) {
+    throw new Error(t('PW_POLICY_TIP'))
+  }
+}
+
+function useCountDown() {
+  const [countDownNum, setCountDownNum] = useState<number>(0)
+  const startCountDown = () => {
+    setCountDownNum(60)
+    const interval = setInterval(() => {
+      setCountDownNum((num) => {
+        if (num <= 1) {
+          clearInterval(interval)
+          return 0
+        }
+        return num - 1
+      })
+    }, 1000)
+  }
+
+  useEffect(() => {
+    return () => {
+      setCountDownNum(0)
+    }
+  }, [])
+
+  return { countDownNum, startCountDown, setCountDownNum }
+}
+
+export function LoginForm() {
+  const { setErrors, setCurrentTab, onSessionCallback, userSessionRender } = useAuthFormState()
+  const [loading, setLoading] = useState<boolean>(false)
+  const [sessionUser, setSessionUser] = useState<any>(null)
+  const loadSession = async () => {
+    try {
+      const ret = await Auth.fetchAuthSession()
+      if (!ret?.userSub) throw new Error('no session')
+      const user = await Auth.getCurrentUser()
+      onSessionCallback?.({ ...ret, user })
+      const tokens = ret.tokens
+      setSessionUser({
+        ...user, signInUserSession: {
+          idToken: { jwtToken: tokens?.idToken?.toString() },
+          accessToken: { jwtToken: tokens?.accessToken.toString() },
+          refreshToken: null
+        }
+      })
+      await (new Promise(resolve => setTimeout(resolve, 100)))
+    } catch (e) {
+      console.warn('no current session:', e)
+      setSessionUser(false)
+    }
+  }
+
+  useEffect(() => {
+    // check current auth session
+    loadSession()
+  }, [])
+
+  if (sessionUser === null) {
+    return (
+      <div className="space-y-2">
+        <Skeleton className="h-4 w-[250px]"/>
+        <Skeleton className="h-4 w-[200px]"/>
+      </div>)
+  }
+
+  const signOut = async () => {
+    await Auth.signOut()
+    setSessionUser(false)
+    setErrors(null)
+  }
+
+  if (sessionUser?.username) {
+    if (userSessionRender) {
+      if (typeof userSessionRender === 'function') {
+        return userSessionRender({ sessionUser, signOut })
+      }
+      return userSessionRender
+    }
+
+    return (
+      <div className={'w-full text-center'}>
+        <p className={'mb-4'}>{t('You are already logged in as')} <strong>{sessionUser.username}</strong></p>
+        <Button variant={'secondary'} className={'w-full'} onClick={signOut}>
+          {t('Sign out')}
+        </Button>
+      </div>
+    )
+  }
+
+  return (
+    <FormGroup onSubmit={async (e) => {
+      setErrors(null)
+      e.preventDefault()
+
+      // get submit form input data
+      const formData = new FormData(e.target as HTMLFormElement)
+      const data = Object.fromEntries(formData.entries())
+
+      // sign in logic here
+      try {
+        setLoading(true)
+        const username = (data.email as string)?.trim()
+        const ret = await Auth.signIn({ username, password: data.password as string })
+        const nextStep = ret?.nextStep?.signInStep
+        if (!nextStep) throw new Error(JSON.stringify(ret))
+        switch (nextStep) {
+          case 'CONFIRM_SIGN_UP':
+          case 'CONFIRM_SIGN_IN_WITH_EMAIL_CODE':
+          case 'CONFIRM_SIGN_IN_WITH_TOTP_CODE':
+            setCurrentTab({ type: 'confirm-code', props: { user: { ...ret, username }, nextStep } })
+            return
+          case 'RESET_PASSWORD':
+            setCurrentTab({ type: 'reset-password', props: { user: { ...ret, username }, nextStep } })
+            return
+          case 'DONE':
+            // signed in
+            await loadSession()
+            return
+          default:
+            throw new Error('Unsupported sign-in step: ' + nextStep)
+        }
+      } catch (e) {
+        setErrors({ password: { message: (e as Error).message, title: t('Bad Response.') } })
+        console.error(e)
+      } finally {
+        setLoading(false)
+      }
+    }}>
+      <InputRow id="email" type="text" required={true} name="email" autoFocus={true} label={t('Email')}/>
+      <InputRow id="password" type="password" required={true} name="password" label={t('Password')}/>
+
+      <div className={'w-full'}>
+        <Button type="submit" disabled={loading} className={'w-full'}>
+          {loading && <Loader2Icon className="animate-spin mr-1" size={16}/>}
+          {t('Sign in')}
+        </Button>
+        <p className={'pt-4 text-center'}>
+
+          <span className={'text-sm'}>
+            <span className={'opacity-50'}>{t('Don\'t have an account?')} </span>
+            <a
+              onClick={() => setCurrentTab('signup')}
+              className={'underline opacity-60 hover:opacity-80'}
+            >{t('Sign up')}</a>
+            <br/>
+            <span className={'opacity-50'}>{t('or')} &nbsp;</span>
+          </span>
+
+          <a onClick={() => {
+            setCurrentTab('reset-password')
+          }} className={'text-sm opacity-60 hover:opacity-80 underline'}>
+            {t('Forgot your password?')}
+          </a>
+        </p>
+      </div>
+    </FormGroup>
+  )
+}
+
+export function SignupForm() {
+  const { setCurrentTab, setErrors } = useAuthFormState()
+  const [loading, setLoading] = useState<boolean>(false)
+
+  return (
+    <>
+      <FormGroup onSubmit={async (e) => {
+        setErrors(null)
+        e.preventDefault()
+
+        // get submit form input data
+        const formData = new FormData(e.target as HTMLFormElement)
+        const data = Object.fromEntries(formData.entries()) as any
+
+        try {
+          validatePasswordPolicy(data.password)
+        } catch (e) {
+          setErrors({
+            password: {
+              message: (e as Error).message,
+              title: t('Invalid Password')
+            }
+          })
+          return
+        }
+
+        if (data.password !== data.confirm_password) {
+          setErrors({
+            confirm_password: {
+              message: t('Passwords do not match.'),
+              title: t('Invalid Password')
+            }
+          })
+          return
+        }
+
+        try {
+          setLoading(true)
+          const ret = await Auth.signUp({
+            username: data.username as string,
+            password: data.password as string,
+            options: {
+              userAttributes: {
+                email: data.email as string,
+              }
+            }
+          })
+
+          if (ret.isSignUpComplete) {
+            // TODO: auto sign in
+            if (ret.nextStep?.signUpStep === 'COMPLETE_AUTO_SIGN_IN') {
+              const { nextStep } = await Auth.autoSignIn()
+              if (nextStep.signInStep === 'DONE') {
+                // signed in
+                setCurrentTab('login')
+              }
+            }
+
+            setCurrentTab('login')
+            return
+          } else {
+            if (ret.nextStep?.signUpStep === 'CONFIRM_SIGN_UP') {
+              setCurrentTab({
+                type: 'confirm-code',
+                props: {
+                  user: { ...ret, username: data.username },
+                  nextStep: 'CONFIRM_SIGN_UP'
+                }
+              })
+            }
+            return
+          }
+        } catch (e: any) {
+          console.error(e)
+          const error = { title: t('Bad Response.'), message: (e as Error).message }
+          let k = 'confirm_password'
+          if (e.name === 'UsernameExistsException') {
+            k = 'username'
+          }
+          setErrors({ [k]: error })
+        } finally {
+          setLoading(false)
+        }
+      }}>
+        <InputRow id="email" type="email" name="email" autoFocus={true} required={true} label={t('Email')}/>
+        <InputRow id="username" type="text" name="username" required={true} label={t('Username')}/>
+        <InputRow id="password" type="password" name="password"
+                  required={true}
+                  placeholder={t('Password')}
+                  label={t('Password')}/>
+        <InputRow id="confirm_password" type="password" name="confirm_password"
+                  required={true}
+                  placeholder={t('Confirm Password')}
+                  label={t('Confirm Password')}/>
+        <div className={'-mt-1'}>
+          <span className={'text-sm opacity-50'}>
+            {t('By signing up, you agree to our')}&nbsp;
+            <a href="https://logseq.com/terms"
+               target={'_blank'}
+               className={'underline hover:opacity-100'}>{t('Terms of Service')}</a>
+            {t(' and ')}
+            <a href="https://logseq.com/privacy-policy"
+               target={'_blank'}
+               className={'underline hover:opacity-100'}>{t('Privacy Policy')}</a>.
+          </span>
+        </div>
+        <div className={'w-full'}>
+          <Button type="submit" disabled={loading} className={'w-full'}>
+            {loading && <Loader2Icon className="animate-spin mr-1" size={16}/>}
+            {t('Create account')}
+          </Button>
+        </div>
+
+        <p className={'pt-1 text-center'}>
+          <a onClick={() => setCurrentTab('login')}
+             className={'text-sm opacity-60 hover:opacity-80 underline'}>
+            {t('Back to login')}
+          </a>
+        </p>
+      </FormGroup>
+    </>
+  )
+}
+
+export function ResetPasswordForm() {
+  const [isSentCode, setIsSentCode] = useState<boolean>(false)
+  const [sentUsername, setSentUsername] = useState<string>('')
+  const { setCurrentTab, setErrors } = useAuthFormState()
+  const { countDownNum, startCountDown } = useCountDown()
+  const [loading, setLoading] = useState<boolean>(false)
+
+  useEffect(() => {
+    setErrors({})
+  }, [isSentCode])
+
+  return (
+    <FormGroup
+      autoComplete={'off'}
+      onSubmit={async (e) => {
+        setErrors(null)
+        e.preventDefault()
+
+        // get submit form input data
+        const formData = new FormData(e.target as HTMLFormElement)
+        const data = Object.fromEntries(formData.entries())
+
+        if (!isSentCode) {
+          try {
+            setLoading(true)
+
+            const username = (data.email as string)?.trim()
+            // send reset code
+            const ret = await Auth.resetPassword({ username })
+            console.debug('[Auth] reset pw code sent: ', ret)
+            setSentUsername(username)
+            startCountDown()
+            setIsSentCode(true)
+          } catch (error) {
+            console.error('Error sending reset code:', error)
+            setErrors({ email: { message: (error as Error).message, title: t('Bad Response.') } })
+          } finally {
+            setLoading(false)
+          }
+        } else {
+          // confirm reset password
+          if ((data.password as string)?.length < 8) {
+            setErrors({
+              password: {
+                message: t('Password must be at least 8 characters.'),
+                title: t('Invalid Password')
+              }
+            })
+            return
+          } else if (data.password !== data.confirm_password) {
+            setErrors({
+              confirm_password: {
+                message: t('Passwords do not match.'),
+                title: t('Invalid Password')
+              }
+            })
+            return
+          } else {
+            try {
+              setLoading(true)
+              const ret = await Auth.confirmResetPassword({
+                username: sentUsername,
+                newPassword: data.password as string,
+                confirmationCode: data.code as string
+              })
+
+              console.debug('[Auth] confirm reset pw: ', ret)
+              setCurrentTab('login')
+            } catch (error) {
+              console.error('Error confirming reset password:', error)
+              setErrors({ 'confirm_password': { message: (error as Error).message, title: t('Bad Response.') } })
+            } finally {
+              setLoading(false)
+            }
+          }
+        }
+      }}>
+      {isSentCode ? (
+        <>
+          <div className={'w-full opacity-60 flex justify-end relative h-0 z-[2]'}>
+            {countDownNum > 0 ? (
+              <span className={'text-sm opacity-50 select-none absolute top-3 right-0'}>
+                {countDownNum}s
+              </span>
+            ) : (<a onClick={async () => {
+              startCountDown()
+              try {
+                const ret = await Auth.resetPassword({ username: sentUsername })
+                console.debug('[Auth] reset pw code re-sent: ', ret)
+              } catch (error) {
+                console.error('Error resending reset code:', error)
+                setErrors({ email: { message: (error as Error).message, title: t('Bad Response.') } })
+              } finally {}
+            }} className={'text-sm opacity-70 hover:opacity-90 underline absolute top-3 right-0 select-none'}>
+              {t('Resend code')}
+            </a>)}
+          </div>
+          <InputRow id="code" type="text" name="code" required={true}
+                    placeholder={'123456'}
+                    autoComplete={'off'}
+                    label={t('Enter the code sent to your email')}/>
+
+          <InputRow id="password" type="password" name="password" required={true}
+                    placeholder={t('New Password')}
+                    label={t('New Password')}/>
+
+          <InputRow label={t('Confirm Password')}
+                    id="confirm_password" type="password" name="confirm_password" required={true}
+                    placeholder={t('Confirm Password')}/>
+
+          <div className={'w-full'}>
+            <Button type="submit"
+                    className={'w-full'}
+                    disabled={loading}
+            >
+              {loading && <Loader2Icon className="animate-spin mr-1" size={16}/>}
+              {t('Reset password')}
+            </Button>
+
+            <p className={'pt-4 text-center'}>
+              <a onClick={() => setCurrentTab('login')}
+                 className={'text-sm opacity-60 hover:opacity-80 underline'}>
+                {t('Back to login')}
+              </a>
+            </p>
+          </div>
+        </>
+      ) : (
+        <>
+          <InputRow id="email" type="email" name="email" required={true}
+                    placeholder={'[email protected]'}
+                    autoFocus={true}
+                    label={t('Enter your email')}/>
+          <div className={'w-full'}>
+            <Button type="submit"
+                    className={'w-full'}
+                    disabled={loading}
+            >
+              {loading && <Loader2Icon className="animate-spin mr-1" size={16}/>}
+              {t('Send code')}
+            </Button>
+
+            <p className={'pt-3 text-center'}>
+              <a onClick={() => setCurrentTab('login')}
+                 className={'text-sm opacity-60 hover:opacity-80 underline'}>
+                {t('Back to login')}
+              </a>
+            </p>
+          </div>
+        </>
+      )}
+    </FormGroup>
+  )
+}
+
+export function ConfirmWithCodeForm(
+  props: { user: any, nextStep: any }
+) {
+  const { setCurrentTab, setErrors } = useAuthFormState()
+  const [loading, setLoading] = useState<boolean>(false)
+  const isFromSignIn = props.user?.hasOwnProperty('isSignedIn')
+  const signUpCodeDeliveryDetails = props.user?.nextStep?.codeDeliveryDetails
+  const { countDownNum, startCountDown, setCountDownNum } = useCountDown()
+
+  return (
+    <FormGroup
+      autoComplete={'off'}
+      onSubmit={async (e) => {
+        setErrors(null)
+        e.preventDefault()
+
+        // get submit form input data
+        const formData = new FormData(e.target as HTMLFormElement)
+        const data = Object.fromEntries(formData.entries())
+
+        try {
+          setLoading(true)
+          if (props.nextStep === 'CONFIRM_SIGN_UP') {
+            const ret = await Auth.confirmSignUp({
+              username: props.user?.username,
+              confirmationCode: data.code as string,
+            })
+
+            if (ret.nextStep?.signUpStep === 'COMPLETE_AUTO_SIGN_IN') {
+              const { nextStep } = await Auth.autoSignIn()
+              if (nextStep.signInStep === 'DONE') {
+                // signed in
+                setCurrentTab('login')
+                return
+              }
+            }
+
+            setCurrentTab('login')
+          } else {
+            const ret = await Auth.confirmSignIn({
+              challengeResponse: data.code as string,
+            })
+
+            console.debug('confirmSignIn: ', ret)
+          }
+        } catch (e) {
+          setErrors({ code: { message: (e as Error).message, title: t('Bad Response.') } })
+          console.error(e)
+        } finally {
+          setLoading(false)
+        }
+      }}>
+
+      <p className={'pb-2 opacity-60'}>
+        {isFromSignIn ? t('CODE_ON_THE_WAY_TIP') : (
+          signUpCodeDeliveryDetails &&
+          <span>{t('We have sent a numeric verification code to your email address at')}&nbsp;<code>
+            {signUpCodeDeliveryDetails.destination}.
+          </code></span>
+        )}
+      </p>
+
+      {/*<pre>*/}
+      {/*  {JSON.stringify(props.user, null, 2)}*/}
+      {/*  {JSON.stringify(props.nextStep, null, 2)}*/}
+      {/*</pre>*/}
+
+      <span className={'w-full flex justify-end relative h-0 z-10'}>
+        {countDownNum > 0 ? (
+          <span className={'text-sm opacity-50 select-none absolute -bottom-8'}>
+            {countDownNum}s
+          </span>
+        ) : <a
+          className={'text-sm opacity-50 hover:opacity-80 active:opacity-50 select-none underline absolute -bottom-8'}
+          onClick={async (e) => {
+            e.stopPropagation()
+            // resend code
+            try {
+              startCountDown()
+              if (props.nextStep === 'CONFIRM_SIGN_UP') {
+                const ret = await Auth.resendSignUpCode({
+                  username: props.user?.username
+                })
+
+                console.debug('resendSignUpCode: ', ret)
+              } else {
+                // await Auth.resendSignInCode(props.user)
+              }
+            } catch (e) {
+              setErrors({ code: { message: (e as Error).message, title: t('Bad Response.') } })
+              setCountDownNum(0)
+              console.error(e)
+            } finally {}
+          }}>{t('Resend code')}</a>
+        }
+      </span>
+      <InputRow id="code" type="text" name="code" required={true}
+                placeholder={'123456'}
+                autoComplete={'off'}
+                autoFocus={true}
+                label={t('Enter the code sent to your email')}/>
+
+      <div className={'w-full'}>
+        <Button type="submit"
+                className={'w-full'}
+                disabled={loading}
+        >
+          {loading && <Loader2Icon className="animate-spin mr-1" size={16}/>}
+          {t('Confirm')}
+        </Button>
+
+        <p className={'pt-4 text-center'}>
+          <a onClick={() => setCurrentTab('login')}
+             className={'text-sm opacity-60 hover:opacity-80 underline'}>
+            {t('Back to login')}
+          </a>
+        </p>
+      </div>
+    </FormGroup>
+  )
+}
+
+export function LSAuthenticator(props: any) {
+  const [errors, setErrors] = React.useState<string | null>(null)
+  const [currentTab, setCurrentTab] = React.useState<'login' | 'signup' | 'reset-password' | 'confirm-code' | any>('login')
+  const onSessionCallback = React.useCallback((session: any) => {
+    props.onSessionCallback?.(session)
+  }, [props.onSessionCallback])
+
+  React.useEffect(() => {
+    setErrors(null)
+  }, [currentTab])
+
+  let content = null
+  // support passing object with type field
+  let _currentTab = currentTab?.type ? currentTab.type : currentTab
+  let _currentTabProps = currentTab?.props || {}
+
+  switch (_currentTab) {
+    case 'login':
+      content = <LoginForm/>
+      break
+    case 'signup':
+      content = <SignupForm/>
+      break
+    case 'reset-password':
+      content = <ResetPasswordForm/>
+      break
+    case 'confirm-code':
+      content = <ConfirmWithCodeForm {..._currentTabProps}/>
+      break
+  }
+
+  return (
+    <AuthFormRootContext.Provider value={{
+      errors, setErrors, setCurrentTab,
+      onSessionCallback, userSessionRender: props.children
+    }}>
+      {props.titleRender?.(_currentTab, t(_currentTab))}
+      <div className={'ls-authenticator-content'}>
+        {content}
+      </div>
+    </AuthFormRootContext.Provider>
+  )
+}

+ 40 - 0
packages/ui/src/i18n.ts

@@ -0,0 +1,40 @@
+export type TranslateFn = (
+  locale: string,
+  dicts: Record<string, any>,
+  key: string,
+  ...args: any
+) => string
+
+let _nsDicts = {}
+let _locale: string = 'en'
+let _translate: TranslateFn = (
+  locale: string,
+  dicts: Record<string, any>,
+  key: string,
+  ...args: any
+) => {
+  return dicts[locale]?.[key] || args[0] || key
+}
+
+export function setTranslate(t: TranslateFn) {
+  _translate = t
+}
+
+export function setLocale(locale: string) {
+  _locale = locale
+}
+
+export function setNSDicts(ns: string, dicts: Record<string, string>) {
+  (_nsDicts as any)[ns] = dicts
+}
+
+export const translate = (
+  ns: string,
+  key: string,
+  ...args: any
+) => {
+  const dicts = (_nsDicts as any)[ns] || {}
+  return _translate(
+    _nsDicts?.hasOwnProperty(_locale) ? _locale : 'en',
+    dicts, key, ...args)
+}

+ 11 - 0
packages/ui/src/ui.ts

@@ -93,10 +93,14 @@ import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
 import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
 import * as uniqolor from 'uniqolor'
 import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
+import { setLocale, setTranslate } from './i18n'
+import * as amplifyAuth from './amplify'
 
 declare global {
   var LSUI: any
   var LSUtils: any
+  var LSI18N: any
+  var LSAuth: any
 }
 
 const shadui = {
@@ -199,6 +203,13 @@ function setupGlobals() {
     isDev: process.env.NODE_ENV === 'development',
     uniqolor,
   }
+
+  window.LSI18N = {
+    setTranslate,
+    setLocale,
+  }
+
+  window.LSAuth = amplifyAuth
 }
 
 // setup

File diff suppressed because it is too large
+ 1183 - 0
packages/ui/yarn.lock


+ 2 - 2
resources/css/shui.css

@@ -5,7 +5,7 @@ html * {
 html[data-theme=light] {
   --accent: var(--rx-gray-12-hsl);
   --accent-foreground: var(--rx-gray-02-hsl);
-  --input: var(--rx-gray-03-hsl);
+  --input: var(--rx-gray-05-hsl);
   --secondary: 240 4.8% 95.9%;
 }
 
@@ -23,7 +23,7 @@ html[data-theme=dark] {
   --muted: 0 0% 15%;
   --popover: 0 0% 7%;
   --popover-foreground: 0 0 95%;
-  --input: 0 0% 25%;
+  --input: 0 0% 16%;
 }
 
 html {

+ 0 - 1
resources/index.html

@@ -49,7 +49,6 @@
 <script defer src="./js/react-dom.production.min.js"></script>
 <script defer src="./js/ui.js"></script>
 <script defer src="./js/main.js"></script>
-<script defer src="./js/amplify.js"></script>
 <script defer src="./js/prop-types.min.js"></script>
 <script defer src="./js/tabler-icons-react.min.js"></script>
 <script defer src="./js/tabler.ext.js"></script>

File diff suppressed because it is too large
+ 0 - 0
resources/js/lsplugin.core.js


+ 1 - 2
resources/mobile/index.html

@@ -2,7 +2,7 @@
 <html lang="en" data-color="logseq">
 <head>
     <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no">
     <link href="./css/silkhq.css" rel="stylesheet" type="text/css">
     <link href="./css/style.css" rel="stylesheet" type="text/css">
     <title>Logseq: A privacy-first platform for knowledge management and collaboration</title>
@@ -23,7 +23,6 @@
 <script defer src="./js/tabler-icons-react.min.js"></script>
 <script defer src="./js/tabler.ext.js"></script>
 <script defer src="./js/ui.js"></script>
-<script defer src="./js/amplify.js"></script>
 <script defer src="./js/silkhq.js"></script>
 <script defer src="./js/main.js"></script>
 <script defer src="./js/code-editor.js"></script>

+ 73 - 72
src/main/frontend/components/block.cljs

@@ -2354,88 +2354,89 @@
                (keyword (str "h" heading ".block-title-wrap.as-heading"
                              (when block-ref? ".as-inline")))
                :span.block-title-wrap)]
-    (->elem
-     elem
-     (merge
-      {:data-hl-type (pu/lookup block :logseq.property.pdf/hl-type)}
-      (when (and marker
-                 (not (string/blank? marker))
-                 (not= "nil" marker))
-        {:data-marker (str (string/lower-case marker))})
-      (when bg-color
-        (let [built-in-color? (ui/built-in-color? bg-color)]
-          {:style {:background-color (if built-in-color?
-                                       (str "var(--ls-highlight-color-" bg-color ")")
-                                       bg-color)
-                   :color (when-not built-in-color? "white")}
-           :class "px-1 with-bg-color"})))
+    (when (seq block-ast-title)
+      (->elem
+       elem
+       (merge
+        {:data-hl-type (pu/lookup block :logseq.property.pdf/hl-type)}
+        (when (and marker
+                   (not (string/blank? marker))
+                   (not= "nil" marker))
+          {:data-marker (str (string/lower-case marker))})
+        (when bg-color
+          (let [built-in-color? (ui/built-in-color? bg-color)]
+            {:style {:background-color (if built-in-color?
+                                         (str "var(--ls-highlight-color-" bg-color ")")
+                                         bg-color)
+                     :color (when-not built-in-color? "white")}
+             :class "px-1 with-bg-color"})))
 
      ;; children
-     (let [area?  (= :area (keyword (pu/lookup block :logseq.property.pdf/hl-type)))
-           hl-ref #(when (not (#{:default :whiteboard-shape} block-type))
-                     [:div.prefix-link
-                      {:on-pointer-down
-                       (fn [^js e]
-                         (let [^js target (.-target e)]
-                           (case block-type
+       (let [area?  (= :area (keyword (pu/lookup block :logseq.property.pdf/hl-type)))
+             hl-ref #(when (not (#{:default :whiteboard-shape} block-type))
+                       [:div.prefix-link
+                        {:on-pointer-down
+                         (fn [^js e]
+                           (let [^js target (.-target e)]
+                             (case block-type
                              ;; pdf annotation
-                             :annotation
-                             (if (and area? (.contains (.-classList target) "blank"))
-                               :actions
-                               (do
-                                 (pdf-assets/open-block-ref! block)
-                                 (util/stop e)))
-
-                             :dune)))}
-
-                      [:span.hl-page
-                       [:strong.forbid-edit
-                        (str "P"
-                             (or (pu/lookup block :logseq.property.pdf/hl-page)
-                                 "?"))]]
-
-                      (when (and area?
-                                 (or
+                               :annotation
+                               (if (and area? (.contains (.-classList target) "blank"))
+                                 :actions
+                                 (do
+                                   (pdf-assets/open-block-ref! block)
+                                   (util/stop e)))
+
+                               :dune)))}
+
+                        [:span.hl-page
+                         [:strong.forbid-edit
+                          (str "P"
+                               (or (pu/lookup block :logseq.property.pdf/hl-page)
+                                   "?"))]]
+
+                        (when (and area?
+                                   (or
                                   ;; db graphs
-                                  (:logseq.property.pdf/hl-image block)
+                                    (:logseq.property.pdf/hl-image block)
                                   ;; file graphs
-                                  (get-in block [:block/properties :hl-stamp])))
-                        (pdf-assets/area-display block))])]
-       (remove-nils
-        (concat
-         (when (config/local-file-based-graph? (state/get-current-repo))
-           [(when (and (not pre-block?)
-                       (not html-export?))
-              (file-block/block-checkbox block (str "mr-1 cursor")))
-            (when (and (not pre-block?)
-                       (not html-export?))
-              (file-block/marker-switch block))
-            (file-block/marker-cp block)
-            (file-block/priority-cp block)])
+                                    (get-in block [:block/properties :hl-stamp])))
+                          (pdf-assets/area-display block))])]
+         (remove-nils
+          (concat
+           (when (config/local-file-based-graph? (state/get-current-repo))
+             [(when (and (not pre-block?)
+                         (not html-export?))
+                (file-block/block-checkbox block (str "mr-1 cursor")))
+              (when (and (not pre-block?)
+                         (not html-export?))
+                (file-block/marker-switch block))
+              (file-block/marker-cp block)
+              (file-block/priority-cp block)])
 
          ;; highlight ref block (inline)
-         (when-not area? [(hl-ref)])
+           (when-not area? [(hl-ref)])
 
-         (conj
-          (map-inline config block-ast-title)
-          (when (= block-type :whiteboard-shape) [:span.mr-1 (ui/icon "whiteboard-element" {:extension? true})]))
+           (conj
+            (map-inline config block-ast-title)
+            (when (= block-type :whiteboard-shape) [:span.mr-1 (ui/icon "whiteboard-element" {:extension? true})]))
 
          ;; highlight ref block (area)
-         (when area? [(hl-ref)])
-
-         (when (and (seq block-ast-title) (ldb/class-instance?
-                                           (entity-plus/entity-memoized (db/get-db) :logseq.class/Cards)
-                                           block))
-           [(ui/tooltip
-             (shui/button
-              {:variant :ghost
-               :size :sm
-               :class "ml-2 !px-1 !h-5 text-xs text-muted-foreground"
-               :on-click (fn [e]
-                           (util/stop e)
-                           (state/pub-event! [:modal/show-cards (:db/id block)]))}
-              "Practice")
-             [:div "Practice cards"])])))))))
+           (when area? [(hl-ref)])
+
+           (when (and (seq block-ast-title) (ldb/class-instance?
+                                             (entity-plus/entity-memoized (db/get-db) :logseq.class/Cards)
+                                             block))
+             [(ui/tooltip
+               (shui/button
+                {:variant :ghost
+                 :size :sm
+                 :class "ml-2 !px-1 !h-5 text-xs text-muted-foreground"
+                 :on-click (fn [e]
+                             (util/stop e)
+                             (state/pub-event! [:modal/show-cards (:db/id block)]))}
+                "Practice")
+               [:div "Practice cards"])]))))))))
 
 (rum/defc block-title-aux
   [config block {:keys [query? *show-query?]}]

+ 1 - 1
src/main/frontend/components/journal.cljs

@@ -35,7 +35,7 @@
       [:div#journals
        (ui/virtualized-list
         {:custom-scroll-parent (util/app-scroll-container-node)
-         :increase-viewport-by {:top 300 :bottom 300}
+         :increase-viewport-by {:top 100 :bottom 100}
          :compute-item-key (fn [idx]
                              (let [id (util/nth-safe data idx)]
                                (str "journal-" id)))

+ 5 - 3
src/main/frontend/components/query.cljs

@@ -61,9 +61,11 @@
            (util/hiccup-keywordize result))
 
          (and db-graph? (not (:built-in-query? config)))
-         (when-let [query (:logseq.property/query current-block)]
-           (when-not (string/blank? (:block/title query))
-             (query-view/query-result (assoc config :id (str (:block/uuid current-block)))
+         (when-let [query-block (:logseq.property/query current-block)]
+           (when-not (string/blank? (:block/title query-block))
+             (query-view/query-result (assoc config
+                                             :id (str (:block/uuid current-block))
+                                             :query query)
                                       current-block result)))
 
          (and (not db-graph?)

+ 1 - 0
src/main/frontend/components/query/view.cljs

@@ -37,4 +37,5 @@
        :view-feature-type :query-result
        :data ids
        :set-data! set-data!
+       :query (:query config)
        :query-entity-ids ids})]))

+ 5 - 2
src/main/frontend/components/theme.cljs

@@ -1,5 +1,6 @@
 (ns frontend.components.theme
-  (:require [electron.ipc :as ipc]
+  (:require [clojure.string :as string]
+            [electron.ipc :as ipc]
             [frontend.components.settings :as settings]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
@@ -68,7 +69,9 @@
 
     (hooks/use-effect!
      #(let [doc js/document.documentElement]
-        (.setAttribute doc "lang" preferred-language)))
+        (.setAttribute doc "lang" preferred-language)
+        (some-> preferred-language (string/lower-case) (js/LSI18N.setLocale)))
+     [preferred-language])
 
     (hooks/use-effect!
      #(js/setTimeout

+ 0 - 56
src/main/frontend/components/user/config.js

@@ -1,56 +0,0 @@
-import {Amplify} from '@aws-amplify/core';
-
-Amplify.configure({
-    Auth: {
-        // REQUIRED only for Federated Authentication - Amazon Cognito Identity Pool ID
-        // identityPoolId: 'XX-XXXX-X:XXXXXXXX-XXXX-1234-abcd-1234567890ab',
-
-        // REQUIRED - Amazon Cognito Region
-        region: 'us-east-1',
-
-        // OPTIONAL - Amazon Cognito Federated Identity Pool Region
-        // Required only if it's different from Amazon Cognito Region
-        // identityPoolRegion: 'XX-XXXX-X',
-
-        // OPTIONAL - Amazon Cognito User Pool ID
-        userPoolId: 'us-east-1_ldvDmC9Fe',
-
-        // OPTIONAL - Amazon Cognito Web Client ID (26-char alphanumeric string)
-        userPoolWebClientId: '41m82unjghlea984vjpk887qcr',
-
-        // OPTIONAL - Enforce user authentication prior to accessing AWS resources or not
-        // mandatorySignIn: false,
-
-        // OPTIONAL - This is used when autoSignIn is enabled for Auth.signUp
-        // 'code' is used for Auth.confirmSignUp, 'link' is used for email link verification
-        // signUpVerificationMethod: 'code', // 'code' | 'link'
-
-        // OPTIONAL - Configuration for cookie storage
-        // Note: if the secure flag is set to true, then the cookie transmission requires a secure protocol
-        cookieStorage: {
-            domain: "localhost",
-            path: "/",
-            expires: 365,
-            sameSite: "strict",
-            secure: true,
-        },
-
-        // OPTIONAL - customized storage object
-        // storage: MyStorage,
-
-        // OPTIONAL - Manually set the authentication flow type. Default is 'USER_SRP_AUTH'
-        authenticationFlowType: 'USER_SRP_AUTH',
-
-        //
-        // // OPTIONAL - Manually set key value pairs that can be passed to Cognito Lambda Triggers
-        // clientMetadata: {myCustomKey: 'myCustomValue'},
-        //
-        // // OPTIONAL - Hosted UI configuration
-        // oauth: {
-        //     domain: 'your_cognito_domain',
-        //     scope: ['phone', 'email', 'profile', 'openid', 'aws.cognito.signin.user.admin'],
-        //     redirectSignIn: 'http://localhost:3000/',
-        //     redirectSignOut: 'http://localhost:3000/',
-        //     responseType: 'code' // or 'token', note that REFRESH token will only be generated when the responseType is code
-    }
-});

+ 34 - 53
src/main/frontend/components/user/login.cljs

@@ -1,7 +1,7 @@
 (ns frontend.components.user.login
   (:require [cljs-bean.core :as bean]
             [clojure.string :as string]
-            [dommy.core :refer-macros [sel]]
+            [dommy.core :refer-macros [sel by-id]]
             [frontend.config :as config]
             [frontend.handler.notification :as notification]
             [frontend.handler.route :as route-handler]
@@ -17,24 +17,24 @@
 
 (defn sign-out!
   []
-  (try (.signOut js/LSAmplify.Auth)
+  (try (.signOut js/LSAuth.Auth)
        (catch :default e (js/console.warn e))))
 
-(defn- setup-configure!
+(defn setup-configure!
   []
   #_:clj-kondo/ignore
-  (def setupAuthConfigure! (.-setupAuthConfigure js/LSAmplify))
+  (defn setupAuthConfigure! [config]
+    (.init js/LSAuth (bean/->js {:authCognito (merge config {:loginWith {:email true}})})))
   #_:clj-kondo/ignore
   (def LSAuthenticator
-    (adapt-class (.-LSAuthenticator js/LSAmplify)))
+    (adapt-class (.-LSAuthenticator js/LSAuth)))
 
-  (.setLanguage js/LSAmplify.I18n (or (:preferred-language @state/state) "en"))
   (setupAuthConfigure!
-   #js {:region              config/REGION,
-        :userPoolId          config/USER-POOL-ID,
-        :userPoolWebClientId config/COGNITO-CLIENT-ID,
-        :identityPoolId      config/IDENTITY-POOL-ID,
-        :oauthDomain         config/OAUTH-DOMAIN}))
+   {:region config/REGION,
+    :userPoolId config/USER-POOL-ID,
+    :userPoolClientId config/COGNITO-CLIENT-ID,
+    :identityPoolId config/IDENTITY-POOL-ID,
+    :oauthDomain config/OAUTH-DOMAIN}))
 
 (rum/defc user-pane
   [_sign-out! user]
@@ -55,45 +55,24 @@
 
 (rum/defc page-impl
   []
-  (let [[ready?, set-ready?] (rum/use-state false)
-        [tab, set-tab!] (rum/use-state :login)
-        *ref-el (rum/use-ref nil)]
-
-    (hooks/use-effect!
-     (fn [] (setup-configure!)
-       (set-ready? true)
-       (js/setTimeout
-        (fn []
-          (when-let [^js el (some-> (rum/deref *ref-el) (.querySelector ".amplify-tabs"))]
-            (let [btn1 (.querySelector el "button")]
-              (.addEventListener el "pointerdown"
-                                 (fn [^js e]
-                                   (if (= (.-target e) btn1)
-                                     (set-tab! :login)
-                                     (set-tab! :create-account)))))))))
-     [])
-
-    (hooks/use-effect!
-     (fn []
-       (when-let [^js el (rum/deref *ref-el)]
-         (js/setTimeout
-          #(some-> (.querySelector el (str "input[name=" (if (= tab :login) "username" "email") "]"))
-                   (.focus)) 100)))
-     [tab])
-
+  (let [*ref-el (rum/use-ref nil)
+        [tab set-tab!] (rum/use-state nil)]
     [:div.cp__user-login
-     {:ref *ref-el}
-     (when ready?
-       (LSAuthenticator
-        {:termsLink "https://blog.logseq.com/terms/"}
-        (fn [^js op]
-          (let [sign-out!'      (.-signOut op)
-                ^js user-proxy (.-user op)
-                ^js user       (try (js/JSON.parse (js/JSON.stringify user-proxy))
-                                    (catch js/Error e
-                                      (js/console.error "Error: Amplify user payload:" e)))
-                user'          (bean/->clj user)]
-            (user-pane sign-out!' user')))))]))
+     {:ref *ref-el
+      :id (str "user-auth-" tab)}
+     (LSAuthenticator
+      {:titleRender (fn [key title]
+                      (set-tab! key)
+                      (shui/card-header
+                       {:class "px-0"}
+                       (shui/card-title
+                        {:class "capitalize"}
+                        (string/replace title "-" " "))))
+       :onSessionCallback #()}
+      (fn [^js op]
+        (let [sign-out!' (.-signOut op)
+              user' (bean/->clj (.-sessionUser op))]
+          (user-pane sign-out!' user'))))]))
 
 (rum/defcs modal-inner <
   shortcut/disable-all-shortcuts
@@ -109,7 +88,9 @@
   (shui/dialog-open!
    (fn [_close] (modal-inner))
    {:label "user-login"
-    :content-props {:onPointerDownOutside #(let [inputs (sel "form[data-amplify-form] input:not([type=checkbox])")
-                                                 inputs (some->> inputs (map (fn [^js e] (.-value e))) (remove string/blank?))]
-                                             (when (seq inputs)
-                                               (.preventDefault %)))}}))
+    :content-props {:onPointerDownOutside #(if (by-id "#user-auth-login")
+                                             (let [inputs (sel ".ls-authenticator-content form input:not([type=checkbox])")
+                                                   inputs (some->> inputs (map (fn [^js e] (.-value e))) (remove string/blank?))]
+                                               (when (seq inputs)
+                                                 (.preventDefault %)))
+                                             (.preventDefault %))}}))

+ 17 - 105
src/main/frontend/components/user/login.css

@@ -1,126 +1,38 @@
 .cp__user-login {
-  [data-amplify-authenticator] [data-amplify-router] {
-    --amplify-components-authenticator-router-background-color: var(--ls-primary-background-color);
-    --amplify-components-field-label-color: var(--ls-primary-text-color);
-    --amplify-components-authenticator-router-border-color: var(--ls-border-color);
-    --amplify-components-tabs-item-color: var(--ls-primary-text-color);
-    --amplify-components-tabs-item-active-color: var(--ls-primary-text-color);
-    --amplify-components-tabs-item-hover-color: var(--ls-primary-text-color);
-    --amplify-components-tabs-item-active-border-color: var(--ls-tertiary-background-color);
-    --amplify-components-tabs-border-width: 0;
-    --amplify-components-authenticator-state-inactive-background-color: var(--ls-tertiary-background-color);
-    --amplify-components-tabs-item-active-background-color: var(--ls-primary-background-color);
-    --amplify-components-button-border-color: var(--ls-border-color);
-    --amplify-components-textfield-border-color: var(--ls-border-color);
-    --amplify-components-button-primary-background-color: var(--color-indigo-600);
-    --amplify-components-text-color: var(--ls-primary-text-color);
-    --amplify-components-button-hover-background-color: var(--ls-primary-background-color);
-    --amplify-components-button-border-width: 0;
-    --amplify-internal-button-loading-background-color: var(--ls-header-button-background);
-    --amplify-components-authenticator-router-border-width: 1px;
-    --amplify-components-button-color: var(--ls-primary-text-color);
-    --amplify-components-divider-label-background-color: var(--ls-primary-background-color);
-    --amplify-components-divider-label-color: var(--ls-primary-text-color);
-    --amplify-components-heading-color: var(--ls-primary-text-color);
-    --amplify-components-button-link-hover-background-color: transparent;
-    --amplify-components-button-link-active-background-color: transparent;
-    --amplify-components-textfield-color: var(--ls-primary-text-color);
-    --amplify-components-checkbox-icon-background-color: var(--color-indigo-600);
+  span.opacity-50, a.opacity-60 {
+    @apply opacity-80;
   }
 
-  [data-amplify-authenticator] [data-amplify-router] {
-    @apply overflow-hidden rounded-[6px] shadow-2xl;
+  p {
+    @apply text-[inherit];
   }
 
-  [data-amplify-authenticator] [data-amplify-container] {
-    place-self: unset;
-  }
+  .ui__alert {
+    @apply bg-red-300 dark:border-red-800 dark:bg-red-800/90 dark:text-red-200;
 
-  [data-amplify-authenticator] [data-amplify-form] {
-    @apply px-4 py-2;
+    svg {
+      @apply dark:text-red-200;
+    }
 
-    @screen sm {
-      @apply px-6 py-4;
+    &-description {
+      @apply -mb-3;
     }
   }
-}
 
-@media (min-width: 30rem) {
-  [data-amplify-authenticator] [data-amplify-container] {
-    width: 100%;
-  }
 }
 
 .ui__dialog-content[label=user-login] {
-  @apply flex items-center top-0 p-0 border-none w-auto;
+  @apply flex items-center top-0 px-6 pt-0 w-auto;
+
+  .ui__card-header {
+    @apply pb-7;
+  }
 
   .ui__dialog-main-content {
-    @apply p-0 min-w-fit relative max-w-[600px] sm:max-w-[90vw] sm:w-[500px];
+    @apply p-0 w-[70vw] relative max-w-[500px] sm:w-[440px];
   }
 
   .ui__modal-close-wrap {
     @apply z-10 top-[4px];
   }
 }
-
-.cp__user {
-  &-login {
-    ::placeholder {
-      color: var(--ls-primary-text-color);
-      opacity: .3;
-    }
-
-    [data-indicator-position=top] > .amplify-tabs-item {
-      margin-top: 0;
-    }
-
-    .amplify-tabs-item {
-      transition: none;
-
-      &:focus {
-        color: var(--ls-primary-text-color);
-      }
-
-      &:hover {
-        opacity: .9;
-      }
-    }
-
-    .amplify-field-group {
-      @apply relative;
-
-      .amplify-button {
-        color: var(--ls-primary-text-color);
-
-        &:active, &:hover, &:focus {
-          background-color: transparent;
-        }
-      }
-    }
-
-    .amplify-field-group__outer-end {
-      @apply absolute right-0 top-0 bottom-0;
-    }
-
-    .amplify-input {
-      border-radius: 4px !important;
-    }
-
-    .amplify-checkboxfield {
-      @apply text-sm;
-
-      .amplify-field__error-message {
-        color: var(--ls-primary-text-color);
-        opacity: .4;
-      }
-    }
-
-    .amplify-text--error {
-      color: var(--ls-error-text-color);
-    }
-  }
-}
-
-.federated-sign-in-container {
-  display: none;
-}

+ 34 - 23
src/main/frontend/components/views.cljs

@@ -333,7 +333,7 @@
             (ui/icon "layout-sidebar-right"))]]))]))
 
 (defn build-columns
-  [config properties & {:keys [with-object-name? with-id? add-tags-column?]
+  [config properties & {:keys [with-object-name? with-id? add-tags-column? advanced-query?]
                         :or {with-object-name? true
                              with-id? true
                              add-tags-column? true}}]
@@ -341,7 +341,8 @@
                      (if (or (some #(= (:db/ident %) :block/tags) properties) (not add-tags-column?))
                        properties
                        (conj properties (db/entity :block/tags)))
-                     (remove nil?))]
+                     (remove nil?))
+        property-keys (set (map :db/ident properties'))]
     (->> (concat
           [{:id :select
             :name "Select"
@@ -372,7 +373,7 @@
              (when-let [ident (or (:db/ident property) (:id property))]
                ;; Hide properties that shouldn't ever be editable or that do not display well in a table
                (when-not (or (contains? #{:logseq.property/built-in? :logseq.property.asset/checksum :logseq.property.class/properties
-                                          :block/created-at :block/updated-at :block/order :block/collapsed?
+                                          :block/created-at :block/updated-at :block/collapsed?
                                           :logseq.property/created-from-property}
                                         ident)
                              (and with-object-name? (= :block/title ident))
@@ -403,16 +404,20 @@
                     :type (:type property)}))))
            properties')
 
-          [{:id :block/created-at
-            :name (t :page/created-at)
-            :type :datetime
-            :header header-cp
-            :cell timestamp-cell-cp}
-           {:id :block/updated-at
-            :name (t :page/updated-at)
-            :type :datetime
-            :header header-cp
-            :cell timestamp-cell-cp}])
+          [(when (or (not advanced-query?)
+                     (and advanced-query? (property-keys :block/created-at)))
+             {:id :block/created-at
+              :name (t :page/created-at)
+              :type :datetime
+              :header header-cp
+              :cell timestamp-cell-cp})
+           (when (or (not advanced-query?)
+                     (and advanced-query? (property-keys :block/updated-at)))
+             {:id :block/updated-at
+              :name (t :page/updated-at)
+              :type :datetime
+              :header header-cp
+              :cell timestamp-cell-cp})])
          (remove nil?))))
 
 (defn- sort-columns
@@ -2161,15 +2166,19 @@
   (state/<invoke-db-worker :thread-api/get-view-data (state/get-current-repo) (:db/id view) opts))
 
 (defn- get-query-columns
-  [config properties]
-  (->> properties
-       (map db/entity)
-       (remove ldb/hidden?)
-       (ldb/sort-by-order)
-       ((fn [cs] (build-columns config cs {:add-tags-column? false})))))
+  [config view-entity properties]
+  (let [advanced-query? (->> (:logseq.property/query view-entity)
+                             :logseq.property.node/display-type
+                             (= :code))]
+    (->> properties
+         (remove #{:logseq.property.embedding/hnsw-label-updated-at})
+         (map db/entity)
+         (ldb/sort-by-order)
+         ((fn [cs] (build-columns config cs {:add-tags-column? false
+                                             :advanced-query? advanced-query?}))))))
 
 (defn- load-view-data-aux
-  [config view-entity view-parent {:keys [query? query-entity-ids sorting filters input
+  [config view-entity view-parent {:keys [query? query query-entity-ids sorting filters input
                                           view-feature-type group-by-property-ident
                                           set-data! set-ref-pages-count! set-ref-matched-children-ids! set-properties! set-loading!]}]
   (c.m/run-task*
@@ -2192,7 +2201,8 @@
                           :filters filters
                           :sorting sorting}
                           query?
-                          (assoc :query-entity-ids query-entity-ids))
+                          (assoc :query-entity-ids query-entity-ids
+                                 :query query))
                    {:keys [data ref-pages-count ref-matched-children-ids properties]}
                    (c.m/<? (<load-view-data view-entity opts))]
                (set-data! data)
@@ -2207,7 +2217,7 @@
                    (reset! *objects-ready? true)))))))))))
 
 (rum/defc view-aux
-  [view-entity {:keys [config view-parent view-feature-type data query-entity-ids set-view-entity!] :as option}]
+  [view-entity {:keys [config view-parent view-feature-type data query-entity-ids query set-view-entity!] :as option}]
   (let [[input set-input!] (hooks/use-state "")
         [properties set-properties!] (hooks/use-state nil)
         db-based? (config/db-based-graph?)
@@ -2234,7 +2244,7 @@
         view-filters (:logseq.property.table/filters view-entity)
         [filters set-filters!] (rum/use-state (or view-filters {}))
         query? (= view-feature-type :query-result)
-        option (if query? (assoc option :columns (get-query-columns config properties)) option)
+        option (if query? (assoc option :columns (get-query-columns config view-entity properties)) option)
         [loading? set-loading!] (hooks/use-state (not query?))
         [data set-data!] (hooks/use-state data)
         [ref-pages-count set-ref-pages-count!] (hooks/use-state nil)
@@ -2242,6 +2252,7 @@
         load-view-data (fn load-view-data []
                          (load-view-data-aux config view-entity view-parent
                                              {:query? query?
+                                              :query query
                                               :query-entity-ids query-entity-ids
                                               :sorting sorting :filters filters :input input
                                               :view-feature-type view-feature-type :group-by-property-ident group-by-property-ident

+ 29 - 7
src/main/frontend/db/async/util.cljs

@@ -1,23 +1,45 @@
 (ns frontend.db.async.util
   "Async util helper"
-  (:require [datascript.core :as d]
+  (:require [clojure.walk :as walk]
+            [datascript.core :as d]
             [frontend.db.conn :as db-conn]
             [frontend.state :as state]
             [promesa.core :as p]))
 
+(defn- transform-pull-query
+  [query]
+  (if (= :find (first query))
+    (walk/postwalk
+     (fn [f]
+       (cond
+         (and (keyword? f) (= f :block/content))
+         :block/title
+
+         (and (list? f) (= 'pull (first f)) (vector? (last f)) (not-any? #{:db/id} f))
+         (list 'pull (second f) (conj (last f) :db/id))
+
+         :else
+         f))
+     query)
+    query))
+
 (defn <q
-  [graph {:keys [transact-db?]
+  [graph {:keys [transact-db? advanced-query?]
           :or {transact-db? true}
           :as opts} & inputs]
   (assert (not-any? fn? inputs) "Async query inputs can't include fns because fn can't be serialized")
   (let [*async-queries (:db/async-queries @state/state)
-        async-requested? (get @*async-queries [inputs opts])]
+        async-requested? (get @*async-queries [inputs opts])
+        inputs' (if advanced-query?
+                  (cons (transform-pull-query (first inputs))
+                        (rest inputs))
+                  inputs)]
     (if (and async-requested? transact-db?)
       (p/promise
        (let [db (db-conn/get-db graph)]
-         (apply d/q (first inputs) db (rest inputs))))
-      (p/let [result (state/<invoke-db-worker :thread-api/q graph inputs)]
-        (swap! *async-queries assoc [inputs opts] true)
+         (apply d/q (first inputs') db (rest inputs'))))
+      (p/let [result (state/<invoke-db-worker :thread-api/q graph inputs')]
+        (swap! *async-queries assoc [inputs' opts] true)
         (when result
           (when (and transact-db? (seq result) (coll? result))
             (when-let [conn (db-conn/get-db graph false)]
@@ -33,7 +55,7 @@
                     (catch :default e
                       (js/console.error "<q failed with:" e)
                       nil))
-                  (js/console.log "<q skipped tx for inputs:" inputs)))))
+                  (js/console.log "<q skipped tx for inputs:" inputs')))))
           result)))))
 
 (defn <pull

+ 2 - 1
src/main/frontend/db/react.cljs

@@ -99,7 +99,8 @@
         q (if util/node-test?
             (fn [query inputs] (apply d/q query db inputs))
             (fn [query inputs]
-              (let [q-f #(apply db-async-util/<q repo {:transact-db? false} (cons query inputs))]
+              (let [q-f #(apply db-async-util/<q repo {:transact-db? false
+                                                       :advanced-query? true} (cons query inputs))]
                 (if built-in-query?
                   ;; delay built-in-queries to not block journal rendering
                   (p/let [_ (p/delay 100)]

+ 1 - 1
src/main/frontend/fs.cljs

@@ -51,7 +51,7 @@
       node-backend
 
       :else
-      nil)))
+      (throw (ex-info "failed to get fs backend" {:dir dir :repo repo :rpath rpath})))))
 
 (defn mkdir!
   [dir]

+ 2 - 0
src/main/frontend/handler.cljs

@@ -8,6 +8,7 @@
             [frontend.components.content :as cp-content]
             [frontend.components.editor :as editor]
             [frontend.components.page :as page]
+            [frontend.components.user.login :as user.login]
             [frontend.components.reference :as reference]
             [frontend.components.whiteboard :as whiteboard]
             [frontend.config :as config]
@@ -143,6 +144,7 @@
 
   (register-components-fns!)
   (user-handler/restore-tokens-from-localstorage)
+  (user.login/setup-configure!)
   (state/set-db-restoring! true)
   (when (util/electron?)
     (el/listen!))

+ 15 - 0
src/main/frontend/handler/user.cljs

@@ -108,6 +108,20 @@
       (when (string/starts-with? key prefix)
         (js/localStorage.removeItem key)))))
 
+(defn auto-fill-refresh-token-from-cognito!
+  []
+  (let [prefix "CognitoIdentityServiceProvider."
+        refresh-token-key (some #(when (string/starts-with? % prefix)
+                                   (when (string/ends-with? % "refreshToken")
+                                     %))
+                                (js/Object.keys js/localStorage))]
+    (when refresh-token-key
+      (let [refresh-token (js/localStorage.getItem refresh-token-key)]
+        (when (and refresh-token (not= refresh-token "undefined"))
+          (state/set-auth-refresh-token refresh-token)
+          (js/localStorage.setItem "refresh-token" refresh-token)))))
+  )
+
 (defn- clear-tokens
   ([]
    (state/set-auth-id-token nil)
@@ -206,6 +220,7 @@
    (:jwtToken (:idToken session))
    (:jwtToken (:accessToken session))
    (:token (:refreshToken session)))
+  (auto-fill-refresh-token-from-cognito!)
   (state/pub-event! [:user/fetch-info-and-graphs]))
 
 (defn ^:export login-with-username-password-e2e

+ 22 - 7
src/main/frontend/mobile/index.css

@@ -2,13 +2,10 @@
   @apply fixed bottom-[100px] h-[70px] p-1.5 rounded-md overflow-y-hidden overflow-x-auto
   bg-[var(--ls-secondary-background-color)] z-[99999];
 
-  box-shadow:
-      /* bottom = shadow-lg */
-      0 10px 15px -3px rgba(0,0,0,0.1),
-      0 4px 6px -4px rgba(0,0,0,0.1),
-      /* top = lighter (closer to shadow-md) */
-      0 -6px 10px -4px rgba(0,0,0,0.08),
-      0 -2px 4px -4px rgba(0,0,0,0.08);
+  box-shadow: /* bottom = shadow-lg */ 0 10px 15px -3px rgba(0, 0, 0, 0.1),
+  0 4px 6px -4px rgba(0, 0, 0, 0.1),
+    /* top = lighter (closer to shadow-md) */ 0 -6px 10px -4px rgba(0, 0, 0, 0.08),
+  0 -2px 4px -4px rgba(0, 0, 0, 0.08);
 
   .action-bar-commands {
     @apply relative flex w-full;
@@ -144,3 +141,21 @@ html.is-zoomed-native-ios {
     }
   }
 }
+
+html.has-mobile-keyboard {
+  .ui__dialog-overlay {
+    &:has(.app-login-modal) {
+      @apply overflow-y-auto;
+    }
+  }
+
+  .ui__dialog-content {
+    &.app-login-modal {
+      margin-bottom: 460px;
+    }
+  }
+}
+
+.ui__dialog-content[label=user-login] {
+  @apply rounded-lg pb-3;
+}

+ 8 - 7
src/main/frontend/worker/db/migrate.cljs

@@ -396,14 +396,15 @@
             :block/name (common-util/page-name-sanity-lc (:block/title page))})))
      pages)))
 
-(defn remove-block-path-refs-datoms
+(defn remove-block-path-refs
   [db]
   (when (d/entity db :block/path-refs)
-    (->> (d/datoms db :avet :block/path-refs)
-         (map :e)
-         (distinct)
-         (map (fn [id]
-                [:db/retract id :block/path-refs])))))
+    (let [remove-datoms (->> (d/datoms db :avet :block/path-refs)
+                             (map :e)
+                             (distinct)
+                             (mapv (fn [id]
+                                     [:db/retract id :block/path-refs])))]
+      (conj remove-datoms [:db/retractEntity :block/path-refs]))))
 
 (defn- remove-position-property-from-url-properties
   [db]
@@ -428,7 +429,7 @@
    ["65.8" {:fix add-missing-page-name}]
    ["65.9" {:properties [:logseq.property.embedding/hnsw-label-updated-at]}]
    ["65.10" {:properties [:block/journal-day :logseq.property.view/sort-groups-by-property :logseq.property.view/sort-groups-desc?]}]
-   ["65.11" {:fix remove-block-path-refs-datoms}]
+   ["65.11" {:fix remove-block-path-refs}]
    ["65.12" {:fix remove-position-property-from-url-properties}]])
 
 (let [[major minor] (last (sort (map (comp (juxt :major :minor) db-schema/parse-schema-version first)

+ 15 - 8
src/main/frontend/worker/db/validate.cljs

@@ -23,11 +23,10 @@
                            (and (:block/tx-id entity) (nil? (:block/title entity)))
                            [[:db/retractEntity (:db/id entity)]]
                            (= :block/path-refs (:db/ident entity))
-                           (concat [[:db/retractEntity (:db/id entity)]]
-                                   (try
-                                     (db-migrate/remove-block-path-refs-datoms db)
-                                     (catch :default _e
-                                       nil)))
+                           (try
+                             (db-migrate/remove-block-path-refs db)
+                             (catch :default _e
+                               nil))
                            (and (= dispatch-key :block) (nil? (:block/title entity)))
                            [[:db/retractEntity (:db/id entity)]]
 
@@ -114,9 +113,17 @@
                                     (->> (d/datoms db :avet (:v d))
                                          (mapcat (fn [d]
                                                    [[:db/retract (:e d) (:a d) (:v d)]
-                                                    [:db/add (:e d) new-db-ident (:v d)]])))))))))]
-    (when (seq tx-data)
-      (ldb/transact! conn tx-data))))
+                                                    [:db/add (:e d) new-db-ident (:v d)]])))))))))
+        ;; FIXME: :logseq.property.table/hidden-columns should be :property type to avoid issues like this
+        hidden-columns-tx-data (->> (d/datoms db :avet :logseq.property.table/hidden-columns)
+                                    (mapcat (fn [d]
+                                              (when (re-find #"^(\d)" (name (:v d)))
+                                                (let [new-value (keyword (namespace (:v d)) (string/replace-first (name (:v d)) #"^(\d)" "NUM-$1"))]
+                                                  [[:db/retract (:e d) (:a d) (:v d)]
+                                                   [:db/add (:e d) (:a d) new-value]])))))
+        tx-data' (concat tx-data hidden-columns-tx-data)]
+    (when (seq tx-data')
+      (ldb/transact! conn tx-data'))))
 
 (defn validate-db
   [conn]

+ 10 - 9
src/main/frontend/worker/pipeline.cljs

@@ -431,14 +431,15 @@
 
 (defn invoke-hooks
   [repo conn {:keys [tx-meta] :as tx-report} context]
-  (let [{:keys [from-disk? new-graph?]} tx-meta]
-    (cond
-      (or from-disk? new-graph?)
-      {:tx-report tx-report}
+  (let [{:keys [from-disk? new-graph? transact-new-graph-refs?]} tx-meta]
+    (when-not transact-new-graph-refs?
+      (cond
+        (or from-disk? new-graph?)
+        {:tx-report tx-report}
 
-      (or (::gp-exporter/new-graph? tx-meta)
-          (and (::sqlite-export/imported-data? tx-meta) (:import-db? tx-meta)))
-      (invoke-hooks-for-imported-graph conn tx-report)
+        (or (::gp-exporter/new-graph? tx-meta)
+            (and (::sqlite-export/imported-data? tx-meta) (:import-db? tx-meta)))
+        (invoke-hooks-for-imported-graph conn tx-report)
 
-      :else
-      (invoke-hooks-default repo conn tx-report context))))
+        :else
+        (invoke-hooks-default repo conn tx-report context)))))

+ 6 - 0
src/main/frontend/worker/rtc/exception.cljs

@@ -57,4 +57,10 @@ the server will put it to s3 and return its presigned-url to clients."}
   (cond
     (instance? Cancelled e) (ex-info "missionary.Cancelled" {:message (.-message e)})
     (instance? js/CloseEvent e) (ex-info "js/CloseEvent" {:type (.-type e)})
+
+    ;; m/race-failure
+    (and (instance? ExceptionInfo e)
+         (contains? (ex-data e) :missionary.core/errors))
+    (ex-info (ex-message e) (update (ex-data e) :missionary.core/errors (fn [errors] (map e->ex-info errors))))
+
     :else e))

+ 0 - 1
src/main/mobile/components/search.cljs

@@ -143,7 +143,6 @@
                            (when-let [id (:block/uuid block)]
                              (p/let [block (db-async/<get-block (state/get-current-repo) id
                                                                 {:children? false
-                                                                 :skip-transact? true
                                                                  :skip-refresh? true})]
                                (when block (mobile-state/open-block-modal! block)))))}
               [:div.flex.flex-col.gap-1.py-1

+ 1 - 0
src/main/mobile/components/settings.cljs

@@ -15,6 +15,7 @@
       :class "text-1xl flex flex-1 w-full my-8"
       :on-click #(shui/dialog-open! login/page-impl
                                     {:close-btn? false
+                                     :label "user-login"
                                      :align :top
                                      :content-props {:class "app-login-modal"}})}
      "Login")

+ 0 - 1
tailwind.all.css

@@ -9,7 +9,6 @@
 @import "inter-ui/inter.css";
 @import "photoswipe/dist/photoswipe.css";
 @import "shepherd.js/dist/css/shepherd.css";
-@import "packages/amplify/dist/amplify.css";
 @import "packages/tldraw/apps/tldraw-logseq/src/styles.css";
 @import "katex/dist/katex.min.css";
 @import "codemirror/lib/codemirror.css";

+ 2 - 1
tailwind.config.js

@@ -129,7 +129,8 @@ module.exports = {
     './resources/**/*.html',
     './deps/shui/src/**/*.cljs',
     './deps/shui/src/**/*.cljc',
-    './packages/ui/@/components/**/*.{ts,tsx}'
+    './packages/ui/@/components/**/*.{ts,tsx}',
+    './packages/ui/src/amplify/**/*.{ts,tsx}'
   ],
   safelist: [
     'bg-black', 'bg-white', 'capitalize-first',

+ 0 - 1
tailwind.mobile.css

@@ -9,7 +9,6 @@
 @import "inter-ui/inter.css";
 
 @import "photoswipe/dist/photoswipe.css";
-@import "packages/amplify/dist/amplify.css";
 @import "katex/dist/katex.min.css";
 @import "codemirror/lib/codemirror.css";
 @import "codemirror/theme/solarized.css";

Some files were not shown because too many files changed in this diff