Compare commits

...

3 Commits

Author SHA1 Message Date
Chris Sexton e9360e5082 fix production build issues 2019-11-08 00:35:20 -05:00
Chris Sexton b67a1cccd5 fix search, new, delete 2019-11-08 00:26:02 -05:00
Chris Sexton 08894f1ef4 working search 2019-11-07 23:39:16 -05:00
11 changed files with 147 additions and 90 deletions

View File

@ -15,6 +15,7 @@ type Entry struct {
db *db.Database
ID int64
Slug string
Title string
Content string
Tags []string
Created time.Time
@ -51,13 +52,18 @@ func PrepareTable(tx *sqlx.Tx) error {
return nil
}
func New(db *db.Database) Entry {
return Entry{
func New(db *db.Database) *Entry {
e := Entry{
db: db,
ID: -1,
Created: time.Now(),
Updated: time.Now(),
Tags: []string{},
}
e.Title = e.GenerateTitle()
e.Slug = e.UniqueSlug()
e.Content = "= " + e.Title
return &e
}
func GetBySlug(db *db.Database, slug string) (Entry, error) {
@ -66,6 +72,7 @@ func GetBySlug(db *db.Database, slug string) (Entry, error) {
if err := db.Get(&e, q, slug); err != nil {
return e, err
}
e.Title = e.GenerateTitle()
return e, e.populateTags()
}
@ -75,19 +82,21 @@ func GetByID(db *db.Database, id int64) (Entry, error) {
if err := db.Get(&e, q, id); err != nil {
return e, err
}
e.Title = e.GenerateTitle()
return e, e.populateTags()
}
func Search(db *db.Database, query string) ([]*Entry, error) {
entries := []*Entry{}
log.Debug().Str("query", query).Msg("searching")
if query != "" {
q := `select * from entries where content like '%?%'`
err := db.Select(&entries, q, query)
q := `select * from entries where content like ? order by updated desc`
err := db.Select(&entries, q, "%"+query+"%")
if err != nil {
return nil, err
}
} else {
q := `select * from entries`
q := `select * from entries order by updated desc`
err := db.Select(&entries, q)
if err != nil {
return nil, err
@ -95,6 +104,7 @@ func Search(db *db.Database, query string) ([]*Entry, error) {
}
for _, e := range entries {
e.db = db
e.Title = e.GenerateTitle()
e.populateTags()
}
return entries, nil
@ -114,12 +124,12 @@ func RemoveBySlug(db *db.Database, slug string) error {
tx.Rollback()
return err
}
q = `delete from entries where entry_id = ?`
q = `delete from entries where id = ?`
if _, err := tx.Exec(q, e.ID); err != nil {
tx.Rollback()
return err
}
return nil
return tx.Commit()
}
func (e *Entry) populateTags() error {
@ -141,18 +151,16 @@ func (e *Entry) removeTag(tag string) error {
return err
}
func (e *Entry) UniqueSlug() string {
func (e *Entry) GenerateTitle() 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{}
@ -179,6 +187,20 @@ func (e *Entry) UniqueSlug() string {
return tmpCandidate
}
func (e *Entry) UniqueSlug() string {
if e.Title == "" {
e.Title = e.GenerateTitle()
}
candidate := e.Title
r := regexp.MustCompile(`[^a-zA-Z0-9 -]`)
candidate = r.ReplaceAllString(candidate, "")
candidate = strings.TrimSpace(candidate)
candidate = strings.ReplaceAll(candidate, " ", "-")
candidate = strings.ToLower(candidate)
return candidate
}
func (e *Entry) Update() error {
if e.ID == -1 {
return e.Create()

View File

@ -7,6 +7,11 @@
<b-nav-item to="/console">Console</b-nav-item>
<b-nav-item to="/about">About</b-nav-item>
</b-navbar-nav>
<b-navbar-nav class="ml-auto">
<b-nav-form>
<b-button @click="newFile">+</b-button>
</b-nav-form>
</b-navbar-nav>
</b-navbar>
<Error/>
<router-view/>
@ -42,6 +47,16 @@
name: 'app',
components: {
Error
},
methods: {
newFile: function() {
this.$store.dispatch('newFile')
.catch(() => {})
.then(file => {
this.$store.dispatch('updateSearch')
this.$router.push({ name: 'console-slug', params: { slug: file.Slug }})
})
}
}
}
</script>

View File

@ -56,32 +56,29 @@
this.$store.dispatch('getFile', slug)
},
tagUpdate: function(newTags) {
if (JSON.stringify(newTags) === JSON.stringify(this.file.Tags))
if (JSON.stringify(newTags) === JSON.stringify(this.$store.state.file.Tags))
return
this.file.Tags = newTags
this.$store.commit('setTags', 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.$store.commit('setContent', 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)
this.$store.commit('setContent', this.content)
this.$store.dispatch('saveFile')
.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)
})
.catch(() => { })
}
},
components: {TagList, Editor}

View File

@ -8,7 +8,7 @@
</b-row>
<b-row>
<b-col cols="10">
<label for="tagList" >Tags</label>
<label for="tagList" :hidden="!tags">Tags</label>
</b-col>
</b-row>
<b-row>

View File

@ -1,13 +1,13 @@
<template>
<b-container fluid>
<b-row>
<b-input placeholder="Search" @update="runQuery" v-model="queryText" />
<b-input placeholder="Search" @update="getResults" v-model="queryText" />
</b-row>
<b-row v-for="item in results" v-bind:key="item.ID">
<b-col>
<b-link
<b-button :hidden="!editMode" size="sm" class="deleteLink" @click="deleteFile(item.Slug)">X</b-button> <b-link
:to="{ name: target, params: { slug: item.Slug } }"
>{{item.Slug}}</b-link>
>{{item.Title}}</b-link>
</b-col>
</b-row>
</b-container>
@ -24,30 +24,39 @@
},
props: {
target: String,
query: String
query: String,
editMode: Boolean
},
watch: {
query: function(newValue) {
this.queryText = newValue
},
queryText: function(newValue) {
this.$store.commit('setQuery', 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)
this.getResults = _.debounce(this.getResults, 1000)
},
methods: {
getResults: function () {
this.$store.dispatch('getSearchResults', null)
this.$store.dispatch('updateSearch')
},
runQuery: function() {
this.$store.dispatch('getSearchResults', this.query)
deleteFile: function(slug) {
this.$store.dispatch('deleteBySlug', slug)
}
}
}
</script>
<style scoped>
.deleteLink {
font-size: x-small;
}
</style>

View File

@ -23,7 +23,7 @@
},
watch: {
tags: function (newValue) {
this.internalTags = newValue
this.internalTags = newValue || []
}
},
computed: {

View File

@ -40,9 +40,28 @@ export default new Vuex.Store({
},
setFile(state, file) {
state.file = file
},
setContent(state, content) {
state.file.Content = content
},
setTags(state, tags) {
state.file.Tags = tags
}
},
actions: {
newFile: function({commit}) {
return new Promise((resolve, reject) => {
axios.post('/v1/entries', {})
.catch(err => {
commit('addError', err)
reject(err)
})
.then(res => {
commit('setFile', res.data)
resolve(res.data)
})
})
},
getFile: function({ commit }, slug) {
if (slug)
return axios.get('/v1/entries/'+slug)
@ -51,29 +70,32 @@ export default new Vuex.Store({
commit('setFile', res.data)
})
},
getSearchResults: function ({dispatch, commit}, query) {
commit('setQuery', query)
dispatch('updateSearch')
},
updateSearch: function ({commit, state}) {
let query = state.query
if (query) {
return new Promise((resolve, reject) => {
let query = state.query || ""
axios.get('/v1/entries?query='+query)
.catch(err => state.addError(err))
.then(res =>{
console.log("getSearchResults:"+res.data)
commit('setResults', res.data)
.catch(err => {
state.addError(err)
reject(err)
})
}
axios.get('/v1/entries')
.catch(err => state.addError(err))
.then(res =>{
console.log("getSearchResults:"+res.data)
commit('setResults', res.data)
resolve(res.data)
})
})
},
saveFile: function(state, file) {
return axios.put('/v1/entries/'+file.Slug, file)
saveFile: function({state}) {
if (state.file)
return axios.put('/v1/entries/'+state.file.Slug, state.file)
},
deleteBySlug: function({dispatch,commit}, slug) {
axios.delete('/v1/entries/'+slug)
.catch(err => {
commit('addError', err)
})
.then(() => {
dispatch('updateSearch')
})
}
},
modules: {

View File

@ -8,7 +8,7 @@
<b-col md="5">
<div>
<b-tabs content-class="mt-3">
<b-tabs content-class="mt-3" v-model="tabIndex">
<b-tab active>
<template v-slot:title>
Editor <span v-bind:class="{ dirty: isDirty, clean: !isDirty }">&bull;</span>
@ -25,7 +25,7 @@
</b-col>
<b-col md="2">
<h2>Search Results</h2>
<SearchResults target="console-slug" :query="query"/>
<SearchResults :editMode="true" target="console-slug" />
</b-col>
</b-row>
</b-container>
@ -49,15 +49,11 @@ export default {
data: function() {
return {
isDirty: false,
query: ''
tabIndex: 0
}
},
provide: {
update: function() {}
},
methods: {
markDirty: function(dirty) {
console.log('markDirty:'+dirty)
this.isDirty = dirty
}
},
@ -65,7 +61,6 @@ export default {
// 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) {
@ -83,13 +78,13 @@ export default {
next(false)
}
}
this.tabIndex = 0
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()
}
}

View File

@ -1,27 +1,20 @@
= Todo
:icons: font
* Operations
* Backend
** Authentication
*** [ ] some kind of user auth
** save endpoint
*** [ ] add authentication/authorization
*** [ ] convert document to adoc (give format?)
*** [ ] check for unique tags
** [ ] search endpoint
*** [ ] search for tags
*** [ ] fulltext search
**** with link:https://blevesearch.com/docs/Getting%20Started/[Bleve]
* [ ] CLI Frontend
* [ ] Operations
** [ ] dockerize the build
** [ ] integrate CI/CD
** [ ] run on https://cabinet.chrissexton.org[cabinet.chrissexton.org]
** [ ] create redirect or https://cab.chrissexton.org[cab.chrissexton.org]
* Vue Frontend
** [ ] spend some time learning about TypeScript/Vue.js style
** Documents
*** [x] adoc editor widget
** Authentication
*** [ ] some kind of user auth
** Views
*** [ ] editor view
*** [ ] public index/search view
* Backend
** [?] save endpoint
*** [ ] need to generate a slug for entries
*** [ ] add authentication/authorization
*** [ ] convert document to adoc (give format?)
*** [x] test in frontend
*** [ ] check for unique tags
*** [ ] set some db fields not null
** [ ] search endpoint
* CLI Frontend

View File

@ -81,8 +81,11 @@ func (web *Web) newEntry(w http.ResponseWriter, r *http.Request) {
}
func (web *Web) allEntries(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
query := r.Form.Get("query")
query := ""
items, ok := r.URL.Query()["query"]
if ok {
query = items[0]
}
entries, err := entry.Search(web.db, query)
if err != nil {
w.WriteHeader(500)
@ -127,6 +130,7 @@ func (web *Web) removeEntry(w http.ResponseWriter, r *http.Request) {
err := entry.RemoveBySlug(web.db, slug)
if err != nil {
log.Error().Msgf("Error deleting: %s", err)
w.WriteHeader(500)
fmt.Fprint(w, err)
return

View File

@ -11,8 +11,8 @@ import (
"github.com/speps/go-hashids"
"github.com/gorilla/mux"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/rs/zerolog/log"
"github.com/stretchr/graceful"
)
@ -44,7 +44,7 @@ func (web *Web) routeSetup() http.Handler {
api := r.PathPrefix("/v1/").Subrouter()
api.HandleFunc("/entries", web.allEntries).Methods(http.MethodGet)
api.HandleFunc("/entries", web.newEntry).Methods(http.MethodPost)
api.HandleFunc("/entries", web.removeEntry).Methods(http.MethodDelete)
api.HandleFunc("/entries/{slug}", web.removeEntry).Methods(http.MethodDelete)
api.HandleFunc("/entries/{slug}", web.editEntry).Methods(http.MethodPut)
api.HandleFunc("/entries/{slug}", web.getEntry).Methods(http.MethodGet)
r.PathPrefix("/").HandlerFunc(web.indexHandler("/index.html"))