FEATURE: Add links to searchable user fields in users directory and user profile (#29338)
* FEATURE: Add links to searchable user fields in users directory and user profile
This commit is contained in:
parent
13c7773036
commit
708533b1e0
|
@ -0,0 +1,57 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { fn, hash } from "@ember/helper";
|
||||||
|
import { on } from "@ember/modifier";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { LinkTo } from "@ember/routing";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
|
||||||
|
export default class DirectoryItemUserFieldValueComponent extends Component {
|
||||||
|
@service router;
|
||||||
|
get fieldData() {
|
||||||
|
const { item, column } = this.args;
|
||||||
|
return item?.user?.user_fields?.[column.user_field_id];
|
||||||
|
}
|
||||||
|
|
||||||
|
get values() {
|
||||||
|
const fieldData = this.fieldData;
|
||||||
|
if (!fieldData || !fieldData.value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fieldData.value
|
||||||
|
.toString()
|
||||||
|
.split(",")
|
||||||
|
.map((v) => v.replace(/-/g, " "))
|
||||||
|
.map((v) => v.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
get isSearchable() {
|
||||||
|
return this.fieldData?.searchable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
refreshRoute(value) {
|
||||||
|
this.router.transitionTo({ queryParams: { name: value } });
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span class="directory-table__value--user-field">
|
||||||
|
{{#if this.values}}
|
||||||
|
{{#if this.isSearchable}}
|
||||||
|
{{#each this.values as |value|}}
|
||||||
|
<LinkTo
|
||||||
|
@route="users"
|
||||||
|
@query={{hash name=value}}
|
||||||
|
{{on "click" (fn this.refreshRoute value)}}
|
||||||
|
class="directory-value-list-item"
|
||||||
|
>{{value}}</LinkTo>
|
||||||
|
{{/each}}
|
||||||
|
{{else}}
|
||||||
|
{{this.values}}
|
||||||
|
{{/if}}
|
||||||
|
{{else}}
|
||||||
|
-
|
||||||
|
{{/if}}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
}
|
|
@ -370,7 +370,17 @@
|
||||||
<span class="user-field-value">
|
<span class="user-field-value">
|
||||||
{{#each uf.value as |v|}}
|
{{#each uf.value as |v|}}
|
||||||
{{! some values are arrays }}
|
{{! some values are arrays }}
|
||||||
<span class="user-field-value-list-item">{{v}}</span>
|
<span class="user-field-value-list-item">
|
||||||
|
{{#if uf.field.searchable}}
|
||||||
|
<LinkTo
|
||||||
|
@route="users"
|
||||||
|
@query={{hash name=v}}
|
||||||
|
{{on "click" (fn this.refreshRoute v)}}
|
||||||
|
>{{v}}</LinkTo>
|
||||||
|
{{else}}
|
||||||
|
{{v}}
|
||||||
|
{{/if}}
|
||||||
|
</span>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{uf.value}}
|
{{uf.value}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
|
@ -248,6 +248,11 @@ export default class UserCardContents extends CardContentsBase.extend(
|
||||||
this._close();
|
this._close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
refreshRoute(value) {
|
||||||
|
this.router.transitionTo({ queryParams: { name: value } });
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
handleShowUser(event) {
|
handleShowUser(event) {
|
||||||
if (wantsNewWindow(event)) {
|
if (wantsNewWindow(event)) {
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
import { htmlSafe } from "@ember/template";
|
|
||||||
|
|
||||||
export default function directoryItemUserFieldValue(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='directory-table__value--user-field'>${content}</span>`
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -260,7 +260,16 @@
|
||||||
<span class="user-field-value">
|
<span class="user-field-value">
|
||||||
{{#each uf.value as |v|}}
|
{{#each uf.value as |v|}}
|
||||||
{{! some values are arrays }}
|
{{! some values are arrays }}
|
||||||
<span class="user-field-value-list-item">{{v}}</span>
|
<span class="user-field-value-list-item">
|
||||||
|
{{#if uf.field.searchable}}
|
||||||
|
<LinkTo
|
||||||
|
@route="users"
|
||||||
|
@query={{hash name=v}}
|
||||||
|
>{{v}}</LinkTo>
|
||||||
|
{{else}}
|
||||||
|
{{v}}
|
||||||
|
{{/if}}
|
||||||
|
</span>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{uf.value}}
|
{{uf.value}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
|
@ -57,6 +57,34 @@ acceptance("User Directory", function () {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Searchable user fields display as links", async function (assert) {
|
||||||
|
pretender.get("/directory_items", () => {
|
||||||
|
return response(cloneJSON(directoryFixtures["directory_items"]));
|
||||||
|
});
|
||||||
|
|
||||||
|
await visit("/u");
|
||||||
|
|
||||||
|
const firstRowUserField = query(
|
||||||
|
".directory .directory-table__body .directory-table__row:first-child .directory-table__value--user-field"
|
||||||
|
);
|
||||||
|
|
||||||
|
const userFieldLink = firstRowUserField.querySelector("a");
|
||||||
|
|
||||||
|
assert.ok(userFieldLink, "User field is displayed as a link");
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
userFieldLink.getAttribute("href"),
|
||||||
|
"/u?name=Blue&order=likes_received",
|
||||||
|
"The link points to the correct URL"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
userFieldLink.textContent.trim(),
|
||||||
|
"Blue",
|
||||||
|
"Link text is correct"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("Visit With Group Filter", async function (assert) {
|
test("Visit With Group Filter", async function (assert) {
|
||||||
await visit("/u?group=trust_level_0");
|
await visit("/u?group=trust_level_0");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
|
@ -75,7 +103,7 @@ acceptance("User Directory", function () {
|
||||||
".directory .directory-table__body .directory-table__row:first-child .directory-table__value--user-field"
|
".directory .directory-table__body .directory-table__row:first-child .directory-table__value--user-field"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.strictEqual(firstRowUserField.textContent, "Blue");
|
assert.strictEqual(firstRowUserField.textContent.trim(), "Blue");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Can sort table via keyboard", async function (assert) {
|
test("Can sort table via keyboard", async function (assert) {
|
||||||
|
|
|
@ -14,7 +14,10 @@ export default {
|
||||||
post_count: 12263,
|
post_count: 12263,
|
||||||
user: {
|
user: {
|
||||||
user_fields: {
|
user_fields: {
|
||||||
3: "Blue",
|
3: {
|
||||||
|
value: ["Blue"],
|
||||||
|
searchable: true
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,6 +4,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.directory {
|
.directory {
|
||||||
|
.directory-value-list-item:not(:empty)
|
||||||
|
~ .directory-value-list-item:not(:empty):before {
|
||||||
|
content: "| ";
|
||||||
|
}
|
||||||
|
|
||||||
margin-bottom: 100px;
|
margin-bottom: 100px;
|
||||||
background: var(--d-content-background);
|
background: var(--d-content-background);
|
||||||
|
|
||||||
|
|
|
@ -123,6 +123,9 @@ class DirectoryItemsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
serializer_opts[:attributes] = active_directory_column_names
|
serializer_opts[:attributes] = active_directory_column_names
|
||||||
|
serializer_opts[:searchable_fields] = UserField.where(searchable: true) if serializer_opts[
|
||||||
|
:user_custom_field_map
|
||||||
|
].present?
|
||||||
|
|
||||||
serialized = serialize_data(result, DirectoryItemSerializer, serializer_opts)
|
serialized = serialize_data(result, DirectoryItemSerializer, serializer_opts)
|
||||||
render_json_dump(
|
render_json_dump(
|
||||||
|
|
|
@ -8,10 +8,25 @@ class DirectoryItemSerializer < ApplicationSerializer
|
||||||
|
|
||||||
def user_fields
|
def user_fields
|
||||||
fields = {}
|
fields = {}
|
||||||
|
user_custom_field_map = @options[:user_custom_field_map] || {}
|
||||||
|
searchable_fields = @options[:searchable_fields] || []
|
||||||
|
|
||||||
object.user_custom_fields.each do |cuf|
|
object.user_custom_fields.each do |custom_field|
|
||||||
user_field_id = @options[:user_custom_field_map][cuf.name]
|
user_field_id = user_custom_field_map[custom_field.name]
|
||||||
fields[user_field_id] = cuf.value if user_field_id
|
next unless user_field_id
|
||||||
|
|
||||||
|
current_value = fields.dig(user_field_id, :value)
|
||||||
|
|
||||||
|
current_value = Array(current_value) if current_value
|
||||||
|
|
||||||
|
new_value = current_value ? current_value << custom_field.value : custom_field.value
|
||||||
|
|
||||||
|
is_searchable = searchable_fields.any? { |field| field.id == user_field_id }
|
||||||
|
|
||||||
|
fields[user_field_id] = {
|
||||||
|
value: new_value.is_a?(Array) ? new_value : [new_value],
|
||||||
|
searchable: is_searchable,
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
fields
|
fields
|
||||||
|
|
|
@ -248,7 +248,9 @@ RSpec.describe DirectoryItemsController do
|
||||||
user_fields.each do |data|
|
user_fields.each do |data|
|
||||||
user = items[data[:order]]["user"]
|
user = items[data[:order]]["user"]
|
||||||
expect(user["username"]).to eq(data[:user].username)
|
expect(user["username"]).to eq(data[:user].username)
|
||||||
expect(user["user_fields"]).to eq({ data[:field].id.to_s => data[:value] })
|
expect(user["user_fields"]).to eq(
|
||||||
|
{ data[:field].id.to_s => { "searchable" => true, "value" => [data[:value]] } },
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -2,23 +2,79 @@
|
||||||
|
|
||||||
RSpec.describe DirectoryItemSerializer do
|
RSpec.describe DirectoryItemSerializer do
|
||||||
fab!(:user)
|
fab!(:user)
|
||||||
|
fab!(:directory_column) do
|
||||||
|
DirectoryColumn.create!(name: "topics_entered", enabled: true, position: 1)
|
||||||
|
end
|
||||||
|
fab!(:user_field_1) { Fabricate(:user_field, name: "user_field_1", searchable: true) }
|
||||||
|
fab!(:user_field_2) { Fabricate(:user_field, name: "user_field_2", searchable: false) }
|
||||||
|
|
||||||
before { DirectoryItem.refresh! }
|
before { DirectoryItem.refresh! }
|
||||||
|
|
||||||
|
context "when serializing user fields" do
|
||||||
|
it "serializes user fields with searchable and non-searchable values" do
|
||||||
|
user.user_custom_fields.create!(name: "user_field_1", value: "Value 1")
|
||||||
|
user.user_custom_fields.create!(name: "user_field_2", value: "Value 2")
|
||||||
|
|
||||||
|
user_fields =
|
||||||
|
serialized_payload(
|
||||||
|
attributes: DirectoryColumn.active_column_names,
|
||||||
|
user_custom_field_map: {
|
||||||
|
"user_field_1" => user_field_1.id,
|
||||||
|
"user_field_2" => user_field_2.id,
|
||||||
|
},
|
||||||
|
searchable_fields: [user_field_1],
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(user_fields).to eq(
|
||||||
|
user_field_1.id => {
|
||||||
|
value: ["Value 1"],
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
user_field_2.id => {
|
||||||
|
value: ["Value 2"],
|
||||||
|
searchable: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "handles multiple values for the same field" do
|
||||||
|
user.user_custom_fields.create!(name: "user_field_1", value: "Value 1")
|
||||||
|
user.user_custom_fields.create!(name: "user_field_1", value: "Another Value")
|
||||||
|
|
||||||
|
user_fields =
|
||||||
|
serialized_payload(
|
||||||
|
attributes: DirectoryColumn.active_column_names,
|
||||||
|
user_custom_field_map: {
|
||||||
|
"user_field_1" => user_field_1.id,
|
||||||
|
},
|
||||||
|
searchable_fields: [],
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(user_fields[user_field_1.id]).to eq(
|
||||||
|
value: ["Value 1", "Another Value"],
|
||||||
|
searchable: false,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when serializing directory columns" do
|
||||||
let :serializer do
|
let :serializer do
|
||||||
directory_item =
|
directory_item =
|
||||||
DirectoryItem.find_by(user: user, period_type: DirectoryItem.period_types[:all])
|
DirectoryItem.find_by(user: user, period_type: DirectoryItem.period_types[:all])
|
||||||
DirectoryItemSerializer.new(directory_item, { attributes: DirectoryColumn.active_column_names })
|
DirectoryItemSerializer.new(
|
||||||
|
directory_item,
|
||||||
|
{ attributes: DirectoryColumn.active_column_names },
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "Serializes attributes for enabled directory_columns" do
|
it "serializes attributes for enabled directory_columns" do
|
||||||
DirectoryColumn.update_all(enabled: true)
|
DirectoryColumn.update_all(enabled: true)
|
||||||
|
|
||||||
payload = serializer.as_json
|
payload = serializer.as_json
|
||||||
expect(payload[:directory_item].keys).to include(*DirectoryColumn.pluck(:name).map(&:to_sym))
|
expect(payload[:directory_item].keys).to include(*DirectoryColumn.pluck(:name).map(&:to_sym))
|
||||||
end
|
end
|
||||||
|
|
||||||
it "Doesn't serialize attributes for disabled directory columns" do
|
it "doesn't serialize attributes for disabled directory columns" do
|
||||||
DirectoryColumn.update_all(enabled: false)
|
DirectoryColumn.update_all(enabled: false)
|
||||||
directory_column = DirectoryColumn.first
|
directory_column = DirectoryColumn.first
|
||||||
directory_column.update(enabled: true)
|
directory_column.update(enabled: true)
|
||||||
|
@ -30,4 +86,12 @@ RSpec.describe DirectoryItemSerializer do
|
||||||
expect(payload[:directory_item]).to have_key(:user)
|
expect(payload[:directory_item]).to have_key(:user)
|
||||||
expect(payload[:directory_item]).to have_key(:time_read)
|
expect(payload[:directory_item]).to have_key(:time_read)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def serialized_payload(serializer_opts)
|
||||||
|
serializer = DirectoryItemSerializer.new(DirectoryItem.find_by(user: user), serializer_opts)
|
||||||
|
serializer.as_json.dig(:directory_item, :user, :user_fields)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue