Skip to content

Commit

Permalink
Merge branch 'main' into fix-display-type-in-override
Browse files Browse the repository at this point in the history
  • Loading branch information
lassopicasso committed Feb 7, 2025
2 parents 10802e0 + ec108d4 commit 2c7c415
Show file tree
Hide file tree
Showing 50 changed files with 1,150 additions and 497 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from 'react';
import { screen, waitFor } from '@testing-library/react';
import { textMock } from '@studio/testing/mocks/i18nMock';
import { useWebSocket } from 'app-development/hooks/useWebSocket';
import { SyncEventsWebSocketHub } from 'app-shared/api/paths';
import { syncEntityUpdateWebSocketHub, syncEventsWebSocketHub } from 'app-shared/api/paths';
import { WSConnector } from 'app-shared/websockets/WSConnector';
import type { SyncError, SyncSuccess } from 'app-shared/types/api/SyncResponses';
import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';
Expand All @@ -12,6 +12,8 @@ import { SyncSuccessQueriesInvalidator } from 'app-shared/queryInvalidator/SyncS
import { WebSocketSyncWrapper } from './WebSocketSyncWrapper';
import { renderWithProviders } from '../../test/testUtils';
import { APP_DEVELOPMENT_BASENAME } from 'app-shared/constants';
import type { EntityUpdated } from 'app-shared/types/api/EntityUpdated';
import { EntityUpdatedQueriesInvalidator } from 'app-shared/queryInvalidator/EntityUpdatedQueriesInvalidator';

jest.mock('app-development/hooks/useWebSocket', () => ({
useWebSocket: jest.fn(),
Expand All @@ -27,8 +29,8 @@ describe('WebSocketSyncWrapper', () => {
renderWebSocketSyncWrapper();

expect(useWebSocket).toHaveBeenCalledWith({
clientsName: ['FileSyncSuccess', 'FileSyncError'],
webSocketUrl: SyncEventsWebSocketHub(),
clientsName: ['FileSyncSuccess', 'FileSyncError', 'EntityUpdated'],
webSocketUrls: [syncEntityUpdateWebSocketHub(), syncEventsWebSocketHub()],
webSocketConnector: WSConnector,
});
});
Expand Down Expand Up @@ -85,6 +87,32 @@ describe('WebSocketSyncWrapper', () => {
);
});
});

it('should invalidate entity queries by resourceName when a message with resourceName is received', async () => {
const entityUpdateMock: EntityUpdated = {
resourceName: 'entityResourceName',
};
const queryClientMock = createQueryClientMock();
const invalidator = EntityUpdatedQueriesInvalidator.getInstance(queryClientMock, org, app);
invalidator.invalidateQueriesByResourceName = jest.fn();

const mockOnWSMessageReceived = jest
.fn()
.mockImplementation((callback: Function) => callback(entityUpdateMock));

(useWebSocket as jest.Mock).mockReturnValue({
...jest.requireActual('app-development/hooks/useWebSocket'),
onWSMessageReceived: mockOnWSMessageReceived,
});

renderWebSocketSyncWrapper();

await waitFor(() => {
expect(invalidator.invalidateQueriesByResourceName).toHaveBeenCalledWith(
entityUpdateMock.resourceName,
);
});
});
});

const mockChildren: ReactNode = <div></div>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,20 @@ import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmen
import { useTranslation } from 'react-i18next';
import type { SyncError, SyncSuccess } from 'app-shared/types/api/SyncResponses';
import { SyncUtils } from 'app-shared/utils/SyncUtils.ts';
import { SyncEventsWebSocketHub } from 'app-shared/api/paths';
import { syncEntityUpdateWebSocketHub, syncEventsWebSocketHub } from 'app-shared/api/paths';
import { useLayoutContext } from '../../contexts/LayoutContext';
import type { EntityUpdated } from 'app-shared/types/api/EntityUpdated';
import { EntityUpdatedQueriesInvalidator } from 'app-shared/queryInvalidator/EntityUpdatedQueriesInvalidator';

enum SyncClientsName {
FileSyncSuccess = 'FileSyncSuccess',
FileSyncError = 'FileSyncError',
}

enum SyncEntityClientName {
EntityUpdated = 'EntityUpdated',
}

