@datagouv/components-next 0.2.0 → 1.0.1

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.
Files changed (155) hide show
  1. package/README.md +1 -1
  2. package/assets/main.css +49 -22
  3. package/dist/Control-BNCDn-8E.js +148 -0
  4. package/dist/{Datafair.client-x39O4yfF.js → Datafair.client-B5lBpOl8.js} +2 -2
  5. package/dist/Event-BOgJUhNR.js +738 -0
  6. package/dist/Image-BN-4XkIn.js +247 -0
  7. package/dist/{JsonPreview.client-BMsC5JcY.js → JsonPreview.client-Doz1Z0BS.js} +23 -23
  8. package/dist/Map-BdT3i2C4.js +7609 -0
  9. package/dist/MapContainer.client-oiieO8H-.js +105 -0
  10. package/dist/OSM-CamriM9b.js +71 -0
  11. package/dist/PdfPreview.client-CdAhkDFJ.js +14513 -0
  12. package/dist/{Pmtiles.client-BaiIo4VZ.js → Pmtiles.client-B0v8tGJQ.js} +3 -3
  13. package/dist/ScaleLine-BiesrgOv.js +165 -0
  14. package/dist/Swagger.client-CsK65JnG.js +4 -0
  15. package/dist/Tile-DCuqwNOI.js +1206 -0
  16. package/dist/TileImage-CmZf8EdU.js +1067 -0
  17. package/dist/View-DcDc7N2K.js +2858 -0
  18. package/dist/{XmlPreview.client-CAdN0w_Y.js → XmlPreview.client-CrjHf74q.js} +17 -17
  19. package/dist/common-C4rDcQpp.js +243 -0
  20. package/dist/components-next.css +1 -1
  21. package/dist/components-next.js +158 -117
  22. package/dist/components.css +1 -1
  23. package/dist/{MapContainer.client-DeSo8EvG.js → index-Bbu9rOHt.js} +4975 -21416
  24. package/dist/leaflet-src-7m1mB8LI.js +6338 -0
  25. package/dist/{main-Dgri3TQL.js → main-CiH8ZmBI.js} +56973 -51462
  26. package/dist/proj-CKwYjU38.js +1569 -0
  27. package/dist/tilecoord-YW3qEH_j.js +884 -0
  28. package/dist/{vue3-xml-viewer.common-D6skc_Ai.js → vue3-xml-viewer.common-Bi_bsV6C.js} +1 -1
  29. package/package.json +6 -2
  30. package/src/components/ActivityList/ActivityList.vue +6 -2
  31. package/src/components/AppLink.vue +4 -1
  32. package/src/components/Avatar.vue +2 -2
  33. package/src/components/AvatarWithName.vue +8 -4
  34. package/src/components/BouncingDots.vue +21 -0
  35. package/src/components/BrandedButton.vue +2 -0
  36. package/src/components/CopyButton.vue +19 -7
  37. package/src/components/DataserviceCard.vue +85 -120
  38. package/src/components/DatasetCard.vue +110 -171
  39. package/src/components/DatasetInformation/DatasetEmbedSection.vue +43 -0
  40. package/src/components/DatasetInformation/DatasetInformationSection.vue +73 -0
  41. package/src/components/DatasetInformation/DatasetSchemaSection.vue +74 -0
  42. package/src/components/DatasetInformation/DatasetSpatialSection.vue +59 -0
  43. package/src/components/DatasetInformation/DatasetTemporalitySection.vue +45 -0
  44. package/src/components/DatasetInformation/index.ts +5 -0
  45. package/src/components/DatasetQuality.vue +23 -16
  46. package/src/components/DatasetQualityInline.vue +13 -17
  47. package/src/components/DatasetQualityScore.vue +12 -15
  48. package/src/components/DatasetQualityTooltipContent.vue +3 -3
  49. package/src/components/DescriptionList.vue +1 -4
  50. package/src/components/DescriptionListDetails.vue +5 -0
  51. package/src/components/DescriptionListTerm.vue +5 -0
  52. package/src/components/DiscussionMessageCard.vue +63 -0
  53. package/src/components/ExtraAccordion.vue +4 -4
  54. package/src/components/Form/BadgeSelect.vue +35 -0
  55. package/src/components/Form/FormatSelect.vue +28 -0
  56. package/src/components/Form/GeozoneSelect.vue +52 -0
  57. package/src/components/Form/GranularitySelect.vue +29 -0
  58. package/src/components/Form/LicenseSelect.vue +30 -0
  59. package/src/components/Form/OrganizationSelect.vue +62 -0
  60. package/src/components/Form/OrganizationTypeSelect.vue +34 -0
  61. package/src/components/Form/ReuseTopicSelect.vue +29 -0
  62. package/src/components/Form/SchemaSelect.vue +30 -0
  63. package/src/components/Form/SearchableSelect.vue +334 -0
  64. package/src/components/Form/SelectGroup.vue +132 -0
  65. package/src/components/Form/TagSelect.vue +38 -0
  66. package/src/components/LeafletMap.vue +31 -0
  67. package/src/components/LicenseBadge.vue +24 -0
  68. package/src/components/LoadingBlock.vue +23 -2
  69. package/src/components/MarkdownViewer.vue +3 -1
  70. package/src/components/ObjectCard.vue +42 -0
  71. package/src/components/ObjectCardBadge.vue +22 -0
  72. package/src/components/ObjectCardHeader.vue +35 -0
  73. package/src/components/ObjectCardOwner.vue +43 -0
  74. package/src/components/ObjectCardShortDescription.vue +28 -0
  75. package/src/components/OrganizationCard.vue +35 -20
  76. package/src/components/OrganizationHorizontalCard.vue +87 -0
  77. package/src/components/OrganizationLogo.vue +1 -1
  78. package/src/components/OrganizationNameWithCertificate.vue +12 -6
  79. package/src/components/OwnerTypeIcon.vue +1 -0
  80. package/src/components/Pagination.vue +1 -1
  81. package/src/components/Placeholder.vue +5 -2
  82. package/src/components/PostCard.vue +62 -0
  83. package/src/components/ProgressBar.vue +31 -0
  84. package/src/components/RadioGroup.vue +32 -0
  85. package/src/components/RadioInput.vue +64 -0
  86. package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
  87. package/src/components/ResourceAccordion/EditButton.vue +2 -3
  88. package/src/components/ResourceAccordion/JsonPreview.client.vue +3 -3
  89. package/src/components/ResourceAccordion/MapContainer.client.vue +21 -17
  90. package/src/components/ResourceAccordion/Metadata.vue +11 -24
  91. package/src/components/ResourceAccordion/PdfPreview.client.vue +70 -74
  92. package/src/components/ResourceAccordion/Pmtiles.client.vue +2 -2
  93. package/src/components/ResourceAccordion/Preview.vue +2 -2
  94. package/src/components/ResourceAccordion/ResourceAccordion.vue +35 -28
  95. package/src/components/ResourceAccordion/ResourceIcon.vue +1 -0
  96. package/src/components/ResourceAccordion/SchemaBadge.vue +2 -2
  97. package/src/components/ResourceAccordion/XmlPreview.client.vue +3 -3
  98. package/src/components/ResourceExplorer/ResourceExplorer.vue +243 -0
  99. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +116 -0
  100. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +410 -0
  101. package/src/components/ReuseCard.vue +8 -28
  102. package/src/components/ReuseHorizontalCard.vue +80 -0
  103. package/src/components/Search/BasicAndAdvancedFilters.vue +49 -0
  104. package/src/components/Search/Filter/AccessTypeFilter.vue +37 -0
  105. package/src/components/Search/Filter/DatasetBadgeFilter.vue +40 -0
  106. package/src/components/Search/Filter/FilterButtonGroup.vue +78 -0
  107. package/src/components/Search/Filter/FormatFamilyFilter.vue +39 -0
  108. package/src/components/Search/Filter/LastUpdateRangeFilter.vue +37 -0
  109. package/src/components/Search/Filter/ProducerTypeFilter.vue +49 -0
  110. package/src/components/Search/Filter/ReuseTypeFilter.vue +42 -0
  111. package/src/components/Search/GlobalSearch.vue +707 -0
  112. package/src/components/Search/SearchInput.vue +63 -0
  113. package/src/components/Search/Sidemenu.vue +38 -0
  114. package/src/components/StatBox.vue +5 -5
  115. package/src/components/Tag.vue +30 -0
  116. package/src/components/Toggletip.vue +11 -4
  117. package/src/components/Tooltip.vue +2 -3
  118. package/src/components/TopicCard.vue +134 -0
  119. package/src/components/radioGroupContext.ts +9 -0
  120. package/src/composables/useDebouncedRef.ts +31 -0
  121. package/src/composables/useHasTabularData.ts +15 -0
  122. package/src/composables/useMetrics.ts +4 -3
  123. package/src/composables/useResourceCapabilities.ts +131 -0
  124. package/src/composables/useRouteQueryBoolean.ts +10 -0
  125. package/src/composables/useSelectModelSync.ts +89 -0
  126. package/src/composables/useStableQueryParams.ts +84 -0
  127. package/src/composables/useTranslation.ts +2 -1
  128. package/src/config.ts +4 -0
  129. package/src/functions/api.ts +25 -6
  130. package/src/functions/api.types.ts +5 -3
  131. package/src/functions/datasets.ts +1 -29
  132. package/src/functions/description.ts +33 -0
  133. package/src/functions/helpers.ts +11 -0
  134. package/src/functions/markdown.ts +60 -16
  135. package/src/functions/metrics.ts +33 -0
  136. package/src/functions/organizations.ts +5 -5
  137. package/src/functions/resourceCapabilities.ts +55 -0
  138. package/src/main.ts +96 -7
  139. package/src/types/dataservices.ts +14 -12
  140. package/src/types/datasets.ts +20 -7
  141. package/src/types/discussions.ts +20 -0
  142. package/src/types/licenses.ts +3 -3
  143. package/src/types/organizations.ts +13 -1
  144. package/src/types/owned.ts +4 -2
  145. package/src/types/pages.ts +70 -0
  146. package/src/types/posts.ts +27 -0
  147. package/src/types/resources.ts +16 -0
  148. package/src/types/reuses.ts +14 -5
  149. package/src/types/search.ts +407 -0
  150. package/src/types/users.ts +12 -3
  151. package/dist/PdfPreview.client-COOkEkRA.js +0 -107
  152. package/dist/Swagger.client-CpLgaLg6.js +0 -4
  153. package/dist/pdf-vue3-IkJO65RH.js +0 -273
  154. package/dist/pdf.min-f72cfa08-CdgJTooZ.js +0 -9501
  155. package/src/components/DatasetInformationPanel.vue +0 -211
