From 3b524c62ef51d71f7977bfa5f204d25b8d2c335f Mon Sep 17 00:00:00 2001 From: David Evans Date: Wed, 4 Sep 2024 08:21:29 +0100 Subject: [PATCH] Use idempotent actions wherever possible, bump dependency to fix server crash, fix issue with popups not dismissing correctly when running locally in strict mode --- backend/package-lock.json | 20 +-- backend/package.json | 4 +- backend/src/index.ts | 5 + backend/src/routers/ApiRetrosRouter.ts | 6 +- backend/src/services/RetroService.test.ts | 2 +- backend/src/services/RetroService.ts | 18 ++- e2e/package-lock.json | 8 +- e2e/package.json | 2 +- frontend/package-lock.json | 20 +-- frontend/package.json | 4 +- frontend/src/actions/autoFacilitate.ts | 15 +- frontend/src/actions/moodRetro.ts | 141 +++++++----------- frontend/src/actions/retro.ts | 49 +++--- frontend/src/api/RetroTracker.ts | 6 +- .../attachments/giphy/GiphyButton.tsx | 30 ++-- frontend/src/components/common/Popup.tsx | 33 ++-- .../retro-formats/mood/MoodRetro.tsx | 6 +- .../retro-settings/SettingsForm.tsx | 2 +- frontend/src/components/retro/RetroPage.tsx | 63 ++++---- scripts/helpers/proc.mjs | 37 ++--- 20 files changed, 219 insertions(+), 252 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index a831bf6..d30d1c9 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -11,12 +11,12 @@ "express": "4.x", "express-static-gzip": "2.x", "ioredis": "5.x", - "json-immutability-helper": "3.1.x", + "json-immutability-helper": "4.0.x", "jwt-simple": "0.5.x", "mongodb": "6.x", "pg": "8.x", "pwd-hasher": "2.x", - "shared-reducer-backend": "3.x", + "shared-reducer": "4.x", "tslib": "2.6.x", "websocket-express": "3.x", "ws": "8.x" @@ -1373,9 +1373,10 @@ } }, "node_modules/json-immutability-helper": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/json-immutability-helper/-/json-immutability-helper-3.1.0.tgz", - "integrity": "sha512-K6V7tORXodxWBejRbfa63w1iqXrXRynIwd9a2Aazr3QvdJuZi5mciuUWNTKPluUOYw2sDPdADvK2PmN6KPAqdQ==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-immutability-helper/-/json-immutability-helper-4.0.0.tgz", + "integrity": "sha512-kVXZ2us3Sn98FfWzY+U4QYOdeffnn/jTznMYDvxSth1C21ixr1cFe4pJudIkZRD4ru6M4Kw+MkOoFIdt5tjKSQ==", + "license": "MIT" }, "node_modules/jwt-simple": { "version": "0.5.6", @@ -1999,10 +2000,11 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, - "node_modules/shared-reducer-backend": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/shared-reducer-backend/-/shared-reducer-backend-3.1.0.tgz", - "integrity": "sha512-CMZV3xEyQyOd57sZN/6OVPS+DcjD5EH8U/WWMn7z6Egzl3Y9mr0Squs9k9jbhuEvZfffDzxufdIHzuddTDspGQ==" + "node_modules/shared-reducer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/shared-reducer/-/shared-reducer-4.0.1.tgz", + "integrity": "sha512-/3vfEKRG2KgZ3uMjJVM7HtXP6kwUYUgZxFORtD1x95B+wwTfOrOE9oXG+uM0kQ/5ge64A459ZeQ7V0FPgDuI+w==", + "license": "MIT" }, "node_modules/side-channel": { "version": "1.0.6", diff --git a/backend/package.json b/backend/package.json index 373c504..52ca14d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,12 +14,12 @@ "express": "4.x", "express-static-gzip": "2.x", "ioredis": "5.x", - "json-immutability-helper": "3.1.x", + "json-immutability-helper": "4.0.x", "jwt-simple": "0.5.x", "mongodb": "6.x", "pg": "8.x", "pwd-hasher": "2.x", - "shared-reducer-backend": "3.x", + "shared-reducer": "4.x", "tslib": "2.6.x", "websocket-express": "3.x", "ws": "8.x" diff --git a/backend/src/index.ts b/backend/src/index.ts index 99a2007..9ecd68b 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -44,6 +44,11 @@ async function refreshApp( } } catch (e) { logError('Failed to start server', e); + // process.exit may lose stream data which has been buffered in NodeJS - wait for it all to be flushed before exiting + await Promise.all([ + new Promise((resolve) => process.stdout.write('', resolve)), + new Promise((resolve) => process.stderr.write('', resolve)), + ]); process.exit(1); } } diff --git a/backend/src/routers/ApiRetrosRouter.ts b/backend/src/routers/ApiRetrosRouter.ts index 4084e50..5cd6fa0 100644 --- a/backend/src/routers/ApiRetrosRouter.ts +++ b/backend/src/routers/ApiRetrosRouter.ts @@ -1,5 +1,5 @@ import { WebSocketExpress, Router, type JWTPayload } from 'websocket-express'; -import sharedReducerBackend from 'shared-reducer-backend'; +import { websocketHandler } from 'shared-reducer/backend'; import { ApiRetroArchivesRouter } from './ApiRetroArchivesRouter'; import { type UserAuthService } from '../services/UserAuthService'; import { type RetroAuthService } from '../services/RetroAuthService'; @@ -33,9 +33,7 @@ export class ApiRetrosRouter extends Router { (token): JWTPayload | null => userAuthService.readAndVerifyToken(token), ); - const wsHandler = sharedReducerBackend.websocketHandler( - retroService.retroBroadcaster, - ); + const wsHandler = websocketHandler(retroService.retroBroadcaster); this.get('/', userAuthMiddleware, async (_, res) => { const userId = WebSocketExpress.getAuthData(res).sub!; diff --git a/backend/src/services/RetroService.test.ts b/backend/src/services/RetroService.test.ts index 3b31fb3..d6aa3af 100644 --- a/backend/src/services/RetroService.test.ts +++ b/backend/src/services/RetroService.test.ts @@ -1,7 +1,7 @@ import { randomBytes } from 'node:crypto'; import { MemoryDb } from '../import-wrappers/collection-storage-wrap'; import { type Spec } from 'json-immutability-helper'; -import { type ChangeInfo, type Subscription } from 'shared-reducer-backend'; +import { type ChangeInfo, type Subscription } from 'shared-reducer/backend'; import { makeRetroItem, type Retro } from '../shared/api-entities'; import { RetroService } from './RetroService'; diff --git a/backend/src/services/RetroService.ts b/backend/src/services/RetroService.ts index c08733a..81b0da1 100644 --- a/backend/src/services/RetroService.ts +++ b/backend/src/services/RetroService.ts @@ -6,7 +6,13 @@ import { encryptByRecordWithMasterKey, migrate, } from '../import-wrappers/collection-storage-wrap'; -import srb, { type Permission } from 'shared-reducer-backend'; +import { + Broadcaster, + CollectionStorageModel, + ReadOnly, + ReadWriteStruct, + type Permission, +} from 'shared-reducer/backend'; import { type Retro, type RetroSummary } from '../shared/api-entities'; import { extractRetro } from '../helpers/jsonParsers'; @@ -30,7 +36,7 @@ function dbErrorMessage(e: any): string { } export class RetroService { - public readonly retroBroadcaster: srb.Broadcaster>; + public readonly retroBroadcaster: Broadcaster>; private readonly retroCollection: Collection; @@ -54,7 +60,7 @@ export class RetroService { ), ); - const model = new srb.CollectionStorageModel( + const model = new CollectionStorageModel( this.retroCollection, 'id', (x) => { @@ -66,16 +72,16 @@ export class RetroService { (e) => new Error(dbErrorMessage(e)), ); - this.retroBroadcaster = srb.Broadcaster.for(model) + this.retroBroadcaster = Broadcaster.for(model) .withReducer>(context.with(listCommands)) .build(); } public getPermissions(allowWrite: boolean): Permission> { if (allowWrite) { - return new srb.ReadWriteStruct(['id', 'ownerId']); + return new ReadWriteStruct(['id', 'ownerId']); } - return srb.ReadOnly; + return ReadOnly; } public async getRetroIdForSlug(slug: string): Promise { diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 9cbbdf8..1ee0489 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -9,7 +9,7 @@ "@tsconfig/strictest": "2.x", "@types/node": "20.x", "@types/selenium-webdriver": "4.x", - "chromedriver": "127.x", + "chromedriver": "128.x", "geckodriver": "4.4.x", "lean-test": "2.x", "prettier": "3.3.2", @@ -265,9 +265,9 @@ } }, "node_modules/chromedriver": { - "version": "127.0.3", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-127.0.3.tgz", - "integrity": "sha512-trUHkFt0n7jGzNOgkO1srOJfz50kKyAGJ016PyV0hrtyKNIGnOC9r3Jlssz19UoEjSzI/1g2shEiIFtDbBYVaw==", + "version": "128.0.0", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-128.0.0.tgz", + "integrity": "sha512-Ggo21z/dFQxTOTgU0vm0V59Mi79yyR+9AUk/KiVAsRfbDRdVZQYQWfgxnIvD/x8KOKn0oB7haRzDO/KfrKyvOA==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", diff --git a/e2e/package.json b/e2e/package.json index 043363c..806dd95 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -10,7 +10,7 @@ "@tsconfig/strictest": "2.x", "@types/node": "20.x", "@types/selenium-webdriver": "4.x", - "chromedriver": "127.x", + "chromedriver": "128.x", "geckodriver": "4.4.x", "lean-test": "2.x", "prettier": "3.3.2", diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 95bb4c4..85b7477 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,7 +8,7 @@ "dependencies": { "@openfonts/open-sans_all": "1.x", "classnames": "2.x", - "json-immutability-helper": "3.1.x", + "json-immutability-helper": "4.0.x", "lean-qr": "2.x", "react": "18.x", "react-dom": "18.x", @@ -16,7 +16,7 @@ "react-hook-final-countdown": "2.x", "react-modal": "3.x", "rxjs": "7.x", - "shared-reducer-frontend": "3.x", + "shared-reducer": "4.x", "wouter": "2.10.1" }, "devDependencies": { @@ -7897,9 +7897,10 @@ } }, "node_modules/json-immutability-helper": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/json-immutability-helper/-/json-immutability-helper-3.1.0.tgz", - "integrity": "sha512-K6V7tORXodxWBejRbfa63w1iqXrXRynIwd9a2Aazr3QvdJuZi5mciuUWNTKPluUOYw2sDPdADvK2PmN6KPAqdQ==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-immutability-helper/-/json-immutability-helper-4.0.0.tgz", + "integrity": "sha512-kVXZ2us3Sn98FfWzY+U4QYOdeffnn/jTznMYDvxSth1C21ixr1cFe4pJudIkZRD4ru6M4Kw+MkOoFIdt5tjKSQ==", + "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", @@ -10361,10 +10362,11 @@ "node": ">=8" } }, - "node_modules/shared-reducer-frontend": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shared-reducer-frontend/-/shared-reducer-frontend-3.0.0.tgz", - "integrity": "sha512-nzI/nqUV6qk0SXQW1HUdkiDMypLpXip1rxrnLPASAMhIO5f/ddxoNOG5LcfRLKShugLAyBdMsdC9FFyzKF/Khg==" + "node_modules/shared-reducer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/shared-reducer/-/shared-reducer-4.0.1.tgz", + "integrity": "sha512-/3vfEKRG2KgZ3uMjJVM7HtXP6kwUYUgZxFORtD1x95B+wwTfOrOE9oXG+uM0kQ/5ge64A459ZeQ7V0FPgDuI+w==", + "license": "MIT" }, "node_modules/shebang-command": { "version": "2.0.0", diff --git a/frontend/package.json b/frontend/package.json index bb815e4..ebf44aa 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,7 +12,7 @@ "dependencies": { "@openfonts/open-sans_all": "1.x", "classnames": "2.x", - "json-immutability-helper": "3.1.x", + "json-immutability-helper": "4.0.x", "lean-qr": "2.x", "react": "18.x", "react-dom": "18.x", @@ -20,7 +20,7 @@ "react-hook-final-countdown": "2.x", "react-modal": "3.x", "rxjs": "7.x", - "shared-reducer-frontend": "3.x", + "shared-reducer": "4.x", "wouter": "2.10.1" }, "devDependencies": { diff --git a/frontend/src/actions/autoFacilitate.ts b/frontend/src/actions/autoFacilitate.ts index 26cd122..e0b6ea5 100644 --- a/frontend/src/actions/autoFacilitate.ts +++ b/frontend/src/actions/autoFacilitate.ts @@ -5,7 +5,10 @@ interface CategoryStats { remaining: number; } -function getCategories(items: RetroItem[]) { +function getCategories( + items: RetroItem[], + isRemaining: (i: RetroItem) => boolean, +) { const categories = new Set(items.map((item) => item.category)); const result = new Map(); @@ -14,7 +17,7 @@ function getCategories(items: RetroItem[]) { result.set(category, { total: all.length, - remaining: all.filter((item) => item.doneTime === 0).length, + remaining: all.filter(isRemaining).length, }); } @@ -39,14 +42,18 @@ function itemPriority(a: RetroItem, b: RetroItem): number { export function autoFacilitate( items: RetroItem[], categoryPreferences: string[], + currentItemID: string | null, ): RetroItem | undefined { - const remainingItems = items.filter((item) => item.doneTime === 0); + const isRemaining = (item: RetroItem) => + item.doneTime === 0 && item.id !== currentItemID; + + const remainingItems = items.filter(isRemaining); if (remainingItems.length === 0) { return undefined; } - const categories = getCategories(items); + const categories = getCategories(items, isRemaining); if (remainingItems.length > 1) { // reserve a preferred item for last diff --git a/frontend/src/actions/moodRetro.ts b/frontend/src/actions/moodRetro.ts index 01417e9..7d34260 100644 --- a/frontend/src/actions/moodRetro.ts +++ b/frontend/src/actions/moodRetro.ts @@ -2,12 +2,20 @@ import { type Retro, type RetroItem } from '../shared/api-entities'; import { type RetroDispatchSpec } from '../api/RetroTracker'; import { setRetroItemDone, setRetroState, addRetroItem } from './retro'; import { autoFacilitate } from './autoFacilitate'; +import { type Spec } from '../api/reducer'; +import { type Condition } from 'json-immutability-helper'; export interface MoodRetroStateT { focusedItemId?: string | null; focusedItemTimeout?: number; } +type NextIDPicker = ( + group: string | undefined, + state: Retro, + focusedItemId: string | null, +) => string | null; + const INITIAL_TIMEOUT = 5 * 60 * 1000 + 999; export const addRetroActionItem = addRetroItem.bind(null, 'action'); @@ -18,36 +26,33 @@ const moodItem = (!group || !item.group || item.group === group) && item.category !== 'action'; -function pickNextItem( - group: string | undefined, - items: RetroItem[], -): RetroItem | undefined { - return autoFacilitate(items.filter(moodItem(group)), ['happy', 'meh']); -} +const pickNextItem: NextIDPicker = (group, { items }, currentItemID) => + autoFacilitate(items.filter(moodItem(group)), ['happy', 'meh'], currentItemID) + ?.id ?? null; -function pickPreviousItem( - group: string | undefined, - items: RetroItem[], -): RetroItem | undefined { +const pickPreviousItem: NextIDPicker = (group, { items }, currentItemID) => { const history = items .filter(moodItem(group)) - .filter((item) => item.doneTime > 0) + .filter((item) => item.doneTime > 0 && item.id !== currentItemID) .sort((a, b) => b.doneTime - a.doneTime); - return history[0]; -} + return history[0]?.id ?? null; +}; -function getState(retro: Retro, group: string | undefined): T { +function getState( + group: string | undefined, + retro: Retro, +): T | Record { if (!group) { return retro.state; } - return retro.groupStates[group] || ({} as T); + return retro.groupStates[group] || {}; } export const allItemsDoneCallback = ( callback?: () => void, ): RetroDispatchSpec => [ - ({ items }: Retro): null => { - if (callback && !pickNextItem(undefined, items)) { + (state: Retro): null => { + if (callback && !pickNextItem(undefined, state, null)) { callback(); } return null; @@ -57,85 +62,49 @@ export const allItemsDoneCallback = ( export const setItemTimeout = ( group: string | undefined, duration: number, -): RetroDispatchSpec => - setRetroState(group, { - focusedItemTimeout: Date.now() + duration, - }); - -export const focusItem = ( - group: string | undefined, - id: string | null, -): RetroDispatchSpec => [ - ...setRetroItemDone(id, false), - ...setRetroState(group, { focusedItemId: id }), -]; +): Spec[] => + setRetroState(group, { focusedItemTimeout: Date.now() + duration }); export const switchFocus = ( group: string | undefined, - markPreviousDone: boolean, - id: string | null, -): RetroDispatchSpec => [ - (retro): RetroDispatchSpec => { - const { focusedItemId = null } = getState(retro, group); - - return [ - ...(markPreviousDone && focusedItemId - ? setRetroItemDone(focusedItemId, true) - : []), - ...focusItem(group, id), - ...setItemTimeout(group, INITIAL_TIMEOUT), - ]; - }, -]; - -const focusNextItem = - (group: string | undefined) => - ({ items }: Retro): RetroDispatchSpec => { - const next = pickNextItem(group, items); - return focusItem(group, next?.id ?? null); - }; - -const focusPreviousItem = - (group: string | undefined) => - ({ items }: Retro): RetroDispatchSpec => { - const next = pickPreviousItem(group, items); - return focusItem(group, next?.id ?? null); - }; - -export const goNext = ( - group: string | undefined, - expectedFocusedItemId?: string, + nextIDPicker: NextIDPicker, + { + expectCurrentId, + setCurrentDone = false, + timeout = INITIAL_TIMEOUT, + }: { + expectCurrentId?: string | undefined; + setCurrentDone?: boolean | undefined; + timeout?: number | undefined; + } = {}, ): RetroDispatchSpec => [ - (retro): RetroDispatchSpec => { - const { focusedItemId = null } = getState(retro, group); - - if (expectedFocusedItemId && focusedItemId !== expectedFocusedItemId) { + (retro): Spec[] => { + const { focusedItemId = null } = getState(group, retro); + if (expectCurrentId && focusedItemId !== expectCurrentId) { return []; } - return [ - ...setRetroItemDone(focusedItemId, true), - focusNextItem(group), - ...setItemTimeout(group, INITIAL_TIMEOUT), + const id = nextIDPicker(group, retro, focusedItemId); + const actions: Spec = [ + 'seq', + ...(focusedItemId ? setRetroItemDone(focusedItemId, setCurrentDone) : []), + ...(id ? setRetroItemDone(id, false) : []), + ...setRetroState(group, { + focusedItemId: id, + focusedItemTimeout: Date.now() + timeout, + }), ]; + const condition: Condition = group + ? { groupStates: { [group]: { focusedItemId: ['~=', focusedItemId] } } } + : { state: { focusedItemId: ['~=', focusedItemId] } }; + return [['if', condition, actions]]; }, ]; +export const goNext = (group: string | undefined, expectCurrentId?: string) => + switchFocus(group, pickNextItem, { expectCurrentId, setCurrentDone: true }); + export const goPrevious = ( group: string | undefined, - expectedFocusedItemId?: string, -): RetroDispatchSpec => [ - (retro): RetroDispatchSpec => { - const { focusedItemId = null } = getState(retro, group); - - if (expectedFocusedItemId && focusedItemId !== expectedFocusedItemId) { - return []; - } - - return [ - ...setRetroItemDone(focusedItemId, false), - focusPreviousItem(group), - ...setItemTimeout(group, 0), - ]; - }, -]; + expectCurrentId?: string, +) => switchFocus(group, pickPreviousItem, { expectCurrentId, timeout: 0 }); diff --git a/frontend/src/actions/retro.ts b/frontend/src/actions/retro.ts index b1458a7..780a127 100644 --- a/frontend/src/actions/retro.ts +++ b/frontend/src/actions/retro.ts @@ -1,8 +1,8 @@ import { + type Retro, type RetroItem, type UserProvidedRetroItemDetails, } from '../shared/api-entities'; -import { type RetroDispatchSpec } from '../api/RetroTracker'; import { type Spec } from '../api/reducer'; const IRRELEVANT_WHITESPACE = /[ \t\v]+/g; @@ -15,7 +15,7 @@ function sanitiseInput(value: string): string { export const setRetroState = ( group: string | undefined, delta: Record, -): RetroDispatchSpec => { +): Spec[] => { if (!group) { return [{ state: ['merge', delta] }]; } @@ -26,7 +26,7 @@ export const addRetroItem = ( category: string, group: string | undefined, { message, ...rest }: Partial, -): RetroDispatchSpec => { +): Spec[] => { const sanitisedMessage = sanitiseInput(message || ''); if (!sanitisedMessage) { return []; @@ -44,23 +44,20 @@ export const addRetroItem = ( ...rest, }; - return [{ items: ['push', item] }]; + return [{ items: ['if', ['none', { id: ['=', item.id] }], ['push', item]] }]; }; -function updateItem( - itemId: string | null, +const updateItem = ( + itemId: string, updater: Spec, -): RetroDispatchSpec { - if (itemId === null) { - return []; - } - return [{ items: ['updateWhere', ['id', itemId], updater] }]; -} +): Spec[] => [ + { items: ['update', ['first', { id: ['=', itemId] }], updater] }, +]; export const editRetroItem = ( itemId: string, { message, ...rest }: Partial, -): RetroDispatchSpec => { +): Spec[] => { const merge: Partial = { ...rest }; if (message !== undefined) { const sanitisedMessage = sanitiseInput(message); @@ -73,30 +70,28 @@ export const editRetroItem = ( }; export const setRetroItemDone = ( - itemId: string | null, + itemId: string, done: boolean, -): RetroDispatchSpec => - updateItem(itemId, { - doneTime: ['=', done ? Date.now() : 0], - }); +): Spec[] => + updateItem(itemId, { doneTime: ['=', done ? Date.now() : 0] }); -export const upvoteRetroItem = (itemId: string): RetroDispatchSpec => - updateItem(itemId, { - votes: ['+', 1], - }); +export const upvoteRetroItem = (itemId: string): Spec[] => + updateItem(itemId, { votes: ['+', 1] }); -export const deleteRetroItem = (itemId: string): RetroDispatchSpec => [ - { items: ['deleteWhere', ['id', itemId]] }, +export const deleteRetroItem = (itemId: string): Spec[] => [ + { items: ['delete', ['all', { id: ['=', itemId] }]] }, ]; -export const clearCovered = (): RetroDispatchSpec => [ +export const clearCovered = (): Spec[] => [ { state: ['=', {}], groupStates: ['=', {}], items: [ 'seq', - ['deleteWhere', { key: 'category', not: 'action' }], - ['deleteWhere', { key: 'doneTime', greaterThan: 0 }], + [ + 'delete', + ['all', ['or', { category: ['!=', 'action'] }, { doneTime: ['>', 0] }]], + ], ], }, ]; diff --git a/frontend/src/api/RetroTracker.ts b/frontend/src/api/RetroTracker.ts index b0f6ae7..019c60f 100644 --- a/frontend/src/api/RetroTracker.ts +++ b/frontend/src/api/RetroTracker.ts @@ -1,4 +1,8 @@ -import SharedReducer, { Dispatch, DispatchSpec } from 'shared-reducer-frontend'; +import { + SharedReducer, + type Dispatch, + type DispatchSpec, +} from 'shared-reducer/frontend'; import { type Retro } from '../shared/api-entities'; import { SubscriptionTracker } from './SubscriptionTracker'; import { context, type Spec } from './reducer'; diff --git a/frontend/src/components/attachments/giphy/GiphyButton.tsx b/frontend/src/components/attachments/giphy/GiphyButton.tsx index f4062a4..bf6a2fe 100644 --- a/frontend/src/components/attachments/giphy/GiphyButton.tsx +++ b/frontend/src/components/attachments/giphy/GiphyButton.tsx @@ -20,23 +20,6 @@ export const GiphyButton = memo( onChange(newAttachment); }); - let popup = null; - if (visible) { - popup = { - title: 'Insert Giphy Image', - content: ( - - ), - keys: { - Escape: hide, - }, - }; - } - return ( <> - + + + ); }, diff --git a/frontend/src/components/common/Popup.tsx b/frontend/src/components/common/Popup.tsx index 6c8129c..329cbd2 100644 --- a/frontend/src/components/common/Popup.tsx +++ b/frontend/src/components/common/Popup.tsx @@ -1,18 +1,14 @@ -import { type FC, useState, type ReactNode } from 'react'; +import { type FC, useState, type PropsWithChildren } from 'react'; import Modal from 'react-modal'; import { useEvent } from '../../hooks/useEvent'; import { useListener } from '../../hooks/useListener'; import './Popup.less'; -export interface PopupData { +interface PropsT { title: string; hideTitle?: boolean; - content: ReactNode; keys?: Record void>; -} - -interface PropsT { - data: PopupData | null; + isOpen: boolean; onClose: () => void; } @@ -20,7 +16,14 @@ function stopProp(e: Event) { e.stopPropagation(); } -export const Popup: FC = ({ data, onClose }) => { +export const Popup: FC> = ({ + title, + hideTitle = false, + keys, + isOpen, + onClose, + children, +}) => { const handleKeyDown = useEvent((e: KeyboardEvent) => { e.stopPropagation(); const t = e.target as Element; @@ -31,7 +34,7 @@ export const Popup: FC = ({ data, onClose }) => { ) { return; } - const fn = data?.keys?.[e.key]; + const fn = keys?.[e.key]; if (fn) { e.preventDefault(); if (!e.repeat) { @@ -47,13 +50,9 @@ export const Popup: FC = ({ data, onClose }) => { useListener(modal, 'keypress', stopProp); useListener(modal, 'keydown', handleKeyDown); - if (!data) { - return null; - } - return ( = ({ data, onClose }) => { contentRef={setModal} aria={{ labelledby: 'modal-heading' }} > -

- {data.title} +

+ {title}

- {data.content} + {children}
); }; diff --git a/frontend/src/components/retro-formats/mood/MoodRetro.tsx b/frontend/src/components/retro-formats/mood/MoodRetro.tsx index 96b09b1..d512f2e 100644 --- a/frontend/src/components/retro-formats/mood/MoodRetro.tsx +++ b/frontend/src/components/retro-formats/mood/MoodRetro.tsx @@ -87,7 +87,7 @@ export const MoodRetro = ({ setItemTimeout(group, duration), ); const handleSelectItem = useFacilitatorAction((id: string | null) => - switchFocus(group, true, id), + switchFocus(group, () => id, { setCurrentDone: true }), ); const handleSetActionItemDone = useAction(setRetroItemDone); const handleGoNext = useFacilitatorAction(() => [ @@ -100,10 +100,10 @@ export const MoodRetro = ({ ArrowRight: handleGoNext, ArrowLeft: handleGoPrevious, Enter: useFacilitatorAction(() => [ - ...switchFocus(group, true, null), + ...switchFocus(group, () => null, { setCurrentDone: true }), ...allItemsDoneCallback(onComplete), ]), - Escape: useFacilitatorAction(() => switchFocus(group, false, null)), + Escape: useFacilitatorAction(() => switchFocus(group, () => null)), }); const createMoodSection = (category: Category): ReactElement => ( diff --git a/frontend/src/components/retro-settings/SettingsForm.tsx b/frontend/src/components/retro-settings/SettingsForm.tsx index 438e8bf..6facf12 100644 --- a/frontend/src/components/retro-settings/SettingsForm.tsx +++ b/frontend/src/components/retro-settings/SettingsForm.tsx @@ -1,5 +1,5 @@ import { useState, memo } from 'react'; -import { actionsSyncedCallback } from 'shared-reducer-frontend'; +import { actionsSyncedCallback } from 'shared-reducer/frontend'; import { type Retro } from '../../shared/api-entities'; import { type RetroDispatch } from '../../api/RetroTracker'; import { Input } from '../common/Input'; diff --git a/frontend/src/components/retro/RetroPage.tsx b/frontend/src/components/retro/RetroPage.tsx index ea64936..99c0b57 100644 --- a/frontend/src/components/retro/RetroPage.tsx +++ b/frontend/src/components/retro/RetroPage.tsx @@ -3,7 +3,7 @@ import { type Retro } from '../../shared/api-entities'; import { type RetroPagePropsT } from '../RetroRouter'; import { ArchivePopup } from './ArchivePopup'; import { Header } from '../common/Header'; -import { Popup, PopupData } from '../common/Popup'; +import { Popup } from '../common/Popup'; import { useEvent } from '../../hooks/useEvent'; import { useWindowSize, type Size } from '../../hooks/env/useWindowSize'; import { clearCovered } from '../../actions/retro'; @@ -15,11 +15,14 @@ import './RetroPage.less'; const BLANK_STATE = {}; -function getState(retro: Retro, group: string | undefined): T { +function getState( + group: string | undefined, + retro: Retro, +): T | Record { if (!group) { return retro.state; } - return retro.groupStates[group] || (BLANK_STATE as T); + return retro.groupStates[group] || BLANK_STATE; } type PropsT = Pick< @@ -64,36 +67,6 @@ export const RetroPage = memo( const canFacilitate = !smallScreen || OPTIONS.enableMobileFacilitation.read(retro.options); - let archivePopup: PopupData | null = null; - if (retroDispatch && archivePopupVisible) { - archivePopup = { - title: 'Create Archive', - content: ( - - ), - keys: { - Enter: performArchive, - Escape: hideArchivePopup, - }, - }; - } - - let invitePopup: PopupData | null = null; - if (invitePopupVisible) { - invitePopup = { - title: 'Invite', - hideTitle: true, - content: , - keys: { - Enter: hideInvitePopup, - Escape: hideInvitePopup, - }, - }; - } - const canArchive = Boolean( retroDispatch && retro && @@ -127,14 +100,32 @@ export const RetroPage = memo( retroFormat={retro.format} retroOptions={retro.options} retroItems={retro.items} - retroState={getState(retro, group)} + retroState={getState(group, retro)} group={group} dispatch={retroDispatch ?? undefined} onComplete={canArchive ? showArchivePopup : undefined} archive={false} /> - - + + + + + + ); }, diff --git a/scripts/helpers/proc.mjs b/scripts/helpers/proc.mjs index 008b1fd..e8990de 100644 --- a/scripts/helpers/proc.mjs +++ b/scripts/helpers/proc.mjs @@ -40,6 +40,15 @@ export function propagateStreamWithPrefix( const suf = target.isTTY ? '\u001B[0m\n' : '\n'; const curLine = Buffer.alloc(MAX_LINE_BUFFER); let curLineP = 0; + const printCurrentLine = (data) => { + target.write(pre); + if (curLineP > 0) { + target.write(curLine.subarray(0, curLineP)); + curLineP = 0; + } + target.write(data); + target.write(suf); + }; stream.on('data', (data) => { let begin = 0; while (true) { @@ -47,34 +56,20 @@ export function propagateStreamWithPrefix( if (p === -1) { break; } - target.write(pre); - if (curLineP > 0) { - target.write(curLine.subarray(0, curLineP)); - curLineP = 0; - } - target.write(data.subarray(begin, p)); - target.write(suf); + printCurrentLine(data.subarray(begin, p)); begin = p + 1; } - if (curLineP + data.length - begin > MAX_LINE_BUFFER) { - target.write(pre); - if (curLineP > 0) { - target.write(curLine.subarray(0, curLineP)); - curLineP = 0; - } - target.write(data.subarray(begin)); - target.write(suf); - } else { - data.copy(curLine, curLineP, begin); - curLineP += data.length - begin; + const remaining = data.length - begin; + if (curLineP + remaining > MAX_LINE_BUFFER) { + printCurrentLine(data.subarray(begin)); + } else if (remaining > 0) { + curLineP += data.copy(curLine, curLineP, begin); } }); return new Promise((resolve) => { stream.on('close', () => { if (curLineP > 0) { - target.write(pre); - target.write(curLine.subarray(0, curLineP)); - target.write(suf); + printCurrentLine(Buffer.of()); } resolve(); });