Skip to content

Commit 7692bf1

Browse files
committed
added new received to global state, make toast more flexible
1 parent fbe1f75 commit 7692bf1

File tree

8 files changed

+114
-81
lines changed

8 files changed

+114
-81
lines changed

packages/core/src/notifications/controller.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export class NotificationsController {
4242

4343
constructor(
4444
private readonly notificationsNode: NotificationsNode,
45-
private readonly fetcher: NotificationFetcher = new LocalFetcher()
45+
private readonly fetcher: NotificationFetcher = new RemoteFetcher()
4646
) {
4747
if (!NotificationsController.#instance) {
4848
// Register on first creation only.
@@ -55,6 +55,7 @@ export class NotificationsController {
5555
startUp: {},
5656
emergency: {},
5757
dismissed: [],
58+
newlyReceived: [],
5859
})
5960
}
6061

@@ -97,6 +98,16 @@ export class NotificationsController {
9798
await this.writeState()
9899
void this.notificationsNode.focusPanel()
99100
}
101+
102+
// Process on-receive behavior for newly received notifications that passes rule engine
103+
const newlyReceivedToDisplay = [...startUp, ...emergency].filter((n) => this.state.newlyReceived.includes(n.id))
104+
if (newlyReceivedToDisplay.length > 0) {
105+
await this.notificationsNode.onReceiveNotifications(newlyReceivedToDisplay)
106+
// remove displayed notifications from newlyReceived
107+
this.state.newlyReceived = this.state.newlyReceived.filter(
108+
(id) => !newlyReceivedToDisplay.some((n) => n.id === id)
109+
)
110+
}
100111
}
101112

