PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.4
Yatra – Travel Booking & Tour Operator Software v3.0.4
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / resources / js / blocks / components / ClassificationMultiSelect.tsx

ClassificationMultiSelect.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.4, at resources/js/blocks/components/ClassificationMultiSelect.tsx

259 lines 6.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import apiFetch from "@wordpress/api-fetch";
2 import type { CSSProperties } from "react";
3 import { useEffect, useMemo, useState } from "@wordpress/element";
4 import {
5 Button,
6 CheckboxControl,
7 RadioControl,
8 SearchControl,
9 Spinner,
10 } from "@wordpress/components";
11 import { sprintf, __ } from "@wordpress/i18n";
12
13 export type ClassificationTaxonomy =
14 | "destination"
15 | "activity"
16 | "trip_category"
17 | "difficulty";
18
19 interface ChoiceItem {
20 id: number;
21 name: string;
22 }
23
24 type Scope = "all" | "narrow";
25
26 interface ClassificationMultiSelectProps {
27 taxonomy: ClassificationTaxonomy;
28 label: string;
29 help: string;
30 value: number[];
31 onChange: (ids: number[]) => void;
32 }
33
34 const listWrapStyle: CSSProperties = {
35 maxHeight: 220,
36 overflowY: "auto",
37 marginTop: 8,
38 padding: "4px 0",
39 border: "1px solid #94949451",
40 borderRadius: 2,
41 };
42
43 export function ClassificationMultiSelect({
44 taxonomy,
45 label,
46 help,
47 value,
48 onChange,
49 }: ClassificationMultiSelectProps) {
50 const [items, setItems] = useState<ChoiceItem[]>([]);
51 const [loading, setLoading] = useState(true);
52 const [loadError, setLoadError] = useState(false);
53 const [search, setSearch] = useState("");
54 const [scope, setScope] = useState<Scope>(() =>
55 value.length > 0 ? "narrow" : "all",
56 );
57
58 useEffect(() => {
59 if (value.length > 0) {
60 setScope("narrow");
61 }
62 }, [value.length]);
63
64 useEffect(() => {
65 let cancelled = false;
66 setLoading(true);
67 setLoadError(false);
68 apiFetch<{ items?: ChoiceItem[] }>({
69 path: `/yatra/v1/block-editor/taxonomy-choices?taxonomy=${encodeURIComponent(
70 taxonomy,
71 )}`,
72 })
73 .then((response) => {
74 if (!cancelled) {
75 setItems(response.items ?? []);
76 }
77 })
78 .catch(() => {
79 if (!cancelled) {
80 setItems([]);
81 setLoadError(true);
82 }
83 })
84 .finally(() => {
85 if (!cancelled) {
86 setLoading(false);
87 }
88 });
89
90 return () => {
91 cancelled = true;
92 };
93 }, [taxonomy]);
94
95 const filteredItems = useMemo(() => {
96 const q = search.trim().toLowerCase();
97 if (q === "") {
98 return items;
99 }
100 return items.filter((i) => i.name.toLowerCase().includes(q));
101 }, [items, search]);
102
103 const selectedSet = useMemo(() => new Set(value), [value]);
104
105 const toggleId = (id: number, checked: boolean) => {
106 const next = new Set(selectedSet);
107 if (checked) {
108 next.add(id);
109 } else {
110 next.delete(id);
111 }
112 onChange([...next].sort((a, b) => a - b));
113 };
114
115 const onScopeChange = (next: string) => {
116 if (next === "all") {
117 setScope("all");
118 onChange([]);
119 setSearch("");
120 } else {
121 setScope("narrow");
122 }
123 };
124
125 if (loading) {
126 return (
127 <fieldset
128 className="yatra-block-taxonomy-field"
129 style={{ margin: "0 0 16px", border: "none", padding: 0 }}
130 >
131 <legend className="components-base-control__label" style={{ padding: 0 }}>
132 {label}
133 </legend>
134 <Spinner />
135 </fieldset>
136 );
137 }
138
139 return (
140 <fieldset
141 className="yatra-block-taxonomy-field"
142 style={{ margin: "0 0 16px", border: "none", padding: 0 }}
143 >
144 <legend className="components-base-control__label" style={{ padding: 0 }}>
145 {label}
146 </legend>
147 <p
148 className="components-base-control__help"
149 style={{ marginTop: 4, marginBottom: 10 }}
150 >
151 {help}
152 </p>
153 {loadError && (
154 <p style={{ color: "#b32d2e", fontSize: 12, marginBottom: 8 }}>
155 {__(
156 "Could not load options. Confirm you can edit posts and Yatra REST is available.",
157 "yatra",
158 )}
159 </p>
160 )}
161 <RadioControl
162 label={__("Listing scope", "yatra")}
163 selected={scope}
164 options={[
165 {
166 label: __("All published (no restriction)", "yatra"),
167 value: "all",
168 },
169 {
170 label: __("Only selected (search below)", "yatra"),
171 value: "narrow",
172 },
173 ]}
174 onChange={onScopeChange}
175 />
176 {scope === "all" && (
177 <p className="components-base-control__help" style={{ marginTop: 4 }}>
178 {__(
179 "The frontend will include every matching published item.",
180 "yatra",
181 )}
182 </p>
183 )}
184 {scope === "narrow" && (
185 <>
186 <SearchControl
187 label={sprintf(
188 /* translators: %d = number of loaded taxonomy items */
189 __("Filter items (%d loaded)", "yatra"),
190 items.length,
191 )}
192 hideLabelFromVision
193 placeholder={__("Type to filter the list…", "yatra")}
194 value={search}
195 onChange={(s) => setSearch(s)}
196 __nextHasNoMarginBottom
197 />
198 {value.length > 0 && (
199 <p className="components-base-control__help" style={{ marginTop: 4 }}>
200 {sprintf(
201 /* translators: %d = number of selected taxonomy items */
202 __("%d selected", "yatra"),
203 value.length,
204 )}
205 </p>
206 )}
207 {items.length === 0 && !loadError ? (
208 <p className="components-base-control__help">
209 {__("No published items of this type yet.", "yatra")}
210 </p>
211 ) : (
212 <div role="group" aria-label={label} style={listWrapStyle}>
213 {filteredItems.length === 0 ? (
214 <p style={{ padding: "8px 12px", margin: 0, fontSize: 12 }}>
215 {__("No matching items.", "yatra")}
216 </p>
217 ) : (
218 filteredItems.map((item) => {
219 const cid = Number(item.id);
220 return (
221 <div
222 key={cid}
223 style={{ padding: "2px 8px" }}
224 >
225 <CheckboxControl
226 label={`${item.name} (${cid})`}
227 checked={selectedSet.has(cid)}
228 onChange={(checked) =>
229 toggleId(cid, checked === true)
230 }
231 __nextHasNoMarginBottom
232 />
233 </div>
234 );
235 })
236 )}
237 </div>
238 )}
239 {value.length > 0 && (
240 <Button
241 variant="link"
242 style={{ paddingLeft: 0, marginTop: 6 }}
243 onClick={() => onChange([])}
244 >
245 {__("Clear selected", "yatra")}
246 </Button>
247 )}
248 <p className="components-base-control__help" style={{ marginTop: 6 }}>
249 {__(
250 "If none are checked, the block behaves like “All” (no taxonomy filter).",
251 "yatra",
252 )}
253 </p>
254 </>
255 )}
256 </fieldset>
257 );
258 }
259