Skip to content

Commit

Permalink
feat(showcase): add elf usage for server state
Browse files Browse the repository at this point in the history
  • Loading branch information
quentinderoubaix committed May 12, 2024
1 parent 84b7415 commit 07807b5
Show file tree
Hide file tree
Showing 50 changed files with 866 additions and 41 deletions.
5 changes: 5 additions & 0 deletions apps/showcase/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@
"@design-factory/design-factory": "~17.1.0",
"@formatjs/intl-numberformat": "~8.10.0",
"@ng-bootstrap/ng-bootstrap": "^16.0.0",
"@ngneat/elf": "^2.5.1",
"@ngneat/elf-devtools": "^1.3.0",
"@ngneat/elf-entities": "^5.0.2",
"@ngneat/elf-persist-state": "^1.2.1",
"@ngneat/elf-requests": "^1.9.2",
"@ngrx/effects": "~17.2.0",
"@ngrx/entity": "~17.2.0",
"@ngrx/store": "~17.2.0",
Expand Down
1 change: 1 addition & 0 deletions apps/showcase/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const appRoutes: Routes = [
{path: 'run-app-locally', loadComponent: () => import('./run-app-locally/index').then((m) => m.RunAppLocallyComponent), title: 'Otter Showcase - Run App Locally'},
{path: 'sdk', loadComponent: () => import('./sdk/index').then((m) => m.SdkComponent), title: 'Otter Showcase - SDK'},
{path: 'placeholder', loadComponent: () => import('./placeholder/index').then((m) => m.PlaceholderComponent), title: 'Otter Showcase - Placeholder'},
{path: 'elf', loadComponent: () => import('./elf/index').then((m) => m.ElfComponent), title: 'Otter Showcase - Elf'},
{path: '**', redirectTo: '/home', pathMatch: 'full'}
];

