Skip to content

Commit 0f22e6e

Browse files
committed
FEATURE: Make it possible to reassign posts on the topic level assign modal
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 now to edit all the assignments in one go.
1 parent 6070678 commit 0f22e6e

18 files changed

+637
-192
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import Component from "@glimmer/component";
2+
import { tracked } from "@glimmer/tracking";
3+
import { action } from "@ember/object";
4+
import Assignment from "./assignment";
5+
6+
export default class AssignUserForm extends Component {
7+
@tracked showValidationErrors = false;
8+
9+
constructor() {
10+
super(...arguments);
11+
12+
this.args.formApi.submit = this.assign;
13+
}
14+
15+
get assigneeIsEmpty() {
16+
return !this.args.model.username && !this.args.model.group_name;
17+
}
18+
19+
@action
20+
async assign() {
21+
if (this.assigneeIsEmpty) {
22+
this.showValidationErrors = true;
23+
return;
24+
}
25+
26+
await this.args.onSubmit();
27+
}
28+
29+
<template>
30+
<Assignment
31+
@assignment={{@model}}
32+
@onSubmit={{this.assign}}
33+
@showValidationErrors={{this.showValidationErrors}}
34+
/>
35+
</template>
36+
}

assets/javascripts/discourse/components/assign-user-form.hbs

Lines changed: 0 additions & 56 deletions
This file was deleted.

assets/javascripts/discourse/components/assign-user-form.js

