Skip to content

Commit c989f42

Browse files
committed
refactor: use Redux for contents/models state management
1 parent 76df6cb commit c989f42

File tree

3 files changed

+166
-134
lines changed

3 files changed

+166
-134
lines changed
Lines changed: 147 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
1-
import { useEffect, useMemo, useState } from "react";
1+
import { useEffect, useMemo, useState, useCallback } from "react";
22
import { ContentItem, ContentModel, Language, User } from "../services/types";
33
import { BlockModel } from "../../shell/views/SearchPage/List/Block";
44
import { AppState } from "shell/store/types";
5-
import { useSelector } from "react-redux";
5+
import { useDispatch, useSelector } from "react-redux";
6+
import { fetchItems } from "shell/store/content";
67

78
type UsersMap = Record<string, string>;
9+
810
type LanguageMap = Record<string, { code: string }>;
911

12+
type SearchBlocksByKeywordProps = {
13+
isLoading: boolean;
14+
};
15+
1016
type SearchResult = {
1117
blocks: BlockModel[];
1218
setBlockKeyword: (term: string) => void;
@@ -22,37 +28,32 @@ const toProperCase = (str?: string): string => {
2228
.trim();
2329
};
2430

25-
export const useSearchBlocksByKeyword = (
26-
contents?: ContentItem[],
27-
isLoading?: boolean
28-
): SearchResult => {
31+
const blockKeywords: string[] = [
32+
"block",
33+
"blocks",
34+
"block model",
35+
"block models",
36+
"blocks models",
37+
];
38+
39+
export const useSearchBlocksByKeyword = ({
40+
isLoading,
41+
}: SearchBlocksByKeywordProps): SearchResult => {
42+
const dispatch = useDispatch();
2943
const [searchTerm, setSearchTerm] = useState("");
30-
const [blockVariants, setBlockVariants] = useState([]);
44+
const [isFetchingVariants, setIsFetchingVariants] = useState(false);
3145

32-
const modelsRaw: ContentModel[] = useSelector(
33-
(state: AppState) => state?.models
34-
);
3546
const users: User[] = useSelector((state: AppState) => state?.users);
3647
const languages: Language[] = useSelector(
3748
(state: AppState) => state?.languages
3849
);
39-
40-
const normalizedSearch = searchTerm.toLowerCase()?.trim();
41-
42-
const blockModels = useMemo<BlockModel[]>(() => {
43-
if (!Object.values(modelsRaw)?.length) return [];
44-
return Object.values(modelsRaw)
45-
?.filter((block: ContentModel) => block?.type === "block")
46-
.map((block) => block as unknown as BlockModel);
47-
}, [modelsRaw]);
48-
49-
const blockModelsMap = useMemo(
50-
() =>
51-
!!blockModels?.length &&
52-
Object.fromEntries(blockModels?.map((model) => [model?.ZUID, model])),
53-
[blockModels]
50+
const defaultLanguage = languages?.find(
51+
(lang: Language) => lang?.default && lang?.active
5452
);
5553

54+
const models = useSelector((state: AppState) => state?.models);
55+
const contents = useSelector((state: AppState) => state?.content);
56+
5657
const usersMap = useMemo<UsersMap>(
5758
() =>
5859
!!users?.length &&
@@ -72,96 +73,145 @@ export const useSearchBlocksByKeyword = (
7273
[languages]
7374
);
7475

75-
useEffect(() => {
76-
if (!normalizedSearch) return;
77-
const blockModelZUIDs = blockModels?.map((block) => block?.ZUID);
78-
const langIDs = Object.keys(languageMap);
79-
const variants = !contents?.length
80-
? []
81-
: contents?.filter(
82-
(content: ContentItem) =>
83-
blockModelZUIDs?.includes(content?.meta?.contentModelZUID) &&
84-
langIDs?.includes(String(content?.meta?.langID))
85-
);
86-
setBlockVariants(variants);
87-
}, [normalizedSearch, blockModels, languageMap, contents]);
76+
const normalizedSearchTerm = searchTerm.toLowerCase()?.trim();
77+
const showAll = blockKeywords.includes(normalizedSearchTerm);
78+
79+
const blocks = useMemo<BlockModel[]>(() => {
80+
const modelsArray = models ? Object.values(models) : [];
81+
if (!modelsArray?.length) return [];
82+
83+
return (
84+
modelsArray
85+
.filter((model: ContentModel) => model?.type === "block")
86+
.map((model) => model as unknown as BlockModel) || []
87+
);
88+
}, [models]);
89+
90+
const variants = useMemo<ContentItem[] | []>(() => {
91+
const contentItems = contents ? Object.values(contents) : [];
92+
if (!contentItems?.length || !blocks?.length || isFetchingVariants) {
93+
return [];
94+
}
95+
96+
const blockModelZUIDs = new Set(blocks.map((block) => block?.ZUID));
97+
const validLangIDs = new Set(languages.map((lang) => lang?.ID));
98+
99+
return contentItems.filter(
100+
(content: ContentItem) =>
101+
blockModelZUIDs.has(content?.meta?.contentModelZUID) &&
102+
validLangIDs.has(content?.meta?.langID)
103+
);
104+
}, [contents, blocks, languages, isFetchingVariants]);
88105

89106
const parsedBlocks: BlockModel[] = useMemo(() => {
90107
if (isLoading) return [];
91108

92-
const processedModels: BlockModel[] = !blockModels?.length
93-
? []
94-
: blockModels?.map((model: BlockModel) => ({
95-
ZUID: model?.ZUID,
96-
label: model?.label,
97-
type: model?.type,
98-
contentModelLabel: "",
99-
contentModelZUID: "",
100-
updatedAt: model?.updatedAt,
101-
createdAt: model?.createdAt,
102-
createdByUserZUID: model?.createdByUserZUID,
103-
langID: null,
104-
url: `/blocks/${model?.ZUID}`,
105-
}));
106-
107-
const processedContent = !blockVariants?.length
109+
const processedBlocks: BlockModel[] = !blocks
108110
? []
109-
: blockVariants?.map((item) => {
110-
const parentModel =
111-
blockModelsMap?.[item?.meta?.contentModelZUID] || null;
111+
: blocks.map((model: BlockModel) => {
112112
return {
113-
ZUID: item?.meta?.ZUID,
114-
label: item?.web?.metaTitle || item?.web?.metaLinkText,
115-
type: !parentModel ? null : parentModel?.type,
116-
contentModelZUID: item?.meta?.contentModelZUID,
117-
contentModelLabel: !parentModel
118-
? null
119-
: parentModel?.label || parentModel?.metaTitle,
120-
updatedAt: item?.meta?.updatedAt || item?.web?.updatedAt,
121-
createdAt: item?.meta?.createdAt || item?.web?.createdAt,
122-
createdByUserZUID:
123-
item?.meta?.createdByUserZUID || item?.web?.createdByUserZUID,
124-
langID: item?.meta?.langID,
125-
url: `/blocks/${item?.meta?.contentModelZUID}/${item?.meta?.ZUID}`,
113+
ZUID: model?.ZUID,
114+
label: model?.label,
115+
title: model?.label,
116+
chipText: "Blocks",
117+
type: model?.type,
118+
contentModelLabel: "",
119+
contentModelZUID: "",
120+
updatedAt: model?.updatedAt,
121+
createdAt: model?.createdAt,
122+
createdByUserZUID: model?.createdByUserZUID,
123+
langID: defaultLanguage?.ID,
124+
url: `/blocks/${model?.ZUID}`,
126125
};
127126
});
128127

129-
return [...processedModels, ...processedContent]
130-
.map((item) => ({
128+
const processedVariants = variants.map((item) => {
129+
const blockModelsMap = blocks.length
130+
? Object.fromEntries(blocks.map((model) => [model?.ZUID, model]))
131+
: {};
132+
const parentModel =
133+
blockModelsMap?.[item?.meta?.contentModelZUID] || null;
134+
const titlePrefix = !item?.meta?.langID
135+
? ""
136+
: `(${languageMap?.[item?.meta?.langID]?.code}) `;
137+
const chipText = parentModel?.label || parentModel?.metaTitle || null;
138+
return {
139+
ZUID: item?.meta?.ZUID,
140+
label: item?.web?.metaTitle || item?.web?.metaLinkText,
141+
title: `${titlePrefix}${
142+
item?.web?.metaTitle || item?.web?.metaLinkText
143+
}`,
144+
chipText: !chipText ? "Blocks" : `${chipText} • Blocks`,
145+
type: parentModel?.type || null,
146+
contentModelZUID: item?.meta?.contentModelZUID,
147+
contentModelLabel: parentModel?.label || parentModel?.metaTitle || null,
148+
updatedAt: item?.meta?.updatedAt || item?.web?.updatedAt,
149+
createdAt: item?.meta?.createdAt || item?.web?.createdAt,
150+
createdByUserZUID:
151+
item?.meta?.createdByUserZUID || item?.web?.createdByUserZUID,
152+
langID: item?.meta?.langID,
153+
url: `/blocks/${item?.meta?.contentModelZUID}/${item?.meta?.ZUID}`,
154+
};
155+
});
156+
157+
const allBlocks = [...processedBlocks, ...processedVariants].map((item) => {
158+
return {
131159
...item,
132-
lang: languageMap[item.langID]?.code,
133160
createdByUserName: toProperCase(usersMap[item.createdByUserZUID] || ""),
134-
title: item.label,
135-
}))
136-
.filter((item) => {
137-
if (!normalizedSearch) return true;
138-
139-
const searchFields = [
140-
item.ZUID,
141-
item.label,
142-
item.createdByUserName,
143-
item.title,
144-
item.contentModelZUID,
145-
item.type,
146-
item.contentModelLabel,
147-
]
148-
.join("\n")
149-
.toLowerCase();
150-
151-
return searchFields.includes(normalizedSearch);
152-
});
161+
};
162+
});
163+
164+
// If search is empty or it's a block keyword, return all items
165+
if (!normalizedSearchTerm || showAll) {
166+
return allBlocks;
167+
}
168+
169+
// Filter based on search term
170+
return allBlocks?.filter((block) => {
171+
const searchFields = [
172+
block?.ZUID,
173+
block?.label,
174+
block?.createdByUserName,
175+
block?.title,
176+
block?.contentModelZUID,
177+
block?.type,
178+
block?.contentModelLabel,
179+
]
180+
.join("\n")
181+
.toLowerCase();
182+
183+
return searchFields.includes(normalizedSearchTerm);
184+
});
153185
}, [
154186
isLoading,
155-
blockVariants,
156-
blockModels,
187+
variants,
188+
blocks,
189+
languages,
190+
users,
191+
normalizedSearchTerm,
192+
showAll,
157193
languageMap,
158194
usersMap,
159-
blockModelsMap,
160-
normalizedSearch,
161195
]);
162196

197+
const setBlockKeyword = useCallback((term: string) => {
198+
setSearchTerm(term);
199+
}, []);
200+
201+
useEffect(() => {
202+
// Fetch all block models and variants if user searches for "blockKeywords"
203+
if (showAll && !!blocks?.length) {
204+
setIsFetchingVariants(true);
205+
Promise.all(
206+
blocks?.map((block) => dispatch(fetchItems(block?.ZUID)))
207+
).then(() => {
208+
setIsFetchingVariants(false);
209+
});
210+
}
211+
}, [showAll, blocks]);
212+
163213
return {
164214
blocks: parsedBlocks,
165-
setBlockKeyword: setSearchTerm,
215+
setBlockKeyword,
166216
};
167217
};

src/shell/views/SearchPage/List/Block.tsx

Lines changed: 13 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FC, useMemo } from "react";
1+
import { FC } from "react";
22
import { Block as BlockIcon } from "@zesty-io/material";
33
import { SvgIconComponent } from "@mui/icons-material";
44
import { isValid, formatDistanceToNow } from "date-fns";
@@ -17,8 +17,9 @@ export type BlockModel = Partial<ContentModel> & {
1717
updatedAt?: string;
1818
lang?: string;
1919
langID: number | null;
20-
title?: string;
21-
url?: string | null;
20+
title: string;
21+
chipText: string;
22+
url: string | null;
2223
};
2324

2425
type Block = {
@@ -32,36 +33,20 @@ export const Block: FC<Block> = ({
3233
style,
3334
loading: parentIsLoading = false,
3435
}) => {
35-
const isVariant = data?.type === "block" && !!data?.contentModelZUID;
36-
const createdRelative = useMemo(() => {
37-
if (!data?.createdAt) return "";
38-
const d = new Date(data.createdAt);
39-
return isValid(d) ? formatDistanceToNow(d, { addSuffix: true }) : "";
40-
}, [data?.createdAt]);
41-
42-
const chips = useMemo(() => {
43-
const preFix =
44-
!!isVariant && !!data?.contentModelLabel
45-
? `${data?.contentModelLabel} • `
46-
: "";
47-
48-
const userName = !data?.createdByUserName
49-
? ""
50-
: ` by ${data?.createdByUserName}`;
51-
return `${preFix}Block • created ${createdRelative}${userName}`;
52-
}, [data]);
53-
54-
const titlePrefix = !!data?.lang ? `(${data?.lang}) ` : "";
55-
const urlPath = isVariant
56-
? `${data?.contentModelZUID}/${data?.ZUID}`
57-
: data?.ZUID;
36+
const dateTimeRaw = new Date(data?.createdAt);
37+
const dateTime = isValid(dateTimeRaw)
38+
? formatDistanceToNow(dateTimeRaw, { addSuffix: true })
39+
: "";
40+
const chips = `${data?.chipText}${dateTime} by ${
41+
data?.createdByUserName || "unknown"
42+
}`;
5843

5944
const loading = parentIsLoading;
6045

6146
return (
6247
<SearchListItem
63-
title={`${titlePrefix}${data?.label}`}
64-
url={`/blocks/${urlPath}`}
48+
title={data?.title}
49+
url={data?.url}
6550
chips={chips}
6651
icon={BlockIcon as SvgIconComponent}
6752
style={style}

src/shell/views/SearchPage/SearchPage.tsx

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,9 @@ export const SearchPage: FC = () => {
6969
);
7070
const { data: langs } = useGetLangsQuery({});
7171

72-
const { blocks, setBlockKeyword } = useSearchBlocksByKeyword(
73-
contents,
74-
isFetchingContent
75-
);
72+
const { blocks, setBlockKeyword } = useSearchBlocksByKeyword({
73+
isLoading: isFetchingContent,
74+
});
7675

7776
const isLoading = isFetchingContent || isFetchingMedia;
7877

@@ -101,13 +100,10 @@ export const SearchPage: FC = () => {
101100
const results: SearchPageItem[] = useMemo(() => {
102101
const sortBy = params.get("sort") || "";
103102

104-
const blockModelZUIDs = Object.values(allModels)
105-
?.filter((model: ContentModel) => model.type === "block")
106-
?.map((model: ContentModel) => model?.ZUID);
107-
108103
//Filter out redundant block model items
109104
const filteredContents = contents?.filter(
110-
(item) => !blockModelZUIDs?.includes(item?.meta?.contentModelZUID)
105+
(item: ContentItem) =>
106+
allModels?.[item?.meta?.contentModelZUID]?.type !== "block"
111107
);
112108
// Content data needs to be reset to [] when api call fails
113109
const contentResults: SearchPageItem[] =
@@ -230,6 +226,7 @@ export const SearchPage: FC = () => {
230226
return consolidatedResults;
231227
}
232228
}, [
229+
allModels,
233230
contents,
234231
models,
235232
blocks,

0 commit comments

Comments
 (0)