Skip to content
Open
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
3 changes: 2 additions & 1 deletion components/Buttons/EditButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { BrandedButton, throwOnNever } from '@datagouv/components-next'
import { RiEdit2Line } from '@remixicon/vue'

const props = defineProps<{
type: 'organizations' | 'users' | 'posts' | 'reuses' | 'dataservices' | 'datasets'
type: 'organizations' | 'users' | 'posts' | 'reuses' | 'dataservices' | 'datasets' | 'topics'
id: string
}>()

Expand All @@ -31,6 +31,7 @@ const link = computed(() => {
case 'reuses':
case 'dataservices':
case 'datasets':
case 'topics':
return base
default:
return throwOnNever(props.type as never, t('Aucun autre type défini'))
Expand Down
142 changes: 142 additions & 0 deletions pages/topics/[id].vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<template>
<div>
<div class="container">
<div
v-if="topic"
class="mt-4 flex gap-4 flex-wrap md:flex-nowrap items-center justify-between"
>
<Breadcrumb class="md:my-0">
<BreadcrumbItem to="/">
{{ $t('Accueil') }}
</BreadcrumbItem>
<BreadcrumbItem>
{{ topic.name }}
</BreadcrumbItem>
</Breadcrumb>
<EditButton
v-if="isMeAdmin()"
:id="topic.id"
type="topics"
/>
</div>
</div>

<LoadingBlock
v-if="topic"
v-slot="{ data: topic }"
:status
:data="topic"
>
<div class="container py-10 min-h-32">
<header class="space-y-3">
<div
v-if="topic.organization"
class="flex gap-2 items-center"
>
<div class="bg-white p-1 rounded-xs border border-gray-default object-contain">
<OrganizationLogo
:organization="topic.organization"
size-class="size-8"
/>
</div>
<CdataLink
class="link block"
:to="topic.organization.page"
>
<OrganizationNameWithCertificate
:organization="topic.organization"
as="h2"
/>
</CdataLink>
</div>
<div
v-else-if="topic.owner"
class="flex gap-2 items-center"
>
<Avatar
:user="topic.owner"
:size="24"
:rounded="true"
/>
<CdataLink
class="link block"
:to="topic.owner.page"
>
{{ topic.owner.first_name }} {{ topic.owner.last_name }}
</CdataLink>
</div>

<h1 class="text-2xl font-extrabold text-gray-title mb-0">
{{ topic.name }}
</h1>

<p class="text-sm text-gray-medium m-0">
{{ $t('Mis à jour {date}', { date: formatDate(topic.last_modified) }) }}
</p>

<div
v-if="topic.tags.length"
class="flex flex-wrap gap-0.5"
>
<span
v-for="tag in topic.tags"
:key="tag"
class="text-xs px-2 py-1 rounded-xl bg-gray-default"
>
{{ tag }}
</span>
</div>
</header>
</div>

<FullPageTabs
:links="[
{ label: $t('Description'), href: `/topics/${route.params.id}` },
{ label: $t('Discussions'), href: `/topics/${route.params.id}/discussions`, count: discussionsCount },
]"
/>
<div
id="page"
class="bg-white pt-5 pb-8"
>
<NuxtPage
class="container"
:topic
/>
</div>
</LoadingBlock>
</div>
</template>

<script setup lang="ts">
import { Avatar, getDescriptionShort, LoadingBlock, OrganizationLogo, OrganizationNameWithCertificate, useFormatDate, type TopicV2 } from '@datagouv/components-next'
import BreadcrumbItem from '~/components/Breadcrumbs/BreadcrumbItem.vue'
import EditButton from '~/components/Buttons/EditButton.vue'
import type { Thread } from '~/types/discussions'
import type { PaginatedArray } from '~/types/types'

definePageMeta({
keepScroll: true,
})

const route = useRoute()
const config = useRuntimeConfig()

const { formatDate } = useFormatDate()

const url = computed(() => `/api/2/topics/${route.params.id}/`)
const { data: topic, status } = await useAPI<TopicV2>(url, { redirectOn404: true, redirectOnSlug: 'id' })

const { data: discussions } = await useAPI<PaginatedArray<Thread>>('/api/1/discussions/', {
query: { for: topic.value?.id, page_size: 1 },
})
const discussionsCount = computed(() => discussions.value?.total ?? 0)

const title = computed(() => `${topic.value?.name} | ${config.public.title}`)
const description = computed(() => topic.value ? getDescriptionShort(topic.value) : '')

useSeoMeta({
title,
description,
})
</script>
16 changes: 16 additions & 0 deletions pages/topics/[id]/discussions.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<template>
<DiscussionsList
:subject="topic"
type="Topic"
/>
</template>

<script setup lang="ts">
import type { TopicV2 } from '@datagouv/components-next'

defineProps<{
topic: TopicV2
}>()

useSeoMeta({ robots: 'noindex' })
</script>
93 changes: 93 additions & 0 deletions pages/topics/[id]/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<template>
<div class="space-y-8">
<MarkdownViewer
v-if="topic.description"
class="w-full min-w-0"
:content="topic.description"
:min-heading="3"
/>

<section v-if="datasets && datasets.total">
<h2 class="uppercase text-sm mb-2.5">
{{ $t('aucun jeu de données associé | {n} jeu de données associé | {n} jeux de données associés', { n: datasets.total }) }}
</h2>
<div
class="grid gap-5"
:class="{ 'lg:grid-cols-2': datasets.total > 1 }"
>
<DatasetCardLg
v-for="dataset in datasets.data"
:key="dataset.id"
:dataset="dataset"
:show-description="datasets.total === 1"
class="m-0 min-w-0"
/>
</div>
<Pagination
class="mt-4"
:page="datasetsPage"
:page-size="datasetsPageSize"
:total-results="datasets.total"
@change="(changedPage: number) => datasetsPage = changedPage"
/>
</section>

<section v-if="reuses && reuses.total">
<h2 class="uppercase text-sm mb-2.5">
{{ $t('aucune réutilisation associée | {n} réutilisation associée | {n} réutilisations associées', { n: reuses.total }) }}
</h2>
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<ReuseCard
v-for="reuse in reuses.data"
:key="reuse.id"
class="min-w-0"
:reuse="reuse"
/>
</div>
<Pagination
class="mt-4"
:page="reusesPage"
:page-size="reusesPageSize"
:total-results="reuses.total"
@change="(changedPage: number) => reusesPage = changedPage"
/>
</section>
</div>
</template>

<script setup lang="ts">
import { MarkdownViewer, Pagination, type DatasetV2, type Reuse, type TopicV2 } from '@datagouv/components-next'
import ReuseCard from '~/components/Reuses/ReuseCard.vue'
import type { PaginatedArray } from '~/types/types'

const props = defineProps<{
topic: TopicV2
}>()

const datasetsPage = ref(1)
const datasetsPageSize = ref(10)
const datasetsQuery = computed(() => ({
page: datasetsPage.value,
page_size: datasetsPageSize.value,
topic: props.topic.id,
}))
const { data: datasets } = await useAPI<PaginatedArray<DatasetV2>>('/api/2/datasets/', { query: datasetsQuery })
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

datasets use api/2/ list endopint when reuses use search endpoint? Shouldn't we use the same logic between the two?
We could also use api/2/topics/<topic_id>/elements/?class=<Reuse,Dataset>?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think TopicElement will give us more information yes. But do we want to use this specific endpoint if we only show the cards?

Will look into why search vs index

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we don't want to use index for reuse since it's slow… But we could maybe wait for opendatateam/udata#3800 to switch from search to index v2 for reuse… Not sure we want to use /elements if we don't use the TopicElement info?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's a bit early for showing elements info so we can keep this strategy, with reuse search for now.
Maybe add a comment so that we update later on when we have reuse v2 or we finally use elements?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 15216c6


const reusesPage = ref(1)
const reusesPageSize = ref(9)
const reusesQuery = computed(() => ({
page: reusesPage.value,
page_size: reusesPageSize.value,
topic: props.topic.id,
}))
// We use the search endpoint for reuses because there is no v2 reuses index
// filtered by topic yet. Once udata#3800 is merged we can switch to the faster
// v2 index, like we already do for datasets above.
// If we ever need the TopicElement data (title/description/extras attached to
// the element), we could instead query /api/2/topics/{id}/elements/ for both
// datasets and reuses.
const { data: reuses } = await useAPI<PaginatedArray<Reuse>>('/api/2/reuses/search/', {
headers: { 'X-Fields': reusesXFields },
query: reusesQuery,
})
</script>
4 changes: 2 additions & 2 deletions types/discussions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Dataservice, DatasetV2, OrganizationReference, Reuse, UserReference } from '@datagouv/components-next'
import type { Dataservice, DatasetV2, OrganizationReference, Reuse, TopicV2, UserReference } from '@datagouv/components-next'
import type { Post } from './posts'

