Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Study] feat(showcase): add elf usage for server state #1772

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading