@sneat/space-components 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,19 @@
1
- import { Component, ViewChild, inject } from '@angular/core';
1
+ import { ChangeDetectionStrategy, Component, computed, effect, inject, signal, viewChild, } from '@angular/core';
2
+ import { toSignal } from '@angular/core/rxjs-interop';
2
3
  import { FormsModule } from '@angular/forms';
3
4
  import { IonButton, IonButtons, IonCard, IonCardContent, IonCardTitle, IonIcon, IonInput, IonItem, IonItemOption, IonItemOptions, IonItemSliding, IonLabel, IonList, IonSkeletonText, IonSpinner, ToastController, } from '@ionic/angular/standalone';
4
5
  import { AnalyticsService } from '@sneat/core';
5
6
  import { ErrorLogger } from '@sneat/core';
6
7
  import { SpaceNavService, SpaceService } from '@sneat/space-services';
7
8
  import { SneatUserService } from '@sneat/auth-core';
8
- import { Subject } from 'rxjs';
9
- import { takeUntil } from 'rxjs/operators';
10
9
  import * as i0 from "@angular/core";
11
10
  import * as i1 from "@angular/forms";
11
+ // Signal-based + OnPush so the card repaints reactively when the user record
12
+ // loads, instead of mutating fields inside an rxjs subscription and relying on
13
+ // Zone change detection. The previous version stayed stuck on "Authenticating..."
14
+ // when the Firestore onSnapshot update landed outside the Angular zone (the
15
+ // record loaded but the view never repainted). toSignal()/computed() repaint
16
+ // correctly under zone.js too — this is not a zoneless change.
12
17
  export class SpacesCardComponent {
13
18
  constructor() {
14
19
  this.errorLogger = inject(ErrorLogger);
@@ -17,36 +22,29 @@ export class SpacesCardComponent {
17
22
  this.spaceService = inject(SpaceService);
18
23
  this.analyticsService = inject(AnalyticsService);
19
24
  this.toastController = inject(ToastController);
20
- this.loadingState = 'Authenticating';
21
- this.spaceName = '';
22
- this.adding = false;
23
- this.showAdd = false; //
24
- this.destroyed = new Subject();
25
- this.subscriptions = [];
26
- this.setUser = (userState) => {
27
- // console.log('SpacesCardComponent => user:', userState);
28
- const user = userState.record;
29
- if (user) {
30
- this.spaces = Object.entries(user?.spaces ? user.spaces : {}).map(([id, team]) => ({ id, brief: team }));
31
- this.spaces.sort((a, b) => (a.brief.title > b.brief.title ? 1 : -1));
32
- this.showAdd = !this.spaces?.length;
33
- if (this.showAdd) {
34
- this.startAddingSpace();
35
- }
25
+ this.addSpaceInput = viewChild('addTeamInput', ...(ngDevMode ? [{ debugName: "addSpaceInput" }] : []));
26
+ this.userState = toSignal(this.userService.userState);
27
+ // undefined => user record not loaded yet (render the loading row).
28
+ this.spaces = computed(() => {
29
+ const record = this.userState()?.record;
30
+ if (!record) {
31
+ return undefined;
36
32
  }
37
- else {
38
- this.spaces = undefined;
33
+ return Object.entries(record.spaces ?? {})
34
+ .map(([id, brief]) => ({ id, brief }))
35
+ .sort((a, b) => (a.brief.title > b.brief.title ? 1 : -1));
36
+ }, ...(ngDevMode ? [{ debugName: "spaces" }] : []));
37
+ this.loadingState = computed(() => this.userState()?.status === 'authenticated' ? 'Loading' : 'Authenticating', ...(ngDevMode ? [{ debugName: "loadingState" }] : []));
38
+ this.showAdd = signal(false, ...(ngDevMode ? [{ debugName: "showAdd" }] : []));
39
+ this.spaceName = signal('', ...(ngDevMode ? [{ debugName: "spaceName" }] : []));
40
+ this.adding = signal(false, ...(ngDevMode ? [{ debugName: "adding" }] : []));
41
+ // Auto-open the "add space" form once we know the user has no spaces.
42
+ effect(() => {
43
+ const spaces = this.spaces();
44
+ if (spaces && spaces.length === 0) {
45
+ this.startAddingSpace();
39
46
  }
40
- };
41
- }
42
- ngOnDestroy() {
43
- // console.log('SpacesCardComponent.ngOnDestroy()');
44
- this.destroyed.next();
45
- this.destroyed.complete();
46
- this.unsubscribe('ngOnDestroy');
47
- }
48
- ngOnInit() {
49
- this.watchUserRecord();
47
+ });
50
48
  }
51
49
  goSpace(space) {
52
50
  this.navService
@@ -55,82 +53,64 @@ export class SpacesCardComponent {
55
53
  }
56
54
  addSpace() {
57
55
  this.analyticsService.logEvent('addSpace');
58
- const title = this.spaceName.trim();
56
+ const title = this.spaceName().trim();
59
57
  if (!title) {
60
- this.toastController
61
- .create({
62
- position: 'middle',
63
- message: 'Space name is required',
64
- color: 'tertiary',
65
- duration: 5000,
66
- keyboardClose: true,
67
- buttons: [{ role: 'cancel', text: 'OK' }],
68
- })
69
- .then((toast) => toast
70
- .present()
71
- .catch((err) => this.errorLogger.logError(err, 'Failed to present toast')))
72
- .catch((err) => this.errorLogger.logError(err, 'Faile to create toast'));
58
+ this.presentToast('Space name is required', 'tertiary');
73
59
  return;
74
60
  }
75
- if (this.spaces?.find((t) => t.brief.title === title)) {
76
- this.toastController
77
- .create({
78
- message: 'You already have a team with the same name',
79
- color: 'danger',
80
- buttons: ['close'],
81
- position: 'middle',
82
- animated: true,
83
- duration: 3000,
84
- })
85
- .then((toast) => {
86
- toast
87
- .present()
88
- .catch((err) => this.errorLogger.logError(err, 'Failed to present toast'));
89
- })
90
- .catch((err) => this.errorLogger.logError(err, 'Failed to create toast'));
61
+ if (this.spaces()?.find((t) => t.brief.title === title)) {
62
+ this.presentToast('You already have a team with the same name', 'danger');
91
63
  return;
92
64
  }
93
65
  const request = {
94
66
  type: 'team',
95
- // memberType: TeamMemberType.creator,
96
67
  title,
97
68
  };
98
- this.adding = true;
69
+ this.adding.set(true);
99
70
  this.spaceService.createSpace(request).subscribe({
100
71
  next: (space) => {
101
72
  this.analyticsService.logEvent('spaceCreated', { space: space.id });
102
- const userTeamBrief2 = {
103
- userContactID: 'TODO: populate userContactID',
104
- title: space?.dbo?.title || space.id,
105
- roles: ['creator'],
106
- // memberType: request.memberType,
107
- type: space?.dbo?.type || 'unknown',
108
- };
109
- if (userTeamBrief2 && !this.spaces?.find((t) => t.id === space.id)) {
110
- this.spaces?.push({ id: space.id, brief: userTeamBrief2 });
111
- }
112
- this.adding = false;
113
- this.spaceName = '';
73
+ this.adding.set(false);
74
+ this.spaceName.set('');
75
+ // The user record updates via Firestore, which recomputes `spaces`.
114
76
  this.goSpace(space);
115
77
  },
116
78
  error: (err) => {
117
79
  this.errorLogger.logError(err, 'Failed to create new team record');
118
- this.adding = false;
80
+ this.adding.set(false);
119
81
  },
120
82
  });
121
83
  }
84
+ presentToast(message, color) {
85
+ this.toastController
86
+ .create({
87
+ position: 'middle',
88
+ message,
89
+ color,
90
+ duration: 5000,
91
+ keyboardClose: true,
92
+ buttons: [{ role: 'cancel', text: 'OK' }],
93
+ })
94
+ .then((toast) => toast
95
+ .present()
96
+ .catch((err) => this.errorLogger.logError(err, 'Failed to present toast')))
97
+ .catch((err) => this.errorLogger.logError(err, 'Failed to create toast'));
98
+ }
122
99
  startAddingSpace() {
123
- this.showAdd = true;
100
+ this.showAdd.set(true);
124
101
  setTimeout(() => {
125
- if (!this.addSpaceInput) {
126
- this.errorLogger.logError('addTeamInput is not set');
102
+ const input = this.addSpaceInput();
103
+ if (!input) {
127
104
  return;
128
105
  }
129
- this.addSpaceInput
106
+ input
130
107
  .setFocus()
131
- .catch((err) => this.errorLogger.logError(err, 'Failed to set focus to "addTeamInput"'));
108
+ .catch((err) => this.errorLogger.logError(err, 'Failed to set focus to addTeamInput'));
132
109
  }, 200);
133
110
  }
111
+ cancelAdd() {
112
+ this.showAdd.set(false);
113
+ }
134
114
  leaveSpace(space, event) {
135
115
  if (event) {
136
116
  event.stopPropagation();
@@ -149,41 +129,12 @@ export class SpacesCardComponent {
149
129
  error: (err) => this.errorLogger.logError(err, `Failed to leave a space: ${space.brief.title}`),
150
130
  });
151
131
  }
152
- watchUserRecord() {
153
- this.userService.userState.pipe(takeUntil(this.destroyed)).subscribe({
154
- next: (userState) => {
155
- // console.log('SpacesCardComponent => user state changed:', userState);
156
- if (userState.status === 'authenticating') {
157
- if (this.loadingState === 'Authenticating') {
158
- this.loadingState = 'Loading';
159
- }
160
- }
161
- const uid = userState.user?.uid;
162
- this.spaces = undefined;
163
- if (!uid) {
164
- this.unsubscribe('user signed out');
165
- return;
166
- }
167
- this.subscriptions.push(this.userService.userState.subscribe({
168
- next: this.setUser,
169
- error: (err) => this.errorLogger.logError(err, 'Failed to get user record'),
170
- }));
171
- },
172
- error: (err) => this.errorLogger.logError(err, 'Failed to get user ID'),
173
- });
174
- }
175
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
176
- unsubscribe(_reason) {
177
- // console.log(`SpacesCardComponent.unsubscribe(reason: ${reason})`);
178
- this.subscriptions.forEach((s) => s.unsubscribe());
179
- this.subscriptions = [];
180
- }
181
132
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: SpacesCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
182
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: SpacesCardComponent, isStandalone: true, selector: "sneat-spaces-card", viewQueries: [{ propertyName: "addSpaceInput", first: true, predicate: IonInput, descendants: true }], ngImport: i0, template: "<ion-card>\n <ion-item>\n <ion-label>\n <ion-card-title color=\"medium\">Spaces</ion-card-title>\n </ion-label>\n <ion-buttons slot=\"end\" (click)=\"startAddingSpace()\">\n @if (!showAdd) {\n <ion-button color=\"primary\">\n <ion-icon name=\"add\" slot=\"start\" />\n <ion-label>Add</ion-label>\n </ion-button>\n }\n </ion-buttons>\n </ion-item>\n @if (spaces) {\n <ion-list>\n @for (space of spaces; track space.id) {\n <ion-item-sliding>\n <ion-item tappable detail (click)=\"goSpace(space)\">\n <ion-label>{{ space.brief.title }}</ion-label>\n <ion-buttons slot=\"end\">\n <ion-button color=\"medium\" (click)=\"leaveSpace(space, $event)\">\n <ion-icon name=\"close-outline\" />\n </ion-button>\n </ion-buttons>\n </ion-item>\n <ion-item-options side=\"start\">\n <ion-item-option color=\"danger\" (click)=\"leaveSpace(space)\"\n >Leave team\n </ion-item-option>\n </ion-item-options>\n <ion-item-options side=\"end\">\n <ion-item-option color=\"danger\" (click)=\"leaveSpace(space)\"\n >Leave team\n </ion-item-option>\n </ion-item-options>\n </ion-item-sliding>\n }\n </ion-list>\n } @else {\n <ion-list>\n <ion-item>\n <ion-spinner name=\"\" slot=\"start\" color=\"medium\" />\n <ion-buttons slot=\"start\">\n <ion-button disabled=\"disabled\" style=\"text-transform: none\"\n >{{ loadingState }}...\n </ion-button>\n </ion-buttons>\n <ion-skeleton-text animated />\n </ion-item>\n </ion-list>\n }\n\n @if (showAdd) {\n <ion-item [disabled]=\"adding\">\n <ion-input\n (keyup.enter)=\"addSpace()\"\n #addTeamInput\n [(ngModel)]=\"spaceName\"\n (keyup.escape)=\"showAdd = false\"\n placeholder=\"New team name\"\n />\n <ion-buttons slot=\"end\">\n <ion-button color=\"primary\" fill=\"solid\" (click)=\"addSpace()\">\n <ion-label>Create</ion-label>\n </ion-button>\n @if (!!spaces?.length) {\n <ion-button (click)=\"showAdd = false\" color=\"medium\" title=\"Cancel\">\n @if (adding) {\n <ion-spinner />\n } @else {\n <ion-icon name=\"close-outline\" />\n }\n </ion-button>\n }\n </ion-buttons>\n </ion-item>\n }\n @if (showAdd) {\n <ion-card-content>\n <p>Enter team name and click \"Create\" button to add a new team.</p>\n </ion-card-content>\n }\n</ion-card>\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: IonInput, selector: "ion-input", inputs: ["accept", "autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "clearOnEdit", "color", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "max", "maxlength", "min", "minlength", "mode", "multiple", "name", "pattern", "placeholder", "readonly", "required", "shape", "size", "spellcheck", "step", "type", "value"] }, { kind: "component", type: IonCard, selector: "ion-card", inputs: ["button", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: IonCardTitle, selector: "ion-card-title", inputs: ["color", "mode"] }, { kind: "component", type: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonList, selector: "ion-list", inputs: ["inset", "lines", "mode"] }, { kind: "component", type: IonItemSliding, selector: "ion-item-sliding", inputs: ["disabled"] }, { kind: "component", type: IonItemOptions, selector: "ion-item-options", inputs: ["side"] }, { kind: "component", type: IonItemOption, selector: "ion-item-option", inputs: ["color", "disabled", "download", "expandable", "href", "mode", "rel", "target", "type"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: IonSkeletonText, selector: "ion-skeleton-text", inputs: ["animated"] }, { kind: "component", type: IonCardContent, selector: "ion-card-content", inputs: ["mode"] }] }); }
133
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: SpacesCardComponent, isStandalone: true, selector: "sneat-spaces-card", viewQueries: [{ propertyName: "addSpaceInput", first: true, predicate: ["addTeamInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<ion-card>\n <ion-item>\n <ion-label>\n <ion-card-title color=\"medium\">Spaces</ion-card-title>\n </ion-label>\n <ion-buttons slot=\"end\" (click)=\"startAddingSpace()\">\n @if (!showAdd()) {\n <ion-button color=\"primary\">\n <ion-icon name=\"add\" slot=\"start\" />\n <ion-label>Add</ion-label>\n </ion-button>\n }\n </ion-buttons>\n </ion-item>\n @if (spaces(); as spaces) {\n <ion-list>\n @for (space of spaces; track space.id) {\n <ion-item-sliding>\n <ion-item tappable detail (click)=\"goSpace(space)\">\n <ion-label>{{ space.brief.title }}</ion-label>\n <ion-buttons slot=\"end\">\n <ion-button color=\"medium\" (click)=\"leaveSpace(space, $event)\">\n <ion-icon name=\"close-outline\" />\n </ion-button>\n </ion-buttons>\n </ion-item>\n <ion-item-options side=\"start\">\n <ion-item-option color=\"danger\" (click)=\"leaveSpace(space)\"\n >Leave team\n </ion-item-option>\n </ion-item-options>\n <ion-item-options side=\"end\">\n <ion-item-option color=\"danger\" (click)=\"leaveSpace(space)\"\n >Leave team\n </ion-item-option>\n </ion-item-options>\n </ion-item-sliding>\n }\n </ion-list>\n } @else {\n <ion-list>\n <ion-item>\n <ion-spinner name=\"\" slot=\"start\" color=\"medium\" />\n <ion-buttons slot=\"start\">\n <ion-button disabled=\"disabled\" style=\"text-transform: none\"\n >{{ loadingState() }}...\n </ion-button>\n </ion-buttons>\n <ion-skeleton-text animated />\n </ion-item>\n </ion-list>\n }\n\n @if (showAdd()) {\n <ion-item [disabled]=\"adding()\">\n <ion-input\n (keyup.enter)=\"addSpace()\"\n #addTeamInput\n [ngModel]=\"spaceName()\"\n (ngModelChange)=\"spaceName.set($event)\"\n (keyup.escape)=\"cancelAdd()\"\n placeholder=\"New team name\"\n />\n <ion-buttons slot=\"end\">\n <ion-button color=\"primary\" fill=\"solid\" (click)=\"addSpace()\">\n <ion-label>Create</ion-label>\n </ion-button>\n @if (!!spaces()?.length) {\n <ion-button (click)=\"cancelAdd()\" color=\"medium\" title=\"Cancel\">\n @if (adding()) {\n <ion-spinner />\n } @else {\n <ion-icon name=\"close-outline\" />\n }\n </ion-button>\n }\n </ion-buttons>\n </ion-item>\n }\n @if (showAdd()) {\n <ion-card-content>\n <p>Enter team name and click \"Create\" button to add a new team.</p>\n </ion-card-content>\n }\n</ion-card>\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: IonInput, selector: "ion-input", inputs: ["accept", "autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "clearOnEdit", "color", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "max", "maxlength", "min", "minlength", "mode", "multiple", "name", "pattern", "placeholder", "readonly", "required", "shape", "size", "spellcheck", "step", "type", "value"] }, { kind: "component", type: IonCard, selector: "ion-card", inputs: ["button", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: IonCardTitle, selector: "ion-card-title", inputs: ["color", "mode"] }, { kind: "component", type: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonList, selector: "ion-list", inputs: ["inset", "lines", "mode"] }, { kind: "component", type: IonItemSliding, selector: "ion-item-sliding", inputs: ["disabled"] }, { kind: "component", type: IonItemOptions, selector: "ion-item-options", inputs: ["side"] }, { kind: "component", type: IonItemOption, selector: "ion-item-option", inputs: ["color", "disabled", "download", "expandable", "href", "mode", "rel", "target", "type"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: IonSkeletonText, selector: "ion-skeleton-text", inputs: ["animated"] }, { kind: "component", type: IonCardContent, selector: "ion-card-content", inputs: ["mode"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
183
134
  }
184
135
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: SpacesCardComponent, decorators: [{
185
136
  type: Component,
186
- args: [{ selector: 'sneat-spaces-card', imports: [
137
+ args: [{ selector: 'sneat-spaces-card', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
187
138
  FormsModule,
188
139
  IonInput,
189
140
  IonCard,
@@ -200,9 +151,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
200
151
  IonSpinner,
201
152
  IonSkeletonText,
202
153
  IonCardContent,
203
- ], template: "<ion-card>\n <ion-item>\n <ion-label>\n <ion-card-title color=\"medium\">Spaces</ion-card-title>\n </ion-label>\n <ion-buttons slot=\"end\" (click)=\"startAddingSpace()\">\n @if (!showAdd) {\n <ion-button color=\"primary\">\n <ion-icon name=\"add\" slot=\"start\" />\n <ion-label>Add</ion-label>\n </ion-button>\n }\n </ion-buttons>\n </ion-item>\n @if (spaces) {\n <ion-list>\n @for (space of spaces; track space.id) {\n <ion-item-sliding>\n <ion-item tappable detail (click)=\"goSpace(space)\">\n <ion-label>{{ space.brief.title }}</ion-label>\n <ion-buttons slot=\"end\">\n <ion-button color=\"medium\" (click)=\"leaveSpace(space, $event)\">\n <ion-icon name=\"close-outline\" />\n </ion-button>\n </ion-buttons>\n </ion-item>\n <ion-item-options side=\"start\">\n <ion-item-option color=\"danger\" (click)=\"leaveSpace(space)\"\n >Leave team\n </ion-item-option>\n </ion-item-options>\n <ion-item-options side=\"end\">\n <ion-item-option color=\"danger\" (click)=\"leaveSpace(space)\"\n >Leave team\n </ion-item-option>\n </ion-item-options>\n </ion-item-sliding>\n }\n </ion-list>\n } @else {\n <ion-list>\n <ion-item>\n <ion-spinner name=\"\" slot=\"start\" color=\"medium\" />\n <ion-buttons slot=\"start\">\n <ion-button disabled=\"disabled\" style=\"text-transform: none\"\n >{{ loadingState }}...\n </ion-button>\n </ion-buttons>\n <ion-skeleton-text animated />\n </ion-item>\n </ion-list>\n }\n\n @if (showAdd) {\n <ion-item [disabled]=\"adding\">\n <ion-input\n (keyup.enter)=\"addSpace()\"\n #addTeamInput\n [(ngModel)]=\"spaceName\"\n (keyup.escape)=\"showAdd = false\"\n placeholder=\"New team name\"\n />\n <ion-buttons slot=\"end\">\n <ion-button color=\"primary\" fill=\"solid\" (click)=\"addSpace()\">\n <ion-label>Create</ion-label>\n </ion-button>\n @if (!!spaces?.length) {\n <ion-button (click)=\"showAdd = false\" color=\"medium\" title=\"Cancel\">\n @if (adding) {\n <ion-spinner />\n } @else {\n <ion-icon name=\"close-outline\" />\n }\n </ion-button>\n }\n </ion-buttons>\n </ion-item>\n }\n @if (showAdd) {\n <ion-card-content>\n <p>Enter team name and click \"Create\" button to add a new team.</p>\n </ion-card-content>\n }\n</ion-card>\n" }]
204
- }], propDecorators: { addSpaceInput: [{
205
- type: ViewChild,
206
- args: [IonInput, { static: false }]
207
- }] } });
154
+ ], template: "<ion-card>\n <ion-item>\n <ion-label>\n <ion-card-title color=\"medium\">Spaces</ion-card-title>\n </ion-label>\n <ion-buttons slot=\"end\" (click)=\"startAddingSpace()\">\n @if (!showAdd()) {\n <ion-button color=\"primary\">\n <ion-icon name=\"add\" slot=\"start\" />\n <ion-label>Add</ion-label>\n </ion-button>\n }\n </ion-buttons>\n </ion-item>\n @if (spaces(); as spaces) {\n <ion-list>\n @for (space of spaces; track space.id) {\n <ion-item-sliding>\n <ion-item tappable detail (click)=\"goSpace(space)\">\n <ion-label>{{ space.brief.title }}</ion-label>\n <ion-buttons slot=\"end\">\n <ion-button color=\"medium\" (click)=\"leaveSpace(space, $event)\">\n <ion-icon name=\"close-outline\" />\n </ion-button>\n </ion-buttons>\n </ion-item>\n <ion-item-options side=\"start\">\n <ion-item-option color=\"danger\" (click)=\"leaveSpace(space)\"\n >Leave team\n </ion-item-option>\n </ion-item-options>\n <ion-item-options side=\"end\">\n <ion-item-option color=\"danger\" (click)=\"leaveSpace(space)\"\n >Leave team\n </ion-item-option>\n </ion-item-options>\n </ion-item-sliding>\n }\n </ion-list>\n } @else {\n <ion-list>\n <ion-item>\n <ion-spinner name=\"\" slot=\"start\" color=\"medium\" />\n <ion-buttons slot=\"start\">\n <ion-button disabled=\"disabled\" style=\"text-transform: none\"\n >{{ loadingState() }}...\n </ion-button>\n </ion-buttons>\n <ion-skeleton-text animated />\n </ion-item>\n </ion-list>\n }\n\n @if (showAdd()) {\n <ion-item [disabled]=\"adding()\">\n <ion-input\n (keyup.enter)=\"addSpace()\"\n #addTeamInput\n [ngModel]=\"spaceName()\"\n (ngModelChange)=\"spaceName.set($event)\"\n (keyup.escape)=\"cancelAdd()\"\n placeholder=\"New team name\"\n />\n <ion-buttons slot=\"end\">\n <ion-button color=\"primary\" fill=\"solid\" (click)=\"addSpace()\">\n <ion-label>Create</ion-label>\n </ion-button>\n @if (!!spaces()?.length) {\n <ion-button (click)=\"cancelAdd()\" color=\"medium\" title=\"Cancel\">\n @if (adding()) {\n <ion-spinner />\n } @else {\n <ion-icon name=\"close-outline\" />\n }\n </ion-button>\n }\n </ion-buttons>\n </ion-item>\n }\n @if (showAdd()) {\n <ion-card-content>\n <p>Enter team name and click \"Create\" button to add a new team.</p>\n </ion-card-content>\n }\n</ion-card>\n" }]
155
+ }], ctorParameters: () => [], propDecorators: { addSpaceInput: [{ type: i0.ViewChild, args: ['addTeamInput', { isSignal: true }] }] } });
208
156
  //# sourceMappingURL=spaces-card.component.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"spaces-card.component.js","sourceRoot":"","sources":["../../../../../../../libs/space/components/src/lib/spaces-card/spaces-card.component.ts","../../../../../../../libs/space/components/src/lib/spaces-card/spaces-card.component.html"],"names":[],"mappings":"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","sourcesContent":["import { Component, OnDestroy, OnInit, ViewChild, inject } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport {\n IonButton,\n IonButtons,\n IonCard,\n IonCardContent,\n IonCardTitle,\n IonIcon,\n IonInput,\n IonItem,\n IonItemOption,\n IonItemOptions,\n IonItemSliding,\n IonLabel,\n IonList,\n IonSkeletonText,\n IonSpinner,\n ToastController,\n} from '@ionic/angular/standalone';\nimport { AnalyticsService, IAnalyticsService } from '@sneat/core';\nimport { IUserSpaceBrief } from '@sneat/auth-models';\nimport { IIdAndBrief } from '@sneat/core';\nimport { ErrorLogger, IErrorLogger } from '@sneat/core';\nimport { ICreateSpaceRequest, ISpaceContext } from '@sneat/space-models';\nimport { SpaceNavService, SpaceService } from '@sneat/space-services';\nimport { ISneatUserState, SneatUserService } from '@sneat/auth-core';\nimport { Subject, Subscription } from 'rxjs';\nimport { takeUntil } from 'rxjs/operators';\n\n@Component({\n selector: 'sneat-spaces-card',\n templateUrl: './spaces-card.component.html',\n imports: [\n FormsModule,\n IonInput,\n IonCard,\n IonItem,\n IonLabel,\n IonCardTitle,\n IonButtons,\n IonButton,\n IonIcon,\n IonList,\n IonItemSliding,\n IonItemOptions,\n IonItemOption,\n IonSpinner,\n IonSkeletonText,\n IonCardContent,\n ],\n})\nexport class SpacesCardComponent implements OnInit, OnDestroy {\n private readonly errorLogger = inject<IErrorLogger>(ErrorLogger);\n private readonly navService = inject(SpaceNavService);\n private readonly userService = inject(SneatUserService);\n private readonly spaceService = inject(SpaceService);\n private readonly analyticsService =\n inject<IAnalyticsService>(AnalyticsService);\n private readonly toastController = inject(ToastController);\n\n @ViewChild(IonInput, { static: false }) addSpaceInput?: IonInput; // TODO: IonInput;\n\n public spaces?: IIdAndBrief<IUserSpaceBrief>[];\n public loadingState: 'Authenticating' | 'Loading' = 'Authenticating';\n public spaceName = '';\n public adding = false;\n public showAdd = false; //\n private readonly destroyed = new Subject<void>();\n private subscriptions: Subscription[] = [];\n\n public ngOnDestroy(): void {\n // console.log('SpacesCardComponent.ngOnDestroy()');\n this.destroyed.next();\n this.destroyed.complete();\n this.unsubscribe('ngOnDestroy');\n }\n\n public ngOnInit(): void {\n this.watchUserRecord();\n }\n\n public goSpace(space: ISpaceContext) {\n this.navService\n .navigateToSpace(space, 'forward')\n .catch(this.errorLogger.logError);\n }\n\n public addSpace() {\n this.analyticsService.logEvent('addSpace');\n const title = this.spaceName.trim();\n if (!title) {\n this.toastController\n .create({\n position: 'middle',\n message: 'Space name is required',\n color: 'tertiary',\n duration: 5000,\n keyboardClose: true,\n buttons: [{ role: 'cancel', text: 'OK' }],\n })\n .then((toast) =>\n toast\n .present()\n .catch((err) =>\n this.errorLogger.logError(err, 'Failed to present toast'),\n ),\n )\n .catch((err) =>\n this.errorLogger.logError(err, 'Faile to create toast'),\n );\n return;\n }\n if (this.spaces?.find((t) => t.brief.title === title)) {\n this.toastController\n .create({\n message: 'You already have a team with the same name',\n color: 'danger',\n buttons: ['close'],\n position: 'middle',\n animated: true,\n duration: 3000,\n })\n .then((toast) => {\n toast\n .present()\n .catch((err) =>\n this.errorLogger.logError(err, 'Failed to present toast'),\n );\n })\n .catch((err) =>\n this.errorLogger.logError(err, 'Failed to create toast'),\n );\n return;\n }\n const request: ICreateSpaceRequest = {\n type: 'team',\n // memberType: TeamMemberType.creator,\n title,\n };\n this.adding = true;\n this.spaceService.createSpace(request).subscribe({\n next: (space) => {\n this.analyticsService.logEvent('spaceCreated', { space: space.id });\n const userTeamBrief2: IUserSpaceBrief = {\n userContactID: 'TODO: populate userContactID',\n title: space?.dbo?.title || space.id,\n roles: ['creator'],\n // memberType: request.memberType,\n type: space?.dbo?.type || 'unknown',\n };\n if (userTeamBrief2 && !this.spaces?.find((t) => t.id === space.id)) {\n this.spaces?.push({ id: space.id, brief: userTeamBrief2 });\n }\n this.adding = false;\n this.spaceName = '';\n this.goSpace(space);\n },\n error: (err) => {\n this.errorLogger.logError(err, 'Failed to create new team record');\n this.adding = false;\n },\n });\n }\n\n public startAddingSpace(): void {\n this.showAdd = true;\n setTimeout(() => {\n if (!this.addSpaceInput) {\n this.errorLogger.logError('addTeamInput is not set');\n return;\n }\n this.addSpaceInput\n .setFocus()\n .catch((err) =>\n this.errorLogger.logError(\n err,\n 'Failed to set focus to \"addTeamInput\"',\n ),\n );\n }, 200);\n }\n\n public leaveSpace(space: IIdAndBrief<IUserSpaceBrief>, event?: Event): void {\n if (event) {\n event.stopPropagation();\n event.preventDefault();\n }\n if (!confirm(`Are you sure you want to leave team ${space.brief.title}?`)) {\n return;\n }\n const userID = this.userService.currentUserID;\n if (!userID) {\n this.errorLogger.logError('Failed to get current user ID');\n return;\n }\n this.spaceService.leaveSpace({ spaceID: space.id }).subscribe({\n next: () => console.log('left space'),\n error: (err: unknown) =>\n this.errorLogger.logError(\n err,\n `Failed to leave a space: ${space.brief.title}`,\n ),\n });\n }\n\n private watchUserRecord(): void {\n this.userService.userState.pipe(takeUntil(this.destroyed)).subscribe({\n next: (userState) => {\n // console.log('SpacesCardComponent => user state changed:', userState);\n if (userState.status === 'authenticating') {\n if (this.loadingState === 'Authenticating') {\n this.loadingState = 'Loading';\n }\n }\n const uid = userState.user?.uid;\n this.spaces = undefined;\n if (!uid) {\n this.unsubscribe('user signed out');\n return;\n }\n this.subscriptions.push(\n this.userService.userState.subscribe({\n next: this.setUser,\n error: (err) =>\n this.errorLogger.logError(err, 'Failed to get user record'),\n }),\n );\n },\n error: (err) => this.errorLogger.logError(err, 'Failed to get user ID'),\n });\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n private unsubscribe(_reason?: string): void {\n // console.log(`SpacesCardComponent.unsubscribe(reason: ${reason})`);\n this.subscriptions.forEach((s) => s.unsubscribe());\n this.subscriptions = [];\n }\n\n private setUser = (userState: ISneatUserState): void => {\n // console.log('SpacesCardComponent => user:', userState);\n const user = userState.record;\n if (user) {\n this.spaces = Object.entries(user?.spaces ? user.spaces : {}).map(\n ([id, team]) => ({ id, brief: team }),\n );\n this.spaces.sort((a, b) => (a.brief.title > b.brief.title ? 1 : -1));\n this.showAdd = !this.spaces?.length;\n if (this.showAdd) {\n this.startAddingSpace();\n }\n } else {\n this.spaces = undefined;\n }\n };\n}\n","<ion-card>\n <ion-item>\n <ion-label>\n <ion-card-title color=\"medium\">Spaces</ion-card-title>\n </ion-label>\n <ion-buttons slot=\"end\" (click)=\"startAddingSpace()\">\n @if (!showAdd) {\n <ion-button color=\"primary\">\n <ion-icon name=\"add\" slot=\"start\" />\n <ion-label>Add</ion-label>\n </ion-button>\n }\n </ion-buttons>\n </ion-item>\n @if (spaces) {\n <ion-list>\n @for (space of spaces; track space.id) {\n <ion-item-sliding>\n <ion-item tappable detail (click)=\"goSpace(space)\">\n <ion-label>{{ space.brief.title }}</ion-label>\n <ion-buttons slot=\"end\">\n <ion-button color=\"medium\" (click)=\"leaveSpace(space, $event)\">\n <ion-icon name=\"close-outline\" />\n </ion-button>\n </ion-buttons>\n </ion-item>\n <ion-item-options side=\"start\">\n <ion-item-option color=\"danger\" (click)=\"leaveSpace(space)\"\n >Leave team\n </ion-item-option>\n </ion-item-options>\n <ion-item-options side=\"end\">\n <ion-item-option color=\"danger\" (click)=\"leaveSpace(space)\"\n >Leave team\n </ion-item-option>\n </ion-item-options>\n </ion-item-sliding>\n }\n </ion-list>\n } @else {\n <ion-list>\n <ion-item>\n <ion-spinner name=\"\" slot=\"start\" color=\"medium\" />\n <ion-buttons slot=\"start\">\n <ion-button disabled=\"disabled\" style=\"text-transform: none\"\n >{{ loadingState }}...\n </ion-button>\n </ion-buttons>\n <ion-skeleton-text animated />\n </ion-item>\n </ion-list>\n }\n\n @if (showAdd) {\n <ion-item [disabled]=\"adding\">\n <ion-input\n (keyup.enter)=\"addSpace()\"\n #addTeamInput\n [(ngModel)]=\"spaceName\"\n (keyup.escape)=\"showAdd = false\"\n placeholder=\"New team name\"\n />\n <ion-buttons slot=\"end\">\n <ion-button color=\"primary\" fill=\"solid\" (click)=\"addSpace()\">\n <ion-label>Create</ion-label>\n </ion-button>\n @if (!!spaces?.length) {\n <ion-button (click)=\"showAdd = false\" color=\"medium\" title=\"Cancel\">\n @if (adding) {\n <ion-spinner />\n } @else {\n <ion-icon name=\"close-outline\" />\n }\n </ion-button>\n }\n </ion-buttons>\n </ion-item>\n }\n @if (showAdd) {\n <ion-card-content>\n <p>Enter team name and click \"Create\" button to add a new team.</p>\n </ion-card-content>\n }\n</ion-card>\n"]}
