Skip to content

Commit

Permalink
Feature/user favorite tags (#266)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
chadweimer authored Jan 31, 2021
1 parent 7a889e1 commit 6fd83f2
Show file tree
Hide file tree
Showing 11 changed files with 135 additions and 73 deletions.
5 changes: 5 additions & 0 deletions db/migrations/postgres/0019_user_favorite_tags.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
BEGIN;

DROP TABLE app_user_favorite_tag;

COMMIT;
11 changes: 11 additions & 0 deletions db/migrations/postgres/0019_user_favorite_tags.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
BEGIN;

CREATE TABLE app_user_favorite_tag (
user_id INTEGER NOT NULL,
tag TEXT NOT NULL,
FOREIGN KEY(user_id) REFERENCES app_user(id) ON DELETE CASCADE
);
CREATE INDEX app_user_favorite_tag_idx ON app_user_favorite_tag(tag);
CREATE INDEX app_user_favorite_tag_user_id_idx ON app_user_favorite_tag(user_id);

COMMIT;
1 change: 1 addition & 0 deletions db/migrations/sqlite3/0005_user_favorite_tags.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE app_user_favorite_tag;
7 changes: 7 additions & 0 deletions db/migrations/sqlite3/0005_user_favorite_tags.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
CREATE TABLE app_user_favorite_tag (
user_id INTEGER NOT NULL,
tag TEXT NOT NULL,
FOREIGN KEY(user_id) REFERENCES app_user(id) ON DELETE CASCADE
);
CREATE INDEX app_user_favorite_tag_idx ON app_user_favorite_tag(tag);
CREATE INDEX app_user_favorite_tag_user_id_idx ON app_user_favorite_tag(user_id);
23 changes: 23 additions & 0 deletions db/user-sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package db
import (
"database/sql"
"errors"
"fmt"

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

var tags []string
if err := d.Db.Select(&tags, "SELECT tag FROM app_user_favorite_tag WHERE user_id = $1 ORDER BY tag ASC", id); err != nil {
return nil, err
}
userSettings.FavoriteTags = tags

return userSettings, nil
}

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

// Deleting and recreating seems inefficient. Maybe make this smarter.
_, err = tx.Exec(
"DELETE FROM app_user_favorite_tag WHERE user_id = $1",
settings.UserID)
if err != nil {
return fmt.Errorf("deleting favorite tags before updating on user: %v", err)
}
for _, tag := range settings.FavoriteTags {
_, err = tx.Exec(
"INSERT INTO app_user_favorite_tag (user_id, tag) VALUES ($1, $2)",
settings.UserID, tag)
if err != nil {
return fmt.Errorf("updating favorite tags on user: %v", err)
}
}

return nil
}