Expand Down
3 changes: 2 additions & 1 deletion apps/showcase/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ export class AppComponent implements OnDestroy {
{
label: 'SDK',
links: [
{ url: '/sdk', label: 'Generator' }
{ url: '/sdk', label: 'Generator' },
{ url: '/elf', label: 'With Elf' }
]
}
];
Expand Down
4 changes: 2 additions & 2 deletions apps/showcase/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgbOffcanvasModule } from '@ng-bootstrap/ng-bootstrap';
import { EffectsModule } from '@ngrx/effects';
import { RuntimeChecks, StoreModule } from '@ngrx/store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
// import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { TranslateCompiler, TranslateModule } from '@ngx-translate/core';
import { ApplicationDevtoolsModule, OTTER_APPLICATION_DEVTOOLS_OPTIONS, prefersReducedMotion } from '@o3r/application';
import { ConfigurationDevtoolsModule, OTTER_CONFIGURATION_DEVTOOLS_OPTIONS } from '@o3r/configuration';
Expand Down Expand Up @@ -90,7 +90,7 @@ export function registerCustomComponents(): Map<string, any> {
BrowserAnimationsModule.withConfig({disableAnimations: prefersReducedMotion()}),
EffectsModule.forRoot([]),
StoreModule.forRoot({}, { runtimeChecks }),
StoreDevtoolsModule.instrument({ maxAge: 25, logOnly: !isDevMode() }),
// StoreDevtoolsModule.instrument({ maxAge: 25, logOnly: !isDevMode() }),
TranslateModule.forRoot({
loader: translateLoaderProvider,
compiler: {
Expand Down
3 changes: 3 additions & 0 deletions apps/showcase/src/app/elf/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# ElfComponent

the elf page
40 changes: 40 additions & 0 deletions apps/showcase/src/app/elf/elf.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { AsyncPipe } from '@angular/common';
import { AfterViewInit, ChangeDetectionStrategy, Component, QueryList, ViewChildren, ViewEncapsulation } from '@angular/core';
import { RouterLink } from '@angular/router';
import { O3rComponent } from '@o3r/core';
import {
CopyTextPresComponent,
ElfPresComponent,
IN_PAGE_NAV_PRES_DIRECTIVES,
InPageNavLink,
InPageNavLinkDirective,
InPageNavPresService
} from '../../components';

@O3rComponent({ componentType: 'Page' })
@Component({
selector: 'o3r-sdk',
standalone: true,
imports: [
CopyTextPresComponent,
RouterLink,
ElfPresComponent,
IN_PAGE_NAV_PRES_DIRECTIVES,
AsyncPipe
],
templateUrl: './elf.template.html',
styleUrls: ['./elf.style.scss'],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ElfComponent implements AfterViewInit {
@ViewChildren(InPageNavLinkDirective)
private readonly inPageNavLinkDirectives!: QueryList<InPageNavLink>;
public links$ = this.inPageNavPresService.links$;

constructor(private readonly inPageNavPresService: InPageNavPresService) {}

public ngAfterViewInit() {
this.inPageNavPresService.initialize(this.inPageNavLinkDirectives);
}
}
35 changes: 35 additions & 0 deletions apps/showcase/src/app/elf/elf.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { PetApi } from '@ama-sdk/showcase-sdk';
import { PetApiFixture } from '@ama-sdk/showcase-sdk/fixtures';
import { AsyncPipe } from '@angular/common';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterModule } from '@angular/router';

import { ElfComponent } from './elf.component';
import '@angular/localize/init';

describe('SdkComponent', () => {
let component: ElfComponent;
let fixture: ComponentFixture<ElfComponent>;
const petApiFixture = new PetApiFixture();
petApiFixture.findPetsByStatus = petApiFixture.findPetsByStatus.mockResolvedValue([]);

beforeEach(() => {
TestBed.configureTestingModule({
imports: [
ElfComponent,
RouterModule.forRoot([]),
AsyncPipe
],
providers: [
{provide: PetApi, useValue: petApiFixture}
]
});
fixture = TestBed.createComponent(ElfComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Empty file.
29 changes: 29 additions & 0 deletions apps/showcase/src/app/elf/elf.template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<h1>ELF Study</h1>
<div class="row">
<div class="right-nav order-1 order-lg-2 col-12 col-lg-2 sticky-lg-top pt-5 pt-lg-0">
<o3r-in-page-nav-pres
id="sdk-nav"
[links]="links$ | async"
>
</o3r-in-page-nav-pres>
</div>
<div class="order-2 order-lg-1 col-12 col-lg-10">
<h2 id="sdk-description">Description</h2>
<div>
<p>This page aims to display showcases of usages with <a href="https://ngneat.github.io/elf/" target="_blank">ELF</a>.</p>
</div>

<h2 id="sdk-example">Example</h2>
<div>
<p>
Let's try to use the API <a href="https://petstore3.swagger.io" target="_blank" rel="noopener">https://petstore3.swagger.io</a>
<br>
Fortunately, this API provides the specification as <a href="https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml" target="_blank" rel="noopener">Yaml file</a>
that we can use to generate an SDK.
<br>
Here, you can check the <a href="https://github.com/AmadeusITGroup/otter/blob/main/packages/@ama-sdk/showcase-sdk" target="_blank" rel="noopener">generated SDK</a>
</p>
<o3r-elf-pres></o3r-elf-pres>
</div>
</div>
</div>
1 change: 1 addition & 0 deletions apps/showcase/src/app/elf/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './elf.component';
Empty file.
159 changes: 159 additions & 0 deletions apps/showcase/src/components/showcase/elf/elf-pres.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { Tag } from '@ama-sdk/showcase-sdk';
import type { Pet } from '@ama-sdk/showcase-sdk';
import { ChangeDetectionStrategy, Component, computed, inject, OnInit, signal, ViewEncapsulation } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { FormsModule } from '@angular/forms';
import { DfMedia } from '@design-factory/design-factory';
import { NgbHighlight, NgbPagination, NgbPaginationPages } from '@ng-bootstrap/ng-bootstrap';
import { O3rComponent } from '@o3r/core';
import { OtterPickerPresComponent } from '../../utilities';
import { PetFacade } from '../../../stores';
import { take } from 'rxjs';

const FILTER_PAG_REGEX = /[^0-9]/g;

@O3rComponent({ componentType: 'Component' })
@Component({
selector: 'o3r-elf-pres',
standalone: true,
imports: [
NgbHighlight,
FormsModule,
NgbPagination,
OtterPickerPresComponent,
NgbPaginationPages
],
templateUrl: './elf-pres.template.html',
styleUrls: ['./elf-pres.style.scss'],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ElfPresComponent implements OnInit {
private readonly mediaService = inject(DfMedia);
private readonly petFacade = inject(PetFacade);

/**
* Name input used to create new pets
*/
public readonly petName = signal('');

/**
* File input used to create new pets
*/
public readonly petImage = signal('');

/**
* Search term used to filter the list of pets
*/
public readonly searchTerm = signal('');

/**
* Number of items to display on a table page
*/
public readonly pageSize = signal(10);

/**
* Currently opened page on the table
*/
public readonly currentPage = signal(1);

/**
* Complete list of pets retrieved from the API
*/
public readonly pets = toSignal(this.petFacade.pets, {initialValue: []});

/**
* Loading state of the API
*/
public readonly isLoading = toSignal(this.petFacade.loading, {initialValue: false});

/**
* Error state of the API
*/
public readonly hasErrors = toSignal(this.petFacade.error, {initialValue: false});

/**
* List of pets filtered according to search term
*/
public readonly filteredPets = computed(() => {
let pets = this.pets();
if (this.searchTerm()) {
const matchString = new RegExp(this.searchTerm().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'), 'i');
const matchTag = (tag: Tag) => tag.name && matchString.test(tag.name);
pets = pets.filter((pet) =>
(pet.id && matchString.test(String(pet.id))) ||
matchString.test(pet.name) ||
(pet.category?.name && matchString.test(pet.category.name)) ||
(pet.tags && pet.tags.some(matchTag)));
}
return pets;
});

/**
* Total amount of pet in the filtered list
*/
public readonly totalPetsAmount = computed(() => this.filteredPets().length);

/**
* List of pets displayed in the currently selected table page
*/
public readonly displayedPets = computed(() =>
this.filteredPets().slice((this.currentPage() - 1) * this.pageSize(), (this.currentPage()) * this.pageSize())
);

/**
* True if screen size is 'xs' or 'sm'
*/
public readonly isSmallScreen = toSignal<boolean>(this.mediaService.getObservable(['xs', 'sm']));

/** Base URL where the images can be fetched */
public baseUrl = location.href.split('/#', 1)[0];

private getNextId() {
return this.pets().reduce<number>((maxId, pet) => pet.id && pet.id < Number.MAX_SAFE_INTEGER ? Math.max(maxId, pet.id) : maxId, 0) + 1;
}

/**
* Trigger a full reload of the list of pets by calling the API
*/
public reload() {
this.petFacade.fetchPets();
}

public ngOnInit() {
this.petFacade.lastFetch.pipe(take(1)).subscribe((lastFetch) => {
if (Date.now() - lastFetch > 300_000) {
this.reload();
}
});
}

/**
* Call the API to create a new pet
*/
public create() {
const pet: Pet = {
id: this.getNextId(),
name: this.petName(),
category: {name: 'otter'},
tags: [{name: 'otter'}],
status: 'available',
photoUrls: this.petName() ? [this.petImage()] : []
};
this.petFacade.createPet(pet);
}

public delete(petToDelete: Pet) {
if (petToDelete.id) {
this.petFacade.deletePet(petToDelete.id);
}
}

public getTags(pet: Pet) {
return pet.tags?.map((tag) => tag.name).join(',');
}

public formatPaginationInput(input: HTMLInputElement) {
input.value = input.value.replace(FILTER_PAG_REGEX, '');
}
}
29 changes: 29 additions & 0 deletions apps/showcase/src/components/showcase/elf/elf-pres.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { PetApi } from '@ama-sdk/showcase-sdk';
import { PetApiFixture } from '@ama-sdk/showcase-sdk/fixtures';
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { ElfPresComponent } from './elf-pres.component';
import '@angular/localize/init';

describe('ElfPresComponent', () => {
let component: ElfPresComponent;
let fixture: ComponentFixture<ElfPresComponent>;
const petApiFixture = new PetApiFixture();
petApiFixture.findPetsByStatus = petApiFixture.findPetsByStatus.mockResolvedValue([]);

beforeEach(() => {
TestBed.configureTestingModule({
imports: [ElfPresComponent],
providers: [
{provide: PetApi, useValue: petApiFixture}
]
});
fixture = TestBed.createComponent(ElfPresComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
18 changes: 18 additions & 0 deletions apps/showcase/src/components/showcase/elf/elf-pres.style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
o3r-elf-pres {
.table-container {
min-height: 41rem;
}

.table-column-photo, .table-column-actions {
width: 2em;
}

.scroll-container {
width: 100%;
overflow-x: auto;
}

td, th {
vertical-align: middle;
}
}
Loading

0 comments on commit 07807b5

Please sign in to comment.