Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions apps/roam/src/components/discourse-nodes/Attributes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { Button, InputGroup, Label, HTMLSelect } from "@blueprintjs/core";
import Description from "roamjs-components/components/Description";
import React, { useRef, useState } from "react";
import { setDiscourseNodeSetting } from "~/components/settings/block-prop/utils/accessors";

type Attribute = {
label: string;
value: string;
};

const NodeAttribute = ({
label,
value,
onChange,
onDelete,
}: Attribute & { onChange: (v: string) => void; onDelete: () => void }) => {
const timeoutRef = useRef(0);
return (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<Label style={{ minWidth: 120 }}>{label}</Label>
<InputGroup
value={value}
className="roamjs-attribute-value"
onChange={(e) => {
clearTimeout(timeoutRef.current);
onChange(e.target.value);
timeoutRef.current = window.setTimeout(() => {}, 500);
}}
/>
<Button
icon={"delete"}
style={{ minWidth: 32 }}
onClick={onDelete}
minimal
/>
</div>
);
};

type AttributesProps = {
nodeType: string;
attributes: Record<string, string>;
overlay: string;
};

const Attributes = ({ nodeType, attributes, overlay }: AttributesProps) => {
const [localAttributes, setLocalAttributes] = useState<Attribute[]>(() =>
Object.entries(attributes).map(([label, value]) => ({ label, value })),
);
const [newAttribute, setNewAttribute] = useState("");
const [selectedOverlay, setSelectedOverlay] = useState(overlay);
const timeoutRef = useRef(0);

const saveAttribute = (label: string, value: string) => {
clearTimeout(timeoutRef.current);
timeoutRef.current = window.setTimeout(() => {
setDiscourseNodeSetting(nodeType, ["attributes", label], value);
}, 500);
};

const deleteAttribute = (label: string) => {
const newAttrs = { ...attributes };
delete newAttrs[label];
setDiscourseNodeSetting(nodeType, ["attributes"], newAttrs);
};

return (
<div>
<div style={{ marginBottom: 32 }}>
{localAttributes.map((a) => (
<NodeAttribute
key={a.label}
{...a}
onChange={(v) => {
saveAttribute(a.label, v);
setLocalAttributes(
localAttributes.map((aa) =>
a.label === aa.label ? { ...a, value: v } : aa,
),
);
}}
onDelete={() => {
deleteAttribute(a.label);
setLocalAttributes(
localAttributes.filter((aa) => a.label !== aa.label),
);
}}
/>
))}
</div>
<div>
<Label style={{ marginBottom: 8 }}>Attribute Label</Label>
<div style={{ display: "flex", alignItems: "center" }}>
<InputGroup
value={newAttribute}
onChange={(e) => setNewAttribute(e.target.value)}
/>
<Button
text={"Add"}
rightIcon={"plus"}
style={{ marginLeft: 16 }}
onClick={() => {
const DEFAULT = "{count:Has Any Relation To:any}";
setDiscourseNodeSetting(nodeType, ["attributes", newAttribute], DEFAULT);
setLocalAttributes([
...localAttributes,
{ label: newAttribute, value: DEFAULT },
]);
setNewAttribute("");
}}
/>
</div>
</div>
<div style={{ marginTop: 24 }}>
<Label>
Overlay
<Description description="Select which attribute is used for the Discourse Overlay" />
<HTMLSelect
value={selectedOverlay}
onChange={(e) => {
setDiscourseNodeSetting(nodeType, ["overlay"], e.target.value);
setSelectedOverlay(e.target.value);
}}
options={["", ...localAttributes.map((a) => a.label)]}
/>
</Label>
</div>
</div>
);
};

export default Attributes;
163 changes: 163 additions & 0 deletions apps/roam/src/components/discourse-nodes/CanvasSettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import {
InputGroup,
Label,
Radio,
RadioGroup,
Tooltip,
Icon,
ControlGroup,
Checkbox,
} from "@blueprintjs/core";
import React, { useState } from "react";
import { setDiscourseNodeSetting } from "~/components/settings/block-prop/utils/accessors";

