Skip to content

Commit 6fd83f2

Browse files
authored
Feature/user favorite tags (#266)
* Add favorite tags for users * Added backend support for user favorite tags * Added ability to set and use favorite tags * Upgraded paper-tags-input so that the label can be set * Fixed indentation
1 parent 7a889e1 commit 6fd83f2

File tree

11 files changed

+135
-73
lines changed

11 files changed

+135
-73
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
BEGIN;
2+
3+
DROP TABLE app_user_favorite_tag;
4+
5+
COMMIT;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
BEGIN;
2+
3+
CREATE TABLE app_user_favorite_tag (
4+
user_id INTEGER NOT NULL,
5+
tag TEXT NOT NULL,
6+
FOREIGN KEY(user_id) REFERENCES app_user(id) ON DELETE CASCADE
7+
);
8+
CREATE INDEX app_user_favorite_tag_idx ON app_user_favorite_tag(tag);
9+
CREATE INDEX app_user_favorite_tag_user_id_idx ON app_user_favorite_tag(user_id);
10+
11+
COMMIT;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DROP TABLE app_user_favorite_tag;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CREATE TABLE app_user_favorite_tag (
2+
user_id INTEGER NOT NULL,
3+
tag TEXT NOT NULL,
4+
FOREIGN KEY(user_id) REFERENCES app_user(id) ON DELETE CASCADE
5+
);
6+
CREATE INDEX app_user_favorite_tag_idx ON app_user_favorite_tag(tag);
7+
CREATE INDEX app_user_favorite_tag_user_id_idx ON app_user_favorite_tag(user_id);

db/user-sql.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package db
33
import (
44
"database/sql"
55
"errors"
6+
"fmt"
67

78
"github.com/chadweimer/gomp/models"
89
"github.com/jmoiron/sqlx"
@@ -105,6 +106,12 @@ func (d *sqlUserDriver) ReadSettings(id int64) (*models.UserSettings, error) {
105106
return nil, err
106107
}
107108

109+
var tags []string
110+
if err := d.Db.Select(&tags, "SELECT tag FROM app_user_favorite_tag WHERE user_id = $1 ORDER BY tag ASC", id); err != nil {
111+
return nil, err
112+
}
113+
userSettings.FavoriteTags = tags
114+
108115
return userSettings, nil
109116
}
110117

@@ -123,6 +130,22 @@ func (d *sqlUserDriver) updateSettingstx(settings *models.UserSettings, tx *sqlx
123130
return err
124131
}
125132

133+
// Deleting and recreating seems inefficient. Maybe make this smarter.
134+
_, err = tx.Exec(
135+
"DELETE FROM app_user_favorite_tag WHERE user_id = $1",
136+
settings.UserID)
137+
if err != nil {
138+
return fmt.Errorf("deleting favorite tags before updating on user: %v", err)
139+
}
140+
for _, tag := range settings.FavoriteTags {
141+
_, err = tx.Exec(
142+
"INSERT INTO app_user_favorite_tag (user_id, tag) VALUES ($1, $2)",
143+
settings.UserID, tag)
144+
if err != nil {
145+
return fmt.Errorf("updating favorite tags on user: %v", err)
146+
}
147+
}
148+
126149
return nil
127150
}
128151

