Skip to content

Commit d11440f

Browse files
authored
Merge pull request #8613 from sagemathinc/starred-files-in-explorer-8259
frontend/explorer: expose starred files
2 parents 8a45dbb + a9a07d3 commit d11440f

File tree

9 files changed

+168
-20
lines changed

9 files changed

+168
-20
lines changed

src/packages/frontend/cspell.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,10 @@
8989
"immutablejs",
9090
"ipynb",
9191
"isabs",
92+
"isactive",
9293
"isdir",
94+
"isopen",
95+
"issymlink",
9396
"kernelspec",
9497
"LLM",
9598
"LLMs",

src/packages/frontend/project/context.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@
33
* License: MS-RSL – see LICENSE.md for details
44
*/
55

6-
import { Context, createContext, useContext, useMemo, useState } from "react";
6+
import {
7+
Context,
8+
createContext,
9+
useContext,
10+
useEffect,
11+
useMemo,
12+
useState,
13+
} from "react";
14+
import * as immutable from "immutable";
715

816
import {
917
ProjectActions,
@@ -120,6 +128,15 @@ export function useProjectContextProvider({
120128
// not each time the active tab is opened!
121129
const manageStarredFiles = useStarredFilesManager(project_id);
122130

131+
// Sync starred files from conat to Redux store for use in computed values
132+
useEffect(() => {
133+
if (actions) {
134+
actions.setState({
135+
starred_files: immutable.List(manageStarredFiles.starred),
136+
});
137+
}
138+
}, [manageStarredFiles.starred, actions]);
139+
123140
const kucalc = useTypedRedux("customize", "kucalc");
124141
const onCoCalcCom = kucalc === KUCALC_COCALC_COM;
125142
const onCoCalcDocker = kucalc === KUCALC_DISABLED;

src/packages/frontend/project/explorer/file-listing/file-listing.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55

66
// Show a file listing.
77

8-
// cSpell:ignore issymlink
9-
108
import { Alert, Spin } from "antd";
119
import * as immutable from "immutable";
1210
import React, { useEffect, useRef, useState } from "react";
@@ -26,6 +24,7 @@ import useVirtuosoScrollHook from "@cocalc/frontend/components/virtuoso-scroll-h
2624
import { WATCH_THROTTLE_MS } from "@cocalc/frontend/conat/listings";
2725
import { ProjectActions } from "@cocalc/frontend/project_actions";
2826
import { MainConfiguration } from "@cocalc/frontend/project_configuration";
27+
import { useStarredFilesManager } from "@cocalc/frontend/project/page/flyouts/store";
2928
import * as misc from "@cocalc/util/misc";
3029
import { FileRow } from "./file-row";
3130
import { ListingHeader } from "./listing-header";
@@ -87,6 +86,7 @@ export const FileListing: React.FC<Props> = ({
8786
isRunning,
8887
}: Props) => {
8988
const [starting, setStarting] = useState<boolean>(false);
89+
const { starred, setStarredPath } = useStarredFilesManager(project_id);
9090

9191
const prev_current_path = usePrevious(current_path);
9292

@@ -136,6 +136,10 @@ export const FileListing: React.FC<Props> = ({
136136
const checked = checked_files.has(misc.path_to_file(current_path, name));
137137
const color = misc.rowBackground({ index, checked });
138138
const { is_public } = file_map[name];
139+
const fullPath = misc.path_to_file(current_path, name);
140+
// For directories, add trailing slash to match flyout convention
141+
const pathForStar = isdir ? `${fullPath}/` : fullPath;
142+
const isStarred = starred.includes(pathForStar);
139143

140144
return (
141145
<FileRow
@@ -159,6 +163,13 @@ export const FileListing: React.FC<Props> = ({
159163
no_select={shift_is_down}
160164
link_target={link_target}
161165
computeServerId={computeServerId}
166+
isStarred={isStarred}
167+
onToggleStar={(path, starState) => {
168+
// For directories, ensure trailing slash
169+
const normalizedPath =
170+
isdir && !path.endsWith("/") ? `${path}/` : path;
171+
setStarredPath(normalizedPath, starState);
172+
}}
162173
/>
163174
);
164175
}

src/packages/frontend/project/explorer/file-listing/file-row.tsx

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ interface Props {
5555
// if given, include a little 'server' tag in this color, and tooltip etc using id.
5656
// Also important for download and preview links!
5757
computeServerId?: number;
58+
isStarred?: boolean;
59+
onToggleStar?: (path: string, starred: boolean) => void;
5860
}
5961

6062
export const FileRow: React.FC<Props> = React.memo((props) => {
@@ -176,6 +178,29 @@ export const FileRow: React.FC<Props> = React.memo((props) => {
176178
}
177179
}
178180

181+
function render_star() {
182+
if (!props.onToggleStar) return null;
183+
const path = full_path();
184+
const starred = props.isStarred ?? false;
185+
const iconName = starred ? "star-filled" : "star";
186+
187+
return (
188+
<Icon
189+
name={iconName}
190+
onClick={(e) => {
191+
e?.preventDefault();
192+
e?.stopPropagation();
193+
props.onToggleStar?.(path, !starred);
194+
}}
195+
style={{
196+
cursor: "pointer",
197+
fontSize: "14pt",
198+
color: starred ? COLORS.STAR : COLORS.GRAY_L,
199+
}}
200+
/>
201+
);
202+
}
203+
179204
function full_path() {
180205
return misc.path_to_file(props.current_path, props.name);
181206
}
@@ -235,12 +260,12 @@ export const FileRow: React.FC<Props> = React.memo((props) => {
235260
return (
236261
<TimeAgo
237262
date={new Date(props.time * 1000).toISOString()}
238-
style={{ color: "#666" }}
263+
style={{ color: COLORS.GRAY_M }}
239264
/>
240265
);
241266
} catch (error) {
242267
return (
243-
<div style={{ color: "#666", display: "inline" }}>
268+
<div style={{ color: COLORS.GRAY_M, display: "inline" }}>
244269
Invalid Date Time
245270
</div>
246271
);
@@ -365,14 +390,17 @@ export const FileRow: React.FC<Props> = React.memo((props) => {
365390
<Col sm={2} xs={12} onClick={handle_click}>
366391
{render_icon()}
367392
</Col>
393+
<Col sm={1} xs={6} style={{ textAlign: "center" }}>
394+
{render_star()}
395+
</Col>
368396
<Col sm={10} xs={24} onClick={handle_click}>
369397
<VisibleXS>
370398
<span style={{ marginLeft: "16px" }} />
371399
</VisibleXS>
372400
{render_name()}
373401
</Col>
374402
<Col
375-
sm={8}
403+
sm={7}
376404
xs={24}
377405
style={{
378406
paddingRight:
@@ -388,7 +416,7 @@ export const FileRow: React.FC<Props> = React.memo((props) => {
388416
<DirectorySize size={props.size} />
389417
</>
390418
) : (
391-
<span className="pull-right" style={{ color: "#666" }}>
419+
<span className="pull-right" style={{ color: COLORS.GRAY_M }}>
392420
{render_download_button(url)}
393421
{render_view_button(url, props.name)}
394422
</span>

src/packages/frontend/project/explorer/file-listing/listing-header.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import React from "react";
77
import { TypedMap } from "@cocalc/frontend/app-framework";
88
import { Icon, Gap, VisibleMDLG } from "@cocalc/frontend/components";
9+
import { COLORS } from "@cocalc/util/theme";
910
import { Col, Row } from "antd";
1011

1112
// TODO: Flatten active_file_sort for easy PureComponent use
@@ -16,7 +17,7 @@ interface Props {
1617

1718
const row_style: React.CSSProperties = {
1819
cursor: "pointer",
19-
color: "#666",
20+
color: COLORS.GRAY_M,
2021
backgroundColor: "#fafafa",
2122
border: "1px solid #eee",
2223
borderRadius: "4px",
@@ -31,7 +32,7 @@ export const ListingHeader: React.FC<Props> = (props: Props) => {
3132

3233
function render_sort_link(
3334
column_name: string,
34-
display_name: string,
35+
display_name: string | React.JSX.Element,
3536
marginLeft?,
3637
) {
3738
return (
@@ -45,7 +46,11 @@ export const ListingHeader: React.FC<Props> = (props: Props) => {
4546
e.preventDefault();
4647
return sort_by(column_name);
4748
}}
48-
style={{ color: "#428bca", fontWeight: "bold" }}
49+
style={{
50+
color: COLORS.FG_BLUE,
51+
fontWeight: "bold",
52+
whiteSpace: "nowrap",
53+
}}
4954
>
5055
{display_name}
5156
<Gap />
@@ -70,10 +75,20 @@ export const ListingHeader: React.FC<Props> = (props: Props) => {
7075
<Col sm={2} xs={6}>
7176
{render_sort_link("type", "Type", "-4px")}
7277
</Col>
78+
<Col sm={1} xs={6} style={{ textAlign: "center" }}>
79+
{render_sort_link(
80+
"starred",
81+
<Icon
82+
name="star-filled"
83+
style={{ color: COLORS.FG_BLUE, fontSize: "12pt" }}
84+
/>,
85+
"0px",
86+
)}
87+
</Col>
7388
<Col sm={10} xs={24}>
7489
{render_sort_link("name", "Name", "-4px")}
7590
</Col>
76-
<Col sm={8} xs={12}>
91+
<Col sm={7} xs={12}>
7792
{render_sort_link("time", "Date Modified", "2px")}
7893
<span className="pull-right">
7994
{render_sort_link("size", "Size/Download/View")}

src/packages/frontend/project/page/flyouts/file-list-item.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,12 @@ const FILE_ITEM_OPENED_STYLE: CSS = {
5252
const FILE_ITEM_ACTIVE_STYLE: CSS = {
5353
...FILE_ITEM_OPENED_STYLE,
5454
color: COLORS.PROJECT.FIXED_LEFT_OPENED,
55-
};
55+
} as const;
5656

5757
const FILE_ITEM_ACTIVE_STYLE_2: CSS = {
5858
...FILE_ITEM_ACTIVE_STYLE,
5959
backgroundColor: COLORS.GRAY_L0,
60-
};
60+
} as const;
6161

6262
const FILE_ITEM_STYLE: CSS = {
6363
flex: "1",
@@ -105,7 +105,7 @@ const CLOSE_ICON_STYLE: CSS = {
105105
top: "1px",
106106
position: "relative",
107107
paddingBottom: "1px",
108-
};
108+
} as const;
109109

110110
interface Item {
111111
isopen?: boolean;
@@ -305,12 +305,23 @@ export const FileListItem = React.memo((props: Readonly<FileListItemProps>) => {
305305

306306
const icon: IconName = isStarred ? "star-filled" : "star";
307307

308+
// In "files" mode, always show yellow star when starred
309+
// In "active" mode, only show yellow star when file is also open
310+
const starColor =
311+
mode === "files"
312+
? isStarred
313+
? COLORS.STAR
314+
: COLORS.GRAY_L
315+
: isStarred && item.isopen
316+
? COLORS.STAR
317+
: COLORS.GRAY_L;
318+
308319
return (
309320
<Icon
310321
name={icon}
311322
style={{
312323
...ICON_STYLE,
313-
color: isStarred && item.isopen ? COLORS.STAR : COLORS.GRAY_L,
324+
color: starColor,
314325
}}
315326
onClick={(e: React.MouseEvent) => {
316327
e?.stopPropagation();

src/packages/frontend/project/page/flyouts/files-header.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,10 @@ export function FilesHeader(props: Readonly<Props>): React.JSX.Element {
218218
);
219219
}
220220

221-
function renderSortButton(name: string, display: string): React.JSX.Element {
221+
function renderSortButton(
222+
name: string,
223+
display: string | React.JSX.Element,
224+
): React.JSX.Element {
222225
const isActive = activeFileSort.get("column_name") === name;
223226
const direction = isActive ? (
224227
<Icon
@@ -319,7 +322,9 @@ export function FilesHeader(props: Readonly<Props>): React.JSX.Element {
319322
<FormattedMessage
320323
id="page.flyouts.files.stale-directory.description"
321324
defaultMessage={"To update, <A>start this project</A>."}
322-
description={"to update the outdated information in a file directory listing of a project"}
325+
description={
326+
"to update the outdated information in a file directory listing of a project"
327+
}
323328
values={{
324329
A: (c) => (
325330
<a
@@ -386,6 +391,10 @@ export function FilesHeader(props: Readonly<Props>): React.JSX.Element {
386391
}}
387392
>
388393
<Radio.Group size="small">
394+
{renderSortButton(
395+
"starred",
396+
<Icon name="star-filled" style={{ fontSize: "10pt" }} />,
397+
)}
389398
{renderSortButton("name", "Name")}
390399
{renderSortButton("size", "Size")}
391400
{renderSortButton("time", "Time")}

src/packages/frontend/project/page/flyouts/files.tsx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export function FilesFlyout({
9999
isRunning: projectIsRunning,
100100
project_id,
101101
actions,
102+
manageStarredFiles,
102103
} = useProjectContext();
103104
const isMountedRef = useIsMountedRef();
104105
const rootRef = useRef<HTMLDivElement>(null as any);
@@ -239,6 +240,21 @@ export function FilesFlyout({
239240
const aExt = a.name.split(".").pop() ?? "";
240241
const bExt = b.name.split(".").pop() ?? "";
241242
return aExt.localeCompare(bExt);
243+
case "starred":
244+
const pathA = path_to_file(current_path, a.name);
245+
const pathB = path_to_file(current_path, b.name);
246+
const starPathA = a.isdir ? `${pathA}/` : pathA;
247+
const starPathB = b.isdir ? `${pathB}/` : pathB;
248+
const starredA = manageStarredFiles.starred.includes(starPathA);
249+
const starredB = manageStarredFiles.starred.includes(starPathB);
250+
251+
if (starredA && !starredB) {
252+
return -1;
253+
} else if (!starredA && starredB) {
254+
return 1;
255+
} else {
256+
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
257+
}
242258
default:
243259
console.warn(`flyout/files: unknown sort column ${col}`);
244260
return 0;
@@ -257,7 +273,7 @@ export function FilesFlyout({
257273
}
258274

259275
if (activeFileSort.get("is_descending")) {
260-
procFiles.reverse(); // inplace op
276+
procFiles.reverse(); // in-place op
261277
}
262278

263279
const isEmpty = procFiles.length === 0;
@@ -451,7 +467,7 @@ export function FilesFlyout({
451467
window.getSelection()?.removeAllRanges();
452468
const file = directoryFiles[index];
453469

454-
// doubleclick straight to open file
470+
// double click straight to open file
455471
if (e.detail === 2) {
456472
setPrevSelected(index);
457473
open(e, index);
@@ -578,6 +594,9 @@ export function FilesFlyout({
578594
: checked_files.includes(
579595
path_to_file(current_path, directoryFiles[index].name),
580596
);
597+
const fullPath = path_to_file(current_path, item.name);
598+
const pathForStar = item.isdir ? `${fullPath}/` : fullPath;
599+
const isStarred = manageStarredFiles.starred.includes(pathForStar);
581600
return (
582601
<FileListItem
583602
mode="files"
@@ -606,6 +625,12 @@ export function FilesFlyout({
606625
toggleSelected(index, item.name, nextState);
607626
}}
608627
checked_files={checked_files}
628+
isStarred={isStarred}
629+
onStar={(starState: boolean) => {
630+
const normalizedPath =
631+
item.isdir && !fullPath.endsWith("/") ? `${fullPath}/` : fullPath;
632+
manageStarredFiles.setStarredPath(normalizedPath, starState);
633+
}}
609634
/>
610635
);
611636
}

0 commit comments

Comments
 (0)