Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/strapi/config/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,17 +126,17 @@ export default ({ env }) => {
entriesQuery: {
populate: {
icon: true,
feature_tag: { fields: ["title"] },
feature_category: { fields: ["title"] },
},
},
transformEntry({ entry }) {
return {
...entry,
feature_tag: entry.feature_tag?.title ?? null,
feature_category: entry.feature_category?.title ?? null,
}
},
settings: {
filterableAttributes: ["feature_tag"],
filterableAttributes: ["feature_category"],
searchableAttributes: ["title", "description"],
},
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"kind": "collectionType",
"collectionName": "feature_categories",
"info": {
"singularName": "feature-category",
"pluralName": "feature-categories",
"displayName": "Feature category"
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {},
"attributes": {
"title": {
"type": "string"
},
"features": {
"type": "relation",
"relation": "oneToMany",
"target": "api::feature.feature",
"mappedBy": "feature_category"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* feature-category controller
*/

import { factories } from "@strapi/strapi"

export default factories.createCoreController(
"api::feature-category.feature-category"
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* feature-category router
*/

import { factories } from "@strapi/strapi"

export default factories.createCoreRouter(
"api::feature-category.feature-category"
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* feature-category service
*/

import { factories } from "@strapi/strapi"

export default factories.createCoreService(
"api::feature-category.feature-category"
)
6 changes: 6 additions & 0 deletions apps/strapi/src/api/feature/content-types/feature/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@
"target": "api::feature-tag.feature-tag",
"inversedBy": "features"
},
"feature_category": {
"type": "relation",
"relation": "manyToOne",
"target": "api::feature-category.feature-category",
"inversedBy": "features"
},
"url": {
"type": "string"
}
Expand Down
35 changes: 35 additions & 0 deletions apps/strapi/types/generated/contentTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -961,6 +961,36 @@ export interface ApiCountryCountry extends Struct.CollectionTypeSchema {
}
}

export interface ApiFeatureCategoryFeatureCategory
extends Struct.CollectionTypeSchema {
collectionName: "feature_categories"
info: {
displayName: "Feature category"
pluralName: "feature-categories"
singularName: "feature-category"
}
options: {
draftAndPublish: true
}
attributes: {
createdAt: Schema.Attribute.DateTime
createdBy: Schema.Attribute.Relation<"oneToOne", "admin::user"> &
Schema.Attribute.Private
features: Schema.Attribute.Relation<"oneToMany", "api::feature.feature">
locale: Schema.Attribute.String & Schema.Attribute.Private
localizations: Schema.Attribute.Relation<
"oneToMany",
"api::feature-category.feature-category"
> &
Schema.Attribute.Private
publishedAt: Schema.Attribute.DateTime
title: Schema.Attribute.String
updatedAt: Schema.Attribute.DateTime
updatedBy: Schema.Attribute.Relation<"oneToOne", "admin::user"> &
Schema.Attribute.Private
}
}

export interface ApiFeatureTagFeatureTag extends Struct.CollectionTypeSchema {
collectionName: "feature_tags"
info: {
Expand Down Expand Up @@ -1005,6 +1035,10 @@ export interface ApiFeatureFeature extends Struct.CollectionTypeSchema {
createdBy: Schema.Attribute.Relation<"oneToOne", "admin::user"> &
Schema.Attribute.Private
description: Schema.Attribute.Text
feature_category: Schema.Attribute.Relation<
"manyToOne",
"api::feature-category.feature-category"
>
feature_tag: Schema.Attribute.Relation<
"manyToOne",
"api::feature-tag.feature-tag"
Expand Down Expand Up @@ -2213,6 +2247,7 @@ declare module "@strapi/strapi" {
"api::cms-comparison.cms-comparison": ApiCmsComparisonCmsComparison
"api::cms.cms": ApiCmsCms
"api::country.country": ApiCountryCountry
"api::feature-category.feature-category": ApiFeatureCategoryFeatureCategory
"api::feature-tag.feature-tag": ApiFeatureTagFeatureTag
"api::feature.feature": ApiFeatureFeature
"api::footer.footer": ApiFooterFooter
Expand Down
4 changes: 2 additions & 2 deletions apps/ui/src/components/elementary/GlobalSearchModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -302,9 +302,9 @@ export function GlobalSearchModal({
<span className="text-foreground font-medium">
{item.title}
</span>
{item.feature_tag && (
{item.feature_category && (
<span className="text-muted-foreground text-xs">
{item.feature_tag}
{item.feature_category}
</span>
)}
</div>
Expand Down
2 changes: 1 addition & 1 deletion apps/ui/src/components/elementary/global-search-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export async function globalSearch({
"title",
"description",
"url",
"feature_tag",
"feature_category",
],
},
],
Expand Down
2 changes: 1 addition & 1 deletion apps/ui/src/components/elementary/global-search-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface FeatureGlobalHit {
readonly title: string
readonly description?: string | null
readonly url?: string | null
readonly feature_tag?: string | null
readonly feature_category?: string | null
}

export interface DocsGlobalHit {
Expand Down
42 changes: 21 additions & 21 deletions apps/ui/src/components/feature-page/FeaturePagesGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ interface FeaturePagesGridProps {
readonly locale: string
readonly initialHits: readonly FeaturePageHit[]
readonly initialTotal: number
// Complete feature-tag list from the Meilisearch facet distribution (not just
// the tags present in the loaded hits).
readonly tagOptions: readonly FilterOption[]
// Complete feature-category list from the Meilisearch facet distribution (not just
// the categories present in the loaded hits).
readonly categoryOptions: readonly FilterOption[]
readonly pageSize?: number
readonly searchAction: (
args: SearchFeaturePagesArgs
Expand All @@ -39,7 +39,7 @@ export function FeaturePagesGrid({
locale,
initialHits,
initialTotal,
tagOptions,
categoryOptions,
pageSize = DEFAULT_PAGE_SIZE,
searchAction,
className,
Expand All @@ -49,9 +49,9 @@ export function FeaturePagesGrid({
const [hits, setHits] = useState<readonly FeaturePageHit[]>(initialHits)
const [total, setTotal] = useState(initialTotal)
const [query, setQuery] = useState("")
const [selectedTags, setSelectedTags] = useState<ReadonlySet<string>>(
new Set()
)
const [selectedCategories, setSelectedCategories] = useState<
ReadonlySet<string>
>(new Set())
const [isPending, startTransition] = useTransition()

const isFirstRender = useRef(true)
Expand All @@ -67,7 +67,7 @@ export function FeaturePagesGrid({
const res = await searchAction({
locale,
query,
featureTagTitles: [...selectedTags],
featureCategoryTitles: [...selectedCategories],
offset: 0,
limit: pageSize,
})
Expand All @@ -78,16 +78,16 @@ export function FeaturePagesGrid({
}, 200)

return () => clearTimeout(handle)
}, [query, selectedTags, locale, pageSize, searchAction])
}, [query, selectedCategories, locale, pageSize, searchAction])

function toggleTag(tag: string) {
setSelectedTags((prev) => {
function toggleCategory(category: string) {
setSelectedCategories((prev) => {
const next = new Set(prev)

if (next.has(tag)) {
next.delete(tag)
if (next.has(category)) {
next.delete(category)
} else {
next.add(tag)
next.add(category)
}

return next
Expand All @@ -99,7 +99,7 @@ export function FeaturePagesGrid({
const res = await searchAction({
locale,
query,
featureTagTitles: [...selectedTags],
featureCategoryTitles: [...selectedCategories],
offset: hits.length,
limit: pageSize,
})
Expand All @@ -118,10 +118,10 @@ export function FeaturePagesGrid({
onQueryChange={setQuery}
searchPlaceholder={t("searchPlaceholder")}
filterLabel={t("filterTagsLabel")}
filterOptions={tagOptions}
selectedValues={selectedTags}
onToggleValue={toggleTag}
idPrefix="feature-tag"
filterOptions={categoryOptions}
selectedValues={selectedCategories}
onToggleValue={toggleCategory}
idPrefix="feature-category"
/>

<div className="flex min-w-0 flex-1 flex-col gap-8">
Expand All @@ -138,9 +138,9 @@ export function FeaturePagesGrid({

return (
<Card key={item.documentId ?? item.title} className="relative">
{item.feature_tag && (
{item.feature_category && (
<span className="bg-strapi-blue-100 text-strapi-blue-600 absolute top-3 left-3 rounded-sm px-2 py-0.5 text-xs font-medium">
{item.feature_tag}
{item.feature_category}
</span>
)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface FeaturePageHit {
readonly title: string
readonly description?: string | null
readonly url?: string | null
readonly feature_tag?: string | null
readonly feature_category?: string | null
readonly icon?: FeatureIconMedia | null
readonly locale?: string
readonly [key: string]: unknown
Expand All @@ -25,7 +25,7 @@ export interface FeaturePagesSearchResult {
export interface SearchFeaturePagesArgs {
readonly locale: string
readonly query: string
readonly featureTagTitles: readonly string[]
readonly featureCategoryTitles: readonly string[]
readonly offset: number
readonly limit: number
}
24 changes: 12 additions & 12 deletions apps/ui/src/components/feature-page/feature-pages-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,32 @@ import type {
SearchFeaturePagesArgs,
} from "./feature-pages-search-types"

const TAG_FACET = "feature_tag"
const CATEGORY_FACET = "feature_category"

function escape(value: string): string {
// eslint-disable-next-line unicorn/prefer-string-raw -- escaping a single backslash in a template literal is awkward
return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')
}

/**
* Full list of feature-tag options for the sidebar, sourced from the Meilisearch
* facet distribution (every tag present in the index, independent of the
* Full list of feature-category options for the sidebar, sourced from the Meilisearch
* facet distribution (every category present in the index, independent of the
* currently loaded hits). Returns [] if the index is unavailable so SSG keeps working.
*/
export async function getFeatureTagFacets(): Promise<FilterOption[]> {
export async function getFeatureCategoryFacets(): Promise<FilterOption[]> {
const index = getMeilisearchClient().index<FeaturePageHit>(
getFeaturesIndexName()
)

try {
const res = await index.search("", { limit: 0, facets: [TAG_FACET] })
const dist = res.facetDistribution?.[TAG_FACET] ?? {}
const res = await index.search("", { limit: 0, facets: [CATEGORY_FACET] })
const dist = res.facetDistribution?.[CATEGORY_FACET] ?? {}

return Object.keys(dist)
.sort((a, b) => a.localeCompare(b))
.map((tag) => ({ label: tag, value: tag }))
.map((category) => ({ label: category, value: category }))
} catch (error) {
console.error("[getFeatureTagFacets] Meilisearch error", error)
console.error("[getFeatureCategoryFacets] Meilisearch error", error)
Sentry.captureException(error)

return []
Expand All @@ -45,7 +45,7 @@ export async function getFeatureTagFacets(): Promise<FilterOption[]> {

export async function searchFeaturePages({
query,
featureTagTitles,
featureCategoryTitles,
offset,
limit,
}: SearchFeaturePagesArgs): Promise<FeaturePagesSearchResult> {
Expand All @@ -55,9 +55,9 @@ export async function searchFeaturePages({

const filter: string[] = []

if (featureTagTitles.length > 0) {
const list = featureTagTitles.map((t) => `"${escape(t)}"`).join(", ")
filter.push(`feature_tag IN [${list}]`)
if (featureCategoryTitles.length > 0) {
const list = featureCategoryTitles.map((t) => `"${escape(t)}"`).join(", ")
filter.push(`feature_category IN [${list}]`)
}

// Resilient against a missing/unavailable Meilisearch index so SSG (e.g. /[locale]/features
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { use } from "react"

import { Container } from "@/components/elementary/Container"
import {
getFeatureTagFacets,
getFeatureCategoryFacets,
searchFeaturePages,
} from "@/components/feature-page/feature-pages-search"
import { FeaturePagesGrid } from "@/components/feature-page/FeaturePagesGrid"
Expand All @@ -25,20 +25,20 @@ export function StrapiDynamicFeaturesGrid({
searchFeaturePages({
locale,
query: "",
featureTagTitles: [],
featureCategoryTitles: [],
offset: 0,
limit: INITIAL_PAGE_SIZE,
})
)
const tagOptions = use(getFeatureTagFacets())
const categoryOptions = use(getFeatureCategoryFacets())

return (
<Container className="py-8 lg:py-12">
<FeaturePagesGrid
locale={locale}
initialHits={initial.hits}
initialTotal={initial.total}
tagOptions={tagOptions}
categoryOptions={categoryOptions}
pageSize={INITIAL_PAGE_SIZE}
searchAction={searchFeaturePages}
/>
Expand Down
Loading