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
import (
"fmt"
"regexp"
"strings"
"time"
"code.chrissexton.org/cws/cabinet/db"
@ -22,10 +25,10 @@ type Entry struct {
func PrepareTable(tx *sqlx.Tx) error {
q := `create table if not exists entries (
id integer primary key,
slug text unique,
content text,
created datetime,
updated datetime,
slug text unique not null,
content text not null,
created datetime not null,
updated datetime not null,
author_id integer
)`
_, err := tx.Exec(q)
@ -35,9 +38,10 @@ func PrepareTable(tx *sqlx.Tx) error {
}
q = `create table if not exists tags (
id integer primary key,
name text,
name text not null,
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)
if err != nil {
@ -74,12 +78,20 @@ func GetByID(db *db.Database, id int64) (Entry, error) {
return e, e.populateTags()
}
func GetAll(db *db.Database) ([]*Entry, error) {
func Search(db *db.Database, query string) ([]*Entry, error) {
entries := []*Entry{}
q := `select * from entries`
err := db.Select(&entries, q)
if err != nil {
return nil, err
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`
err := db.Select(&entries, q)
if err != nil {
return nil, err
}
}
for _, e := range entries {
e.db = db
@ -117,13 +129,91 @@ func (e *Entry) populateTags() error {
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 {
if e.ID == -1 {
return e.Create()
}
old, err := GetByID(e.db, e.ID)
if err != nil {
return err
}
e.Slug = e.UniqueSlug()
q := `update entries set slug=?, content=?, updated=?, author_id=? where id=?`
_, err := e.db.Exec(q, e.Slug, e.Content, e.Updated.Unix(), e.AuthorID, e.ID)
return err
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 {

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<template>
<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>
</template>
@ -20,7 +20,13 @@
},
watch: {
content: function(newValue) {
console.log('text update\n'+newValue)
let editor = this.$refs.myEditor.editor
editor.renderer.updateFull()
this.text = newValue
},
text: function() {
this.$emit('change', this.text)
}
},
methods: {

View File

@ -3,20 +3,13 @@
<b-container fluid>
<b-row>
<b-col>
<Editor :content="content" />
<Editor :content="content" @change="updateContent"/>
</b-col>
</b-row>
<b-row>
<b-col cols="1"><label style="padding-top: 0.5em" for="tagList">Tags</label></b-col>
<b-col cols="10">
<label for="tagList">Tags</label>
</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>
<TagList id="tagList" :tags="tags" @change="tagUpdate"/>
</b-col>
</b-row>
</b-container>
@ -24,8 +17,10 @@
</template>
<script>
import Editor from "./Editor";
import TagList from "./TagList";
import Editor from "./Editor"
import TagList from "./TagList"
import _ from 'lodash'
export default {
name: "MainEditor",
props: {
@ -35,23 +30,18 @@
if (this.$props.slug) {
this.getFile(this.$props.slug)
}
},
data: function () {
return {
file: ''
}
this.save = _.debounce(this.save, 2000)
},
computed: {
tags: function() {
if (this.file) {
return this.file.tags
tags: function () {
if (this.$store.state.file) {
return this.$store.state.file.Tags
}
return []
},
content: function () {
console.log('file:' + this.file)
if (this.file) {
return this.file.contents
if (this.$store.state.file) {
return this.$store.state.file.Content
}
return "= Main Editor"
}
@ -62,10 +52,35 @@
}
},
methods: {
getFile: function(slug) {
getFile: function (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: {
tags: function() {
if (this.file) {
return this.file.tags
if (this.$store.state.file) {
return this.$store.state.file.Tags
}
return []
},
content: function () {
if (this.file) {
return this.file.contents
if (this.$store.state.file) {
return this.$store.state.file.Content
}
return null
}
@ -60,9 +60,6 @@
methods: {
getFile: function(slug) {
this.$store.dispatch('getFile', slug)
.then(file => {
this.file = file
})
}
},
components: {TagList, Viewer}

View File

@ -1,13 +1,13 @@
<template>
<b-container fluid>
<b-row>
<b-input placeholder="Search" @update="runQuery" v-model="query" />
<b-input placeholder="Search" @update="runQuery" v-model="queryText" />
</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-link
:to="{ name: target, params: { slug: item.slug } }"
>{{item.title}}</b-link>
:to="{ name: target, params: { slug: item.Slug } }"
>{{item.Slug}}</b-link>
</b-col>
</b-row>
</b-container>
@ -19,35 +19,34 @@
name: "SearchResults",
data: function() {
return {
results: []
queryText: null
}
},
props: {
target: 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() {
this.getResults()
this.runQuery = _.debounce(this.runQuery, 1000)
},
methods: {
getResults: function () {
this.$store.dispatch('getSearchResults')
.catch(res => {
this.$store.commit('addError', res.message);
})
.then(res => {
this.results = res
})
this.$store.dispatch('getSearchResults', null)
},
runQuery: function() {
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>
<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">
<b-badge pill v-for="(tag, idx) in tags" v-bind:key="idx" class="tag">
{{tag}}
@ -12,20 +12,36 @@
<script>
export default {
name: "TagList",
data: function () {
return {
internalTags: []
}
},
props: {
tags: Array,
readOnly: Boolean
},
watch: {
tags: function (newValue) {
this.internalTags = newValue
}
},
computed: {
tagString: function() {
return this.tags.join(' ')
tagString: function () {
return this.internalTags.join(',')
}
},
methods: {
changed: function (value) {
this.internalTags = value.split(',')
this.$emit('change', this.internalTags)
}
}
}
</script>
<style scoped>
.tag {
margin-right: 0.5em;
}
.tag {
margin-right: 0.5em;
}
</style>

View File

@ -1,16 +1,26 @@
<template>
<div>
<pre>
{{content}}
</pre>
<div v-html="adoc">
</div>
</template>
<script>
let asciidoctor = require('asciidoctor')()
export default {
name: "Viewer",
props: {
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>

View File

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

View File

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

View File

@ -1,22 +1,31 @@
<template>
<b-container fluid>
<b-row>
<b-col>
<h1>Console</h1>
</b-col>
</b-row>
<b-row>
<b-col md="5">
<h2>Scratchpad</h2>
<ScratchPad />
</b-col>
<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 md="2">
<h2>Search Results</h2>
<SearchResults target="console-slug" />
<SearchResults target="console-slug" :query="query"/>
</b-col>
</b-row>
</b-container>
@ -25,6 +34,7 @@
<script>
// @ is an alias to /src
import MainEditor from "../components/MainEditor";
import MainView from "../components/MainView";
import ScratchPad from "../components/ScratchPad";
import SearchResults from "../components/Search";
@ -33,7 +43,54 @@ export default {
components: {
SearchResults,
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>
@ -42,4 +99,10 @@ export default {
h2 {
font-size: large;
}
.dirty {
color: red;
}
.clean {
display: none;
}
</style>

View File

@ -2,6 +2,21 @@
# 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":
version "7.5.5"
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"
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:
version "4.10.1"
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"
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:
version "10.0.3"
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:
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:
version "3.2.6"
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"
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:
version "1.9.0"
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"
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:
version "7.1.5"
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"
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:
version "1.1.4"
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"
integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
os-locale@^3.0.0:
os-locale@^3.0.0, os-locale@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a"
integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==
@ -7753,6 +7823,11 @@ unset-value@^1.0.0:
has-value "^0.3.1"
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:
version "1.2.0"
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"
decamelize "^1.2.0"
yargs-parser@^13.1.1:
yargs-parser@^13.0.0, yargs-parser@^13.1.1:
version "13.1.1"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0"
integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==
@ -8254,6 +8329,23 @@ yargs@12.0.5:
y18n "^3.2.1 || ^4.0.0"
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:
version "13.3.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83"

View File

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

View File

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