Skip to content

Commit 1398283

Browse files
authored
feat: attachments, query-params plugin & test coverage (#62)
- **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.
1 parent 83cbc47 commit 1398283

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+3020
-1453
lines changed

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
COUCHDB_USER=admin
2+
COUCHDB_PASSWORD=adminadmin
13
POSTGRES_NAME=postgres
24
POSTGRES_USER=postgres
35
POSTGRES_PASSWORD=postgres

.eslintrc.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66
"es2020": true,
77
"node": true
88
},
9-
"extends": [
10-
"plugin:prettier/recommended"
11-
],
9+
"extends": ["plugin:prettier/recommended"],
1210
"parser": "@typescript-eslint/parser",
1311
"parserOptions": {
1412
"project": "tsconfig.json",

README.md

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,19 @@ import { getRxDatabaseCreator } from '@ngx-odm/rxdb/config';
5555
@NgModule({
5656
imports: [
5757
// ... other imports
58-
NgxRxdbModule.forRoot(getRxDatabaseCreator({
59-
name: 'demo', // <- name (required, 'ngx')
60-
storage: getRxStorageDexie(), // <- storage (not required, 'dexie')
61-
localDocuments: true,
62-
multiInstance: true, // <- multiInstance (optional, default: true)
63-
ignoreDuplicate: false,
64-
options: {
65-
storageType: 'dexie|memory', // <- storageType (optional, use if you want defaults provided automatically)
66-
dumpPath: 'assets/dump.json', // path to datbase dump file (optional)
67-
},
68-
})),
58+
NgxRxdbModule.forRoot(
59+
getRxDatabaseCreator({
60+
name: 'demo', // <- name (required, 'ngx')
61+
storage: getRxStorageDexie(), // <- storage (not required, 'dexie')
62+
localDocuments: true,
63+
multiInstance: true, // <- multiInstance (optional, default: true)
64+
ignoreDuplicate: false,
65+
options: {
66+
storageType: 'dexie|memory', // <- storageType (optional, use if you want defaults provided automatically)
67+
dumpPath: 'assets/dump.json', // path to datbase dump file (optional)
68+
},
69+
})
70+
),
6971
],
7072
providers: [],
7173
bootstrap: [AppComponent],
@@ -116,7 +118,9 @@ export class TodosModule {
116118
```typescript
117119
@Injectable()
118120
export class TodosService {
119-
private collectionService: NgxRxdbCollection<Todo> = inject<NgxRxdbCollection<Todo>>(NgxRxdbCollectionService);
121+
private collectionService: NgxRxdbCollection<Todo> = inject<NgxRxdbCollection<Todo>>(
122+
NgxRxdbCollectionService
123+
);
120124
// store & get filter as property of a `local` document
121125
filter$ = this.collectionService
122126
.getLocal('local', 'filterValue')
@@ -172,7 +176,7 @@ export const appConfig: ApplicationConfig = {
172176
localDocuments: true,
173177
multiInstance: true,
174178
ignoreDuplicate: false,
175-
storage: getRxStorageDexie()
179+
storage: getRxStorageDexie(),
176180
})
177181
),
178182
],
@@ -189,7 +193,7 @@ import { provideRxCollection } from '@ngx-odm/rxdb';
189193
@Component({
190194
standalone: true,
191195
// ...
192-
providers: [provideRxCollection(config)]
196+
providers: [provideRxCollection(config)],
193197
})
194198
export class StandaloneComponent {
195199
readonly todoCollection = inject(NgxRxdbCollectionService<Todo>);
@@ -243,7 +247,14 @@ By using this module you can simplify your work with RxDB in Angular application
243247
- optionally provide syncronization with remote db (CouchDB, Kinto etc.) as DB options
244248
- Automatically initialize RxCollection for each _lazy-loaded Feature module / standalone component_ with config
245249
- Work with documents via _NgxRxdbCollectionService_ with unified methods instead of using _RxCollection_ directly (though you still have access to _RxCollection_ and _RxDatabase_ instance)
250+
- simple methods to work database & documents (with queries)
251+
- simple methods to work with local documents
252+
- simple methods to work with attachments
253+
- simple replication sync initialization
246254
- Work with signals and entities with `@ngrx/signals` and `@ngrx/entity` (optionally _zoneless_) (see [example](examples/standalone/src/app/todos/todos.store.ts))
255+
- 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` )
256+
- provide Observable of current URL (automatically for Angular)
257+
- simple methods to set or patch filter, sort, limit, skip
247258

248259
<!-- ## Diagrams
249260
@@ -268,7 +279,6 @@ Project inspired by
268279

269280
## Notes
270281

271-
272282
## Contact
273283

274284
Created by [@voznik](https://github.com/voznik) - feel free to contact me!

docker-compose.couch.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
version: '3'
2+
services:
3+
db:
4+
image: couchdb:3
5+
environment:
6+
COUCHDB_USER: ${COUCHDB_USER}
7+
COUCHDB_PASSWORD: ${COUCHDB_PASSWORD}
8+
ports:
9+
- '5984:5984'
10+
volumes:
11+
- ./tmp/couchdb:/opt/couchdb/data

examples/demo/project.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626
"production": {
2727
"fileReplacements": [
2828
{
29-
"replace": "examples/demo/src/environments/environment.ts",
30-
"with": "examples/demo/src/environments/environment.prod.ts"
29+
"replace": "examples/shared/environment.ts",
30+
"with": "examples/shared/environment.prod.ts"
3131
}
3232
],
3333
"budgets": [

examples/demo/src/app/app.module.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
44
import { RouterModule, Routes } from '@angular/router';
55
import { NgxRxdbModule } from '@ngx-odm/rxdb';
66
import { getRxDatabaseCreator } from '@ngx-odm/rxdb/config';
7+
import { RxDBAttachmentsPlugin } from 'rxdb/plugins/attachments';
8+
import { RxDBLeaderElectionPlugin } from 'rxdb/plugins/leader-election';
79
import { AppComponent } from './app.component';
810

911
const routes: Routes = [
@@ -27,10 +29,15 @@ const routes: Routes = [
2729
NgxRxdbModule.forRoot(
2830
getRxDatabaseCreator({
2931
name: 'demo',
30-
localDocuments: true,
32+
localDocuments: false,
3133
multiInstance: true,
3234
ignoreDuplicate: false,
3335
options: {
36+
plugins: [
37+
// will be loaded by together with core plugins
38+
RxDBAttachmentsPlugin,
39+
RxDBLeaderElectionPlugin,
40+
],
3441
storageType: localStorage['_ngx_rxdb_storage'] ?? 'dexie',
3542
dumpPath: 'assets/data/db.dump.json',
3643
},
Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
/* @import "https://unpkg.com/open-props"; */
2+
13
.clear-completed:disabled {
24
color: #999;
35
cursor: not-allowed;
46
text-decoration: none;
57
}
68

7-
.todo-list li label+.last-modified {
9+
.todo-list li label + .last-modified {
810
position: absolute;
911
bottom: 4px;
1012
right: 24px;
@@ -14,6 +16,40 @@
1416
text-decoration: none !important;
1517
}
1618

17-
.todo-list li:hover label:not(.editing)+.last-modified {
19+
.todo-list li:hover label:not(.editing) + .last-modified {
1820
display: block;
1921
}
22+
23+
dialog {
24+
position: fixed;
25+
top: 50%;
26+
left: 50%;
27+
transform: translate(-50%, -50%);
28+
z-index: 1000;
29+
width: 80%;
30+
max-width: 500px;
31+
padding: 20px;
32+
border: 1px solid #ccc;
33+
box-shadow:
34+
0 2px 4px 0 rgba(0, 0, 0, 0.2),
35+
0 25px 50px 0 rgba(0, 0, 0, 0.1);
36+
background: white;
37+
color: #111111;
38+
font-family: unset;
39+
}
40+
41+
dialog > form {
42+
padding-top: 1em;
43+
border-top: 1px solid #ccc;
44+
width: 100%;
45+
display: flex;
46+
justify-content: space-between;
47+
}
48+
dialog > form > button {
49+
display: block;
50+
}
51+
52+
dialog::backdrop {
53+
/* make the backdrop a semi-transparent black */
54+
background-color: rgba(0, 0, 0, 0.4);
55+
}

examples/demo/src/app/todos/todos.component.html

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ <h1>todos</h1>
3131
<li
3232
*ngFor="let todo of $.todos | byStatus: $.filter; trackBy: trackByFn"
3333
[class.completed]="todo.completed"
34+
(contextmenu)="showContextMenu($event, todo)"
3435
>
3536
<div class="view">
3637
<input
@@ -58,7 +59,7 @@ <h1>todos</h1>
5859
<footer
5960
class="footer"
6061
[hidden]="$.count === 0"
61-
*ngrxLet="($.todos | byStatus: 'ACTIVE')?.length; let remainig"
62+
*ngrxLet="($.todos | byStatus: 'ACTIVE' : true)?.length; let remainig"
6263
>
6364
<ng-container *ngIf="remainig">
6465
<span class="todo-count" [ngPlural]="remainig">
@@ -68,6 +69,12 @@ <h1>todos</h1>
6869
</span>
6970
</ng-container>
7071
<ul class="filters">
72+
<li>
73+
<a href="javascript:void(0);" (click)="todosService.sortTodos('desc')">&#8675;</a>
74+
</li>
75+
<li>
76+
<a href="javascript:void(0);" (click)="todosService.sortTodos('asc')">&#8673;</a>
77+
</li>
7178
<li>
7279
<a
7380
href="javascript:void(0);"
@@ -106,3 +113,42 @@ <h1>todos</h1>
106113
</button>
107114
</footer>
108115
</section>
116+
117+
<dialog [open]="isDialogOpen" class="todo-dialog">
118+
<header>Attachments:</header>
119+
<ul *ngIf="selectedTodo?._attachments">
120+
<li *ngFor="let attachment of selectedTodo._attachments | keyvalue">
121+
<a
122+
href="javascript:void(0);"
123+
(click)="todosService.downloadAttachment(selectedTodo.id, attachment.key)"
124+
>
125+
{{ attachment.key }} - {{ attachment.value.type }}
126+
</a>
127+
&nbsp;
128+
<button
129+
type="button"
130+
class="destroy"
131+
(click)="
132+
todosService.removeAttachment(selectedTodo.id, attachment.key);
133+
isDialogOpen = false
134+
"
135+
>
136+
🗙
137+
</button>
138+
</li>
139+
</ul>
140+
<form method="dialog" (ngSubmit)="selectedTodo = undefined; isDialogOpen = false">
141+
<button type="button" (click)="fileInput.click()">Upload attachment</button>
142+
<input
143+
type="file"
144+
accept=".txt"
145+
#fileInput
146+
style="display: none"
147+
(change)="
148+
todosService.uploadAttachment(selectedTodo.id, $any($event.target).files[0]);
149+
isDialogOpen = false
150+
"
151+
/>
152+
<button>Close Dialog</button>
153+
</form>
154+
</dialog>
Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,15 @@
1-
import { animate, query, stagger, style, transition, trigger } from '@angular/animations';
21
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
32
import { Title } from '@angular/platform-browser';
3+
import { Todo, todosListAnimation } from '@shared';
44
import { Observable, tap } from 'rxjs';
5-
import { Todo } from './todos.model';
65
import { TodosService } from './todos.service';
76

8-
const listAnimation = trigger('listAnimation', [
9-
transition('* <=> *', [
10-
query(
11-
':enter',
12-
[
13-
style({ opacity: 0 }),
14-
stagger('50ms', animate('60ms ease-in', style({ opacity: 1 }))),
15-
],
16-
{ optional: true }
17-
),
18-
query(':leave', stagger('10ms', animate('50ms ease-out', style({ opacity: 0 }))), {
19-
optional: true,
20-
}),
21-
]),
22-
]);
23-
247
@Component({
258
selector: 'demo-todos',
269
templateUrl: './todos.component.html',
2710
styleUrls: ['./todos.component.css'],
2811
changeDetection: ChangeDetectionStrategy.OnPush,
29-
animations: [listAnimation],
12+
animations: [todosListAnimation],
3013
})
3114
export class TodosComponent {
3215
private title = inject(Title);
@@ -42,7 +25,16 @@ export class TodosComponent {
4225
);
4326
count$ = this.todosService.count$;
4427

28+
isDialogOpen = false;
29+
selectedTodo: Todo = undefined;
30+
4531
trackByFn = (index: number, item: Todo) => {
4632
return item.last_modified;
4733
};
34+
35+
showContextMenu(event: Event, todo: Todo) {
36+
event.preventDefault();
37+
this.selectedTodo = todo;
38+
this.isDialogOpen = true;
39+
}
4840
}

examples/demo/src/app/todos/todos.module.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,11 @@ import { RouterModule } from '@angular/router';
55
import { LetDirective, PushPipe } from '@ngrx/component';
66
import { NgxRxdbModule } from '@ngx-odm/rxdb';
77
import { NgxRxdbCollection, NgxRxdbCollectionService } from '@ngx-odm/rxdb/collection';
8+
import { TODOS_COLLECTION_CONFIG, Todo } from '@shared';
89
import { TodosComponent } from './todos.component';
9-
import { TODOS_COLLECTION_CONFIG } from './todos.config';
10-
import { Todo } from './todos.model';
1110
import { TodosPipe } from './todos.pipe';
12-
import { todosReplicationStateFactory } from './todos.replication';
1311
import { TodosService } from './todos.service';
1412

15-
TODOS_COLLECTION_CONFIG.options.replicationStateFactory = todosReplicationStateFactory;
16-
1713
@NgModule({
1814
imports: [
1915
RouterModule.forChild([{ path: '', component: TodosComponent }]),

examples/demo/src/app/todos/todos.pipe.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { Pipe, PipeTransform } from '@angular/core';
2-
import { Todo, TodosFilter } from './todos.model';
1+
import { Pipe, PipeTransform, inject } from '@angular/core';
2+
import { RXDB_CONFIG_COLLECTION } from '@ngx-odm/rxdb/config';
3+
import { Todo, TodosFilter } from '@shared';
34

45
@Pipe({ name: 'byStatus' })
56
export class TodosPipe implements PipeTransform {
6-
transform(value: Todo[], status: TodosFilter): Todo[] {
7-
if (!value) {
7+
colConfig = inject(RXDB_CONFIG_COLLECTION);
8+
transform(value: Todo[], status: TodosFilter, force = false): Todo[] {
9+
if (!value || (this.colConfig.options.useQueryParams && !force)) {
810
return value;
911
}
1012
if (status === 'ALL') {

0 commit comments

Comments
 (0)