import React, { useContext } from 'react'; import { Breadcrumb, Spinner, CardColumns, Card, Jumbotron, Row, Col, Form, Button } from 'react-bootstrap'; import { Redirect, Link } from 'react-router-dom'; import { UserContext } from './auth.js'; import { DateTime } from 'luxon'; import { AsyncTypeahead } from 'react-bootstrap-typeahead'; import './app.css'; // Set parent on all node children function set_children_parents(node) { if (node.children) { node.children.forEach(function(item) { item.parent = node; set_children_parents(item); }); } return node; } function Node(props) { const node = props.node; let change_datetime = null; if (node.updated_at) { const updated_at = node.updated_at ? DateTime.fromISO(node.updated_at).toLocal() : null; change_datetime = Updated {updated_at.toRelative()}; } else { const created_at = DateTime.fromISO(node.created_at).toLocal(); change_datetime = Created {created_at.toRelative()}; } let parents = []; parents.push({node.name}); //parents.push(
  • // {node.name} //
  • ); for (let current = node.parent; current; current = current.parent) { parents.push(
  • {current.name}
  • ); } if (parents.length > 1) { parents.reverse(); parents = {parents}; } else { parents = ''; } let children = ''; if (node.children && node.children.length > 0) { children = node.children.map(item => {item.name} {item.children ? '+' : ''} ); } let fields = node.fields.map((field, index) => {field.name} {field.value} ); let buttons = ''; if (props.editable) { // Using Button here throws errors because of some navigation attribute, so just set the class names manually buttons = <> Edit Delete ; } return {change_datetime} {buttons} {parents} {node.name} {fields} {children} {props.editable ? Add : ''} ; } class NodeEditor extends React.Component { static contextType = UserContext; constructor(props) { super(props); this.state = { loading: false, name: '', errors: {}, parent: '', parent_id: null, validated: false, fields: [], parents_searching: false, parents: [], redirect_id: null, }; if (props.location.state !== undefined) { const node = props.location.state.node; if (node !== undefined) { this.state.id = node.id; this.state.name = node.name; this.state.parent = node.parent ? node.parent.name : ''; this.state.parent_id = node.parent_id; this.state.fields = [...node.fields]; } const parent = props.location.state.parent; const parent_id = props.location.state.parent_id; if (parent !== undefined && parent_id !== undefined) { this.state.parent = parent; this.state.parent_id = parent_id; } } this.on_name_change = this.on_name_change.bind(this); this.on_parent_change = this.on_parent_change.bind(this); this.add_field = this.add_field.bind(this); this.remove_field = this.remove_field.bind(this); this.on_field_change = this.on_field_change.bind(this); this.on_parents_search = this.on_parents_search.bind(this); this.save = this.save.bind(this); } render() { if (this.state.redirect_id !== null) { return ; } if (this.state.loading) { return Loading… ; } const that = this; return

    {this.title}

    Name {this.state.errors.name} Parent true} labelKey="name" isLoading={this.state.parents_searching} getSuggestionValue={value => value} renderMenuItemChildren={(option, props) => (
    {option.name}
    )} /> {this.state.errors.parent}
    Fields {this.state.fields.map(function(field, index) { return {that.state.errors[`field-${index}-name`]} {that.state.errors[`field-${index}-value`]} ; })}
    ; } on_name_change(event) { this.setState({name: event.target.value}); } on_parent_change(items) { let item = items[0] || {}; this.setState({parent: item.name || '', parent_id: item.id || null}); } add_field(event) { event.preventDefault(); this.setState({fields: [...this.state.fields, {name: '', value: ''}], validated: false}); } remove_field(event) { let fields = [...this.state.fields]; fields.splice(event.target.dataset.index, 1); this.setState({fields: fields, validated: false}); } on_field_change(event) { const index = event.target.dataset.index; let fields = [...this.state.fields]; let field = {...this.state.fields[index]}; field[event.target.dataset.field] = event.target.value; fields[index] = field; this.setState({fields: fields, validated: false}); } async on_parents_search(q) { this.setState({parents_searching: true}); let token = await this.context.get_token(); let response = await fetch('/api/search', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/x-www-form-urlencoded', }, body: 'q=' + encodeURIComponent(q), }); let data = await response.json(); this.setState({parents: data, parents_searching: false}); } validate() { let errors = {}; if (this.state.name === '') { errors.name = 'Name must be specified'; } if (this.state.parent !== '' && this.state.parent_id === null) { errors.parent = 'Parent must be empty or set to a valid value'; } this.state.fields.forEach(function(field, index) { if (field.name === '') { errors[`field-${index}-name`] = 'Name must be specified'; } if (field.value === '') { errors[`field-${index}-value`] = 'Value must be specified'; } }); this.setState({validated: true, errors: errors}); // Clear existing errors document.querySelectorAll('form :invalid').forEach(e => e.setCustomValidity('')); for (let key in errors) { document.getElementById(key).setCustomValidity(errors[key]); } if (errors.length > 0) { return false; } return true; } } class ViewNode extends React.Component { static contextType = UserContext; constructor(props) { super(props); let node = null; if (props.location.state !== undefined) { node = props.location.state.node; } this.state = { ready: node !== null, node: node, }; this.mounted = false; this.loading_id = null; this.load_abort = null; this.load_signal = null; } async componentDidMount() { this.mounted = true; try { await this.load(); } catch (error) { if (!(error instanceof DOMException && error.code === 20)) { throw error; } } } componentWillUnmount() { this.mounted = false; } async componentDidUpdate() { // Set and reload if we have received a new node in the location state if (this.state.node !== null && this.props.location.state !== undefined && this.props.location.state.node !== null && this.state.node.id !== this.props.location.state.node.id) { this.setState({node: this.props.location.state.node}); try { await this.load(); } catch (error) { if (!(error instanceof DOMException && error.code === 20)) { throw error; } } } // Load if the current node is not set, or if the node's id does not match the param id if (this.state.node === null || this.props.match.params.id !== this.state.node.id) { try { await this.load(); } catch (error) { if (!(error instanceof DOMException && error.code === 20)) { throw error; } } } } render() { if (!this.state.ready) { return Loading… ; } return ; } async load() { if (!this.mounted || this.loading_id === this.props.match.params.id) { return; } if (this.load_abort !== null && !this.load_signal.aborted) { this.load_abort.abort(); } let abort = this.load_abort = new AbortController(); let signal = this.load_signal = abort.signal; this.loading_id = this.props.match.params.id; if (this.state.node === null && this.state.ready) { this.setState({ready: false}); } const token = await this.context.get_token(); let response = await fetch(`/api/nodes/${this.props.match.params.id}`, { headers: { Authorization: `Bearer ${token}`, }, signal: signal, }); const node = await response.json(); if (signal.aborted) { return; } set_children_parents(node); if (this.mounted && node.id === this.props.match.params.id) { this.setState({node: node, ready: true}); } } } class UserHome extends React.Component { static contextType = UserContext; constructor(props) { super(props); this.state = { ready: false, nodes: [], }; } async componentDidMount() { const token = await this.context.get_token() let response = await fetch('/api/nodes', { headers: { Authorization: `Bearer ${token}`, } }); const nodes = await response.json(); nodes.forEach(set_children_parents); this.setState({nodes: nodes, ready: true}); } render() { if (!this.state.ready) { return Loading… ; } return {this.state.nodes.map(item => )} ; } } function Main() { const user = useContext(UserContext); if (user.is_logged_in) { return ; } return

    Unmess

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla pharetra fringilla dolor, sed tempor est. Maecenas malesuada feugiat nisl ut hendrerit. Donec faucibus augue quis mi fermentum, ac tempus neque dignissim. Nullam egestas bibendum enim, at feugiat lorem consectetur eu. Ut faucibus, dolor gravida ultricies malesuada, leo ex volutpat turpis, at placerat lectus mauris ut tortor. Phasellus sit amet lorem laoreet nunc sollicitudin pharetra. Pellentesque laoreet est lacinia velit porta aliquam ut quis sem. Sed vel convallis purus. Nunc elementum fermentum leo sit amet elementum.

    } class AddNode extends NodeEditor { componentDidMount() { this.title = 'Add new item'; } async save(event) { if (!this.validate()) { return; } let node = { name: this.state.name, parent_id: this.state.parent_id, fields: this.state.fields, } let token = await this.context.get_token(); let response = await fetch('/api/nodes', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json', }, body: JSON.stringify(node), }); let data = await response.json(); if (!response.ok) { window.flash({header: 'Add node', text: 'Adding node failed'}); return; } this.setState({redirect_id: data.id}); } } class EditNode extends NodeEditor { async componentDidMount() { this.title = 'Edit item'; if (this.state.name === '') { this.setState({loading: true}); this.load(); } } async load() { let token = await this.context.get_token(); let response = await fetch(`/api/nodes/${this.props.match.params.id}`, { method: 'GET', headers: { 'Authorization': 'Bearer ' + token, }, }); if (!response.ok) { window.flash({header: 'Edit node', text: 'Failed to load node'}); return; } let node = await response.json(); this.setState({ loading: false, id: node.id, name: node.name, parent: node.parent ? node.parent.name : '', parent_id: node.parent_id, fields: [...node.fields], }); } async save(event) { if (!this.validate()) { return; } let node = { name: this.state.name, parent_id: this.state.parent_id, fields: this.state.fields, }; let token = await this.context.get_token(); let response = await fetch(`/api/nodes/${this.state.id}`, { method: 'PUT', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json', }, body: JSON.stringify(node), }); if (!response.ok) { window.flash({header: 'Edit node', text: 'Saving node failed'}); return; } this.setState({redirect_id: this.state.id}); } } class DeleteNode extends React.Component { static contextType = UserContext; constructor(props) { super(props); let node = null; if (props.location.state !== undefined) { node = props.location.state.node; } this.state = { node: node, redirect: null, }; this.on_delete = this.on_delete.bind(this); this.on_cancel = this.on_cancel.bind(this); } render() { if (this.state.redirect) { return ; } return

    Delete {this.state.node.name}

    ; } async on_delete() { let path = '/'; if (this.state.node.parent != null) { path = `/node/${this.state.node.parent.id}`; } let token = await this.context.get_token(); let response = await fetch(`/api/nodes/${this.state.node.id}`, { method: 'DELETE', headers: { Authorization: `Bearer ${token}`, }, }); if (!response.ok) { window.flash({header: `Delete ${this.state.node.name}`, text: 'Deletion failed'}); return; } window.flash({header: `Delete ${this.state.node.name}`, text: `${this.state.node.name} has been deleted`}); this.setState({redirect: path}); } on_cancel() { this.setState({redirect: `/node/${this.state.node.id}`}); } } class SearchNodes extends React.Component { static contextType = UserContext; constructor(props) { super(props); let params = new URLSearchParams(props.location.search); this.state = { query: params.get('q'), loading: true, results: [], }; this.last_query = null; } componentDidMount() { this.search(); } componentDidUpdate() { let params = new URLSearchParams(this.props.location.search); let new_query = params.get('q'); if (new_query !== this.state.query) { this.setState({query: new_query}); } if (this.last_query !== this.state.query) { this.search(); } } render() { if (this.state.loading) { return Loading… ; } if (this.state.results.length === 0) { return
    No results were found for {this.state.query}.
    ; } return {this.state.results.map(item => )} ; } async search() { this.last_query = this.state.query; this.setState({loading: true}); let token = await this.context.get_token(); let response = await fetch(`/api/search`, { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/x-www-form-urlencoded', }, body: 'q=' + encodeURIComponent(this.state.query), }); let results = await response.json(); this.setState({ loading: false, results: results, }); } } export { Main, AddNode, EditNode, DeleteNode, ViewNode, SearchNodes };