Skip to content

Commit 5bbe56e

Browse files
committed
[gephi-lite] Improves Data table
Details: - Improves updateNode and updateEdge actions - Data cells are now editable
1 parent 0450d29 commit 5bbe56e

File tree

9 files changed

+155
-15
lines changed

9 files changed

+155
-15
lines changed

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/gephi-lite/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"@octokit/auth-oauth-device": "^7.1.2",
3939
"@octokit/core": "^6.1.3",
4040
"@ouestware/atoms": "^0.0.1-alpha.3",
41+
"@ouestware/hooks": "^0.0.1-alpha.6",
4142
"@react-sigma/core": "^5.0.2",
4243
"@sigma/node-image": "^3.0.0",
4344
"@tanstack/react-table": "^8.21.3",

packages/gephi-lite/src/components/Dropdown.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@ const Dropdown: FC<{ children: ReactNode; options: Option[] }> = ({ children: ta
2424
<div className="dropdown-menu show over-modal position-relative">
2525
{options.map((option, i) =>
2626
option.type === "divider" ? (
27-
<div className="dropdown-divider" key={i} />
27+
<div key={i} className="dropdown-divider" />
2828
) : option.type === "text" ? (
29-
<div className="dropdown-item-text">{option.content}</div>
29+
<div key={i} className="dropdown-item-text">
30+
{option.content}
31+
</div>
3032
) : (
3133
<button key={i} className={cx("dropdown-item", option.disabled && "disabled")} onClick={option.onClick}>
3234
{option.label}

packages/gephi-lite/src/core/graph/index.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -241,9 +241,16 @@ const createEdge: Producer<GraphDataset, [string, Attributes, string, string]> =
241241
};
242242
};
243243
};
244-
const updateNode: Producer<GraphDataset, [string, Attributes]> = (node, attributes) => {
244+
const updateNode: Producer<GraphDataset, [string, Attributes, { merge?: boolean }?]> = (
245+
node,
246+
attributes,
247+
{ merge } = {},
248+
) => {
245249
return (state) => {
246-
const { data, renderingData } = cleanNode(node, attributes);
250+
const { data, renderingData } = cleanNode(
251+
node,
252+
merge ? { ...state.nodeData[node], ...state.nodeRenderingData[node], ...attributes } : attributes,
253+
);
247254
const newNodeFieldModel = newItemModel<"nodes">("nodes", data, state.nodeFields);
248255
return {
249256
...state,
@@ -253,9 +260,16 @@ const updateNode: Producer<GraphDataset, [string, Attributes]> = (node, attribut
253260
};
254261
};
255262
};
256-
const updateEdge: Producer<GraphDataset, [string, Attributes]> = (edge, attributes) => {
263+
const updateEdge: Producer<GraphDataset, [string, Attributes, { merge?: boolean }?]> = (
264+
edge,
265+
attributes,
266+
{ merge } = {},
267+
) => {
257268
return (state) => {
258-
const { data, renderingData } = cleanEdge(edge, attributes);
269+
const { data, renderingData } = cleanEdge(
270+
edge,
271+
merge ? { ...state.edgeData[edge], ...state.edgeRenderingData[edge], ...attributes } : attributes,
272+
);
259273
const newEdgeFieldModel = newItemModel<"edges">("edges", data, state.edgeFields);
260274
return {
261275
...state,

packages/gephi-lite/src/styles/_data-table.scss

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
@use "sass:map";
2+
13
.data-table {
24
background: $gray-100;
35
border: $table-border-color;
@@ -90,6 +92,17 @@
9092

9193
td {
9294
display: flex;
95+
96+
padding: map.get($spacers, 1);
97+
&.editable {
98+
padding: 0;
99+
100+
.data-cell {
101+
width: 100%;
102+
padding: map.get($spacers, 1);
103+
@extend .text-ellipsis;
104+
}
105+
}
93106
}
94107
}
95108
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { ItemType, Scalar } from "@gephi/gephi-lite-sdk";
2+
import { FC, MouseEventHandler, forwardRef, useEffect, useRef, useState } from "react";
3+
import ReactLinkify from "react-linkify";
4+
import TetherComponent from "react-tether";
5+
6+
import { useGraphDatasetActions } from "../../../core/context/dataContexts";
7+
import { DEFAULT_LINKIFY_PROPS } from "../../../utils/url";
8+
9+
export const ReadDataCell = forwardRef<HTMLSpanElement, { value: Scalar; onDoubleClick?: MouseEventHandler }>(
10+
({ value, onDoubleClick }, ref) => {
11+
return (
12+
<span ref={ref} className="data-cell" onDoubleClick={onDoubleClick}>
13+
<ReactLinkify {...DEFAULT_LINKIFY_PROPS}>{value}</ReactLinkify>
14+
</span>
15+
);
16+
},
17+
);
18+
19+
export const EditDataCell: FC<{ type: ItemType; id: string; field: string; value: Scalar; close: () => void }> = ({
20+
type,
21+
id,
22+
field,
23+
close,
24+
value: initialValue,
25+
}) => {
26+
const [value, setValue] = useState(initialValue + "");
27+
const { updateNode, updateEdge } = useGraphDatasetActions();
28+
const update = type === "nodes" ? updateNode : updateEdge;
29+
30+
const targetWrapper = useRef<HTMLDivElement>(null);
31+
const elementWrapper = useRef<HTMLFormElement>(null);
32+
33+
// Handle interactions:
34+
useEffect(() => {
35+
const handleClickBody = (e: MouseEvent) => {
36+
if (!elementWrapper.current || !targetWrapper.current) return;
37+
38+
const node = e.target as Node;
39+
if (!elementWrapper.current.contains(node) && !targetWrapper.current.contains(node)) {
40+
close();
41+
}
42+
};
43+
44+
setTimeout(() => {
45+
document.body.addEventListener("click", handleClickBody);
46+
}, 0);
47+
return () => {
48+
document.body.removeEventListener("click", handleClickBody);
49+
};
50+
}, [close]);
51+
52+
return (
53+
<TetherComponent
54+
attachment="top left"
55+
targetAttachment="top left"
56+
className=""
57+
constraints={[{ to: "scrollparent", attachment: "together", pin: true }]}
58+
renderTarget={(ref) => (
59+
<div ref={ref}>
60+
<ReadDataCell ref={targetWrapper} value={value} />
61+
</div>
62+
)}
63+
renderElement={(ref) => (
64+
<div ref={ref}>
65+
<form
66+
ref={elementWrapper}
67+
className="bg-light"
68+
onSubmit={(e) => {
69+
e.preventDefault();
70+
update(id, { [field]: value }, { merge: true });
71+
close();
72+
}}
73+
>
74+
<textarea autoFocus className="form-control" value={value} onChange={(e) => setValue(e.target.value)} />
75+
<div className="text-end">
76+
<button className="btn btn-small">Save cell</button>
77+
</div>
78+
</form>
79+
</div>
80+
)}
81+
/>
82+
);
83+
};
84+
85+
export const DataCell: FC<{ type: ItemType; id: string; field: string; value: Scalar }> = (props) => {
86+
const [isEditing, setIsEditing] = useState(false);
87+
88+
return !isEditing ? (
89+
<ReadDataCell value={props.value} onDoubleClick={() => setIsEditing(true)} />
90+
) : (
91+
<EditDataCell {...props} close={() => setIsEditing(false)} />
92+
);
93+
};

packages/gephi-lite/src/views/dataPage/dataTable/DataTable.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ const TableBodyRow: FC<{
3030
>
3131
{row.getVisibleCells().map((cell) => {
3232
return (
33-
<td key={cell.id} style={{ ...getCommonPinningStyles(cell.column) }}>
33+
<td
34+
key={cell.id}
35+
style={{ ...getCommonPinningStyles(cell.column) }}
36+
className={cx(!(cell.column.id in SPECIFIC_COLUMNS) && "editable")}
37+
>
3438
{flexRender(cell.column.columnDef.cell, cell.getContext())}
3539
</td>
3640
);

packages/gephi-lite/src/views/dataPage/dataTable/useDataTableColumns.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { ColumnDef, createColumnHelper } from "@tanstack/react-table";
22
import { useMemo } from "react";
33
import { PiDotsThreeVertical } from "react-icons/pi";
4-
import ReactLinkify from "react-linkify";
54

65
import Dropdown from "../../../components/Dropdown";
76
import { EdgeComponentById } from "../../../components/Edge";
@@ -14,8 +13,8 @@ import {
1413
useSelectionActions,
1514
} from "../../../core/context/dataContexts";
1615
import { useModal } from "../../../core/modals";
17-
import { DEFAULT_LINKIFY_PROPS } from "../../../utils/url";
1816
import ConfirmModal from "../../graphPage/modals/ConfirmModal";
17+
import { DataCell } from "./DataCell";
1918
import { Arrow, ItemRow, SPECIFIC_COLUMNS } from "./consts";
2019

2120
function getReadOnlyColumn(field: keyof ItemRow, size = 180): ColumnDef<ItemRow> {
@@ -35,7 +34,9 @@ function getReadOnlyColumn(field: keyof ItemRow, size = 180): ColumnDef<ItemRow>
3534
arrow={header.column.getIsSorted() || null}
3635
wrapper={({ children }) => (
3736
<div>
38-
<button className="btn small p-0">{children}</button>
37+
<button className="btn small p-0" onClick={header.column.getToggleSortingHandler()}>
38+
{children}
39+
</button>
3940
</div>
4041
)}
4142
/>
@@ -251,9 +252,12 @@ export const useDataTableColumns = (itemIDs: string[]) => {
251252
</>
252253
),
253254
cell: (props) => (
254-
<span className="text-ellipsis">
255-
<ReactLinkify {...DEFAULT_LINKIFY_PROPS}>{props.row.getValue(`field::${field.id}`)}</ReactLinkify>
256-
</span>
255+
<DataCell
256+
type={type}
257+
id={props.row.getValue("id")}
258+
field={field.id}
259+
value={props.row.getValue(`field::${field.id}`)}
260+
/>
257261
),
258262
})),
259263
],

packages/gephi-lite/src/views/graphPage/modals/edition/UpdateNodeModal.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { toNumber } from "@gephi/gephi-lite-sdk";
1+
import { FieldModel, NodeRenderingData, toNumber } from "@gephi/gephi-lite-sdk";
22
import cx from "classnames";
33
import { fromPairs, isNil, omit, pick } from "lodash";
44
import { FC, useMemo } from "react";
@@ -10,7 +10,6 @@ import { FaTimes } from "react-icons/fa";
1010

1111
import { Modal } from "../../../../components/modals";
1212
import { useGraphDataset, useGraphDatasetActions, useSelectionActions } from "../../../../core/context/dataContexts";
13-
import { FieldModel, NodeRenderingData } from "../../../../core/graph/types";
1413
import { ModalProps } from "../../../../core/modals/types";
1514
import { useNotifications } from "../../../../core/notifications";
1615
import { Scalar } from "../../../../core/types";

0 commit comments

Comments
 (0)