Skip to content

Commit 01a2574

Browse files
committed
Add test app to cover adoption
1 parent 3228cd9 commit 01a2574

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+2583
-37
lines changed

pnpm-lock.yaml

+297-37
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
const base = require('@warp-drive/internal-config/eslint/base.cjs');
2+
const ignore = require('@warp-drive/internal-config/eslint/ignore.cjs');
3+
const imports = require('@warp-drive/internal-config/eslint/imports.cjs');
4+
const isolation = require('@warp-drive/internal-config/eslint/isolation.cjs');
5+
const node = require('@warp-drive/internal-config/eslint/node.cjs');
6+
const parser = require('@warp-drive/internal-config/eslint/parser.cjs');
7+
const qunit = require('@warp-drive/internal-config/eslint/qunit.cjs');
8+
const typescript = require('@warp-drive/internal-config/eslint/typescript.cjs');
9+
10+
module.exports = {
11+
...parser.defaults(),
12+
...base.settings(),
13+
14+
plugins: [...base.plugins(), ...imports.plugins()],
15+
extends: [...base.extend()],
16+
rules: Object.assign(
17+
base.rules(),
18+
imports.rules(),
19+
isolation.rules({
20+
allowedImports: [
21+
'@ember/application',
22+
'@ember/debug',
23+
'@ember/routing/route',
24+
'@ember/service',
25+
'@glimmer/component',
26+
'@glimmer/tracking',
27+
],
28+
}),
29+
{}
30+
),
31+
32+
ignorePatterns: ignore.ignoreRules(),
33+
34+
overrides: [
35+
node.config(),
36+
node.defaults({
37+
files: ['./server/**/*.{js,ts}'],
38+
}),
39+
typescript.defaults(),
40+
qunit.defaults({
41+
files: ['tests/**/*.{js,ts}'],
42+
allowedImports: [],
43+
}),
44+
],
45+
};

tests/incremental-json-api/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Incremental JSON:API
2+
3+
Demonstrates the configuration we recommend for JSON:API applications

tests/incremental-json-api/app/app.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import Application from '@ember/application';
2+
3+
import loadInitializers from 'ember-load-initializers';
4+
5+
import config from './config/environment';
6+
import Resolver from './resolver';
7+
8+
class App extends Application {
9+
modulePrefix = config.modulePrefix;
10+
podModulePrefix = config.podModulePrefix;
11+
override Resolver = Resolver;
12+
}
13+
14+
loadInitializers(App, config.modulePrefix);
15+
16+
export default App;