export const formatHexColor = (color: string) => {
if (!color) return "";
const COLOR_TEST = /^[0-9a-f]{6}$/i;
if (color.startsWith("#")) {
return color;
} else if (COLOR_TEST.test(color)) {
return "#" + color;
}
return "";
};

type CanvasSettingsProps = {
nodeType: string;
canvasSettings: Record<string, string>;
graphOverview: boolean;
};

const CanvasSettings = ({ nodeType, canvasSettings, graphOverview }: CanvasSettingsProps) => {
const [color, setColor] = useState<string>(() =>
formatHexColor(canvasSettings.color || ""),
);
const [alias, setAlias] = useState<string>(canvasSettings.alias || "");
const [queryBuilderAlias, setQueryBuilderAlias] = useState<string>(
canvasSettings["query-builder-alias"] || "",
);
const [isKeyImage, setIsKeyImage] = useState(
canvasSettings["key-image"] === "true",
);
const [keyImageOption, setKeyImageOption] = useState(
canvasSettings["key-image-option"] || "first-image",
);
const [isGraphOverview, setIsGraphOverview] = useState(graphOverview);

const saveCanvasSetting = (key: string, value: string) => {
setDiscourseNodeSetting(nodeType, ["canvasSettings", key], value);
};

return (
<div>
<Checkbox
className="mb-4"
checked={isGraphOverview}
onChange={(e) => {
const target = e.target as HTMLInputElement;
setIsGraphOverview(target.checked);
setDiscourseNodeSetting(nodeType, ["graphOverview"], target.checked);
}}
>
Graph Overview
<Tooltip content={"Include this node type in the graph overview"}>
<Icon
icon={"info-sign"}
iconSize={12}
className={"ml-2 align-middle opacity-80"}
/>
</Tooltip>
</Checkbox>
<div className="mb-4">
<Label style={{ marginBottom: "4px" }}>Color Picker</Label>
<ControlGroup>
<InputGroup
style={{ width: 120 }}
type={"color"}
value={color}
onChange={(e) => {
setColor(e.target.value);
saveCanvasSetting("color", e.target.value.replace("#", ""));
}}
/>
<Tooltip content={color ? "Unset" : "Color not set"}>
<Icon
className={"ml-2 align-middle opacity-80"}
icon={color ? "delete" : "info-sign"}
onClick={() => {
setColor("");
saveCanvasSetting("color", "");
}}
/>
</Tooltip>
</ControlGroup>
</div>
<Label style={{ width: 240 }}>
Display Alias
<InputGroup
value={alias}
onChange={(e) => {
setAlias(e.target.value);
saveCanvasSetting("alias", e.target.value);
}}
/>
</Label>
<Checkbox
style={{ width: 240, lineHeight: "normal" }}
checked={isKeyImage}
onChange={(e) => {
const target = e.target as HTMLInputElement;
setIsKeyImage(target.checked);
if (target.checked) {
if (!keyImageOption) setKeyImageOption("first-image");
saveCanvasSetting("key-image", "true");
} else {
saveCanvasSetting("key-image", "false");
}
}}
>
Key Image
<Tooltip content={"Add an image to the Discourse Node"}>
<Icon
icon={"info-sign"}
iconSize={12}
className={"ml-2 align-middle opacity-80"}
/>
</Tooltip>
</Checkbox>
<RadioGroup
disabled={!isKeyImage}
selectedValue={keyImageOption || "first-image"}
label="Key Image Location"
onChange={(e) => {
const target = e.target as HTMLInputElement;
setKeyImageOption(target.value);
saveCanvasSetting("key-image-option", target.value);
}}
>
<Radio label="First image on page" value="first-image" />
<Radio value="query-builder">
Query Builder reference
<Tooltip content={"Use a Query Builder alias or block reference"}>
<Icon
icon={"info-sign"}
iconSize={12}
className={"ml-2 align-middle opacity-80"}
/>
</Tooltip>
</Radio>
</RadioGroup>
<InputGroup
style={{ width: 240 }}
disabled={keyImageOption !== "query-builder" || !isKeyImage}
value={queryBuilderAlias}
onChange={(e) => {
setQueryBuilderAlias(e.target.value);
saveCanvasSetting("query-builder-alias", e.target.value);
}}
/>
</div>
);
};

export default CanvasSettings;
Loading