1- import { Button , InputGroup , Label } from "@blueprintjs/core" ;
1+ import { Button , InputGroup , Label , HTMLSelect } from "@blueprintjs/core" ;
22import 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
1514const 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+
122248export default NodeAttributes ;
0 commit comments