Skip to content

Commit e5bfb10

Browse files
committed
Merge branch 'master' into issue-7850
2 parents 2e1828c + 96adf43 commit e5bfb10

File tree

10 files changed

+268
-138
lines changed

10 files changed

+268
-138
lines changed

src/packages/frontend/components/smart-anchor-tag.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ import { file_associations } from "@cocalc/frontend/file-associations";
2020
import { isCoCalcURL, parseCoCalcURL } from "@cocalc/frontend/lib/cocalc-urls";
2121
import Fragment, { FragmentId } from "@cocalc/frontend/misc/fragment-id";
2222
import { ProjectTitle } from "@cocalc/frontend/projects/project-title";
23-
import { filename_extension, path_split } from "@cocalc/util/misc";
23+
import {
24+
containingPath,
25+
filename_extension,
26+
path_split,
27+
} from "@cocalc/util/misc";
2428
import { TITLE as SERVERS_TITLE } from "../project/servers";
2529
import { alert_message } from "@cocalc/frontend/alerts";
2630

@@ -345,7 +349,8 @@ function InternalRelativeLink({ project_id, path, href, title, children }) {
345349
onClick={(e) => {
346350
e.preventDefault();
347351
e.stopPropagation();
348-
const url = new URL("http://dummy/" + href);
352+
const dir = containingPath(path);
353+
const url = new URL("http://dummy/" + join(dir, href));
349354
const fragmentId = Fragment.decode(url.hash);
350355
const hrefPlain = url.pathname.slice(1);
351356
let target;
@@ -355,11 +360,7 @@ function InternalRelativeLink({ project_id, path, href, title, children }) {
355360
} else {
356361
// different file in the same project, with link being relative
357362
// to current path.
358-
target = join(
359-
"files",
360-
path ? path_split(path).head : "",
361-
decodeURI(hrefPlain),
362-
);
363+
target = join("files", decodeURI(hrefPlain));
363364
}
364365
loadTarget(
365366
"projects",

src/packages/frontend/frame-editors/jupyter-editor/editor.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ const JUPYTER_MENUS = {
369369
"cell toolbar metadata",
370370
"cell toolbar attachments",
371371
"cell toolbar tags",
372+
"cell toolbar ids",
372373
],
373374
},
374375
{
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3+
* License: MS-RSL – see LICENSE.md for details
4+
*/
5+
6+
/*
7+
The tag editing toolbar functionality for cells.
8+
*/
9+
10+
import { Button, Input, Space, Tooltip } from "antd";
11+
import { useMemo, useState } from "@cocalc/frontend/app-framework";
12+
import { A } from "@cocalc/frontend/components";
13+
import { Map as ImmutableMap } from "immutable";
14+
import type { JupyterActions } from "./browser-actions";
15+
import { useEffect } from "react";
16+
import useNotebookFrameActions from "@cocalc/frontend/frame-editors/jupyter-editor/cell-notebook/hook";
17+
18+
interface Props {
19+
actions: JupyterActions;
20+
cell: ImmutableMap<string, any>;
21+
}
22+
23+
export default function IdsToolbar({ actions, cell }: Props) {
24+
const [input, setInput] = useState<string>(cell.get("id"));
25+
const frameActions = useNotebookFrameActions();
26+
27+
useEffect(() => {
28+
setInput(cell.get("id"));
29+
}, [cell.get("id")]);
30+
31+
const valid = useMemo(() => {
32+
return isValid(input);
33+
}, [input]);
34+
35+
function setId() {
36+
let id = input;
37+
if (!valid || id == cell.get("id")) {
38+
return;
39+
}
40+
const ids = new Set(actions.store.get_cell_ids_list() ?? []);
41+
if (ids.has(id)) {
42+
let n = 1;
43+
let pattern = `-${n}`;
44+
while (ids.has(`${id.slice(0, 64 - pattern.length)}${pattern}`)) {
45+
n += 1;
46+
pattern = `-${n}`;
47+
}
48+
id = `${id.slice(0, 64 - pattern.length)}${pattern}`;
49+
}
50+
if (id != cell.get("id")) {
51+
const frame = frameActions.current;
52+
actions.setCellId(cell.get("id"), id);
53+
setTimeout(() => frame?.set_cur_id(id), 1);
54+
}
55+
}
56+
57+
return (
58+
<div style={{ width: "100%", paddingTop: "2.5px" }}>
59+
<Space style={{ float: "right" }}>
60+
<Input
61+
onFocus={() => actions.blur_lock()}
62+
onBlur={() => {
63+
actions.focus_unlock();
64+
setId();
65+
}}
66+
value={input}
67+
onChange={(e) => setInput(e.target.value)}
68+
size="small"
69+
onPressEnter={() => {
70+
setId();
71+
}}
72+
/>
73+
<Tooltip
74+
title={
75+
<>
76+
<A
77+
style={{ color: "white" }}
78+
href="https://jupyter.org/enhancement-proposals/62-cell-id/cell-id.html"
79+
>
80+
Jupyter cell IDs
81+
</A>{" "}
82+
must be between 1 and 64 characters and use only letters, numbers,
83+
dashes and underscores.
84+
</>
85+
}
86+
>
87+
<Button
88+
size="small"
89+
danger={!valid}
90+
type={"primary"}
91+
disabled={input == cell.get("id")}
92+
onClick={setId}
93+
>
94+
{!valid ? "Invalid Id" : "Cell Id"}
95+
</Button>
96+
</Tooltip>
97+
</Space>
98+
</div>
99+
);
100+
}
101+
102+
const regExp = /^[a-zA-Z0-9-_]{1,64}$/;
103+
function isValid(id: string) {
104+
// true if it matches the regexp ^[a-zA-Z0-9-_]+$ and is between 1 and 64 characters in length,
105+
// as defined in https://github.com/jupyter/nbformat/blob/main/nbformat/v4/nbformat.v4.5.schema.json#L97
106+
return regExp.test(id);
107+
}

src/packages/frontend/jupyter/cell-toolbar-tags.tsx

Lines changed: 60 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -7,123 +7,87 @@
77
The tag editing toolbar functionality for cells.
88
*/
99

10-
import { Button, FormControl } from "@cocalc/frontend/antd-bootstrap";
11-
import { React, useState } from "../app-framework";
10+
import { Button, Input } from "antd";
11+
import { useState } from "@cocalc/frontend/app-framework";
1212
import { Map as ImmutableMap } from "immutable";
13-
import { Icon } from "../components";
14-
import * as misc from "@cocalc/util/misc";
13+
import { Icon } from "@cocalc/frontend/components";
14+
import { keys, split } from "@cocalc/util/misc";
1515
import { JupyterActions } from "./browser-actions";
1616

17-
const TAG_STYLE: React.CSSProperties = {
17+
const TAG_STYLE = {
1818
padding: "3px 5px",
1919
margin: "3px 3px",
2020
background: "#5bc0de",
2121
borderRadius: "3px",
2222
color: "white",
2323
display: "inline-block",
24-
};
24+
} as const;
2525

26-
interface TagsToolbarProps {
26+
interface Props {
2727
actions: JupyterActions;
2828
cell: ImmutableMap<string, any>;
2929
}
3030

31-
export const TagsToolbar: React.FC<TagsToolbarProps> = React.memo(
32-
(props: TagsToolbarProps) => {
33-
const { actions, cell } = props;
34-
const [input, set_input] = useState<string>("");
31+
export default function TagsToolbar({ actions, cell }: Props) {
32+
const [input, setInput] = useState<string>("");
3533

36-
function remove_tag(tag: string): void {
37-
actions.remove_tag(cell.get("id"), tag);
38-
}
39-
40-
function render_tag(tag: string) {
41-
return (
42-
<span key={tag} style={TAG_STYLE}>
43-
{tag}
44-
<Icon
45-
name="times"
46-
style={{ marginLeft: "5px", cursor: "pointer" }}
47-
onClick={() => remove_tag(tag)}
48-
/>
49-
</span>
50-
);
51-
}
52-
53-
function render_tags() {
54-
const tags = cell.get("tags");
55-
if (tags == null) {
56-
return;
57-
}
58-
// TODO: skip toJS call and just use immutable functions?
59-
return (
60-
<div style={{ flex: 1 }}>
61-
{misc
62-
.keys(tags.toJS())
63-
.sort()
64-
.map((tag) => render_tag(tag))}
65-
</div>
66-
);
67-
}
68-
69-
function render_tag_input() {
70-
return (
71-
<FormControl
72-
onFocus={actions.blur_lock}
73-
onBlur={actions.focus_unlock}
74-
type="text"
75-
value={input}
76-
onChange={(e: any) => set_input(e.target.value)}
77-
style={{ height: "34px" }}
78-
bsSize={"small"}
79-
onKeyDown={(e) => {
80-
if (e.which === 13) {
81-
add_tags();
82-
return;
83-
}
84-
}}
34+
function renderTag(tag: string) {
35+
return (
36+
<span key={tag} style={TAG_STYLE}>
37+
{tag}
38+
<Icon
39+
name="times"
40+
style={{ marginLeft: "5px", cursor: "pointer" }}
41+
onClick={() => actions.remove_tag(cell.get("id"), tag)}
8542
/>
86-
);
87-
}
43+
</span>
44+
);
45+
}
8846

89-
function add_tags() {
90-
for (const tag of misc.split(input)) {
91-
actions.add_tag(cell.get("id"), tag, false);
92-
}
93-
actions._sync();
94-
set_input("");
47+
function renderTags() {
48+
const tags = cell.get("tags");
49+
if (tags == null) {
50+
return;
9551
}
52+
// TODO: skip toJS call and just use immutable functions?
53+
return (
54+
<div style={{ flex: 1 }}>{keys(tags.toJS()).sort().map(renderTag)}</div>
55+
);
56+
}
9657

97-
function render_add_button() {
98-
return (
99-
<Button
100-
bsSize="small"
101-
disabled={input.length === 0}
102-
title="Add tag or tags (separate by spaces)"
103-
onClick={add_tags}
104-
style={{ height: "34px" }}
105-
>
106-
Add
107-
</Button>
108-
);
58+
function addTags() {
59+
for (const tag of split(input)) {
60+
actions.add_tag(cell.get("id"), tag, false);
10961
}
62+
actions._sync();
63+
setInput("");
64+
}
11065

111-
function render_input() {
112-
return (
66+
return (
67+
<div style={{ width: "100%" }}>
68+
<div style={{ display: "flex", float: "right" }}>
69+
{renderTags()}
11370
<div style={{ display: "flex" }}>
114-
{render_tag_input()}
115-
{render_add_button()}
116-
</div>
117-
);
118-
}
119-
120-
return (
121-
<div style={{ width: "100%" }}>
122-
<div style={{ display: "flex", float: "right" }}>
123-
{render_tags()}
124-
{render_input()}
71+
<Input
72+
onFocus={actions.blur_lock}
73+
onBlur={actions.focus_unlock}
74+
value={input}
75+
onChange={(e: any) => setInput(e.target.value)}
76+
size="small"
77+
onPressEnter={() => {
78+
addTags();
79+
}}
80+
/>
81+
<Button
82+
size="small"
83+
disabled={input.length === 0}
84+
title="Add tag or tags (separate by spaces)"
85+
onClick={addTags}
86+
>
87+
Add
88+
</Button>
12589
</div>
12690
</div>
127-
);
128-
}
129-
);
91+
</div>
92+
);
93+
}

src/packages/frontend/jupyter/cell-toolbar.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import { JupyterActions } from "./browser-actions";
1515
import { Attachments } from "./cell-toolbar-attachments";
1616
import { Metadata } from "./cell-toolbar-metadata";
1717
import { Slideshow } from "./cell-toolbar-slideshow";
18-
import { TagsToolbar } from "./cell-toolbar-tags";
18+
import TagsToolbar from "./cell-toolbar-tags";
19+
import IdsToolbar from "./cell-toolbar-ids";
1920
import { CreateAssignmentToolbar } from "./nbgrader/cell-toolbar-create-assignment";
2021
import { PROMPT_MIN_WIDTH } from "./prompt/base";
2122

@@ -36,6 +37,7 @@ const TOOLBARS = {
3637
slideshow: Slideshow,
3738
attachments: Attachments,
3839
tags: TagsToolbar,
40+
ids: IdsToolbar,
3941
metadata: Metadata,
4042
create_assignment: CreateAssignmentToolbar,
4143
} as const;

src/packages/frontend/jupyter/commands.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,20 @@ export function commands(actions: AllActions): {
100100
r: true,
101101
},
102102

103+
"cell toolbar ids": {
104+
i: "tags-filled",
105+
m: defineMessage({
106+
id: "jupyter.commands.cell_toolbar_ids.label",
107+
defaultMessage: "Edit cell IDs toolbar",
108+
}),
109+
menu: defineMessage({
110+
id: "jupyter.commands.cell_toolbar_ids.menu",
111+
defaultMessage: "Id's",
112+
}),
113+
f: () => actions.jupyter_actions?.cell_toolbar("ids"),
114+
r: true,
115+
},
116+
103117
"cell toolbar tags": {
104118
i: "tags-outlined",
105119
m: defineMessage({

0 commit comments

Comments
 (0)