-
Notifications
You must be signed in to change notification settings - Fork 216
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
bookmarks: for starred files, first idea
- Loading branch information
1 parent
a18f271
commit ea5a872
Showing
18 changed files
with
801 additions
and
38 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
/* | ||
* This file is part of CoCalc: Copyright © 2024 Sagemath, Inc. | ||
* License: MS-RSL – see LICENSE.md for details | ||
*/ | ||
|
||
import { merge, sortBy, throttle, uniq, xor } from "lodash"; | ||
import { useState } from "react"; | ||
import useAsyncEffect from "use-async-effect"; | ||
|
||
import api from "@cocalc/frontend/client/api"; | ||
import { STARRED } from "@cocalc/util/consts/bookmarks"; | ||
import { GetStarredBookmarks } from "@cocalc/util/types/bookmarks"; | ||
import { | ||
FlyoutActiveStarred, | ||
getFlyoutActiveStarred, | ||
storeFlyoutState, | ||
} from "./state"; | ||
|
||
// Additionally to local storage, we back the state of the starred files in the database. | ||
// Errors with the API are ignored, because we primarily rely on local storage. | ||
// The only really important situation to think of are when there is nothing in local storage but in the database, | ||
// or when there is | ||
export function useStarredFilesManager(project_id: string) { | ||
const [starred, setStarred] = useState<FlyoutActiveStarred>( | ||
getFlyoutActiveStarred(project_id), | ||
); | ||
|
||
// once after mounting this, we update the starred bookmarks (which merges with what we have) and then stores it | ||
useAsyncEffect(async () => { | ||
await updateStarred(); | ||
}, []); | ||
|
||
function setStarredLS(starred: string[]) { | ||
setStarred(starred); | ||
storeFlyoutState(project_id, "active", { starred: starred }); | ||
} | ||
|
||
// TODO: there are also add/remove API endpoints, but for now we stick with set. Hardly worth optimizing. | ||
function setStarredPath(path: string, starState: boolean) { | ||
const next = starState | ||
? [...starred, path] | ||
: starred.filter((p) => p !== path); | ||
setStarredLS(next); | ||
storeStarred(next); | ||
} | ||
|
||
async function storeStarred(starred: string[]) { | ||
try { | ||
await api("bookmarks/set", { | ||
type: STARRED, | ||
project_id, | ||
payload: starred, | ||
}); | ||
} catch (err) { | ||
console.error("api error", err); | ||
} | ||
} | ||
|
||
// this is called once, when the flyout/tabs component is mounted | ||
// throtteld, to usually take 1 sec from opening the panel to loading the stars | ||
const updateStarred = throttle( | ||
async () => { | ||
try { | ||
const data: GetStarredBookmarks = await api("bookmarks/get", { | ||
type: STARRED, | ||
project_id, | ||
}); | ||
|
||
const { type, status } = data; | ||
|
||
if (type !== STARRED) { | ||
console.error( | ||
`flyout/store/starred type must be ${STARRED} but we got`, | ||
type, | ||
); | ||
return; | ||
} | ||
|
||
if (status === "success") { | ||
const { payload } = data; | ||
if ( | ||
Array.isArray(payload) && | ||
payload.every((x) => typeof x === "string") | ||
) { | ||
payload.sort(); // sorted for the xor check below | ||
const next = sortBy(uniq(merge(starred, payload))); | ||
setStarredLS(next); | ||
if (xor(payload, next).length > 0) { | ||
// if there is a change (e.g. nothing in the database stored yet), store the stars | ||
await storeStarred(next); | ||
} | ||
} else { | ||
console.error("flyout/store/starred invalid payload", payload); | ||
} | ||
} else if (status === "error") { | ||
const { error } = data; | ||
console.error("flyout/store/starred error", error); | ||
} else { | ||
console.error("flyout/store/starred error: unknown status", status); | ||
} | ||
} catch (err) { | ||
console.error("api error", err); | ||
} | ||
}, | ||
1000, | ||
{ trailing: true, leading: false }, | ||
); | ||
|
||
return { | ||
starred, | ||
setStarredPath, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
/* | ||
* This file is part of CoCalc: Copyright © 2024 Sagemath, Inc. | ||
* License: MS-RSL – see LICENSE.md for details | ||
*/ | ||
|
||
import { z } from "../framework"; | ||
|
||
import { | ||
LoadStarredFilesBookmarksProps, | ||
SaveStarredFilesBoookmarksProps, | ||
} from "@cocalc/server/bookmarks/starred"; | ||
import { STARRED } from "@cocalc/util/consts/bookmarks"; | ||
import { GetStarredBookmarks } from "@cocalc/util/types/bookmarks"; | ||
import { ProjectIdSchema } from "./projects/common"; | ||
|
||
const ERROR = z.object({ | ||
status: z.literal("error"), | ||
error: z.string(), | ||
}); | ||
|
||
const COMMON_STARRED = z.object({ | ||
project_id: ProjectIdSchema, | ||
type: z.literal(STARRED), | ||
}); | ||
|
||
export const BookmarkSetInputSchema = COMMON_STARRED.extend({ | ||
payload: z.string().array(), | ||
}); | ||
|
||
export const BookmarkSetOutputSchema = z.union([ | ||
COMMON_STARRED.merge(z.object({ status: z.literal("success") })), | ||
ERROR, | ||
]); | ||
|
||
export const BookmarkAddInputSchema = BookmarkSetInputSchema; | ||
export const BookmarkAddOutputSchema = BookmarkSetOutputSchema; | ||
|
||
export const BookmarkRemoveInputSchema = BookmarkSetInputSchema; | ||
export const BookmarkRemoveOutputSchema = BookmarkSetOutputSchema; | ||
|
||
export const BookmarkGetInputSchema = COMMON_STARRED; | ||
export const BookmarkGetOutputSchema = z.union([ | ||
z | ||
.object({ | ||
status: z.literal("success"), | ||
payload: z | ||
.array(z.string()) | ||
.describe( | ||
"Array of file path strings, as they are in the starred tabs flyout", | ||
), | ||
last_edited: z | ||
.number() | ||
.optional() | ||
.describe("UNIX epoch timestamp, when bookmark was last edited"), | ||
}) | ||
.merge(COMMON_STARRED), | ||
ERROR.merge(COMMON_STARRED), | ||
]); | ||
|
||
export type BookmarkSetInputType = z.infer<typeof BookmarkSetInputSchema>; | ||
export type BookmarkSetOutputType = z.infer<typeof BookmarkSetOutputSchema>; | ||
export type BookmarkAddInputType = z.infer<typeof BookmarkAddInputSchema>; | ||
export type BookmarkAddOutputType = z.infer<typeof BookmarkRemoveOutputSchema>; | ||
export type BookmarkRemoveInputType = z.infer<typeof BookmarkRemoveInputSchema>; | ||
export type BookmarkRemoveOutputType = z.infer<typeof BookmarkAddOutputSchema>; | ||
export type BookmarkGetInputType = z.infer<typeof BookmarkGetInputSchema>; | ||
export type BookmarkGetOutputType = z.infer<typeof BookmarkGetOutputSchema>; | ||
|
||
// consistency checks | ||
export const _1: Omit<SaveStarredFilesBoookmarksProps, "mode" | "account_id"> = | ||
{} as Omit<BookmarkSetInputType, typeof STARRED>; | ||
|
||
export const _2: Omit<LoadStarredFilesBookmarksProps, "account_id"> = | ||
{} as Omit<BookmarkGetInputType, typeof STARRED>; | ||
|
||
export const _3: BookmarkGetOutputType = {} as GetStarredBookmarks; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { createMocks } from "lib/api/test-framework"; | ||
// import get from "./bookmarks/get"; | ||
import set from "./bookmarks/set"; | ||
|
||
describe("/api/v2/bookmarks", () => { | ||
test("set then get", async () => { | ||
const { req, res } = createMocks({ | ||
method: "POST", | ||
url: "/api/v2/bookmarks/set", | ||
body: { | ||
type: "starred-files", | ||
payload: { stars: ["foo.md", "bar.ipynb"] }, | ||
}, | ||
}); | ||
|
||
await set(req, res); | ||
expect(res.statusCode).toBe(200); | ||
console.log(res._getJSONData()); | ||
}); | ||
}); |
Oops, something went wrong.