pub struct Schema { /* private fields */ }Expand description
One table’s presentation: the columns, how a new row starts, and any completion lists the columns draw on.
The table’s route segment and heading are not set here. They come from the
table’s own name and title, which the server fills in on the way out, so
the two cannot disagree.
Implementations§
Source§impl Schema
impl Schema
Sourcepub fn new(columns: impl IntoIterator<Item = Column>) -> Self
pub fn new(columns: impl IntoIterator<Item = Column>) -> Self
Examples found in repository?
examples/library/main.rs (lines 166-222)
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 }Sourcepub fn sortable(self) -> Self
pub fn sortable(self) -> Self
Let the browser sort the view by any column. This is a view setting only: writes always send rows in their stored order, and drag reordering is disabled while a sort is active. Leave it off on a table whose row order is itself meaningful.
Examples found in repository?
examples/library/main.rs (line 223)
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 }Sourcepub fn new_row(self, new_row: NewRow) -> Self
pub fn new_row(self, new_row: NewRow) -> Self
Examples found in repository?
examples/library/main.rs (lines 229-245)
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 }Sourcepub fn datalist(self, name: impl Into<String>, list: Datalist) -> Self
pub fn datalist(self, name: impl Into<String>, list: Datalist) -> 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 }
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 StructuralPartialEq for Schema
Auto Trait Implementations§
impl Freeze for Schema
impl RefUnwindSafe for Schema
impl Send for Schema
impl Sync for Schema
impl Unpin for Schema
impl UnsafeUnpin for Schema
impl UnwindSafe for Schema
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