Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/biome.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '20.18.1' # version that's pinned in Dockerfile for kpi release
node-version: '20.19.0' # version that's pinned in Dockerfile for kpi release
check-latest: true # download newer semver match if available
cache: 'npm'

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/chromatic.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
ref: "main"
- uses: actions/setup-node@v6
with:
node-version: 20.18.1
node-version: 20.19.0
- name: Install dependencies
run: npm ci
- name: Run Chromatic
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/npm-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:

matrix:
node-version:
- '20.18.1' # version that's pinned in Dockerfile for kpi release
- '20.19.0' # version that's pinned in Dockerfile for kpi release
- '22' # version that's active (v22 until Nov, 2025), see https://nodejs.org/en/about/previous-releases
# - '24' # version that's current (v24 since May, 2025), see https://nodejs.org/en/about/previous-releases
fail-fast: false # Let each job finish
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ jobs:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 20.18.1 # version that's pinned in Dockerfile for kpi release. FYI v22 doesn't work out of box.
node-version: 20.19.0 # version that's pinned in Dockerfile for kpi release. FYI v22 doesn't work out of box.
check-latest: true # download newer semver match if available
cache: 'npm'

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/storybook-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
deploy:
runs-on: ubuntu-latest
container:
image: node:20
image: node:20.19
permissions:
contents: read
id-token: write
Expand Down
2 changes: 1 addition & 1 deletion .node-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v20.18.1
v20.19.0
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v20.18.1
v20.19.0
60 changes: 32 additions & 28 deletions .storybook/main.js β†’ .storybook/main.mjs
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
const path = require('path')
const webpack = require('webpack')
let SpeedMeasurePlugin
if (process.env.MEASURE) {
SpeedMeasurePlugin = require('speed-measure-webpack-plugin')
}
module.exports = {
import path, { dirname } from 'path'
import { fileURLToPath } from 'url'
import webpack from 'webpack'

const __dirname = dirname(fileURLToPath(import.meta.url))

export default {
stories: ['../jsapp/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-a11y',
// NB:
// 'storybook-addon-swc' may improve build speed in the future.
// - At time of writing, the build performance gains are negated because it
// switches to a slower refresh plugin and also causes other compatibility
// issues in Storybook 6.
// - Testing with React 16.14.0 and Storybook 7 (beta) seemed to perform
// well.
'storybook-dark-mode',
'@storybook/addon-webpack5-compiler-swc',
'@storybook/addon-webpack5-compiler-babel',
'storybook-addon-remix-react-router',
'@storybook/addon-docs',
],
Expand All @@ -32,9 +24,12 @@ module.exports = {
webpackFinal: async (config, { configType }) => {
config.plugins.push(new webpack.ProvidePlugin({ $: ['jquery', 'default'] }))

// Storybook has its own webpack config, so mirror app support for `*.svg?react` imports.
// Without this, such imports resolve to URLs and React tries to render them as invalid tag names.
// Exclude .module.css and .module.scss from Storybook's default CSS rules
config.module.rules.forEach((rule) => {
if (rule && rule.test instanceof RegExp && rule.test.test('.css')) {
rule.exclude = /\.module\.(css|scss)$/
}
// Mirror app support for `*.svg?react` imports.
if (rule && rule.test instanceof RegExp && rule.test.test('.svg')) {
rule.resourceQuery = {
not: [/react/],
Expand Down Expand Up @@ -103,13 +98,29 @@ module.exports = {
'sass-loader',
],
},
{
test: /\.module\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]--[hash:base64:5]',
},
sourceMap: true,
},
},
],
},
)

// Build speed improvements
applySpeedTweaks(config)

// Print speed measurement if env variable MEASURE is set
if (process.env.MEASURE) {
const { default: SpeedMeasurePlugin } = await import('speed-measure-webpack-plugin')
const smp = new SpeedMeasurePlugin()
return smp.wrap(config)
}
Expand All @@ -119,6 +130,7 @@ module.exports = {
// Build speed improvements
applySpeedTweaks(config)
if (process.env.MEASURE) {
const { default: SpeedMeasurePlugin } = await import('speed-measure-webpack-plugin')
const smp = new SpeedMeasurePlugin()
return smp.wrap(config)
}
Expand All @@ -137,14 +149,6 @@ function applySpeedTweaks(config) {
// 'import/no-unresolved': [2, { caseSensitive: true }]
config.plugins = config.plugins.filter((plugin) => plugin.constructor.name !== 'CaseSensitivePathsPlugin')

// Use swc to make the Terser step faster
if (config.mode === 'production') {
const TerserPlugin = require('terser-webpack-plugin')
config.optimization.minimizer = [
new TerserPlugin({
minify: TerserPlugin.swcMinify,
terserOptions: {},
}),
]
}
// Note: Removed custom TerserPlugin with swcMinify because it doesn't handle
// ESM code properly (import.meta, export statements). Let Storybook use its default minifier.
}
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ RUN apt-get -qq update && \
libproj-dev \
locales \
# pin an exact Node version for stability. update this regularly.
nodejs=$(apt-cache show nodejs | grep -F 'Version: 20.18.1' | cut -f 2 -d ' ') \
nodejs=$(apt-cache show nodejs | grep -F 'Version: 20.19.0' | cut -f 2 -d ' ') \
openjdk-17-jre \
postgresql-client \
procps \
Expand Down
2 changes: 1 addition & 1 deletion jsapp/js/account/DeleteAccountModal.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Button, Center, type ModalProps } from '@mantine/core'
import type { Meta, StoryObj } from '@storybook/react-webpack5'
import { useArgs } from 'storybook/internal/preview-api'
import { useArgs } from 'storybook/preview-api'
import DeleteAccountModal from './DeleteAccountModal'

const RenderModal = ({ ...args }: ModalProps) => {
Expand Down
2 changes: 1 addition & 1 deletion jsapp/js/api/orval.operationName.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { keyword } from 'esutils'
const { keyword } = require('esutils')

/**
* Copied some orvel internals and modified one line in order to drop the annoying `ApiV2` prefix to everything.
Expand Down
17 changes: 8 additions & 9 deletions jsapp/js/components/common/actionIcon.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,20 +96,19 @@ export const Preview = () => (
padding: '10px',
}}
>
{actionIconVariants.map((variant) =>
actionIconSizes.map((size) => {
{actionIconVariants.map((variant, index1) =>
actionIconSizes.map((size, index2) => {
const actionIconProps: ActionIconProps = {
variant,
size: size,
iconName: 'more',
}
return (
<>
<ActionIcon {...actionIconProps} />
<ActionIcon {...actionIconProps} loading />
<ActionIcon {...actionIconProps} disabled />
</>
)
// Returning as array rather than wrapped in React.Fragment for the key uniqueness to work
return [
<ActionIcon {...actionIconProps} key={`${index1}-${index2}-default`} />,
<ActionIcon {...actionIconProps} key={`${index1}-${index2}-loading`} loading />,
<ActionIcon {...actionIconProps} key={`${index1}-${index2}-disabled`} disabled />,
]
}),
)}
</div>
Expand Down
4 changes: 2 additions & 2 deletions jsapp/js/components/common/avatar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,8 @@ const bulkUsernames = [
export const BulkColorsTest: Story = {
render: () => (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px' }}>
{bulkUsernames.map((username) => (
<div key={username}>
{bulkUsernames.map((username, index) => (
<div key={index}>
<Avatar size='m' username={username} />
</div>
))}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { DecoratorFunction } from '@storybook/types'
import type { Decorator } from '@storybook/react'

const BULK_PROCESSING_BANNER_KEY_PREFIX = 'kpiBulkProcessingBanner-'

Expand All @@ -15,7 +15,7 @@ export function clearAllBulkProcessingBannerDismissals() {
* Storybook decorator that resets banner-dismissal session state whenever a
* story is rendered, so testers can always see the banner again on revisit.
*/
export const withBulkProcessingBannerSessionReset: DecoratorFunction = (Story) => {
export const withBulkProcessingBannerSessionReset: Decorator = (Story) => {
clearAllBulkProcessingBannerDismissals()
return Story()
}
12 changes: 7 additions & 5 deletions jsapp/js/components/submissions/DataTableWrapper.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
// or default/empty data. This file is intentionally minimal and focused to avoid these problems.

import { Box } from '@mantine/core'
import type { Decorator } from '@storybook/react'
import type { Meta, StoryObj } from '@storybook/react-webpack5'
import type { DecoratorFunction } from '@storybook/types'
import React, { useEffect } from 'react'
Comment thread
greptile-apps[bot] marked this conversation as resolved.
import { reactRouterParameters, withRouter } from 'storybook-addon-remix-react-router'
import { expect, waitFor } from 'storybook/test'
Expand All @@ -16,6 +16,7 @@ import { actions } from '#/actions'
import { ActionIdEnum } from '#/api/models/actionIdEnum'
import { BulkActionResponseStatusEnum } from '#/api/models/bulkActionResponseStatusEnum'
import { QuestionTypeName } from '#/constants'
import type { AssetResponse } from '#/dataInterface'
import assetFactory from '#/endpoints/asset.factory'
import assetMock from '#/endpoints/asset.mocks'
import assetDataFactory from '#/endpoints/assetData.factory'
Expand All @@ -42,15 +43,15 @@ import {
// Storybook preview root does not have a fixed height by default, which breaks flexbox stretching for table header
// cells. By adding a wrapper with a fixed height to the story, we ensure that `.rt-tr` and `.rt-th` flex children can
// stretch to fill the row height β€” just like in the real UI.
const fixedHeightDecorator: DecoratorFunction = (Story) => <Box h={480}>{Story()}</Box>
const fixedHeightDecorator: Decorator = (Story) => <Box h={480}>{Story()}</Box>

// Decorator to show the LimitNotifications banner in stories.
// The banner has a guard chain: it only shows if subscriptionStore.isInitialised is true.
// Problem: the store is a MobX singleton that normally gets filled via jQuery AJAX,
// but MSW (our Storybook mock layer) only intercepts fetch calls, not jQuery.
// Solution: manually populate the store on mount and clean up on unmount.
// Only add this decorator to stories that need to show limit banners.
const initializeSubscriptionStoreDecorator: DecoratorFunction = (Story) => {
const initializeSubscriptionStoreDecorator: Decorator = (Story) => {
useEffect(() => {
subscriptionStore.isInitialised = true
subscriptionStore.isPending = false
Expand Down Expand Up @@ -116,8 +117,9 @@ const initializeSubscriptionStoreDecorator: DecoratorFunction = (Story) => {
// Decorator to set the hash for the current asset UID, so that (deprecated) `getCurrentPath` works.
// This replaces the previous loader. It reads the UID from the story's args.asset.uid (if present).
// This ensures each story sets the correct hash for its asset, regardless of which asset is used.
const setAssetHashDecorator: DecoratorFunction = (Story, context) => {
const assetUid = context.args?.asset?.uid
const setAssetHashDecorator: Decorator = (Story, context) => {
const args = context.args as { asset?: AssetResponse }
const assetUid = args.asset?.uid
if (assetUid) {
window.location.hash = ROUTES.FORM_TABLE.replace(':uid', assetUid)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,11 @@ export default function FormLanguagesManager(props: FormLanguagesManagerProps) {
})
}, [queryClient, tableQueryKey, tableRows, pagination.start, pagination.limit])

// tableRows is intentionally excluded from the queryKey: updates are pushed
// synchronously via setQueryData above, so the queryFn only runs on
// initial mount to seed the cache.
// eslint-disable-next-line @tanstack/query/exhaustive-deps -- tableRows is intentionally excluded
const tableQuery = useQuery<PaginatedListResponse<TranslationRowItem>>({
// tableRows is intentionally excluded from the key: updates are pushed
// synchronously via setQueryData above, so the queryFn only runs on
// initial mount to seed the cache.
// eslint-disable-next-line @tanstack/query/exhaustive-deps
queryKey: tableQueryKey,
queryFn: async () => {
return {
Expand Down
4 changes: 2 additions & 2 deletions jsapp/js/query/queryClient.mocks.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { DecoratorFunction } from '@storybook/types'
import type { Decorator } from '@storybook/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { applyManageProjectsMutationDefaults } from '#/api/mutation-defaults/manage-projects-and-library-content'
import { applySurveyDataMutationDefaults } from '#/api/mutation-defaults/survey-data'
import { applyUserTeamOrganizationMutationDefaults } from '#/api/mutation-defaults/user-team-organization-usage'

export const queryClientDecorator: DecoratorFunction = (Story) => {
export const queryClientDecorator: Decorator = (Story) => {
// We define a new QueryClient for each story to avoid sharing state between stories.
const mockQueryClient = new QueryClient({
defaultOptions: {
Expand Down
2 changes: 1 addition & 1 deletion jsapp/js/sidebar/SidebarFormsListCategory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,14 @@ export default function SidebarFormsListCategory(props: SidebarFormsListCategory
limit: ITEMS_PER_PAGE,
})

// queryFilter and props.context are embedded in queryKey via Orval helper params and branching
const query = useInfiniteQuery<
SidebarFormsListCategoryResponse,
Error,
InfiniteData<SidebarFormsListCategoryResponse>,
readonly unknown[],
number
>({
// eslint-disable-next-line @tanstack/query/exhaustive-deps -- queryFilter and props.context are embedded in queryKey via Orval helper params and branching
queryKey,
initialPageParam: 0,
queryFn: async ({ pageParam = 0 }: { pageParam: number }) => {
Expand Down
3 changes: 2 additions & 1 deletion msw-mocks/mockServiceWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* - Please do NOT modify this file.
*/

const PACKAGE_VERSION = '2.11.3'
const PACKAGE_VERSION = '2.14.6'
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()
Expand Down Expand Up @@ -205,6 +205,7 @@ async function resolveMainClient(event) {
* @param {FetchEvent} event
* @param {Client | undefined} client
* @param {string} requestId
* @param {number} requestInterceptedAt
* @returns {Promise<Response>}
*/
async function getResponse(event, client, requestId, requestInterceptedAt) {
Expand Down
2 changes: 1 addition & 1 deletion orval.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { operationName } from './jsapp/js/api/orval.operationName'
const { operationName } = require('./jsapp/js/api/orval.operationName')

module.exports = {
'kpi-v2': {
Expand Down
Loading
Loading