| 83 |
83 |
|
|
| 84 |
84 |
|
use makeover_layout as layout;
|
| 85 |
85 |
|
use quasi_declare::declare;
|
| 86 |
|
- |
use quasi_router::screen::{Rest, Tag};
|
| 87 |
|
- |
use quasi_router::{Action, Document, Node, RegionKind, Request, Response, RouteError, Slot};
|
|
86 |
+ |
use quasi_router::screen::{Jump, Rest, Tag};
|
|
87 |
+ |
use quasi_router::{Action, Document, Node, RegionKind, RouteError, Slot};
|
| 88 |
88 |
|
use quasi_webview::Webview;
|
| 89 |
89 |
|
|
| 90 |
90 |
|
use crate::constants;
|
| 139 |
139 |
|
pub showing_end: u32,
|
| 140 |
140 |
|
}
|
| 141 |
141 |
|
|
|
142 |
+ |
/// One page the strip offers, and whether it is the one being read.
|
|
143 |
+ |
///
|
|
144 |
+ |
/// `here` is carried per row rather than worked out by comparing each page
|
|
145 |
+ |
/// against the current one. `quasi_router::Jump::here` has the reason: a strip
|
|
146 |
+ |
/// is a loop, and a residual holds one compiled body per loop, so "exactly one
|
|
147 |
+ |
/// row differs" is not a property of the body when the difference is a
|
|
148 |
+ |
/// comparison the body does not make.
|
|
149 |
+ |
pub struct Offered {
|
|
150 |
+ |
/// Which page, counting from one.
|
|
151 |
+ |
pub page: usize,
|
|
152 |
+ |
/// Whether it is the one being read.
|
|
153 |
+ |
pub here: bool,
|
|
154 |
+ |
}
|
|
155 |
+ |
|
|
156 |
+ |
impl Page<'_> {
|
|
157 |
+ |
/// Where this page starts, which is what a `Rest` counts from.
|
|
158 |
+ |
///
|
|
159 |
+ |
/// Suppliers rather than expressions because the declared form has no
|
|
160 |
+ |
/// arithmetic, and these are the same sums [`load`] makes.
|
|
161 |
+ |
fn offset(&self) -> usize {
|
|
162 |
+ |
(self.current_page as usize - 1) * constants::FEED_PAGE_SIZE as usize
|
|
163 |
+ |
}
|
|
164 |
+ |
|
|
165 |
+ |
fn per(&self) -> usize {
|
|
166 |
+ |
constants::FEED_PAGE_SIZE as usize
|
|
167 |
+ |
}
|
|
168 |
+ |
|
|
169 |
+ |
fn total(&self) -> usize {
|
|
170 |
+ |
self.total_items as usize
|
|
171 |
+ |
}
|
|
172 |
+ |
|
|
173 |
+ |
fn previous(&self) -> u32 {
|
|
174 |
+ |
self.current_page.saturating_sub(1)
|
|
175 |
+ |
}
|
|
176 |
+ |
|
|
177 |
+ |
fn next(&self) -> u32 {
|
|
178 |
+ |
self.current_page + 1
|
|
179 |
+ |
}
|
|
180 |
+ |
|
|
181 |
+ |
fn has_previous(&self) -> bool {
|
|
182 |
+ |
self.current_page > 1
|
|
183 |
+ |
}
|
|
184 |
+ |
|
|
185 |
+ |
fn has_next(&self) -> bool {
|
|
186 |
+ |
self.current_page < self.total_pages
|
|
187 |
+ |
}
|
|
188 |
+ |
|
|
189 |
+ |
/// The pages the strip offers, marked. The window is the handler's, which
|
|
190 |
+ |
/// is `build_pagination_range`; nothing here windows anything.
|
|
191 |
+ |
fn offered(&self) -> Vec<Offered> {
|
|
192 |
+ |
self.pagination_range
|
|
193 |
+ |
.iter()
|
|
194 |
+ |
.map(|at| Offered {
|
|
195 |
+ |
page: *at as usize,
|
|
196 |
+ |
here: *at == self.current_page,
|
|
197 |
+ |
})
|
|
198 |
+ |
.collect()
|
|
199 |
+ |
}
|
|
200 |
+ |
}
|
|
201 |
+ |
|
| 142 |
202 |
|
/// The library's Feed panel, in its region, for the tab route to answer with.
|
| 143 |
203 |
|
#[must_use]
|
| 144 |
204 |
|
pub fn library_fragment(page: &Page<'_>) -> String {
|
| 145 |
205 |
|
use quasi_axum::Serves as _;
|
| 146 |
206 |
|
|
| 147 |
207 |
|
let mut slot = Slot::new(LIBRARY_REGION, RegionKind::Pane);
|
| 148 |
|
- |
for node in body(page, Surface::Panel) {
|
|
208 |
+ |
for node in panel_body(page) {
|
| 149 |
209 |
|
slot = slot.with(node);
|
| 150 |
210 |
|
}
|
| 151 |
211 |
|
Webview::new().fragment(&Node::Region(slot))
|
| 240 |
300 |
|
})
|
| 241 |
301 |
|
}
|
| 242 |
302 |
|
|
| 243 |
|
- |
/// The public feed page, described.
|
| 244 |
|
- |
pub fn screen(viewer: &super::Viewer, request: Request) -> Result<Response, RouteError> {
|
| 245 |
|
- |
// Moved out of the request rather than borrowed: the signature is quasi's,
|
| 246 |
|
- |
// so the request arrives owned and nothing else here reads it.
|
| 247 |
|
- |
let carried = request.carried;
|
|
303 |
+ |
/// The one read this page makes, for the mount that serves it from a residual.
|
|
304 |
+ |
///
|
|
305 |
+ |
/// Answers `Loaded` rather than `Page`, because a `Page` borrows it: the mount
|
|
306 |
+ |
/// holds this and takes the borrow twice, once to state the document and once
|
|
307 |
+ |
/// to fill the markup, from the same read.
|
|
308 |
+ |
pub(crate) fn reading(
|
|
309 |
+ |
viewer: &super::Viewer,
|
|
310 |
+ |
carried: &super::Carried,
|
|
311 |
+ |
) -> Result<Loaded, RouteError> {
|
| 248 |
312 |
|
let asked = carried
|
| 249 |
|
- |
.get("page")
|
|
313 |
+ |
.asked("page")
|
| 250 |
314 |
|
.and_then(|value| value.trim().parse::<u32>().ok());
|
| 251 |
|
- |
let loaded = viewer
|
|
315 |
+ |
viewer
|
| 252 |
316 |
|
.block_on(load(&viewer.app.db, viewer.reader()?.id, asked))
|
| 253 |
|
- |
.map_err(|_| RouteError::internal("your feed could not be read"))?;
|
| 254 |
|
- |
Ok(page_screen(&loaded.page()).into())
|
|
317 |
+ |
.map_err(|_| RouteError::internal("your feed could not be read"))
|
| 255 |
318 |
|
}
|
| 256 |
319 |
|
|
| 257 |
320 |
|
declare! {
|
| 260 |
323 |
|
/// The body arrives as a list of members and is spread into the pane one at
|
| 261 |
324 |
|
/// a time, which is the same loop both callers wrote by hand: a fill has no
|
| 262 |
325 |
|
/// region of its own, so nothing can place it whole.
|
| 263 |
|
- |
shape page_screen(page: &Page<'_>) -> Screen;
|
|
326 |
+ |
pub(crate) shape page_screen(page: &Page<'_>) -> Screen;
|
| 264 |
327 |
|
|
| 265 |
328 |
|
screen single "Feed - Makenotwork" {
|
| 266 |
329 |
|
measured MEASURE;
|
| 270 |
333 |
|
documented Document::default().classed(crate::shell::body_class(MEASURE, &["feed-page"]));
|
| 271 |
334 |
|
summarised "Items from the users, projects and tags you follow.";
|
| 272 |
335 |
|
|
| 273 |
|
- |
region PAGE_REGION as Pane {
|
| 274 |
|
- |
page "Your Feed";
|
| 275 |
|
- |
include each body(page, Surface::Page);
|
| 276 |
|
- |
}
|
|
336 |
+ |
include page_region(page);
|
|
337 |
+ |
}
|
|
338 |
+ |
}
|
|
339 |
+ |
|
|
340 |
+ |
declare! {
|
|
341 |
+ |
/// The page's one region, split out so it can be staged.
|
|
342 |
+ |
#[staged]
|
|
343 |
+ |
pub(crate) shape page_region(page: &Page<'_>) -> Slot;
|
|
344 |
+ |
|
|
345 |
+ |
region PAGE_REGION as Pane {
|
|
346 |
+ |
page "Your Feed";
|
|
347 |
+ |
include each page_body(page);
|
| 277 |
348 |
|
}
|
| 278 |
349 |
|
}
|
| 279 |
350 |
|
|
| 297 |
368 |
|
///
|
| 298 |
369 |
|
/// The only thing that differs between them, so it is one value rather than two
|
| 299 |
370 |
|
/// copies of the body. See the module header.
|
| 300 |
|
- |
#[derive(Clone, Copy)]
|
| 301 |
|
- |
enum Surface {
|
| 302 |
|
- |
/// The library tab panel: a page control swaps the panel.
|
| 303 |
|
- |
Panel,
|
| 304 |
|
- |
/// The public page: a page control replaces the document.
|
| 305 |
|
- |
Page,
|
| 306 |
|
- |
}
|
| 307 |
|
- |
|
| 308 |
|
- |
impl Surface {
|
| 309 |
|
- |
/// What going to `page` calls, on this surface.
|
| 310 |
|
- |
fn address(self, page: u32) -> Action {
|
| 311 |
|
- |
match self {
|
| 312 |
|
- |
Self::Panel => Action::get(format!("{LIBRARY_ROUTE}?page={page}"))
|
| 313 |
|
- |
.awaiting()
|
| 314 |
|
- |
.replacing(LIBRARY_REGION),
|
| 315 |
|
- |
Self::Page => Action::get(format!("{PATH}?page={page}")).navigating(),
|
| 316 |
|
- |
}
|
| 317 |
|
- |
}
|
|
371 |
+ |
/// What going to `page` calls on the library panel: a swap of the panel, in
|
|
372 |
+ |
/// place, leaving the rest of the library where it is.
|
|
373 |
+ |
///
|
|
374 |
+ |
/// The page surface's answer is not here. It is written in `page_table`'s own
|
|
375 |
+ |
/// `more` body, because that surface is served from a residual and a supplier
|
|
376 |
+ |
/// handing over a whole `Action` is markup the derivation cannot see into. See
|
|
377 |
+ |
/// the note on `panel_table`.
|
|
378 |
+ |
fn panel_address(page: u32) -> Action {
|
|
379 |
+ |
Action::get(format!("{LIBRARY_ROUTE}?page={page}"))
|
|
380 |
+ |
.awaiting()
|
|
381 |
+ |
.replacing(LIBRARY_REGION)
|
| 318 |
382 |
|
}
|
| 319 |
383 |
|
|
| 320 |
384 |
|
declare! {
|
| 321 |
|
- |
/// The surface's contents, in order.
|
| 322 |
|
- |
shape body(page: &Page<'_>, surface: Surface) -> Vec<Node>;
|
|
385 |
+ |
/// The library panel's contents, in order.
|
|
386 |
+ |
///
|
|
387 |
+ |
/// One of a pair with [`page_body`], and the pair is what the surface split
|
|
388 |
+ |
/// costs. Everything the two surfaces say the same way is said once --
|
|
389 |
+ |
/// the empty state, the count, every row and every cell. What they cannot
|
|
390 |
+ |
/// share is the pager: a page control on the panel swaps the panel in place
|
|
391 |
+ |
/// and one on the page navigates, which is different markup rather than a
|
|
392 |
+ |
/// different address, so it cannot be a value either surface hands over.
|
|
393 |
+ |
/// See `panel_table`.
|
|
394 |
+ |
shape panel_body(page: &Page<'_>) -> Vec<Node>;
|
| 323 |
395 |
|
|
| 324 |
396 |
|
include empty() when page.items.is_empty();
|
| 325 |
397 |
|
|
| 326 |
398 |
|
text "Showing {page.showing_start}-{page.showing_end} of {page.total_items} items"
|
| 327 |
399 |
|
unless page.items.is_empty();
|
| 328 |
|
- |
include table(page, surface) unless page.items.is_empty();
|
|
400 |
+ |
include panel_table(page) unless page.items.is_empty();
|
|
401 |
+ |
}
|
|
402 |
+ |
|
|
403 |
+ |
declare! {
|
|
404 |
+ |
/// The public page's contents, in order. See [`panel_body`].
|
|
405 |
+ |
#[staged]
|
|
406 |
+ |
pub(crate) shape page_body(page: &Page<'_>) -> Vec<Node>;
|
|
407 |
+ |
|
|
408 |
+ |
include empty() when page.items.is_empty();
|
|
409 |
+ |
|
|
410 |
+ |
text "Showing {page.showing_start}-{page.showing_end} of {page.total_items} items"
|
|
411 |
+ |
unless page.items.is_empty();
|
|
412 |
+ |
include page_table(page) unless page.items.is_empty();
|
| 329 |
413 |
|
}
|
| 330 |
414 |
|
|
| 331 |
415 |
|
declare! {
|
| 336 |
420 |
|
/// `ui::empty_state_with_action` -- and said the same thing with the same
|
| 337 |
421 |
|
/// way out. One spelling now, which is one of the things converting both
|
| 338 |
422 |
|
/// surfaces together buys.
|
|
423 |
+ |
#[constant]
|
| 339 |
424 |
|
shape empty() -> Node;
|
| 340 |
425 |
|
|
| 341 |
426 |
|
empty "Nothing here yet. Follow users, projects, or tags to see their items here." {
|
| 344 |
429 |
|
}
|
| 345 |
430 |
|
|
| 346 |
431 |
|
declare! {
|
| 347 |
|
- |
/// The five columns, and the rows under them.
|
| 348 |
|
- |
shape table(page: &Page<'_>, surface: Surface) -> Node;
|
|
432 |
+ |
/// The five columns, and the rows under them, for the library panel.
|
|
433 |
+ |
///
|
|
434 |
+ |
/// # Why there are two of these
|
|
435 |
+ |
///
|
|
436 |
+ |
/// The column list is written twice, here and in [`page_table`], and that
|
|
437 |
+ |
/// is the price of the surface split rather than an oversight. What forced
|
|
438 |
+ |
/// it: a page control on the panel is
|
|
439 |
+ |
/// `Action::get(..).awaiting().replacing(LIBRARY_REGION)` and one on the
|
|
440 |
+ |
/// page is `Action::get(..).navigating()`, which is different MARKUP, not a
|
|
441 |
+ |
/// different address. A residual holds one markup per position, and the
|
|
442 |
+ |
/// page surface is served from one.
|
|
443 |
+ |
///
|
|
444 |
+ |
/// Saying the difference as guards on the action's modifiers was refused on
|
|
445 |
+ |
/// reading: three guards that have to agree is what wiki
|
|
446 |
+ |
/// `quasi-declare-form` section 24 calls one question written three times,
|
|
447 |
+ |
/// and a derivation varies them one at a time.
|
|
448 |
+ |
///
|
|
449 |
+ |
/// **The two lists are held together by a test rather than by hope.**
|
|
450 |
+ |
/// `the_two_surfaces_draw_the_same_columns` renders both and compares the
|
|
451 |
+ |
/// heading rows, so a column added to one and forgotten in the other fails
|
|
452 |
+ |
/// there. Everything below the headings -- every row, every cell -- is
|
|
453 |
+ |
/// [`row`], said once.
|
|
454 |
+ |
shape panel_table(page: &Page<'_>) -> Node;
|
| 349 |
455 |
|
|
| 350 |
456 |
|
table {
|
| 351 |
457 |
|
column "Type" {
|
| 369 |
475 |
|
include row(item);
|
| 370 |
476 |
|
}
|
| 371 |
477 |
|
|
| 372 |
|
- |
more rest(page, surface) when page.total_pages over 1;
|
|
478 |
+ |
more panel_rest(page) when page.total_pages over 1;
|
|
479 |
+ |
}
|
|
480 |
+ |
}
|
|
481 |
+ |
|
|
482 |
+ |
declare! {
|
|
483 |
+ |
/// The same five columns and rows, for the public page. See [`panel_table`].
|
|
484 |
+ |
///
|
|
485 |
+ |
/// The pager is described rather than supplied, which is what puts this
|
|
486 |
+ |
/// surface on the residual seam: a `Rest` handed over whole has no
|
|
487 |
+ |
/// sentinel, and everything a described one carries is a number or an
|
|
488 |
+ |
/// address. The directions are written back, then the strip, then forward,
|
|
489 |
+ |
/// because that is the order they draw in and `quasi-declare` holds this to
|
|
490 |
+ |
/// it.
|
|
491 |
+ |
#[staged]
|
|
492 |
+ |
pub(crate) shape page_table(page: &Page<'_>) -> Node;
|
|
493 |
+ |
|
|
494 |
+ |
table {
|
|
495 |
+ |
column "Type" {
|
|
496 |
+ |
width Content;
|
|
497 |
+ |
}
|
|
498 |
+ |
column "Name" {
|
|
499 |
+ |
width Fill;
|
|
500 |
+ |
priority Essential;
|
|
501 |
+ |
}
|
|
502 |
+ |
column "Tag" {
|
|
503 |
+ |
width Content;
|
|
504 |
+ |
}
|
|
505 |
+ |
column "Price" {
|
|
506 |
+ |
width Content;
|
|
507 |
+ |
}
|
|
508 |
+ |
column "Date" {
|
|
509 |
+ |
width Content;
|
|
510 |
+ |
}
|
|
511 |
+ |
|
|
512 |
+ |
for item in page.items.iter() {
|
|
513 |
+ |
include row(item);
|
|
514 |
+ |
}
|
|
515 |
+ |
|
|
516 |
+ |
more Rest::page(page.offset(), page.per()).of(page.total()) {
|
|
517 |
+ |
back Action::get("{PATH}?page={page.previous()}").navigating()
|
|
518 |
+ |
when page.has_previous();
|
|
519 |
+ |
// The strip, one control per page the handler windowed to. Which
|
|
520 |
+ |
// one the reader is on is a readout rather than a control, which is
|
|
521 |
+ |
// two markups at one position and is why `Op::Arms` had to exist.
|
|
522 |
+ |
for offered in page.offered().iter() {
|
|
523 |
+ |
jumping Jump::new(
|
|
524 |
+ |
offered.page,
|
|
525 |
+ |
Action::get("{PATH}?page={offered.page}").navigating()
|
|
526 |
+ |
) {
|
|
527 |
+ |
here when offered.here;
|
|
528 |
+ |
}
|
|
529 |
+ |
}
|
|
530 |
+ |
forward Action::get("{PATH}?page={page.next()}").navigating()
|
|
531 |
+ |
when page.has_next();
|
|
532 |
+ |
} when page.total_pages over 1;
|
| 373 |
533 |
|
}
|
| 374 |
534 |
|
}
|
| 375 |
535 |
|
|
| 394 |
554 |
|
/// checkable when both halves are in front of you, and here they never are.
|
| 395 |
555 |
|
/// The names must match [`table`]'s `column` strings exactly, since a name
|
| 396 |
556 |
|
/// no column carries is dropped rather than reported.
|
|
557 |
+ |
#[staged]
|
| 397 |
558 |
|
shape row(item: &DiscoverItem) -> Row;
|
| 398 |
559 |
|
|
| 399 |
560 |
|
cells {
|
| 406 |
567 |
|
text item.creator.clone();
|
| 407 |
568 |
|
}
|
| 408 |
569 |
|
cell at "Tag" item.primary_tag.clone();
|
| 409 |
|
- |
cell at "Price" price(item) {
|
| 410 |
|
- |
token Tag::badge("Free").tone(layout::Tone::Success) when item.is_free;
|
|
570 |
+ |
// One question with two shapes rather than two questions. A free item
|
|
571 |
+ |
// says so with a badge and a priced one reads its price, and the cell
|
|
572 |
+ |
// carries `cell-value` in the second case and not in the first -- so
|
|
573 |
+ |
// the two are different markup at one position, which is a dispatch.
|
|
574 |
+ |
//
|
|
575 |
+ |
// Said as two guarded cells it would be one question written twice
|
|
576 |
+ |
// (wiki `quasi-declare-form` section 24, rule one) and a derivation
|
|
577 |
+ |
// varies guards one at a time, so it would render a Price column
|
|
578 |
+ |
// holding two cells. quasicoherent `cbb63155`.
|
|
579 |
+ |
given item.is_free {
|
|
580 |
+ |
true -> cell at "Price" "" {
|
|
581 |
+ |
token Tag::badge("Free").tone(layout::Tone::Success);
|
|
582 |
+ |
}
|
|
583 |
+ |
otherwise -> cell at "Price" price(item);
|
| 411 |
584 |
|
}
|
| 412 |
585 |
|
cell at "Date" item.date.clone();
|
| 413 |
586 |
|
|
| 417 |
590 |
|
|
| 418 |
591 |
|
/// What the reader has not been shown, and every way to ask for it.
|
| 419 |
592 |
|
///
|
|
593 |
+ |
/// The panel's, and the panel's alone. A supplier is the right shape here
|
|
594 |
+ |
/// because this surface is not on the residual seam: it builds nodes per
|
|
595 |
+ |
/// request, so handing the renderer a whole `Rest` costs nothing. The page
|
|
596 |
+ |
/// surface describes its pager instead, in `page_table`.
|
|
597 |
+ |
///
|
| 420 |
598 |
|
/// The table asks for this only when there is more than one page, which is what
|
| 421 |
599 |
|
/// `{% if total_pages > 1 %}` said: a set that arrived whole has no rest, and a
|
| 422 |
|
- |
/// pager drawn over one would be two disabled buttons and the number 1. R9 means
|
| 423 |
|
- |
/// this is still called on a single-page feed, and the answer is thrown away.
|
| 424 |
|
- |
fn rest(page: &Page<'_>, surface: Surface) -> Rest {
|
| 425 |
|
- |
let per = constants::FEED_PAGE_SIZE as usize;
|
| 426 |
|
- |
let from = (page.current_page as usize).saturating_sub(1) * per;
|
| 427 |
|
- |
let mut rest = Rest::page(from, per).of(page.total_items as usize);
|
|
600 |
+ |
/// pager drawn over one would be one control and the number 1. R9 means this is
|
|
601 |
+ |
/// still called on a single-page feed, and the answer is thrown away.
|
|
602 |
+ |
fn panel_rest(page: &Page<'_>) -> Rest {
|
|
603 |
+ |
let mut rest = Rest::page(page.offset(), page.per()).of(page.total());
|
| 428 |
604 |
|
|
| 429 |
|
- |
if page.current_page > 1 {
|
| 430 |
|
- |
rest = rest.back(surface.address(page.current_page - 1));
|
|
605 |
+ |
if page.has_previous() {
|
|
606 |
+ |
rest = rest.back(panel_address(page.previous()));
|
| 431 |
607 |
|
}
|
| 432 |
|
- |
if page.current_page < page.total_pages {
|
| 433 |
|
- |
rest = rest.forward(surface.address(page.current_page + 1));
|
|
608 |
+ |
if page.has_next() {
|
|
609 |
+ |
rest = rest.forward(panel_address(page.next()));
|
| 434 |
610 |
|
}
|
| 435 |
611 |
|
// Which page the reader is on is carried per jump rather than compared
|
| 436 |
612 |
|
// against the paging inside each renderer. `Jump::here`'s reason is
|
| 437 |
613 |
|
// `Choice::chosen`'s: a strip is a loop, and a residual holds one compiled
|
| 438 |
614 |
|
// body per loop.
|
| 439 |
|
- |
for jump in page.pagination_range {
|
| 440 |
|
- |
let at = *jump as usize;
|
| 441 |
|
- |
let jumping = quasi_router::screen::Jump::new(at, surface.address(*jump));
|
| 442 |
|
- |
rest = rest.jumping(if at == page.current_page as usize {
|
|
615 |
+ |
for offered in page.offered() {
|
|
616 |
+ |
let jumping =
|
|
617 |
+ |
quasi_router::screen::Jump::new(offered.page, panel_address(offered.page as u32));
|
|
618 |
+ |
rest = rest.jumping(if offered.here {
|
| 443 |
619 |
|
jumping.here()
|
| 444 |
620 |
|
} else {
|
| 445 |
621 |
|
jumping
|
| 449 |
625 |
|
rest
|
| 450 |
626 |
|
}
|
| 451 |
627 |
|
|
|
628 |
+ |
/// Two items, one priced and one free, as the tests draw them.
|
|
629 |
+ |
///
|
|
630 |
+ |
/// Module-level rather than inside `mod tests` because `quasi::residuals` needs
|
|
631 |
+ |
/// them too, and the price cell's two shapes are exactly what its filling test
|
|
632 |
+ |
/// is crossing. Test-only.
|
|
633 |
+ |
#[cfg(test)]
|
|
634 |
+ |
pub(crate) fn sample_items() -> Vec<DiscoverItem> {
|
|
635 |
+ |
vec![tests::item("itm_1", false), tests::item("itm_2", true)]
|
|
636 |
+ |
}
|
|
637 |
+ |
|
| 452 |
638 |
|
#[cfg(test)]
|
| 453 |
639 |
|
mod tests {
|
| 454 |
640 |
|
use super::*;
|
| 455 |
641 |
|
|
| 456 |
|
- |
fn item(id: &str, free: bool) -> DiscoverItem {
|
|
642 |
+ |
pub(super) fn item(id: &str, free: bool) -> DiscoverItem {
|
| 457 |
643 |
|
DiscoverItem {
|
| 458 |
644 |
|
id: id.to_string(),
|
| 459 |
645 |
|
name: format!("Item {id}"),
|
| 694 |
880 |
|
assert!(!html.contains("page-title"), "{html}");
|
| 695 |
881 |
|
}
|
| 696 |
882 |
|
|
| 697 |
|
- |
/// No `?page=` grammar survives in a template: both addresses are built
|
| 698 |
|
- |
/// here, from one function, on the surface that reads them.
|
|
883 |
+ |
/// No `?page=` grammar survives in a template: each surface builds its own
|
|
884 |
+ |
/// addresses in one place, and neither is a template's.
|
| 699 |
885 |
|
#[test]
|
| 700 |
|
- |
fn the_page_grammar_lives_in_one_place() {
|
|
886 |
+ |
fn the_page_grammar_lives_in_one_place_per_surface() {
|
| 701 |
887 |
|
assert!(
|
| 702 |
|
- |
Surface::Panel
|
| 703 |
|
- |
.address(3)
|
|
888 |
+ |
panel_address(3)
|
| 704 |
889 |
|
.destination
|
| 705 |
890 |
|
.route()
|
| 706 |
891 |
|
.is_some_and(|route| route == "/library/tabs/feed?page=3")
|
| 707 |
892 |
|
);
|
| 708 |
|
- |
assert!(
|
| 709 |
|
- |
Surface::Page
|
| 710 |
|
- |
.address(3)
|
| 711 |
|
- |
.destination
|
| 712 |
|
- |
.route()
|
| 713 |
|
- |
.is_some_and(|route| route == "/feed?page=3")
|
| 714 |
|
- |
);
|
|
893 |
+ |
|
|
894 |
+ |
// The page surface's is in `page_table`'s own `more` body, so it is read
|
|
895 |
+ |
// out of the markup rather than out of a function.
|
|
896 |
+ |
let html = public(3, 8);
|
|
897 |
+ |
assert!(html.contains("href=\"/feed?page=2\""), "{html}");
|
|
898 |
+ |
assert!(html.contains("href=\"/feed?page=4\""), "{html}");
|
|
899 |
+ |
}
|
|
900 |
+ |
|
|
901 |
+ |
/// The two surfaces draw the same columns, which is what holds the split
|
|
902 |
+ |
/// tables together.
|
|
903 |
+ |
///
|
|
904 |
+ |
/// `panel_table` and `page_table` repeat the column list, because their
|
|
905 |
+ |
/// pagers are different markup and a residual holds one. Nothing in the
|
|
906 |
+ |
/// compiler pairs the two lists, so this does: a column added to one and
|
|
907 |
+ |
/// forgotten in the other changes one heading row and not the other.
|
|
908 |
+ |
#[test]
|
|
909 |
+ |
fn the_two_surfaces_draw_the_same_columns() {
|
|
910 |
+ |
use quasi_axum::Serves as _;
|
|
911 |
+ |
|
|
912 |
+ |
fn headings(html: &str) -> Vec<&str> {
|
|
913 |
+ |
html.match_indices("columnheader")
|
|
914 |
+ |
.map(|(at, _)| {
|
|
915 |
+ |
let rest = &html[at..];
|
|
916 |
+ |
let from = rest.find('>').map_or(0, |at| at + 1);
|
|
917 |
+ |
let to = rest[from..].find('<').map_or(0, |at| at + from);
|
|
918 |
+ |
&rest[from..to]
|
|
919 |
+ |
})
|
|
920 |
+ |
.collect()
|
|
921 |
+ |
}
|
|
922 |
+ |
|
|
923 |
+ |
let (items, range) = page(4);
|
|
924 |
+ |
let held = Page {
|
|
925 |
+ |
items: &items,
|
|
926 |
+ |
total_items: 40,
|
|
927 |
+ |
current_page: 1,
|
|
928 |
+ |
total_pages: 4,
|
|
929 |
+ |
pagination_range: &range,
|
|
930 |
+ |
showing_start: 1,
|
|
931 |
+ |
showing_end: 20,
|
|
932 |
+ |
};
|
|
933 |
+ |
let panel = Webview::new().fragment(&panel_table(&held));
|
|
934 |
+ |
let public = Webview::new().fragment(&page_table(&held));
|
|
935 |
+ |
|
|
936 |
+ |
assert_eq!(headings(&panel), headings(&public));
|
|
937 |
+ |
assert_eq!(headings(&panel).len(), 5, "{panel}");
|
| 715 |
938 |
|
}
|
| 716 |
939 |
|
}
|