diff options
6 files changed, 282 insertions, 96 deletions
diff --git a/server/sonar-web/src/main/js/app/components/indexation/IndexationNotification.tsx b/server/sonar-web/src/main/js/app/components/indexation/IndexationNotification.tsx index 84444f6878b..00e1fb09675 100644 --- a/server/sonar-web/src/main/js/app/components/indexation/IndexationNotification.tsx +++ b/server/sonar-web/src/main/js/app/components/indexation/IndexationNotification.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { isBefore } from 'date-fns'; import * as React from 'react'; import withIndexationContext, { WithIndexationContextProps, @@ -26,6 +27,7 @@ import { hasGlobalPermission } from '../../../helpers/users'; import { IndexationNotificationType } from '../../../types/indexation'; import { Permissions } from '../../../types/permissions'; import { CurrentUser, isLoggedIn } from '../../../types/users'; +import withAppStateContext, { WithAppStateContextProps } from '../app-state/withAppStateContext'; import withCurrentUserContext from '../current-user/withCurrentUserContext'; import IndexationNotificationHelper from './IndexationNotificationHelper'; import IndexationNotificationRenderer from './IndexationNotificationRenderer'; @@ -36,15 +38,24 @@ interface Props extends WithIndexationContextProps { interface State { notificationType?: IndexationNotificationType; + shouldDisplaySurveyLink: boolean; } -const COMPLETED_NOTIFICATION_DISPLAY_DURATION = 5000; +type IndexationNotificationProps = Props & WithIndexationContextProps & WithAppStateContextProps; + +const SPRIG_SURVEY_LIMIT_DATE = new Date('2025-07-01T00:00:00+01:00'); + +export class IndexationNotification extends React.PureComponent< + IndexationNotificationProps, + State +> { + state: State = { + shouldDisplaySurveyLink: false, + }; -export class IndexationNotification extends React.PureComponent<Props, State> { - state: State = {}; isSystemAdmin = false; - constructor(props: Props) { + constructor(props: IndexationNotificationProps) { super(props); this.isSystemAdmin = @@ -62,9 +73,20 @@ export class IndexationNotification extends React.PureComponent<Props, State> { } } + dismissBanner = () => { + this.setState({ notificationType: undefined }); + }; + refreshNotification() { const { isCompleted, hasFailures } = this.props.indexationContext.status; + const currentSqsVersion = this.props.appState.version; + this.setState({ + shouldDisplaySurveyLink: + isBefore(new Date(), SPRIG_SURVEY_LIMIT_DATE) && + IndexationNotificationHelper.getLastIndexationSQSVersion() !== currentSqsVersion, + }); + if (!isCompleted) { IndexationNotificationHelper.markCompletedNotificationAsToDisplay(); @@ -73,26 +95,31 @@ export class IndexationNotification extends React.PureComponent<Props, State> { ? IndexationNotificationType.InProgressWithFailure : IndexationNotificationType.InProgress, }); - } else if (hasFailures) { + + return; + } + + IndexationNotificationHelper.saveLastIndexationSQSVersion(this.props.appState.version); + + if (hasFailures) { this.setState({ notificationType: IndexationNotificationType.CompletedWithFailure }); - } else if (IndexationNotificationHelper.shouldDisplayCompletedNotification()) { + return; + } + + if (IndexationNotificationHelper.shouldDisplayCompletedNotification()) { this.setState({ notificationType: IndexationNotificationType.Completed, }); IndexationNotificationHelper.markCompletedNotificationAsDisplayed(); - - // Hide after some time - setTimeout(() => { - this.refreshNotification(); - }, COMPLETED_NOTIFICATION_DISPLAY_DURATION); - } else { - this.setState({ notificationType: undefined }); + return; } + + this.setState({ notificationType: undefined }); } render() { - const { notificationType } = this.state; + const { notificationType, shouldDisplaySurveyLink } = this.state; const { indexationContext: { @@ -103,6 +130,8 @@ export class IndexationNotification extends React.PureComponent<Props, State> { return !this.isSystemAdmin ? null : ( <IndexationNotificationRenderer completedCount={completedCount} + onDismissBanner={this.dismissBanner} + shouldDisplaySurveyLink={shouldDisplaySurveyLink} total={total} type={notificationType} /> @@ -110,4 +139,6 @@ export class IndexationNotification extends React.PureComponent<Props, State> { } } -export default withCurrentUserContext(withIndexationContext(IndexationNotification)); +export default withCurrentUserContext( + withIndexationContext(withAppStateContext(IndexationNotification)), +); diff --git a/server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationHelper.ts b/server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationHelper.ts index 41211329fd7..ffe785edb03 100644 --- a/server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationHelper.ts +++ b/server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationHelper.ts @@ -25,6 +25,7 @@ import { IndexationStatus } from '../../../types/indexation'; const POLLING_INTERVAL_MS = 5000; const LS_INDEXATION_COMPLETED_NOTIFICATION_SHOULD_BE_DISPLAYED = 'display_indexation_completed_notification'; +const LS_LAST_INDEXATION_SQS_VERSION = 'last_indexation_sqs_version'; export default class IndexationNotificationHelper { private static interval?: NodeJS.Timeout; @@ -74,4 +75,12 @@ export default class IndexationNotificationHelper { get(LS_INDEXATION_COMPLETED_NOTIFICATION_SHOULD_BE_DISPLAYED) ?? false.toString(), ); } + + static saveLastIndexationSQSVersion(version: string) { + save(LS_LAST_INDEXATION_SQS_VERSION, version); + } + + static getLastIndexationSQSVersion() { + return get(LS_LAST_INDEXATION_SQS_VERSION); + } } diff --git a/server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationRenderer.tsx b/server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationRenderer.tsx index be82875356e..2bddeadc942 100644 --- a/server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationRenderer.tsx +++ b/server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationRenderer.tsx @@ -20,26 +20,28 @@ /* eslint-disable react/no-unused-prop-types */ import styled from '@emotion/styled'; -import { FormattedMessage } from 'react-intl'; import { - FlagErrorIcon, - FlagSuccessIcon, - FlagWarningIcon, + IconCheckCircle, + IconError, + IconWarning, + IconX, Link, + LinkHighlight, Spinner, - ThemeColors, - themeBorder, - themeColor, -} from '~design-system'; +} from '@sonarsource/echoes-react'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { InteractiveIconBase, ThemeColors, themeBorder, themeColor } from '~design-system'; import { queryToSearchString } from '~sonar-aligned/helpers/urls'; import DocumentationLink from '../../../components/common/DocumentationLink'; import { DocLink } from '../../../helpers/doc-links'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { translate } from '../../../helpers/l10n'; import { IndexationNotificationType } from '../../../types/indexation'; import { TaskStatuses, TaskTypes } from '../../../types/tasks'; -export interface IndexationNotificationRendererProps { +interface IndexationNotificationRendererProps { completedCount?: number; + onDismissBanner: () => void; + shouldDisplaySurveyLink: boolean; total?: number; type?: IndexationNotificationType; } @@ -65,8 +67,13 @@ const NOTIFICATION_COLORS: { }, }; -export default function IndexationNotificationRenderer(props: IndexationNotificationRendererProps) { - const { completedCount, total, type } = props; +const SPRIG_SURVEY_LINK = + 'https://a.sprig.com/U1h4UFpySUNwN2ZtfnNpZDowNWUyNmRkZC01MmUyLTQ4OGItOTA3ZC05M2VjYjQxZTYzN2Y='; + +export default function IndexationNotificationRenderer( + props: Readonly<IndexationNotificationRendererProps>, +) { + const { type } = props; return ( <div className={type === undefined ? 'sw-hidden' : ''}> @@ -76,62 +83,126 @@ export default function IndexationNotificationRenderer(props: IndexationNotifica aria-live="assertive" role="alert" > - {type !== undefined && ( - <> - {renderIcon(type)} - {type === IndexationNotificationType.Completed && renderCompletedBanner()} - - {type === IndexationNotificationType.CompletedWithFailure && - renderCompletedWithFailureBanner()} - - {type === IndexationNotificationType.InProgress && - renderInProgressBanner(completedCount as number, total as number)} - - {type === IndexationNotificationType.InProgressWithFailure && - renderInProgressWithFailureBanner(completedCount as number, total as number)} - </> - )} + <IndexationStatusIcon type={type} /> + <IndexationBanner {...props} /> </StyledBanner> </div> ); } -function renderIcon(type: IndexationNotificationType) { +function IndexationStatusIcon(props: Readonly<{ type?: IndexationNotificationType }>) { + const { type } = props; + switch (type) { case IndexationNotificationType.Completed: - return <FlagSuccessIcon />; + return <IconCheckCircle color="echoes-color-icon-success" />; case IndexationNotificationType.CompletedWithFailure: case IndexationNotificationType.InProgressWithFailure: - return <FlagErrorIcon />; + return <IconError color="echoes-color-icon-danger" />; case IndexationNotificationType.InProgress: - return <FlagWarningIcon />; + return <IconWarning color="echoes-color-icon-warning" />; default: return null; } } -function renderCompletedBanner() { - return <span>{translate('indexation.completed')}</span>; +function IndexationBanner(props: Readonly<IndexationNotificationRendererProps>) { + const { completedCount, onDismissBanner, shouldDisplaySurveyLink, total, type } = props; + + switch (type) { + case IndexationNotificationType.Completed: + return ( + <CompletedBanner + onDismissBanner={onDismissBanner} + shouldDisplaySurveyLink={shouldDisplaySurveyLink} + /> + ); + case IndexationNotificationType.CompletedWithFailure: + return <CompletedWithFailureBanner shouldDisplaySurveyLink={shouldDisplaySurveyLink} />; + case IndexationNotificationType.InProgress: + return <InProgressBanner completedCount={completedCount as number} total={total as number} />; + case IndexationNotificationType.InProgressWithFailure: + return ( + <InProgressWithFailureBanner + completedCount={completedCount as number} + total={total as number} + /> + ); + default: + return null; + } +} + +function SurveyLink() { + return ( + <span className="sw-ml-2"> + <FormattedMessage + id="indexation.upgrade_survey_link" + values={{ + link: (text) => ( + <Link highlight={LinkHighlight.Default} shouldOpenInNewTab to={SPRIG_SURVEY_LINK}> + {text} + </Link> + ), + }} + /> + </span> + ); +} + +function CompletedBanner( + props: Readonly<{ onDismissBanner: () => void; shouldDisplaySurveyLink: boolean }>, +) { + const { onDismissBanner, shouldDisplaySurveyLink } = props; + + const intl = useIntl(); + + return ( + <div className="sw-flex sw-flex-1 sw-items-center"> + <FormattedMessage id="indexation.completed" /> + {shouldDisplaySurveyLink && <SurveyLink />} + <div className="sw-flex sw-flex-1 sw-justify-end"> + <BannerDismissIcon + className="sw-ml-2 sw-px-1/2" + Icon={IconX} + aria-label={intl.formatMessage({ id: 'dismiss' })} + onClick={onDismissBanner} + size="small" + /> + </div> + </div> + ); } -function renderCompletedWithFailureBanner() { +function CompletedWithFailureBanner(props: Readonly<{ shouldDisplaySurveyLink: boolean }>) { + const { shouldDisplaySurveyLink } = props; + + const { formatMessage } = useIntl(); + return ( <span> <FormattedMessage defaultMessage={translate('indexation.completed_with_error')} id="indexation.completed_with_error" values={{ - link: renderBackgroundTasksPageLink( - true, - translate('indexation.completed_with_error.link'), + link: ( + <BackgroundTasksPageLink + hasError + text={formatMessage({ id: 'indexation.completed_with_error.link' })} + /> ), }} /> + {shouldDisplaySurveyLink && <SurveyLink />} </span> ); } -function renderInProgressBanner(completedCount: number, total: number) { +function InProgressBanner(props: Readonly<{ completedCount: number; total: number }>) { + const { completedCount, total } = props; + + const { formatMessage } = useIntl(); + return ( <> <span> @@ -139,25 +210,32 @@ function renderInProgressBanner(completedCount: number, total: number) { <FormattedMessage id="indexation.features_partly_available" values={{ - link: renderIndexationDocPageLink(), + link: <IndexationDocPageLink />, }} /> </span> <span className="sw-flex sw-items-center"> <Spinner className="sw-mr-1 -sw-mb-1/2" /> - {translateWithParameters( - 'indexation.progression', - completedCount.toString(), - total.toString(), - )} + <FormattedMessage + id="indexation.progression" + values={{ + count: completedCount, + total, + }} + /> </span> <span> <FormattedMessage id="indexation.admin_link" values={{ - link: renderBackgroundTasksPageLink(false, translate('background_tasks.page')), + link: ( + <BackgroundTasksPageLink + hasError={false} + text={formatMessage({ id: 'background_tasks.page' })} + /> + ), }} /> </span> @@ -165,7 +243,11 @@ function renderInProgressBanner(completedCount: number, total: number) { ); } -function renderInProgressWithFailureBanner(completedCount: number, total: number) { +function InProgressWithFailureBanner(props: Readonly<{ completedCount: number; total: number }>) { + const { completedCount, total } = props; + + const { formatMessage } = useIntl(); + return ( <> <span> @@ -173,7 +255,7 @@ function renderInProgressWithFailureBanner(completedCount: number, total: number <FormattedMessage id="indexation.features_partly_available" values={{ - link: renderIndexationDocPageLink(), + link: <IndexationDocPageLink />, }} /> </span> @@ -186,9 +268,11 @@ function renderInProgressWithFailureBanner(completedCount: number, total: number values={{ count: completedCount, total, - link: renderBackgroundTasksPageLink( - true, - translate('indexation.progression_with_error.link'), + link: ( + <BackgroundTasksPageLink + hasError + text={formatMessage({ id: 'indexation.progression_with_error.link' })} + /> ), }} /> @@ -197,7 +281,9 @@ function renderInProgressWithFailureBanner(completedCount: number, total: number ); } -function renderBackgroundTasksPageLink(hasError: boolean, text: string) { +function BackgroundTasksPageLink(props: Readonly<{ hasError: boolean; text: string }>) { + const { hasError, text } = props; + return ( <Link to={{ @@ -213,7 +299,7 @@ function renderBackgroundTasksPageLink(hasError: boolean, text: string) { ); } -function renderIndexationDocPageLink() { +function IndexationDocPageLink() { return ( <DocumentationLink className="sw-whitespace-nowrap" to={DocLink.InstanceAdminReindexation}> <FormattedMessage id="indexation.features_partly_available.link" /> @@ -230,3 +316,12 @@ const StyledBanner = styled.div<{ type: IndexationNotificationType }>` background-color: ${({ type }) => themeColor(NOTIFICATION_COLORS[type].background)}; border-bottom: ${({ type }) => themeBorder('default', NOTIFICATION_COLORS[type].border)}; `; + +// There's currently no banner in Echoes so let's use the legacy design-system components for now +const BannerDismissIcon = styled(InteractiveIconBase)` + --background: ${themeColor('successBackground')}; + --backgroundHover: ${themeColor('successText', 0.1)}; + --color: ${themeColor('successText')}; + --colorHover: ${themeColor('successText')}; + --focus: ${themeColor('bannerIconFocus', 0.2)}; +`; diff --git a/server/sonar-web/src/main/js/app/components/indexation/__tests__/IndexationNotification-test.tsx b/server/sonar-web/src/main/js/app/components/indexation/__tests__/IndexationNotification-test.tsx index 07cb968d520..ab005ef0d4a 100644 --- a/server/sonar-web/src/main/js/app/components/indexation/__tests__/IndexationNotification-test.tsx +++ b/server/sonar-web/src/main/js/app/components/indexation/__tests__/IndexationNotification-test.tsx @@ -18,9 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { act } from '@testing-library/react'; -import { byText } from '~sonar-aligned/helpers/testSelector'; -import { mockCurrentUser, mockLoggedInUser } from '../../../../helpers/testMocks'; +import { byRole, byText } from '~sonar-aligned/helpers/testSelector'; +import { mockAppState, mockCurrentUser, mockLoggedInUser } from '../../../../helpers/testMocks'; import { renderComponent } from '../../../../helpers/testReactTestingUtils'; import { Permissions } from '../../../../types/permissions'; import { IndexationNotification } from '../IndexationNotification'; @@ -40,6 +39,7 @@ describe('Completed banner', () => { rerender( <IndexationNotification + appState={mockAppState()} currentUser={mockCurrentUser()} indexationContext={{ status: { completedCount: 23, hasFailures: false, isCompleted: true, total: 42 }, @@ -64,77 +64,118 @@ describe('Completed banner', () => { expect(IndexationNotificationHelper.shouldDisplayCompletedNotification).toHaveBeenCalled(); }); - it('should be hidden once completed without failure', () => { - jest.useFakeTimers(); - - jest - .mocked(IndexationNotificationHelper.shouldDisplayCompletedNotification) - .mockReturnValueOnce(true); - - renderIndexationNotification({ + it('should start progress > progress with failure > complete with failure', () => { + const { rerender } = renderIndexationNotification({ indexationContext: { - status: { hasFailures: false, isCompleted: true }, + status: { completedCount: 23, hasFailures: false, isCompleted: false, total: 42 }, }, }); - expect(IndexationNotificationHelper.markCompletedNotificationAsDisplayed).toHaveBeenCalled(); + expect(byText('indexation.progression2342').get()).toBeInTheDocument(); - act(() => jest.runOnlyPendingTimers()); + rerender( + <IndexationNotification + appState={mockAppState()} + currentUser={mockLoggedInUser({ permissions: { global: [Permissions.Admin] } })} + indexationContext={{ + status: { completedCount: 23, hasFailures: true, isCompleted: false, total: 42 }, + }} + />, + ); - expect(IndexationNotificationHelper.markCompletedNotificationAsDisplayed).toHaveBeenCalled(); + expect(byText(/^indexation\.progression_with_error\.link/).get()).toBeInTheDocument(); - jest.useRealTimers(); + rerender( + <IndexationNotification + appState={mockAppState()} + currentUser={mockLoggedInUser({ permissions: { global: [Permissions.Admin] } })} + indexationContext={{ + status: { completedCount: 23, hasFailures: true, isCompleted: true, total: 42 }, + }} + />, + ); + expect(byText('indexation.completed_with_error').get()).toBeInTheDocument(); }); - it('should start progress > progress with failure > complete with failure', () => { + it('should start progress > success > disappear', () => { const { rerender } = renderIndexationNotification({ indexationContext: { status: { completedCount: 23, hasFailures: false, isCompleted: false, total: 42 }, }, }); - expect(byText('indexation.progression.23.42').get()).toBeInTheDocument(); + expect(byText('indexation.progression2342').get()).toBeInTheDocument(); rerender( <IndexationNotification + appState={mockAppState()} currentUser={mockLoggedInUser({ permissions: { global: [Permissions.Admin] } })} indexationContext={{ - status: { completedCount: 23, hasFailures: true, isCompleted: false, total: 42 }, + status: { completedCount: 23, hasFailures: false, isCompleted: true, total: 42 }, }} />, ); + expect(IndexationNotificationHelper.shouldDisplayCompletedNotification).toHaveBeenCalled(); + }); - expect(byText(/^indexation\.progression_with_error\.link/).get()).toBeInTheDocument(); + it('should show survey link when indexation follows an upgrade', () => { + jest + .mocked(IndexationNotificationHelper.shouldDisplayCompletedNotification) + .mockReturnValueOnce(true); + jest + .mocked(IndexationNotificationHelper.getLastIndexationSQSVersion) + .mockReturnValueOnce('11.0'); + + const { rerender } = renderIndexationNotification({ + appState: mockAppState({ version: '12.0' }), + indexationContext: { + status: { completedCount: 42, hasFailures: false, isCompleted: true, total: 42 }, + }, + }); + + expect(byText('indexation.upgrade_survey_link').get()).toBeInTheDocument(); rerender( <IndexationNotification + appState={mockAppState({ version: '12.0' })} currentUser={mockLoggedInUser({ permissions: { global: [Permissions.Admin] } })} indexationContext={{ status: { completedCount: 23, hasFailures: true, isCompleted: true, total: 42 }, }} />, ); - expect(byText('indexation.completed_with_error').get()).toBeInTheDocument(); + + expect(byText('indexation.upgrade_survey_link').get()).toBeInTheDocument(); }); - it('should start progress > success > disappear', () => { + it('should not show survey link when indexation does not follow an upgrade', () => { + jest + .mocked(IndexationNotificationHelper.shouldDisplayCompletedNotification) + .mockReturnValueOnce(true); + jest + .mocked(IndexationNotificationHelper.getLastIndexationSQSVersion) + .mockReturnValueOnce('12.0'); + const { rerender } = renderIndexationNotification({ + appState: mockAppState({ version: '12.0' }), indexationContext: { - status: { completedCount: 23, hasFailures: false, isCompleted: false, total: 42 }, + status: { completedCount: 42, hasFailures: false, isCompleted: true, total: 42 }, }, }); - expect(byText('indexation.progression.23.42').get()).toBeInTheDocument(); + expect(byRole('indexation.upgrade_survey_link').query()).not.toBeInTheDocument(); rerender( <IndexationNotification + appState={mockAppState({ version: '12.0' })} currentUser={mockLoggedInUser({ permissions: { global: [Permissions.Admin] } })} indexationContext={{ - status: { completedCount: 23, hasFailures: false, isCompleted: true, total: 42 }, + status: { completedCount: 23, hasFailures: true, isCompleted: true, total: 42 }, }} />, ); - expect(IndexationNotificationHelper.shouldDisplayCompletedNotification).toHaveBeenCalled(); + + expect(byRole('indexation.upgrade_survey_link').query()).not.toBeInTheDocument(); }); it('should not see notification if not admin', () => { @@ -145,13 +186,14 @@ describe('Completed banner', () => { currentUser: mockLoggedInUser(), }); - expect(byText('indexation.progression.23.42').query()).not.toBeInTheDocument(); + expect(byText('indexation.progression2342').query()).not.toBeInTheDocument(); }); }); function renderIndexationNotification(props?: Partial<IndexationNotification['props']>) { return renderComponent( <IndexationNotification + appState={mockAppState()} currentUser={mockLoggedInUser({ permissions: { global: [Permissions.Admin] } })} indexationContext={{ status: { completedCount: 23, hasFailures: false, isCompleted: false, total: 42 }, diff --git a/server/sonar-web/src/main/js/app/components/indexation/__tests__/IndexationNotificationRenderer-test.tsx b/server/sonar-web/src/main/js/app/components/indexation/__tests__/IndexationNotificationRenderer-test.tsx index b9c958ec7ab..e57085f14b2 100644 --- a/server/sonar-web/src/main/js/app/components/indexation/__tests__/IndexationNotificationRenderer-test.tsx +++ b/server/sonar-web/src/main/js/app/components/indexation/__tests__/IndexationNotificationRenderer-test.tsx @@ -59,5 +59,13 @@ describe('Indexation notification renderer', () => { }); function renderIndexationNotificationRenderer(status: IndexationNotificationType) { - renderComponent(<IndexationNotificationRenderer completedCount={23} total={42} type={status} />); + renderComponent( + <IndexationNotificationRenderer + completedCount={23} + onDismissBanner={() => undefined} + shouldDisplaySurveyLink={false} + total={42} + type={status} + />, + ); } diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index ba23172b611..21d5616bea8 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -5789,7 +5789,7 @@ indexation.in_progress=Reindexing in progress. indexation.details_unavailable=Details are unavailable until this process is complete. indexation.features_partly_available=Most features are available. Some details only show upon completion. {link} indexation.features_partly_available.link=More info -indexation.progression={0} out of {1} projects reindexed. +indexation.progression={count} out of {total} projects reindexed. indexation.progression_with_error={count} out of {total} projects reindexed with some {link}. indexation.progression_with_error.link=tasks failing indexation.completed=All project data has been reloaded. @@ -5805,6 +5805,7 @@ indexation.filter_unavailable.description=This filter is unavailable until this indexation.learn_more=Learn more: indexation.reindexing=Reindexing indexation.filters_unavailable=Some filters are unavailable until this process is complete. {link} +indexation.upgrade_survey_link=Help us improve the upgrade experience. <link>Click here to share your thoughts.</link> #------------------------------------------------------------------------------ |