1
+ {"version":3,"file":"spaces-card.component.js","sourceRoot":"","sources":["../../../../../../../libs/space/components/src/lib/spaces-card/spaces-card.component.ts","../../../../../../../libs/space/components/src/lib/spaces-card/spaces-card.component.html"],"names":[],"mappings":"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","sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n computed,\n effect,\n inject,\n signal,\n viewChild,\n} from '@angular/core';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { FormsModule } from '@angular/forms';\nimport {\n IonButton,\n IonButtons,\n IonCard,\n IonCardContent,\n IonCardTitle,\n IonIcon,\n IonInput,\n IonItem,\n IonItemOption,\n IonItemOptions,\n IonItemSliding,\n IonLabel,\n IonList,\n IonSkeletonText,\n IonSpinner,\n ToastController,\n} from '@ionic/angular/standalone';\nimport { AnalyticsService, IAnalyticsService } from '@sneat/core';\nimport { IUserSpaceBrief } from '@sneat/auth-models';\nimport { IIdAndBrief } from '@sneat/core';\nimport { ErrorLogger, IErrorLogger } from '@sneat/core';\nimport { ICreateSpaceRequest, ISpaceContext } from '@sneat/space-models';\nimport { SpaceNavService, SpaceService } from '@sneat/space-services';\nimport { SneatUserService } from '@sneat/auth-core';\n\n// Signal-based + OnPush so the card repaints reactively when the user record\n// loads, instead of mutating fields inside an rxjs subscription and relying on\n// Zone change detection. The previous version stayed stuck on \"Authenticating...\"\n// when the Firestore onSnapshot update landed outside the Angular zone (the\n// record loaded but the view never repainted). toSignal()/computed() repaint\n// correctly under zone.js too — this is not a zoneless change.\n@Component({\n selector: 'sneat-spaces-card',\n templateUrl: './spaces-card.component.html',\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [\n FormsModule,\n IonInput,\n IonCard,\n IonItem,\n IonLabel,\n IonCardTitle,\n IonButtons,\n IonButton,\n IonIcon,\n IonList,\n IonItemSliding,\n IonItemOptions,\n IonItemOption,\n IonSpinner,\n IonSkeletonText,\n IonCardContent,\n ],\n})\nexport class SpacesCardComponent {\n private readonly errorLogger = inject<IErrorLogger>(ErrorLogger);\n private readonly navService = inject(SpaceNavService);\n private readonly userService = inject(SneatUserService);\n private readonly spaceService = inject(SpaceService);\n private readonly analyticsService =\n inject<IAnalyticsService>(AnalyticsService);\n private readonly toastController = inject(ToastController);\n\n private readonly addSpaceInput = viewChild<IonInput>('addTeamInput');\n\n private readonly userState = toSignal(this.userService.userState);\n\n // undefined => user record not loaded yet (render the loading row).\n protected readonly spaces = computed<\n IIdAndBrief<IUserSpaceBrief>[] | undefined\n >(() => {\n const record = this.userState()?.record;\n if (!record) {\n return undefined;\n }\n return Object.entries(record.spaces ?? {})\n .map(([id, brief]) => ({ id, brief }))\n .sort((a, b) => (a.brief.title > b.brief.title ? 1 : -1));\n });\n\n protected readonly loadingState = computed(() =>\n this.userState()?.status === 'authenticated' ? 'Loading' : 'Authenticating',\n );\n\n protected readonly showAdd = signal(false);\n protected readonly spaceName = signal('');\n protected readonly adding = signal(false);\n\n public constructor() {\n // Auto-open the \"add space\" form once we know the user has no spaces.\n effect(() => {\n const spaces = this.spaces();\n if (spaces && spaces.length === 0) {\n this.startAddingSpace();\n }\n });\n }\n\n protected goSpace(space: ISpaceContext): void {\n this.navService\n .navigateToSpace(space, 'forward')\n .catch(this.errorLogger.logError);\n }\n\n protected addSpace(): void {\n this.analyticsService.logEvent('addSpace');\n const title = this.spaceName().trim();\n if (!title) {\n this.presentToast('Space name is required', 'tertiary');\n return;\n }\n if (this.spaces()?.find((t) => t.brief.title === title)) {\n this.presentToast('You already have a team with the same name', 'danger');\n return;\n }\n const request: ICreateSpaceRequest = {\n type: 'team',\n title,\n };\n this.adding.set(true);\n this.spaceService.createSpace(request).subscribe({\n next: (space) => {\n this.analyticsService.logEvent('spaceCreated', { space: space.id });\n this.adding.set(false);\n this.spaceName.set('');\n // The user record updates via Firestore, which recomputes `spaces`.\n this.goSpace(space);\n },\n error: (err) => {\n this.errorLogger.logError(err, 'Failed to create new team record');\n this.adding.set(false);\n },\n });\n }\n\n private presentToast(message: string, color: string): void {\n this.toastController\n .create({\n position: 'middle',\n message,\n color,\n duration: 5000,\n keyboardClose: true,\n buttons: [{ role: 'cancel', text: 'OK' }],\n })\n .then((toast) =>\n toast\n .present()\n .catch((err) =>\n this.errorLogger.logError(err, 'Failed to present toast'),\n ),\n )\n .catch((err) => this.errorLogger.logError(err, 'Failed to create toast'));\n }\n\n public startAddingSpace(): void {\n this.showAdd.set(true);\n setTimeout(() => {\n const input = this.addSpaceInput();\n if (!input) {\n return;\n }\n input\n .setFocus()\n .catch((err) =>\n this.errorLogger.logError(err, 'Failed to set focus to addTeamInput'),\n );\n }, 200);\n }\n\n protected cancelAdd(): void {\n this.showAdd.set(false);\n }\n\n protected leaveSpace(\n space: IIdAndBrief<IUserSpaceBrief>,\n event?: Event,\n ): void {\n if (event) {\n event.stopPropagation();\n event.preventDefault();\n }\n if (!confirm(`Are you sure you want to leave team ${space.brief.title}?`)) {\n return;\n }\n const userID = this.userService.currentUserID;\n if (!userID) {\n this.errorLogger.logError('Failed to get current user ID');\n return;\n }\n this.spaceService.leaveSpace({ spaceID: space.id }).subscribe({\n next: () => console.log('left space'),\n error: (err: unknown) =>\n this.errorLogger.logError(\n err,\n `Failed to leave a space: ${space.brief.title}`,\n ),\n });\n }\n}\n","<ion-card>\n <ion-item>\n <ion-label>\n <ion-card-title color=\"medium\">Spaces</ion-card-title>\n </ion-label>\n <ion-buttons slot=\"end\" (click)=\"startAddingSpace()\">\n @if (!showAdd()) {\n <ion-button color=\"primary\">\n <ion-icon name=\"add\" slot=\"start\" />\n <ion-label>Add</ion-label>\n </ion-button>\n }\n </ion-buttons>\n </ion-item>\n @if (spaces(); as spaces) {\n <ion-list>\n @for (space of spaces; track space.id) {\n <ion-item-sliding>\n <ion-item tappable detail (click)=\"goSpace(space)\">\n <ion-label>{{ space.brief.title }}</ion-label>\n <ion-buttons slot=\"end\">\n <ion-button color=\"medium\" (click)=\"leaveSpace(space, $event)\">\n <ion-icon name=\"close-outline\" />\n </ion-button>\n </ion-buttons>\n </ion-item>\n <ion-item-options side=\"start\">\n <ion-item-option color=\"danger\" (click)=\"leaveSpace(space)\"\n >Leave team\n </ion-item-option>\n </ion-item-options>\n <ion-item-options side=\"end\">\n <ion-item-option color=\"danger\" (click)=\"leaveSpace(space)\"\n >Leave team\n </ion-item-option>\n </ion-item-options>\n </ion-item-sliding>\n }\n </ion-list>\n } @else {\n <ion-list>\n <ion-item>\n <ion-spinner name=\"\" slot=\"start\" color=\"medium\" />\n <ion-buttons slot=\"start\">\n <ion-button disabled=\"disabled\" style=\"text-transform: none\"\n >{{ loadingState() }}...\n </ion-button>\n </ion-buttons>\n <ion-skeleton-text animated />\n </ion-item>\n </ion-list>\n }\n\n @if (showAdd()) {\n <ion-item [disabled]=\"adding()\">\n <ion-input\n (keyup.enter)=\"addSpace()\"\n #addTeamInput\n [ngModel]=\"spaceName()\"\n (ngModelChange)=\"spaceName.set($event)\"\n (keyup.escape)=\"cancelAdd()\"\n placeholder=\"New team name\"\n />\n <ion-buttons slot=\"end\">\n <ion-button color=\"primary\" fill=\"solid\" (click)=\"addSpace()\">\n <ion-label>Create</ion-label>\n </ion-button>\n @if (!!spaces()?.length) {\n <ion-button (click)=\"cancelAdd()\" color=\"medium\" title=\"Cancel\">\n @if (adding()) {\n <ion-spinner />\n } @else {\n <ion-icon name=\"close-outline\" />\n }\n </ion-button>\n }\n </ion-buttons>\n </ion-item>\n }\n @if (showAdd()) {\n <ion-card-content>\n <p>Enter team name and click \"Create\" button to add a new team.</p>\n </ion-card-content>\n }\n</ion-card>\n"]}
@@ -1,33 +1,28 @@
1
- import { OnDestroy, OnInit } from '@angular/core';
2
- import { IonInput } from '@ionic/angular/standalone';
3
1
  import { IUserSpaceBrief } from '@sneat/auth-models';
