@memberjunction/ng-list-management 5.40.1 → 5.41.0

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.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=list-invitations.component.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-invitations.component.test.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/list-invitations/__tests__/list-invitations.component.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,145 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ /**
3
+ * Recompute-trigger coverage for ListInvitationsComponent.visibleInvitations
4
+ * (PR #2841): the precomputed per-tab list is rebuilt only when the source data
5
+ * (`invitations`) or the active tab changes — NOT on every change-detection pass
6
+ * (a getter there would allocate a fresh map+filter each CD tick).
7
+ *
8
+ * The recompute also hydrates the Pending→Expired client boundary: a Pending
9
+ * invitation whose ExpiresAt is in the past surfaces under the Expired tab
10
+ * without waiting for the server-side flip.
11
+ *
12
+ * Pins:
13
+ * (1) recompute runs on data load (loadInvitations calls it),
14
+ * (2) recompute runs on tab change (setTab),
15
+ * (3) the Pending→Expired hydration boundary is applied during recompute,
16
+ * (4) visibleInvitations is a precomputed FIELD (stable identity between reads),
17
+ * (5) countFor reflects the same hydration boundary.
18
+ *
19
+ * inject(ChangeDetectorRef) is stubbed; BaseAngularComponent is mocked.
20
+ */
21
+ vi.mock('@angular/core', () => ({
22
+ Component: () => (t) => t,
23
+ Input: () => () => { },
24
+ Output: () => () => { },
25
+ EventEmitter: class {
26
+ emit() { }
27
+ },
28
+ ChangeDetectorRef: class {
29
+ detectChanges() { }
30
+ markForCheck() { }
31
+ },
32
+ ChangeDetectionStrategy: { OnPush: 1 },
33
+ OnInit: class {
34
+ },
35
+ inject: () => ({ detectChanges() { }, markForCheck() { } }),
36
+ }));
37
+ vi.mock('@memberjunction/ng-base-types', () => {
38
+ class MockBaseAngularComponent {
39
+ Provider = null;
40
+ get ProviderToUse() { return this.Provider; }
41
+ }
42
+ return { BaseAngularComponent: MockBaseAngularComponent };
43
+ });
44
+ vi.mock('@memberjunction/core', () => ({ RunView: class {
45
+ } }));
46
+ vi.mock('@memberjunction/graphql-dataprovider', () => ({
47
+ GraphQLDataProvider: class {
48
+ },
49
+ GraphQLListsClient: class {
50
+ },
51
+ }));
52
+ import { ListInvitationsComponent } from '../list-invitations.component';
53
+ function row(id, status, expiresInMs) {
54
+ return {
55
+ ID: id,
56
+ Email: `${id}@example.com`,
57
+ Role: 'Viewer',
58
+ Status: status,
59
+ ExpiresAt: new Date(Date.now() + expiresInMs),
60
+ CreatedAt: new Date(),
61
+ Token: `tok-${id}`,
62
+ };
63
+ }
64
+ function makeComponent() {
65
+ return new ListInvitationsComponent();
66
+ }
67
+ /** Set source data + run the (private) recompute, mirroring loadInvitations(). */
68
+ function loadData(component, rows) {
69
+ component.invitations = rows;
70
+ component.recomputeVisibleInvitations();
71
+ }
72
+ const HOUR = 3_600_000;
73
+ describe('ListInvitationsComponent — visibleInvitations recompute', () => {
74
+ let component;
75
+ beforeEach(() => {
76
+ component = makeComponent();
77
+ });
78
+ it('recomputes visibleInvitations on data load (default Pending tab)', () => {
79
+ loadData(component, [
80
+ row('p1', 'Pending', +HOUR),
81
+ row('a1', 'Accepted', +HOUR),
82
+ ]);
83
+ expect(component.visibleInvitations.map((i) => i.ID)).toEqual(['p1']);
84
+ });
85
+ it('recomputes when the active tab changes (setTab)', () => {
86
+ loadData(component, [
87
+ row('p1', 'Pending', +HOUR),
88
+ row('a1', 'Accepted', +HOUR),
89
+ row('r1', 'Revoked', +HOUR),
90
+ ]);
91
+ expect(component.activeTab).toBe('Pending');
92
+ expect(component.visibleInvitations.map((i) => i.ID)).toEqual(['p1']);
93
+ component.setTab('Accepted');
94
+ expect(component.visibleInvitations.map((i) => i.ID)).toEqual(['a1']);
95
+ component.setTab('Revoked');
96
+ expect(component.visibleInvitations.map((i) => i.ID)).toEqual(['r1']);
97
+ });
98
+ it('hydrates a past-due Pending invite as Expired (the client boundary)', () => {
99
+ loadData(component, [
100
+ row('p-live', 'Pending', +HOUR), // still valid
101
+ row('p-stale', 'Pending', -HOUR), // expired in the past
102
+ ]);
103
+ // Pending tab shows only the still-live one
104
+ expect(component.visibleInvitations.map((i) => i.ID)).toEqual(['p-live']);
105
+ // Expired tab surfaces the past-due Pending one without a server flip
106
+ component.setTab('Expired');
107
+ expect(component.visibleInvitations.map((i) => i.ID)).toEqual(['p-stale']);
108
+ });
109
+ it('does NOT reclassify an Accepted/Revoked row even when its ExpiresAt is in the past', () => {
110
+ loadData(component, [
111
+ row('a-old', 'Accepted', -HOUR),
112
+ row('r-old', 'Revoked', -HOUR),
113
+ ]);
114
+ component.setTab('Expired');
115
+ expect(component.visibleInvitations).toHaveLength(0);
116
+ component.setTab('Accepted');
117
+ expect(component.visibleInvitations.map((i) => i.ID)).toEqual(['a-old']);
118
+ component.setTab('Revoked');
119
+ expect(component.visibleInvitations.map((i) => i.ID)).toEqual(['r-old']);
120
+ });
121
+ it('countFor applies the same Pending→Expired hydration boundary', () => {
122
+ loadData(component, [
123
+ row('p-live', 'Pending', +HOUR),
124
+ row('p-stale', 'Pending', -HOUR),
125
+ row('a1', 'Accepted', +HOUR),
126
+ ]);
127
+ expect(component.countFor('Pending')).toBe(1); // only the live one
128
+ expect(component.countFor('Expired')).toBe(1); // the stale Pending one
129
+ expect(component.countFor('Accepted')).toBe(1);
130
+ expect(component.countFor('Revoked')).toBe(0);
131
+ });
132
+ it('visibleInvitations is a precomputed field — re-reading it (no recompute) returns the same array', () => {
133
+ loadData(component, [row('p1', 'Pending', +HOUR)]);
134
+ const first = component.visibleInvitations;
135
+ const second = component.visibleInvitations;
136
+ expect(second).toBe(first); // stable identity (not re-allocated per access)
137
+ });
138
+ it('re-load with new data refreshes the visible list', () => {
139
+ loadData(component, [row('p1', 'Pending', +HOUR)]);
140
+ expect(component.visibleInvitations.map((i) => i.ID)).toEqual(['p1']);
141
+ loadData(component, [row('p2', 'Pending', +HOUR), row('p3', 'Pending', +HOUR)]);
142
+ expect(component.visibleInvitations.map((i) => i.ID)).toEqual(['p2', 'p3']);
143
+ });
144
+ });
145
+ //# sourceMappingURL=list-invitations.component.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-invitations.component.test.js","sourceRoot":"","sources":["../../../../../src/lib/components/list-invitations/__tests__/list-invitations.component.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect, beforeEach, vi } from 'vitest';\n\n/**\n * Recompute-trigger coverage for ListInvitationsComponent.visibleInvitations\n * (PR #2841): the precomputed per-tab list is rebuilt only when the source data\n * (`invitations`) or the active tab changes — NOT on every change-detection pass\n * (a getter there would allocate a fresh map+filter each CD tick).\n *\n * The recompute also hydrates the Pending→Expired client boundary: a Pending\n * invitation whose ExpiresAt is in the past surfaces under the Expired tab\n * without waiting for the server-side flip.\n *\n * Pins:\n * (1) recompute runs on data load (loadInvitations calls it),\n * (2) recompute runs on tab change (setTab),\n * (3) the Pending→Expired hydration boundary is applied during recompute,\n * (4) visibleInvitations is a precomputed FIELD (stable identity between reads),\n * (5) countFor reflects the same hydration boundary.\n *\n * inject(ChangeDetectorRef) is stubbed; BaseAngularComponent is mocked.\n */\n\nvi.mock('@angular/core', () => ({\n Component: () => (t: Function) => t,\n Input: () => () => {},\n Output: () => () => {},\n EventEmitter: class { emit() {} },\n ChangeDetectorRef: class { detectChanges() {} markForCheck() {} },\n ChangeDetectionStrategy: { OnPush: 1 },\n OnInit: class {},\n inject: () => ({ detectChanges() {}, markForCheck() {} }),\n}));\n\nvi.mock('@memberjunction/ng-base-types', () => {\n class MockBaseAngularComponent {\n Provider: unknown = null;\n get ProviderToUse(): unknown { return this.Provider; }\n }\n return { BaseAngularComponent: MockBaseAngularComponent };\n});\n\nvi.mock('@memberjunction/core', () => ({ RunView: class {} }));\nvi.mock('@memberjunction/graphql-dataprovider', () => ({\n GraphQLDataProvider: class {},\n GraphQLListsClient: class {},\n}));\n\nimport { ListInvitationsComponent } from '../list-invitations.component';\n\ntype Status = 'Pending' | 'Accepted' | 'Expired' | 'Revoked';\ninterface Row {\n ID: string;\n Email: string;\n Role: 'Editor' | 'Viewer';\n Status: Status;\n ExpiresAt: Date;\n CreatedAt: Date;\n Token: string;\n}\n\nfunction row(id: string, status: Status, expiresInMs: number): Row {\n return {\n ID: id,\n Email: `${id}@example.com`,\n Role: 'Viewer',\n Status: status,\n ExpiresAt: new Date(Date.now() + expiresInMs),\n CreatedAt: new Date(),\n Token: `tok-${id}`,\n };\n}\n\nfunction makeComponent(): ListInvitationsComponent {\n return new ListInvitationsComponent();\n}\n\n/** Set source data + run the (private) recompute, mirroring loadInvitations(). */\nfunction loadData(component: ListInvitationsComponent, rows: Row[]): void {\n (component as unknown as { invitations: Row[] }).invitations = rows;\n (component as unknown as { recomputeVisibleInvitations: () => void }).recomputeVisibleInvitations();\n}\n\nconst HOUR = 3_600_000;\n\ndescribe('ListInvitationsComponent — visibleInvitations recompute', () => {\n let component: ListInvitationsComponent;\n\n beforeEach(() => {\n component = makeComponent();\n });\n\n it('recomputes visibleInvitations on data load (default Pending tab)', () => {\n loadData(component, [\n row('p1', 'Pending', +HOUR),\n row('a1', 'Accepted', +HOUR),\n ]);\n expect(component.visibleInvitations.map((i) => i.ID)).toEqual(['p1']);\n });\n\n it('recomputes when the active tab changes (setTab)', () => {\n loadData(component, [\n row('p1', 'Pending', +HOUR),\n row('a1', 'Accepted', +HOUR),\n row('r1', 'Revoked', +HOUR),\n ]);\n expect(component.activeTab).toBe('Pending');\n expect(component.visibleInvitations.map((i) => i.ID)).toEqual(['p1']);\n\n component.setTab('Accepted');\n expect(component.visibleInvitations.map((i) => i.ID)).toEqual(['a1']);\n\n component.setTab('Revoked');\n expect(component.visibleInvitations.map((i) => i.ID)).toEqual(['r1']);\n });\n\n it('hydrates a past-due Pending invite as Expired (the client boundary)', () => {\n loadData(component, [\n row('p-live', 'Pending', +HOUR), // still valid\n row('p-stale', 'Pending', -HOUR), // expired in the past\n ]);\n // Pending tab shows only the still-live one\n expect(component.visibleInvitations.map((i) => i.ID)).toEqual(['p-live']);\n\n // Expired tab surfaces the past-due Pending one without a server flip\n component.setTab('Expired');\n expect(component.visibleInvitations.map((i) => i.ID)).toEqual(['p-stale']);\n });\n\n it('does NOT reclassify an Accepted/Revoked row even when its ExpiresAt is in the past', () => {\n loadData(component, [\n row('a-old', 'Accepted', -HOUR),\n row('r-old', 'Revoked', -HOUR),\n ]);\n component.setTab('Expired');\n expect(component.visibleInvitations).toHaveLength(0);\n component.setTab('Accepted');\n expect(component.visibleInvitations.map((i) => i.ID)).toEqual(['a-old']);\n component.setTab('Revoked');\n expect(component.visibleInvitations.map((i) => i.ID)).toEqual(['r-old']);\n });\n\n it('countFor applies the same Pending→Expired hydration boundary', () => {\n loadData(component, [\n row('p-live', 'Pending', +HOUR),\n row('p-stale', 'Pending', -HOUR),\n row('a1', 'Accepted', +HOUR),\n ]);\n expect(component.countFor('Pending')).toBe(1); // only the live one\n expect(component.countFor('Expired')).toBe(1); // the stale Pending one\n expect(component.countFor('Accepted')).toBe(1);\n expect(component.countFor('Revoked')).toBe(0);\n });\n\n it('visibleInvitations is a precomputed field — re-reading it (no recompute) returns the same array', () => {\n loadData(component, [row('p1', 'Pending', +HOUR)]);\n const first = component.visibleInvitations;\n const second = component.visibleInvitations;\n expect(second).toBe(first); // stable identity (not re-allocated per access)\n });\n\n it('re-load with new data refreshes the visible list', () => {\n loadData(component, [row('p1', 'Pending', +HOUR)]);\n expect(component.visibleInvitations.map((i) => i.ID)).toEqual(['p1']);\n loadData(component, [row('p2', 'Pending', +HOUR), row('p3', 'Pending', +HOUR)]);\n expect(component.visibleInvitations.map((i) => i.ID)).toEqual(['p2', 'p3']);\n });\n});\n"]}
@@ -31,6 +31,14 @@ export declare class ListInvitationsComponent extends BaseAngularComponent imple
31
31
  ListName: string | null;
