frontend updating and rendering

This commit is contained in:
Chris Sexton 2019-11-07 20:54:00 -05:00
parent f1f3c3dd5b
commit 6ac7ba8fd0
15 changed files with 456 additions and 127 deletions

View File

@ -1,6 +1,9 @@
package entry package entry
import ( import (
"fmt"
"regexp"
"strings"
"time" "time"
"code.chrissexton.org/cws/cabinet/db" "code.chrissexton.org/cws/cabinet/db"
@ -22,10 +25,10 @@ type Entry struct {
func PrepareTable(tx *sqlx.Tx) error { func PrepareTable(tx *sqlx.Tx) error {
q := `create table if not exists entries ( q := `create table if not exists entries (
id integer primary key, id integer primary key,
slug text unique, slug text unique not null,
content text, content text not null,
created datetime, created datetime not null,
updated datetime, updated datetime not null,
author_id integer author_id integer
)` )`
_, err := tx.Exec(q) _, err := tx.Exec(q)
@ -35,9 +38,10 @@ func PrepareTable(tx *sqlx.Tx) error {
} }
q = `create table if not exists tags ( q = `create table if not exists tags (
id integer primary key, id integer primary key,
name text, name text not null,
entry_id integer, entry_id integer,
foreign key(entry_id) references entries(id) foreign key(entry_id) references entries(id),
constraint unique_name_id unique (name, entry_id)
)` )`
_, err = tx.Exec(q) _, err = tx.Exec(q)
if err != nil { if err != nil {
@ -74,13 +78,21 @@ func GetByID(db *db.Database, id int64) (Entry, error) {
return e, e.populateTags() return e, e.populateTags()
} }
func GetAll(db *db.Database) ([]*Entry, error) { func Search(db *db.Database, query string) ([]*Entry, error) {
entries := []*Entry{} entries := []*Entry{}
if query != "" {
q := `select * from entries where content like '%?%'`
err := db.Select(&entries, q, query)
if err != nil {
return nil, err
}
} else {
q := `select * from entries` q := `select * from entries`
err := db.Select(&entries, q) err := db.Select(&entries, q)
if err != nil { if err != nil {
return nil, err return nil, err
} }
}
for _, e := range entries { for _, e := range entries {
e.db = db e.db = db
e.populateTags() e.populateTags()
@ -117,15 +129,93 @@ func (e *Entry) populateTags() error {
return err return err
} }
func (e *Entry) addTag(tag string) error {
q := `insert into tags (name,entry_id) values (?,?)`
_, err := e.db.Exec(q, tag, e.ID)
return err
}
func (e *Entry) removeTag(tag string) error {
q := `delete from tags where name=? and entry_id=?`
_, err := e.db.Exec(q, tag, e.ID)
return err
}
func (e *Entry) UniqueSlug() string {
candidate := strings.Split(e.Content, "\n")[0]
candidateNumber := 0
r := regexp.MustCompile(`[^a-zA-Z0-9 -]`)
candidate = r.ReplaceAllString(candidate, "")
candidate = strings.TrimSpace(candidate)
candidate = strings.ReplaceAll(candidate, " ", "-")
if len(candidate) == 0 {
candidate = "untitled"
}
candidate = strings.ToLower(candidate)
q := `select slug from entries where slug like ?`
slugs := []string{}
if err := e.db.Select(&slugs, q, candidate+"%"); err != nil {
log.Debug().Err(err).Msgf("Could not get candidate slugs: %s", err)
return candidate
}
contains := func(s string, ss []string) bool {
for _, e := range ss {
if s == e {
return true
}
}
return false
}
tmpCandidate := candidate
for contains(tmpCandidate, slugs) {
candidateNumber++
tmpCandidate = fmt.Sprintf("%s-%d", candidate, candidateNumber)
}
return tmpCandidate
}
func (e *Entry) Update() error { func (e *Entry) Update() error {
if e.ID == -1 { if e.ID == -1 {
return e.Create() return e.Create()
} }
q := `update entries set slug=?, content=?, updated=?, author_id=? where id=?` old, err := GetByID(e.db, e.ID)
_, err := e.db.Exec(q, e.Slug, e.Content, e.Updated.Unix(), e.AuthorID, e.ID) if err != nil {
return err return err
} }
e.Slug = e.UniqueSlug()
q := `update entries set slug=?, content=?, updated=?, author_id=? where id=?`
if _, err = e.db.Exec(q, e.Slug, e.Content, e.Updated.Unix(), e.AuthorID, e.ID); err != nil {
return err
}
for _, t := range e.Tags {
if !contains(old.Tags, t) {
e.addTag(t)
}
}
for _, t := range old.Tags {
if !contains(e.Tags, t) {
e.removeTag(t)
}
}
return nil
}
func contains(items []string, entry string) bool {
for _, e := range items {
if e == entry {
return true
}
}
return false
}
func (e *Entry) Create() error { func (e *Entry) Create() error {
if e.ID != -1 { if e.ID != -1 {
return e.Update() return e.Update()

View File

@ -8,6 +8,8 @@
"lint": "vue-cli-service lint" "lint": "vue-cli-service lint"
}, },
"dependencies": { "dependencies": {
"asciidoctor": "^2.0.3",
"axios": "^0.19.0",
"bootstrap": "^4.3.1", "bootstrap": "^4.3.1",
"bootstrap-vue": "^2.0.4", "bootstrap-vue": "^2.0.4",
"brace": "latest", "brace": "latest",

View File

@ -1,7 +1,7 @@
<template> <template>
<div id="app"> <div id="app">
<b-navbar type="dark" variant="dark"> <b-navbar type="dark" variant="dark">
<b-navbar-brand>Cabinet</b-navbar-brand> <b-navbar-brand>🗄 Cabinet</b-navbar-brand>
<b-navbar-nav> <b-navbar-nav>
<b-nav-item to="/">Home</b-nav-item> <b-nav-item to="/">Home</b-nav-item>
<b-nav-item to="/console">Console</b-nav-item> <b-nav-item to="/console">Console</b-nav-item>

View File

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<editor v-model="text" @init="editorInit" lang="asciidoc" theme="github" width="100%" height="500" /> <editor ref="myEditor" v-model="text" @init="editorInit" lang="asciidoc" theme="github" width="100%" height="500" />
</div> </div>
</template> </template>
@ -20,7 +20,13 @@
}, },
watch: { watch: {
content: function(newValue) { content: function(newValue) {
console.log('text update\n'+newValue)
let editor = this.$refs.myEditor.editor
editor.renderer.updateFull()
this.text = newValue this.text = newValue
},
text: function() {
this.$emit('change', this.text)
} }
}, },
methods: { methods: {

View File

@ -3,20 +3,13 @@
<b-container fluid> <b-container fluid>
<b-row> <b-row>
<b-col> <b-col>
<Editor :content="content" /> <Editor :content="content" @change="updateContent"/>
</b-col> </b-col>
</b-row> </b-row>
<b-row> <b-row>
<b-col cols="1"><label style="padding-top: 0.5em" for="tagList">Tags</label></b-col>
<b-col cols="10"> <b-col cols="10">
<label for="tagList">Tags</label> <TagList id="tagList" :tags="tags" @change="tagUpdate"/>
</b-col>
</b-row>
<b-row>
<b-col cols="10">
<TagList id="tagList" :tags="tags" />
</b-col>
<b-col cols="1">
<b-button variant="primary">Save</b-button>
</b-col> </b-col>
</b-row> </b-row>
</b-container> </b-container>
@ -24,8 +17,10 @@
</template> </template>
<script> <script>
import Editor from "./Editor"; import Editor from "./Editor"
import TagList from "./TagList"; import TagList from "./TagList"
import _ from 'lodash'
export default { export default {
name: "MainEditor", name: "MainEditor",
props: { props: {
@ -35,23 +30,18 @@
if (this.$props.slug) { if (this.$props.slug) {
this.getFile(this.$props.slug) this.getFile(this.$props.slug)
} }
}, this.save = _.debounce(this.save, 2000)
data: function () {
return {
file: ''
}
}, },
computed: { computed: {
tags: function () { tags: function () {
if (this.file) { if (this.$store.state.file) {
return this.file.tags return this.$store.state.file.Tags
} }
return [] return []
}, },
content: function () { content: function () {
console.log('file:' + this.file) if (this.$store.state.file) {
if (this.file) { return this.$store.state.file.Content
return this.file.contents
} }
return "= Main Editor" return "= Main Editor"
} }
@ -64,8 +54,33 @@
methods: { methods: {
getFile: function (slug) { getFile: function (slug) {
this.$store.dispatch('getFile', slug) this.$store.dispatch('getFile', slug)
.then(file => { },
this.file = file tagUpdate: function(newTags) {
if (JSON.stringify(newTags) === JSON.stringify(this.file.Tags))
return
this.file.Tags = newTags
this.$emit('markDirty', true)
this.save()
},
updateContent: function (newContent) {
if (this.$store.state.file.Content === newContent)
return
this.$store.state.file.Content = newContent
this.$emit('markDirty', true)
this.save()
},
save: function () {
this.$store.state.file.Content = this.content
console.log("Saving file: " + this.file.Slug)
this.$store.dispatch('saveFile', this.file)
.then(res => {
this.$emit('markDirty', false)
this.$store.dispatch('updateSearch')
if (res.data.Slug != this.$route.params.slug)
this.$router.replace({params: { slug: res.data.Slug }})
})
.catch(err => {
console.log('err:' + err)
}) })
} }
}, },

View File

@ -40,14 +40,14 @@
}, },
computed: { computed: {
tags: function() { tags: function() {
if (this.file) { if (this.$store.state.file) {
return this.file.tags return this.$store.state.file.Tags
} }
return [] return []
}, },
content: function () { content: function () {
if (this.file) { if (this.$store.state.file) {
return this.file.contents return this.$store.state.file.Content
} }
return null return null
} }
@ -60,9 +60,6 @@
methods: { methods: {
getFile: function(slug) { getFile: function(slug) {
this.$store.dispatch('getFile', slug) this.$store.dispatch('getFile', slug)
.then(file => {
this.file = file
})
} }
}, },
components: {TagList, Viewer} components: {TagList, Viewer}

View File

@ -1,13 +1,13 @@
<template> <template>
<b-container fluid> <b-container fluid>
<b-row> <b-row>
<b-input placeholder="Search" @update="runQuery" v-model="query" /> <b-input placeholder="Search" @update="runQuery" v-model="queryText" />
</b-row> </b-row>
<b-row v-for="item in results" v-bind:key="item.id"> <b-row v-for="item in results" v-bind:key="item.ID">
<b-col> <b-col>
<b-link <b-link
:to="{ name: target, params: { slug: item.slug } }" :to="{ name: target, params: { slug: item.Slug } }"
>{{item.title}}</b-link> >{{item.Slug}}</b-link>
</b-col> </b-col>
</b-row> </b-row>
</b-container> </b-container>
@ -19,35 +19,34 @@
name: "SearchResults", name: "SearchResults",
data: function() { data: function() {
return { return {
results: [] queryText: null
} }
}, },
props: { props: {
target: String, target: String,
query: String query: String
}, },
watch: {
query: function(newValue) {
this.queryText = newValue
}
},
computed: {
results: function() {
console.log("results:"+this.$store.state.searchResults)
return this.$store.state.searchResults
}
},
created() { created() {
this.getResults() this.getResults()
this.runQuery = _.debounce(this.runQuery, 1000) this.runQuery = _.debounce(this.runQuery, 1000)
}, },
methods: { methods: {
getResults: function () { getResults: function () {
this.$store.dispatch('getSearchResults') this.$store.dispatch('getSearchResults', null)
.catch(res => {
this.$store.commit('addError', res.message);
})
.then(res => {
this.results = res
})
}, },
runQuery: function() { runQuery: function() {
this.$store.dispatch('getSearchResults', this.query) this.$store.dispatch('getSearchResults', this.query)
.catch(res => {
this.$store.commit('addError', res.message);
})
.then(res => {
this.results = res
})
} }
} }
} }

View File

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<b-input type="text" placeholder="tags" :value="tagString" :hidden="readOnly"/> <b-input type="text" placeholder="tags" :value="tagString" @update="changed" :hidden="readOnly"/>
<div :hidden="!readOnly"> <div :hidden="!readOnly">
<b-badge pill v-for="(tag, idx) in tags" v-bind:key="idx" class="tag"> <b-badge pill v-for="(tag, idx) in tags" v-bind:key="idx" class="tag">
{{tag}} {{tag}}
@ -12,13 +12,29 @@
<script> <script>
export default { export default {
name: "TagList", name: "TagList",
data: function () {
return {
internalTags: []
}
},
props: { props: {
tags: Array, tags: Array,
readOnly: Boolean readOnly: Boolean
}, },
watch: {
tags: function (newValue) {
this.internalTags = newValue
}
},
computed: { computed: {
tagString: function () { tagString: function () {
return this.tags.join(' ') return this.internalTags.join(',')
}
},
methods: {
changed: function (value) {
this.internalTags = value.split(',')
this.$emit('change', this.internalTags)
} }
} }
} }

View File

@ -1,16 +1,26 @@
<template> <template>
<div> <div v-html="adoc">
<pre>
{{content}}
</pre>
</div> </div>
</template> </template>
<script> <script>
let asciidoctor = require('asciidoctor')()
export default { export default {
name: "Viewer", name: "Viewer",
props: { props: {
content: String content: String
},
computed: {
adoc: function () {
if (!this.content)
return ''
let doc = asciidoctor.convert(this.content, {
'safe': 'safe',
'attributes': {'showtitle': true, 'icons': 'font'}
})
return doc
}
} }
} }
</script> </script>

View File

@ -6,11 +6,6 @@ import Console from '../views/Console.vue'
Vue.use(VueRouter) Vue.use(VueRouter)
const routes = [ const routes = [
{
path: '/',
name: 'home',
component: Home
},
{ {
path: '/view/:slug', path: '/view/:slug',
name: 'home-slug', name: 'home-slug',
@ -43,6 +38,11 @@ const routes = [
// this generates a separate chunk (about.[hash].js) for this route // this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited. // which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue') component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
},
{
path: '/',
name: 'home',
component: Home
} }
] ]

View File

@ -1,25 +1,29 @@
import Vue from 'vue' import Vue from 'vue'
import Vuex from 'vuex' import Vuex from 'vuex'
import _ from 'lodash' // import _ from 'lodash'
import axios from 'axios'
Vue.use(Vuex) Vue.use(Vuex)
function getRandomInt(min, max) { // function getRandomInt(min, max) {
min = Math.ceil(min) // min = Math.ceil(min)
max = Math.floor(max) // max = Math.floor(max)
return Math.floor(Math.random() * (max - min + 1)) + min // return Math.floor(Math.random() * (max - min + 1)) + min
} // }
var files = { // var files = {
'test-1': {id: 0, title: 'test 1', contents: '= test 1', slug: 'test-1', tags: ['a']}, // 'test-1': {id: 0, title: 'test 1', contents: '= test 1', slug: 'test-1', tags: ['a']},
'test-2': {id: 1, title: 'test 2', contents: '= test 2', slug: 'test-2', tags: ['b']}, // 'test-2': {id: 1, title: 'test 2', contents: '= test 2', slug: 'test-2', tags: ['b']},
'test-3': {id: 2, title: 'test 3', contents: '= test 3', slug: 'test-3', tags: ['a','b']}, // 'test-3': {id: 2, title: 'test 3', contents: '= test 3', slug: 'test-3', tags: ['a','b']},
'test-4': {id: 3, title: 'test 4', contents: '= test 4', slug: 'test-4', tags: ['c']} // 'test-4': {id: 3, title: 'test 4', contents: '= test 4', slug: 'test-4', tags: ['c']}
} // }
export default new Vuex.Store({ export default new Vuex.Store({
state: { state: {
errs: [] errs: [],
searchResults: [],
query: null,
file: null
}, },
mutations: { mutations: {
clearError(state) { clearError(state) {
@ -27,25 +31,49 @@ export default new Vuex.Store({
}, },
addError(state, err) { addError(state, err) {
state.errs.push(err) state.errs.push(err)
},
setResults(state, results) {
state.searchResults = results
},
setQuery(state, query) {
state.query = query
},
setFile(state, file) {
state.file = file
} }
}, },
actions: { actions: {
getFile: function(_, slug) { getFile: function({ commit }, slug) {
console.log('getFile('+slug+')') if (slug)
return new Promise(function (resolve) { return axios.get('/v1/entries/'+slug)
setTimeout(() => resolve(files[slug]), getRandomInt(0, 1000)) .catch(err => commit('addError', err))
.then(res => {
commit('setFile', res.data)
}) })
}, },
getSearchResults: function (state, query) { getSearchResults: function ({dispatch, commit}, query) {
console.log('getSearchResults: '+query) commit('setQuery', query)
let values = Object.values(files) dispatch('updateSearch')
if (query) },
values = _.filter(values, (o) => { updateSearch: function ({commit, state}) {
return o.title.indexOf(query) != -1 let query = state.query
if (query) {
axios.get('/v1/entries?query='+query)
.catch(err => state.addError(err))
.then(res =>{
console.log("getSearchResults:"+res.data)
commit('setResults', res.data)
}) })
return new Promise(function (resolve) { }
setTimeout(() => resolve(values, getRandomInt(0, 1000))) axios.get('/v1/entries')
.catch(err => state.addError(err))
.then(res =>{
console.log("getSearchResults:"+res.data)
commit('setResults', res.data)
}) })
},
saveFile: function(state, file) {
return axios.put('/v1/entries/'+file.Slug, file)
} }
}, },
modules: { modules: {

View File

@ -1,22 +1,31 @@
<template> <template>
<b-container fluid> <b-container fluid>
<b-row>
<b-col>
<h1>Console</h1>
</b-col>
</b-row>
<b-row> <b-row>
<b-col md="5"> <b-col md="5">
<h2>Scratchpad</h2> <h2>Scratchpad</h2>
<ScratchPad /> <ScratchPad />
</b-col> </b-col>
<b-col md="5"> <b-col md="5">
<h2>Editor</h2>
<MainEditor :slug="$route.params.slug" /> <div>
<b-tabs content-class="mt-3">
<b-tab active>
<template v-slot:title>
Editor <span v-bind:class="{ dirty: isDirty, clean: !isDirty }">&bull;</span>
</template>
<MainEditor :slug="$route.params.slug" @markDirty="markDirty"/>
</b-tab>
<b-tab title="Viewer">
<MainView :slug="$route.params.slug" />
</b-tab>
</b-tabs>
</div>
</b-col> </b-col>
<b-col md="2"> <b-col md="2">
<h2>Search Results</h2> <h2>Search Results</h2>
<SearchResults target="console-slug" /> <SearchResults target="console-slug" :query="query"/>
</b-col> </b-col>
</b-row> </b-row>
</b-container> </b-container>
@ -25,6 +34,7 @@
<script> <script>
// @ is an alias to /src // @ is an alias to /src
import MainEditor from "../components/MainEditor"; import MainEditor from "../components/MainEditor";
import MainView from "../components/MainView";
import ScratchPad from "../components/ScratchPad"; import ScratchPad from "../components/ScratchPad";
import SearchResults from "../components/Search"; import SearchResults from "../components/Search";
@ -33,7 +43,54 @@ export default {
components: { components: {
SearchResults, SearchResults,
ScratchPad, ScratchPad,
MainEditor MainEditor,
MainView
},
data: function() {
return {
isDirty: false,
query: ''
}
},
provide: {
update: function() {}
},
methods: {
markDirty: function(dirty) {
console.log('markDirty:'+dirty)
this.isDirty = dirty
}
},
beforeRouteEnter (to, from, next) {
// called before the route that renders this component is confirmed.
// does NOT have access to `this` component instance,
// because it has not been created yet when this guard is called!
console.log('beforeRouteEnter'+to+from)
next()
},
beforeRouteUpdate (to, from, next) {
// called when the route that renders this component has changed,
// but this component is reused in the new route.
// For example, for a route with dynamic params `/foo/:id`, when we
// navigate between `/foo/1` and `/foo/2`, the same `Foo` component instance
// will be reused, and this hook will be called when that happens.
// has access to `this` component instance.
if (this.isDirty) {
const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
if (answer) {
next()
} else {
next(false)
}
}
next()
},
beforeRouteLeave (to, from, next) {
// called when the route that renders this component is about to
// be navigated away from.
// has access to `this` component instance.
console.log('beforeRouteLeave'+to+from)
next()
} }
} }
</script> </script>
@ -42,4 +99,10 @@ export default {
h2 { h2 {
font-size: large; font-size: large;
} }
.dirty {
color: red;
}
.clean {
display: none;
}
</style> </style>

View File

@ -2,6 +2,21 @@
# yarn lockfile v1 # yarn lockfile v1
"@asciidoctor/cli@2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@asciidoctor/cli/-/cli-2.0.1.tgz#5e0154ff06da21aedb1840bff5d49ca6ee7d520d"
integrity sha512-XNqnHPMVTRMDlKk6EEiA61P0WImHqOecoky1p8HIek+0u1jbKbiDku8WraC2EaNy8IWyEjfrdnrDYGvogBVqzA==
dependencies:
yargs "13.2.2"
"@asciidoctor/core@2.0.3":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@asciidoctor/core/-/core-2.0.3.tgz#95630424eb2e346acc51dede1e6599159fbd5d44"
integrity sha512-iN0zV/tsL36nTltJyk5IdcRqDW34HHiwb9PrgSoTGDCIEML6C3HYnJOQi+jKq7A//Gt1nL1SHohaCHkyw3swEg==
dependencies:
asciidoctor-opal-runtime "0.3.0"
unxhr "1.0.1"
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5": "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5":
version "7.5.5" version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d"
@ -1385,6 +1400,22 @@ array-unique@^0.3.2:
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
asciidoctor-opal-runtime@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/asciidoctor-opal-runtime/-/asciidoctor-opal-runtime-0.3.0.tgz#df327a870ddd3cd5eb0e162d64ed4dcdd3fe3fee"
integrity sha512-YapVwl2qbbs6sIe1dvAlMpBzQksFVTSa2HOduOKFNhZlE9bNmn+moDgGVvjWPbzMPo/g8gItyTHfWB2u7bQxag==
dependencies:
glob "7.1.3"
unxhr "1.0.1"
asciidoctor@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/asciidoctor/-/asciidoctor-2.0.3.tgz#350a41d051c5f99d1d8fa4abdc7a9c80da48f90a"
integrity sha512-6UB/oB0jRQOy6/DoFBH1nUzAAGt7Aa6snfXKZyvaxKxQHcuvFuJcvS2zVjAH2eURQidkBLy+J0VHBVjRWs8CiQ==
dependencies:
"@asciidoctor/cli" "2.0.1"
"@asciidoctor/core" "2.0.3"
asn1.js@^4.0.0: asn1.js@^4.0.0:
version "4.10.1" version "4.10.1"
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0"
@ -1474,6 +1505,14 @@ aws4@^1.8.0:
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
axios@^0.19.0:
version "0.19.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.0.tgz#8e09bff3d9122e133f7b8101c8fbdd00ed3d2ab8"
integrity sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==
dependencies:
follow-redirects "1.5.10"
is-buffer "^2.0.2"
babel-eslint@^10.0.3: babel-eslint@^10.0.3:
version "10.0.3" version "10.0.3"
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.3.tgz#81a2c669be0f205e19462fed2482d33e4687a88a" resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.3.tgz#81a2c669be0f205e19462fed2482d33e4687a88a"
@ -2609,6 +2648,13 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
dependencies: dependencies:
ms "2.0.0" ms "2.0.0"
debug@=3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
dependencies:
ms "2.0.0"
debug@^3.0.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6: debug@^3.0.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6:
version "3.2.6" version "3.2.6"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
@ -3517,6 +3563,13 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3" inherits "^2.0.3"
readable-stream "^2.3.6" readable-stream "^2.3.6"
follow-redirects@1.5.10:
version "1.5.10"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
dependencies:
debug "=3.1.0"
follow-redirects@^1.0.0: follow-redirects@^1.0.0:
version "1.9.0" version "1.9.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.9.0.tgz#8d5bcdc65b7108fe1508649c79c12d732dcedb4f" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.9.0.tgz#8d5bcdc65b7108fe1508649c79c12d732dcedb4f"
@ -3692,6 +3745,18 @@ glob-to-regexp@^0.3.0:
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=
glob@7.1.3:
version "7.1.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^7.0.3, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: glob@^7.0.3, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
version "7.1.5" version "7.1.5"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.5.tgz#6714c69bee20f3c3e64c4dd905553e532b40cdc0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.5.tgz#6714c69bee20f3c3e64c4dd905553e532b40cdc0"
@ -4282,6 +4347,11 @@ is-buffer@^1.1.5:
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
is-buffer@^2.0.2:
version "2.0.4"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623"
integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==
is-callable@^1.1.4: is-callable@^1.1.4:
version "1.1.4" version "1.1.4"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75"
@ -5590,7 +5660,7 @@ os-homedir@^1.0.0:
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
os-locale@^3.0.0: os-locale@^3.0.0, os-locale@^3.1.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a"
integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==
@ -7753,6 +7823,11 @@ unset-value@^1.0.0:
has-value "^0.3.1" has-value "^0.3.1"
isobject "^3.0.0" isobject "^3.0.0"
unxhr@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/unxhr/-/unxhr-1.0.1.tgz#92200322d66c728993de771f9e01eeb21f41bc7b"
integrity sha512-MAhukhVHyaLGDjyDYhy8gVjWJyhTECCdNsLwlMoGFoNJ3o79fpQhtQuzmAE4IxCMDwraF4cW8ZjpAV0m9CRQbg==
upath@^1.1.1: upath@^1.1.1:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894"
@ -8228,7 +8303,7 @@ yargs-parser@^11.1.1:
camelcase "^5.0.0" camelcase "^5.0.0"
decamelize "^1.2.0" decamelize "^1.2.0"
yargs-parser@^13.1.1: yargs-parser@^13.0.0, yargs-parser@^13.1.1:
version "13.1.1" version "13.1.1"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0"
integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ== integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==
@ -8254,6 +8329,23 @@ yargs@12.0.5:
y18n "^3.2.1 || ^4.0.0" y18n "^3.2.1 || ^4.0.0"
yargs-parser "^11.1.1" yargs-parser "^11.1.1"
yargs@13.2.2:
version "13.2.2"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.2.tgz#0c101f580ae95cea7f39d927e7770e3fdc97f993"
integrity sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==
dependencies:
cliui "^4.0.0"
find-up "^3.0.0"
get-caller-file "^2.0.1"
os-locale "^3.1.0"
require-directory "^2.1.1"
require-main-filename "^2.0.0"
set-blocking "^2.0.0"
string-width "^3.0.0"
which-module "^2.0.0"
y18n "^4.0.0"
yargs-parser "^13.0.0"
yargs@^13.0.0: yargs@^13.0.0:
version "13.3.0" version "13.3.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83" resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83"

View File

@ -9,18 +9,18 @@
* Vue Frontend * Vue Frontend
** [ ] spend some time learning about TypeScript/Vue.js style ** [ ] spend some time learning about TypeScript/Vue.js style
** Documents ** Documents
*** [ ] adoc editor widget *** [x] adoc editor widget
** Authentication ** Authentication
*** [ ] some kind of user auth *** [ ] some kind of user auth
** Views ** Views
*** [ ] editor view *** [ ] editor view
*** [ ] public index/search view *** [ ] public index/search view
* Backend * Backend
** [ ] save endpoint ** [?] save endpoint
*** [ ] need to generate a slug for entries *** [ ] need to generate a slug for entries
*** [ ] add authentication/authorization *** [ ] add authentication/authorization
*** [ ] convert document to adoc (give format?) *** [ ] convert document to adoc (give format?)
*** [ ] test in frontend *** [x] test in frontend
*** [ ] check for unique tags *** [ ] check for unique tags
*** [ ] set some db fields not null *** [ ] set some db fields not null
** [ ] search endpoint ** [ ] search endpoint

View File

@ -4,6 +4,9 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"time"
"github.com/rs/zerolog/log"
"code.chrissexton.org/cws/cabinet/entry" "code.chrissexton.org/cws/cabinet/entry"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@ -28,19 +31,25 @@ func (web *Web) editEntry(w http.ResponseWriter, r *http.Request) {
return return
} }
newEntry.ID = oldEntry.ID oldEntry.Content = newEntry.Content
err = newEntry.Update() oldEntry.Tags = newEntry.Tags
oldEntry.Updated = time.Now()
err = oldEntry.Update()
if err != nil { if err != nil {
w.WriteHeader(500) w.WriteHeader(500)
fmt.Fprint(w, err) fmt.Fprint(w, err)
return return
} }
resp, err := json.Marshal(newEntry) resp, err := json.Marshal(oldEntry)
if err != nil { if err != nil {
w.WriteHeader(500) w.WriteHeader(500)
fmt.Fprint(w, err) fmt.Fprint(w, err)
return return
} }
log.Debug().Interface("oldEntry", oldEntry).Msg("Got a PUT")
w.Header().Set("content-type", "application/json") w.Header().Set("content-type", "application/json")
fmt.Fprint(w, string(resp)) fmt.Fprint(w, string(resp))
} }
@ -72,7 +81,9 @@ func (web *Web) newEntry(w http.ResponseWriter, r *http.Request) {
} }
func (web *Web) allEntries(w http.ResponseWriter, r *http.Request) { func (web *Web) allEntries(w http.ResponseWriter, r *http.Request) {
entries, err := entry.GetAll(web.db) r.ParseForm()
query := r.Form.Get("query")
entries, err := entry.Search(web.db, query)
if err != nil { if err != nil {
w.WriteHeader(500) w.WriteHeader(500)
fmt.Fprint(w, err) fmt.Fprint(w, err)