Feature: Mass award badge (#8694)

* UI: Mass grant a badge from the admin ui

* Send the uploaded CSV and badge ID to the backend

* Read the CSV and grant badge in batches

* UX: Communicate the result to the user

* Don't award if badge is disabled

* Create a 'send_notification' method to remove duplicated code, slightly shrink badge image. Replace router transition with href.

* Dynamically discover current route
This commit is contained in:
Roman Rizzi
2020-01-13 11:20:26 -03:00
committed by GitHub
parent eb105ba79d
commit d69c5eebcf
15 changed files with 291 additions and 34 deletions
@@ -0,0 +1,35 @@
import Controller from "@ember/controller";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
export default Controller.extend({
saving: false,
actions: {
massAward() {
const file = document.querySelector("#massAwardCSVUpload").files[0];
if (this.model && file) {
const options = {
type: "POST",
processData: false,
contentType: false,
data: new FormData()
};
options.data.append("file", file);
this.set("saving", true);
ajax(`/admin/badges/award/${this.model.id}`, options)
.then(() => {
bootbox.alert(I18n.t("admin.badges.mass_award.success"));
})
.catch(popupAjaxError)
.finally(() => this.set("saving", false));
} else {
bootbox.alert(I18n.t("admin.badges.mass_award.aborted"));
}
}
}
});
@@ -1,2 +1,18 @@
import Controller from "@ember/controller";
export default Controller.extend();
import { inject as service } from "@ember/service";
import discourseComputed from "discourse-common/utils/decorators";
export default Controller.extend({
routing: service("-routing"),
@discourseComputed("routing.currentRouteName")
selectedRoute() {
const currentRoute = this.routing.currentRouteName;
const indexRoute = "adminBadges.index";
if (currentRoute === indexRoute) {
return "adminBadges.show";
} else {
return this.routing.currentRouteName;
}
}
});
@@ -0,0 +1,12 @@
import Route from "discourse/routes/discourse";
export default Route.extend({
model(params) {
if (params.badge_id !== "new") {
return this.modelFor("adminBadges").findBy(
"id",
parseInt(params.badge_id, 10)
);
}
}
});
@@ -190,6 +190,7 @@ export default function() {
"adminBadges",
{ path: "/badges", resetNamespace: true },
function() {
this.route("award", { path: "/award/:badge_id" });
this.route("show", { path: "/:badge_id" });
}
);
@@ -0,0 +1,22 @@
{{#d-section class="award-badge"}}
<form class="form-horizontal">
<h1>{{i18n 'admin.badges.mass_award.title'}}</h1>
<div class='badge-preview'>
{{#if model}}
{{icon-or-image model}}
<span class="badge-display-name">{{model.name}}</span>
{{else}}
<span class='badge-placeholder'>{{I18n 'admin.badges.mass_award.no_badge_selected'}}</span>
{{/if}}
</div>
<div>
<h4>{{I18n 'admin.badges.mass_award.upload_csv'}}</h4>
<input type='file' id='massAwardCSVUpload' accept='.csv' />
</div>
{{d-button
class="btn-primary"
action=(action 'massAward')
disabled=saving
label="admin.badges.save"}}
</form>
{{/d-section}}
@@ -6,13 +6,18 @@
{{d-icon "plus"}}
<span>{{i18n 'admin.badges.new'}}</span>
{{/link-to}}
{{#link-to 'adminBadges.award' 'new' class="btn btn-primary"}}
{{d-icon "certificate"}}
<span>{{i18n 'admin.badges.mass_award.button'}}</span>
{{/link-to}}
</div>
</div>
<div class='content-list'>
<ul class="admin-badge-list">
{{#each model as |badge|}}
<li class="admin-badge-list-item">
{{#link-to 'adminBadges.show' badge.id}}
{{#link-to selectedRoute badge.id}}
{{badge-button badge=badge}}
{{#if badge.newBadge}}
<span class="list-badge">{{i18n 'filters.new.lower_title'}}</span>