12.1.0 • Published 8 days ago

@jussanjuan/angular-pjsj v12.1.0

Weekly downloads
46
License
-
Repository
-
Last release
8 days ago

Angular PJSJ

La motivación proncipal de la librearia es la de crear elementos comunes que se presentan en distintos desarrollos para poder distribuirlos desde este repositorio y centralizar su evolución. Con esta libreria vas a poder crear aplicaciones angular de manera mucho mas ágil, centrado la total atención en el problema a resolver y no al desarrollo de componentes como modales, alertas, tablas, etc.

Getting Started

Install

npm i @jussanjuan/angular-pjsj
    import { AngularPjsjModule } from '@jussanjuan/angular-pjsj';
    
    NgModule({
          declarations: [
            ...
          ],
          imports: [
            ...
            AngularPjsjModule,
            ...
          ],

Dependencidas necesarias

npm i @angular/flex-layout
npm i @angular/material
npm i @angular/cdk

Configuración de Clase

La configuración de clase de una entidad es el elemento central sobre el cual se crearon los demas desarrollos genéricos. Esta configuración contiene desde el nombre de la entidad hasta el tipo de cada uno de los campos de la entidad. Cada una de las entidades que se quieran mostrar con los componentes debe tener definido el metodo estatico getClassConfig() : ClassConfig que va a retornar su configuración particular

Clases

ClassConfig

AtributosTipoObservaciones
entityNameStringTexto estatico que será el nombre con el cual se presentará la entidad. Por ejemplo el entityName de una entidad Person es 'Persona'.
orderFilter?string[]Arreglo de String que contiene el nombre los atributos y el orden en el que se van a mostrar en el selector de atributos a filtrar
idKeysstring[]Arreglo que contiene el nombre de los atributos que forman la clave primaria o Id de la entidad
deleteKeys?string[]Arreglo que contiene los atributos que se van a mostrar en un cartel de confirmación de eliminación de un elemento
fieldConfigFieldConfigHGeneric[]Arreglo con toda la configuración de cada uno de los campos de la entidad en cuestion

FieldConfigHGeneric

AtributosTipoObservaciones
keystringNombre del campo en la entidad. Por ejemplo si tenemos la clase Person en TypeScript que tiene el atributo de clase name entonces la key debe ser el string 'name'
displayStringEs el texto con el que se va a presentar ese campo al usuario final. Siguiendo el ejemplo de Persona cuyo campo se llama name entonces un buen valor para display sería el string 'Nombre de Persona'
isColumnBooleanCampo booleano que va a indicar si el campo en cuastion se va a mostrar al usuario o no, en la tabla generica o en otro componente genérico.
isFilterBooleanCampo booleano que va a indicar si ese campo va a ser el valor de un filtro o no.
typeFilterFieldTypeEste campo es el tipo de filtro que se va a pintar en el formulario de filtros del componente Filtro Genérico. Dependiendo del valor de este atributo se va a pintar un input del tipo texto o númerico o un select, etc
url?stringEn caso de ser un campo de tipo select el que se va a pintar en un filtro. Se puede usar esta propiedad para indicarle al componente de filtro que debe ir a buscar la información del select de un servicio rest que debe ser un get
options?any[]En caso de ser un campo de tipo select simple se puede usar esta propiedad para indicarle los valores que se van a mostrar. Un ejemplo es el select del sexo de una persona que siempre van a ser dos valores 'Masculino' y 'Femenino'. Otro uso de este atributo es para los datos de tipo booleano que se desean mostrar de una forma especifica en el componente que los muestre. En este caso se van a levantar los dos primeros valores como los representativos del mismo. Siendo el valor en la posición 0 del arreglo para el valor True y el contenido de la posición 1 es como se va a mostrar el valor False del atributo en cuestion. Un ejemplo es si queremos indicar si una persona esta casada o no y tenemos el campo isMarried en la clase y queremos mostrar los valores 'Si' cuando sea True y 'No' cuando sea False, entonces este campo debe contener el arreglo 'Si', 'No'. Adicionalmente en las tablas como en otros componentes se puede enviar HTML para pintar el campo a conveniencia.
optionsCheck?any[]Atributo auxiliar para uso exclusivo e interno del filtro genérico

FieldType

Esta clase es un enumerador y los valores son los siguientes:

ValoresObservaciones
inputTextEste enumerador va a indicar que el campo es del tipo texto, para trabajarlo como tal en html
inputNumberEste enumerador va a indicar que el campo es del tipo número.
inputPasswordEste enumerador va a indicar que el campo es del tipo privado.
radioButtonEste enumerador va a indicar que el campo es un radio button. Se va a requerir del atributo ClassConfig.options o url para mostrar sus valores
textareaEste enumerador va a indicar que el campo es del tipo texto y que la entrada es de una longitud considerable.
selectEste enumerador va a indicar que el valor del campo se debe ingresar desde un selector de opciones. Por lo tanto tiene un número finito de opciones y que se tienen que recuperar ya sea desde la ClassConfig.url o de manera estatica de ClassConfig.options.
dateEste enumerador va a indicar que el valor del campo es una fecha y se le debe aplicar filtros para mostrarlas en el formato correcto. El ingreso del valor será un DatePicker.
fileEste enumerador va a indicar qeu el valor de entrada es un archivo. Aún no esta soportado por los componentes genéricos
checkEste enumerador va a indicar que el valor es un dato booleano.

Ejemplo

Este ejemplo es sobre la clase Hero donde tenemos el nombre del heroe su identificador y superpoderes.

    
    import { ClassConfig, FieldType } from 'projects/angular-pjsj/src/public-api';
    import { Superpower } from './superpower';

    export class Hero {
        id: number;
        name: string;
        powers?: Superpower[];

        public static getClassConfig(): ClassConfig {
            return {
                entityName: 'Heroes',
                idKeys: ['id'],
                fieldConfig: [
                    {
                    key: 'id',
                    display: 'Identificador',
                    isColumn: true,
                    isFilter: false,
                    typeFilter: FieldType.inputText
                },{
                    key: 'name',
                    display: 'Nombre',
                    isColumn: true,
                    isFilter: true,
                    typeFilter: FieldType.inputText
                },{
                    key: 'powers',
                    display: 'Superpoderes',
                    isColumn: false,
                    isFilter: false,
                    typeFilter: FieldType.select,
                    url: 'http:localhost:8080/selects/superpowers'
                }]
            }
        }
    }

Componentes

Home Genérico (GUI)

Este componente genera una GUI compuesta de un navbar, donde se muestra el título de la aplicación y un sidenav, donde se generan links de acceso a los potenciales recursos, más un link para realizar el logout de la aplicación. La GUI se generará en base a la clase de configuración HomeConfig.

Inputs de Home Genérico

  • Configuración de componente: La configuración es una instancia de HomeConfig y es donde se obtiene toda la información necesaria para mostrar y crear la GUI genérica.

Outputs de Home Genérico

  • Evento de logout: El componente Home expone la propiedad logoutEvent para emitir el evento click del link de logout (Salir). La propiedad se debe asociar a un método (p.e. 'logoutMethod()') donde se defina la lógica asociada al evento.

Modo de uso:

    <g-home [homeConfig]="homeConfig" (logoutEvent)="logoutMethod()"> <!--Cuerpo de la aplicación --> </g-home>

Clases de configuración.

HomeConfig

AtributosTipoObservaciones
appNameStringTexto estático que será el nombre de la aplicación que se mostrará en el navbar.
linksMenuLinkMenu[]Arreglo de LinkMenu que contiene el nombre del link, path del recurso y el string del ícono que se mostrará en los links del sidenav.

LinkMenu

AtributosTipoObservaciones
nameStringNombre que se mostrará en el link.
pathStringTexto que representa la ruta al recurso que invoca el link.
iconStringTexto que representa el ícono que acompaña el link.

Filtro Genérico

Este componente genera filtros de una entidad según su configuración de clase. El formulario de filtros se generará en base a la configuración de cada campo. Por ejemplo si un campo tiene como typeFilter el tipo TypeFilter.inputText, entonces el filtro para ese campo sera un input del tipo texto y asi. Este componente esta compuesto de la siguiente manera:

            (*1)                                                                                                 
 Búscar ${entityName} por filtro 

 (*2)                          (*3)                           (*4)                (*5)                                      
 +------------------------+   +------------------------+      +--------------+    +------------------------+
 | Seleccionar campo    V |   | ${Input dinamico}      |      | Boton Búscar |    | Boton Limpiar Filtros  |
 +------------------------+   +------------------------+      +--------------+    +------------------------+
  • Referenicias:
    1. Es el nombre de la entidad recuperada del campo ClassConfig.entityName.
    2. El seleccionador de campos es un select donde se muestran todos los valores del atributo ClassConfig.fieldConfig.*.display, es decir el campo display de cada uno de los elementos del arreglo fieldConfig.
    3. En este espacio se va a poner el elemento que corresponda. Ya sea un DatePicker para un campo del tipo fecha, un input texto para otro del tipo string y asi.
    4. Este boton es el que libera el evento de busqueda e indica al componente que lo use que los valor a filtrar cambiaron, asi tambien envia toda la información del nuevo filtro.
    5. Elimina todos los valores de los filtros.

Inputs de Filtro Genérico

  • Configuración de clase: La configuración de clase es una instancia de ClassConfig y es donde se obtienen toda la información necesaria para mostrarla y crear los inputs genéricos.

Modo de uso:

    <g-filter #gFilter [classConfig]="classConfig"></g-filter>

Tabla Generica

Esta tabla esta diseñada para poder adaptarse y presentar cualquier tipo de información. Para poder usar está tabla se debe ingresar los siguientes elementos como "input":

  • Instancia del service a usar: Es una instancia de un servicio angular que herede de la superclase GenericService. Este servicio se debe inyectar en el componente que utilice La tabla generica. La tabla generica útiliza una instancia de GenericService de una cierta entidad para traer sus datos.
    +------------------------+          +-----------------------------+                                                  
    |                        |          |                             |                                         
    |    Tabla Generica      |          |       GenericService<C,R>     |                                         
    |                        |          |                             |                                         
    +------------^-----------+          +--------------^--------------+                                         
                 |                                     |                                                        
                 |Envia instancia                      |Hereda Funcionalidades                                  
                 |de ServiceE                          |                                                        
                 |                                     |                                                        
    +------------|-----------+              +----------|----------+                  +-------------------------+
    |                        |  Se Inyecta  |                     |  Solicita Datos  |                         |
    |  Componente que        <---------------       ServiceE      -------------------|       Web Service       |
    |  Usa tabla Generica    |              |                     |                  |                         |
    +------------------------+              +---------------------+                  +-------------------------+
  • Configuración de clase: La configuración de clase es una instancia de ClassConfig y es donde se obtienen toda la información necesaria para mostrar la información de cada campo de una cierta entidad.
  • Opcional Filtro: es una instancia del componente de g-filter. De esta manera se va a vincular con la tabla para indicarle cuando se filtre o no un dato y mostrar los elementos filtrados.
  • Opcional Acciones: Se entiende por acción a las acciones que se desean realizar por cada elemento de la tabla. Un ejemplo puede ser: ver el detalle, editar el elemento, etc. Cada una de las acciones se va a corresponder con un boton en la tabla en la columna de Acciones. El input del componente es un Arreglo de ActionConfig
AtributoObservaciónes
eventInstancia de @angular/core/EventEmitter que es la que va a tener la función que manejará el click del botón.
textTexto estatic que va a ser el contenido del boton
textResolver?Función que recibe como parametro el elemento y retorna el texto del boton. Se va a invocar cada vez que cambie el elemento.
colorResolver?Función que recibe como parametro el elemento y puede retornar el color del boton. Siendo los colores posibles 'warn', 'accent', 'primary' o 'none'.

Ejemplos

  • app.component.html
<g-home #gHome [homeConfig]="homeConfig" (logoutEvent)="logoutMethod()">

    <router-outlet></router-outlet>

</g-home>
  • app.component.ts
import { Component, EventEmitter } from '@angular/core';
import { HomeConfig } from 'projects/angular-pjsj/src/lib/domain/generic/home-config';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent {
  homeConfig: HomeConfig = this.getHomeConfig();

  getHomeConfig(): HomeConfig {
    return {
      appName: 'Example App',
      linksMenu: [
        {
          name: 'Heroes',
          path: 'heroes',
          icon: 'fas fa-address-book'
        }, {
          name: 'Opción 1',
          path: 'opcion-1',
          icon: 'fas fa-adjust'
        }, {
          name: 'Opción 2',
          path: 'opcion-2',
          icon: 'fas fa-anchor'
        }, {
          name: 'Opción 3',
          path: 'opcion-3',
          icon: 'icono-arg-seguridad-red'
        }]
    }
  }

  constructor() { }

  salir() {
    // TODO lógica para evento logout del componente GHome.
    console.log("Click en Salir");
  }
}
  • app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HeroesComponent } from './heroes/heroes.component';
import { Page1Component } from './page1/page1.component';
import { Page2Component } from './page2/page2.component';
import { Page3Component } from './page3/page3.component';

const routes: Routes = [
  { path: '', 
    redirectTo: '/heroes',
    pathMatch: 'full'
  }, {
    path: 'heroes',
    component: HeroesComponent
  }, {
    path: 'opcion-1',
    component: Page1Component
  }, {
    path: 'opcion-2',
    component: Page2Component
  }, {
    path: 'opcion-3',
    component: Page3Component
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { };
  • heroes.component.html
    <g-filter #gFilter [classConfig]="classConfig"></g-filter>
    
    <g-table 
        #tabservice 
        [gFilter]="gFilter" 
        [classConfig]="classConfig" 
        [service]="service" 
        [actions]="actions">
    </g-table>
  • heroes.component.ts
import { Component, EventEmitter } from '@angular/core';
import { Hero } from './hero';
import { HeroService } from './hero.service';
import { ClassConfig, ActionConfig, MessageService } from 'projects/angular-pjsj/src/public-api';
import { MatDialog } from '@angular/material/dialog';
import { ConfigSuperpowerComponent } from './config-superpower/config-superpower.component';

@Component({
  selector: 'app-heroes',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class HeroesComponent {
  classConfig: ClassConfig = Hero.getClassConfig();
  actions: ActionConfig[];
  
  constructor(public service: HeroService, private messageSerivce: MessageService, private dialog: MatDialog) {
    // Creción del evento de Carteles
    let dialogs = new ActionConfig();
    dialogs.event = new EventEmitter();
    // Se subscribe la funcion que se va a ejecutar al click del boton
    dialogs.event.subscribe((hero) => this.detail(hero));
    // Se agrega el texto del boton
    dialogs.text = 'Carteles';
    // Se agrega un color al boton, adicionalmente puede cambiar según los atributos del heroe
    dialogs.colorResolver = (hero) => {
      return 'accent'
    }
    let superpowers = new ActionConfig();
    superpowers.event = new EventEmitter();
    superpowers.event.subscribe((hero) => this.powerConfig(hero));
    superpowers.text = 'Super Poderes';
    this.actions = [superpowers, dialogs];
  }

  detail(hero: Hero) {
    ...
    ...
  }

  powerConfig(hero) {
     ...
     ...
  }
}
  • heroes.service.ts
import { Injectable } from '@angular/core';
import { Hero } from './hero';
import { HttpClient } from '@angular/common/http';
import { LoadService, MessageService, GenericService } from 'projects/angular-pjsj/src/public-api';

@Injectable({
  providedIn: 'root'
})
export class HeroService extends GenericService<Hero,Hero> {
  url: string;

  constructor(httpClient: HttpClient, loadService: LoadService, messageService: MessageService) {
    super(httpClient, loadService, messageService);
    this.url = '/heroes';
  }

}

Entity Data

Muestra todos los campos de una entidad que tengan en el atributo "column = true" del FieldConfig correspondiente.

Ejemplo de uso:

  • hero-detail.component.html
<mat-dialog-content>
    <entity-data [classConfig]="heroCC" [entity]="hero"></entity-data>
</mat-dialog-content>
<mat-dialog-actions>
    <button mat-raised-button [mat-dialog-close]="false">Cerrar</button>
</mat-dialog-actions>
import { Component, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { Hero } from '../hero';

@Component({
  selector: 'app-hero-detail',
  templateUrl: './hero-detail.component.html',
  styleUrls: ['./hero-detail.component.sass']
})
export class HeroDetailComponent implements OnInit {

  heroCC = Hero.getClassConfig();
  hero: Hero;
  constructor(
    @Inject(MAT_DIALOG_DATA) public data: any
    ) {

      this.hero = data;
    }

  ngOnInit(): void {
  }

}

En el ejemplo anterior podemos ver como usarlo en un MatDialog.

Carteles

Message service

Este servicio de mensajes es el que centraliza todos los llamados a los carteles de alerta, dialogo, etc. Gracias a este servicio podemos mantener un unico estilo de carteles, alertas y formas para todas las aplicaciónes, pre construidos.

Ejemplo:

 messageServiceTest(hero: Hero) {
    this.messageSerivce.showError(['ERROR ::: Nombre: '+ hero.name, 'ERROR ::: Id: '+hero.id]).subscribe(() => {
      this.messageSerivce.showConfirmDialog('Heroe: ' + hero.name +' <p>Id: ' + hero.id +'</p>', 'Dialogo INFO!! Detalle del Heroe').subscribe(() => {
        this.messageSerivce.showAlertConfirmDialog('Heroe: ' + hero.name +' <p>Id: ' + hero.id +'</p>', 'Alerta!!  Detalle del Heroe').subscribe(() => {
          this.messageSerivce.showInfo('Info:: Heroe: ' + hero.name );
        });
      });
    });
  }

Indicadores de carga de información

Load service

Este servicio expone distintos metodos a los cuales se deben subscribir para poder reconocer cuando se esta ejecutando un request cualquiera y cuando termina. Esta pensado para que se indique al usuario de alguna manera que su pedido esta siendo procesado y que se esta esperando una respuesta del servicio web.

MetodoParametrosRetornoObservaciones

Pignus

Para poder conectarse con pignus necesitamos crear un componente y tenerlo vinculado a la ruta "/login". De esta manera se realiza con Angular Router:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';


const routes: Routes = [ {
    path: 'login',
    component: LoginTestComponent
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})

El componente LoginTestComponent debe ser un componente especial que se crea de una manera especifica, extendiendo de la clase PignusLogin, proveida desde la libreria. Donde la clase LoginTestComponent quedaria de la siguiente manera:

import { Component} from '@angular/core';
import { ActivatedRoute, Router, NavigationExtras } from '@angular/router';
import { LoadService, PignusLogin, AuthPignusService } from '@jussanjuan/angular-pjsj';
import { environment } from 'src/environments/environment';


@Component({
  selector: 'app-login-test',
  templateUrl: './login-test.component.html',
  styleUrls: ['./login-test.component.sass']
})
export class LoginTestComponent extends PignusLogin {
  
  constructor(  authService: AuthTestService,
      private router: Router,
     activatedRoute: ActivatedRoute,
     private loadService: LoadService) {
    super(authService, activatedRoute);
  }
  /**
   * Este metodo se invoca cuando detecta un acceso no permitido a la plataforma. 
   * Desde este espacio se debe siempre redirigir a la ruta de login de pignus, pero tambien se pueden ejecutar 
   * validaciones de datos, generar carteles, etc.
   * Pero sin redirigir a Pignus no se va a poder utilizar las acciones que sean privadas en un servidor
   *  
   */
  public redirectNotAuth() {
    console.log(LoginTestComponent.toString() + ": No autenticado")
    // urlLogin es la url de login de Pignus
    window.location.href = environment.urlLogin;
  }
  /**
   * Este metodo se invoca cuando se autentica correctamente con pignus. 
   * Desde este espacio se pueden ejecutar diferentes tipos de redirecciones, validaciones y demás.
   *  
   */
  public successAuth() {
    console.log(LoginTestComponent.toString() + ": Autenticado")
    // Cuando se autentique correctamente, es necesario redirigirlo a la ruta que intentaba acceder. O en esta caso siempre al home
    this.router.navigate(['/home']);
  }

}

Como podemos ver en el ejemplo tenemos que implementar dos métodos abstractos, donde cada uno tiene su acción especifica a ejecutar y que se ejecutan en lugares especificos.

Además tenemos que tener en cuenta que existe una buena practica y es la de agregar como propiedad de configuración del entorno la ruta de redirección a Pignus. Ya que para QA es una ruta, para desarrollo es otra y para producción otra.

Para lo cual necesitamos ingresar una entrada en el archivo de src/app/enviroments/enviroment.ts para desarrollo/QA y en src/app/enviroments/enviroment-prod.ts para producción.

Un ejemplo de la configuración de la ruta de redirección es la siguiente:

export const environment = {
  production: false,
  urlLogin : 'http://${direccion-servidor-pignus}/api/?publickey=${public-key-sistema-en-cuestion}'
};

ADVERTENCIA!! NO se debe implementar la interfaz OnInit en el componente de Login ya que esta implementada en la clase abstracta y es usado el Metodo NgOnInit() para ejecutar lógica especifica de autenticación.

Especificacion de la clase PignusLogin | Método | Abstracto | Observaciones | | :------ | :-------- | :------------ | | NgOnInit | No | Metodo sobre escrito desde la interzaf OnInit y es el encargado de iniciar la ejecución del método initalEvaluate | | initalEvaluate | No | Método que ejecuta la lógica necesaria para saber si el usuario esta logueado o no. En caso de tener authcode y no JWT, invoca al metodo exchange del AuthPignusService. Si no tiene authcode, invoca el método abstracto redirectNotAuth, en caso de reconocer que esta todo bien invoca al método abstracto successAuth | | redirectNotAuth | Si | Este metodo se invoca cuando detecta un acceso no permitido a la plataforma. Desde este espacio se debe siempre redirigir a la ruta de login de pignus, pero tambien se pueden ejecutar validaciones de datos, generar carteles, etc. Pero sin redirigir a Pignus no se va a poder utilizar las acciones que sean privadas en un servidor. | | successAuth | Si | Este metodo se invoca cuando se autentica correctamente con pignus. Desde este espacio se pueden ejecutar diferentes tipos de redirecciones, validaciones y demás. |

Interceptor

Por último agregar interceptor, ya que sin agregarlo lo anterior no va a funcionar correctamente. Este es el que llama la ruta /login cuando encuentra un problema de permisos.

@NgModule({
  ...
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: PignusHttpInterceptor,
      multi: true
    }
  ],
  ....
})
export class AppModule { }

Code scaffolding

Run ng generate component component-name --project angular-pjsj to generate a new component. You can also use ng generate directive|pipe|service|class|guard|interface|enum|module --project angular-pjsj.

Note: Don't forget to add --project angular-pjsj or else it will be added to the default project in your angular.json file.

Build

Run ng build angular-pjsj to build the project. The build artifacts will be stored in the dist/ directory.

Publishing

After building your library with ng build angular-pjsj, go to the dist folder cd dist/angular-pjsj and run npm publish.

Running unit tests

Run ng test angular-pjsj to execute the unit tests via Karma.

Further help

To get more help on the Angular CLI use ng help or go check out the Angular CLI README.

12.1.0

8 days ago

12.0.0

27 days ago

11.12.0

29 days ago

11.14.0

29 days ago

11.13.0

29 days ago

11.11.0

2 months ago

11.10.0

5 months ago

11.9.12

5 months ago

11.9.11

5 months ago

11.9.10

5 months ago

11.9.9

5 months ago

11.9.8

5 months ago

11.9.5

1 year ago

11.9.6

1 year ago

11.9.3

1 year ago

11.9.7

1 year ago

11.8.6

2 years ago

11.8.7

2 years ago

11.8.4

2 years ago

11.8.8

2 years ago

11.8.9

2 years ago

11.8.3

2 years ago

0.18.3

2 years ago

11.9.1

2 years ago

11.7.3

2 years ago

11.9.2

2 years ago

11.9.0

2 years ago

0.17.3

2 years ago

11.6.3

2 years ago

11.5.3

2 years ago

0.17.2

2 years ago

11.4.2

2 years ago

11.4.3

2 years ago

11.4.0

2 years ago

11.4.1

2 years ago

11.2.0

2 years ago

11.3.0

2 years ago

0.17.1

3 years ago

0.17.0

3 years ago

0.17.0-B

3 years ago

11.1.0

3 years ago

11.0.1

3 years ago

11.0.1-B

3 years ago

11.0.0

3 years ago

0.16.1-B

3 years ago

0.16.0

3 years ago

0.15.6

4 years ago

0.15.5

4 years ago

0.15.4

4 years ago

0.15.3

4 years ago

0.15.2

4 years ago

0.15.0

4 years ago

0.15.1

4 years ago

0.14.0

4 years ago

0.13.2-B

4 years ago

0.13.1-B

4 years ago

0.13.0

4 years ago

0.13.1

4 years ago

0.12.0

4 years ago

0.11.0

4 years ago

0.10.3

4 years ago

0.10.2

4 years ago

0.10.1

4 years ago

0.10.0

4 years ago

0.9.2

4 years ago

0.9.1

4 years ago

0.9.0

4 years ago

0.8.0

4 years ago

0.7.2

4 years ago

0.7.1

4 years ago

0.7.0

4 years ago

0.6.2

4 years ago

0.6.1

4 years ago

0.6.0

4 years ago

0.5.0

4 years ago

0.4.3

5 years ago

0.4.2

5 years ago

0.3.3

5 years ago

0.3.2

5 years ago

0.3.1

5 years ago

0.3.0

5 years ago

0.1.0

5 years ago