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 223)
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 }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 226)
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 }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