4
2
  import { IIdAndBrief } from '@sneat/core';
5
3
  import { ISpaceContext } from '@sneat/space-models';
6
4
  import * as i0 from "@angular/core";
7
- export declare class SpacesCardComponent implements OnInit, OnDestroy {
5
+ export declare class SpacesCardComponent {
8
6
  private readonly errorLogger;
9
7
  private readonly navService;
10
8
  private readonly userService;
11
9
  private readonly spaceService;
12
10
  private readonly analyticsService;
13
11
  private readonly toastController;
14
- addSpaceInput?: IonInput;
15
- spaces?: IIdAndBrief<IUserSpaceBrief>[];
16
- loadingState: 'Authenticating' | 'Loading';
17
- spaceName: string;
18
- adding: boolean;
19
- showAdd: boolean;
20
- private readonly destroyed;
21
- private subscriptions;
22
- ngOnDestroy(): void;
23
- ngOnInit(): void;
24
- goSpace(space: ISpaceContext): void;
25
- addSpace(): void;
12
+ private readonly addSpaceInput;
13
+ private readonly userState;
14
+ protected readonly spaces: import("@angular/core").Signal<IIdAndBrief<IUserSpaceBrief>[] | undefined>;
15
+ protected readonly loadingState: import("@angular/core").Signal<"Loading" | "Authenticating">;
16
+ protected readonly showAdd: import("@angular/core").WritableSignal<boolean>;
17
+ protected readonly spaceName: import("@angular/core").WritableSignal<string>;
18
+ protected readonly adding: import("@angular/core").WritableSignal<boolean>;
19
+ constructor();
20
+ protected goSpace(space: ISpaceContext): void;
21
+ protected addSpace(): void;
22
+ private presentToast;
26
23
  startAddingSpace(): void;
27
- leaveSpace(space: IIdAndBrief<IUserSpaceBrief>, event?: Event): void;
28
- private watchUserRecord;
29
- private unsubscribe;
30
- private setUser;
24
+ protected cancelAdd(): void;
25
+ protected leaveSpace(space: IIdAndBrief<IUserSpaceBrief>, event?: Event): void;
31
26
  static ɵfac: i0.ɵɵFactoryDeclaration<SpacesCardComponent, never>;
32
27
  static ɵcmp: i0.ɵɵComponentDeclaration<SpacesCardComponent, "sneat-spaces-card", never, {}, {}, never, never, true, never>;
33
28
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sneat/space-components",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },