Skip to content

Commit

Permalink
feat: attachments, query-params plugin & test coverage (#62)
Browse files Browse the repository at this point in the history
- **New Features**
	- Introduce a plugin for collection filtering functionality with query parameters in the URL for persisting collection queries.
	- Introduce methods in collection service to work with attachments
	- Example: show usage of query-params plugin for filtering documents in Todo collection 
	- Example: Implemented sorting functionality for todo items with ascending and descending options.
	- Example: Added a context menu for todo items with options for managing attachments (upload, download, remove).
- **Enhancements**
	- `withCollectionService` accepts query, subscribes to documents with query (provided or via plugin)
	- `withCollectionService` introduces computed `countAll`, `countFiltered` for use-cases of displaying entities 
- **Bug Fixes**
	- Example: Fixed CouchDB sync 
- **Refactor**
	- Reorganized imports and adjusted module setups across various components and services.
- **Tests**
	- Expanded test coverage for `withCollectionService` and new functionalities like managing attachments and handling query parameters.
  • Loading branch information
voznik authored Feb 29, 2024
1 parent 83cbc47 commit 1398283
Show file tree
Hide file tree
Showing 68 changed files with 3,020 additions and 1,453 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
COUCHDB_USER=admin
COUCHDB_PASSWORD=adminadmin
POSTGRES_NAME=postgres
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
Expand Down
4 changes: 1 addition & 3 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@
"es2020": true,
"node": true
},
"extends": [
"plugin:prettier/recommended"
],
"extends": ["plugin:prettier/recommended"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "tsconfig.json",
Expand Down
40 changes: 25 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,19 @@ import { getRxDatabaseCreator } from '@ngx-odm/rxdb/config';
@NgModule({
imports: [
// ... other imports
NgxRxdbModule.forRoot(getRxDatabaseCreator({
name: 'demo', // <- name (required, 'ngx')
storage: getRxStorageDexie(), // <- storage (not required, 'dexie')
localDocuments: true,
multiInstance: true, // <- multiInstance (optional, default: true)
ignoreDuplicate: false,
options: {
storageType: 'dexie|memory', // <- storageType (optional, use if you want defaults provided automatically)
dumpPath: 'assets/dump.json', // path to datbase dump file (optional)
},
})),
NgxRxdbModule.forRoot(
getRxDatabaseCreator({
name: 'demo', // <- name (required, 'ngx')
storage: getRxStorageDexie(), // <- storage (not required, 'dexie')
localDocuments: true,
multiInstance: true, // <- multiInstance (optional, default: true)
ignoreDuplicate: false,
options: {
storageType: 'dexie|memory', // <- storageType (optional, use if you want defaults provided automatically)
dumpPath: 'assets/dump.json', // path to datbase dump file (optional)
},
})
),
],
providers: [],
bootstrap: [AppComponent],
Expand Down Expand Up @@ -116,7 +118,9 @@ export class TodosModule {
```typescript
@Injectable()
export class TodosService {
private collectionService: NgxRxdbCollection<Todo> = inject<NgxRxdbCollection<Todo>>(NgxRxdbCollectionService);
private collectionService: NgxRxdbCollection<Todo> = inject<NgxRxdbCollection<Todo>>(
NgxRxdbCollectionService
);
// store & get filter as property of a `local` document
filter$ = this.collectionService
.getLocal('local', 'filterValue')
Expand Down Expand Up @@ -172,7 +176,7 @@ export const appConfig: ApplicationConfig = {
localDocuments: true,
multiInstance: true,
ignoreDuplicate: false,
storage: getRxStorageDexie()
storage: getRxStorageDexie(),
})
),
],
Expand All @@ -189,7 +193,7 @@ import { provideRxCollection } from '@ngx-odm/rxdb';
@Component({
standalone: true,
// ...
providers: [provideRxCollection(config)]
providers: [provideRxCollection(config)],
})
export class StandaloneComponent {
readonly todoCollection = inject(NgxRxdbCollectionService<Todo>);
Expand Down Expand Up @@ -243,7 +247,14 @@ By using this module you can simplify your work with RxDB in Angular application
- optionally provide syncronization with remote db (CouchDB, Kinto etc.) as DB options
- Automatically initialize RxCollection for each _lazy-loaded Feature module / standalone component_ with config
- Work with documents via _NgxRxdbCollectionService_ with unified methods instead of using _RxCollection_ directly (though you still have access to _RxCollection_ and _RxDatabase_ instance)
- simple methods to work database & documents (with queries)
- simple methods to work with local documents
- simple methods to work with attachments
- simple replication sync initialization
- Work with signals and entities with `@ngrx/signals` and `@ngrx/entity` (optionally _zoneless_) (see [example](examples/standalone/src/app/todos/todos.store.ts))
- Persist collection query ([mango-query-syntax](https://github.com/cloudant/mango)) in URL with new plugin `query-params-plugin` (in demo, set localStorage `_ngx_rxdb_queryparams` )
- provide Observable of current URL (automatically for Angular)
- simple methods to set or patch filter, sort, limit, skip

<!-- ## Diagrams
Expand All @@ -268,7 +279,6 @@ Project inspired by

## Notes


## Contact

Created by [@voznik](https://github.com/voznik) - feel free to contact me!
11 changes: 11 additions & 0 deletions docker-compose.couch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
version: '3'
services:
db:
image: couchdb:3
environment:
COUCHDB_USER: ${COUCHDB_USER}
COUCHDB_PASSWORD: ${COUCHDB_PASSWORD}
ports:
- '5984:5984'
volumes:
- ./tmp/couchdb:/opt/couchdb/data
4 changes: 2 additions & 2 deletions examples/demo/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
"production": {
"fileReplacements": [
{
"replace": "examples/demo/src/environments/environment.ts",
"with": "examples/demo/src/environments/environment.prod.ts"
"replace": "examples/shared/environment.ts",
"with": "examples/shared/environment.prod.ts"
}
],
"budgets": [
Expand Down
9 changes: 8 additions & 1 deletion examples/demo/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule, Routes } from '@angular/router';
import { NgxRxdbModule } from '@ngx-odm/rxdb';
import { getRxDatabaseCreator } from '@ngx-odm/rxdb/config';
import { RxDBAttachmentsPlugin } from 'rxdb/plugins/attachments';
import { RxDBLeaderElectionPlugin } from 'rxdb/plugins/leader-election';
import { AppComponent } from './app.component';

const routes: Routes = [
Expand All @@ -27,10 +29,15 @@ const routes: Routes = [
NgxRxdbModule.forRoot(
getRxDatabaseCreator({
name: 'demo',
localDocuments: true,
localDocuments: false,
multiInstance: true,
ignoreDuplicate: false,
options: {
plugins: [
// will be loaded by together with core plugins
RxDBAttachmentsPlugin,
RxDBLeaderElectionPlugin,
],
storageType: localStorage['_ngx_rxdb_storage'] ?? 'dexie',
dumpPath: 'assets/data/db.dump.json',
},
Expand Down
40 changes: 38 additions & 2 deletions examples/demo/src/app/todos/todos.component.css
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
/* @import "https://unpkg.com/open-props"; */

.clear-completed:disabled {
color: #999;
cursor: not-allowed;
text-decoration: none;
}

.todo-list li label+.last-modified {
.todo-list li label + .last-modified {
position: absolute;
bottom: 4px;
right: 24px;
Expand All @@ -14,6 +16,40 @@
text-decoration: none !important;
}

.todo-list li:hover label:not(.editing)+.last-modified {
.todo-list li:hover label:not(.editing) + .last-modified {
display: block;
}

dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1000;
width: 80%;
max-width: 500px;
padding: 20px;
border: 1px solid #ccc;
box-shadow:
0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.1);
background: white;
color: #111111;
font-family: unset;
}

dialog > form {
padding-top: 1em;
border-top: 1px solid #ccc;
width: 100%;
display: flex;
justify-content: space-between;
}
dialog > form > button {
display: block;
}

dialog::backdrop {
/* make the backdrop a semi-transparent black */
background-color: rgba(0, 0, 0, 0.4);
}
48 changes: 47 additions & 1 deletion examples/demo/src/app/todos/todos.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ <h1>todos</h1>
<li
*ngFor="let todo of $.todos | byStatus: $.filter; trackBy: trackByFn"
[class.completed]="todo.completed"
(contextmenu)="showContextMenu($event, todo)"
>
<div class="view">
<input
Expand Down Expand Up @@ -58,7 +59,7 @@ <h1>todos</h1>
<footer
class="footer"
[hidden]="$.count === 0"
*ngrxLet="($.todos | byStatus: 'ACTIVE')?.length; let remainig"
*ngrxLet="($.todos | byStatus: 'ACTIVE' : true)?.length; let remainig"
>
<ng-container *ngIf="remainig">
<span class="todo-count" [ngPlural]="remainig">
Expand All @@ -68,6 +69,12 @@ <h1>todos</h1>
</span>
</ng-container>
<ul class="filters">
<li>
<a href="javascript:void(0);" (click)="todosService.sortTodos('desc')">&#8675;</a>
</li>
<li>
<a href="javascript:void(0);" (click)="todosService.sortTodos('asc')">&#8673;</a>
</li>
<li>
<a
href="javascript:void(0);"
Expand Down Expand Up @@ -106,3 +113,42 @@ <h1>todos</h1>
</button>
</footer>
</section>

<dialog [open]="isDialogOpen" class="todo-dialog">
<header>Attachments:</header>
<ul *ngIf="selectedTodo?._attachments">
<li *ngFor="let attachment of selectedTodo._attachments | keyvalue">
<a
href="javascript:void(0);"
(click)="todosService.downloadAttachment(selectedTodo.id, attachment.key)"
>
{{ attachment.key }} - {{ attachment.value.type }}
</a>
&nbsp;
<button
type="button"
class="destroy"
(click)="
todosService.removeAttachment(selectedTodo.id, attachment.key);
isDialogOpen = false
"
>
🗙
</button>
</li>
</ul>
<form method="dialog" (ngSubmit)="selectedTodo = undefined; isDialogOpen = false">
<button type="button" (click)="fileInput.click()">Upload attachment</button>
<input
type="file"
accept=".txt"
#fileInput
style="display: none"
(change)="
todosService.uploadAttachment(selectedTodo.id, $any($event.target).files[0]);
isDialogOpen = false
"
/>
<button>Close Dialog</button>
</form>
</dialog>
30 changes: 11 additions & 19 deletions examples/demo/src/app/todos/todos.component.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,15 @@
import { animate, query, stagger, style, transition, trigger } from '@angular/animations';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { Todo, todosListAnimation } from '@shared';
import { Observable, tap } from 'rxjs';
import { Todo } from './todos.model';
import { TodosService } from './todos.service';

const listAnimation = trigger('listAnimation', [
transition('* <=> *', [
query(
':enter',
[
style({ opacity: 0 }),
stagger('50ms', animate('60ms ease-in', style({ opacity: 1 }))),
],
{ optional: true }
),
query(':leave', stagger('10ms', animate('50ms ease-out', style({ opacity: 0 }))), {
optional: true,
}),
]),
]);

@Component({
selector: 'demo-todos',
templateUrl: './todos.component.html',
styleUrls: ['./todos.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [listAnimation],
animations: [todosListAnimation],
})
export class TodosComponent {
private title = inject(Title);
Expand All @@ -42,7 +25,16 @@ export class TodosComponent {
);
count$ = this.todosService.count$;

isDialogOpen = false;
selectedTodo: Todo = undefined;

trackByFn = (index: number, item: Todo) => {
return item.last_modified;
};

showContextMenu(event: Event, todo: Todo) {
event.preventDefault();
this.selectedTodo = todo;
this.isDialogOpen = true;
}
}
6 changes: 1 addition & 5 deletions examples/demo/src/app/todos/todos.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,11 @@ import { RouterModule } from '@angular/router';
import { LetDirective, PushPipe } from '@ngrx/component';
import { NgxRxdbModule } from '@ngx-odm/rxdb';
import { NgxRxdbCollection, NgxRxdbCollectionService } from '@ngx-odm/rxdb/collection';
import { TODOS_COLLECTION_CONFIG, Todo } from '@shared';
import { TodosComponent } from './todos.component';
import { TODOS_COLLECTION_CONFIG } from './todos.config';
import { Todo } from './todos.model';
import { TodosPipe } from './todos.pipe';
import { todosReplicationStateFactory } from './todos.replication';
import { TodosService } from './todos.service';

TODOS_COLLECTION_CONFIG.options.replicationStateFactory = todosReplicationStateFactory;

@NgModule({
imports: [
RouterModule.forChild([{ path: '', component: TodosComponent }]),
Expand Down
10 changes: 6 additions & 4 deletions examples/demo/src/app/todos/todos.pipe.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Pipe, PipeTransform } from '@angular/core';
import { Todo, TodosFilter } from './todos.model';
import { Pipe, PipeTransform, inject } from '@angular/core';
import { RXDB_CONFIG_COLLECTION } from '@ngx-odm/rxdb/config';
import { Todo, TodosFilter } from '@shared';

@Pipe({ name: 'byStatus' })
export class TodosPipe implements PipeTransform {
transform(value: Todo[], status: TodosFilter): Todo[] {
if (!value) {
colConfig = inject(RXDB_CONFIG_COLLECTION);
transform(value: Todo[], status: TodosFilter, force = false): Todo[] {
if (!value || (this.colConfig.options.useQueryParams && !force)) {
return value;
}
if (status === 'ALL') {
Expand Down
Loading

0 comments on commit 1398283

Please sign in to comment.