@datagouv/components-next 1.0.2-dev.235 → 1.0.2-dev.237

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,29 +1,49 @@
1
1
  <template>
2
2
  <span class="inline-flex items-center space-x-1">
3
- <Avatar
4
- v-bind="$attrs"
5
- :user="user"
6
- :size
7
- :rounded="true"
8
- />
9
- <component
10
- :is="withLink ? AppLink : 'span'"
11
- :to="withLink ? user.page : undefined"
12
- class="truncate"
13
- :class="{ 'font-bold': withLink }"
14
- >
15
- {{ user.first_name }}
16
- {{ user.last_name }}
17
- </component>
3
+ <!-- A deleted account has no name left to show and no page left to link to, so it
4
+ gets a neutral mark instead of the identicon generated from its identifier. -->
5
+ <template v-if="isDeleted">
6
+ <span
7
+ class="inline-flex flex-none items-center justify-center rounded-full border border-gray-default text-gray-medium"
8
+ :style="{ width: `${size}px`, height: `${size}px` }"
9
+ >
10
+ <RiUserLine
11
+ :size="String(Math.round(size * 0.6))"
12
+ aria-hidden="true"
13
+ />
14
+ </span>
15
+ <span class="rounded bg-gray-some px-1.5 py-0.5 text-[11px] leading-5 text-gray-medium">{{ t('Compte supprimé') }}</span>
16
+ </template>
17
+ <template v-else>
18
+ <Avatar
19
+ v-bind="$attrs"
20
+ :user="user"
21
+ :size
22
+ :rounded="true"
23
+ />
24
+ <component
25
+ :is="withLink ? AppLink : 'span'"
26
+ :to="withLink ? user.page : undefined"
27
+ class="truncate"
28
+ :class="{ 'font-bold': withLink }"
29
+ >
30
+ {{ user.first_name }}
31
+ {{ user.last_name }}
32
+ </component>
33
+ </template>
18
34
  </span>
19
35
  </template>
20
36
 
21
37
  <script setup lang="ts">
38
+ import { RiUserLine } from '@remixicon/vue'
39
+ import { computed } from 'vue'
40
+ import { useTranslation } from '../composables/useTranslation'
41
+ import { isDeletedUser } from '../functions/users'
22
42
  import type { User, UserReference } from '../types/users'
23
43
  import AppLink from './AppLink.vue'
24
44
  import Avatar from './Avatar.vue'
25
45
 
