FEATURE: add user status to user preferences (#18532)

This commit is contained in:
Andrei Prigorshnev 2022-10-12 23:35:25 +04:00 committed by GitHub
parent 231dc10bbd
commit 0fe111e492
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 410 additions and 2 deletions

View File

@ -11,6 +11,7 @@ import getURL from "discourse-common/lib/get-url";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { inject as service } from "@ember/service";
import { next } from "@ember/runloop";
import showModal from "discourse/lib/show-modal";
export default Controller.extend(CanCheckEmails, {
dialog: service(),
@ -22,16 +23,19 @@ export default Controller.extend(CanCheckEmails, {
"title",
"primary_group_id",
"flair_group_id",
"status",
];
this.set("revoking", {});
},
canEditName: setting("enable_names"),
canSelectUserStatus: setting("enable_user_status"),
canSaveUser: true,
newNameInput: null,
newTitleInput: null,
newPrimaryGroupInput: null,
newStatus: null,
revoking: null,
@ -146,6 +150,19 @@ export default Controller.extend(CanCheckEmails, {
});
},
@action
showUserStatusModal(status) {
showModal("user-status", {
title: "user_status.set_custom_status",
modalClass: "user-status",
model: {
status,
saveAction: async (s) => this.set("newStatus", s),
deleteAction: async () => this.set("newStatus", null),
},
});
},
actions: {
save() {
this.set("saved", false);
@ -155,6 +172,7 @@ export default Controller.extend(CanCheckEmails, {
title: this.newTitleInput,
primary_group_id: this.newPrimaryGroupInput,
flair_group_id: this.newFlairGroupId,
status: this.newStatus,
});
return this.model

View File

@ -69,6 +69,7 @@ let userFields = [
"user_notification_schedule",
"sidebar_category_ids",
"sidebar_tag_names",
"status",
];
export function addSaveableUserField(fieldName) {

View File

@ -31,6 +31,7 @@ export default RestrictedUserRoute.extend({
newTitleInput: user.get("title"),
newPrimaryGroupInput: user.get("primary_group_id"),
newFlairGroupId: user.get("flair_group_id"),
newStatus: user.status,
});
},

View File

@ -173,6 +173,24 @@
</div>
{{/if}}
{{#if this.canSelectUserStatus}}
<div class="control-group pref-user-status">
<label class="control-label">{{i18n "user.status.title"}}</label>
<div class="controls">
{{#if this.newStatus}}
<UserStatusMessage @status={{this.newStatus}} @showDescription={{true}} />
{{else}}
<span class="static">{{i18n "user.status.not_set"}}</span>
{{/if}}
<DButton
@action={{action "showUserStatusModal"}}
@actionParam={{this.newStatus}}
@class="btn-default btn-small pad-left"
@icon="pencil-alt"/>
</div>
</div>
{{/if}}
{{#if this.canSelectPrimaryGroup}}
<div class="control-group pref-primary-group">
<label class="control-label">{{i18n "user.primary_group.title"}}</label>

View File

@ -0,0 +1,129 @@
import {
acceptance,
exists,
query,
updateCurrentUser,
} from "discourse/tests/helpers/qunit-helpers";
import { click, fillIn, visit } from "@ember/test-helpers";
import { test } from "qunit";
async function openUserStatusModal() {
await click(".pref-user-status .btn-default");
}
async function pickEmoji(emoji) {
await click(".btn-emoji");
await fillIn(".emoji-picker-content .filter", emoji);
await click(".results .emoji");
}
async function setStatus(status) {
await openUserStatusModal();
await pickEmoji(status.emoji);
await fillIn(".user-status-description", status.description);
await click(".modal-footer .btn-primary"); // save and close modal
}
acceptance("User Profile - Account - User Status", function (needs) {
const username = "eviltrout";
const status = {
emoji: "tooth",
description: "off to dentist",
};
needs.user({ username, status });
test("doesn't render status block if status is disabled in site settings", async function (assert) {
this.siteSettings.enable_user_status = false;
await visit(`/u/${username}/preferences/account`);
assert.notOk(exists(".pref-user-status"));
});
test("renders status block if status is enabled in site settings", async function (assert) {
this.siteSettings.enable_user_status = true;
await visit(`/u/${username}/preferences/account`);
assert.ok(
exists(".pref-user-status .user-status-message"),
"status is shown"
);
assert.ok(
exists(`.pref-user-status .emoji[alt='${status.emoji}']`),
"status emoji is correct"
);
assert.equal(
query(
`.pref-user-status .user-status-message-description`
).innerText.trim(),
status.description,
"status description is correct"
);
});
test("the status modal sets status", async function (assert) {
this.siteSettings.enable_user_status = true;
updateCurrentUser({ status: null });
await visit(`/u/${username}/preferences/account`);
assert.notOk(
exists(".pref-user-status .user-status-message"),
"status isn't shown"
);
await setStatus(status);
assert.ok(
exists(".pref-user-status .user-status-message"),
"status is shown"
);
assert.ok(
exists(`.pref-user-status .emoji[alt='${status.emoji}']`),
"status emoji is correct"
);
assert.equal(
query(
`.pref-user-status .user-status-message-description`
).innerText.trim(),
status.description,
"status description is correct"
);
});
test("the status modal updates status", async function (assert) {
this.siteSettings.enable_user_status = true;
await visit(`/u/${username}/preferences/account`);
const newStatus = { emoji: "surfing_man", description: "surfing" };
await setStatus(newStatus);
assert.ok(
exists(".pref-user-status .user-status-message"),
"status is shown"
);
assert.ok(
exists(`.pref-user-status .emoji[alt='${newStatus.emoji}']`),
"status emoji is correct"
);
assert.equal(
query(
`.pref-user-status .user-status-message-description`
).innerText.trim(),
newStatus.description,
"status description is correct"
);
});
test("the status modal clears status", async function (assert) {
this.siteSettings.enable_user_status = true;
await visit(`/u/${username}/preferences/account`);
await openUserStatusModal();
await click(".btn.delete-status");
assert.notOk(
exists(".pref-user-status .user-status-message"),
"status isn't shown"
);
});
});

View File

@ -1965,6 +1965,10 @@ class UsersController < ApplicationController
end
end
if SiteSetting.enable_user_status
permitted << { status: [:emoji, :description, :ends_at] }
end
result = params
.permit(permitted, theme_ids: [], seen_popups: [])
.reverse_merge(

View File

@ -218,9 +218,17 @@ class UserUpdater
update_sidebar_tag_section_links(attributes[:sidebar_tag_names])
end
if SiteSetting.enable_user_status?
update_user_status(attributes[:status])
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)
saved = (!save_options || user.user_option.save) &&
(user_notification_schedule.nil? || user_notification_schedule.save) &&
user_profile.save &&
user.save
if saved && (name_changed && old_user_name.casecmp(attributes.fetch(:name)) != 0)
StaffActionLogger.new(@actor).log_name_change(
user.id,
@ -341,6 +349,14 @@ class UserUpdater
end
end
def update_user_status(status)
if status.blank?
@user.clear_status!
else
@user.set_status!(status[:description], status[:emoji], status[:ends_at])
end
end
attr_reader :user, :guardian
def format_url(website)

View File

@ -1793,6 +1793,9 @@ en:
title: "Flair"
none: "(none)"
instructions: "icon displayed next to your profile picture"
status:
title: "Custom Status"
not_set: "Not set"
primary_group:
title: "Primary Group"
none: "(none)"

View File

@ -2472,6 +2472,224 @@ RSpec.describe UsersController do
expect(response.status).to eq(400)
end
end
context "with user status" do
context "as a regular user" do
before do
SiteSetting.enable_user_status = true
sign_in(user)
end
it "sets user status" do
status = {
emoji: "tooth",
description: "off to dentist",
}
put "/u/#{user.username}.json", params: {
status: status
}
expect(response.status).to eq(200)
user.reload
expect(user.user_status).not_to be_nil
expect(user.user_status.emoji).to eq(status[:emoji])
expect(user.user_status.description).to eq(status[:description])
end
it "updates user status" do
user.set_status!("off to dentist", "tooth")
user.reload
new_status = {
emoji: "surfing_man",
description: "surfing",
}
put "/u/#{user.username}.json", params: {
status: new_status
}
expect(response.status).to eq(200)
user.reload
expect(user.user_status).not_to be_nil
expect(user.user_status.emoji).to eq(new_status[:emoji])
expect(user.user_status.description).to eq(new_status[:description])
end
it "clears user status" do
user.set_status!("off to dentist", "tooth")
user.reload
put "/u/#{user.username}.json", params: {
status: nil
}
expect(response.status).to eq(200)
user.reload
expect(user.user_status).to be_nil
end
it "can't set status of another user" do
put "/u/#{user1.username}.json", params: {
status: {
emoji: "tooth",
description: "off to dentist",
}
}
expect(response.status).to eq(403)
user1.reload
expect(user1.user_status).to be_nil
end
it "can't update status of another user" do
old_status = {
emoji: "tooth",
description: "off to dentist",
}
user1.set_status!(old_status[:description], old_status[:emoji])
user1.reload
new_status = {
emoji: "surfing_man",
description: "surfing",
}
put "/u/#{user1.username}.json", params: {
status: new_status
}
expect(response.status).to eq(403)
user1.reload
expect(user1.user_status).not_to be_nil
expect(user1.user_status.emoji).to eq(old_status[:emoji])
expect(user1.user_status.description).to eq(old_status[:description])
end
it "can't clear status of another user" do
user1.set_status!("off to dentist", "tooth")
user1.reload
put "/u/#{user1.username}.json", params: {
status: nil
}
expect(response.status).to eq(403)
user1.reload
expect(user1.user_status).not_to be_nil
end
context 'when user status is disabled' do
before do
SiteSetting.enable_user_status = false
end
it "doesn't set user status" do
put "/u/#{user.username}.json", params: {
status: {
emoji: "tooth",
description: "off to dentist",
}
}
expect(response.status).to eq(200)
user.reload
expect(user.user_status).to be_nil
end
it "doesn't update user status" do
old_status = {
emoji: "tooth",
description: "off to dentist",
}
user.set_status!(old_status[:description], old_status[:emoji])
user.reload
new_status = {
emoji: "surfing_man",
description: "surfing",
}
put "/u/#{user.username}.json", params: {
status: new_status
}
expect(response.status).to eq(200)
user.reload
expect(user.user_status).not_to be_nil
expect(user.user_status.emoji).to eq(old_status[:emoji])
expect(user.user_status.description).to eq(old_status[:description])
end
it "doesn't clear user status" do
user.set_status!("off to dentist", "tooth")
user.reload
put "/u/#{user.username}.json", params: {
status: nil
}
expect(response.status).to eq(200)
user.reload
expect(user.user_status).not_to be_nil
end
end
end
context 'as a staff user' do
before do
SiteSetting.enable_user_status = true
sign_in(moderator)
end
it "sets another user's status" do
status = {
emoji: "tooth",
description: "off to dentist",
}
put "/u/#{user.username}.json", params: {
status: status
}
expect(response.status).to eq(200)
user.reload
expect(user.user_status).not_to be_nil
expect(user.user_status.emoji).to eq(status[:emoji])
expect(user.user_status.description).to eq(status[:description])
end
it "updates another user's status" do
user.set_status!("off to dentist", "tooth")
user.reload
new_status = {
emoji: "surfing_man",
description: "surfing",
}
put "/u/#{user.username}.json", params: {
status: new_status
}
expect(response.status).to eq(200)
user.reload
expect(user.user_status).not_to be_nil
expect(user.user_status.emoji).to eq(new_status[:emoji])
expect(user.user_status.description).to eq(new_status[:description])
end
it "clears another user's status" do
user.set_status!("off to dentist", "tooth")
user.reload
put "/u/#{user.username}.json", params: {
status: nil
}
expect(response.status).to eq(200)
user.reload
expect(user.user_status).to be_nil
end
end
end
end
describe '#badge_title' do