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

feat(contact): display Altinn Servicedesk contact if user belongs to org #14371

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
41 changes: 41 additions & 0 deletions backend/src/Designer/Controllers/ContactController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Threading.Tasks;
using Altinn.Studio.Designer.Helpers;
using Altinn.Studio.Designer.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Altinn.Studio.Designer.Controllers
{
[Route("designer/api/[controller]")]
[ApiController]
public class ContactController : ControllerBase
{
private readonly IGitea _giteaService;

public ContactController(IGitea giteaService)
{
_giteaService = giteaService;
}

[AllowAnonymous]
[HttpGet("belongs-to-org")]
public async Task<IActionResult> BelongsToOrg()
{
bool isNotAuthenticated = string.IsNullOrEmpty(AuthenticationHelper.GetDeveloperUserName(HttpContext));
if (isNotAuthenticated)
{
return Ok(new BelongsToOrgDto { BelongsToOrg = false });
}

try
{
var organizations = await _giteaService.GetUserOrganizations();
return Ok(new BelongsToOrgDto { BelongsToOrg = organizations.Count > 0 });
}
catch
{
return Ok(new BelongsToOrgDto { BelongsToOrg = false });
}
Fixed Show fixed Hide fixed
}
}
}
7 changes: 7 additions & 0 deletions backend/src/Designer/Models/Dto/BelongsToOrg.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using System.Text.Json.Serialization;

