Skip to content

Commit efa3860

Browse files
committed
created ror component
Signed-off-by: Maximilian Inckmann <[email protected]>
1 parent cc1ff3e commit efa3860

File tree

4 files changed

+248
-5
lines changed

4 files changed

+248
-5
lines changed

packages/stencil-library/src/components/pid-component/pid-component.stories.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,19 @@ export const ORCID: Story = {
178178
},
179179
};
180180

181+
export const ROR: Story = {
182+
args: {
183+
value: 'https://ror.org/04t3en479',
184+
},
185+
parameters: {
186+
docs: {
187+
source: {
188+
code: `<pid-component value='https://ror.org/04t3en479'></pid-component>`,
189+
},
190+
},
191+
},
192+
};
193+
181194
export const Date: Story = {
182195
args: {
183196
value: '2022-11-11T08:01:20.557+00:00',
@@ -370,7 +383,7 @@ export const TypedPIDMakerExampleText: Story = {
370383
},
371384
decorators: [
372385
(story: () => unknown) => html`
373-
<p class="align-middle items-center w-2/3">
386+
<p class="w-2/3 items-center align-middle">
374387
The Typed PID Maker is an entry point to integrate digital resources into the FAIR Digital Object (FAIR DO) ecosystem. It allows creating PIDs for resources and to provide
375388
them with the necessary metadata to ensure that the resources can be found and understood. <br />
376389
As a result, a machine-readable representation of all kinds of research artifacts allows act on such FAIR Digital Objects which present themselves as PID, e.g., ${story()},
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
2+
import { FunctionalComponent, h } from '@stencil/core';
3+
import { GenericIdentifierType } from '../utils/GenericIdentifierType';
4+
import { FoldableItem } from '../utils/FoldableItem';
5+
import { FoldableAction } from '../utils/FoldableAction';
6+
7+
/**
8+
* This class specifies a custom renderer for Research Organization Registry (ROR) identifiers.
9+
* It fetches organization details from the ROR API v2 and displays them.
10+
* @extends GenericIdentifierType
11+
*/
12+
export class RORType extends GenericIdentifierType {
13+
private rorData: Record<string, any>;
14+
private label: string;
15+
private acronym: string;
16+
17+
getSettingsKey(): string {
18+
return 'RORType';
19+
}
20+
21+
/**
22+
* Checks if the provided value is a valid ROR ID format
23+
* ROR IDs typically have the format: https://ror.org/XXXXXXXXX where X is an alphanumeric character
24+
* @returns {boolean} Whether the value has the correct ROR ID format
25+
*/
26+
hasCorrectFormat(): boolean {
27+
const regex = new RegExp('^https?://ror.org/[0-9a-z]{9}$', 'i');
28+
return regex.test(this.value);
29+
}
30+
31+
/**
32+
* Extracts the ROR ID from the full URL
33+
* @returns {string} The ROR ID
34+
*/
35+
private getRorId(): string {
36+
return this.value.split('/').pop();
37+
}
38+
39+
/**
40+
* Fetches organization data from the ROR API v2
41+
* @returns {Promise<void>}
42+
*/
43+
async init(): Promise<void> {
44+
try {
45+
// Extract the ROR ID from the URL
46+
const rorId = this.getRorId();
47+
48+
// Fetch data from ROR API v2
49+
const response = await fetch(`https://api.ror.org/v2/organizations/${rorId}`);
50+
if (!response.ok) {
51+
throw new Error(`Failed to fetch ROR data: ${response.status}`);
52+
}
53+
54+
this.rorData = await response.json();
55+
56+
if (!this.rorData) return;
57+
58+
// Initialize name, acronym, and labels
59+
if (!this.rorData.names || this.rorData.names.length === 0) {
60+
this.label = 'Unknown';
61+
this.items.push(new FoldableItem(0, 'Name', 'Unknown', 'No names available for this organization'));
62+
return;
63+
} else {
64+
for (const name of this.rorData.names) {
65+
const types = name.types || [];
66+
if (types.includes('acronym')) {
67+
this.acronym = name.value;
68+
this.items.push(new FoldableItem(20, 'Acronym', name.value, 'Short form of the organization name'));
69+
} else if (types.includes('ror_display')) {
70+
this.label = name.value;
71+
this.items.push(new FoldableItem(1, 'Display Name', name.value, 'Name used for display purposes'));
72+
} else if (types.includes('alias')) {
73+
this.items.push(new FoldableItem(5, 'Alias', name.value, 'Alternative name for the organization'));
74+
} else if (types.includes('label')) {
75+
this.items.push(new FoldableItem(15, 'Label', name.value, 'Name in another language or script'));
76+
}
77+
}
78+
}
79+
80+
// Add ROR ID
81+
this.items.push(new FoldableItem(20, 'ROR ID', this.rorData.id, 'Unique identifier for the organization in the ROR registry', null, null, false));
82+
this.actions.push(new FoldableAction(10, 'View on ROR', this.rorData.id, 'primary'));
83+
84+
// Add status of the organization
85+
this.items.push(new FoldableItem(30, 'Status', this.rorData.status || 'Unknown', 'Current status of the organization in the ROR registry'));
86+
87+
// Add types of the organization
88+
if (!this.rorData.types || this.rorData.types.length === 0) {
89+
this.items.push(new FoldableItem(25, 'Type', 'Unknown', 'Type of organization'));
90+
} else {
91+
for (const type of this.rorData.types) {
92+
this.items.push(new FoldableItem(25, 'Type', type, 'Type of organization'));
93+
}
94+
}
95+
96+
// Add externel references
97+
if (this.rorData.links && this.rorData.relationships.links > 0) {
98+
for (const link of this.rorData.links) {
99+
if (link.type && link.type.isEmpty()) {
100+
this.items.push(new FoldableItem(35, `Link to ${link.type}`, link.value, 'External link related to the organization'));
101+
} else {
102+
this.items.push(new FoldableItem(35, `Link`, link.value, 'External link related to the organization'));
103+
}
104+
}
105+
}
106+
107+
// Add external identifiers
108+
if (this.rorData.external_ids && this.rorData.external_ids.length > 0) {
109+
for (const external of this.rorData.external_ids) {
110+
const type = external.type;
111+
const value = external.preferred || external.all[0];
112+
this.items.push(new FoldableItem(40, `External ID: ${type}`, value, `Identifier from another system: ${type}`));
113+
}
114+
}
115+
116+
// Add related organizations with tooltips for relationship types
117+
if (this.rorData.relationships && this.rorData.relationships.length > 0) {
118+
const relationshipTypes = {
119+
parent: {
120+
title: 'Parent Organization',
121+
tooltip: 'Organization that this organization is part of',
122+
},
123+
child: {
124+
title: 'Child Organization',
125+
tooltip: 'Organization that is part of this organization',
126+
},
127+
related: {
128+
title: 'Related Organization',
129+
tooltip: 'Organization that is related to this organization',
130+
},
131+
predecessor: {
132+
title: 'Predecessor Organization',
133+
tooltip: 'Organization that preceded this organization',
134+
},
135+
successor: {
136+
title: 'Successor Organization',
137+
tooltip: 'Organization that succeeded this organization',
138+
},
139+
};
140+
141+
for (const rel of this.rorData.relationships) {
142+
const relationType = relationshipTypes[rel.type] || { title: rel.type, tooltip: `${rel.type} organization` };
143+
144+
this.items.push(new FoldableItem(90, relationType.title, `https://ror.org/${rel.id}`, relationType.tooltip));
145+
}
146+
}
147+
148+
// Add locations with country code and coordinates
149+
if (this.rorData.locations && this.rorData.locations.length > 0) {
150+
for (const location of this.rorData.locations) {
151+
const details = location.geonames_details;
152+
if (details.country_code) {
153+
this.items.push(new FoldableItem(50, 'Country', details.country_code, 'Country where the organization is located'));
154+
}
155+
if (details.lat && details.lng) {
156+
this.items.push(new FoldableItem(55, 'Coordinates', `${details.lat}, ${details.lng}`, 'Geographic coordinates of the organization'));
157+
const osmUrl = `https://www.openstreetmap.org/?mlat=${details.lat}&mlon=${details.lng}&zoom=15`;
158+
this.actions.push(new FoldableAction(20, 'View on OpenStreetMap', osmUrl, 'secondary'));
159+
}
160+
}
161+
}
162+
} catch (error) {
163+
console.error('Error fetching ROR data:', error);
164+
// Add an error item
165+
this.items.push(new FoldableItem(0, 'Error', `Failed to fetch data from ROR API: ${error.message}`));
166+
}
167+
}
168+
169+
/**
170+
* Renders a preview of the ROR organization
171+
* @returns {FunctionalComponent} The preview component
172+
*/
173+
renderPreview(): FunctionalComponent {
174+
// If data is not yet loaded, show the ROR ID
175+
if (!this.rorData) {
176+
return <span class="font-mono text-sm">Loading ROR: {this.value}...</span>;
177+
}
178+
179+
// If data is loaded, show organization name and ID
180+
return (
181+
<span class={'inline-flex flex-nowrap items-center align-top font-mono'}>
182+
<svg
183+
xmlns="http://www.w3.org/2000/svg"
184+
// xmlns:xlink="http://www.w3.org/1999/xlink"
185+
// xmlns:serif="http://www.serif.com/"
186+
// width="100%"
187+
// height="100%"
188+
viewBox="0 0 164 118"
189+
version="1.1"
190+
// xml:space="preserve"
191+
class={'mr-1 h-5 flex-none items-center p-0.5'}
192+
style={{ fillRule: 'evenodd', clipRule: 'evenodd', strokeLinejoin: 'round', strokeMiterlimit: '2' }}
193+
>
194+
<g transform="matrix(0.994301,0,0,0.989352,0,0)">
195+
<rect x="0" y="0" width="164.94" height="119.27" style={{ fill: 'white' }} />
196+
</g>
197+
<g transform="matrix(1,0,0,1,-0.945,-0.815)">
198+
<path d="M68.65,4.16L56.52,22.74L44.38,4.16L68.65,4.16Z" style={{ fill: 'rgb(83,186,161)', fillRule: 'nonzero' }} />
199+
<path d="M119.41,4.16L107.28,22.74L95.14,4.16L119.41,4.16Z" style={{ fill: 'rgb(83,186,161)', fillRule: 'nonzero' }} />
200+
<path d="M44.38,115.47L56.52,96.88L68.65,115.47L44.38,115.47Z" style={{ fill: 'rgb(83,186,161)', fillRule: 'nonzero' }} />
201+
<path d="M95.14,115.47L107.28,96.88L119.41,115.47L95.14,115.47Z" style={{ fill: 'rgb(83,186,161)', fillRule: 'nonzero' }} />
202+
<path
203+
d="M145.53,63.71C149.83,62.91 153.1,61 155.33,57.99C157.57,54.98 158.68,51.32 158.68,47.03C158.68,43.47 158.06,40.51 156.83,38.13C155.6,35.75 153.93,33.86 151.84,32.45C149.75,31.05 147.31,30.04 144.53,29.44C141.75,28.84 138.81,28.54 135.72,28.54L112.16,28.54L112.16,47.37C111.97,46.82 111.77,46.28 111.55,45.74C109.92,41.79 107.64,38.42 104.71,35.64C101.78,32.86 98.32,30.72 94.3,29.23C90.29,27.74 85.9,26.99 81.14,26.99C76.38,26.99 72,27.74 67.98,29.23C63.97,30.72 60.5,32.86 57.57,35.64C54.95,38.13 52.85,41.1 51.27,44.54C51.04,42.07 50.46,39.93 49.53,38.13C48.3,35.75 46.63,33.86 44.54,32.45C42.45,31.05 40.01,30.04 37.23,29.44C34.45,28.84 31.51,28.54 28.42,28.54L4.87,28.54L4.87,89.42L18.28,89.42L18.28,65.08L24.9,65.08L37.63,89.42L53.71,89.42L38.24,63.71C42.54,62.91 45.81,61 48.04,57.99C48.14,57.85 48.23,57.7 48.33,57.56C48.31,58.03 48.3,58.5 48.3,58.98C48.3,63.85 49.12,68.27 50.75,72.22C52.38,76.17 54.66,79.54 57.59,82.32C60.51,85.1 63.98,87.24 68,88.73C72.01,90.22 76.4,90.97 81.16,90.97C85.92,90.97 90.3,90.22 94.32,88.73C98.33,87.24 101.8,85.1 104.73,82.32C107.65,79.54 109.93,76.17 111.57,72.22C111.79,71.69 111.99,71.14 112.18,70.59L112.18,89.42L125.59,89.42L125.59,65.08L132.21,65.08L144.94,89.42L161.02,89.42L145.53,63.71ZM36.39,50.81C35.67,51.73 34.77,52.4 33.68,52.83C32.59,53.26 31.37,53.52 30.03,53.6C28.68,53.69 27.41,53.73 26.2,53.73L18.29,53.73L18.29,39.89L27.06,39.89C28.26,39.89 29.5,39.98 30.76,40.15C32.02,40.32 33.14,40.65 34.11,41.14C35.08,41.63 35.89,42.33 36.52,43.25C37.15,44.17 37.47,45.4 37.47,46.95C37.47,48.6 37.11,49.89 36.39,50.81ZM98.74,66.85C97.85,69.23 96.58,71.29 94.91,73.04C93.25,74.79 91.26,76.15 88.93,77.13C86.61,78.11 84.01,78.59 81.15,78.59C78.28,78.59 75.69,78.1 73.37,77.13C71.05,76.16 69.06,74.79 67.39,73.04C65.73,71.29 64.45,69.23 63.56,66.85C62.67,64.47 62.23,61.85 62.23,58.98C62.23,56.17 62.67,53.56 63.56,51.15C64.45,48.74 65.72,46.67 67.39,44.92C69.05,43.17 71.04,41.81 73.37,40.83C75.69,39.86 78.28,39.37 81.15,39.37C84.02,39.37 86.61,39.86 88.93,40.83C91.25,41.8 93.24,43.17 94.91,44.92C96.57,46.67 97.85,48.75 98.74,51.15C99.63,53.56 100.07,56.17 100.07,58.98C100.07,61.85 99.63,64.47 98.74,66.85ZM143.68,50.81C142.96,51.73 142.06,52.4 140.97,52.83C139.88,53.26 138.66,53.52 137.32,53.6C135.97,53.69 134.7,53.73 133.49,53.73L125.58,53.73L125.58,39.89L134.35,39.89C135.55,39.89 136.79,39.98 138.05,40.15C139.31,40.32 140.43,40.65 141.4,41.14C142.37,41.63 143.18,42.33 143.81,43.25C144.44,44.17 144.76,45.4 144.76,46.95C144.76,48.6 144.4,49.89 143.68,50.81Z"
204+
style={{ fill: 'rgb(32,40,38)', fillRule: 'nonzero' }}
205+
/>
206+
</g>
207+
</svg>
208+
<span class={'flex-none items-center px-1'}>
209+
{this.label}
210+
{this.acronym ? ' (' + this.acronym + ')' : ''}
211+
</span>
212+
</span>
213+
);
214+
}
215+
216+
/**
217+
* Returns the data fetched from ROR
218+
* @returns {unknown} The data fetched from ROR API
219+
*/
220+
get data(): unknown {
221+
return this.rorData;
222+
}
223+
}