@@ -0,0 +1,84 @@
1
+ import { ref, watch, type Ref } from 'vue'
2
+ import type { SearchTypeConfig } from '../types/search'
3
+
4
+ type FilterRefs = Record<string, Ref<unknown>>
5
+
6
+ interface StableQueryParamsOptions {
7
+ typeConfig: SearchTypeConfig | undefined
8
+ allFilters: FilterRefs
9
+ q: Ref<string>
10
+ sort: Ref<string | undefined>
11
+ page: Ref<number>
12
+ pageSize: number
13
+ }
14
+
15
+ /**
16
+ * Creates a stable ref for query params that only updates when content actually changes.
17
+ * Applies hiddenFilters first, then user filters (which can override hiddenFilters).
18
+ */
19
+ export function useStableQueryParams(options: StableQueryParamsOptions) {
20
+ const { typeConfig, allFilters, q, sort, page, pageSize } = options
21
+ const stableParams = ref<Record<string, unknown>>({})
22
+
23
+ const buildParams = () => {
24
+ const params: Record<string, unknown> = {}
25
+
26
+ // 1. Apply hiddenFilters first (can be overridden by user filters)
27
+ if (typeConfig?.hiddenFilters) {
28
+ for (const hf of typeConfig.hiddenFilters) {
29
+ if (hf) {
30
+ params[hf.key as string] = hf.value
31
+ }
32
+ }
33
+ }
34
+
35
+ // 2. Get enabled filters for this type
36
+ const enabledFilters = [
37
+ ...(typeConfig?.basicFilters ?? []),
38
+ ...(typeConfig?.advancedFilters ?? []),
39
+ ]
40
+
41
+ // 3. Apply user filter values (only enabled ones)
42
+ // Skip undefined/null/empty values so they're not sent to the API
43
+ for (const filterName of enabledFilters) {
44
+ const filterRef = allFilters[filterName as string]
45
+ if (filterRef) {
46
+ const value = filterRef.value
47
+ if (value !== undefined && value !== '' && value !== null) {
48
+ params[filterName as string] = value
49
+ }
50
+ }
51
+ }
52
+
53
+ // 4. Always include q, sort (if valid for this type), page, page_size
54
+ if (q.value) {
55
+ params.q = q.value
56
+ }
57
+ if (sort.value) {
58
+ const validSortValues = typeConfig?.sortOptions?.map(o => o.value as string) ?? []
59
+ if (validSortValues.includes(sort.value)) {
60
+ params.sort = sort.value
61
+ }
62
+ }
63
+ params.page = page.value
64
+ params.page_size = pageSize
65
+
66
+ return params
67
+ }
68
+
69
+ // Watch all dependencies and update only if content changed
70
+ watch(
71
+ [q, sort, page, ...Object.values(allFilters)],
72
+ () => {
73
+ const newParams = buildParams()
74
+ // JSON.stringify comparison is safe here because buildParams() builds the object deterministically
75
+ // (keys are always added in the same order), avoiding the key ordering edge case.
76
+ if (JSON.stringify(newParams) !== JSON.stringify(stableParams.value)) {
77
+ stableParams.value = newParams
78
+ }
79
+ },
80
+ { immediate: true },
81
+ )
82
+
83
+ return stableParams
84
+ }
@@ -21,7 +21,8 @@ function detectLanguage(): string {
21
21
  const acceptLanguage = header
22
22
  if (acceptLanguage) {
23
23
  const primaryLang = acceptLanguage.split(';')[0]!.split(',')[0]!.split('-')[0]!.toLowerCase()
24
- return primaryLang
24
+ // Ignore wildcard * language, that should fallback to client side detection or default language
25
+ if (primaryLang !== '*') return primaryLang
25
26
  }
26
27
  }
