frontend updating and rendering
This commit is contained in:
parent
f1f3c3dd5b
commit
6ac7ba8fd0
116
entry/entry.go
116
entry/entry.go
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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 }">•</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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
19
web/entry.go
19
web/entry.go
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue