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:
Jean 2024-11-06 13:35:30 -04:00 committed by GitHub
parent 13c7773036
commit 708533b1e0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 227 additions and 39 deletions

View File

@ -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>
}

View File

@ -370,7 +370,17 @@
<span class="user-field-value">
{{#each uf.value as |v|}}
{{! 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}}
{{uf.value}}
{{/each}}

View File

@ -248,6 +248,11 @@ export default class UserCardContents extends CardContentsBase.extend(
this._close();
}
@action
refreshRoute(value) {
this.router.transitionTo({ queryParams: { name: value } });
}
@action
handleShowUser(event) {
if (wantsNewWindow(event)) {

View File

@ -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>`
);
}

View File

@ -260,7 +260,16 @@
<span class="user-field-value">
{{#each uf.value as |v|}}
{{! 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}}
{{uf.value}}
{{/each}}

View File

@ -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) {
await visit("/u?group=trust_level_0");
assert.ok(
@ -75,7 +103,7 @@ acceptance("User Directory", function () {
".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) {

View File

@ -14,7 +14,10 @@ export default {
post_count: 12263,
user: {
user_fields: {
3: "Blue",
3: {
value: ["Blue"],
searchable: true
},
},
},
},

View File

@ -4,6 +4,11 @@
}
.directory {
.directory-value-list-item:not(:empty)
~ .directory-value-list-item:not(:empty):before {
content: "| ";
}
margin-bottom: 100px;
background: var(--d-content-background);

View File

@ -123,6 +123,9 @@ class DirectoryItemsController < ApplicationController
end
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)
render_json_dump(

View File

@ -8,10 +8,25 @@ class DirectoryItemSerializer < ApplicationSerializer
def user_fields
fields = {}
user_custom_field_map = @options[:user_custom_field_map] || {}
searchable_fields = @options[:searchable_fields] || []
object.user_custom_fields.each do |cuf|
user_field_id = @options[:user_custom_field_map][cuf.name]
fields[user_field_id] = cuf.value if user_field_id
object.user_custom_fields.each do |custom_field|
user_field_id = user_custom_field_map[custom_field.name]
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
fields

View File

@ -248,7 +248,9 @@ RSpec.describe DirectoryItemsController do
user_fields.each do |data|
user = items[data[:order]]["user"]
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

View File

@ -2,32 +2,96 @@
RSpec.describe DirectoryItemSerializer do
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! }
let :serializer do
directory_item =
DirectoryItem.find_by(user: user, period_type: DirectoryItem.period_types[:all])
DirectoryItemSerializer.new(directory_item, { attributes: DirectoryColumn.active_column_names })
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
it "Serializes attributes for enabled directory_columns" do
DirectoryColumn.update_all(enabled: true)
context "when serializing directory columns" do
let :serializer do
directory_item =
DirectoryItem.find_by(user: user, period_type: DirectoryItem.period_types[:all])
DirectoryItemSerializer.new(
directory_item,
{ attributes: DirectoryColumn.active_column_names },
)
end
payload = serializer.as_json
expect(payload[:directory_item].keys).to include(*DirectoryColumn.pluck(:name).map(&:to_sym))
it "serializes attributes for enabled directory_columns" do
DirectoryColumn.update_all(enabled: true)
payload = serializer.as_json
expect(payload[:directory_item].keys).to include(*DirectoryColumn.pluck(:name).map(&:to_sym))
end
it "doesn't serialize attributes for disabled directory columns" do
DirectoryColumn.update_all(enabled: false)
directory_column = DirectoryColumn.first
directory_column.update(enabled: true)
payload = serializer.as_json
expect(payload[:directory_item].keys.count).to eq(4)
expect(payload[:directory_item]).to have_key(directory_column.name.to_sym)
expect(payload[:directory_item]).to have_key(:id)
expect(payload[:directory_item]).to have_key(:user)
expect(payload[:directory_item]).to have_key(:time_read)
end
end
it "Doesn't serialize attributes for disabled directory columns" do
DirectoryColumn.update_all(enabled: false)
directory_column = DirectoryColumn.first
directory_column.update(enabled: true)
private
payload = serializer.as_json
expect(payload[:directory_item].keys.count).to eq(4)
expect(payload[:directory_item]).to have_key(directory_column.name.to_sym)
expect(payload[:directory_item]).to have_key(:id)
expect(payload[:directory_item]).to have_key(:user)
expect(payload[:directory_item]).to have_key(:time_read)
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