32
32
  activeTab: InvitationStatus;
33
33
  invitations: InvitationRow[];
34
+ /**
35
+ * Precomputed list of invitations matching the active tab, recomputed only when
36
+ * the source data ({@link invitations}) or {@link activeTab} changes (see
37
+ * {@link recomputeVisibleInvitations}) — NOT on every change-detection cycle.
38
+ * Bound directly as the @for source in the template, so a getter here would
39
+ * allocate a fresh array (map + filter) every CD tick.
40
+ */
41
+ visibleInvitations: InvitationRow[];
34
42
  loading: boolean;
35
43
  errorMessage: string | null;
36
44
  submitting: boolean;
@@ -40,7 +48,16 @@ export declare class ListInvitationsComponent extends BaseAngularComponent imple
40
48
  private initialized;
41
49
  ngOnInit(): Promise<void>;
42
50
  setTab(tab: InvitationStatus): void;
43
- get visibleInvitations(): InvitationRow[];
51
+ /**
52
+ * Recompute the precomputed {@link visibleInvitations} array. Called only when
53
+ * the source data or the active tab changes — keeping the per-row hydration
54
+ * (map) + filter out of the per-CD-cycle hot path.
55
+ *
56
+ * We hydrate Status="Pending" rows whose ExpiresAt is in the past as Expired
57
+ * on the client so users see them without waiting for a server-side flip. The
58
+ * server's Accept path also lazily marks them.
59
+ */
60
+ private recomputeVisibleInvitations;
44
61
  countFor(status: InvitationStatus): number;
