Skip to content

Commit

Permalink
chore(GetInTouch): Modularize contact URL management using GetInTouch…
Browse files Browse the repository at this point in the history
…With class (#13139)

chore(GetInTouch): Modularize contact URL management using GetInTouchWith class
  • Loading branch information
framitdavid authored Aug 13, 2024
1 parent a16615c commit 3016b0c
Show file tree
Hide file tree
Showing 19 changed files with 267 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export function DeployPage() {
isError: permissionsIsError,
} = useDeployPermissionsQuery(org, app);
useInvalidator();

if (orgsIsPending || permissionsIsPending) {
return (
<AltinnContentLoader width={1200} height={600} title={t('app_deployment.loading')}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,21 @@ import { Trans, useTranslation } from 'react-i18next';
import cn from 'classnames';
import type { AlertProps } from '@digdir/designsystemet-react';
import { Alert, Heading, Paragraph } from '@digdir/designsystemet-react';
import { EmailContactProvider } from 'app-shared/getInTouch/providers';
import { GetInTouchWith } from 'app-shared/getInTouch';

type NoEnvironmentsAlertProps = AlertProps;
export const NoEnvironmentsAlert = ({ ...rest }: NoEnvironmentsAlertProps) => {
const { t } = useTranslation();

const contactByEmail = new GetInTouchWith(new EmailContactProvider());
return (
<Alert severity='warning' className={cn(rest.className)} {...rest}>
<Heading level={2} size='small' spacing>
{t('app_deployment.no_env_title')}
</Heading>
<Paragraph spacing>
<Trans i18nKey='app_deployment.no_env_1'>
<a href='mailto:tjenesteeier@altinn.no' />
<a href={contactByEmail.url('serviceOwner')} />
</Trans>
</Paragraph>
<Paragraph>
Expand Down
5 changes: 4 additions & 1 deletion frontend/language/src/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
"contact.email.link": "<0 href=\"mailto:servicedesk@altinn.no\">servicedesk@altinn.no</0>",
"contact.github_issue.content": "Hvis du har behov for funksjonalitet eller ser feil og mangler i Studio som vi må fikse, kan du opprette en sak i Github, så ser vi på den.",
"contact.github_issue.heading": "Rapporter feil og mangler til oss",
"contact.github_issue.link": "<0 href=\"https://github.com/Altinn/altinn-studio/issues/new/choose\">Opprett sak i Github</0>",
"contact.github_issue.link_label": "Opprett sak i Github",
"contact.slack.content": "Hvis du har spørsmål om hvordan du bygger en app, kan du snakke direkte med utviklingsteamet i Altinn Studio på Slack. De hjelper deg med å",
"contact.slack.content_list": "<0>bygge appene slik du ønsker</0><0>svare på spørsmål og veilede deg</0><0>ta imot innspill på ny funksjonalitet</0>",
"contact.slack.heading": "Skriv melding til oss Slack",
Expand Down Expand Up @@ -273,6 +273,9 @@
"general.saving": "Lagrer",
"general.search": "Søk",
"general.select_component": "Velg komponent",
"general.select_field": "Velg felt",
"general.service_description_header": "Beskrivelse",
"general.service_desk.email": "servicedesk@altinn.no",
"general.service_name": "Navn",
"general.service_owner": "Eier",
"general.test_environment_alt": "Testmiljøet",
Expand Down
25 changes: 25 additions & 0 deletions frontend/packages/shared/src/getInTouch/GetInTouchWith.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { GetInTouchWith } from 'app-shared/getInTouch/GetInTouchWith';
import {
EmailContactProvider,
GitHubIssueContactProvider,
SlackContactProvider,
} from 'app-shared/getInTouch/providers';

describe('GetInTouchWith', () => {
it('should be high-level module that support low-level module', () => {
const contact = new GetInTouchWith(new EmailContactProvider());
expect(contact.url('serviceOwner')).toBe('mailto:tjenesteeier@altinn.no');
});

it('should have the same API regardless of used low-level implementation module', () => {
const contactByEmail = new GetInTouchWith(new EmailContactProvider());
const contactBySlack = new GetInTouchWith(new SlackContactProvider());
const contactByGitHubIssue = new GetInTouchWith(new GitHubIssueContactProvider());

expect(contactByEmail.url('serviceDesk')).toBe('mailto:servicedesk@altinn.no');
expect(contactBySlack.url('altinn')).toBe('https://altinn.slack.com');
expect(contactByGitHubIssue.url('bugReport')).toBe(
'https://github.com/Altinn/altinn-studio/issues/new?labels=kind/bug,status/triage&template=bug_report.yml',
);
});
});
9 changes: 9 additions & 0 deletions frontend/packages/shared/src/getInTouch/GetInTouchWith.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { type GetInTouchProvider } from './interfaces/GetInTouchProvider';

export class GetInTouchWith<T, Options> {
constructor(private contactProvider: GetInTouchProvider<T, Options>) {}

public url(selectedChannel: T, options?: Options): string {
return this.contactProvider.buildContactUrl(selectedChannel, options);
}
}
1 change: 1 addition & 0 deletions frontend/packages/shared/src/getInTouch/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { GetInTouchWith } from './GetInTouchWith';
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type Options<K extends string, V = string> = Partial<Record<K, V>>;

export interface GetInTouchProvider<T, Options = null> {
buildContactUrl: (channel: T, options?: Options) => string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { EmailContactProvider } from 'app-shared/getInTouch/providers/EmailContactProvider';

describe('EmailContactProvider', () => {
it('should return correct email based on selectedChannel', () => {
const emailContactProvider = new EmailContactProvider();

expect(emailContactProvider.buildContactUrl('serviceOwner')).toBe(
'mailto:tjenesteeier@altinn.no',
);
expect(emailContactProvider.buildContactUrl('serviceDesk')).toBe(
'mailto:servicedesk@altinn.no',
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { type GetInTouchProvider } from '../interfaces/GetInTouchProvider';

type EmailChannel = 'serviceDesk' | 'serviceOwner';

const emailChannelMap: Record<EmailChannel, string> = {
serviceDesk: 'mailto:servicedesk@altinn.no',
serviceOwner: 'mailto:tjenesteeier@altinn.no',
};

export class EmailContactProvider implements GetInTouchProvider<EmailChannel> {
public buildContactUrl(selectedChannel: EmailChannel): string {
return emailChannelMap[selectedChannel];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { type GetInTouchProvider, type Options } from '../interfaces/GetInTouchProvider';

type BugReportFields = 'title' | 'steps-to-reproduce' | 'additional-information';
type FeatureRequestFields = 'title' | 'description' | 'additional-information';

type GitHubIssueContactOptions = Options<BugReportFields | FeatureRequestFields, string>;

type GitHubIssueTypes = 'featureRequest' | 'bugReport' | 'choose';
type MappableGithubTypes = Exclude<GitHubIssueTypes, 'choose'>;

type GitHubChannelConfig = {
labels: Array<string>;
template: string;
};

const gitHubIssueType: Record<MappableGithubTypes, GitHubChannelConfig> = {
featureRequest: {
labels: ['kind/feature-request', 'status/triage'],
template: 'feature_request.yml',
},
bugReport: {
labels: ['kind/bug', 'status/triage'],
template: 'bug_report.yml',
},
};

export class GitHubIssueContactProvider
implements GetInTouchProvider<GitHubIssueTypes, GitHubIssueContactOptions>
{
private readonly githubRepoUrl: string = 'https://github.com/Altinn/altinn-studio';
private readonly githubIssueUrl: string = `${this.githubRepoUrl}/issues/new`;

public buildContactUrl(
selectedIssueType: GitHubIssueTypes,
options?: GitHubIssueContactOptions,
): string {
if (selectedIssueType === 'choose') return `${this.githubIssueUrl}/${selectedIssueType}`;
return (
this.githubIssueUrl + this.optionToUrlParams(gitHubIssueType[selectedIssueType], options)
);
}

private optionToUrlParams(
selectedConfig: GitHubChannelConfig,
options?: GitHubIssueContactOptions,
): string {
const labels = selectedConfig.labels.join(',');
const optionsQueryParams = options ? `&${this.mapOptionsToQueryParams(options)}` : '';
return `?labels=${labels}&template=${selectedConfig.template}${optionsQueryParams}`;
}

private mapOptionsToQueryParams(options: GitHubIssueContactOptions): string {
return Object.entries(options)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { GitHubIssueContactProvider } from 'app-shared/getInTouch/providers/GitHubIssueContactProvider';

describe('GitHubIssuesContactProvider', () => {
it('should return correct link based on selected issue type', () => {
const gitHubIssuesContactProvider = new GitHubIssueContactProvider();
expect(gitHubIssuesContactProvider.buildContactUrl('featureRequest')).toBe(
'https://github.com/Altinn/altinn-studio/issues/new?labels=kind/feature-request,status/triage&template=feature_request.yml',
);

expect(gitHubIssuesContactProvider.buildContactUrl('bugReport')).toBe(
'https://github.com/Altinn/altinn-studio/issues/new?labels=kind/bug,status/triage&template=bug_report.yml',
);

expect(gitHubIssuesContactProvider.buildContactUrl('choose')).toBe(
'https://github.com/Altinn/altinn-studio/issues/new/choose',
);
});

it('should support options to prefill form', () => {
const gitHubIssuesContactProvider = new GitHubIssueContactProvider();
expect(
gitHubIssuesContactProvider.buildContactUrl('bugReport', {
title: 'title of the issue',
'additional-information': 'cannot read property of undefined, reading id',
}),
).toBe(
'https://github.com/Altinn/altinn-studio/issues/new?labels=kind/bug,status/triage&template=bug_report.yml&title=title%20of%20the%20issue&additional-information=cannot%20read%20property%20of%20undefined%2C%20reading%20id',
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { SlackContactProvider } from 'app-shared/getInTouch/providers/SlackContactProvider';

describe('SlackContactProvider', () => {
it('should return correct Slack link based on selectedChannel', () => {
const slackContactProvider = new SlackContactProvider();

expect(slackContactProvider.buildContactUrl('altinn')).toBe('https://altinn.slack.com');
expect(slackContactProvider.buildContactUrl('product-altinn-studio')).toBe(
'https://altinn.slack.com/archives/C02EJ9HKQA3',
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { type GetInTouchProvider } from '../interfaces/GetInTouchProvider';

type SlackChannel = 'product-altinn-studio' | 'altinn';

const slackChannelMap: Record<SlackChannel, string> = {
'product-altinn-studio': 'https://altinn.slack.com/archives/C02EJ9HKQA3',
altinn: 'https://altinn.slack.com',
};

export class SlackContactProvider implements GetInTouchProvider<SlackChannel> {
public buildContactUrl(selectedChannel: SlackChannel): string {
return slackChannelMap[selectedChannel];
}
}
3 changes: 3 additions & 0 deletions frontend/packages/shared/src/getInTouch/providers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { EmailContactProvider } from './EmailContactProvider';
export { GitHubIssueContactProvider } from './GitHubIssueContactProvider';
export { SlackContactProvider } from './SlackContactProvider';
10 changes: 7 additions & 3 deletions frontend/studio-root/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ import { useTranslation, Trans } from 'react-i18next';

import './App.css';
import { PageLayout } from '../pages/PageLayout';
import { Contact } from '../pages/Contact/Contact';
import { ContactPage } from '../pages/Contact/ContactPage';
import { EmailContactProvider } from 'app-shared/getInTouch/providers';
import { GetInTouchWith } from 'app-shared/getInTouch';

export const App = (): JSX.Element => {
return (
<div className={classes.root}>
<Routes>
<Route element={<PageLayout />}>
<Route path='/contact' element={<Contact />} />
<Route path='/contact' element={<ContactPage />} />
<Route path='*' element={<NotFoundPage />} />
</Route>
</Routes>
Expand All @@ -25,13 +27,15 @@ export const App = (): JSX.Element => {
const NotFoundPage = () => {
const { t } = useTranslation();

const contactByEmail = new GetInTouchWith(new EmailContactProvider());

return (
<StudioNotFoundPage
title={t('not_found_page.heading')}
body={
<Paragraph size='small'>
<Trans i18nKey='not_found_page.text'>
<Link href='mailto:tjenesteeier@altinn.no'>tjenesteeier@altinn.no</Link>
<Link href={contactByEmail.url('serviceOwner')}>tjenesteeier@altinn.no</Link>
</Trans>
</Paragraph>
}
Expand Down
31 changes: 0 additions & 31 deletions frontend/studio-root/pages/Contact/Contact.test.tsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,6 @@
flex: 1;
}

.link a {
font-size: 1rem;
}

@media (min-width: 768px) {
.content {
margin: var(--fds-spacing-18);
Expand Down
47 changes: 47 additions & 0 deletions frontend/studio-root/pages/Contact/ContactPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react';
import { screen, render } from '@testing-library/react';
import { textMock } from '@studio/testing/mocks/i18nMock';
import { ContactPage } from './ContactPage';

describe('ContactPage', () => {
it('should display the main heading', () => {
render(<ContactPage />);
expect(
screen.getByRole('heading', { name: textMock('general.contact'), level: 1 }),
).toBeInTheDocument();
});

it('should display the contact by email section with its content and link', () => {
render(<ContactPage />);
expect(
screen.getByRole('heading', { name: textMock('contact.email.heading') }),
).toBeInTheDocument();
expect(screen.getByText(textMock('contact.email.content'))).toBeInTheDocument();
expect(
screen.getByRole('link', { name: textMock('general.service_desk.email') }),
).toBeInTheDocument();
});

it('should display the contact by Slack section with its content, list, and link', () => {
render(<ContactPage />);

expect(
screen.getByRole('heading', { name: textMock('contact.slack.heading') }),
).toBeInTheDocument();
expect(screen.getByText(textMock('contact.slack.content'))).toBeInTheDocument();
expect(screen.getByText(textMock('contact.slack.content_list'))).toBeInTheDocument();
expect(screen.getByRole('link', { name: textMock('contact.slack.link') })).toBeInTheDocument();
});

it('should display the bug report and feature request section with its content and link', () => {
render(<ContactPage />);

expect(
screen.getByRole('heading', { name: textMock('contact.github_issue.heading') }),
).toBeInTheDocument();
expect(screen.getByText(textMock('contact.github_issue.content'))).toBeInTheDocument();
expect(
screen.getByRole('link', { name: textMock('contact.github_issue.link_label') }),
).toBeInTheDocument();
});
});
Loading

0 comments on commit 3016b0c

Please sign in to comment.