Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dashboard: Add a save button to the google analytics id form #4336

Merged
merged 12 commits into from
Sep 8, 2020
14 changes: 13 additions & 1 deletion assets/src/dashboard/app/views/editorSettings/components.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ import { rgba } from 'polished';
import {
TypographyPresets,
StandardViewContentGutter,
Button,
} from '../../../components';
import { visuallyHiddenStyles } from '../../../utils/visuallyHiddenStyles';
import { Link } from '../../../components/link';
import { KEYBOARD_USER_SELECTOR } from '../../../constants';
import { BUTTON_TYPES, KEYBOARD_USER_SELECTOR } from '../../../constants';

export const Wrapper = styled.div`
margin: 0 107px;
Expand Down Expand Up @@ -161,4 +162,15 @@ export const RemoveLogoButton = styled.button`
}
`;

export const SaveButton = styled(Button).attrs({ type: BUTTON_TYPES.PRIMARY })`
float: right;
`;

export const ErrorText = styled.p`
${TypographyPresets.ExtraSmall};
color: ${({ theme }) => theme.colors.danger};
margin-left: 1em;
padding-top: 0.25em;
`;

export const VisuallyHiddenDescription = styled.span(visuallyHiddenStyles);
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ import { __ } from '@wordpress/i18n';
* Internal dependencies
*/
import { validateGoogleAnalyticsIdFormat } from '../../../../utils';
import { InlineInputForm } from '../../../../components';
import { TextInput } from '../../../../components';
import {
FormContainer,
InlineLink,
SettingForm,
SettingHeading,
TextInputHelperText,
SaveButton,
ErrorText,
} from '../components';

export const TEXT = {
Expand All @@ -55,47 +57,67 @@ export const TEXT = {
function GoogleAnalyticsSettings({ googleAnalyticsId, handleUpdate }) {
const [analyticsId, setAnalyticsId] = useState(googleAnalyticsId);
const [inputError, setInputError] = useState('');
const canSave = analyticsId !== googleAnalyticsId && !inputError;
const disableSaveButton = !canSave;

useEffect(() => {
setAnalyticsId(googleAnalyticsId);
}, [googleAnalyticsId]);

const handleCancelUpdateId = useCallback(() => {
setAnalyticsId(googleAnalyticsId);
}, [googleAnalyticsId, setAnalyticsId]);
const handleUpdateId = useCallback((event) => {
const { value } = event.target;
setAnalyticsId(value);

if (value.length === 0 || validateGoogleAnalyticsIdFormat(value)) {
setInputError('');

return;
}

const handleUpdateId = useCallback(
(value) => {
if (value.length === 0 || validateGoogleAnalyticsIdFormat(value)) {
setInputError('');
return handleUpdate(value);
setInputError(TEXT.INPUT_ERROR);
}, []);

const handleOnSave = useCallback(() => {
if (canSave) {
handleUpdate(analyticsId);
}
}, [canSave, analyticsId, handleUpdate]);

const handleOnKeyDown = useCallback(
(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleOnSave();
}
return setInputError(TEXT.INPUT_ERROR);
},
[handleUpdate, setInputError]
[handleOnSave]
);

return (
<SettingForm onSubmit={(e) => e.preventDefault()}>
<SettingHeading htmlFor="gaTrackingID">
{TEXT.SECTION_HEADING}
</SettingHeading>
<FormContainer>
<InlineInputForm
<TextInput
label={TEXT.ARIA_LABEL}
id="gaTrackingId"
value={analyticsId}
onEditCancel={handleCancelUpdateId}
onEditComplete={handleUpdateId}
onChange={handleUpdateId}
onKeyDown={handleOnKeyDown}
placeholder={TEXT.PLACEHOLDER}
error={inputError}
dmmulroy marked this conversation as resolved.
Show resolved Hide resolved
noAutoFocus={true}
/>
{inputError && <ErrorText>{inputError}</ErrorText>}
<TextInputHelperText>
{TEXT.CONTEXT}
<InlineLink href={TEXT.CONTEXT_ARTICLE_LINK}>
{TEXT.CONTEXT_ARTICLE}
</InlineLink>
</TextInputHelperText>
<SaveButton isDisabled={disableSaveButton} onClick={handleOnSave}>
{__('Save', 'web-stories')}
</SaveButton>
dmmulroy marked this conversation as resolved.
Show resolved Hide resolved
</FormContainer>
</SettingForm>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,24 @@ import { renderWithTheme } from '../../../../../testUtils';
import GoogleAnalyticsSettings, { TEXT } from '../';

describe('Editor Settings: Google Analytics <GoogleAnalytics />', function () {
const mockUpdate = jest.fn();
let googleAnalyticsId;
let mockUpdate;

beforeEach(() => {
googleAnalyticsId = '';
mockUpdate = jest.fn((id) => {
googleAnalyticsId = id;
});
});

afterEach(() => {
googleAnalyticsId = '';
});

it('should render google analytics input and helper text by default', function () {
const { getByRole, getByText } = renderWithTheme(
<GoogleAnalyticsSettings
googleAnalyticsId={''}
googleAnalyticsId={googleAnalyticsId}
handleUpdate={mockUpdate}
/>
);
Expand All @@ -43,29 +55,90 @@ describe('Editor Settings: Google Analytics <GoogleAnalytics />', function () {
});

it('should call mockUpdate when enter is keyed on input', function () {
const { getByRole } = renderWithTheme(
let { getByRole, rerender } = renderWithTheme(
<GoogleAnalyticsSettings
googleAnalyticsId={googleAnalyticsId}
handleUpdate={mockUpdate}
/>
);

let input = getByRole('textbox');

fireEvent.change(input, { target: { value: 'UA-098754-33' } });
fireEvent.keyDown(input, { key: 'Enter', keyCode: 13 });

// rerender to get updated googleAnalyticsId prop
rerender(
<GoogleAnalyticsSettings
googleAnalyticsId={''}
googleAnalyticsId={googleAnalyticsId}
handleUpdate={mockUpdate}
/>
);

expect(mockUpdate).toHaveBeenCalledTimes(1);

fireEvent.change(input, { target: { value: '' } });
fireEvent.keyDown(input, { key: 'Enter', keyCode: 13 });

// rerender to get updated googleAnalyticsId prop
rerender(
<GoogleAnalyticsSettings
googleAnalyticsId={googleAnalyticsId}
handleUpdate={mockUpdate}
/>
);

expect(mockUpdate).toHaveBeenCalledTimes(2);

fireEvent.change(input, { target: { value: 'NOT A VALID ID!!!' } });

fireEvent.keyDown(input, { key: 'Enter', keyCode: 13 });

expect(mockUpdate).toHaveBeenCalledTimes(2);
});

it('should call mockUpdate when the save button is clicked', function () {
const { getByRole, rerender } = renderWithTheme(
<GoogleAnalyticsSettings
googleAnalyticsId={googleAnalyticsId}
handleUpdate={mockUpdate}
/>
);

const input = getByRole('textbox');
const button = getByRole('button');

fireEvent.change(input, { target: { value: 'UA-098754-33' } });

fireEvent.keyDown(input, { key: 'enter', keyCode: 13 });
fireEvent.click(button);

// rerender to get updated googleAnalyticsId prop
rerender(
<GoogleAnalyticsSettings
googleAnalyticsId={googleAnalyticsId}
handleUpdate={mockUpdate}
/>
);

expect(mockUpdate).toHaveBeenCalledTimes(1);

fireEvent.change(input, { target: { value: '' } });

fireEvent.keyDown(input, { key: 'enter', keyCode: 13 });
fireEvent.click(button);

// rerender to get updated googleAnalyticsId prop
rerender(
<GoogleAnalyticsSettings
googleAnalyticsId={googleAnalyticsId}
handleUpdate={mockUpdate}
/>
);

expect(mockUpdate).toHaveBeenCalledTimes(2);

fireEvent.change(input, { target: { value: 'NOT A VALID ID!!!' } });

fireEvent.keyDown(input, { key: 'enter', keyCode: 13 });
fireEvent.click(button);

expect(mockUpdate).toHaveBeenCalledTimes(2);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ describe('Settings View', () => {
expect(PageHeading).toBeTruthy();
});

it('should update the tracking id', async () => {
it('should update the tracking id when pressning Enter', async () => {
const settingsView = await fixture.screen.getByTestId('editor-settings');

const input = within(settingsView).getByRole('textbox');
Expand All @@ -108,11 +108,98 @@ describe('Settings View', () => {

const { googleAnalyticsId } = await getSettingsState();

const newInput = await fixture.screen.getByRole('textbox');
expect(newInput.value).toBe(googleAnalyticsId);
expect(input.value).toBe(googleAnalyticsId);
});

it("it should not allow an update of google analytics id when id format doesn't match required format", async () => {
it('should update the tracking id by clicking the save button', async () => {
const settingsView = await fixture.screen.getByTestId('editor-settings');

const { getByRole } = within(settingsView);

const input = getByRole('textbox');
const button = getByRole('button', { name: /Save/ });

await fixture.events.hover(input);

await fixture.events.click(input);

const inputLength = input.value.length;

for (let iter = 0; iter < inputLength; iter++) {
// disable eslint to prevent overlapping .act calls
// eslint-disable-next-line no-await-in-loop
await fixture.events.keyboard.press('Backspace');
}

await fixture.events.keyboard.type('UA-009345-6');
await fixture.events.click(button);

const { googleAnalyticsId } = await getSettingsState();

expect(input.value).toBe(googleAnalyticsId);
});

it('should allow the analytics id to saved as an empty string', async () => {
const settingsView = await fixture.screen.getByTestId('editor-settings');
const { googleAnalyticsId: initialId } = await getSettingsState();

expect(initialId).not.toEqual('');

const { getByRole } = within(settingsView);

const input = getByRole('textbox');
const button = getByRole('button', { name: /Save/ });

await fixture.events.hover(input);

await fixture.events.click(input);

const inputLength = input.value.length;

for (let iter = 0; iter < inputLength; iter++) {
// disable eslint to prevent overlapping .act calls
// eslint-disable-next-line no-await-in-loop
await fixture.events.keyboard.press('Backspace');
}

await fixture.events.click(button);

const { googleAnalyticsId: analyticsId } = await getSettingsState();

expect(analyticsId).toEqual('');
});

it('should not allow an invalid analytics id to saved', async () => {
const settingsView = await fixture.screen.getByTestId('editor-settings');
const { googleAnalyticsId: initialId } = await getSettingsState();

expect(initialId).not.toEqual('');

const { getByRole } = within(settingsView);

const input = getByRole('textbox');
const button = getByRole('button', { name: /Save/ });

await fixture.events.hover(input);

await fixture.events.click(input);

const inputLength = input.value.length;

for (let iter = 0; iter < inputLength; iter++) {
// disable eslint to prevent overlapping .act calls
// eslint-disable-next-line no-await-in-loop
await fixture.events.keyboard.press('Backspace');
}
await fixture.events.keyboard.type('INVALID');
await fixture.events.click(button);

const { googleAnalyticsId: analyticsId } = await getSettingsState();

expect(analyticsId).toEqual(initialId);
});

it("should not allow an update of google analytics id when id format doesn't match required format", async () => {
const settingsView = await fixture.screen.getByTestId('editor-settings');

const input = within(settingsView).getByRole('textbox');
Expand Down
3 changes: 2 additions & 1 deletion assets/src/dashboard/app/views/myStories/header/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
DASHBOARD_VIEWS,
STORY_STATUSES,
STORY_SORT_MENU_ITEMS,
TEXT_INPUT_DEBOUNCE,
} from '../../../../constants';
import {
StoriesPropType,
Expand Down Expand Up @@ -118,7 +119,7 @@ function Header({

const [debouncedTypeaheadChange] = useDebouncedCallback(
search.setKeyword,
300
TEXT_INPUT_DEBOUNCE
dmmulroy marked this conversation as resolved.
Show resolved Hide resolved
);

return (
Expand Down
2 changes: 2 additions & 0 deletions assets/src/dashboard/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ export const STORIES_PER_REQUEST = 24;

export const DEFAULT_DATE_FORMAT = 'Y-m-d';

export const TEXT_INPUT_DEBOUNCE = 300;

export * from './components';
export * from './direction';
export * from './pageStructure';
Expand Down