@memberjunction/ng-list-management 5.40.2 → 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.
- package/dist/lib/components/list-invitations/__tests__/list-invitations.component.test.d.ts +2 -0
- package/dist/lib/components/list-invitations/__tests__/list-invitations.component.test.d.ts.map +1 -0
- package/dist/lib/components/list-invitations/__tests__/list-invitations.component.test.js +145 -0
- package/dist/lib/components/list-invitations/__tests__/list-invitations.component.test.js.map +1 -0
- package/dist/lib/components/list-invitations/list-invitations.component.d.ts +18 -1
- package/dist/lib/components/list-invitations/list-invitations.component.d.ts.map +1 -1
- package/dist/lib/components/list-invitations/list-invitations.component.js +23 -5
- package/dist/lib/components/list-invitations/list-invitations.component.js.map +1 -1
- package/package.json +12 -12
package/dist/lib/components/list-invitations/__tests__/list-invitations.component.test.d.ts.map
ADDED
|
@@ -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
|
-
|
|
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;
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
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.
|
|
37
|
-
"@memberjunction/core-entities": "5.
|
|
38
|
-
"@memberjunction/global": "5.
|
|
39
|
-
"@memberjunction/graphql-dataprovider": "5.
|
|
40
|
-
"@memberjunction/lists-base": "5.
|
|
41
|
-
"@memberjunction/ng-base-types": "5.
|
|
42
|
-
"@memberjunction/ng-container-directives": "5.
|
|
43
|
-
"@memberjunction/ng-shared": "5.
|
|
44
|
-
"@memberjunction/ng-shared-generic": "5.
|
|
45
|
-
"@memberjunction/ng-ui-components": "5.
|
|
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,
|