type WebSocketSyncWrapperProps = {
children: React.ReactNode;
};
Expand All @@ -28,18 +34,32 @@ export const WebSocketSyncWrapper = ({
const queryClient = useQueryClient();
const { selectedLayoutSetName } = useLayoutContext();
const invalidator = SyncSuccessQueriesInvalidator.getInstance(queryClient, org, app);
const entityUpdateInvalidator = EntityUpdatedQueriesInvalidator.getInstance(
queryClient,
org,
app,
);

useEffect(() => {
invalidator.layoutSetName = selectedLayoutSetName;
}, [invalidator, selectedLayoutSetName]);

const { onWSMessageReceived } = useWebSocket({
webSocketUrl: SyncEventsWebSocketHub(),
clientsName: [SyncClientsName.FileSyncSuccess, SyncClientsName.FileSyncError],
webSocketUrls: [syncEntityUpdateWebSocketHub(), syncEventsWebSocketHub()],
clientsName: [
SyncClientsName.FileSyncSuccess,
SyncClientsName.FileSyncError,
SyncEntityClientName.EntityUpdated,
],
webSocketConnector: WSConnector,
});

onWSMessageReceived<SyncError | SyncSuccess>((message): ReactElement => {
onWSMessageReceived<SyncError | SyncSuccess | EntityUpdated>((message): ReactElement => {
if ('resourceName' in message) {
entityUpdateInvalidator.invalidateQueriesByResourceName(message.resourceName as string);
return;
}

const isErrorMessage = 'errorCode' in message;
if (isErrorMessage) {
toast.error(t(SyncUtils.getSyncErrorMessage(message)), { toastId: message.errorCode });
Expand All @@ -52,6 +72,5 @@ export const WebSocketSyncWrapper = ({
invalidator.invalidateQueriesByFileLocation(message.source.name);
}
});

return <>{children}</>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { type ImageOption } from '../ImageOption';

const defaultProps: DeployProps = {
appDeployedVersion: 'test',
lastBuildId: '',
isDeploymentInProgress: false,
envName: 'tt02',
isProduction: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { StudioError, StudioSpinner } from '@studio/components';

export interface DeployProps {
appDeployedVersion: string;
lastBuildId: string;
isDeploymentInProgress: boolean;
envName: string;
isProduction: boolean;
Expand All @@ -19,7 +18,6 @@ export interface DeployProps {

export const Deploy = ({
appDeployedVersion,
lastBuildId,
isDeploymentInProgress,
envName,
isProduction,
Expand All @@ -34,7 +32,7 @@ export const Deploy = ({
isPending: permissionsIsPending,
isError: permissionsIsError,
} = useDeployPermissionsQuery(org, app, { hideDefaultError: true });
const { data, mutate, isPending } = useCreateDeploymentMutation(org, app, {
const { mutate, isPending: isPendingCreateDeployment } = useCreateDeploymentMutation(org, app, {
hideDefaultError: true,
});

Expand Down Expand Up @@ -85,14 +83,13 @@ export const Deploy = ({
},
);

const deployIsPending = isPending || (!!data?.build?.id && data?.build?.id !== lastBuildId);
const deployInProgress = deployIsPending || isDeploymentInProgress;
const deployInProgress: boolean = isPendingCreateDeployment || isDeploymentInProgress;

return (
<DeployDropdown
appDeployedVersion={appDeployedVersion}
disabled={deployInProgress}
isPending={deployIsPending}
isPending={deployInProgress}
selectedImageTag={selectedImageTag}
setSelectedImageTag={setSelectedImageTag}
startDeploy={startDeploy}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ export const DeploymentEnvironment = ({
<div className={classes.content}>
<Deploy
appDeployedVersion={kubernetesDeployment?.version}
lastBuildId={lastPipelineDeployment?.build?.id}
isDeploymentInProgress={isDeploymentInProgress}
envName={envName}
isProduction={isProduction}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { UseQueryResult, QueryMeta } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import { useServicesContext } from 'app-shared/contexts/ServicesContext';
import { QueryKey } from 'app-shared/types/QueryKey';
import { DEPLOYMENTS_REFETCH_INTERVAL } from 'app-shared/constants';
import type { DeploymentsResponse } from 'app-shared/types/api/DeploymentsResponse';

export const useAppDeploymentsQuery = (
Expand All @@ -14,7 +13,6 @@ export const useAppDeploymentsQuery = (
return useQuery<DeploymentsResponse>({
queryKey: [QueryKey.AppDeployments, owner, app],
queryFn: () => getDeployments(owner, app),
refetchInterval: DEPLOYMENTS_REFETCH_INTERVAL,
meta,
});
};
14 changes: 7 additions & 7 deletions frontend/app-development/hooks/useWebSocket/useWebSocket.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,29 @@ describe('useWebSocket', () => {
it('should create web socket connection with provided webSocketUrl', () => {
renderHook(() =>
useWebSocket({
webSocketUrl: 'ws://jest-test-mocked-url.com',
webSocketUrls: ['ws://jest-test-mocked-url.com'],
clientsName: clientsNameMock,
webSocketConnector: WSConnector,
}),
);
expect(WSConnector.getInstance).toHaveBeenCalledWith('ws://jest-test-mocked-url.com', [
'MessageClientOne',
'MessageClientTwo',
]);
expect(WSConnector.getInstance).toHaveBeenCalledWith(
['ws://jest-test-mocked-url.com'],
['MessageClientOne', 'MessageClientTwo'],
);
});

it('should provide a function to listen to messages', () => {
const { result } = renderHook(() =>
useWebSocket({
webSocketUrl: 'ws://jest-test-mocked-url.com',
webSocketUrls: ['ws://jest-test-mocked-url.com'],
clientsName: clientsNameMock,
webSocketConnector: WSConnector,
}),
);
const callback = jest.fn();
result.current.onWSMessageReceived(callback);
expect(
WSConnector.getInstance('ws://jest-test-mocked-url.com', clientsNameMock).onMessageReceived,
WSConnector.getInstance(['ws://jest-test-mocked-url.com'], clientsNameMock).onMessageReceived,
).toHaveBeenCalledWith(callback);
});
});
10 changes: 5 additions & 5 deletions frontend/app-development/hooks/useWebSocket/useWebSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@ type UseWebSocketResult = {
};

type UseWebsocket = {
webSocketUrl: string;
clientsName: string[];
webSocketUrls: Array<string>;
clientsName: Array<string>;
webSocketConnector: typeof WSConnector;
};

export const useWebSocket = ({
webSocketUrl,
webSocketUrls,
clientsName,
webSocketConnector,
}: UseWebsocket): UseWebSocketResult => {
const wsConnectionRef = useRef(null);
useEffect(() => {
wsConnectionRef.current = webSocketConnector.getInstance(webSocketUrl, clientsName);
}, [webSocketConnector, webSocketUrl, clientsName]);
wsConnectionRef.current = webSocketConnector.getInstance(webSocketUrls, clientsName);
}, [webSocketConnector, webSocketUrls, clientsName]);

const onWSMessageReceived = <T>(callback: (message: T) => void): void => {
wsConnectionRef.current?.onMessageReceived(callback);
Expand Down
6 changes: 3 additions & 3 deletions frontend/app-development/layout/PageLayout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { RoutePaths } from 'app-development/enums/RoutePaths';
import { repoStatus } from 'app-shared/mocks/mocks';
import { HeaderMenuItemKey } from 'app-development/enums/HeaderMenuItemKey';
import { useWebSocket } from 'app-development/hooks/useWebSocket';
import { SyncEventsWebSocketHub } from 'app-shared/api/paths';
import { syncEntityUpdateWebSocketHub, syncEventsWebSocketHub } from 'app-shared/api/paths';
import { WSConnector } from 'app-shared/websockets/WSConnector';

jest.mock('app-development/hooks/useWebSocket', () => ({
Expand Down Expand Up @@ -78,8 +78,8 @@ describe('PageLayout', () => {
await resolveAndWaitForSpinnerToDisappear();

expect(useWebSocket).toHaveBeenCalledWith({
clientsName: ['FileSyncSuccess', 'FileSyncError'],
webSocketUrl: SyncEventsWebSocketHub(),
clientsName: ['FileSyncSuccess', 'FileSyncError', 'EntityUpdated'],
webSocketUrls: [syncEntityUpdateWebSocketHub(), syncEventsWebSocketHub()],
webSocketConnector: WSConnector,
});
});
Expand Down
3 changes: 3 additions & 0 deletions frontend/language/src/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -1694,6 +1694,7 @@
"ux_editor.modal_properties_data_model_label.metadata": "Metadata",
"ux_editor.modal_properties_data_model_label.postPlace": "Poststed",
"ux_editor.modal_properties_data_model_label.questions": "Spørsmål",
"ux_editor.modal_properties_data_model_label.radioknapper": "Radioknapper",
"ux_editor.modal_properties_data_model_label.zipCode": "Postnummer",
"ux_editor.modal_properties_data_model_link": "Legg til en datamodellknytning",
"ux_editor.modal_properties_data_model_link_multiple_attachments": "Legg til knytning for flere vedlegg",
Expand Down Expand Up @@ -1880,6 +1881,8 @@
"ux_editor.properties_panel.subform_table_columns.choose_component_description": "Listen viser bare de komponentene som har minst én datamodellbinding og en ledetekst",
"ux_editor.properties_panel.subform_table_columns.column_cell_content": "Celleinnhold (query): <0>{{cellContent}}</0>",
"ux_editor.properties_panel.subform_table_columns.column_header": "Kolonne {{columnNumber}}",
"ux_editor.properties_panel.subform_table_columns.column_multiple_data_model_bindings_description": "Komponenten inneholder flere felt og kan kun vise ett felt per kolonne",
"ux_editor.properties_panel.subform_table_columns.column_multiple_data_model_bindings_label": "Velg et felt",
"ux_editor.properties_panel.subform_table_columns.column_title_edit": "Kolonnetittel",
"ux_editor.properties_panel.subform_table_columns.column_title_error": "Kolonnetittel er påkrevd",
"ux_editor.properties_panel.subform_table_columns.column_title_unedit": "Kolonnetittel:",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const defaultProps: StudioTextResourcePickerProps = {
textResources,
noTextResourceOptionLabel,
};
const arbitraryTextResourceIndex = 129;

describe('StudioTextResourcePicker', () => {
beforeEach(jest.clearAllMocks);
Expand Down Expand Up @@ -53,15 +54,15 @@ describe('StudioTextResourcePicker', () => {
const user = userEvent.setup();
renderTextResourcePicker();
await user.click(getCombobox());
const textResourceToPick = textResources[129];
const textResourceToPick = textResources[arbitraryTextResourceIndex];
await user.click(screen.getByRole('option', { name: expectedOptionName(textResourceToPick) }));
await waitFor(expect(onValueChange).toBeCalled);
expect(onValueChange).toHaveBeenCalledTimes(1);
expect(onValueChange).toHaveBeenCalledWith(textResourceToPick.id);
});

it("Renders with the text of the text resource of which the ID is given by the component's value prop", () => {
const pickedTextResource = textResources[129];
const pickedTextResource = textResources[arbitraryTextResourceIndex];
renderTextResourcePicker({ value: pickedTextResource.id });
expect(getCombobox()).toHaveValue(pickedTextResource.value);
});
Expand All @@ -75,12 +76,12 @@ describe('StudioTextResourcePicker', () => {

it('Renders with the no text resource option selected by default', () => {
renderTextResourcePicker();
expect(getCombobox()).toHaveValue(noTextResourceOptionLabel);
expect(getCombobox()).toHaveValue('');
});

it('Calls the onValueChange callback with null when the user selects the unset option', async () => {
const user = userEvent.setup();
const value = textResources[129].id;
const value = textResources[arbitraryTextResourceIndex].id;
renderTextResourcePicker({ value });
await user.click(getCombobox());
await user.click(screen.getByRole('option', { name: noTextResourceOptionLabel }));
Expand All @@ -89,6 +90,16 @@ describe('StudioTextResourcePicker', () => {
expect(onValueChange).toHaveBeenCalledWith(null);
});

it('Does not apply other changes to the textfield than the ones triggered by the user when the user changes from a valid to an invalid value', async () => {
const user = userEvent.setup();
const chosenTextResource = textResources[arbitraryTextResourceIndex];
renderTextResourcePicker({ value: chosenTextResource.id });
const combobox = getCombobox();
await user.type(combobox, '{backspace}');
const newExpectedValue = chosenTextResource.value.slice(0, -1);
expect(combobox).toHaveValue(newExpectedValue);
});

it('Forwards the ref', () => {
testRefForwarding<HTMLInputElement>((ref) => renderTextResourcePicker({}, ref), getCombobox);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const StudioTextResourcePicker = forwardRef<HTMLInputElement, StudioTextR
<StudioCombobox
hideLabel
onValueChange={handleValueChange}
value={value ? [value] : ['']}
value={value ? [value] : []}
{...rest}
ref={ref}
>
Expand All @@ -41,9 +41,12 @@ export const StudioTextResourcePicker = forwardRef<HTMLInputElement, StudioTextR
function renderNoTextResourceOption(label: string): ReactElement {
// This cannot be a component function since the option component must be a direct child of the combobox component.
return (
<StudioCombobox.Option className={classes.noTextResourceOption} value=''>
{label}
</StudioCombobox.Option>
<StudioCombobox.Option
aria-label={label}
className={classes.noTextResourceOption}
description={label}
value=''
/>
);
}

Expand Down
1 change: 1 addition & 0 deletions frontend/libs/studio-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import classes from './style/studioBetaTag.module.css';
export * from './components';
export * from './hooks';
export * from './style/studio-variables.css';
export type { TextResource } from './types/TextResource';
export { classes as studioBetaTagClasses };
Loading

0 comments on commit 2c7c415

Please sign in to comment.