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 235505ff301ebd30c8797c5de8a7fe323378eb90 Author: Benjamin <poussin@codelutin.com> Date: Sat May 16 01:06:12 2020 +0200 correction policy sur bookmark ajout preference (debut) amelioration affichage bookmark recherche par clique sur tag (et mise a jour zone de recherche) --- .gitlab-ci.yml | 3 + cmd/bow/main.go | 8 +- migrate/001_init_schema.sql | 8 +- pkg/http/opensearchResource.go | 2 +- pkg/http/router.go | 45 +++++++---- pkg/repository/database.go | 8 +- pkg/repository/userRepository.go | 2 +- web/package.json | 2 +- web/src/class-component-hooks.js | 8 ++ web/src/components/Bookmark.vue | 89 ++++++++++++++++------ web/src/components/SearchInput.vue | 23 +++--- web/src/components/Tags.vue | 18 ----- web/src/components/bookmark/Alias.vue | 25 ++++++ web/src/components/bookmark/BookmarkDate.vue | 41 ++++++++++ web/src/components/{ => bookmark}/Description.vue | 9 +++ web/src/components/{ => bookmark}/LinkCount.vue | 0 web/src/components/bookmark/Tags.vue | 86 +++++++++++++++++++++ web/src/components/bookmark/Visit.vue | 32 ++++++++ web/src/components/layout/Header.vue | 2 +- web/src/components/preferences/Action.vue | 37 +++++++++ web/src/components/preferences/Actions.vue | 53 +++++++++++++ web/src/components/preferences/MaxResultEditor.vue | 43 +++++++++++ web/src/main.js | 11 +-- web/src/views/Home.vue | 17 +---- web/src/views/Preferences.vue | 28 +++++-- 25 files changed, 492 insertions(+), 108 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2767dd4..a1080b9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -30,3 +30,6 @@ deploy: - for f in script config secret; do if [ -d $f ]; then FILES="$FILES $f"; fi; done - zip -r $ZIP_NAME $FILES - curl -F "name=$CI_PROJECT_NAME" -F "flavour=$CI_COMMIT_REF_SLUG" -F "target=saas" -F "info=${CI_COMMIT_REF_SLUG/master/latest}(${CI_COMMIT_SHORT_SHA}) - $CI_COMMIT_MESSAGE" -F "composeFile=$STACK_FILE" -F "zip=@$ZIP_NAME" "https://swarm-deployer.cloud.codelutin.com/api/v1/projects?swarm-deployer-to..." + environment: + name: ${CI_BUILD_REF_NAME} + url: https://bookmarks.cl diff --git a/cmd/bow/main.go b/cmd/bow/main.go index 78cd526..6c6dbb2 100644 --- a/cmd/bow/main.go +++ b/cmd/bow/main.go @@ -13,13 +13,19 @@ func main() { databaseURL := os.Getenv("DATABASE_URL") secretKey := os.Getenv("SECRET_KEY") bowPublicURL := os.Getenv("BOW_PUBLIC_URL") + migrateDir := os.Getenv("DATABASE_MIGRATION_DIR") if secretKey == "" { // for dev only secretKey = "AZERTYUIOPQSDFGHJKLMWXCVBN" } + + if migrateDir == "" { + migrateDir = "migrate" + } + log.Println("Init database") - repository.Init(databaseURL, true) + repository.Init(databaseURL, migrateDir, true) utils.JwtInit([]byte(secretKey)) // u := model.BowUser{ID: utils.GenUUID(), Login: "bpoussin", Emails: []string{"benjamin@pouss.in"}, Password: "toto"} diff --git a/migrate/001_init_schema.sql b/migrate/001_init_schema.sql index 6e3e0bc..bdec668 100644 --- a/migrate/001_init_schema.sql +++ b/migrate/001_init_schema.sql @@ -208,13 +208,16 @@ CREATE POLICY bowgroup_access_update ON bowgroup FOR UPDATE USING (admin @> ('{' || current_user || '}')::uuid[]); --- tout le monde peut lire les url et les publicAlias des bookmarks (peut etre faire une table PublicAlias pour les sortir) +-- Seul le propriétaire peut tout faire sur son bookmark CREATE POLICY bookmark_access ON bookmark - USING (true); + FOR ALL + USING (owner = current_user::uuid); -- si un bookmark a comme tag '@toto' et proprietaire '1234' alors il est visible de toutes les utilisateurs -- appartenant (admin, writer, reader) au groupe 'toto' dont l'utilisateur '1234' est admin ou writer CREATE POLICY bookmark_access_group ON bookmark + FOR SELECT + TO person USING ( current_user::uuid in (SELECT unnest(admin || writer || reader) FROM bowgroup WHERE tags @> ('{@' || name || '}')::text[] AND ('{' || owner || '}')::uuid[] <@ (admin || writer))); @@ -236,6 +239,7 @@ CREATE POLICY bowUser_nobody_insert ON bowUser TO nobody WITH CHECK (true); +-- pour lire les urls et les alias publics CREATE POLICY bookmark_nobody_access on bookmark FOR SELECT TO nobody diff --git a/pkg/http/opensearchResource.go b/pkg/http/opensearchResource.go index 013be16..9855b33 100644 --- a/pkg/http/opensearchResource.go +++ b/pkg/http/opensearchResource.go @@ -139,7 +139,7 @@ func getAction(currentUser model.BowUser, ask string) model.Action { } var result model.Action - for _, a := range currentUser.Actions { + for _, a := range actions { prefix := a.Prefix log.Printf("getAction %v == %v %v (%v && %v || %v) \n", ask, result, a, strings.HasPrefix(ask, prefix), result.Prefix == "", len(prefix) > len(result.Prefix)) if strings.HasPrefix(ask, prefix) && (result.Prefix == "" || len(prefix) > len(result.Prefix)) { diff --git a/pkg/http/router.go b/pkg/http/router.go index 69d38a6..04a0c55 100644 --- a/pkg/http/router.go +++ b/pkg/http/router.go @@ -38,20 +38,24 @@ func Start(bowPublicURL string, addr string) { s.HandleFunc("/system/liveness", isAlive).Methods(http.MethodGet, http.MethodOptions) s.HandleFunc("/users", createUser).Methods(http.MethodPost, http.MethodOptions) s.HandleFunc("/users/auth", createAuth).Methods(http.MethodPost, http.MethodOptions) - s.HandleFunc("/users/{id}", getUser).Methods(http.MethodGet, http.MethodOptions) - s.HandleFunc("/users/{id}", deleteUser).Methods(http.MethodDelete, http.MethodOptions) - s.HandleFunc("/users/{id}/auth", createAuth).Methods(http.MethodPost, http.MethodOptions) - s.HandleFunc("/users/{id}/auth", deleteAuth).Methods(http.MethodDelete, http.MethodOptions) - s.HandleFunc("/users/{id}/password", updateUserPassword).Methods(http.MethodPut, http.MethodOptions) - s.HandleFunc("/users/{id}/token", addUserToken).Methods(http.MethodPost, http.MethodOptions) - s.HandleFunc("/users/{id}/unconfirmedemails", addUserUnconfirmedEmail).Methods(http.MethodPost, http.MethodOptions) - s.HandleFunc("/users/{id}/unconfirmedemails/{token}", confirmUserEmail).Methods(http.MethodGet, http.MethodOptions) - s.HandleFunc("/users/{id}/authenticationinfo", updateUserAuthenticationInfo).Methods(http.MethodPut, http.MethodOptions) - s.HandleFunc("/users/{id}/actions", updateUserActions).Methods(http.MethodPut, http.MethodOptions) - s.HandleFunc("/users/{id}/autoscreenshot", updateUserAutoScreenshot).Methods(http.MethodPut, http.MethodOptions) - s.HandleFunc("/users/{id}/autofavicon", updateUserAutoFavicon).Methods(http.MethodPut, http.MethodOptions) - s.HandleFunc("/users/{id}/maxtagincloud", updateUserMaxTagInCloud).Methods(http.MethodPut, http.MethodOptions) - s.HandleFunc("/users/{id}/maxresult", updateUserMaxResult).Methods(http.MethodPut, http.MethodOptions) + + u := s.PathPrefix("/users/{id}").Subrouter() + u.Use(convertCurrentToId) + u.HandleFunc("", getUser).Methods(http.MethodGet, http.MethodOptions) + u.HandleFunc("", deleteUser).Methods(http.MethodDelete, http.MethodOptions) + u.HandleFunc("/auth", createAuth).Methods(http.MethodPost, http.MethodOptions) + u.HandleFunc("/auth", deleteAuth).Methods(http.MethodDelete, http.MethodOptions) + u.HandleFunc("/password", updateUserPassword).Methods(http.MethodPut, http.MethodOptions) + u.HandleFunc("/token", addUserToken).Methods(http.MethodPost, http.MethodOptions) + u.HandleFunc("/unconfirmedemails", addUserUnconfirmedEmail).Methods(http.MethodPost, http.MethodOptions) + u.HandleFunc("/unconfirmedemails/{token}", confirmUserEmail).Methods(http.MethodGet, http.MethodOptions) + u.HandleFunc("/authenticationinfo", updateUserAuthenticationInfo).Methods(http.MethodPut, http.MethodOptions) + u.HandleFunc("/actions", updateUserActions).Methods(http.MethodPut, http.MethodOptions) + u.HandleFunc("/autoscreenshot", updateUserAutoScreenshot).Methods(http.MethodPut, http.MethodOptions) + u.HandleFunc("/autofavicon", updateUserAutoFavicon).Methods(http.MethodPut, http.MethodOptions) + u.HandleFunc("/maxtagincloud", updateUserMaxTagInCloud).Methods(http.MethodPut, http.MethodOptions) + u.HandleFunc("/maxresult", updateUserMaxResult).Methods(http.MethodPut, http.MethodOptions) + s.HandleFunc("/bookmarks", getBookmarks).Methods(http.MethodGet, http.MethodOptions) s.HandleFunc("/bookmarks", addBookmark).Methods(http.MethodPost, http.MethodOptions) s.HandleFunc("/bookmarks/tags", getTags).Methods(http.MethodGet, http.MethodOptions) @@ -60,6 +64,7 @@ func Start(bowPublicURL string, addr string) { s.HandleFunc("/bookmarks/{id}", updateBookmark).Methods(http.MethodPut, http.MethodOptions) s.HandleFunc("/bookmarks/{id}/visit", addOneVisit).Methods(http.MethodGet, http.MethodPost, http.MethodOptions) s.HandleFunc("/bookmarks/{id}/authenticationinfo", updateBookmarkAuthenticationInfo).Methods(http.MethodPut, http.MethodOptions) + s.HandleFunc("/opensearch", doActions).Methods(http.MethodGet, http.MethodPost, http.MethodOptions).Queries(constant.Action, "{ask}") s.HandleFunc("/opensearch", doSuggestion).Methods(http.MethodGet, http.MethodPost, http.MethodOptions).Queries(constant.Suggestion, "{ask}") s.HandleFunc("/opensearch", opensearchFile).Methods(http.MethodGet, http.MethodOptions) @@ -115,6 +120,18 @@ func withoutAuthenticationEndpoint(r *http.Request) bool { return result } +func convertCurrentToId(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + id := mux.Vars(r)["id"] + if id == "current" { + currentUser := r.Context().Value(constant.User).(model.BowUser) + mux.Vars(r)["id"] = currentUser.ID + } + + next.ServeHTTP(w, r) + }) +} + func authentication(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if withoutAuthenticationEndpoint(r) { diff --git a/pkg/repository/database.go b/pkg/repository/database.go index 128b596..b7fb676 100644 --- a/pkg/repository/database.go +++ b/pkg/repository/database.go @@ -33,11 +33,11 @@ var bookmakFields = make(map[string]string) /* Init initialise la connexion a la base en utilisant */ -func Init(databaseURL string, doMigration bool) { +func Init(databaseURL string, migrateDir string, doMigration bool) { computeFieldAvailable() if doMigration { - migrateDatabase(databaseURL) + migrateDatabase(databaseURL, migrateDir) } poolConfig, err := pgxpool.ParseConfig(databaseURL) @@ -96,7 +96,7 @@ func extractErrorLine(source string, position int) (errorLineExtract, error) { return ele, nil } -func migrateDatabase(databaseURL string) { +func migrateDatabase(databaseURL string, migrateDir string) { ctx := context.Background() dbConfig, err := pgx.ParseConfig(databaseURL) @@ -119,7 +119,7 @@ func migrateDatabase(databaseURL string) { data["nobody_password"] = dbConfig.Password migrator.Data = data - err = migrator.LoadMigrations("migrate") + err = migrator.LoadMigrations(migrateDir) if err != nil { log.Fatalf("Error loading migrations:\n %v\n", err) } diff --git a/pkg/repository/userRepository.go b/pkg/repository/userRepository.go index 620fcb5..24d4acb 100644 --- a/pkg/repository/userRepository.go +++ b/pkg/repository/userRepository.go @@ -28,7 +28,7 @@ func UserJSON(currentUser model.BowUser, id string, fields ...string) (string, e askedFields = strings.Join(fields, ", ") } - allFields := strings.ReplaceAll(askedFields, "unconfirmedemailsList,", "") + allFields := strings.ReplaceAll(askedFields, "unconfirmedemails,", "") q := &query{sql: fmt.Sprintf(` WITH unconfirmedemailsList as (select id as eid, jsonb_array_elements(unconfirmedemails)::jsonb->'email' as unconfirmedemailsList from bowuser u where id=$1), diff --git a/web/package.json b/web/package.json index 5d1d465..e265de3 100644 --- a/web/package.json +++ b/web/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "serve": "vue-cli-service serve", + "serve": "vue-cli-service serve --port 7780", "build": "vue-cli-service build", "lint": "vue-cli-service lint" }, diff --git a/web/src/class-component-hooks.js b/web/src/class-component-hooks.js new file mode 100644 index 0000000..7dd318c --- /dev/null +++ b/web/src/class-component-hooks.js @@ -0,0 +1,8 @@ +import Component from 'vue-class-component' + +// Register the router hooks with their names +Component.registerHooks([ + 'beforeRouteEnter', + 'beforeRouteLeave', + 'beforeRouteUpdate' +]) diff --git a/web/src/components/Bookmark.vue b/web/src/components/Bookmark.vue index aecb3e7..ecf83cc 100644 --- a/web/src/components/Bookmark.vue +++ b/web/src/components/Bookmark.vue @@ -1,44 +1,58 @@ <template> +<div> <div class="bookmark"> - <LinkCount :bookmarkId="bookmark.id" :link="bookmark.uri"> - <img v-if="bookmark.favicon" :src="hexaToImg(bookmark.favicon)" /> - </LinkCount> - <LinkCount :bookmarkId="bookmark.id" :link="bookmark.uri"> + <LinkCount class="screenshot" :bookmarkId="bookmark.id" :link="bookmark.uri"> <img v-if="bookmark.screenshot" :src="hexaToImg(bookmark.screenshot)" /> </LinkCount> - <LinkCount :bookmarkId="bookmark.id" :link="bookmark.uri"> - {{ bookmark.uri }} - </LinkCount> - <span>{{ bookmark.visit }}</span> - <!-- <span>{{ bookmark.authenticationinfo }}</span> faire afficher une popup de modification de l'objet auth--> - <span>{{ bookmark.importdate || bookmark.creationdate }}</span> - <Description :description="bookmark.description"></Description> - <Tags :tags="bookmark.tags"></Tags> - <!-- <span>{{ bookmark.lang }}</span> --> - <!-- <span>{{ bookmark.owner }}</span> si mon id n'est pas le meme alors c'est le bookmark d'un autre partage par un group --> - <span>{{ bookmark.privatealias }}</span> - <span>{{ bookmark.publicalias }}</span> - <span>{{ bookmark.updatedate }}</span> - <button @click.prevent="edit">Edit</button> + <div class="info"> + <div class="info-header"> + <Visit :visit="bookmark.visit"></Visit> + <LinkCount :bookmarkId="bookmark.id" :link="bookmark.uri"> + <img v-if="bookmark.favicon" :src="hexaToImg(bookmark.favicon)" /> + </LinkCount> + <LinkCount class="uri" :bookmarkId="bookmark.id" :link="bookmark.uri"> + {{ bookmark.uri }} + </LinkCount> + <button @click.prevent="edit">Edit</button> + </div> + <!-- <span>{{ bookmark.authenticationinfo }}</span> faire afficher une popup de modification de l'objet auth--> + <Description :description="bookmark.description"></Description> + <div class="info-footer"> + <BookmarkDate :creationDate="date" :updateDate="bookmark.updatedate"></BookmarkDate> + <Tags :tags="bookmark.tags"></Tags> + <!-- <span>{{ bookmark.lang }}</span> --> + <!-- <span>{{ bookmark.owner }}</span> si mon id n'est pas le meme alors c'est le bookmark d'un autre partage par un group --> + <Aliases :bookmarkId="bookmark.id" :link="bookmark.uri" :aliases="bookmark.privatealias" title="alias privé"></Aliases> + <Aliases :bookmarkId="bookmark.id" :link="bookmark.uri" :aliases="bookmark.publicalias" title="alias public"></Aliases> + </div> + </div> </div> +</div> </template> <script> import { Component, Prop, Vue } from 'vue-property-decorator' -import Tags from '@/components/Tags' -import Description from '@/components/Description' -import LinkCount from '@/components/LinkCount' +import Aliases from '@/components/bookmark/Alias' +import BookmarkDate from '@/components/bookmark/BookmarkDate' +import Tags from '@/components/bookmark/Tags' +import Description from '@/components/bookmark/Description' +import LinkCount from '@/components/bookmark/LinkCount' +import Visit from '@/components/bookmark/Visit' @Component({ name: 'Bookmark', props: ['bookmark'], - components: { Description, LinkCount, Tags } + components: { Aliases, BookmarkDate, Description, LinkCount, Tags, Visit } }) class Bookmark extends Vue { @Prop bookmark + get date() { + return this.bookmark.importdate || this.bookmark.creationdate + } + edit() { - this.$router.push({ name: 'Edit', params: { id: this.bookmark.id }}) + this.$router.push({ name: 'Edit', params: { id: this.bookmark.id } }) } hexaToImg(hexa) { @@ -64,6 +78,7 @@ export default Bookmark .bookmark { display: flex; flex-direction: row; + align-items: stretch; justify-content: space-around; //center; align-items: center; @@ -71,5 +86,31 @@ export default Bookmark background-color: var(--color-bg-bookmark); border: 1px solid #eee; + + .screenshot { + flex-grow: 0; + width: 65px; + margin: 2px; + } + + .info, .uri { + flex-grow: 2; + } + + .info { + display: flex; + flex-direction: column; + + .info-header, .info-footer { + margin: 2px; + display: flex; + flex-direction: row; + justify-content: space-between; + } + } + + Description { + width: 100%; + } } -</style> \ No newline at end of file +</style> diff --git a/web/src/components/SearchInput.vue b/web/src/components/SearchInput.vue index 487e01b..775fee5 100644 --- a/web/src/components/SearchInput.vue +++ b/web/src/components/SearchInput.vue @@ -20,7 +20,7 @@ </template> <script> -import { Component, Vue } from 'vue-property-decorator' +import { Component, Vue, Watch } from 'vue-property-decorator' @Component({ name: 'SearchInput', @@ -75,13 +75,13 @@ class SearchInput extends Vue { console.log('search', this.mQuery) } - update() { - this.tags = this.$route.query.tags - this.fulltext = this.$route.query.fulltext - this.query = this.$route.query.query - this.orderby = this.$route.query.orderby - this.orderdesc = this.$route.query.orderdesc - this.first = this.$route.query.first + update(route) { + this.tags = route.query.tags + this.fulltext = route.query.fulltext + this.query = route.query.query + this.orderby = route.query.orderby + this.orderdesc = route.query.orderdesc + this.first = route.query.first this.mTags = this.tags || '' this.mFulltext = this.fulltext || '' @@ -91,9 +91,10 @@ class SearchInput extends Vue { this.mFirst = this.first || 0 } - beforeMount() { - console.log('beforeMount SearchInput') - this.update() + @Watch('$route', { immediate: true, deep: true }) + onRouteChange(to, from) { + console.log('onRouteChange SearchInput', to, from) + this.update(to) } } diff --git a/web/src/components/Tags.vue b/web/src/components/Tags.vue deleted file mode 100644 index 2bf67d2..0000000 --- a/web/src/components/Tags.vue +++ /dev/null @@ -1,18 +0,0 @@ -<template> - <div class="tags">{{ (tags || []).join(" ") }}</div> -</template> - -<script> -import { Component, Prop, Vue } from 'vue-property-decorator' - -@Component({ - name: 'Tags', - props: ['tags'], - components: {} -}) -class Tags extends Vue { - @Prop tags -} - -export default Tags -</script> diff --git a/web/src/components/bookmark/Alias.vue b/web/src/components/bookmark/Alias.vue new file mode 100644 index 0000000..e932e23 --- /dev/null +++ b/web/src/components/bookmark/Alias.vue @@ -0,0 +1,25 @@ +<template> + <span class="aliases"> + <LinkCount class="alias" v-for="alias in aliases || []" :key="alias" :bookmarkId="bookmarkId" :link="link">{{ + alias + }}</LinkCount> + </span> +</template> + +<script> +import { Component, Prop, Vue } from 'vue-property-decorator' +import LinkCount from '@/components/bookmark/LinkCount' + +@Component({ + name: 'Aliases', + props: ['bookmarkId', 'link', 'aliases'], + components: { LinkCount } +}) +class Aliases extends Vue { + @Prop bookmarkId + @Prop link + @Prop aliases +} + +export default Aliases +</script> diff --git a/web/src/components/bookmark/BookmarkDate.vue b/web/src/components/bookmark/BookmarkDate.vue new file mode 100644 index 0000000..09e0820 --- /dev/null +++ b/web/src/components/bookmark/BookmarkDate.vue @@ -0,0 +1,41 @@ +<template> + <div :title="allDate" class="date">{{ shortCreationDate }}</div> +</template> + +<script> +import { Component, Prop, Vue } from 'vue-property-decorator' + +@Component({ + name: 'BookmarkDate', + props: ['creationDate', 'updateDate'], + components: {} +}) +class BookmarkDate extends Vue { + @Prop creationDate + @Prop updateDate + + get shortCreationDate() { + let d = new Date(this.creationDate) + return `${d.getFullYear()}/${this.padding0(d.getMonth() + 1)}/${this.padding0(d.getDate())}` + } + + padding0(number) { + if (number <= 9) { + return '0' + number + } + return number + } + + get allDate() { + let c = new Date(this.creationDate) + let result = `created at ${c.toString()}` + if (this.updateDate) { + let u = new Date(this.updateDate) + result += `\nupdated at ${u.toString()}` + } + return result + } +} + +export default BookmarkDate +</script> diff --git a/web/src/components/Description.vue b/web/src/components/bookmark/Description.vue similarity index 78% rename from web/src/components/Description.vue rename to web/src/components/bookmark/Description.vue index 016039f..a8b1d8e 100644 --- a/web/src/components/Description.vue +++ b/web/src/components/bookmark/Description.vue @@ -18,3 +18,12 @@ class Description extends Vue { export default Description </script> + +<style scoped lang="less"> +.description { + width: 100%; + textarea { + width: 100%; + } +} +</style> \ No newline at end of file diff --git a/web/src/components/LinkCount.vue b/web/src/components/bookmark/LinkCount.vue similarity index 100% rename from web/src/components/LinkCount.vue rename to web/src/components/bookmark/LinkCount.vue diff --git a/web/src/components/bookmark/Tags.vue b/web/src/components/bookmark/Tags.vue new file mode 100644 index 0000000..c3b892c --- /dev/null +++ b/web/src/components/bookmark/Tags.vue @@ -0,0 +1,86 @@ +<template> + <div class="tags"> + <span @click.prevent="addTag(tag)" class="tag" v-for="tag in tags || []" :key="tag">{{ tag }}</span> + </div> +</template> + +<script> +import { Component, Prop, Vue } from 'vue-property-decorator' + +@Component({ + name: 'Tags', + props: ['tags'], + components: {} +}) +class Tags extends Vue { + @Prop tags + + addTag(tag) { + console.log(tag) + let tags; + if (this.$route.query.tags) { + tags = this.$route.query.tags + ' ' + tag + } else { + tags = tag + } + this.$router.push({ name: 'Home', query: { tags } }) + } +} + +export default Tags +</script> + +<style scoped lang="less"> +.tags { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; +} +.tags .tag { + float: left; + position: relative; + width: auto; + height: 20px; + margin-left: 20px; + padding: 0 12px; + line-height: 20px; + background: #1f8dd6; + color: #fff; + // font-size: 18px; + // font-weight: 600; + text-decoration: none; +} + +.tags .tag:before { + content: ''; + position: absolute; + top: 0; + width: 0; + height: 0; + border-style: solid; + + right: -10px; + border-color: transparent transparent transparent #1f8dd6; + border-width: 10px 0 10px 10px; +} + +.tags .tag:after { + content: ''; + position: absolute; + top: 9px; + width: 4px; + height: 4px; + border-radius: 2px; + background: #fff; + box-shadow: -1px -1px 2px #004977; + right: -2px; +} + +.tag:hover { + background: #555; +} +.tag:hover:before { + border-color: transparent transparent transparent #555; +} +</style> diff --git a/web/src/components/bookmark/Visit.vue b/web/src/components/bookmark/Visit.vue new file mode 100644 index 0000000..ff306ea --- /dev/null +++ b/web/src/components/bookmark/Visit.vue @@ -0,0 +1,32 @@ +<template> + <span class="visit" title="nombre de visite">{{ visit }}</span> +</template> + +<script> +import { Component, Prop, Vue } from 'vue-property-decorator' + +@Component({ + name: 'Visit', + props: ['visit'], + components: {} +}) +class Visit extends Vue { + @Prop visit +} + +export default Visit +</script> + +<style scoped lang="less"> + .visit { + border-radius: 50%; + color: white; + background-color: rgb(163, 19, 9); + text-align: center; + padding: 8px; + } + + .visit * { + vertical-align: middle; + } +</style> diff --git a/web/src/components/layout/Header.vue b/web/src/components/layout/Header.vue index 5598a51..b7efaf2 100644 --- a/web/src/components/layout/Header.vue +++ b/web/src/components/layout/Header.vue @@ -47,7 +47,7 @@ class Header extends Vue { loadData() { console.log('loadData Header') - this.user = this.$store.get('bow-user') + this.user = this.$store.get('bow-user') || {} this.authenticated = !!this.$store.getCookie('bow-token') } diff --git a/web/src/components/preferences/Action.vue b/web/src/components/preferences/Action.vue new file mode 100644 index 0000000..53eca5f --- /dev/null +++ b/web/src/components/preferences/Action.vue @@ -0,0 +1,37 @@ +<template> + <div class="action"> + <input class="prefix" v-model="action.prefix" /> + <input class="action-item" v-model="action.action" /> + <input class="suggest" v-model="action.suggest" /> + </div> +</template> + +<script> +import { Component, Prop, Vue } from 'vue-property-decorator' + +@Component({ + name: 'Action', + props: ['action'], + components: {} +}) +class Action extends Vue { + @Prop action +} + +export default Action +</script> + +<style scoped lang="less"> +.action { + display: flex; + flex-direction: row; + align-items: stretch; + + .prefix { + width: 5em; + } + .action-item, .suggest { + flex-grow: 1 + } +} +</style> diff --git a/web/src/components/preferences/Actions.vue b/web/src/components/preferences/Actions.vue new file mode 100644 index 0000000..b7f7fcf --- /dev/null +++ b/web/src/components/preferences/Actions.vue @@ -0,0 +1,53 @@ +<template> + <div class="actions"> + <Action v-for="(action, i) in actions" :key="i" :action="action"></Action> + <div class="buttons"> + <button class="add" @click.prevent="addAction">Add action</button> + <button class="save" @click.prevent="save">Save actions</button> + </div> + </div> +</template> + +<script> +import { Component, Prop, Vue } from 'vue-property-decorator' +import Action from '@/components/preferences/Action' + +@Component({ + name: 'Actions', + props: ['actions'], + components: { Action } +}) +class Actions extends Vue { + @Prop actions + + addAction() { + this.actions.push({ prefix: '', action: '', suggest: '' }) + } + + save() { + this.$fetch.put('/users/current/actions', this.actions).then( + () => { + this.errorMsg = 'saved' + }, + (err) => { + console.log('ko', err) + this.errorMsg = err.cause + } + ) + } +} + +export default Actions +</script> + +<style scoped lang="less"> +.actions { + display: flex; + flex-direction: column; +} +.buttons { + display: flex; + flex-direction: row; + justify-content: center; +} +</style> diff --git a/web/src/components/preferences/MaxResultEditor.vue b/web/src/components/preferences/MaxResultEditor.vue new file mode 100644 index 0000000..e5991ae --- /dev/null +++ b/web/src/components/preferences/MaxResultEditor.vue @@ -0,0 +1,43 @@ +<template> + <div class="max-result-editor"> + <label for="maxresult">Max result</label><input id="maxresult" class="maxresult" v-model="user.maxresult" /> + <button class="save" @click.prevent="save">Save max result</button> + <span class="errorMsg">{{ errorMsg }}</span> + </div> +</template> + +<script> +import { Component, Prop, Vue } from 'vue-property-decorator' + +@Component({ + name: 'MaxResultEditor', + props: ['user'], + components: {} +}) +class MaxResultEditor extends Vue { + @Prop user + + errorMsg = '' + + save() { + this.$fetch.put('/users/current/maxresult', {maxresult: parseInt(this.user.maxresult)}).then( + () => { + this.errorMsg = 'saved' + }, + (err) => { + console.log('ko', err) + this.errorMsg = err.cause + } + ) + } +} + +export default MaxResultEditor +</script> + +<style scoped lang="less"> +.max-result-editor { + display: flex; + flex-direction: row; +} +</style> diff --git a/web/src/main.js b/web/src/main.js index 631e1e9..3d471b5 100644 --- a/web/src/main.js +++ b/web/src/main.js @@ -1,17 +1,12 @@ +// Make sure to register before importing any components +import './class-component-hooks' + import Vue from 'vue' import App from './App.vue' import router from './router' import FetchHelper from '@/utils/FetchHelper.js' -import Component from 'vue-class-component' import StoreHelper from './utils/Store' -// Register the router hooks with their names -Component.registerHooks([ - 'beforeRouteEnter', - 'beforeRouteLeave', - 'beforeRouteUpdate' -]) - window.BACKEND_URL = process.env.VUE_APP_BACKEND_URL window.FRONTEND_URL = process.env.BASE_URL diff --git a/web/src/views/Home.vue b/web/src/views/Home.vue index 96c2bbd..404de4c 100644 --- a/web/src/views/Home.vue +++ b/web/src/views/Home.vue @@ -64,27 +64,12 @@ class Home extends Vue { console.log('beforeMounted home') this.fetchBookmark() } - - // beforeUpdate() { - // console.log('beforeUpdate home') - // this.fetchBookmark() - // } - - beforeRouteEnter(to, from, next) { - console.log('beforeRouteEnter home', to, from, next) - next() - } - - beforeRouteUpdate(to, from, next) { - console.log('beforeRouteUpdate home', to, from, next) - next() - } } export default Home </script> -<style lang="less"> +<style scoped lang="less"> .bookmarks-list { padding-top: var(--margin--large); padding-left: var(--margin--medium); diff --git a/web/src/views/Preferences.vue b/web/src/views/Preferences.vue index 8710f4a..249c824 100644 --- a/web/src/views/Preferences.vue +++ b/web/src/views/Preferences.vue @@ -2,30 +2,46 @@ <div class="preferences"> <div>{{ errorMsg }}</div> <a :href="bookmarkletAdd">bookmarklet add</a> + <MaxResultEditor :user="user"></MaxResultEditor> + <Actions :actions="user.actions"></Actions> </div> </template> <script> // @ is an alias to /src import { Component, Vue } from 'vue-property-decorator' +import MaxResultEditor from '@/components/preferences/MaxResultEditor' +import Actions from '@/components/preferences/Actions' @Component({ - name: 'Preferences' + name: 'Preferences', + components: {MaxResultEditor, Actions} }) class Preferences extends Vue { - errorMsg = '' user = {} - bookmarkletAdd=`javascript:document.location='${location.protocol + '//' + location.host}/edit/new?uri='+encodeURIComponent(location.href)+'&description='+encodeURIComponent(document.title + '\\n' + (document.getSelection() || ''))` + bookmarkletAdd = `javascript:document.location='${location.protocol}//${location.host}/edit/new?uri='+encodeURIComponent(location.href)+'&description='+encodeURIComponent(document.title + '\\n' + (document.getSelection() || ''))` + + load() { + this.$fetch.get('/users/current').then( + (user) => { + console.log('ok', user) + this.user = user + }, + (err) => { + console.log('ko', err) + this.errorMsg = err.cause + } + ) + } beforeMount() { console.log('beforeMounted preference') + this.load() } - } export default Preferences </script> -<style lang="less"> -</style> +<style lang="less"></style> -- To stop receiving notification emails like this one, please contact chorem.org SCM administrator <admin+scm@chorem.org>.