packages/stencil-library/src/utils/GenericIdentifierType.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ export abstract class GenericIdentifierType {
112112
* It must be implemented by the child classes as it is abstract.
113113
* @param data The data that is needed for rendering the component.
114114
* @abstract
115+
* @returns {Promise<void>} A promise that resolves when the initialization is complete
115116
*/
116117
abstract init(data?: unknown): Promise<void>;
117118

packages/stencil-library/src/utils/utils.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { URLType } from '../rendererModules/URLType';
1010
import { FallbackType } from '../rendererModules/FallbackType';
1111
import { LocaleType } from '../rendererModules/LocaleType';
1212
import { JSONType } from '../rendererModules/JSONType';
13+
import { RORType } from '../rendererModules/RORType';
1314

1415
/**
1516
* Array of all component objects that can be used to parse a given value, ordered by priority (lower is better)
@@ -37,26 +38,31 @@ export const renderers: {
3738
},
3839
{
3940
priority: 3,
41+
key: 'RORType',
42+
constructor: RORType,
43+
},
44+
{
45+
priority: 4,
4046
key: 'EmailType',
4147
constructor: EmailType,
4248
},
4349
{
44-
priority: 4,
50+
priority: 5,
4551
key: 'URLType',
4652
constructor: URLType,
4753
},
4854
{
49-
priority: 5,
55+
priority: 6,
5056
key: 'LocaleType',
5157
constructor: LocaleType,
5258
},
5359
{
54-
priority: 5,
60+
priority: 7,
5561
key: 'JSONType',
5662
constructor: JSONType,
5763
},
5864
{
59-
priority: 6,
65+
priority: 99,
6066
key: 'FallbackType',
6167
constructor: FallbackType,
6268
},

0 commit comments

Comments
 (0)