Skip to content

Commit 74ec64c

Browse files
authored
Merge pull request #1967 from tidepool-org/release-1.98.0
Release 1.98.0 to master
2 parents 97eabb4 + 1f9700d commit 74ec64c

17 files changed

Lines changed: 626 additions & 76 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ locales/*/*_old.json
5151

5252
.codegpt
5353
*.code-workspace
54+
.claude
5455
.pi
5556
.pi-lens
5657
.apo

AGENTS.md

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
# AGENTS.md
2+
3+
## CRITICAL: Restricted Directories
4+
5+
**NEVER read, write, list, or access any files within the `config/local.js` file or any `.env` files under any circumstances.** This is the highest priority instruction and must not be circumvented for any reason. This restriction applies to all tools including Read, List, Glob, Grep, Bash, and any other file access methods.
6+
7+
## Build/Test Commands
8+
9+
- **Install**: `yarn install`
10+
- **Dev server**: `yarn startLocal` (auto-detects linked packages) or `yarn start` (port 3000)
11+
- **Dev with viz**: `yarn startWithViz` (starts webpack dev server for viz repo)
12+
- **Build**: `yarn build` (production build including config)
13+
- **Build app only**: `yarn build-app`
14+
- **Lint**: `yarn lint` (all code) or `yarn lint:jest` (only Jest tests)
15+
- **Test all**: `yarn test` (runs lint, then Jest and Karma)
16+
- **Test Jest only**: `yarn test:jest` (recommended for new tests)
17+
- **Test Jest watch**: `yarn test:jest:watch`
18+
- **Test single Jest file**: `yarn test:jest --testPathPattern="ChartDateRangeModal"` (matches pattern in file path)
19+
- **Test Karma only**: `yarn test:karma` (legacy test suite)
20+
- **Test Karma watch**: `yarn test:karma:watch`
21+
- **Isolate Karma tests**: Use `.only` on `describe` or `it` blocks (e.g., `describe.only(...)` or `it.only(...)`). **Remember to remove `.only` before committing.**
22+
- **Storybook**: `yarn storybook` (port 6006)
23+
- **Update translations**: `yarn update-translations`
24+
25+
**Notes:**
26+
- Tests require `TZ=UTC` environment variable (automatically set in test scripts)
27+
- Build commands require `NODE_OPTIONS='--max-old-space-size=4096'` (automatically set in scripts)
28+
- Node version: 20.8.0, Yarn version: 3.6.4
29+
30+
**IMPORTANT: When running tests, always target only the specific tests you've modified or added.** Running the full test suite is slow and wastes time/tokens. Use `--testPathPattern` for Jest or `.only` for Karma to run targeted tests.
31+
32+
## Project Structure
33+
34+
- `app/` - Application source code
35+
- `app/components/` - Reusable components
36+
- `app/pages/` - Page-level components
37+
- `app/redux/` - Redux actions, reducers, store
38+
- `app/themes/` - theme-ui theme configuration
39+
- `app/core/` - Utilities and helpers
40+
- `test/` - Karma/Mocha tests (legacy, mirrors app/ structure)
41+
- `__tests__/` - Jest tests (new tests, mirrors app/ structure)
42+
- `stories/` - Storybook stories
43+
- `config/` - Environment configuration
44+
45+
## Code Style (ESLint: babel-eslint, react-hooks)
46+
47+
### Import Ordering
48+
Group imports in this order with blank lines between groups:
49+
1. React imports (`react`, `react-dom`)
50+
2. PropTypes
51+
3. Redux (`react-redux`, `connected-react-router`)
52+
4. Third-party libraries (moment, formik, etc.)
53+
5. Lodash specific imports (e.g., `import get from 'lodash/get'`)
54+
6. theme-ui (`import { Box, Flex, Text, Divider } from 'theme-ui'`)
55+
7. Local imports (components, utilities, actions, etc.)
56+
57+
Example:
58+
```javascript
59+
import React, { useState, useEffect } from 'react';
60+
import PropTypes from 'prop-types';
61+
import { useDispatch, useSelector } from 'react-redux';
62+
import { withTranslation } from 'react-i18next';
63+
import moment from 'moment';
64+
import get from 'lodash/get';
65+
import map from 'lodash/map';
66+
import { Box, Flex, Text } from 'theme-ui';
67+
import Button from '../../components/elements/Button';
68+
import * as actions from '../../redux/actions/async';
69+
```
70+
71+
### General Style Rules
72+
- Use ES6: `const`/`let` (never `var`), arrow functions, destructuring
73+
- Strings: Single quotes (enforced by ESLint)
74+
- Semicolons: Required
75+
- Lodash: Use specific imports (`import get from 'lodash/get'`), not full lodash
76+
- PropTypes: Required for all component props
77+
- Naming:
78+
- Components: PascalCase (`DataConnections.js`)
79+
- Utilities: camelCase (`personutils.js`)
80+
- Constants: UPPER_SNAKE_CASE
81+
- React: Functional components with hooks (useState, useEffect, useCallback, useMemo)
82+
- Redux: `useDispatch()` and `useSelector()` hooks, not `connect()`
83+
- Translations: Use `react-i18next` with `useTranslation()` hook or `withTranslation()` HOC
84+
85+
### theme-ui Patterns
86+
- Use theme-ui components for layout: `Box`, `Flex`, `Text`, `Divider`, `Link`
87+
- Use variant prop for styling: `variant="containers.smallBordered"`
88+
- Use sx prop for custom styles: `sx={{ textAlign: 'center' }}`
89+
- Common patterns:
90+
```javascript
91+
<Box variant="containers.smallBordered" p={4} mb={3}>
92+
<Flex sx={{ justifyContent: 'space-between' }}>
93+
<Text>Content</Text>
94+
</Flex>
95+
<Divider my={3} />
96+
</Box>
97+
```
98+
99+
### Hook Usage Patterns
100+
- Extract complex logic into custom hooks
101+
- Use `useCallback` for functions passed as props to prevent re-renders
102+
- Use `useMemo` for expensive computations
103+
- Follow react-hooks/exhaustive-deps rules (ESLint warnings guide you)
104+
105+
## Testing Patterns
106+
107+
### Framework Choice
108+
- **New tests**: Use Jest with @testing-library/react in `__tests__/`
109+
- **Legacy tests**: Karma/Mocha in `test/` (maintain existing, don't expand)
110+
- **Minor updates to existing code**: When updating existing code that only has Karma/Mocha tests in `test/`, add tests to the existing test file rather than creating a new Jest test file. This keeps related tests together and avoids duplication.
111+
112+
### Jest Tests (Preferred)
113+
Located in `__tests__/` mirroring `app/` structure:
114+
```javascript
115+
/* global jest, expect, describe, beforeEach, afterEach, it */
116+
117+
import React from 'react';
118+
import { render, screen } from '@testing-library/react';
119+
import userEvent from '@testing-library/user-event';
120+
import ComponentName from '@app/components/ComponentName';
121+
122+
describe('ComponentName', () => {
123+
const mockFn = jest.fn();
124+
125+
beforeEach(() => {
126+
mockFn.mockClear();
127+
});
128+
129+
it('should render correctly', () => {
130+
render(<ComponentName prop={mockFn} />);
131+
expect(screen.getByText('Expected Text')).toBeInTheDocument();
132+
});
133+
});
134+
```
135+
136+
### Karma Tests (Legacy)
137+
Located in `test/` mirroring `app/` structure:
138+
```javascript
139+
/* global chai, sinon, describe, it, expect, beforeEach, afterEach */
140+
141+
import ComponentName from '../../../../app/components/ComponentName';
142+
143+
describe('ComponentName', () => {
144+
const stub = sinon.stub();
145+
146+
beforeEach(() => {
147+
stub.reset();
148+
});
149+
150+
it('should render correctly', () => {
151+
// Enzyme or manual DOM testing
152+
});
153+
});
154+
```
155+
156+
### Common Patterns
157+
- Mock functions: `jest.fn()` (Jest) or `sinon.stub()` (Karma)
158+
- Clean up in `afterEach` or `beforeEach`
159+
- Use descriptive test names: "should do X when Y"
160+
- Test user interactions with `userEvent` (Jest/@testing-library)
161+
162+
## Code Reuse Guidelines
163+
164+
When implementing new features or adding device-specific logic:
165+
166+
- **Prefer extending existing methods** over creating new device-specific methods
167+
- Add optional parameters (e.g., `opts = {}`) to existing functions to customize behavior
168+
- Use patterns like `variant`, `sx`, or conditional props to adapt generic components for specific use cases
169+
- Only create new components/methods when the logic is fundamentally different, not just when parameters vary
170+
- This reduces duplication, simplifies testing, and makes the codebase easier to maintain
171+
- Example: Instead of `SpecialButton`, extend `Button` with a `variant` prop
172+
173+
## Git Commit Messages
174+
175+
**After completing ANY task that modifies files**, provide a commit message suggestion in this format:
176+
177+
```
178+
<Imperative summary (50 chars or less)>
179+
180+
<Optional body: 2-4 sentences>
181+
182+
<Optional bullet points, one per line with "- ">
183+
```
184+
185+
**Rules:**
186+
- Summary: 50 chars max, imperative mood ("Add X", not "Added X")
187+
- Body: Concise, blank line between sections
188+
- Bullets: Use "- " prefix for lists
189+
190+
**Examples:**
191+
192+
```
193+
Add OAuth consent dialog with reproductive health notice
194+
195+
Implemented accept status rendering with image, dividers,
196+
and mobile-responsive layout for ŌURA data consent.
197+
198+
- Added consent_data.png image
199+
- Implemented responsive Flex layout
200+
- Added dividers for accept status only
201+
```
202+
203+
```
204+
Fix import ordering in DataConnections component
205+
206+
Reorganized imports to follow project conventions with
207+
proper grouping and spacing between categories.
208+
209+
- Moved theme-ui imports to correct position
210+
- Added blank lines between import groups
211+
```
212+
213+
## Git Command Restrictions
214+
215+
- **Only use read-only git commands** such as `git status`, `git log`, `git diff`, `git show`, `git branch -l`, `git remote -v`
216+
- **Never run git commands that write or modify the git tree** such as `git commit`, `git push`, `git pull`, `git merge`, `git rebase`, `git checkout`, `git reset`, `git add`, `git rm`, `git stash`, `git cherry-pick`, `git revert`
217+
218+
## Agent Task Delegation Strategy
219+
220+
For complex multi-file tasks, use a **hybrid delegation pattern** to balance token efficiency with quality:
221+
222+
**Premium agents (e.g., Opus)** should handle:
223+
- Initial planning and task breakdown
224+
- Files requiring synthesis across multiple sources
225+
- Architecture decisions and cross-cutting concerns
226+
- Redux state management and complex hooks
227+
- Final review and integration of delegated work
228+
229+
**General agents** should handle (in parallel when independent):
230+
- Well-scoped, single-component implementations with clear specifications
231+
- Repetitive tasks with established patterns (e.g., similar form fields)
232+
- Test file creation from detailed templates or examples
233+
- Translation key additions
234+
235+
**Pattern for feature implementation:**
236+
1. Premium agent analyzes requirements and creates detailed component specs
237+
2. Delegate independent components to general agents in parallel
238+
3. Premium agent writes integration logic and Redux actions
239+
4. Premium agent reviews and integrates all pieces
240+
241+
This approach minimizes token usage on premium models while ensuring quality on tasks requiring judgment and synthesis.

__tests__/unit/pages/dashboard/PatientDrawer/StackedDaily/StackedDaily.test.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ jest.mock('@tidepool/viz', () => {
4949
},
5050
utils: {
5151
...originalModule.utils,
52-
getLocalizedCeiling: jest.fn(val => mockLocalizedCeiling),
52+
datetime: {
53+
...originalModule.utils.datetime,
54+
getLocalizedCeiling: jest.fn(val => mockLocalizedCeiling),
55+
},
5356
}
5457
};
5558
});

app/components/datasources/DataConnections.js

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import DataSourceDisconnectDialog from './DataSourceDisconnectDialog';
3232
import { Box, BoxProps } from 'theme-ui';
3333
import dexcomLogo from '../../core/icons/dexcom_logo.png';
3434
import libreLogo from '../../core/icons/libre_logo.svg';
35+
import ouraLogo from '../../core/icons/oura_logo.png';
3536
import twiistLogo from '../../core/icons/twiist_logo.svg';
3637
import { colors } from '../../themes/baseTheme';
3738

@@ -73,6 +74,24 @@ export const providers = {
7374
message: t('Disconnecting here has stopped new data collection from your FreeStyle Libre device. To fully revoke consent for sharing data with Tidepool, log into your FreeStyle Libre or LibreView app, access the "Connected Apps" page, and click "Manage" and then "Disconnect" next to Tidepool.'),
7475
},
7576
},
77+
oura: {
78+
id: 'oauth/oura',
79+
displayName: 'Oura',
80+
displayOrderIndex: 3,
81+
restrictedTokenCreate: {
82+
paths: [
83+
'/v1/oauth/oura',
84+
],
85+
},
86+
dataSourceFilter: {
87+
providerType: 'oauth',
88+
providerName: 'oura',
89+
},
90+
logoImage: ouraLogo,
91+
requiresLoggedInUser: true,
92+
requiresExistingDataSource: true,
93+
connectedMessage: t('Data donation only, not viewable on the platform'),
94+
},
7695
twiist: {
7796
id: 'oauth/twiist',
7897
displayName: 'twiist',
@@ -95,9 +114,10 @@ export const availableProviders = orderBy(keys(providers), provider => providers
95114

96115
export const getActiveProviders = (overrides = {}) => {
97116
const activeProviders = defaults(overrides, {
117+
abbott: true,
98118
dexcom: true,
119+
oura: true,
99120
twiist: true,
100-
abbott: true,
101121
});
102122

103123
return filter(availableProviders, provider => activeProviders[provider]);
@@ -221,9 +241,15 @@ export const getConnectStateUI = (patient, isLoggedInUser, providerName) => {
221241
let patientConnectedIcon;
222242
let patientConnectedText = t('Connected');
223243

244+
// Providers whose data is collected but never surfaced in the platform show a fixed
245+
// connected message in place of the data-time-based copy, but only once the first
246+
// import has landed — the generic awaiting message still shows before then.
247+
const providerConnectedMessage = providers[providerName]?.connectedMessage;
248+
const showProviderConnectedMessage = !!providerConnectedMessage && !!dataSource?.lastImportTime;
249+
224250
if (!dataSource?.lastImportTime && !providers[providerName]?.indeterminateDataImportTime) {
225-
patientConnectedMessage = t('This can take a few minutes');
226-
patientConnectedText = t('Connecting');
251+
patientConnectedMessage = t('Awaiting data, this can take a few minutes');
252+
patientConnectedText = t('Connected');
227253
} else if (!dataSource?.latestDataTime) {
228254
patientConnectedMessage = t('No data found as of {{timeAgo}}', { timeAgo });
229255
} else {
@@ -277,8 +303,8 @@ export const getConnectStateUI = (patient, isLoggedInUser, providerName) => {
277303
connected: {
278304
color: colors.text.primary,
279305
handler: isLoggedInUser ? 'disconnect' : null,
280-
message: isLoggedInUser && (providerName !== 'twiist') ? patientConnectedMessage : null, // Temporarily hide the message for twiist while we await a backend data source fix
281-
icon: isLoggedInUser ? patientConnectedIcon : CheckCircleRoundedIcon,
306+
message: isLoggedInUser && (providerName !== 'twiist') ? (showProviderConnectedMessage ? providerConnectedMessage : patientConnectedMessage) : null, // Temporarily hide the message for twiist while we await a backend data source fix
307+
icon: isLoggedInUser ? (showProviderConnectedMessage ? CheckCircleRoundedIcon : patientConnectedIcon) : CheckCircleRoundedIcon,
282308
text: isLoggedInUser ? patientConnectedText : t('Connected'),
283309
},
284310
disconnected: {
@@ -307,14 +333,25 @@ export const getConnectStateUI = (patient, isLoggedInUser, providerName) => {
307333
};
308334

309335
export const getDataConnectionProps = (patient, isLoggedInUser, selectedClinicId, setActiveHandler) => reduce(availableProviders, (result, providerName) => {
310-
result[providerName] = {};
311-
312-
let connectState;
313336

337+
const provider = providers[providerName];
314338
const dataSource = getCurrentDataSourceForProvider(patient, providerName);
315339
const connectStateUI = getConnectStateUI(patient, isLoggedInUser, providerName);
316340
const inviteExpired = dataSource?.expirationTime < moment.utc().toISOString();
317341

342+
// If the provider requires a logged in user to create the connection, then ensure that is the case.
343+
if (!!provider.requiresLoggedInUser && !isLoggedInUser) {
344+
return result;
345+
}
346+
347+
// If the provider requires an existing data source to create the connection, then ensure that is the case.
348+
// This mechanism can be used to limit access to certain providers to only users who have previously connected
349+
// or where Tidepool has created a data source on their behalf.
350+
if (!!provider.requiresExistingDataSource && !dataSource) {
351+
return result;
352+
}
353+
354+
let connectState;
318355
if (dataSource?.state) {
319356
connectState = includes(keys(connectStateUI), dataSource.state)
320357
? dataSource.state
@@ -342,7 +379,10 @@ export const getDataConnectionProps = (patient, isLoggedInUser, selectedClinicId
342379
buttonStyle,
343380
emailRequired,
344381
patientUpdates,
345-
} = getProviderHandlers(patient, selectedClinicId, providers[providerName])[handler] || {};
382+
} = getProviderHandlers(patient, selectedClinicId, provider)[handler] || {};
383+
384+
// Now that we have everything we need, add the provider
385+
result[providerName] = {};
346386

347387
if (action) {
348388
result[providerName].buttonDisabled = buttonDisabled;
@@ -360,7 +400,7 @@ export const getDataConnectionProps = (patient, isLoggedInUser, selectedClinicId
360400
result[providerName].stateColor = color;
361401
result[providerName].stateText = text;
362402
result[providerName].providerName = providerName;
363-
result[providerName].logoImage = providers[providerName]?.logoImage;
403+
result[providerName].logoImage = provider?.logoImage;
364404
result[providerName].logoImageLabel = `${providerName} logo`;
365405

366406
return result;
@@ -387,7 +427,7 @@ export const DataConnections = (props) => {
387427
const [patientUpdates, setPatientUpdates] = useState({});
388428
const [activeHandler, setActiveHandler] = useState(null);
389429
const dataConnectionProps = getDataConnectionProps(patient, isLoggedInUser, selectedClinicId, setActiveHandler);
390-
const activeProviders = getActiveProviders();
430+
const activeProviders = filter(getActiveProviders(), providerName => !!dataConnectionProps[providerName]); // Only those with connection props
391431

392432
const {
393433
sendingPatientDataProviderConnectRequest,

0 commit comments

Comments
 (0)