From 2656680b71affcad47a0fe6495426331d33913e1 Mon Sep 17 00:00:00 2001 From: Eric BREHAULT Date: Thu, 28 May 2020 08:56:43 +0200 Subject: [PATCH 1/3] upgrade to latest pastanaga --- projects/grange/src/lib/components/breadcrumbs.html | 8 ++++---- projects/grange/src/lib/components/breadcrumbs.scss | 6 +++++- projects/grange/src/lib/views/folder.html | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/projects/grange/src/lib/components/breadcrumbs.html b/projects/grange/src/lib/components/breadcrumbs.html index 565dff5..b15a904 100644 --- a/projects/grange/src/lib/components/breadcrumbs.html +++ b/projects/grange/src/lib/components/breadcrumbs.html @@ -2,14 +2,14 @@ diff --git a/projects/grange/src/lib/components/breadcrumbs.scss b/projects/grange/src/lib/components/breadcrumbs.scss index e3e5c5a..64e425b 100644 --- a/projects/grange/src/lib/components/breadcrumbs.scss +++ b/projects/grange/src/lib/components/breadcrumbs.scss @@ -61,8 +61,12 @@ right: - rythm(2); top: 1px; } - & ::ng-deep .pa-button-link { + & .pa-button { white-space: nowrap; + text-decoration: none; + &:hover { + text-decoration: underline; + } } & .o-breadcrumb-item-more ::ng-deep svg { fill: $separator-color; diff --git a/projects/grange/src/lib/views/folder.html b/projects/grange/src/lib/views/folder.html index 8bfe6ad..75fde98 100644 --- a/projects/grange/src/lib/views/folder.html +++ b/projects/grange/src/lib/views/folder.html @@ -4,6 +4,6 @@

{{ (context | async).title }}

folder.empty
\ No newline at end of file From cacbabe29a13d79ff22f655772f7f34afd369d83 Mon Sep 17 00:00:00 2001 From: Eric BREHAULT Date: Thu, 28 May 2020 08:57:09 +0200 Subject: [PATCH 2/3] fix updateContext and add onComplete --- projects/grange/src/lib/grange.service.ts | 33 +++++++++++++++++------ projects/grange/src/lib/state/effects.ts | 22 +++------------ projects/grange/src/lib/state/state.ts | 2 +- 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/projects/grange/src/lib/grange.service.ts b/projects/grange/src/lib/grange.service.ts index f0e7006..5ef73b3 100644 --- a/projects/grange/src/lib/grange.service.ts +++ b/projects/grange/src/lib/grange.service.ts @@ -4,9 +4,9 @@ import { Traverser } from 'angular-traversal'; import { GrangeCore } from '@guillotinaweb/grange-core'; import { GrangeState } from './state/state'; import { PastanagaService } from '@guillotinaweb/pastanaga-angular'; -import { Observable } from 'rxjs'; +import { Observable, AsyncSubject } from 'rxjs'; import { TraverserSelectors, TraverserActions } from '@guillotinaweb/ngx-state-traverser'; -import { take } from 'rxjs/operators'; +import { take, concatMap, tap, skip } from 'rxjs/operators'; @Injectable({ providedIn: 'root' @@ -26,12 +26,29 @@ export class Grange { return this.store.pipe(select(TraverserSelectors.getContext)); } - updateContext(changes: any) { + updateContext(changes: any): {onComplete: Observable} { + const onComplete = new AsyncSubject(); + let initialContext: any; + let path: string; this.getContext().pipe( - take(1) - ).subscribe(context => this.store.dispatch(new TraverserActions.UpdateTraverserResource({ - path: this.core.api.getPath(context['@id']), - changes - }))); + take(1), + tap(context => { + initialContext = context; + path = this.core.api.getPath(context['@id']); + this.store.dispatch(new TraverserActions.UpdateTraverserResource({path, changes})); + }), + concatMap(newContext => this.core.resource.update(path, changes)), + ).subscribe( + () => { + onComplete.next(true); + onComplete.complete(); + }, + () => { + this.store.dispatch(new TraverserActions.UpdateTraverserResource({path, changes: initialContext})); + onComplete.next(false); + onComplete.complete(); + }, + ); + return {onComplete}; } } diff --git a/projects/grange/src/lib/state/effects.ts b/projects/grange/src/lib/state/effects.ts index 0b48094..27d8ef3 100644 --- a/projects/grange/src/lib/state/effects.ts +++ b/projects/grange/src/lib/state/effects.ts @@ -1,11 +1,10 @@ import { Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; -import { tap, switchMap, take, concatMap, filter, catchError, map } from 'rxjs/operators'; -import { TraverserActions, TraverserSelectors } from '@guillotinaweb/ngx-state-traverser'; -import { select, Store } from '@ngrx/store'; +import { tap } from 'rxjs/operators'; +import { TraverserActions } from '@guillotinaweb/ngx-state-traverser'; +import { Store } from '@ngrx/store'; import { GrangeState } from './state'; import { GrangeCore } from '@guillotinaweb/grange-core'; -import { EMPTY, of } from 'rxjs'; @Injectable() export class GrangeEffects { @@ -16,21 +15,6 @@ export class GrangeEffects { tap(() => window.scrollTo(0, 0)) ); - @Effect() - updateResource = this.actions.pipe( - ofType(TraverserActions.Types.UpdateTraverserResource), - switchMap(action => this.store.pipe( - select(TraverserSelectors.getObjectByPath(action.payload.path)), - take(1), - filter(resource => !!resource['@id']), - concatMap(resource => this.core.resource.update(resource['@id'], resource).pipe( - map(() => EMPTY), - // if error on update, we traverse the resource in order to put the backend version in our local state - catchError(() => of(new TraverserActions.Traverse(this.core.api.getPath(resource['@id'])))) - )), - )), - ); - constructor( private readonly actions: Actions, private readonly store: Store, diff --git a/projects/grange/src/lib/state/state.ts b/projects/grange/src/lib/state/state.ts index 0c69217..5ed6cd3 100644 --- a/projects/grange/src/lib/state/state.ts +++ b/projects/grange/src/lib/state/state.ts @@ -13,4 +13,4 @@ export interface GrangeState { export const initialState: GrangeState = { breadcrumbs: [], -}; \ No newline at end of file +}; From 46dab9bb22e11ddd6d9ee553be520b2c1a8a6941 Mon Sep 17 00:00:00 2001 From: Eric BREHAULT Date: Thu, 28 May 2020 08:57:19 +0200 Subject: [PATCH 3/3] better demo, better doc --- CHANGELOG.md | 10 ++ README.md | 92 ++++++++++++++++++- g-api/config.yaml | 12 +++ package-lock.json | 26 +----- package.json | 2 +- projects/demo/src/app/app.component.ts | 2 + projects/demo/src/app/app.module.ts | 6 +- .../demo/src/app/canvas/canvas.component.ts | 4 +- .../demo/src/app/player/player.component.html | 4 + .../demo/src/app/player/player.component.ts | 37 ++++++++ projects/grange/package.json | 2 +- 11 files changed, 166 insertions(+), 31 deletions(-) create mode 100644 projects/demo/src/app/player/player.component.html create mode 100644 projects/demo/src/app/player/player.component.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4371be3..6720bbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +# 1.3.1 (2020-05-28) + +### Improvement +- Improve the demo +- More documentation +- `updateContext` returns `onComplete` observable + +### Bug fix: +- Fix `updateContext` method + # 1.3.0 (2020-05-24) ### Feature diff --git a/README.md b/README.md index e2e116d..e5967c6 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,8 @@ The good thing about a Grange app is it is totally neutral about the Guillotina ## Building a basic Grange app +### Init a Grange project + If you don't have one already, create an angular project: ``` ng new my-project @@ -107,6 +109,92 @@ npm start The Angular app is now offering all the Grange standard views (login, content creation, view, etc.). +### Create custom views + +We have in our Guillotina `config.yaml` file a custom content type named `player`: +```yaml + player: + title: Player + inherited_interface: guillotina.interfaces.IItem + inherited_class: guillotina.content.Item + add_permission: guillotina.AddContent + properties: + team: + type: guillotina.schema.TextLine + title: Team + rank: + type: guillotina.schema.Int + title: Rank +``` + +We would like to use our own custom form to edit `player` contents. + +We create regular Angular component named `PlayerComponent` and we declare it in our `AppComponent` to become the `player` edit view: + +```typescript +this.grange.traverser.addView('edit', 'player', PlayerComponent); +``` + +The template is a simple form (based on Pastanaga UI elements, but any form elements would work): + +```html +Title +Team +Rank +Save +``` + +In the component itself, we inject `grange` service: +```typescript +constructor(private grange: Grange) { } +``` + +Thank to this service, we can get the values we need from the context: +```typescript +ngOnInit() { + this.grange.getContext().subscribe(context => { + this.title = context.title; + this.team = context.team; + this.rank = context.rank; + }); +} +``` + +The good thing about `getContext()` is it returns an Observable that will emit the context object everytime it changes in our state, so our form is always reflecting the current state values. + +`grange` service also allows us to save the changes the user enters in the form: +```typescript +save() { + this.grange.updateContext({ + title: this.title, + team: this.team, + rank: this.rank, + }); +} +``` +`updateContext()` updates the state (hence the form is immediately updated because `getContext()` will emit the new values), and it also updates Guillotina backend by doing a PATCH call. + +If we want to take an action after saving – like redirecting to the home page – `updateContext()` has a `onComplete` property which is a boolean observable (returning `true` for success, and `false` if saving produced a backend error): + +```typescript +save() { + this.grange.updateContext({ + title: this.title, + team: this.team, + rank: this.rank, + }).onComplete.subscribe(success => { + if (success) { + this.grange.ui.toaster.open('Saved', 2000); + this.grange.traverser.traverse('/'); + } else { + this.grange.ui.toaster.open('Error when saving.', 'common.dismiss'); + } + }); +} +``` + +See [the full code example](projects/demo/src/app). + ## Reference ### Grange views @@ -160,7 +248,7 @@ import { Grange } from 'grange'; constructor(private grange: Grange) {} notify() { - this.grange.ui.toaster.open('Hello'); + this.grange.ui.toaster.open('Hello', 'Close'); } ``` @@ -233,7 +321,7 @@ It allows to select state information or to dispatch actions. See below the "Sta `this.grange.ui` -It gives access to the main Pastanaga services: calendar, popup, sidebar, and translate. +It gives access to the main Pastanaga services: calendar, dialog, popup, sidebar, toaster and translate. See "Pastanaga UI library" section below. diff --git a/g-api/config.yaml b/g-api/config.yaml index 869da65..a0ffdd9 100644 --- a/g-api/config.yaml +++ b/g-api/config.yaml @@ -102,6 +102,18 @@ behaviors: title: Text default: Hello contents: + player: + title: Player + inherited_interface: guillotina.interfaces.IItem + inherited_class: guillotina.content.Item + add_permission: guillotina.AddContent + properties: + team: + type: guillotina.schema.TextLine + title: Team + rank: + type: guillotina.schema.Int + title: Rank canvas: title: Canvas inherited_interface: guillotina.interfaces.IItem diff --git a/package-lock.json b/package-lock.json index f260f7f..34539b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3075,20 +3075,10 @@ } } }, - "@guillotinaweb/grange-core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@guillotinaweb/grange-core/-/grange-core-1.2.1.tgz", - "integrity": "sha512-u5s+u+2/4t3ScQ+rfRcFasQy8OLCo7R09mXrAqYGjTYLT7hcfIk/KMEx8yUbasJNOIEZDKCqUg/bivlQYx7xNA==" - }, - "@guillotinaweb/ngx-state-traverser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@guillotinaweb/ngx-state-traverser/-/ngx-state-traverser-1.2.0.tgz", - "integrity": "sha512-Pgd1ppBuZCN2mcUEtNySVXzf+xMkzD7/sxzkEZeJjOYxKTSi8r3ckX9VlpvIsDSOuznKJe0mwBHCrE0HQdY8XQ==" - }, "@guillotinaweb/pastanaga-angular": { - "version": "1.17.18", - "resolved": "https://registry.npmjs.org/@guillotinaweb/pastanaga-angular/-/pastanaga-angular-1.17.18.tgz", - "integrity": "sha512-HTbWMqRW4dTh0gt5sw1LzOLRzF8aysze5kO2otUoKCpfS1XySfqjaGIqRdf8bfl8OR1vCLPwgQCN6/YqAGqetA==" + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/@guillotinaweb/pastanaga-angular/-/pastanaga-angular-1.18.2.tgz", + "integrity": "sha512-O7NAwJAJu/2mKbIuEX2Co4kMZ6jqPDIJ1yZ7hP2VsoU8mBwXYdq8Izqumexaosbm6P/AUN+qRdStZXuZANGYyg==" }, "@istanbuljs/schema": { "version": "0.1.2", @@ -4092,11 +4082,6 @@ "tslib": "^1.9.0" } }, - "angular-traversal": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/angular-traversal/-/angular-traversal-1.4.3.tgz", - "integrity": "sha512-ZQA1nQQo9nL7okPBJv61XipIeA+EkPw1iOO0BrCkBjudpvZrLhMYwLP5slHgBqpsSVcXX3yB1GFpNH+ER1bHLw==" - }, "ansi-align": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", @@ -11472,11 +11457,6 @@ } } }, - "ngx-schema-form": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ngx-schema-form/-/ngx-schema-form-2.5.0.tgz", - "integrity": "sha512-KeQBJY0VIG5K7OXWDikEmAnn12AYRlmiqrbEXvKEKr2EdHElEa7pu7vHt2qgPDMNXwWsOsMbxVDA9MTp4SUbnA==" - }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", diff --git a/package.json b/package.json index 953775b..031fb80 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@guillotinaweb/grange-core": "latest", "@guillotinaweb/grange-form": "latest", "@guillotinaweb/ngx-state-traverser": "latest", - "@guillotinaweb/pastanaga-angular": "^1.17.18", + "@guillotinaweb/pastanaga-angular": "^1.18.2", "@ngrx/core": "^1.2.0", "@ngrx/effects": "^8.4.0", "@ngrx/store": "^8.4.0", diff --git a/projects/demo/src/app/app.component.ts b/projects/demo/src/app/app.component.ts index ba90b8d..d49c865 100644 --- a/projects/demo/src/app/app.component.ts +++ b/projects/demo/src/app/app.component.ts @@ -1,6 +1,7 @@ import { Component } from '@angular/core'; import { GrangeViews, Grange } from '../../../grange/src'; import { CanvasComponent } from './canvas/canvas.component'; +import { PlayerComponent } from './player/player.component'; @Component({ selector: 'app-root', @@ -16,6 +17,7 @@ export class AppComponent { this.views.initialize(); this.grange.core.auth.isAuthenticated.subscribe(auth => this.isAuthenticated = auth.state); this.grange.traverser.addView('view', 'canvas', CanvasComponent); + this.grange.traverser.addView('edit', 'player', PlayerComponent); } logout() { diff --git a/projects/demo/src/app/app.module.ts b/projects/demo/src/app/app.module.ts index 56e74a1..db4b914 100644 --- a/projects/demo/src/app/app.module.ts +++ b/projects/demo/src/app/app.module.ts @@ -4,17 +4,19 @@ import { environment } from '../environments/environment'; import { AppComponent } from './app.component'; import { CanvasComponent } from './canvas/canvas.component'; +import { PlayerComponent } from './player/player.component'; import { GrangeRootModule } from '../../../grange/src'; import { TraversalModule } from 'angular-traversal'; import { StoreModule } from '@ngrx/store'; import { StoreDevtoolsModule } from '@ngrx/store-devtools'; -import { ButtonModule } from '@guillotinaweb/pastanaga-angular'; +import { ButtonModule, TextFieldModule } from '@guillotinaweb/pastanaga-angular'; import { AngularSvgIconModule } from 'angular-svg-icon'; @NgModule({ declarations: [ AppComponent, CanvasComponent, + PlayerComponent, ], imports: [ BrowserModule, @@ -27,6 +29,8 @@ import { AngularSvgIconModule } from 'angular-svg-icon'; }), AngularSvgIconModule.forRoot(), ButtonModule, + TextFieldModule, + ButtonModule, ], providers: [ { diff --git a/projects/demo/src/app/canvas/canvas.component.ts b/projects/demo/src/app/canvas/canvas.component.ts index 2d56772..61deb72 100644 --- a/projects/demo/src/app/canvas/canvas.component.ts +++ b/projects/demo/src/app/canvas/canvas.component.ts @@ -1,8 +1,6 @@ import { Component, OnInit } from '@angular/core'; import { Grange } from '../../../../grange/src'; -import { select } from '@ngrx/store'; -import { TraverserSelectors } from '@guillotinaweb/ngx-state-traverser'; -import { map, tap, concatMap } from 'rxjs/operators'; +import { map, tap } from 'rxjs/operators'; import { Observable } from 'rxjs'; @Component({ diff --git a/projects/demo/src/app/player/player.component.html b/projects/demo/src/app/player/player.component.html new file mode 100644 index 0000000..35738a4 --- /dev/null +++ b/projects/demo/src/app/player/player.component.html @@ -0,0 +1,4 @@ +Title +Team +Rank +Save \ No newline at end of file diff --git a/projects/demo/src/app/player/player.component.ts b/projects/demo/src/app/player/player.component.ts new file mode 100644 index 0000000..e727380 --- /dev/null +++ b/projects/demo/src/app/player/player.component.ts @@ -0,0 +1,37 @@ +import { Component, OnInit } from '@angular/core'; +import { Grange } from '../../../../grange/src'; + +@Component({ + selector: 'app-player', + templateUrl: 'player.component.html' +}) +export class PlayerComponent implements OnInit { + title = ''; + team = ''; + rank = 0; + + constructor(private grange: Grange) { } + + ngOnInit() { + this.grange.getContext().subscribe(context => { + this.title = context.title; + this.team = context.team; + this.rank = context.rank; + }); + } + + save() { + this.grange.updateContext({ + title: this.title, + team: this.team, + rank: this.rank, + }).onComplete.subscribe(success => { + if (success) { + this.grange.ui.toaster.open('Saved', 2000); + this.grange.traverser.traverse('/'); + } else { + this.grange.ui.toaster.open('Error when saving.', 'common.dismiss'); + } + }); + } +} diff --git a/projects/grange/package.json b/projects/grange/package.json index a40230d..753a1fb 100644 --- a/projects/grange/package.json +++ b/projects/grange/package.json @@ -1,6 +1,6 @@ { "name": "@guillotinaweb/grange", - "version": "1.3.0", + "version": "1.3.1", "license": "MIT", "author": { "name": "Eric Brehault",