@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.
- package/README.md +1 -1
- package/assets/main.css +49 -22
- package/dist/Control-BNCDn-8E.js +148 -0
- package/dist/{Datafair.client-x39O4yfF.js → Datafair.client-B5lBpOl8.js} +2 -2
- package/dist/Event-BOgJUhNR.js +738 -0
- package/dist/Image-BN-4XkIn.js +247 -0
- package/dist/{JsonPreview.client-BMsC5JcY.js → JsonPreview.client-Doz1Z0BS.js} +23 -23
- package/dist/Map-BdT3i2C4.js +7609 -0
- package/dist/MapContainer.client-oiieO8H-.js +105 -0
- package/dist/OSM-CamriM9b.js +71 -0
- package/dist/PdfPreview.client-CdAhkDFJ.js +14513 -0
- package/dist/{Pmtiles.client-BaiIo4VZ.js → Pmtiles.client-B0v8tGJQ.js} +3 -3
- package/dist/ScaleLine-BiesrgOv.js +165 -0
- package/dist/Swagger.client-CsK65JnG.js +4 -0
- package/dist/Tile-DCuqwNOI.js +1206 -0
- package/dist/TileImage-CmZf8EdU.js +1067 -0
- package/dist/View-DcDc7N2K.js +2858 -0
- package/dist/{XmlPreview.client-CAdN0w_Y.js → XmlPreview.client-CrjHf74q.js} +17 -17
- package/dist/common-C4rDcQpp.js +243 -0
- package/dist/components-next.css +1 -1
- package/dist/components-next.js +158 -117
- package/dist/components.css +1 -1
- package/dist/{MapContainer.client-DeSo8EvG.js → index-Bbu9rOHt.js} +4975 -21416
- package/dist/leaflet-src-7m1mB8LI.js +6338 -0
- package/dist/{main-Dgri3TQL.js → main-CiH8ZmBI.js} +56973 -51462
- package/dist/proj-CKwYjU38.js +1569 -0
- package/dist/tilecoord-YW3qEH_j.js +884 -0
- package/dist/{vue3-xml-viewer.common-D6skc_Ai.js → vue3-xml-viewer.common-Bi_bsV6C.js} +1 -1
- package/package.json +6 -2
- package/src/components/ActivityList/ActivityList.vue +6 -2
- package/src/components/AppLink.vue +4 -1
- package/src/components/Avatar.vue +2 -2
- package/src/components/AvatarWithName.vue +8 -4
- package/src/components/BouncingDots.vue +21 -0
- package/src/components/BrandedButton.vue +2 -0
- package/src/components/CopyButton.vue +19 -7
- package/src/components/DataserviceCard.vue +85 -120
- package/src/components/DatasetCard.vue +110 -171
- package/src/components/DatasetInformation/DatasetEmbedSection.vue +43 -0
- package/src/components/DatasetInformation/DatasetInformationSection.vue +73 -0
- package/src/components/DatasetInformation/DatasetSchemaSection.vue +74 -0
- package/src/components/DatasetInformation/DatasetSpatialSection.vue +59 -0
- package/src/components/DatasetInformation/DatasetTemporalitySection.vue +45 -0
- package/src/components/DatasetInformation/index.ts +5 -0
- package/src/components/DatasetQuality.vue +23 -16
- package/src/components/DatasetQualityInline.vue +13 -17
- package/src/components/DatasetQualityScore.vue +12 -15
- package/src/components/DatasetQualityTooltipContent.vue +3 -3
- package/src/components/DescriptionList.vue +1 -4
- package/src/components/DescriptionListDetails.vue +5 -0
- package/src/components/DescriptionListTerm.vue +5 -0
- package/src/components/DiscussionMessageCard.vue +63 -0
- package/src/components/ExtraAccordion.vue +4 -4
- package/src/components/Form/BadgeSelect.vue +35 -0
- package/src/components/Form/FormatSelect.vue +28 -0
- package/src/components/Form/GeozoneSelect.vue +52 -0
- package/src/components/Form/GranularitySelect.vue +29 -0
- package/src/components/Form/LicenseSelect.vue +30 -0
- package/src/components/Form/OrganizationSelect.vue +62 -0
- package/src/components/Form/OrganizationTypeSelect.vue +34 -0
- package/src/components/Form/ReuseTopicSelect.vue +29 -0
- package/src/components/Form/SchemaSelect.vue +30 -0
- package/src/components/Form/SearchableSelect.vue +334 -0
- package/src/components/Form/SelectGroup.vue +132 -0
- package/src/components/Form/TagSelect.vue +38 -0
- package/src/components/LeafletMap.vue +31 -0
- package/src/components/LicenseBadge.vue +24 -0
- package/src/components/LoadingBlock.vue +23 -2
- package/src/components/MarkdownViewer.vue +3 -1
- package/src/components/ObjectCard.vue +42 -0
- package/src/components/ObjectCardBadge.vue +22 -0
- package/src/components/ObjectCardHeader.vue +35 -0
- package/src/components/ObjectCardOwner.vue +43 -0
- package/src/components/ObjectCardShortDescription.vue +28 -0
- package/src/components/OrganizationCard.vue +35 -20
- package/src/components/OrganizationHorizontalCard.vue +87 -0
- package/src/components/OrganizationLogo.vue +1 -1
- package/src/components/OrganizationNameWithCertificate.vue +12 -6
- package/src/components/OwnerTypeIcon.vue +1 -0
- package/src/components/Pagination.vue +1 -1
- package/src/components/Placeholder.vue +5 -2
- package/src/components/PostCard.vue +62 -0
- package/src/components/ProgressBar.vue +31 -0
- package/src/components/RadioGroup.vue +32 -0
- package/src/components/RadioInput.vue +64 -0
- package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
- package/src/components/ResourceAccordion/EditButton.vue +2 -3
- package/src/components/ResourceAccordion/JsonPreview.client.vue +3 -3
- package/src/components/ResourceAccordion/MapContainer.client.vue +21 -17
- package/src/components/ResourceAccordion/Metadata.vue +11 -24
- package/src/components/ResourceAccordion/PdfPreview.client.vue +70 -74
- package/src/components/ResourceAccordion/Pmtiles.client.vue +2 -2
- package/src/components/ResourceAccordion/Preview.vue +2 -2
- package/src/components/ResourceAccordion/ResourceAccordion.vue +35 -28
- package/src/components/ResourceAccordion/ResourceIcon.vue +1 -0
- package/src/components/ResourceAccordion/SchemaBadge.vue +2 -2
- package/src/components/ResourceAccordion/XmlPreview.client.vue +3 -3
- package/src/components/ResourceExplorer/ResourceExplorer.vue +243 -0
- package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +116 -0
- package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +410 -0
- package/src/components/ReuseCard.vue +8 -28
- package/src/components/ReuseHorizontalCard.vue +80 -0
- package/src/components/Search/BasicAndAdvancedFilters.vue +49 -0
- package/src/components/Search/Filter/AccessTypeFilter.vue +37 -0
- package/src/components/Search/Filter/DatasetBadgeFilter.vue +40 -0
- package/src/components/Search/Filter/FilterButtonGroup.vue +78 -0
- package/src/components/Search/Filter/FormatFamilyFilter.vue +39 -0
- package/src/components/Search/Filter/LastUpdateRangeFilter.vue +37 -0
- package/src/components/Search/Filter/ProducerTypeFilter.vue +49 -0
- package/src/components/Search/Filter/ReuseTypeFilter.vue +42 -0
- package/src/components/Search/GlobalSearch.vue +707 -0
- package/src/components/Search/SearchInput.vue +63 -0
- package/src/components/Search/Sidemenu.vue +38 -0
- package/src/components/StatBox.vue +5 -5
- package/src/components/Tag.vue +30 -0
- package/src/components/Toggletip.vue +11 -4
- package/src/components/Tooltip.vue +2 -3
- package/src/components/TopicCard.vue +134 -0
- package/src/components/radioGroupContext.ts +9 -0
- package/src/composables/useDebouncedRef.ts +31 -0
- package/src/composables/useHasTabularData.ts +15 -0
- package/src/composables/useMetrics.ts +4 -3
- package/src/composables/useResourceCapabilities.ts +131 -0
- package/src/composables/useRouteQueryBoolean.ts +10 -0
- package/src/composables/useSelectModelSync.ts +89 -0
- package/src/composables/useStableQueryParams.ts +84 -0
- package/src/composables/useTranslation.ts +2 -1
- package/src/config.ts +4 -0
- package/src/functions/api.ts +25 -6
- package/src/functions/api.types.ts +5 -3
- package/src/functions/datasets.ts +1 -29
- package/src/functions/description.ts +33 -0
- package/src/functions/helpers.ts +11 -0
- package/src/functions/markdown.ts +60 -16
- package/src/functions/metrics.ts +33 -0
- package/src/functions/organizations.ts +5 -5
- package/src/functions/resourceCapabilities.ts +55 -0
- package/src/main.ts +96 -7
- package/src/types/dataservices.ts +14 -12
- package/src/types/datasets.ts +20 -7
- package/src/types/discussions.ts +20 -0
- package/src/types/licenses.ts +3 -3
- package/src/types/organizations.ts +13 -1
- package/src/types/owned.ts +4 -2
- package/src/types/pages.ts +70 -0
- package/src/types/posts.ts +27 -0
- package/src/types/resources.ts +16 -0
- package/src/types/reuses.ts +14 -5
- package/src/types/search.ts +407 -0
- package/src/types/users.ts +12 -3
- package/dist/PdfPreview.client-COOkEkRA.js +0 -107
- package/dist/Swagger.client-CpLgaLg6.js +0 -4
- package/dist/pdf-vue3-IkJO65RH.js +0 -273
- package/dist/pdf.min-f72cfa08-CdgJTooZ.js +0 -9501
- 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
|
-
|
|
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>
|
package/src/functions/api.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
-
|
|
69
|
-
|
|
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}
|
|
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
|
+
}
|
package/src/functions/helpers.ts
CHANGED
|
@@ -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
|
|
20
|
-
function
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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(
|
|
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
|
|
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'
|
package/src/functions/metrics.ts
CHANGED
|
@@ -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
|
+
}
|