diff --git a/frontend/src/components/VErrorSection/VServerTimeout.vue b/frontend/src/components/VErrorSection/VServerTimeout.vue index 1bf49d2be6e..0892d3d243b 100644 --- a/frontend/src/components/VErrorSection/VServerTimeout.vue +++ b/frontend/src/components/VErrorSection/VServerTimeout.vue @@ -1,6 +1,6 @@ diff --git a/frontend/src/components/VErrorSection/meta/VErrorSection.stories.mdx b/frontend/src/components/VErrorSection/meta/VErrorSection.stories.mdx index 5b1d028d52e..5558332e01d 100644 --- a/frontend/src/components/VErrorSection/meta/VErrorSection.stories.mdx +++ b/frontend/src/components/VErrorSection/meta/VErrorSection.stories.mdx @@ -12,6 +12,7 @@ import VNoResults from "~/components/VErrorSection/VNoResults.vue" import VServerTimeout from "~/components/VErrorSection/VServerTimeout.vue" import { NO_RESULT, SERVER_TIMEOUT } from "~/constants/errors" +import { INCLUDE_SENSITIVE_QUERY_PARAM } from "~/constants/content-safety" @@ -31,7 +32,7 @@ export const NoResultsTemplate = (args) => ({ - + `, components: { VErrorSection, VErrorImage, VNoResults }, @@ -46,12 +47,7 @@ export const NoResultsTemplate = (args) => ({ args={{ errorCode: NO_RESULT, type: "image", - query: { - license: "", - license_type: "", - mature: false, - q: "sad person", - }, + searchTerm: "sad person", }} > {NoResultsTemplate.bind({})} diff --git a/frontend/src/constants/content-safety.ts b/frontend/src/constants/content-safety.ts index e92a97c2337..d299a539954 100644 --- a/frontend/src/constants/content-safety.ts +++ b/frontend/src/constants/content-safety.ts @@ -27,3 +27,6 @@ export const SENSITIVE_MEDIA_STATES = [ ] as const export type SensitiveMediaVisibility = (typeof SENSITIVE_MEDIA_STATES)[number] + +export const INCLUDE_SENSITIVE_QUERY_PARAM = + "unstable__include_sensitive_results" diff --git a/frontend/src/constants/filters.ts b/frontend/src/constants/filters.ts index d44bfe3b79d..2a463de7972 100644 --- a/frontend/src/constants/filters.ts +++ b/frontend/src/constants/filters.ts @@ -23,10 +23,13 @@ export interface Filters { audioProviders: FilterItem[] imageProviders: FilterItem[] searchBy: FilterItem[] - mature: FilterItem[] + includeSensitiveResults: FilterItem[] } export type FilterCategory = keyof Filters -export type NonMatureFilterCategory = Exclude +export type NonMatureFilterCategory = Exclude< + FilterCategory, + "includeSensitiveResults" +> /** * List of filters available for each search type. The order of the keys @@ -43,7 +46,7 @@ export const mediaFilterKeys = deepFreeze>( "sizes", "imageProviders", "searchBy", - "mature", + "includeSensitiveResults", ], [AUDIO]: [ "licenseTypes", @@ -53,11 +56,16 @@ export const mediaFilterKeys = deepFreeze>( "lengths", "audioProviders", "searchBy", - "mature", + "includeSensitiveResults", ], [VIDEO]: [], [MODEL_3D]: [], - [ALL_MEDIA]: ["licenseTypes", "licenses", "searchBy", "mature"], + [ALL_MEDIA]: [ + "licenseTypes", + "licenses", + "searchBy", + "includeSensitiveResults", + ], } ) @@ -101,7 +109,7 @@ const filterCodesPerCategory = deepFreeze>({ audioProviders: [], imageProviders: [], searchBy: ["creator"], - mature: ["mature"], + includeSensitiveResults: ["includeSensitiveResults"], }) /** * Converts the filterCodesPerCategory object into the format that's used by the filter store. diff --git a/frontend/src/stores/search.ts b/frontend/src/stores/search.ts index 48d0ff78cda..9c53e1acaa0 100644 --- a/frontend/src/stores/search.ts +++ b/frontend/src/stores/search.ts @@ -33,6 +33,7 @@ import { mediaFilterKeys, mediaUniqueFilterKeys, } from "~/constants/filters" +import { INCLUDE_SENSITIVE_QUERY_PARAM } from "~/constants/content-safety" import { useProviderStore } from "~/stores/provider" import { useFeatureFlagStore } from "~/stores/feature-flag" @@ -70,7 +71,11 @@ function computeQueryParams( return queryKeys.reduce( (obj, key) => { if (key !== "q" && query[key]?.length) { - obj[key] = query[key] + if (key !== INCLUDE_SENSITIVE_QUERY_PARAM) { + obj[key] = query[key] + } else if (query[key] === "includeSensitiveResults") { + obj[key] = "true" + } } return obj }, @@ -114,11 +119,11 @@ export const useSearchStore = defineStore("search", { }, /** - * Returns the number of checked filters, excluding the `mature` filter. + * Returns the number of checked filters, excluding the `includeSensitiveResults` filter. */ appliedFilterCount(state) { const filterKeys = mediaFilterKeys[state.searchType].filter( - (f) => f !== "mature" + (f) => f !== "includeSensitiveResults" ) return filterKeys.reduce((count, filterCategory) => { return ( @@ -131,11 +136,14 @@ export const useSearchStore = defineStore("search", { * Returns the object with filters for selected search type, * with codes, names for i18n labels, and checked status. * - * Excludes `searchBy` and `mature` filters that we don't display. + * Excludes `searchBy` and `includeSensitiveResults` filters that we don't display. */ searchFilters(state) { return mediaFilterKeys[state.searchType] - .filter((filterKey) => !["searchBy", "mature"].includes(filterKey)) + .filter( + (filterKey) => + !["searchBy", "includeSensitiveResults"].includes(filterKey) + ) .reduce((obj, filterKey) => { obj[filterKey] = this.filters[filterKey] return obj @@ -143,7 +151,7 @@ export const useSearchStore = defineStore("search", { }, /** - * True if any filter for selected search type except `mature` is checked. + * True if any filter for selected search type except `includeSensitiveResults` is checked. */ isAnyFilterApplied() { const filterEntries = Object.entries(this.searchFilters) as [ @@ -152,7 +160,8 @@ export const useSearchStore = defineStore("search", { ][] return filterEntries.some( ([filterKey, filterItems]) => - filterKey !== "mature" && filterItems.some((filter) => filter.checked) + filterKey !== "includeSensitiveResults" && + filterItems.some((filter) => filter.checked) ) }, /** @@ -413,12 +422,11 @@ export const useSearchStore = defineStore("search", { this.searchType = queryStringToSearchType(path) if (!isSearchTypeSupported(this.searchType)) return - // When setting filters from URL query, 'mature' has a value of 'true', - // but we need the 'mature' code. Creating a local shallow copy to prevent mutation. - if (query.mature === "true") { - query.mature = "mature" - } else { - delete query.mature + // TODO: Convert the 'unstable__include_sensitive_results=true' query param + // to `includeSensitiveResults` filterType and the filterCode with the same name: + // includeSensitiveResults: { code: includeSensitiveResults, name: '...', checked: true } + if (!(query[INCLUDE_SENSITIVE_QUERY_PARAM] === "true")) { + delete query[INCLUDE_SENSITIVE_QUERY_PARAM] } const newFilterData = queryToFilterData({ diff --git a/frontend/src/utils/search-query-transform.ts b/frontend/src/utils/search-query-transform.ts index b258e5bf744..442d50b2cd1 100644 --- a/frontend/src/utils/search-query-transform.ts +++ b/frontend/src/utils/search-query-transform.ts @@ -11,6 +11,7 @@ import { SupportedSearchType, supportedSearchTypes, } from "~/constants/media" +import { INCLUDE_SENSITIVE_QUERY_PARAM } from "~/constants/content-safety" import { getParameterByName } from "~/utils/url-params" import { deepClone } from "~/utils/clone" @@ -28,7 +29,7 @@ export interface ApiQueryParams { category?: string source?: string length?: string - mature?: string + [INCLUDE_SENSITIVE_QUERY_PARAM]?: string page?: string /** * A conditional to show audio waveform data. @@ -58,7 +59,7 @@ const filterPropertyMappings: Record = { audioProviders: "source", imageProviders: "source", searchBy: "searchBy", - mature: "mature", + includeSensitiveResults: INCLUDE_SENSITIVE_QUERY_PARAM, } const getMediaFilterTypes = (searchType: SearchType) => { @@ -70,14 +71,14 @@ const getMediaFilterTypes = (searchType: SearchType) => { /** * Joins all the filters which have the checked property `true` * to a string separated by commas for the API request URL, e.g.: "by,nd-nc,nc-sa". - * Mature is a special case, and is converted to `true`. + * `includeSensitiveResults` is a special case, and is converted to `true`. */ const filterToString = (filterItem: FilterItem[]) => { const filterString = filterItem .filter((f) => f.checked) .map((filterItem) => filterItem.code) .join(",") - return filterString === "mature" ? "true" : filterString + return filterString === INCLUDE_SENSITIVE_QUERY_PARAM ? "true" : filterString } /** @@ -199,7 +200,10 @@ export const queryToFilterData = ({ } else { const queryDataKey = filterPropertyMappings[filterDataKey] if (query[queryDataKey]) { - if (queryDataKey === "mature" && query[queryDataKey].length > 0) { + if ( + queryDataKey === INCLUDE_SENSITIVE_QUERY_PARAM && + query[queryDataKey].length > 0 + ) { filters[filterDataKey][0].checked = true } else { const filterValues = query[queryDataKey].split(",") diff --git a/frontend/test/unit/specs/stores/search-store.spec.js b/frontend/test/unit/specs/stores/search-store.spec.js index 74503cef0ac..23df8bc2241 100644 --- a/frontend/test/unit/specs/stores/search-store.spec.js +++ b/frontend/test/unit/specs/stores/search-store.spec.js @@ -13,6 +13,7 @@ import { supportedSearchTypes, VIDEO, } from "~/constants/media" +import { INCLUDE_SENSITIVE_QUERY_PARAM } from "~/constants/content-safety" import { useSearchStore } from "~/stores/search" import { useFeatureFlagStore } from "~/stores/feature-flag" @@ -30,19 +31,19 @@ describe("Search Store", () => { describe("getters", () => { /** * Check for some special cases: - * - `mature` and `searchBy`. + * - `includeSensitiveResults` and `searchBy`. * - several options for single filter. * - media specific filters that are unique (durations). * - media specific filters that have the same API param (extensions) */ it.each` - query | searchType | filterCount - ${{ licenses: ["by"], mature: ["mature"] }} | ${IMAGE} | ${1} - ${{ licenses: ["by"], searchBy: ["creator"] }} | ${ALL_MEDIA} | ${2} - ${{ licenses: ["cc0", "pdm", "by", "by-nc"] }} | ${ALL_MEDIA} | ${4} - ${{ lengths: ["medium"] }} | ${AUDIO} | ${1} - ${{ imageExtensions: ["svg"] }} | ${IMAGE} | ${1} - ${{ audioExtensions: ["mp3"] }} | ${AUDIO} | ${1} + query | searchType | filterCount + ${{ licenses: ["by"], includeSensitiveResults: ["includeSensitiveResults"] }} | ${IMAGE} | ${1} + ${{ licenses: ["by"], searchBy: ["creator"] }} | ${ALL_MEDIA} | ${2} + ${{ licenses: ["cc0", "pdm", "by", "by-nc"] }} | ${ALL_MEDIA} | ${4} + ${{ lengths: ["medium"] }} | ${AUDIO} | ${1} + ${{ imageExtensions: ["svg"] }} | ${IMAGE} | ${1} + ${{ audioExtensions: ["mp3"] }} | ${AUDIO} | ${1} `( "returns correct filter status for $query and searchType $searchType", ({ query, searchType, filterCount }) => { @@ -95,7 +96,7 @@ describe("Search Store", () => { /** * For non-supported search types, the filters fall back to 'All content' filters. * Number of displayed filters is one less than the number of mediaFilterKeys - * because `mature` filter is not displayed. + * because `includeSensitiveResults` filter is not displayed. */ it.each` searchType | filterTypeCount @@ -120,7 +121,7 @@ describe("Search Store", () => { ) /** * Check for some special cases: - * - `mature` and `searchBy`. + * - `includeSensitiveResults` and `searchBy`. * - several options for single filter. * - media specific filters that are unique (durations). * - media specific filters that have the same API param (extensions) @@ -128,18 +129,18 @@ describe("Search Store", () => { * - more than one value for a parameter in the query (q=cat&q=dog). */ it.each` - query | expectedQueryParams | searchType - ${{ q: "cat", license: "by", mature: "true" }} | ${{ q: "cat", license: "by", mature: "true" }} | ${IMAGE} - ${{ license: "by", mature: "true" }} | ${{ q: "", license: "by", mature: "true" }} | ${IMAGE} - ${{ license: "", mature: "" }} | ${{ q: "" }} | ${IMAGE} - ${{ q: "cat", license: "by", searchBy: "creator" }} | ${{ q: "cat", license: "by", searchBy: "creator" }} | ${ALL_MEDIA} - ${{ q: "cat", license: "pdm,cc0,by,by-nc" }} | ${{ q: "cat", license: "pdm,cc0,by,by-nc" }} | ${ALL_MEDIA} - ${{ q: "cat", length: "medium" }} | ${{ q: "cat" }} | ${IMAGE} - ${{ q: "cat", length: "medium" }} | ${{ q: "cat", length: "medium" }} | ${AUDIO} - ${{ q: "cat", extension: "svg" }} | ${{ q: "cat", extension: "svg" }} | ${IMAGE} - ${{ q: "cat", extension: "mp3" }} | ${{ q: "cat", extension: "mp3" }} | ${AUDIO} - ${{ q: "cat", extension: "svg" }} | ${{ q: "cat" }} | ${AUDIO} - ${{ q: ["cat", "dog"], license: ["by", "cc0"] }} | ${{ q: "cat", license: "by" }} | ${IMAGE} + query | expectedQueryParams | searchType + ${{ [INCLUDE_SENSITIVE_QUERY_PARAM]: "true", q: "cat", license: "by" }} | ${{ q: "cat", license: "by", [INCLUDE_SENSITIVE_QUERY_PARAM]: "true" }} | ${IMAGE} + ${{ [INCLUDE_SENSITIVE_QUERY_PARAM]: "true", license: "by" }} | ${{ q: "", license: "by", [INCLUDE_SENSITIVE_QUERY_PARAM]: "true" }} | ${IMAGE} + ${{ license: "", [INCLUDE_SENSITIVE_QUERY_PARAM]: "" }} | ${{ q: "" }} | ${IMAGE} + ${{ q: "cat", license: "by", searchBy: "creator" }} | ${{ q: "cat", license: "by", searchBy: "creator" }} | ${ALL_MEDIA} + ${{ q: "cat", license: "pdm,cc0,by,by-nc" }} | ${{ q: "cat", license: "pdm,cc0,by,by-nc" }} | ${ALL_MEDIA} + ${{ q: "cat", length: "medium" }} | ${{ q: "cat" }} | ${IMAGE} + ${{ q: "cat", length: "medium" }} | ${{ q: "cat", length: "medium" }} | ${AUDIO} + ${{ q: "cat", extension: "svg" }} | ${{ q: "cat", extension: "svg" }} | ${IMAGE} + ${{ q: "cat", extension: "mp3" }} | ${{ q: "cat", extension: "mp3" }} | ${AUDIO} + ${{ q: "cat", extension: "svg" }} | ${{ q: "cat" }} | ${AUDIO} + ${{ q: ["cat", "dog"], license: ["by", "cc0"] }} | ${{ q: "cat", license: "by" }} | ${IMAGE} `( "returns correct searchQueryParams and filter status for $query and searchType $searchType", ({ query, expectedQueryParams, searchType }) => { @@ -193,13 +194,13 @@ describe("Search Store", () => { ) it.each` - query | path | searchType - ${{ license: "cc0,by", q: "cat" }} | ${"/search/"} | ${ALL_MEDIA} - ${{ searchBy: "creator", q: "dog" }} | ${"/search/image/"} | ${IMAGE} - ${{ mature: "true", q: "galah" }} | ${"/search/audio/"} | ${AUDIO} - ${{ length: "medium" }} | ${"/search/image"} | ${IMAGE} + query | path | searchType + ${{ license: "cc0,by", q: "cat" }} | ${"/search/"} | ${ALL_MEDIA} + ${{ searchBy: "creator", q: "dog" }} | ${"/search/image/"} | ${IMAGE} + ${{ [INCLUDE_SENSITIVE_QUERY_PARAM]: "true", q: "galah" }} | ${"/search/audio/"} | ${AUDIO} + ${{ length: "medium" }} | ${"/search/image"} | ${IMAGE} `( - "`setSearchStateFromUrl` should set '$searchType' from query $query and path '$path'", + "`setSearchStateFromUrl` should set '$searchType' from query $query and path '$path'", ({ query, path, searchType }) => { const searchStore = useSearchStore() const expectedQuery = { ...searchStore.searchQueryParams, ...query } @@ -220,7 +221,7 @@ describe("Search Store", () => { ${[["licenses", "by"], ["licenses", "by-nc-sa"]]} | ${["license", "by,by-nc-sa"]} ${[["licenseTypes", "commercial"], ["licenseTypes", "modification"]]} | ${["license_type", "commercial,modification"]} ${[["searchBy", "creator"]]} | ${["searchBy", "creator"]} - ${[["mature", "mature"]]} | ${["mature", "true"]} + ${[["includeSensitiveResults", "includeSensitiveResults"]]} | ${[[INCLUDE_SENSITIVE_QUERY_PARAM], "true"]} ${[["sizes", "large"]]} | ${["size", undefined]} `( "toggleFilter updates the query values to $query", @@ -258,15 +259,15 @@ describe("Search Store", () => { ) it.each` - filterType | codeIdx - ${"licenses"} | ${0} - ${"licenseTypes"} | ${0} - ${"imageExtensions"} | ${0} - ${"imageCategories"} | ${0} - ${"searchBy"} | ${0} - ${"aspectRatios"} | ${0} - ${"sizes"} | ${0} - ${"mature"} | ${0} + filterType | codeIdx + ${"licenses"} | ${0} + ${"licenseTypes"} | ${0} + ${"imageExtensions"} | ${0} + ${"imageCategories"} | ${0} + ${"searchBy"} | ${0} + ${"aspectRatios"} | ${0} + ${"sizes"} | ${0} + ${"includeSensitiveResults"} | ${0} `( "toggleFilter updates $filterType filter state", ({ filterType, codeIdx }) => { @@ -350,15 +351,15 @@ describe("Search Store", () => { }) it.each` - filterType | code | idx - ${"licenses"} | ${"cc0"} | ${1} - ${"licenseTypes"} | ${"modification"} | ${1} - ${"imageExtensions"} | ${"svg"} | ${3} - ${"imageCategories"} | ${"photograph"} | ${0} - ${"searchBy"} | ${"creator"} | ${0} - ${"mature"} | ${"mature"} | ${-0} - ${"aspectRatios"} | ${"tall"} | ${0} - ${"sizes"} | ${"medium"} | ${1} + filterType | code | idx + ${"licenses"} | ${"cc0"} | ${1} + ${"licenseTypes"} | ${"modification"} | ${1} + ${"imageExtensions"} | ${"svg"} | ${3} + ${"imageCategories"} | ${"photograph"} | ${0} + ${"searchBy"} | ${"creator"} | ${0} + ${"includeSensitiveResults"} | ${"includeSensitiveResults"} | ${0} + ${"aspectRatios"} | ${"tall"} | ${0} + ${"sizes"} | ${"medium"} | ${1} `( "toggleFilter should set filter '$code' of type '$filterType", ({ filterType, code, idx }) => { diff --git a/frontend/test/unit/specs/utils/search-query-transform.spec.js b/frontend/test/unit/specs/utils/search-query-transform.spec.js index 03b22c925a5..4b09800372e 100644 --- a/frontend/test/unit/specs/utils/search-query-transform.spec.js +++ b/frontend/test/unit/specs/utils/search-query-transform.spec.js @@ -7,6 +7,7 @@ import { import { AUDIO, IMAGE } from "~/constants/media" import { filterData, initFilters } from "~/constants/filters" +import { INCLUDE_SENSITIVE_QUERY_PARAM } from "~/constants/content-safety" describe("searchQueryTransform", () => { it("converts initial filters to query data", () => { @@ -88,7 +89,9 @@ describe("searchQueryTransform", () => { { code: "brooklynmuseum", checked: true }, ], searchBy: [{ code: "creator", checked: true }], - mature: [{ code: "mature", checked: true }], + includeSensitiveResults: [ + { code: "includeSensitiveResults", checked: true }, + ], } const expectedQueryData = { aspect_ratio: "tall", @@ -96,7 +99,7 @@ describe("searchQueryTransform", () => { extension: "jpg", license: "cc0", license_type: "commercial", - mature: "true", + [INCLUDE_SENSITIVE_QUERY_PARAM]: "includeSensitiveResults", searchBy: "creator", size: "medium", source: "animaldiversity,brooklynmuseum", @@ -247,8 +250,12 @@ describe("searchQueryTransform", () => { searchBy: [ { code: "creator", checked: true, name: "filters.searchBy.creator" }, ], - mature: [ - { code: "mature", name: "filters.mature.mature", checked: true }, + includeSensitiveResults: [ + { + code: "includeSensitiveResults", + name: "filters.mature.mature", + checked: true, + }, ], } const query = { @@ -260,7 +267,7 @@ describe("searchQueryTransform", () => { length: "medium", source: "jamendo", searchBy: "creator", - mature: "true", + includeSensitiveResults: "true", } const testFilters = deepClone(filters) testFilters.audioProviders = [ @@ -289,7 +296,7 @@ describe("searchQueryTransform", () => { * exist in `filters.audioProviders` list before. Other values either exist in * `filters.imageProviders` list, or do not exist at all, so they are discarded. * Valid filter items for categories that exist for all search types - * (`license`, `license_type`, `searchBy`, `mature`) are set to checked. + * (`license`, `license_type`, `searchBy`, `includeSensitiveResults`) are set to checked. * Invalid filter items for valid categories (`nonexistent` in `license`) * are discarded. */ @@ -302,7 +309,7 @@ describe("searchQueryTransform", () => { length: "medium", source: "animaldiversity,wikimedia,nonexistent,wikimedia_audio,jamendo", searchBy: "creator", - mature: "true", + [INCLUDE_SENSITIVE_QUERY_PARAM]: "true", } const expectedFilters = deepClone(filters) const setChecked = (code, filterCategory) => { @@ -315,7 +322,7 @@ describe("searchQueryTransform", () => { setChecked("commercial", "licenseTypes") setChecked("medium", "lengths") setChecked("creator", "searchBy") - setChecked("mature", "mature") + setChecked("includeSensitiveResults", "includeSensitiveResults") setChecked("jamendo", "audioProviders") setChecked("wikimedia_audio", "audioProviders") @@ -337,10 +344,13 @@ describe("searchQueryTransform", () => { source: "animaldiversity,brooklynmuseum", q: "cat", searchBy: "creator", - mature: "true", + [INCLUDE_SENSITIVE_QUERY_PARAM]: "true", } const queryString = - "http://localhost:8443/search/image?q=cat&license=cc0&license_type=commercial&category=photograph&extension=jpg&aspect_ratio=tall&size=medium&source=animaldiversity,brooklynmuseum&searchBy=creator&mature=true" + "http://localhost:8443/search/image?q=cat&license=cc0&license_type=commercial" + + "&category=photograph&extension=jpg&aspect_ratio=tall&size=medium" + + "&source=animaldiversity,brooklynmuseum&searchBy=creator" + + `&${INCLUDE_SENSITIVE_QUERY_PARAM}=true` const result = queryStringToQueryData(queryString) expect(result).toEqual(expectedQueryData) })