1use std::collections::{HashMap, HashSet};
13
14use tablo_ui::{
15 ButtonSize, ButtonVariant, FieldLegendVariant, button, field_error as ui_field_error,
16 field_legend, field_set,
17};
18use topcoat::{Result, context::Cx, view::*};
19
20use super::{Schema, fields::Field, tree::Source};
21use crate::form::{FieldError, FieldErrors};
22
23pub trait RepeaterItem: Sized + Send + Sync + 'static {
29 fn schema() -> Schema;
31
32 fn parse(
38 cx: &Cx,
39 values: &HashMap<String, String>,
40 ) -> std::result::Result<Self, Vec<FieldError>>;
41
42 fn write(&self, out: &mut HashMap<String, String>);
44}
45
46pub(crate) const ROW: &str = "__row__";
48
49pub const MAX_ROWS: usize = 100;
52
53pub(crate) struct RepeaterControl {
55 item: fn() -> Schema,
56 pub(crate) add_label: Option<String>,
58}
59
60impl RepeaterControl {
61 pub(crate) fn new<T: RepeaterItem>() -> Self {
62 Self {
63 item: T::schema,
64 add_label: None,
65 }
66 }
67
68 pub(crate) fn fold(
79 &self,
80 key: &str,
81 values: &mut HashMap<String, String>,
82 ) -> std::result::Result<(), String> {
83 let Some(order) = values.remove(key) else {
84 return Ok(());
85 };
86 let leaves: Vec<String> = (self.item)()
87 .fields()
88 .map(|field| field.name().to_string())
89 .collect();
90 let mut seen = HashSet::new();
91 let mut rows = Vec::new();
92 for token in order.split(',').map(str::trim).filter(|t| !t.is_empty()) {
93 if rows.len() == MAX_ROWS {
94 return Err(format!("`{key}` lists more than {MAX_ROWS} rows"));
95 }
96 let row: usize = token
97 .parse()
98 .map_err(|_| format!("`{key}` lists `{token}`, which is not a row"))?;
99 if !seen.insert(row) {
100 return Err(format!("`{key}` lists row {row} twice"));
101 }
102 let item: HashMap<String, String> = leaves
103 .iter()
104 .filter_map(|leaf| {
105 let value = values.remove(&row_key(key, &row.to_string(), leaf))?;
106 Some((leaf.clone(), value))
107 })
108 .collect();
109 if item.is_empty() {
110 return Err(format!("`{key}` lists row {row}, which posts nothing"));
111 }
112 rows.push(item);
113 }
114 values.insert(key.to_string(), encode(&rows));
115 Ok(())
116 }
117
118 pub(crate) async fn check(&self, cx: &Cx, key: &str, value: &str, errors: &mut FieldErrors) {
122 for (row, item) in decode(value).unwrap_or_default().iter().enumerate() {
123 let (schema, values) = self.row(key, &row.to_string(), item);
124 Box::pin(schema.check_controls(cx, &values, &HashMap::new(), errors)).await;
125 }
126 }
127
128 fn row(
131 &self,
132 key: &str,
133 row: &str,
134 item: &HashMap<String, String>,
135 ) -> (Schema, HashMap<String, String>) {
136 let prefix = row_key(key, row, "");
137 let mut schema = (self.item)();
138 for field in &mut schema.fields {
139 field.prefix(&prefix);
140 }
141 let values = item
142 .iter()
143 .map(|(leaf, value)| (format!("{prefix}{leaf}"), value.clone()))
144 .collect();
145 (schema, values)
146 }
147
148 pub(crate) async fn render<'a>(
151 &self,
152 cx: &'a Cx,
153 field: &Field,
154 source: &Source<'_>,
155 ) -> Result<BoxView<'a>> {
156 let key = field.name().to_string();
157 let items = source.value(&key).and_then(decode).unwrap_or_default();
158 let mut rows = Vec::with_capacity(items.len());
159 for (row, item) in items.iter().enumerate() {
160 rows.push(
161 self.render_row(cx, &key, &row.to_string(), item, source.errors(), source)
162 .await?,
163 );
164 }
165 let blank = self
166 .render_row(cx, &key, ROW, &HashMap::new(), &FieldErrors::new(), source)
167 .await?;
168 let order = (0..items.len())
169 .map(|row| row.to_string())
170 .collect::<Vec<_>>()
171 .join(",");
172 let next = items.len().to_string();
173 let label = field.label_str().to_string();
174 let add = self
175 .add_label
176 .clone()
177 .unwrap_or_else(|| "Add item".to_string());
178 let error = source.error_for(field);
179 let legend_id = format!("{}-legend", source.id(&key));
180 Ok(view! {
181 cx =>
182 field_set(
183 attrs: attributes! {
184 class="ac-field gap-3"
185 data-repeater=(key.clone())
186 data-repeater-next=(next)
187 },
188 field_legend(
189 variant: FieldLegendVariant::Label,
190 attrs: attributes! { id=(legend_id.clone()) class="mb-0" },
191 (label)
192 )
193 <input type="hidden" name=(key) value=(order) data-repeater-order="">
194 if let Some(error) = error {
195 ui_field_error(attrs: attributes! { class="ac-error" }, (error))
196 }
197 <ol
198 class="flex flex-col gap-3"
199 aria-labelledby=(legend_id)
200 data-repeater-rows=""
201 >
202 for row in rows {
203 (row)
204 }
205 </ol>
206 <template data-repeater-blank="">(blank)</template>
207 <div>
208 button(
209 variant: ButtonVariant::Outline,
210 size: ButtonSize::Sm,
211 attrs: attributes! { type="button" data-repeater-add="" },
212 (add)
213 )
214 </div>
215 )
216 }
217 .boxed())
218 }
219
220 async fn render_row<'a>(
223 &self,
224 cx: &'a Cx,
225 key: &str,
226 row: &str,
227 item: &HashMap<String, String>,
228 errors: &FieldErrors,
229 source: &Source<'_>,
230 ) -> Result<BoxView<'a>> {
231 let (schema, values) = self.row(key, row, item);
232 let mut rendered = Source::form(&values, errors).scoped(source.scope());
233 if let Some(options) = source.options() {
234 rendered = rendered.options_at(options);
235 }
236 let controls = schema.render(cx, rendered).await?;
237 let row = row.to_string();
238 let action = |label: &'static str, data: Attributes| {
239 view! {
240 cx =>
241 button(
242 variant: ButtonVariant::Ghost,
243 size: ButtonSize::Sm,
244 attrs: attributes! { type="button" (data) },
245 (label)
246 )
247 }
248 };
249 Ok(view! {
250 cx =>
251 <li
252 class="flex flex-col gap-4 rounded-lg border border-border p-4"
253 data-repeater-row=(row)
254 >
255 (controls)
256 <div class="flex flex-wrap justify-end gap-2">
257 (action("Move up", attributes! { cx => data-repeater-up="" }))
258 (action("Move down", attributes! { cx => data-repeater-down="" }))
259 (action("Remove", attributes! { cx => data-repeater-remove="" }))
260 </div>
261 </li>
262 }
263 .boxed())
264 }
265}
266
267pub(crate) fn row_key(key: &str, row: &str, leaf: &str) -> String {
269 format!("{key}.{row}.{leaf}")
270}
271
272fn encode(rows: &[HashMap<String, String>]) -> String {
274 serde_json::to_string(rows).expect("a list of string maps serializes")
275}
276
277pub(crate) fn decode(value: &str) -> Option<Vec<HashMap<String, String>>> {
280 if value.trim().is_empty() {
281 return Some(Vec::new());
282 }
283 serde_json::from_str(value).ok()
284}
285
286pub fn write_items<T: RepeaterItem>(items: &[T]) -> String {
289 let rows: Vec<HashMap<String, String>> = items
290 .iter()
291 .map(|item| {
292 let mut out = HashMap::new();
293 item.write(&mut out);
294 out
295 })
296 .collect();
297 encode(&rows)
298}
299
300pub fn parse_items<T: RepeaterItem>(
309 cx: &Cx,
310 key: &str,
311 values: &HashMap<String, String>,
312) -> std::result::Result<Vec<T>, Vec<FieldError>> {
313 let Some(rows) = decode(values.get(key).map_or("", String::as_str)) else {
314 return Err(vec![FieldError::invalid(key, "The rows could not be read")]);
315 };
316 let mut items = Vec::with_capacity(rows.len());
317 let mut errors = Vec::new();
318 for (row, values) in rows.iter().enumerate() {
319 match T::parse(cx, values) {
320 Ok(item) => items.push(item),
321 Err(failures) => errors.extend(failures.into_iter().map(|failure| FieldError {
322 key: row_key(key, &row.to_string(), &failure.key),
323 kind: failure.kind,
324 })),
325 }
326 }
327 if errors.is_empty() {
328 Ok(items)
329 } else {
330 Err(errors)
331 }
332}
333
334#[cfg(test)]
335mod tests;