FEATURE: Decouple category/tag presence in sidebar from notifi level (#17273)
This commit is contained in:
parent
db53c6650b
commit
3266350e80
|
@ -1,9 +1,15 @@
|
|||
import I18n from "I18n";
|
||||
|
||||
import { cached } from "@glimmer/tracking";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { action } from "@ember/object";
|
||||
|
||||
import GlimmerComponent from "discourse/components/glimmer";
|
||||
import CategorySectionLink from "discourse/lib/sidebar/categories-section/category-section-link";
|
||||
|
||||
export default class SidebarCategoriesSection extends GlimmerComponent {
|
||||
@service router;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
|
@ -20,11 +26,32 @@ export default class SidebarCategoriesSection extends GlimmerComponent {
|
|||
|
||||
@cached
|
||||
get sectionLinks() {
|
||||
return this.site.trackedCategoriesList.map((trackedCategory) => {
|
||||
return new CategorySectionLink({
|
||||
category: trackedCategory,
|
||||
topicTrackingState: this.topicTrackingState,
|
||||
});
|
||||
});
|
||||
const links = [];
|
||||
|
||||
for (const category of this.currentUser.sidebarCategories) {
|
||||
links.push(
|
||||
new CategorySectionLink({
|
||||
category,
|
||||
topicTrackingState: this.topicTrackingState,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
get noCategoriesText() {
|
||||
const url = `/u/${this.currentUser.username}/preferences/sidebar`;
|
||||
|
||||
return `${I18n.t(
|
||||
"sidebar.sections.categories.none"
|
||||
)} <a href="${url}">${I18n.t(
|
||||
"sidebar.sections.categories.click_to_get_started"
|
||||
)}</a>`;
|
||||
}
|
||||
|
||||
@action
|
||||
editTracked() {
|
||||
this.router.transitionTo("preferences.sidebar", this.currentUser);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
import I18n from "I18n";
|
||||
|
||||
import { cached } from "@glimmer/tracking";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { action } from "@ember/object";
|
||||
|
||||
import GlimmerComponent from "discourse/components/glimmer";
|
||||
import TagSectionLink from "discourse/lib/sidebar/tags-section/tag-section-link";
|
||||
|
||||
export default class SidebarTagsSection extends GlimmerComponent {
|
||||
@service router;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
|
@ -20,11 +26,30 @@ export default class SidebarTagsSection extends GlimmerComponent {
|
|||
|
||||
@cached
|
||||
get sectionLinks() {
|
||||
return this.currentUser.trackedTags.map((trackedTag) => {
|
||||
return new TagSectionLink({
|
||||
tagName: trackedTag,
|
||||
topicTrackingState: this.topicTrackingState,
|
||||
});
|
||||
});
|
||||
const links = [];
|
||||
|
||||
for (const tagName of this.currentUser.sidebarTagNames) {
|
||||
links.push(
|
||||
new TagSectionLink({
|
||||
tagName,
|
||||
topicTrackingState: this.topicTrackingState,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
get noTagsText() {
|
||||
const url = `/u/${this.currentUser.username}/preferences/sidebar`;
|
||||
|
||||
return `${I18n.t("sidebar.sections.tags.none")} <a href="${url}">${I18n.t(
|
||||
"sidebar.sections.tags.click_to_get_started"
|
||||
)}</a>`;
|
||||
}
|
||||
|
||||
@action
|
||||
editTracked() {
|
||||
this.router.transitionTo("preferences.sidebar", this.currentUser);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import Controller from "@ember/controller";
|
||||
import { action } from "@ember/object";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
|
||||
export default class extends Controller {
|
||||
@tracked saved = false;
|
||||
@tracked selectedSiderbarCategories = [];
|
||||
@tracked selectedSidebarTagNames = [];
|
||||
|
||||
@action
|
||||
tagUpdated(tagNames) {
|
||||
this.selectedSidebarTagNames = tagNames;
|
||||
this.model.set("sidebar_tag_names", tagNames);
|
||||
this.saved = false;
|
||||
}
|
||||
|
||||
@action
|
||||
categoryUpdated(categories) {
|
||||
this.selectedSiderbarCategories = categories;
|
||||
this.model.set("sidebarCategoryIds", categories.mapBy("id"));
|
||||
this.saved = false;
|
||||
}
|
||||
|
||||
@action
|
||||
save() {
|
||||
this.model
|
||||
.save()
|
||||
.then(() => {
|
||||
this.saved = true;
|
||||
this.initialSidebarCategoryIds = this.model.sidebarCategoryIds;
|
||||
this.initialSidebarTagNames = this.model.initialSidebarTagNames;
|
||||
})
|
||||
.catch((error) => {
|
||||
this.model.set("sidebarCategoryIds", this.initialSidebarCategoryIds);
|
||||
this.model.set("sidebar_tag_names", this.initialSidebarTagNames);
|
||||
popupAjaxError(error);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import EmberObject, { computed, get, getProperties } from "@ember/object";
|
||||
import cookie, { removeCookie } from "discourse/lib/cookie";
|
||||
import { defaultHomepage, escapeExpression } from "discourse/lib/utilities";
|
||||
import { equal, filterBy, gt, or } from "@ember/object/computed";
|
||||
import { alias, equal, filterBy, gt, or } from "@ember/object/computed";
|
||||
import getURL, { getURLWithCDN } from "discourse-common/lib/get-url";
|
||||
import { A } from "@ember/array";
|
||||
import Badge from "discourse/models/badge";
|
||||
|
@ -62,6 +62,8 @@ let userFields = [
|
|||
"primary_group_id",
|
||||
"flair_group_id",
|
||||
"user_notification_schedule",
|
||||
"sidebar_category_ids",
|
||||
"sidebar_tag_names",
|
||||
];
|
||||
|
||||
export function addSaveableUserField(fieldName) {
|
||||
|
@ -307,6 +309,35 @@ const User = RestModel.extend({
|
|||
@discourseComputed("silenced_till")
|
||||
silencedTillDate: longDate,
|
||||
|
||||
sidebarCategoryIds: alias("sidebar_category_ids"),
|
||||
|
||||
@discourseComputed("sidebar_tag_names.[]")
|
||||
sidebarTagNames(sidebarTagNames) {
|
||||
if (!sidebarTagNames || sidebarTagNames.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return sidebarTagNames;
|
||||
},
|
||||
|
||||
@discourseComputed("sidebar_category_ids.[]")
|
||||
sidebarCategories(sidebarCategoryIds) {
|
||||
if (!sidebarCategoryIds || sidebarCategoryIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Site.current().categoriesList.filter((category) => {
|
||||
if (
|
||||
this.siteSettings.suppress_uncategorized_badge &&
|
||||
category.isUncategorizedCategory
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return sidebarCategoryIds.includes(category.id);
|
||||
});
|
||||
},
|
||||
|
||||
changeUsername(new_username) {
|
||||
return ajax(userPath(`${this.username_lower}/preferences/username`), {
|
||||
type: "PUT",
|
||||
|
@ -385,6 +416,12 @@ const User = RestModel.extend({
|
|||
}
|
||||
});
|
||||
|
||||
["sidebar_category_ids", "sidebar_tag_names"].forEach((prop) => {
|
||||
if (data[prop]?.length === 0) {
|
||||
data[prop] = null;
|
||||
}
|
||||
});
|
||||
|
||||
return this._saveUserData(data, updatedState);
|
||||
},
|
||||
|
||||
|
|
|
@ -166,6 +166,7 @@ export default function () {
|
|||
this.route("tags");
|
||||
this.route("interface");
|
||||
this.route("apps");
|
||||
this.route("sidebar");
|
||||
|
||||
this.route("username");
|
||||
this.route("email");
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import RestrictedUserRoute from "discourse/routes/restricted-user";
|
||||
|
||||
export default RestrictedUserRoute.extend({
|
||||
showFooter: true,
|
||||
|
||||
setupController(controller, user) {
|
||||
const props = {
|
||||
model: user,
|
||||
selectedSiderbarCategories: user.sidebarCategories,
|
||||
initialSidebarCategoryIds: user.sidebarCategoryIds,
|
||||
};
|
||||
|
||||
if (this.siteSettings.tagging_enabled) {
|
||||
props.selectedSidebarTagNames = user.sidebarTagNames;
|
||||
props.initialSidebarTagNames = user.sidebarTagNames;
|
||||
}
|
||||
|
||||
controller.setProperties(props);
|
||||
},
|
||||
});
|
|
@ -2,7 +2,10 @@
|
|||
@sectionName="categories"
|
||||
@headerRoute="discovery.categories"
|
||||
@headerLinkText={{i18n "sidebar.sections.categories.header_link_text"}}
|
||||
@headerLinkTitle={{i18n "sidebar.sections.categories.header_link_title"}} >
|
||||
@headerLinkTitle={{i18n "sidebar.sections.categories.header_link_title"}}
|
||||
@headerAction={{this.editTracked}}
|
||||
@headerActionTitle={{i18n "sidebar.sections.categories.header_action_title"}}
|
||||
@headerActionIcon="pencil-alt" >
|
||||
|
||||
{{#if (gt this.sectionLinks.length 0)}}
|
||||
{{#each this.sectionLinks as |sectionLink|}}
|
||||
|
@ -19,7 +22,7 @@
|
|||
{{/each}}
|
||||
{{else}}
|
||||
<Sidebar::SectionMessage>
|
||||
{{i18n "sidebar.sections.categories.no_tracked_categories"}}
|
||||
{{html-safe this.noCategoriesText}}
|
||||
</Sidebar::SectionMessage>
|
||||
{{/if}}
|
||||
</Sidebar::Section>
|
||||
|
|
|
@ -2,7 +2,10 @@
|
|||
@sectionName="tags"
|
||||
@headerRoute="tags"
|
||||
@headerLinkText={{i18n "sidebar.sections.tags.header_link_text"}}
|
||||
@headerLinkTitle={{i18n "sidebar.sections.tags.header_link_title"}} >
|
||||
@headerLinkTitle={{i18n "sidebar.sections.tags.header_link_title"}}
|
||||
@headerAction={{this.editTracked}}
|
||||
@headerActionTitle={{i18n "sidebar.sections.tags.header_action_title"}}
|
||||
@headerActionIcon="pencil-alt" >
|
||||
|
||||
{{#if (gt this.sectionLinks.length 0)}}
|
||||
{{#each this.sectionLinks as |sectionLink|}}
|
||||
|
@ -18,7 +21,7 @@
|
|||
{{/each}}
|
||||
{{else}}
|
||||
<Sidebar::SectionMessage>
|
||||
{{i18n "sidebar.sections.tags.no_tracked_tags"}}
|
||||
{{html-safe this.noTagsText}}
|
||||
</Sidebar::SectionMessage>
|
||||
{{/if}}
|
||||
</Sidebar::Section>
|
||||
|
|
|
@ -49,6 +49,13 @@
|
|||
{{i18n "user.preferences_nav.interface"}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{#if this.currentUser.experimental_sidebar_enabled}}
|
||||
<li class="indent nav-sidebar">
|
||||
{{#link-to "preferences.sidebar"}}
|
||||
{{i18n "user.preferences_nav.sidebar"}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/if}}
|
||||
|
||||
{{plugin-outlet name="user-preferences-nav-under-interface" tagName="span" connectorTagName="div" args=(hash model=model)}}
|
||||
|
||||
|
|
|
@ -133,10 +133,6 @@
|
|||
<fieldset class="control-group other">
|
||||
<legend class="control-label">{{i18n "user.other_settings"}}</legend>
|
||||
|
||||
{{#if siteSettings.enable_experimental_sidebar}}
|
||||
{{preference-checkbox labelKey="user.enable_experimental_sidebar" checked=model.user_option.enable_experimental_sidebar class="pref-external-links"}}
|
||||
{{/if}}
|
||||
|
||||
{{preference-checkbox labelKey="user.external_links_in_new_tab" checked=model.user_option.external_links_in_new_tab class="pref-external-links"}}
|
||||
{{preference-checkbox labelKey="user.enable_quoting" checked=model.user_option.enable_quoting class="pref-enable-quoting"}}
|
||||
{{preference-checkbox labelKey="user.enable_defer" checked=model.user_option.enable_defer class="pref-defer-undread"}}
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
<div class="control-group preferences-sidebar-options">
|
||||
<legend class="control-label">{{i18n "user.experimental_sidebar.options"}}</legend>
|
||||
|
||||
{{preference-checkbox
|
||||
labelKey="user.experimental_sidebar.enable"
|
||||
checked=model.user_option.enable_experimental_sidebar
|
||||
class="preferences-sidebar-enable-checkbox"}}
|
||||
</div>
|
||||
|
||||
{{#if model.experimental_sidebar_enabled}}
|
||||
<div class="control-group preferences-sidebar-categories">
|
||||
<legend class="control-label">{{i18n "user.experimental_sidebar.categories_section"}}</legend>
|
||||
|
||||
<div class="controls">
|
||||
{{category-selector
|
||||
categories=this.selectedSiderbarCategories
|
||||
onChange=(action this.categoryUpdated)
|
||||
options=(hash
|
||||
allowUncategorized=(not this.siteSettings.suppress_uncategorized_badge)
|
||||
displayCategoryDescription=true
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
|
||||
<div class="instructions">{{i18n "user.experimental_sidebar.categories_section_instruction"}}</div>
|
||||
</div>
|
||||
|
||||
{{#if this.siteSettings.tagging_enabled}}
|
||||
<div class="control-group preferences-sidebar-tags">
|
||||
<legend class="control-label">{{i18n "user.experimental_sidebar.tags_section"}}</legend>
|
||||
|
||||
<div class="controls">
|
||||
{{tag-chooser
|
||||
tags=this.selectedSidebarTagNames
|
||||
everyTag=true
|
||||
unlimitedTagCount=true
|
||||
onChange=(action this.tagUpdated)
|
||||
options=(hash
|
||||
allowAny=false
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
|
||||
<div class="instructions">{{i18n "user.experimental_sidebar.tags_section_instruction"}}</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{save-controls model=model action=(action "save") saved=saved}}
|
|
@ -9,10 +9,9 @@ import {
|
|||
publishToMessageBus,
|
||||
query,
|
||||
queryAll,
|
||||
updateCurrentUser,
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import selectKit from "discourse/tests/helpers/select-kit-helper";
|
||||
import Site from "discourse/models/site";
|
||||
import { NotificationLevels } from "discourse/lib/notification-levels";
|
||||
import discoveryFixture from "discourse/tests/fixtures/discovery-fixtures";
|
||||
import categoryFixture from "discourse/tests/fixtures/category-fixtures";
|
||||
import { cloneJSON } from "discourse-common/lib/object";
|
||||
|
@ -34,12 +33,9 @@ acceptance(
|
|||
return category.id === Site.current().uncategorized_category_id;
|
||||
});
|
||||
|
||||
category1.set("notification_level", NotificationLevels.TRACKING);
|
||||
|
||||
uncategorizedCategory.set(
|
||||
"notification_level",
|
||||
NotificationLevels.TRACKING
|
||||
);
|
||||
updateCurrentUser({
|
||||
sidebar_category_ids: [category1.id, uncategorizedCategory.id],
|
||||
});
|
||||
|
||||
await visit("/");
|
||||
|
||||
|
@ -58,7 +54,11 @@ acceptance(
|
|||
);
|
||||
|
||||
acceptance("Sidebar - Categories Section", function (needs) {
|
||||
needs.user({ experimental_sidebar_enabled: true });
|
||||
needs.user({
|
||||
experimental_sidebar_enabled: true,
|
||||
sidebar_category_ids: [],
|
||||
sidebar_tag_names: [],
|
||||
});
|
||||
|
||||
needs.settings({
|
||||
suppress_uncategorized_badge: false,
|
||||
|
@ -76,19 +76,13 @@ acceptance("Sidebar - Categories Section", function (needs) {
|
|||
server.get("/c/:categorySlug/:categoryId/find_by_slug.json", () => {
|
||||
return helper.response(cloneJSON(categoryFixture["/c/1/show.json"]));
|
||||
});
|
||||
|
||||
server.post("/category/:categoryId/notifications", () => {
|
||||
return helper.response({});
|
||||
});
|
||||
});
|
||||
|
||||
const setupTrackedCategories = function () {
|
||||
const setupUserSidebarCategories = function () {
|
||||
const categories = Site.current().categories;
|
||||
const category1 = categories[0];
|
||||
const category2 = categories[1];
|
||||
category1.set("notification_level", NotificationLevels.TRACKING);
|
||||
category2.set("notification_level", NotificationLevels.TRACKING);
|
||||
|
||||
updateCurrentUser({ sidebar_category_ids: [category1.id, category2.id] });
|
||||
return { category1, category2 };
|
||||
};
|
||||
|
||||
|
@ -103,27 +97,42 @@ acceptance("Sidebar - Categories Section", function (needs) {
|
|||
);
|
||||
});
|
||||
|
||||
test("category section links when user does not have any tracked categories", async function (assert) {
|
||||
test("clicking on section header button", async function (assert) {
|
||||
await visit("/");
|
||||
await click(".sidebar-section-categories .sidebar-section-header-button");
|
||||
|
||||
assert.strictEqual(
|
||||
query(".sidebar-section-message").textContent.trim(),
|
||||
I18n.t("sidebar.sections.categories.no_tracked_categories"),
|
||||
"the no tracked categories message is displayed"
|
||||
currentURL(),
|
||||
"/u/eviltrout/preferences/sidebar",
|
||||
"it should transition to user preferences sidebar page"
|
||||
);
|
||||
});
|
||||
|
||||
test("uncategorized category is shown when tracked", async function (assert) {
|
||||
test("category section links when user has not added any categories", async function (assert) {
|
||||
await visit("/");
|
||||
|
||||
assert.ok(
|
||||
exists(".sidebar-section-message"),
|
||||
"the no categories message is displayed"
|
||||
);
|
||||
|
||||
await click(".sidebar-section-message a");
|
||||
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
"/u/eviltrout/preferences/sidebar",
|
||||
"it should transition to user preferences sidebar page"
|
||||
);
|
||||
});
|
||||
|
||||
test("uncategorized category is shown when added to sidebar", async function (assert) {
|
||||
const categories = Site.current().categories;
|
||||
|
||||
const uncategorizedCategory = categories.find((category) => {
|
||||
return category.id === Site.current().uncategorized_category_id;
|
||||
return category.isUncategorizedCategory;
|
||||
});
|
||||
|
||||
uncategorizedCategory.set(
|
||||
"notification_level",
|
||||
NotificationLevels.TRACKING
|
||||
);
|
||||
updateCurrentUser({ sidebar_category_ids: [uncategorizedCategory.id] });
|
||||
|
||||
await visit("/");
|
||||
|
||||
|
@ -133,8 +142,8 @@ acceptance("Sidebar - Categories Section", function (needs) {
|
|||
);
|
||||
});
|
||||
|
||||
test("category section links for tracked categories", async function (assert) {
|
||||
const { category1, category2 } = setupTrackedCategories();
|
||||
test("category section links", async function (assert) {
|
||||
const { category1, category2 } = setupUserSidebarCategories();
|
||||
|
||||
await visit("/");
|
||||
|
||||
|
@ -196,8 +205,8 @@ acceptance("Sidebar - Categories Section", function (needs) {
|
|||
);
|
||||
});
|
||||
|
||||
test("visiting category discovery new route for tracked categories", async function (assert) {
|
||||
const { category1 } = setupTrackedCategories();
|
||||
test("visiting category discovery new route", async function (assert) {
|
||||
const { category1 } = setupUserSidebarCategories();
|
||||
|
||||
await visit(`/c/${category1.slug}/${category1.id}/l/new`);
|
||||
|
||||
|
@ -214,8 +223,8 @@ acceptance("Sidebar - Categories Section", function (needs) {
|
|||
);
|
||||
});
|
||||
|
||||
test("visiting category discovery unread route for tracked categories", async function (assert) {
|
||||
const { category1 } = setupTrackedCategories();
|
||||
test("visiting category discovery unread route", async function (assert) {
|
||||
const { category1 } = setupUserSidebarCategories();
|
||||
|
||||
await visit(`/c/${category1.slug}/${category1.id}/l/unread`);
|
||||
|
||||
|
@ -232,8 +241,8 @@ acceptance("Sidebar - Categories Section", function (needs) {
|
|||
);
|
||||
});
|
||||
|
||||
test("visiting category discovery top route for tracked categories", async function (assert) {
|
||||
const { category1 } = setupTrackedCategories();
|
||||
test("visiting category discovery top route", async function (assert) {
|
||||
const { category1 } = setupUserSidebarCategories();
|
||||
|
||||
await visit(`/c/${category1.slug}/${category1.id}/l/top`);
|
||||
|
||||
|
@ -250,58 +259,8 @@ acceptance("Sidebar - Categories Section", function (needs) {
|
|||
);
|
||||
});
|
||||
|
||||
test("updating category notification level", async function (assert) {
|
||||
const { category1, category2 } = setupTrackedCategories();
|
||||
|
||||
await visit(`/c/${category1.slug}/${category1.id}/l/top`);
|
||||
|
||||
assert.ok(
|
||||
exists(`.sidebar-section-link-${category1.slug}`),
|
||||
`has ${category1.name} section link in sidebar`
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
exists(`.sidebar-section-link-${category2.slug}`),
|
||||
`has ${category2.name} section link in sidebar`
|
||||
);
|
||||
|
||||
const notificationLevelsDropdown = selectKit(".notifications-button");
|
||||
|
||||
await notificationLevelsDropdown.expand();
|
||||
|
||||
await notificationLevelsDropdown.selectRowByValue(
|
||||
NotificationLevels.REGULAR
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
!exists(`.sidebar-section-link-${category1.slug}`),
|
||||
`does not have ${category1.name} section link in sidebar`
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
exists(`.sidebar-section-link-${category2.slug}`),
|
||||
`has ${category2.name} section link in sidebar`
|
||||
);
|
||||
|
||||
await notificationLevelsDropdown.expand();
|
||||
|
||||
await notificationLevelsDropdown.selectRowByValue(
|
||||
NotificationLevels.TRACKING
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
exists(`.sidebar-section-link-${category1.slug}`),
|
||||
`has ${category1.name} section link in sidebar`
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
exists(`.sidebar-section-link-${category2.slug}`),
|
||||
`has ${category2.name} section link in sidebar`
|
||||
);
|
||||
});
|
||||
|
||||
test("new and unread count for categories link", async function (assert) {
|
||||
const { category1, category2 } = setupTrackedCategories();
|
||||
const { category1, category2 } = setupUserSidebarCategories();
|
||||
|
||||
this.container.lookup("topic-tracking-state:main").loadStates([
|
||||
{
|
||||
|
@ -426,7 +385,7 @@ acceptance("Sidebar - Categories Section", function (needs) {
|
|||
});
|
||||
|
||||
test("clean up topic tracking state state changed callbacks when section is destroyed", async function (assert) {
|
||||
setupTrackedCategories();
|
||||
setupUserSidebarCategories();
|
||||
|
||||
await visit("/");
|
||||
|
||||
|
|
|
@ -13,8 +13,6 @@ import {
|
|||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import discoveryFixture from "discourse/tests/fixtures/discovery-fixtures";
|
||||
import { cloneJSON } from "discourse-common/lib/object";
|
||||
import selectKit from "discourse/tests/helpers/select-kit-helper";
|
||||
import { NotificationLevels } from "discourse/lib/notification-levels";
|
||||
|
||||
acceptance("Sidebar - Tags section - tagging disabled", function (needs) {
|
||||
needs.settings({
|
||||
|
@ -43,6 +41,7 @@ acceptance("Sidebar - Tags section", function (needs) {
|
|||
tracked_tags: ["tag1"],
|
||||
watched_tags: ["tag2", "tag3"],
|
||||
watching_first_post_tags: [],
|
||||
sidebar_tag_names: ["tag1", "tag2", "tag3"],
|
||||
});
|
||||
|
||||
needs.pretender((server, helper) => {
|
||||
|
@ -59,16 +58,6 @@ acceptance("Sidebar - Tags section", function (needs) {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
server.put("/tag/:tagId/notifications", (request) => {
|
||||
return helper.response({
|
||||
watched_tags: [],
|
||||
watching_first_post_tags: [],
|
||||
regular_tags: [request.params.tagId],
|
||||
tracked_tags: [],
|
||||
muted_tags: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("clicking on section header link", async function (assert) {
|
||||
|
@ -82,11 +71,20 @@ acceptance("Sidebar - Tags section", function (needs) {
|
|||
);
|
||||
});
|
||||
|
||||
test("section content when user does not have any tracked tags", async function (assert) {
|
||||
test("clicking on section header button", async function (assert) {
|
||||
await visit("/");
|
||||
await click(".sidebar-section-tags .sidebar-section-header-button");
|
||||
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
"/u/eviltrout/preferences/sidebar",
|
||||
"it should transition to user preferences sidebar page"
|
||||
);
|
||||
});
|
||||
|
||||
test("section content when user has not added any tags", async function (assert) {
|
||||
updateCurrentUser({
|
||||
tracked_tags: [],
|
||||
watched_tags: [],
|
||||
watching_first_post_tags: [],
|
||||
sidebar_tag_names: [],
|
||||
});
|
||||
|
||||
await visit("/");
|
||||
|
@ -95,12 +93,14 @@ acceptance("Sidebar - Tags section", function (needs) {
|
|||
query(
|
||||
".sidebar-section-tags .sidebar-section-message"
|
||||
).textContent.trim(),
|
||||
I18n.t("sidebar.sections.tags.no_tracked_tags"),
|
||||
"the no tracked tags message is displayed"
|
||||
`${I18n.t("sidebar.sections.tags.none")} ${I18n.t(
|
||||
"sidebar.sections.tags.click_to_get_started"
|
||||
)}`,
|
||||
"the no tags message is displayed"
|
||||
);
|
||||
});
|
||||
|
||||
test("tag section links for tracked tags", async function (assert) {
|
||||
test("tag section links for user", async function (assert) {
|
||||
await visit("/");
|
||||
|
||||
assert.strictEqual(
|
||||
|
@ -166,7 +166,7 @@ acceptance("Sidebar - Tags section", function (needs) {
|
|||
);
|
||||
});
|
||||
|
||||
test("visiting tag discovery top route for tracked tags", async function (assert) {
|
||||
test("visiting tag discovery top route", async function (assert) {
|
||||
await visit(`/tag/tag1/l/top`);
|
||||
|
||||
assert.strictEqual(
|
||||
|
@ -181,7 +181,7 @@ acceptance("Sidebar - Tags section", function (needs) {
|
|||
);
|
||||
});
|
||||
|
||||
test("visiting tag discovery new route for tracked tags", async function (assert) {
|
||||
test("visiting tag discovery new ", async function (assert) {
|
||||
await visit(`/tag/tag1/l/new`);
|
||||
|
||||
assert.strictEqual(
|
||||
|
@ -196,7 +196,7 @@ acceptance("Sidebar - Tags section", function (needs) {
|
|||
);
|
||||
});
|
||||
|
||||
test("visiting tag discovery unread route for tracked tags", async function (assert) {
|
||||
test("visiting tag discovery unread route", async function (assert) {
|
||||
await visit(`/tag/tag1/l/unread`);
|
||||
|
||||
assert.strictEqual(
|
||||
|
@ -341,21 +341,4 @@ acceptance("Sidebar - Tags section", function (needs) {
|
|||
initialCallbackCount
|
||||
);
|
||||
});
|
||||
|
||||
test("updating tags notification levels", async function (assert) {
|
||||
await visit(`/tag/tag1/l/unread`);
|
||||
|
||||
const notificationLevelsDropdown = selectKit(".notifications-button");
|
||||
|
||||
await notificationLevelsDropdown.expand();
|
||||
|
||||
await notificationLevelsDropdown.selectRowByValue(
|
||||
NotificationLevels.REGULAR
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
!exists(".sidebar-section-tags .sidebar-section-link-tag1"),
|
||||
"tag1 section link is removed from sidebar"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,213 @@
|
|||
import { test } from "qunit";
|
||||
|
||||
import { click, visit } from "@ember/test-helpers";
|
||||
|
||||
import {
|
||||
acceptance,
|
||||
exists,
|
||||
updateCurrentUser,
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import selectKit from "discourse/tests/helpers/select-kit-helper";
|
||||
|
||||
acceptance("User Preferences - Sidebar - Tagging Disabled", function (needs) {
|
||||
needs.settings({
|
||||
tagging_enabled: false,
|
||||
});
|
||||
|
||||
needs.user({
|
||||
experimental_sidebar_enabled: true,
|
||||
sidebar_category_ids: [],
|
||||
});
|
||||
|
||||
test("user should not see tag chooser", async function (assert) {
|
||||
await visit("/u/eviltrout/preferences/sidebar");
|
||||
|
||||
assert.ok(!exists(".tag-chooser"), "tag chooser is not displayed");
|
||||
});
|
||||
});
|
||||
|
||||
acceptance("User Preferences - Sidebar", function (needs) {
|
||||
needs.user({
|
||||
experimental_sidebar_enabled: true,
|
||||
sidebar_category_ids: [],
|
||||
sidebar_tag_names: [],
|
||||
});
|
||||
|
||||
needs.settings({
|
||||
tagging_enabled: true,
|
||||
});
|
||||
|
||||
let updateUserRequestBody = null;
|
||||
|
||||
needs.hooks.afterEach(() => {
|
||||
updateUserRequestBody = null;
|
||||
});
|
||||
|
||||
needs.pretender((server, helper) => {
|
||||
server.put("/u/eviltrout.json", (request) => {
|
||||
updateUserRequestBody = helper.parsePostData(request.requestBody);
|
||||
|
||||
// if only the howto category is updated, intentionally cause an error
|
||||
if (
|
||||
updateUserRequestBody["sidebar_category_ids[]"]?.[0] === "10" ||
|
||||
updateUserRequestBody["sidebar_tag_names[]"]?.[0] === "gazelle"
|
||||
) {
|
||||
// This request format will cause an error
|
||||
return helper.response(400, {});
|
||||
} else {
|
||||
return helper.response({ user: {} });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("user encountering error when adding categories to sidebar", async function (assert) {
|
||||
updateCurrentUser({ sidebar_category_ids: [6] });
|
||||
|
||||
await visit("/");
|
||||
|
||||
assert.ok(
|
||||
exists(".sidebar-section-categories .sidebar-section-link-support"),
|
||||
"support category is present in sidebar"
|
||||
);
|
||||
|
||||
await click(".sidebar-section-categories .sidebar-section-header-button");
|
||||
|
||||
const categorySelector = selectKit(".category-selector");
|
||||
await categorySelector.expand();
|
||||
await categorySelector.selectKitSelectRowByName("howto");
|
||||
await categorySelector.deselectItemByName("support");
|
||||
|
||||
assert.ok(
|
||||
exists(".sidebar-section-categories .sidebar-section-link-howto"),
|
||||
"howto category has been added to sidebar"
|
||||
);
|
||||
|
||||
await click(".save-changes");
|
||||
|
||||
assert.deepEqual(
|
||||
updateUserRequestBody["sidebar_category_ids[]"],
|
||||
["10"],
|
||||
"contains the right request body to update user's sidebar category links"
|
||||
);
|
||||
|
||||
assert.ok(exists(".modal-body"), "error message is displayed");
|
||||
|
||||
await click(".modal .d-button-label");
|
||||
|
||||
assert.ok(
|
||||
!exists(".sidebar-section-categories .sidebar-section-link-howto"),
|
||||
"howto category has been removed from sidebar"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
exists(".sidebar-section-categories .sidebar-section-link-support"),
|
||||
"support category is added back to sidebar"
|
||||
);
|
||||
});
|
||||
|
||||
test("user adding categories to sidebar", async function (assert) {
|
||||
await visit("/");
|
||||
await click(".sidebar-section-categories .sidebar-section-header-button");
|
||||
|
||||
const categorySelector = selectKit(".category-selector");
|
||||
await categorySelector.expand();
|
||||
await categorySelector.selectKitSelectRowByName("support");
|
||||
await categorySelector.selectKitSelectRowByName("bug");
|
||||
|
||||
assert.ok(
|
||||
exists(".sidebar-section-categories .sidebar-section-link-support"),
|
||||
"support category has been added to sidebar"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
exists(".sidebar-section-categories .sidebar-section-link-bug"),
|
||||
"bug category has been added to sidebar"
|
||||
);
|
||||
|
||||
await click(".save-changes");
|
||||
|
||||
assert.deepEqual(
|
||||
updateUserRequestBody["sidebar_category_ids[]"],
|
||||
["6", "1"],
|
||||
"contains the right request body to update user's sidebar category links"
|
||||
);
|
||||
});
|
||||
|
||||
test("user encountering error when adding tags to sidebar", async function (assert) {
|
||||
updateCurrentUser({ sidebar_tag_names: ["monkey"] });
|
||||
|
||||
await visit("/");
|
||||
|
||||
assert.ok(
|
||||
exists(".sidebar-section-tags .sidebar-section-link-monkey"),
|
||||
"monkey tag is present in sidebar"
|
||||
);
|
||||
|
||||
await click(".sidebar-section-tags .sidebar-section-header-button");
|
||||
|
||||
const tagChooser = selectKit(".tag-chooser");
|
||||
await tagChooser.expand();
|
||||
await tagChooser.selectKitSelectRowByName("gazelle");
|
||||
await tagChooser.deselectItemByName("monkey");
|
||||
|
||||
assert.ok(
|
||||
exists(".sidebar-section-tags .sidebar-section-link-gazelle"),
|
||||
"gazelle tag has been added to sidebar"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
!exists(".sidebar-section-tags .sidebar-section-link-monkey"),
|
||||
"monkey tag has been removed from sidebar"
|
||||
);
|
||||
|
||||
await click(".save-changes");
|
||||
|
||||
assert.deepEqual(
|
||||
updateUserRequestBody["sidebar_tag_names[]"],
|
||||
["gazelle"],
|
||||
"contains the right request body to update user's sidebar tag links"
|
||||
);
|
||||
|
||||
assert.ok(exists(".modal-body"), "error message is displayed");
|
||||
|
||||
await click(".modal .d-button-label");
|
||||
|
||||
assert.ok(
|
||||
!exists(".sidebar-section-tags .sidebar-section-link-gazelle"),
|
||||
"gazelle tag has been removed from sidebar"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
exists(".sidebar-section-tags .sidebar-section-link-monkey"),
|
||||
"monkey tag is added back to sidebar"
|
||||
);
|
||||
});
|
||||
|
||||
test("user adding tags to sidebar", async function (assert) {
|
||||
await visit("/");
|
||||
await click(".sidebar-section-tags .sidebar-section-header-button");
|
||||
|
||||
const tagChooser = selectKit(".tag-chooser");
|
||||
await tagChooser.expand();
|
||||
await tagChooser.selectKitSelectRowByName("monkey");
|
||||
await tagChooser.selectKitSelectRowByName("gazelle");
|
||||
|
||||
assert.ok(
|
||||
exists(".sidebar-section-tags .sidebar-section-link-monkey"),
|
||||
"monkey tag has been added to sidebar"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
exists(".sidebar-section-tags .sidebar-section-link-gazelle"),
|
||||
"gazelle tag has been added to sidebar"
|
||||
);
|
||||
|
||||
await click(".save-changes");
|
||||
|
||||
assert.deepEqual(
|
||||
updateUserRequestBody["sidebar_tag_names[]"],
|
||||
["monkey", "gazelle"],
|
||||
"contains the right request body to update user's sidebar tag links"
|
||||
);
|
||||
});
|
||||
});
|
|
@ -10,12 +10,16 @@ export function parsePostData(query) {
|
|||
const item = part.split("=");
|
||||
const firstSeg = decodeURIComponent(item[0]);
|
||||
const m = /^([^\[]+)\[(.+)\]/.exec(firstSeg);
|
||||
|
||||
const val = decodeURIComponent(item[1]).replace(/\+/g, " ");
|
||||
const isArray = firstSeg.endsWith("[]");
|
||||
|
||||
if (m) {
|
||||
let key = m[1];
|
||||
result[key] = result[key] || {};
|
||||
result[key][m[2].replace("][", ".")] = val;
|
||||
} else if (isArray) {
|
||||
result[firstSeg] ||= [];
|
||||
result[firstSeg].push(val);
|
||||
} else {
|
||||
result[firstSeg] = val;
|
||||
}
|
||||
|
|
|
@ -298,6 +298,10 @@ export default function selectKit(selector) {
|
|||
await click(`${selector} .selected-content [data-value="${value}"]`);
|
||||
},
|
||||
|
||||
async deselectItemByName(name) {
|
||||
await click(`${selector} .selected-content [data-name="${name}"]`);
|
||||
},
|
||||
|
||||
exists() {
|
||||
return exists(selector);
|
||||
},
|
||||
|
|
|
@ -16,7 +16,7 @@ export default MultiSelectComponent.extend({
|
|||
selectKitOptions: {
|
||||
filterable: true,
|
||||
allowAny: false,
|
||||
allowUncategorized: "allowUncategorized",
|
||||
allowUncategorized: true,
|
||||
displayCategoryDescription: false,
|
||||
selectedChoiceComponent: "selected-choice-category",
|
||||
},
|
||||
|
@ -35,6 +35,14 @@ export default MultiSelectComponent.extend({
|
|||
content: computed("categories.[]", "blockedCategories.[]", function () {
|
||||
const blockedCategories = makeArray(this.blockedCategories);
|
||||
return Category.list().filter((category) => {
|
||||
if (category.isUncategorizedCategory) {
|
||||
if (this.attrs.options?.allowUncategorized !== undefined) {
|
||||
return this.attrs.options.allowUncategorized;
|
||||
}
|
||||
|
||||
return this.selectKitOptions.allowUncategorized;
|
||||
}
|
||||
|
||||
return (
|
||||
this.categories.includes(category) ||
|
||||
!blockedCategories.includes(category)
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
.select-kit {
|
||||
.category-row {
|
||||
.select-kit-row.category-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
.category-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -7,6 +10,10 @@
|
|||
-webkit-box-flex: 0;
|
||||
-ms-flex: 1 1 auto;
|
||||
flex: 1 1 auto;
|
||||
|
||||
.category-name {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
.category-desc p {
|
||||
margin: 0;
|
||||
|
@ -17,6 +24,11 @@
|
|||
margin-top: 1px;
|
||||
}
|
||||
}
|
||||
.category-desc {
|
||||
margin: 0;
|
||||
font-size: var(--font-down-1);
|
||||
color: var(--primary-medium);
|
||||
}
|
||||
.topic-count {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
@ -1814,7 +1814,7 @@ class UsersController < ApplicationController
|
|||
:card_background_upload_url,
|
||||
:primary_group_id,
|
||||
:flair_group_id,
|
||||
:featured_topic_id
|
||||
:featured_topic_id,
|
||||
]
|
||||
|
||||
editable_custom_fields = User.editable_user_custom_fields(by_staff: current_user.try(:staff?))
|
||||
|
@ -1824,6 +1824,22 @@ class UsersController < ApplicationController
|
|||
permitted.concat UserUpdater::TAG_NAMES.keys
|
||||
permitted << UserUpdater::NOTIFICATION_SCHEDULE_ATTRS
|
||||
|
||||
if current_user&.user_option&.enable_experimental_sidebar
|
||||
if params.has_key?(:sidebar_category_ids) && params[:sidebar_category_ids].blank?
|
||||
params[:sidebar_category_ids] = []
|
||||
end
|
||||
|
||||
permitted << { sidebar_category_ids: [] }
|
||||
|
||||
if SiteSetting.tagging_enabled
|
||||
if params.has_key?(:sidebar_tag_names) && params[:sidebar_tag_names].blank?
|
||||
params[:sidebar_tag_names] = []
|
||||
end
|
||||
|
||||
permitted << { sidebar_tag_names: [] }
|
||||
end
|
||||
end
|
||||
|
||||
result = params
|
||||
.permit(permitted, theme_ids: [])
|
||||
.reverse_merge(
|
||||
|
|
|
@ -113,6 +113,7 @@ class Category < ActiveRecord::Base
|
|||
has_many :tag_groups, through: :category_tag_groups
|
||||
|
||||
has_many :category_required_tag_groups, -> { order(order: :asc) }, dependent: :destroy
|
||||
has_many :sidebar_section_links, as: :linkable, dependent: :delete_all
|
||||
|
||||
belongs_to :reviewable_by_group, class_name: 'Group'
|
||||
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class SidebarSectionLink < ActiveRecord::Base
|
||||
belongs_to :user
|
||||
belongs_to :linkable, polymorphic: true
|
||||
|
||||
validates :user_id, presence: true, uniqueness: { scope: [:linkable_type, :linkable_id] }
|
||||
validates :linkable_id, presence: true
|
||||
validates :linkable_type, presence: true
|
||||
validate :ensure_supported_linkable_type, if: :will_save_change_to_linkable_type?
|
||||
|
||||
SUPPORTED_LINKABLE_TYPES = %w{Category Tag}
|
||||
|
||||
private def ensure_supported_linkable_type
|
||||
if (!SUPPORTED_LINKABLE_TYPES.include?(self.linkable_type)) || (self.linkable_type == 'Tag' && !SiteSetting.tagging_enabled)
|
||||
self.errors.add(:linkable_type, I18n.t("activerecord.errors.models.sidebar_section_link.attributes.linkable_type.invalid"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: sidebar_section_links
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# user_id :integer not null
|
||||
# linkable_id :integer not null
|
||||
# linkable_type :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# idx_unique_sidebar_section_links (user_id,linkable_type,linkable_id) UNIQUE
|
||||
#
|
|
@ -45,6 +45,7 @@ class Tag < ActiveRecord::Base
|
|||
|
||||
belongs_to :target_tag, class_name: "Tag", optional: true
|
||||
has_many :synonyms, class_name: "Tag", foreign_key: "target_tag_id", dependent: :destroy
|
||||
has_many :sidebar_section_links, as: :linkable, dependent: :delete_all
|
||||
|
||||
after_save :index_search
|
||||
after_save :update_synonym_associations
|
||||
|
|
|
@ -107,6 +107,10 @@ class User < ActiveRecord::Base
|
|||
|
||||
belongs_to :uploaded_avatar, class_name: 'Upload'
|
||||
|
||||
has_many :sidebar_section_links, dependent: :delete_all
|
||||
has_many :category_sidebar_section_links, -> { where(linkable_type: "Category") }, class_name: 'SidebarSectionLink'
|
||||
has_many :sidebar_tags, through: :sidebar_section_links, source: :linkable, source_type: "Tag"
|
||||
|
||||
delegate :last_sent_email_address, to: :email_logs
|
||||
|
||||
validates_presence_of :username
|
||||
|
|
|
@ -72,7 +72,9 @@ class CurrentUserSerializer < BasicUserSerializer
|
|||
:bookmark_auto_delete_preference,
|
||||
:pending_posts_count,
|
||||
:experimental_sidebar_enabled,
|
||||
:status
|
||||
:status,
|
||||
:sidebar_category_ids,
|
||||
:sidebar_tag_names
|
||||
|
||||
delegate :user_stat, to: :object, private: true
|
||||
delegate :any_posts, :draft_count, :pending_posts_count, :read_faq?, to: :user_stat
|
||||
|
@ -313,6 +315,22 @@ class CurrentUserSerializer < BasicUserSerializer
|
|||
SiteSetting.enable_experimental_sidebar
|
||||
end
|
||||
|
||||
def sidebar_category_ids
|
||||
object.category_sidebar_section_links.pluck(:linkable_id)
|
||||
end
|
||||
|
||||
def include_sidebar_category_ids?
|
||||
include_experimental_sidebar_enabled? && object.user_option.enable_experimental_sidebar
|
||||
end
|
||||
|
||||
def sidebar_tag_names
|
||||
object.sidebar_tags.pluck(:name)
|
||||
end
|
||||
|
||||
def include_sidebar_tag_names?
|
||||
include_sidebar_category_ids? && SiteSetting.tagging_enabled
|
||||
end
|
||||
|
||||
def include_status?
|
||||
SiteSetting.enable_user_status
|
||||
end
|
||||
|
|
|
@ -202,6 +202,14 @@ class UserUpdater
|
|||
updated_associated_accounts(attributes[:user_associated_accounts])
|
||||
end
|
||||
|
||||
if attributes.key?(:sidebar_category_ids)
|
||||
update_sidebar_category_section_links(attributes[:sidebar_category_ids])
|
||||
end
|
||||
|
||||
if attributes.key?(:sidebar_tag_names) && SiteSetting.tagging_enabled
|
||||
update_sidebar_tag_section_links(attributes[:sidebar_tag_names])
|
||||
end
|
||||
|
||||
name_changed = user.name_changed?
|
||||
if (saved = (!save_options || user.user_option.save) && (user_notification_schedule.nil? || user_notification_schedule.save) && user_profile.save && user.save) &&
|
||||
(name_changed && old_user_name.casecmp(attributes.fetch(:name)) != 0)
|
||||
|
@ -283,6 +291,48 @@ class UserUpdater
|
|||
|
||||
private
|
||||
|
||||
def delete_all_sidebar_section_links(linkable_type)
|
||||
SidebarSectionLink.where(user: user, linkable_type: linkable_type).delete_all
|
||||
end
|
||||
|
||||
def update_sidebar_section_links(linkable_type, new_linkable_ids)
|
||||
if new_linkable_ids.blank?
|
||||
SidebarSectionLink.where(user: user, linkable_type: linkable_type).delete_all
|
||||
else
|
||||
existing_linkable_ids = SidebarSectionLink.where(user: user, linkable_type: linkable_type).pluck(:linkable_id)
|
||||
|
||||
to_delete = existing_linkable_ids - new_linkable_ids
|
||||
to_insert = new_linkable_ids - existing_linkable_ids
|
||||
|
||||
to_insert_attributes = to_insert.map do |linkable_id|
|
||||
{
|
||||
linkable_type: linkable_type,
|
||||
linkable_id: linkable_id,
|
||||
user_id: user.id
|
||||
}
|
||||
end
|
||||
|
||||
SidebarSectionLink.where(user: user, linkable_type: linkable_type, linkable_id: to_delete).delete_all if to_delete.present?
|
||||
SidebarSectionLink.insert_all(to_insert_attributes) if to_insert_attributes.present?
|
||||
end
|
||||
end
|
||||
|
||||
def update_sidebar_tag_section_links(tag_names)
|
||||
if tag_names.blank?
|
||||
delete_all_sidebar_section_links('Tag')
|
||||
else
|
||||
update_sidebar_section_links('Tag', Tag.where(name: tag_names).pluck(:id))
|
||||
end
|
||||
end
|
||||
|
||||
def update_sidebar_category_section_links(category_ids)
|
||||
if category_ids.blank?
|
||||
delete_all_sidebar_section_links('Category')
|
||||
else
|
||||
update_sidebar_section_links('Category', Category.secured(guardian).where(id: category_ids).pluck(:id))
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :user, :guardian
|
||||
|
||||
def format_url(website)
|
||||
|
|
|
@ -1161,7 +1161,13 @@ en:
|
|||
external_links_in_new_tab: "Open all external links in a new tab"
|
||||
enable_quoting: "Enable quote reply for highlighted text"
|
||||
enable_defer: "Enable defer to mark topics unread"
|
||||
enable_experimental_sidebar: "Enable experimental sidebar"
|
||||
experimental_sidebar:
|
||||
enable: "Enable experimental sidebar"
|
||||
options: "Options"
|
||||
categories_section: "Categories Section"
|
||||
categories_section_instruction: "Selected categories will be displayed under Sidebar's categories section."
|
||||
tags_section: "Tags Section"
|
||||
tags_section_instruction: "Selected tags will be displayed under Sidebar's tags section."
|
||||
change: "change"
|
||||
featured_topic: "Featured Topic"
|
||||
moderator: "%{user} is a moderator"
|
||||
|
@ -1306,6 +1312,7 @@ en:
|
|||
tags: "Tags"
|
||||
interface: "Interface"
|
||||
apps: "Apps"
|
||||
sidebar: "Sidebar"
|
||||
|
||||
change_password:
|
||||
success: "(email sent)"
|
||||
|
@ -4056,13 +4063,17 @@ en:
|
|||
unread_with_count: "Unread (%{count})"
|
||||
archive: "Archive"
|
||||
tags:
|
||||
no_tracked_tags: "You are not tracking any tags."
|
||||
none: "You have not added any tags."
|
||||
click_to_get_started: "Click here to get started."
|
||||
header_link_title: "all tags"
|
||||
header_link_text: "Tags"
|
||||
header_action_title: "edit your sidebar tags"
|
||||
categories:
|
||||
no_tracked_categories: "You are not tracking any categories."
|
||||
none: "You have not added any categories."
|
||||
click_to_get_started: "Click here to get started."
|
||||
header_link_title: "all categories"
|
||||
header_link_text: "Categories"
|
||||
header_action_title: "edit your sidebar categories"
|
||||
topics:
|
||||
header_link_title: "home"
|
||||
header_link_text: "Topics"
|
||||
|
|
|
@ -640,6 +640,11 @@ en:
|
|||
base:
|
||||
invalid_url: "Replacement URL is invalid"
|
||||
invalid_tag_list: "Replacement tag list is invalid"
|
||||
sidebar_section_link:
|
||||
attributes:
|
||||
linkable_type:
|
||||
invalid: "is not valid"
|
||||
|
||||
|
||||
uncategorized_category_name: "Uncategorized"
|
||||
|
||||
|
|
|
@ -488,6 +488,7 @@ Discourse::Application.routes.draw do
|
|||
get "#{root_path}/:username/preferences/users" => "users#preferences", constraints: { username: RouteFormat.username }
|
||||
get "#{root_path}/:username/preferences/tags" => "users#preferences", constraints: { username: RouteFormat.username }
|
||||
get "#{root_path}/:username/preferences/interface" => "users#preferences", constraints: { username: RouteFormat.username }
|
||||
get "#{root_path}/:username/preferences/sidebar" => "users#preferences", constraints: { username: RouteFormat.username }
|
||||
get "#{root_path}/:username/preferences/apps" => "users#preferences", constraints: { username: RouteFormat.username }
|
||||
post "#{root_path}/:username/preferences/email" => "users_email#create", constraints: { username: RouteFormat.username }
|
||||
put "#{root_path}/:username/preferences/email" => "users_email#update", constraints: { username: RouteFormat.username }
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateSidebarSectionLinks < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
create_table :sidebar_section_links do |t|
|
||||
t.integer :user_id, null: false
|
||||
t.integer :linkable_id, null: false
|
||||
t.string :linkable_type, null: false
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :sidebar_section_links, [:user_id, :linkable_type, :linkable_id], unique: true, name: 'idx_unique_sidebar_section_links'
|
||||
end
|
||||
end
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Fabricator(:sidebar_section_link) do
|
||||
user
|
||||
end
|
||||
|
||||
Fabricator(:category_sidebar_section_link, from: :sidebar_section_link) do
|
||||
linkable(fabricator: :category)
|
||||
end
|
||||
|
||||
Fabricator(:tag_sidebar_section_link, from: :sidebar_section_link) do
|
||||
linkable(fabricator: :tag)
|
||||
end
|
|
@ -30,6 +30,17 @@ describe Category do
|
|||
expect(cats.errors[:name]).to be_present
|
||||
end
|
||||
|
||||
context 'associations' do
|
||||
it 'should delete associated sidebar_section_links when category is destroyed' do
|
||||
category_sidebar_section_link = Fabricate(:category_sidebar_section_link)
|
||||
category_sidebar_section_link_2 = Fabricate(:category_sidebar_section_link, linkable: category_sidebar_section_link.linkable)
|
||||
tag_sidebar_section_link = Fabricate(:tag_sidebar_section_link)
|
||||
|
||||
expect { category_sidebar_section_link.linkable.destroy! }.to change { SidebarSectionLink.count }.from(3).to(1)
|
||||
expect(SidebarSectionLink.first).to eq(tag_sidebar_section_link)
|
||||
end
|
||||
end
|
||||
|
||||
describe "slug" do
|
||||
it "converts to lower" do
|
||||
category = Category.create!(name: "Hello World", slug: "Hello-World", user: user)
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe SidebarSectionLink do
|
||||
fab!(:user) { Fabricate(:user) }
|
||||
|
||||
context "validations" do
|
||||
it 'is not valid when linkable already exists for the current user' do
|
||||
category_sidebar_section_link = Fabricate(:category_sidebar_section_link, user: user)
|
||||
|
||||
sidebar_section_link = SidebarSectionLink.new(user: user, linkable: category_sidebar_section_link.linkable)
|
||||
|
||||
expect(sidebar_section_link.valid?).to eq(false)
|
||||
expect(sidebar_section_link.errors.details[:user_id][0][:error]).to eq(:taken)
|
||||
end
|
||||
|
||||
describe '#linkable_type' do
|
||||
it "is not valid when linkable_type is not supported" do
|
||||
sidebar_section_link = SidebarSectionLink.new(user: user, linkable_id: 1, linkable_type: 'sometype')
|
||||
|
||||
expect(sidebar_section_link.valid?).to eq(false)
|
||||
|
||||
expect(sidebar_section_link.errors[:linkable_type]).to eq([
|
||||
I18n.t("activerecord.errors.models.sidebar_section_link.attributes.linkable_type.invalid")
|
||||
])
|
||||
end
|
||||
|
||||
it "is not valid when linkable_type is Tag and SiteSetting.tagging_enabled is false" do
|
||||
SiteSetting.tagging_enabled = false
|
||||
sidebar_section_link = SidebarSectionLink.new(user: user, linkable_id: 1, linkable_type: 'Tag')
|
||||
|
||||
expect(sidebar_section_link.valid?).to eq(false)
|
||||
|
||||
expect(sidebar_section_link.errors[:linkable_type]).to eq([
|
||||
I18n.t("activerecord.errors.models.sidebar_section_link.attributes.linkable_type.invalid")
|
||||
])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -13,12 +13,24 @@ describe Tag do
|
|||
let(:tag) { Fabricate(:tag) }
|
||||
let(:tag2) { Fabricate(:tag) }
|
||||
let(:topic) { Fabricate(:topic, tags: [tag]) }
|
||||
fab!(:user) { Fabricate(:user) }
|
||||
|
||||
before do
|
||||
SiteSetting.tagging_enabled = true
|
||||
SiteSetting.min_trust_level_to_tag_topics = 0
|
||||
end
|
||||
|
||||
context 'associations' do
|
||||
it 'should delete associated sidebar_section_links when tag is destroyed' do
|
||||
tag_sidebar_section_link = Fabricate(:tag_sidebar_section_link)
|
||||
tag_sidebar_section_link_2 = Fabricate(:tag_sidebar_section_link, linkable: tag_sidebar_section_link.linkable)
|
||||
category_sidebar_section_link = Fabricate(:category_sidebar_section_link)
|
||||
|
||||
expect { tag_sidebar_section_link.linkable.destroy! }.to change { SidebarSectionLink.count }.from(3).to(1)
|
||||
expect(SidebarSectionLink.first).to eq(category_sidebar_section_link)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'new' do
|
||||
subject { Fabricate.build(:tag) }
|
||||
|
||||
|
|
|
@ -11,6 +11,15 @@ RSpec.describe User do
|
|||
|
||||
it { is_expected.to have_many(:pending_posts).class_name('ReviewableQueuedPost').with_foreign_key(:created_by_id) }
|
||||
|
||||
context 'associations' do
|
||||
it 'should delete sidebar_section_links when a user is destroyed' do
|
||||
Fabricate(:category_sidebar_section_link, user: user)
|
||||
Fabricate(:tag_sidebar_section_link, user: user)
|
||||
|
||||
expect { user.destroy! }.to change { SidebarSectionLink.where(user: user).count }.from(2).to(0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'validations' do
|
||||
describe '#username' do
|
||||
it { is_expected.to validate_presence_of :username }
|
||||
|
|
|
@ -2297,6 +2297,102 @@ describe UsersController do
|
|||
json = response.parsed_body
|
||||
expect(json['user']['id']).to eq user.id
|
||||
end
|
||||
|
||||
context 'experimental sidebar' do
|
||||
before do
|
||||
SiteSetting.enable_experimental_sidebar = true
|
||||
user.user_option.update!(enable_experimental_sidebar: true)
|
||||
end
|
||||
|
||||
it "should allow user to update UserOption#enable_experimental_sidebar" do
|
||||
put "/u/#{user.username}.json", params: { enable_experimental_sidebar: 'false' }
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(user.reload.user_option.enable_experimental_sidebar).to eq(false)
|
||||
|
||||
put "/u/#{user.username}.json", params: { enable_experimental_sidebar: 'true' }
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(user.reload.user_option.enable_experimental_sidebar).to eq(true)
|
||||
end
|
||||
|
||||
it 'does not remove category or tag sidebar section links when params are not present' do
|
||||
Fabricate(:category_sidebar_section_link, user: user)
|
||||
Fabricate(:tag_sidebar_section_link, user: user)
|
||||
|
||||
expect do
|
||||
put "/u/#{user.username}.json"
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
end.to_not change { user.sidebar_section_links.count }
|
||||
end
|
||||
|
||||
it "should allow user to remove all category sidebar section links" do
|
||||
Fabricate(:category_sidebar_section_link, user: user)
|
||||
|
||||
expect do
|
||||
put "/u/#{user.username}.json", params: { sidebar_category_ids: nil }
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
end.to change { user.sidebar_section_links.count }.from(1).to(0)
|
||||
end
|
||||
|
||||
it "should allow user to modify category sidebar section links" do
|
||||
category = Fabricate(:category)
|
||||
restricted_category = Fabricate(:category, read_restricted: true)
|
||||
category_siderbar_section_link = Fabricate(:category_sidebar_section_link, user: user)
|
||||
|
||||
put "/u/#{user.username}.json", params: { sidebar_category_ids: [category.id, restricted_category.id] }
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(user.sidebar_section_links.count).to eq(1)
|
||||
expect(SidebarSectionLink.exists?(id: category_siderbar_section_link.id)).to eq(false)
|
||||
|
||||
sidebar_section_link = user.sidebar_section_links.first
|
||||
|
||||
expect(sidebar_section_link.linkable).to eq(category)
|
||||
end
|
||||
|
||||
it 'should allow user to remove all tag sidebar section links' do
|
||||
SiteSetting.tagging_enabled = true
|
||||
|
||||
Fabricate(:tag_sidebar_section_link, user: user)
|
||||
|
||||
expect do
|
||||
put "/u/#{user.username}.json", params: { sidebar_tag_names: nil }
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
end.to change { user.sidebar_section_links.count }.from(1).to(0)
|
||||
end
|
||||
|
||||
it 'should not allow user to add tag sidebar section links when tagging is disabled' do
|
||||
SiteSetting.tagging_enabled = false
|
||||
|
||||
tag = Fabricate(:tag)
|
||||
|
||||
put "/u/#{user.username}.json", params: { sidebar_tag_names: [tag.name] }
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(user.reload.sidebar_section_links.count).to eq(0)
|
||||
end
|
||||
|
||||
it "should allow user to add tag sidebar section links" do
|
||||
SiteSetting.tagging_enabled = true
|
||||
|
||||
tag = Fabricate(:tag)
|
||||
tag_sidebar_section_link = Fabricate(:tag_sidebar_section_link, user: user)
|
||||
|
||||
put "/u/#{user.username}.json", params: { sidebar_tag_names: [tag.name, "somerandomtag"] }
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(user.sidebar_section_links.count).to eq(1)
|
||||
expect(SidebarSectionLink.exists?(id: tag_sidebar_section_link.id)).to eq(false)
|
||||
|
||||
sidebar_section_link = user.sidebar_section_links.first
|
||||
|
||||
expect(sidebar_section_link.linkable).to eq(tag)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'without permission to update' do
|
||||
|
@ -2309,21 +2405,6 @@ describe UsersController do
|
|||
expect(response).to be_forbidden
|
||||
expect(user.reload.name).not_to eq 'Jim Tom'
|
||||
end
|
||||
|
||||
context 'enabling experimental sidebar' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it "should be able to update UserOption#enable_experimental_sidebar" do
|
||||
SiteSetting.enable_experimental_sidebar = true
|
||||
|
||||
put "/u/#{user.username}.json", params: { enable_experimental_sidebar: 'true' }
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(user.user_option.enable_experimental_sidebar).to eq(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe CurrentUserSerializer do
|
||||
fab!(:user) { Fabricate(:user) }
|
||||
subject(:serializer) { described_class.new(user, scope: guardian, root: false) }
|
||||
|
||||
let(:guardian) { Guardian.new }
|
||||
let(:guardian) { Guardian.new(user) }
|
||||
|
||||
context "when SSO is not enabled" do
|
||||
fab!(:user) { Fabricate(:user) }
|
||||
|
||||
it "should not include the external_id field" do
|
||||
payload = serializer.as_json
|
||||
expect(payload).not_to have_key(:external_id)
|
||||
|
@ -31,7 +30,6 @@ RSpec.describe CurrentUserSerializer do
|
|||
end
|
||||
|
||||
context "#top_category_ids" do
|
||||
fab!(:user) { Fabricate(:user) }
|
||||
fab!(:category1) { Fabricate(:category) }
|
||||
fab!(:category2) { Fabricate(:category) }
|
||||
fab!(:category3) { Fabricate(:category) }
|
||||
|
@ -61,7 +59,6 @@ RSpec.describe CurrentUserSerializer do
|
|||
end
|
||||
|
||||
context "#muted_tag" do
|
||||
fab!(:user) { Fabricate(:user) }
|
||||
fab!(:tag) { Fabricate(:tag) }
|
||||
|
||||
let!(:tag_user) do
|
||||
|
@ -79,7 +76,6 @@ RSpec.describe CurrentUserSerializer do
|
|||
end
|
||||
|
||||
context "#second_factor_enabled" do
|
||||
fab!(:user) { Fabricate(:user) }
|
||||
let(:guardian) { Guardian.new(user) }
|
||||
let(:json) { serializer.as_json }
|
||||
|
||||
|
@ -109,8 +105,6 @@ RSpec.describe CurrentUserSerializer do
|
|||
end
|
||||
|
||||
context "#groups" do
|
||||
fab!(:user) { Fabricate(:user) }
|
||||
|
||||
it "should only show visible groups" do
|
||||
Fabricate.build(:group, visibility_level: Group.visibility_levels[:public])
|
||||
hidden_group = Fabricate.build(:group, visibility_level: Group.visibility_levels[:owners])
|
||||
|
@ -128,8 +122,6 @@ RSpec.describe CurrentUserSerializer do
|
|||
end
|
||||
|
||||
context "#has_topic_draft" do
|
||||
fab!(:user) { Fabricate(:user) }
|
||||
|
||||
it "is not included by default" do
|
||||
payload = serializer.as_json
|
||||
expect(payload).not_to have_key(:has_topic_draft)
|
||||
|
@ -207,4 +199,83 @@ RSpec.describe CurrentUserSerializer do
|
|||
expect(json.keys).not_to include :status
|
||||
end
|
||||
end
|
||||
|
||||
describe '#sidebar_tag_names' do
|
||||
fab!(:tag_sidebar_section_link) { Fabricate(:tag_sidebar_section_link, user: user) }
|
||||
fab!(:tag_sidebar_section_link_2) { Fabricate(:tag_sidebar_section_link, user: user) }
|
||||
|
||||
it "is not included when SiteSeting.enable_experimental_sidebar is false" do
|
||||
SiteSetting.enable_experimental_sidebar = false
|
||||
|
||||
json = serializer.as_json
|
||||
|
||||
expect(json[:sidebar_tag_names]).to eq(nil)
|
||||
end
|
||||
|
||||
it "is not included when SiteSeting.tagging_enabled is false" do
|
||||
SiteSetting.enable_experimental_sidebar = true
|
||||
SiteSetting.tagging_enabled = false
|
||||
|
||||
json = serializer.as_json
|
||||
|
||||
expect(json[:sidebar_tag_names]).to eq(nil)
|
||||
end
|
||||
|
||||
it "is not included when experimental sidebar has not been enabled by user" do
|
||||
SiteSetting.enable_experimental_sidebar = true
|
||||
SiteSetting.tagging_enabled = true
|
||||
user.user_option.update!(enable_experimental_sidebar: false)
|
||||
|
||||
json = serializer.as_json
|
||||
|
||||
expect(json[:sidebar_tag_names]).to eq(nil)
|
||||
end
|
||||
|
||||
it "is present when experimental sidebar has been enabled by user" do
|
||||
SiteSetting.enable_experimental_sidebar = true
|
||||
SiteSetting.tagging_enabled = true
|
||||
user.user_option.update!(enable_experimental_sidebar: true)
|
||||
|
||||
json = serializer.as_json
|
||||
|
||||
expect(json[:sidebar_tag_names]).to contain_exactly(
|
||||
tag_sidebar_section_link.linkable.name,
|
||||
tag_sidebar_section_link_2.linkable.name
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#sidebar_category_ids' do
|
||||
fab!(:category_sidebar_section_link) { Fabricate(:category_sidebar_section_link, user: user) }
|
||||
fab!(:category_sidebar_section_link_2) { Fabricate(:category_sidebar_section_link, user: user) }
|
||||
|
||||
it "is not included when SiteSeting.enable_experimental_sidebar is false" do
|
||||
SiteSetting.enable_experimental_sidebar = false
|
||||
|
||||
json = serializer.as_json
|
||||
|
||||
expect(json[:sidebar_category_ids]).to eq(nil)
|
||||
end
|
||||
|
||||
it "is not included when experimental sidebar has not been enabled by user" do
|
||||
SiteSetting.enable_experimental_sidebar = true
|
||||
user.user_option.update!(enable_experimental_sidebar: false)
|
||||
|
||||
json = serializer.as_json
|
||||
|
||||
expect(json[:sidebar_category_ids]).to eq(nil)
|
||||
end
|
||||
|
||||
it "is present when experimental sidebar has been enabled by user" do
|
||||
SiteSetting.enable_experimental_sidebar = true
|
||||
user.user_option.update!(enable_experimental_sidebar: true)
|
||||
|
||||
json = serializer.as_json
|
||||
|
||||
expect(json[:sidebar_category_ids]).to contain_exactly(
|
||||
category_sidebar_section_link.linkable_id,
|
||||
category_sidebar_section_link_2.linkable_id
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue