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 398)
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 }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 154)
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 }
417
418 fn validate(&self, rows: &[Branch], _ctx: &Context) -> Result<Vec<ValidationError>, ApiError> {
419 let mut errors = Vec::new();
420 for (idx, row) in rows.iter().enumerate() {
421 if row.code.trim().is_empty() {
422 errors.push(ValidationError::field(
423 idx + 1,
424 "code",
425 "a branch needs a code",
426 ));
427 }
428 }
429 Ok(errors)
430 }
431}
432
433// ── The On loan view ────────────────────────────────────────────────────────
434
435/// What is out on loan from one branch, in two sections: the books still
436/// within their time, and the ones past it.
437///
438/// A view computes its rows rather than storing them, so nothing here is in a
439/// file: the counts, the days, and which section a book falls into are worked
440/// out per request from the tables the example already ships. The due dates
441/// are fixed in the data, so as real time passes more of them fall overdue,
442/// which is what an example of an overdue list should do.
443struct OnLoan;
444
445/// Days since 1970-01-01 for a `YYYY-MM-DD` date, or nothing for text that is
446/// not one. Howard Hinnant's civil-days algorithm, which needs no calendar
447/// library and no dependency.
448fn days_from_civil(date: &str) -> Option<i64> {
449 let mut parts = date.split('-');
450 let y: i64 = parts.next()?.parse().ok()?;
451 let m: i64 = parts.next()?.parse().ok()?;
452 let d: i64 = parts.next()?.parse().ok()?;
453 if parts.next().is_some() || !(1..=12).contains(&m) || !(1..=31).contains(&d) {
454 return None;
455 }
456
457 let y = if m <= 2 { y - 1 } else { y };
458 let era = if y >= 0 { y } else { y - 399 } / 400;
459 let yoe = y - era * 400;
460 let mp = (m + 9) % 12;
461 let doy = (153 * mp + 2) / 5 + d - 1;
462 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
463 Some(era * 146_097 + doe - 719_468)
464}
465
466/// The `YYYY-MM-DD` date `days` days after 1970-01-01, which is the inverse of
467/// `days_from_civil` and the other half of the same algorithm.
468fn civil_from_days(days: i64) -> String {
469 let z = days + 719_468;
470 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
471 let doe = z - era * 146_097;
472 let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
473 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
474 let mp = (5 * doy + 2) / 153;
475 let d = doy - (153 * mp + 2) / 5 + 1;
476 let m = if mp < 10 { mp + 3 } else { mp - 9 };
477 let y = yoe + era * 400 + i64::from(m <= 2);
478 format!("{y:04}-{m:02}-{d:02}")
479}
480
481fn today() -> i64 {
482 let seconds = std::time::SystemTime::now()
483 .duration_since(std::time::UNIX_EPOCH)
484 .map(|d| d.as_secs() as i64)
485 .unwrap_or(0);
486 seconds / 86_400
487}
488
489impl OnLoan {
490 fn branches(ctx: &Context) -> Result<Vec<Branch>, ApiError> {
491 ctx.optional_rows(BRANCHES_FILE)
492 }
493
494 /// The columns both sections show. They are the same in each, so the two
495 /// line up; a section that wanted a column of its own would say so here.
496 fn columns() -> Vec<Column> {
497 vec![
498 Column::string("title", "Title").width_ch(30).href("link"),
499 Column::string("author", "Author").width_ch(18),
500 Column::string("due", "Due").width_ch(10),
501 Column::number("days", "Days").width_ch(4),
502 ]
503 }
504
505 fn row(book: &Book, days: i64) -> Loan {
506 Loan {
507 title: book.title.clone(),
508 author: format!("{} {}", book.author_first, book.author_last)
509 .trim()
510 .to_string(),
511 due: if book.due.is_empty() {
512 "—".to_string()
513 } else {
514 book.due.clone()
515 },
516 days: days.abs(),
517 link: book.link.clone(),
518 }
519 }
520
521 /// A list of options with "any of them" in front of it, whose value is
522 /// empty. Choosing it clears the parameter, which is an answer of its own.
523 fn any<'a>(label: &str, values: impl IntoIterator<Item = &'a str>) -> Vec<SelectOption> {
524 std::iter::once(SelectOption::labelled("", label))
525 .chain(values.into_iter().map(SelectOption::from))
526 .collect()
527 }
528}
529
530/// A row of either section. A view hands over its own type, so the shape of a
531/// row is written down once here rather than assembled field by field.
532#[derive(Serialize)]
533struct Loan {
534 title: String,
535 author: String,
536 due: String,
537 days: i64,
538 link: String,
539}
540
541impl ViewLogic for OnLoan {
542 fn name(&self) -> &'static str {
543 "on-loan"
544 }
545
546 fn title(&self) -> &'static str {
547 "On loan"
548 }
549
550 fn params(&self, ctx: &Context, asked: &ViewArgs) -> Result<Vec<Param>, ApiError> {
551 let branches = Self::branches(ctx)?;
552 let options: Vec<SelectOption> = branches
553 .iter()
554 .map(|b| SelectOption::labelled(&b.code, &b.name))
555 .collect();
556 let first = branches.first().map(|b| b.code.clone()).unwrap_or_default();
557
558 // A genre and one of its subgenres. The second list follows the first
559 // one's answer, which is why `params` is told what was asked before
560 // anything is settled. A subgenre belonging to the genre chosen last
561 // time is no answer to the question being asked now, and falls back to
562 // every subgenre of the genre chosen this time.
563 let genres: Vec<Genre> = ctx.optional_rows(GENRES_FILE)?;
564 let mut named: Vec<&str> = genres.iter().map(|g| g.genre.as_str()).collect();
565 named.sort_unstable();
566 named.dedup();
567
568 let chosen = asked.get_or("genre", "");
569 let mut subgenres: Vec<&str> = genres
570 .iter()
571 .filter(|g| g.genre == chosen)
572 .map(|g| g.subgenre.as_str())
573 .collect();
574 subgenres.sort_unstable();
575
576 Ok(vec![
577 Param::select("branch", "Branch", options).default(first),
578 Param::select("genre", "Genre", Self::any("Every genre", named)).default(""),
579 Param::select(
580 "subgenre",
581 "Subgenre",
582 Self::any("Every subgenre", subgenres),
583 )
584 .default(""),
585 Param::string("author", "Author"),
586 ])
587 }
588
589 fn render(&self, args: &ViewArgs, ctx: &Context) -> Result<ViewData, ApiError> {
590 let books: Vec<Book> = ctx.optional_rows(BOOKS_FILE)?;
591 let branches = Self::branches(ctx)?;
592 let code = args.get_or("branch", "");
593 let branch = branches.iter().find(|b| b.code == code);
594 let named = branch.map(|b| b.name.as_str()).unwrap_or(code);
595
596 let genre = args.get_or("genre", "");
597 let subgenre = args.get_or("subgenre", "");
598 let author = args.get_or("author", "").trim().to_lowercase();
599
600 // The books this branch holds that answer the rest of the question,
601 // whether they are out or not. A parameter left empty asks nothing.
602 let held: Vec<&Book> = books
603 .iter()
604 .filter(|b| b.shelved.contains_key(code))
605 .filter(|b| genre.is_empty() || b.genre == genre)
606 .filter(|b| subgenre.is_empty() || b.subgenre == subgenre)
607 .filter(|b| {
608 author.is_empty()
609 || format!("{} {}", b.author_first, b.author_last)
610 .to_lowercase()
611 .contains(&author)
612 })
613 .collect();
614
615 let now = today();
616 let mut out = Vec::new();
617 let mut overdue = Vec::new();
618 for book in held.iter().filter(|b| b.lent.unwrap_or(false)) {
619 let days = days_from_civil(&book.due).map(|due| now - due).unwrap_or(0);
620 if days > 0 {
621 overdue.push(OnLoan::row(book, days));
622 } else {
623 out.push(OnLoan::row(book, days));
624 }
625 }
626
627 let lent = out.len() + overdue.len();
628 Ok(ViewData::new()
629 .note(format!(
630 "{lent} of {} book(s) at {named} are out on loan. A title links to its catalogue entry.",
631 held.len()
632 ))
633 .section(
634 Section::new(OnLoan::columns())
635 .heading("Out")
636 .note("Days left before they are due.")
637 .rows(out)?,
638 )
639 .section(
640 Section::new(OnLoan::columns())
641 .heading("Overdue")
642 .note("Days past due. Chase these.")
643 .rows(overdue)?,
644 ))
645 }
646}
647
648// ── The Branches view, which is a card per branch ────────────────────────────
649
650/// Every branch as a card, grouped by whether it is open.
651///
652/// A card is what a view answers with when the question is "how do these things
653/// stand" rather than "what are the values of these fields": there are no
654/// columns here, and each card says how one branch stands, what it is called,
655/// and two counts, with the whole card a link to the branch's own page.
656struct BranchCards;
657
658/// How many of the books a branch holds, and how many of those are out.
659fn shelf_counts(books: &[Book], code: &str) -> (usize, usize) {
660 let here: Vec<&Book> = books
661 .iter()
662 .filter(|b| b.shelved.contains_key(code))
663 .collect();
664 let out = here.iter().filter(|b| b.lent.unwrap_or(false)).count();
665 (here.len(), out)
666}
667
668fn librarian(branch: &Branch) -> String {
669 format!("{} {}", branch.librarian_first, branch.librarian_last)
670 .trim()
671 .to_string()
672}
673
674impl ViewLogic for BranchCards {
675 fn name(&self) -> &'static str {
676 "all-branches"
677 }
678
679 fn title(&self) -> &'static str {
680 "All branches"
681 }
682
683 fn render(&self, _args: &ViewArgs, ctx: &Context) -> Result<ViewData, ApiError> {
684 let branches: Vec<Branch> = ctx.optional_rows(BRANCHES_FILE)?;
685 let books: Vec<Book> = ctx.optional_rows(BOOKS_FILE)?;
686
687 let card = |branch: &Branch| {
688 let (here, out) = shelf_counts(&books, &branch.code);
689 let open = branch.open.unwrap_or(false);
690 let mut card = Card::new(branch.name.as_str())
691 .status(if open {
692 Status::new("Open", Tone::Good)
693 } else {
694 // A shut branch is a fact about it rather than a fault, so
695 // it is neutral—which is also what reads quieter.
696 Status::new("Shut", Tone::Neutral)
697 })
698 .identifier(branch.code.as_str())
699 .subtitle(format!(
700 "{}, {} staff",
701 librarian(branch),
702 branch.staff.unwrap_or(0)
703 ))
704 .row("Books here", here)
705 .row("Out on loan", out)
706 .link(ViewLink::new("branch").arg("branch", &branch.code));
707 if branch.hours.is_empty() {
708 card = card.sentence(
709 "No opening hours are recorded, so nothing here says when it can be visited.",
710 );
711 }
712 card
713 };
714
715 let group = |heading: &str, open: bool| {
716 CardGroup::new(heading).cards(
717 branches
718 .iter()
719 .filter(|b| b.open.unwrap_or(false) == open)
720 .map(&card),
721 )
722 };
723
724 Ok(ViewData::new()
725 .note("A card is a branch. Open one for what it holds and what is out.")
726 .group(group("Open", true))
727 .group(group("Shut", false)))
728 }
729}
730
731// ── The Branch view, which is one branch in detail ───────────────────────────
732
733/// One branch: what it has out, what is on its shelves, and when it is open.
734///
735/// It is reached from a card rather than from the switcher, so its one
736/// parameter is hidden: the page draws no control for it, and the card's link
737/// is what answers it. The parameter is a select all the same, so a link to a
738/// branch that has since gone falls back to the first one rather than to a page
739/// about nothing.
740struct BranchDetail;
741
742impl BranchDetail {
743 /// The books a branch holds, best rated first, which is the order the
744 /// numbered section is a ranking in.
745 fn by_rating<'a>(books: &'a [Book], code: &str, lent: bool) -> Vec<&'a Book> {
746 let mut held: Vec<&Book> = books
747 .iter()
748 .filter(|b| b.shelved.contains_key(code) && b.lent.unwrap_or(false) == lent)
749 .collect();
750 held.sort_by(|a, b| {
751 b.rating
752 .unwrap_or(0.0)
753 .total_cmp(&a.rating.unwrap_or(0.0))
754 .then_with(|| a.title.cmp(&b.title))
755 });
756 held
757 }
758
759 /// What a form asks before a book goes out. The four kinds of field are all
760 /// here, since this is the page the bundle's form controls are developed
761 /// against.
762 fn loan_form(title: &str) -> Form {
763 Form::new("lend-a-book")
764 .arg("title", title)
765 .field(Field::text("borrower", "Borrower"))
766 .field(Field::number("days", "Days out").default(21))
767 .field(Field::date("from", "Date lent").default(civil_from_days(today())))
768 .field(
769 Field::one_of(
770 "condition",
771 "Condition it left in",
772 ["As new", "Good", "Worn"],
773 )
774 .default("Good"),
775 )
776 }
777}
778
779impl ViewLogic for BranchDetail {
780 fn name(&self) -> &'static str {
781 "branch"
782 }
783
784 fn title(&self) -> &'static str {
785 "Branch"
786 }
787
788 /// This page is about one branch, and which one arrives through a card's
789 /// link. A switcher entry for it would open whichever branch the parameter
790 /// happens to default to, which is nobody's question.
791 fn in_switcher(&self) -> bool {
792 false
793 }
794
795 fn params(&self, ctx: &Context, _asked: &ViewArgs) -> Result<Vec<Param>, ApiError> {
796 let branches: Vec<Branch> = ctx.optional_rows(BRANCHES_FILE)?;
797 let codes: Vec<SelectOption> = branches
798 .iter()
799 .map(|b| SelectOption::labelled(&b.code, &b.name))
800 .collect();
801 let first = branches.first().map(|b| b.code.clone()).unwrap_or_default();
802 Ok(vec![
803 Param::select("branch", "Branch", codes)
804 .default(first)
805 .hidden(),
806 ])
807 }Trait Implementations§
Source§impl Clone for SelectOption
impl Clone for SelectOption
Source§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