26
- withDefaults(defineProps<{
46
+ const props = withDefaults(defineProps<{
27
47
  user: User | UserReference
28
48
  size?: number
29
49
  withLink?: boolean
@@ -31,4 +51,8 @@ withDefaults(defineProps<{
31
51
  size: 12,
32
52
  withLink: true,
33
53
  })
54
+
55
+ const { t } = useTranslation()
56
+
57
+ const isDeleted = computed(() => isDeletedUser(props.user))
34
58
  </script>
@@ -4,7 +4,8 @@
4
4
  icon-only
5
5
  :icon="RiPencilLine"
6
6
  color="warning"
7
- :title="t('Éditer le fichier')"
7
+ :disabled="!!disabledReason"
8
+ :title="disabledReason ?? t('Éditer le fichier')"
8
9
  data-testid="edit-button"
9
10
  />
10
11
  </template>
@@ -19,10 +20,12 @@ type Props = {
19
20
  datasetId: string
20
21
  isCommunityResource?: boolean
21
22
  resourceId: string
23
+ disabledReason?: string | null
22
24
  }
23
25
 
24
26
  const props = withDefaults(defineProps<Props>(), {
25
27
  isCommunityResource: false,
28
+ disabledReason: null,
26
29
  })
27
30
 
28
31
  const { t } = useTranslation()
@@ -173,6 +173,7 @@
173
173
  :dataset-id="dataset.id"
174
174
  :resource-id="resource.id"
175
175
  :is-community-resource="isCommunityResource"
176
+ :disabled-reason="editDisabledReason"
176
177
  size="xs"
177
178
  />
178
179
  </p>
@@ -327,7 +328,7 @@ import { trackEvent } from '../../functions/matomo'
327
328
  import CopyButton from '../CopyButton.vue'
328
329
  import { useComponentsConfig } from '../../config'
329
330
  import { getOwnerName } from '../../functions/owned'
330
- import { getResourceFormatIcon, getResourceTitleId, detectOgcService, getResourceFilesize, isImagePreviewFormat, resolveResourceExternalUrl } from '../../functions/resources'
331
+ import { getResourceFormatIcon, getResourceTitleId, detectOgcService, getResourceFilesize, isGeopfSynced, isImagePreviewFormat, resolveResourceExternalUrl } from '../../functions/resources'
331
332
  import BrandedButton from '../BrandedButton.vue'
332
333
  import { useTranslation } from '../../composables/useTranslation'
333
334
  import { useHasTabularData } from '../../composables/useHasTabularData'
@@ -384,6 +385,10 @@ const hasPmtiles = computed(() => {
384
385
  return props.resource.extras['analysis:parsing:pmtiles_url'] || props.resource.format === 'pmtiles'
385
386
  })
386
387
 
388
+ const editDisabledReason = computed(() => {
389
+ return isGeopfSynced(props.resource) ? t('Vous ne pouvez pas modifier cette ressource car elle est synchronisée avec cartes.gouv.fr') : null
390
+ })
391
+
387
392
  const hasDatafairPreview = computed(() => {
388
393
  // Checks if there are the corresponding extras for a datafair preview.
389
394
  // Limited only to datasets published by certified organizations since it will load an iframe.
@@ -34,3 +34,114 @@ export function getActivityTranslation(activity: Activity): string {
34
34
 
35
35
  return translations[activity.key] || activity.label || activity.key
36
36
  }
37
+
38
+ /**
39
+ * `changes` holds the model attribute paths the backend recorded, which are neither
40
+ * translated nor meant to be read as is.
41
+ */
42
+ function getChangeLabels(): Record<string, string> {
43
+ const { t } = useTranslation()
44
+
45
+ return {
46
+ // Shared across the models an activity can be about
47
+ 'title': t('Titre'),
48
+ 'name': t('Nom'),
49
+ 'acronym': t('Sigle'),
50
+ 'slug': t('Identifiant d\'URL'),
51
+ 'description': t('Description'),
52
+ 'description_short': t('Description courte'),
53
+ 'tags': t('Mots-clés'),
54
+ 'license': t('Licence'),
55
+ 'private': t('Visibilité'),
56
+ 'featured': t('Mise en avant'),
57
+ 'badges': t('Badges'),
58
+ 'owner': t('Propriétaire'),
59
+ 'organization': t('Organisation'),
60
+ 'archived': t('Archivage'),
61
+ 'archived_at': t('Archivage'),
62
+ 'archived_reason': t('Motif d\'archivage'),
63
+ 'deleted': t('Suppression'),
64
+ 'deleted_at': t('Suppression'),
65
+ 'extras': t('Données supplémentaires'),
66
+ 'url': t('URL'),
67
+ 'created_at': t('Date de création'),
68
+ 'created_at_internal': t('Date de création'),
69
+ 'last_modified_internal': t('Date de modification'),
70
+ 'spatial': t('Couverture géographique'),
71
+ 'spatial.zones': t('Couverture géographique (zones)'),
72
+ 'spatial.geom': t('Couverture géographique (tracé)'),
73
+ 'spatial.granularity': t('Granularité géographique'),
74
+ 'contact_points': t('Points de contact'),
75
+
76
+ // Dataset
77
+ 'frequency': t('Fréquence de mise à jour'),
78
+ 'frequency_date': t('Date de prochaine mise à jour'),
79
+ 'temporal_coverage': t('Couverture temporelle'),
80
+ 'schema': t('Schéma'),
81
+ 'last_update': t('Date de dernière mise à jour'),
82
+ 'access_type': t('Type d\'accès'),
83
+ 'access_audiences': t('Publics concernés'),
84
+ 'access_type_reason_category': t('Motif de restriction d\'accès'),
85
+
86
+ // Reuse
87
+ 'type': t('Type'),
88
+ 'image': t('Image'),
89
+ 'datasets': t('Jeux de données'),
90
+ 'dataservices': t('Services de données'),
91
+ 'topic': t('Thématique'),
92
+
93
+ // Organization
94
+ 'image_url': t('Logo'),
95
+ 'logo': t('Logo'),
96
+ 'business_number_id': t('Numéro SIRET'),
97
+ 'presentation_blocs': t('Blocs de présentation'),
98
+ 'presentation_blocs_published_at': t('Publication des blocs de présentation'),
99
+ 'members': t('Membres'),
100
+ 'teams': t('Équipes'),
101
+ 'zone': t('Zone'),
102
+
103
+ // Dataservice
104
+ 'base_api_url': t('URL de base de l\'API'),
105
+ 'machine_documentation_url': t('Documentation machine'),
106
+ 'technical_documentation_url': t('Documentation technique'),
107
+ 'business_documentation_url': t('Documentation fonctionnelle'),
108
+ 'rate_limiting': t('Limitation de débit'),
109
+ 'rate_limiting_url': t('URL de la limitation de débit'),
110
+ 'availability': t('Disponibilité'),
111
+ 'availability_url': t('URL de la disponibilité'),
112
+ 'format': t('Format'),
113
+ 'uri': t('URI'),
114
+ 'issued_at': t('Date de publication'),
115
+ 'modified_at': t('Date de modification'),
116
+ 'metadata_modified_at': t('Date de modification des métadonnées'),
117
+ 'backend': t('Moissonnage : connecteur'),
118
+ 'domain': t('Moissonnage : domaine'),
119
+ 'source_id': t('Moissonnage : identifiant de la source'),
120
+ 'source_url': t('Moissonnage : URL de la source'),
121
+ 'remote_id': t('Moissonnage : identifiant distant'),
122
+ 'remote_url': t('Moissonnage : URL distante'),
123
+
124
+ // Topic
125
+ 'color': t('Couleur'),
126
+ 'elements': t('Éléments'),
127
+ }
128
+ }
129
+
130
+ /**
131
+ * The same attribute name can mean different things depending on what the activity is
132
+ * about, `related_to_kind` being what tells them apart.
133
+ */
134
+ const CHANGE_LABELS_BY_KIND: Record<string, Record<string, string>> = {
135
+ Organization: { url: 'Site web' },
136
+ }
137
+
138
+ export function getActivityChangeLabel(activity: Activity, change: string): string {
139
+ const { t } = useTranslation()
140
+ const perKind = CHANGE_LABELS_BY_KIND[activity.related_to_kind]?.[change]
141
+ if (perKind) return t(perKind)
142
+
143
+ const labels = getChangeLabels()
144
+ // An embedded attribute the backend reports on its own (`spatial.zones`) falls back
145
+ // to the label of the attribute holding it rather than to its raw path.
146
+ return labels[change] ?? labels[change.split('.')[0]!] ?? change
147
+ }
@@ -179,6 +179,10 @@ export function getResourceExternalUrl(dataset: Dataset | DatasetV2 | Omit<Datas
179
179
  return `${dataset.page}${isCommunityResource(resource) ? '/community-resources' : ''}?resource_id=${resource.id}`
180
180
  }
181
181
 
182
+ export function isGeopfSynced(resource: Resource | CommunityResource): boolean {
183
+ return resource.geopf?.push_status != null || typeof resource.geopf?.offering_id === 'string'
184
+ }
185
+
182
186
  export function resolveResourceExternalUrl<R extends Resource | CommunityResource>(
183
187
  dataset: Dataset | DatasetV2 | Omit<Dataset, 'resources' | 'community_resources'>,
184
188
  resource: R,
@@ -1,9 +1,18 @@
1
- import type { UserReference } from '../types/users'
1
+ import type { User, UserReference } from '../types/users'
2
2
  import { useComponentsConfig } from '../config'
3
3
 
4
4
  // Accept UserReference or UserSuggest (which has avatar_url instead of avatar_thumbnail)
5
5
  type UserWithAvatar = UserReference | (Pick<UserReference, 'id'> & { avatar_url?: string | null })
6
6
 
7
+ /**
8
+ * Deleting an account anonymises it in place: the names become `DELETED` and the slug
9
+ * `deleted-<id>`. The slug is what identifies it, being unique and out of the user's
10
+ * reach, where someone may legitimately be named DELETED.
11
+ */
12
+ export function isDeletedUser(user: User | UserReference): boolean {
13
+ return user.slug.startsWith('deleted-')
14
+ }
15
+
7
16
  export function useGetUserAvatar() {
8
17
  const config = useComponentsConfig()
9
18
  return (user: UserWithAvatar, size: number) => {
package/src/main.ts CHANGED
@@ -30,6 +30,7 @@ import { getDefaultDatasetConfig, getDefaultDataserviceConfig, getDefaultReuseCo
30
30
  import { useSearchFilter } from './composables/useSearchFilter'
31
31
  import type { UseSearchFilterOptions } from './composables/useSearchFilter'
32
32
 
33
+ import ActivityActor from './components/ActivityList/ActivityActor.vue'
33
34
  import ActivityList from './components/ActivityList/ActivityList.vue'
34
35
  import UserActivityList from './components/ActivityList/UserActivityList.vue'
35
36
  import AnimatedLoader from './components/AnimatedLoader.vue'
@@ -341,6 +342,7 @@ const datagouv: Plugin<PluginConfig> = {
341
342
  export {
342
343
  datagouv,
343
344
  useComponentsConfig,
345
+ ActivityActor,
344
346
  ActivityList,
345
347
  AnimatedLoader,
346
348
  AppLink,
@@ -1,5 +1,5 @@
1
- import type { Organization } from './organizations'
2
- import type { User } from './users'
1
+ import type { OrganizationReference } from './organizations'
2
+ import type { UserReference } from './users'
3
3
 
4
4
  export type ActivityKey = 'dataset:created' | 'dataset:updated' | 'dataset:deleted' | 'dataset:discussed' | 'dataset:followed'
5
5
  | 'dataset:resource:added' | 'dataset:resource:updated' | 'dataset:resource:deleted'
@@ -8,9 +8,29 @@ export type ActivityKey = 'dataset:created' | 'dataset:updated' | 'dataset:delet
8
8
  | 'reuse:created' | 'reuse:updated' | 'reuse:deleted' | 'reuse:discussed' | 'reuse:followed'
9
9
  | 'user:followed' | 'topic:created' | 'topic:updated'
10
10
 
11
+ /**
12
+ * Who an action is attributed to. Not necessarily a person: harvesting and API tokens
13
+ * act on their own behalf. `class` is the discriminator every API reference carries.
14
+ */
15
+ export type ActivityActor
16
+ = | UserReference
17
+ | { class: 'HarvestSource', id: string, name: string }
18
+ | { class: 'ApiToken', id: string, name: string }
19
+
20
+ export type ActivityExtras = {
21
+ /** Set on the `dataset:resource:*` keys. */
22
+ resource_id?: string
23
+ /**
24
+ * Copied when the activity is emitted, so a resource removed or renamed since still
25
+ * reads under the name it had that day. Absent from the activities recorded before
26
+ * the backend started storing it.
27
+ */
28
+ resource_title?: string
29
+ }
30
+
11
31
  export type Activity = {
12
- actor: User
13
- organization: Organization | null
32
+ actor: ActivityActor
33
+ organization: OrganizationReference | null
14
34
  related_to: string
15
35
  related_to_id: string
16
36
  related_to_kind: string
@@ -19,6 +39,6 @@ export type Activity = {
19
39
  label: string
20
40
  key: ActivityKey
21
41
  icon: string
22
- extras: Record<string, unknown>
42
+ extras: ActivityExtras
23
43
  changes?: Array<string>
24
44
  }
@@ -12,6 +12,11 @@ export type FileResourceFileType = 'file'
12
12
 
13
13
  export type ResourceFileType = RemoteResourceFileType | FileResourceFileType
14
14
 
15
+ export type GeopfResourceMetadata = {
16
+ push_status?: 'pending' | 'done' | 'error' | 'timeout' | null
17
+ offering_id?: string | null
18
+ }
19
+
15
20
  export type Resource = {
16
21
  id: string
17
22
  title: string
@@ -22,6 +27,7 @@ export type Resource = {
22
27
  extras: Record<string, unknown>
23
28
  metrics: { views: number }
24
29
  harvest: Harvest
30
+ geopf: GeopfResourceMetadata | null
25
31
  filesize: number | null
26
32
  filetype: ResourceFileType
27
33
  format: string