FEATURE: add setting to show tags by group (#138)
* FEATURE: add setting to show tags by group
This commit is contained in:
parent
e95388ea0b
commit
d4ab4080db
|
@ -48,6 +48,7 @@ export default Controller.extend({
|
||||||
categories: readOnly("model.categories"),
|
categories: readOnly("model.categories"),
|
||||||
topics: alias("model.topics.topic_list.topics"),
|
topics: alias("model.topics.topic_list.topics"),
|
||||||
tags: readOnly("model.tags"),
|
tags: readOnly("model.tags"),
|
||||||
|
tagGroups: readOnly("model.tag_groups"),
|
||||||
topicCount: alias("model.topic_count"),
|
topicCount: alias("model.topic_count"),
|
||||||
emptyResults: equal("topicCount", 0),
|
emptyResults: equal("topicCount", 0),
|
||||||
|
|
||||||
|
@ -142,6 +143,39 @@ export default Controller.extend({
|
||||||
return tags;
|
return tags;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@discourseComputed("tagGroups", "tagSort", "tagFilter")
|
||||||
|
sortedTagGroups(tagGroups, tagSort, filter) {
|
||||||
|
let { type, direction } = tagSort;
|
||||||
|
let sortedTagGroups = [...tagGroups];
|
||||||
|
|
||||||
|
if (type === "numeric") {
|
||||||
|
sortedTagGroups.forEach((group) => {
|
||||||
|
group.totalCount = group.tags.reduce(
|
||||||
|
(acc, curr) => acc + curr.count,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
sortedTagGroups.sort((a, b) => b.totalCount - a.totalCount);
|
||||||
|
} else {
|
||||||
|
sortedTagGroups.sort((a, b) =>
|
||||||
|
a.name.toLowerCase().localeCompare(b.name.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (direction === "desc") {
|
||||||
|
sortedTagGroups.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.showTagFilter) {
|
||||||
|
return sortedTagGroups.filter((tag) =>
|
||||||
|
tag.id.toLowerCase().includes(filter.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortedTagGroups;
|
||||||
|
},
|
||||||
|
|
||||||
@discourseComputed("tagSort")
|
@discourseComputed("tagSort")
|
||||||
tagSortNumericIcon(tagSort) {
|
tagSortNumericIcon(tagSort) {
|
||||||
if (tagSort.type === "numeric" && tagSort.direction === "asc") {
|
if (tagSort.type === "numeric" && tagSort.direction === "asc") {
|
||||||
|
@ -192,9 +226,17 @@ export default Controller.extend({
|
||||||
return !!filterTags;
|
return !!filterTags;
|
||||||
},
|
},
|
||||||
|
|
||||||
@discourseComputed()
|
@discourseComputed("siteSettings.tagging_enabled", "shouldShowTagsByGroup")
|
||||||
shouldShowTags() {
|
shouldShowTags(tagging_enabled, shouldShowTagsByGroup) {
|
||||||
return this.siteSettings.tagging_enabled;
|
return tagging_enabled && !shouldShowTagsByGroup;
|
||||||
|
},
|
||||||
|
|
||||||
|
@discourseComputed(
|
||||||
|
"siteSettings.show_tags_by_group",
|
||||||
|
"siteSettings.docs_tag_groups"
|
||||||
|
)
|
||||||
|
shouldShowTagsByGroup(show_tags_by_group, docs_tag_groups) {
|
||||||
|
return show_tags_by_group && Boolean(docs_tag_groups);
|
||||||
},
|
},
|
||||||
|
|
||||||
@discourseComputed()
|
@discourseComputed()
|
||||||
|
|
|
@ -139,6 +139,68 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#if (and tagGroups shouldShowTagsByGroup)}}
|
||||||
|
<div class="docs-items docs-tags">
|
||||||
|
<section class="item-controls">
|
||||||
|
<h3>{{i18n "docs.tags"}}</h3>
|
||||||
|
<div class="item-controls-buttons">
|
||||||
|
<DButton
|
||||||
|
class={{if
|
||||||
|
(eq tagSort.type "alpha")
|
||||||
|
"tags-alphabet active"
|
||||||
|
"tags-alphabet"
|
||||||
|
}}
|
||||||
|
@icon={{this.tagSortAlphaIcon}}
|
||||||
|
@action={{toggleTagSort}}
|
||||||
|
@actionParam="alpha"
|
||||||
|
/>
|
||||||
|
<DButton
|
||||||
|
class={{if
|
||||||
|
(eq tagSort.type "numeric")
|
||||||
|
"tags-amount active"
|
||||||
|
"tags-amount"
|
||||||
|
}}
|
||||||
|
@icon={{this.tagSortNumericIcon}}
|
||||||
|
@action={{toggleTagSort}}
|
||||||
|
@actionParam="numeric"
|
||||||
|
/>
|
||||||
|
<PluginOutlet @name="tags-controls-buttons-bottom" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{{#if showTagFilter}}
|
||||||
|
<Input
|
||||||
|
@value={{tagFilter}}
|
||||||
|
class="filter"
|
||||||
|
placeholder={{i18n "docs.tags_filter_placeholder"}}
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
<PluginOutlet
|
||||||
|
@name="before-docs-tag-list"
|
||||||
|
@connectorTagName="div"
|
||||||
|
@outletArgs={{hash
|
||||||
|
tags=tags
|
||||||
|
updateSelectedTags=updateSelectedTags
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ul>
|
||||||
|
{{#each sortedTagGroups as |tagGroup|}}
|
||||||
|
<li class="docs-filter-tag-group-{{tagGroup.id}}">
|
||||||
|
{{tagGroup.name}}
|
||||||
|
<ul>
|
||||||
|
{{#each tagGroup.tags as |tag|}}
|
||||||
|
<li class="docs-filter-tag-{{id}}">
|
||||||
|
<DocsTag
|
||||||
|
@tag={{tag}}
|
||||||
|
@selectTag={{action "updateSelectedTags" tag}}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -4,5 +4,7 @@ en:
|
||||||
docs_categories: "A list of category slugs to include in docs"
|
docs_categories: "A list of category slugs to include in docs"
|
||||||
docs_tags: "A list of tags to include in docs"
|
docs_tags: "A list of tags to include in docs"
|
||||||
docs_add_solved_filter: "Adds a filter for solved topics -- requires Discourse Solved to be installed and enabled"
|
docs_add_solved_filter: "Adds a filter for solved topics -- requires Discourse Solved to be installed and enabled"
|
||||||
|
show_tags_by_group: "Organize tags using Tag Groups. Create groups to categorize related tags."
|
||||||
|
docs_tag_groups: "The Group Tags used to show tags by group."
|
||||||
docs_add_to_top_menu: "Adds a link to the top menu to navigate to the Docs view"
|
docs_add_to_top_menu: "Adds a link to the top menu to navigate to the Docs view"
|
||||||
docs_add_search_menu_tip: "Adds the tip \"in:docs\" to the search menu random tips"
|
docs_add_search_menu_tip: "Adds the tip \"in:docs\" to the search menu random tips"
|
||||||
|
|
|
@ -6,6 +6,13 @@ plugins:
|
||||||
type: category_list
|
type: category_list
|
||||||
default: ""
|
default: ""
|
||||||
client: true
|
client: true
|
||||||
|
show_tags_by_group:
|
||||||
|
default: false
|
||||||
|
client: true
|
||||||
|
docs_tag_groups:
|
||||||
|
type: tag_group_list
|
||||||
|
default: ""
|
||||||
|
client: true
|
||||||
docs_tags:
|
docs_tags:
|
||||||
type: tag_list
|
type: tag_list
|
||||||
default: ""
|
default: ""
|
||||||
|
|
|
@ -21,7 +21,8 @@ module Docs
|
||||||
opts = { no_definitions: true, limit: false }
|
opts = { no_definitions: true, limit: false }
|
||||||
tq = TopicQuery.new(@user, opts)
|
tq = TopicQuery.new(@user, opts)
|
||||||
results = tq.list_docs_topics
|
results = tq.list_docs_topics
|
||||||
results = results.left_outer_joins(:tags)
|
results =
|
||||||
|
results.left_outer_joins(SiteSetting.show_tags_by_group ? { tags: :tag_groups } : :tags)
|
||||||
results = results.references(:categories)
|
results = results.references(:categories)
|
||||||
results =
|
results =
|
||||||
results.where("topics.category_id IN (?)", Query.categories).or(
|
results.where("topics.category_id IN (?)", Query.categories).or(
|
||||||
|
@ -99,8 +100,25 @@ module Docs
|
||||||
INNER JOIN topic_tags ttx ON ttx.topic_id = topics.id
|
INNER JOIN topic_tags ttx ON ttx.topic_id = topics.id
|
||||||
INNER JOIN tags t2 ON t2.id = ttx.tag_id
|
INNER JOIN tags t2 ON t2.id = ttx.tag_id
|
||||||
SQL
|
SQL
|
||||||
tags = count_query.group("t2.name").reorder("").count
|
|
||||||
tags = create_tags_object(tags)
|
if SiteSetting.show_tags_by_group
|
||||||
|
enabled_tag_groups = SiteSetting.docs_tag_groups.split("|")
|
||||||
|
subquery = TagGroup.where(name: enabled_tag_groups).select(:id)
|
||||||
|
results = results.joins(tags: :tag_groups).where(tag_groups: { id: subquery })
|
||||||
|
|
||||||
|
tags =
|
||||||
|
count_query
|
||||||
|
.joins(tags: :tag_groups)
|
||||||
|
.where(tag_groups: { id: subquery })
|
||||||
|
.group("tag_groups.id", "tag_groups.name", "tags.name")
|
||||||
|
.reorder("")
|
||||||
|
.count
|
||||||
|
|
||||||
|
tags = create_group_tags_object(tags)
|
||||||
|
else
|
||||||
|
tags = count_query.group("t2.name").reorder("").count
|
||||||
|
tags = create_tags_object(tags)
|
||||||
|
end
|
||||||
|
|
||||||
categories =
|
categories =
|
||||||
results
|
results
|
||||||
|
@ -145,7 +163,33 @@ module Docs
|
||||||
topic_list["load_more_url"] = nil
|
topic_list["load_more_url"] = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
{ tags: tags, categories: categories, topics: topic_list, topic_count: results_length }
|
tags_key = SiteSetting.show_tags_by_group ? :tag_groups : :tags
|
||||||
|
{
|
||||||
|
tags_key => tags,
|
||||||
|
:categories => categories,
|
||||||
|
:topics => topic_list,
|
||||||
|
:topic_count => results_length,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_group_tags_object(tags)
|
||||||
|
tags_hash = ActiveSupport::OrderedHash.new
|
||||||
|
allowed_tags = DiscourseTagging.filter_allowed_tags(Guardian.new(@user)).map(&:name)
|
||||||
|
|
||||||
|
tags.each do |group_tags_data, count|
|
||||||
|
group_tag_id, group_tag_name, tag_name = group_tags_data
|
||||||
|
active = @filters[:tags]&.include?(tag_name)
|
||||||
|
|
||||||
|
tags_hash[group_tag_id] ||= { id: group_tag_id, name: group_tag_name, tags: [] }
|
||||||
|
tags_hash[group_tag_id][:tags] << { id: tag_name, count: count, active: active }
|
||||||
|
end
|
||||||
|
|
||||||
|
tags_hash
|
||||||
|
.transform_values do |group|
|
||||||
|
group[:tags] = group[:tags].filter { |tag| allowed_tags.include?(tag[:id]) }
|
||||||
|
group
|
||||||
|
end
|
||||||
|
.values
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_tags_object(tags)
|
def create_tags_object(tags)
|
||||||
|
|
|
@ -8,6 +8,14 @@ describe Docs::DocsController do
|
||||||
fab!(:topic2) { Fabricate(:topic, title: "I love pineapple today", category: category) }
|
fab!(:topic2) { Fabricate(:topic, title: "I love pineapple today", category: category) }
|
||||||
fab!(:tag) { Fabricate(:tag, topics: [topic], name: "test") }
|
fab!(:tag) { Fabricate(:tag, topics: [topic], name: "test") }
|
||||||
|
|
||||||
|
def get_tag_attributes(tag)
|
||||||
|
{ "id" => tag.name, "count" => 1 }
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_tags_from_response(response_tags)
|
||||||
|
response_tags.map { |tag| tag.except("active") }
|
||||||
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
SiteSetting.tagging_enabled = true
|
SiteSetting.tagging_enabled = true
|
||||||
SiteSetting.docs_enabled = true
|
SiteSetting.docs_enabled = true
|
||||||
|
@ -97,6 +105,57 @@ describe Docs::DocsController do
|
||||||
expect(tags.size).to eq(3)
|
expect(tags.size).to eq(3)
|
||||||
expect(topics.size).to eq(1)
|
expect(topics.size).to eq(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "when show_tags_by_group is enabled" do
|
||||||
|
fab!(:tag4) { Fabricate(:tag, topics: [topic], name: "test4") }
|
||||||
|
|
||||||
|
fab!(:tag_group_1) { Fabricate(:tag_group, name: "test-test2", tag_names: %w[test test2]) }
|
||||||
|
fab!(:tag_group_2) do
|
||||||
|
Fabricate(:tag_group, name: "test3-test4", tag_names: %w[test3 test4])
|
||||||
|
end
|
||||||
|
fab!(:non_docs_tag_group) do
|
||||||
|
Fabricate(:tag_group, name: "non-docs-group", tag_names: %w[test3])
|
||||||
|
end
|
||||||
|
fab!(:empty_tag_group) { Fabricate(:tag_group, name: "empty-group") }
|
||||||
|
|
||||||
|
let(:docs_json_path) { "/#{GlobalSetting.docs_path}.json" }
|
||||||
|
let(:parsed_body) { response.parsed_body }
|
||||||
|
let(:tag_groups) { parsed_body["tag_groups"] }
|
||||||
|
let(:tag_ids) { tag_groups.map { |group| group["id"] } }
|
||||||
|
|
||||||
|
before do
|
||||||
|
SiteSetting.show_tags_by_group = true
|
||||||
|
SiteSetting.docs_tag_groups = "test-test2|test3-test4"
|
||||||
|
get docs_json_path
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should add groups to the tags attribute" do
|
||||||
|
get docs_json_path
|
||||||
|
expect(get_tags_from_response(tag_groups[0]["tags"])).to contain_exactly(
|
||||||
|
*[tag, tag2].map { |t| get_tag_attributes(t) },
|
||||||
|
)
|
||||||
|
expect(get_tags_from_response(tag_groups[1]["tags"])).to contain_exactly(
|
||||||
|
*[tag3, tag4].map { |t| get_tag_attributes(t) },
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "only displays tag groups that are enabled" do
|
||||||
|
SiteSetting.docs_tag_groups = "test3-test4"
|
||||||
|
get docs_json_path
|
||||||
|
expect(tag_groups.size).to eq(1)
|
||||||
|
expect(get_tags_from_response(tag_groups[0]["tags"])).to contain_exactly(
|
||||||
|
*[tag3, tag4].map { |t| get_tag_attributes(t) },
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not return tag groups without tags" do
|
||||||
|
expect(tag_ids).not_to include(empty_tag_group.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not return non-docs tag groups" do
|
||||||
|
expect(tag_ids).not_to include(non_docs_tag_group.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when filtering by category" do
|
context "when filtering by category" do
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
} from "discourse/tests/helpers/qunit-helpers";
|
} from "discourse/tests/helpers/qunit-helpers";
|
||||||
import { test } from "qunit";
|
import { test } from "qunit";
|
||||||
import docsFixtures from "../fixtures/docs";
|
import docsFixtures from "../fixtures/docs";
|
||||||
|
import docsShowTagGroupsFixtures from "../fixtures/docs-show-tag-groups";
|
||||||
import { click, visit } from "@ember/test-helpers";
|
import { click, visit } from "@ember/test-helpers";
|
||||||
|
|
||||||
let DOCS_URL_PATH = "docs";
|
let DOCS_URL_PATH = "docs";
|
||||||
|
@ -73,6 +74,82 @@ acceptance("Docs", function (needs) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
acceptance("Docs - with tag groups enabled", function (needs) {
|
||||||
|
needs.user();
|
||||||
|
needs.site({ docs_path: DOCS_URL_PATH });
|
||||||
|
needs.settings({
|
||||||
|
docs_enabled: true,
|
||||||
|
navigation_menu: "legacy",
|
||||||
|
});
|
||||||
|
|
||||||
|
function getRootElementText(selector) {
|
||||||
|
return Array.from(query(selector).childNodes)
|
||||||
|
.filter((node) => node.nodeType === Node.TEXT_NODE)
|
||||||
|
.map((node) => node.textContent.trim())
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertTagGroup(assert, tagGroup) {
|
||||||
|
let groupTagSelector = `.docs-filter-tag-group-${tagGroup.id}`;
|
||||||
|
assert.equal(
|
||||||
|
getRootElementText(groupTagSelector),
|
||||||
|
tagGroup.expectedTagGroupName
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
query(`${groupTagSelector} .docs-tag .docs-item-id`).innerText,
|
||||||
|
tagGroup.expectedTagName
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
query(`${groupTagSelector} .docs-tag .docs-item-count`).innerText,
|
||||||
|
tagGroup.expectedCount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
needs.pretender((server, helper) => {
|
||||||
|
server.get("/" + DOCS_URL_PATH + ".json", () => {
|
||||||
|
return helper.response(docsShowTagGroupsFixtures);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Show tag groups", async function (assert) {
|
||||||
|
this.siteSettings.tagging_enabled = true;
|
||||||
|
this.siteSettings.show_tags_by_group = true;
|
||||||
|
this.siteSettings.docs_tag_groups =
|
||||||
|
"my-tag-group-1|my-tag-group-2|my-tag-group-3";
|
||||||
|
|
||||||
|
await visit("/");
|
||||||
|
await click("#toggle-hamburger-menu");
|
||||||
|
await click(".docs-link");
|
||||||
|
|
||||||
|
assert.equal(query(".docs-category .docs-item-id").innerText, "bug");
|
||||||
|
assert.equal(query(".docs-category .docs-item-count").innerText, "119");
|
||||||
|
|
||||||
|
const expectedTagGroups = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
expectedTagGroupName: "my-tag-group-1",
|
||||||
|
expectedTagName: "something 1",
|
||||||
|
expectedCount: "50",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
expectedTagGroupName: "my-tag-group-2",
|
||||||
|
expectedTagName: "something 2",
|
||||||
|
expectedCount: "10",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
expectedTagGroupName: "my-tag-group-3",
|
||||||
|
expectedTagName: "something 3",
|
||||||
|
expectedCount: "1",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let tagGroup of expectedTagGroups) {
|
||||||
|
assertTagGroup(assert, tagGroup);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
acceptance("Docs - empty state", function (needs) {
|
acceptance("Docs - empty state", function (needs) {
|
||||||
needs.user();
|
needs.user();
|
||||||
needs.site({ docs_path: DOCS_URL_PATH });
|
needs.site({ docs_path: DOCS_URL_PATH });
|
||||||
|
|
|
@ -0,0 +1,135 @@
|
||||||
|
export default {
|
||||||
|
tag_groups: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "my-tag-group-1",
|
||||||
|
tags: [
|
||||||
|
{
|
||||||
|
id: "something 1",
|
||||||
|
count: 50,
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "something 2",
|
||||||
|
count: 10,
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "my-tag-group-2",
|
||||||
|
tags: [
|
||||||
|
{
|
||||||
|
id: "something 2",
|
||||||
|
count: 10,
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: "my-tag-group-3",
|
||||||
|
tags: [
|
||||||
|
{
|
||||||
|
id: "something 3",
|
||||||
|
count: 1,
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: "my-tag-group-4",
|
||||||
|
tags: [
|
||||||
|
{
|
||||||
|
id: "something 4",
|
||||||
|
count: 1,
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
categories: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
count: 119,
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
topics: {
|
||||||
|
users: [
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
username: "cvx",
|
||||||
|
name: "Jarek",
|
||||||
|
avatar_template: "/letter_avatar/cvx/{size}/2.png",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
primary_groups: [],
|
||||||
|
topic_list: {
|
||||||
|
can_create_topic: true,
|
||||||
|
draft: null,
|
||||||
|
draft_key: "new_topic",
|
||||||
|
draft_sequence: 94,
|
||||||
|
per_page: 30,
|
||||||
|
top_tags: ["something"],
|
||||||
|
topics: [
|
||||||
|
{
|
||||||
|
id: 54881,
|
||||||
|
title: "Importing from Software X",
|
||||||
|
fancy_title: "Importing from Software X",
|
||||||
|
slug: "importing-from-software-x",
|
||||||
|
posts_count: 112,
|
||||||
|
reply_count: 72,
|
||||||
|
highest_post_number: 122,
|
||||||
|
image_url: null,
|
||||||
|
created_at: "2016-12-28T14:59:29.396Z",
|
||||||
|
last_posted_at: "2020-11-14T16:21:35.720Z",
|
||||||
|
bumped: true,
|
||||||
|
bumped_at: "2020-11-14T16:21:35.720Z",
|
||||||
|
archetype: "regular",
|
||||||
|
unseen: false,
|
||||||
|
pinned: false,
|
||||||
|
unpinned: null,
|
||||||
|
visible: true,
|
||||||
|
closed: false,
|
||||||
|
archived: false,
|
||||||
|
bookmarked: null,
|
||||||
|
liked: null,
|
||||||
|
tags: ["something"],
|
||||||
|
views: 15222,
|
||||||
|
like_count: 167,
|
||||||
|
has_summary: true,
|
||||||
|
last_poster_username: "cvx",
|
||||||
|
category_id: 1,
|
||||||
|
pinned_globally: false,
|
||||||
|
featured_link: null,
|
||||||
|
has_accepted_answer: false,
|
||||||
|
posters: [
|
||||||
|
{
|
||||||
|
extras: null,
|
||||||
|
description: "Original Poster",
|
||||||
|
user_id: 2,
|
||||||
|
primary_group_id: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extras: null,
|
||||||
|
description: "Frequent Poster",
|
||||||
|
user_id: 2,
|
||||||
|
primary_group_id: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extras: "latest",
|
||||||
|
description: "Most Recent Poster",
|
||||||
|
user_id: 2,
|
||||||
|
primary_group_id: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
load_more_url: "/docs.json?page=1",
|
||||||
|
},
|
||||||
|
search_count: null,
|
||||||
|
};
|
Loading…
Reference in New Issue