Compare commits
3 Commits
b2a584847f
...
ec120e7d26
Author | SHA1 | Date | |
---|---|---|---|
ec120e7d26 | |||
953bb67940 | |||
871f393e01 |
@ -17,6 +17,7 @@
|
|||||||
"@radix-ui/react-separator": "^1.0.3",
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
|
"@tanstack/react-virtual": "^3.2.1",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"dbtype": "workspace:*",
|
"dbtype": "workspace:*",
|
||||||
|
@ -4,6 +4,7 @@ import TagBadge from "@/components/gallery/TagBadge.tsx";
|
|||||||
import { Fragment, useLayoutEffect, useRef, useState } from "react";
|
import { Fragment, useLayoutEffect, useRef, useState } from "react";
|
||||||
import { LazyImage } from "./LazyImage.tsx";
|
import { LazyImage } from "./LazyImage.tsx";
|
||||||
import StyledLink from "./StyledLink.tsx";
|
import StyledLink from "./StyledLink.tsx";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
function clipTagsWhenOverflow(tags: string[], limit: number) {
|
function clipTagsWhenOverflow(tags: string[], limit: number) {
|
||||||
let l = 0;
|
let l = 0;
|
||||||
@ -17,7 +18,7 @@ function clipTagsWhenOverflow(tags: string[], limit: number) {
|
|||||||
return tags;
|
return tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GalleryCard({
|
function GalleryCardImpl({
|
||||||
doc: x
|
doc: x
|
||||||
}: { doc: Document; }) {
|
}: { doc: Document; }) {
|
||||||
const ref = useRef<HTMLUListElement>(null);
|
const ref = useRef<HTMLUListElement>(null);
|
||||||
@ -85,3 +86,5 @@ export function GalleryCard({
|
|||||||
</div>
|
</div>
|
||||||
</Card>;
|
</Card>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const GalleryCard = React.memo(GalleryCardImpl);
|
@ -40,7 +40,7 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
<ResizablePanel minSize={minSize} collapsible maxSize={minSize}>
|
<ResizablePanel minSize={minSize} collapsible maxSize={minSize}>
|
||||||
<NavList />
|
<NavList />
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<ResizableHandle withHandle />
|
<ResizableHandle withHandle className="z-20" />
|
||||||
<ResizablePanel >
|
<ResizablePanel >
|
||||||
{children}
|
{children}
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
|
@ -32,7 +32,7 @@ export function NavItem({
|
|||||||
export function NavList() {
|
export function NavList() {
|
||||||
const loginInfo = useLogin();
|
const loginInfo = useLogin();
|
||||||
|
|
||||||
return <aside className="h-screen flex flex-col">
|
return <aside className="h-dvh flex flex-col">
|
||||||
<nav className="flex flex-col items-center gap-4 px-2 sm:py-5 flex-1">
|
<nav className="flex flex-col items-center gap-4 px-2 sm:py-5 flex-1">
|
||||||
<NavItem icon={<MagnifyingGlassIcon className="h-5 w-5" />} to="/search" name="Search" />
|
<NavItem icon={<MagnifyingGlassIcon className="h-5 w-5" />} to="/search" name="Search" />
|
||||||
<NavItem icon={<ActivityLogIcon className="h-5 w-5" />} to="/tags" name="Tags" />
|
<NavItem icon={<ActivityLogIcon className="h-5 w-5" />} to="/tags" name="Tags" />
|
||||||
|
@ -1,7 +1,20 @@
|
|||||||
export const BASE_API_URL = 'http://localhost:5173/';
|
export const BASE_API_URL = import.meta.env.VITE_API_URL ?? window.location.origin;
|
||||||
|
|
||||||
export async function fetcher(url: string) {
|
export function makeApiUrl(pathnameAndQueryparam: string) {
|
||||||
const u = new URL(url, BASE_API_URL);
|
return new URL(pathnameAndQueryparam, BASE_API_URL).toString();
|
||||||
const res = await fetch(u);
|
}
|
||||||
|
|
||||||
|
export class ApiError extends Error {
|
||||||
|
constructor(public readonly status: number, message: string) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetcher(url: string, init?: RequestInit) {
|
||||||
|
const u = makeApiUrl(url);
|
||||||
|
const res = await fetch(u, init);
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new ApiError(res.status, await res.text());
|
||||||
|
}
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ export function useDifferenceDoc() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function commit(path: string, type: string) {
|
export async function commit(path: string, type: string) {
|
||||||
const res = await fetch("/api/diff/commit", {
|
const data = await fetcher("/api/diff/commit", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify([{ path, type }]),
|
body: JSON.stringify([{ path, type }]),
|
||||||
headers: {
|
headers: {
|
||||||
@ -22,11 +22,11 @@ export async function commit(path: string, type: string) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
mutate("/api/diff/list");
|
mutate("/api/diff/list");
|
||||||
return res.ok;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function commitAll(type: string) {
|
export async function commitAll(type: string) {
|
||||||
const res = await fetch("/api/diff/commitall", {
|
const data = await fetcher("/api/diff/commitall", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ type }),
|
body: JSON.stringify({ type }),
|
||||||
headers: {
|
headers: {
|
||||||
@ -34,5 +34,5 @@ export async function commitAll(type: string) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
mutate("/api/diff/list");
|
mutate("/api/diff/list");
|
||||||
return res.ok;
|
return data;
|
||||||
}
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import useSWRInifinite from "swr/infinite";
|
import useSWRInifinite from "swr/infinite";
|
||||||
import type { Document } from "dbtype/api";
|
import type { Document } from "dbtype/api";
|
||||||
import { fetcher } from "./fetcher";
|
import { fetcher } from "./fetcher";
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
interface SearchParams {
|
interface SearchParams {
|
||||||
word?: string;
|
word?: string;
|
||||||
@ -9,24 +10,33 @@ interface SearchParams {
|
|||||||
cursor?: number;
|
cursor?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSearchGallery({
|
function makeSearchParams({
|
||||||
word, tags, limit, cursor,
|
word, tags, limit, cursor,
|
||||||
}: SearchParams) {
|
}: SearchParams){
|
||||||
|
const search = new URLSearchParams();
|
||||||
|
if (word) search.set("word", word);
|
||||||
|
if (tags) search.set("allow_tag", tags);
|
||||||
|
if (limit) search.set("limit", limit.toString());
|
||||||
|
if (cursor) search.set("cursor", cursor.toString());
|
||||||
|
return search;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSearchGallery(searchParams: SearchParams = {}) {
|
||||||
|
return useSWR<Document[]>(`/api/doc/search?${makeSearchParams(searchParams).toString()}`, fetcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSearchGalleryInfinite(searchParams: SearchParams = {}) {
|
||||||
return useSWRInifinite<
|
return useSWRInifinite<
|
||||||
{
|
{
|
||||||
data: Document[];
|
data: Document[];
|
||||||
nextCursor: number | null;
|
nextCursor: number | null;
|
||||||
|
startCursor: number | null;
|
||||||
hasMore: boolean;
|
hasMore: boolean;
|
||||||
}
|
}
|
||||||
>((index, previous) => {
|
>((index, previous) => {
|
||||||
if (!previous && index > 0) return null;
|
if (!previous && index > 0) return null;
|
||||||
if (previous && !previous.hasMore) return null;
|
if (previous && !previous.hasMore) return null;
|
||||||
const search = new URLSearchParams();
|
const search = makeSearchParams(searchParams)
|
||||||
if (word) search.set("word", word);
|
|
||||||
if (tags) search.set("allow_tag", tags);
|
|
||||||
if (limit) search.set("limit", limit.toString());
|
|
||||||
if (cursor) search.set("cursor", cursor.toString());
|
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
return `/api/doc/search?${search.toString()}`;
|
return `/api/doc/search?${search.toString()}`;
|
||||||
}
|
}
|
||||||
@ -35,9 +45,11 @@ export function useSearchGallery({
|
|||||||
search.set("cursor", last.id.toString());
|
search.set("cursor", last.id.toString());
|
||||||
return `/api/doc/search?${search.toString()}`;
|
return `/api/doc/search?${search.toString()}`;
|
||||||
}, async (url) => {
|
}, async (url) => {
|
||||||
|
const limit = searchParams.limit;
|
||||||
const res = await fetcher(url);
|
const res = await fetcher(url);
|
||||||
return {
|
return {
|
||||||
data: res,
|
data: res,
|
||||||
|
startCursor: res.length === 0 ? null : res[0].id,
|
||||||
nextCursor: res.length === 0 ? null : res[res.length - 1].id,
|
nextCursor: res.length === 0 ? null : res[res.length - 1].id,
|
||||||
hasMore: limit ? res.length === limit : (res.length === 20),
|
hasMore: limit ? res.length === limit : (res.length === 20),
|
||||||
};
|
};
|
||||||
|
@ -26,7 +26,7 @@ export function DifferencePage() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="relative">
|
<CardHeader className="relative">
|
||||||
<Button className="absolute right-2 top-8" variant="ghost"
|
<Button className="absolute right-2 top-8" variant="ghost"
|
||||||
onClick={() => commitAll("comic")}
|
onClick={() => {commitAll("comic")}}
|
||||||
>Commit All</Button>
|
>Commit All</Button>
|
||||||
<CardTitle className="text-2xl">Difference</CardTitle>
|
<CardTitle className="text-2xl">Difference</CardTitle>
|
||||||
<CardDescription>Scanned Files List</CardDescription>
|
<CardDescription>Scanned Files List</CardDescription>
|
||||||
@ -45,8 +45,7 @@ export function DifferencePage() {
|
|||||||
<Button
|
<Button
|
||||||
className="flex-none ml-2"
|
className="flex-none ml-2"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => commit(y.path, y.type)}
|
onClick={() => {commit(y.path, y.type)}}>
|
||||||
>
|
|
||||||
Commit
|
Commit
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,10 +2,12 @@ import { useLocation, useSearch } from "wouter";
|
|||||||
import { Button } from "@/components/ui/button.tsx";
|
import { Button } from "@/components/ui/button.tsx";
|
||||||
import { GalleryCard } from "@/components/gallery/GalleryCard.tsx";
|
import { GalleryCard } from "@/components/gallery/GalleryCard.tsx";
|
||||||
import TagBadge from "@/components/gallery/TagBadge.tsx";
|
import TagBadge from "@/components/gallery/TagBadge.tsx";
|
||||||
import { useSearchGallery } from "../hook/useSearchGallery.ts";
|
import { useSearchGalleryInfinite } from "../hook/useSearchGallery.ts";
|
||||||
import { Spinner } from "../components/Spinner.tsx";
|
import { Spinner } from "../components/Spinner.tsx";
|
||||||
import TagInput from "@/components/gallery/TagInput.tsx";
|
import TagInput from "@/components/gallery/TagInput.tsx";
|
||||||
import { useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { Separator } from "@/components/ui/separator.tsx";
|
||||||
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||||
|
|
||||||
export default function Gallery() {
|
export default function Gallery() {
|
||||||
const search = useSearch();
|
const search = useSearch();
|
||||||
@ -14,11 +16,36 @@ export default function Gallery() {
|
|||||||
const tags = searchParams.get("allow_tag") ?? undefined;
|
const tags = searchParams.get("allow_tag") ?? undefined;
|
||||||
const limit = searchParams.get("limit");
|
const limit = searchParams.get("limit");
|
||||||
const cursor = searchParams.get("cursor");
|
const cursor = searchParams.get("cursor");
|
||||||
const { data, error, isLoading, size, setSize } = useSearchGallery({
|
const { data, error, isLoading, size, setSize } = useSearchGalleryInfinite({
|
||||||
word, tags,
|
word, tags,
|
||||||
limit: limit ? Number.parseInt(limit) : undefined,
|
limit: limit ? Number.parseInt(limit) : undefined,
|
||||||
cursor: cursor ? Number.parseInt(cursor) : undefined
|
cursor: cursor ? Number.parseInt(cursor) : undefined
|
||||||
});
|
});
|
||||||
|
const parentRef = useRef<HTMLDivElement>(null);
|
||||||
|
const virtualizer = useVirtualizer({
|
||||||
|
count: size,
|
||||||
|
// biome-ignore lint/style/noNonNullAssertion: <explanation>
|
||||||
|
getScrollElement: () => parentRef.current!,
|
||||||
|
estimateSize: (index) => {
|
||||||
|
const docs = data?.[index];
|
||||||
|
if (!docs) return 8;
|
||||||
|
return docs.data.length * (200 + 8) + 37 + 8;
|
||||||
|
},
|
||||||
|
overscan: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const virtualItems = virtualizer.getVirtualItems();
|
||||||
|
useEffect(() => {
|
||||||
|
const lastItems = virtualItems.slice(-1);
|
||||||
|
// console.log(virtualItems);
|
||||||
|
if (lastItems.some(x => x.index >= size - 1)) {
|
||||||
|
const last = lastItems[0];
|
||||||
|
const docs = data?.[last.index];
|
||||||
|
if (docs?.hasMore) {
|
||||||
|
setSize(size + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [virtualItems, setSize, size, data]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <div className="p-4">Loading...</div>
|
return <div className="p-4">Loading...</div>
|
||||||
@ -26,36 +53,59 @@ export default function Gallery() {
|
|||||||
if (error) {
|
if (error) {
|
||||||
return <div className="p-4">Error: {String(error)}</div>
|
return <div className="p-4">Error: {String(error)}</div>
|
||||||
}
|
}
|
||||||
|
if (!data) {
|
||||||
|
return <div className="p-4">No data</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const isLoadingMore = data && size > 0 && (data[size - 1] === undefined);
|
const isLoadingMore = data && size > 0 && (data[size - 1] === undefined);
|
||||||
const isReachingEnd = data && data[size - 1]?.hasMore === false;
|
const isReachingEnd = data && data[size - 1]?.hasMore === false;
|
||||||
|
|
||||||
return (<div className="p-4 grid gap-2 overflow-auto h-screen items-start content-start">
|
return (<div className="p-4 grid gap-2 overflow-auto h-dvh items-start content-start" ref={parentRef}>
|
||||||
<Search />
|
<Search />
|
||||||
{(word || tags) &&
|
{(word || tags) &&
|
||||||
<div className="bg-primary rounded-full p-1 sticky top-0 shadow-md">
|
<div className="bg-primary rounded-full p-1 sticky top-0 shadow-md z-20">
|
||||||
{word && <span className="text-primary-foreground ml-4">Search: {word}</span>}
|
{word && <span className="text-primary-foreground ml-4">Search: {word}</span>}
|
||||||
{tags && <span className="text-primary-foreground ml-4">Tags: <ul className="inline-flex">{tags.split(",").map(x => <TagBadge tagname={x} key={x} />)}</ul></span>}
|
{tags && <span className="text-primary-foreground ml-4">Tags: <ul className="inline-flex">{
|
||||||
|
tags.split(",").map(x => <TagBadge tagname={x} key={x} />)}
|
||||||
|
</ul></span>}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{
|
{data?.length === 0 && <div className="p-4 text-3xl">No results</div>}
|
||||||
data?.length === 0 && <div className="p-4 text-3xl">No results</div>
|
<div className="w-full relative"
|
||||||
}
|
style={{ height: virtualizer.getTotalSize() }}>
|
||||||
{
|
{// TODO: date based grouping
|
||||||
// TODO: date based grouping
|
virtualItems.map((item) => {
|
||||||
data?.map((docs) => {
|
const isLoaderRow = item.index === size - 1 && isLoadingMore;
|
||||||
return docs.data.map((x) => {
|
if (isLoaderRow) {
|
||||||
return (
|
return <div key={item.index} className="w-full flex justify-center top-0 left-0 absolute"
|
||||||
<GalleryCard doc={x} key={x.id} />
|
style={{
|
||||||
);
|
height: `${item.size}px`,
|
||||||
});
|
transform: `translateY(${item.start}px)`
|
||||||
})
|
}}>
|
||||||
}
|
<Spinner />
|
||||||
{
|
</div>;
|
||||||
<Button className="w-full" onClick={() => setSize(size + 1)}
|
}
|
||||||
disabled={isReachingEnd || isLoadingMore}
|
const docs = data[item.index];
|
||||||
> {isLoadingMore && <Spinner className="mr-1" />}{size + 1} Load more</Button>
|
if (!docs) return null;
|
||||||
}
|
return <div className="w-full grid gap-2 content-start top-0 left-0 absolute mb-2" key={item.index}
|
||||||
|
style={{
|
||||||
|
height: `${item.size}px`,
|
||||||
|
transform: `translateY(${item.start}px)`
|
||||||
|
}}>
|
||||||
|
{docs.startCursor && <div>
|
||||||
|
<h3 className="text-3xl">Start with {docs.startCursor}</h3>
|
||||||
|
<Separator />
|
||||||
|
</div>}
|
||||||
|
{docs?.data?.map((x) => {
|
||||||
|
return (
|
||||||
|
<GalleryCard doc={x} key={x.id} />
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -70,7 +120,7 @@ function Search() {
|
|||||||
<TagInput className="flex-1" input={word} onInputChange={setWord}
|
<TagInput className="flex-1" input={word} onInputChange={setWord}
|
||||||
tags={tags} onTagsChange={setTags}
|
tags={tags} onTagsChange={setTags}
|
||||||
/>
|
/>
|
||||||
<Button className="flex-none" onClick={()=>{
|
<Button className="flex-none" onClick={() => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (tags.length > 0) {
|
if (tags.length > 0) {
|
||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { useEffect } from "react";
|
|
||||||
import { type TernaryDarkMode, useTernaryDarkMode, useMediaQuery } from "usehooks-ts";
|
import { type TernaryDarkMode, useTernaryDarkMode, useMediaQuery } from "usehooks-ts";
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { atom, useAtomValue, setAtomValue } from "../lib/atom.ts";
|
import { atom, useAtomValue, setAtomValue } from "../lib/atom.ts";
|
||||||
import { BASE_API_URL } from "../hook/fetcher.ts";
|
import { makeApiUrl } from "../hook/fetcher.ts";
|
||||||
|
|
||||||
type LoginLocalStorage = {
|
type LoginLocalStorage = {
|
||||||
username: string;
|
username: string;
|
||||||
@ -24,7 +24,7 @@ function getUserSessions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function refresh() {
|
export async function refresh() {
|
||||||
const u = new URL("/api/user/refresh", BASE_API_URL);
|
const u = makeApiUrl("/api/user/refresh");
|
||||||
const res = await fetch(u, {
|
const res = await fetch(u, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
@ -50,7 +50,7 @@ export async function refresh() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const doLogout = async () => {
|
export const doLogout = async () => {
|
||||||
const u = new URL("/api/user/logout", BASE_API_URL);
|
const u = makeApiUrl("/api/user/logout");
|
||||||
const req = await fetch(u, {
|
const req = await fetch(u, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
@ -81,7 +81,7 @@ export const doLogin = async (userLoginInfo: {
|
|||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
}): Promise<string | LoginLocalStorage> => {
|
}): Promise<string | LoginLocalStorage> => {
|
||||||
const u = new URL("/api/user/login", BASE_API_URL);
|
const u = makeApiUrl("/api/user/login");
|
||||||
const res = await fetch(u, {
|
const res = await fetch(u, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(userLoginInfo),
|
body: JSON.stringify(userLoginInfo),
|
||||||
|
@ -61,10 +61,10 @@ class SqliteDocumentAccessor implements DocumentAccessor {
|
|||||||
return await this.kysely.transaction().execute(async (trx) => {
|
return await this.kysely.transaction().execute(async (trx) => {
|
||||||
const { tags, additional, ...rest } = c;
|
const { tags, additional, ...rest } = c;
|
||||||
const id_lst = await trx.insertInto("document").values({
|
const id_lst = await trx.insertInto("document").values({
|
||||||
additional: JSON.stringify(additional),
|
additional: JSON.stringify(additional),
|
||||||
created_at: Date.now(),
|
created_at: Date.now(),
|
||||||
...rest,
|
...rest,
|
||||||
})
|
})
|
||||||
.returning("id")
|
.returning("id")
|
||||||
.executeTakeFirst() as { id: number };
|
.executeTakeFirst() as { id: number };
|
||||||
const id = id_lst.id;
|
const id = id_lst.id;
|
||||||
@ -150,7 +150,7 @@ class SqliteDocumentAccessor implements DocumentAccessor {
|
|||||||
.selectFrom("document")
|
.selectFrom("document")
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.$if(allow_tag.length > 0, (qb) => {
|
.$if(allow_tag.length > 0, (qb) => {
|
||||||
return allow_tag.reduce((prevQb ,tag, index) => {
|
return allow_tag.reduce((prevQb, tag, index) => {
|
||||||
return prevQb.innerJoin(`doc_tag_relation as tags_${index}`, `tags_${index}.doc_id`, "document.id")
|
return prevQb.innerJoin(`doc_tag_relation as tags_${index}`, `tags_${index}.doc_id`, "document.id")
|
||||||
.where(`tags_${index}.tag_name`, "=", tag);
|
.where(`tags_${index}.tag_name`, "=", tag);
|
||||||
}, qb) as unknown as typeof qb;
|
}, qb) as unknown as typeof qb;
|
||||||
@ -161,11 +161,16 @@ class SqliteDocumentAccessor implements DocumentAccessor {
|
|||||||
.$if(!use_offset && cursor !== undefined, (qb) => qb.where("id", "<", cursor as number))
|
.$if(!use_offset && cursor !== undefined, (qb) => qb.where("id", "<", cursor as number))
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.$if(eager_loading, (qb) => {
|
.$if(eager_loading, (qb) => {
|
||||||
return qb.select(eb => jsonArrayFrom(
|
return qb.select(eb =>
|
||||||
eb.selectFrom("doc_tag_relation")
|
eb.selectFrom(e =>
|
||||||
.select(["doc_tag_relation.tag_name"])
|
e.selectFrom("doc_tag_relation")
|
||||||
.whereRef("document.id", "=", "doc_tag_relation.doc_id")
|
.select(["doc_tag_relation.tag_name"])
|
||||||
).as("tags")).withPlugin(new MyParseJSONResultsPlugin("tags"))
|
.whereRef("document.id", "=", "doc_tag_relation.doc_id")
|
||||||
|
.as("agg")
|
||||||
|
).select(e => e.fn.agg<string>("json_group_array", ["agg.tag_name"])
|
||||||
|
.as("tags_list")
|
||||||
|
).as("tags")
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.orderBy("id", "desc")
|
.orderBy("id", "desc")
|
||||||
.execute();
|
.execute();
|
||||||
@ -173,7 +178,7 @@ class SqliteDocumentAccessor implements DocumentAccessor {
|
|||||||
...x,
|
...x,
|
||||||
content_hash: x.content_hash ?? "",
|
content_hash: x.content_hash ?? "",
|
||||||
additional: x.additional !== null ? (JSON.parse(x.additional)) : {},
|
additional: x.additional !== null ? (JSON.parse(x.additional)) : {},
|
||||||
tags: x.tags?.map((x: { tag_name: string }) => x.tag_name) ?? [],
|
tags: JSON.parse(x.tags ?? "[]"), //?.map((x: { tag_name: string }) => x.tag_name) ?? [],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
async findByPath(path: string, filename?: string): Promise<Document[]> {
|
async findByPath(path: string, filename?: string): Promise<Document[]> {
|
||||||
|
@ -32,6 +32,9 @@ importers:
|
|||||||
'@radix-ui/react-tooltip':
|
'@radix-ui/react-tooltip':
|
||||||
specifier: ^1.0.7
|
specifier: ^1.0.7
|
||||||
version: 1.0.7(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.0.7(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@tanstack/react-virtual':
|
||||||
|
specifier: ^3.2.1
|
||||||
|
version: 3.2.1(react-dom@18.2.0)(react@18.2.0)
|
||||||
class-variance-authority:
|
class-variance-authority:
|
||||||
specifier: ^0.7.0
|
specifier: ^0.7.0
|
||||||
version: 0.7.0
|
version: 0.7.0
|
||||||
@ -1786,6 +1789,21 @@ packages:
|
|||||||
defer-to-connect: 2.0.1
|
defer-to-connect: 2.0.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@tanstack/react-virtual@3.2.1(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-i9Nt0ssIh2bSjomJZlr6Iq5usT/9+ewo2/fKHRNk6kjVKS8jrhXbnO8NEawarCuBx/efv0xpoUUKKGxa0cQb4Q==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
dependencies:
|
||||||
|
'@tanstack/virtual-core': 3.2.1
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@tanstack/virtual-core@3.2.1:
|
||||||
|
resolution: {integrity: sha512-nO0d4vRzsmpBQCJYyClNHPPoUMI4nXNfrm6IcCRL33ncWMoNVpURh9YebEHPw8KrtsP2VSJIHE4gf4XFGk1OGg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@tokenizer/token@0.3.0:
|
/@tokenizer/token@0.3.0:
|
||||||
resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
|
resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
|
||||||
dev: true
|
dev: true
|
||||||
|
Loading…
Reference in New Issue
Block a user