Expand Down
7 changes: 4 additions & 3 deletions models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ type User struct {

// UserSettings represents the settings for an individual user
type UserSettings struct {
UserID int64 `json:"userId" db:"user_id"`
HomeTitle *string `json:"homeTitle" db:"home_title"`
HomeImageURL *string `json:"homeImageUrl" db:"home_image_url"`
UserID int64 `json:"userId" db:"user_id"`
HomeTitle *string `json:"homeTitle" db:"home_title"`
HomeImageURL *string `json:"homeImageUrl" db:"home_image_url"`
FavoriteTags []string `json:"favoriteTags"`
}

// Scan implements the sql.Scanner interface
Expand Down
6 changes: 3 additions & 3 deletions static/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion static/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"@cwmr/paper-fab-speed-dial": "^3.0.0",
"@cwmr/paper-password-input": "^3.0.0",
"@cwmr/paper-search": "^3.0.1",
"@cwmr/paper-tags-input": "^3.1.0",
"@cwmr/paper-tags-input": "^3.1.2",
"@polymer/app-layout": "^3.1.0",
"@polymer/app-route": "^3.0.0",
"@polymer/app-storage": "^3.0.3",
Expand Down
15 changes: 8 additions & 7 deletions static/src/components/tag-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {customElement, property } from '@polymer/decorators';
import { IronAjaxElement } from '@polymer/iron-ajax/iron-ajax.js';
import { PaperTagsInput } from '@cwmr/paper-tags-input/paper-tags-input.js';
import { GompBaseElement } from '../common/gomp-base-element.js';
import { UserSettings } from '../models/models.js';
import '@polymer/iron-ajax/iron-ajax.js';
import '@polymer/iron-icon/iron-icon.js';
import '@polymer/iron-icons/iron-icons.js';
Expand Down Expand Up @@ -52,7 +53,7 @@ export class TagInput extends GompBaseElement {
<input type="hidden" slot="input">
</paper-input-container>
<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>
<iron-ajax bubbles="" id="getSettingsAjax" url="/api/v1/users/current/settings" on-request="handleGetSettingsRequest" on-response="handleGetSettingsResponse"></iron-ajax>
`;
}

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

protected suggestedTags: string[] = [];

private get getSuggestedTagsAjax(): IronAjaxElement {
return this.$.getSuggestedTagsAjax as IronAjaxElement;
private get getSettingsAjax(): IronAjaxElement {
return this.$.getSettingsAjax as IronAjaxElement;
}
private get tagsElement(): PaperTagsInput {
return this.$.tags as PaperTagsInput;
}

public refresh() {
this.getSuggestedTagsAjax.generateRequest();
this.getSettingsAjax.generateRequest();
}

protected onSuggestedTagClicked(e: {model: {item: string}}) {
Expand All @@ -81,10 +82,10 @@ export class TagInput extends GompBaseElement {
this.splice('suggestedTags', suggestedTagIndex, 1);
}
}
protected handleGetSuggestedTagsRequest() {
protected handleGetSettingsRequest() {
this.suggestedTags = [];
}
protected handleGetSuggestedTagsResponse(e: CustomEvent<{response: string[]}>) {
this.suggestedTags = e.detail.response;
protected handleGetSettingsResponse(e: CustomEvent<{response: UserSettings}>) {
this.suggestedTags = e.detail.response.favoriteTags;
}
}
7 changes: 4 additions & 3 deletions static/src/models/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,10 @@ export interface User {
}

export interface UserSettings {
userId: string;
homeTitle: string;
homeImageUrl: string;
userId: string;
homeTitle: string;
homeImageUrl: string;
favoriteTags: string[];
}

interface RecipeBase {
Expand Down
124 changes: 68 additions & 56 deletions static/src/settings-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import '@polymer/paper-fab/paper-fab.js';
import '@polymer/paper-input/paper-input.js';
import '@polymer/paper-spinner/paper-spinner.js';
import '@cwmr/paper-password-input/paper-password-input.js';
import '@cwmr/paper-tags-input/paper-tags-input.js';
import './shared-styles.js';

@customElement('settings-view')
Expand Down Expand Up @@ -63,55 +64,59 @@ export class SettingsView extends GompBaseElement {
}
@media screen and (max-width: 600px) {
}
</style>
<div class="container">
<paper-card>
<div class="card-content">
<h3>Security Settings</h3>
<paper-input label="Username" value="[[currentUser.username]]" always-float-label="" disabled=""></paper-input>
<paper-input label="Access Level" value="[[currentUser.accessLevel]]" always-float-label="" disabled=""></paper-input>
<paper-password-input label="Current Password" value="{{currentPassword}}" always-float-label=""></paper-password-input>
<paper-password-input label="New Password" value="{{newPassword}}" always-float-label=""></paper-password-input>
<paper-password-input label="Confirm Password" value="{{repeatPassword}}" always-float-label=""></paper-password-input>
</div>
<div class="card-actions">
<paper-button on-click="onUpdatePasswordClicked">
<iron-icon icon="icons:lock-outline"></iron-icon>
<span>Update Password</span>
<paper-button>
</div>
</paper-card>
</div>
<div class="container">
<paper-card>
<div class="card-content">
<h3>Home Settings</h3>
<paper-input label="Title" always-float-label="" value="{{userSettings.homeTitle}}">
<paper-icon-button slot="suffix" icon="icons:save" on-click="onSaveButtonClicked"></paper-icon-button>
</paper-input>
<form id="homeImageForm" enctype="multipart/form-data">
<paper-input-container always-float-label="">
<label slot="label">Image</label>
<iron-input slot="input">
<input id="homeImageFile" name="file_content" type="file" accept=".jpg,.jpeg,.png">
</iron-input>
<paper-icon-button slot="suffix" icon="icons:file-upload" on-click="onUploadButtonClicked"></paper-icon-button>
</paper-input-container>
</form>
<img alt="Home Image" src="[[userSettings.homeImageUrl]]" class="responsive" hidden\$="[[!userSettings.homeImageUrl]]">
</div>
</paper-card>
</div>
<paper-dialog id="uploadingDialog" with-backdrop="">
<h3><paper-spinner active=""></paper-spinner>Uploading</h3>
</paper-dialog>
<a href="/create"><paper-fab icon="icons:add" class="green"></paper-fab></a>
<iron-ajax bubbles="" id="putPasswordAjax" url="/api/v1/users/current/password" method="PUT" on-response="handlePutPasswordResponse" on-error="handlePutPasswordError"></iron-ajax>
<iron-ajax bubbles="" id="getSettingsAjax" url="/api/v1/users/current/settings" on-response="handleGetSettingsResponse"></iron-ajax>
<iron-ajax bubbles="" id="putSettingsAjax" url="/api/v1/users/current/settings" method="PUT" on-response="handlePutSettingsResponse" on-error="handlePutSettingsError"></iron-ajax>
<iron-ajax bubbles="" id="postImageAjax" url="/api/v1/uploads" method="POST" on-request="handlePostImageRequest" on-response="handlePostImageResponse" on-error="handlePostImageError"></iron-ajax>
</style>
<div class="container">
<paper-card>
<div class="card-content">
<h3>Security Settings</h3>
<paper-input label="Username" value="[[currentUser.username]]" always-float-label="" disabled=""></paper-input>
<paper-input label="Access Level" value="[[currentUser.accessLevel]]" always-float-label="" disabled=""></paper-input>
<paper-password-input label="Current Password" value="{{currentPassword}}" always-float-label=""></paper-password-input>
<paper-password-input label="New Password" value="{{newPassword}}" always-float-label=""></paper-password-input>
<paper-password-input label="Confirm Password" value="{{repeatPassword}}" always-float-label=""></paper-password-input>
</div>
<div class="card-actions">
<paper-button on-click="onUpdatePasswordClicked">
<iron-icon icon="icons:lock-outline"></iron-icon>
<span>Update Password</span>
<paper-button>
</div>
</paper-card>
</div>
<div class="container">
<paper-card>
<div class="card-content">
<h3>Settings</h3>
<paper-tags-input id="tags" label="Favorite Tags" tags="{{userSettings.favoriteTags}}"></paper-tags-input>
<paper-input label="Home Title" always-float-label="" value="{{userSettings.homeTitle}}"></paper-input>
<form id="homeImageForm" enctype="multipart/form-data">
<paper-input-container always-float-label="">
<label slot="label">Home Image</label>
<iron-input slot="input">
<input id="homeImageFile" name="file_content" type="file" accept=".jpg,.jpeg,.png">
</iron-input>
</paper-input-container>
</form>
<img alt="Home Image" src="[[userSettings.homeImageUrl]]" class="responsive" hidden\$="[[!userSettings.homeImageUrl]]">
</div>
<div class="card-actions">
<paper-button on-click="onSaveButtonClicked">
<iron-icon icon="icons:save"></iron-icon>
<span>Save</span>
<paper-button>
</div>
</paper-card>
</div>
<paper-dialog id="uploadingDialog" with-backdrop="">
<h3><paper-spinner active=""></paper-spinner>Uploading</h3>
</paper-dialog>
<a href="/create"><paper-fab icon="icons:add" class="green"></paper-fab></a>
<iron-ajax bubbles="" id="putPasswordAjax" url="/api/v1/users/current/password" method="PUT" on-response="handlePutPasswordResponse" on-error="handlePutPasswordError"></iron-ajax>
<iron-ajax bubbles="" id="getSettingsAjax" url="/api/v1/users/current/settings" on-response="handleGetSettingsResponse"></iron-ajax>
<iron-ajax bubbles="" id="putSettingsAjax" url="/api/v1/users/current/settings" method="PUT" on-response="handlePutSettingsResponse" on-error="handlePutSettingsError"></iron-ajax>
<iron-ajax bubbles="" id="postImageAjax" url="/api/v1/uploads" method="POST" on-request="handlePostImageRequest" on-response="handlePostImageResponse" on-error="handlePostImageError"></iron-ajax>
`;
}

Expand Down Expand Up @@ -167,12 +172,14 @@ export class SettingsView extends GompBaseElement {
this.putPasswordAjax.generateRequest();
}
protected onSaveButtonClicked() {
this.putSettingsAjax.body = JSON.stringify(this.userSettings) as any;
this.putSettingsAjax.generateRequest();
}
protected onUploadButtonClicked() {
this.postImageAjax.body = new FormData(this.homeImageForm);
this.postImageAjax.generateRequest();
// If there's no image to upload, go directly to saving
if (!this.homeImageFile.value) {
this.saveSettings();
} else {
// We start by uploading the image, after which the rest of the settings will be saved
this.postImageAjax.body = new FormData(this.homeImageForm);
this.postImageAjax.generateRequest();
}
}

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

const location = req.xhr.getResponseHeader('Location');
this.userSettings.homeImageUrl = location;
this.onSaveButtonClicked();
this.saveSettings();
}
protected handlePostImageError() {
this.uploadingDialog.close();
this.showToast('Upload failed!');
}

private saveSettings() {
this.putSettingsAjax.body = JSON.stringify(this.userSettings) as any;
this.putSettingsAjax.generateRequest();
}

protected refresh() {
this.getSettingsAjax.generateRequest();
}
Expand Down

0 comments on commit 6fd83f2

Please sign in to comment.