Skip to content

Commit d574ec5

Browse files
feat: add certified notification (#863)
1 parent 15d21e2 commit d574ec5

30 files changed

Lines changed: 261 additions & 84 deletions

‎components/AdminSidebar/AdminSidebarMenu/AdminSidebarMenu.vue‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@
199199

200200
<script setup lang="ts">
201201
import { Avatar, OrganizationLogo } from '@datagouv/components-next'
202-
import type { Organization, User } from '@datagouv/components-next'
202+
import type { OrganizationReference, User } from '@datagouv/components-next'
203203
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'
204204
import { RiAlarmWarningLine, RiArticleLine, RiAwardLine, RiBarChartBoxLine, RiBookShelfLine, RiBuilding2Line, RiChat3Line, RiDatabase2Line, RiGitPullRequestLine, RiGroup3Line, RiLineChartLine, RiParentLine, RiPlanetLine, RiRobot2Line, RiServerLine, RiUserLine } from '@remixicon/vue'
205205
import { key, type AccordionRegister } from '~/components/Accordion/injectionKey'
@@ -213,7 +213,7 @@ const props = defineProps<{
213213
/**
214214
* An organization, to show a menu with its logo and name
215215
*/
216-
organization?: Organization
216+
organization?: OrganizationReference
217217
}>()
218218
219219
defineEmits<{

‎components/Dataservices/DescribeDataservice.vue‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,7 @@ const availabilityDataserviceAccordionId = useId()
548548
const contactPointAccordionId = useId()
549549
550550
const ownedOptions = computed<Array<Owned>>(() => {
551-
return [...user.value.organizations.map(organization => ({ organization, owner: null })), { owner: user.value, organization: null }]
551+
return [...user.value.organizations.map(organization => ({ organization, owner: null })), { owner: { ...user.value, class: 'User' as const }, organization: null }]
552552
})
553553
554554
const machineDocumentationUrlWarningMessage = t(`Il est fortement recommandé d'ajouter une documentation OpenAPI ou Swagger à votre API.`)

‎components/Notifications/MembershipRequest.vue‎

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<div class="flex-none">
44
<RiUserAddLine class="size-4" />
55
</div>
6-
<div class="flex-1">
6+
<div class="flex-1 truncate">
77
<p class="m-0 text-xs font-bold">
88
<NuxtLink
99
class="after:absolute after:inset-0 bg-none"
@@ -13,10 +13,22 @@
1313
</NuxtLink>
1414
</p>
1515
<p class="m-0 text-xs">
16-
{{ $t('de {name}', { name: `${notification.details.request_user.first_name} ${notification.details.request_user.last_name}` }) }}
16+
{{ $t('de') }}
17+
<AvatarWithName
18+
:user="notification.details.request_user"
19+
:with-link="false"
20+
/>
1721
</p>
18-
<p class="m-0 text-xs">
19-
{{ $t('à {org}', { org: notification.details.request_organization.name }) }}
22+
<p class="flex items-center gap-1 m-0 text-xs">
23+
{{ $t('à') }}
24+
<OrganizationOwner
25+
:organization="notification.details.request_organization as OrganizationReference"
26+
logo-size-class="size-3"
27+
:logo-no-border="true"
28+
name-size="xs"
29+
name-color="text-gray-plain"
30+
:with-link="false"
31+
/>
2032
</p>
2133
</div>
2234
<div class="flex-none flex m-0 gap-1.5">
@@ -32,7 +44,7 @@
3244
</template>
3345

3446
<script setup lang="ts">
35-
import { useFormatDate } from '@datagouv/components-next'
47+
import { AvatarWithName, useFormatDate, type OrganizationReference } from '@datagouv/components-next'
3648
import { RiUserAddLine } from '@remixicon/vue'
3749
import type { DeepReadonly } from 'vue'
3850
import type { MembershipRequestNotification } from '~/types/notifications'
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<template>
2+
<div class="p-3 flex gap-3 relative hover:bg-gray-some">
3+
<div class="flex-none">
4+
<RiCheckboxCircleLine class="size-4" />
5+
</div>
6+
<div class="flex-1 truncate">
7+
<p class="m-0 text-xs font-bold">
8+
{{ $t(`Votre organisation a été {badge}`, { badge }) }}
9+
</p>
10+
<OrganizationOwner
11+
:organization="notification.details.organization as OrganizationReference"
12+
logo-size-class="size-3"
13+
:logo-no-border="true"
14+
name-size="xs"
15+
name-color="text-gray-plain"
16+
:with-link="false"
17+
/>
18+
</div>
19+
<div class="flex-none flex m-0 gap-1.5">
20+
<p class="m-0 text-xs">
21+
{{ formatDate(notification.created_at) }}
22+
</p>
23+
<AnimatedLoader
24+
v-if="loading"
25+
class="size-2"
26+
/>
27+
<div
28+
v-else-if="!notification.handled_at"
29+
class="size-2 rounded-full bg-new-primary mt-0.5"
30+
/>
31+
</div>
32+
<button
33+
v-if="!notification.handled_at"
34+
class="after:absolute after:inset-0 bg-none"
35+
:title="$t('Marquer la notification comme lue')"
36+
@click="markAsRead"
37+
/>
38+
</div>
39+
</template>
40+
41+
<script setup lang="ts">
42+
import { RiCheckboxCircleLine } from '@remixicon/vue'
43+
import type { DeepReadonly } from 'vue'
44+
import { AnimatedLoader, throwOnNever, useFormatDate } from '@datagouv/components-next'
45+
import type { OrganizationReference } from '@datagouv/components-next'
46+
import type { NewBadgeNotification } from '~/types/notifications'
47+
48+
const props = defineProps<{
49+
notification: DeepReadonly<NewBadgeNotification>
50+
}>()
51+
52+
const { refreshNotifications } = useNotifications()
53+
const { formatDate } = useFormatDate()
54+
const { $api } = useNuxtApp()
55+
const { t } = useTranslation()
56+
const loading = ref(false)
57+
58+
const badge = computed(() => {
59+
switch (props.notification.details.kind) {
60+
case 'certified':
61+
return t('certifiée')
62+
case 'association':
63+
return t('identifiée comme association')
64+
case 'company':
65+
return t('identifiée comme entreprise')
66+
case 'local-authority':
67+
return t('identifiée comme collectivité territoriale')
68+
case 'public-service':
69+
return t('identifiée comme service public')
70+
default:
71+
return throwOnNever(props.notification.details.kind, 'No other type')
72+
}
73+
})
74+
75+
async function markAsRead() {
76+
try {
77+
loading.value = true
78+
await $api(`/api/1/notifications/${props.notification.id}/read/`, { method: 'POST' })
79+
await refreshNotifications()
80+
}
81+
finally {
82+
loading.value = false
83+
}
84+
}
85+
</script>

‎components/Notifications/NotificationsList.vue‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,17 @@
1212
v-if="notification.details.class === 'TransferRequestNotificationDetails'"
1313
:notification="notification as TransferRequestNotification"
1414
/>
15+
<NotificationNewBadge
16+
v-else-if="notification.details.class === 'NewBadgeNotificationDetails'"
17+
:notification="notification as NewBadgeNotification"
18+
/>
1519
</div>
1620
</div>
1721
</template>
1822
1923
<script setup lang="ts">
2024
import type { DeepReadonly } from 'vue'
21-
import type { MembershipRequestNotification, TransferRequestNotification, UserNotification } from '~/types/notifications'
25+
import type { MembershipRequestNotification, NewBadgeNotification, TransferRequestNotification, UserNotification } from '~/types/notifications'
2226

2327
defineProps<{
2428
notifications: DeepReadonly<Array<UserNotification>>

‎components/Notifications/TransferRequest.vue‎

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,44 @@
77
<p class="m-0 text-xs font-bold">
88
<NuxtLink
99
class="after:absolute after:inset-0 bg-none"
10-
:to="details.link"
10+
:to="link"
1111
>
1212
{{ $t(`Demande de transfert`) }}
1313
</NuxtLink>
1414
</p>
15-
<p class="m-0 text-xs">
16-
{{ $t('de {source}', { source: details.sourceName }) }}
15+
<p class="flex items-center gap-1 m-0 text-xs">
16+
{{ $t('de') }}
17+
<AvatarWithName
18+
v-if="notification.details.transfer_owner.class == 'User'"
19+
:user="notification.details.transfer_owner"
20+
:with-link="false"
21+
/>
22+
<OrganizationOwner
23+
v-else
24+
:organization="notification.details.transfer_owner as OrganizationReference"
25+
logo-size-class="size-3"
26+
:logo-no-border="true"
27+
name-size="xs"
28+
name-color="text-gray-plain"
29+
:with-link="false"
30+
/>
1731
</p>
18-
<p class="m-0 text-xs">
19-
{{ $t('à {recipient}', { recipient: details.recipientName }) }}
32+
<p class="flex items-center gap-1 m-0 text-xs">
33+
{{ $t('à') }}
34+
<AvatarWithName
35+
v-if="notification.details.transfer_recipient.class == 'User'"
36+
:user="notification.details.transfer_recipient"
37+
:with-link="false"
38+
/>
39+
<OrganizationOwner
40+
v-else
41+
:organization="notification.details.transfer_recipient as OrganizationReference"
42+
logo-size-class="size-3"
43+
:logo-no-border="true"
44+
name-size="xs"
45+
name-color="text-gray-plain"
46+
:with-link="false"
47+
/>
2048
</p>
2149
</div>
2250
<div class="flex-none flex m-0 gap-1.5">
@@ -32,7 +60,7 @@
3260
</template>
3361

3462
<script setup lang="ts">
35-
import { useFormatDate } from '@datagouv/components-next'
63+
import { AvatarWithName, useFormatDate, type OrganizationReference } from '@datagouv/components-next'
3664
import { RiSendPlaneLine } from '@remixicon/vue'
3765
import type { DeepReadonly } from 'vue'
3866
import type { TransferRequestNotification } from '~/types/notifications'
@@ -43,24 +71,18 @@ const props = defineProps<{
4371
4472
const user = useMe()
4573
46-
const details = computed(() => {
74+
const link = computed(() => {
4775
const recipient = props.notification.details.transfer_recipient
48-
const source = props.notification.details.transfer_owner
49-
const sourceName = 'first_name' in source ? `${source.first_name} ${source.last_name}` : source.name
5076
const subject = props.notification.details.transfer_subject
51-
const subjectPath = `${subject.class.toLowerCase()}s`
52-
if ('first_name' in recipient) {
53-
return {
54-
link: user.value.id === recipient.id ? `/admin/me/${subjectPath}` : `/admin/users/${recipient.id}/${subjectPath}`,
55-
recipientName: `${recipient.first_name} ${recipient.last_name}`,
56-
sourceName,
57-
}
58-
}
59-
return {
60-
link: `/admin/organizations/${recipient.id}/${subjectPath}`,
61-
recipientName: recipient.name,
62-
sourceName,
77+
const subjectPath = {
78+
Dataset: 'datasets',
79+
Reuse: 'reuses',
80+
Dataservice: 'dataservices',
81+
}[subject.class]
82+
if (recipient.class == 'User') {
83+
return user.value.id === recipient.id ? `/admin/me/${subjectPath}` : `/admin/users/${recipient.id}/${subjectPath}`
6384
}
85+
return `/admin/organizations/${recipient.id}/${subjectPath}`
6486
})
6587
6688
const { formatDate } = useFormatDate()

‎components/OrganizationOwner.vue‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,40 @@
66
:size-class="logoSizeClass"
77
/>
88
</div>
9-
<CdataLink
10-
:to="organization.page"
11-
class="link flex-1 min-w-0"
9+
<component
10+
:is="withLink ? CdataLink : 'span'"
11+
:to="withLink ? organization.page : undefined"
12+
class="flex-1 min-w-0"
13+
:class="{ 'link font-bold': withLink }"
1214
>
1315
<OrganizationNameWithCertificate
1416
class="w-full"
1517
:as
18+
:color-class="nameColor"
1619
:organization
20+
:size="nameSize"
1721
/>
18-
</CdataLink>
22+
</component>
1923
</div>
2024
</template>
2125

2226
<script setup lang="ts">
2327
import { OrganizationLogo, OrganizationNameWithCertificate, type Organization, type OrganizationReference } from '@datagouv/components-next'
28+
import CdataLink from './CdataLink.vue'
2429
2530
withDefaults(defineProps<{
2631
organization: Organization | OrganizationReference
2732
logoSizeClass?: string
33+
nameSize?: 'base' | 'sm' | 'xs'
34+
nameColor?: string
2835
logoNoBorder?: boolean
2936
as?: string
37+
withLink?: boolean
3038
}>(), {
3139
logoSizeClass: 'size-8',
3240
logoNoBorder: false,
41+
withLink: true,
42+
nameColor: 'text-new-primary',
43+
nameSize: 'base',
3344
})
3445
</script>

‎components/ProducerSelect.vue‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
</template>
5757

5858
<script setup lang="ts">
59-
import { useGetUserAvatar, BrandedButton, PaddedContainer, type Organization, type Owned, type User, OrganizationLogo } from '@datagouv/components-next'
59+
import { useGetUserAvatar, BrandedButton, PaddedContainer, OrganizationLogo, type Owned, type OrganizationReference, type UserReference } from '@datagouv/components-next'
6060
6161
const getUserAvatar = useGetUserAvatar()
6262
@@ -84,7 +84,7 @@ const ownedOptions = computed<Array<Owned>>(() => {
8484
if (props.organizationsOnly) {
8585
return organizations
8686
}
87-
return [...organizations, { owner: user.value, organization: null }]
87+
return [...organizations, { owner: { ...user.value, class: 'User' }, organization: null }]
8888
})
8989
9090
const hasOrganizations = computed(() => {
@@ -97,7 +97,7 @@ const suggest = computed(() => {
9797
return async (query: string) => {
9898
if (!query) return Promise.resolve(ownedOptions.value)
9999
100-
const organizations = await $api<Array<Organization>>('/api/1/organizations/suggest/', {
100+
const organizations = await $api<Array<OrganizationReference>>('/api/1/organizations/suggest/', {
101101
query: {
102102
q: query,
103103
size: 5,
@@ -108,7 +108,7 @@ const suggest = computed(() => {
108108
return organizations.map(organization => ({ organization, owner: null }))
109109
}
110110
111-
const users = await $api<Array<User>>('/api/1/users/suggest/', {
111+
const users = await $api<Array<UserReference>>('/api/1/users/suggest/', {
112112
query: {
113113
q: query,
114114
size: 5,

‎components/Reuses/DescribeReuse.vue‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ const { data: types } = await useAPI<Array<ReuseType>>('/api/1/reuses/types', {
377377
const { data: topics } = await useAPI<Array<ReuseTopic>>('/api/1/reuses/topics', { lazy: true })
378378
379379
const ownedOptions = computed<Array<Owned>>(() => {
380-
return [...user.value.organizations.map(organization => ({ organization, owner: null })), { owner: user.value, organization: null }]
380+
return [...user.value.organizations.map(organization => ({ organization, owner: null })), { owner: { ...user.value, class: 'User' }, organization: null }]
381381
})
382382
383383
const { form, touch, getFirstError, getFirstWarning, validate } = useForm(reuseForm, {

‎composables/useCurrentOwned.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Organization, User } from '@datagouv/components-next'
1+
import type { Organization, OrganizationReference, User } from '@datagouv/components-next'
22
import { keyBy } from 'lodash-es'
33

44
/**
@@ -8,11 +8,11 @@ import { keyBy } from 'lodash-es'
88
export function useCurrentOwnedSetters() {
99
const me = useMaybeMe()
1010

11-
const organizations = useState('organizations', () => keyBy(me.value?.organizations, org => org.id))
11+
const organizations = useState<Record<string, Organization | OrganizationReference>>('organizations', () => keyBy(me.value?.organizations, org => org.id))
1212
const users = useState<Record<string, User | Me>>('users', () => (me.value?.id ? { [me.value.id]: me.value } : {}))
1313
const currentOwnedId = useState<{ organization: string } | { user: string } | null>('currentOrganizationId', () => null)
1414

15-
const setCurrentOrganization = (organization: Organization) => {
15+
const setCurrentOrganization = (organization: Organization | OrganizationReference) => {
1616
if (currentOwnedId.value && 'organization' in currentOwnedId.value && currentOwnedId.value.organization === organization.id) return
1717
currentOwnedId.value = { organization: organization.id }
1818
organizations.value[organization.id] = organization

0 commit comments

Comments
 (0)