pub struct SelectOption {
pub value: String,
pub label: Option<String>,
}Expand description
One choice in a select. label is what the browser shows when it differs
from the stored value.
Fields§
§value: String§label: Option<String>Implementations§
Source§impl SelectOption
impl SelectOption
Sourcepub fn new(value: impl Into<String>) -> Self
pub fn new(value: impl Into<String>) -> Self
Examples found in repository?
examples/library/main.rs (line 397)
378 fn schema(&self, _ctx: &Context) -> Result<Schema, ApiError> {
379 // Row order here is the order the branches are listed in, which is a
380 // choice the table makes, so this table is not sortable and rows are
381 // dragged into place instead.
382 Ok(Schema::new([
383 Column::string("code", "Code").width_ch(3),
384 Column::string("name", "Name").width_ch(22),
385 Column::string("librarian_first", "Librarian").width_ch(8),
386 Column::string("librarian_last", "Surname")
387 .width_ch(8)
388 .datalist("librarian-names"),
389 Column::number("staff", "Staff").int_only().width_ch(2),
390 Column::boolean("open", "Open"),
391 Column::map(
392 "hours",
393 "Hours",
394 MapSpec::new("Day", "Hours")
395 .key_options(["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"])
396 .value_options([
397 SelectOption::new("09:00-17:00"),
398 SelectOption::new("12:00-20:00"),
399 ])
400 .allow_new_keys()
401 .allow_new_values(),
402 ),
403 ])
404 .datalist(
405 "librarian-names",
406 Datalist::from_rows(["librarian_last"], " "),
407 )
408 .new_row(
409 NewRow::new()
410 .with("code", "")
411 .with("name", "")
412 .with("librarian_first", "")
413 .with("librarian_last", ""),
414 ))
415 }Sourcepub fn labelled(value: impl Into<String>, label: impl Into<String>) -> Self
pub fn labelled(value: impl Into<String>, label: impl Into<String>) -> Self
Examples found in repository?
examples/library/main.rs (line 153)
125 fn schema(&self, ctx: &Context) -> Result<Schema, ApiError> {
126 let genres = Self::genres(ctx)?;
127 let branches = Self::branches(ctx)?;
128
129 let mut names: Vec<&str> = genres.iter().map(|g| g.genre.as_str()).collect();
130 names.sort_unstable();
131 names.dedup();
132
133 let mut by_genre = OptionsBy::new("genre");
134 for genre in &names {
135 let subgenres: Vec<&str> = genres
136 .iter()
137 .filter(|g| g.genre == *genre)
138 .map(|g| g.subgenre.as_str())
139 .collect();
140 by_genre.insert(*genre, subgenres);
141 }
142
143 // Wide enough for the longest subgenre there is, computed here so the
144 // browser does not have to measure anything. It is the count of
145 // characters, nothing more: what the control puts around them is the
146 // bundle's business.
147 let widest = genres.iter().map(|g| g.subgenre.len()).max().unwrap_or(0);
148 let width_ch = u16::try_from(widest.max(8)).unwrap_or(u16::MAX);
149
150 // A key that shows a branch's name and stores its code.
151 let branch_keys: Vec<SelectOption> = branches
152 .iter()
153 .map(|b| SelectOption::labelled(&b.code, &b.name))
154 .collect();
155
156 let mut publishers: Vec<String> = ctx
157 .optional_rows::<Book>(BOOKS_FILE)?
158 .iter()
159 .map(|b| b.publisher.trim().to_string())
160 .filter(|p| !p.is_empty())
161 .collect();
162 publishers.sort_unstable();
163 publishers.dedup();
164
165 Ok(Schema::new([
166 Column::string("title", "Title").width_ch(30),
167 Column::string("author_first", "First").width_ch(12),
168 Column::string("author_last", "Last").width_ch(14),
169 Column::select("genre", "Genre", names)
170 .allow_empty()
171 .cascades_to(["subgenre"]),
172 Column::select_by("subgenre", "Subgenre", by_genre)
173 .allow_empty()
174 .width_ch(width_ch),
175 Column::select(
176 "edition",
177 "Edition",
178 [
179 SelectOption::labelled("1", "First (1)"),
180 SelectOption::labelled("2", "Second (2)"),
181 SelectOption::labelled("3", "Third (3)"),
182 ],
183 )
184 .allow_empty()
185 .numeric_value(),
186 // A year is a whole number; a rating is not, which is what the
187 // absence of int_only means.
188 Column::number("year", "Year").int_only().width_ch(4),
189 Column::number("copies", "Copies").int_only().width_ch(3),
190 Column::number("rating", "Rating").width_ch(3),
191 Column::boolean("lent", "Lent"),
192 Column::string("publisher", "Publisher")
193 .width_ch(20)
194 .datalist("publishers"),
195 Column::string("donor", "Donated by")
196 .width_ch(20)
197 .datalist("reader-names"),
198 Column::spaced_string("call_number", "Call no.").width_ch(14),
199 Column::string("pronunciation", "Say")
200 .width_ch(16)
201 .speak(Speak::new(
202 "http://127.0.0.1:8765/say?text={value}",
203 "table-editor-speech-url",
204 )),
205 Column::map(
206 "shelved",
207 "Shelved",
208 MapSpec::new("Branch", "Copies")
209 .chips_show_key()
210 .key_options(branch_keys)
211 .value_options([
212 SelectOption::labelled("one", "One"),
213 SelectOption::labelled("several", "Several"),
214 SelectOption::labelled("many", "Many"),
215 ]),
216 ),
217 Column::string("due", "Due").width_ch(10),
218 Column::string("link", "Catalogue").width_ch(30),
219 Column::computed("shelf", "Shelf mark", "shelf").width_ch(18),
220 Column::text("notes", "Notes").wide(),
221 ])
222 .sortable()
223 .datalist("publishers", Datalist::fixed(publishers))
224 .datalist(
225 "reader-names",
226 Datalist::from_rows(["author_last", "author_first"], ", "),
227 )
228 .new_row(
229 NewRow::new()
230 .with("title", "")
231 .with("author_first", "")
232 .with("author_last", "")
233 .with("genre", "")
234 .with("subgenre", "")
235 .with("publisher", "")
236 .with("donor", "")
237 .with("call_number", "")
238 .with("pronunciation", "")
239 .with("due", "")
240 .with("link", "")
241 .with("notes", "")
242 .with("copies", 1)
243 .carry_forward(["genre", "subgenre", "publisher"]),
244 ))
245 }
246
247 fn validate(&self, rows: &[Book], ctx: &Context) -> Result<Vec<ValidationError>, ApiError> {
248 let genres = Self::genres(ctx)?;
249 let branches = Self::branches(ctx)?;
250 let mut errors = Vec::new();
251
252 for (idx, row) in rows.iter().enumerate() {
253 let line = idx + 1;
254 if row.title.trim().is_empty() {
255 errors.push(ValidationError::field(
256 line,
257 "title",
258 "a book needs a title",
259 ));
260 }
261
262 // A subgenre stands or falls with the genre it was chosen under.
263 if !genres.is_empty() && !row.subgenre.is_empty() {
264 let known = genres
265 .iter()
266 .any(|g| g.genre == row.genre && g.subgenre == row.subgenre);
267 if !known {
268 errors.push(ValidationError::field(
269 line,
270 "subgenre",
271 format!("not a subgenre of {}", row.genre),
272 ));
273 }
274 }
275
276 if !branches.is_empty() {
277 for branch in row.shelved.keys() {
278 if !branches.iter().any(|b| b.code == *branch) {
279 errors.push(ValidationError::field(
280 line,
281 "shelved",
282 format!("no branch has the code {branch}"),
283 ));
284 }
285 }
286 }
287 }
288
289 Ok(errors)
290 }
291
292 fn derive(&self, rows: &[Book], _ctx: &Context) -> Result<Vec<Value>, ApiError> {
293 Ok(rows
294 .iter()
295 .map(|row| json!({ "shelf": Self::shelf_mark(row) }))
296 .collect())
297 }
298
299 fn siblings(&self, ctx: &Context) -> Result<Value, ApiError> {
300 Ok(json!({ "genres": Self::genres(ctx)? }))
301 }
302}
303
304// ── Genres ──────────────────────────────────────────────────────────────────
305
306struct Genres;
307
308impl TableLogic for Genres {
309 type Row = Genre;
310
311 fn name(&self) -> &'static str {
312 "genres"
313 }
314
315 fn file(&self) -> &'static str {
316 GENRES_FILE
317 }
318
319 fn title(&self) -> &'static str {
320 "Genres"
321 }
322
323 fn schema(&self, _ctx: &Context) -> Result<Schema, ApiError> {
324 Ok(Schema::new([
325 Column::string("genre", "Genre").width_ch(18),
326 Column::string("subgenre", "Subgenre").width_ch(22),
327 ])
328 .sortable()
329 .new_row(
330 NewRow::new()
331 .with("genre", "")
332 .with("subgenre", "")
333 .carry_forward(["genre"]),
334 ))
335 }
336
337 fn validate(&self, rows: &[Genre], _ctx: &Context) -> Result<Vec<ValidationError>, ApiError> {
338 let mut errors = Vec::new();
339 for (idx, row) in rows.iter().enumerate() {
340 if row.genre.trim().is_empty() {
341 errors.push(ValidationError::field(
342 idx + 1,
343 "genre",
344 "a genre is needed",
345 ));
346 }
347 if row.subgenre.trim().is_empty() {
348 errors.push(ValidationError::field(
349 idx + 1,
350 "subgenre",
351 "a subgenre is needed",
352 ));
353 }
354 }
355 Ok(errors)
356 }
357}
358
359// ── Branches ────────────────────────────────────────────────────────────────
360
361struct Branches;
362
363impl TableLogic for Branches {
364 type Row = Branch;
365
366 fn name(&self) -> &'static str {
367 "branches"
368 }
369
370 fn file(&self) -> &'static str {
371 BRANCHES_FILE
372 }
373
374 fn title(&self) -> &'static str {
375 "Branches"
376 }
377
378 fn schema(&self, _ctx: &Context) -> Result<Schema, ApiError> {
379 // Row order here is the order the branches are listed in, which is a
380 // choice the table makes, so this table is not sortable and rows are
381 // dragged into place instead.
382 Ok(Schema::new([
383 Column::string("code", "Code").width_ch(3),
384 Column::string("name", "Name").width_ch(22),
385 Column::string("librarian_first", "Librarian").width_ch(8),
386 Column::string("librarian_last", "Surname")
387 .width_ch(8)
388 .datalist("librarian-names"),
389 Column::number("staff", "Staff").int_only().width_ch(2),
390 Column::boolean("open", "Open"),
391 Column::map(
392 "hours",
393 "Hours",
394 MapSpec::new("Day", "Hours")
395 .key_options(["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"])
396 .value_options([
397 SelectOption::new("09:00-17:00"),
398 SelectOption::new("12:00-20:00"),
399 ])
400 .allow_new_keys()
401 .allow_new_values(),
402 ),
403 ])
404 .datalist(
405 "librarian-names",
406 Datalist::from_rows(["librarian_last"], " "),
407 )
408 .new_row(
409 NewRow::new()
410 .with("code", "")
411 .with("name", "")
412 .with("librarian_first", "")
413 .with("librarian_last", ""),
414 ))
415 }
416
417 fn validate(&self, rows: &[Branch], _ctx: &Context) -> Result<Vec<ValidationError>, ApiError> {
418 let mut errors = Vec::new();
419 for (idx, row) in rows.iter().enumerate() {
420 if row.code.trim().is_empty() {
421 errors.push(ValidationError::field(
422 idx + 1,
423 "code",
424 "a branch needs a code",
425 ));
426 }
427 }
428 Ok(errors)
429 }
430}
431
432// ── The On loan view ────────────────────────────────────────────────────────
433
434/// What is out on loan from one branch, in two sections: the books still
435/// within their time, and the ones past it.
436///
437/// A view computes its rows rather than storing them, so nothing here is in a
438/// file: the counts, the days, and which section a book falls into are worked
439/// out per request from the tables the example already ships. The due dates
440/// are fixed in the data, so as real time passes more of them fall overdue,
441/// which is what an example of an overdue list should do.
442struct OnLoan;
443
444/// Days since 1970-01-01 for a `YYYY-MM-DD` date, or nothing for text that is
445/// not one. Howard Hinnant's civil-days algorithm, which needs no calendar
446/// library and no dependency.
447fn days_from_civil(date: &str) -> Option<i64> {
448 let mut parts = date.split('-');
449 let y: i64 = parts.next()?.parse().ok()?;
450 let m: i64 = parts.next()?.parse().ok()?;
451 let d: i64 = parts.next()?.parse().ok()?;
452 if parts.next().is_some() || !(1..=12).contains(&m) || !(1..=31).contains(&d) {
453 return None;
454 }
455
456 let y = if m <= 2 { y - 1 } else { y };
457 let era = if y >= 0 { y } else { y - 399 } / 400;
458 let yoe = y - era * 400;
459 let mp = (m + 9) % 12;
460 let doy = (153 * mp + 2) / 5 + d - 1;
461 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
462 Some(era * 146_097 + doe - 719_468)
463}
464
465fn today() -> i64 {
466 let seconds = std::time::SystemTime::now()
467 .duration_since(std::time::UNIX_EPOCH)
468 .map(|d| d.as_secs() as i64)
469 .unwrap_or(0);
470 seconds / 86_400
471}
472
473impl OnLoan {
474 fn branches(ctx: &Context) -> Result<Vec<Branch>, ApiError> {
475 ctx.optional_rows(BRANCHES_FILE)
476 }
477
478 /// The columns both sections show. They are the same in each, so the two
479 /// line up; a section that wanted a column of its own would say so here.
480 fn columns() -> Vec<Column> {
481 vec![
482 Column::string("title", "Title").width_ch(30).href("link"),
483 Column::string("author", "Author").width_ch(18),
484 Column::string("due", "Due").width_ch(10),
485 Column::number("days", "Days").width_ch(4),
486 ]
487 }
488
489 fn row(book: &Book, days: i64) -> Loan {
490 Loan {
491 title: book.title.clone(),
492 author: format!("{} {}", book.author_first, book.author_last)
493 .trim()
494 .to_string(),
495 due: if book.due.is_empty() {
496 "—".to_string()
497 } else {
498 book.due.clone()
499 },
500 days: days.abs(),
501 link: book.link.clone(),
502 }
503 }
504
505 /// A list of options with "any of them" in front of it, whose value is
506 /// empty. Choosing it clears the parameter, which is an answer of its own.
507 fn any<'a>(label: &str, values: impl IntoIterator<Item = &'a str>) -> Vec<SelectOption> {
508 std::iter::once(SelectOption::labelled("", label))
509 .chain(values.into_iter().map(SelectOption::from))
510 .collect()
511 }
512}
513
514/// A row of either section. A view hands over its own type, so the shape of a
515/// row is written down once here rather than assembled field by field.
516#[derive(Serialize)]
517struct Loan {
518 title: String,
519 author: String,
520 due: String,
521 days: i64,
522 link: String,
523}
524
525impl ViewLogic for OnLoan {
526 fn name(&self) -> &'static str {
527 "on-loan"
528 }
529
530 fn title(&self) -> &'static str {
531 "On loan"
532 }
533
534 fn params(&self, ctx: &Context, asked: &ViewArgs) -> Result<Vec<Param>, ApiError> {
535 let branches = Self::branches(ctx)?;
536 let options: Vec<SelectOption> = branches
537 .iter()
538 .map(|b| SelectOption::labelled(&b.code, &b.name))
539 .collect();
540 let first = branches.first().map(|b| b.code.clone()).unwrap_or_default();
541
542 // A genre and one of its subgenres. The second list follows the first
543 // one's answer, which is why `params` is told what was asked before
544 // anything is settled. A subgenre belonging to the genre chosen last
545 // time is no answer to the question being asked now, and falls back to
546 // every subgenre of the genre chosen this time.
547 let genres: Vec<Genre> = ctx.optional_rows(GENRES_FILE)?;
548 let mut named: Vec<&str> = genres.iter().map(|g| g.genre.as_str()).collect();
549 named.sort_unstable();
550 named.dedup();
551
552 let chosen = asked.get_or("genre", "");
553 let mut subgenres: Vec<&str> = genres
554 .iter()
555 .filter(|g| g.genre == chosen)
556 .map(|g| g.subgenre.as_str())
557 .collect();
558 subgenres.sort_unstable();
559
560 Ok(vec![
561 Param::select("branch", "Branch", options).default(first),
562 Param::select("genre", "Genre", Self::any("Every genre", named)).default(""),
563 Param::select(
564 "subgenre",
565 "Subgenre",
566 Self::any("Every subgenre", subgenres),
567 )
568 .default(""),
569 Param::string("author", "Author"),
570 ])
571 }Trait Implementations§
Source§impl Clone for SelectOption
impl Clone for SelectOption
Source§fn clone(&self) -> SelectOption
fn clone(&self) -> SelectOption
Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
Performs copy-assignment from
source. Read moreSource§impl Debug for SelectOption
impl Debug for SelectOption
impl Eq for SelectOption
Source§impl From<&String> for SelectOption
impl From<&String> for SelectOption
Source§impl From<&str> for SelectOption
impl From<&str> for SelectOption
Source§impl From<String> for SelectOption
impl From<String> for SelectOption
Source§impl PartialEq for SelectOption
impl PartialEq for SelectOption
Source§impl Serialize for SelectOption
impl Serialize for SelectOption
impl StructuralPartialEq for SelectOption
Auto Trait Implementations§
impl Freeze for SelectOption
impl RefUnwindSafe for SelectOption
impl Send for SelectOption
impl Sync for SelectOption
impl Unpin for SelectOption
impl UnsafeUnpin for SelectOption
impl UnwindSafe for SelectOption
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