export type DiscussionSortedBy = 'title' | 'created' | 'closed'
Expand All @@ -12,7 +12,7 @@ export type Subject = {
class: string
}

export type DiscussionSubjectTypes = Dataservice | DatasetV2 | Reuse | Post
export type DiscussionSubjectTypes = Dataservice | DatasetV2 | Reuse | Post | TopicV2

export type DiscussionSubject = {
class: 'Dataservice' | 'Dataset' | 'Reuse' | 'Post' | 'Topic' | 'Organization'
Expand Down
10 changes: 10 additions & 0 deletions utils/discussions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ export function getSubjectTitle(subject: DiscussionSubjectTypes) {
if ('title' in subject) {
return subject.title
}
if ('name' in subject) {
return subject.name
}

return throwOnNever(subject as never, `Unknown type ${subject}`)
};
Expand All @@ -31,6 +34,13 @@ export function getSubjectPage(subject: DiscussionSubjectTypes) {
if (subject === null) {
return ''
}
// TODO: remove once udata#3765 is merged. Until then the topic API doesn't
// return `page`, so the `'page' in subject` check below fails at runtime and
// we'd hit throwOnNever. Matching on `elements` (always present on topics)
// avoids the crash while the field is missing.
if ('elements' in subject) {
return subject.page
}
Comment thread
ThibaudDauce marked this conversation as resolved.
if ('page' in subject) {
return subject.page
}
Expand Down
Loading