trying in vain to buildmy own json editor
This commit is contained in:
parent
261fe25549
commit
eb75c859f2
52
client/package-lock.json
generated
52
client/package-lock.json
generated
@ -12,6 +12,7 @@
|
|||||||
"@emotion/styled": "^11.13.0",
|
"@emotion/styled": "^11.13.0",
|
||||||
"@libsql/client": "^0.11.0",
|
"@libsql/client": "^0.11.0",
|
||||||
"@mdxeditor/editor": "^3.14.0",
|
"@mdxeditor/editor": "^3.14.0",
|
||||||
|
"@mui/icons-material": "^6.1.5",
|
||||||
"@mui/material": "^6.1.5",
|
"@mui/material": "^6.1.5",
|
||||||
"@react-sigma/core": "^4.0.3",
|
"@react-sigma/core": "^4.0.3",
|
||||||
"@tanstack/react-query": "^5.56.2",
|
"@tanstack/react-query": "^5.56.2",
|
||||||
@ -23,6 +24,7 @@
|
|||||||
"graphology-layout": "^0.6.1",
|
"graphology-layout": "^0.6.1",
|
||||||
"graphology-layout-force": "^0.2.4",
|
"graphology-layout-force": "^0.2.4",
|
||||||
"highlight.js": "^11.10.0",
|
"highlight.js": "^11.10.0",
|
||||||
|
"json-edit-react": "^1.17.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.53.0",
|
"react-hook-form": "^7.53.0",
|
||||||
@ -1994,6 +1996,31 @@
|
|||||||
"url": "https://opencollective.com/mui-org"
|
"url": "https://opencollective.com/mui-org"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@mui/icons-material": {
|
||||||
|
"version": "6.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.1.5.tgz",
|
||||||
|
"integrity": "sha512-SbxFtO5I4cXfvhjAMgGib/t2lQUzcEzcDFYiRHRufZUeMMeXuoKaGsptfwAHTepYkv0VqcCwvxtvtWbpZLAbjQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.25.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/mui-org"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@mui/material": "^6.1.5",
|
||||||
|
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@mui/material": {
|
"node_modules/@mui/material": {
|
||||||
"version": "6.1.5",
|
"version": "6.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.5.tgz",
|
||||||
@ -5515,6 +5542,19 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/json-edit-react": {
|
||||||
|
"version": "1.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-edit-react/-/json-edit-react-1.17.1.tgz",
|
||||||
|
"integrity": "sha512-UDGkBpgPoJplyT2MaPbGbKQhQjgvswaDWNbKCEkorq3r72DVm5qQErvribrc/ioZsqSnzCeDT8EwzMi4L2Az1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"object-property-assigner": "^1.3.0",
|
||||||
|
"object-property-extractor": "^1.0.11"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/json-parse-even-better-errors": {
|
"node_modules/json-parse-even-better-errors": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
|
||||||
@ -6669,6 +6709,18 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/object-property-assigner": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-property-assigner/-/object-property-assigner-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-3R6x1l1sQA4jQBOqxkxH3DHw0PMh7K5UY4x1Yc5QYbbkzZpAJ8l9w+Trq/64ZRe2thiOsa8JpB4M7D61hnkjRA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/object-property-extractor": {
|
||||||
|
"version": "1.0.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-property-extractor/-/object-property-extractor-1.0.12.tgz",
|
||||||
|
"integrity": "sha512-X6rMK1O6O3EU4O06BtATezWKr+5idX6Yo0PndVqr8NlED3Eoa2YE+VKQgQL4EK0+vh+YA83Sjj9Nsi/PuXXeDQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/object.assign": {
|
"node_modules/object.assign": {
|
||||||
"version": "4.1.5",
|
"version": "4.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz",
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
"@emotion/styled": "^11.13.0",
|
"@emotion/styled": "^11.13.0",
|
||||||
"@libsql/client": "^0.11.0",
|
"@libsql/client": "^0.11.0",
|
||||||
"@mdxeditor/editor": "^3.14.0",
|
"@mdxeditor/editor": "^3.14.0",
|
||||||
|
"@mui/icons-material": "^6.1.5",
|
||||||
"@mui/material": "^6.1.5",
|
"@mui/material": "^6.1.5",
|
||||||
"@react-sigma/core": "^4.0.3",
|
"@react-sigma/core": "^4.0.3",
|
||||||
"@tanstack/react-query": "^5.56.2",
|
"@tanstack/react-query": "^5.56.2",
|
||||||
@ -18,6 +19,7 @@
|
|||||||
"graphology-layout": "^0.6.1",
|
"graphology-layout": "^0.6.1",
|
||||||
"graphology-layout-force": "^0.2.4",
|
"graphology-layout-force": "^0.2.4",
|
||||||
"highlight.js": "^11.10.0",
|
"highlight.js": "^11.10.0",
|
||||||
|
"json-edit-react": "^1.17.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.53.0",
|
"react-hook-form": "^7.53.0",
|
||||||
|
@ -1,28 +1,22 @@
|
|||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
import { useForm } from "react-hook-form";
|
import { Fab } from '@mui/material';
|
||||||
|
|
||||||
import './CommandEntry.css';
|
import './CommandEntry.css';
|
||||||
|
|
||||||
const CommandEntry = forwardRef(({ setCommand, onSubmitCommand, command }, ref) => {
|
const CommandEntry = forwardRef(({ setCommand, onSubmitCommand, command }, ref) => {
|
||||||
const { register, handleSubmit, setValue } = useForm();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="commandline" style={{display: "none"}}>
|
<div className="commandline" >
|
||||||
<form onSubmit={
|
<input
|
||||||
handleSubmit((data) => { onSubmitCommand(data.command); })
|
ref={ref}
|
||||||
}>
|
onChange={(e) => setCommand(e.target.value)}
|
||||||
<input {...register("command")}
|
autoFocus
|
||||||
ref={ref}
|
type="text"
|
||||||
onChange={(e) => {
|
placeholder="Enter a command!"
|
||||||
setCommand(e.target.value);
|
value={command} />
|
||||||
setValue("command", e.target.value);
|
<Fab onClick={ () => onSubmitCommand(command) } >
|
||||||
}}
|
Send
|
||||||
autoFocus
|
</Fab>
|
||||||
type="text"
|
|
||||||
placeholder="Enter a command!"
|
|
||||||
value={command} />
|
|
||||||
<button type="submit">Do</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -9,7 +9,7 @@ import useWebSocket, { ReadyState } from 'react-use-websocket';
|
|||||||
import Page from '../page/Page.jsx';
|
import Page from '../page/Page.jsx';
|
||||||
import ExtendedAttributes from '../page/ExtendedAttributes.jsx';
|
import ExtendedAttributes from '../page/ExtendedAttributes.jsx';
|
||||||
import MessageFeed from './MessageFeed.jsx';
|
import MessageFeed from './MessageFeed.jsx';
|
||||||
import Sidebar from './Sidebar.jsx';
|
import WordSidebar from './WordSidebar.jsx';
|
||||||
import CommandEntry from './CommandEntry.jsx';
|
import CommandEntry from './CommandEntry.jsx';
|
||||||
|
|
||||||
function Live({...props}) {
|
function Live({...props}) {
|
||||||
@ -23,6 +23,7 @@ function Live({...props}) {
|
|||||||
const [ editing, setEditing ] = useState(false);
|
const [ editing, setEditing ] = useState(false);
|
||||||
const [ connecting, setConnecting ] = useState(true);
|
const [ connecting, setConnecting ] = useState(true);
|
||||||
|
|
||||||
|
const editorRef = useRef(null);
|
||||||
const commandEntryRef = useRef(null);
|
const commandEntryRef = useRef(null);
|
||||||
|
|
||||||
//setting up the websocket and using its data!
|
//setting up the websocket and using its data!
|
||||||
@ -148,6 +149,7 @@ function Live({...props}) {
|
|||||||
onChangeTitle={(e) => setTitle(e.target.value)}
|
onChangeTitle={(e) => setTitle(e.target.value)}
|
||||||
onChangeText={setText}
|
onChangeText={setText}
|
||||||
onChangeType={() => setType(!type)}
|
onChangeType={() => setType(!type)}
|
||||||
|
ref={editorRef}
|
||||||
{...props} />
|
{...props} />
|
||||||
<ExtendedAttributes
|
<ExtendedAttributes
|
||||||
attributes={fetchAttributesQuery.data} />
|
attributes={fetchAttributesQuery.data} />
|
||||||
@ -163,12 +165,12 @@ function Live({...props}) {
|
|||||||
}[readyState]}</button>
|
}[readyState]}</button>
|
||||||
<button onClick={()=> setMessageHistory([])}>Clear History</button>
|
<button onClick={()=> setMessageHistory([])}>Clear History</button>
|
||||||
</MessageFeed>
|
</MessageFeed>
|
||||||
<Sidebar verbs={verbs} sendWord={(word) => {
|
<WordSidebar verbs={verbs} sendWord={(word) => {
|
||||||
setCommand((command + " " + word).trim());
|
setCommand((command + " " + word).trim());
|
||||||
commandEntryRef.current.focus();
|
commandEntryRef.current.focus();
|
||||||
// maybe set focus to the command entry?
|
// maybe set focus to the command entry?
|
||||||
}}>
|
}}>
|
||||||
</Sidebar>
|
</WordSidebar>
|
||||||
<CommandEntry
|
<CommandEntry
|
||||||
ref={commandEntryRef}
|
ref={commandEntryRef}
|
||||||
setCommand={(val) => {setCommand(val)}}
|
setCommand={(val) => {setCommand(val)}}
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import { useEffect, useState, useRef } from 'react';
|
import { useEffect, useState, useRef } from 'react';
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { IconButton, SwipeableDrawer } from '@mui/material';
|
||||||
|
|
||||||
|
import MenuIcon from '@mui/icons-material/Menu';
|
||||||
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
|
||||||
import './MessageFeed.css';
|
import './MessageFeed.css';
|
||||||
|
|
||||||
@ -8,11 +12,34 @@ function MessageFeed({messages=[], children}) {
|
|||||||
const [ open, setOpen ] = useState(false);
|
const [ open, setOpen ] = useState(false);
|
||||||
const listContainer = useRef(null);
|
const listContainer = useRef(null);
|
||||||
|
|
||||||
useEffect(() => listContainer.current.scrollTo(0, listContainer.current.scrollHeight), [listContainer, messages])
|
useEffect(() => {
|
||||||
|
if (listContainer.current)
|
||||||
|
listContainer.current.scrollTo(0, listContainer.current.scrollHeight);
|
||||||
|
}, [listContainer, messages])
|
||||||
|
|
||||||
return (
|
return (<>
|
||||||
<div className={`messages-tray ${!open?'messages-hidden':''}`}>
|
<IconButton
|
||||||
<button style={{float: 'right'}} onClick={() => setOpen(!open)}>{open?'Hide':'Show'}</button>
|
size="large"
|
||||||
|
style={{
|
||||||
|
visible: open,
|
||||||
|
position: "fixed",
|
||||||
|
padding: "2rem",
|
||||||
|
left: 0,
|
||||||
|
top: 0
|
||||||
|
}}
|
||||||
|
onClick={() => setOpen(true)} >
|
||||||
|
<MenuIcon />
|
||||||
|
</IconButton>
|
||||||
|
<SwipeableDrawer
|
||||||
|
anchor="left"
|
||||||
|
open={open}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
onOpen={() => setOpen(true)}>
|
||||||
|
<IconButton
|
||||||
|
variant="text"
|
||||||
|
onClick={() => setOpen(false)} >
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
<h2>Message history:</h2>
|
<h2>Message history:</h2>
|
||||||
<ol ref={listContainer}>
|
<ol ref={listContainer}>
|
||||||
{messages.map((message, idx) =>
|
{messages.map((message, idx) =>
|
||||||
@ -22,8 +49,8 @@ function MessageFeed({messages=[], children}) {
|
|||||||
)}
|
)}
|
||||||
</ol>
|
</ol>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</SwipeableDrawer>
|
||||||
);
|
</>);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MessageFeed;
|
export default MessageFeed;
|
@ -1,33 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { useLoggedIn } from '../AuthProvider.jsx';
|
|
||||||
import { fetchCurrentVerbs } from '../apiTools.jsx';
|
|
||||||
import { SwipeableDrawer } from '@mui/material';
|
|
||||||
|
|
||||||
import './Sidebar.css';
|
|
||||||
|
|
||||||
function Sidebar({children, verbs, hidden=true, sendWord=(()=>null)}) {
|
|
||||||
const [open, setOpen] = useState(!hidden);
|
|
||||||
const loggedIn = useLoggedIn();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SwipeableDrawer
|
|
||||||
anchor="right"
|
|
||||||
open={open}
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
onOpen={() => setOpen(true)}>
|
|
||||||
<ul>
|
|
||||||
{verbs.map( (name) => (
|
|
||||||
<li key={name}>
|
|
||||||
<button onClick={() => { console.log("clicked button for", name); sendWord(name);}}>
|
|
||||||
{name}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
) )}
|
|
||||||
{children}
|
|
||||||
</ul>
|
|
||||||
</SwipeableDrawer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Sidebar;
|
|
58
client/src/embodied/WordSidebar.jsx
Normal file
58
client/src/embodied/WordSidebar.jsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useLoggedIn } from '../AuthProvider.jsx';
|
||||||
|
import { fetchCurrentVerbs } from '../apiTools.jsx';
|
||||||
|
import { Button, IconButton, SwipeableDrawer } from '@mui/material';
|
||||||
|
|
||||||
|
import MenuIcon from '@mui/icons-material/Menu';
|
||||||
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
|
||||||
|
import './Sidebar.css';
|
||||||
|
|
||||||
|
function WordSidebar({children, verbs, hidden=true, sendWord=(()=>null)}) {
|
||||||
|
const [open, setOpen] = useState(!hidden);
|
||||||
|
const loggedIn = useLoggedIn();
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
<IconButton
|
||||||
|
size="large"
|
||||||
|
variant="contained"
|
||||||
|
style={{
|
||||||
|
visible: open,
|
||||||
|
position: "fixed",
|
||||||
|
padding: "2rem",
|
||||||
|
right: 0,
|
||||||
|
top: 0
|
||||||
|
}}
|
||||||
|
onClick={() => setOpen(true)} >
|
||||||
|
<MenuIcon />
|
||||||
|
</IconButton>
|
||||||
|
<SwipeableDrawer
|
||||||
|
hideBackdrop={true}
|
||||||
|
disableBackdropTransition={true}
|
||||||
|
anchor="right"
|
||||||
|
open={open}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
onOpen={() => setOpen(true)}>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => setOpen(false)} >
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
<ul>
|
||||||
|
{verbs.map( (name) => (
|
||||||
|
<li key={name}>
|
||||||
|
<Button onClick={() => {
|
||||||
|
console.log("clicked button for", name);
|
||||||
|
sendWord(name);
|
||||||
|
}}>
|
||||||
|
{name}
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
) )}
|
||||||
|
{children}
|
||||||
|
</ul>
|
||||||
|
</SwipeableDrawer>
|
||||||
|
</>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WordSidebar;
|
@ -1,36 +1,55 @@
|
|||||||
function ExtendedAttributes({attributes, ...props}) {
|
import AttrList from './components/AttrList.jsx';
|
||||||
|
import JsonDisplay from './components/JsonDisplay.jsx';
|
||||||
|
|
||||||
|
function ExtendedAttributes({attributes, ...props}) {
|
||||||
|
console.log(attributes);
|
||||||
|
|
||||||
|
if (!attributes)
|
||||||
|
return <section className="page-contents">This object has no attributes.</section>;
|
||||||
|
|
||||||
|
const {parent, location, contents, verbs, prepositions} = attributes;
|
||||||
|
|
||||||
|
let newAttributes = {}
|
||||||
|
for (const prop in attributes) {
|
||||||
|
if (Object.hasOwn(attributes, prop)) {
|
||||||
|
newAttributes[prop] = attributes[prop]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete newAttributes["parent"];
|
||||||
|
delete newAttributes["location"];
|
||||||
|
delete newAttributes["contents"];
|
||||||
|
delete newAttributes["verbs"];
|
||||||
|
delete newAttributes["prepositions"];
|
||||||
|
|
||||||
if (!attributes) return;
|
|
||||||
return <>
|
return <>
|
||||||
<section className="page-contents">
|
<section className="page-contents">
|
||||||
{attributes.parent ?
|
{parent ?
|
||||||
`The parent of this object is #${attributes.parent}.`
|
`The parent of this object is #${parent}.`
|
||||||
:
|
:
|
||||||
"This object has no parent."}
|
"This object has no parent."}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{attributes.location && <section className="page-contents">
|
<section className="page-contents">
|
||||||
This object lives in #{JSON.stringify(attributes.location)}.
|
{location ?
|
||||||
</section>}
|
`This object lives in #${location}`
|
||||||
|
:
|
||||||
|
"This object has no location."}
|
||||||
|
</section>
|
||||||
|
|
||||||
{attributes.contents && typeof attributes.contents.map === 'function' && <section className="page-contents">
|
<AttrList
|
||||||
<h4>Contents of this location:</h4>
|
list={contents}
|
||||||
<ul>
|
title="Objects in this location:" />
|
||||||
{attributes.contents.map((objectNum) => <li key={objectNum}>{objectNum}</li>)}
|
|
||||||
</ul>
|
<AttrList
|
||||||
</section>}
|
list={verbs}
|
||||||
{attributes.verbs && <section className="page-contents">
|
title="Verbs defined on this object:" />
|
||||||
<h4>Verbs defined on this object:</h4>
|
|
||||||
<ul>
|
<AttrList
|
||||||
{attributes.verbs.map((verbNum) => <li key={verbNum}>{verbNum}</li>)}
|
list={prepositions}
|
||||||
</ul>
|
title="Prepositions this verb expects:" />
|
||||||
</section>}
|
|
||||||
{attributes.prepositions && <section className="page-contents">
|
<JsonDisplay obj={newAttributes} />
|
||||||
<h4>Prepositions this verb expects:</h4>
|
|
||||||
<ul>
|
|
||||||
{attributes.prepositions.map((prep) => <li key={prep}>{prep}</li>)}
|
|
||||||
</ul>
|
|
||||||
</section>}
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
109
client/src/page/ExtendedAttributesEdit.jsx
Normal file
109
client/src/page/ExtendedAttributesEdit.jsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import AttrListEdit from './components/AttrListEdit.jsx';
|
||||||
|
import JsonDisplay from './components/JsonDisplay.jsx';
|
||||||
|
|
||||||
|
function ExtendedAttributesEdit({attributes, setAttributes, ...props}) {
|
||||||
|
console.log("rendering with attributes: ", attributes);
|
||||||
|
|
||||||
|
if (!attributes) {
|
||||||
|
return <section className="page-contents">Attributes is falsy!</section>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function attributeListSetter(attributeName) {
|
||||||
|
return (newValue) => setAttributes((a) => {
|
||||||
|
if (a === null) return a;
|
||||||
|
let newList = {...a}
|
||||||
|
newList[attributeName] = newValue;
|
||||||
|
return newList;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let newAttributes = {...attributes,
|
||||||
|
parent: undefined,
|
||||||
|
location: undefined,
|
||||||
|
contents: undefined,
|
||||||
|
verbs: undefined,
|
||||||
|
prepositions: undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
let parentSnip;
|
||||||
|
if (!attributes.hasOwnProperty("parent") || attributes.parent === null || attributes.parent === undefined) {
|
||||||
|
parentSnip = <section className="page-contents">
|
||||||
|
This object does not have a parent.
|
||||||
|
<button
|
||||||
|
onClick={() => setAttributes((a) => {return {...a, parent: ""}}) }
|
||||||
|
>
|
||||||
|
Set Parent
|
||||||
|
</button>
|
||||||
|
</section>;
|
||||||
|
} else {
|
||||||
|
parentSnip = <section className="page-contents">
|
||||||
|
The parent of this object is #
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={`${attributes.parent}`}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
setAttributes((a) => {return {...a, parent: Number(val) };});
|
||||||
|
}}
|
||||||
|
/>.
|
||||||
|
<button
|
||||||
|
onClick={() =>setAttributes((a) => {return {...a, parent: undefined};})} >
|
||||||
|
Clear Parent
|
||||||
|
</button>
|
||||||
|
</section>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let locationSnip;
|
||||||
|
if (!attributes.hasOwnProperty("location") || attributes.location === null || attributes.location === undefined) {
|
||||||
|
locationSnip = <section className="page-contents">
|
||||||
|
This object does not have a location.
|
||||||
|
<button
|
||||||
|
onClick={() => setAttributes((a) => {return {...a, location: ""}}) }
|
||||||
|
>
|
||||||
|
Set Location
|
||||||
|
</button>
|
||||||
|
</section>;
|
||||||
|
} else {
|
||||||
|
locationSnip = <section className="page-contents">
|
||||||
|
The location of this object is #
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={attributes.location}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
setAttributes((a) => {return {...a, location: Number(val) };});
|
||||||
|
}}
|
||||||
|
/>.
|
||||||
|
<button
|
||||||
|
onClick={() => setAttributes((a) => {return {...a, location: undefined};})} >
|
||||||
|
Clear Location
|
||||||
|
</button>
|
||||||
|
</section>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
{parentSnip}
|
||||||
|
|
||||||
|
{locationSnip}
|
||||||
|
|
||||||
|
<AttrListEdit
|
||||||
|
title="Contents of this location:"
|
||||||
|
list={attributes.contents}
|
||||||
|
onChange={attributeListSetter('contents')} />
|
||||||
|
|
||||||
|
<AttrListEdit
|
||||||
|
title="Verbs defined on this object:"
|
||||||
|
list={attributes.verbs}
|
||||||
|
onChange={attributeListSetter('verbs')} />
|
||||||
|
|
||||||
|
<AttrListEdit
|
||||||
|
title="Prepositions this verb expects:"
|
||||||
|
list={attributes.prepositions}
|
||||||
|
onChange={attributeListSetter('prepositions')} />
|
||||||
|
|
||||||
|
<JsonDisplay
|
||||||
|
obj={newAttributes} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExtendedAttributesEdit;
|
@ -1,10 +1,14 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { apiUrl, fetchPage, fetchPageAttributes, postPage } from '../apiTools.jsx';
|
import { apiUrl, fetchPage, fetchPageAttributes, postPage } from '../apiTools.jsx';
|
||||||
|
|
||||||
import { useLoggedIn } from '../AuthProvider.jsx';
|
import { useLoggedIn } from '../AuthProvider.jsx';
|
||||||
|
|
||||||
import Page from './Page.jsx';
|
import Page from './Page.jsx';
|
||||||
import ExtendedAttributes from './ExtendedAttributes.jsx';
|
import ExtendedAttributes from './ExtendedAttributes.jsx';
|
||||||
|
//import ExtendedAttributesEdit from './ExtendedAttributesEdit.jsx';
|
||||||
|
import { JsonEditor } from 'json-edit-react';
|
||||||
|
|
||||||
import './Pages.css';
|
import './Pages.css';
|
||||||
|
|
||||||
@ -14,68 +18,74 @@ function GhostPage({editing, ...props}) {
|
|||||||
const loggedIn = useLoggedIn();
|
const loggedIn = useLoggedIn();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|
||||||
const linkClick = (e) => {
|
|
||||||
if (e.target.tagName != "A") { return; }
|
|
||||||
if (!e.target.href.includes(window.location.origin)) return;
|
|
||||||
e.preventDefault();
|
|
||||||
navigate(e.target.href.replace(window.location.origin, ""));
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchPageQuery = useQuery({
|
const fetchPageQuery = useQuery({
|
||||||
queryKey: ['page', pagenumber, null],
|
queryKey: ['page', pagenumber, null],
|
||||||
queryFn: () => fetchPage(pagenumber),
|
queryFn: () => fetchPage(pagenumber),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [title, setTitle] = useState(null);
|
||||||
|
useEffect(() => setTitle(fetchPageQuery.data?.title), [fetchPageQuery.data?.title]);
|
||||||
|
|
||||||
|
const [type, setType] = useState(false);
|
||||||
|
useEffect(() => setType(fetchPageQuery.data?.type == 1), [fetchPageQuery.data?.type]);
|
||||||
|
|
||||||
|
const editorRef = useRef(null);
|
||||||
|
|
||||||
const fetchAttributesQuery = useQuery({
|
const fetchAttributesQuery = useQuery({
|
||||||
queryKey: ['attributes', pagenumber],
|
queryKey: ['attributes', pagenumber],
|
||||||
queryFn: () => fetchPageAttributes(pagenumber)
|
queryFn: () => fetchPageAttributes(pagenumber)
|
||||||
});
|
});
|
||||||
|
|
||||||
const [title, setTitle] = useState(null);
|
const [attributes, setAttributes] = useState(null);
|
||||||
useEffect(() => {
|
useEffect(() => setAttributes(fetchAttributesQuery.data), [fetchAttributesQuery.data]);
|
||||||
setTitle(fetchPageQuery.data?.title);
|
|
||||||
}, [fetchPageQuery.data?.title]);
|
|
||||||
|
|
||||||
const [text, setText] = useState(null);
|
|
||||||
useEffect(() => {
|
|
||||||
setText(fetchPageQuery.data?.description);
|
|
||||||
}, [fetchPageQuery.data?.description]);
|
|
||||||
|
|
||||||
const [type, setType] = useState(false);
|
|
||||||
useEffect(() => {
|
|
||||||
setType(fetchPageQuery.data?.type == 1);
|
|
||||||
}, [fetchPageQuery.data?.type]);
|
|
||||||
|
|
||||||
const postMutation = useMutation({ // for changing the value when we're done with it
|
const postMutation = useMutation({ // for changing the value when we're done with it
|
||||||
mutationFn: postPage,
|
mutationFn: postPage,
|
||||||
onSettled: async (data, error, variables) => {
|
onSettled: async (data, error, variables) => {
|
||||||
// Invalidate and refetch
|
// Invalidate and refetch
|
||||||
await queryClient.invalidateQueries(['page', variables.number, null])
|
await queryClient.invalidateQueries(['page', variables.number, null]);
|
||||||
console.log("shoulda just invalidated the thing");
|
|
||||||
navigate(`/${pagenumber}`);
|
navigate(`/${pagenumber}`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function submitChanges(e) {
|
function submitChanges(e) {
|
||||||
postMutation.mutate({
|
if (editorRef.current) {
|
||||||
number: pagenumber,
|
postMutation.mutate({
|
||||||
title: title,
|
number: pagenumber,
|
||||||
description: text,
|
title: title,
|
||||||
type: type ? 1 : 0,
|
description: editorRef.current.getMarkdown(),
|
||||||
});
|
type: type ? 1 : 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const navigateLinkClick = (e) => {
|
||||||
|
if (e.target.tagName != "A") return;
|
||||||
|
if (!e.target.href.includes(window.location.origin)) return;
|
||||||
|
if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
navigate(e.target.href.replace(window.location.origin, ""));
|
||||||
|
};
|
||||||
|
|
||||||
return <div className="main-column">
|
return <div className="main-column">
|
||||||
<Page
|
<Page
|
||||||
page={{...fetchPageQuery.data, description: text, title: title, type: type }}
|
page={{...fetchPageQuery.data, title: title, type: type }}
|
||||||
editing={editing}
|
editing={editing}
|
||||||
linkClick={linkClick}
|
linkClick={navigateLinkClick}
|
||||||
onChangeTitle={(e) => setTitle(e.target.value)}
|
onChangeTitle={(e) => setTitle(e.target.value)}
|
||||||
onChangeText={setText}
|
|
||||||
onChangeType={() => setType(!type)}
|
onChangeType={() => setType(!type)}
|
||||||
|
ref={editorRef}
|
||||||
{...props}/>
|
{...props}/>
|
||||||
<ExtendedAttributes
|
{ !editing ?
|
||||||
attributes={fetchAttributesQuery.data} />
|
<ExtendedAttributes
|
||||||
|
attributes={attributes} />
|
||||||
|
:
|
||||||
|
<ExtendedAttributesEdit
|
||||||
|
attributes={attributes}
|
||||||
|
setAttributes={setAttributes} />
|
||||||
|
}
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate(`/${pagenumber}/history`)}>
|
onClick={() => navigate(`/${pagenumber}/history`)}>
|
||||||
History
|
History
|
||||||
@ -86,10 +96,15 @@ function GhostPage({editing, ...props}) {
|
|||||||
onClick={submitChanges}>
|
onClick={submitChanges}>
|
||||||
{postMutation.isPending ? "Updating..." : "Update"}
|
{postMutation.isPending ? "Updating..." : "Update"}
|
||||||
</button>)}
|
</button>)}
|
||||||
|
{editing && (
|
||||||
|
<button
|
||||||
|
onClick={() => navigate(`/${pagenumber}`)}>
|
||||||
|
Return without saving
|
||||||
|
</button>)}
|
||||||
{!editing && !editid && (
|
{!editing && !editid && (
|
||||||
<button
|
<button
|
||||||
disabled={!loggedIn}
|
disabled={!loggedIn}
|
||||||
onClick={() => navigate(`/${pagenumber}/edit`, {replace: true})}>
|
onClick={() => navigate(`/${pagenumber}/edit`)}>
|
||||||
Edit Page
|
Edit Page
|
||||||
</button>)}
|
</button>)}
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, forwardRef } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { apiUrl, fetchPage, fetchPageAtEdit, postPage } from '../apiTools.jsx';
|
import { apiUrl, fetchPage, fetchPageAtEdit, postPage } from '../apiTools.jsx';
|
||||||
import { useFixLinks } from '../clientStuff.jsx';
|
import { useFixLinks } from '../clientStuff.jsx';
|
||||||
|
|
||||||
|
import { Switch } from '@mui/material';
|
||||||
|
|
||||||
import { MDXEditor,
|
import { MDXEditor,
|
||||||
headingsPlugin, quotePlugin, listsPlugin,
|
headingsPlugin, quotePlugin, listsPlugin,
|
||||||
thematicBreakPlugin, linkPlugin, diffSourcePlugin } from '@mdxeditor/editor';
|
thematicBreakPlugin, linkPlugin, diffSourcePlugin } from '@mdxeditor/editor';
|
||||||
@ -11,27 +13,27 @@ import './Pages.css';
|
|||||||
import 'highlight.js/styles/a11y-dark.min.css';
|
import 'highlight.js/styles/a11y-dark.min.css';
|
||||||
import '@mdxeditor/editor/style.css';
|
import '@mdxeditor/editor/style.css';
|
||||||
|
|
||||||
function Page({
|
const Page = forwardRef(({
|
||||||
page=null,
|
page=null,
|
||||||
editing, historical,
|
editing, historical,
|
||||||
linkClick=()=>{},
|
linkClick=()=>{},
|
||||||
onChangeTitle=()=>{},
|
onChangeTitle=()=>{},
|
||||||
onChangeType=()=>{},
|
onChangeType=()=>{},
|
||||||
onChangeText=()=>{},
|
}, ref) => {
|
||||||
}) {
|
|
||||||
let {title, description, html, lua, time, author, type} = page || {};
|
let {title, description, html, lua, time, author, type} = page || {};
|
||||||
if (!title) title = "";
|
|
||||||
if (!html) html = "[body missing]";
|
|
||||||
if (!lua) lua = "[no definition]";
|
|
||||||
if (!description) description = "[body missing]";
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ref.current && description) {
|
||||||
|
ref.current.setMarkdown(description);
|
||||||
|
}
|
||||||
|
}, [description, ref.current, editing])
|
||||||
return (<>
|
return (<>
|
||||||
<header>
|
<header>
|
||||||
<h1>
|
<h1>
|
||||||
<a href="/" onClick={linkClick}>🌳</a>
|
<a href="/" onClick={linkClick}>🌳</a>
|
||||||
{page?.number}.
|
{page?.number}.
|
||||||
{ editing ?
|
{ editing ?
|
||||||
<input disabled={!editing} value={title} onChange={onChangeTitle}/>
|
<input disabled={!editing} value={title || ""} onChange={onChangeTitle}/>
|
||||||
:
|
:
|
||||||
<span>{title}</span>
|
<span>{title}</span>
|
||||||
}
|
}
|
||||||
@ -39,8 +41,11 @@ function Page({
|
|||||||
{ historical && <p>saved at {time} by user #{author}</p> }
|
{ historical && <p>saved at {time} by user #{author}</p> }
|
||||||
{ editing ?
|
{ editing ?
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" checked={type} onChange={onChangeType}/>
|
noun
|
||||||
{type ? "verb" : "noun"}
|
<Switch
|
||||||
|
checked={type}
|
||||||
|
onChange={onChangeType}/>
|
||||||
|
verb
|
||||||
</label>
|
</label>
|
||||||
:
|
:
|
||||||
<br/>
|
<br/>
|
||||||
@ -50,7 +55,7 @@ function Page({
|
|||||||
<section className="page-contents">
|
<section className="page-contents">
|
||||||
{ editing ?
|
{ editing ?
|
||||||
<MDXEditor
|
<MDXEditor
|
||||||
markdown={description}
|
markdown="nothin here"
|
||||||
plugins={[
|
plugins={[
|
||||||
headingsPlugin(),
|
headingsPlugin(),
|
||||||
quotePlugin(),
|
quotePlugin(),
|
||||||
@ -59,7 +64,7 @@ function Page({
|
|||||||
linkPlugin(),
|
linkPlugin(),
|
||||||
diffSourcePlugin({ diffMarkdown: 'ahhhh do not look upon me!', viewMode: 'source' }),
|
diffSourcePlugin({ diffMarkdown: 'ahhhh do not look upon me!', viewMode: 'source' }),
|
||||||
]}
|
]}
|
||||||
onChange={onChangeText}
|
ref={ref}
|
||||||
/>
|
/>
|
||||||
:
|
:
|
||||||
( type ?
|
( type ?
|
||||||
@ -73,6 +78,6 @@ function Page({
|
|||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
export default Page;
|
export default Page;
|
19
client/src/page/components/AttrList.jsx
Normal file
19
client/src/page/components/AttrList.jsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
function AttrList({list, title, onChange, children}) {
|
||||||
|
if (!list)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (!list || !list.map || typeof list.map != 'function')
|
||||||
|
return <section className="page-contents">
|
||||||
|
<h4>{title}</h4>
|
||||||
|
value is not a list: {list}
|
||||||
|
</section>;
|
||||||
|
|
||||||
|
return <section className="page-contents">
|
||||||
|
<h4>{title}</h4>
|
||||||
|
<ul>
|
||||||
|
{list.map((objectNum) => <li key={objectNum}>{objectNum}</li>)}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AttrList;
|
44
client/src/page/components/AttrListEdit.jsx
Normal file
44
client/src/page/components/AttrListEdit.jsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
function AttrListEdit({list, title, onChange, children}) {
|
||||||
|
if (list === null || list === undefined)
|
||||||
|
return <section className="page-contents">
|
||||||
|
<h4>{title}</h4>
|
||||||
|
Not set.
|
||||||
|
<button onClick={() => onChange([])}>Make empty list</button>
|
||||||
|
</section>;
|
||||||
|
|
||||||
|
if (!list.map || typeof list.map != 'function')
|
||||||
|
return <section className="page-contents">
|
||||||
|
<h4>{title}</h4>
|
||||||
|
value is not a list: {list}
|
||||||
|
<button onClick={() => onChange([])}>Make empty list</button>
|
||||||
|
<button onClick={() => onChange(undefined)}>Clear field</button>
|
||||||
|
</section>;
|
||||||
|
|
||||||
|
return <section className="page-contents">
|
||||||
|
<h4>{title}</h4>
|
||||||
|
<ul>
|
||||||
|
{list.map((objectNum, index) => (
|
||||||
|
<li key={index}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={objectNum}
|
||||||
|
onChange={(e) => onChange((list) => { list[index] = Number(e.target.value); } )}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
onChange((list) => {
|
||||||
|
delete list[index];
|
||||||
|
return list;
|
||||||
|
})
|
||||||
|
}>
|
||||||
|
clear element
|
||||||
|
</button>
|
||||||
|
</li>)
|
||||||
|
)}
|
||||||
|
<li><input value="" placeholder="add element..."/></li>
|
||||||
|
</ul>
|
||||||
|
<button onClick={() => onChange(undefined)}>Clear field</button>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AttrListEdit;
|
15
client/src/page/components/JsonDisplay.jsx
Normal file
15
client/src/page/components/JsonDisplay.jsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
function JsonDisplay({obj}) {
|
||||||
|
if (!obj)
|
||||||
|
return <section className="page-contents">No additional attributes found.</section>;
|
||||||
|
|
||||||
|
let empty = true;
|
||||||
|
for (const prop in obj) {
|
||||||
|
if (Object.hasOwn(obj, prop) && obj[prop] !== undefined) empty = false;
|
||||||
|
}
|
||||||
|
if (empty)
|
||||||
|
return <section className="page-contents">No additional attributes found.</section>;
|
||||||
|
|
||||||
|
return <section className="page-contents">{JSON.stringify(obj)}</section>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default JsonDisplay;
|
Loading…
Reference in New Issue
Block a user