Skip to content

Commit 7015148

Browse files
committed
migrate attributes overlay and suggestive rules
1 parent 108d55f commit 7015148

File tree

6 files changed

+235
-151
lines changed

6 files changed

+235
-151
lines changed

apps/roam/src/components/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export { default as DefaultFilters } from "./settings/DefaultFilters";
22
export { default as DiscourseContext } from "./DiscourseContext";
33
export { default as DiscourseContextOverlay } from "./DiscourseContextOverlay";
4-
export { default as DiscourseNodeAttributes } from "./settings/DiscourseNodeAttributes";
4+
export { default as DiscourseNodeAttributes, DiscourseNodeAttributesTab } from "./settings/DiscourseNodeAttributes";
55
export { default as DiscourseNodeCanvasSettings } from "./settings/DiscourseNodeCanvasSettings";
66
export { default as DiscourseNodeIndex } from "./settings/DiscourseNodeIndex";
77
export { default as DiscourseNodeMenu } from "./DiscourseNodeMenu";
Lines changed: 181 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,22 @@
1-
import { Button, InputGroup, Label } from "@blueprintjs/core";
1+
import { Button, InputGroup, Label, HTMLSelect } from "@blueprintjs/core";
22
import React, { useRef, useState } from "react";
3-
import createBlock from "roamjs-components/writes/createBlock";
4-
import getBasicTreeByParentUid from "roamjs-components/queries/getBasicTreeByParentUid";
5-
import getFirstChildUidByBlockUid from "roamjs-components/queries/getFirstChildUidByBlockUid";
6-
import updateBlock from "roamjs-components/writes/updateBlock";
7-
import deleteBlock from "roamjs-components/writes/deleteBlock";
8-
9-
type Attribute = {
10-
uid: string;
3+
import Description from "roamjs-components/components/Description";
4+
import {
5+
getDiscourseNodeSetting,
6+
setDiscourseNodeSetting,
7+
} from "./utils/accessors";
8+
9+
type AttributeEntry = {
1110
label: string;
1211
value: string;
1312
};
1413

1514
const NodeAttribute = ({
16-
uid,
1715
label,
1816
value,
1917
onChange,
2018
onDelete,
21-
}: Attribute & { onChange: (v: string) => void; onDelete: () => void }) => {
19+
}: AttributeEntry & { onChange: (v: string) => void; onDelete: () => void }) => {
2220
const timeoutRef = useRef(0);
2321
return (
2422
<div
@@ -34,12 +32,10 @@ const NodeAttribute = ({
3432
className="roamjs-attribute-value"
3533
onChange={(e) => {
3634
clearTimeout(timeoutRef.current);
37-
onChange(e.target.value);
35+
const newValue = e.target.value;
36+
onChange(newValue);
3837
timeoutRef.current = window.setTimeout(() => {
39-
updateBlock({
40-
text: e.target.value,
41-
uid: getFirstChildUidByBlockUid(uid),
42-
});
38+
// onChange already updates the parent state which saves
4339
}, 500);
4440
}}
4541
/>
@@ -53,34 +49,60 @@ const NodeAttribute = ({
5349
);
5450
};
5551

56-
const NodeAttributes = ({ uid }: { uid: string }) => {
57-
const [attributes, setAttributes] = useState<Attribute[]>(() =>
58-
getBasicTreeByParentUid(uid).map((t) => ({
59-
uid: t.uid,
60-
label: t.text,
61-
value: t.children[0]?.text,
62-
})),
63-
);
52+
const NodeAttributes = ({ nodeType }: { nodeType: string }) => {
53+
const [attributes, setAttributes] = useState<AttributeEntry[]>(() => {
54+
const record =
55+
getDiscourseNodeSetting<Record<string, string>>(nodeType, [
56+
"attributes",
57+
]) ?? {};
58+
return Object.entries(record).map(([label, value]) => ({ label, value }));
59+
});
6460
const [newAttribute, setNewAttribute] = useState("");
61+
62+
const saveAttributes = (newAttributes: AttributeEntry[]) => {
63+
const record: Record<string, string> = {};
64+
for (const attr of newAttributes) {
65+
record[attr.label] = attr.value;
66+
}
67+
setDiscourseNodeSetting(nodeType, ["attributes"], record);
68+
};
69+
70+
const handleChange = (label: string, newValue: string) => {
71+
const newAttributes = attributes.map((a) =>
72+
a.label === label ? { ...a, value: newValue } : a,
73+
);
74+
setAttributes(newAttributes);
75+
saveAttributes(newAttributes);
76+
};
77+
78+
const handleDelete = (label: string) => {
79+
const newAttributes = attributes.filter((a) => a.label !== label);
80+
setAttributes(newAttributes);
81+
saveAttributes(newAttributes);
82+
};
83+
84+
const handleAdd = () => {
85+
if (!newAttribute.trim()) return;
86+
const DEFAULT = "{count:Has Any Relation To:any}";
87+
const newAttributes = [
88+
...attributes,
89+
{ label: newAttribute.trim(), value: DEFAULT },
90+
];
91+
setAttributes(newAttributes);
92+
saveAttributes(newAttributes);
93+
setNewAttribute("");
94+
};
95+
6596
return (
6697
<div>
6798
<div style={{ marginBottom: 32 }}>
6899
{attributes.map((a) => (
69100
<NodeAttribute
70-
key={a.uid}
71-
{...a}
72-
onChange={(v) =>
73-
setAttributes(
74-
attributes.map((aa) =>
75-
a.uid === aa.uid ? { ...a, value: v } : aa,
76-
),
77-
)
78-
}
79-
onDelete={() =>
80-
deleteBlock(a.uid).then(() =>
81-
setAttributes(attributes.filter((aa) => a.uid !== aa.uid)),
82-
)
83-
}
101+
key={a.label}
102+
label={a.label}
103+
value={a.value}
104+
onChange={(v) => handleChange(a.label, v)}
105+
onDelete={() => handleDelete(a.label)}
84106
/>
85107
))}
86108
</div>
@@ -90,33 +112,137 @@ const NodeAttributes = ({ uid }: { uid: string }) => {
90112
<InputGroup
91113
value={newAttribute}
92114
onChange={(e) => setNewAttribute(e.target.value)}
115+
onKeyDown={(e) => {
116+
if (e.key === "Enter") {
117+
handleAdd();
118+
}
119+
}}
93120
/>
94121
<Button
95122
text={"Add"}
96123
rightIcon={"plus"}
97124
style={{ marginLeft: 16 }}
98-
onClick={() => {
99-
const DEFAULT = "{count:Has Any Relation To:any}";
100-
createBlock({
101-
node: {
102-
text: newAttribute,
103-
children: [{ text: DEFAULT }],
104-
},
105-
parentUid: uid,
106-
order: attributes.length,
107-
}).then((uid) => {
108-
setAttributes([
109-
...attributes,
110-
{ uid, label: newAttribute, value: DEFAULT },
111-
]);
112-
setNewAttribute("");
113-
});
114-
}}
125+
onClick={handleAdd}
126+
disabled={!newAttribute.trim()}
115127
/>
116128
</div>
117129
</div>
118130
</div>
119131
);
120132
};
121133

134+
export const DiscourseNodeAttributesTab = ({
135+
nodeType,
136+
}: {
137+
nodeType: string;
138+
}) => {
139+
const [attributes, setAttributes] = useState<AttributeEntry[]>(() => {
140+
const record =
141+
getDiscourseNodeSetting<Record<string, string>>(nodeType, [
142+
"attributes",
143+
]) ?? {};
144+
return Object.entries(record).map(([label, value]) => ({ label, value }));
145+
});
146+
const [newAttribute, setNewAttribute] = useState("");
147+
const [overlay, setOverlay] = useState<string>(
148+
() => getDiscourseNodeSetting<string>(nodeType, ["overlay"]) ?? "",
149+
);
150+
151+
const saveAttributes = (newAttributes: AttributeEntry[]) => {
152+
const record: Record<string, string> = {};
153+
for (const attr of newAttributes) {
154+
record[attr.label] = attr.value;
155+
}
156+
setDiscourseNodeSetting(nodeType, ["attributes"], record);
157+
};
158+
159+
const handleChange = (label: string, newValue: string) => {
160+
const newAttributes = attributes.map((a) =>
161+
a.label === label ? { ...a, value: newValue } : a,
162+
);
163+
setAttributes(newAttributes);
164+
saveAttributes(newAttributes);
165+
};
166+
167+
const handleDelete = (label: string) => {
168+
const newAttributes = attributes.filter((a) => a.label !== label);
169+
setAttributes(newAttributes);
170+
saveAttributes(newAttributes);
171+
// Clear overlay if deleted attribute was selected
172+
if (overlay === label) {
173+
setOverlay("");
174+
setDiscourseNodeSetting(nodeType, ["overlay"], "");
175+
}
176+
};
177+
178+
const handleAdd = () => {
179+
if (!newAttribute.trim()) return;
180+
const DEFAULT = "{count:Has Any Relation To:any}";
181+
const newAttributes = [
182+
...attributes,
183+
{ label: newAttribute.trim(), value: DEFAULT },
184+
];
185+
setAttributes(newAttributes);
186+
saveAttributes(newAttributes);
187+
setNewAttribute("");
188+
};
189+
190+
const handleOverlayChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
191+
const newValue = e.target.value;
192+
setOverlay(newValue);
193+
setDiscourseNodeSetting(nodeType, ["overlay"], newValue);
194+
};
195+
196+
const attributeLabels = attributes.map((a) => a.label);
197+
198+
return (
199+
<div className="flex flex-col gap-4">
200+
<div>
201+
<div style={{ marginBottom: 32 }}>
202+
{attributes.map((a) => (
203+
<NodeAttribute
204+
key={a.label}
205+
label={a.label}
206+
value={a.value}
207+
onChange={(v) => handleChange(a.label, v)}
208+
onDelete={() => handleDelete(a.label)}
209+
/>
210+
))}
211+
</div>
212+
<div>
213+
<Label style={{ marginBottom: 8 }}>Attribute Label</Label>
214+
<div style={{ display: "flex", alignItems: "center" }}>
215+
<InputGroup
216+
value={newAttribute}
217+
onChange={(e) => setNewAttribute(e.target.value)}
218+
onKeyDown={(e) => {
219+
if (e.key === "Enter") {
220+
handleAdd();
221+
}
222+
}}
223+
/>
224+
<Button
225+
text={"Add"}
226+
rightIcon={"plus"}
227+
style={{ marginLeft: 16 }}
228+
onClick={handleAdd}
229+
disabled={!newAttribute.trim()}
230+
/>
231+
</div>
232+
</div>
233+
</div>
234+
<Label>
235+
Overlay
236+
<Description description="Select which attribute is used for the Discourse Overlay" />
237+
<HTMLSelect
238+
value={overlay}
239+
onChange={handleOverlayChange}
240+
fill
241+
options={["", ...attributeLabels]}
242+
/>
243+
</Label>
244+
</div>
245+
);
246+
};
247+
122248
export default NodeAttributes;

apps/roam/src/components/settings/DiscourseNodeSpecification.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import createBlock from "roamjs-components/writes/createBlock";
44
import { Checkbox } from "@blueprintjs/core";
55
import getBasicTreeByParentUid from "roamjs-components/queries/getBasicTreeByParentUid";
66
import deleteBlock from "roamjs-components/writes/deleteBlock";
7-
import refreshConfigTree from "~/utils/refreshConfigTree";
87
import getDiscourseNodes from "~/utils/getDiscourseNodes";
98
import getDiscourseNodeFormatExpression from "~/utils/getDiscourseNodeFormatExpression";
109
import QueryEditor from "~/components/QueryEditor";
@@ -73,9 +72,6 @@ const NodeSpecification = ({
7372
const scratchNode = getSubTree({ tree, key: "scratch" });
7473
Promise.all(scratchNode.children.map((c) => deleteBlock(c.uid)));
7574
}
76-
return () => {
77-
refreshConfigTree();
78-
};
7975
}, [parentUid, setMigrated, enabled]);
8076
return (
8177
<div className={"roamjs-node-specification"}>

0 commit comments

Comments
 (0)