45
62
  formatExpiry(d: Date): string;
46
63
  get canSend(): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"list-invitations.component.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/list-invitations/list-invitations.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgE,MAAM,EAAU,MAAM,eAAe,CAAC;AAE7G,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;;AAKrE;;;;;;;;;;GAUG;AAEH,KAAK,gBAAgB,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;AAEvE,UAAU,aAAa;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC1B,MAAM,EAAE,gBAAgB,CAAC;IACzB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,qBAOa,wBAAyB,SAAQ,oBAAqB,YAAW,MAAM;IAClF,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA6B;IAEjD,IACI,MAAM,IAAI,MAAM,GAAG,IAAI,CAE1B;IACD,IAAI,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAK9B;IACD,OAAO,CAAC,OAAO,CAAuB;IAEtC,yEAAyE;IAChE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAQ;IAEjC,SAAS,EAAE,gBAAgB,CAAa;IACxC,WAAW,EAAE,aAAa,EAAE,CAAM;IAClC,OAAO,UAAS;IAChB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAQ;IACnC,UAAU,UAAS;IAGnB,QAAQ,SAAM;IACd,OAAO,EAAE,QAAQ,GAAG,QAAQ,CAAY;IACxC,WAAW,SAAO;IAEzB,OAAO,CAAC,WAAW,CAAS;IAEtB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAKxB,MAAM,CAAC,GAAG,EAAE,gBAAgB,GAAG,IAAI;IAK1C,IAAW,kBAAkB,IAAI,aAAa,EAAE,CAW/C;IAEM,QAAQ,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM;IAW1C,YAAY,CAAC,CAAC,EAAE,IAAI,GAAG,MAAM;IASpC,IAAW,OAAO,IAAI,OAAO,CAE5B;IAEY,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IA2B/B,MAAM,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYxD;;;;OAIG;IACU,MAAM,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;YAiBxC,eAAe;yCA3IlB,wBAAwB;2CAAxB,wBAAwB;CA8KpC"}