models/user.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ type User struct {
2929

3030
// UserSettings represents the settings for an individual user
3131
type UserSettings struct {
32-
UserID int64 `json:"userId" db:"user_id"`
33-
HomeTitle *string `json:"homeTitle" db:"home_title"`
34-
HomeImageURL *string `json:"homeImageUrl" db:"home_image_url"`
32+
UserID int64 `json:"userId" db:"user_id"`
33+
HomeTitle *string `json:"homeTitle" db:"home_title"`
34+
HomeImageURL *string `json:"homeImageUrl" db:"home_image_url"`
35+
FavoriteTags []string `json:"favoriteTags"`
3536
}
3637

3738
// Scan implements the sql.Scanner interface

static/package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

static/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"@cwmr/paper-fab-speed-dial": "^3.0.0",
3434
"@cwmr/paper-password-input": "^3.0.0",
3535
"@cwmr/paper-search": "^3.0.1",
36-
"@cwmr/paper-tags-input": "^3.1.0",
36+
"@cwmr/paper-tags-input": "^3.1.2",
3737
"@polymer/app-layout": "^3.1.0",
3838
"@polymer/app-route": "^3.0.0",
3939
"@polymer/app-storage": "^3.0.3",

static/src/components/tag-input.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {customElement, property } from '@polymer/decorators';
44
import { IronAjaxElement } from '@polymer/iron-ajax/iron-ajax.js';
55
import { PaperTagsInput } from '@cwmr/paper-tags-input/paper-tags-input.js';
66
import { GompBaseElement } from '../common/gomp-base-element.js';
7+
import { UserSettings } from '../models/models.js';
78
import '@polymer/iron-ajax/iron-ajax.js';
89
import '@polymer/iron-icon/iron-icon.js';
910
import '@polymer/iron-icons/iron-icons.js';
@@ -52,7 +53,7 @@ export class TagInput extends GompBaseElement {
5253
<input type="hidden" slot="input">
5354
</paper-input-container>
5455
55-
<iron-ajax bubbles="" id="getSuggestedTagsAjax" url="/api/v1/tags" params="{&quot;sort&quot;: &quot;frequency&quot;, &quot;dir&quot;: &quot;desc&quot;, &quot;count&quot;: 12}" on-request="handleGetSuggestedTagsRequest" on-response="handleGetSuggestedTagsResponse"></iron-ajax>
56+
<iron-ajax bubbles="" id="getSettingsAjax" url="/api/v1/users/current/settings" on-request="handleGetSettingsRequest" on-response="handleGetSettingsResponse"></iron-ajax>
5657
`;
5758
}
5859

@@ -61,15 +62,15 @@ export class TagInput extends GompBaseElement {
6162

6263
protected suggestedTags: string[] = [];
6364

64-
private get getSuggestedTagsAjax(): IronAjaxElement {
65-
return this.$.getSuggestedTagsAjax as IronAjaxElement;
65+
private get getSettingsAjax(): IronAjaxElement {
66+
return this.$.getSettingsAjax as IronAjaxElement;
6667
}
6768
private get tagsElement(): PaperTagsInput {
6869
return this.$.tags as PaperTagsInput;
6970
}
7071

7172
public refresh() {
72-
this.getSuggestedTagsAjax.generateRequest();
73+
this.getSettingsAjax.generateRequest();
7374
}
7475

7576
protected onSuggestedTagClicked(e: {model: {item: string}}) {
@@ -81,10 +82,10 @@ export class TagInput extends GompBaseElement {
8182
this.splice('suggestedTags', suggestedTagIndex, 1);
8283
}
8384
}
84-
protected handleGetSuggestedTagsRequest() {
85+
protected handleGetSettingsRequest() {
8586
this.suggestedTags = [];
8687
}
87-
protected handleGetSuggestedTagsResponse(e: CustomEvent<{response: string[]}>) {
88-
this.suggestedTags = e.detail.response;
88+
protected handleGetSettingsResponse(e: CustomEvent<{response: UserSettings}>) {
89+
this.suggestedTags = e.detail.response.favoriteTags;
8990
}
9091
}

static/src/models/models.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,10 @@ export interface User {
5050
}
5151

5252
export interface UserSettings {
53-
userId: string;
54-
homeTitle: string;
55-
homeImageUrl: string;
53+
userId: string;
54+
homeTitle: string;
55+
homeImageUrl: string;
56+
favoriteTags: string[];
5657
}
5758

5859
interface RecipeBase {

static/src/settings-view.ts

Lines changed: 68 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import '@polymer/paper-fab/paper-fab.js';
1717
import '@polymer/paper-input/paper-input.js';
1818
import '@polymer/paper-spinner/paper-spinner.js';
1919
import '@cwmr/paper-password-input/paper-password-input.js';
20+
import '@cwmr/paper-tags-input/paper-tags-input.js';
2021
import './shared-styles.js';
2122

2223
@customElement('settings-view')
@@ -63,55 +64,59 @@ export class SettingsView extends GompBaseElement {
6364
}
6465
@media screen and (max-width: 600px) {
6566
}
66-
</style>
67-
<div class="container">
68-
<paper-card>
69-
<div class="card-content">
70-
<h3>Security Settings</h3>
71-
<paper-input label="Username" value="[[currentUser.username]]" always-float-label="" disabled=""></paper-input>
72-
<paper-input label="Access Level" value="[[currentUser.accessLevel]]" always-float-label="" disabled=""></paper-input>
73-
<paper-password-input label="Current Password" value="{{currentPassword}}" always-float-label=""></paper-password-input>
74-
<paper-password-input label="New Password" value="{{newPassword}}" always-float-label=""></paper-password-input>
75-
<paper-password-input label="Confirm Password" value="{{repeatPassword}}" always-float-label=""></paper-password-input>
76-
</div>
77-
<div class="card-actions">
78-
<paper-button on-click="onUpdatePasswordClicked">
79-
<iron-icon icon="icons:lock-outline"></iron-icon>
80-
<span>Update Password</span>
81-
<paper-button>
82-
</div>
83-
</paper-card>
84-
</div>
85-
<div class="container">
86-
<paper-card>
87-
<div class="card-content">
88-
<h3>Home Settings</h3>
89-
<paper-input label="Title" always-float-label="" value="{{userSettings.homeTitle}}">
90-
<paper-icon-button slot="suffix" icon="icons:save" on-click="onSaveButtonClicked"></paper-icon-button>
91-
</paper-input>
92-
<form id="homeImageForm" enctype="multipart/form-data">
93-
<paper-input-container always-float-label="">
94-
<label slot="label">Image</label>
95-
<iron-input slot="input">
96-
<input id="homeImageFile" name="file_content" type="file" accept=".jpg,.jpeg,.png">
97-
</iron-input>
98-
<paper-icon-button slot="suffix" icon="icons:file-upload" on-click="onUploadButtonClicked"></paper-icon-button>
99-
</paper-input-container>
100-
</form>
101-
<img alt="Home Image" src="[[userSettings.homeImageUrl]]" class="responsive" hidden\$="[[!userSettings.homeImageUrl]]">
102-
</div>
103-
</paper-card>
104-
</div>
105-
<paper-dialog id="uploadingDialog" with-backdrop="">
106-
<h3><paper-spinner active=""></paper-spinner>Uploading</h3>
107-
</paper-dialog>
108-
109-
<a href="/create"><paper-fab icon="icons:add" class="green"></paper-fab></a>
110-
111-
<iron-ajax bubbles="" id="putPasswordAjax" url="/api/v1/users/current/password" method="PUT" on-response="handlePutPasswordResponse" on-error="handlePutPasswordError"></iron-ajax>
112-
<iron-ajax bubbles="" id="getSettingsAjax" url="/api/v1/users/current/settings" on-response="handleGetSettingsResponse"></iron-ajax>
113-
<iron-ajax bubbles="" id="putSettingsAjax" url="/api/v1/users/current/settings" method="PUT" on-response="handlePutSettingsResponse" on-error="handlePutSettingsError"></iron-ajax>
114-
<iron-ajax bubbles="" id="postImageAjax" url="/api/v1/uploads" method="POST" on-request="handlePostImageRequest" on-response="handlePostImageResponse" on-error="handlePostImageError"></iron-ajax>
67+
</style>
68+
<div class="container">
69+
<paper-card>
70+
<div class="card-content">
71+
<h3>Security Settings</h3>
72+
<paper-input label="Username" value="[[currentUser.username]]" always-float-label="" disabled=""></paper-input>
73+
<paper-input label="Access Level" value="[[currentUser.accessLevel]]" always-float-label="" disabled=""></paper-input>
74+
<paper-password-input label="Current Password" value="{{currentPassword}}" always-float-label=""></paper-password-input>
75+
<paper-password-input label="New Password" value="{{newPassword}}" always-float-label=""></paper-password-input>
76+
<paper-password-input label="Confirm Password" value="{{repeatPassword}}" always-float-label=""></paper-password-input>
77+
</div>
78+
<div class="card-actions">
79+
<paper-button on-click="onUpdatePasswordClicked">
80+
<iron-icon icon="icons:lock-outline"></iron-icon>
81+
<span>Update Password</span>
82+
<paper-button>
83+
</div>
84+
</paper-card>
85+
</div>
86+
<div class="container">
87+
<paper-card>
88+
<div class="card-content">
89+
<h3>Settings</h3>
90+
<paper-tags-input id="tags" label="Favorite Tags" tags="{{userSettings.favoriteTags}}"></paper-tags-input>
91+
<paper-input label="Home Title" always-float-label="" value="{{userSettings.homeTitle}}"></paper-input>
92+
<form id="homeImageForm" enctype="multipart/form-data">
93+
<paper-input-container always-float-label="">
94+
<label slot="label">Home Image</label>
95+
<iron-input slot="input">
96+
<input id="homeImageFile" name="file_content" type="file" accept=".jpg,.jpeg,.png">
97+
</iron-input>
98+
</paper-input-container>
99+
</form>
100+
<img alt="Home Image" src="[[userSettings.homeImageUrl]]" class="responsive" hidden\$="[[!userSettings.homeImageUrl]]">
101+
</div>
102+
<div class="card-actions">
103+
<paper-button on-click="onSaveButtonClicked">
104+
<iron-icon icon="icons:save"></iron-icon>
105+
<span>Save</span>
106+
<paper-button>
107+
</div>
108+
</paper-card>
109+
</div>
110+
<paper-dialog id="uploadingDialog" with-backdrop="">
111+
<h3><paper-spinner active=""></paper-spinner>Uploading</h3>
112+
</paper-dialog>
113+
114+
<a href="/create"><paper-fab icon="icons:add" class="green"></paper-fab></a>
115+
116+
<iron-ajax bubbles="" id="putPasswordAjax" url="/api/v1/users/current/password" method="PUT" on-response="handlePutPasswordResponse" on-error="handlePutPasswordError"></iron-ajax>
117+
<iron-ajax bubbles="" id="getSettingsAjax" url="/api/v1/users/current/settings" on-response="handleGetSettingsResponse"></iron-ajax>
118+
<iron-ajax bubbles="" id="putSettingsAjax" url="/api/v1/users/current/settings" method="PUT" on-response="handlePutSettingsResponse" on-error="handlePutSettingsError"></iron-ajax>
119+
<iron-ajax bubbles="" id="postImageAjax" url="/api/v1/uploads" method="POST" on-request="handlePostImageRequest" on-response="handlePostImageResponse" on-error="handlePostImageError"></iron-ajax>
115120
`;
116121
}
117122

@@ -167,12 +172,14 @@ export class SettingsView extends GompBaseElement {
167172
this.putPasswordAjax.generateRequest();
168173
}
169174
protected onSaveButtonClicked() {
170-
this.putSettingsAjax.body = JSON.stringify(this.userSettings) as any;
171-
this.putSettingsAjax.generateRequest();
172-
}
173-
protected onUploadButtonClicked() {
174-
this.postImageAjax.body = new FormData(this.homeImageForm);
175-
this.postImageAjax.generateRequest();
175+
// If there's no image to upload, go directly to saving
176+
if (!this.homeImageFile.value) {
177+
this.saveSettings();
178+
} else {
179+
// We start by uploading the image, after which the rest of the settings will be saved
180+
this.postImageAjax.body = new FormData(this.homeImageForm);
181+
this.postImageAjax.generateRequest();
182+
}
176183
}
177184

178185
protected isActiveChanged(isActive: boolean) {
@@ -212,13 +219,18 @@ export class SettingsView extends GompBaseElement {
212219

213220
const location = req.xhr.getResponseHeader('Location');
214221
this.userSettings.homeImageUrl = location;
215-
this.onSaveButtonClicked();
222+
this.saveSettings();
216223
}
217224
protected handlePostImageError() {
218225
this.uploadingDialog.close();
219226
this.showToast('Upload failed!');
220227
}
221228

229+
private saveSettings() {
230+
this.putSettingsAjax.body = JSON.stringify(this.userSettings) as any;
231+
this.putSettingsAjax.generateRequest();
232+
}
233+
222234
protected refresh() {
223235
this.getSettingsAjax.generateRequest();
224236
}

0 commit comments

Comments
 (0)