PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.15
Yatra – Travel Booking & Tour Operator Software v3.0.15
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 2.0.10 All 83 releases
yatra / resources / js / blocks / components / ClassificationMultiSelect.tsx

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

260 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
132 className="components-base-control__label"
133 style={{ padding: 0 }}
134 >
135 {label}
136 </legend>
137 <Spinner />
138 </fieldset>
139 );
140 }
141
142 return (
143 <fieldset
144 className="yatra-block-taxonomy-field"
145 style={{ margin: "0 0 16px", border: "none", padding: 0 }}
146 >
147 <legend className="components-base-control__label" style={{ padding: 0 }}>
148 {label}
149 </legend>
150 <p
151 className="components-base-control__help"
152 style={{ marginTop: 4, marginBottom: 10 }}
153 >
154 {help}
155 </p>
156 {loadError && (
157 <p style={{ color: "#b32d2e", fontSize: 12, marginBottom: 8 }}>
158 {__(
159 "Could not load options. Confirm you can edit posts and Yatra REST is available.",
160 "yatra",
161 )}
162 </p>
163 )}
164 <RadioControl
165 label={__("Listing scope", "yatra")}
166 selected={scope}
167 options={[
168 {
169 label: __("All published (no restriction)", "yatra"),
170 value: "all",
171 },
172 {
173 label: __("Only selected (search below)", "yatra"),
174 value: "narrow",
175 },
176 ]}
177 onChange={onScopeChange}
178 />
179 {scope === "all" && (
180 <p className="components-base-control__help" style={{ marginTop: 4 }}>
181 {__(
182 "The frontend will include every matching published item.",
183 "yatra",
184 )}
185 </p>
186 )}
187 {scope === "narrow" && (
188 <>
189 <SearchControl
190 label={sprintf(
191 /* translators: %d = number of loaded taxonomy items */
192 __("Filter items (%d loaded)", "yatra"),
193 items.length,
194 )}
195 hideLabelFromVision
196 placeholder={__("Type to filter the list…", "yatra")}
197 value={search}
198 onChange={(s) => setSearch(s)}
199 __nextHasNoMarginBottom
200 />
201 {value.length > 0 && (
202 <p
203 className="components-base-control__help"
204 style={{ marginTop: 4 }}
205 >
206 {sprintf(
207 /* translators: %d = number of selected taxonomy items */
208 __("%d selected", "yatra"),
209 value.length,
210 )}
211 </p>
212 )}
213 {items.length === 0 && !loadError ? (
214 <p className="components-base-control__help">
215 {__("No published items of this type yet.", "yatra")}
216 </p>
217 ) : (
218 <div role="group" aria-label={label} style={listWrapStyle}>
219 {filteredItems.length === 0 ? (
220 <p style={{ padding: "8px 12px", margin: 0, fontSize: 12 }}>
221 {__("No matching items.", "yatra")}
222 </p>
223 ) : (
224 filteredItems.map((item) => {
225 const cid = Number(item.id);
226 return (
227 <div key={cid} style={{ padding: "2px 8px" }}>
228 <CheckboxControl
229 label={`${item.name} (${cid})`}
230 checked={selectedSet.has(cid)}
231 onChange={(checked) => toggleId(cid, checked === true)}
232 __nextHasNoMarginBottom
233 />
234 </div>
235 );
236 })
237 )}
238 </div>
239 )}
240 {value.length > 0 && (
241 <Button
242 variant="link"
243 style={{ paddingLeft: 0, marginTop: 6 }}
244 onClick={() => onChange([])}
245 >
246 {__("Clear selected", "yatra")}
247 </Button>
248 )}
249 <p className="components-base-control__help" style={{ marginTop: 6 }}>
250 {__(
251 "If none are checked, the block behaves like “All” (no taxonomy filter).",
252 "yatra",
253 )}
254 </p>
255 </>
256 )}
257 </fieldset>
258 );
259 }
260