1
+ {"version":3,"file":"list-invitations.component.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/list-invitations/list-invitations.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgE,MAAM,EAAU,MAAM,eAAe,CAAC;AAE7G,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;;AAKrE;;;;;;;;;;GAUG;AAEH,KAAK,gBAAgB,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;AAEvE,UAAU,aAAa;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC1B,MAAM,EAAE,gBAAgB,CAAC;IACzB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,qBAOa,wBAAyB,SAAQ,oBAAqB,YAAW,MAAM;IAClF,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA6B;IAEjD,IACI,MAAM,IAAI,MAAM,GAAG,IAAI,CAE1B;IACD,IAAI,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAK9B;IACD,OAAO,CAAC,OAAO,CAAuB;IAEtC,yEAAyE;IAChE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAQ;IAEjC,SAAS,EAAE,gBAAgB,CAAa;IACxC,WAAW,EAAE,aAAa,EAAE,CAAM;IAEzC;;;;;;OAMG;IACI,kBAAkB,EAAE,aAAa,EAAE,CAAM;IACzC,OAAO,UAAS;IAChB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAQ;IACnC,UAAU,UAAS;IAGnB,QAAQ,SAAM;IACd,OAAO,EAAE,QAAQ,GAAG,QAAQ,CAAY;IACxC,WAAW,SAAO;IAEzB,OAAO,CAAC,WAAW,CAAS;IAEtB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAKxB,MAAM,CAAC,GAAG,EAAE,gBAAgB,GAAG,IAAI;IAM1C;;;;;;;;OAQG;IACH,OAAO,CAAC,2BAA2B;IAU5B,QAAQ,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM;IAW1C,YAAY,CAAC,CAAC,EAAE,IAAI,GAAG,MAAM;IASpC,IAAW,OAAO,IAAI,OAAO,CAE5B;IAEY,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IA2B/B,MAAM,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYxD;;;;OAIG;IACU,MAAM,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;YAiBxC,eAAe;yCA3JlB,wBAAwB;2CAAxB,wBAAwB;CAiMpC"}
@@ -150,6 +150,14 @@ export class ListInvitationsComponent extends BaseAngularComponent {
150
150
  ListName = null;
151
151
  activeTab = 'Pending';
152
152
  invitations = [];
153
+ /**
154
+ * Precomputed list of invitations matching the active tab, recomputed only when
155
+ * the source data ({@link invitations}) or {@link activeTab} changes (see
156
+ * {@link recomputeVisibleInvitations}) — NOT on every change-detection cycle.
157
+ * Bound directly as the @for source in the template, so a getter here would
158
+ * allocate a fresh array (map + filter) every CD tick.
159
+ */
160
+ visibleInvitations = [];
153
161
  loading = false;
154
162
  errorMessage = null;
155
163
  submitting = false;
@@ -165,17 +173,24 @@ export class ListInvitationsComponent extends BaseAngularComponent {
165
173
  }
166
174
  setTab(tab) {
167
175
  this.activeTab = tab;
176
+ this.recomputeVisibleInvitations();
168
177
  this.cdr.markForCheck();
169
178
  }
170
- get visibleInvitations() {
171
- // We hydrate Status="Pending" rows whose ExpiresAt is in the past as
172
- // Expired on the client so users see them without waiting for a
173
- // server-side flip. The server's Accept path also lazily marks them.
179
+ /**
180
+ * Recompute the precomputed {@link visibleInvitations} array. Called only when
181
+ * the source data or the active tab changes — keeping the per-row hydration
182
+ * (map) + filter out of the per-CD-cycle hot path.
183
+ *
184
+ * We hydrate Status="Pending" rows whose ExpiresAt is in the past as Expired
185
+ * on the client so users see them without waiting for a server-side flip. The
186
+ * server's Accept path also lazily marks them.
187
+ */
188
+ recomputeVisibleInvitations() {
174
189
  const now = Date.now();
175
190
  const hydrated = this.invitations.map((inv) => inv.Status === 'Pending' && inv.ExpiresAt.getTime() < now
176
191
  ? { ...inv, Status: 'Expired' }
177
192
  : inv);
178
- return hydrated.filter((i) => i.Status === this.activeTab);
193
+ this.visibleInvitations = hydrated.filter((i) => i.Status === this.activeTab);
179
194
  }
180
195
  countFor(status) {
181
196
  const now = Date.now();
@@ -282,6 +297,7 @@ export class ListInvitationsComponent extends BaseAngularComponent {
282
297
  if (!result.Success) {
283
298
  this.errorMessage = result.ErrorMessage ?? 'Failed to load invitations';
284
299
  this.invitations = [];
300
+ this.recomputeVisibleInvitations();
285
301
  return;
286
302
  }
287
303
  this.invitations = (result.Results ?? []).map((r) => ({
@@ -293,10 +309,12 @@ export class ListInvitationsComponent extends BaseAngularComponent {
293
309
  CreatedAt: new Date(r.__mj_CreatedAt),
294
310
  Token: String(r.Token),
295
311
  }));
312
+ this.recomputeVisibleInvitations();
296
313
  }
297
314
  catch (e) {
298
315
  this.errorMessage = e instanceof Error ? e.message : String(e);
299
316
  this.invitations = [];
317
+ this.recomputeVisibleInvitations();
300
318
  }
301
319
  finally {
302
320
  this.loading = false;
@@ -1 +1 @@
1
- {"version":3,"file":"list-invitations.component.js","sourceRoot":"","sources":["../../../../src/lib/components/list-invitations/list-invitations.component.ts","../../../../src/lib/components/list-invitations/list-invitations.component.html"],"names":[],"mappings":"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","sourcesContent":["import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit, inject } from '@angular/core';\n\nimport { BaseAngularComponent } from '@memberjunction/ng-base-types';\nimport { RunView } from '@memberjunction/core';\nimport type { MJListInvitationEntity } from '@memberjunction/core-entities';\nimport { GraphQLDataProvider, GraphQLListsClient } from '@memberjunction/graphql-dataprovider';\n\n/**\n * Invitations management UI (mockup 16). For a single List:\n * - Pending / Accepted / Expired / Revoked tabs.\n * - Send new invitation form (email + role + TTL).\n * - Revoke / Resend actions on Pending invites.\n *\n * Reads invitations directly via `RunView` (cheaper than going through\n * GraphQL — the entity is already accessible). Mutations route through\n * `GraphQLListsClient` so the audit-log + notification side effects\n * fire server-side.\n */\n\ntype InvitationStatus = 'Pending' | 'Accepted' | 'Expired' | 'Revoked';\n\ninterface InvitationRow {\n ID: string;\n Email: string;\n Role: 'Editor' | 'Viewer';\n Status: InvitationStatus;\n ExpiresAt: Date;\n CreatedAt: Date;\n Token: string;\n}\n\n@Component({\n standalone: false,\n selector: 'mj-list-invitations',\n templateUrl: './list-invitations.component.html',\n styleUrls: ['./list-invitations.component.css'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class ListInvitationsComponent extends BaseAngularComponent implements OnInit {\n private readonly cdr = inject(ChangeDetectorRef);\n\n @Input()\n get ListID(): string | null {\n return this._listId;\n }\n set ListID(value: string | null) {\n if (this._listId !== value) {\n this._listId = value;\n if (this.initialized && value) void this.loadInvitations();\n }\n }\n private _listId: string | null = null;\n\n /** Optional display name for the list — used in the empty-state copy. */\n @Input() ListName: string | null = null;\n\n public activeTab: InvitationStatus = 'Pending';\n public invitations: InvitationRow[] = [];\n public loading = false;\n public errorMessage: string | null = null;\n public submitting = false;\n\n // Send-new-invitation form\n public newEmail = '';\n public newRole: 'Editor' | 'Viewer' = 'Viewer';\n public newTtlHours = 168; // 7 days default\n\n private initialized = false;\n\n async ngOnInit(): Promise<void> {\n this.initialized = true;\n if (this._listId) await this.loadInvitations();\n }\n\n public setTab(tab: InvitationStatus): void {\n this.activeTab = tab;\n this.cdr.markForCheck();\n }\n\n public get visibleInvitations(): InvitationRow[] {\n // We hydrate Status=\"Pending\" rows whose ExpiresAt is in the past as\n // Expired on the client so users see them without waiting for a\n // server-side flip. The server's Accept path also lazily marks them.\n const now = Date.now();\n const hydrated = this.invitations.map<InvitationRow>((inv) =>\n inv.Status === 'Pending' && inv.ExpiresAt.getTime() < now\n ? { ...inv, Status: 'Expired' }\n : inv,\n );\n return hydrated.filter((i) => i.Status === this.activeTab);\n }\n\n public countFor(status: InvitationStatus): number {\n const now = Date.now();\n let count = 0;\n for (const inv of this.invitations) {\n const effective: InvitationStatus =\n inv.Status === 'Pending' && inv.ExpiresAt.getTime() < now ? 'Expired' : inv.Status;\n if (effective === status) count++;\n }\n return count;\n }\n\n public formatExpiry(d: Date): string {\n const diff = d.getTime() - Date.now();\n if (diff < 0) return `expired ${d.toLocaleDateString()}`;\n const hours = Math.floor(diff / 3_600_000);\n if (hours < 24) return `expires in ${hours}h`;\n const days = Math.floor(hours / 24);\n return `expires in ${days}d`;\n }\n\n public get canSend(): boolean {\n return !!this._listId && this.newEmail.trim().length > 0 && this.newEmail.includes('@') && !this.submitting;\n }\n\n public async SendInvitation(): Promise<void> {\n if (!this.canSend || !this._listId) return;\n this.submitting = true;\n this.cdr.markForCheck();\n try {\n const provider = this.ProviderToUse as unknown as GraphQLDataProvider;\n const client = new GraphQLListsClient(provider);\n const result = await client.Invite({\n ListID: this._listId,\n Email: this.newEmail.trim(),\n Role: this.newRole,\n TtlHours: this.newTtlHours,\n });\n if (result.Success) {\n this.newEmail = '';\n await this.loadInvitations();\n } else {\n this.errorMessage = `Failed to send: ${result.Message}`;\n }\n } catch (e) {\n this.errorMessage = e instanceof Error ? e.message : String(e);\n } finally {\n this.submitting = false;\n this.cdr.markForCheck();\n }\n }\n\n public async Revoke(invitationId: string): Promise<void> {\n const provider = this.ProviderToUse as unknown as GraphQLDataProvider;\n const client = new GraphQLListsClient(provider);\n const result = await client.RevokeInvitation(invitationId);\n if (result.Success) {\n await this.loadInvitations();\n } else {\n this.errorMessage = `Failed to revoke: ${result.Message}`;\n this.cdr.markForCheck();\n }\n }\n\n /**\n * \"Resend\" = create a fresh invitation for the same email + role. The\n * old one stays Pending until either accepted or expired; users see\n * both in the Pending tab until one is consumed.\n */\n public async Resend(inv: InvitationRow): Promise<void> {\n const provider = this.ProviderToUse as unknown as GraphQLDataProvider;\n const client = new GraphQLListsClient(provider);\n if (!this._listId) return;\n const result = await client.Invite({\n ListID: this._listId,\n Email: inv.Email,\n Role: inv.Role,\n TtlHours: 168,\n });\n if (result.Success) await this.loadInvitations();\n else {\n this.errorMessage = `Failed to resend: ${result.Message}`;\n this.cdr.markForCheck();\n }\n }\n\n private async loadInvitations(): Promise<void> {\n if (!this._listId) return;\n this.loading = true;\n this.errorMessage = null;\n this.cdr.markForCheck();\n try {\n const rv = RunView.FromMetadataProvider(this.ProviderToUse);\n const result = await rv.RunView<MJListInvitationEntity>({\n EntityName: 'MJ: List Invitations',\n ExtraFilter: `ListID='${this._listId.replace(/'/g, \"''\")}'`,\n OrderBy: '__mj_CreatedAt DESC',\n ResultType: 'simple',\n });\n if (!result.Success) {\n this.errorMessage = result.ErrorMessage ?? 'Failed to load invitations';\n this.invitations = [];\n return;\n }\n this.invitations = (result.Results ?? []).map((r) => ({\n ID: String(r.ID),\n Email: String(r.Email),\n Role: r.Role as 'Editor' | 'Viewer',\n Status: r.Status as InvitationStatus,\n ExpiresAt: new Date(r.ExpiresAt as unknown as string),\n CreatedAt: new Date(r.__mj_CreatedAt as unknown as string),\n Token: String(r.Token),\n }));\n } catch (e) {\n this.errorMessage = e instanceof Error ? e.message : String(e);\n this.invitations = [];\n } finally {\n this.loading = false;\n this.cdr.markForCheck();\n }\n }\n}\n","<div class=\"invitations\">\n <!-- Send new invitation -->\n <div class=\"invitation-send\">\n <div class=\"invitation-send__heading\">Send invitation</div>\n <form class=\"invitation-send__form\" (submit)=\"$event.preventDefault(); SendInvitation()\">\n <input\n class=\"mj-input invitation-send__email\"\n type=\"email\"\n [(ngModel)]=\"newEmail\"\n name=\"newEmail\"\n placeholder=\"recipient@example.com\"\n required />\n <select class=\"mj-input mj-select invitation-send__role\" [(ngModel)]=\"newRole\" name=\"newRole\">\n <option value=\"Viewer\">Viewer</option>\n <option value=\"Editor\">Editor</option>\n </select>\n <input\n class=\"mj-input invitation-send__ttl\"\n type=\"number\"\n [(ngModel)]=\"newTtlHours\"\n name=\"newTtlHours\"\n title=\"Hours until expiry\"\n min=\"1\"\n max=\"720\" />\n <button mjButton variant=\"primary\" type=\"submit\" [disabled]=\"!canSend\">\n @if (submitting) {\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n } @else {\n <i class=\"fa-solid fa-paper-plane\"></i>\n }\n {{ submitting ? 'Sending…' : 'Send' }}\n </button>\n </form>\n </div>\n\n @if (errorMessage) {\n <div class=\"invitation-error\">\n <i class=\"fa-solid fa-triangle-exclamation\"></i>\n {{ errorMessage }}\n </div>\n }\n\n <!-- Status tabs -->\n <div class=\"invitation-tabs\">\n @for (s of ['Pending', 'Accepted', 'Expired', 'Revoked']; track s) {\n <button\n class=\"invitation-tab\"\n [class.invitation-tab--active]=\"activeTab === s\"\n (click)=\"setTab($any(s))\">\n {{ s }}\n <span class=\"invitation-tab__count\">{{ countFor($any(s)) }}</span>\n </button>\n }\n </div>\n\n @if (loading) {\n <div class=\"invitation-state\">\n <mj-loading text=\"Loading invitations...\" size=\"medium\"></mj-loading>\n </div>\n } @else if (visibleInvitations.length === 0) {\n <div class=\"invitation-state invitation-state--empty\">\n <i class=\"fa-solid fa-envelope\"></i>\n <span>No {{ activeTab.toLowerCase() }} invitations.</span>\n </div>\n } @else {\n <table class=\"invitation-table\">\n <thead>\n <tr>\n <th>Email</th>\n <th>Role</th>\n <th>Sent</th>\n <th>Expiry</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n @for (inv of visibleInvitations; track inv.ID) {\n <tr>\n <td>{{ inv.Email }}</td>\n <td>\n <span class=\"role-chip\" [class.role-chip--editor]=\"inv.Role === 'Editor'\">\n {{ inv.Role }}\n </span>\n </td>\n <td>{{ inv.CreatedAt.toLocaleDateString() }}</td>\n <td>{{ formatExpiry(inv.ExpiresAt) }}</td>\n <td class=\"invitation-actions\">\n @if (activeTab === 'Pending') {\n <button mjButton size=\"sm\" variant=\"outline\" (click)=\"Resend(inv)\" title=\"Send a fresh invitation\">\n <i class=\"fa-solid fa-paper-plane\"></i>\n Resend\n </button>\n <button mjButton size=\"sm\" variant=\"danger\" (click)=\"Revoke(inv.ID)\">\n <i class=\"fa-solid fa-xmark\"></i>\n Revoke\n </button>\n } @else if (activeTab === 'Expired') {\n <button mjButton size=\"sm\" variant=\"outline\" (click)=\"Resend(inv)\">\n <i class=\"fa-solid fa-rotate-right\"></i>\n Re-send\n </button>\n }\n </td>\n </tr>\n }\n </tbody>\n </table>\n }\n</div>\n"]}
1
+ {"version":3,"file":"list-invitations.component.js","sourceRoot":"","sources":["../../../../src/lib/components/list-invitations/list-invitations.component.ts","../../../../src/lib/components/list-invitations/list-invitations.component.html"],"names":[],"mappings":"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","sourcesContent":["import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit, inject } from '@angular/core';\n\nimport { BaseAngularComponent } from '@memberjunction/ng-base-types';\nimport { RunView } from '@memberjunction/core';\nimport type { MJListInvitationEntity } from '@memberjunction/core-entities';\nimport { GraphQLDataProvider, GraphQLListsClient } from '@memberjunction/graphql-dataprovider';\n\n/**\n * Invitations management UI (mockup 16). For a single List:\n * - Pending / Accepted / Expired / Revoked tabs.\n * - Send new invitation form (email + role + TTL).\n * - Revoke / Resend actions on Pending invites.\n *\n * Reads invitations directly via `RunView` (cheaper than going through\n * GraphQL — the entity is already accessible). Mutations route through\n * `GraphQLListsClient` so the audit-log + notification side effects\n * fire server-side.\n */\n\ntype InvitationStatus = 'Pending' | 'Accepted' | 'Expired' | 'Revoked';\n\ninterface InvitationRow {\n ID: string;\n Email: string;\n Role: 'Editor' | 'Viewer';\n Status: InvitationStatus;\n ExpiresAt: Date;\n CreatedAt: Date;\n Token: string;\n}\n\n@Component({\n standalone: false,\n selector: 'mj-list-invitations',\n templateUrl: './list-invitations.component.html',\n styleUrls: ['./list-invitations.component.css'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class ListInvitationsComponent extends BaseAngularComponent implements OnInit {\n private readonly cdr = inject(ChangeDetectorRef);\n\n @Input()\n get ListID(): string | null {\n return this._listId;\n }\n set ListID(value: string | null) {\n if (this._listId !== value) {\n this._listId = value;\n if (this.initialized && value) void this.loadInvitations();\n }\n }\n private _listId: string | null = null;\n\n /** Optional display name for the list — used in the empty-state copy. */\n @Input() ListName: string | null = null;\n\n public activeTab: InvitationStatus = 'Pending';\n public invitations: InvitationRow[] = [];\n\n /**\n * Precomputed list of invitations matching the active tab, recomputed only when\n * the source data ({@link invitations}) or {@link activeTab} changes (see\n * {@link recomputeVisibleInvitations}) — NOT on every change-detection cycle.\n * Bound directly as the @for source in the template, so a getter here would\n * allocate a fresh array (map + filter) every CD tick.\n */\n public visibleInvitations: InvitationRow[] = [];\n public loading = false;\n public errorMessage: string | null = null;\n public submitting = false;\n\n // Send-new-invitation form\n public newEmail = '';\n public newRole: 'Editor' | 'Viewer' = 'Viewer';\n public newTtlHours = 168; // 7 days default\n\n private initialized = false;\n\n async ngOnInit(): Promise<void> {\n this.initialized = true;\n if (this._listId) await this.loadInvitations();\n }\n\n public setTab(tab: InvitationStatus): void {\n this.activeTab = tab;\n this.recomputeVisibleInvitations();\n this.cdr.markForCheck();\n }\n\n /**\n * Recompute the precomputed {@link visibleInvitations} array. Called only when\n * the source data or the active tab changes — keeping the per-row hydration\n * (map) + filter out of the per-CD-cycle hot path.\n *\n * We hydrate Status=\"Pending\" rows whose ExpiresAt is in the past as Expired\n * on the client so users see them without waiting for a server-side flip. The\n * server's Accept path also lazily marks them.\n */\n private recomputeVisibleInvitations(): void {\n const now = Date.now();\n const hydrated = this.invitations.map<InvitationRow>((inv) =>\n inv.Status === 'Pending' && inv.ExpiresAt.getTime() < now\n ? { ...inv, Status: 'Expired' }\n : inv,\n );\n this.visibleInvitations = hydrated.filter((i) => i.Status === this.activeTab);\n }\n\n public countFor(status: InvitationStatus): number {\n const now = Date.now();\n let count = 0;\n for (const inv of this.invitations) {\n const effective: InvitationStatus =\n inv.Status === 'Pending' && inv.ExpiresAt.getTime() < now ? 'Expired' : inv.Status;\n if (effective === status) count++;\n }\n return count;\n }\n\n public formatExpiry(d: Date): string {\n const diff = d.getTime() - Date.now();\n if (diff < 0) return `expired ${d.toLocaleDateString()}`;\n const hours = Math.floor(diff / 3_600_000);\n if (hours < 24) return `expires in ${hours}h`;\n const days = Math.floor(hours / 24);\n return `expires in ${days}d`;\n }\n\n public get canSend(): boolean {\n return !!this._listId && this.newEmail.trim().length > 0 && this.newEmail.includes('@') && !this.submitting;\n }\n\n public async SendInvitation(): Promise<void> {\n if (!this.canSend || !this._listId) return;\n this.submitting = true;\n this.cdr.markForCheck();\n try {\n const provider = this.ProviderToUse as unknown as GraphQLDataProvider;\n const client = new GraphQLListsClient(provider);\n const result = await client.Invite({\n ListID: this._listId,\n Email: this.newEmail.trim(),\n Role: this.newRole,\n TtlHours: this.newTtlHours,\n });\n if (result.Success) {\n this.newEmail = '';\n await this.loadInvitations();\n } else {\n this.errorMessage = `Failed to send: ${result.Message}`;\n }\n } catch (e) {\n this.errorMessage = e instanceof Error ? e.message : String(e);\n } finally {\n this.submitting = false;\n this.cdr.markForCheck();\n }\n }\n\n public async Revoke(invitationId: string): Promise<void> {\n const provider = this.ProviderToUse as unknown as GraphQLDataProvider;\n const client = new GraphQLListsClient(provider);\n const result = await client.RevokeInvitation(invitationId);\n if (result.Success) {\n await this.loadInvitations();\n } else {\n this.errorMessage = `Failed to revoke: ${result.Message}`;\n this.cdr.markForCheck();\n }\n }\n\n /**\n * \"Resend\" = create a fresh invitation for the same email + role. The\n * old one stays Pending until either accepted or expired; users see\n * both in the Pending tab until one is consumed.\n */\n public async Resend(inv: InvitationRow): Promise<void> {\n const provider = this.ProviderToUse as unknown as GraphQLDataProvider;\n const client = new GraphQLListsClient(provider);\n if (!this._listId) return;\n const result = await client.Invite({\n ListID: this._listId,\n Email: inv.Email,\n Role: inv.Role,\n TtlHours: 168,\n });\n if (result.Success) await this.loadInvitations();\n else {\n this.errorMessage = `Failed to resend: ${result.Message}`;\n this.cdr.markForCheck();\n }\n }\n\n private async loadInvitations(): Promise<void> {\n if (!this._listId) return;\n this.loading = true;\n this.errorMessage = null;\n this.cdr.markForCheck();\n try {\n const rv = RunView.FromMetadataProvider(this.ProviderToUse);\n const result = await rv.RunView<MJListInvitationEntity>({\n EntityName: 'MJ: List Invitations',\n ExtraFilter: `ListID='${this._listId.replace(/'/g, \"''\")}'`,\n OrderBy: '__mj_CreatedAt DESC',\n ResultType: 'simple',\n });\n if (!result.Success) {\n this.errorMessage = result.ErrorMessage ?? 'Failed to load invitations';\n this.invitations = [];\n this.recomputeVisibleInvitations();\n return;\n }\n this.invitations = (result.Results ?? []).map((r) => ({\n ID: String(r.ID),\n Email: String(r.Email),\n Role: r.Role as 'Editor' | 'Viewer',\n Status: r.Status as InvitationStatus,\n ExpiresAt: new Date(r.ExpiresAt as unknown as string),\n CreatedAt: new Date(r.__mj_CreatedAt as unknown as string),\n Token: String(r.Token),\n }));\n this.recomputeVisibleInvitations();\n } catch (e) {\n this.errorMessage = e instanceof Error ? e.message : String(e);\n this.invitations = [];\n this.recomputeVisibleInvitations();\n } finally {\n this.loading = false;\n this.cdr.markForCheck();\n }\n }\n}\n","<div class=\"invitations\">\n <!-- Send new invitation -->\n <div class=\"invitation-send\">\n <div class=\"invitation-send__heading\">Send invitation</div>\n <form class=\"invitation-send__form\" (submit)=\"$event.preventDefault(); SendInvitation()\">\n <input\n class=\"mj-input invitation-send__email\"\n type=\"email\"\n [(ngModel)]=\"newEmail\"\n name=\"newEmail\"\n placeholder=\"recipient@example.com\"\n required />\n <select class=\"mj-input mj-select invitation-send__role\" [(ngModel)]=\"newRole\" name=\"newRole\">\n <option value=\"Viewer\">Viewer</option>\n <option value=\"Editor\">Editor</option>\n </select>\n <input\n class=\"mj-input invitation-send__ttl\"\n type=\"number\"\n [(ngModel)]=\"newTtlHours\"\n name=\"newTtlHours\"\n title=\"Hours until expiry\"\n min=\"1\"\n max=\"720\" />\n <button mjButton variant=\"primary\" type=\"submit\" [disabled]=\"!canSend\">\n @if (submitting) {\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n } @else {\n <i class=\"fa-solid fa-paper-plane\"></i>\n }\n {{ submitting ? 'Sending…' : 'Send' }}\n </button>\n </form>\n </div>\n\n @if (errorMessage) {\n <div class=\"invitation-error\">\n <i class=\"fa-solid fa-triangle-exclamation\"></i>\n {{ errorMessage }}\n </div>\n }\n\n <!-- Status tabs -->\n <div class=\"invitation-tabs\">\n @for (s of ['Pending', 'Accepted', 'Expired', 'Revoked']; track s) {\n <button\n class=\"invitation-tab\"\n [class.invitation-tab--active]=\"activeTab === s\"\n (click)=\"setTab($any(s))\">\n {{ s }}\n <span class=\"invitation-tab__count\">{{ countFor($any(s)) }}</span>\n </button>\n }\n </div>\n\n @if (loading) {\n <div class=\"invitation-state\">\n <mj-loading text=\"Loading invitations...\" size=\"medium\"></mj-loading>\n </div>\n } @else if (visibleInvitations.length === 0) {\n <div class=\"invitation-state invitation-state--empty\">\n <i class=\"fa-solid fa-envelope\"></i>\n <span>No {{ activeTab.toLowerCase() }} invitations.</span>\n </div>\n } @else {\n <table class=\"invitation-table\">\n <thead>\n <tr>\n <th>Email</th>\n <th>Role</th>\n <th>Sent</th>\n <th>Expiry</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n @for (inv of visibleInvitations; track inv.ID) {\n <tr>\n <td>{{ inv.Email }}</td>\n <td>\n <span class=\"role-chip\" [class.role-chip--editor]=\"inv.Role === 'Editor'\">\n {{ inv.Role }}\n </span>\n </td>\n <td>{{ inv.CreatedAt.toLocaleDateString() }}</td>\n <td>{{ formatExpiry(inv.ExpiresAt) }}</td>\n <td class=\"invitation-actions\">\n @if (activeTab === 'Pending') {\n <button mjButton size=\"sm\" variant=\"outline\" (click)=\"Resend(inv)\" title=\"Send a fresh invitation\">\n <i class=\"fa-solid fa-paper-plane\"></i>\n Resend\n </button>\n <button mjButton size=\"sm\" variant=\"danger\" (click)=\"Revoke(inv.ID)\">\n <i class=\"fa-solid fa-xmark\"></i>\n Revoke\n </button>\n } @else if (activeTab === 'Expired') {\n <button mjButton size=\"sm\" variant=\"outline\" (click)=\"Resend(inv)\">\n <i class=\"fa-solid fa-rotate-right\"></i>\n Re-send\n </button>\n }\n </td>\n </tr>\n }\n </tbody>\n </table>\n }\n</div>\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memberjunction/ng-list-management",
3
- "version": "5.40.1",
3
+ "version": "5.41.0",
4
4
  "description": "MemberJunction: Angular Components for managing entity list membership with beautiful, responsive UI",
5
5
  "main": "./dist/public-api.js",
6
6
  "typings": "./dist/public-api.d.ts",
@@ -8,7 +8,7 @@
8
8
  "/dist"
9
9
  ],
10
10
  "scripts": {
11
- "test": "echo \"No tests configured yet\"",
11
+ "test": "vitest run",
12
12
  "build": "ngc",
13
13
  "test:watch": "vitest"
14
14
  },
@@ -33,16 +33,16 @@
33
33
  "rxjs": "^7.8.2"
34
34
  },
35
35
  "dependencies": {
36
- "@memberjunction/core": "5.40.1",
37
- "@memberjunction/core-entities": "5.40.1",
38
- "@memberjunction/global": "5.40.1",
39
- "@memberjunction/graphql-dataprovider": "5.40.1",
40
- "@memberjunction/lists-base": "5.40.1",
41
- "@memberjunction/ng-base-types": "5.40.1",
42
- "@memberjunction/ng-container-directives": "5.40.1",
43
- "@memberjunction/ng-shared": "5.40.1",
44
- "@memberjunction/ng-shared-generic": "5.40.1",
45
- "@memberjunction/ng-ui-components": "5.40.1",
36
+ "@memberjunction/core": "5.41.0",
37
+ "@memberjunction/core-entities": "5.41.0",
38
+ "@memberjunction/global": "5.41.0",
39
+ "@memberjunction/graphql-dataprovider": "5.41.0",
40
+ "@memberjunction/lists-base": "5.41.0",
41
+ "@memberjunction/ng-base-types": "5.41.0",
42
+ "@memberjunction/ng-container-directives": "5.41.0",
43
+ "@memberjunction/ng-shared": "5.41.0",
44
+ "@memberjunction/ng-shared-generic": "5.41.0",
45
+ "@memberjunction/ng-ui-components": "5.41.0",
46
46
  "tslib": "^2.8.1"
47
47
  },
48
48
  "sideEffects": false,