Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions components/editor/id-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { useEntityEditorTabs } from "@/lib/state/entity-editor-tabs-state"
import { Button } from "@/components/ui/button"
import { RenameEntityModal } from "@/components/modals/rename-entity-modal"
import { useGoToFileExplorer } from "@/lib/hooks"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"

export function IDField({ value }: { value: string }) {
const [, copy] = useCopyToClipboard()
Expand Down Expand Up @@ -71,15 +72,21 @@ export function IDField({ value }: { value: string }) {
}, [value])

return (
<div className="flex grow justify-start pl-3 items-center rounded-lg">
<div className="flex grow justify-start pl-3 items-center rounded-lg min-w-0">
<RenameEntityModal
entityId={value}
onOpenChange={setRenameEntityModalOpen}
open={renameEntityModalOpen}
/>

<Icon className="size-4 pointer-events-none text-muted-foreground mr-2 shrink-0" />
<span className="truncate grow">{value}</span>
<Tooltip delayDuration={500}>
<TooltipTrigger asChild>
<span className="truncate grow">{value}</span>
</TooltipTrigger>
<TooltipContent>{value}</TooltipContent>
</Tooltip>

<DropdownMenu>
<DropdownMenuTrigger className="p-2" asChild>
<Button variant={"outline"} id={"id-dropdown-trigger"}>
Expand Down
4 changes: 3 additions & 1 deletion components/entity-browser/entity-browser-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useContext } from "react"
import { CrateDataContext } from "@/components/providers/crate-data-provider"
import { Skeleton } from "@/components/ui/skeleton"
import { EntityBrowserItem } from "@/components/entity-browser/entity-browser-item"
import { useEditorState } from "@/lib/state/editor-state"

