pub enum Datalist {
Fixed {
options: Vec<String>,
},
Live {
from_rows: FromRows,
},
}Expand description
A completion list a column’s input draws on, in one of two forms: a fixed list the server computed, or one the browser computes live from the rows on screen.
Variants§
Fixed
A list the server built, typically from a sibling table.
Live
A list built from the rows on screen.
Implementations§
Source§impl Datalist
impl Datalist
Sourcepub fn fixed(options: impl IntoIterator<Item = impl Into<String>>) -> Self
pub fn fixed(options: impl IntoIterator<Item = impl Into<String>>) -> Self
Examples found in repository?
examples/library/main.rs (line 224)
126 fn schema(&self, ctx: &Context) -> Result<Schema, ApiError> {
127 let genres = Self::genres(ctx)?;
128 let branches = Self::branches(ctx)?;
129
130 let mut names: Vec<&str> = genres.iter().map(|g| g.genre.as_str()).collect();
131 names.sort_unstable();
132 names.dedup();
133
134 let mut by_genre = OptionsBy::new("genre");
135 for genre in &names {
136 let subgenres: Vec<&str> = genres
137 .iter()
138 .filter(|g| g.genre == *genre)
139 .map(|g| g.subgenre.as_str())
140 .collect();
141 by_genre.insert(*genre, subgenres);
142 }
143
144 // Wide enough for the longest subgenre there is, computed here so the
145 // browser does not have to measure anything. It is the count of
146 // characters, nothing more: what the control puts around them is the
147 // bundle's business.
148 let widest = genres.iter().map(|g| g.subgenre.len()).max().unwrap_or(0);
149 let width_ch = u16::try_from(widest.max(8)).unwrap_or(u16::MAX);
150
151 // A key that shows a branch's name and stores its code.
152 let branch_keys: Vec<SelectOption> = branches
153 .iter()
154 .map(|b| SelectOption::labelled(&b.code, &b.name))
155 .collect();
156
157 let mut publishers: Vec<String> = ctx
158 .optional_rows::<Book>(BOOKS_FILE)?
159 .iter()
160 .map(|b| b.publisher.trim().to_string())
161 .filter(|p| !p.is_empty())
162 .collect();
163 publishers.sort_unstable();
164 publishers.dedup();
165
166 Ok(Schema::new([
167 Column::string("title", "Title").width_ch(30),
168 Column::string("author_first", "First").width_ch(12),
169 Column::string("author_last", "Last").width_ch(14),
170 Column::select("genre", "Genre", names)
171 .allow_empty()
172 .cascades_to(["subgenre"]),
173 Column::select_by("subgenre", "Subgenre", by_genre)
174 .allow_empty()
175 .width_ch(width_ch),
176 Column::select(
177 "edition",
178 "Edition",
179 [
180 SelectOption::labelled("1", "First (1)"),
181 SelectOption::labelled("2", "Second (2)"),
182 SelectOption::labelled("3", "Third (3)"),
183 ],
184 )
185 .allow_empty()
186 .numeric_value(),
187 // A year is a whole number; a rating is not, which is what the
188 // absence of int_only means.
189 Column::number("year", "Year").int_only().width_ch(4),
190 Column::number("copies", "Copies").int_only().width_ch(3),
191 Column::number("rating", "Rating").width_ch(3),
192 Column::boolean("lent", "Lent"),
193 Column::string("publisher", "Publisher")
194 .width_ch(20)
195 .datalist("publishers"),
196 Column::string("donor", "Donated by")
197 .width_ch(20)
198 .datalist("reader-names"),
199 Column::spaced_string("call_number", "Call no.").width_ch(14),
200 Column::string("pronunciation", "Say")
201 .width_ch(16)
202 .speak(Speak::new(
203 "http://127.0.0.1:8765/say?text={value}",
204 "table-editor-speech-url",
205 )),
206 Column::map(
207 "shelved",
208 "Shelved",
209 MapSpec::new("Branch", "Copies")
210 .chips_show_key()
211 .key_options(branch_keys)
212 .value_options([
213 SelectOption::labelled("one", "One"),
214 SelectOption::labelled("several", "Several"),
215 SelectOption::labelled("many", "Many"),
216 ]),
217 ),
218 Column::string("due", "Due").width_ch(10),
219 Column::string("link", "Catalogue").width_ch(30),
220 Column::computed("shelf", "Shelf mark", "shelf").width_ch(18),
221 Column::text("notes", "Notes").wide(),
222 ])
223 .sortable()
224 .datalist("publishers", Datalist::fixed(publishers))
225 .datalist(
226 "reader-names",
227 Datalist::from_rows(["author_last", "author_first"], ", "),
228 )
229 .new_row(
230 NewRow::new()
231 .with("title", "")
232 .with("author_first", "")
233 .with("author_last", "")
234 .with("genre", "")
235 .with("subgenre", "")
236 .with("publisher", "")
237 .with("donor", "")
238 .with("call_number", "")
239 .with("pronunciation", "")
240 .with("due", "")
241 .with("link", "")
242 .with("notes", "")
243 .with("copies", 1)
244 .carry_forward(["genre", "subgenre", "publisher"]),
245 ))
246 }Sourcepub fn from_rows(
fields: impl IntoIterator<Item = impl Into<String>>,
separator: impl Into<String>,
) -> Self
pub fn from_rows( fields: impl IntoIterator<Item = impl Into<String>>, separator: impl Into<String>, ) -> Self
Examples found in repository?
examples/library/main.rs (line 227)
126 fn schema(&self, ctx: &Context) -> Result<Schema, ApiError> {
127 let genres = Self::genres(ctx)?;
128 let branches = Self::branches(ctx)?;
129
130 let mut names: Vec<&str> = genres.iter().map(|g| g.genre.as_str()).collect();
131 names.sort_unstable();
132 names.dedup();
133
134 let mut by_genre = OptionsBy::new("genre");
135 for genre in &names {
136 let subgenres: Vec<&str> = genres
137 .iter()
138 .filter(|g| g.genre == *genre)
139 .map(|g| g.subgenre.as_str())
140 .collect();
141 by_genre.insert(*genre, subgenres);
142 }
143
144 // Wide enough for the longest subgenre there is, computed here so the
145 // browser does not have to measure anything. It is the count of
146 // characters, nothing more: what the control puts around them is the
147 // bundle's business.
148 let widest = genres.iter().map(|g| g.subgenre.len()).max().unwrap_or(0);
149 let width_ch = u16::try_from(widest.max(8)).unwrap_or(u16::MAX);
150
151 // A key that shows a branch's name and stores its code.
152 let branch_keys: Vec<SelectOption> = branches
153 .iter()
154 .map(|b| SelectOption::labelled(&b.code, &b.name))
155 .collect();
156
157 let mut publishers: Vec<String> = ctx
158 .optional_rows::<Book>(BOOKS_FILE)?
159 .iter()
160 .map(|b| b.publisher.trim().to_string())
161 .filter(|p| !p.is_empty())
162 .collect();
163 publishers.sort_unstable();
164 publishers.dedup();
165
166 Ok(Schema::new([
167 Column::string("title", "Title").width_ch(30),
168 Column::string("author_first", "First").width_ch(12),
169 Column::string("author_last", "Last").width_ch(14),
170 Column::select("genre", "Genre", names)
171 .allow_empty()
172 .cascades_to(["subgenre"]),
173 Column::select_by("subgenre", "Subgenre", by_genre)
174 .allow_empty()
175 .width_ch(width_ch),
176 Column::select(
177 "edition",
178 "Edition",
179 [
180 SelectOption::labelled("1", "First (1)"),
181 SelectOption::labelled("2", "Second (2)"),
182 SelectOption::labelled("3", "Third (3)"),
183 ],
184 )
185 .allow_empty()
186 .numeric_value(),
187 // A year is a whole number; a rating is not, which is what the
188 // absence of int_only means.
189 Column::number("year", "Year").int_only().width_ch(4),
190 Column::number("copies", "Copies").int_only().width_ch(3),
191 Column::number("rating", "Rating").width_ch(3),
192 Column::boolean("lent", "Lent"),
193 Column::string("publisher", "Publisher")
194 .width_ch(20)
195 .datalist("publishers"),
196 Column::string("donor", "Donated by")
197 .width_ch(20)
198 .datalist("reader-names"),
199 Column::spaced_string("call_number", "Call no.").width_ch(14),
200 Column::string("pronunciation", "Say")
201 .width_ch(16)
202 .speak(Speak::new(
203 "http://127.0.0.1:8765/say?text={value}",
204 "table-editor-speech-url",
205 )),
206 Column::map(
207 "shelved",
208 "Shelved",
209 MapSpec::new("Branch", "Copies")
210 .chips_show_key()
211 .key_options(branch_keys)
212 .value_options([
213 SelectOption::labelled("one", "One"),
214 SelectOption::labelled("several", "Several"),
215 SelectOption::labelled("many", "Many"),
216 ]),
217 ),
218 Column::string("due", "Due").width_ch(10),
219 Column::string("link", "Catalogue").width_ch(30),
220 Column::computed("shelf", "Shelf mark", "shelf").width_ch(18),
221 Column::text("notes", "Notes").wide(),
222 ])
223 .sortable()
224 .datalist("publishers", Datalist::fixed(publishers))
225 .datalist(
226 "reader-names",
227 Datalist::from_rows(["author_last", "author_first"], ", "),
228 )
229 .new_row(
230 NewRow::new()
231 .with("title", "")
232 .with("author_first", "")
233 .with("author_last", "")
234 .with("genre", "")
235 .with("subgenre", "")
236 .with("publisher", "")
237 .with("donor", "")
238 .with("call_number", "")
239 .with("pronunciation", "")
240 .with("due", "")
241 .with("link", "")
242 .with("notes", "")
243 .with("copies", 1)
244 .carry_forward(["genre", "subgenre", "publisher"]),
245 ))
246 }
247
248 fn validate(&self, rows: &[Book], ctx: &Context) -> Result<Vec<ValidationError>, ApiError> {
249 let genres = Self::genres(ctx)?;
250 let branches = Self::branches(ctx)?;
251 let mut errors = Vec::new();
252
253 for (idx, row) in rows.iter().enumerate() {
254 let line = idx + 1;
255 if row.title.trim().is_empty() {
256 errors.push(ValidationError::field(
257 line,
258 "title",
259 "a book needs a title",
260 ));
261 }
262
263 // A subgenre stands or falls with the genre it was chosen under.
264 if !genres.is_empty() && !row.subgenre.is_empty() {
265 let known = genres
266 .iter()
267 .any(|g| g.genre == row.genre && g.subgenre == row.subgenre);
268 if !known {
269 errors.push(ValidationError::field(
270 line,
271 "subgenre",
272 format!("not a subgenre of {}", row.genre),
273 ));
274 }
275 }
276
277 if !branches.is_empty() {
278 for branch in row.shelved.keys() {
279 if !branches.iter().any(|b| b.code == *branch) {
280 errors.push(ValidationError::field(
281 line,
282 "shelved",
283 format!("no branch has the code {branch}"),
284 ));
285 }
286 }
287 }
288 }
289
290 Ok(errors)
291 }
292
293 fn derive(&self, rows: &[Book], _ctx: &Context) -> Result<Vec<Value>, ApiError> {
294 Ok(rows
295 .iter()
296 .map(|row| json!({ "shelf": Self::shelf_mark(row) }))
297 .collect())
298 }
299
300 fn siblings(&self, ctx: &Context) -> Result<Value, ApiError> {
301 Ok(json!({ "genres": Self::genres(ctx)? }))
302 }
303}
304
305// ── Genres ──────────────────────────────────────────────────────────────────
306
307struct Genres;
308
309impl TableLogic for Genres {
310 type Row = Genre;
311
312 fn name(&self) -> &'static str {
313 "genres"
314 }
315
316 fn file(&self) -> &'static str {
317 GENRES_FILE
318 }
319
320 fn title(&self) -> &'static str {
321 "Genres"
322 }
323
324 fn schema(&self, _ctx: &Context) -> Result<Schema, ApiError> {
325 Ok(Schema::new([
326 Column::string("genre", "Genre").width_ch(18),
327 Column::string("subgenre", "Subgenre").width_ch(22),
328 ])
329 .sortable()
330 .new_row(
331 NewRow::new()
332 .with("genre", "")
333 .with("subgenre", "")
334 .carry_forward(["genre"]),
335 ))
336 }
337
338 fn validate(&self, rows: &[Genre], _ctx: &Context) -> Result<Vec<ValidationError>, ApiError> {
339 let mut errors = Vec::new();
340 for (idx, row) in rows.iter().enumerate() {
341 if row.genre.trim().is_empty() {
342 errors.push(ValidationError::field(
343 idx + 1,
344 "genre",
345 "a genre is needed",
346 ));
347 }
348 if row.subgenre.trim().is_empty() {
349 errors.push(ValidationError::field(
350 idx + 1,
351 "subgenre",
352 "a subgenre is needed",
353 ));
354 }
355 }
356 Ok(errors)
357 }
358}
359
360// ── Branches ────────────────────────────────────────────────────────────────
361
362struct Branches;
363
364impl TableLogic for Branches {
365 type Row = Branch;
366
367 fn name(&self) -> &'static str {
368 "branches"
369 }
370
371 fn file(&self) -> &'static str {
372 BRANCHES_FILE
373 }
374
375 fn title(&self) -> &'static str {
376 "Branches"
377 }
378
379 fn schema(&self, _ctx: &Context) -> Result<Schema, ApiError> {
380 // Row order here is the order the branches are listed in, which is a
381 // choice the table makes, so this table is not sortable and rows are
382 // dragged into place instead.
383 Ok(Schema::new([
384 Column::string("code", "Code").width_ch(3),
385 Column::string("name", "Name").width_ch(22),
386 Column::string("librarian_first", "Librarian").width_ch(8),
387 Column::string("librarian_last", "Surname")
388 .width_ch(8)
389 .datalist("librarian-names"),
390 Column::number("staff", "Staff").int_only().width_ch(2),
391 Column::boolean("open", "Open"),
392 Column::map(
393 "hours",
394 "Hours",
395 MapSpec::new("Day", "Hours")
396 .key_options(["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"])
397 .value_options([
398 SelectOption::new("09:00-17:00"),
399 SelectOption::new("12:00-20:00"),
400 ])
401 .allow_new_keys()
402 .allow_new_values(),
403 ),
404 ])
405 .datalist(
406 "librarian-names",
407 Datalist::from_rows(["librarian_last"], " "),
408 )
409 .new_row(
410 NewRow::new()
411 .with("code", "")
412 .with("name", "")
413 .with("librarian_first", "")
414 .with("librarian_last", ""),
415 ))
416 }Trait Implementations§
impl Eq for Datalist
impl StructuralPartialEq for Datalist
Auto Trait Implementations§
impl Freeze for Datalist
impl RefUnwindSafe for Datalist
impl Send for Datalist
impl Sync for Datalist
impl Unpin for Datalist
impl UnsafeUnpin for Datalist
impl UnwindSafe for Datalist
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more