27
28
  catch {
package/src/config.ts CHANGED
@@ -15,6 +15,8 @@ export type PluginConfig = {
15
15
  metricsApiUrl?: string
16
16
  schemaValidataUrl?: string
17
17
  schemaDocumentationUrl?: string
18
+ schemasSiteUrl?: string
19
+ schemasSiteName?: string
18
20
  tabularApiUrl?: string
19
21
  tabularApiPageSize?: number
20
22
  tabularAllowRemote?: boolean
@@ -27,6 +29,8 @@ export type PluginConfig = {
27
29
  textClamp?: string | Component | null
28
30
  appLink?: Component | null
29
31
  clientOnly?: Component | null
32
+ searchDebounce?: number
33
+ forumUrl?: string
30
34
  }
31
35
 
32
36
  export const configKey = Symbol() as InjectionKey<PluginConfig>
@@ -1,9 +1,17 @@
1
- import { reactive, ref, toValue, watchEffect, type ComputedRef, type Ref } from 'vue'
1
+ import { ref, toValue, watchEffect, onMounted, type ComputedRef, type MaybeRefOrGetter, type Ref } from 'vue'
2
2
  import { ofetch } from 'ofetch'
3
3
  import { useComponentsConfig } from '../config'
4
4
  import { useTranslation } from '../composables/useTranslation'
5
5
  import type { AsyncData, AsyncDataRequestStatus, UseFetchOptions } from './api.types'
6
6
 
7
+ function deepToValue(obj: MaybeRefOrGetter<Record<string, unknown> | undefined>): Record<string, unknown> | undefined {
8
+ const val = toValue(obj)
9
+ if (!val || typeof val !== 'object') return val
10
+ return Object.fromEntries(
11
+ Object.entries(val).map(([k, v]) => [k, toValue(v as MaybeRefOrGetter<unknown>)]),
12
+ )
13
+ }
14
+
7
15
  export async function useFetch<DataT, ErrorT = never>(
8
16
  url: string | Request | Ref<string | Request> | ComputedRef<string | null> | (() => string | Request),
9
17
  options?: UseFetchOptions<DataT>,
@@ -23,11 +31,13 @@ export async function useFetch<DataT, ErrorT = never>(
23
31
  const execute = async () => {
24
32
  const urlValue = toValue(url)
25
33
  if (!urlValue) return
26
- const fetchOptions = reactive(options ?? {})
34
+ const params = deepToValue(options?.params)
35
+ const query = deepToValue(options?.query)
27
36
  status.value = 'pending'
28
37
  try {
29
38
  data.value = await ofetch<DataT | null>(urlValue, {
30
39
  baseURL: config.apiBase,
40
+ params: params ?? query,
31
41
  onRequest(param) {
32
42
  if (config.onRequest) {
33
43
  if (Array.isArray(config.onRequest)) {
@@ -55,7 +65,6 @@ export async function useFetch<DataT, ErrorT = never>(
55
65
  onRequestError: config.onRequestError,
56
66
  onResponse: config.onResponse,
57
67
  onResponseError: config.onResponseError,
58
- ...fetchOptions,
59
68
  })
60
69
  status.value = 'success'
61
70
  }
@@ -65,9 +74,19 @@ export async function useFetch<DataT, ErrorT = never>(
65
74
  }
66
75
  }
67
76
 
68
- watchEffect(async () => {
69
- await execute()
70
- })
77
+ // When server is false, only start watching after mount (client-side only)
78
+ if (options?.server === false) {
79
+ onMounted(() => {
80
+ watchEffect(async () => {
81
+ await execute()
82
+ })
83
+ })
84
+ }
85
+ else {
86
+ watchEffect(async () => {
87
+ await execute()
88
+ })
89
+ }
71
90
 
72
91
  return {
73
92
  data,
@@ -1,10 +1,12 @@
1
1
  import type { ComputedRef, Ref, WatchSource } from 'vue'
2
2
 
3
+ type MaybeRef<T> = T | Ref<T> | ComputedRef<T>
4
+
3
5
  export type UseFetchOptions<DataT> = {
4
6
  key?: string
5
7
  method?: string
6
- query?: Record<string, unknown>
7
- params?: Record<string, unknown>
8
+ query?: MaybeRef<Record<string, unknown>>
9
+ params?: MaybeRef<Record<string, unknown>>
8
10
  body?: RequestInit['body'] | Record<string, unknown>
9
11
  headers?: Record<string, string> | [key: string, value: string][] | Headers
10
12
  baseURL?: string
@@ -33,5 +35,5 @@ export type AsyncDataRequestStatus = 'idle' | 'pending' | 'success' | 'error'
33
35
 
34
36
  export type UseFetchFunction = (<DataT, ErrorT>(
35
37
  url: string | Request | Ref<string | Request> | ComputedRef<string | null> | (() => string | Request),
36
- options?: UseFetchOptions<DataT>
38
+ options?: UseFetchOptions<DataT>,
37
39
  ) => Promise<AsyncData<DataT, ErrorT>>)
@@ -1,11 +1,6 @@
1
1
  import { useComponentsConfig } from '../config'
2
2
  import type { Dataset, DatasetV2 } from '../types/datasets'
3
3
  import type { CommunityResource, Resource } from '../types/resources'
4
- import { removeMarkdown } from './markdown'
5
-
6
- // Dataset description constants
7
- export const DESCRIPTION_SHORT_MAX_LENGTH = 200
8
- export const DESCRIPTION_MIN_LENGTH = 200
9
4
 
10
5
  function constructUrl(baseUrl: string, path: string): string {
11
6
  const url = new URL(baseUrl)
@@ -25,30 +20,7 @@ export function isCommunityResource(resource: Resource | CommunityResource): boo
25
20
  }
26
21
 
27
22
  export function getResourceExternalUrl(dataset: Dataset | DatasetV2 | Omit<Dataset, 'resources' | 'community_resources'>, resource: Resource | CommunityResource): string {
28
- return `${dataset.page}#/${isCommunityResource(resource) ? 'community-resources' : 'resources'}/${resource.id}`
29
- }
30
-
31
- /**
32
- * Returns the short description to display.
33
- * If description_short is provided, it is used.
34
- * Otherwise, the first DESCRIPTION_SHORT_MAX_LENGTH characters of description are used.
35
- */
36
- export async function getDescriptionShort(
37
- description: string | null | undefined,
38
- descriptionShort: string | null | undefined,
39
- ): Promise<string> {
40
- if (descriptionShort?.trim()) {
41
- return descriptionShort
42
- }
43
- if (description?.trim()) {
44
- // description field is a markdown field that may contain HTML tags, so we should trim it
45
- const plainText = (await removeMarkdown(description)).trim()
46
- if (plainText.length > DESCRIPTION_SHORT_MAX_LENGTH) {
47
- return `${plainText.substring(0, DESCRIPTION_SHORT_MAX_LENGTH - 1)}…`
48
- }
49
- return plainText
50
- }
51
- return ''
23
+ return `${dataset.page}${isCommunityResource(resource) ? '/community-resources' : ''}?resource_id=${resource.id}`
52
24
  }
53
25
 
54
26
  export function getResourceFilesize(resource: Resource): null | number {
@@ -0,0 +1,33 @@
1
+ import { removeMarkdownSync } from './markdown'
2
+
3
+ // Dataset description constants
4
+
5
+ // Form validation (client-side rules)
6
+ export const DESCRIPTION_SHORT_MAX_LENGTH = 200 // max for `description_short` (+ truncation/output cap)
7
+ export const DESCRIPTION_MIN_LENGTH = 200 // min (recommendation) for `description` (not AI gating)
8
+
9
+ // AI gating (enable AI suggestions; not validation)
10
+ export const AI_SUGGESTION_MIN_DESCRIPTION_LENGTH = 200 // min `description` length to enable suggestions
11
+
12
+ /**
13
+ * Returns the short description to display.
14
+ * If description_short is provided, it is used.
15
+ * Otherwise, the first DESCRIPTION_SHORT_MAX_LENGTH characters of description are used.
16
+ */
17
+ export function getDescriptionShort({ description, descriptionShort }: {
18
+ description: string | null | undefined
19
+ descriptionShort?: string | null | undefined
20
+ }) {
21
+ if (descriptionShort?.trim()) {
22
+ return descriptionShort
23
+ }
24
+ if (description?.trim()) {
25
+ // description field is a markdown field that may contain HTML tags, so we should trim it
26
+ const plainText = removeMarkdownSync(description).trim()
27
+ if (plainText.length > DESCRIPTION_SHORT_MAX_LENGTH) {
28
+ return `${plainText.substring(0, DESCRIPTION_SHORT_MAX_LENGTH - 1)}…`
29
+ }
30
+ return plainText
31
+ }
32
+ return ''
33
+ }
@@ -37,3 +37,14 @@ export const summarize = (val: number, fractionDigits = 0) => {
37
37
  }
38
38
  return `${toFixedIfNotZero(val)}Y`
39
39
  }
40
+
41
+ export const escapeCsvValue = (value: string | number | null | undefined): string => {
42
+ if (value === null || value === undefined || value === '') {
43
+ return ''
44
+ }
45
+ const stringValue = String(value)
46
+ if (stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n')) {
47
+ return `"${stringValue.replace(/"/g, '""')}"`
48
+ }
49
+ return stringValue
50
+ }
@@ -16,26 +16,57 @@ import remarkGfm from 'remark-gfm'
16
16
  import strip from 'strip-markdown'
17
17
 
18
18
  // Copied from https://github.com/potato4d/rehype-plugin-image-native-lazy-loading/blob/v1.2.0/src/index.ts
19
- function lazyLoadPlugin(this: Processor): Transformer {
20
- function visitor(el: hast.Element) {
21
- if (el.tagName !== 'img') {
22
- return
23
- }
24
- el.properties = {
25
- ...(el.properties || {}),
26
- loading: 'lazy',
27
- }
19
+ function rehypeLazyLoad(this: Processor): Transformer {
20
+ return function transformer(htmlAST: Node): Node {
21
+ visit(htmlAST, 'element', function visitor(el: hast.Element) {
22
+ if (el.tagName !== 'img') {
23
+ return
24
+ }
25
+ el.properties = {
26
+ ...(el.properties || {}),
27
+ loading: 'lazy',
28
+ }
29
+ })
30
+ return htmlAST
28
31
  }
32
+ }
33
+
34
+ function rehypeNoHeadings(this: Processor): Transformer {
35
+ return function transformer(htmlAST: Node): Node {
36
+ visit(htmlAST, 'element', function visitor(el: hast.Element) {
37
+ if (el.tagName !== 'h1' && el.tagName !== 'h2' && el.tagName !== 'h3' && el.tagName !== 'h4' && el.tagName !== 'h5' && el.tagName !== 'h6') {
38
+ return
39
+ }
40
+
41
+ const classes = {
42
+ h1: 'text-3xl leading-8',
43
+ h2: 'text-2xl leading-7',
44
+ h3: 'text-xl leading-6',
45
+ h4: 'text-base',
46
+ h5: 'text-sm leading-6',
47
+ h6: 'text-sm leading-6',
48
+ }[el.tagName]
29
49
 
30
- function transformer(htmlAST: Node): Node {
31
- visit(htmlAST, 'element', visitor)
50
+ el.properties = {
51
+ ...(el.properties || {}),
52
+ class: `font-extrabold ${classes}`,
53
+ }
54
+ el.tagName = 'div'
55
+ })
32
56
  return htmlAST
33
57
  }
34
-
35
- return transformer
36
58
  }
37
59
 
38
- export function formatMarkdown(md: string, minDepth = 3) {
60
+ export function formatMarkdown(md: string, config: number | { minDepth: number, noHeadings: boolean } = 3) {
61
+ let minDepth: number
62
+ let noHeadings = false
63
+ if (typeof config === 'number') {
64
+ minDepth = config
65
+ }
66
+ else {
67
+ minDepth = config.minDepth
68
+ noHeadings = config.noHeadings
69
+ }
39
70
  const result = unified()
40
71
  .use(behead, { minDepth: minDepth > 1 ? minDepth : undefined } as Options)
41
72
  // Take Markdown as input and turn it into MD syntax tree
@@ -55,14 +86,14 @@ export function formatMarkdown(md: string, minDepth = 3) {
55
86
  .use(rehypeSanitize)
56
87
  // Serialize syntax tree to HTML
57
88
  .use(rehypeStringify)
58
- .use(lazyLoadPlugin)
89
+ .use(noHeadings ? [rehypeLazyLoad, rehypeNoHeadings] : [rehypeLazyLoad])
59
90
  // And finally, process the input
60
91
  .processSync(md)
61
92
 
62
93
  return String(result)
63
94
  }
64
95
 
65
- export async function removeMarkdown(text: string) {
96
+ export async function removeMarkdownAsync(text: string) {
66
97
  const file = await unified()
67
98
  // Take Markdown as input and turn it into MD syntax tree
68
99
  .use(remarkParse, { fragment: true })
@@ -73,6 +104,19 @@ export async function removeMarkdown(text: string) {
73
104
  return String(file)
74
105
  }
75
106
 
107
+ export function removeMarkdownSync(text: string) {
108
+ const file = unified()
109
+ // Take Markdown as input and turn it into MD syntax tree
110
+ .use(remarkParse, { fragment: true })
111
+ .use(remarkGfm)
112
+ .use(strip)
113
+ .use(remarkStringify)
114
+ .processSync(text)
115
+ return String(file)
116
+ }
117
+
118
+ export { removeMarkdownAsync as removeMarkdown }
119
+
76
120
  const prose = 'prose prose-neutral max-w-none prose-strong:text-gray-plain'
77
121
  const proseSm = 'prose-p:text-sm prose-sm'
78
122
  const proseTable = 'prose-table:bg-gray-some prose-table:overflow-visible prose-thead:border-b-2 prose-thead:border-black prose-tr:data-[is-header=true]:border-b-2 prose-tr:data-[is-header=true]:border-black prose-tr:even:bg-gray-lower prose-tr:border-b-0 *:prose-th:m-0 *:prose-td:m-0 prose-th:p-4 prose-td:p-4'
@@ -1,3 +1,8 @@
1
+ import { escapeCsvValue } from './helpers'
2
+ import { ofetch } from 'ofetch'
3
+ import type { DatasetV2 } from '../types/datasets'
4
+ import type { PaginatedArray } from '../types/api'
5
+
1
6
  export type OrganizationMetrics = {
2
7
  downloads: Record<string, number>
3
8
  downloadsTotal: number
@@ -114,6 +119,34 @@ export async function getDatasetMetrics(datasetId: string, metricsApi: string):
114
119
  }
115
120
  }
116
121
 
122
+ export async function createDatasetsForOrganizationMetricsUrl(organizationId: string, metricsApi: string, apiBase: string) {
123
+ let data = 'dataset_title,dataset_id,month,monthly_visit,monthly_download_resource\n'
124
+
125
+ // fetch datasets info from organization datasets
126
+ const datasets: Record<string, Record<string, string>> = {}
127
+ let datasetsUrl: string | null = `/api/2/datasets/?organization=${organizationId}&page_size=200`
128
+ while (datasetsUrl) {
129
+ const body: PaginatedArray<DatasetV2> = await ofetch(datasetsUrl, { baseURL: apiBase, credentials: 'include' })
130
+ datasetsUrl = body.next_page
131
+ for (const row of body.data) {
132
+ datasets[row.id] = { title: row.title }
133
+ }
134
+ }
135
+
136
+ // fetch datasets metrics for the organization
137
+ let metricsUrl: string | null = `${metricsApi}/api/datasets/data/?organization_id__exact=${organizationId}&metric_month__sort=desc&page_size=50`
138
+ while (metricsUrl) {
139
+ const body: { links: { next: string | null }, data: Array<{ dataset_id: string, metric_month: string, monthly_visit: number, monthly_download_resource: number }> } = await ofetch(metricsUrl)
140
+ metricsUrl = body.links.next
141
+ for (const row of body.data) {
142
+ const datasetTitle = datasets[row.dataset_id]?.title || ''
143
+ data += `${escapeCsvValue(datasetTitle)},${escapeCsvValue(row.dataset_id)},${escapeCsvValue(row.metric_month)},${row.monthly_visit},${row.monthly_download_resource}\n`
144
+ }
145
+ }
146
+
147
+ return URL.createObjectURL(new Blob([data], { type: 'text/csv' }))
148
+ }
149
+
117
150
  export async function getDataserviceMetrics(dataserviceId: string, metricsApi: string): Promise<DataserviceMetrics> {
118
151
  // Fetching last 12 months
119
152
  const response = await fetch(`${metricsApi}/api/dataservices/data/?dataservice_id__exact=${dataserviceId}&metric_month__sort=desc&page_size=12`)
@@ -1,7 +1,7 @@
1
1
  import type { Component } from 'vue'
2
2
  import { RiBankLine, RiBuilding2Line, RiCommunityLine, RiGovernmentLine, RiUserLine } from '@remixicon/vue'
3
3
  import { useComponentsConfig } from '../config'
4
- import type { OrganizationReference } from '../types/organizations'
4
+ import type { Organization, OrganizationReference } from '../types/organizations'
5
5
  import { useTranslation } from '../composables/useTranslation'
6
6
 
7
7
  export const CERTIFIED = 'certified'
@@ -22,11 +22,11 @@ function constructUrl(baseUrl: string, path: string): string {
22
22
  return url.toString()
23
23
  }
24
24
 
25
- export function isType(organization: OrganizationReference, type: OrganizationTypes) {
25
+ export function isType(organization: Organization | OrganizationReference, type: OrganizationTypes) {
26
26
  return hasBadge(organization, type)
27
27
  }
28
28
 
29
- export function hasBadge(organization: OrganizationReference, kind: string) {
29
+ export function hasBadge(organization: Organization | OrganizationReference, kind: string) {
30
30
  return organization.badges.some(badge => badge.kind === kind)
31
31
  }
32
32
 
@@ -68,7 +68,7 @@ export function findOrganizationType(searched: OrganizationTypes | UserType) {
68
68
  return getOrganizationTypes().find(type => type.type === searched)!
69
69
  }
70
70
 
71
- export function getOrganizationType(organization: OrganizationReference): OrganizationTypes {
71
+ export function getOrganizationType(organization: Organization | OrganizationReference): OrganizationTypes {
72
72
  if (isType(organization, LOCAL_AUTHORITY)) {
73
73
  return LOCAL_AUTHORITY
74
74
  }
@@ -86,7 +86,7 @@ export function getOrganizationType(organization: OrganizationReference): Organi
86
86
  }
87
87
  }
88
88
 
89
- export function isOrganizationCertified(organization: OrganizationReference | null): boolean {
89
+ export function isOrganizationCertified(organization: Organization | OrganizationReference | null): boolean {
90
90
  if (!organization) return false
91
91
  return hasBadge(organization, CERTIFIED) && (isType(organization, PUBLIC_SERVICE) || isType(organization, LOCAL_AUTHORITY))
92
92
  }
@@ -0,0 +1,55 @@
1
+ import type { Resource, WfsMetadata, OgcLayerInfo } from '../types/resources'
2
+
3
+ const WFS_EXPORT_FORMATS = [
4
+ {
5
+ name: 'csv',
6
+ mimetype: 'csv',
7
+ },
8
+ {
9
+ name: 'json',
10
+ mimetype: 'application/json',
11
+ },
12
+ {
13
+ name: 'shp',
14
+ mimetype: 'SHAPE-ZIP',
15
+ },
16
+ {
17
+ name: 'gml',
18
+ mimetype: 'application/gml+xml',
19
+ },
20
+ {
21
+ name: 'kml',
22
+ mimetype: 'KML',
23
+ },
24
+ {
25
+ name: 'gpkg',
26
+ mimetype: 'application/geopackage+sqlite3',
27
+ },
28
+ ]
29
+
30
+ function buildWfsDownloadUrl(baseUrl: string, wfsMetadata: WfsMetadata, format: { name: string, mimetype: string }, layer: OgcLayerInfo) {
31
+ const version = wfsMetadata.version
32
+ const query = new URLSearchParams({
33
+ SERVICE: 'WFS',
34
+ REQUEST: 'GetFeature',
35
+ VERSION: version,
36
+ ...(Number(version.split('.')[0]) >= 2 ? { TYPENAMES: layer.name } : { TYPENAME: layer.name }),
37
+ OUTPUTFORMAT: format.mimetype,
38
+ ...(layer.default_crs ? { SRSNAME: layer.default_crs } : {}),
39
+ })
40
+ return `${baseUrl.split('?')[0]}?${query.toString()}`
41
+ }
42
+
43
+ export function getWfsExportFormats(resource: Pick<Resource, 'extras' | 'url'>) {
44
+ const wfsMetadata = resource.extras['analysis:parsing:ogc_metadata'] as WfsMetadata | null
45
+ if (!wfsMetadata || wfsMetadata.format !== `wfs`) return []
46
+ const outputFormats = wfsMetadata.output_formats.map((format: string) => format.toLowerCase())
47
+ const layer = wfsMetadata.detected_layer
48
+ if (!layer) return []
49
+ const formats = WFS_EXPORT_FORMATS.filter(format => outputFormats.includes(format.mimetype.toLowerCase()))
50
+ .map(format => ({
51
+ url: buildWfsDownloadUrl(resource.url, wfsMetadata, format, layer),
52
+ format: format.name,
53
+ }))
54
+ return formats
55
+ }