102113
/**
@@ -138,10 +149,10 @@ export class NotificationsController {
138149
category,
139150
addedNotifications.map((n: any) => n.id).join(', ')
140151
)
141-
await this.notificationsNode.onReceiveNotifications(addedNotifications)
152+
this.state.newlyReceived.push(...addedNotifications.map((n: any) => n.id))
142153
}
143154

144-
this.state[category].payload = JSON.parse(response.content)
155+
this.state[category].payload = newPayload
145156
this.state[category].eTag = response.eTag
146157
await this.writeState()
147158

packages/core/src/notifications/panelNode.ts

Lines changed: 28 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ export class NotificationsNode implements TreeNode {
136136
case 'modal':
137137
// Render blocking modal
138138
getLogger('notifications').verbose(`rendering modal for notificaiton: ${notification.id} ...`)
139-
await this.renderModal(notification)
139+
await this.showInformationWindow(notification, 'modal')
140140
break
141141
case 'openUrl':
142142
if (!notification.uiRenderInstructions.onClick.url) {
@@ -188,34 +188,39 @@ export class NotificationsNode implements TreeNode {
188188
}
189189

190190
/**
191-
* Renders a blocking modal with the notification's content and buttons.
191+
* Renders information window with the notification's content and buttons.
192+
* Can be either a blocking modal or a bottom-right corner toast
192193
* Handles the button click actions based on the button type.
193194
*/
194-
public async renderModal(notification: ToolkitNotification) {
195-
if (!notification.uiRenderInstructions.actions) {
195+
public async showInformationWindow(notification: ToolkitNotification, type: string = 'toast') {
196+
const isModal = type === 'modal'
197+
198+
// modal has to have defined actions(buttons)
199+
if (!notification.uiRenderInstructions.actions && isModal) {
196200
throw new ToolkitError('no button defined for modal')
197201
return
198202
}
199-
200-
const buttons = notification.uiRenderInstructions.actions
201-
203+
const buttons = notification.uiRenderInstructions.actions ?? []
202204
const buttonLabels = buttons.map((actions) => actions.displayText['en-US'])
203-
204205
const detail = notification.uiRenderInstructions.content['en-US'].description
205206

206-
const selectedButton = await vscode.window.showInformationMessage(
207-
notification.uiRenderInstructions.content['en-US'].title,
208-
{ modal: true, detail },
207+
// we use toastPreview to display as titlefor toast, since detail won't be shown
208+
const title = isModal
209+
? notification.uiRenderInstructions.content['en-US'].title
210+
: (notification.uiRenderInstructions.content['en-US'].toastPreview ??
211+
notification.uiRenderInstructions.content['en-US'].title)
212+
213+
const selectedText = await vscode.window.showInformationMessage(
214+
title,
215+
{ modal: isModal, detail },
209216
...buttonLabels
210217
)
211218

212-
if (selectedButton) {
213-
const buttons = notification.uiRenderInstructions.actions.find(
214-
(actions) => actions.displayText['en-US'] === selectedButton
215-
)
219+
if (selectedText) {
220+
const selectedButton = buttons.find((actions) => actions.displayText['en-US'] === selectedText)
216221
// Different button options
217-
if (buttons) {
218-
switch (buttons.type) {
222+
if (selectedButton) {
223+
switch (selectedButton.type) {
219224
case 'openTxt':
220225
await this.showReadonlyTextDocument(
221226
notification.uiRenderInstructions.content['en-US'].description
@@ -225,8 +230,10 @@ export class NotificationsNode implements TreeNode {
225230
await this.updateAndReload(notification.displayIf.extensionId)
226231
break
227232
case 'openUrl':
228-
if (buttons.url) {
229-
await vscode.env.openExternal(vscode.Uri.parse(buttons.url))
233+
if (selectedButton.url) {
234+
await vscode.env.openExternal(vscode.Uri.parse(selectedButton.url))
235+
} else {
236+
throw new ToolkitError('url not provided')
230237
}
231238
break
232239
default:
@@ -238,19 +245,7 @@ export class NotificationsNode implements TreeNode {
238245

239246
public async onReceiveNotifications(notifications: ToolkitNotification[]) {
240247
for (const notification of notifications) {
241-
switch (notification.uiRenderInstructions.onRecieve) {
242-
case 'modal':
243-
// Handle modal case
244-
void this.renderModal(notification)
245-
break
246-
case 'toast':
247-
// toast case, no user input needed
248-
void vscode.window.showInformationMessage(
249-
notification.uiRenderInstructions.content['en-US'].descriptionPreview ??
250-
notification.uiRenderInstructions.content['en-US'].title
251-
)
252-
break
253-
}
248+
void this.showInformationWindow(notification, notification.uiRenderInstructions.onRecieve)
254249
}
255250
}
256251

@@ -285,4 +280,4 @@ export function registerProvider(provider: ResourceTreeDataProvider) {
285280
NotificationsNode.instance.provider = provider
286281
}
287282

288-
export const testNotificationsNode = new NotificationsNode()
283+
//export const testNotificationsNode = new NotificationsNode()

packages/core/src/notifications/types.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export interface UIRenderInstructions {
5151
[`en-US`]: {
5252
title: string
5353
description: string
54-
descriptionPreview?: string // optional property for toast
54+
toastPreview?: string // optional property for toast
5555
}
5656
}
5757
onRecieve: string
@@ -99,14 +99,16 @@ export type NotificationsState = {
9999

100100
// Util
101101
dismissed: string[]
102+
newlyReceived: string[]
102103
}
103104

104105
export const NotificationsStateConstructor: TypeConstructor<NotificationsState> = (v: unknown): NotificationsState => {
105106
const isNotificationsState = (v: Partial<NotificationsState>): v is NotificationsState => {
106-
const requiredKeys: (keyof NotificationsState)[] = ['startUp', 'emergency', 'dismissed']
107+
const requiredKeys: (keyof NotificationsState)[] = ['startUp', 'emergency', 'dismissed', 'newlyReceived']
107108
return (
108109
requiredKeys.every((key) => key in v) &&
109110
Array.isArray(v.dismissed) &&
111+
Array.isArray(v.newlyReceived) &&
110112
typeof v.startUp === 'object' &&
111113
typeof v.emergency === 'object'
112114
)
@@ -118,7 +120,7 @@ export const NotificationsStateConstructor: TypeConstructor<NotificationsState>
118120
throw new Error('Cannot cast to NotificationsState.')
119121
}
120122

121-
export type NotificationType = keyof Omit<NotificationsState, 'dismissed'>
123+
export type NotificationType = keyof Omit<NotificationsState, 'dismissed' | 'newlyReceived'>
122124

123125
export interface RuleContext {
124126
readonly ideVersion: typeof vscode.version

packages/core/src/test/notifications/controller.test.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ import { randomUUID } from '../../shared'
1414
import { installFakeClock } from '../testUtil'
1515
import { NotificationFetcher, RemoteFetcher, ResourceResponse } from '../../notifications/controller'
1616
import { HttpResourceFetcher } from '../../shared/resourcefetcher/httpResourceFetcher'
17-
import { testNotificationsNode } from '../../notifications/panelNode'
17+
18+
// one test node to use across different tests
19+
// re-declaration would cause a command conflict
20+
export const panelNode: NotificationsNode = new NotificationsNode()
1821

1922
describe('Notifications Controller', function () {
20-
const panelNode: NotificationsNode = testNotificationsNode
2123
const ruleEngine: RuleEngine = new RuleEngine({
2224
ideVersion: '1.83.0',
2325
extensionVersion: '1.20.0',
@@ -117,6 +119,7 @@ describe('Notifications Controller', function () {
117119
},
118120
emergency: {},
119121
dismissed: [],
122+
newlyReceived: ['id:startup2'],
120123
})
121124
assert.equal(panelNode.startUpNotifications.length, 1)
122125
assert.equal(panelNode.emergencyNotifications.length, 0)
@@ -147,6 +150,7 @@ describe('Notifications Controller', function () {
147150
eTag,
148151
},
149152
dismissed: [content.notifications[0].id],
153+
newlyReceived: ['id:emergency2'],
150154
})
151155
assert.equal(panelNode.startUpNotifications.length, 0)
152156
assert.equal(panelNode.emergencyNotifications.length, 1)
@@ -203,6 +207,7 @@ describe('Notifications Controller', function () {
203207
eTag: eTag2,
204208
},
205209
dismissed: [emergencyContent.notifications[0].id],
210+
newlyReceived: ['id:startup2', 'id:emergency2'],
206211
})
207212
assert.equal(panelNode.startUpNotifications.length, 1)
208213
assert.equal(panelNode.emergencyNotifications.length, 1)
@@ -235,6 +240,7 @@ describe('Notifications Controller', function () {
235240
},
236241
emergency: {},
237242
dismissed: [],
243+
newlyReceived: [],
238244
})
239245

240246
await dismissNotification(content.notifications[1])
@@ -247,6 +253,7 @@ describe('Notifications Controller', function () {
247253
},
248254
emergency: {},
249255
dismissed: [content.notifications[1].id],
256+
newlyReceived: [],
250257
})
251258

252259
assert.equal(panelNode.getChildren().length, 1)
@@ -285,6 +292,7 @@ describe('Notifications Controller', function () {
285292
},
286293
emergency: {},
287294
dismissed: [content.notifications[0].id],
295+
newlyReceived: [],
288296
})
289297

290298
assert.equal(panelNode.getChildren().length, 1)
@@ -336,6 +344,7 @@ describe('Notifications Controller', function () {
336344
},
337345
emergency: {},
338346
dismissed: [],
347+
newlyReceived: ['id:startup2'],
339348
})
340349
assert.equal(panelNode.getChildren().length, 1)
341350

@@ -352,6 +361,7 @@ describe('Notifications Controller', function () {
352361
},
353362
emergency: {},
354363
dismissed: [],
364+
newlyReceived: ['id:startup2'],
355365
})
356366
assert.equal(panelNode.getChildren().length, 1)
357367
})
@@ -389,6 +399,7 @@ describe('Notifications Controller', function () {
389399
eTag: '1',
390400
},
391401
dismissed: [emergencyContent.notifications[0].id, startUpContent.notifications[0].id],
402+
newlyReceived: [],
392403
})
393404

394405
const emptyContent = {
@@ -411,6 +422,7 @@ describe('Notifications Controller', function () {
411422
eTag: '1',
412423
},
413424
dismissed: [emergencyContent.notifications[0].id],
425+
newlyReceived: [],
414426
})
415427
assert.equal(panelNode.getChildren().length, 1)
416428

@@ -430,6 +442,7 @@ describe('Notifications Controller', function () {
430442
eTag: '1',
431443
},
432444
dismissed: [],
445+
newlyReceived: [],
433446
})
434447

435448
assert.equal(panelNode.getChildren().length, 0)
@@ -446,6 +459,27 @@ describe('Notifications Controller', function () {
446459
assert.doesNotThrow(() => new NotificationsController(panelNode, fetcher).pollForStartUp(ruleEngine))
447460
assert.ok(wasCalled)
448461
})
462+
463+
it('calls onReceiveNotifications when a new valid notification is added', async function () {
464+
const eTag = randomUUID()
465+
const content = {
466+
schemaVersion: '1.x',
467+
notifications: [getValidTestNotification('id:newValidNotification')],
468+
}
469+
fetcher.setStartUpContent({
470+
eTag,
471+
content: JSON.stringify(content),
472+
})
473+
474+
const onReceiveSpy = sinon.spy(panelNode, 'onReceiveNotifications')
475+
476+
await controller.pollForStartUp(ruleEngine)
477+
478+
assert.equal(onReceiveSpy.callCount, 1)
479+
assert.deepStrictEqual(onReceiveSpy.args[0][0], [content.notifications[0]])
480+
481+
onReceiveSpy.restore()
482+
})
449483
})
450484

451485
describe('RemoteFetcher', function () {

packages/core/src/test/notifications/rendering.test.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,15 @@
66
import * as vscode from 'vscode'
77
import * as sinon from 'sinon'
88
import assert from 'assert'
9-
import { testNotificationsNode, NotificationsNode } from '../../notifications/panelNode'
109
import { ToolkitNotification } from '../../notifications/types'
1110
import fs from '../../shared/fs/fs'
1211
import path from 'path'
1312
import { tempDirPath } from '../../shared/filesystemUtilities'
1413
import { getTestWindow } from '../shared/vscode/window'
14+
import { panelNode } from './controller.test'
1515

1616
describe('Notifications Rendering', function () {
1717
let sandbox: sinon.SinonSandbox
18-
const panelNode: NotificationsNode = testNotificationsNode
1918

2019
beforeEach(function () {
2120
sandbox = sinon.createSandbox()
@@ -33,13 +32,15 @@ describe('Notifications Rendering', function () {
3332
.stub(vscode.workspace, 'openTextDocument')
3433
.resolves({} as vscode.TextDocument)
3534
const writeFileStub = sandbox.stub(fs, 'writeFile').resolves()
35+
const expectedFilePath = path.join(tempDirPath, 'AWSToolkitNotifications.txt')
36+
37+
await fs.writeFile(expectedFilePath, '')
3638

3739
await panelNode.openNotification(notification)
3840

3941
assert.ok(openTxtDocumentStub.calledOnce)
4042
assert.ok(txtDocumentStub.calledOnce)
4143

42-
const expectedFilePath = path.join(tempDirPath, 'AWSToolkitNotifications.txt')
4344
assert.ok(writeFileStub.calledWith(expectedFilePath, expectedContent))
4445
}
4546

@@ -62,7 +63,7 @@ describe('Notifications Rendering', function () {
6263
await panelNode.onReceiveNotifications([notification])
6364

6465
const expectedMessage =
65-
notification.uiRenderInstructions.content['en-US'].descriptionPreview ??
66+
notification.uiRenderInstructions.content['en-US'].toastPreview ??
6667
notification.uiRenderInstructions.content['en-US'].title
6768

6869
const shownMessages = testWindow.shownMessages
@@ -146,7 +147,7 @@ function getToastURLTestNotification(): ToolkitNotification {
146147
[`en-US`]: {
147148
title: 'test',
148149
description: 'This is a url notification.',
149-
descriptionPreview: 'test toast preview',
150+
toastPreview: 'test toast preview',
150151
},
151152
},
152153
onRecieve: 'toast',

0 commit comments

Comments
 (0)