tests/incremental-json-api/app/components/.gitkeep

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<div>
2+
{{#if this.books.data}}
3+
<ul>
4+
{{#each this.books.data as |book|}}
5+
<li>
6+
<strong>{{book.title}}</strong> ({{book.genre}}) - {{book.publicationDate}}
7+
<br> by <em>{{book.author}}</em> ISBN: {{book.isbn}}
8+
</li>
9+
{{/each}}
10+
</ul>
11+
<div>
12+
{{#each this.links.filteredPages as |page|}}
13+
<PageLink class={{if (eq page.link this.books.links.self) 'active'}} @title="page {{page.index}}" @link={{page.link}} @text="{{page.index}}" @action={{this.updatePage}} />
14+
{{#if (mod page.index 11)}}
15+
<br>
16+
{{/if}}
17+
{{/each}}
18+
</div>
19+
{{else}}
20+
<p>No books found</p>
21+
{{/if}}
22+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { service } from '@ember/service';
2+
import Component from '@glimmer/component';
3+
import { cached, tracked } from '@glimmer/tracking';
4+
5+
import { query } from '@ember-data/json-api/request';
6+
import { filterEmpty } from '@ember-data/request-utils';
7+
import type Store from '@ember-data/store';
8+
import type { Document } from '@ember-data/store/-private/document';
9+
10+
import type Book from '../models/book';
11+
import type { ApiPage } from '../utils/pagination-links';
12+
import { PaginationLinks } from '../utils/pagination-links';
13+
14+
export interface BookListSignature {
15+
Element: HTMLDivElement;
16+
Args: {
17+
sort: string | null;
18+
filter: string | null;
19+
genre: string | null;
20+
author: string | null;
21+
page: number | null;
22+
limit: number | null;
23+
};
24+
}
25+
26+
class AsyncContent<T> {
27+
@tracked content: T | undefined;
28+
}
29+
30+
export default class BookListComponent extends Component<BookListSignature> {
31+
@service declare store: Store;
32+
@tracked currentUrl: string | null = null;
33+
links = new PaginationLinks();
34+
dataWrapper = new AsyncContent<Document<Book[]>>();
35+
36+
// we use this to detect inbound data changes
37+
_firstPageOptions: { url: string } | null = null;
38+
39+
@cached
40+
get firstPageOptions(): { url: string } {
41+
const { sort, filter, genre, author, page, limit } = this.args;
42+
43+
const options = query('book', filterEmpty({ sort, filter, genre, author, page, limit }));
44+
this._firstPageOptions = options;
45+
return options;
46+
}
47+
48+
@cached
49+
get currentPage() {
50+
const _firstPageOptions = this._firstPageOptions;
51+
const firstPageOptions = this.firstPageOptions;
52+
const currentUrl = this.currentUrl;
53+
54+
// if the first page options changed, we need to fetch a new first page
55+
if (_firstPageOptions?.url !== firstPageOptions.url) {
56+
return this.fetchPage(firstPageOptions);
57+
}
58+
59+
return this.fetchPage(currentUrl ? { url: currentUrl } : firstPageOptions);
60+
}
61+
62+
get books(): Document<Book[]> | null {
63+
return this.currentPage.content || null;
64+
}
65+
66+
fetchPage(options: { url: string }) {
67+
const dataWrapper = this.dataWrapper;
68+
const future = this.store.request<Document<Book[]>>(options);
69+
70+
void future.then((books) => {
71+
dataWrapper.content = books.content;
72+
this.links.addPage(books.content as unknown as ApiPage);
73+
});
74+
75+
return dataWrapper;
76+
}
77+
78+
updatePage = (url: string) => {
79+
this.currentUrl = url;
80+
};
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<div>
2+
<label for="author">Author</label>
3+
<select name="author" id="author" {{on 'input' this.update}}>
4+
<option value="">--</option>
5+
{{#each @authors as |author|}}
6+
<option value={{author.name}} selected={{eq this.author author.name}}>{{author.name}}</option>
7+
{{/each}}
8+
</select>
9+
10+
<label for="genre">Genre</label>
11+
<select name="genre" id="genre" {{on 'input' this.update}}>
12+
<option value="">--</option>
13+
{{#each @genres as |genre|}}
14+
<option value={{genre.name}} selected={{eq this.genre genre.name}}>{{genre.name}}</option>
15+
{{/each}}
16+
</select>
17+
18+
<label for="title">Title Includes</label>
19+
<input type="text" name="title" id="title" placeholder="--" value={{this.filter}} {{on 'input' this.update}}/>
20+
21+
<br>
22+
23+
<label for="sort">Sort By</label>
24+
<select name="sort" id="sort" {{on 'input' this.update}}>
25+
<option value="">--</option>
26+
{{#each this.sortOptions as |sortOption|}}
27+
<option value={{sortOption}} selected={{eq this.sort sortOption}}>{{sortOption}}</option>
28+
{{/each}}
29+
</select>
30+
31+
<label for="sortDirection">Direction</label>
32+
<select name="sort direction" id="sortDirection" disabled={{not this.sort}} {{on 'input' this.update}}>
33+
{{#if this.sort}}
34+
<option value="asc" selected={{eq this.sortDirection "asc"}}>Ascending</option>
35+
<option value="desc" selected={{eq this.sortDirection "desc"}}>Descending</option>
36+
{{else}}
37+
<option value="">--</option>
38+
{{/if}}
39+
</select>
40+
41+
{{#if this.sort}}
42+
<label for="sort2">Then Sort By</label>
43+
<select name="secondary sort" id="sort2" {{on 'input' this.update}}>
44+
<option value="">--</option>
45+
{{#each this.sortOptions as |sortOption|}}
46+
<option value={{sortOption}} selected={{eq this.sort2 sortOption}}>{{sortOption}}</option>
47+
{{/each}}
48+
</select>
49+
50+
<label for="sort2Direction">Direction</label>
51+
<select name="secondary sort direction" id="sort2Direction" disabled={{not this.sort2}} {{on 'input' this.update}}>
52+
{{#if this.sort2}}
53+
<option value="asc" selected={{eq this.sort2Direction "asc"}}>Ascending</option>
54+
<option value="desc" selected={{eq this.sort2Direction "desc"}}>Descending</option>
55+
{{else}}
56+
<option value="">--</option>
57+
{{/if}}
58+
</select>
59+
{{/if}}
60+
61+
<hr>
62+
</div>
63+
<BookList @genre={{this.genre}} @author={{this.author}} @filter={{this.title}} @sort={{this.sortQuery}} />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { service } from '@ember/service';
2+
import Component from '@glimmer/component';
3+
import { cached, tracked } from '@glimmer/tracking';
4+
5+
import type Store from '@ember-data/store';
6+
7+
export interface BookSearchSignature {
8+
Element: HTMLDivElement;
9+
Args: null;
10+
}
11+
12+
type SearchKeys = 'sort2' | 'sort' | 'title' | 'genre' | 'author' | 'sortDirection' | 'sort2Direction';
13+
14+
export default class BookListComponent extends Component<BookSearchSignature> {
15+
@service declare store: Store;
16+
17+
@tracked sort: string | null = 'title';
18+
@tracked sort2: string | null = 'publicationDate';
19+
@tracked title: string | null = null;
20+
@tracked genre: string | null = null;
21+
@tracked author: string | null = null;
22+
@tracked sortDirection = 'asc';
23+
@tracked sort2Direction = 'asc';
24+
_lastSortDirection = 'asc';
25+
_lastSort2Direction = 'asc';
26+
27+
@cached
28+
get sortOptions() {
29+
return Object.keys(this.store.getSchemaDefinitionService().attributesDefinitionFor({ type: 'book' }));
30+
}
31+
32+
@cached
33+
get sortQuery() {
34+
const sort1 = this.sort ? `${this.sort}:${this.sortDirection}` : '';
35+
const sort2 = sort1 && this.sort2 ? `${this.sort2}:${this.sort2Direction}` : '';
36+
return sort2 ? `${sort1},${sort2}` : sort1;
37+
}
38+
39+
update = (event: InputEvent & { target: HTMLInputElement }) => {
40+
event.preventDefault();
41+
const name = event.target.id as SearchKeys;
42+
this[name] = event.target.value;
43+
44+
if (name === 'sort') {
45+
this.sortDirection =
46+
event.target.value === '' ? '' : this._lastSortDirection === '' ? 'asc' : this._lastSortDirection;
47+
this._lastSortDirection = this.sortDirection;
48+
}
49+
50+
if (name === 'sort2') {
51+
this.sort2Direction =
52+
event.target.value === '' ? '' : this._lastSort2Direction === '' ? 'asc' : this._lastSort2Direction;
53+
this._lastSort2Direction = this.sort2Direction;
54+
}
55+
};
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<VerticalCollection
2+
@items={{this.books}}
3+
@tagName="ul"
4+
@staticHeight={{true}}
5+
@estimateHeight={{60}}
6+
@bufferSize={{10}}
7+
@lastReached={{this.next}}
8+
@containerSelector=".scroll-container"
9+
as |book|
10+
>
11+
<li>
12+
<strong>{{book.title}}</strong> ({{book.genre}}) - {{book.publicationDate}}
13+
<br> by <em>{{book.author}}</em> ISBN: {{book.isbn}}
14+
</li>
15+
</VerticalCollection>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { service } from '@ember/service';
2+
import Component from '@glimmer/component';
3+
import { tracked } from '@glimmer/tracking';
4+
5+
import type Store from '@ember-data/store';
6+
import type { Document } from '@ember-data/store/-private/document';
7+
8+
import type Book from '../models/book';
9+
10+
export interface InfiniteBookSignature {
11+
Element: HTMLUListElement;
12+
Args: {
13+
allBooks: Document<Book[]>;
14+
};
15+
}
16+
17+
class Pages<T> {
18+
@tracked pages: Document<T[]>[] = [];
19+
@tracked data: T[] = [];
20+
21+
constructor(page: Document<T[]>) {
22+
this.pages = [page];
23+
this.data = page.data!.slice();
24+
}
25+
26+
addPage(page: Document<T[]>) {
27+
this.pages.push(page);
28+
this.data = this.data.concat(page.data!);
29+
}
30+
}
31+
32+
export default class InfiniteBookComponent extends Component<InfiniteBookSignature> {
33+
@service declare store: Store;
34+
pageCollection = new Pages(this.args.allBooks);
35+
36+
get books(): Book[] {
37+
return this.pageCollection.data;
38+
}
39+
40+
next = async () => {
41+
const page = this.pageCollection.pages.at(-1);
42+
const result = await page?.next();
43+
if (result) {
44+
this.pageCollection.addPage(result);
45+
}
46+
};
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{{#if (or (eq @link '.') (eq @link '...'))}}{{@link}}{{else}}
2+
<button ...attributes title="{{@title}}" type="button" {{on "click" (fn @action @link)}} disabled={{not @link}}>{{@text}}</button>
3+
{{/if}}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export default config;
2+
3+
/**
4+
* Type declarations for
5+
* import config from './config/environment'
6+
*
7+
* For now these need to be managed by the developer
8+
* since different ember addons can materialize new entries.
9+
*/
10+
declare const config: {
11+
environment: 'production' | 'development' | 'testing';
12+
modulePrefix: string;
13+
podModulePrefix: string;
14+
locationType: string;
15+
rootURL: string;
16+
apiCacheHardExpires: number;
17+
apiCacheSoftExpires: number;
18+
};

tests/incremental-json-api/app/helpers/.gitkeep

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function add(a: number, b: number): number {
2+
return a + b;
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function and(...args: unknown[]): boolean {
2+
return args.every(Boolean);
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function eq(a: unknown, b: unknown) {
2+
return a === b;
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function gt(a: number, b: number): boolean {
2+
return a > b;
3+
}

0 commit comments

Comments
 (0)