diff --git a/public/editor-client/src/Elements/Filters/index.ts b/public/editor-client/src/Elements/Filters/index.ts new file mode 100644 index 0000000000..310303ea3e --- /dev/null +++ b/public/editor-client/src/Elements/Filters/index.ts @@ -0,0 +1,37 @@ +import { getFields } from "../../api"; +import { Filters } from "../../types/Filters"; +import { t } from "../../utils/i18n"; +import { converter, parseFields } from "./utils"; + +export const handler: Filters["handler"] = async (res, rej, data) => { + try { + const result = await getFields(data); + + const convertedValue = converter(result); + + res(convertedValue ?? []); + } catch (e) { + rej(t("Failed to load sources")); + } +}; + +export const possibleValues: Filters["possibleValues"] = async ( + res, + rej, + { type, search, optionSource, postId, loopAttributes } +) => { + try { + const result = await getFields({ postId, loopAttributes }); + + const convertPossibleValues = parseFields( + result, + optionSource, + type, + search + ); + + res(convertPossibleValues ?? []); + } catch (e) { + rej(t("Failed to load sources")); + } +}; diff --git a/public/editor-client/src/Elements/Filters/utils.ts b/public/editor-client/src/Elements/Filters/utils.ts new file mode 100644 index 0000000000..985c4c3b57 --- /dev/null +++ b/public/editor-client/src/Elements/Filters/utils.ts @@ -0,0 +1,105 @@ +import { + Choice, + FilterFieldsData, + fromStringQueryTypeSource, + isChoiceArray, + isPossibleValueArray, + PossibleValue, + QueryTypeSource +} from "../../types/Filters"; +import { isAllValuesValid } from "../../utils/reader/array"; + +export const createQueryTypeSource = ( + query: string, + type: string, + source: string, + filterBy: string +): QueryTypeSource | undefined => + fromStringQueryTypeSource(`${query}|||${type}|||${source}|||${filterBy}`); + +export const converter = (data: FilterFieldsData): Choice[] => { + const arr = Object.values(data).reduce((acc: Choice[], cur) => { + const field = cur.map((item) => { + return { + value: + createQueryTypeSource( + getQuery(item.filterQuery, item.optionQuery, item.optionSource), + item.type, + item.optionSource, + item.filterBy + ) ?? "", + title: item.label + }; + }); + return [...acc, ...field]; + }, []); + + return arr ?? []; +}; + +export const getQuery = ( + filterQuery: string, + optionQuery: string, + optionSource: string +): string => filterQuery || optionQuery || `metaKey=${optionSource}`; + +export const parseFields = ( + data: FilterFieldsData, + optionSource: string, + type: string, + search?: string +): Choice[] => { + const allItem = { value: "all", title: type === "inc" ? "All" : "None" }; + + const selectedItem = Object.values(data).reduce((acc, cur) => { + const field = cur.filter((item) => item.optionQuery === optionSource); + + return [...acc, ...field]; + }, [])[0]; + + if (selectedItem.optionSource === "tax") { + const parsedValues: PossibleValue[] = isPossibleValueArray( + selectedItem.possibleValues + ) + ? selectedItem.possibleValues + : []; + const items: Choice[] = parsedValues.map((it) => ({ + value: it.term_id, + title: it.name + })); + + if (search) { + return [ + allItem, + ...items.filter((it) => + it.title.toLocaleLowerCase().includes(search.toLocaleLowerCase()) + ) + ]; + } + + if (isAllValuesValid(items)) { + return [allItem, ...items]; + } + } + + if (selectedItem.optionSource === "meta") { + const items = isChoiceArray(selectedItem.possibleValues) + ? selectedItem.possibleValues + : []; + + if (search) { + return [ + allItem, + ...items.filter((it) => + it.title.toLocaleLowerCase().includes(search.toLocaleLowerCase()) + ) + ]; + } + + if (isAllValuesValid(items)) { + return [allItem, ...items]; + } + } + + return [allItem]; +}; diff --git a/public/editor-client/src/api/index.ts b/public/editor-client/src/api/index.ts index b53628f339..195d409cae 100644 --- a/public/editor-client/src/api/index.ts +++ b/public/editor-client/src/api/index.ts @@ -1262,3 +1262,40 @@ export const updateGlobalBlocks = async ( }; //#endregion + +//#region FiltersFields + +export const getFields = async (data: { + postId: string; + loopAttributes: string; +}) => { + try { + const config = getConfig(); + + if (!config) { + throw new Error(t("Invalid __BRZ_PLUGIN_ENV__")); + } + + const { editorVersion, url, hash, actions } = config; + + const body = new URLSearchParams({ + hash, + version: editorVersion, + action: actions.filterFields, + ...data + }); + + const result = await request(url, { + method: "POST", + body + }); + + const r = await result.json(); + + return r.data; + } catch (e) { + throw new Error(t("Fail to load fields!")); + } +}; + +//#endregion diff --git a/public/editor-client/src/config.ts b/public/editor-client/src/config.ts index 9176f937e6..56b9635fd0 100644 --- a/public/editor-client/src/config.ts +++ b/public/editor-client/src/config.ts @@ -52,6 +52,8 @@ interface Actions { heartBeat: string; takeOver: string; getFonts: string; + + filterFields: string; } interface ProjectStatus { @@ -282,6 +284,10 @@ const actionsReader = parseStrict({ getFonts: pipe( mPipe(Obj.readKey("getFonts"), Str.read), throwOnNullish("Invalid actions: getFonts") + ), + filterFields: pipe( + mPipe(Obj.readKey("filterFields"), Str.read), + throwOnNullish("Invalid actions: filterFields") ) }); diff --git a/public/editor-client/src/index.ts b/public/editor-client/src/index.ts index acb8f5ed65..bb30cb1a2a 100644 --- a/public/editor-client/src/index.ts +++ b/public/editor-client/src/index.ts @@ -12,6 +12,7 @@ import { defaultPopups, defaultStories } from "./defaultTemplates"; +import { handler as filters, possibleValues } from "./Elements/Filters"; import { placeholderData, placeholders } from "./dynamicContent"; import { handler as posts } from "./Elements/Posts"; import { uploadedFonts } from "./fonts"; @@ -90,8 +91,20 @@ if (window.__VISUAL_CONFIG__) { // Elements if (window.__VISUAL_CONFIG__.elements) { set(window.__VISUAL_CONFIG__.elements, ["posts", "handler"], posts); + set(window.__VISUAL_CONFIG__.elements, ["filters", "handler"], filters); + set( + window.__VISUAL_CONFIG__.elements, + ["filters", "possibleValues"], + possibleValues + ); } else { set(window.__VISUAL_CONFIG__, ["elements", "posts", "handler"], posts); + set(window.__VISUAL_CONFIG__, ["elements", "filters", "handler"], filters); + set( + window.__VISUAL_CONFIG__, + ["elements", "filters", "possibleValues"], + possibleValues + ); } // Dynamic Content diff --git a/public/editor-client/src/types/Filters.ts b/public/editor-client/src/types/Filters.ts new file mode 100644 index 0000000000..92c41592c9 --- /dev/null +++ b/public/editor-client/src/types/Filters.ts @@ -0,0 +1,112 @@ +import { pass } from "fp-utilities"; +import { Literal } from "../utils/types"; +import { NewType } from "./NewType"; +import { Response } from "./Response"; + +export type Choice = { title: string; value: Literal }; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const isChoice = (values: any): values is Choice => { + return typeof values === "object" && "title" in values && "value" in values; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const isChoiceArray = (arr: any): arr is Choice[] => { + if (!Array.isArray(arr)) return false; + for (const item of arr) { + if (!isChoice(item)) return false; + } + return true; +}; + +export type QueryTypeSource = NewType; + +export const isQueryTypeSource = (s: string): s is QueryTypeSource => + s.split("|||").length === 3; + +export const fromStringQueryTypeSource = pass(isQueryTypeSource); + +interface FieldsCommon { + filterQuery: string; + label: string; + optionQuery: string; + optionSource: string; + filterBy: string; + type: string; +} + +export interface FilterField extends FieldsCommon { + possibleValues: PossibleValue[] | Choice[]; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const isPossibleValue = (values: any): values is PossibleValue => { + return ( + typeof values === "object" && + "count" in values && + "description" in values && + "filter" in values && + "name" in values && + "parent" in values && + "slug" in values && + "taxonomy" in values && + "term_group" in values && + "term_id" in values && + "term_taxonomy_id" in values + ); +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const isPossibleValueArray = (arr: any): arr is PossibleValue[] => { + if (!Array.isArray(arr)) return false; + for (const item of arr) { + if (!isPossibleValue(item)) return false; + } + return true; +}; + +export type PossibleValue = { + count: number; + description: string; + filter: string; + name: string; + parent: number; + slug: string; + taxonomy: string; + term_group: number; + term_id: number; + term_taxonomy_id: number; +}; + +export type FilterFieldsData = { + fields: FilterField[]; + taxonomies: FilterField[]; + metaFields: FilterField[]; + authors: FilterField[]; +}; + +export interface FiltersHandlerArgs { + postId: string; + loopAttributes: string; +} + +export interface FiltersValuesArgs { + postId: string; + loopAttributes: string; + optionSource: string; + type: string; + search?: string; +} + +export interface Filters { + handler: ( + res: Response, + rej: Response, + args: FiltersHandlerArgs + ) => void; + possibleValues: ( + res: Response, + rej: Response, + data: FiltersValuesArgs + ) => void; +} diff --git a/public/editor-client/src/types/global.d.ts b/public/editor-client/src/types/global.d.ts index 65d1c87534..7bfa81fee2 100644 --- a/public/editor-client/src/types/global.d.ts +++ b/public/editor-client/src/types/global.d.ts @@ -21,6 +21,7 @@ import { AddMediaData, AddMediaGallery } from "./Media"; import { OnChange } from "./OnChange"; import { PopupConditions } from "./PopupConditions"; import { Posts } from "./Posts"; +import { Filters } from "./Filters"; import { Data } from "./Publish"; import { SavedBlocks, SavedLayouts, SavedPopups } from "./SavedBlocks"; import { Screenshots } from "./Screenshots"; @@ -185,6 +186,7 @@ export interface VISUAL_CONFIG { elements?: { posts: Posts; + filters: Filters; }; //#endregion diff --git a/public/editor-client/src/utils/reader/array.ts b/public/editor-client/src/utils/reader/array.ts new file mode 100644 index 0000000000..86786f4894 --- /dev/null +++ b/public/editor-client/src/utils/reader/array.ts @@ -0,0 +1,12 @@ +import { isT } from "fp-utilities"; +import { Reader } from "./types"; + +export const read: Reader> = (v) => { + if (Array.isArray(v)) { + return v; + } + + return undefined; +}; + +export const isAllValuesValid = (arr: Array) => arr.every(isT);