@databiosphere/findable-ui 21.0.0 → 21.1.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.
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "21.0.0"
2
+ ".": "21.1.1"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [21.1.1](https://github.com/DataBiosphere/findable-ui/compare/v21.1.0...v21.1.1) (2025-01-31)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * quote values in tsv export when they contain syntactic characters ([#314](https://github.com/DataBiosphere/findable-ui/issues/314)) ([#315](https://github.com/DataBiosphere/findable-ui/issues/315)) ([d6832a5](https://github.com/DataBiosphere/findable-ui/commit/d6832a56f5245281e635902000dcecaab62c155f))
9
+
10
+ ## [21.1.0](https://github.com/DataBiosphere/findable-ui/compare/v21.0.0...v21.1.0) (2025-01-30)
11
+
12
+
13
+ ### Features
14
+
15
+ * support unifying export views [#4102](https://github.com/DataBiosphere/findable-ui/issues/4102) ([#312](https://github.com/DataBiosphere/findable-ui/issues/312)) ([cbeb6e5](https://github.com/DataBiosphere/findable-ui/commit/cbeb6e5c6c264049767c3e7a5bebe4c72e25e6b1))
16
+
3
17
  ## [21.0.0](https://github.com/DataBiosphere/findable-ui/compare/v20.0.0...v21.0.0) (2025-01-06)
4
18
 
5
19
 
@@ -6,7 +6,9 @@ export declare const COLLATOR_CASE_INSENSITIVE: Intl.Collator;
6
6
  * Values to determine the index for each param.
7
7
  * https://host/explore/[slug]/[param-uuid]/[param-tab]
8
8
  * - ExploreView 0 returns the current UUID
9
- * - ExploreView 1 returns the current tab
9
+ * - ExploreView 1 returns the current tab (or choose export)
10
+ * - ExportView 2 returns the export method
10
11
  */
11
12
  export declare const PARAMS_INDEX_UUID = 0;
12
13
  export declare const PARAMS_INDEX_TAB = 1;
14
+ export declare const PARAMS_INDEX_EXPORT_METHOD = 2;
@@ -9,7 +9,9 @@ export const COLLATOR_CASE_INSENSITIVE = new Intl.Collator("en", {
9
9
  * Values to determine the index for each param.
10
10
  * https://host/explore/[slug]/[param-uuid]/[param-tab]
11
11
  * - ExploreView 0 returns the current UUID
12
- * - ExploreView 1 returns the current tab
12
+ * - ExploreView 1 returns the current tab (or choose export)
13
+ * - ExportView 2 returns the export method
13
14
  */
14
15
  export const PARAMS_INDEX_UUID = 0;
15
- export const PARAMS_INDEX_TAB = 1;
16
+ export const PARAMS_INDEX_TAB = 1; // Note "export" (i.e. not a tab) can possibly be at this index too
17
+ export const PARAMS_INDEX_EXPORT_METHOD = 2;
@@ -63,11 +63,17 @@ function formatDataToTSV(data) {
63
63
  .map((row) => {
64
64
  return row
65
65
  .map((data) => {
66
- // Concatenate array values.
67
- if (Array.isArray(data)) {
68
- return data.join(", ");
69
- }
70
- return data;
66
+ // Use empty string in place of undefined and null.
67
+ if (data === undefined || data === null)
68
+ return "";
69
+ // Convert to string.
70
+ const dataString = Array.isArray(data)
71
+ ? data.join(", ")
72
+ : String(data);
73
+ // Quote if necessary.
74
+ return /[\t\r\n"]/.test(dataString)
75
+ ? `"${dataString.replaceAll('"', '""')}"`
76
+ : dataString;
71
77
  })
72
78
  .join("\t");
73
79
  })
@@ -133,8 +133,10 @@ export interface EntityConfig<T = any, I = any> extends TabConfig {
133
133
  entityMapper?: EntityMapper<T, I>;
134
134
  exploreMode: ExploreMode;
135
135
  explorerTitle?: SiteConfig["explorerTitle"];
136
+ export?: ExportConfig;
136
137
  getId?: GetIdFunction<T>;
137
138
  getTitle?: GetTitleFunction<T>;
139
+ hideTabs?: boolean;
138
140
  list: ListConfig<T>;
139
141
  listView?: ListViewConfig;
140
142
  options?: Options;
@@ -0,0 +1,6 @@
1
+ import { ExportConfig } from "../config/entities";
2
+ /**
3
+ * Returns the export configuration for the given entity.
4
+ * @returns export configuration.
5
+ */
6
+ export declare const useEntityExportConfig: () => ExportConfig;
@@ -0,0 +1,12 @@
1
+ import { useConfig } from "./useConfig";
2
+ /**
3
+ * Returns the export configuration for the given entity.
4
+ * @returns export configuration.
5
+ */
6
+ export const useEntityExportConfig = () => {
7
+ const { entityConfig } = useConfig();
8
+ if (!entityConfig.export) {
9
+ throw new Error("This entity config does not have an export field set");
10
+ }
11
+ return entityConfig.export;
12
+ };
@@ -30,11 +30,11 @@ export const EntityDetailView = (props) => {
30
30
  const { push, query } = useRouter();
31
31
  const { entityConfig } = useConfig();
32
32
  const { mainColumn, sideColumn } = currentTab;
33
- const { detail, route: entityRoute } = entityConfig;
33
+ const { detail, hideTabs, route: entityRoute } = entityConfig;
34
34
  const { detailOverviews, top } = detail;
35
35
  const uuid = query.params?.[PARAMS_INDEX_UUID];
36
36
  const isDetailOverview = detailOverviews?.includes(currentTab.label);
37
- const tabs = getTabs(entityConfig);
37
+ const tabs = hideTabs ? [] : getTabs(entityConfig);
38
38
  const title = useEntityHeadTitle(response);
39
39
  if (!response) {
40
40
  return React.createElement("span", null); //TODO: return the loading UI component
@@ -50,5 +50,5 @@ export const EntityDetailView = (props) => {
50
50
  return (React.createElement(Fragment, null,
51
51
  title && (React.createElement(Head, null,
52
52
  React.createElement("title", null, title))),
53
- React.createElement(DetailView, { isDetailOverview: isDetailOverview, mainColumn: React.createElement(ComponentCreator, { components: mainColumn, response: response }), sideColumn: sideColumn ? (React.createElement(ComponentCreator, { components: sideColumn, response: response })) : undefined, Tabs: React.createElement(Tabs, { onTabChange: onTabChange, tabs: tabs, value: tabRoute }), top: React.createElement(ComponentCreator, { components: top, response: response }) })));
53
+ React.createElement(DetailView, { isDetailOverview: isDetailOverview, mainColumn: React.createElement(ComponentCreator, { components: mainColumn, response: response }), sideColumn: sideColumn ? (React.createElement(ComponentCreator, { components: sideColumn, response: response })) : undefined, Tabs: hideTabs ? (React.createElement(React.Fragment, null)) : (React.createElement(Tabs, { onTabChange: onTabChange, tabs: tabs, value: tabRoute })), top: React.createElement(ComponentCreator, { components: top, response: response }) })));
54
54
  };
@@ -0,0 +1,2 @@
1
+ import { EntityDetailViewProps } from "views/EntityDetailView/entityDetailView";
2
+ export declare const EntityExportMethodView: (props: EntityDetailViewProps) => JSX.Element;
@@ -0,0 +1,41 @@
1
+ import { useRouter } from "next/router";
2
+ import React from "react";
3
+ import { PARAMS_INDEX_EXPORT_METHOD } from "../../common/constants";
4
+ import { ComponentCreator } from "../../components/ComponentCreator/ComponentCreator";
5
+ import { BackPageView } from "../../components/Layout/components/BackPage/backPageView";
6
+ import { useEntityExportConfig } from "../../hooks/useEntityExportConfig";
7
+ import { useFetchEntity } from "../../hooks/useFetchEntity";
8
+ import { useUpdateURLCatalogParams } from "../../hooks/useUpdateURLCatalogParam";
9
+ export const EntityExportMethodView = (props) => {
10
+ // Update the catalog param if necessary.
11
+ useUpdateURLCatalogParams();
12
+ // Grab the entity to be exported.
13
+ const { response } = useFetchEntity(props);
14
+ // Get the column definitions for the entity export.
15
+ const { query } = useRouter();
16
+ const { exportMethods, tabs } = useEntityExportConfig();
17
+ const { sideColumn } = tabs[0];
18
+ const { mainColumn, top } = getExportMethodConfig(exportMethods, query) || {};
19
+ // Wait for the entity to be fetched.
20
+ if (!response) {
21
+ return React.createElement("span", null);
22
+ }
23
+ return (React.createElement(BackPageView, { mainColumn: React.createElement(ComponentCreator, { components: mainColumn || [], response: response }), sideColumn: sideColumn ? (React.createElement(ComponentCreator, { components: sideColumn, response: response })) : undefined, top: React.createElement(ComponentCreator, { components: top || [], response: response }) }));
24
+ };
25
+ /**
26
+ * Returns the export method configuration for the given pathname.
27
+ * @param exportMethods - Export methods config.
28
+ * @param query - Router query object.
29
+ * @returns export method configuration.
30
+ */
31
+ function getExportMethodConfig(exportMethods, query) {
32
+ // Determine the selected export method from the URL.
33
+ const exportMethodRoute = query.params?.[PARAMS_INDEX_EXPORT_METHOD];
34
+ if (!exportMethodRoute) {
35
+ return;
36
+ }
37
+ // Find the config for the selected export method.
38
+ return exportMethods.find(({ route }) => {
39
+ return route.includes(exportMethodRoute);
40
+ });
41
+ }
@@ -0,0 +1,2 @@
1
+ import { EntityDetailViewProps } from "views/EntityDetailView/entityDetailView";
2
+ export declare const EntityExportView: (props: EntityDetailViewProps) => JSX.Element;
@@ -0,0 +1,21 @@
1
+ import React from "react";
2
+ import { ComponentCreator } from "../../components/ComponentCreator/ComponentCreator";
3
+ import { BackPageView } from "../../components/Layout/components/BackPage/backPageView";
4
+ import { useEntityExportConfig } from "../../hooks/useEntityExportConfig";
5
+ import { useFetchEntity } from "../../hooks/useFetchEntity";
6
+ import { useUpdateURLCatalogParams } from "../../hooks/useUpdateURLCatalogParam";
7
+ export const EntityExportView = (props) => {
8
+ // Update the catalog param if necessary.
9
+ useUpdateURLCatalogParams();
10
+ // Grab the entity to be exported.
11
+ const { response } = useFetchEntity(props);
12
+ // Get the column definitions for the entity export.
13
+ const { tabs, top } = useEntityExportConfig();
14
+ const currentTab = tabs[0];
15
+ const { mainColumn, sideColumn } = currentTab;
16
+ // Wait for the entity to be fetched.
17
+ if (!response) {
18
+ return React.createElement("span", null);
19
+ }
20
+ return (React.createElement(BackPageView, { mainColumn: React.createElement(ComponentCreator, { components: mainColumn, response: response }), sideColumn: sideColumn ? (React.createElement(ComponentCreator, { components: sideColumn, response: response })) : undefined, top: React.createElement(ComponentCreator, { components: top, response: response }) }));
21
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@databiosphere/findable-ui",
3
- "version": "21.0.0",
3
+ "version": "21.1.1",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
@@ -10,7 +10,9 @@ export const COLLATOR_CASE_INSENSITIVE = new Intl.Collator("en", {
10
10
  * Values to determine the index for each param.
11
11
  * https://host/explore/[slug]/[param-uuid]/[param-tab]
12
12
  * - ExploreView 0 returns the current UUID
13
- * - ExploreView 1 returns the current tab
13
+ * - ExploreView 1 returns the current tab (or choose export)
14
+ * - ExportView 2 returns the export method
14
15
  */
15
16
  export const PARAMS_INDEX_UUID = 0;
16
- export const PARAMS_INDEX_TAB = 1;
17
+ export const PARAMS_INDEX_TAB = 1; // Note "export" (i.e. not a tab) can possibly be at this index too
18
+ export const PARAMS_INDEX_EXPORT_METHOD = 2;
@@ -99,11 +99,16 @@ function formatDataToTSV(data: TableData[][]): string {
99
99
  .map((row) => {
100
100
  return row
101
101
  .map((data) => {
102
- // Concatenate array values.
103
- if (Array.isArray(data)) {
104
- return data.join(", ");
105
- }
106
- return data;
102
+ // Use empty string in place of undefined and null.
103
+ if (data === undefined || data === null) return "";
104
+ // Convert to string.
105
+ const dataString = Array.isArray(data)
106
+ ? data.join(", ")
107
+ : String(data);
108
+ // Quote if necessary.
109
+ return /[\t\r\n"]/.test(dataString)
110
+ ? `"${dataString.replaceAll('"', '""')}"`
111
+ : dataString;
107
112
  })
108
113
  .join("\t");
109
114
  })
@@ -176,8 +176,10 @@ export interface EntityConfig<T = any, I = any> extends TabConfig {
176
176
  entityMapper?: EntityMapper<T, I>;
177
177
  exploreMode: ExploreMode;
178
178
  explorerTitle?: SiteConfig["explorerTitle"];
179
+ export?: ExportConfig;
179
180
  getId?: GetIdFunction<T>;
180
181
  getTitle?: GetTitleFunction<T>;
182
+ hideTabs?: boolean;
181
183
  list: ListConfig<T>;
182
184
  listView?: ListViewConfig;
183
185
  options?: Options;
@@ -0,0 +1,16 @@
1
+ import { ExportConfig } from "../config/entities";
2
+ import { useConfig } from "./useConfig";
3
+
4
+ /**
5
+ * Returns the export configuration for the given entity.
6
+ * @returns export configuration.
7
+ */
8
+ export const useEntityExportConfig = (): ExportConfig => {
9
+ const { entityConfig } = useConfig();
10
+
11
+ if (!entityConfig.export) {
12
+ throw new Error("This entity config does not have an export field set");
13
+ }
14
+
15
+ return entityConfig.export;
16
+ };
@@ -41,11 +41,11 @@ export const EntityDetailView = (props: EntityDetailViewProps): JSX.Element => {
41
41
  const { push, query } = useRouter();
42
42
  const { entityConfig } = useConfig();
43
43
  const { mainColumn, sideColumn } = currentTab;
44
- const { detail, route: entityRoute } = entityConfig;
44
+ const { detail, hideTabs, route: entityRoute } = entityConfig;
45
45
  const { detailOverviews, top } = detail;
46
46
  const uuid = query.params?.[PARAMS_INDEX_UUID];
47
47
  const isDetailOverview = detailOverviews?.includes(currentTab.label);
48
- const tabs = getTabs(entityConfig);
48
+ const tabs = hideTabs ? [] : getTabs(entityConfig);
49
49
  const title = useEntityHeadTitle(response);
50
50
 
51
51
  if (!response) {
@@ -78,7 +78,13 @@ export const EntityDetailView = (props: EntityDetailViewProps): JSX.Element => {
78
78
  <ComponentCreator components={sideColumn} response={response} />
79
79
  ) : undefined
80
80
  }
81
- Tabs={<Tabs onTabChange={onTabChange} tabs={tabs} value={tabRoute} />}
81
+ Tabs={
82
+ hideTabs ? (
83
+ <></>
84
+ ) : (
85
+ <Tabs onTabChange={onTabChange} tabs={tabs} value={tabRoute} />
86
+ )
87
+ }
82
88
  top={<ComponentCreator components={top} response={response} />}
83
89
  />
84
90
  </Fragment>
@@ -0,0 +1,67 @@
1
+ import { useRouter } from "next/router";
2
+ import type { ParsedUrlQuery } from "querystring";
3
+ import React from "react";
4
+ import { EntityDetailViewProps } from "views/EntityDetailView/entityDetailView";
5
+ import { PARAMS_INDEX_EXPORT_METHOD } from "../../common/constants";
6
+ import { ComponentCreator } from "../../components/ComponentCreator/ComponentCreator";
7
+ import { BackPageView } from "../../components/Layout/components/BackPage/backPageView";
8
+ import { ExportMethodConfig } from "../../config/entities";
9
+ import { useEntityExportConfig } from "../../hooks/useEntityExportConfig";
10
+ import { useFetchEntity } from "../../hooks/useFetchEntity";
11
+ import { useUpdateURLCatalogParams } from "../../hooks/useUpdateURLCatalogParam";
12
+
13
+ export const EntityExportMethodView = (
14
+ props: EntityDetailViewProps
15
+ ): JSX.Element => {
16
+ // Update the catalog param if necessary.
17
+ useUpdateURLCatalogParams();
18
+
19
+ // Grab the entity to be exported.
20
+ const { response } = useFetchEntity(props);
21
+
22
+ // Get the column definitions for the entity export.
23
+ const { query } = useRouter();
24
+ const { exportMethods, tabs } = useEntityExportConfig();
25
+ const { sideColumn } = tabs[0];
26
+ const { mainColumn, top } = getExportMethodConfig(exportMethods, query) || {};
27
+
28
+ // Wait for the entity to be fetched.
29
+ if (!response) {
30
+ return <span></span>;
31
+ }
32
+
33
+ return (
34
+ <BackPageView
35
+ mainColumn={
36
+ <ComponentCreator components={mainColumn || []} response={response} />
37
+ }
38
+ sideColumn={
39
+ sideColumn ? (
40
+ <ComponentCreator components={sideColumn} response={response} />
41
+ ) : undefined
42
+ }
43
+ top={<ComponentCreator components={top || []} response={response} />}
44
+ />
45
+ );
46
+ };
47
+
48
+ /**
49
+ * Returns the export method configuration for the given pathname.
50
+ * @param exportMethods - Export methods config.
51
+ * @param query - Router query object.
52
+ * @returns export method configuration.
53
+ */
54
+ function getExportMethodConfig(
55
+ exportMethods: ExportMethodConfig[],
56
+ query: ParsedUrlQuery
57
+ ): ExportMethodConfig | undefined {
58
+ // Determine the selected export method from the URL.
59
+ const exportMethodRoute = query.params?.[PARAMS_INDEX_EXPORT_METHOD];
60
+ if (!exportMethodRoute) {
61
+ return;
62
+ }
63
+ // Find the config for the selected export method.
64
+ return exportMethods.find(({ route }) => {
65
+ return route.includes(exportMethodRoute);
66
+ });
67
+ }
@@ -0,0 +1,39 @@
1
+ import React from "react";
2
+ import { EntityDetailViewProps } from "views/EntityDetailView/entityDetailView";
3
+ import { ComponentCreator } from "../../components/ComponentCreator/ComponentCreator";
4
+ import { BackPageView } from "../../components/Layout/components/BackPage/backPageView";
5
+ import { useEntityExportConfig } from "../../hooks/useEntityExportConfig";
6
+ import { useFetchEntity } from "../../hooks/useFetchEntity";
7
+ import { useUpdateURLCatalogParams } from "../../hooks/useUpdateURLCatalogParam";
8
+
9
+ export const EntityExportView = (props: EntityDetailViewProps): JSX.Element => {
10
+ // Update the catalog param if necessary.
11
+ useUpdateURLCatalogParams();
12
+
13
+ // Grab the entity to be exported.
14
+ const { response } = useFetchEntity(props);
15
+
16
+ // Get the column definitions for the entity export.
17
+ const { tabs, top } = useEntityExportConfig();
18
+ const currentTab = tabs[0];
19
+ const { mainColumn, sideColumn } = currentTab;
20
+
21
+ // Wait for the entity to be fetched.
22
+ if (!response) {
23
+ return <span></span>;
24
+ }
25
+
26
+ return (
27
+ <BackPageView
28
+ mainColumn={
29
+ <ComponentCreator components={mainColumn} response={response} />
30
+ }
31
+ sideColumn={
32
+ sideColumn ? (
33
+ <ComponentCreator components={sideColumn} response={response} />
34
+ ) : undefined
35
+ }
36
+ top={<ComponentCreator components={top} response={response} />}
37
+ />
38
+ );
39
+ };