Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions frontend/src/components/ContactSelection.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.contact-selection {
&::part(items) {
display: flex;
flex-direction: column;
align-items: flex-start;
order: 2;
width: 100%;
gap: var(--ds-size-2);
margin-top: var(--ds-size-3);
}

& > :is(del, button[type='reset']) {
order: 1;
}
}
60 changes: 60 additions & 0 deletions frontend/src/components/ContactSelection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useMemo } from 'react'
import './ContactSelection.css'
import { EXPERIMENTAL_Suggestion as Suggestion, type SuggestionItem, Field } from '@digdir/designsystemet-react'
import type { Contact } from '@ssbno-statreg/shared'
import { formatContact } from '../lib/utils'

export type ContactSelectionProps = {
contacts: Contact[]
selected: string[]
setSelected: (selected: string[]) => void
}

export function ContactSelection({ contacts, selected, setSelected }: ContactSelectionProps) {

Check warning on line 13 in frontend/src/components/ContactSelection.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=statisticsnorway_ssbno-statreg&issues=AZ9q3JPR2k4KVpyyikgP&open=AZ9q3JPR2k4KVpyyikgP&pullRequest=428
const contactMap = useMemo(
() =>
contacts.reduce<Record<string, Contact>>((record, contact) => {
record[contact.principalName] = contact
return record
}, {}),
[contacts]
)

const selectedItems: SuggestionItem[] = selected.map((principalName) => ({
label: formatContact(contactMap[principalName]),
value: principalName,
}))

const options = useMemo(
Comment thread
Glenruben marked this conversation as resolved.
() =>
contacts.map((contact) => (
<Suggestion.Option
className='contact-selection-item'
key={contact.principalName}
label={formatContact(contact)}
value={contact.principalName}
>
{formatContact(contact)}
</Suggestion.Option>
)),
[contacts]
)

return (
<Field>
<Suggestion
className='contact-selection'
multiple
onSelectedChange={(items) => setSelected(items.map((item) => item.value))}
selected={selectedItems}
>
<Suggestion.Input />
<Suggestion.Clear aria-label='Tøm valgte kontakter' />
<Suggestion.List className='contact-selection-list'>
<Suggestion.Empty>Ingen treff</Suggestion.Empty>
{options}
</Suggestion.List>
</Suggestion>
</Field>
)
}
10 changes: 6 additions & 4 deletions frontend/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,14 @@ export function formatVariant(variant?: Variant): string {
return [frequency, revision].join(', ')
}

export function formatContact(contact: Contact): string {
const username = contact.principalName?.split('@')[0]
return [contact.name ?? '', username ? `(${username})` : ''].filter(Boolean).join(' ')
}

export function formatContacts(contacts?: Contact[]): string[] {
if (!contacts) return []
return contacts.map((contact) => {
const username = contact.principalName?.split('@')[0]
return [contact.name ?? '', username ? `(${username})` : ''].filter(Boolean).join(' ')
})
return contacts.map(formatContact)
}

export function getPublishTimeFilterForDate(selectedDate: Date | undefined) {
Expand Down
14 changes: 13 additions & 1 deletion frontend/src/views/ShowStatistic.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,19 @@
}

