diff --git a/app/assets/javascripts/discourse/app/models/action-summary.js b/app/assets/javascripts/discourse/app/models/action-summary.js index 6554d0ccd38..3b8ce55cbb6 100644 --- a/app/assets/javascripts/discourse/app/models/action-summary.js +++ b/app/assets/javascripts/discourse/app/models/action-summary.js @@ -3,8 +3,8 @@ import { ajax } from "discourse/lib/ajax"; import { popupAjaxError } from "discourse/lib/ajax-error"; import RestModel from "discourse/models/rest"; -export default RestModel.extend({ - canToggle: or("can_undo", "can_act"), +export default class ActionSummary extends RestModel { + @or("can_undo", "can_act") canToggle; // Remove it removeAction() { @@ -14,11 +14,11 @@ export default RestModel.extend({ can_act: true, can_undo: false, }); - }, + } togglePromise(post) { return this.acted ? this.undo(post) : this.act(post); - }, + } toggle(post) { if (!this.acted) { @@ -28,7 +28,7 @@ export default RestModel.extend({ this.undo(post); return false; } - }, + } // Perform this action act(post, opts) { @@ -76,7 +76,7 @@ export default RestModel.extend({ popupAjaxError(error); this.removeAction(post); }); - }, + } // Undo this action undo(post) { @@ -90,5 +90,5 @@ export default RestModel.extend({ post.updateActionsSummary(result); return { acted: false }; }); - }, -}); + } +} diff --git a/app/assets/javascripts/discourse/app/models/archetype.js b/app/assets/javascripts/discourse/app/models/archetype.js index bd4eedffe0b..6f9e60ecc48 100644 --- a/app/assets/javascripts/discourse/app/models/archetype.js +++ b/app/assets/javascripts/discourse/app/models/archetype.js @@ -2,8 +2,8 @@ import { gt, not } from "@ember/object/computed"; import { propertyEqual } from "discourse/lib/computed"; import RestModel from "discourse/models/rest"; -export default RestModel.extend({ - hasOptions: gt("options.length", 0), - isDefault: propertyEqual("id", "site.default_archetype"), - notDefault: not("isDefault"), -}); +export default class Archetype extends RestModel { + @gt("options.length", 0) hasOptions; + @propertyEqual("id", "site.default_archetype") isDefault; + @not("isDefault") notDefault; +} diff --git a/app/assets/javascripts/discourse/app/models/associated-group.js b/app/assets/javascripts/discourse/app/models/associated-group.js index e914aaf824e..b5de9175d1e 100644 --- a/app/assets/javascripts/discourse/app/models/associated-group.js +++ b/app/assets/javascripts/discourse/app/models/associated-group.js @@ -2,16 +2,12 @@ import EmberObject from "@ember/object"; import { ajax } from "discourse/lib/ajax"; import { popupAjaxError } from "discourse/lib/ajax-error"; -const AssociatedGroup = EmberObject.extend(); - -AssociatedGroup.reopenClass({ - list() { +export default class AssociatedGroup extends EmberObject { + static list() { return ajax("/associated_groups") .then((result) => { return result.associated_groups.map((ag) => AssociatedGroup.create(ag)); }) .catch(popupAjaxError); - }, -}); - -export default AssociatedGroup; + } +} diff --git a/app/assets/javascripts/discourse/app/models/badge-grouping.js b/app/assets/javascripts/discourse/app/models/badge-grouping.js index 9e668bef95c..e60b79f5ea7 100644 --- a/app/assets/javascripts/discourse/app/models/badge-grouping.js +++ b/app/assets/javascripts/discourse/app/models/badge-grouping.js @@ -2,15 +2,15 @@ import RestModel from "discourse/models/rest"; import discourseComputed from "discourse-common/utils/decorators"; import I18n from "discourse-i18n"; -export default RestModel.extend({ +export default class BadgeGrouping extends RestModel { @discourseComputed("name") i18nNameKey() { return this.name.toLowerCase().replace(/\s/g, "_"); - }, + } @discourseComputed("name") displayName() { const i18nKey = `badges.badge_grouping.${this.i18nNameKey}.name`; return I18n.t(i18nKey, { defaultValue: this.name }); - }, -}); + } +} diff --git a/app/assets/javascripts/discourse/app/models/badge.js b/app/assets/javascripts/discourse/app/models/badge.js index 01c369418fc..bf0f0afb5a7 100644 --- a/app/assets/javascripts/discourse/app/models/badge.js +++ b/app/assets/javascripts/discourse/app/models/badge.js @@ -7,63 +7,8 @@ import RestModel from "discourse/models/rest"; import getURL from "discourse-common/lib/get-url"; import discourseComputed from "discourse-common/utils/decorators"; -const Badge = RestModel.extend({ - newBadge: none("id"), - image: alias("image_url"), - - @discourseComputed - url() { - return getURL(`/badges/${this.id}/${this.slug}`); - }, - - updateFromJson(json) { - if (json.badge) { - Object.keys(json.badge).forEach((key) => this.set(key, json.badge[key])); - } - if (json.badge_types) { - json.badge_types.forEach((badgeType) => { - if (badgeType.id === this.badge_type_id) { - this.set("badge_type", Object.create(badgeType)); - } - }); - } - }, - - @discourseComputed("badge_type.name") - badgeTypeClassName(type) { - type = type || ""; - return `badge-type-${type.toLowerCase()}`; - }, - - save(data) { - let url = "/admin/badges", - type = "POST"; - - if (this.id) { - // We are updating an existing badge. - url += `/${this.id}`; - type = "PUT"; - } - - return ajax(url, { type, data }).then((json) => { - this.updateFromJson(json); - return this; - }); - }, - - destroy() { - if (this.newBadge) { - return Promise.resolve(); - } - - return ajax(`/admin/badges/${this.id}`, { - type: "DELETE", - }); - }, -}); - -Badge.reopenClass({ - createFromJson(json) { +export default class Badge extends RestModel { + static createFromJson(json) { // Create BadgeType objects. const badgeTypes = {}; if ("badge_types" in json) { @@ -103,9 +48,9 @@ Badge.reopenClass({ } else { return badges; } - }, + } - findAll(opts) { + static findAll(opts) { let listable = ""; if (opts && opts.onlyListable) { listable = "?only_listable=true"; @@ -114,13 +59,65 @@ Badge.reopenClass({ return ajax(`/badges.json${listable}`, { data: opts }).then((badgesJson) => Badge.createFromJson(badgesJson) ); - }, + } - findById(id) { + static findById(id) { return ajax(`/badges/${id}`).then((badgeJson) => Badge.createFromJson(badgeJson) ); - }, -}); + } -export default Badge; + @none("id") newBadge; + + @alias("image_url") image; + + @discourseComputed + url() { + return getURL(`/badges/${this.id}/${this.slug}`); + } + + updateFromJson(json) { + if (json.badge) { + Object.keys(json.badge).forEach((key) => this.set(key, json.badge[key])); + } + if (json.badge_types) { + json.badge_types.forEach((badgeType) => { + if (badgeType.id === this.badge_type_id) { + this.set("badge_type", Object.create(badgeType)); + } + }); + } + } + + @discourseComputed("badge_type.name") + badgeTypeClassName(type) { + type = type || ""; + return `badge-type-${type.toLowerCase()}`; + } + + save(data) { + let url = "/admin/badges", + type = "POST"; + + if (this.id) { + // We are updating an existing badge. + url += `/${this.id}`; + type = "PUT"; + } + + return ajax(url, { type, data }).then((json) => { + this.updateFromJson(json); + return this; + }); + } + + destroy() { + if (this.newBadge) { + return Promise.resolve(); + } + + return ajax(`/admin/badges/${this.id}`, { + type: "DELETE", + }); + } +} diff --git a/app/assets/javascripts/discourse/app/models/bookmark.js b/app/assets/javascripts/discourse/app/models/bookmark.js index 26c11f3cd9b..8898862a8ae 100644 --- a/app/assets/javascripts/discourse/app/models/bookmark.js +++ b/app/assets/javascripts/discourse/app/models/bookmark.js @@ -25,13 +25,33 @@ export const AUTO_DELETE_PREFERENCES = { export const NO_REMINDER_ICON = "bookmark"; export const WITH_REMINDER_ICON = "discourse-bookmark-clock"; -const Bookmark = RestModel.extend({ - newBookmark: none("id"), +export default class Bookmark extends RestModel { + static create(args) { + args = args || {}; + args.currentUser = args.currentUser || User.current(); + args.user = User.create(args.user); + return super.create(args); + } + + static createFor(user, bookmarkableType, bookmarkableId) { + return Bookmark.create({ + bookmarkable_type: bookmarkableType, + bookmarkable_id: bookmarkableId, + user_id: user.id, + auto_delete_preference: user.user_option.bookmark_auto_delete_preference, + }); + } + + static async applyTransformations(bookmarks) { + await applyModelTransformations("bookmark", bookmarks); + } + + @none("id") newBookmark; @computed get url() { return getURL(`/bookmarks/${this.id}`); - }, + } destroy() { if (this.newBookmark) { @@ -41,14 +61,14 @@ const Bookmark = RestModel.extend({ return ajax(this.url, { type: "DELETE", }); - }, + } attachedTo() { return { target: this.bookmarkable_type.toLowerCase(), targetId: this.bookmarkable_id, }; - }, + } togglePin() { if (this.newBookmark) { @@ -58,16 +78,16 @@ const Bookmark = RestModel.extend({ return ajax(this.url + "/toggle_pin", { type: "PUT", }); - }, + } pinAction() { return this.pinned ? "unpin" : "pin"; - }, + } @discourseComputed("highest_post_number", "url") lastPostUrl(highestPostNumber) { return this.urlForPostNumber(highestPostNumber); - }, + } // Helper to build a Url with a post number urlForPostNumber(postNumber) { @@ -76,7 +96,7 @@ const Bookmark = RestModel.extend({ url += `/${postNumber}`; } return url; - }, + } // returns createdAt if there's no bumped date @discourseComputed("bumped_at", "createdAt") @@ -86,7 +106,7 @@ const Bookmark = RestModel.extend({ } else { return createdAt; } - }, + } @discourseComputed("bumpedAt", "createdAt") bumpedAtTitle(bumpedAt, createdAt) { @@ -101,7 +121,7 @@ const Bookmark = RestModel.extend({ })}\n${I18n.t("topic.bumped_at", { date: longDate(bumpedAt) })}` : I18n.t("topic.created_at", { date: longDate(createdAt) }); } - }, + } @discourseComputed("name", "reminder_at") reminderTitle(name, reminderAt) { @@ -118,12 +138,12 @@ const Bookmark = RestModel.extend({ return I18n.t("bookmarks.created_generic", { name: name || "", }); - }, + } @discourseComputed("created_at") createdAt(created_at) { return new Date(created_at); - }, + } @discourseComputed("tags") visibleListTags(tags) { @@ -141,12 +161,12 @@ const Bookmark = RestModel.extend({ }); return newTags; - }, + } @computed("category_id") get category() { return Category.findById(this.category_id); - }, + } @discourseComputed("reminder_at", "currentUser") formattedReminder(bookmarkReminderAt, currentUser) { @@ -156,12 +176,12 @@ const Bookmark = RestModel.extend({ currentUser?.user_option?.timezone || moment.tz.guess() ) ); - }, + } @discourseComputed("reminder_at") reminderAtExpired(bookmarkReminderAt) { return moment(bookmarkReminderAt) < moment(); - }, + } @discourseComputed() topicForList() { @@ -178,34 +198,10 @@ const Bookmark = RestModel.extend({ last_read_post_number: this.last_read_post_number, highest_post_number: this.highest_post_number, }); - }, + } @discourseComputed("bookmarkable_type") bookmarkableTopicAlike(bookmarkable_type) { return ["Topic", "Post"].includes(bookmarkable_type); - }, -}); - -Bookmark.reopenClass({ - create(args) { - args = args || {}; - args.currentUser = args.currentUser || User.current(); - args.user = User.create(args.user); - return this._super(args); - }, - - createFor(user, bookmarkableType, bookmarkableId) { - return Bookmark.create({ - bookmarkable_type: bookmarkableType, - bookmarkable_id: bookmarkableId, - user_id: user.id, - auto_delete_preference: user.user_option.bookmark_auto_delete_preference, - }); - }, - - async applyTransformations(bookmarks) { - await applyModelTransformations("bookmark", bookmarks); - }, -}); - -export default Bookmark; + } +} diff --git a/app/assets/javascripts/discourse/app/models/category-list.js b/app/assets/javascripts/discourse/app/models/category-list.js index 2311eed3a7c..eefc465af89 100644 --- a/app/assets/javascripts/discourse/app/models/category-list.js +++ b/app/assets/javascripts/discourse/app/models/category-list.js @@ -8,38 +8,8 @@ import Topic from "discourse/models/topic"; import { bind } from "discourse-common/utils/decorators"; import I18n from "discourse-i18n"; -const CategoryList = ArrayProxy.extend({ - init() { - this.set("content", this.categories || []); - this._super(...arguments); - this.set("page", 1); - this.set("fetchedLastPage", false); - }, - - @bind - async loadMore() { - if (this.isLoading || this.fetchedLastPage) { - return; - } - - this.set("isLoading", true); - - const data = { page: this.page + 1 }; - const result = await ajax("/categories.json", { data }); - - this.set("page", data.page); - if (result.category_list.categories.length === 0) { - this.set("fetchedLastPage", true); - } - this.set("isLoading", false); - - const newCategoryList = CategoryList.categoriesFrom(this.store, result); - newCategoryList.forEach((c) => this.categories.pushObject(c)); - }, -}); - -CategoryList.reopenClass({ - categoriesFrom(store, result, parentCategory = null) { +export default class CategoryList extends ArrayProxy { + static categoriesFrom(store, result, parentCategory = null) { // Find the period that is most relevant const statPeriod = ["week", "month"].find( @@ -67,9 +37,9 @@ CategoryList.reopenClass({ } }); return categories; - }, + } - _buildCategoryResult(c, statPeriod) { + static _buildCategoryResult(c, statPeriod) { if (c.parent_category_id) { c.parentCategory = Category.findById(c.parent_category_id); } @@ -126,9 +96,9 @@ CategoryList.reopenClass({ const record = Site.current().updateCategory(c); record.setupGroupsAndPermissions(); return record; - }, + } - listForParent(store, category) { + static listForParent(store, category) { return ajax( `/categories.json?parent_category_id=${category.get("id")}` ).then((result) => @@ -138,9 +108,9 @@ CategoryList.reopenClass({ parentCategory: category, }) ); - }, + } - list(store) { + static list(store) { return PreloadStore.getAndRemove("categories_list", () => ajax("/categories.json") ).then((result) => @@ -151,7 +121,33 @@ CategoryList.reopenClass({ can_create_topic: result.category_list.can_create_topic, }) ); - }, -}); + } -export default CategoryList; + init() { + this.set("content", this.categories || []); + super.init(...arguments); + this.set("page", 1); + this.set("fetchedLastPage", false); + } + + @bind + async loadMore() { + if (this.isLoading || this.fetchedLastPage) { + return; + } + + this.set("isLoading", true); + + const data = { page: this.page + 1 }; + const result = await ajax("/categories.json", { data }); + + this.set("page", data.page); + if (result.category_list.categories.length === 0) { + this.set("fetchedLastPage", true); + } + this.set("isLoading", false); + + const newCategoryList = CategoryList.categoriesFrom(this.store, result); + newCategoryList.forEach((c) => this.categories.pushObject(c)); + } +} diff --git a/app/assets/javascripts/discourse/app/models/category.js b/app/assets/javascripts/discourse/app/models/category.js index c4e36382ec3..f15bd040237 100644 --- a/app/assets/javascripts/discourse/app/models/category.js +++ b/app/assets/javascripts/discourse/app/models/category.js @@ -14,370 +14,9 @@ import { MultiCache } from "discourse-common/utils/multi-cache"; const STAFF_GROUP_NAME = "staff"; const CATEGORY_ASYNC_SEARCH_CACHE = {}; -const Category = RestModel.extend({ - permissions: null, - - @on("init") - setupGroupsAndPermissions() { - const availableGroups = this.available_groups; - if (!availableGroups) { - return; - } - this.set("availableGroups", availableGroups); - - const groupPermissions = this.group_permissions; - - if (groupPermissions) { - this.set( - "permissions", - groupPermissions.map((elem) => { - availableGroups.removeObject(elem.group_name); - return elem; - }) - ); - } - }, - - @discourseComputed("required_tag_groups", "minimum_required_tags") - minimumRequiredTags() { - if (this.required_tag_groups?.length > 0) { - // it should require the max between the bare minimum set in the category and the sum of the min_count of the - // required_tag_groups - return Math.max( - this.required_tag_groups.reduce((sum, rtg) => sum + rtg.min_count, 0), - this.minimum_required_tags || 0 - ); - } else { - return this.minimum_required_tags > 0 ? this.minimum_required_tags : null; - } - }, - - @discourseComputed - availablePermissions() { - return [ - PermissionType.create({ id: PermissionType.FULL }), - PermissionType.create({ id: PermissionType.CREATE_POST }), - PermissionType.create({ id: PermissionType.READONLY }), - ]; - }, - - @discourseComputed("id") - searchContext(id) { - return { type: "category", id, category: this }; - }, - - @discourseComputed("parentCategory.ancestors") - ancestors(parentAncestors) { - return [...(parentAncestors || []), this]; - }, - - @discourseComputed("parentCategory.level") - level(parentLevel) { - if (!parentLevel) { - return parentLevel === 0 ? 1 : 0; - } else { - return parentLevel + 1; - } - }, - - @discourseComputed("has_children", "subcategories") - isParent(hasChildren, subcategories) { - return hasChildren || (subcategories && subcategories.length > 0); - }, - - @discourseComputed("subcategories") - isGrandParent(subcategories) { - return ( - subcategories && - subcategories.some( - (cat) => cat.subcategories && cat.subcategories.length > 0 - ) - ); - }, - - @discourseComputed("notification_level") - isMuted(notificationLevel) { - return notificationLevel === NotificationLevels.MUTED; - }, - - @discourseComputed("isMuted", "subcategories") - isHidden(isMuted, subcategories) { - if (!isMuted) { - return false; - } else if (!subcategories) { - return true; - } - - if (subcategories.some((cat) => !cat.isHidden)) { - return false; - } - - return true; - }, - - @discourseComputed("isMuted", "subcategories") - hasMuted(isMuted, subcategories) { - if (isMuted) { - return true; - } else if (!subcategories) { - return false; - } - - if (subcategories.some((cat) => cat.hasMuted)) { - return true; - } - - return false; - }, - - @discourseComputed("notification_level") - notificationLevelString(notificationLevel) { - // Get the key from the value - const notificationLevelString = Object.keys(NotificationLevels).find( - (key) => NotificationLevels[key] === notificationLevel - ); - if (notificationLevelString) { - return notificationLevelString.toLowerCase(); - } - }, - - @discourseComputed("name") - path() { - return `/c/${Category.slugFor(this)}/${this.id}`; - }, - - @discourseComputed("path") - url(path) { - return getURL(path); - }, - - @discourseComputed - fullSlug() { - return Category.slugFor(this).replace(/\//g, "-"); - }, - - @discourseComputed("name") - nameLower(name) { - return name.toLowerCase(); - }, - - @discourseComputed("url") - unreadUrl(url) { - return `${url}/l/unread`; - }, - - @discourseComputed("url") - newUrl(url) { - return `${url}/l/new`; - }, - - @discourseComputed("color", "text_color") - style(color, textColor) { - return `background-color: #${color}; color: #${textColor}`; - }, - - @discourseComputed("topic_count") - moreTopics(topicCount) { - return topicCount > (this.num_featured_topics || 2); - }, - - @discourseComputed("topic_count", "subcategories.[]") - totalTopicCount(topicCount, subcategories) { - if (subcategories) { - subcategories.forEach((subcategory) => { - topicCount += subcategory.topic_count; - }); - } - return topicCount; - }, - - @discourseComputed("default_slow_mode_seconds") - defaultSlowModeMinutes(seconds) { - return seconds ? seconds / 60 : null; - }, - - @discourseComputed("notification_level") - isTracked(notificationLevel) { - return notificationLevel >= NotificationLevels.TRACKING; - }, - - get unreadTopicsCount() { - return this.topicTrackingState.countUnread({ categoryId: this.id }); - }, - - get newTopicsCount() { - return this.topicTrackingState.countNew({ categoryId: this.id }); - }, - - save() { - const id = this.id; - const url = id ? `/categories/${id}` : "/categories"; - - return ajax(url, { - contentType: "application/json", - data: JSON.stringify({ - name: this.name, - slug: this.slug, - color: this.color, - text_color: this.text_color, - secure: this.secure, - permissions: this._permissionsForUpdate(), - auto_close_hours: this.auto_close_hours, - auto_close_based_on_last_post: this.get( - "auto_close_based_on_last_post" - ), - default_slow_mode_seconds: this.default_slow_mode_seconds, - position: this.position, - email_in: this.email_in, - email_in_allow_strangers: this.email_in_allow_strangers, - mailinglist_mirror: this.mailinglist_mirror, - parent_category_id: this.parent_category_id, - uploaded_logo_id: this.get("uploaded_logo.id"), - uploaded_logo_dark_id: this.get("uploaded_logo_dark.id"), - uploaded_background_id: this.get("uploaded_background.id"), - uploaded_background_dark_id: this.get("uploaded_background_dark.id"), - allow_badges: this.allow_badges, - category_setting_attributes: this.category_setting, - custom_fields: this.custom_fields, - topic_template: this.topic_template, - form_template_ids: this.form_template_ids, - all_topics_wiki: this.all_topics_wiki, - allow_unlimited_owner_edits_on_first_post: - this.allow_unlimited_owner_edits_on_first_post, - allowed_tags: this.allowed_tags, - allowed_tag_groups: this.allowed_tag_groups, - allow_global_tags: this.allow_global_tags, - required_tag_groups: this.required_tag_groups, - sort_order: this.sort_order, - sort_ascending: this.sort_ascending, - topic_featured_link_allowed: this.topic_featured_link_allowed, - show_subcategory_list: this.show_subcategory_list, - num_featured_topics: this.num_featured_topics, - default_view: this.default_view, - subcategory_list_style: this.subcategory_list_style, - default_top_period: this.default_top_period, - minimum_required_tags: this.minimum_required_tags, - navigate_to_first_post_after_read: this.get( - "navigate_to_first_post_after_read" - ), - search_priority: this.search_priority, - reviewable_by_group_name: this.reviewable_by_group_name, - read_only_banner: this.read_only_banner, - default_list_filter: this.default_list_filter, - }), - type: id ? "PUT" : "POST", - }); - }, - - _permissionsForUpdate() { - const permissions = this.permissions; - let rval = {}; - if (permissions.length) { - permissions.forEach((p) => (rval[p.group_name] = p.permission_type)); - } else { - // empty permissions => staff-only access - rval[STAFF_GROUP_NAME] = PermissionType.FULL; - } - return rval; - }, - - destroy() { - return ajax(`/categories/${this.id || this.slug}`, { - type: "DELETE", - }); - }, - - addPermission(permission) { - this.permissions.addObject(permission); - this.availableGroups.removeObject(permission.group_name); - }, - - removePermission(group_name) { - const permission = this.permissions.findBy("group_name", group_name); - if (permission) { - this.permissions.removeObject(permission); - this.availableGroups.addObject(group_name); - } - }, - - updatePermission(group_name, type) { - this.permissions.forEach((p, i) => { - if (p.group_name === group_name) { - this.set(`permissions.${i}.permission_type`, type); - } - }); - }, - - @discourseComputed("topics") - latestTopic(topics) { - if (topics && topics.length) { - return topics[0]; - } - }, - - @discourseComputed("topics") - featuredTopics(topics) { - if (topics && topics.length) { - return topics.slice(0, this.num_featured_topics || 2); - } - }, - - setNotification(notification_level) { - User.currentProp( - "muted_category_ids", - User.current().calculateMutedIds( - notification_level, - this.id, - "muted_category_ids" - ) - ); - - const url = `/category/${this.id}/notifications`; - return ajax(url, { data: { notification_level }, type: "POST" }).then( - (data) => { - User.current().set( - "indirectly_muted_category_ids", - data.indirectly_muted_category_ids - ); - this.set("notification_level", notification_level); - this.notifyPropertyChange("notification_level"); - } - ); - }, - - @discourseComputed("id") - isUncategorizedCategory(id) { - return Category.isUncategorized(id); - }, - - get canCreateTopic() { - return this.permission === PermissionType.FULL; - }, - - get subcategoryWithCreateTopicPermission() { - return this.subcategories?.find( - (subcategory) => subcategory.canCreateTopic - ); - }, -}); - -let _uncategorized; - -const categoryMultiCache = new MultiCache(async (ids) => { - const result = await ajax("/categories/find", { data: { ids } }); - - return new Map( - result["categories"].map((category) => [category.id, category]) - ); -}); - -export function resetCategoryCache() { - categoryMultiCache.reset(); -} - -Category.reopenClass({ +export default class Category extends RestModel { // Sort subcategories directly under parents - sortCategories(categories) { + static sortCategories(categories) { const children = new Map(); categories.forEach((category) => { @@ -392,20 +31,20 @@ Category.reopenClass({ values.flatMap((c) => [c, reduce(children.get(c.id) || [])]).flat(); return reduce(children.get(-1) || []); - }, + } - isUncategorized(categoryId) { + static isUncategorized(categoryId) { return categoryId === Site.currentProp("uncategorized_category_id"); - }, + } - slugEncoded() { + static slugEncoded() { let siteSettings = getOwnerWithFallback(this).lookup( "service:site-settings" ); return siteSettings.slug_generation_method === "encoded"; - }, + } - findUncategorized() { + static findUncategorized() { _uncategorized = _uncategorized || Category.list().findBy( @@ -413,9 +52,9 @@ Category.reopenClass({ Site.currentProp("uncategorized_category_id") ); return _uncategorized; - }, + } - slugFor(category, separator = "/", depth = 3) { + static slugFor(category, separator = "/", depth = 3) { if (!category) { return ""; } @@ -434,21 +73,21 @@ Category.reopenClass({ return !slug || slug.trim().length === 0 ? `${result}${id}-category` : result + slug; - }, + } - list() { + static list() { return Site.currentProp("categoriesList"); - }, + } - listByActivity() { + static listByActivity() { return Site.currentProp("sortedCategories"); - }, + } - _idMap() { + static _idMap() { return Site.currentProp("categoriesById"); - }, + } - findSingleBySlug(slug) { + static findSingleBySlug(slug) { if (!this.slugEncoded()) { return Category.list().find((c) => Category.slugFor(c) === slug); } else { @@ -456,16 +95,16 @@ Category.reopenClass({ (c) => Category.slugFor(c) === encodeURI(slug) ); } - }, + } - findById(id) { + static findById(id) { if (!id) { return; } return Category._idMap()[id]; - }, + } - findByIds(ids = []) { + static findByIds(ids = []) { const categories = []; ids.forEach((id) => { const found = Category.findById(id); @@ -474,14 +113,14 @@ Category.reopenClass({ } }); return categories; - }, + } - hasAsyncFoundAll(ids) { + static hasAsyncFoundAll(ids) { const loadedCategoryIds = Site.current().loadedCategoryIds || new Set(); return ids.every((id) => loadedCategoryIds.has(id)); - }, + } - async asyncFindByIds(ids = []) { + static async asyncFindByIds(ids = []) { ids = ids.map((x) => parseInt(x, 10)); if (!Site.current().lazy_load_categories) { @@ -508,9 +147,9 @@ Category.reopenClass({ Site.current().set("loadedCategoryIds", loadedCategoryIds); return categories; - }, + } - findBySlugAndParent(slug, parentCategory) { + static findBySlugAndParent(slug, parentCategory) { if (this.slugEncoded()) { slug = encodeURI(slug); } @@ -520,9 +159,9 @@ Category.reopenClass({ (category.parentCategory || null) === parentCategory ); }); - }, + } - findBySlugPath(slugPath) { + static findBySlugPath(slugPath) { let category = null; for (const slug of slugPath) { @@ -534,9 +173,9 @@ Category.reopenClass({ } return category; - }, + } - async asyncFindBySlugPathWithID(slugPathWithID) { + static async asyncFindBySlugPathWithID(slugPathWithID) { const result = await ajax("/categories/find", { data: { slug_path_with_id: slugPathWithID }, }); @@ -546,9 +185,9 @@ Category.reopenClass({ ); return categories[categories.length - 1]; - }, + } - findBySlugPathWithID(slugPathWithID) { + static findBySlugPathWithID(slugPathWithID) { let parts = slugPathWithID.split("/").filter(Boolean); // slugs found by star/glob pathing in ember do not automatically url decode - ensure that these are decoded if (this.slugEncoded()) { @@ -575,9 +214,9 @@ Category.reopenClass({ } return category; - }, + } - findBySlug(slug, parentSlug) { + static findBySlug(slug, parentSlug) { const categories = Category.list(); let category; @@ -615,34 +254,34 @@ Category.reopenClass({ } return category; - }, + } - fetchVisibleGroups(id) { + static fetchVisibleGroups(id) { return ajax(`/c/${id}/visible_groups.json`); - }, + } - reloadById(id) { + static reloadById(id) { return ajax(`/c/${id}/show.json`); - }, + } - reloadBySlugPath(slugPath) { + static reloadBySlugPath(slugPath) { return ajax(`/c/${slugPath}/find_by_slug.json`); - }, + } - reloadCategoryWithPermissions(params, store, site) { + static reloadCategoryWithPermissions(params, store, site) { return this.reloadBySlugPath(params.slug).then((result) => this._includePermissions(result.category, store, site) ); - }, + } - _includePermissions(category, store, site) { + static _includePermissions(category, store, site) { const record = store.createRecord("category", category); record.setupGroupsAndPermissions(); site.updateCategory(record); return record; - }, + } - search(term, opts) { + static search(term, opts) { let limit = 5; let parentCategoryId; @@ -713,9 +352,9 @@ Category.reopenClass({ } return data.sortBy("read_restricted"); - }, + } - async asyncSearch(term, opts) { + static async asyncSearch(term, opts) { opts ||= {}; const data = { @@ -735,7 +374,364 @@ Category.reopenClass({ return result["categories"].map((category) => Site.current().updateCategory(category) ); - }, + } + + permissions = null; + + @on("init") + setupGroupsAndPermissions() { + const availableGroups = this.available_groups; + if (!availableGroups) { + return; + } + this.set("availableGroups", availableGroups); + + const groupPermissions = this.group_permissions; + + if (groupPermissions) { + this.set( + "permissions", + groupPermissions.map((elem) => { + availableGroups.removeObject(elem.group_name); + return elem; + }) + ); + } + } + + @discourseComputed("required_tag_groups", "minimum_required_tags") + minimumRequiredTags() { + if (this.required_tag_groups?.length > 0) { + // it should require the max between the bare minimum set in the category and the sum of the min_count of the + // required_tag_groups + return Math.max( + this.required_tag_groups.reduce((sum, rtg) => sum + rtg.min_count, 0), + this.minimum_required_tags || 0 + ); + } else { + return this.minimum_required_tags > 0 ? this.minimum_required_tags : null; + } + } + + @discourseComputed + availablePermissions() { + return [ + PermissionType.create({ id: PermissionType.FULL }), + PermissionType.create({ id: PermissionType.CREATE_POST }), + PermissionType.create({ id: PermissionType.READONLY }), + ]; + } + + @discourseComputed("id") + searchContext(id) { + return { type: "category", id, category: this }; + } + + @discourseComputed("parentCategory.ancestors") + ancestors(parentAncestors) { + return [...(parentAncestors || []), this]; + } + + @discourseComputed("parentCategory.level") + level(parentLevel) { + if (!parentLevel) { + return parentLevel === 0 ? 1 : 0; + } else { + return parentLevel + 1; + } + } + + @discourseComputed("has_children", "subcategories") + isParent(hasChildren, subcategories) { + return hasChildren || (subcategories && subcategories.length > 0); + } + + @discourseComputed("subcategories") + isGrandParent(subcategories) { + return ( + subcategories && + subcategories.some( + (cat) => cat.subcategories && cat.subcategories.length > 0 + ) + ); + } + + @discourseComputed("notification_level") + isMuted(notificationLevel) { + return notificationLevel === NotificationLevels.MUTED; + } + + @discourseComputed("isMuted", "subcategories") + isHidden(isMuted, subcategories) { + if (!isMuted) { + return false; + } else if (!subcategories) { + return true; + } + + if (subcategories.some((cat) => !cat.isHidden)) { + return false; + } + + return true; + } + + @discourseComputed("isMuted", "subcategories") + hasMuted(isMuted, subcategories) { + if (isMuted) { + return true; + } else if (!subcategories) { + return false; + } + + if (subcategories.some((cat) => cat.hasMuted)) { + return true; + } + + return false; + } + + @discourseComputed("notification_level") + notificationLevelString(notificationLevel) { + // Get the key from the value + const notificationLevelString = Object.keys(NotificationLevels).find( + (key) => NotificationLevels[key] === notificationLevel + ); + if (notificationLevelString) { + return notificationLevelString.toLowerCase(); + } + } + + @discourseComputed("name") + path() { + return `/c/${Category.slugFor(this)}/${this.id}`; + } + + @discourseComputed("path") + url(path) { + return getURL(path); + } + + @discourseComputed + fullSlug() { + return Category.slugFor(this).replace(/\//g, "-"); + } + + @discourseComputed("name") + nameLower(name) { + return name.toLowerCase(); + } + + @discourseComputed("url") + unreadUrl(url) { + return `${url}/l/unread`; + } + + @discourseComputed("url") + newUrl(url) { + return `${url}/l/new`; + } + + @discourseComputed("color", "text_color") + style(color, textColor) { + return `background-color: #${color}; color: #${textColor}`; + } + + @discourseComputed("topic_count") + moreTopics(topicCount) { + return topicCount > (this.num_featured_topics || 2); + } + + @discourseComputed("topic_count", "subcategories.[]") + totalTopicCount(topicCount, subcategories) { + if (subcategories) { + subcategories.forEach((subcategory) => { + topicCount += subcategory.topic_count; + }); + } + return topicCount; + } + + @discourseComputed("default_slow_mode_seconds") + defaultSlowModeMinutes(seconds) { + return seconds ? seconds / 60 : null; + } + + @discourseComputed("notification_level") + isTracked(notificationLevel) { + return notificationLevel >= NotificationLevels.TRACKING; + } + + get unreadTopicsCount() { + return this.topicTrackingState.countUnread({ categoryId: this.id }); + } + + get newTopicsCount() { + return this.topicTrackingState.countNew({ categoryId: this.id }); + } + + save() { + const id = this.id; + const url = id ? `/categories/${id}` : "/categories"; + + return ajax(url, { + contentType: "application/json", + data: JSON.stringify({ + name: this.name, + slug: this.slug, + color: this.color, + text_color: this.text_color, + secure: this.secure, + permissions: this._permissionsForUpdate(), + auto_close_hours: this.auto_close_hours, + auto_close_based_on_last_post: this.get( + "auto_close_based_on_last_post" + ), + default_slow_mode_seconds: this.default_slow_mode_seconds, + position: this.position, + email_in: this.email_in, + email_in_allow_strangers: this.email_in_allow_strangers, + mailinglist_mirror: this.mailinglist_mirror, + parent_category_id: this.parent_category_id, + uploaded_logo_id: this.get("uploaded_logo.id"), + uploaded_logo_dark_id: this.get("uploaded_logo_dark.id"), + uploaded_background_id: this.get("uploaded_background.id"), + uploaded_background_dark_id: this.get("uploaded_background_dark.id"), + allow_badges: this.allow_badges, + category_setting_attributes: this.category_setting, + custom_fields: this.custom_fields, + topic_template: this.topic_template, + form_template_ids: this.form_template_ids, + all_topics_wiki: this.all_topics_wiki, + allow_unlimited_owner_edits_on_first_post: + this.allow_unlimited_owner_edits_on_first_post, + allowed_tags: this.allowed_tags, + allowed_tag_groups: this.allowed_tag_groups, + allow_global_tags: this.allow_global_tags, + required_tag_groups: this.required_tag_groups, + sort_order: this.sort_order, + sort_ascending: this.sort_ascending, + topic_featured_link_allowed: this.topic_featured_link_allowed, + show_subcategory_list: this.show_subcategory_list, + num_featured_topics: this.num_featured_topics, + default_view: this.default_view, + subcategory_list_style: this.subcategory_list_style, + default_top_period: this.default_top_period, + minimum_required_tags: this.minimum_required_tags, + navigate_to_first_post_after_read: this.get( + "navigate_to_first_post_after_read" + ), + search_priority: this.search_priority, + reviewable_by_group_name: this.reviewable_by_group_name, + read_only_banner: this.read_only_banner, + default_list_filter: this.default_list_filter, + }), + type: id ? "PUT" : "POST", + }); + } + + _permissionsForUpdate() { + const permissions = this.permissions; + let rval = {}; + if (permissions.length) { + permissions.forEach((p) => (rval[p.group_name] = p.permission_type)); + } else { + // empty permissions => staff-only access + rval[STAFF_GROUP_NAME] = PermissionType.FULL; + } + return rval; + } + + destroy() { + return ajax(`/categories/${this.id || this.slug}`, { + type: "DELETE", + }); + } + + addPermission(permission) { + this.permissions.addObject(permission); + this.availableGroups.removeObject(permission.group_name); + } + + removePermission(group_name) { + const permission = this.permissions.findBy("group_name", group_name); + if (permission) { + this.permissions.removeObject(permission); + this.availableGroups.addObject(group_name); + } + } + + updatePermission(group_name, type) { + this.permissions.forEach((p, i) => { + if (p.group_name === group_name) { + this.set(`permissions.${i}.permission_type`, type); + } + }); + } + + @discourseComputed("topics") + latestTopic(topics) { + if (topics && topics.length) { + return topics[0]; + } + } + + @discourseComputed("topics") + featuredTopics(topics) { + if (topics && topics.length) { + return topics.slice(0, this.num_featured_topics || 2); + } + } + + setNotification(notification_level) { + User.currentProp( + "muted_category_ids", + User.current().calculateMutedIds( + notification_level, + this.id, + "muted_category_ids" + ) + ); + + const url = `/category/${this.id}/notifications`; + return ajax(url, { data: { notification_level }, type: "POST" }).then( + (data) => { + User.current().set( + "indirectly_muted_category_ids", + data.indirectly_muted_category_ids + ); + this.set("notification_level", notification_level); + this.notifyPropertyChange("notification_level"); + } + ); + } + + @discourseComputed("id") + isUncategorizedCategory(id) { + return Category.isUncategorized(id); + } + + get canCreateTopic() { + return this.permission === PermissionType.FULL; + } + + get subcategoryWithCreateTopicPermission() { + return this.subcategories?.find( + (subcategory) => subcategory.canCreateTopic + ); + } +} + +let _uncategorized; + +const categoryMultiCache = new MultiCache(async (ids) => { + const result = await ajax("/categories/find", { data: { ids } }); + + return new Map( + result["categories"].map((category) => [category.id, category]) + ); }); -export default Category; +export function resetCategoryCache() { + categoryMultiCache.reset(); +} diff --git a/app/assets/javascripts/discourse/app/models/draft.js b/app/assets/javascripts/discourse/app/models/draft.js index 4275cab1630..02033f1fc74 100644 --- a/app/assets/javascripts/discourse/app/models/draft.js +++ b/app/assets/javascripts/discourse/app/models/draft.js @@ -1,26 +1,24 @@ import EmberObject from "@ember/object"; import { ajax } from "discourse/lib/ajax"; -const Draft = EmberObject.extend(); - -Draft.reopenClass({ - clear(key, sequence) { +export default class Draft extends EmberObject { + static clear(key, sequence) { return ajax(`/drafts/${key}.json`, { type: "DELETE", data: { draft_key: key, sequence }, }); - }, + } - get(key) { + static get(key) { return ajax(`/drafts/${key}.json`); - }, + } - getLocal(key, current) { + static getLocal(key, current) { // TODO: implement this return current; - }, + } - save(key, sequence, data, clientId, { forceSave = false } = {}) { + static save(key, sequence, data, clientId, { forceSave = false } = {}) { data = typeof data === "string" ? data : JSON.stringify(data); return ajax("/drafts.json", { type: "POST", @@ -33,7 +31,5 @@ Draft.reopenClass({ }, ignoreUnsent: false, }); - }, -}); - -export default Draft; + } +} diff --git a/app/assets/javascripts/discourse/app/models/group-history.js b/app/assets/javascripts/discourse/app/models/group-history.js index c13785717c7..c72c7cd6b24 100644 --- a/app/assets/javascripts/discourse/app/models/group-history.js +++ b/app/assets/javascripts/discourse/app/models/group-history.js @@ -2,9 +2,9 @@ import RestModel from "discourse/models/rest"; import discourseComputed from "discourse-common/utils/decorators"; import I18n from "discourse-i18n"; -export default RestModel.extend({ +export default class GroupHistory extends RestModel { @discourseComputed("action") actionTitle(action) { return I18n.t(`group_histories.actions.${action}`); - }, -}); + } +} diff --git a/app/assets/javascripts/discourse/app/models/group.js b/app/assets/javascripts/discourse/app/models/group.js index f1e476ec9ef..580efaad6b0 100644 --- a/app/assets/javascripts/discourse/app/models/group.js +++ b/app/assets/javascripts/discourse/app/models/group.js @@ -11,34 +11,56 @@ import Topic from "discourse/models/topic"; import User from "discourse/models/user"; import discourseComputed, { observes } from "discourse-common/utils/decorators"; -const Group = RestModel.extend({ - user_count: 0, - limit: null, - offset: null, +export default class Group extends RestModel { + static findAll(opts) { + return ajax("/groups/search.json", { data: opts }).then((groups) => + groups.map((g) => Group.create(g)) + ); + } - request_count: 0, - requestersLimit: null, - requestersOffset: null, + static loadMembers(name, opts) { + return ajax(`/groups/${name}/members.json`, { data: opts }); + } + static mentionable(name) { + return ajax(`/groups/${name}/mentionable`); + } + + static messageable(name) { + return ajax(`/groups/${name}/messageable`); + } + + static checkName(name) { + return ajax("/groups/check-name", { data: { group_name: name } }); + } + + user_count = 0; + limit = null; + offset = null; + request_count = 0; + requestersLimit = null; + requestersOffset = null; + + @equal("mentionable_level", 99) canEveryoneMention; init() { - this._super(...arguments); + super.init(...arguments); this.setProperties({ members: [], requesters: [] }); - }, + } @discourseComputed("automatic_membership_email_domains") emailDomains(value) { return isEmpty(value) ? "" : value; - }, + } @discourseComputed("associated_group_ids") associatedGroupIds(value) { return isEmpty(value) ? [] : value; - }, + } @discourseComputed("automatic") type(automatic) { return automatic ? "automatic" : "custom"; - }, + } async reloadMembers(params, refresh) { if (isEmpty(this.name) || !this.can_see_members) { @@ -73,7 +95,7 @@ const Group = RestModel.extend({ limit: response.meta.limit, offset: response.meta.offset, }); - }, + } findRequesters(params, refresh) { if (isEmpty(this.name) || !this.can_see_members) { @@ -103,7 +125,7 @@ const Group = RestModel.extend({ requestersOffset: result.meta.offset, }); }); - }, + } async removeOwner(member) { await ajax(`/admin/groups/${this.id}/owners.json`, { @@ -111,7 +133,7 @@ const Group = RestModel.extend({ data: { user_id: member.id }, }); await this.reloadMembers({}, true); - }, + } async removeMember(member, params) { await ajax(`/groups/${this.id}/members.json`, { @@ -119,7 +141,7 @@ const Group = RestModel.extend({ data: { user_id: member.id }, }); await this.reloadMembers(params, true); - }, + } async leave() { await ajax(`/groups/${this.id}/leave.json`, { @@ -127,7 +149,7 @@ const Group = RestModel.extend({ }); this.set("can_see_members", this.members_visibility_level < 2); await this.reloadMembers({}, true); - }, + } async addMembers(usernames, filter, notifyUsers, emails = []) { const response = await ajax(`/groups/${this.id}/members.json`, { @@ -139,14 +161,14 @@ const Group = RestModel.extend({ } else { await this.reloadMembers(); } - }, + } async join() { await ajax(`/groups/${this.id}/join.json`, { type: "PUT", }); await this.reloadMembers({}, true); - }, + } async addOwners(usernames, filter, notifyUsers) { const response = await ajax(`/groups/${this.id}/owners.json`, { @@ -159,44 +181,42 @@ const Group = RestModel.extend({ } else { await this.reloadMembers({}, true); } - }, + } _filterMembers(usernames) { return this.reloadMembers({ filter: usernames.join(",") }); - }, + } @discourseComputed("display_name", "name") displayName(groupDisplayName, name) { return groupDisplayName || name; - }, + } @discourseComputed("flair_bg_color") flairBackgroundHexColor(flairBgColor) { return flairBgColor ? flairBgColor.replace(new RegExp("[^0-9a-fA-F]", "g"), "") : null; - }, + } @discourseComputed("flair_color") flairHexColor(flairColor) { return flairColor ? flairColor.replace(new RegExp("[^0-9a-fA-F]", "g"), "") : null; - }, - - canEveryoneMention: equal("mentionable_level", 99), + } @discourseComputed("visibility_level") isPrivate(visibilityLevel) { return visibilityLevel > 1; - }, + } @observes("isPrivate", "canEveryoneMention") _updateAllowMembershipRequests() { if (this.isPrivate || !this.canEveryoneMention) { this.set("allow_membership_requests", false); } - }, + } @dependentKeyCompat get watchingCategories() { @@ -210,14 +230,14 @@ const Group = RestModel.extend({ } return Category.findByIds(this.get("watching_category_ids")); - }, + } set watchingCategories(categories) { this.set( "watching_category_ids", categories.map((c) => c.id) ); - }, + } @dependentKeyCompat get trackingCategories() { @@ -231,14 +251,14 @@ const Group = RestModel.extend({ } return Category.findByIds(this.get("tracking_category_ids")); - }, + } set trackingCategories(categories) { this.set( "tracking_category_ids", categories.map((c) => c.id) ); - }, + } @dependentKeyCompat get watchingFirstPostCategories() { @@ -252,14 +272,14 @@ const Group = RestModel.extend({ } return Category.findByIds(this.get("watching_first_post_category_ids")); - }, + } set watchingFirstPostCategories(categories) { this.set( "watching_first_post_category_ids", categories.map((c) => c.id) ); - }, + } @dependentKeyCompat get regularCategories() { @@ -273,14 +293,14 @@ const Group = RestModel.extend({ } return Category.findByIds(this.get("regular_category_ids")); - }, + } set regularCategories(categories) { this.set( "regular_category_ids", categories.map((c) => c.id) ); - }, + } @dependentKeyCompat get mutedCategories() { @@ -294,14 +314,14 @@ const Group = RestModel.extend({ } return Category.findByIds(this.get("muted_category_ids")); - }, + } set mutedCategories(categories) { this.set( "muted_category_ids", categories.map((c) => c.id) ); - }, + } asJSON() { const attrs = { @@ -382,7 +402,7 @@ const Group = RestModel.extend({ } return attrs; - }, + } async create() { const response = await ajax("/admin/groups", { @@ -397,21 +417,21 @@ const Group = RestModel.extend({ }); await this.reloadMembers(); - }, + } save(opts = {}) { return ajax(`/groups/${this.id}`, { type: "PUT", data: Object.assign({ group: this.asJSON() }, opts), }); - }, + } destroy() { if (!this.id) { return; } return ajax(`/admin/groups/${this.id}`, { type: "DELETE" }); - }, + } findLogs(offset, filters) { return ajax(`/groups/${this.name}/logs.json`, { @@ -422,7 +442,7 @@ const Group = RestModel.extend({ all_loaded: results["all_loaded"], }); }); - }, + } findPosts(opts) { opts = opts || {}; @@ -445,7 +465,7 @@ const Group = RestModel.extend({ return EmberObject.create(p); }); }); - }, + } setNotification(notification_level, userId) { this.set("group_user.notification_level", notification_level); @@ -453,38 +473,12 @@ const Group = RestModel.extend({ data: { notification_level, user_id: userId }, type: "POST", }); - }, + } requestMembership(reason) { return ajax(`/groups/${this.name}/request_membership.json`, { type: "POST", data: { reason }, }); - }, -}); - -Group.reopenClass({ - findAll(opts) { - return ajax("/groups/search.json", { data: opts }).then((groups) => - groups.map((g) => Group.create(g)) - ); - }, - - loadMembers(name, opts) { - return ajax(`/groups/${name}/members.json`, { data: opts }); - }, - - mentionable(name) { - return ajax(`/groups/${name}/mentionable`); - }, - - messageable(name) { - return ajax(`/groups/${name}/messageable`); - }, - - checkName(name) { - return ajax("/groups/check-name", { data: { group_name: name } }); - }, -}); - -export default Group; + } +} diff --git a/app/assets/javascripts/discourse/app/models/invite.js b/app/assets/javascripts/discourse/app/models/invite.js index 2382fe60350..00ffdf8b3d6 100644 --- a/app/assets/javascripts/discourse/app/models/invite.js +++ b/app/assets/javascripts/discourse/app/models/invite.js @@ -9,65 +9,16 @@ import Topic from "discourse/models/topic"; import User from "discourse/models/user"; import discourseComputed from "discourse-common/utils/decorators"; -const Invite = EmberObject.extend({ - save(data) { - const promise = this.id - ? ajax(`/invites/${this.id}`, { type: "PUT", data }) - : ajax("/invites", { type: "POST", data }); - - return promise.then((result) => this.setProperties(result)); - }, - - destroy() { - return ajax("/invites", { - type: "DELETE", - data: { id: this.id }, - }).then(() => this.set("destroyed", true)); - }, - - reinvite() { - return ajax("/invites/reinvite", { - type: "POST", - data: { email: this.email }, - }) - .then(() => this.set("reinvited", true)) - .catch(popupAjaxError); - }, - - @discourseComputed("invite_key") - shortKey(key) { - return key.slice(0, 4) + "..."; - }, - - @discourseComputed("groups") - groupIds(groups) { - return groups ? groups.map((group) => group.id) : []; - }, - - @discourseComputed("topics.firstObject") - topic(topicData) { - return topicData ? Topic.create(topicData) : null; - }, - - @discourseComputed("email", "domain") - emailOrDomain(email, domain) { - return email || domain; - }, - - topicId: alias("topics.firstObject.id"), - topicTitle: alias("topics.firstObject.title"), -}); - -Invite.reopenClass({ - create() { - const result = this._super.apply(this, arguments); +export default class Invite extends EmberObject { + static create() { + const result = super.create(...arguments); if (result.user) { result.user = User.create(result.user); } return result; - }, + } - findInvitedBy(user, filter, search, offset) { + static findInvitedBy(user, filter, search, offset) { if (!user) { Promise.resolve(); } @@ -87,15 +38,60 @@ Invite.reopenClass({ result.invites = result.invites.map((i) => Invite.create(i)); return EmberObject.create(result); }); - }, + } - reinviteAll() { + static reinviteAll() { return ajax("/invites/reinvite-all", { type: "POST" }); - }, + } - destroyAllExpired() { + static destroyAllExpired() { return ajax("/invites/destroy-all-expired", { type: "POST" }); - }, -}); + } -export default Invite; + @alias("topics.firstObject.id") topicId; + @alias("topics.firstObject.title") topicTitle; + + save(data) { + const promise = this.id + ? ajax(`/invites/${this.id}`, { type: "PUT", data }) + : ajax("/invites", { type: "POST", data }); + + return promise.then((result) => this.setProperties(result)); + } + + destroy() { + return ajax("/invites", { + type: "DELETE", + data: { id: this.id }, + }).then(() => this.set("destroyed", true)); + } + + reinvite() { + return ajax("/invites/reinvite", { + type: "POST", + data: { email: this.email }, + }) + .then(() => this.set("reinvited", true)) + .catch(popupAjaxError); + } + + @discourseComputed("invite_key") + shortKey(key) { + return key.slice(0, 4) + "..."; + } + + @discourseComputed("groups") + groupIds(groups) { + return groups ? groups.map((group) => group.id) : []; + } + + @discourseComputed("topics.firstObject") + topic(topicData) { + return topicData ? Topic.create(topicData) : null; + } + + @discourseComputed("email", "domain") + emailOrDomain(email, domain) { + return email || domain; + } +} diff --git a/app/assets/javascripts/discourse/app/models/live-post-counts.js b/app/assets/javascripts/discourse/app/models/live-post-counts.js index bac51ff8250..3dd8baf44c3 100644 --- a/app/assets/javascripts/discourse/app/models/live-post-counts.js +++ b/app/assets/javascripts/discourse/app/models/live-post-counts.js @@ -1,14 +1,10 @@ import EmberObject from "@ember/object"; import { ajax } from "discourse/lib/ajax"; -const LivePostCounts = EmberObject.extend({}); - -LivePostCounts.reopenClass({ - find() { +export default class LivePostCounts extends EmberObject { + static find() { return ajax("/about/live_post_counts.json").then((result) => LivePostCounts.create(result) ); - }, -}); - -export default LivePostCounts; + } +} diff --git a/app/assets/javascripts/discourse/app/models/login-method.js b/app/assets/javascripts/discourse/app/models/login-method.js index 84bd2d3a071..5da0cb4bc0d 100644 --- a/app/assets/javascripts/discourse/app/models/login-method.js +++ b/app/assets/javascripts/discourse/app/models/login-method.js @@ -7,11 +7,31 @@ import getURL from "discourse-common/lib/get-url"; import discourseComputed from "discourse-common/utils/decorators"; import I18n from "discourse-i18n"; -const LoginMethod = EmberObject.extend({ +export default class LoginMethod extends EmberObject { + static buildPostForm(url) { + // Login always happens in an anonymous context, with no CSRF token + // So we need to fetch it before sending a POST request + return updateCsrfToken().then(() => { + const form = document.createElement("form"); + form.setAttribute("style", "display:none;"); + form.setAttribute("method", "post"); + form.setAttribute("action", url); + + const input = document.createElement("input"); + input.setAttribute("name", "authenticity_token"); + input.setAttribute("value", Session.currentProp("csrfToken")); + form.appendChild(input); + + document.body.appendChild(form); + + return form; + }); + } + @discourseComputed title() { return this.title_override || I18n.t(`login.${this.name}.title`); - }, + } @discourseComputed screenReaderTitle() { @@ -19,12 +39,12 @@ const LoginMethod = EmberObject.extend({ this.title_override || I18n.t(`login.${this.name}.sr_title`, { defaultValue: this.title }) ); - }, + } @discourseComputed prettyName() { return this.pretty_name_override || I18n.t(`login.${this.name}.name`); - }, + } doLogin({ reconnect = false, signup = false, params = {} } = {}) { if (this.customLogin) { @@ -56,30 +76,8 @@ const LoginMethod = EmberObject.extend({ } return LoginMethod.buildPostForm(authUrl).then((form) => form.submit()); - }, -}); - -LoginMethod.reopenClass({ - buildPostForm(url) { - // Login always happens in an anonymous context, with no CSRF token - // So we need to fetch it before sending a POST request - return updateCsrfToken().then(() => { - const form = document.createElement("form"); - form.setAttribute("style", "display:none;"); - form.setAttribute("method", "post"); - form.setAttribute("action", url); - - const input = document.createElement("input"); - input.setAttribute("name", "authenticity_token"); - input.setAttribute("value", Session.currentProp("csrfToken")); - form.appendChild(input); - - document.body.appendChild(form); - - return form; - }); - }, -}); + } +} let methods; @@ -101,5 +99,3 @@ export function findAll() { export function clearAuthMethods() { methods = undefined; } - -export default LoginMethod; diff --git a/app/assets/javascripts/discourse/app/models/pending-post.js b/app/assets/javascripts/discourse/app/models/pending-post.js index be0ed7748f8..3df8c2439b0 100644 --- a/app/assets/javascripts/discourse/app/models/pending-post.js +++ b/app/assets/javascripts/discourse/app/models/pending-post.js @@ -5,27 +5,27 @@ import RestModel from "discourse/models/rest"; import discourseComputed from "discourse-common/utils/decorators"; import Category from "./category"; -const PendingPost = RestModel.extend({ - expandedExcerpt: null, - postUrl: reads("topic_url"), - truncated: false, +export default class PendingPost extends RestModel { + expandedExcerpt = null; + + @reads("topic_url") postUrl; + + truncated = false; init() { - this._super(...arguments); + super.init(...arguments); cook(this.raw_text).then((cooked) => { this.set("expandedExcerpt", cooked); }); - }, + } @discourseComputed("username") userUrl(username) { return userPath(username.toLowerCase()); - }, + } @discourseComputed("category_id") category() { return Category.findById(this.category_id); - }, -}); - -export default PendingPost; + } +} diff --git a/app/assets/javascripts/discourse/app/models/permission-type.js b/app/assets/javascripts/discourse/app/models/permission-type.js index 90a3727883d..e0fec7e90c2 100644 --- a/app/assets/javascripts/discourse/app/models/permission-type.js +++ b/app/assets/javascripts/discourse/app/models/permission-type.js @@ -6,20 +6,18 @@ export function buildPermissionDescription(id) { return I18n.t("permission_types." + PermissionType.DESCRIPTION_KEYS[id]); } -const PermissionType = EmberObject.extend({ +export default class PermissionType extends EmberObject { + static FULL = 1; + static CREATE_POST = 2; + static READONLY = 3; + static DESCRIPTION_KEYS = { + 1: "full", + 2: "create_post", + 3: "readonly", + }; + @discourseComputed("id") description(id) { return buildPermissionDescription(id); - }, -}); - -PermissionType.FULL = 1; -PermissionType.CREATE_POST = 2; -PermissionType.READONLY = 3; -PermissionType.DESCRIPTION_KEYS = { - 1: "full", - 2: "create_post", - 3: "readonly", -}; - -export default PermissionType; + } +} diff --git a/app/assets/javascripts/discourse/app/models/post-action-type.js b/app/assets/javascripts/discourse/app/models/post-action-type.js index 35a93b54c5d..2ab8b85969a 100644 --- a/app/assets/javascripts/discourse/app/models/post-action-type.js +++ b/app/assets/javascripts/discourse/app/models/post-action-type.js @@ -3,6 +3,6 @@ import RestModel from "discourse/models/rest"; export const MAX_MESSAGE_LENGTH = 500; -export default RestModel.extend({ - notCustomFlag: not("is_custom_flag"), -}); +export default class PostActionType extends RestModel { + @not("is_custom_flag") notCustomFlag; +} diff --git a/app/assets/javascripts/discourse/app/models/post-stream.js b/app/assets/javascripts/discourse/app/models/post-stream.js index ce70c6af701..174a013b443 100644 --- a/app/assets/javascripts/discourse/app/models/post-stream.js +++ b/app/assets/javascripts/discourse/app/models/post-stream.js @@ -33,23 +33,33 @@ export function resetLastEditNotificationClick() { _lastEditNotificationClick = null; } -export default RestModel.extend({ - _identityMap: null, - posts: null, - stream: null, - userFilters: null, - loaded: null, - loadingAbove: null, - loadingBelow: null, - loadingFilter: null, - loadingNearPost: null, - stagingPost: null, - postsWithPlaceholders: null, - timelineLookup: null, - filterRepliesToPostNumber: null, - filterUpwardsPostID: null, - filter: null, - topicSummary: null, +export default class PostStream extends RestModel { + posts = null; + stream = null; + userFilters = null; + loaded = null; + loadingAbove = null; + loadingBelow = null; + loadingFilter = null; + loadingNearPost = null; + stagingPost = null; + postsWithPlaceholders = null; + timelineLookup = null; + filterRepliesToPostNumber = null; + filterUpwardsPostID = null; + filter = null; + topicSummary = null; + lastId = null; + + @or("loadingAbove", "loadingBelow", "loadingFilter", "stagingPost") loading; + @not("loading") notLoading; + @equal("filter", "summary") summary; + @and("notLoading", "hasPosts", "lastPostNotLoaded") canAppendMore; + @and("notLoading", "hasPosts", "firstPostNotLoaded") canPrependMore; + @not("firstPostPresent") firstPostNotLoaded; + @not("loadedAllPosts") lastPostNotLoaded; + + _identityMap = null; init() { this._identityMap = {}; @@ -75,12 +85,7 @@ export default RestModel.extend({ timelineLookup: [], topicSummary: new TopicSummary(), }); - }, - - loading: or("loadingAbove", "loadingBelow", "loadingFilter", "stagingPost"), - notLoading: not("loading"), - - summary: equal("filter", "summary"), + } @discourseComputed( "isMegaTopic", @@ -89,20 +94,17 @@ export default RestModel.extend({ ) filteredPostsCount(isMegaTopic, streamLength, topicHighestPostNumber) { return isMegaTopic ? topicHighestPostNumber : streamLength; - }, + } @discourseComputed("posts.[]") hasPosts() { return this.get("posts.length") > 0; - }, + } @discourseComputed("hasPosts", "filteredPostsCount") hasLoadedData(hasPosts, filteredPostsCount) { return hasPosts && filteredPostsCount > 0; - }, - - canAppendMore: and("notLoading", "hasPosts", "lastPostNotLoaded"), - canPrependMore: and("notLoading", "hasPosts", "firstPostNotLoaded"), + } @discourseComputed("hasLoadedData", "posts.[]") firstPostPresent(hasLoadedData) { @@ -111,16 +113,12 @@ export default RestModel.extend({ } return !!this.posts.findBy("post_number", 1); - }, - - firstPostNotLoaded: not("firstPostPresent"), - - lastId: null, + } @discourseComputed("isMegaTopic", "stream.lastObject", "lastId") lastPostId(isMegaTopic, streamLastId, lastId) { return isMegaTopic ? lastId : streamLastId; - }, + } @discourseComputed("hasLoadedData", "lastPostId", "posts.@each.id") loadedAllPosts(hasLoadedData, lastPostId) { @@ -132,9 +130,7 @@ export default RestModel.extend({ } return !!this.posts.findBy("id", lastPostId); - }, - - lastPostNotLoaded: not("loadedAllPosts"), + } /** Returns a JS Object of current stream filter options. It should match the query @@ -167,7 +163,7 @@ export default RestModel.extend({ } return result; - }, + } @discourseComputed("streamFilters.[]", "topic.posts_count", "posts.length") hasNoFilters() { @@ -176,7 +172,7 @@ export default RestModel.extend({ streamFilters && (streamFilters.filter === "summary" || streamFilters.username_filters) ); - }, + } /** Returns the window of posts above the current set in the stream, bound to the top of the stream. @@ -206,7 +202,7 @@ export default RestModel.extend({ startIndex = 0; } return stream.slice(startIndex, firstIndex); - }, + } /** Returns the window of posts below the current set in the stream, bound by the bottom of the @@ -234,7 +230,7 @@ export default RestModel.extend({ lastIndex + 1, lastIndex + this.get("topic.chunk_size") + 1 ); - }, + } cancelFilter() { this.setProperties({ @@ -244,7 +240,7 @@ export default RestModel.extend({ mixedHiddenPosts: false, filter: null, }); - }, + } refreshAndJumpToSecondVisible() { return this.refresh({}).then(() => { @@ -252,20 +248,20 @@ export default RestModel.extend({ DiscourseURL.jumpToPost(this.posts[1].get("post_number")); } }); - }, + } showTopReplies() { this.cancelFilter(); this.set("filter", "summary"); return this.refreshAndJumpToSecondVisible(); - }, + } // Filter the stream to a particular user. filterParticipant(username) { this.cancelFilter(); this.userFilters.addObject(username); return this.refreshAndJumpToSecondVisible(); - }, + } filterReplies(postNumber, postId) { this.cancelFilter(); @@ -295,7 +291,7 @@ export default RestModel.extend({ highlightPost(postNumber); }); }); - }, + } filterUpwards(postID) { this.cancelFilter(); @@ -316,7 +312,7 @@ export default RestModel.extend({ }); } }); - }, + } /** Loads a new set of posts into the stream. If you provide a `nearPost` option and the post @@ -378,7 +374,7 @@ export default RestModel.extend({ .finally(() => { this.set("loadingNearPost", null); }); - }, + } // Fill in a gap of posts before a particular post fillGapBefore(post, gap) { @@ -420,7 +416,7 @@ export default RestModel.extend({ } } return Promise.resolve(); - }, + } // Fill in a gap of posts after a particular post fillGapAfter(post, gap) { @@ -436,7 +432,7 @@ export default RestModel.extend({ }); } return Promise.resolve(); - }, + } gapExpanded() { this.appEvents.trigger("post-stream:refresh"); @@ -446,7 +442,7 @@ export default RestModel.extend({ if (this.streamFilters && this.streamFilters.replies_to_post_number) { this.set("streamFilters.mixedHiddenPosts", true); } - }, + } // Appends the next window of posts to the stream. Call it when scrolling downwards. appendMore() { @@ -493,7 +489,7 @@ export default RestModel.extend({ this.set("loadingBelow", false); }); } - }, + } // Prepend the previous window of posts to the stream. Call it when scrolling upwards. prependMore() { @@ -535,7 +531,7 @@ export default RestModel.extend({ this.set("loadingAbove", false); }); } - }, + } /** Stage a post for insertion in the stream. It should be rendered right away under the @@ -573,7 +569,7 @@ export default RestModel.extend({ } return "offScreen"; - }, + } // Commit the post we staged. Call this after a save succeeds. commitPost(post) { @@ -587,7 +583,7 @@ export default RestModel.extend({ this.stream.removeObject(-1); this._identityMap[-1] = null; this.set("stagingPost", false); - }, + } /** Undo a post we've staged in the stream. Remove it from being rendered and revert the @@ -607,7 +603,7 @@ export default RestModel.extend({ }); // TODO unfudge reply count on parent post - }, + } prependPost(post) { this._initUserModels(post); @@ -618,7 +614,7 @@ export default RestModel.extend({ } return post; - }, + } appendPost(post) { this._initUserModels(post); @@ -639,7 +635,7 @@ export default RestModel.extend({ } } return post; - }, + } removePosts(posts) { if (isEmpty(posts)) { @@ -655,12 +651,12 @@ export default RestModel.extend({ allPosts.removeObjects(posts); postIds.forEach((id) => delete identityMap[id]); }); - }, + } // Returns a post from the identity map if it's been inserted. findLoadedPost(id) { return this._identityMap[id]; - }, + } loadPostByPostNumber(postNumber) { const url = `/posts/by_number/${this.get("topic.id")}/${postNumber}`; @@ -669,7 +665,7 @@ export default RestModel.extend({ return ajax(url).then((post) => { return this.storePost(store.createRecord("post", post)); }); - }, + } loadNearestPostToDate(date) { const url = `/posts/by-date/${this.get("topic.id")}/${date}`; @@ -678,7 +674,7 @@ export default RestModel.extend({ return ajax(url).then((post) => { return this.storePost(store.createRecord("post", post)); }); - }, + } loadPost(postId) { const url = "/posts/" + postId; @@ -692,7 +688,7 @@ export default RestModel.extend({ return this.storePost(store.createRecord("post", p)); }); - }, + } /* mainly for backwards compatibility with plugins, used in quick messages plugin * TODO: remove July 2022 @@ -705,7 +701,7 @@ export default RestModel.extend({ } ); return this.triggerNewPostsInStream([postId], opts); - }, + } /** Finds and adds posts to the stream by id. Typically this would happen if we receive a message @@ -768,7 +764,7 @@ export default RestModel.extend({ } return resolved; - }, + } triggerRecoveredPost(postId) { const existing = this._identityMap[postId]; @@ -814,7 +810,7 @@ export default RestModel.extend({ } }); } - }, + } triggerDeletedPost(postId) { const existing = this._identityMap[postId]; @@ -832,13 +828,13 @@ export default RestModel.extend({ }); } return Promise.resolve(); - }, + } triggerDestroyedPost(postId) { const existing = this._identityMap[postId]; this.removePosts([existing]); return Promise.resolve(); - }, + } triggerChangedPost(postId, updatedAt, opts) { opts = opts || {}; @@ -861,7 +857,7 @@ export default RestModel.extend({ }); } return resolved; - }, + } triggerLikedPost(postId, likesCount, userID, eventType) { const resolved = Promise.resolve(); @@ -873,7 +869,7 @@ export default RestModel.extend({ } return resolved; - }, + } triggerReadPost(postId, readersCount) { const resolved = Promise.resolve(); @@ -886,7 +882,7 @@ export default RestModel.extend({ }); return resolved; - }, + } triggerChangedTopicStats() { if (this.firstPostNotLoaded) { @@ -897,7 +893,7 @@ export default RestModel.extend({ const firstPost = this.posts.findBy("post_number", 1); return firstPost.id; }); - }, + } postForPostNumber(postNumber) { if (!this.hasPosts) { @@ -907,7 +903,7 @@ export default RestModel.extend({ return this.posts.find((p) => { return p.get("post_number") === postNumber; }); - }, + } /** Returns the closest post given a postNumber that may not exist in the stream. @@ -935,12 +931,12 @@ export default RestModel.extend({ }); return closest; - }, + } // Get the index of a post in the stream. (Use this for the topic progress bar.) progressIndexOfPost(post) { return this.progressIndexOfPostId(post); - }, + } // Get the index in the stream of a post id. (Use this for the topic progress bar.) progressIndexOfPostId(post) { @@ -952,7 +948,7 @@ export default RestModel.extend({ const index = this.stream.indexOf(postId); return index + 1; } - }, + } /** Returns the closest post number given a postNumber that may not exist in the stream. @@ -982,7 +978,7 @@ export default RestModel.extend({ }); return closest; - }, + } closestDaysAgoFor(postNumber) { const timelineLookup = this.timelineLookup || []; @@ -1007,7 +1003,7 @@ export default RestModel.extend({ if (val) { return val[1]; } - }, + } // Find a postId for a postNumber, respecting gaps findPostIdForPostNumber(postNumber) { @@ -1037,7 +1033,7 @@ export default RestModel.extend({ } sum++; } - }, + } updateFromJson(postStreamData) { const posts = this.posts; @@ -1057,7 +1053,7 @@ export default RestModel.extend({ // Update our attributes this.setProperties(postStreamData); } - }, + } /** Stores a post in our identity map, and sets up the references it needs to @@ -1094,7 +1090,7 @@ export default RestModel.extend({ this._identityMap[post.get("id")] = post; } return post; - }, + } fetchNextWindow(postNumber, asc, callback) { let includeSuggested = !this.get("topic.suggested_topics"); @@ -1124,7 +1120,7 @@ export default RestModel.extend({ }); } }); - }, + } findPostsByIds(postIds, opts) { const identityMap = this._identityMap; @@ -1134,7 +1130,7 @@ export default RestModel.extend({ return this.loadIntoIdentityMap(unloaded, opts).then(() => { return postIds.map((p) => identityMap[p]).compact(); }); - }, + } loadIntoIdentityMap(postIds, opts) { if (isEmpty(postIds)) { @@ -1164,7 +1160,7 @@ export default RestModel.extend({ posts.forEach((p) => this.storePost(store.createRecord("post", p))); } }); - }, + } backfillExcerpts(streamPosition) { this._excerpts = this._excerpts || []; @@ -1211,7 +1207,7 @@ export default RestModel.extend({ }); return this._excerpts.loading; - }, + } excerpt(streamPosition) { if (this.isMegaTopic) { @@ -1234,11 +1230,11 @@ export default RestModel.extend({ }) .catch((e) => reject(e)); }); - }, + } indexOf(post) { return this.stream.indexOf(post.get("id")); - }, + } // Handles an error loading a topic based on a HTTP status code. Updates // the text to the correct values. @@ -1259,19 +1255,19 @@ export default RestModel.extend({ topic.set("errorMessage", I18n.t("topic.server_error.description")); topic.set("noRetry", error.jqXHR.status === 403); } - }, + } collapseSummary() { this.topicSummary.collapse(); - }, + } showSummary(currentUser) { this.topicSummary.generateSummary(currentUser, this.get("topic.id")); - }, + } processSummaryUpdate(update) { this.topicSummary.processUpdate(update); - }, + } _initUserModels(post) { post.user = User.create({ @@ -1286,7 +1282,7 @@ export default RestModel.extend({ if (post.mentioned_users) { post.mentioned_users = post.mentioned_users.map((u) => User.create(u)); } - }, + } _checkIfShouldShowRevisions() { if (_lastEditNotificationClick) { @@ -1306,7 +1302,7 @@ export default RestModel.extend({ }); } } - }, + } _setSuggestedTopics(result) { if (!result.suggested_topics) { @@ -1321,5 +1317,5 @@ export default RestModel.extend({ if (this.topic.isPrivateMessage) { this.pmTopicTrackingState.startTracking(); } - }, -}); + } +} diff --git a/app/assets/javascripts/discourse/app/models/published-page.js b/app/assets/javascripts/discourse/app/models/published-page.js index cdd36bb9de3..099dd2cd02f 100644 --- a/app/assets/javascripts/discourse/app/models/published-page.js +++ b/app/assets/javascripts/discourse/app/models/published-page.js @@ -2,8 +2,9 @@ import { computed } from "@ember/object"; import RestModel from "discourse/models/rest"; import { getAbsoluteURL } from "discourse-common/lib/get-url"; -export default RestModel.extend({ - url: computed("slug", function () { +export default class PublishedPage extends RestModel { + @computed("slug") + get url() { return getAbsoluteURL(`/pub/${this.slug}`); - }), -}); + } +} diff --git a/app/assets/javascripts/discourse/app/models/result-set.js b/app/assets/javascripts/discourse/app/models/result-set.js index 644e75638bc..a3847eaf27f 100644 --- a/app/assets/javascripts/discourse/app/models/result-set.js +++ b/app/assets/javascripts/discourse/app/models/result-set.js @@ -2,24 +2,23 @@ import ArrayProxy from "@ember/array/proxy"; import { Promise } from "rsvp"; import discourseComputed from "discourse-common/utils/decorators"; -export default ArrayProxy.extend({ - loading: false, - loadingMore: false, - totalRows: 0, - refreshing: false, - - content: null, - loadMoreUrl: null, - refreshUrl: null, - findArgs: null, - store: null, - __type: null, - resultSetMeta: null, +export default class ResultSet extends ArrayProxy { + loading = false; + loadingMore = false; + totalRows = 0; + refreshing = false; + content = null; + loadMoreUrl = null; + refreshUrl = null; + findArgs = null; + store = null; + resultSetMeta = null; + __type = null; @discourseComputed("totalRows", "length") canLoadMore(totalRows, length) { return length < totalRows; - }, + } loadMore() { const loadMoreUrl = this.loadMoreUrl; @@ -37,7 +36,7 @@ export default ArrayProxy.extend({ } return Promise.resolve(); - }, + } refresh() { if (this.refreshing) { @@ -53,5 +52,5 @@ export default ArrayProxy.extend({ return this.store .refreshResults(this, this.__type, refreshUrl) .finally(() => this.set("refreshing", false)); - }, -}); + } +} diff --git a/app/assets/javascripts/discourse/app/models/reviewable-history.js b/app/assets/javascripts/discourse/app/models/reviewable-history.js index bb143b712c9..0fac81c219f 100644 --- a/app/assets/javascripts/discourse/app/models/reviewable-history.js +++ b/app/assets/javascripts/discourse/app/models/reviewable-history.js @@ -5,6 +5,6 @@ export const CREATED = 0; export const TRANSITIONED_TO = 1; export const EDITED = 2; -export default RestModel.extend({ - created: equal("reviewable_history_type", CREATED), -}); +export default class ReviewableHistory extends RestModel { + @equal("reviewable_history_type", CREATED) created; +} diff --git a/app/assets/javascripts/discourse/app/models/reviewable.js b/app/assets/javascripts/discourse/app/models/reviewable.js index a83f6d1d4f8..4c348d5d280 100644 --- a/app/assets/javascripts/discourse/app/models/reviewable.js +++ b/app/assets/javascripts/discourse/app/models/reviewable.js @@ -12,7 +12,13 @@ export const REJECTED = 2; export const IGNORED = 3; export const DELETED = 4; -const Reviewable = RestModel.extend({ +export default class Reviewable extends RestModel { + static munge(json) { + // ensure we are not overriding category computed property + delete json.category; + return json; + } + @discourseComputed("type", "topic") resolvedType(type, topic) { // Display "Queued Topic" if the post will create a topic @@ -21,26 +27,26 @@ const Reviewable = RestModel.extend({ } return type; - }, + } @discourseComputed("resolvedType") humanType(resolvedType) { return I18n.t(`review.types.${underscore(resolvedType)}.title`, { defaultValue: "", }); - }, + } @discourseComputed("humanType") humanTypeCssClass(humanType) { return "-" + dasherize(humanType); - }, + } @discourseComputed("resolvedType") humanNoun(resolvedType) { return I18n.t(`review.types.${underscore(resolvedType)}.noun`, { defaultValue: "reviewable", }); - }, + } @discourseComputed("humanNoun") flaggedReviewableContextQuestion(humanNoun) { @@ -66,12 +72,12 @@ const Reviewable = RestModel.extend({ reviewable_human_score_types: listOfQuestions, reviewable_type: humanNoun, }); - }, + } @discourseComputed("category_id") category() { return Category.findById(this.category_id); - }, + } update(updates) { // If no changes, do nothing @@ -92,15 +98,5 @@ const Reviewable = RestModel.extend({ this.setProperties(updated); }); - }, -}); - -Reviewable.reopenClass({ - munge(json) { - // ensure we are not overriding category computed property - delete json.category; - return json; - }, -}); - -export default Reviewable; + } +} diff --git a/app/assets/javascripts/discourse/app/models/session.js b/app/assets/javascripts/discourse/app/models/session.js index f47f0da4a4e..d684efabe89 100644 --- a/app/assets/javascripts/discourse/app/models/session.js +++ b/app/assets/javascripts/discourse/app/models/session.js @@ -3,14 +3,10 @@ import RestModel from "discourse/models/rest"; // A data model representing current session data. You can put transient // data here you might want later. It is not stored or serialized anywhere. -const Session = RestModel.extend({ - hasFocus: null, +export default class Session extends RestModel.extend().reopenClass(Singleton) { + hasFocus = null; init() { this.set("highestSeenByTopic", {}); - }, -}); - -Session.reopenClass(Singleton); - -export default Session; + } +} diff --git a/app/assets/javascripts/discourse/app/models/site.js b/app/assets/javascripts/discourse/app/models/site.js index 5b2c525269e..a6ae21dc1d5 100644 --- a/app/assets/javascripts/discourse/app/models/site.js +++ b/app/assets/javascripts/discourse/app/models/site.js @@ -13,149 +13,17 @@ import deprecated from "discourse-common/lib/deprecated"; import { getOwnerWithFallback } from "discourse-common/lib/get-owner"; import discourseComputed from "discourse-common/utils/decorators"; -const Site = RestModel.extend({ - isReadOnly: alias("is_readonly"), - - init() { - this._super(...arguments); - - this.topicCountDesc = ["topic_count:desc"]; - this.categories = this.categories || []; - }, - - @discourseComputed("notification_types") - notificationLookup(notificationTypes) { - const result = []; - Object.keys(notificationTypes).forEach( - (k) => (result[notificationTypes[k]] = k) - ); - return result; - }, - - @discourseComputed("post_action_types.[]") - flagTypes() { - const postActionTypes = this.post_action_types; - if (!postActionTypes) { - return []; - } - return postActionTypes.filterBy("is_flag", true); - }, - - categoriesByCount: sort("categories", "topicCountDesc"), - - collectUserFields(fields) { - fields = fields || {}; - - let siteFields = this.user_fields; - - if (!isEmpty(siteFields)) { - return siteFields.map((f) => { - let value = fields ? fields[f.id.toString()] : null; - value = value || htmlSafe("—"); - return { name: f.name, value }; - }); - } - return []; - }, - - // Sort subcategories under parents - @discourseComputed("categoriesByCount", "categories.[]") - sortedCategories(categories) { - return Category.sortCategories(categories); - }, - - // Returns it in the correct order, by setting - @discourseComputed("categories.[]") - categoriesList(categories) { - return this.siteSettings.fixed_category_positions - ? categories - : this.sortedCategories; - }, - - @discourseComputed("categories.[]", "categories.@each.notification_level") - trackedCategoriesList(categories) { - const trackedCategories = []; - - for (const category of categories) { - if (category.isTracked) { - if ( - this.siteSettings.allow_uncategorized_topics || - !category.isUncategorizedCategory - ) { - trackedCategories.push(category); - } - } - } - - return trackedCategories; - }, - - postActionTypeById(id) { - return this.get("postActionByIdLookup.action" + id); - }, - - topicFlagTypeById(id) { - return this.get("topicFlagByIdLookup.action" + id); - }, - - removeCategory(id) { - const categories = this.categories; - const existingCategory = categories.findBy("id", id); - if (existingCategory) { - categories.removeObject(existingCategory); - delete this.categoriesById.categoryId; - } - }, - - updateCategory(newCategory) { - const categories = this.categories; - const categoryId = get(newCategory, "id"); - const existingCategory = categories.findBy("id", categoryId); - - // Don't update null permissions - if (newCategory.permission === null) { - delete newCategory.permission; - } - - if (existingCategory) { - existingCategory.setProperties(newCategory); - return existingCategory; - } else { - // TODO insert in right order? - newCategory = this.store.createRecord("category", newCategory); - categories.pushObject(newCategory); - this.categoriesById[categoryId] = newCategory; - newCategory.set( - "parentCategory", - this.categoriesById[newCategory.parent_category_id] - ); - newCategory.set( - "subcategories", - this.categories.filterBy("parent_category_id", categoryId) - ); - if (newCategory.parentCategory) { - if (!newCategory.parentCategory.subcategories) { - newCategory.parentCategory.set("subcategories", []); - } - newCategory.parentCategory.subcategories.pushObject(newCategory); - } - return newCategory; - } - }, -}); - -Site.reopenClass(Singleton, { - // The current singleton will retrieve its attributes from the `PreloadStore`. - createCurrent() { +export default class Site extends RestModel.extend().reopenClass(Singleton) { + static createCurrent() { const store = getOwnerWithFallback(this).lookup("service:store"); const siteAttributes = PreloadStore.get("site"); siteAttributes["isReadOnly"] = PreloadStore.get("isReadOnly"); siteAttributes["isStaffWritesOnly"] = PreloadStore.get("isStaffWritesOnly"); return store.createRecord("site", siteAttributes); - }, + } - create() { - const result = this._super.apply(this, arguments); + static create() { + const result = super.create.apply(this, arguments); const store = result.store; if (result.categories) { @@ -233,8 +101,136 @@ Site.reopenClass(Singleton, { } return result; - }, -}); + } + + @alias("is_readonly") isReadOnly; + + @sort("categories", "topicCountDesc") categoriesByCount; + init() { + super.init(...arguments); + + this.topicCountDesc = ["topic_count:desc"]; + this.categories = this.categories || []; + } + + @discourseComputed("notification_types") + notificationLookup(notificationTypes) { + const result = []; + Object.keys(notificationTypes).forEach( + (k) => (result[notificationTypes[k]] = k) + ); + return result; + } + + @discourseComputed("post_action_types.[]") + flagTypes() { + const postActionTypes = this.post_action_types; + if (!postActionTypes) { + return []; + } + return postActionTypes.filterBy("is_flag", true); + } + + collectUserFields(fields) { + fields = fields || {}; + + let siteFields = this.user_fields; + + if (!isEmpty(siteFields)) { + return siteFields.map((f) => { + let value = fields ? fields[f.id.toString()] : null; + value = value || htmlSafe("—"); + return { name: f.name, value }; + }); + } + return []; + } + + // Sort subcategories under parents + @discourseComputed("categoriesByCount", "categories.[]") + sortedCategories(categories) { + return Category.sortCategories(categories); + } + + // Returns it in the correct order, by setting + @discourseComputed("categories.[]") + categoriesList(categories) { + return this.siteSettings.fixed_category_positions + ? categories + : this.sortedCategories; + } + + @discourseComputed("categories.[]", "categories.@each.notification_level") + trackedCategoriesList(categories) { + const trackedCategories = []; + + for (const category of categories) { + if (category.isTracked) { + if ( + this.siteSettings.allow_uncategorized_topics || + !category.isUncategorizedCategory + ) { + trackedCategories.push(category); + } + } + } + + return trackedCategories; + } + + postActionTypeById(id) { + return this.get("postActionByIdLookup.action" + id); + } + + topicFlagTypeById(id) { + return this.get("topicFlagByIdLookup.action" + id); + } + + removeCategory(id) { + const categories = this.categories; + const existingCategory = categories.findBy("id", id); + if (existingCategory) { + categories.removeObject(existingCategory); + delete this.categoriesById.categoryId; + } + } + + updateCategory(newCategory) { + const categories = this.categories; + const categoryId = get(newCategory, "id"); + const existingCategory = categories.findBy("id", categoryId); + + // Don't update null permissions + if (newCategory.permission === null) { + delete newCategory.permission; + } + + if (existingCategory) { + existingCategory.setProperties(newCategory); + return existingCategory; + } else { + // TODO insert in right order? + newCategory = this.store.createRecord("category", newCategory); + categories.pushObject(newCategory); + this.categoriesById[categoryId] = newCategory; + newCategory.set( + "parentCategory", + this.categoriesById[newCategory.parent_category_id] + ); + newCategory.set( + "subcategories", + this.categories.filterBy("parent_category_id", categoryId) + ); + if (newCategory.parentCategory) { + if (!newCategory.parentCategory.subcategories) { + newCategory.parentCategory.set("subcategories", []); + } + newCategory.parentCategory.subcategories.pushObject(newCategory); + } + return newCategory; + } + } +} if (typeof Discourse !== "undefined") { let warned = false; @@ -252,5 +248,3 @@ if (typeof Discourse !== "undefined") { }, }); } - -export default Site; diff --git a/app/assets/javascripts/discourse/app/models/static-page.js b/app/assets/javascripts/discourse/app/models/static-page.js index 230cba23044..60644c690b9 100644 --- a/app/assets/javascripts/discourse/app/models/static-page.js +++ b/app/assets/javascripts/discourse/app/models/static-page.js @@ -3,10 +3,8 @@ import $ from "jquery"; import { Promise } from "rsvp"; import { ajax } from "discourse/lib/ajax"; -const StaticPage = EmberObject.extend(); - -StaticPage.reopenClass({ - find(path) { +export default class StaticPage extends EmberObject { + static find(path) { return new Promise((resolve) => { // Models shouldn't really be doing Ajax request, but this is a huge speed boost if we // preload content. @@ -23,7 +21,5 @@ StaticPage.reopenClass({ ); } }); - }, -}); - -export default StaticPage; + } +} diff --git a/app/assets/javascripts/discourse/app/models/tag-group.js b/app/assets/javascripts/discourse/app/models/tag-group.js index e34e5cae711..64d6412a918 100644 --- a/app/assets/javascripts/discourse/app/models/tag-group.js +++ b/app/assets/javascripts/discourse/app/models/tag-group.js @@ -2,7 +2,7 @@ import PermissionType from "discourse/models/permission-type"; import RestModel from "discourse/models/rest"; import discourseComputed from "discourse-common/utils/decorators"; -export default RestModel.extend({ +export default class TagGroup extends RestModel { @discourseComputed("permissions") permissionName(permissions) { if (!permissions) { @@ -16,5 +16,5 @@ export default RestModel.extend({ } else { return "private"; } - }, -}); + } +} diff --git a/app/assets/javascripts/discourse/app/models/tag.js b/app/assets/javascripts/discourse/app/models/tag.js index 0de4528e07b..9d9e72443b6 100644 --- a/app/assets/javascripts/discourse/app/models/tag.js +++ b/app/assets/javascripts/discourse/app/models/tag.js @@ -2,16 +2,16 @@ import { readOnly } from "@ember/object/computed"; import RestModel from "discourse/models/rest"; import discourseComputed from "discourse-common/utils/decorators"; -export default RestModel.extend({ - pmOnly: readOnly("pm_only"), +export default class Tag extends RestModel { + @readOnly("pm_only") pmOnly; @discourseComputed("count", "pm_count") totalCount(count, pmCount) { return pmCount ? count + pmCount : count; - }, + } @discourseComputed("id") searchContext(id) { return { type: "tag", id, tag: this, name: id }; - }, -}); + } +} diff --git a/app/assets/javascripts/discourse/app/models/topic-details.js b/app/assets/javascripts/discourse/app/models/topic-details.js index 2d7096e21e4..509c906042b 100644 --- a/app/assets/javascripts/discourse/app/models/topic-details.js +++ b/app/assets/javascripts/discourse/app/models/topic-details.js @@ -8,10 +8,10 @@ import RestModel from "discourse/models/rest"; When showing topics in lists and such this information should not be required. **/ -const TopicDetails = RestModel.extend({ - store: service(), +export default class TopicDetails extends RestModel { + @service store; - loaded: false, + loaded = false; updateFromJson(details) { const topic = this.topic; @@ -31,7 +31,7 @@ const TopicDetails = RestModel.extend({ this.setProperties(details); this.set("loaded", true); - }, + } updateNotifications(level) { return ajax(`/t/${this.get("topic.id")}/notifications`, { @@ -43,7 +43,7 @@ const TopicDetails = RestModel.extend({ notifications_reason_id: null, }); }); - }, + } removeAllowedGroup(group) { const groups = this.allowed_groups; @@ -55,7 +55,7 @@ const TopicDetails = RestModel.extend({ }).then(() => { groups.removeObject(groups.findBy("name", name)); }); - }, + } removeAllowedUser(user) { const users = this.allowed_users; @@ -67,7 +67,5 @@ const TopicDetails = RestModel.extend({ }).then(() => { users.removeObject(users.findBy("username", username)); }); - }, -}); - -export default TopicDetails; + } +} diff --git a/app/assets/javascripts/discourse/app/models/topic-list.js b/app/assets/javascripts/discourse/app/models/topic-list.js index 93ca8a55f9d..7a54c8ac3a7 100644 --- a/app/assets/javascripts/discourse/app/models/topic-list.js +++ b/app/assets/javascripts/discourse/app/models/topic-list.js @@ -40,9 +40,88 @@ function displayCategoryInList(site, category) { return true; } -const TopicList = RestModel.extend({ - session: service(), - canLoadMore: notEmpty("more_topics_url"), +export default class TopicList extends RestModel { + static topicsFrom(store, result, opts) { + if (!result) { + return; + } + + opts = opts || {}; + let listKey = opts.listKey || "topics"; + + // Stitch together our side loaded data + + const users = extractByKey(result.users, User); + const groups = extractByKey(result.primary_groups, EmberObject); + + if (result.topic_list.categories) { + result.topic_list.categories.forEach((c) => { + Site.current().updateCategory(c); + }); + } + + return result.topic_list[listKey].map((t) => { + t.posters.forEach((p) => { + p.user = users[p.user_id]; + p.extraClasses = p.extras; + if (p.primary_group_id) { + p.primary_group = groups[p.primary_group_id]; + if (p.primary_group) { + p.extraClasses = `${p.extraClasses || ""} group-${ + p.primary_group.name + }`; + } + } + }); + + if (t.participants) { + t.participants.forEach((p) => (p.user = users[p.user_id])); + } + + return store.createRecord("topic", t); + }); + } + + static munge(json, store) { + json.inserted = json.inserted || []; + json.can_create_topic = json.topic_list.can_create_topic; + json.more_topics_url = json.topic_list.more_topics_url; + json.for_period = json.topic_list.for_period; + json.loaded = true; + json.per_page = json.topic_list.per_page; + json.topics = this.topicsFrom(store, json); + + if (json.topic_list.shared_drafts) { + json.sharedDrafts = this.topicsFrom(store, json, { + listKey: "shared_drafts", + }); + } + + return json; + } + + static find(filter, params) { + deprecated( + `TopicList.find is deprecated. Use \`findFiltered("topicList")\` on the \`store\` service instead.`, + { + id: "topic-list-find", + since: "3.1.0.beta5", + dropFrom: "3.2.0.beta1", + } + ); + + const store = getOwnerWithFallback(this).lookup("service:store"); + return store.findFiltered("topicList", { filter, params }); + } + + // hide the category when it has no children + static hideUniformCategory(list, category) { + list.set("hideCategory", !displayCategoryInList(list.site, category)); + } + + @service session; + + @notEmpty("more_topics_url") canLoadMore; forEachNew(topics, callback) { const topicIds = new Set(); @@ -53,7 +132,7 @@ const TopicList = RestModel.extend({ callback(topic); } }); - }, + } updateSortParams(order, ascending) { let params = { ...(this.params || {}) }; @@ -67,7 +146,7 @@ const TopicList = RestModel.extend({ } this.set("params", params); - }, + } updateNewListSubsetParam(subset) { let params = { ...(this.params || {}) }; @@ -79,7 +158,7 @@ const TopicList = RestModel.extend({ } this.set("params", params); - }, + } loadMore() { if (this.loadingMore) { @@ -128,7 +207,7 @@ const TopicList = RestModel.extend({ // Return a promise indicating no more results return Promise.resolve(); } - }, + } // loads topics with these ids "before" the current topics loadBefore(topic_ids, storeInSession) { @@ -152,87 +231,5 @@ const TopicList = RestModel.extend({ this.session.set("topicList", this); } }); - }, -}); - -TopicList.reopenClass({ - topicsFrom(store, result, opts) { - if (!result) { - return; - } - - opts = opts || {}; - let listKey = opts.listKey || "topics"; - - // Stitch together our side loaded data - - const users = extractByKey(result.users, User); - const groups = extractByKey(result.primary_groups, EmberObject); - - if (result.topic_list.categories) { - result.topic_list.categories.forEach((c) => { - Site.current().updateCategory(c); - }); - } - - return result.topic_list[listKey].map((t) => { - t.posters.forEach((p) => { - p.user = users[p.user_id]; - p.extraClasses = p.extras; - if (p.primary_group_id) { - p.primary_group = groups[p.primary_group_id]; - if (p.primary_group) { - p.extraClasses = `${p.extraClasses || ""} group-${ - p.primary_group.name - }`; - } - } - }); - - if (t.participants) { - t.participants.forEach((p) => (p.user = users[p.user_id])); - } - - return store.createRecord("topic", t); - }); - }, - - munge(json, store) { - json.inserted = json.inserted || []; - json.can_create_topic = json.topic_list.can_create_topic; - json.more_topics_url = json.topic_list.more_topics_url; - json.for_period = json.topic_list.for_period; - json.loaded = true; - json.per_page = json.topic_list.per_page; - json.topics = this.topicsFrom(store, json); - - if (json.topic_list.shared_drafts) { - json.sharedDrafts = this.topicsFrom(store, json, { - listKey: "shared_drafts", - }); - } - - return json; - }, - - find(filter, params) { - deprecated( - `TopicList.find is deprecated. Use \`findFiltered("topicList")\` on the \`store\` service instead.`, - { - id: "topic-list-find", - since: "3.1.0.beta5", - dropFrom: "3.2.0.beta1", - } - ); - - const store = getOwnerWithFallback(this).lookup("service:store"); - return store.findFiltered("topicList", { filter, params }); - }, - - // hide the category when it has no children - hideUniformCategory(list, category) { - list.set("hideCategory", !displayCategoryInList(list.site, category)); - }, -}); - -export default TopicList; + } +} diff --git a/app/assets/javascripts/discourse/app/models/topic-timer.js b/app/assets/javascripts/discourse/app/models/topic-timer.js index 735ed43c82f..618219f36f5 100644 --- a/app/assets/javascripts/discourse/app/models/topic-timer.js +++ b/app/assets/javascripts/discourse/app/models/topic-timer.js @@ -1,10 +1,8 @@ import { ajax } from "discourse/lib/ajax"; import RestModel from "discourse/models/rest"; -const TopicTimer = RestModel.extend({}); - -TopicTimer.reopenClass({ - update( +export default class TopicTimer extends RestModel { + static update( topicId, time, basedOnLastPost, @@ -32,7 +30,5 @@ TopicTimer.reopenClass({ type: "POST", data, }); - }, -}); - -export default TopicTimer; + } +} diff --git a/app/assets/javascripts/discourse/app/models/topic-tracking-state.js b/app/assets/javascripts/discourse/app/models/topic-tracking-state.js index d05e53fc793..7701f9968cd 100644 --- a/app/assets/javascripts/discourse/app/models/topic-tracking-state.js +++ b/app/assets/javascripts/discourse/app/models/topic-tracking-state.js @@ -47,19 +47,19 @@ function hasMutedTags(topicTags, mutedTags, siteSettings) { ); } -const TopicTrackingState = EmberObject.extend({ - messageCount: 0, +export default class TopicTrackingState extends EmberObject { + messageCount = 0; init() { - this._super(...arguments); + super.init(...arguments); this.states = new Map(); this.stateChangeCallbacks = {}; this._trackedTopicLimit = 4000; - }, + } willDestroy() { - this._super(...arguments); + super.willDestroy(...arguments); this.messageBus.unsubscribe("/latest", this._processChannelPayload); @@ -75,7 +75,7 @@ const TopicTrackingState = EmberObject.extend({ this.messageBus.unsubscribe("/delete", this.onDeleteMessage); this.messageBus.unsubscribe("/recover", this.onRecoverMessage); this.messageBus.unsubscribe("/destroy", this.onDestroyMessage); - }, + } /** * Subscribe to MessageBus channels which are used for publishing changes @@ -134,19 +134,19 @@ const TopicTrackingState = EmberObject.extend({ this.onDestroyMessage, meta["/destroy"] ?? messageBusDefaultNewMessageId ); - }, + } @bind onDeleteMessage(msg) { this.modifyStateProp(msg, "deleted", true); this.incrementMessageCount(); - }, + } @bind onRecoverMessage(msg) { this.modifyStateProp(msg, "deleted", false); this.incrementMessageCount(); - }, + } @bind onDestroyMessage(msg) { @@ -159,15 +159,15 @@ const TopicTrackingState = EmberObject.extend({ ) { DiscourseURL.redirectTo("/"); } - }, + } mutedTopics() { return (this.currentUser && this.currentUser.muted_topics) || []; - }, + } unmutedTopics() { return (this.currentUser && this.currentUser.unmuted_topics) || []; - }, + } trackMutedOrUnmutedTopic(data) { let topics, key; @@ -183,7 +183,7 @@ const TopicTrackingState = EmberObject.extend({ createdAt: Date.now(), }); this.currentUser && this.currentUser.set(key, topics); - }, + } pruneOldMutedAndUnmutedTopics() { const now = Date.now(); @@ -196,15 +196,15 @@ const TopicTrackingState = EmberObject.extend({ this.currentUser && this.currentUser.set("muted_topics", mutedTopics) && this.currentUser.set("unmuted_topics", unmutedTopics); - }, + } isMutedTopic(topicId) { return !!this.mutedTopics().findBy("topicId", topicId); - }, + } isUnmutedTopic(topicId) { return !!this.unmutedTopics().findBy("topicId", topicId); - }, + } /** * Updates the topic's last_read_post_number to the highestSeen post @@ -233,7 +233,7 @@ const TopicTrackingState = EmberObject.extend({ this.modifyStateProp(topicId, "last_read_post_number", highestSeen); this.incrementMessageCount(); } - }, + } /** * Used to count incoming topics which will be displayed in a message @@ -321,7 +321,7 @@ const TopicTrackingState = EmberObject.extend({ // hasIncoming relies on this count this.set("incomingCount", this.newIncoming.length); - }, + } /** * Resets the number of incoming topics to 0 and flushes the new topics @@ -333,7 +333,7 @@ const TopicTrackingState = EmberObject.extend({ resetTracking() { this.newIncoming = []; this.set("incomingCount", 0); - }, + } /** * Track how many new topics came for the specified filter. @@ -375,7 +375,7 @@ const TopicTrackingState = EmberObject.extend({ this.set("filterTag", tag); this.set("filter", filter); this.set("incomingCount", 0); - }, + } /** * Used to determine whether to show the message at the top of the topic list @@ -386,7 +386,7 @@ const TopicTrackingState = EmberObject.extend({ @discourseComputed("incomingCount") hasIncoming(incomingCount) { return incomingCount && incomingCount > 0; - }, + } /** * Removes the topic ID provided from the tracker state. @@ -400,7 +400,7 @@ const TopicTrackingState = EmberObject.extend({ if (this.states.delete(this._stateKey(topicId))) { this._afterStateChange(); } - }, + } /** * Removes multiple topics from the state at once, and increments @@ -415,7 +415,7 @@ const TopicTrackingState = EmberObject.extend({ topicIds.forEach((topicId) => this.removeTopic(topicId)); this.incrementMessageCount(); this._afterStateChange(); - }, + } /** * If we have a cached topic list, we can update it from our tracking information @@ -466,7 +466,7 @@ const TopicTrackingState = EmberObject.extend({ }); } }); - }, + } /** * Uses the provided topic list to apply changes to the in-memory topic @@ -509,25 +509,25 @@ const TopicTrackingState = EmberObject.extend({ } this.incrementMessageCount(); - }, + } incrementMessageCount() { this.incrementProperty("messageCount"); - }, + } _generateCallbackId() { return Math.random().toString(12).slice(2, 11); - }, + } onStateChange(cb) { let callbackId = this._generateCallbackId(); this.stateChangeCallbacks[callbackId] = cb; return callbackId; - }, + } offStateChange(callbackId) { delete this.stateChangeCallbacks[callbackId]; - }, + } getSubCategoryIds(categoryId) { const result = [categoryId]; @@ -542,7 +542,7 @@ const TopicTrackingState = EmberObject.extend({ } return new Set(result); - }, + } countCategoryByState({ type, @@ -606,7 +606,7 @@ const TopicTrackingState = EmberObject.extend({ return true; }).length; - }, + } countNew({ categoryId, tagId, noSubcategories, customFilterFn } = {}) { return this.countCategoryByState({ @@ -616,7 +616,7 @@ const TopicTrackingState = EmberObject.extend({ noSubcategories, customFilterFn, }); - }, + } countUnread({ categoryId, tagId, noSubcategories, customFilterFn } = {}) { return this.countCategoryByState({ @@ -626,7 +626,7 @@ const TopicTrackingState = EmberObject.extend({ noSubcategories, customFilterFn, }); - }, + } countNewAndUnread({ categoryId, @@ -641,7 +641,7 @@ const TopicTrackingState = EmberObject.extend({ noSubcategories, customFilterFn, }); - }, + } /** * Calls the provided callback for each of the currently tracked topics @@ -657,7 +657,7 @@ const TopicTrackingState = EmberObject.extend({ this._trackedTopics(opts).forEach((trackedTopic) => { fn(trackedTopic.topic, trackedTopic.newTopic, trackedTopic.unreadTopic); }); - }, + } /** * Using the array of tags provided, tallies up all topics via forEachTracked @@ -710,7 +710,7 @@ const TopicTrackingState = EmberObject.extend({ ); return counts; - }, + } countCategory(category_id, tagId) { let sum = 0; @@ -728,7 +728,7 @@ const TopicTrackingState = EmberObject.extend({ } } return sum; - }, + } lookupCount({ type, category, tagId, noSubcategories, customFilterFn } = {}) { if (type === "latest") { @@ -782,7 +782,7 @@ const TopicTrackingState = EmberObject.extend({ return this.countCategory(categoryId, tagId); } } - }, + } loadStates(data) { if (!data || data.length === 0) { @@ -796,7 +796,7 @@ const TopicTrackingState = EmberObject.extend({ if (modified) { this._afterStateChange(); } - }, + } _setState({ topic, data, skipAfterStateChange }) { const stateKey = this._stateKey(topic); @@ -813,11 +813,11 @@ const TopicTrackingState = EmberObject.extend({ } else { return false; } - }, + } modifyState(topic, data) { this._setState({ topic, data }); - }, + } modifyStateProp(topic, prop, data) { const state = this.findState(topic); @@ -825,11 +825,11 @@ const TopicTrackingState = EmberObject.extend({ state[prop] = data; this._afterStateChange(); } - }, + } findState(topicOrId) { return this.states.get(this._stateKey(topicOrId)); - }, + } /* * private @@ -860,7 +860,7 @@ const TopicTrackingState = EmberObject.extend({ } } } - }, + } // this updates the topic in the state to match the // topic from the list (e.g. updates category, highest read post @@ -908,7 +908,7 @@ const TopicTrackingState = EmberObject.extend({ } return newState; - }, + } // this stops sync of tracking state when list is filtered, in the past this // would cause the tracking state to become inconsistent. @@ -925,7 +925,7 @@ const TopicTrackingState = EmberObject.extend({ } return shouldCompensate; - }, + } // any state that is not in the provided list must be updated // based on the filter selected so we do not have any incorrect @@ -957,7 +957,7 @@ const TopicTrackingState = EmberObject.extend({ this.modifyState(topicKey, newState); } - }, + } // processes the data sent via messageBus, called by establishChannels @bind @@ -1054,7 +1054,7 @@ const TopicTrackingState = EmberObject.extend({ this.incrementMessageCount(); } } - }, + } _dismissNewTopics(topicIds) { topicIds.forEach((topicId) => { @@ -1062,7 +1062,7 @@ const TopicTrackingState = EmberObject.extend({ }); this.incrementMessageCount(); - }, + } _dismissNewPosts(topicIds) { topicIds.forEach((topicId) => { @@ -1078,13 +1078,13 @@ const TopicTrackingState = EmberObject.extend({ }); this.incrementMessageCount(); - }, + } _addIncoming(topicId) { if (!this.newIncoming.includes(topicId)) { this.newIncoming.push(topicId); } - }, + } _trackedTopics(opts = {}) { return Array.from(this.states.values()) @@ -1096,7 +1096,7 @@ const TopicTrackingState = EmberObject.extend({ } }) .compact(); - }, + } _stateKey(topicOrId) { if (typeof topicOrId === "number") { @@ -1106,17 +1106,17 @@ const TopicTrackingState = EmberObject.extend({ } else { return `t${topicOrId.topic_id}`; } - }, + } _afterStateChange() { this.notifyPropertyChange("states"); Object.values(this.stateChangeCallbacks).forEach((cb) => cb()); - }, + } _maxStateSizeReached() { return this.states.size >= this._trackedTopicLimit; - }, -}); + } +} export function startTracking(tracking) { PreloadStore.getAndRemove("topicTrackingStates").then((data) => @@ -1127,5 +1127,3 @@ export function startTracking(tracking) { tracking.establishChannels(meta) ); } - -export default TopicTrackingState; diff --git a/app/assets/javascripts/discourse/app/models/user-action-group.js b/app/assets/javascripts/discourse/app/models/user-action-group.js index 0f48a5555f9..80db4430042 100644 --- a/app/assets/javascripts/discourse/app/models/user-action-group.js +++ b/app/assets/javascripts/discourse/app/models/user-action-group.js @@ -1,10 +1,10 @@ import EmberObject from "@ember/object"; -export default EmberObject.extend({ +export default class UserActionGroup extends EmberObject { push(item) { if (!this.items) { this.items = []; } return this.items.push(item); - }, -}); + } +} diff --git a/app/assets/javascripts/discourse/app/models/user-action-stat.js b/app/assets/javascripts/discourse/app/models/user-action-stat.js index eeb94bf8f65..dbe2efcd3c9 100644 --- a/app/assets/javascripts/discourse/app/models/user-action-stat.js +++ b/app/assets/javascripts/discourse/app/models/user-action-stat.js @@ -3,16 +3,16 @@ import RestModel from "discourse/models/rest"; import UserAction from "discourse/models/user-action"; import discourseComputed from "discourse-common/utils/decorators"; -export default RestModel.extend({ +export default class UserActionStat extends RestModel { + @i18n("action_type", "user_action_groups.%@") description; + @discourseComputed("action_type") isPM(actionType) { return ( actionType === UserAction.TYPES.messages_sent || actionType === UserAction.TYPES.messages_received ); - }, - - description: i18n("action_type", "user_action_groups.%@"), + } @discourseComputed("action_type") isResponse(actionType) { @@ -20,5 +20,5 @@ export default RestModel.extend({ actionType === UserAction.TYPES.replies || actionType === UserAction.TYPES.quotes ); - }, -}); + } +} diff --git a/app/assets/javascripts/discourse/app/models/user-action.js b/app/assets/javascripts/discourse/app/models/user-action.js index 01e898751a5..aa194418313 100644 --- a/app/assets/javascripts/discourse/app/models/user-action.js +++ b/app/assets/javascripts/discourse/app/models/user-action.js @@ -26,153 +26,28 @@ Object.keys(UserActionTypes).forEach( (k) => (InvertedActionTypes[k] = UserActionTypes[k]) ); -const UserAction = RestModel.extend({ - @discourseComputed("category_id") - category() { - return Category.findById(this.category_id); - }, +export default class UserAction extends RestModel { + static TYPES = UserActionTypes; - @discourseComputed("action_type") - descriptionKey(action) { - if (action === null || UserAction.TO_SHOW.includes(action)) { - if (this.isPM) { - return this.sameUser ? "sent_by_you" : "sent_by_user"; - } else { - return this.sameUser ? "posted_by_you" : "posted_by_user"; - } - } + static TYPES_INVERTED = InvertedActionTypes; - if (this.topicType) { - return this.sameUser ? "you_posted_topic" : "user_posted_topic"; - } + static TO_COLLAPSE = [ + UserActionTypes.likes_given, + UserActionTypes.likes_received, + UserActionTypes.edits, + UserActionTypes.bookmarks, + ]; - if (this.postReplyType) { - if (this.reply_to_post_number) { - return this.sameUser ? "you_replied_to_post" : "user_replied_to_post"; - } else { - return this.sameUser ? "you_replied_to_topic" : "user_replied_to_topic"; - } - } + static TO_SHOW = [ + UserActionTypes.likes_given, + UserActionTypes.likes_received, + UserActionTypes.edits, + UserActionTypes.bookmarks, + UserActionTypes.messages_sent, + UserActionTypes.messages_received, + ]; - if (this.mentionType) { - if (this.sameUser) { - return "you_mentioned_user"; - } else { - return this.targetUser ? "user_mentioned_you" : "user_mentioned_user"; - } - } - }, - - @discourseComputed("username") - sameUser(username) { - return username === User.currentProp("username"); - }, - - @discourseComputed("target_username") - targetUser(targetUsername) { - return targetUsername === User.currentProp("username"); - }, - - presentName: or("name", "username"), - targetDisplayName: or("target_name", "target_username"), - actingDisplayName: or("acting_name", "acting_username"), - - @discourseComputed("target_username") - targetUserUrl(username) { - return userPath(username); - }, - - @discourseComputed("username") - usernameLower(username) { - return username.toLowerCase(); - }, - - @discourseComputed("usernameLower") - userUrl(usernameLower) { - return userPath(usernameLower); - }, - - @discourseComputed() - postUrl() { - return postUrl(this.slug, this.topic_id, this.post_number); - }, - - @discourseComputed() - replyUrl() { - return postUrl(this.slug, this.topic_id, this.reply_to_post_number); - }, - - replyType: equal("action_type", UserActionTypes.replies), - postType: equal("action_type", UserActionTypes.posts), - topicType: equal("action_type", UserActionTypes.topics), - bookmarkType: equal("action_type", UserActionTypes.bookmarks), - messageSentType: equal("action_type", UserActionTypes.messages_sent), - messageReceivedType: equal("action_type", UserActionTypes.messages_received), - mentionType: equal("action_type", UserActionTypes.mentions), - isPM: or("messageSentType", "messageReceivedType"), - postReplyType: or("postType", "replyType"), - - addChild(action) { - let groups = this.childGroups; - if (!groups) { - groups = { - likes: UserActionGroup.create({ icon: "heart" }), - stars: UserActionGroup.create({ icon: "star" }), - edits: UserActionGroup.create({ icon: "pencil-alt" }), - bookmarks: UserActionGroup.create({ icon: "bookmark" }), - }; - } - this.set("childGroups", groups); - - const bucket = (function () { - switch (action.action_type) { - case UserActionTypes.likes_given: - case UserActionTypes.likes_received: - return "likes"; - case UserActionTypes.edits: - return "edits"; - case UserActionTypes.bookmarks: - return "bookmarks"; - } - })(); - const current = groups[bucket]; - if (current) { - current.push(action); - } - }, - - @discourseComputed( - "childGroups", - "childGroups.likes.items", - "childGroups.likes.items.[]", - "childGroups.stars.items", - "childGroups.stars.items.[]", - "childGroups.edits.items", - "childGroups.edits.items.[]", - "childGroups.bookmarks.items", - "childGroups.bookmarks.items.[]" - ) - children() { - const g = this.childGroups; - let rval = []; - if (g) { - rval = [g.likes, g.stars, g.edits, g.bookmarks].filter(function (i) { - return i.get("items") && i.get("items").length > 0; - }); - } - return rval; - }, - - switchToActing() { - this.setProperties({ - username: this.acting_username, - name: this.actingDisplayName, - }); - }, -}); - -UserAction.reopenClass({ - collapseStream(stream) { + static collapseStream(stream) { const uniq = {}; const collapsed = []; let pos = 0; @@ -204,26 +79,147 @@ UserAction.reopenClass({ } }); return collapsed; - }, + } - TYPES: UserActionTypes, - TYPES_INVERTED: InvertedActionTypes, + @or("name", "username") presentName; + @or("target_name", "target_username") targetDisplayName; + @or("acting_name", "acting_username") actingDisplayName; + @equal("action_type", UserActionTypes.replies) replyType; + @equal("action_type", UserActionTypes.posts) postType; + @equal("action_type", UserActionTypes.topics) topicType; + @equal("action_type", UserActionTypes.bookmarks) bookmarkType; + @equal("action_type", UserActionTypes.messages_sent) messageSentType; + @equal("action_type", UserActionTypes.messages_received) messageReceivedType; + @equal("action_type", UserActionTypes.mentions) mentionType; + @or("messageSentType", "messageReceivedType") isPM; + @or("postType", "replyType") postReplyType; - TO_COLLAPSE: [ - UserActionTypes.likes_given, - UserActionTypes.likes_received, - UserActionTypes.edits, - UserActionTypes.bookmarks, - ], + @discourseComputed("category_id") + category() { + return Category.findById(this.category_id); + } - TO_SHOW: [ - UserActionTypes.likes_given, - UserActionTypes.likes_received, - UserActionTypes.edits, - UserActionTypes.bookmarks, - UserActionTypes.messages_sent, - UserActionTypes.messages_received, - ], -}); + @discourseComputed("action_type") + descriptionKey(action) { + if (action === null || UserAction.TO_SHOW.includes(action)) { + if (this.isPM) { + return this.sameUser ? "sent_by_you" : "sent_by_user"; + } else { + return this.sameUser ? "posted_by_you" : "posted_by_user"; + } + } -export default UserAction; + if (this.topicType) { + return this.sameUser ? "you_posted_topic" : "user_posted_topic"; + } + + if (this.postReplyType) { + if (this.reply_to_post_number) { + return this.sameUser ? "you_replied_to_post" : "user_replied_to_post"; + } else { + return this.sameUser ? "you_replied_to_topic" : "user_replied_to_topic"; + } + } + + if (this.mentionType) { + if (this.sameUser) { + return "you_mentioned_user"; + } else { + return this.targetUser ? "user_mentioned_you" : "user_mentioned_user"; + } + } + } + + @discourseComputed("username") + sameUser(username) { + return username === User.currentProp("username"); + } + + @discourseComputed("target_username") + targetUser(targetUsername) { + return targetUsername === User.currentProp("username"); + } + + @discourseComputed("target_username") + targetUserUrl(username) { + return userPath(username); + } + + @discourseComputed("username") + usernameLower(username) { + return username.toLowerCase(); + } + + @discourseComputed("usernameLower") + userUrl(usernameLower) { + return userPath(usernameLower); + } + + @discourseComputed() + postUrl() { + return postUrl(this.slug, this.topic_id, this.post_number); + } + + @discourseComputed() + replyUrl() { + return postUrl(this.slug, this.topic_id, this.reply_to_post_number); + } + + addChild(action) { + let groups = this.childGroups; + if (!groups) { + groups = { + likes: UserActionGroup.create({ icon: "heart" }), + stars: UserActionGroup.create({ icon: "star" }), + edits: UserActionGroup.create({ icon: "pencil-alt" }), + bookmarks: UserActionGroup.create({ icon: "bookmark" }), + }; + } + this.set("childGroups", groups); + + const bucket = (function () { + switch (action.action_type) { + case UserActionTypes.likes_given: + case UserActionTypes.likes_received: + return "likes"; + case UserActionTypes.edits: + return "edits"; + case UserActionTypes.bookmarks: + return "bookmarks"; + } + })(); + const current = groups[bucket]; + if (current) { + current.push(action); + } + } + + @discourseComputed( + "childGroups", + "childGroups.likes.items", + "childGroups.likes.items.[]", + "childGroups.stars.items", + "childGroups.stars.items.[]", + "childGroups.edits.items", + "childGroups.edits.items.[]", + "childGroups.bookmarks.items", + "childGroups.bookmarks.items.[]" + ) + children() { + const g = this.childGroups; + let rval = []; + if (g) { + rval = [g.likes, g.stars, g.edits, g.bookmarks].filter(function (i) { + return i.get("items") && i.get("items").length > 0; + }); + } + return rval; + } + + switchToActing() { + this.setProperties({ + username: this.acting_username, + name: this.actingDisplayName, + }); + } +} diff --git a/app/assets/javascripts/discourse/app/models/user-badge.js b/app/assets/javascripts/discourse/app/models/user-badge.js index 5ee048e949b..122121a5a0e 100644 --- a/app/assets/javascripts/discourse/app/models/user-badge.js +++ b/app/assets/javascripts/discourse/app/models/user-badge.js @@ -7,34 +7,8 @@ import Topic from "discourse/models/topic"; import User from "discourse/models/user"; import discourseComputed from "discourse-common/utils/decorators"; -const UserBadge = EmberObject.extend({ - @discourseComputed - postUrl() { - if (this.topic_title) { - return "/t/-/" + this.topic_id + "/" + this.post_number; - } - }, // avoid the extra bindings for now - - revoke() { - return ajax("/user_badges/" + this.id, { - type: "DELETE", - }); - }, - - favorite() { - this.toggleProperty("is_favorite"); - return ajax(`/user_badges/${this.id}/toggle_favorite`, { - type: "PUT", - }).catch((e) => { - // something went wrong, switch the UI back: - this.toggleProperty("is_favorite"); - popupAjaxError(e); - }); - }, -}); - -UserBadge.reopenClass({ - createFromJson(json) { +export default class UserBadge extends EmberObject { + static createFromJson(json) { // Create User objects. if (json.users === undefined) { json.users = []; @@ -105,7 +79,7 @@ UserBadge.reopenClass({ } return userBadges; } - }, + } /** Find all badges for a given username. @@ -115,7 +89,7 @@ UserBadge.reopenClass({ @param {Object} options @returns {Promise} a promise that resolves to an array of `UserBadge`. **/ - findByUsername(username, options) { + static findByUsername(username, options) { if (!username) { return Promise.resolve([]); } @@ -126,7 +100,7 @@ UserBadge.reopenClass({ return ajax(url).then(function (json) { return UserBadge.createFromJson(json); }); - }, + } /** Find all badge grants for a given badge ID. @@ -135,7 +109,7 @@ UserBadge.reopenClass({ @param {String} badgeId @returns {Promise} a promise that resolves to an array of `UserBadge`. **/ - findByBadgeId(badgeId, options) { + static findByBadgeId(badgeId, options) { if (!options) { options = {}; } @@ -146,7 +120,7 @@ UserBadge.reopenClass({ }).then(function (json) { return UserBadge.createFromJson(json); }); - }, + } /** Grant the badge having id `badgeId` to the user identified by `username`. @@ -156,7 +130,7 @@ UserBadge.reopenClass({ @param {String} username username of the user to be granted the badge. @returns {Promise} a promise that resolves to an instance of `UserBadge`. **/ - grant(badgeId, username, reason) { + static grant(badgeId, username, reason) { return ajax("/user_badges", { type: "POST", data: { @@ -167,7 +141,29 @@ UserBadge.reopenClass({ }).then(function (json) { return UserBadge.createFromJson(json); }); - }, -}); + } -export default UserBadge; + @discourseComputed + postUrl() { + if (this.topic_title) { + return "/t/-/" + this.topic_id + "/" + this.post_number; + } + } // avoid the extra bindings for now + + revoke() { + return ajax("/user_badges/" + this.id, { + type: "DELETE", + }); + } + + favorite() { + this.toggleProperty("is_favorite"); + return ajax(`/user_badges/${this.id}/toggle_favorite`, { + type: "PUT", + }).catch((e) => { + // something went wrong, switch the UI back: + this.toggleProperty("is_favorite"); + popupAjaxError(e); + }); + } +} diff --git a/app/assets/javascripts/discourse/app/models/user-draft.js b/app/assets/javascripts/discourse/app/models/user-draft.js index 732d996454d..43056b1f0de 100644 --- a/app/assets/javascripts/discourse/app/models/user-draft.js +++ b/app/assets/javascripts/discourse/app/models/user-draft.js @@ -9,16 +9,16 @@ import User from "discourse/models/user"; import discourseComputed from "discourse-common/utils/decorators"; import I18n from "discourse-i18n"; -export default RestModel.extend({ +export default class UserDraft extends RestModel { @discourseComputed("draft_username") editableDraft(draftUsername) { return draftUsername === User.currentProp("username"); - }, + } @discourseComputed("username_lower") userUrl(usernameLower) { return userPath(usernameLower); - }, + } @discourseComputed("topic_id") postUrl(topicId) { @@ -27,7 +27,7 @@ export default RestModel.extend({ } return postUrl(this.slug, this.topic_id, this.post_number); - }, + } @discourseComputed("draft_key") draftType(draftKey) { @@ -39,5 +39,5 @@ export default RestModel.extend({ default: return false; } - }, -}); + } +} diff --git a/app/assets/javascripts/discourse/app/models/user-drafts-stream.js b/app/assets/javascripts/discourse/app/models/user-drafts-stream.js index f9d460e98e3..3fa13694b0c 100644 --- a/app/assets/javascripts/discourse/app/models/user-drafts-stream.js +++ b/app/assets/javascripts/discourse/app/models/user-drafts-stream.js @@ -10,17 +10,16 @@ import RestModel from "discourse/models/rest"; import UserDraft from "discourse/models/user-draft"; import discourseComputed from "discourse-common/utils/decorators"; -export default RestModel.extend({ - limit: 30, - - loading: false, - hasMore: false, - content: null, +export default class UserDraftsStream extends RestModel { + limit = 30; + loading = false; + hasMore = false; + content = null; init() { - this._super(...arguments); + super.init(...arguments); this.reset(); - }, + } reset() { this.setProperties({ @@ -28,19 +27,19 @@ export default RestModel.extend({ hasMore: true, content: [], }); - }, + } @discourseComputed("content.length", "loading") noContent(contentLength, loading) { return contentLength === 0 && !loading; - }, + } remove(draft) { this.set( "content", this.content.filter((item) => item.draft_key !== draft.draft_key) ); - }, + } findItems(site) { if (site) { @@ -92,5 +91,5 @@ export default RestModel.extend({ .finally(() => { this.set("loading", false); }); - }, -}); + } +}