FEATURE: Make it possible to reassign posts on the topic level assign modal (#562)
This adds a new dropdown to the topic level assign modal. A topic may contain several assignments, the topic itself may be assigned and also some of the replies may be assigned. With this new dropdown, it's possible to edit all the assignments from this modal. Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
This commit is contained in:
parent
6070678a1c
commit
c1d4633ba4
|
@ -0,0 +1,36 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import Assignment from "./assignment";
|
||||||
|
|
||||||
|
export default class AssignUserForm extends Component {
|
||||||
|
@tracked showValidationErrors = false;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
|
||||||
|
this.args.formApi.submit = this.assign;
|
||||||
|
}
|
||||||
|
|
||||||
|
get assigneeIsEmpty() {
|
||||||
|
return !this.args.model.username && !this.args.model.group_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async assign() {
|
||||||
|
if (this.assigneeIsEmpty) {
|
||||||
|
this.showValidationErrors = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.args.onSubmit();
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Assignment
|
||||||
|
@assignment={{@model}}
|
||||||
|
@onSubmit={{this.assign}}
|
||||||
|
@showValidationErrors={{this.showValidationErrors}}
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
}
|
|
@ -1,56 +0,0 @@
|
||||||
<div class="control-group {{if this.assigneeError 'assignee-error'}}">
|
|
||||||
<label>{{i18n "discourse_assign.assign_modal.assignee_label"}}</label>
|
|
||||||
<AssigneeChooser
|
|
||||||
autocomplete="off"
|
|
||||||
@id="assignee-chooser"
|
|
||||||
@value={{this.assigneeName}}
|
|
||||||
@onChange={{this.assignUsername}}
|
|
||||||
@showUserStatus={{true}}
|
|
||||||
@options={{hash
|
|
||||||
mobilePlacementStrategy="absolute"
|
|
||||||
includeGroups=true
|
|
||||||
customSearchOptions=(hash
|
|
||||||
assignableGroups=true defaultSearchResults=this.taskActions.suggestions
|
|
||||||
)
|
|
||||||
groupMembersOf=this.taskActions.allowedGroups
|
|
||||||
maximum=1
|
|
||||||
tabindex=1
|
|
||||||
expandedOnInsert=(not this.assigneeName)
|
|
||||||
caretUpIcon="search"
|
|
||||||
caretDownIcon="search"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{{#if this.assigneeError}}
|
|
||||||
<span class="error-label">
|
|
||||||
{{d-icon "exclamation-triangle"}}
|
|
||||||
{{i18n "discourse_assign.assign_modal.choose_assignee"}}
|
|
||||||
</span>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#if this.siteSettings.enable_assign_status}}
|
|
||||||
<div class="control-group assign-status">
|
|
||||||
<label>{{i18n "discourse_assign.assign_modal.status_label"}}</label>
|
|
||||||
<ComboBox
|
|
||||||
@id="assign-status"
|
|
||||||
@content={{this.availableStatuses}}
|
|
||||||
@value={{this.status}}
|
|
||||||
@onChange={{fn (mut @model.status)}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<div class="control-group assign-status">
|
|
||||||
<label>
|
|
||||||
{{i18n "discourse_assign.assign_modal.note_label"}} <span
|
|
||||||
class="label-optional"
|
|
||||||
>{{i18n "discourse_assign.assign_modal.optional_label"}}</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<Textarea
|
|
||||||
id="assign-modal-note"
|
|
||||||
@value={{@model.note}}
|
|
||||||
{{on "keydown" this.handleTextAreaKeydown}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
|
@ -1,63 +0,0 @@
|
||||||
import Component from "@glimmer/component";
|
|
||||||
import { tracked } from "@glimmer/tracking";
|
|
||||||
import { action } from "@ember/object";
|
|
||||||
import { inject as service } from "@ember/service";
|
|
||||||
|
|
||||||
export default class AssignUserForm extends Component {
|
|
||||||
@service taskActions;
|
|
||||||
@service siteSettings;
|
|
||||||
@service capabilities;
|
|
||||||
|
|
||||||
@tracked assigneeError = false;
|
|
||||||
@tracked
|
|
||||||
assigneeName = this.args.model.username || this.args.model.group_name;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super(...arguments);
|
|
||||||
|
|
||||||
this.args.formApi.submit = this.assign;
|
|
||||||
}
|
|
||||||
|
|
||||||
get availableStatuses() {
|
|
||||||
return this.siteSettings.assign_statuses
|
|
||||||
.split("|")
|
|
||||||
.map((status) => ({ id: status, name: status }));
|
|
||||||
}
|
|
||||||
|
|
||||||
get status() {
|
|
||||||
return (
|
|
||||||
this.args.model.status || this.siteSettings.assign_statuses.split("|")[0]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
handleTextAreaKeydown(event) {
|
|
||||||
if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
|
|
||||||
this.assign();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
async assign() {
|
|
||||||
if (!this.assigneeName) {
|
|
||||||
this.assigneeError = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.args.onSubmit();
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
assignUsername([name]) {
|
|
||||||
this.assigneeName = name;
|
|
||||||
this.assigneeError = false;
|
|
||||||
|
|
||||||
if (this.taskActions.allowedGroupsForAssignment.includes(name)) {
|
|
||||||
this.args.model.username = null;
|
|
||||||
this.args.model.group_name = name;
|
|
||||||
} else {
|
|
||||||
this.args.model.username = name;
|
|
||||||
this.args.model.group_name = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,134 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { hash } from "@ember/helper";
|
||||||
|
import { TextArea } from "@ember/legacy-built-in-components";
|
||||||
|
import { on } from "@ember/modifier";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
import icon from "discourse-common/helpers/d-icon";
|
||||||
|
import i18n from "discourse-common/helpers/i18n";
|
||||||
|
import ComboBox from "select-kit/components/combo-box";
|
||||||
|
import not from "truth-helpers/helpers/not";
|
||||||
|
import AssigneeChooser from "./assignee-chooser";
|
||||||
|
|
||||||
|
export default class Assignment extends Component {
|
||||||
|
@service siteSettings;
|
||||||
|
@service taskActions;
|
||||||
|
|
||||||
|
get assignee() {
|
||||||
|
return this.args.assignment.username || this.args.assignment.group_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
get status() {
|
||||||
|
return this.args.assignment.status || this.assignStatuses[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
get assignStatuses() {
|
||||||
|
return this.siteSettings.assign_statuses.split("|").filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
get assignStatusOptions() {
|
||||||
|
return this.assignStatuses.map((status) => ({ id: status, name: status }));
|
||||||
|
}
|
||||||
|
|
||||||
|
get assigneeIsEmpty() {
|
||||||
|
return !this.args.assignment.username && !this.args.assignment.group_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
get showAssigneeIeEmptyError() {
|
||||||
|
return this.assigneeIsEmpty && this.args.showValidationErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
handleTextAreaKeydown(event) {
|
||||||
|
if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
|
||||||
|
this.args.onSubmit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
markAsEdited() {
|
||||||
|
this.args.assignment.isEdited = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
setAssignee([newAssignee]) {
|
||||||
|
if (this.taskActions.allowedGroupsForAssignment.includes(newAssignee)) {
|
||||||
|
this.args.assignment.username = null;
|
||||||
|
this.args.assignment.group_name = newAssignee;
|
||||||
|
} else {
|
||||||
|
this.args.assignment.username = newAssignee;
|
||||||
|
this.args.assignment.group_name = null;
|
||||||
|
}
|
||||||
|
this.markAsEdited();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
setStatus(status) {
|
||||||
|
this.args.assignment.status = status;
|
||||||
|
this.markAsEdited();
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="control-group
|
||||||
|
{{if this.showAssigneeIeEmptyError 'assignee-error'}}"
|
||||||
|
>
|
||||||
|
<label>{{i18n "discourse_assign.assign_modal.assignee_label"}}</label>
|
||||||
|
<AssigneeChooser
|
||||||
|
autocomplete="off"
|
||||||
|
@id="assignee-chooser"
|
||||||
|
@value={{this.assignee}}
|
||||||
|
@onChange={{this.setAssignee}}
|
||||||
|
@showUserStatus={{true}}
|
||||||
|
@options={{hash
|
||||||
|
mobilePlacementStrategy="absolute"
|
||||||
|
includeGroups=true
|
||||||
|
customSearchOptions=(hash
|
||||||
|
assignableGroups=true
|
||||||
|
defaultSearchResults=this.taskActions.suggestions
|
||||||
|
)
|
||||||
|
groupMembersOf=this.taskActions.allowedGroups
|
||||||
|
maximum=1
|
||||||
|
tabindex=1
|
||||||
|
expandedOnInsert=(not this.assignee)
|
||||||
|
caretUpIcon="search"
|
||||||
|
caretDownIcon="search"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{{#if this.showAssigneeIeEmptyError}}
|
||||||
|
<span class="error-label">
|
||||||
|
{{icon "exclamation-triangle"}}
|
||||||
|
{{i18n "discourse_assign.assign_modal.choose_assignee"}}
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if this.siteSettings.enable_assign_status}}
|
||||||
|
<div class="control-group assign-status">
|
||||||
|
<label>{{i18n "discourse_assign.assign_modal.status_label"}}</label>
|
||||||
|
<ComboBox
|
||||||
|
@id="assign-status"
|
||||||
|
@content={{this.assignStatusOptions}}
|
||||||
|
@value={{this.status}}
|
||||||
|
@onChange={{this.setStatus}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<div class="control-group assign-status">
|
||||||
|
<label>
|
||||||
|
{{i18n "discourse_assign.assign_modal.note_label"}} <span
|
||||||
|
class="label-optional"
|
||||||
|
>{{i18n "discourse_assign.assign_modal.optional_label"}}</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<TextArea
|
||||||
|
id="assign-modal-note"
|
||||||
|
@value={{@assignment.note}}
|
||||||
|
{{on "keydown" this.handleTextAreaKeydown}}
|
||||||
|
{{on "input" this.markAsEdited}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
import DButton from "discourse/components/d-button";
|
||||||
|
import DModal from "discourse/components/d-modal";
|
||||||
|
import DModalCancel from "discourse/components/d-modal-cancel";
|
||||||
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
import I18n from "I18n";
|
||||||
|
import TopicAssignments from "../topic-assignments";
|
||||||
|
|
||||||
|
export default class EditTopicAssignments extends Component {
|
||||||
|
@service taskActions;
|
||||||
|
|
||||||
|
@tracked assignments = this.topic.assignments();
|
||||||
|
|
||||||
|
get title() {
|
||||||
|
if (this.topic.isAssigned() || this.topic.hasAssignedPosts()) {
|
||||||
|
return I18n.t("edit_assignments_modal.title");
|
||||||
|
} else {
|
||||||
|
return I18n.t("discourse_assign.assign_modal.title");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get topic() {
|
||||||
|
return this.args.model.topic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async submit() {
|
||||||
|
this.args.closeModal();
|
||||||
|
try {
|
||||||
|
await this.#assign();
|
||||||
|
} catch (error) {
|
||||||
|
popupAjaxError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #assign() {
|
||||||
|
for (const assignment of this.assignments) {
|
||||||
|
if (assignment.isEdited) {
|
||||||
|
await this.taskActions.putAssignment(assignment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DModal class="assign" @title={{this.title}} @closeModal={{@closeModal}}>
|
||||||
|
<:body>
|
||||||
|
<TopicAssignments @assignments={{this.assignments}} />
|
||||||
|
</:body>
|
||||||
|
<:footer>
|
||||||
|
<DButton
|
||||||
|
class="btn-primary"
|
||||||
|
@action={{this.submit}}
|
||||||
|
@label={{if
|
||||||
|
this.model.reassign
|
||||||
|
"discourse_assign.reassign.title"
|
||||||
|
"discourse_assign.assign_modal.assign"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DModalCancel @close={{@closeModal}} />
|
||||||
|
</:footer>
|
||||||
|
</DModal>
|
||||||
|
</template>
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import i18n from "discourse-common/helpers/i18n";
|
||||||
|
import I18n from "I18n";
|
||||||
|
import ComboBox from "select-kit/components/combo-box";
|
||||||
|
import Assignment from "./assignment";
|
||||||
|
|
||||||
|
export default class TopicAssignments extends Component {
|
||||||
|
@tracked selectedAssignment = this.args.assignments.find((a) => a.id === 0);
|
||||||
|
|
||||||
|
get assignmentOptions() {
|
||||||
|
return this.args.assignments.map((a) => this.#toComboBoxOption(a));
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
selectAssignment(id) {
|
||||||
|
this.selectedAssignment = this.args.assignments.find((a) => a.id === id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#toComboBoxOption(assignment) {
|
||||||
|
const option = { id: assignment.id };
|
||||||
|
if (assignment.targetType === "Topic") {
|
||||||
|
option.name = I18n.t("edit_assignments_modal.topic");
|
||||||
|
} else {
|
||||||
|
option.name = `${I18n.t("edit_assignments_modal.post")} #${
|
||||||
|
assignment.postNumber
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="control-group target">
|
||||||
|
<label>{{i18n "discourse_assign.assign_modal.assignment_label"}}</label>
|
||||||
|
<ComboBox
|
||||||
|
@value={{this.selectedAssignment.id}}
|
||||||
|
@content={{this.assignmentOptions}}
|
||||||
|
@onChange={{this.selectAssignment}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Assignment
|
||||||
|
@assignment={{this.selectedAssignment}}
|
||||||
|
@onSubmit={{this.submit}}
|
||||||
|
@showValidationErrors={{false}}
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ import { htmlSafe } from "@ember/template";
|
||||||
import { renderAvatar } from "discourse/helpers/user-avatar";
|
import { renderAvatar } from "discourse/helpers/user-avatar";
|
||||||
import { iconHTML } from "discourse-common/lib/icon-library";
|
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
|
import EditTopicAssignments from "../components/modal/edit-topic-assignments";
|
||||||
|
|
||||||
const DEPENDENT_KEYS = [
|
const DEPENDENT_KEYS = [
|
||||||
"topic.assigned_to_user",
|
"topic.assigned_to_user",
|
||||||
|
@ -22,6 +23,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
const taskActions = getOwner(this).lookup("service:task-actions");
|
const taskActions = getOwner(this).lookup("service:task-actions");
|
||||||
|
const modal = getOwner(this).lookup("service:modal");
|
||||||
const firstPostId = this.topic.postStream.firstPostId;
|
const firstPostId = this.topic.postStream.firstPostId;
|
||||||
|
|
||||||
switch (id) {
|
switch (id) {
|
||||||
|
@ -42,9 +44,10 @@ export default {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "reassign": {
|
case "reassign": {
|
||||||
await taskActions.showAssignModal(this.topic, {
|
await modal.show(EditTopicAssignments, {
|
||||||
targetType: "Topic",
|
model: {
|
||||||
isAssigned: this.topic.isAssigned(),
|
topic: this.topic,
|
||||||
|
},
|
||||||
onSuccess: () =>
|
onSuccess: () =>
|
||||||
this.appEvents.trigger("post-stream:refresh", { id: firstPostId }),
|
this.appEvents.trigger("post-stream:refresh", { id: firstPostId }),
|
||||||
});
|
});
|
||||||
|
@ -64,6 +67,7 @@ export default {
|
||||||
noneItem() {
|
noneItem() {
|
||||||
return topicLevelUnassignButton(this.topic.uniqueAssignees());
|
return topicLevelUnassignButton(this.topic.uniqueAssignees());
|
||||||
},
|
},
|
||||||
|
|
||||||
content() {
|
content() {
|
||||||
const content = [];
|
const content = [];
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,9 @@ import discourseComputed from "discourse-common/utils/decorators";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import BulkAssign from "../components/bulk-actions/assign-user";
|
import BulkAssign from "../components/bulk-actions/assign-user";
|
||||||
import BulkActionsAssignUser from "../components/bulk-actions/bulk-assign-user";
|
import BulkActionsAssignUser from "../components/bulk-actions/bulk-assign-user";
|
||||||
|
import EditTopicAssignments from "../components/modal/edit-topic-assignments";
|
||||||
import TopicLevelAssignMenu from "../components/topic-level-assign-menu";
|
import TopicLevelAssignMenu from "../components/topic-level-assign-menu";
|
||||||
|
import { extendTopicModel } from "../models/topic";
|
||||||
|
|
||||||
const PLUGIN_ID = "discourse-assign";
|
const PLUGIN_ID = "discourse-assign";
|
||||||
|
|
||||||
|
@ -38,50 +40,6 @@ function defaultTitle(topic) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function extendTopicModel(api) {
|
|
||||||
api.modifyClass("model:topic", {
|
|
||||||
pluginId: PLUGIN_ID,
|
|
||||||
|
|
||||||
assignees() {
|
|
||||||
const result = [];
|
|
||||||
|
|
||||||
if (this.assigned_to_user) {
|
|
||||||
result.push(this.assigned_to_user);
|
|
||||||
}
|
|
||||||
|
|
||||||
const postAssignees = this.assignedPosts().map((p) => p.assigned_to);
|
|
||||||
result.push(...postAssignees);
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
|
|
||||||
uniqueAssignees() {
|
|
||||||
const map = new Map();
|
|
||||||
this.assignees().forEach((user) => map.set(user.username, user));
|
|
||||||
return [...map.values()];
|
|
||||||
},
|
|
||||||
|
|
||||||
assignedPosts() {
|
|
||||||
if (!this.indirectly_assigned_to) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.values(this.indirectly_assigned_to);
|
|
||||||
},
|
|
||||||
|
|
||||||
isAssigned() {
|
|
||||||
return this.assigned_to_user || this.assigned_to_group;
|
|
||||||
},
|
|
||||||
|
|
||||||
isAssignedTo(user) {
|
|
||||||
return this.assigned_to_user?.username === user.username;
|
|
||||||
},
|
|
||||||
|
|
||||||
hasAssignedPosts() {
|
|
||||||
return !!this.assignedPosts().length;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerTopicFooterButtons(api) {
|
function registerTopicFooterButtons(api) {
|
||||||
registerTopicFooterDropdown(TopicLevelAssignMenu);
|
registerTopicFooterDropdown(TopicLevelAssignMenu);
|
||||||
|
|
||||||
|
@ -110,6 +68,7 @@ function registerTopicFooterButtons(api) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const taskActions = getOwner(this).lookup("service:task-actions");
|
const taskActions = getOwner(this).lookup("service:task-actions");
|
||||||
|
const modal = getOwner(this).lookup("service:modal");
|
||||||
|
|
||||||
if (this.topic.isAssigned()) {
|
if (this.topic.isAssigned()) {
|
||||||
this.set("topic.assigned_to_user", null);
|
this.set("topic.assigned_to_user", null);
|
||||||
|
@ -121,7 +80,10 @@ function registerTopicFooterButtons(api) {
|
||||||
id: this.topic.postStream.firstPostId,
|
id: this.topic.postStream.firstPostId,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await taskActions.showAssignModal(this.topic, {
|
await modal.show(EditTopicAssignments, {
|
||||||
|
model: {
|
||||||
|
topic: this.topic,
|
||||||
|
},
|
||||||
onSuccess: () =>
|
onSuccess: () =>
|
||||||
this.appEvents.trigger("post-stream:refresh", {
|
this.appEvents.trigger("post-stream:refresh", {
|
||||||
id: this.topic.postStream.firstPostId,
|
id: this.topic.postStream.firstPostId,
|
||||||
|
@ -723,6 +685,9 @@ function initialize(api) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.appEvents.trigger("header:update-topic", topic);
|
this.appEvents.trigger("header:update-topic", topic);
|
||||||
|
this.appEvents.trigger("post-stream:refresh", {
|
||||||
|
id: topic.postStream.posts[0].id,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -833,7 +798,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
withPluginApi("0.13.0", (api) => {
|
withPluginApi("0.13.0", (api) => {
|
||||||
extendTopicModel(api);
|
extendTopicModel(api, PLUGIN_ID);
|
||||||
initialize(api);
|
initialize(api);
|
||||||
registerTopicFooterButtons(api);
|
registerTopicFooterButtons(api);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import EmberObject from "@ember/object";
|
||||||
|
|
||||||
|
export class Assignment extends EmberObject {
|
||||||
|
static fromTopic(topic) {
|
||||||
|
const assignment = new Assignment();
|
||||||
|
assignment.id = 0;
|
||||||
|
assignment.username = topic.assigned_to_user?.username;
|
||||||
|
assignment.groupName = topic.assigned_to_group?.name;
|
||||||
|
assignment.status = topic.assignment_status;
|
||||||
|
assignment.note = topic.assignment_note;
|
||||||
|
assignment.targetId = topic.id;
|
||||||
|
assignment.targetType = "Topic";
|
||||||
|
return assignment;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromPost(post) {
|
||||||
|
const assignment = new Assignment();
|
||||||
|
assignment.username = post.assigned_to.username;
|
||||||
|
assignment.groupName = post.assigned_to.name;
|
||||||
|
assignment.status = post.assignment_status;
|
||||||
|
assignment.note = post.assignment_note;
|
||||||
|
assignment.targetId = post.postId;
|
||||||
|
assignment.targetType = "Post";
|
||||||
|
assignment.postNumber = post.post_number;
|
||||||
|
assignment.id = post.post_number;
|
||||||
|
return assignment;
|
||||||
|
}
|
||||||
|
|
||||||
|
// to-do rename to groupName, some components use both this model
|
||||||
|
// and models from server, that's why we have to call it "group_name" now
|
||||||
|
@tracked group_name;
|
||||||
|
@tracked username;
|
||||||
|
@tracked status;
|
||||||
|
@tracked note;
|
||||||
|
targetId;
|
||||||
|
targetType;
|
||||||
|
postNumber;
|
||||||
|
isEdited = false;
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { Assignment } from "./assignment";
|
||||||
|
|
||||||
|
export function extendTopicModel(api, pluginId) {
|
||||||
|
api.modifyClass("model:topic", {
|
||||||
|
pluginId,
|
||||||
|
|
||||||
|
assignees() {
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
if (this.assigned_to_user) {
|
||||||
|
result.push(this.assigned_to_user);
|
||||||
|
}
|
||||||
|
|
||||||
|
const postAssignees = this.assignedPosts().map((p) => p.assigned_to);
|
||||||
|
result.push(...postAssignees);
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
uniqueAssignees() {
|
||||||
|
const map = new Map();
|
||||||
|
this.assignees().forEach((user) => map.set(user.username, user));
|
||||||
|
return [...map.values()];
|
||||||
|
},
|
||||||
|
|
||||||
|
assignedPosts() {
|
||||||
|
if (!this.indirectly_assigned_to) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.entries(this.indirectly_assigned_to).map(([key, value]) => {
|
||||||
|
value.postId = key;
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
assignments() {
|
||||||
|
return [this.topicAssignment(), ...this.postAssignments()].compact();
|
||||||
|
},
|
||||||
|
|
||||||
|
postAssignments() {
|
||||||
|
if (!this.indirectly_assigned_to) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.entries(this.indirectly_assigned_to).map(([key, value]) => {
|
||||||
|
value.postId = key;
|
||||||
|
return Assignment.fromPost(value);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
topicAssignment() {
|
||||||
|
return Assignment.fromTopic(this);
|
||||||
|
},
|
||||||
|
|
||||||
|
isAssigned() {
|
||||||
|
return this.assigned_to_user || this.assigned_to_group;
|
||||||
|
},
|
||||||
|
|
||||||
|
isAssignedTo(user) {
|
||||||
|
return this.assigned_to_user?.username === user.username;
|
||||||
|
},
|
||||||
|
|
||||||
|
hasAssignedPosts() {
|
||||||
|
return !!this.postAssignments().length;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
|
@ -115,4 +115,18 @@ export default class TaskActions extends Service {
|
||||||
popupAjaxError(error);
|
popupAjaxError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async putAssignment(assignment) {
|
||||||
|
await ajax("/assign/assign", {
|
||||||
|
type: "PUT",
|
||||||
|
data: {
|
||||||
|
username: assignment.username,
|
||||||
|
group_name: assignment.group_name,
|
||||||
|
target_id: assignment.targetId,
|
||||||
|
target_type: assignment.targetType,
|
||||||
|
note: assignment.note,
|
||||||
|
status: assignment.status,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,6 +73,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.target {
|
||||||
|
.combo-box {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.email-group-user-chooser {
|
.email-group-user-chooser {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
|
|
@ -69,7 +69,9 @@ en:
|
||||||
reassign_title: "Reassign Topic"
|
reassign_title: "Reassign Topic"
|
||||||
assign: "Assign"
|
assign: "Assign"
|
||||||
assignee_label: Assignee
|
assignee_label: Assignee
|
||||||
|
assignment_label: Assignment
|
||||||
choose_assignee: Choose a user to assign.
|
choose_assignee: Choose a user to assign.
|
||||||
|
edit_assignments_title: "Edit Assignments"
|
||||||
note_label: Note
|
note_label: Note
|
||||||
optional_label: "(optional)"
|
optional_label: "(optional)"
|
||||||
status_label: Status
|
status_label: Status
|
||||||
|
@ -177,3 +179,7 @@ en:
|
||||||
assign_list_with_unread:
|
assign_list_with_unread:
|
||||||
one: "Assign list - %{count} unread assignment"
|
one: "Assign list - %{count} unread assignment"
|
||||||
other: "Assign list - %{count} unread assignments"
|
other: "Assign list - %{count} unread assignments"
|
||||||
|
edit_assignments_modal:
|
||||||
|
title: "Edit assignments"
|
||||||
|
topic: "Topic"
|
||||||
|
post: "Post"
|
||||||
|
|
|
@ -0,0 +1,169 @@
|
||||||
|
import { click, settled, visit } from "@ember/test-helpers";
|
||||||
|
import { test } from "qunit";
|
||||||
|
import {
|
||||||
|
acceptance,
|
||||||
|
publishToMessageBus,
|
||||||
|
} from "discourse/tests/helpers/qunit-helpers";
|
||||||
|
import topicWithAssignedPosts from "../fixtures/topic-with-assigned-posts";
|
||||||
|
|
||||||
|
const topic = topicWithAssignedPosts();
|
||||||
|
const firstReply = topic.post_stream.posts[1];
|
||||||
|
const secondReply = topic.post_stream.posts[2];
|
||||||
|
const new_assignee_1 = "user_1";
|
||||||
|
const new_assignee_2 = "user_2";
|
||||||
|
|
||||||
|
acceptance("Discourse Assign | Edit assignments modal", function (needs) {
|
||||||
|
needs.user({ can_assign: true });
|
||||||
|
needs.settings({
|
||||||
|
assign_enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
needs.pretender((server, helper) => {
|
||||||
|
server.get("/t/44.json", () => helper.response(topic));
|
||||||
|
server.put("/assign/assign", () => {
|
||||||
|
return helper.response({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
const suggestions = [
|
||||||
|
{ username: new_assignee_1 },
|
||||||
|
{ username: new_assignee_2 },
|
||||||
|
];
|
||||||
|
server.get("/assign/suggestions", () =>
|
||||||
|
helper.response({
|
||||||
|
assign_allowed_for_groups: [],
|
||||||
|
suggestions,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
server.get("/u/search/users", () =>
|
||||||
|
helper.response({
|
||||||
|
users: suggestions,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reassigning topic", async function (assert) {
|
||||||
|
await visit("/t/assignment-topic/44");
|
||||||
|
await openModal();
|
||||||
|
await selectAssignee(new_assignee_1);
|
||||||
|
|
||||||
|
await submitModal();
|
||||||
|
await receiveAssignedMessage(topic, new_assignee_1);
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom(".post-stream article#post_1 .assigned-to .assigned-to--user a")
|
||||||
|
.hasText(new_assignee_1, "The topic is assigned to a new assignee");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reassigning posts", async function (assert) {
|
||||||
|
await visit("/t/assignment-topic/44");
|
||||||
|
await openModal();
|
||||||
|
|
||||||
|
await selectPost(1);
|
||||||
|
await expandAssigneeChooser();
|
||||||
|
await selectAssignee(new_assignee_1);
|
||||||
|
|
||||||
|
await selectPost(2);
|
||||||
|
await expandAssigneeChooser();
|
||||||
|
await selectAssignee(new_assignee_2);
|
||||||
|
|
||||||
|
await submitModal();
|
||||||
|
await receiveAssignedMessage(firstReply, new_assignee_1);
|
||||||
|
await receiveAssignedMessage(secondReply, new_assignee_2);
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom(".post-stream article#post_2 .assigned-to .assigned-to-username")
|
||||||
|
.hasText(new_assignee_1, "The first reply is assigned to a new assignee");
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom(".post-stream article#post_3 .assigned-to .assigned-to-username")
|
||||||
|
.hasText(
|
||||||
|
new_assignee_2,
|
||||||
|
"The second reply is assigned to a new assignee"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reassigning topic and posts in one go", async function (assert) {
|
||||||
|
await visit("/t/assignment-topic/44");
|
||||||
|
await openModal();
|
||||||
|
await selectAssignee(new_assignee_1);
|
||||||
|
|
||||||
|
await selectPost(1);
|
||||||
|
await expandAssigneeChooser();
|
||||||
|
await selectAssignee(new_assignee_2);
|
||||||
|
|
||||||
|
await selectPost(2);
|
||||||
|
await expandAssigneeChooser();
|
||||||
|
await selectAssignee(new_assignee_2);
|
||||||
|
|
||||||
|
await submitModal();
|
||||||
|
await receiveAssignedMessage(topic, new_assignee_1);
|
||||||
|
await receiveAssignedMessage(firstReply, new_assignee_2);
|
||||||
|
await receiveAssignedMessage(secondReply, new_assignee_2);
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom(".post-stream article#post_1 .assigned-to .assigned-to--user a")
|
||||||
|
.hasText(new_assignee_1, "The topic is assigned to a new assignee");
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom(".post-stream article#post_2 .assigned-to .assigned-to-username")
|
||||||
|
.hasText(new_assignee_2, "The first reply is assigned to a new assignee");
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom(".post-stream article#post_3 .assigned-to .assigned-to-username")
|
||||||
|
.hasText(
|
||||||
|
new_assignee_2,
|
||||||
|
"The second reply is assigned to a new assignee"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function expandAssigneeChooser() {
|
||||||
|
await click(
|
||||||
|
".modal-container #assignee-chooser-header .select-kit-header-wrapper"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openModal() {
|
||||||
|
await click("#topic-footer-dropdown-reassign .btn");
|
||||||
|
await click(`li[data-value='reassign']`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo remove this function and all calls to it after we start updating UI right away
|
||||||
|
// (there is no need to wait for a message bus message in the browser of a user
|
||||||
|
// who did reassignment, but we do that at the moment)
|
||||||
|
async function receiveAssignedMessage(target, newAssignee) {
|
||||||
|
const targetIsAPost = !!target.topic_id;
|
||||||
|
|
||||||
|
let topicId, postId;
|
||||||
|
if (targetIsAPost) {
|
||||||
|
topicId = target.topic_id;
|
||||||
|
postId = target.id;
|
||||||
|
} else {
|
||||||
|
topicId = target.id;
|
||||||
|
postId = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await publishToMessageBus("/staff/topic-assignment", {
|
||||||
|
type: "assigned",
|
||||||
|
topic_id: topicId,
|
||||||
|
post_id: postId,
|
||||||
|
assigned_type: "User",
|
||||||
|
assigned_to: {
|
||||||
|
username: newAssignee,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await settled();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectPost(number) {
|
||||||
|
await click(".target .single-select .select-kit-header-wrapper");
|
||||||
|
await click(`li[title='Post #${number}']`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectAssignee(username) {
|
||||||
|
await click(`.email-group-user-chooser-row[data-value='${username}']`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitModal() {
|
||||||
|
await click(".d-modal__footer .btn-primary");
|
||||||
|
}
|
||||||
|
});
|
|
@ -5,7 +5,7 @@ import {
|
||||||
publishToMessageBus,
|
publishToMessageBus,
|
||||||
updateCurrentUser,
|
updateCurrentUser,
|
||||||
} from "discourse/tests/helpers/qunit-helpers";
|
} from "discourse/tests/helpers/qunit-helpers";
|
||||||
import topicWithAssignedPost from "../fixtures/topic-with-assigned-post";
|
import topicWithAssignedPosts from "../fixtures/topic-with-assigned-posts";
|
||||||
|
|
||||||
const new_assignee_username = "new_assignee";
|
const new_assignee_username = "new_assignee";
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ const selectors = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const topic = topicWithAssignedPost();
|
const topic = topicWithAssignedPosts();
|
||||||
const post = topic.post_stream.posts[1];
|
const post = topic.post_stream.posts[1];
|
||||||
|
|
||||||
acceptance("Discourse Assign | Post popup menu", function (needs) {
|
acceptance("Discourse Assign | Post popup menu", function (needs) {
|
||||||
|
|
|
@ -5,9 +5,9 @@ import {
|
||||||
publishToMessageBus,
|
publishToMessageBus,
|
||||||
updateCurrentUser,
|
updateCurrentUser,
|
||||||
} from "discourse/tests/helpers/qunit-helpers";
|
} from "discourse/tests/helpers/qunit-helpers";
|
||||||
import topicWithAssignedPost from "../fixtures/topic-with-assigned-post";
|
import topicWithAssignedPosts from "../fixtures/topic-with-assigned-posts";
|
||||||
|
|
||||||
const topic = topicWithAssignedPost();
|
const topic = topicWithAssignedPosts();
|
||||||
const post = topic.post_stream.posts[1];
|
const post = topic.post_stream.posts[1];
|
||||||
|
|
||||||
acceptance("Discourse Assign | Topic level assign menu", function (needs) {
|
acceptance("Discourse Assign | Topic level assign menu", function (needs) {
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
import topicFixtures from "discourse/tests/fixtures/topic";
|
|
||||||
import { cloneJSON } from "discourse-common/lib/object";
|
|
||||||
|
|
||||||
export default function topicWithAssignedPost() {
|
|
||||||
const username = "eviltrout";
|
|
||||||
const topic = cloneJSON(topicFixtures["/t/28830/1.json"]);
|
|
||||||
const secondPost = topic.post_stream.posts[1];
|
|
||||||
|
|
||||||
topic["indirectly_assigned_to"] = {
|
|
||||||
[secondPost.id]: {
|
|
||||||
assigned_to: {
|
|
||||||
username,
|
|
||||||
},
|
|
||||||
post_number: 1,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
secondPost["assigned_to_user"] = { username };
|
|
||||||
|
|
||||||
return topic;
|
|
||||||
}
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
import topicFixtures from "discourse/tests/fixtures/topic";
|
||||||
|
import { cloneJSON } from "discourse-common/lib/object";
|
||||||
|
|
||||||
|
export default function topicWithAssignedPosts() {
|
||||||
|
const username = "eviltrout";
|
||||||
|
const topic = cloneJSON(topicFixtures["/t/28830/1.json"]);
|
||||||
|
const firstReply = topic.post_stream.posts[1];
|
||||||
|
const secondReply = topic.post_stream.posts[2];
|
||||||
|
|
||||||
|
topic["indirectly_assigned_to"] = {
|
||||||
|
[firstReply.id]: {
|
||||||
|
assigned_to: {
|
||||||
|
username,
|
||||||
|
},
|
||||||
|
post_number: 1,
|
||||||
|
},
|
||||||
|
[secondReply.id]: {
|
||||||
|
assigned_to: {
|
||||||
|
username,
|
||||||
|
},
|
||||||
|
post_number: 2,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
firstReply["assigned_to_user"] = { username };
|
||||||
|
secondReply["assigned_to_user"] = { username };
|
||||||
|
|
||||||
|
return topic;
|
||||||
|
}
|
Loading…
Reference in New Issue