FEATURE: Add user custom fields to user directory (#13238)
This commit is contained in:
parent
2334c3622e
commit
0cba4d73c1
|
@ -5,4 +5,5 @@ export default Component.extend({
|
|||
tagName: "tr",
|
||||
classNameBindings: ["me"],
|
||||
me: propertyEqual("item.user.id", "currentUser.id"),
|
||||
columns: null,
|
||||
});
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import Component from "@ember/component";
|
||||
import { action } from "@ember/object";
|
||||
|
||||
export default Component.extend({
|
||||
classNames: ["directory-table-container"],
|
||||
|
||||
@action
|
||||
setActiveHeader(header) {
|
||||
// After render, scroll table left to ensure the order by column is visible
|
||||
const scrollPixels =
|
||||
header.offsetLeft + header.offsetWidth + 10 - this.element.offsetWidth;
|
||||
|
||||
if (scrollPixels > 0) {
|
||||
this.element.scrollLeft = scrollPixels;
|
||||
}
|
||||
},
|
||||
});
|
|
@ -1,6 +1,4 @@
|
|||
import Component from "@ember/component";
|
||||
import I18n from "I18n";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||
|
||||
export default Component.extend({
|
||||
|
@ -10,15 +8,8 @@ export default Component.extend({
|
|||
labelKey: null,
|
||||
chevronIcon: null,
|
||||
columnIcon: null,
|
||||
|
||||
@discourseComputed("field", "labelKey")
|
||||
title(field, labelKey) {
|
||||
if (!labelKey) {
|
||||
labelKey = `directory.${this.field}`;
|
||||
}
|
||||
|
||||
return I18n.t(labelKey + "_long", { defaultValue: I18n.t(labelKey) });
|
||||
},
|
||||
translated: false,
|
||||
onActiveRender: null,
|
||||
|
||||
toggleProperties() {
|
||||
if (this.order === this.field) {
|
||||
|
@ -40,13 +31,12 @@ export default Component.extend({
|
|||
},
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
this.set("id", `table-header-toggle-${this.field.replace(/\s/g, "")}`);
|
||||
this.toggleChevron();
|
||||
},
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
if (this.icon) {
|
||||
let columnIcon = iconHTML(this.icon);
|
||||
this.set("columnIcon", `${columnIcon}`.htmlSafe());
|
||||
didRender() {
|
||||
if (this.onActiveRender && this.chevronIcon) {
|
||||
this.onActiveRender(this.element);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
import Controller from "@ember/controller";
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import EmberObject, { action } from "@ember/object";
|
||||
import { extractError } from "discourse/lib/ajax-error";
|
||||
import { reload } from "discourse/helpers/page-reloader";
|
||||
|
||||
const UP = "up";
|
||||
const DOWN = "down";
|
||||
|
||||
export default Controller.extend(ModalFunctionality, {
|
||||
loading: true,
|
||||
columns: null,
|
||||
labelKey: null,
|
||||
|
||||
onShow() {
|
||||
ajax("directory-columns.json")
|
||||
.then((response) => {
|
||||
this.setProperties({
|
||||
loading: false,
|
||||
columns: response.directory_columns
|
||||
.sort((a, b) => (a.position > b.position ? 1 : -1))
|
||||
.map((c) => EmberObject.create(c)),
|
||||
});
|
||||
})
|
||||
.catch(extractError);
|
||||
},
|
||||
|
||||
@action
|
||||
save() {
|
||||
this.set("loading", true);
|
||||
const data = {
|
||||
directory_columns: this.columns.map((c) =>
|
||||
c.getProperties("id", "enabled", "position")
|
||||
),
|
||||
};
|
||||
|
||||
ajax("directory-columns.json", { type: "PUT", data })
|
||||
.then(() => {
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
this.set("loading", false);
|
||||
this.flash(extractError(e), "error");
|
||||
});
|
||||
},
|
||||
|
||||
@action
|
||||
resetToDefault() {
|
||||
let resetColumns = this.columns;
|
||||
resetColumns
|
||||
.sort((a, b) =>
|
||||
(a.automatic_position || a.user_field.position + 1000) >
|
||||
(b.automatic_position || b.user_field.position + 1000)
|
||||
? 1
|
||||
: -1
|
||||
)
|
||||
.forEach((column, index) => {
|
||||
column.setProperties({
|
||||
position: column.automatic_position || index + 1,
|
||||
enabled: column.automatic,
|
||||
});
|
||||
});
|
||||
this.set("columns", resetColumns);
|
||||
this.notifyPropertyChange("columns");
|
||||
},
|
||||
|
||||
@action
|
||||
moveUp(column) {
|
||||
this._moveColumn(UP, column);
|
||||
},
|
||||
|
||||
@action
|
||||
moveDown(column) {
|
||||
this._moveColumn(DOWN, column);
|
||||
},
|
||||
|
||||
_moveColumn(direction, column) {
|
||||
if (
|
||||
(direction === UP && column.position === 1) ||
|
||||
(direction === DOWN && column.position === this.columns.length)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const positionOnClick = column.position;
|
||||
const newPosition =
|
||||
direction === UP ? positionOnClick - 1 : positionOnClick + 1;
|
||||
|
||||
const previousColumn = this.columns.find((c) => c.position === newPosition);
|
||||
|
||||
column.set("position", newPosition);
|
||||
previousColumn.set("position", positionOnClick);
|
||||
|
||||
this.set(
|
||||
"columns",
|
||||
this.columns.sort((a, b) => (a.position > b.position ? 1 : -1))
|
||||
);
|
||||
this.notifyPropertyChange("columns");
|
||||
},
|
||||
});
|
|
@ -1,6 +1,7 @@
|
|||
import Controller, { inject as controller } from "@ember/controller";
|
||||
import { action } from "@ember/object";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
import { equal } from "@ember/object/computed";
|
||||
import { longDate } from "discourse/lib/formatter";
|
||||
import { observes } from "discourse-common/utils/decorators";
|
||||
|
@ -9,13 +10,14 @@ export default Controller.extend({
|
|||
application: controller(),
|
||||
queryParams: ["period", "order", "asc", "name", "group", "exclude_usernames"],
|
||||
period: "weekly",
|
||||
order: "likes_received",
|
||||
order: "",
|
||||
asc: null,
|
||||
name: "",
|
||||
group: null,
|
||||
nameInput: null,
|
||||
exclude_usernames: null,
|
||||
isLoading: false,
|
||||
columns: null,
|
||||
|
||||
showTimeRead: equal("period", "all"),
|
||||
|
||||
|
@ -23,9 +25,15 @@ export default Controller.extend({
|
|||
this.set("isLoading", true);
|
||||
|
||||
this.set("nameInput", params.name);
|
||||
this.set("order", params.order);
|
||||
|
||||
const custom_field_columns = this.columns.filter((c) => !c.automatic);
|
||||
const user_field_ids = custom_field_columns
|
||||
.map((c) => c.user_field_id)
|
||||
.join("|");
|
||||
|
||||
this.store
|
||||
.find("directoryItem", params)
|
||||
.find("directoryItem", Object.assign(params, { user_field_ids }))
|
||||
.then((model) => {
|
||||
const lastUpdatedAt = model.get("resultSetMeta.last_updated_at");
|
||||
this.setProperties({
|
||||
|
@ -39,6 +47,11 @@ export default Controller.extend({
|
|||
});
|
||||
},
|
||||
|
||||
@action
|
||||
showEditColumnsModal() {
|
||||
showModal("edit-user-directory-columns");
|
||||
},
|
||||
|
||||
@action
|
||||
onFilterChanged(filter) {
|
||||
discourseDebounce(this, this._setName, filter, 500);
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import { htmlSafe } from "@ember/template";
|
||||
import { registerUnbound } from "discourse-common/lib/helpers";
|
||||
import I18n from "I18n";
|
||||
|
||||
export default registerUnbound("mobile-directory-item-label", function (args) {
|
||||
// Args should include key/values { item, column }
|
||||
|
||||
const count = args.item.get(args.column.name);
|
||||
return htmlSafe(I18n.t(`directory.${args.column.name}`, { count }));
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
import { htmlSafe } from "@ember/template";
|
||||
import { registerUnbound } from "discourse-common/lib/helpers";
|
||||
|
||||
export default registerUnbound(
|
||||
"directory-item-user-field-value",
|
||||
function (args) {
|
||||
// Args should include key/values { item, column }
|
||||
|
||||
const value =
|
||||
args.item.user && args.item.user.user_fields
|
||||
? args.item.user.user_fields[args.column.user_field_id]
|
||||
: null;
|
||||
const content = value || "-";
|
||||
return htmlSafe(`<span class='user-field-value'>${content}</span>`);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,11 @@
|
|||
import { htmlSafe } from "@ember/template";
|
||||
import { registerUnbound } from "discourse-common/lib/helpers";
|
||||
import { number } from "discourse/lib/formatter";
|
||||
|
||||
export default registerUnbound("directory-item-value", function (args) {
|
||||
// Args should include key/values { item, column }
|
||||
|
||||
return htmlSafe(
|
||||
`<span class='number'>${number(args.item.get(args.column.name))}</span>`
|
||||
);
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
import { registerUnbound } from "discourse-common/lib/helpers";
|
||||
import I18n from "I18n";
|
||||
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
|
||||
export default registerUnbound("directory-table-header-title", function (args) {
|
||||
// Args should include key/values { field, labelKey, icon, translated }
|
||||
|
||||
let html = "";
|
||||
if (args.icon) {
|
||||
html += iconHTML(args.icon);
|
||||
}
|
||||
let labelKey = args.labelKey || `directory.${args.field}`;
|
||||
|
||||
html += args.translated
|
||||
? args.field
|
||||
: I18n.t(labelKey + "_long", { defaultValue: I18n.t(labelKey) });
|
||||
return htmlSafe(html);
|
||||
});
|
|
@ -1,5 +1,6 @@
|
|||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
import I18n from "I18n";
|
||||
import PreloadStore from "discourse/lib/preload-store";
|
||||
|
||||
export default DiscourseRoute.extend({
|
||||
queryParams: {
|
||||
|
@ -36,11 +37,14 @@ export default DiscourseRoute.extend({
|
|||
},
|
||||
|
||||
model(params) {
|
||||
return params;
|
||||
const columns = PreloadStore.get("directoryColumns");
|
||||
params.order = params.order || columns[0].name;
|
||||
return { params, columns };
|
||||
},
|
||||
|
||||
setupController(controller, params) {
|
||||
controller.loadUsers(params);
|
||||
setupController(controller, model) {
|
||||
controller.set("columns", model.columns);
|
||||
controller.loadUsers(model.params);
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
<td>{{user-info user=item.user}}</td>
|
||||
<td>{{number item.likes_received}}</td>
|
||||
<td>{{number item.likes_given}}</td>
|
||||
<td>{{number item.topic_count}}</td>
|
||||
<td>{{number item.post_count}}</td>
|
||||
<td>{{number item.topics_entered}}</td>
|
||||
<td>{{number item.posts_read}}</td>
|
||||
<td>{{number item.days_visited}}</td>
|
||||
{{#each columns as |column|}}
|
||||
<td>
|
||||
{{#if column.automatic}}
|
||||
{{directory-item-value item=item column=column}}
|
||||
{{else}}
|
||||
{{directory-item-user-field-value item=item column=column}}
|
||||
{{/if}}
|
||||
</td>
|
||||
{{/each}}
|
||||
|
||||
{{#if showTimeRead}}
|
||||
<td><span class="time-read">{{format-duration item.time_read}}</span></td>
|
||||
{{/if}}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<table>
|
||||
<thead>
|
||||
{{table-header-toggle field="username" order=order asc=asc}}
|
||||
{{#each columns as |column|}}
|
||||
{{table-header-toggle
|
||||
field=column.name
|
||||
icon=column.icon
|
||||
order=order
|
||||
asc=asc
|
||||
translated=column.user_field_id
|
||||
onActiveRender=setActiveHeader
|
||||
}}
|
||||
{{/each}}
|
||||
|
||||
{{#if showTimeRead}}
|
||||
<th>{{i18n "directory.time_read"}}</th>
|
||||
{{/if}}
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each items as |item|}}
|
||||
{{directory-item item=item columns=columns showTimeRead=showTimeRead}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
|
@ -1 +1,4 @@
|
|||
<span class="header-contents">{{columnIcon}}{{title}}{{chevronIcon}}</span>
|
||||
<span class="header-contents" id={{id}}>
|
||||
{{directory-table-header-title field=field labelKey=labelKey icon=icon translated=translated}}
|
||||
{{chevronIcon}}
|
||||
</span>
|
||||
|
|
|
@ -1,11 +1,33 @@
|
|||
{{user-info user=item.user}}
|
||||
{{user-stat value=item.likes_received label="directory.likes_received" icon="heart"}}
|
||||
{{user-stat value=item.likes_given label="directory.likes_given" icon="heart"}}
|
||||
{{user-stat value=item.topic_count label="directory.topic_count"}}
|
||||
{{user-stat value=item.post_count label="directory.post_count"}}
|
||||
{{user-stat value=item.topics_entered label="directory.topics_entered"}}
|
||||
{{user-stat value=item.posts_read label="directory.posts_read"}}
|
||||
{{user-stat value=item.days_visited label="directory.days_visited"}}
|
||||
|
||||
{{#each columns as |column|}}
|
||||
{{#if column.automatic}}
|
||||
<div class="user-stat">
|
||||
<span class="value">
|
||||
{{directory-item-value item=item column=column}}
|
||||
</span>
|
||||
<span class="label">
|
||||
{{#if column.icon}}
|
||||
{{d-icon column.icon}}
|
||||
{{/if}}
|
||||
{{mobile-directory-item-label item=item column=column}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{{else}}
|
||||
{{#if (get item.user.user_fields column.user_field_id)}}
|
||||
<div class="user-stat">
|
||||
<span class="value user-field">
|
||||
{{directory-item-user-field-value item=item column=column}}
|
||||
</span>
|
||||
<span class="label">
|
||||
{{column.name}}
|
||||
</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
|
||||
{{#if showTimeRead}}
|
||||
{{user-stat value=item.time_read label="directory.time_read" type="duration"}}
|
||||
{{/if}}
|
||||
|
|
|
@ -17,13 +17,20 @@
|
|||
placeholderKey="directory.filter_name"
|
||||
class="filter-name no-blur"
|
||||
}}
|
||||
{{#if currentUser.staff}}
|
||||
{{d-button
|
||||
icon="wrench"
|
||||
action=(action "showEditColumnsModal")
|
||||
class="btn-default open-edit-columns-btn"
|
||||
}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#conditional-loading-spinner condition=model.loading}}
|
||||
{{#if model.length}}
|
||||
<div class="total-rows">{{i18n "directory.total_rows" count=model.totalRows}}</div>
|
||||
{{#each model as |item|}}
|
||||
{{directory-item tagName="div" class="user" item=item showTimeRead=showTimeRead}}
|
||||
{{directory-item tagName="div" class="user" item=item columns=columns showTimeRead=showTimeRead}}
|
||||
{{/each}}
|
||||
|
||||
{{conditional-loading-spinner condition=model.loadingMore}}
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
{{#d-modal-body title="directory.edit_columns.title"}}
|
||||
{{#if loading}}
|
||||
{{loading-spinner size="large"}}
|
||||
{{else}}
|
||||
<div class="edit-directory-columns-container">
|
||||
{{#each columns as |column|}}
|
||||
<div class="edit-directory-column">
|
||||
<div class="left-content">
|
||||
<label class="column-name">
|
||||
{{input type="checkbox" checked=column.enabled}}
|
||||
{{#if column.automatic}}
|
||||
{{directory-table-header-title field=column.name labelKey=labelKey icon=column.icon}}
|
||||
{{else}}
|
||||
{{directory-table-header-title field=column.user_field.name translated=true}}
|
||||
{{/if}}
|
||||
</label>
|
||||
</div>
|
||||
<div class="right-content">
|
||||
{{d-button
|
||||
icon="arrow-up"
|
||||
class="button-secondary move-column-up"
|
||||
action=(action "moveUp" column)
|
||||
}}
|
||||
{{d-button
|
||||
icon="arrow-down"
|
||||
class="button-secondary"
|
||||
action=(action "moveDown" column)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/d-modal-body}}
|
||||
|
||||
<div class="modal-footer">
|
||||
{{d-button
|
||||
class="btn-primary"
|
||||
label="directory.edit_columns.save"
|
||||
action=(action "save")
|
||||
}}
|
||||
|
||||
{{d-button
|
||||
class="btn-secondary reset-to-default"
|
||||
label="directory.edit_columns.reset_to_default"
|
||||
action=(action "resetToDefault")
|
||||
}}
|
||||
</div>
|
|
@ -25,33 +25,19 @@
|
|||
placeholderKey="directory.filter_name"
|
||||
class="filter-name no-blur"
|
||||
}}
|
||||
{{#if currentUser.staff}}
|
||||
{{d-button
|
||||
icon="wrench"
|
||||
action=(action "showEditColumnsModal")
|
||||
class="btn-default open-edit-columns-btn"
|
||||
}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#conditional-loading-spinner condition=isLoading}}
|
||||
{{#if model.length}}
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
{{table-header-toggle field="username" order=order asc=asc}}
|
||||
{{table-header-toggle field="likes_received" order=order asc=asc icon="heart"}}
|
||||
{{table-header-toggle field="likes_given" order=order asc=asc icon="heart"}}
|
||||
{{table-header-toggle field="topic_count" order=order asc=asc}}
|
||||
{{table-header-toggle field="post_count" order=order asc=asc}}
|
||||
{{table-header-toggle field="topics_entered" order=order asc=asc}}
|
||||
{{table-header-toggle field="posts_read" order=order asc=asc}}
|
||||
{{table-header-toggle field="days_visited" order=order asc=asc}}
|
||||
{{#if showTimeRead}}
|
||||
<th>{{i18n "directory.time_read"}}</th>
|
||||
{{/if}}
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each model as |item|}}
|
||||
{{directory-item item=item showTimeRead=showTimeRead}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{directory-table items=model columns=columns showTimeRead=showTimeRead order=order asc=asc}}
|
||||
{{conditional-loading-spinner condition=model.loadingMore}}
|
||||
{{else}}
|
||||
<div class="clearfix"></div>
|
||||
|
|
|
@ -4,6 +4,7 @@ import { visit } from "@ember/test-helpers";
|
|||
|
||||
acceptance("User Directory - Mobile", function (needs) {
|
||||
needs.mobileView();
|
||||
|
||||
test("Visit Page", async function (assert) {
|
||||
await visit("/u");
|
||||
assert.ok(exists(".directory .user"), "has a list of users");
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import { acceptance, exists } from "discourse/tests/helpers/qunit-helpers";
|
||||
import {
|
||||
acceptance,
|
||||
exists,
|
||||
query,
|
||||
queryAll,
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import { test } from "qunit";
|
||||
import { visit } from "@ember/test-helpers";
|
||||
import { click, visit } from "@ember/test-helpers";
|
||||
|
||||
acceptance("User Directory", function () {
|
||||
test("Visit Page", async function (assert) {
|
||||
|
@ -25,4 +30,106 @@ acceptance("User Directory", function () {
|
|||
assert.ok($("body.users-page").length, "has the body class");
|
||||
assert.ok(exists(".directory table tr"), "has a list of users");
|
||||
});
|
||||
|
||||
test("Custom user fields are present", async function (assert) {
|
||||
await visit("/u");
|
||||
|
||||
const firstRow = query(".users-directory table tr");
|
||||
const columnData = firstRow.querySelectorAll("td");
|
||||
const favoriteColorTd = columnData[columnData.length - 1];
|
||||
|
||||
assert.equal(favoriteColorTd.querySelector("span").textContent, "Blue");
|
||||
});
|
||||
});
|
||||
|
||||
acceptance("User directory - Editing columns", function (needs) {
|
||||
needs.user({ moderator: true, admin: true });
|
||||
|
||||
test("The automatic columns are checked and the user field columns are unchecked by default", async function (assert) {
|
||||
await visit("/u");
|
||||
await click(".open-edit-columns-btn");
|
||||
|
||||
const columns = queryAll(
|
||||
".edit-directory-columns-container .edit-directory-column"
|
||||
);
|
||||
assert.equal(columns.length, 8);
|
||||
|
||||
const checked = queryAll(
|
||||
".edit-directory-columns-container .edit-directory-column input[type='checkbox']:checked"
|
||||
);
|
||||
assert.equal(checked.length, 7);
|
||||
|
||||
const unchecked = queryAll(
|
||||
".edit-directory-columns-container .edit-directory-column input[type='checkbox']:not(:checked)"
|
||||
);
|
||||
assert.equal(unchecked.length, 1);
|
||||
});
|
||||
|
||||
const fetchColumns = function () {
|
||||
return queryAll(".edit-directory-columns-container .edit-directory-column");
|
||||
};
|
||||
|
||||
test("Reordering and restoring default positions", async function (assert) {
|
||||
await visit("/u");
|
||||
await click(".open-edit-columns-btn");
|
||||
|
||||
let columns;
|
||||
columns = fetchColumns();
|
||||
assert.equal(
|
||||
columns[3].querySelector(".column-name").textContent.trim(),
|
||||
"Replies Posted"
|
||||
);
|
||||
assert.equal(
|
||||
columns[4].querySelector(".column-name").textContent.trim(),
|
||||
"Topics Viewed"
|
||||
);
|
||||
|
||||
// Click on row 4 and see if they are swapped
|
||||
await click(columns[4].querySelector(".move-column-up"));
|
||||
|
||||
columns = fetchColumns();
|
||||
assert.equal(
|
||||
columns[3].querySelector(".column-name").textContent.trim(),
|
||||
"Topics Viewed"
|
||||
);
|
||||
assert.equal(
|
||||
columns[4].querySelector(".column-name").textContent.trim(),
|
||||
"Replies Posted"
|
||||
);
|
||||
|
||||
const moveUserFieldColumnUpBtn = columns[columns.length - 1].querySelector(
|
||||
".move-column-up"
|
||||
);
|
||||
await click(moveUserFieldColumnUpBtn);
|
||||
await click(moveUserFieldColumnUpBtn);
|
||||
await click(moveUserFieldColumnUpBtn);
|
||||
|
||||
columns = fetchColumns();
|
||||
assert.equal(
|
||||
columns[4].querySelector(".column-name").textContent.trim(),
|
||||
"Favorite Color"
|
||||
);
|
||||
assert.equal(
|
||||
columns[5].querySelector(".column-name").textContent.trim(),
|
||||
"Replies Posted"
|
||||
);
|
||||
|
||||
// Now click restore default and check order of column names
|
||||
await click(".reset-to-default");
|
||||
|
||||
let columnNames = queryAll(
|
||||
".edit-directory-columns-container .edit-directory-column .column-name"
|
||||
).toArray();
|
||||
columnNames = columnNames.map((el) => el.textContent.trim());
|
||||
assert.deepEqual(columnNames, [
|
||||
"Received",
|
||||
"Given",
|
||||
"Topics Created",
|
||||
"Replies Posted",
|
||||
"Topics Viewed",
|
||||
"Posts Read",
|
||||
"Days Visited",
|
||||
"Favorite Color",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,7 +11,12 @@ export default {
|
|||
likes_given: 7725,
|
||||
topics_entered: 11453,
|
||||
topic_count: 184,
|
||||
post_count: 12263
|
||||
post_count: 12263,
|
||||
user: {
|
||||
user_fields: {
|
||||
3: "Blue"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
|
|
|
@ -929,4 +929,102 @@ export function applyDefaultHandlers(pretender) {
|
|||
|
||||
return [404, { "Content-Type": "application/html" }, ""];
|
||||
});
|
||||
|
||||
pretender.get("directory-columns.json", () => {
|
||||
return response(200, {
|
||||
directory_columns: [
|
||||
{
|
||||
id: 1,
|
||||
name: "likes_received",
|
||||
automatic: true,
|
||||
enabled: true,
|
||||
automatic_position: 1,
|
||||
position: 1,
|
||||
icon: "heart",
|
||||
user_field: null,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "likes_given",
|
||||
automatic: true,
|
||||
enabled: true,
|
||||
automatic_position: 2,
|
||||
position: 2,
|
||||
icon: "heart",
|
||||
user_field: null,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "topic_count",
|
||||
automatic: true,
|
||||
enabled: true,
|
||||
automatic_position: 3,
|
||||
position: 3,
|
||||
icon: null,
|
||||
user_field: null,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "post_count",
|
||||
automatic: true,
|
||||
enabled: true,
|
||||
automatic_position: 4,
|
||||
position: 4,
|
||||
icon: null,
|
||||
user_field: null,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "topics_entered",
|
||||
automatic: true,
|
||||
enabled: true,
|
||||
automatic_position: 5,
|
||||
position: 5,
|
||||
icon: null,
|
||||
user_field: null,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "posts_read",
|
||||
automatic: true,
|
||||
enabled: true,
|
||||
automatic_position: 6,
|
||||
position: 6,
|
||||
icon: null,
|
||||
user_field: null,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "days_visited",
|
||||
automatic: true,
|
||||
enabled: true,
|
||||
automatic_position: 7,
|
||||
position: 7,
|
||||
icon: null,
|
||||
user_field: null,
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: null,
|
||||
automatic: false,
|
||||
enabled: false,
|
||||
automatic_position: null,
|
||||
position: 8,
|
||||
icon: null,
|
||||
user_field: {
|
||||
id: 3,
|
||||
name: "Favorite Color",
|
||||
description: "User's favorite color",
|
||||
field_type: "text",
|
||||
editable: false,
|
||||
required: false,
|
||||
show_on_profile: false,
|
||||
show_on_user_card: true,
|
||||
searchable: true,
|
||||
position: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -229,6 +229,12 @@ function setupTestsCommon(application, container, config) {
|
|||
});
|
||||
|
||||
PreloadStore.reset();
|
||||
PreloadStore.store(
|
||||
"directoryColumns",
|
||||
JSON.parse(
|
||||
'[{"name":"likes_given","automatic":true,"icon":"heart","user_field_id":null},{"name":"posts_read","automatic":true,"icon":null,"user_field_id":null},{"name":"likes_received","automatic":true,"icon":"heart","user_field_id":null},{"name":"topic_count","automatic":true,"icon":null,"user_field_id":null},{"name":"post_count","automatic":true,"icon":null,"user_field_id":null},{"name":"topics_entered","automatic":true,"icon":null,"user_field_id":null},{"name":"days_visited","automatic":true,"icon":null,"user_field_id":null},{"name":"Favorite Color","automatic":false,"icon":null,"user_field_id":3}]'
|
||||
)
|
||||
);
|
||||
|
||||
sinon.stub(ScrollingDOMMethods, "screenNotFull");
|
||||
sinon.stub(ScrollingDOMMethods, "bindOnScroll");
|
||||
|
|
|
@ -1,6 +1,16 @@
|
|||
.directory {
|
||||
margin-bottom: 100px;
|
||||
|
||||
.directory-table-container {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.open-edit-columns-btn {
|
||||
vertical-align: top;
|
||||
padding: 0.45em 0.8em;
|
||||
}
|
||||
|
||||
&.users-directory {
|
||||
.period-chooser {
|
||||
.selected-name {
|
||||
|
@ -61,6 +71,13 @@
|
|||
.time-read {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.user-field-value {
|
||||
font-size: var(--font-up-1);
|
||||
color: var(--primary-medium);
|
||||
@media screen and (max-width: $small-width) {
|
||||
font-size: $font-0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
th.sortable {
|
||||
|
@ -82,3 +99,50 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-user-directory-columns-modal {
|
||||
.edit-directory-columns-container {
|
||||
.edit-directory-column {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid var(--primary-low);
|
||||
|
||||
.column-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.d-icon-heart {
|
||||
color: var(--love);
|
||||
margin: 0 0.25em 0 0;
|
||||
}
|
||||
|
||||
.move-column-up {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.left-content,
|
||||
.right-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.reset-to-default {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-user-directory-columns-modal .modal-inner-container {
|
||||
min-width: 450px;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,10 @@
|
|||
font-size: $font-up-1;
|
||||
}
|
||||
|
||||
.open-edit-columns-btn {
|
||||
margin: -0.7em 0 0.5em;
|
||||
}
|
||||
|
||||
&.users-directory {
|
||||
.filter-name {
|
||||
width: 100%;
|
||||
|
@ -38,6 +42,9 @@
|
|||
flex: 1 1 50%;
|
||||
.value {
|
||||
font-weight: bold;
|
||||
&.user-field {
|
||||
font-size: var(--font-down-1);
|
||||
}
|
||||
}
|
||||
.label {
|
||||
margin-left: 0.2em;
|
||||
|
@ -49,3 +56,7 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-user-directory-columns-modal .modal-inner-container {
|
||||
width: 90%;
|
||||
}
|
||||
|
|
|
@ -35,6 +35,9 @@ class Admin::UserFieldsController < Admin::AdminController
|
|||
update_options(field)
|
||||
|
||||
if field.save
|
||||
if !field.show_on_profile && !field.show_on_user_card
|
||||
DirectoryColumn.where(user_field_id: field.id).destroy_all
|
||||
end
|
||||
render_serialized(field, UserFieldSerializer, root: 'user_field')
|
||||
else
|
||||
render_json_error(field)
|
||||
|
|
|
@ -603,6 +603,7 @@ class ApplicationController < ActionController::Base
|
|||
store_preloaded("customEmoji", custom_emoji)
|
||||
store_preloaded("isReadOnly", @readonly_mode.to_s)
|
||||
store_preloaded("activatedThemes", activated_themes_json)
|
||||
store_preloaded("directoryColumns", directory_columns_json)
|
||||
end
|
||||
|
||||
def preload_current_user_data
|
||||
|
@ -614,6 +615,20 @@ class ApplicationController < ActionController::Base
|
|||
store_preloaded("topicTrackingStates", MultiJson.dump(serializer))
|
||||
end
|
||||
|
||||
def directory_columns_json
|
||||
DirectoryColumn
|
||||
.left_joins(:user_field)
|
||||
.where(enabled: true)
|
||||
.order(:position)
|
||||
.pluck('directory_columns.name',
|
||||
'directory_columns.automatic',
|
||||
'directory_columns.icon',
|
||||
'user_fields.id',
|
||||
'user_fields.name')
|
||||
.map { |column| { name: column[0] || column[4], automatic: column[1], icon: column[2], user_field_id: column[3] } }
|
||||
.to_json
|
||||
end
|
||||
|
||||
def custom_html_json
|
||||
target = view_context.mobile_view? ? :mobile : :desktop
|
||||
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DirectoryColumnsController < ApplicationController
|
||||
requires_login
|
||||
|
||||
def index
|
||||
raise Discourse::NotFound unless guardian.is_staff?
|
||||
|
||||
ensure_user_fields_have_columns
|
||||
|
||||
columns = DirectoryColumn.includes(:user_field).all
|
||||
render_json_dump(directory_columns: serialize_data(columns, DirectoryColumnSerializer))
|
||||
end
|
||||
|
||||
def update
|
||||
raise Discourse::NotFound unless guardian.is_staff?
|
||||
params.require(:directory_columns)
|
||||
directory_column_params = params.permit(directory_columns: {})
|
||||
directory_columns = DirectoryColumn.all
|
||||
|
||||
has_enabled_column = directory_column_params[:directory_columns].values.any? do |column_data|
|
||||
column_data[:enabled].to_s == "true"
|
||||
end
|
||||
raise Discourse::InvalidParameters, "Must have at least one column enabled" unless has_enabled_column
|
||||
|
||||
directory_column_params[:directory_columns].values.each do |column_data|
|
||||
existing_column = directory_columns.detect { |c| c.id == column_data[:id].to_i }
|
||||
if (existing_column.enabled != column_data[:enabled] || existing_column.position != column_data[:position].to_i)
|
||||
existing_column.update(enabled: column_data[:enabled], position: column_data[:position])
|
||||
end
|
||||
end
|
||||
|
||||
render json: success_json
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_user_fields_have_columns
|
||||
user_fields_without_column =
|
||||
UserField.left_outer_joins(:directory_column)
|
||||
.where(directory_column: { user_field_id: nil })
|
||||
.where("show_on_profile=? OR show_on_user_card=?", true, true)
|
||||
|
||||
return unless user_fields_without_column.count > 0
|
||||
|
||||
next_position = DirectoryColumn.maximum("position") + 1
|
||||
|
||||
new_directory_column_attrs = []
|
||||
user_fields_without_column.each do |user_field|
|
||||
new_directory_column_attrs.push({
|
||||
user_field_id: user_field.id,
|
||||
enabled: false,
|
||||
automatic: false,
|
||||
position: next_position
|
||||
})
|
||||
|
||||
next_position += 1
|
||||
end
|
||||
|
||||
DirectoryColumn.insert_all(new_directory_column_attrs)
|
||||
end
|
||||
end
|
|
@ -32,6 +32,14 @@ class DirectoryItemsController < ApplicationController
|
|||
result = result.order("directory_items.#{order} #{dir}, directory_items.id")
|
||||
elsif params[:order] === 'username'
|
||||
result = result.order("users.#{order} #{dir}, directory_items.id")
|
||||
else
|
||||
user_field = UserField.find_by(name: params[:order])
|
||||
if user_field
|
||||
result = result
|
||||
.joins(:user)
|
||||
.joins("LEFT OUTER JOIN user_custom_fields ON user_custom_fields.user_id = users.id AND user_custom_fields.name = 'user_field_#{user_field.id}'")
|
||||
.order("user_custom_fields.name = 'user_field_#{user_field.id}' ASC, user_custom_fields.value #{dir}")
|
||||
end
|
||||
end
|
||||
|
||||
if period_type == DirectoryItem.period_types[:all]
|
||||
|
@ -84,7 +92,14 @@ class DirectoryItemsController < ApplicationController
|
|||
end
|
||||
|
||||
last_updated_at = DirectoryItem.last_updated_at(period_type)
|
||||
render_json_dump(directory_items: serialize_data(result, DirectoryItemSerializer),
|
||||
|
||||
serializer_opts = {}
|
||||
if params[:user_field_ids]
|
||||
serializer_opts[:user_field_ids] = params[:user_field_ids]&.split("|")&.map(&:to_i)
|
||||
end
|
||||
|
||||
serialized = serialize_data(result, DirectoryItemSerializer, serializer_opts)
|
||||
render_json_dump(directory_items: serialized,
|
||||
meta: {
|
||||
last_updated_at: last_updated_at,
|
||||
total_rows_directory_items: result_count,
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DirectoryColumn < ActiveRecord::Base
|
||||
belongs_to :user_field
|
||||
end
|
|
@ -7,6 +7,7 @@ class UserField < ActiveRecord::Base
|
|||
validates_presence_of :description, :field_type
|
||||
validates_presence_of :name, unless: -> { field_type == "confirm" }
|
||||
has_many :user_field_options, dependent: :destroy
|
||||
has_one :directory_column, dependent: :destroy
|
||||
accepts_nested_attributes_for :user_field_options
|
||||
|
||||
after_save :queue_index_search
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DirectoryColumnSerializer < ApplicationSerializer
|
||||
attributes :id,
|
||||
:name,
|
||||
:automatic,
|
||||
:enabled,
|
||||
:automatic_position,
|
||||
:position,
|
||||
:icon
|
||||
|
||||
has_one :user_field, serializer: UserFieldSerializer, embed: :objects
|
||||
end
|
|
@ -4,6 +4,16 @@ class DirectoryItemSerializer < ApplicationSerializer
|
|||
|
||||
class UserSerializer < UserNameSerializer
|
||||
include UserPrimaryGroupMixin
|
||||
|
||||
attributes :user_fields
|
||||
|
||||
def user_fields
|
||||
object.user_fields(@options[:user_field_ids])
|
||||
end
|
||||
|
||||
def include_user_fields?
|
||||
user_fields.present?
|
||||
end
|
||||
end
|
||||
|
||||
attributes :id,
|
||||
|
@ -23,5 +33,4 @@ class DirectoryItemSerializer < ApplicationSerializer
|
|||
def include_time_read?
|
||||
object.period_type == DirectoryItem.period_types[:all]
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -646,6 +646,10 @@ en:
|
|||
total_rows:
|
||||
one: "%{count} user"
|
||||
other: "%{count} users"
|
||||
edit_columns:
|
||||
title: "Edit Directory Columns"
|
||||
save: "Save"
|
||||
reset_to_default: "Reset to default"
|
||||
|
||||
group_histories:
|
||||
actions:
|
||||
|
|
|
@ -387,6 +387,8 @@ Discourse::Application.routes.draw do
|
|||
get ".well-known/change-password", to: redirect(relative_url_root + 'my/preferences/account', status: 302)
|
||||
|
||||
get "user-cards" => "users#cards", format: :json
|
||||
get "directory-columns" => "directory_columns#index", format: :json
|
||||
put "directory-columns" => "directory_columns#update", format: :json
|
||||
|
||||
%w{users u}.each_with_index do |root_path, index|
|
||||
get "#{root_path}" => "users#index", constraints: { format: 'html' }
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
class CreateDirectoryColumns < ActiveRecord::Migration[6.1]
|
||||
def up
|
||||
create_table :directory_columns do |t|
|
||||
t.string :name, null: true
|
||||
t.integer :automatic_position, null: true
|
||||
t.string :icon, null: true
|
||||
t.integer :user_field_id, null: true
|
||||
t.boolean :automatic, null: false
|
||||
t.boolean :enabled, null: false
|
||||
t.integer :position, null: false
|
||||
t.datetime :created_at, default: -> { 'CURRENT_TIMESTAMP' }
|
||||
end
|
||||
|
||||
add_index :directory_columns, [:enabled, :position, :user_field_id], name: "directory_column_index"
|
||||
|
||||
create_automatic_columns
|
||||
end
|
||||
|
||||
def down
|
||||
drop_table :directory_columns
|
||||
end
|
||||
|
||||
def create_automatic_columns
|
||||
DB.exec(
|
||||
<<~SQL
|
||||
INSERT INTO directory_columns (
|
||||
name, automatic, enabled, automatic_position, position, icon
|
||||
)
|
||||
VALUES
|
||||
( 'likes_received', true, true, 1, 1, 'heart' ),
|
||||
( 'likes_given', true, true, 2, 2, 'heart' ),
|
||||
( 'topic_count', true, true, 3, 3, NULL ),
|
||||
( 'post_count', true, true, 4, 4, NULL ),
|
||||
( 'topics_entered', true, true, 5, 5, NULL ),
|
||||
( 'posts_read', true, true, 6, 6, NULL ),
|
||||
( 'days_visited', true, true, 7, 7, NULL );
|
||||
SQL
|
||||
)
|
||||
end
|
||||
end
|
|
@ -124,6 +124,21 @@ describe Admin::UserFieldsController do
|
|||
user_field.reload
|
||||
expect(user_field.user_field_options.size).to eq(2)
|
||||
end
|
||||
|
||||
it "removes directory column record if not public" do
|
||||
next_position = DirectoryColumn.maximum("position") + 1
|
||||
DirectoryColumn.create(
|
||||
user_field_id: user_field.id,
|
||||
enabled: false,
|
||||
automatic: false,
|
||||
position: next_position
|
||||
)
|
||||
expect {
|
||||
put "/admin/customize/user_fields/#{user_field.id}.json", params: {
|
||||
user_field: { show_on_profile: false, show_on_user_card: false, searchable: true }
|
||||
}
|
||||
}.to change { DirectoryColumn.count }.by(-1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe DirectoryColumnsController do
|
||||
fab!(:user) { Fabricate(:user) }
|
||||
fab!(:admin) { Fabricate(:admin) }
|
||||
|
||||
describe "#index" do
|
||||
fab!(:public_user_field) { Fabricate(:user_field, show_on_profile: true) }
|
||||
fab!(:private_user_field) { Fabricate(:user_field, show_on_profile: false, show_on_user_card: false) }
|
||||
|
||||
it "creates directory column records for public user fields" do
|
||||
sign_in(admin)
|
||||
|
||||
expect {
|
||||
get "/directory-columns.json"
|
||||
}.to change { DirectoryColumn.count }.by(1)
|
||||
end
|
||||
|
||||
it "returns a 403 when not logged in as staff member" do
|
||||
sign_in(user)
|
||||
get "/directory-columns.json"
|
||||
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#update" do
|
||||
let(:first_directory_column_id) { DirectoryColumn.first.id }
|
||||
let(:second_directory_column_id) { DirectoryColumn.second.id }
|
||||
let(:params) {
|
||||
{
|
||||
directory_columns: {
|
||||
"0": {
|
||||
id: first_directory_column_id,
|
||||
enabled: false,
|
||||
position: 1
|
||||
},
|
||||
"1": {
|
||||
id: second_directory_column_id,
|
||||
enabled: true,
|
||||
position: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it "updates exising directory columns" do
|
||||
sign_in(admin)
|
||||
|
||||
expect {
|
||||
put "/directory-columns.json", params: params
|
||||
}.to change { DirectoryColumn.find(first_directory_column_id).enabled }.from(true).to(false)
|
||||
end
|
||||
|
||||
it "does not let all columns be disabled" do
|
||||
sign_in(admin)
|
||||
bad_params = params
|
||||
bad_params[:directory_columns][:"1"][:enabled] = false
|
||||
|
||||
put "/directory-columns.json", params: bad_params
|
||||
|
||||
expect(response.status).to eq(400)
|
||||
end
|
||||
|
||||
it "returns a 404 when not logged in as a staff member" do
|
||||
sign_in(user)
|
||||
put "/directory-columns.json", params: params
|
||||
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue