diff --git a/client/html/tag_categories.tpl b/client/html/tag_categories.tpl index cc8fe6a..e835291 100644 --- a/client/html/tag_categories.tpl +++ b/client/html/tag_categories.tpl @@ -10,67 +10,7 @@ - <% for (let category of ctx.tagCategories) { %> - <% if (category.default) { %> - - <% } else { %> - - <% } %> - - <% if (ctx.canEditName) { %> - <%= ctx.makeTextInput({value: category.name, required: true}) %> - <% } else { %> - <%= category.name %> - <% } %> - - - <% if (ctx.canEditColor) { %> - <%= ctx.makeColorInput({value: category.color}) %> - <% } else { %> - <%= category.color %> - <% } %> - - - - <%= category.usages %> - - - <% if (ctx.canDelete) { %> - - <% if (category.usages) { %> - Remove - <% } else { %> - Remove - <% } %> - - <% } %> - <% if (ctx.canSetDefault) { %> - - Make default - - <% } %> - - <% } %> - - - - <%= ctx.makeTextInput({required: true}) %> - - - <%= ctx.makeColorInput({value: '#000000'}) %> - - - 0 - - - Remove - - - Make default - - - <% if (ctx.canCreate) { %> diff --git a/client/html/tag_category_row.tpl b/client/html/tag_category_row.tpl new file mode 100644 index 0000000..2867640 --- /dev/null +++ b/client/html/tag_category_row.tpl @@ -0,0 +1,41 @@ + class='default' <% } %> +> + + <% if (ctx.canEditName) { %> + <%= ctx.makeTextInput({value: ctx.tagCategory.name, required: true}) %> + <% } else { %> + <%= ctx.tagCategory.name %> + <% } %> + + + <% if (ctx.canEditColor) { %> + <%= ctx.makeColorInput({value: ctx.tagCategory.color}) %> + <% } else { %> + <%= ctx.tagCategory.color %> + <% } %> + + + <% if (ctx.tagCategory.name) { %> + + <%= ctx.tagCategory.tagCount %> + + <% } else { %> + <%= ctx.tagCategory.tagCount %> + <% } %> + + <% if (ctx.canDelete) { %> + + <% if (ctx.tagCategory.tagCount) { %> + Remove + <% } else { %> + Remove + <% } %> + + <% } %> + <% if (ctx.canSetDefault) { %> + + Make default + + <% } %> + diff --git a/client/html/tag_delete.tpl b/client/html/tag_delete.tpl index 0d03f2c..e94de4e 100644 --- a/client/html/tag_delete.tpl +++ b/client/html/tag_delete.tpl @@ -1,6 +1,6 @@
- <% if (ctx.tag.usages) { %> + <% if (ctx.tag.postCount) { %>

For extra paranoia safety, only tags that are unused can be deleted.

Check which posts are tagged with <%= ctx.tag.names[0] %>.

<% } else { %> diff --git a/client/html/tags_page.tpl b/client/html/tags_page.tpl index cca8daa..b6326d6 100644 --- a/client/html/tags_page.tpl +++ b/client/html/tags_page.tpl @@ -71,7 +71,7 @@ <% } %> - <%= tag.usages %> + <%= tag.postCount %> <%= ctx.makeRelativeTime(tag.lastEditTime) %> diff --git a/client/js/api.js b/client/js/api.js index de76119..d470528 100644 --- a/client/js/api.js +++ b/client/js/api.js @@ -22,6 +22,15 @@ class Api extends events.EventTarget { 'administrator', 'nobody', ]; + this.rankNames = new Map([ + ['anonymous', 'Anonymous'], + ['restricted', 'Restricted user'], + ['regular', 'Regular user'], + ['power', 'Power user'], + ['moderator', 'Moderator'], + ['administrator', 'Administrator'], + ['nobody', 'Nobody'], + ]); } get(url, options) { diff --git a/client/js/controllers/comments_controller.js b/client/js/controllers/comments_controller.js index a7fedad..8d672da 100644 --- a/client/js/controllers/comments_controller.js +++ b/client/js/controllers/comments_controller.js @@ -7,29 +7,18 @@ const topNavigation = require('../models/top_navigation.js'); const PageController = require('../controllers/page_controller.js'); const CommentsPageView = require('../views/comments_page_view.js'); +const fields = ['id', 'comments', 'commentCount', 'thumbnailUrl']; + class CommentsController { constructor(ctx) { topNavigation.activate('comments'); - const proxy = PageController.createHistoryCacheProxy( - ctx, page => { - const url = - '/posts/?query=sort:comment-date+comment-count-min:1' + - `&page=${page}&pageSize=10&fields=` + - 'id,comments,commentCount,thumbnailUrl'; - return api.get(url); - }); - this._pageController = new PageController({ searchQuery: ctx.searchQuery, clientUrl: '/comments/' + misc.formatSearchQuery({page: '{page}'}), requestPage: page => { - return proxy(page).then(response => { - return Promise.resolve(Object.assign( - {}, - response, - {results: PostList.fromResponse(response.results)})); - }); + return PostList.search( + 'sort:comment-date+comment-count-min:1', page, 10, fields); }, pageRenderer: pageCtx => { Object.assign(pageCtx, { diff --git a/client/js/controllers/home_controller.js b/client/js/controllers/home_controller.js index 37856a3..02e022e 100644 --- a/client/js/controllers/home_controller.js +++ b/client/js/controllers/home_controller.js @@ -2,6 +2,7 @@ const api = require('../api.js'); const config = require('../config.js'); +const Info = require('../models/info.js'); const topNavigation = require('../models/top_navigation.js'); const HomeView = require('../views/home_view.js'); @@ -16,20 +17,20 @@ class HomeController { canListPosts: api.hasPrivilege('posts:list'), }); - api.get('/info') - .then(response => { + Info.get() + .then(info => { this._homeView.setStats({ - diskUsage: response.diskUsage, - postCount: response.postCount, + diskUsage: info.diskUsage, + postCount: info.postCount, }); this._homeView.setFeaturedPost({ - featuredPost: response.featuredPost, - featuringUser: response.featuringUser, - featuringTime: response.featuringTime, + featuredPost: info.featuredPost, + featuringUser: info.featuringUser, + featuringTime: info.featuringTime, }); }, - response => { - this._homeView.showError(response.description); + errorMessage => { + this._homeView.showError(errorMessage); }); } diff --git a/client/js/controllers/page_controller.js b/client/js/controllers/page_controller.js index 5228674..5fd9517 100644 --- a/client/js/controllers/page_controller.js +++ b/client/js/controllers/page_controller.js @@ -28,22 +28,6 @@ class PageController { showError(message) { this._view.showError(message); } - - static createHistoryCacheProxy(routerCtx, requestPage) { - return page => { - if (routerCtx.state.response) { - return new Promise((resolve, reject) => { - resolve(routerCtx.state.response); - }); - } - const promise = requestPage(page); - promise.then(response => { - routerCtx.state.response = response; - routerCtx.save(); - }); - return promise; - }; - } } module.exports = PageController; diff --git a/client/js/controllers/post_controller.js b/client/js/controllers/post_controller.js index cc87f28..ebaa80f 100644 --- a/client/js/controllers/post_controller.js +++ b/client/js/controllers/post_controller.js @@ -5,6 +5,7 @@ const misc = require('../util/misc.js'); const settings = require('../models/settings.js'); const Comment = require('../models/comment.js'); const Post = require('../models/post.js'); +const PostList = require('../models/post_list.js'); const topNavigation = require('../models/top_navigation.js'); const PostView = require('../views/post_view.js'); const EmptyView = require('../views/empty_view.js'); @@ -15,8 +16,8 @@ class PostController { Promise.all([ Post.get(id), - api.get(`/post/${id}/around?fields=id&query=` + - this._decorateSearchQuery( + PostList.getAround( + id, this._decorateSearchQuery( searchQuery ? searchQuery.text : '')), ]).then(responses => { const [post, aroundResponse] = responses; @@ -53,9 +54,9 @@ class PostController { this._view.commentListControl.addEventListener( 'delete', e => this._evtDeleteComment(e)); } - }, response => { + }, errorMessage => { this._view = new EmptyView(); - this._view.showError(response.description); + this._view.showError(errorMessage); }); } diff --git a/client/js/controllers/post_list_controller.js b/client/js/controllers/post_list_controller.js index 8d33ec8..5e2714f 100644 --- a/client/js/controllers/post_list_controller.js +++ b/client/js/controllers/post_list_controller.js @@ -3,11 +3,16 @@ const api = require('../api.js'); const settings = require('../models/settings.js'); const misc = require('../util/misc.js'); +const PostList = require('../models/post_list.js'); const topNavigation = require('../models/top_navigation.js'); const PageController = require('../controllers/page_controller.js'); const PostsHeaderView = require('../views/posts_header_view.js'); const PostsPageView = require('../views/posts_page_view.js'); +const fields = [ + 'id', 'thumbnailUrl', 'type', + 'score', 'favoriteCount', 'commentCount', 'tags']; + class PostListController { constructor(ctx) { topNavigation.activate('posts'); @@ -16,16 +21,11 @@ class PostListController { searchQuery: ctx.searchQuery, clientUrl: '/posts/' + misc.formatSearchQuery({ text: ctx.searchQuery.text, page: '{page}'}), - requestPage: PageController.createHistoryCacheProxy( - ctx, - page => { - const text - = this._decorateSearchQuery(ctx.searchQuery.text); - return api.get( - `/posts/?query=${text}&page=${page}&pageSize=40` + - '&fields=id,type,tags,score,favoriteCount,' + - 'commentCount,thumbnailUrl'); - }), + requestPage: page => { + return PostList.search( + this._decorateSearchQuery(ctx.searchQuery.text), + page, 40, fields); + }, headerRenderer: headerCtx => { return new PostsHeaderView(headerCtx); }, diff --git a/client/js/controllers/tag_categories_controller.js b/client/js/controllers/tag_categories_controller.js index 00a6c93..e381b3f 100644 --- a/client/js/controllers/tag_categories_controller.js +++ b/client/js/controllers/tag_categories_controller.js @@ -3,6 +3,7 @@ const api = require('../api.js'); const tags = require('../tags.js'); const misc = require('../util/misc.js'); +const TagCategoryList = require('../models/tag_category_list.js'); const topNavigation = require('../models/top_navigation.js'); const TagCategoriesView = require('../views/tag_categories_view.js'); const EmptyView = require('../views/empty_view.js'); @@ -10,66 +11,35 @@ const EmptyView = require('../views/empty_view.js'); class TagCategoriesController { constructor() { topNavigation.activate('tags'); - api.get('/tag-categories/').then(response => { + TagCategoryList.get().then(response => { + this._tagCategories = response.results; this._view = new TagCategoriesView({ - tagCategories: response.results, + tagCategories: this._tagCategories, canEditName: api.hasPrivilege('tagCategories:edit:name'), canEditColor: api.hasPrivilege('tagCategories:edit:color'), canDelete: api.hasPrivilege('tagCategories:delete'), canCreate: api.hasPrivilege('tagCategories:create'), canSetDefault: api.hasPrivilege('tagCategories:setDefault'), - saveChanges: (...args) => { - return this._saveTagCategories(...args); - }, - getCategories: () => { - return api.get('/tag-categories/').then(response => { - return Promise.resolve(response.results); - }, response => { - return Promise.reject(response); - }); - } }); + this._view.addEventListener('submit', e => this._evtSubmit(e)); }, response => { this._view = new EmptyView(); this._view.showError(response.description); }); } - _saveTagCategories( - addedCategories, - changedCategories, - removedCategories, - defaultCategory) { - let promises = []; - for (let category of addedCategories) { - promises.push(api.post('/tag-categories/', category)); - } - for (let category of changedCategories) { - promises.push( - api.put('/tag-category/' + category.originalName, category)); - } - for (let name of removedCategories) { - promises.push(api.delete('/tag-category/' + name)); - } - Promise.all(promises) - .then( - () => { - if (!defaultCategory) { - return Promise.resolve(); - } - return api.put( - '/tag-category/' + defaultCategory + '/default'); - }, response => { - return Promise.reject(response); - }) - .then( - () => { - tags.refreshExport(); - this._view.showSuccess('Changes saved.'); - }, - response => { - this._view.showError(response.description); - }); + _evtSubmit(e) { + this._view.clearMessages(); + this._view.disableForm(); + this._tagCategories.save() + .then(() => { + tags.refreshExport(); + this._view.enableForm(); + this._view.showSuccess('Changes saved.'); + }, errorMessage => { + this._view.enableForm(); + this._view.showError(errorMessage); + }); } } diff --git a/client/js/controllers/tag_controller.js b/client/js/controllers/tag_controller.js index 99316f4..9c2d744 100644 --- a/client/js/controllers/tag_controller.js +++ b/client/js/controllers/tag_controller.js @@ -3,27 +3,19 @@ const router = require('../router.js'); const api = require('../api.js'); const tags = require('../tags.js'); +const Tag = require('../models/tag.js'); const topNavigation = require('../models/top_navigation.js'); const TagView = require('../views/tag_view.js'); const EmptyView = require('../views/empty_view.js'); class TagController { constructor(ctx, section) { - new Promise((resolve, reject) => { - if (ctx.state.tag) { - resolve(ctx.state.tag); - return; - } - api.get('/tag/' + ctx.params.name).then(response => { - ctx.state.tag = response; - ctx.save(); - resolve(ctx.state.tag); - }, response => { - reject(response.description); - }); - }).then(tag => { + Tag.get(ctx.params.name).then(tag => { topNavigation.activate('tags'); + this._name = ctx.params.name; + tag.addEventListener('change', e => this._evtSaved(e)); + const categories = {}; for (let category of tags.getAllCategories()) { categories[category.name] = category.name; @@ -50,19 +42,20 @@ class TagController { }); } + _evtSaved(e) { + if (this._name !== e.detail.tag.names[0]) { + router.replace('/tag/' + e.detail.tag.names[0], null, false); + } + } + _evtChange(e) { this._view.clearMessages(); this._view.disableForm(); - return api.put('/tag/' + e.detail.tag.names[0], { - names: e.detail.names, - category: e.detail.category, - implications: e.detail.implications, - suggestions: e.detail.suggestions, - }).then(response => { - // TODO: update header links and text - if (e.detail.names && e.detail.names[0] !== e.detail.tag.names[0]) { - router.replace('/tag/' + e.detail.names[0], null, false); - } + e.detail.tag.names = e.detail.names; + e.detail.tag.category = e.detail.category; + e.detail.tag.implications = e.detail.implications; + e.detail.tag.suggestions = e.detail.suggestions; + e.detail.tag.save().then(() => { this._view.showSuccess('Tag saved.'); this._view.enableForm(); }, response => { @@ -74,17 +67,11 @@ class TagController { _evtMerge(e) { this._view.clearMessages(); this._view.disableForm(); - return api.post( - '/tag-merge/', - {remove: e.detail.tag.names[0], mergeTo: e.detail.targetTagName} - ).then(response => { - // TODO: update header links and text - router.replace( - '/tag/' + e.detail.targetTagName + '/merge', null, false); + e.detail.tag.merge(e.detail.targetTagName).then(() => { this._view.showSuccess('Tag merged.'); this._view.enableForm(); - }, response => { - this._view.showError(response.description); + }, errorMessage => { + this._view.showError(errorMessage); this._view.enableForm(); }); } @@ -92,13 +79,14 @@ class TagController { _evtDelete(e) { this._view.clearMessages(); this._view.disableForm(); - return api.delete('/tag/' + e.detail.tag.names[0]).then(response => { - const ctx = router.show('/tags/'); - ctx.controller.showSuccess('Tag deleted.'); - }, response => { - this._view.showError(response.description); - this._view.enableForm(); - }); + e.detail.tag.delete() + .then(() => { + const ctx = router.show('/tags/'); + ctx.controller.showSuccess('Tag deleted.'); + }, errorMessage => { + this._view.showError(errorMessage); + this._view.enableForm(); + }); } } diff --git a/client/js/controllers/tag_list_controller.js b/client/js/controllers/tag_list_controller.js index f948de4..0c1e27a 100644 --- a/client/js/controllers/tag_list_controller.js +++ b/client/js/controllers/tag_list_controller.js @@ -2,11 +2,15 @@ const api = require('../api.js'); const misc = require('../util/misc.js'); +const TagList = require('../models/tag_list.js'); const topNavigation = require('../models/top_navigation.js'); const PageController = require('../controllers/page_controller.js'); const TagsHeaderView = require('../views/tags_header_view.js'); const TagsPageView = require('../views/tags_page_view.js'); +const fields = [ + 'names', 'suggestions', 'implications', 'lastEditTime', 'usages']; + class TagListController { constructor(ctx) { topNavigation.activate('tags'); @@ -15,15 +19,9 @@ class TagListController { searchQuery: ctx.searchQuery, clientUrl: '/tags/' + misc.formatSearchQuery({ text: ctx.searchQuery.text, page: '{page}'}), - requestPage: PageController.createHistoryCacheProxy( - ctx, - page => { - const text = ctx.searchQuery.text; - return api.get( - `/tags/?query=${text}&page=${page}&pageSize=50` + - '&fields=names,suggestions,implications,' + - 'lastEditTime,usages'); - }), + requestPage: page => { + return TagList.search(ctx.searchQuery.text, page, 50, fields); + }, headerRenderer: headerCtx => { Object.assign(headerCtx, { canEditTagCategories: diff --git a/client/js/controllers/user_controller.js b/client/js/controllers/user_controller.js index 161c568..f9cacb7 100644 --- a/client/js/controllers/user_controller.js +++ b/client/js/controllers/user_controller.js @@ -4,39 +4,20 @@ const router = require('../router.js'); const api = require('../api.js'); const config = require('../config.js'); const views = require('../util/views.js'); +const User = require('../models/user.js'); const topNavigation = require('../models/top_navigation.js'); const UserView = require('../views/user_view.js'); const EmptyView = require('../views/empty_view.js'); -const rankNames = new Map([ - ['anonymous', 'Anonymous'], - ['restricted', 'Restricted user'], - ['regular', 'Regular user'], - ['power', 'Power user'], - ['moderator', 'Moderator'], - ['administrator', 'Administrator'], - ['nobody', 'Nobody'], -]); - class UserController { constructor(ctx, section) { - new Promise((resolve, reject) => { - if (ctx.state.user) { - resolve(ctx.state.user); - return; - } - api.get('/user/' + ctx.params.name).then(response => { - response.rankName = rankNames.get(response.rank); - ctx.state.user = response; - ctx.save(); - resolve(ctx.state.user); - }, response => { - reject(response.description); - }); - }).then(user => { + User.get(ctx.params.name).then(user => { const isLoggedIn = api.isLoggedIn(user); const infix = isLoggedIn ? 'self' : 'any'; + this._name = ctx.params.name; + user.addEventListener('change', e => this._evtSaved(e)); + const myRankIndex = api.user ? api.allRanks.indexOf(api.user.rank) : 0; @@ -48,7 +29,7 @@ class UserController { if (rankIdx > myRankIndex) { continue; } - ranks[rankIdentifier] = rankNames.get(rankIdentifier); + ranks[rankIdentifier] = api.rankNames.get(rankIdentifier); } if (isLoggedIn) { @@ -77,64 +58,64 @@ class UserController { }); } + _evtSaved(e) { + if (this._name !== e.detail.user.name) { + router.replace( + '/user/' + e.detail.user.name + '/edit', null, false); + } + } + _evtChange(e) { this._view.clearMessages(); this._view.disableForm(); const isLoggedIn = api.isLoggedIn(e.detail.user); const infix = isLoggedIn ? 'self' : 'any'; - const files = []; - const data = {}; - if (e.detail.name) { - data.name = e.detail.name; + if (e.detail.name !== undefined) { + e.detail.user.name = e.detail.name; } - if (e.detail.password) { - data.password = e.detail.password; + if (e.detail.email !== undefined) { + e.detail.user.email = e.detail.email; } - if (api.hasPrivilege('users:edit:' + infix + ':email')) { - data.email = e.detail.email; - } - if (e.detail.rank) { - data.rank = e.detail.rank; - } - if (e.detail.avatarStyle && - (e.detail.avatarStyle != e.detail.user.avatarStyle || - e.detail.avatarContent)) { - data.avatarStyle = e.detail.avatarStyle; - } - if (e.detail.avatarContent) { - files.avatar = e.detail.avatarContent; + if (e.detail.rank !== undefined) { + e.detail.user.rank = e.detail.rank; } - api.put('/user/' + e.detail.user.name, data, files) - .then(response => { - return isLoggedIn ? - api.login( - data.name || api.userName, - data.password || api.userPassword, - false) : - Promise.resolve(); - }, response => { - return Promise.reject(response.description); - }).then(() => { - if (data.name && data.name !== e.detail.user.name) { - // TODO: update header links and text - router.replace('/user/' + data.name + '/edit', null, false); - } - this._view.showSuccess('Settings updated.'); - this._view.enableForm(); - }, errorMessage => { - this._view.showError(errorMessage); - this._view.enableForm(); - }); + if (e.detail.password !== undefined) { + e.detail.user.password = e.detail.password; + } + + if (e.detail.avatarStyle !== undefined) { + e.detail.user.avatarStyle = e.detail.avatarStyle; + if (e.detail.avatarContent) { + e.detail.user.avatarContent = e.detail.avatarContent; + } + } + + e.detail.user.save().then(() => { + return isLoggedIn ? + api.login( + e.detail.name || api.userName, + e.detail.password || api.userPassword, + false) : + Promise.resolve(); + }, errorMessage => { + return Promise.reject(errorMessage); + }).then(() => { + this._view.showSuccess('Settings updated.'); + this._view.enableForm(); + }, errorMessage => { + this._view.showError(errorMessage); + this._view.enableForm(); + }); } _evtDelete(e) { this._view.clearMessages(); this._view.disableForm(); const isLoggedIn = api.isLoggedIn(e.detail.user); - api.delete('/user/' + e.detail.user.name) - .then(response => { + e.detail.user.delete() + .then(() => { if (isLoggedIn) { api.forget(); api.logout(); @@ -146,7 +127,7 @@ class UserController { const ctx = router.show('/'); ctx.controller.showSuccess('Account deleted.'); } - }, response => { + }, errorMessage => { this._view.showError(response.description); this._view.enableForm(); }); diff --git a/client/js/controllers/user_list_controller.js b/client/js/controllers/user_list_controller.js index 892568f..660dcf4 100644 --- a/client/js/controllers/user_list_controller.js +++ b/client/js/controllers/user_list_controller.js @@ -2,6 +2,7 @@ const api = require('../api.js'); const misc = require('../util/misc.js'); +const UserList = require('../models/user_list.js'); const topNavigation = require('../models/top_navigation.js'); const PageController = require('../controllers/page_controller.js'); const UsersHeaderView = require('../views/users_header_view.js'); @@ -15,13 +16,9 @@ class UserListController { searchQuery: ctx.searchQuery, clientUrl: '/users/' + misc.formatSearchQuery({ text: ctx.searchQuery.text, page: '{page}'}), - requestPage: PageController.createHistoryCacheProxy( - ctx, - page => { - const text = ctx.searchQuery.text; - return api.get( - `/users/?query=${text}&page=${page}&pageSize=30`); - }), + requestPage: page => { + return UserList.search(ctx.searchQuery.text, page); + }, headerRenderer: headerCtx => { return new UsersHeaderView(headerCtx); }, diff --git a/client/js/controllers/user_registration_controller.js b/client/js/controllers/user_registration_controller.js index a23aaa0..623765a 100644 --- a/client/js/controllers/user_registration_controller.js +++ b/client/js/controllers/user_registration_controller.js @@ -2,6 +2,7 @@ const router = require('../router.js'); const api = require('../api.js'); +const User = require('../models/user.js'); const topNavigation = require('../models/top_navigation.js'); const RegistrationView = require('../views/registration_view.js'); @@ -15,11 +16,11 @@ class UserRegistrationController { _evtRegister(e) { this._view.clearMessages(); this._view.disableForm(); - api.post('/users/', { - name: e.detail.name, - password: e.detail.password, - email: e.detail.email - }).then(() => { + const user = new User(); + user.name = e.detail.name; + user.email = e.detail.email; + user.password = e.detail.password; + user.save().then(() => { api.forget(); return api.login(e.detail.name, e.detail.password, false); }, response => { diff --git a/client/js/controls/tag_auto_complete_control.js b/client/js/controls/tag_auto_complete_control.js index a9fb7f0..8626b5a 100644 --- a/client/js/controls/tag_auto_complete_control.js +++ b/client/js/controls/tag_auto_complete_control.js @@ -25,7 +25,7 @@ class TagAutoCompleteControl extends AutoCompleteControl { return Array.from(allTags.entries()) .filter(kv => match(transform(kv[0]), text)) .sort((kv1, kv2) => { - return kv2[1].usages - kv1[1].usages; + return kv2[1].postCount - kv1[1].postCount; }) .map(kv => { const category = kv[1].category; diff --git a/client/js/models/abstract_list.js b/client/js/models/abstract_list.js new file mode 100644 index 0000000..ba2a3f4 --- /dev/null +++ b/client/js/models/abstract_list.js @@ -0,0 +1,59 @@ +'use strict'; + +const events = require('../events.js'); + +class AbstractList extends events.EventTarget { + constructor() { + super(); + this._list = []; + } + + static fromResponse(response) { + const ret = new this(); + for (let item of response) { + const addedItem = this._itemClass.fromResponse(item); + addedItem.addEventListener('delete', e => { + ret.remove(addedItem); + }); + ret._list.push(addedItem); + } + return ret; + } + + add(item) { + item.addEventListener('delete', e => { + this.remove(item); + }); + this._list.push(item); + const detail = {}; + detail[this.constructor._itemName] = item; + this.dispatchEvent(new CustomEvent('add', { + detail: detail, + })); + } + + remove(itemToRemove) { + for (let [index, item] of this._list.entries()) { + if (item !== itemToRemove) { + continue; + } + this._list.splice(index, 1); + const detail = {}; + detail[this.constructor._itemName] = itemToRemove; + this.dispatchEvent(new CustomEvent('remove', { + detail: detail, + })); + return; + } + } + + get length() { + return this._list.length; + } + + [Symbol.iterator]() { + return this._list[Symbol.iterator](); + } +} + +module.exports = AbstractList; diff --git a/client/js/models/comment.js b/client/js/models/comment.js index 6333265..d65e13b 100644 --- a/client/js/models/comment.js +++ b/client/js/models/comment.js @@ -6,16 +6,14 @@ const events = require('../events.js'); class Comment extends events.EventTarget { constructor() { super(); - this.commentList = null; - - this._id = null; - this._postId = null; - this._text = null; - this._user = null; + this._id = null; + this._postId = null; + this._text = null; + this._user = null; this._creationTime = null; this._lastEditTime = null; - this._score = null; - this._ownScore = null; + this._score = null; + this._ownScore = null; } static create(postId) { @@ -30,16 +28,16 @@ class Comment extends events.EventTarget { return comment; } - get id() { return this._id; } - get postId() { return this._postId; } - get text() { return this._text; } - get user() { return this._user; } + get id() { return this._id; } + get postId() { return this._postId; } + get text() { return this._text; } + get user() { return this._user; } get creationTime() { return this._creationTime; } get lastEditTime() { return this._lastEditTime; } - get score() { return this._score; } - get ownScore() { return this._ownScore; } + get score() { return this._score; } + get ownScore() { return this._ownScore; } - set text(value) { this._text = value; } + set text(value) { this._text = value; } save() { let promise = null; @@ -61,7 +59,7 @@ class Comment extends events.EventTarget { return promise.then(response => { this._updateFromResponse(response); this.dispatchEvent(new CustomEvent('change', { - details: { + detail: { comment: this, }, })); @@ -74,11 +72,8 @@ class Comment extends events.EventTarget { delete() { return api.delete('/comment/' + this._id) .then(response => { - if (this.commentList) { - this.commentList.remove(this); - } this.dispatchEvent(new CustomEvent('delete', { - details: { + detail: { comment: this, }, })); @@ -93,7 +88,7 @@ class Comment extends events.EventTarget { .then(response => { this._updateFromResponse(response); this.dispatchEvent(new CustomEvent('changeScore', { - details: { + detail: { comment: this, }, })); @@ -104,14 +99,14 @@ class Comment extends events.EventTarget { } _updateFromResponse(response) { - this._id = response.id; - this._postId = response.postId; - this._text = response.text; - this._user = response.user; + this._id = response.id; + this._postId = response.postId; + this._text = response.text; + this._user = response.user; this._creationTime = response.creationTime; this._lastEditTime = response.lastEditTime; - this._score = parseInt(response.score); - this._ownScore = parseInt(response.ownScore); + this._score = parseInt(response.score); + this._ownScore = parseInt(response.ownScore); } } diff --git a/client/js/models/comment_list.js b/client/js/models/comment_list.js index 8f4a044..a8e1150 100644 --- a/client/js/models/comment_list.js +++ b/client/js/models/comment_list.js @@ -1,59 +1,12 @@ 'use strict'; -const events = require('../events.js'); +const AbstractList = require('./abstract_list.js'); const Comment = require('./comment.js'); -class CommentList extends events.EventTarget { - constructor(comments) { - super(); - this._list = []; - } - - static fromResponse(commentsResponse) { - const commentList = new CommentList(); - for (let commentResponse of commentsResponse) { - const comment = Comment.fromResponse(commentResponse); - comment.commentList = commentList; - commentList._list.push(comment); - } - return commentList; - } - - get comments() { - return [...this._list]; - } - - add(comment) { - comment.commentList = this; - this._list.push(comment); - this.dispatchEvent(new CustomEvent('add', { - detail: { - comment: comment, - }, - })); - } - - remove(commentToRemove) { - for (let [index, comment] of this._list.entries()) { - if (comment.id === commentToRemove.id) { - this._list.splice(index, 1); - break; - } - } - this.dispatchEvent(new CustomEvent('remove', { - detail: { - comment: commentToRemove, - }, - })); - } - - get length() { - return this._list.length; - } - - [Symbol.iterator]() { - return this._list[Symbol.iterator](); - } +class CommentList extends AbstractList { } +CommentList._itemClass = Comment; +CommentList._itemName = 'comment'; + module.exports = CommentList; diff --git a/client/js/models/info.js b/client/js/models/info.js new file mode 100644 index 0000000..9533e23 --- /dev/null +++ b/client/js/models/info.js @@ -0,0 +1,16 @@ +'use strict'; + +const api = require('../api.js'); + +class Info { + static get() { + return api.get('/info') + .then(response => { + return Promise.resolve(response); + }, response => { + return Promise.reject(response.errorMessage); + }); + } +} + +module.exports = Info; diff --git a/client/js/models/post.js b/client/js/models/post.js index 80963ec..0a228e1 100644 --- a/client/js/models/post.js +++ b/client/js/models/post.js @@ -7,67 +7,66 @@ const CommentList = require('./comment_list.js'); class Post extends events.EventTarget { constructor() { super(); - this._id = null; - this._type = null; - this._mimeType = null; - this._creationTime = null; - this._user = null; - this._safety = null; - this._contentUrl = null; - this._thumbnailUrl = null; - this._canvasWidth = null; - this._canvasHeight = null; - this._fileSize = null; + this._id = null; + this._type = null; + this._mimeType = null; + this._creationTime = null; + this._user = null; + this._safety = null; + this._contentUrl = null; + this._thumbnailUrl = null; + this._canvasWidth = null; + this._canvasHeight = null; + this._fileSize = null; - this._tags = []; - this._notes = []; - this._comments = []; - this._relations = []; + this._tags = []; + this._notes = []; + this._comments = []; + this._relations = []; - this._score = null; + this._score = null; this._favoriteCount = null; - this._ownScore = null; - this._ownFavorite = null; + this._ownScore = null; + this._ownFavorite = null; } + get id() { return this._id; } + get type() { return this._type; } + get mimeType() { return this._mimeType; } + get creationTime() { return this._creationTime; } + get user() { return this._user; } + get safety() { return this._safety; } + get contentUrl() { return this._contentUrl; } + get thumbnailUrl() { return this._thumbnailUrl; } + get canvasWidth() { return this._canvasWidth || 800; } + get canvasHeight() { return this._canvasHeight || 450; } + get fileSize() { return this._fileSize || 0; } + + get tags() { return this._tags; } + get notes() { return this._notes; } + get comments() { return this._comments; } + get relations() { return this._relations; } + + get score() { return this._score; } + get favoriteCount() { return this._favoriteCount; } + get ownFavorite() { return this._ownFavorite; } + get ownScore() { return this._ownScore; } + static fromResponse(response) { - const post = new Post(); - post._updateFromResponse(response); - return post; + const ret = new Post(); + ret._updateFromResponse(response); + return ret; } static get(id) { return api.get('/post/' + id) .then(response => { - const post = Post.fromResponse(response); - return Promise.resolve(post); + return Promise.resolve(Post.fromResponse(response)); }, response => { - return Promise.reject(response); + return Promise.reject(response.description); }); } - get id() { return this._id; } - get type() { return this._type; } - get mimeType() { return this._mimeType; } - get creationTime() { return this._creationTime; } - get user() { return this._user; } - get safety() { return this._safety; } - get contentUrl() { return this._contentUrl; } - get thumbnailUrl() { return this._thumbnailUrl; } - get canvasWidth() { return this._canvasWidth || 800; } - get canvasHeight() { return this._canvasHeight || 450; } - get fileSize() { return this._fileSize || 0; } - - get tags() { return this._tags; } - get notes() { return this._notes; } - get comments() { return this._comments; } - get relations() { return this._relations; } - - get score() { return this._score; } - get favoriteCount() { return this._favoriteCount; } - get ownFavorite() { return this._ownFavorite; } - get ownScore() { return this._ownScore; } - setScore(score) { return api.put('/post/' + this._id + '/score', {score: score}) .then(response => { @@ -75,13 +74,13 @@ class Post extends events.EventTarget { this._updateFromResponse(response); if (this._ownFavorite !== prevFavorite) { this.dispatchEvent(new CustomEvent('changeFavorite', { - details: { + detail: { post: this, }, })); } this.dispatchEvent(new CustomEvent('changeScore', { - details: { + detail: { post: this, }, })); @@ -98,13 +97,13 @@ class Post extends events.EventTarget { this._updateFromResponse(response); if (this._ownScore !== prevScore) { this.dispatchEvent(new CustomEvent('changeScore', { - details: { + detail: { post: this, }, })); } this.dispatchEvent(new CustomEvent('changeFavorite', { - details: { + detail: { post: this, }, })); @@ -121,13 +120,13 @@ class Post extends events.EventTarget { this._updateFromResponse(response); if (this._ownScore !== prevScore) { this.dispatchEvent(new CustomEvent('changeScore', { - details: { + detail: { post: this, }, })); } this.dispatchEvent(new CustomEvent('changeFavorite', { - details: { + detail: { post: this, }, })); @@ -138,27 +137,27 @@ class Post extends events.EventTarget { } _updateFromResponse(response) { - this._id = response.id; - this._type = response.type; - this._mimeType = response.mimeType; - this._creationTime = response.creationTime; - this._user = response.user; - this._safety = response.safety; - this._contentUrl = response.contentUrl; - this._thumbnailUrl = response.thumbnailUrl; - this._canvasWidth = response.canvasWidth; - this._canvasHeight = response.canvasHeight; - this._fileSize = response.fileSize; + this._id = response.id; + this._type = response.type; + this._mimeType = response.mimeType; + this._creationTime = response.creationTime; + this._user = response.user; + this._safety = response.safety; + this._contentUrl = response.contentUrl; + this._thumbnailUrl = response.thumbnailUrl; + this._canvasWidth = response.canvasWidth; + this._canvasHeight = response.canvasHeight; + this._fileSize = response.fileSize; - this._tags = response.tags; - this._notes = response.notes; - this._comments = CommentList.fromResponse(response.comments); - this._relations = response.relations; + this._tags = response.tags; + this._notes = response.notes; + this._comments = CommentList.fromResponse(response.comments || []); + this._relations = response.relations; - this._score = response.score; + this._score = response.score; this._favoriteCount = response.favoriteCount; - this._ownScore = response.ownScore; - this._ownFavorite = response.ownFavorite; + this._ownScore = response.ownScore; + this._ownFavorite = response.ownFavorite; } }; diff --git a/client/js/models/post_list.js b/client/js/models/post_list.js index 4595239..451ce28 100644 --- a/client/js/models/post_list.js +++ b/client/js/models/post_list.js @@ -1,33 +1,35 @@ 'use strict'; -const events = require('../events.js'); +const api = require('../api.js'); +const AbstractList = require('./abstract_list.js'); const Post = require('./post.js'); -class PostList extends events.EventTarget { - constructor(posts) { - super(); - this._list = []; +class PostList extends AbstractList { + static getAround(id, searchQuery) { + return api.get(`/post/${id}/around?fields=id&query=${searchQuery}`) + .then(response => { + return Promise.resolve(response); + }).catch(response => { + return Promise.reject(response.description); + }); } - static fromResponse(postsResponse) { - const postList = new PostList(); - for (let postResponse of postsResponse) { - postList._list.push(Post.fromResponse(postResponse)); - } - return postList; - } - - get posts() { - return [...this._list]; - } - - get length() { - return this._list.length; - } - - [Symbol.iterator]() { - return this._list[Symbol.iterator](); + static search(text, page, pageSize, fields) { + const url = + `/posts/?query=${text}` + + `&page=${page}` + + `&pageSize=${pageSize}` + + `&fields=${fields.join(',')}`; + return api.get(url).then(response => { + return Promise.resolve(Object.assign( + {}, + response, + {results: PostList.fromResponse(response.results)})); + }); } } +PostList._itemClass = Post; +PostList._itemName = 'post'; + module.exports = PostList; diff --git a/client/js/models/tag.js b/client/js/models/tag.js new file mode 100644 index 0000000..055aae4 --- /dev/null +++ b/client/js/models/tag.js @@ -0,0 +1,114 @@ +'use strict'; + +const api = require('../api.js'); +const events = require('../events.js'); + +class Tag extends events.EventTarget { + constructor() { + super(); + this._origName = null; + this._names = null; + this._category = null; + this._suggestions = null; + this._implications = null; + this._postCount = null; + this._creationTime = null; + this._lastEditTime = null; + } + + get names() { return this._names; } + get category() { return this._category; } + get suggestions() { return this._suggestions; } + get implications() { return this._implications; } + get postCount() { return this._postCount; } + get creationTime() { return this._creationTime; } + get lastEditTime() { return this._lastEditTime; } + + set names(value) { this._names = value; } + set category(value) { this._category = value; } + set implications(value) { this._implications = value; } + set suggestions(value) { this._suggestions = value; } + + static fromResponse(response) { + const ret = new Tag(); + ret._updateFromResponse(response); + return ret; + } + + static get(id) { + return api.get('/tag/' + id) + .then(response => { + return Promise.resolve(Tag.fromResponse(response)); + }, response => { + return Promise.reject(response.description); + }); + } + + save() { + const detail = { + names: this.names, + category: this.category, + implications: this.implications, + suggestions: this.suggestions, + }; + let promise = this._origName ? + api.put('/tag/' + this._origName, detail) : + api.post('/tags', detail); + return promise + .then(response => { + this._updateFromResponse(response); + this.dispatchEvent(new CustomEvent('change', { + detail: { + tag: this, + }, + })); + return Promise.resolve(); + }, response => { + return Promise.reject(response.description); + }); + } + + merge(targetName) { + return api.post('/tag-merge/', { + remove: this._origName, + mergeTo: targetName, + }).then(response => { + this._updateFromResponse(response); + this.dispatchEvent(new CustomEvent('change', { + detail: { + tag: this, + }, + })); + return Promise.resolve(); + }, response => { + return Promise.reject(response.description); + }); + } + + delete() { + return api.delete('/tag/' + this._origName) + .then(response => { + this.dispatchEvent(new CustomEvent('delete', { + detail: { + tag: this, + }, + })); + return Promise.resolve(); + }, response => { + return Promise.reject(response.description); + }); + } + + _updateFromResponse(response) { + this._origName = response.names ? response.names[0] : null; + this._names = response.names; + this._category = response.category; + this._implications = response.implications; + this._suggestions = response.suggestions; + this._creationTime = response.creationTime; + this._lastEditTime = response.lastEditTime; + this._postCount = response.usages; + } +}; + +module.exports = Tag; diff --git a/client/js/models/tag_category.js b/client/js/models/tag_category.js new file mode 100644 index 0000000..0f5216e --- /dev/null +++ b/client/js/models/tag_category.js @@ -0,0 +1,87 @@ +'use strict'; + +const api = require('../api.js'); +const events = require('../events.js'); + +class TagCategory extends events.EventTarget { + constructor() { + super(); + this._name = ''; + this._color = '#000000'; + this._tagCount = 0; + this._isDefault = false; + this._origName = null; + this._origColor = null; + } + + get name() { return this._name; } + get color() { return this._color; } + get tagCount() { return this._tagCount; } + get isDefault() { return this._isDefault; } + get isTransient() { return !this._origName; } + + set name(value) { this._name = value; } + set color(value) { this._color = value; } + + static fromResponse(response) { + const ret = new TagCategory(); + ret._updateFromResponse(response); + return ret; + } + + save() { + const data = {}; + if (this.name !== this._origName) { + data.name = this.name; + } + if (this.color !== this._origColor) { + data.color = this.color; + } + + if (!Object.keys(data).length) { + return Promise.resolve(); + } + + let promise = this._origName ? + api.put('/tag-category/' + this._origName, data) : + api.post('/tag-categories', data); + + return promise + .then(response => { + this._updateFromResponse(response); + this.dispatchEvent(new CustomEvent('change', { + detail: { + tagCategory: this, + }, + })); + return Promise.resolve(); + }, response => { + return Promise.reject(response.description); + }); + } + + delete() { + return api.delete('/tag-category/' + this._origName) + .then(response => { + this.dispatchEvent(new CustomEvent('delete', { + detail: { + tagCategory: this, + }, + })); + return Promise.resolve(); + }, response => { + return Promise.reject(response.description); + }); + } + + _updateFromResponse(response) { + this._name = response.name; + this._color = response.color; + this._isDefault = response.default; + this._tagCount = response.usages; + this._origName = this.name; + this._origColor = this.color; + } +} + +module.exports = TagCategory; diff --git a/client/js/models/tag_category_list.js b/client/js/models/tag_category_list.js new file mode 100644 index 0000000..8b2b0d0 --- /dev/null +++ b/client/js/models/tag_category_list.js @@ -0,0 +1,80 @@ +'use strict'; + +const api = require('../api.js'); +const AbstractList = require('./abstract_list.js'); +const TagCategory = require('./tag_category.js'); + +class TagCategoryList extends AbstractList { + constructor() { + super(); + this._defaultCategory = null; + this._origDefaultCategory = null; + this._deletedCategories = []; + this.addEventListener('remove', e => this._evtCategoryDeleted(e)); + } + + static fromResponse(response) { + const ret = super.fromResponse(response); + ret._defaultCategory = null; + for (let tagCategory of ret) { + if (tagCategory.isDefault) { + ret._defaultCategory = tagCategory; + } + } + ret._origDefaultCategory = ret._defaultCategory; + return ret; + } + + static get() { + return api.get('/tag-categories/').then(response => { + return Promise.resolve(Object.assign( + {}, + response, + {results: TagCategoryList.fromResponse(response.results)})); + }); + } + + get defaultCategory() { + return this._defaultCategory; + } + + set defaultCategory(tagCategory) { + this._defaultCategory = tagCategory; + } + + save() { + let promises = []; + for (let tagCategory of this) { + promises.push(tagCategory.save()); + } + for (let tagCategory of this._deletedCategories) { + promises.push(tagCategory.delete()); + } + + if (this._defaultCategory !== this._origDefaultCategory) { + promises.push( + api.put( + `/tag-category/${this._defaultCategory.name}/default`)); + } + + return Promise.all(promises) + .then(response => { + this._deletedCategories = []; + return Promise.resolve(); + }, errorMessage => { + return Promise.reject( + errorMessage.description || errorMessage); + }); + } + + _evtCategoryDeleted(e) { + if (!e.detail.tagCategory.isTransient) { + this._deletedCategories.push(e.detail.tagCategory); + } + } +} + +TagCategoryList._itemClass = TagCategory; +TagCategoryList._itemName = 'tagCategory'; + +module.exports = TagCategoryList; diff --git a/client/js/models/tag_list.js b/client/js/models/tag_list.js new file mode 100644 index 0000000..acd70d5 --- /dev/null +++ b/client/js/models/tag_list.js @@ -0,0 +1,24 @@ +'use strict'; + +const api = require('../api.js'); +const AbstractList = require('./abstract_list.js'); +const Tag = require('./tag.js'); + +class TagList extends AbstractList { + static search(text, page, pageSize, fields) { + const url = + `/tags/?query=${text}` + + `&page=${page}` + + `&pageSize=${pageSize}` + + `&fields=${fields.join(',')}`; + return api.get(url).then(response => { + response.results = TagList.fromResponse(response.results); + return Promise.resolve(response); + }); + } +} + +TagList._itemClass = Tag; +TagList._itemName = 'tag'; + +module.exports = TagList; diff --git a/client/js/models/user.js b/client/js/models/user.js new file mode 100644 index 0000000..4ebcac0 --- /dev/null +++ b/client/js/models/user.js @@ -0,0 +1,149 @@ +'use strict'; + +const api = require('../api.js'); +const events = require('../events.js'); + +class User extends events.EventTarget { + constructor() { + super(); + this._name = null; + this._rank = null; + this._email = null; + this._avatarStyle = null; + this._avatarUrl = null; + this._creationTime = null; + this._lastLoginTime = null; + this._commentCount = null; + this._favoritePostCount = null; + this._uploadedPostCount = null; + this._likedPostCount = null; + this._dislikedPostCount = null; + + this._origName = null; + this._origEmail = null; + this._origRank = null; + this._origAvatarStyle = null; + + this._password = null; + this._avatarContent = null; + } + + get name() { return this._name; } + get rank() { return this._rank; } + get email() { return this._email; } + get avatarStyle() { return this._avatarStyle; } + get avatarUrl() { return this._avatarUrl; } + get creationTime() { return this._creationTime; } + get lastLoginTime() { return this._lastLoginTime; } + get commentCount() { return this._commentCount; } + get favoritePostCount() { return this._favoritePostCount; } + get uploadedPostCount() { return this._uploadedPostCount; } + get likedPostCount() { return this._likedPostCount; } + get dislikedPostCount() { return this._dislikedPostCount; } + get rankName() { return api.rankNames.get(this.rank); } + get avatarContent() { throw 'Invalid operation'; } + get password() { throw 'Invalid operation'; } + + set name(value) { this._name = value; } + set rank(value) { this._rank = value; } + set email(value) { this._email = value || null; } + set avatarStyle(value) { this._avatarStyle = value; } + set avatarContent(value) { this._avatarContent = value; } + set password(value) { this._password = value; } + + static fromResponse(response) { + const ret = new User(); + ret._updateFromResponse(response); + return ret; + } + + static get(name) { + return api.get('/user/' + name) + .then(response => { + return Promise.resolve(User.fromResponse(response)); + }, response => { + return Promise.reject(response.description); + }); + } + + save() { + const files = []; + const data = {}; + if (this.name !== this._origName) { + data.name = this.name; + } + if (this._password) { + data.password = this._password; + } + + if (this.email !== this._origEmail) { + data.email = this.email; + } + + if (this.rank !== this._origRank) { + data.rank = this.rank; + } + if (this.avatarStyle !== this._origAvatarStyle) { + data.avatarStyle = this.avatarStyle; + } + if (this._avatarContent) { + files.avatar = this._avatarContent; + } + + let promise = this._origName ? + api.put('/user/' + this._origName, data, files) : + api.post('/users', data, files); + + return promise + .then(response => { + this._updateFromResponse(response); + this.dispatchEvent(new CustomEvent('change', { + detail: { + user: this, + }, + })); + return Promise.resolve(); + }, response => { + return Promise.reject(response.description); + }); + } + + delete() { + return api.delete('/user/' + this._origName) + .then(response => { + this.dispatchEvent(new CustomEvent('delete', { + detail: { + user: this, + }, + })); + return Promise.resolve(); + }, response => { + return Promise.reject(response.description); + }); + } + + _updateFromResponse(response) { + this._name = response.name; + this._rank = response.rank; + this._email = response.email; + this._avatarStyle = response.avatarStyle; + this._avatarUrl = response.avatarUrl; + this._creationTime = response.creationTime; + this._lastLoginTime = response.lastLoginTime; + this._commentCount = response.commentCount; + this._favoritePostCount = response.favoritePostCount; + this._uploadedPostCount = response.uploadedPostCount; + this._likedPostCount = response.likedPostCount; + this._dislikedPostCount = response.dislikedPostCount; + + this._origName = this.name; + this._origRank = this.rank; + this._origEmail = this.email; + this._origAvatarStyle = this.avatarStyle; + + this._password = null; + this._avatarContent = null; + } +}; + +module.exports = User; diff --git a/client/js/models/user_list.js b/client/js/models/user_list.js new file mode 100644 index 0000000..c829afd --- /dev/null +++ b/client/js/models/user_list.js @@ -0,0 +1,22 @@ +'use strict'; + +const api = require('../api.js'); +const AbstractList = require('./abstract_list.js'); +const User = require('./user.js'); + +class UserList extends AbstractList { + static search(text, page) { + const url = `/users/?query=${text}&page=${page}&pageSize=30`; + return api.get(url).then(response => { + return Promise.resolve(Object.assign( + {}, + response, + {results: UserList.fromResponse(response.results)})); + }); + } +} + +UserList._itemClass = User; +UserList._itemName = 'user'; + +module.exports = UserList; diff --git a/client/js/views/tag_categories_view.js b/client/js/views/tag_categories_view.js index aa72471..ac97db2 100644 --- a/client/js/views/tag_categories_view.js +++ b/client/js/views/tag_categories_view.js @@ -1,40 +1,59 @@ 'use strict'; +const events = require('../events.js'); const views = require('../util/views.js'); +const TagCategory = require('../models/tag_category.js'); const template = views.getTemplate('tag-categories'); +const rowTemplate = views.getTemplate('tag-category-row'); -class TagCategoriesView { +class TagCategoriesView extends events.EventTarget { constructor(ctx) { + super(); + this._ctx = ctx; this._hostNode = document.getElementById('content-holder'); - const sourceNode = template(ctx); - const formNode = sourceNode.querySelector('form'); - const newRowTemplate = sourceNode.querySelector('.add-template'); - const tableBodyNode = sourceNode.querySelector('tbody'); - const addLinkNode = sourceNode.querySelector('a.add'); + views.replaceContent(this._hostNode, template(ctx)); + views.decorateValidator(this._formNode); - newRowTemplate.parentNode.removeChild(newRowTemplate); - views.decorateValidator(formNode); - - for (let row of tableBodyNode.querySelectorAll('tr')) { - this._addRowHandlers(row); - } - - if (addLinkNode) { - addLinkNode.addEventListener('click', e => { - e.preventDefault(); - let newRow = newRowTemplate.cloneNode(true); - tableBody.appendChild(newRow); - this._addRowHandlers(row); - }); - } - - formNode.addEventListener('submit', e => { - this._evtSaveButtonClick(e, ctx); + const categoriesToAdd = Array.from(ctx.tagCategories); + categoriesToAdd.sort((a, b) => { + if (b.isDefault) { + return 1; + } else if (a.isDefault) { + return -1; + } + return a.name.localeCompare(b.name); }); + for (let tagCategory of categoriesToAdd) { + this._addTagCategoryRowNode(tagCategory); + } - views.replaceContent(this._hostNode, sourceNode); + if (this._addLinkNode) { + this._addLinkNode.addEventListener( + 'click', e => this._evtAddButtonClick(e)); + } + + ctx.tagCategories.addEventListener( + 'add', e => this._evtTagCategoryAdded(e)); + + ctx.tagCategories.addEventListener( + 'remove', e => this._evtTagCategoryDeleted(e)); + + this._formNode.addEventListener( + 'submit', e => this._evtSaveButtonClick(e, ctx)); + } + + enableForm() { + views.enableForm(this._formNode); + } + + disableForm() { + views.disableForm(this._formNode); + } + + clearMessages() { + views.clearMessages(this._hostNode); } showSuccess(message) { @@ -45,88 +64,100 @@ class TagCategoriesView { views.showError(this._hostNode, message); } - _evtSaveButtonClick(e, ctx) { - e.preventDefault(); - - views.clearMessages(this._hostNode); - const tableBodyNode = this._hostNode.querySelector('tbody'); - - ctx.getCategories().then(categories => { - let existingCategories = {}; - for (let category of categories) { - existingCategories[category.name] = category; - } - - let defaultCategory = null; - let addedCategories = []; - let removedCategories = []; - let changedCategories = []; - let allNames = []; - for (let row of tableBodyNode.querySelectorAll('tr')) { - let name = row.getAttribute('data-category'); - let category = { - originalName: name, - name: row.querySelector('.name input').value, - color: row.querySelector('.color input').value, - }; - if (row.classList.contains('default')) { - defaultCategory = category.name; - } - if (!name) { - if (category.name) { - addedCategories.push(category); - } - } else { - const existingCategory = existingCategories[name]; - if (existingCategory.color !== category.color || - existingCategory.name !== category.name) { - changedCategories.push(category); - } - } - allNames.push(name); - } - for (let name of Object.keys(existingCategories)) { - if (allNames.indexOf(name) === -1) { - removedCategories.push(name); - } - } - ctx.saveChanges( - addedCategories, - changedCategories, - removedCategories, - defaultCategory); - }); + get _formNode() { + return this._hostNode.querySelector('form'); } - _evtRemoveButtonClick(e, row, link) { + get _tableBodyNode() { + return this._hostNode.querySelector('tbody'); + } + + get _addLinkNode() { + return this._hostNode.querySelector('a.add'); + } + + _addTagCategoryRowNode(tagCategory) { + const rowNode = rowTemplate( + Object.assign( + {}, this._ctx, {tagCategory: tagCategory})); + + const nameInput = rowNode.querySelector('.name input'); + if (nameInput) { + nameInput.addEventListener( + 'change', e => this._evtNameChange(e, rowNode)); + } + + const colorInput = rowNode.querySelector('.color input'); + if (colorInput) { + colorInput.addEventListener( + 'change', e => this._evtColorChange(e, rowNode)); + } + + const removeLinkNode = rowNode.querySelector('.remove a'); + if (removeLinkNode) { + removeLinkNode.addEventListener( + 'click', e => this._evtDeleteButtonClick(e, rowNode)); + } + + const defaultLinkNode = rowNode.querySelector('.set-default a'); + if (defaultLinkNode) { + defaultLinkNode.addEventListener( + 'click', e => this._evtSetDefaultButtonClick(e, rowNode)); + } + + this._tableBodyNode.appendChild(rowNode); + + rowNode._tagCategory = tagCategory; + tagCategory._rowNode = rowNode; + } + + _removeTagCategoryRowNode(tagCategory) { + const rowNode = tagCategory._rowNode; + rowNode.parentNode.removeChild(rowNode); + } + + _evtTagCategoryAdded(e) { + this._addTagCategoryRowNode(e.detail.tagCategory); + } + + _evtTagCategoryDeleted(e) { + this._removeTagCategoryRowNode(e.detail.tagCategory); + } + + _evtAddButtonClick(e) { e.preventDefault(); - if (link.classList.contains('inactive')) { + this._ctx.tagCategories.add(new TagCategory()); + } + + _evtNameChange(e, rowNode) { + rowNode._tagCategory.name = e.target.value; + } + + _evtColorChange(e, rowNode) { + rowNode._tagCategory.color = e.target.value; + } + + _evtDeleteButtonClick(e, rowNode, link) { + e.preventDefault(); + if (e.target.classList.contains('inactive')) { return; } - row.parentNode.removeChild(row); + this._ctx.tagCategories.remove(rowNode._tagCategory); } - _evtSetDefaultButtonClick(e, row) { + _evtSetDefaultButtonClick(e, rowNode) { e.preventDefault(); - const oldRowNode = row.parentNode.querySelector('tr.default'); + this._ctx.tagCategories.defaultCategory = rowNode._tagCategory; + const oldRowNode = rowNode.parentNode.querySelector('tr.default'); if (oldRowNode) { oldRowNode.classList.remove('default'); } - row.classList.add('default'); + rowNode.classList.add('default'); } - _addRowHandlers(row) { - const removeLink = row.querySelector('.remove a'); - if (removeLink) { - removeLink.addEventListener( - 'click', e => this._evtRemoveButtonClick(e, row, removeLink)); - } - - const defaultLink = row.querySelector('.set-default a'); - if (defaultLink) { - defaultLink.addEventListener( - 'click', e => this._evtSetDefaultButtonClick(e, row)); - } + _evtSaveButtonClick(e, ctx) { + e.preventDefault(); + this.dispatchEvent(new CustomEvent('submit')); } } diff --git a/client/js/views/tag_view.js b/client/js/views/tag_view.js index 11c5ecc..52a6f31 100644 --- a/client/js/views/tag_view.js +++ b/client/js/views/tag_view.js @@ -12,16 +12,21 @@ class TagView extends events.EventTarget { constructor(ctx) { super(); - this._hostNode = document.getElementById('content-holder'); + this._ctx = ctx; + ctx.tag.addEventListener('change', e => this._evtChange(e)); ctx.section = ctx.section || 'summary'; + + this._hostNode = document.getElementById('content-holder'); + this._install(); + } + + _install() { + const ctx = this._ctx; views.replaceContent(this._hostNode, template(ctx)); for (let item of this._hostNode.querySelectorAll('[data-name]')) { - if (item.getAttribute('data-name') === ctx.section) { - item.className = 'active'; - } else { - item.className = ''; - } + item.classList.toggle( + 'active', item.getAttribute('data-name') === ctx.section); } ctx.hostNode = this._hostNode.querySelector('.tag-content-holder'); @@ -65,6 +70,11 @@ class TagView extends events.EventTarget { showError(message) { this._view.showError(message); } + + _evtChange(e) { + this._ctx.tag = e.detail.tag; + this._install(this._ctx); + } } module.exports = TagView; diff --git a/client/js/views/user_edit_view.js b/client/js/views/user_edit_view.js index 75dfa05..d3a17c3 100644 --- a/client/js/views/user_edit_view.js +++ b/client/js/views/user_edit_view.js @@ -60,12 +60,12 @@ class UserEditView extends events.EventTarget { e.preventDefault(); this.dispatchEvent(new CustomEvent('submit', { detail: { - user: this._user, - name: this._userNameFieldNode.value, - password: this._passwordFieldNode.value, - email: this._emailFieldNode.value, - rank: this._rankFieldNode.value, - avatarStyle: this._avatarStyleFieldNode.value, + user: this._user, + name: (this._userNameFieldNode || {}).value, + email: (this._emailFieldNode || {}).value, + rank: (this._rankFieldNode || {}).value, + avatarStyle: (this._avatarStyleFieldNode || {}).value, + password: (this._passwordFieldNode || {}).value, avatarContent: this._avatarContent, }, })); diff --git a/client/js/views/user_view.js b/client/js/views/user_view.js index 734501d..6411c7e 100644 --- a/client/js/views/user_view.js +++ b/client/js/views/user_view.js @@ -12,10 +12,17 @@ class UserView extends events.EventTarget { constructor(ctx) { super(); - this._hostNode = document.getElementById('content-holder'); + this._ctx = ctx; + ctx.user.addEventListener('change', e => this._evtChange(e)); ctx.section = ctx.section || 'summary'; - views.replaceContent(this._hostNode, template(ctx)); + this._hostNode = document.getElementById('content-holder'); + this._install(); + } + + _install() { + const ctx = this._ctx; + views.replaceContent(this._hostNode, template(ctx)); for (let item of this._hostNode.querySelectorAll('[data-name]')) { if (item.getAttribute('data-name') === ctx.section) { item.className = 'active'; @@ -61,6 +68,11 @@ class UserView extends events.EventTarget { disableForm() { this._view.disableForm(); } + + _evtChange(e) { + this._ctx.user = e.detail.user; + this._install(this._ctx); + } } module.exports = UserView;