-
Notifications
You must be signed in to change notification settings - Fork 218
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #7964 from sagemathinc/bookmark-stars
bookmarks: starting with starred files
- Loading branch information
Showing
24 changed files
with
924 additions
and
70 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
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,119 @@ | ||
/* | ||
* 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_FILES } from "@cocalc/util/consts/bookmarks"; | ||
import { | ||
GetStarredBookmarks, | ||
GetStarredBookmarksPayload, | ||
SetStarredBookmarks, | ||
} 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(stars: string[]) { | ||
try { | ||
const payload: SetStarredBookmarks = { | ||
type: STARRED_FILES, | ||
project_id, | ||
stars, | ||
}; | ||
await api("bookmarks/set", payload); | ||
} 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 payload: GetStarredBookmarksPayload = { | ||
type: STARRED_FILES, | ||
project_id, | ||
}; | ||
const data: GetStarredBookmarks = await api("bookmarks/get", payload); | ||
|
||
const { type, status } = data; | ||
|
||
if (type !== STARRED_FILES) { | ||
console.error( | ||
`flyout/store/starred type must be ${STARRED_FILES} but we got`, | ||
type, | ||
); | ||
return; | ||
} | ||
|
||
if (status === "success") { | ||
const { stars } = data; | ||
if ( | ||
Array.isArray(stars) && | ||
stars.every((x) => typeof x === "string") | ||
) { | ||
stars.sort(); // sorted for the xor check below | ||
const next = sortBy(uniq(merge(starred, stars))); | ||
setStarredLS(next); | ||
if (xor(stars, 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", stars); | ||
} | ||
} 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,63 @@ | ||
/* | ||
* This file is part of CoCalc: Copyright © 2024 Sagemath, Inc. | ||
* License: MS-RSL – see LICENSE.md for details | ||
*/ | ||
|
||
import isCollaborator from "@cocalc/server/projects/is-collaborator"; | ||
import getAccountId from "lib/account/get-account"; | ||
import getParams from "lib/api/get-params"; | ||
|
||
import { | ||
BookmarkGetInputSchema, | ||
BookmarkSetInputSchema, | ||
} from "lib/api/schema/bookmarks"; | ||
|
||
// Process a request for the api/v2/bookmarks/* endpoints | ||
|
||
// TODO: deduplicate this with proper typing | ||
|
||
export async function processSetRequest(req) { | ||
// ATTN: very confusing: this is the account_id or project_id for project level API keys | ||
// Since bookmakrs are account specific (and collaborators shouldn't snoop on others), we block project keys | ||
// In the future, there might be project-wide stars, which are not account specific. | ||
const account_id = await getAccountId(req); | ||
if (!account_id) { | ||
throw Error("must be signed in"); | ||
} | ||
|
||
const data = BookmarkSetInputSchema.parse(getParams(req)); | ||
|
||
if (account_id === data.project_id) { | ||
throw new Error( | ||
`As of now, you cannot use a project-level API key to modify account specific bookmarks. Use the account level API key!`, | ||
); | ||
} | ||
|
||
if (!(await isCollaborator({ account_id, project_id: data.project_id }))) { | ||
throw Error("user must be a collaborator on the project"); | ||
} | ||
|
||
return { ...data, account_id }; | ||
} | ||
|
||
export async function processGetRequest(req) { | ||
// ATTN: very confusing: this is the account_id or project_id for project level API keys | ||
const account_id = await getAccountId(req); | ||
if (!account_id) { | ||
throw Error("must be signed in"); | ||
} | ||
|
||
const data = BookmarkGetInputSchema.parse(getParams(req)); | ||
|
||
if (account_id === data.project_id) { | ||
throw new Error( | ||
`As of now, you cannot use a project-level API key to modify account specific bookmarks. Use the account level API key!`, | ||
); | ||
} | ||
|
||
if (!(await isCollaborator({ account_id, project_id: data.project_id }))) { | ||
throw Error("user must be a collaborator on the project"); | ||
} | ||
|
||
return { ...data, account_id }; | ||
} |
Oops, something went wrong.