Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix search persistence #155

Merged
merged 1 commit into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 16 additions & 49 deletions src/container.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,22 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useContext, Fragment } from "react";
import { observer } from "mobx-react-lite";
import Layout, { LayoutDummy } from "./layout";
import Preferences from "./views/preferences";
import Journals from "./views/journals";
import Documents from "./views/documents";
import Editor from "./views/edit";
import { SearchProvider } from "./views/documents/SearchProvider";
import {
useJournalsLoader,
JournalsStoreContext,
} from "./hooks/useJournalsLoader";
import { Alert, Pane } from "evergreen-ui";
import { Alert } from "evergreen-ui";
import { Routes, Route, Navigate } from "react-router-dom";
import { useSearchParams } from "react-router-dom";
import useClient from "./hooks/useClient";
import {
SearchV2Store,
SearchStoreContext,
} from "./views/documents/SearchStore";

export default observer(function Container() {
const { journalsStore, loading, loadingErr } = useJournalsLoader();
const client = useClient();
const [params, setParams] = useSearchParams();
const [searchStore, setSearchStore] = useState<null | SearchV2Store>(null);

// This is more like an effect. This smells. Maybe just roll this all up into
// a hook.
if (journalsStore && !loading && !searchStore) {
const store = new SearchV2Store(
client,
journalsStore,
setParams,
params.getAll("search"),
);
store.search();
setSearchStore(store);
}

// The identity of this function changes on every render
// The store is not re-created, so needs updated.
// This is a bit of a hack, but it works.
useEffect(() => {
if (searchStore) {
searchStore.setTokensUrl = setParams;
}
}, [setParams]);

if (loading || !searchStore) {
if (loading) {
return (
<LayoutDummy>
<h1>Loading Journals...</h1>
Expand All @@ -66,21 +36,18 @@ export default observer(function Container() {

return (
<JournalsStoreContext.Provider value={journalsStore!}>
<SearchStoreContext.Provider value={searchStore}>
<Layout>
<Routes>
<Route element={<Journals />} path="journals" />
<Route element={<Preferences />} path="preferences" />
<Route element={<Editor />} path="edit/new" />
<Route element={<Editor />} path="edit/:document" />
<Route
element={<Documents store={searchStore} />}
path="documents"
/>
<Route path="*" element={<Navigate to="documents" replace />} />
</Routes>
</Layout>
</SearchStoreContext.Provider>
<Layout>
<Routes>
<Route path="journals" element={<Journals />} />
<Route path="preferences" element={<Preferences />} />
<Route path="documents" element={<SearchProvider />}>
<Route index element={<Documents />} />
<Route path="edit/new" element={<Editor />} />
<Route path="edit/:document" element={<Editor />} />
</Route>
<Route path="*" element={<Navigate to="documents" replace />} />
</Routes>
</Layout>
</JournalsStoreContext.Provider>
);
});
8 changes: 4 additions & 4 deletions src/views/documents/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import {
} from "evergreen-ui";
import TagSearch from "./search";
import { Link } from "react-router-dom";
import { SearchV2Store } from "./SearchStore";
import { SearchStore } from "./SearchStore";
import { JournalsStoreContext } from "../../hooks/useJournalsLoader";
import { JournalResponse } from "../../hooks/useClient";

interface Props {
store: SearchV2Store;
store: SearchStore;
children: any;
empty?: boolean;
}
Expand All @@ -43,7 +43,7 @@ export function Layout(props: Props) {
<TagSearch store={props.store} />
</Pane>
<Pane>
<Link to="/edit/new">Create new</Link>
<Link to="/documents/edit/new">Create new</Link>
</Pane>
<Pane marginTop={24}>{props.children}</Pane>
</Pane>
Expand All @@ -53,7 +53,7 @@ export function Layout(props: Props) {
interface SidebarProps {
isShown: boolean;
setIsShown: (isShown: boolean) => void;
search: SearchV2Store;
search: SearchStore;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,32 +30,17 @@ const parsers: Record<SearchToken["type"], TokenParser<any>> = {
};

/**
* Any object holding observable tokens can be used
* Helper for parsing, adding, and removing search tokens
*/
export interface ITokensStore {
tokens: IObservableArray<SearchToken>;
setTokens: (tokens: SearchToken[]) => void;
}

/**
* View model for displaying, adding, and removing search tokens
*/
export class TagSearchStore {
constructor(private store: ITokensStore) {}

// TODO: Rename. These are stringified tokens, not SearchToken's
// which is confusing?
@computed
get searchTokens() {
return this.store.tokens.map((token) => {
const parser = parsers[token.type];
return parser.serialize(token);
});
}
export class SearchParser {
serializeToken = (token: SearchToken) => {
const parser = parsers[token.type];
return parser.serialize(token);
};

/**
* For a given search (string), get the right parser
* and the parsed value.
* For a given search component (ex: in:chronicles), get the right parser
* and the parsed value (ex: { type: 'in', value: 'chronicles' })
*
* @param tokenStr - The raw string from the search input
*/
Expand All @@ -72,7 +57,6 @@ export class TagSearchStore {
}

const [, prefix, value] = matches;
// todo: same todo as above
if (!value) return;

const parser: TokenParser = (parsers as any)[prefix];
Expand All @@ -84,36 +68,38 @@ export class TagSearchStore {
return [parser, parsedToken];
}

/**
* Add a raw array of (search string) tokens to the store
*
* @param tokens - An array of strings representing tokens
*/
@action
addTokens = (tokens: string[]) => {
// todo: Why am I not doing this atomically?
for (const token of tokens) {
this.addToken(token);
}
};

@action
addToken = (tokenStr: string) => {
parseToken = (tokenStr: string) => {
const results = this.parserFor(tokenStr);
if (!results) return;

const [parser, parsedToken] = results;
const tokens = parser.add(this.store.tokens, parsedToken);
this.store.setTokens(tokens);
const [_, parsedToken] = results;
return parsedToken;
};

parseTokens = (tokenStr: string[]) => {
let parsedTokens: SearchToken[] = [];
tokenStr.forEach((token) => {
const parsedToken = this.parseToken(token);
if (!parsedToken) return;

// todo: fix type
parsedTokens.push(parsedToken as any);
});

return parsedTokens;
};

mergeToken = (tokens: SearchToken[], token: SearchToken) => {
const parser = parsers[token.type];
return parser.add(tokens, token);
};

@action
removeToken = (tokenStr: string) => {
removeToken = (tokens: any[], tokenStr: string) => {
const results = this.parserFor(tokenStr);
if (!results) return;
if (!results) return tokens;

const [parser, parsedToken] = results;
const tokens = parser.remove(this.store.tokens.slice(), parsedToken);
this.store.setTokens(tokens);
return parser.remove(tokens, parsedToken);
};
}
52 changes: 52 additions & 0 deletions src/views/documents/SearchProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React, { useContext, useState, useEffect } from "react";
import { Outlet } from "react-router-dom";
import { useSearchParams } from "react-router-dom";
import useClient from "../../hooks/useClient";
import { JournalsStoreContext } from "../../hooks/useJournalsLoader";
import { SearchStore, SearchStoreContext } from "./SearchStore";
import { LayoutDummy } from "../../layout";

export function SearchProvider(props: any) {
const jstore = useContext(JournalsStoreContext);
const client = useClient();
const [params, setParams] = useSearchParams();
const [searchStore, setSearchStore] = useState<null | SearchStore>(null);

// This is more like an effect. This smells. Maybe just roll this all up into
// a hook.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably should remove this (copied from container); this component is sufficiently special purpose.

if (jstore && !searchStore) {
const store = new SearchStore(
client,
jstore,
setParams,
params.getAll("search"),
);
store.search();
setSearchStore(store);
}

// The identity of this function changes on every render
// The store is not re-created, so needs updated.
// This is a bit of a hack, but it works.
useEffect(() => {
if (searchStore) {
searchStore.setTokensUrl = setParams;
}
}, [setParams]);

if (!searchStore) {
return (
<React.Fragment>
<LayoutDummy />;
</React.Fragment>
);
}

return (
<React.Fragment>
<SearchStoreContext.Provider value={searchStore}>
<Outlet />
</SearchStoreContext.Provider>
</React.Fragment>
);
}
Loading
Loading