This is an automated email from the git hooks/post-receive script. New commit to branch bow-v2-go in repository bow. See https://gitlab.nuiton.org/chorem/bow.git commit 38b4e5283009de65ce9d668af5306cd25e912e27 Author: Benjamin <poussin@codelutin.com> Date: Wed May 20 03:40:35 2020 +0200 introduction de vuex pour avoir un etat partage sur la recherche en cours ajout nuage de tags ajout indication de navigation --- pkg/repository/bookmarkRepository.go | 40 ++++++--- web/package.json | 3 +- web/src/App.vue | 4 +- web/src/components/CloudTags.vue | 59 +++++++++++++ web/src/components/layout/Header.vue | 12 +-- web/src/components/layout/Sidebar.vue | 153 +++++++++++++++++----------------- web/src/main.js | 10 ++- web/src/router/index.js | 12 +-- web/src/store/index.js | 22 +++++ web/src/views/Home.vue | 17 +++- web/src/views/Login.vue | 17 ++-- web/yarn.lock | 5 ++ 12 files changed, 229 insertions(+), 125 deletions(-) diff --git a/pkg/repository/bookmarkRepository.go b/pkg/repository/bookmarkRepository.go index 64eac14..8e1db09 100644 --- a/pkg/repository/bookmarkRepository.go +++ b/pkg/repository/bookmarkRepository.go @@ -15,6 +15,9 @@ import ( /* BookmarkJSON retourne le bookmark au format json +si id est non vide alors fait une recherche exact sur l'id +si uri est non vide alors fait une recherche exact sur l'uri +sinon fait une recherche sur les critères: tags, fulltext */ func BookmarkJSON(currentUser model.BowUser, id string, uri string, tags string, fulltext string, orderBy string, orderDesc bool, first int) (string, error) { var result string @@ -45,9 +48,20 @@ func BookmarkJSON(currentUser model.BowUser, id string, uri string, tags string, result, err = q.QueryString(currentUser, uri) } else { tagsJSON := "{" + strings.Join(strings.Fields(tags), ",") + "}" - q := &query{sql: fmt.Sprintf(` - WITH __all AS (select * from bookmark where tags @> $1::text[] order by %s %s OFFSET %d LIMIT %d) SELECT json_agg(__all.*) as j FROM __all - `, orderBy, orderDirection, first, maxResult)} + q := &query{sql: fmt.Sprintf(`WITH + __query AS (select * from bookmark where tags @> $1::text[]), + __orderby AS (select '%[1]s' as orderby), + __orderdesc AS (select to_json('%[2]s' = 'desc') as orderdesc), + __first AS (select %[3]d as first), + __limit AS (select %[4]d as limit), + __total AS (select count(id) as total from __query), + __allTags AS (select unnest(tags) as tag from __query), + __tags AS (select tag, count(tag) from __allTags group by tag order by 2 desc), + __jsonTags AS (select ('[' || json_agg(a.tag) || ', ' || json_agg(a.count) || ']')::json as tags from __tags a), + __result AS (select * from __query order by %[1]s %[2]s OFFSET %[3]d LIMIT %[4]d), + __jsonResult AS (SELECT json_agg(r.*) as result FROM __result r) + select row_to_json(v) from (select * from __first, __limit, __total, __orderby, __orderdesc, __jsonTags, __jsonResult) v; + `, orderBy, orderDirection, first, maxResult)} result, err = q.QueryString(currentUser, tagsJSON) } @@ -115,7 +129,7 @@ func URIsFromAliasJSON(currentUser model.BowUser, alias string) (string, error) if err != nil { return "", utils.NewHTTPError500(err, currentUser) } - + return result, nil } @@ -130,15 +144,15 @@ func URIFromAlias(currentUser model.BowUser, alias string) (string, error) { if err != nil { return "", utils.NewHTTPError500(err, currentUser) } - - if result == "" { - // s'il n'y a pas de resultat recherche dans les alias public - q = &query{asNobody: true, sql: `select uri from bookmark where publicAlias && $1::text[];`} - result, err = q.QueryString(currentUser, arg) - if err != nil { - return "", utils.NewHTTPError500(err, currentUser) - } - } + + if result == "" { + // s'il n'y a pas de resultat recherche dans les alias public + q = &query{asNobody: true, sql: `select uri from bookmark where publicAlias && $1::text[];`} + result, err = q.QueryString(currentUser, arg) + if err != nil { + return "", utils.NewHTTPError500(err, currentUser) + } + } return result, nil } diff --git a/web/package.json b/web/package.json index 2bb54f5..ae2d420 100644 --- a/web/package.json +++ b/web/package.json @@ -13,7 +13,8 @@ "vue": "^2.6.11", "vue-property-decorator": "^8.4.1", "vue-router": "^3.1.6", - "vue-select": "^3.9.5" + "vue-select": "^3.9.5", + "vuex": "^3.4.0" }, "devDependencies": { "@vue/cli-plugin-babel": "~4.3.0", diff --git a/web/src/App.vue b/web/src/App.vue index daf25ae..4b2d485 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -28,7 +28,7 @@ class App extends Vue { let old = document.getElementById(linkId) old && old.remove() - let token = this.$store.getCookie('bow-token') + let token = this.$storage.getCookie('bow-token') if (token) { // <link title="bow" type="application/opensearchdescription+xml" rel="search" href="<%= VUE_APP_BOW_PUBLIC_API_URL %>/api/v1/opensearch"> let link = document.createElement('link') @@ -44,7 +44,7 @@ class App extends Vue { beforeMount() { console.log('beforeMounted App') this.insertOpenSearch() - this.$store.addListener('bow-user', this.insertOpenSearch, this) + this.$storage.addListener('bow-user', this.insertOpenSearch, this) } } export default App diff --git a/web/src/components/CloudTags.vue b/web/src/components/CloudTags.vue new file mode 100644 index 0000000..8bf9d7b --- /dev/null +++ b/web/src/components/CloudTags.vue @@ -0,0 +1,59 @@ +<template> + <div class="cloud-tags"> + <span class="tag" v-for="(tag, i) in tags" :key="tag" :style="{fontSize: getFont(counts[i]) + 'px'}">{{ tag }}</span> + </div> +</template> + +<script> +import { Component, Vue } from 'vue-property-decorator' + +@Component({ + name: 'CloudTags', + components: {} +}) +class CloudTags extends Vue { + + get user() { + return this.$storage.get('bow-user') + } + + get tags() { + return this.$store.state.tags[0].slice(0, this.user.maxtagincloud) + } + + get counts() { + return this.$store.state.tags[1] + } + + get tmax() { + return this.counts[0] || 1 + } + + get tmin() { + return this.counts[this.counts.length - 1] || 0 + } + + getFont(count) { + let result = 30 * (count - this.tmin) / (this.tmax - this.tmin) + if (result < 10) { + result = 10 + } + + return result + } +} + +export default CloudTags +</script> + +<style lang="less" scoped> +.cloud-tags { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; +} +.tag { + margin: 4px; +} +</style> \ No newline at end of file diff --git a/web/src/components/layout/Header.vue b/web/src/components/layout/Header.vue index b7efaf2..221ffbe 100644 --- a/web/src/components/layout/Header.vue +++ b/web/src/components/layout/Header.vue @@ -41,26 +41,26 @@ class Header extends Vue { this.$fetch .delete(`/users/${this.user.id}/auth`) .then(() => this.loadData()) - .then(() => this.$store.delete('bow-user')) + .then(() => this.$storage.delete('bow-user')) .then(() => this.$router.push({ name: 'Login' })) } loadData() { console.log('loadData Header') - this.user = this.$store.get('bow-user') || {} - this.authenticated = !!this.$store.getCookie('bow-token') + this.user = this.$storage.get('bow-user') || {} + this.authenticated = !!this.$storage.getCookie('bow-token') } beforeMount() { console.log('beforeMounted Header') this.loadData() - this.$store.addListener('bow-user', this.loadData, this) + this.$storage.addListener('bow-user', this.loadData, this) } // beforeUpdate() { // console.log('beforeUpdate Header') - // this.user = this.$store.get('bow-user') - // this.token = this.$store.getCookie('bow-token') + // this.user = this.$storage.get('bow-user') + // this.token = this.$storage.getCookie('bow-token') // } } diff --git a/web/src/components/layout/Sidebar.vue b/web/src/components/layout/Sidebar.vue index 4b75544..13d7c22 100644 --- a/web/src/components/layout/Sidebar.vue +++ b/web/src/components/layout/Sidebar.vue @@ -1,7 +1,7 @@ <template> <div class="sidebar"> <div class="sidebar-logo"> - <a href="/"><img src="../../assets/img/logos/bow.svg" alt="Bow Logo" /></a> + <a href="/"><img src="../../assets/img/logos/bow.svg" alt="Bow Logo"/></a> </div> <div class="sidebar-menu"> <div class="sidebar-menu-link"> @@ -38,108 +38,107 @@ class Sidebar extends Vue { loadData() { console.log('loadData Header') - this.user = this.$store.get('bow-user') || {} + this.user = this.$storage.get('bow-user') || {} } beforeMount() { this.statsUrl = this.$fetch.createUrl('/system/stats') this.loadData() - this.$store.addListener('bow-user', this.loadData, this) + this.$storage.addListener('bow-user', this.loadData, this) } - } export default Sidebar </script> <style scoped lang="less"> - .sidebar { - position: fixed; - left: 0; +.sidebar { + position: fixed; + left: 0; + + width: var(--sidebar-width); + height: 100%; + /* FIXME LK : TMP */ + overflow: hidden; +} - width: var(--sidebar-width); - height: 100%; - /* FIXME LK : TMP */ overflow: hidden; - } +.sidebar-logo { + display: flex; + justify-content: center; + position: fixed; - .sidebar-logo { - display: flex; - justify-content: center; - position: fixed; + width: var(--sidebar-width); + height: var(--header-height); + padding: 20px; + + text-align: center; + + background-color: #fff; + border-right: 1px solid #eee; + + font-size: 36px; +} + +.sidebar-menu { + display: flex; + flex-direction: column; + justify-content: space-between; + + height: 100%; + padding-top: var(--header-height); + + background-color: var(--color-bg-sidebar); +} - width: var(--sidebar-width); - height: var(--header-height); - padding: 20px; +.sidebar-menu-link { + display: flex; + justify-content: center; + align-items: center; - text-align: center; + position: relative; + flex: 1; + max-height: 150px; - background-color: #FFF; - border-right: 1px solid #EEE; + text-align: center; - font-size: 36px; + .icon { + color: var(--color-white); + font-size: 30px; } + a, + span { + width: 100%; - .sidebar-menu { + z-index: 10; + } + a { display: flex; - flex-direction: column; - justify-content: space-between; + align-items: center; + justify-content: center; height: 100%; - padding-top: var(--header-height); - - background-color: var(--color-bg-sidebar); } + &:hover .icon { + color: #fff; + } + &:before { + content: ''; + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 0; - .sidebar-menu-link { - display: flex; - justify-content: center; - align-items: center; + background-color: var(--color-hover); + opacity: 0; - position: relative; - flex: 1; - max-height: 150px; - - text-align: center; - - .icon { - color: var(--color-white); - font-size: 30px; - } - a, - span { - width: 100%; - - z-index: 10; - } - a { - display: flex; - align-items: center; - justify-content: center; - - height: 100%; - } - &:hover .icon { - color: #FFF; - } - &:before{ - content: ''; - position: absolute; - top: 0; - left: 0; - - height: 100%; - width: 0; - - background-color: var(--color-hover); - opacity: 0; - - transition: width 0.4s ease-in-out, opacity 0.4s ease-in-out; - } - &:hover:before { - width: 100%; - - opacity: 1; - } + transition: width 0.4s ease-in-out, opacity 0.4s ease-in-out; } + &:hover:before { + width: 100%; + + opacity: 1; + } +} </style> diff --git a/web/src/main.js b/web/src/main.js index 3d471b5..4a0ecfe 100644 --- a/web/src/main.js +++ b/web/src/main.js @@ -4,6 +4,7 @@ import './class-component-hooks' import Vue from 'vue' import App from './App.vue' import router from './router' +import store from './store' import FetchHelper from '@/utils/FetchHelper.js' import StoreHelper from './utils/Store' @@ -13,17 +14,18 @@ window.FRONTEND_URL = process.env.BASE_URL // register utilities function on window, Vue and Vue instance if (typeof window !== 'undefined') { window.$fetch = FetchHelper - window.$store = StoreHelper + window.$storage = StoreHelper } Vue.$fetch = FetchHelper -Vue.$store = StoreHelper +Vue.$storage = StoreHelper Vue.config.productionTip = false Vue.prototype.$fetch = FetchHelper -Vue.prototype.$store = StoreHelper +Vue.prototype.$storage = StoreHelper new Vue({ router, - render: h => h(App) + store, + render: (h) => h(App) }).$mount('#app') diff --git a/web/src/router/index.js b/web/src/router/index.js index 437a8e0..29185ad 100644 --- a/web/src/router/index.js +++ b/web/src/router/index.js @@ -28,7 +28,7 @@ const routes = [ meta: { requiresAuth: true }, - props: route => ({ + props: (route) => ({ id: route.query.id, tags: route.query.tags, fulltext: route.query.fulltext, @@ -45,7 +45,7 @@ const routes = [ meta: { requiresAuth: true }, - props: route => ({ + props: (route) => ({ id: route.params.id, uri: route.query.uri, description: route.query.description, @@ -61,8 +61,7 @@ const routes = [ // route level code-splitting // 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') + component: () => import(/* webpackChunkName: "about" */ '../views/About.vue') } ] @@ -73,10 +72,7 @@ const router = new VueRouter({ }) router.beforeEach((to, from, next) => { - if ( - to.matched.some(record => record.meta.requiresAuth) && - !window.$store.getCookie('bow-token') - ) { + if (to.matched.some((record) => record.meta.requiresAuth) && !window.$storage.getCookie('bow-token')) { next({ path: '/login', params: { nextUrl: to.fullPath } diff --git a/web/src/store/index.js b/web/src/store/index.js new file mode 100644 index 0000000..bbbbc16 --- /dev/null +++ b/web/src/store/index.js @@ -0,0 +1,22 @@ +import Vue from 'vue' +import Vuex from 'vuex' + +Vue.use(Vuex) + +export default new Vuex.Store({ + state: { + user: {}, + tags: [[],[]], + result: [], + size: 0 + }, + getters: {}, + mutations: { + queryResponse(state, payload) { + state.tags = payload.tags + state.result = payload.result + state.size = payload.size + } + }, + actions: {} +}) diff --git a/web/src/views/Home.vue b/web/src/views/Home.vue index 404de4c..a6008e9 100644 --- a/web/src/views/Home.vue +++ b/web/src/views/Home.vue @@ -1,6 +1,12 @@ <template> <div class="home"> <div>{{ errorMsg }}</div> + <CloudTags></CloudTags> + <div class="navigation"> + <button><<</button> <button><</button> + {{ 1 + queryResponse.first }}-{{ queryResponse.first + queryResponse.limit }} + <button>></button> <button>>></button> / {{ queryResponse.total }} + </div> <div class="bookmarks-list"> <Bookmark v-for="bookmark in bookmarks" @@ -15,11 +21,12 @@ // @ is an alias to /src import { Component, Prop, Vue } from 'vue-property-decorator' import Bookmark from '@/components/Bookmark' +import CloudTags from '@/components/CloudTags' @Component({ name: 'Home', props: ['id', 'tags', 'fulltext', 'query', 'orderby', 'orderdesc', 'first'], - components: { Bookmark } + components: { CloudTags, Bookmark } }) class Home extends Vue { @Prop id @@ -38,7 +45,10 @@ class Home extends Vue { mFirst = this.first || 0 errorMsg = '' - bookmarks = [] + queryResponse = {} + get bookmarks() { + return this.queryResponse.result + } fetchBookmark() { let searchParams = new URLSearchParams() @@ -52,7 +62,8 @@ class Home extends Vue { this.$fetch.get(`/bookmarks?${searchParams.toString()}`).then( data => { - this.bookmarks = data + this.queryResponse = data + this.$store.commit('queryResponse', data) }, err => { this.errorMsg = err.cause diff --git a/web/src/views/Login.vue b/web/src/views/Login.vue index acac899..8d647bc 100644 --- a/web/src/views/Login.vue +++ b/web/src/views/Login.vue @@ -5,12 +5,7 @@ <div class="form-row"> <label for="login">Login</label> - <input - v-model="data.email" - type="text" - name="login" - placeholder="somebody@somewhere.world" - /> + <input v-model="data.email" type="text" name="login" placeholder="somebody@somewhere.world" /> </div> <div class="form-row"> @@ -47,17 +42,17 @@ class Login extends Vue { login() { this.$fetch.post('/users/auth', this.data).then( - user => { + (user) => { console.log('ok', user) - this.$store.set('bow-user', user) + this.$storage.set('bow-user', user) - if(this.$route.params.nextUrl != null){ + if (this.$route.params.nextUrl != null) { this.$router.push(this.$route.params.nextUrl) } else { - this.$router.push({ name: 'Home'}) + this.$router.push({ name: 'Home' }) } }, - err => { + (err) => { console.log('ko', err) this.errorMsg = err.cause } diff --git a/web/yarn.lock b/web/yarn.lock index f9561a4..06c9027 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -8029,6 +8029,11 @@ vue@^2.6.10, vue@^2.6.11: resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35..." integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ== +vuex@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.4.0.tgz#20cc086062d750769fce1febb..." + integrity sha512-ajtqwEW/QhnrBZQsZxCLHThZZaa+Db45c92Asf46ZDXu6uHXgbfVuBaJ4gzD2r4UX0oMJHstFwd2r2HM4l8umg== + watchpack@^1.6.0: version "1.6.1" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.1.tgz#280da0a87185921..." -- To stop receiving notification emails like this one, please contact chorem.org SCM administrator <admin+scm@chorem.org>.