.show-statistic-contacts-container {
.edit-contact-button {
.show-statistic-contacts-heading {
display: flex;
align-items: center;
gap: var(--ds-size-2);
}

.show-statistic-contacts-content {
margin-top: var(--ds-size-4);
}

.show-statistic-contacts-button-wrapper {
display: flex;
gap: var(--ds-size-3);
margin-top: var(--ds-size-4);
}
}
91 changes: 74 additions & 17 deletions frontend/src/views/ShowStatistic.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
import { useState, useEffect } from 'react'
import { useNavigate, useParams, Link as ReactRouterLink } from 'react-router'
import { Heading, Paragraph, List, Link, Button, Divider, Details, Card, Table } from '@digdir/designsystemet-react'
import { PencilWritingIcon, PersonPencilIcon } from '@navikt/aksel-icons'
import { PencilWritingIcon } from '@navikt/aksel-icons'
import { StatisticStatusTag } from '../components/StatisticStatusTag'
import { VariantCard } from '../components/VariantCard'
import client from '../api'
import {
StatisticStatus,
type Contact,
type RegionLevel,
type ReleaseListing,
type StatisticDetails,
type Variant,
} from '@ssbno-statreg/shared'

import './ShowStatistic.css'
import { formatContacts, formatPublishTime, formatRevisionName, formatVariant } from '../lib/utils'
import { formatContact, formatPublishTime, formatRevisionName, formatVariant } from '../lib/utils'
import { ApprovalStatusBadge } from '../components/ApprovalStatus'
import { useAuth } from '../context/AuthContext'
import { ErrorAlert } from '../components/ErrorAlert'
import { ContactSelection } from '../components/ContactSelection'

type ReleaseRowProps = {
release: ReleaseListing
Expand Down Expand Up @@ -96,6 +98,9 @@ function SimpleReleaseRow({ release }: ReleaseRowProps) {
export default function ShowStatistic() {
const [statistic, setStatistic] = useState<StatisticDetails>({})
const [releases, setReleases] = useState<ReleaseListing[]>([])
const [allContacts, setAllContacts] = useState<Contact[]>([])
const [selectedContacts, setSelectedContacts] = useState<string[]>([])
const [isEditingContacts, setIsEditingContacts] = useState(false)
const { shortname } = useParams()
const { auth } = useAuth()
const [apiError, setApiError] = useState<string[]>([])
Expand All @@ -112,6 +117,7 @@ export default function ShowStatistic() {
}

setStatistic(data)
setSelectedContacts(data.contacts?.map((c) => c.principalName) ?? [])
}

async function fetchReleases(shortname: string) {
Expand All @@ -130,13 +136,43 @@ export default function ShowStatistic() {
if (shortname) fetchReleases(shortname)
}, [shortname])

useEffect(() => {
async function fetchContacts() {
const { data, error } = await client.GET('/contacts')

if (error) {
setApiError((prev) => [...prev, error.message])
return
}

setAllContacts(data ?? [])
}
fetchContacts()
}, [])

async function saveContacts() {
if (!shortname) return

const { data, error } = await client.PUT('/statistics/{shortname}/contacts', {
params: { path: { shortname } },
body: { principalNames: selectedContacts },
})

if (error) {
setApiError((prev) => [...prev, error.message])
return
}

setStatistic((prev) => ({ ...prev, contacts: data }))
setIsEditingContacts(false)
}

const statusCode = statistic.status?.code as keyof typeof StatisticStatus
const englishName = statistic.name_en ?? '-'
const division = formatDivision(statistic.division)
const regionLevels = statistic.statistic_region_levels ?? []
const mainLanguage = formatMainLanguage(statistic.main_language)
const startYear = formatStartYear(statistic.first_released_at)
const contacts = formatContacts(statistic.contacts)
const mockContinuedBy = ['putegjeld', 'k2', 'k3']
const variants = statistic.variants ?? []
const cancelledVariants = formatCancelledVariants(variants)
Expand Down Expand Up @@ -211,20 +247,41 @@ export default function ShowStatistic() {
</div>

<div className='show-statistic-contacts-container'>
<Heading data-size='xs'>Kontaktpersoner</Heading>
<Paragraph>Kontaktpersoner kan endres uten godkjenning</Paragraph>
{contacts.map((contact) => (
<Paragraph key={contact}>{contact}</Paragraph>
))}
{!auth?.isAdmin && (
<Button
variant='tertiary'
className='edit-contact-button'
onClick={() => alert('Rediger kontakter er ikke implementert ennå.')}
>
<PersonPencilIcon /> Rediger kontakt
</Button>
)}
<div className='show-statistic-contacts-heading'>
<Heading data-size='xs'>Kontaktpersoner</Heading>
{!auth?.isAdmin && !isEditingContacts && (
<Button
variant='tertiary'
data-size='sm'
aria-label='Rediger kontakter'
onClick={() => setIsEditingContacts(true)}
>
<PencilWritingIcon aria-hidden />
</Button>
)}
</div>
<Paragraph>Navn vises under overskriften 'Kontakt' på statistikksiden på ssb.no</Paragraph>
<div className='show-statistic-contacts-content'>
{!isEditingContacts &&
statistic.contacts?.map((contact) => (
<Paragraph key={contact.principalName}>
<Link href='#' onClick={() => alert('Kontaktside ikke implementert')}>
{formatContact(contact)}
</Link>
</Paragraph>
))}
{isEditingContacts && (
<>
<ContactSelection contacts={allContacts} selected={selectedContacts} setSelected={setSelectedContacts} />
<div className='show-statistic-contacts-button-wrapper'>
<Button onClick={saveContacts}>Lagre</Button>
<Button variant='tertiary' onClick={() => setIsEditingContacts(false)}>
Avbryt
</Button>
</div>
</>
)}
</div>
</div>

<div>
Expand Down
20 changes: 14 additions & 6 deletions shared/openapi/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ components:
application/json:
schema:
type: array
items:
items:
$ref: '#/components/schemas/Blocked_release_date'

schemas:
Expand Down Expand Up @@ -133,6 +133,8 @@ components:

Contact:
type: object
required:
- principalName
properties:
name: { type: string, readOnly: true }
principalName: { type: string, readOnly: true }
Expand Down Expand Up @@ -257,7 +259,11 @@ components:
status:
type: object
properties:
code: { type: string, description: "Is one of K (Kommende), A (Aktiv), IA (Ikke-aktiv), UT (Opphørt), SA (Sammenslått) or SP (Splittet)" }
code:
{
type: string,
description: 'Is one of K (Kommende), A (Aktiv), IA (Ikke-aktiv), UT (Opphørt), SA (Sammenslått) or SP (Splittet)',
}
name: { type: string }
name_en: { type: string }
approval_status: { type: string, description: " Is one of 'GODKJENT', 'AVVIST' or 'FORSLAG'" }
Expand Down Expand Up @@ -349,7 +355,7 @@ components:
created_at: { type: string, format: date-time, readOnly: true }
variants:
type: array
items:
items:
$ref: '#/components/schemas/Variant'
contacts:
type: array
Expand Down Expand Up @@ -400,7 +406,7 @@ paths:
application/json:
schema:
type: array
items:
items:
$ref: '#/components/schemas/Contact'
default:
$ref: '#/components/responses/Error'
Expand Down Expand Up @@ -478,7 +484,7 @@ paths:
type: integer
statistics:
type: array
items:
items:
$ref: '#/components/schemas/Statistic_listing'
default:
$ref: '#/components/responses/Error'
Expand Down Expand Up @@ -693,6 +699,8 @@ paths:
schema:
type: array
items: { $ref: '#/components/schemas/Contact' }
default:
$ref: '#/components/responses/Error'

/releases:
get:
Expand Down Expand Up @@ -731,7 +739,7 @@ paths:
type: integer
releases:
type: array
items:
items:
$ref: '#/components/schemas/Release_listing'
default:
$ref: '#/components/responses/Error'
Expand Down
3 changes: 2 additions & 1 deletion shared/src/api-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,7 @@ export interface paths {
'application/json': components['schemas']['Contact'][]
}
}
default: components['responses']['Error']
}
}
post?: never
Expand Down Expand Up @@ -1046,7 +1047,7 @@ export interface components {
}
Contact: {
readonly name?: string
readonly principalName?: string
readonly principalName: string
}
Division: {
code?: string
Expand Down