export function EntityBrowserContent({
defaultSectionOpen,
Expand All @@ -15,6 +16,7 @@ export function EntityBrowserContent({
onSectionOpenChange(): void
}) {
const crate = useContext(CrateDataContext)
const rootEntityId = useEditorState((s) => s.getRootEntityId())

if (!crate.crateData)
return (
Expand All @@ -33,7 +35,7 @@ export function EntityBrowserContent({

return (
<div id="entity-browser-content" className="flex flex-col p-2 overflow-y-auto">
<EntityBrowserItem entityId={"./"} />
{rootEntityId && <EntityBrowserItem entityId={rootEntityId} />}
<EntityBrowserSection
section={"Data"}
defaultSectionOpen={defaultSectionOpen}
Expand Down
70 changes: 65 additions & 5 deletions lib/backend/BrowserBasedCrateService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ import { opfsFunctions } from "@/lib/opfs-worker/functions"
import fileDownload from "js-file-download"
import { addBasePath } from "next/dist/client/add-base-path"
import { CrateServiceBase } from "@/lib/backend/CrateServiceBase"
import { changeEntityId, encodeFilePath, isDataEntity, isFolderDataEntity } from "@/lib/utils"
import {
changeEntityId,
encodeFilePath,
extractOrcidIdentifier,
extractRorIdentifier,
isDataEntity,
isFolderDataEntity
} from "@/lib/utils"
import * as z from "zod/mini"

const template: (name: string, description: string) => ICrate = (
Expand Down Expand Up @@ -292,12 +299,65 @@ export class BrowserBasedCrateService extends CrateServiceBase {
}
}

importEntityFromOrcid(): Promise<string> {
throw "Not supported in browser-based environment yet"
async importEntityFromOrcid(crateId: string, url: string): Promise<string> {
const orcid = extractOrcidIdentifier(url)

const req = await fetch(`https://pub.orcid.org/v3.0/${orcid}`, {
headers: {
Accept: "application/json"
}
})
if (req.ok) {
const json = (await req.json()) as OrcidProfile

const entity: IEntity = {
"@id": "https://orcid.org/" + json["orcid-identifier"].path,
"@type": "Person",
name:
json.person.name["given-names"].value +
" " +
json.person.name["family-name"].value
}

if (await this.createEntity(crateId, entity)) {
return entity["@id"]
} else {
throw "Could not create entity. Is the identifier already in use?"
}
} else {
throw `Could not fetch ORCID profile (${req.status})`
}
}

importOrganizationFromRor(): Promise<string> {
throw "Not supported in browser-based environment yet"
async importOrganizationFromRor(crateId: string, url: string): Promise<string> {
const orcid = extractRorIdentifier(url)

const req = await fetch(`https://api.ror.org/v2/organizations/${orcid}`, {
headers: {
Accept: "application/json"
}
})
if (req.ok) {
const json = (await req.json()) as RorRecord

const entity: IEntity = {
"@id": json.id,
"@type": "Organization",
name:
json.names.find(
(n) => n.types.includes("ror_display") || n.types.includes("label")
)?.value ?? "",
url: json.links.find((l) => l.type === "website")?.value ?? json.id
}

if (await this.createEntity(crateId, entity)) {
return entity["@id"]
} else {
throw "Could not create entity. Is the identifier already in use?"
}
} else {
throw `Could not fetch ROR organization (${req.status})`
}
}

async addCustomContextPair(crateId: string, key: string, value: string) {
Expand Down
255 changes: 255 additions & 0 deletions lib/backend/types/OrcidProfileInterface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
interface OrcidProfile {
'orcid-identifier': OrcidIdentifier;
preferences: Preferences;
history: History;
person: Person;
'activities-summary': ActivitiesSummary;
path: string;
}

interface OrcidIdentifier {
uri: string;
path: string;
host: string;
}

interface Preferences {
locale: string;
}

interface History {
'creation-method': string;
'completion-date': string | null;
'submission-date': DateValue;
'last-modified-date': DateValue;
claimed: boolean;
source: null;
'deactivation-date': string | null;
'verified-email': boolean;
'verified-primary-email': boolean;
}

interface DateValue {
value: number;
}

interface Person {
'last-modified-date': DateValue | null;
name: Name;
'other-names': OtherNames;
biography: null;
'researcher-urls': ResearcherUrls;
emails: Emails;
addresses: Addresses;
keywords: Keywords;
'external-identifiers': ExternalIdentifiers;
path: string;
}

interface Name {
'created-date': DateValue;
'last-modified-date': DateValue;
'given-names': ValueObject;
'family-name': ValueObject;
'credit-name': null;
source: null;
visibility: string;
path: string;
}

interface ValueObject {
value: string;
}

interface OtherNames {
'last-modified-date': DateValue | null;
'other-name': any[];
path: string;
}

interface ResearcherUrls {
'last-modified-date': DateValue | null;
'researcher-url': any[];
path: string;
}

interface Emails {
'last-modified-date': DateValue | null;
email: any[];
path: string;
}

interface Addresses {
'last-modified-date': DateValue | null;
address: any[];
path: string;
}

interface Keywords {
'last-modified-date': DateValue | null;
keyword: any[];
path: string;
}

interface ExternalIdentifiers {
'last-modified-date': DateValue | null;
'external-identifier': any[];
path: string;
}

interface ActivitiesSummary {
'last-modified-date': DateValue;
distinctions: AffiliationSection;
educations: AffiliationSection;
employments: EmploymentsSection;
fundings: GroupSection;
'invited-positions': AffiliationSection;
memberships: AffiliationSection;
'peer-reviews': GroupSection;
qualifications: AffiliationSection;
'research-resources': GroupSection;
services: AffiliationSection;
works: WorksSection;
path: string;
}

interface AffiliationSection {
'last-modified-date': DateValue | null;
'affiliation-group': AffiliationGroup[];
path: string;
}

interface EmploymentsSection {
'last-modified-date': DateValue | null;
'affiliation-group': EmploymentAffiliationGroup[];
path: string;
}

interface AffiliationGroup {
'last-modified-date': DateValue;
'external-ids': ExternalIds;
summaries: any[];
}

interface EmploymentAffiliationGroup {
'last-modified-date': DateValue;
'external-ids': ExternalIds;
summaries: EmploymentSummaryWrapper[];
}

interface EmploymentSummaryWrapper {
'employment-summary': EmploymentSummary;
}

interface EmploymentSummary {
'created-date': DateValue;
'last-modified-date': DateValue;
source: Source;
'put-code': number;
'department-name': string;
'role-title': string;
'start-date': PartialDate;
'end-date': PartialDate | null;
organization: Organization;
url: string | null;
'external-ids': ExternalIds | null;
'display-index': string;
visibility: string;
path: string;
}

interface Source {
'source-orcid': OrcidIdentifier | null;
'source-client-id': ClientId | null;
'source-name': ValueObject;
'assertion-origin-orcid': null;
'assertion-origin-client-id': null;
'assertion-origin-name': null;
}

interface ClientId {
uri: string;
path: string;
host: string;
}

interface PartialDate {
year: ValueObject | null;
month: ValueObject | null;
day: ValueObject | null;
}

interface Organization {
name: string;
address: Address;
'disambiguated-organization': DisambiguatedOrganization;
}

interface Address {
city: string;
region: string | null;
country: string;
}

interface DisambiguatedOrganization {
'disambiguated-organization-identifier': string;
'disambiguation-source': string;
}

interface ExternalIds {
'external-id': ExternalId[];
}

interface ExternalId {
'external-id-type': string;
'external-id-value': string;
'external-id-normalized': NormalizedValue;
'external-id-normalized-error': null;
'external-id-url': ValueObject;
'external-id-relationship': string;
}

interface NormalizedValue {
value: string;
transient: boolean;
}

interface GroupSection {
'last-modified-date': DateValue | null;
group: any[];
path: string;
}

interface WorksSection {
'last-modified-date': DateValue;
group: WorkGroup[];
path: string;
}

interface WorkGroup {
'last-modified-date': DateValue;
'external-ids': ExternalIds;
'work-summary': WorkSummary[];
}

interface WorkSummary {
'put-code': number;
'created-date': DateValue;
'last-modified-date': DateValue;
source: Source;
title: Title;
'external-ids': ExternalIds;
url: ValueObject;
type: string;
'publication-date': PartialDate;
'journal-title': null;
visibility: string;
path: string;
'display-index': string;
}

interface Title {
title: ValueObject;
subtitle: null;
'translated-title': null;
}
Loading