public class BelongsToOrgDto
{
[JsonPropertyName("belongsToOrg")]
public bool BelongsToOrg { get; set; }
}
5 changes: 5 additions & 0 deletions frontend/language/src/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,16 @@
"code_list_editor.text_resource.label.select": "Finn ledetekst for verdi nummer {{number}}",
"code_list_editor.text_resource.label.value": "Oppgi ledetekst for verdi nummer {{number}}",
"code_list_editor.value_item": "Verdi for alternativ {{number}}",
"contact.altinn_servicedesk.content": "Er du tjenesteeier og har du behov for hjelp? Ta kontakt med oss!",
"contact.altinn_servicedesk.heading": "Altinn Servicedesk",
"contact.email.content": "Du kan skrive en e-post til Altinn servicedesk hvis du har spørsmål om å opprette organisasjoner eller miljøer, opplever tekniske problemer eller har spørsmål om dokumentasjonen eller andre ting.",
"contact.email.heading": "Send e-post",
"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_label": "Opprett sak i Github",
"contact.serviceDesk.email": "<b>E-post:</b> <a>[email protected]</a>",
"contact.serviceDesk.emergencyPhone": "<b>Vakttelefon:</b> <a>94 49 00 02</a> (tilgjengelig kl. 15:45–07:00)",
"contact.serviceDesk.phone": "<b>Telefon:</b> <a>75 00 62 99</a>",
"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
3 changes: 3 additions & 0 deletions frontend/packages/shared/src/api/paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,6 @@ export const processEditorDataTypePath = (org, app, dataTypeId, taskId) => `${ba

// Event Hubs
export const SyncEventsWebSocketHub = () => '/sync-hub';

// Contact
export const belongsToOrg = () => `${basePath}/contact/belongs-to-org`;
4 changes: 4 additions & 0 deletions frontend/packages/shared/src/api/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
appMetadataPath,
appPolicyPath,
appVersionPath,
belongsToOrg,
branchStatusPath,
dataModelMetadataPath,
dataModelPath,
Expand Down Expand Up @@ -165,3 +166,6 @@ export const getAltinn2DelegationsCount = (org: string, serviceCode: string, ser
// ProcessEditor
export const getBpmnFile = (org: string, app: string) => get<string>(processEditorPath(org, app));
export const getProcessTaskType = (org: string, app: string, taskId: string) => get<string>(`${processTaskTypePath(org, app, taskId)}`);

// Contact Page
export const fetchBelongsToGiteaOrg = () => get(belongsToOrg());
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { type GetInTouchProvider } from '../interfaces/GetInTouchProvider';

type PhoneChannel = 'phone' | 'emergencyPhone';

const phoneChannelMap: Record<PhoneChannel, string> = {
phone: 'tel:75006299',
emergencyPhone: 'tel:94490002',
};

export class PhoneContactProvider implements GetInTouchProvider<PhoneChannel> {
public buildContactUrl(selectedChannel: PhoneChannel): string {
return phoneChannelMap[selectedChannel];
}
}
7 changes: 1 addition & 6 deletions frontend/packages/shared/src/types/QueryKey.ts
framitdavid marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export enum QueryKey {
AppPolicy = 'AppPolicy',
AppReleases = 'AppReleases',
AppVersion = 'AppVersion',
BelongsToOrg = 'BelongsToOrg',
BranchStatus = 'BranchStatus',
CurrentUser = 'CurrentUser',
DataModelMetadata = 'DataModelMetadata',
Expand All @@ -14,7 +15,6 @@ export enum QueryKey {
DeployPermissions = 'DeployPermissions',
Environments = 'Environments',
FetchBpmn = 'FetchBpmn',
FetchTextResources = 'FetchTextResources',
FormComponent = 'FormComponent',
FormLayoutSettings = 'FormLayoutSettings',
FormLayouts = 'FormLayouts',
Expand All @@ -24,7 +24,6 @@ export enum QueryKey {
InstanceId = 'InstanceId',
JsonSchema = 'JsonSchema',
LayoutNames = 'LayoutNames',
LayoutSchema = 'LayoutSchema',
LayoutSets = 'LayoutSets',
LayoutSetsExtended = 'LayoutSetsExtended',
OptionList = 'OptionList',
Expand All @@ -36,7 +35,6 @@ export enum QueryKey {
ProcessTaskDataType = 'ProcessTaskDataType',
RepoMetadata = 'RepoMetadata',
RepoPullData = 'RepoPullData',
RepoReset = 'RepoReset',
RepoStatus = 'RepoStatus',
RepoDiff = 'RepoDiff',
RuleConfig = 'RuleConfig',
Expand All @@ -59,9 +57,6 @@ export enum QueryKey {
ResourcePolicyAccessPackages = 'ResourcePolicyAccessPackages',
ResourcePolicyAccessPackageServices = 'ResourcePolicyAccessPackageServices',
ResourcePublishStatus = 'ResourcePublishStatus',
ResourceSectors = 'ResourceSectors',
ResourceThematicEurovoc = 'ResourceThematicEurovoc',
ResourceThematicLos = 'ResourceThematicLos',
SingleResource = 'SingleResource',
ValidatePolicy = 'ValidatePolicy',
ValidateResource = 'ValidateResource',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import classes from './ContactSection.module.css';
export type ContactSectionProps = {
title: string;
description: string;
link: {
link?: {
name: string;
href: string;
};
Expand All @@ -31,7 +31,7 @@ export const ContactSection = ({
</StudioHeading>
<StudioParagraph spacing>{description}</StudioParagraph>
{additionalContent && <span>{additionalContent}</span>}
<StudioLink href={link.href}>{link.name}</StudioLink>
{link && <StudioLink href={link.href}>{link.name}</StudioLink>}
</div>
</section>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, { type ReactElement } from 'react';
import { GetInTouchWith } from 'app-shared/getInTouch';
import { EmailContactProvider } from 'app-shared/getInTouch/providers';
import { StudioList, StudioLink } from '@studio/components';
import { Trans } from 'react-i18next';
import { PhoneContactProvider } from 'app-shared/getInTouch/providers/PhoneContactProvider';

export const ContactServiceDesk = (): ReactElement => {
const contactByEmail = new GetInTouchWith(new EmailContactProvider());
const contactByPhone = new GetInTouchWith(new PhoneContactProvider());
return (
<StudioList.Root>
<StudioList.Unordered>
<StudioList.Item>
<Trans
i18nKey='contact.serviceDesk.phone'
components={{
b: <b />,
a: <StudioLink href={contactByPhone.url('phone')}>{null}</StudioLink>,
}}
/>
</StudioList.Item>

<StudioList.Item>
<Trans
i18nKey='contact.serviceDesk.emergencyPhone'
values={{ phoneNumber: contactByPhone.url('phone') }}
components={{
b: <b />,
a: <StudioLink href={contactByPhone.url('emergencyPhone')}>{null}</StudioLink>,
}}
/>
</StudioList.Item>

<StudioList.Item>
<Trans
i18nKey='contact.serviceDesk.email'
values={{ phoneNumber: contactByPhone.url('phone') }}
components={{
b: <b />,
a: <StudioLink href={contactByEmail.url('serviceOwner')}>{null}</StudioLink>,
}}
/>
</StudioList.Item>
</StudioList.Unordered>
</StudioList.Root>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ContactServiceDesk } from './ContactServiceDesk';
26 changes: 26 additions & 0 deletions frontend/studio-root/pages/Contact/ContactPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ import React from 'react';
import { screen, render } from '@testing-library/react';
import { textMock } from '@studio/testing/mocks/i18nMock';
import { ContactPage } from './ContactPage';
import { useFetchBelongsToOrgQuery } from '../hooks/queries/useFetchBelongsToOrgQuery';

jest.mock('../hooks/queries/useFetchBelongsToOrgQuery');

(useFetchBelongsToOrgQuery as jest.Mock).mockReturnValue({
data: { belongsToOrg: false },
});

describe('ContactPage', () => {
it('should display the main heading', () => {
Expand Down Expand Up @@ -44,4 +51,23 @@ describe('ContactPage', () => {
screen.getByRole('link', { name: textMock('contact.github_issue.link_label') }),
).toBeInTheDocument();
});

it('should not render contact info for "Altinn Servicedesk" if the user does not belong to a org', () => {
(useFetchBelongsToOrgQuery as jest.Mock).mockReturnValue({
data: { belongsToOrg: false },
});
render(<ContactPage />);

expect(screen.queryByRole('heading', { name: textMock('contact.altinn_servicedesk.heading') }));
expect(screen.queryByText(textMock('contact.altinn_servicedesk.content')));
});

it('should display contact information to "Altinn Servicedesk"', () => {
framitdavid marked this conversation as resolved.
Show resolved Hide resolved
(useFetchBelongsToOrgQuery as jest.Mock).mockReturnValue({
data: { belongsToOrg: true },
});
render(<ContactPage />);
expect(screen.getByRole('heading', { name: textMock('contact.altinn_servicedesk.heading') }));
expect(screen.getByText(textMock('contact.altinn_servicedesk.content')));
});
});
25 changes: 22 additions & 3 deletions frontend/studio-root/pages/Contact/ContactPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import classes from './ContactPage.module.css';
import { Trans, useTranslation } from 'react-i18next';
import { EnvelopeClosedIcon, SlackIcon, GitHubIcon } from '@studio/icons';
import { EnvelopeClosedIcon, SlackIcon, GitHubIcon, PersonHeadsetIcon } from '@studio/icons';
import { GetInTouchWith } from 'app-shared/getInTouch';
import {
EmailContactProvider,
Expand All @@ -14,14 +14,22 @@ import {
StudioParagraph,
} from '@studio/components';
import { ContactSection, type ContactSectionProps } from '../../components/ContactSection';
import { ContactServiceDesk } from '../../components/ContactServiceDesk';
import { useFetchBelongsToOrgQuery } from '../hooks/queries/useFetchBelongsToOrgQuery';

type ContactSectionMetadata = {
shouldHideSection?: boolean;
};

export const ContactPage = (): React.ReactElement => {
const { t } = useTranslation();
const contactByEmail = new GetInTouchWith(new EmailContactProvider());
const contactBySlack = new GetInTouchWith(new SlackContactProvider());
const contactByGitHubIssue = new GetInTouchWith(new GitHubIssueContactProvider());

const contactSections: Array<ContactSectionProps> = [
const { data: belongsToOrgData } = useFetchBelongsToOrgQuery();

const contactSections: Array<ContactSectionProps & ContactSectionMetadata> = [
{
title: t('contact.email.heading'),
description: t('contact.email.content'),
Expand Down Expand Up @@ -58,6 +66,13 @@ export const ContactPage = (): React.ReactElement => {
},
Icon: GitHubIcon,
},
{
title: t('contact.altinn_servicedesk.heading'),
additionalContent: <ContactServiceDesk />,
description: t('contact.altinn_servicedesk.content'),
Icon: PersonHeadsetIcon,
shouldHideSection: !belongsToOrgData?.belongsToOrg,
},
];

return (
Expand All @@ -69,11 +84,15 @@ export const ContactPage = (): React.ReactElement => {
{t('general.contact')}
</StudioHeading>
</div>
{contactSections.map((contactSection) => (
{contactSections.filter(filterHiddenSections).map((contactSection) => (
<ContactSection {...contactSection} key={contactSection.title} />
))}
</div>
</div>
</StudioPageImageBackgroundContainer>
);
};

function filterHiddenSections(section: ContactSectionProps & ContactSectionMetadata): boolean {
return !section.shouldHideSection;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useQuery, type UseQueryResult } from '@tanstack/react-query';
import { useServicesContext } from 'app-shared/contexts/ServicesContext';
import { QueryKey } from 'app-shared/types/QueryKey';

type BelongsToOrg = {
belongsToOrg: boolean;
};

export const useFetchBelongsToOrgQuery = (): UseQueryResult<BelongsToOrg> => {
const { fetchBelongsToGiteaOrg } = useServicesContext();

return useQuery({
queryKey: [QueryKey.BelongsToOrg],
queryFn: () => fetchBelongsToGiteaOrg(),
});
};
Loading