Lines changed: 0 additions & 63 deletions
This file was deleted.
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import Component from "@glimmer/component";
2+
import { hash } from "@ember/helper";
3+
import { TextArea } from "@ember/legacy-built-in-components";
4+
import { on } from "@ember/modifier";
5+
import { action } from "@ember/object";
6+
import { inject as service } from "@ember/service";
7+
import icon from "discourse-common/helpers/d-icon";
8+
import i18n from "discourse-common/helpers/i18n";
9+
import ComboBox from "select-kit/components/combo-box";
10+
import not from "truth-helpers/helpers/not";
11+
import AssigneeChooser from "./assignee-chooser";
12+
13+
export default class Assignment extends Component {
14+
@service siteSettings;
15+
@service taskActions;
16+
17+
get assignee() {
18+
return this.args.assignment.username || this.args.assignment.group_name;
19+
}
20+
21+
get status() {
22+
return this.args.assignment.status || this.assignStatuses[0];
23+
}
24+
25+
get assignStatuses() {
26+
return this.siteSettings.assign_statuses.split("|").filter(Boolean);
27+
}
28+
29+
get assignStatusOptions() {
30+
return this.assignStatuses.map((status) => ({ id: status, name: status }));
31+
}
32+
33+
get assigneeIsEmpty() {
34+
return !this.args.assignment.username && !this.args.assignment.group_name;
35+
}
36+
37+
get showAssigneeIeEmptyError() {
38+
return this.assigneeIsEmpty && this.args.showValidationErrors;
39+
}
40+
41+
@action
42+
handleTextAreaKeydown(event) {
43+
if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
44+
this.args.onSubmit();
45+
}
46+
}
47+
48+
@action
49+
markAsEdited() {
50+
this.args.assignment.isEdited = true;
51+
}
52+
53+
@action
54+
setAssignee([newAssignee]) {
55+
if (this.taskActions.allowedGroupsForAssignment.includes(newAssignee)) {
56+
this.args.assignment.username = null;
57+
this.args.assignment.group_name = newAssignee;
58+
} else {
59+
this.args.assignment.username = newAssignee;
60+
this.args.assignment.group_name = null;
61+
}
62+
this.markAsEdited();
63+
}
64+
65+
@action
66+
setStatus(status) {
67+
this.args.assignment.status = status;
68+
this.markAsEdited();
69+
}
70+
71+
<template>
72+
<div
73+
class="control-group
74+
{{if this.showAssigneeIeEmptyError 'assignee-error'}}"
75+
>
76+
<label>{{i18n "discourse_assign.assign_modal.assignee_label"}}</label>
77+
<AssigneeChooser
78+
autocomplete="off"
79+
@id="assignee-chooser"
80+
@value={{this.assignee}}
81+
@onChange={{this.setAssignee}}
82+
@showUserStatus={{true}}
83+
@options={{hash
84+
mobilePlacementStrategy="absolute"
85+
includeGroups=true
86+
customSearchOptions=(hash
87+
assignableGroups=true
88+
defaultSearchResults=this.taskActions.suggestions
89+
)
90+
groupMembersOf=this.taskActions.allowedGroups
91+
maximum=1
92+
tabindex=1
93+
expandedOnInsert=(not this.assignee)
94+
caretUpIcon="search"
95+
caretDownIcon="search"
96+
}}
97+
/>
98+
99+
{{#if this.showAssigneeIeEmptyError}}
100+
<span class="error-label">
101+
{{icon "exclamation-triangle"}}
102+
{{i18n "discourse_assign.assign_modal.choose_assignee"}}
103+
</span>
104+
{{/if}}
105+
</div>
106+
107+
{{#if this.siteSettings.enable_assign_status}}
108+
<div class="control-group assign-status">
109+
<label>{{i18n "discourse_assign.assign_modal.status_label"}}</label>
110+
<ComboBox
111+
@id="assign-status"
112+
@content={{this.assignStatusOptions}}
113+
@value={{this.status}}
114+
@onChange={{this.setStatus}}
115+
/>
116+
</div>
117+
{{/if}}
118+
119+
<div class="control-group assign-status">
120+
<label>
121+
{{i18n "discourse_assign.assign_modal.note_label"}}&nbsp;<span
122+
class="label-optional"
123+
>{{i18n "discourse_assign.assign_modal.optional_label"}}</span>
124+
</label>
125+
126+
<TextArea
127+
id="assign-modal-note"
128+
@value={{@assignment.note}}
129+
{{on "keydown" this.handleTextAreaKeydown}}
130+
{{on "input" this.markAsEdited}}
131+
/>
132+
</div>
133+
</template>
134+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import Component from "@glimmer/component";
2+
import { tracked } from "@glimmer/tracking";
3+
import { action } from "@ember/object";
4+
import { inject as service } from "@ember/service";
5+
import DButton from "discourse/components/d-button";
6+
import DModal from "discourse/components/d-modal";
7+
import DModalCancel from "discourse/components/d-modal-cancel";
8+
import { popupAjaxError } from "discourse/lib/ajax-error";
9+
import I18n from "I18n";
10+
import TopicAssignments from "../topic-assignments";
11+
12+
export default class EditTopicAssignments extends Component {
13+
@service taskActions;
14+
15+
@tracked assignments = this.topic.assignments();
16+
17+
get title() {
18+
if (this.topic.isAssigned() || this.topic.hasAssignedPosts()) {
19+
return I18n.t("edit_assignments_modal.title");
20+
} else {
21+
return I18n.t("discourse_assign.assign_modal.title");
22+
}
23+
}
24+
25+
get topic() {
26+
return this.args.model.topic;
27+
}
28+
29+
@action
30+
async submit() {
31+
this.args.closeModal();
32+
try {
33+
await this.#assign();
34+
} catch (error) {
35+
popupAjaxError(error);
36+
}
37+
}
38+
39+
async #assign() {
40+
for (const assignment of this.assignments) {
41+
if (assignment.isEdited) {
42+
await this.taskActions.putAssignment(assignment);
43+
}
44+
}
45+
}
46+
47+
<template>
48+
<DModal class="assign" @title={{this.title}} @closeModal={{@closeModal}}>
49+
<:body>
50+
<TopicAssignments @assignments={{this.assignments}} />
51+
</:body>
52+
<:footer>
53+
<DButton
54+
class="btn-primary"
55+
@action={{this.submit}}
56+
@label={{if
57+
this.model.reassign
58+
"discourse_assign.reassign.title"
59+
"discourse_assign.assign_modal.assign"
60+
}}
61+
/>
62+
63+
<DModalCancel @close={{@closeModal}} />
64+
</:footer>
65+
</DModal>
66+
</template>
67+
}

0 commit comments

Comments
 (0)