diff --git a/app/profile/compare/comparePage.js b/app/profile/compare/comparePage.js
index 89d41b10a..1f6ada1b8 100644
--- a/app/profile/compare/comparePage.js
+++ b/app/profile/compare/comparePage.js
@@ -4,13 +4,14 @@
import { useSearchParams } from 'next/navigation'
import { Suspense, useEffect, useState } from 'react'
+import ErrorDisplay from '../../../components/ErrorDisplay'
+import LoadingSpinner from '../../../components/LoadingSpinner'
import api from '../../../lib/api-client'
-import { prettyId } from '../../../lib/utils'
+import { getNoteAuthorIds, getNoteAuthors, prettyId } from '../../../lib/utils'
+import CommonLayout from '../../CommonLayout'
import Compare from './Compare'
+
import styles from './Compare.module.scss'
-import ErrorDisplay from '../../../components/ErrorDisplay'
-import CommonLayout from '../../CommonLayout'
-import LoadingSpinner from '../../../components/LoadingSpinner'
const addMetadata = (profile, fieldName) => {
const localProfile = { ...profile }
@@ -111,14 +112,10 @@ function Page() {
publication.apiVersion === 1
? publication.content.title
: publication.content.title.value,
- authors:
- publication.apiVersion === 1
- ? publication.content.authors
- : publication.content.authors.value,
- authorids: (publication.apiVersion === 1
- ? publication.content.authorids
- : publication.content.authorids.value
- ).filter((id) => id),
+ authors: getNoteAuthors(publication, publication.apiVersion !== 1),
+ authorids: getNoteAuthorIds(publication, publication.apiVersion !== 1).filter(
+ (id) => id
+ ),
}))
}
return []
diff --git a/app/profile/edit/page.js b/app/profile/edit/page.js
index a51580234..3e2c2c8c3 100644
--- a/app/profile/edit/page.js
+++ b/app/profile/edit/page.js
@@ -1,15 +1,17 @@
'use client'
+import { useRouter } from 'next/navigation'
/* globals promptMessage,promptError: false */
import { useEffect, useState } from 'react'
-import { useRouter } from 'next/navigation'
+import LoadingSpinner from '../../../components/LoadingSpinner'
import ProfileEditor from '../../../components/profile/ProfileEditor'
+import useUser from '../../../hooks/useUser'
import api from '../../../lib/api-client'
import { formatProfileData } from '../../../lib/profiles'
-import useUser from '../../../hooks/useUser'
+import { getNoteAuthorIds, prettyId } from '../../../lib/utils'
import LimitedStateAlert from './LimitedStateAlert'
+
import styles from './Edit.module.scss'
-import LoadingSpinner from '../../../components/LoadingSpinner'
export default function Page() {
const [profile, setProfile] = useState(null)
@@ -23,10 +25,10 @@ export default function Page() {
let authorIds
let invitation
if (note.invitations) {
- authorIds = note.content.authorids?.value
+ authorIds = getNoteAuthorIds(note, true)
invitation = note.invitations[0]
} else {
- authorIds = note.content.authorids
+ authorIds = getNoteAuthorIds(note, false)
// oxlint-disable-next-line prefer-destructuring
invitation = note.invitation
}
@@ -38,6 +40,7 @@ export default function Page() {
'OpenReview.net/Archive/-/Direct_Upload_Revision',
'DBLP.org/-/Record': 'DBLP.org/-/Author_Coreference',
[`${process.env.SUPER_USER}/Public_Article/ORCID.org/-/Record`]: `${process.env.SUPER_USER}/Public_Article/-/Author_Removal`,
+ [`${process.env.SUPER_USER}/Public_Article/DBLP.org/-/Record`]: `${process.env.SUPER_USER}/Public_Article/-/Author_Removal`,
}
if (!authorIds) {
throw new Error(`Note ${noteId} is missing author ids`)
@@ -73,6 +76,10 @@ export default function Page() {
content: {
author_index: { value: matchedIdx[0] },
author_id: { value: '' },
+ ...(invitationMap[invitation] ===
+ `${process.env.SUPER_USER}/Public_Article/-/Author_Removal` && {
+ author_name: { value: prettyId(profileId) },
+ }),
},
}
: {
diff --git a/components/DblpPublicationTable.js b/components/DblpPublicationTable.js
index 317b7717e..e088e191a 100644
--- a/components/DblpPublicationTable.js
+++ b/components/DblpPublicationTable.js
@@ -17,28 +17,39 @@ export default function DblpPublicationTable({
orPublicationsImportedByOtherProfile,
maxNumberofPublicationsToImport,
}) {
+ const { user } = useUser(true)
const [profileIdsRequested, setProfileIdsRequested] = useState([])
- const pubsCouldNotImport = [] // either existing or associated with other profile
- const pubsCouldImport = []
- dblpPublications.forEach((dblpPub) => {
- const titleMatch = (orPub) =>
- orPub.title === dblpPub.formattedTitle &&
- orPub.authorCount === dblpPub.authorCount &&
- orPub.venue === dblpPub.venue
- const existing = orPublications.find(titleMatch)
- const existingWithOtherProfile = orPublicationsImportedByOtherProfile.find(titleMatch)
- if (existing || existingWithOtherProfile || dblpPub.authorIndex === -1) {
- pubsCouldNotImport.push(dblpPub.key)
- } else {
- pubsCouldImport.push(dblpPub.key)
+
+ const categorizedDblpPublications = dblpPublications.map((dblpPub) => {
+ const externalIdOrtitleMatch = (orPub) =>
+ orPub.externalId === dblpPub.externalId ||
+ (orPub.title === dblpPub.formattedTitle &&
+ orPub.authorCount === dblpPub.authorCount &&
+ orPub.venue === dblpPub.venue)
+ const existing = orPublications.find(externalIdOrtitleMatch)
+ const existingWithOtherProfile =
+ orPublicationsImportedByOtherProfile.find(externalIdOrtitleMatch)
+
+ return {
+ ...dblpPub,
+ existing,
+ existingWithOtherProfile,
+ couldNotImport: existing || existingWithOtherProfile || dblpPub.authorIndex === -1,
}
})
- const allExistInOpenReview = dblpPublications.length === pubsCouldNotImport.length
- const allChecked =
- dblpPublications.length - pubsCouldNotImport.length === selectedPublications.length
- const dblpPublicationsGroupedByYear = groupBy(dblpPublications, (p) => p.year)
+ const pubsCouldNotImport = categorizedDblpPublications.flatMap((p) =>
+ p.couldNotImport ? [p.key] : []
+ )
+ const pubsCouldImport = categorizedDblpPublications
+ .map((p) => p.key)
+ .filter((key) => !pubsCouldNotImport.includes(key))
+ const allExistInOpenReview = categorizedDblpPublications.length === pubsCouldNotImport.length
+ const allChecked =
+ categorizedDblpPublications.length - pubsCouldNotImport.length ===
+ selectedPublications.length
+ const dblpPublicationsGroupedByYear = groupBy(categorizedDblpPublications, (p) => p.year)
const toggleSelectAll = (checked) => {
if (!checked) {
setSelectedPublications([])
@@ -156,13 +167,9 @@ export default function DblpPublicationTable({
>
),
body: publicationsOfYear.map((publication) => {
- const titleMatch = (orPub) =>
- orPub.title === publication.formattedTitle &&
- orPub.authorCount === publication.authorCount &&
- orPub.venue === publication.venue
- const existingPublication = orPublications.find(titleMatch)
+ const existingPublication = publication.existing
const existingPublicationOfOtherProfile =
- orPublicationsImportedByOtherProfile.find(titleMatch)
+ publication.existingWithOtherProfile
const category = existingPublication
? 'existing-publication'
: existingPublicationOfOtherProfile
@@ -186,6 +193,7 @@ export default function DblpPublicationTable({
source={publication.source}
profileIdsRequested={profileIdsRequested}
setProfileIdsRequested={setProfileIdsRequested}
+ user={user}
/>
)
}),
@@ -217,8 +225,8 @@ const DblpPublicationRow = ({
source,
profileIdsRequested,
setProfileIdsRequested,
+ user,
}) => {
- const { user } = useUser(true)
const [error, setError] = useState(null)
const [profileMergeStatus, setProfileMergeStatus] = useState(null)
const profileMergeInvitationId = `${process.env.SUPER_USER}/Support/-/Profile_Merge`
diff --git a/components/EditorComponents/ContentFieldEditor.js b/components/EditorComponents/ContentFieldEditor.js
index b623fe259..5c37fc2b3 100644
--- a/components/EditorComponents/ContentFieldEditor.js
+++ b/components/EditorComponents/ContentFieldEditor.js
@@ -1,5 +1,5 @@
import { useContext, useState } from 'react'
-import { get, set } from 'lodash'
+import { get, set, unset } from 'lodash'
import EditorComponentContext from '../EditorComponentContext'
import { Tab, TabList, TabPanel, TabPanels, Tabs } from '../Tabs'
import CodeEditorWidget from './CodeEditorWidget'
@@ -150,6 +150,10 @@ const JsonEditor = ({ existingFields, onFieldChange }) => {
// { value: 'group', description: 'Group ID (Profile Search)' },
// { value: 'profile', description: 'Profile ID (Profile Search)' },
{ value: 'group[]', description: 'Group ID Array (Profile Search)' },
+ {
+ value: 'author{}',
+ description: 'Author Object (Profile Search with Institution)',
+ },
// { value: 'profile[]', description: 'Profile ID Array (Profile Search)' },
// { value: 'note', description: 'Note ID (Not implemented)' },
// { value: 'note[]', description: 'Note ID Array (Not implemented)' },
@@ -187,7 +191,7 @@ const JsonEditor = ({ existingFields, onFieldChange }) => {
},
},
shouldBeShown: (formData) =>
- !['const', 'file', 'date', 'boolean', 'json'].includes(formData.dataType),
+ !['const', 'file', 'date', 'boolean', 'json', 'author{}'].includes(formData.dataType),
getValue: function (existingValue) {
return get(existingValue, this.valuePath)
},
@@ -432,12 +436,63 @@ const JsonEditor = ({ existingFields, onFieldChange }) => {
},
valuePath: 'value.param.markdown',
},
+ institution: {
+ order: 9,
+ description: 'whether to include institution info',
+ value: {
+ param: {
+ input: 'checkbox',
+ type: 'boolean',
+ enum: [{ value: true, description: 'Include institution info' }],
+ optional: true,
+ },
+ },
+ shouldBeShown: (formData) => formData.dataType === 'author{}',
+ getValue: function (existingValue) {
+ const hasInstitution = get(existingValue, this.valuePath)
+ return !!hasInstitution
+ },
+ valuePath: 'value.param.properties.institutions',
+ },
}
const onFormChange = (updatedForm) => {
const updatedField = Object.entries(fieldSpecificationsOfJsonField).reduce(
(prev, [key, config]) => {
- if (config.valuePath) {
+ if (key === 'dataType' && updatedForm[key] === 'author{}') {
+ set(prev, config.valuePath, updatedForm[key])
+ set(prev, 'value.param.properties', {
+ fullname: { param: { type: 'string' } },
+ username: { param: { type: 'string' } },
+ })
+ } else if (key === 'institution') {
+ const includeInstitution = updatedForm[key]
+ // eslint-disable-next-line no-unused-expressions
+ includeInstitution
+ ? set(prev, 'value.param.properties.institutions', {
+ param: {
+ type: 'object{}',
+ properties: {
+ name: {
+ param: {
+ type: 'string',
+ },
+ },
+ domain: {
+ param: {
+ type: 'string',
+ },
+ },
+ country: {
+ param: {
+ type: 'string',
+ },
+ },
+ },
+ },
+ })
+ : unset(prev, 'value.param.properties.institutions', undefined)
+ } else if (config.valuePath) {
set(prev, config.valuePath, updatedForm[key])
} else {
set(prev, key, updatedForm[key])
diff --git a/components/EditorComponents/ProfileSearchWithInstitutionWidget.js b/components/EditorComponents/ProfileSearchWithInstitutionWidget.js
new file mode 100644
index 000000000..39abb1c5a
--- /dev/null
+++ b/components/EditorComponents/ProfileSearchWithInstitutionWidget.js
@@ -0,0 +1,618 @@
+/* globals promptError, $: false */
+
+import { useCallback, useContext, useEffect, useRef, useState } from 'react'
+import {
+ DndContext,
+ PointerSensor,
+ pointerWithin,
+ TouchSensor,
+ useSensor,
+ useSensors,
+} from '@dnd-kit/core'
+import { throttle, maxBy } from 'lodash'
+import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable'
+import useUser from '../../hooks/useUser'
+import EditorComponentContext from '../EditorComponentContext'
+import styles from '../../styles/components/ProfileSearchWidget.module.scss'
+import api from '../../lib/api-client'
+import { getProfileName, inflect, isValidEmail } from '../../lib/utils'
+import Icon from '../Icon'
+import IconButton from '../IconButton'
+import MultiSelectorDropdown from '../MultiSelectorDropdown'
+import LoadingSpinner from '../LoadingSpinner'
+import PaginationLinks from '../PaginationLinks'
+
+const mapDisplayAuthorsToEditorValue = (displayAuthors, hasInstitutionProperty) =>
+ displayAuthors.map((p) => ({
+ username: p.username,
+ fullname: p.fullname,
+ ...(hasInstitutionProperty && {
+ institutions: p.selectedInstitutions.map((institution) => ({
+ name: institution.name,
+ domain: institution.domain,
+ country: institution.country,
+ })),
+ }),
+ }))
+
+const getTitle = (profile) => {
+ if (!profile.content) return null
+ const latestHistory =
+ profile.content.history?.find((p) => !p.end) || maxBy(profile.content.history, 'end')
+
+ const title = latestHistory
+ ? `${latestHistory.position ? `${latestHistory.position}` : ''}${
+ latestHistory.institution?.name ? ` at ${latestHistory.institution?.name}` : ''
+ }${latestHistory.institution?.domain ? ` (${latestHistory.institution?.domain})` : ''}`
+ : ''
+ return title
+}
+
+const getCurrentInstitutionOptionsFromProfile = (profile) => {
+ const institutionOptions = []
+ const currentYear = new Date().getFullYear()
+ profile?.content?.history?.forEach((history) => {
+ const { institution } = history
+ if (institutionOptions.find((p) => p.value === institution.domain)) return
+ if (history.end && history.end < currentYear) return
+ institutionOptions.push({
+ label: `${institution.name} (${institution.domain})`,
+ value: institution.domain,
+ name: institution.name,
+ domain: institution.domain,
+ country: institution.country,
+ })
+ })
+ return institutionOptions
+}
+
+const checkIsInAuthorList = (selectedAuthors, profileToCheck) => {
+ const profileIds = profileToCheck.content?.names?.map((p) => p.username) ?? []
+ return selectedAuthors?.find((p) => profileIds.includes(p.username))
+}
+
+const Author = ({
+ fieldName,
+ username,
+ profile,
+ showArrowButton,
+ showDragSort,
+ displayAuthors,
+ setDisplayAuthors,
+ allowAddRemove,
+ allowInstitutionChange,
+ hasInstitutionProperty,
+ onChange,
+ clearError,
+ hasError,
+}) => {
+ const { listeners, setNodeRef, transform, transition, isDragging } = useSortable({
+ id: username,
+ })
+
+ const style = {
+ transform: transform ? `translate(${transform.x}px, ${transform.y}px)` : 'none',
+ transition,
+ }
+
+ const increaseAuthorIndex = () => {
+ const authorIndex = displayAuthors.findIndex((p) => p.username === username)
+ const updatedValue = [...displayAuthors]
+ updatedValue.splice(authorIndex, 1)
+ updatedValue.splice(authorIndex + 1, 0, displayAuthors[authorIndex])
+
+ setDisplayAuthors(updatedValue)
+ onChange({
+ fieldName,
+ value: mapDisplayAuthorsToEditorValue(updatedValue, hasInstitutionProperty),
+ })
+ clearError?.()
+ }
+
+ const handleInstitutionChange = (newValues) => {
+ const updatedAuthors = displayAuthors.map((p) => {
+ if (p.username !== username) return p
+ const selectedInstitutions = p.institutionOptions.filter((option) =>
+ newValues.includes(option.value)
+ )
+ return {
+ ...p,
+ selectedInstitutions,
+ }
+ })
+ setDisplayAuthors(updatedAuthors)
+ onChange({
+ fieldName,
+ value: mapDisplayAuthorsToEditorValue(updatedAuthors, true),
+ })
+ clearError?.()
+ }
+
+ if (!profile) return null
+
+ return (
+
+
+ {showDragSort && (
+
+
+
+ )}
+
+
+ {allowAddRemove && (
+ {
+ const updatedAuthors = displayAuthors.filter((p) => p.username !== username)
+
+ setDisplayAuthors(updatedAuthors)
+ onChange({
+ fieldName,
+ value: mapDisplayAuthorsToEditorValue(
+ updatedAuthors,
+ hasInstitutionProperty
+ ),
+ })
+ clearError?.()
+ }}
+ extraClasses="remove-button"
+ />
+ )}
+ {showArrowButton && }
+
+
+
+ {hasInstitutionProperty && (
+
+ p.domain)}
+ setSelectedValues={handleInstitutionChange}
+ displayTextFn={(selectedValues) => {
+ if (!profile.institutionOptions.length) return 'No Active Institution'
+ return selectedValues.length === 0
+ ? `${allowInstitutionChange ? 'Add' : 'No'} Institution`
+ : `${inflect(selectedValues.length, 'Institution', 'Institutions', true)} added`
+ }}
+ optionsDisabled={!allowInstitutionChange}
+ />
+
+ )}
+
+ )
+}
+
+const ProfileSearchResultRow = ({
+ profile,
+ setProfileSearchResults,
+ setSearchTerm,
+ displayAuthors,
+ setDisplayAuthors,
+ field,
+ onChange,
+ clearError,
+ hasInstitutionProperty,
+}) => {
+ const fieldName = Object.keys(field ?? {})[0]
+
+ if (!profile) return null
+ const preferredId = profile.content.names?.find((p) => p.preferred)?.username ?? profile.id
+
+ return (
+
+
+
+
{getTitle(profile)}
+
+
+ {profile.content?.emailsConfirmed?.map((email, index) => (
+ {email}
+ ))}
+
+
+ {
+ const institutionOptions = getCurrentInstitutionOptionsFromProfile(profile)
+ const updatedAuthors = displayAuthors.concat({
+ username: preferredId,
+ fullname: getProfileName(profile),
+ institutionOptions,
+ selectedInstitutions: institutionOptions.length ? [institutionOptions[0]] : [],
+ })
+ setDisplayAuthors(updatedAuthors)
+ onChange({
+ fieldName,
+ value: mapDisplayAuthorsToEditorValue(updatedAuthors, hasInstitutionProperty),
+ })
+
+ clearError?.()
+ setProfileSearchResults(null)
+ setSearchTerm('')
+ }}
+ />
+
+
+ )
+}
+
+const ProfileSearchFormAndResults = ({
+ displayAuthors,
+ setDisplayAuthors,
+ error,
+ field,
+ onChange,
+ clearError,
+ hasInstitutionProperty,
+}) => {
+ const [searchTerm, setSearchTerm] = useState(null)
+ const [isLoading, setIsLoading] = useState(false)
+ const [pageNumber, setPageNumber] = useState(null)
+ const [totalCount, setTotalCount] = useState(0)
+ const [profileSearchResults, setProfileSearchResults] = useState(null)
+ const { accessToken } = useUser()
+ const pageSize = 20
+
+ // eslint-disable-next-line no-shadow
+ const searchProfiles = async (searchTerm, pageNumber, showLoadingSpinner = true) => {
+ const cleanSearchTerm = searchTerm.trim()
+ let paramKey = 'fullname'
+ let paramValue = cleanSearchTerm.toLowerCase()
+ if (isValidEmail(cleanSearchTerm)) {
+ paramKey = 'email'
+ } else if (cleanSearchTerm.startsWith('~')) {
+ paramKey = 'id'
+ paramValue = cleanSearchTerm
+ }
+
+ if (showLoadingSpinner) setIsLoading(true)
+ try {
+ const results = await api.get(
+ '/profiles/search',
+ {
+ [paramKey]: paramValue,
+ es: true,
+ limit: pageSize,
+ offset: pageSize * (pageNumber - 1),
+ },
+ { accessToken }
+ )
+ setTotalCount(results.count > 200 ? 200 : results.count)
+ setProfileSearchResults(results.profiles.filter((p) => p.content.emails?.length))
+ } catch (apiError) {
+ promptError(apiError.message)
+ }
+ if (showLoadingSpinner) setIsLoading(false)
+ }
+
+ const displayResults = () => {
+ if (!profileSearchResults) return null
+ if (!profileSearchResults.length)
+ return (
+
+
+ No results found for your search query.
+
+
+ )
+
+ return (
+
+ <>
+ {profileSearchResults.map((profile, index) => (
+
+ ))}
+
+ >
+
+ )
+ }
+
+ useEffect(() => {
+ if (!searchTerm || !pageNumber) return
+ searchProfiles(searchTerm, pageNumber, false)
+ }, [pageNumber])
+
+ useEffect(() => {
+ if (!profileSearchResults?.length) return
+ $('[data-toggle="tooltip"]').tooltip()
+ }, [profileSearchResults])
+
+ if (isLoading) return
+ return (
+ <>
+
+
+ {displayResults()}
+ >
+ )
+}
+
+const ProfileSearchWithInstitutionWidget = () => {
+ const { user, accessToken, isRefreshing } = useUser()
+ const {
+ field,
+ onChange,
+ value,
+ error,
+ error: { index: errorIndex } = {},
+ clearError,
+ } = useContext(EditorComponentContext)
+
+ const reorderOnly = Array.isArray(field?.authors?.value)
+ const allowAddRemove = !reorderOnly && !field.authors?.value.param.elements // reorder with institution change
+ const allowInstitutionChange = !reorderOnly
+
+ const hasInstitutionProperty =
+ field?.authors?.value?.param?.properties?.institutions || // add institution
+ field?.authors?.value?.param?.elements || // reorder with institution change
+ (reorderOnly && field.authors.value?.[0]?.institutions) // reorder only institution exists
+
+ const sensors = useSensors(useSensor(PointerSensor), useSensor(TouchSensor))
+ const [displayAuthors, setDisplayAuthors] = useState(null)
+ const displayAuthorsRef = useRef(displayAuthors)
+
+ const handleDragOver = useCallback(
+ throttle((event) => {
+ const { active, over } = event
+
+ if (over && active.id !== over.id) {
+ const currentAuthors = displayAuthorsRef.current
+ const oldIndex = currentAuthors.findIndex((p) => p.username === active.id)
+ const newIndex = currentAuthors.findIndex((p) => p.username === over.id)
+ const updatedValue = arrayMove(currentAuthors, oldIndex, newIndex)
+ setDisplayAuthors(updatedValue)
+ }
+ }, 250),
+ []
+ )
+
+ const handleDragEnd = () => {
+ onChange({
+ fieldName: 'authors',
+ value: mapDisplayAuthorsToEditorValue(displayAuthors, hasInstitutionProperty),
+ })
+ clearError?.()
+ }
+
+ const getProfiles = async (authorIds) => {
+ try {
+ const { profiles } = await api.post(
+ '/profiles/search',
+ {
+ ids: authorIds,
+ },
+ { accessToken }
+ )
+ return profiles
+ } catch (apiError) {
+ promptError(apiError.message)
+ return []
+ }
+ }
+
+ const setDisplayAuthorNewEditor = async () => {
+ const profileResults = await getProfiles([user.profile.id])
+ const currentUserProfile = profileResults?.[0]
+ const institutionOptions = getCurrentInstitutionOptionsFromProfile(currentUserProfile)
+ const username = user.profile.id
+ const fullname = getProfileName(currentUserProfile)
+ const selectedInstitutions = institutionOptions.length ? [institutionOptions[0]] : []
+ const authors = [
+ {
+ username,
+ fullname,
+ selectedInstitutions,
+ institutionOptions,
+ },
+ ]
+ setDisplayAuthors(authors)
+ onChange({
+ fieldName: 'authors',
+ value: mapDisplayAuthorsToEditorValue(authors, hasInstitutionProperty),
+ })
+ }
+
+ const setDisplayAuthorExistingValue = async () => {
+ const authorIds = value.map((p) => p.username)
+ const profileResults = await getProfiles(authorIds)
+ if (!hasInstitutionProperty) {
+ setDisplayAuthors(value)
+ return
+ }
+ const authors = value.map((author) => {
+ const profile = profileResults.find((p) =>
+ p.content.names.find((q) => q.username === author.username)
+ )
+ const institutionOptionsFromProfile = getCurrentInstitutionOptionsFromProfile(profile)
+ const institutionOptionsFromValue = author.institutions.flatMap((institution) => {
+ if (institutionOptionsFromProfile.find((p) => p.domain === institution.domain))
+ // institution selected before still exist in profile
+ return []
+ return {
+ label: `${institution.name} (${institution.domain})`,
+ value: institution.domain,
+ name: institution.name,
+ domain: institution.domain,
+ country: institution.country,
+ }
+ })
+
+ return {
+ username: author.username,
+ fullname: author.fullname,
+ selectedInstitutions: author.institutions,
+ institutionOptions: [...institutionOptionsFromValue, ...institutionOptionsFromProfile],
+ }
+ })
+ setDisplayAuthors(authors)
+ }
+
+ useEffect(() => {
+ if (isRefreshing) return
+ if (!value) {
+ setDisplayAuthorNewEditor()
+ return
+ }
+ setDisplayAuthorExistingValue()
+ }, [isRefreshing])
+
+ useEffect(() => {
+ if (!value?.length) return
+ $('[data-toggle="tooltip"]').tooltip()
+ displayAuthorsRef.current = displayAuthors
+ }, [value, displayAuthors])
+
+ return (
+
+
+
+ p.username) ?? []}
+ strategy={() => null}
+ >
+ {displayAuthors?.map((profile, index) => {
+ const showArrowButton =
+ displayAuthors.length !== 1 && index !== displayAuthors.length - 1
+ const hasError = errorIndex === index
+ return (
+ 5}
+ displayAuthors={displayAuthors}
+ setDisplayAuthors={setDisplayAuthors}
+ allowAddRemove={allowAddRemove}
+ allowInstitutionChange={allowInstitutionChange}
+ hasInstitutionProperty={hasInstitutionProperty}
+ onChange={onChange}
+ clearError={clearError}
+ hasError={hasError}
+ />
+ )
+ })}
+
+
+
+
+ {allowAddRemove && (
+
+ )}
+
+ )
+}
+
+export default ProfileSearchWithInstitutionWidget
diff --git a/components/MultiSelectorDropdown.js b/components/MultiSelectorDropdown.js
index 543846a32..399989ea3 100644
--- a/components/MultiSelectorDropdown.js
+++ b/components/MultiSelectorDropdown.js
@@ -4,6 +4,7 @@ export default function MultiSelectorDropdown({
selectedValues,
setSelectedValues,
disabled,
+ optionsDisabled = false,
extraClass = undefined,
displayTextFn = undefined,
}) {
@@ -11,6 +12,7 @@ export default function MultiSelectorDropdown({
const numOptions = allValues.length
const handleSelectAllChange = () => {
+ if (optionsDisabled) return
if (selectedValues?.length === numOptions) {
setSelectedValues([])
} else {
@@ -19,6 +21,7 @@ export default function MultiSelectorDropdown({
}
const handleSelectValueChange = (value) => {
+ if (optionsDisabled) return
if (selectedValues?.includes(value)) {
setSelectedValues(selectedValues?.filter((v) => v !== value))
} else {
@@ -56,6 +59,7 @@ export default function MultiSelectorDropdown({
type="checkbox"
checked={selectedValues?.length === numOptions}
onChange={(e) => handleSelectAllChange(e.target.value)}
+ disabled={optionsDisabled}
/>
Select All
@@ -68,6 +72,7 @@ export default function MultiSelectorDropdown({
type="checkbox"
checked={selectedValues?.includes(option.value)}
onChange={(e) => handleSelectValueChange(e.target.value)}
+ disabled={optionsDisabled}
/>
{option.label}
diff --git a/components/Note.js b/components/Note.js
index a26e6dc99..c19c9412a 100644
--- a/components/Note.js
+++ b/components/Note.js
@@ -3,7 +3,7 @@ import NoteAuthors, { NoteAuthorsV2 } from './NoteAuthors'
import NoteReaders from './NoteReaders'
import NoteContent, { NoteContentV2 } from './NoteContent'
import Icon from './Icon'
-import { prettyId, forumDate, inflect } from '../lib/utils'
+import { prettyId, forumDate, inflect, getNoteAuthorIds, getNoteAuthors } from '../lib/utils'
import Collapse from './Collapse'
import ClientForumDate from './ClientForumDate'
@@ -111,6 +111,8 @@ const Note = ({ note, invitation, options }) => {
export const NoteV2 = ({ note, options }) => {
const privatelyRevealed = options.showPrivateIcon && !note.readers.includes('everyone')
const omitContentFields = ['pdf', 'html'].concat(options.omitFields ?? [])
+ const authorIds = getNoteAuthorIds(note, true)
+ const authors = getNoteAuthors(note, true)
const renderNoteContent = () => {
if (!options.showContents || (note.ddate && note.ddate <= Date.now())) return null
@@ -170,8 +172,8 @@ export const NoteV2 = ({ note, options }) => {
options.customAuthor(note)
) : (
diff --git a/components/NoteActivity.js b/components/NoteActivity.js
index 777956516..384e96ab8 100644
--- a/components/NoteActivity.js
+++ b/components/NoteActivity.js
@@ -6,13 +6,22 @@ import { NoteAuthorsV2 } from './NoteAuthors'
import { NoteContentV2 } from './NoteContent'
import Collapse from './Collapse'
import Icon from './Icon'
-import { prettyId, prettyInvitationId, buildNoteTitle } from '../lib/utils'
+import {
+ prettyId,
+ prettyInvitationId,
+ buildNoteTitle,
+ getNoteAuthorIds,
+ getNoteAuthors,
+} from '../lib/utils'
dayjs.extend(relativeTime)
+
export default function NoteActivity({ note, showGroup, showActionButtons }) {
const { details } = note
const { id, forum, content = {} } = note.note
+ const authorIds = getNoteAuthorIds(note.note, true)
+ const authors = getNoteAuthors(note.note, true)
let actionDescription
if (details.isDeleted) {
@@ -81,11 +90,11 @@ export default function NoteActivity({ note, showGroup, showActionButtons }) {
- {content.authors && content.authorids && (
+ {authors && authorIds && (
diff --git a/components/NoteAuthors.js b/components/NoteAuthors.js
index 21d6c9789..675b91d0d 100644
--- a/components/NoteAuthors.js
+++ b/components/NoteAuthors.js
@@ -1,11 +1,11 @@
+import isEqual from 'lodash/isEqual'
/* globals $: false */
import uniqBy from 'lodash/uniqBy'
-import isEqual from 'lodash/isEqual'
import zip from 'lodash/zip'
import Link from 'next/link'
+import { prettyId } from '../lib/utils'
import ExpandableList from './ExpandableList'
import Icon from './Icon'
-import { prettyId } from '../lib/utils'
const maxAuthorsToShow = 20
@@ -96,7 +96,22 @@ const NoteAuthors = ({ authors, authorIds, signatures, original }) => {
)
}
-export const NoteAuthorsV2 = ({ authors, authorIds, signatures, noteReaders }) => {
+export const NoteAuthorsV2 = ({
+ authors: authorsProp,
+ authorIds: authorIdsProp,
+ signatures,
+ noteReaders,
+ showAuthorInstitutions,
+}) => {
+ if (showAuthorInstitutions && !authorIdsProp?.value) {
+ return
+ }
+
+ // forum note pass raw authors (for NoteAuthorsWithInstitutions)
+ // note list pass string array
+ const authors = authorsProp?.value ?? authorsProp
+ const authorIds = authorIdsProp?.value ?? authorIdsProp
+
let showPrivateLabel = false
const sortedReaders = noteReaders ? [...noteReaders].sort() : []
if (Array.isArray(authorIds?.readers) && !isEqual(sortedReaders, authorIds.readers.sort())) {
@@ -104,8 +119,8 @@ export const NoteAuthorsV2 = ({ authors, authorIds, signatures, noteReaders }) =
}
let authorsList
- if (authors?.value?.length > 0) {
- authorsList = zip(authors?.value, authorIds?.value || [])
+ if (authors?.length > 0) {
+ authorsList = zip(authors, authorIds || [])
} else if (signatures?.length > 0) {
authorsList = signatures.map((id) => [prettyId(id), id])
} else {
@@ -180,4 +195,102 @@ export const NoteAuthorsV2 = ({ authors, authorIds, signatures, noteReaders }) =
)
}
+export const NoteAuthorsWithInstitutions = ({ authors, noteReaders }) => {
+ let showPrivateLabel = false
+ const sortedReaders = noteReaders ? [...noteReaders].sort() : []
+ if (Array.isArray(authors?.readers) && !isEqual(sortedReaders, authors.readers.sort())) {
+ showPrivateLabel = !authors.readers.includes('everyone')
+ }
+
+ if (!authors?.value) return null
+ const uniqueInstitutions = uniqBy(
+ authors.value
+ .map((p) => p.institutions)
+ .flat()
+ .filter((p) => p?.domain),
+ (p) => p.domain
+ )
+
+ const institutionIndexMap = new Map(
+ uniqueInstitutions.map((institution, index) => [institution.domain, index + 1])
+ )
+
+ const authorsLinks = authors.value.map((author) => {
+ if (!author) return null
+ if (!author.username) return
{author.fullname}
+ if (author.username.startsWith('https://dblp.org')) {
+ return (
+
+ {author.fullname}
+
+ )
+ }
+
+ const institutionNumbers = (author.institutions || []).map((institution) =>
+ institutionIndexMap.get(institution.domain)
+ )
+
+ return (
+
+
+ {author.fullname}
+
+ {institutionNumbers.length > 0 && {institutionNumbers.join(',')}}
+
+ )
+ })
+
+ return (
+ <>
+
+ {showPrivateLabel && (
+ prettyId(p))
+ .join(', ')}`}
+ />
+ )}
+
+ {uniqueInstitutions.length > 0 && (
+
+ {uniqueInstitutions.map((institution) => {
+ const institutionIndex = institutionIndexMap.get(institution.domain)
+ return (
+
+ {institutionIndex} {institution.name} ({institution.domain})
+
+ )
+ })}
+
+ )}
+ >
+ )
+}
+
export default NoteAuthors
diff --git a/components/NoteEditor.js b/components/NoteEditor.js
index d48f85116..3b37e3583 100644
--- a/components/NoteEditor.js
+++ b/components/NoteEditor.js
@@ -352,7 +352,12 @@ const NoteEditor = ({
}))
}
- if (fieldName === 'authors' && Array.isArray(fieldDescription?.value)) return null
+ if (
+ fieldName === 'authors' &&
+ Array.isArray(fieldDescription?.value) &&
+ fieldDescription.value.every((p) => typeof p !== 'object') // object author reorder should still show widget
+ )
+ return null
return (
@@ -567,7 +572,7 @@ const NoteEditor = ({
formData.authors = noteEditorData.authorids.map((p) => p.authorName)
formData.authorids = noteEditorData.authorids.map((p) => p.authorId)
}
- } else {
+ } else if (!noteEditorData.authors) {
formData.authors = { delete: true }
formData.authorids = { delete: true }
}
@@ -603,12 +608,16 @@ const NoteEditor = ({
if (error.errors) {
setErrors(
error.errors.map((p) => {
- const fieldName = getErrorFieldName(p.details.path)
+ const { fieldName, index } = getErrorFieldName(p.details.path)
const fieldNameInError =
fieldName === 'notePDateValue' ? 'Publication Date' : prettyField(fieldName)
if (isNonDeletableError(p.details.invalidValue))
- return { fieldName, message: `${fieldNameInError} is not deletable` }
- return { fieldName, message: p.message.replace(fieldName, fieldNameInError) }
+ return { fieldName, message: `${fieldNameInError} is not deletable`, index }
+ return {
+ fieldName,
+ message: p.message.replace(fieldName, fieldNameInError),
+ index,
+ }
})
)
const hasOnlyMissingFieldsError = error.errors.every(
@@ -620,7 +629,7 @@ const NoteEditor = ({
: 'Some info submitted are invalid.'
)
} else if (error.details?.path) {
- const fieldName = getErrorFieldName(error.details.path)
+ const { fieldName, index } = getErrorFieldName(error.details.path)
const fieldNameInError =
fieldName === 'notePDateValue' ? 'Publication Date' : prettyField(fieldName)
const prettyErrorMessage = isNonDeletableError(error.details.invalidValue)
@@ -630,6 +639,7 @@ const NoteEditor = ({
{
fieldName,
message: prettyErrorMessage,
+ index,
},
])
displayError(prettyErrorMessage)
diff --git a/components/SourceGroupedNoteList.js b/components/SourceGroupedNoteList.js
index eb8048828..07041bf43 100644
--- a/components/SourceGroupedNoteList.js
+++ b/components/SourceGroupedNoteList.js
@@ -1,16 +1,24 @@
import { useState } from 'react'
-import Note, { NoteV2 } from './Note'
-import { NoteAuthorsV2 } from './NoteAuthors'
-import { buildNoteTitle, buildNoteUrl, prettyId } from '../lib/utils'
+import { getImportSourceIcon } from '../lib/profiles'
+import {
+ buildNoteTitle,
+ buildNoteUrl,
+ getNoteAuthorIds,
+ getNoteAuthors,
+ prettyId,
+} from '../lib/utils'
import ClientForumDate from './ClientForumDate'
import Icon from './Icon'
+import Note, { NoteV2 } from './Note'
+import { NoteAuthorsV2 } from './NoteAuthors'
import NoteReaders from './NoteReaders'
-import { getImportSourceIcon } from '../lib/profiles'
const MultiSourceNote = ({ notes, displayOptions }) => {
const [noteToShow, setNoteToShow] = useState(notes[0])
const { id, forum, content, invitations, readers, signatures } = noteToShow
const privatelyRevealed = !noteToShow?.readers?.includes('everyone')
+ const authorIds = getNoteAuthorIds(noteToShow, true)
+ const authors = getNoteAuthors(noteToShow, true)
const sources = [
'DBLP.org/-/Record',
@@ -32,8 +40,8 @@ const MultiSourceNote = ({ notes, displayOptions }) => {
@@ -93,8 +101,8 @@ const SourceGroupedNoteList = ({ notes, displayOptions }) => {
return prev
}
const title = curr.content.title.value
- const authors = curr.content.authors.value.join(',')
- const key = `${title}|${authors}`
+ const authorNames = getNoteAuthors(curr, true).join(',')
+ const key = `${title}|${authorNames}`
if (!prev[key]) {
prev[key] = []
diff --git a/components/browser/NoteEntity.js b/components/browser/NoteEntity.js
index 586bd9e1e..dce13e65b 100644
--- a/components/browser/NoteEntity.js
+++ b/components/browser/NoteEntity.js
@@ -2,15 +2,8 @@
/* globals promptError: false */
import React, { useContext } from 'react'
-import EdgeBrowserContext from './EdgeBrowserContext'
-import EditEdgeDropdown from './EditEdgeDropdown'
-import EditEdgeToggle from './EditEdgeToggle'
-import NoteAuthors from './NoteAuthors'
-import NoteContent from './NoteContent'
-import ScoresList from './ScoresList'
-import EditEdgeTwoDropdowns from './EditEdgeTwoDropdowns'
+import useUser from '../../hooks/useUser'
import api from '../../lib/api-client'
-
import {
getInterpolatedValues,
getSignatures,
@@ -18,7 +11,13 @@ import {
isInGroupInvite,
isNotInGroupInvite,
} from '../../lib/edge-utils'
-import useUser from '../../hooks/useUser'
+import EdgeBrowserContext from './EdgeBrowserContext'
+import EditEdgeDropdown from './EditEdgeDropdown'
+import EditEdgeToggle from './EditEdgeToggle'
+import EditEdgeTwoDropdowns from './EditEdgeTwoDropdowns'
+import NoteAuthors from './NoteAuthors'
+import NoteContent from './NoteContent'
+import ScoresList from './ScoresList'
export default function NoteEntity(props) {
const { editInvitations, traverseInvitation, availableSignaturesInvitationMap, version } =
@@ -50,6 +49,12 @@ export default function NoteEntity(props) {
if (editEdges?.length) extraClasses.push('is-editable')
if (props.isSelected) extraClasses.push('is-selected')
+ let { authors, authorids } = content
+
+ if (!authorids) {
+ authorids = authors.map((p) => p.username)
+ authors = authors.map((p) => p.fullname)
+ }
// Event handlers
const handleClick = (e) => {
if (!props.canTraverse) return
@@ -328,8 +333,8 @@ export default function NoteEntity(props) {
diff --git a/components/profile/ImportedPublicationsSection.js b/components/profile/ImportedPublicationsSection.js
index 688247c07..d31996758 100644
--- a/components/profile/ImportedPublicationsSection.js
+++ b/components/profile/ImportedPublicationsSection.js
@@ -97,6 +97,16 @@ const ImportedPublicationsSection = ({
{ sort: 'tmdate:desc' }
)
.then((notes) => notes.map((note) => ({ ...note, apiVersion: 2 })))
+ const v2NotesWithObjectAuthorSchema = await api
+ .getAll(
+ '/notes',
+ {
+ 'content.authors.username': profileId,
+ invitations: [`${process.env.SUPER_USER}/Public_Article/DBLP.org/-/Record`],
+ },
+ { sort: 'tmdate:desc' }
+ )
+ .then((notes) => notes.map((note) => ({ ...note, apiVersion: 2 })))
const v1Notes = await api
.getAll(
'/notes',
@@ -108,7 +118,7 @@ const ImportedPublicationsSection = ({
)
.then((notes) => notes.map((note) => ({ ...note, apiVersion: 1 })))
- const allNotes = v2Notes.concat(v1Notes)
+ const allNotes = v2Notes.concat(v2NotesWithObjectAuthorSchema, v1Notes)
setPublications(allNotes)
setTotalCount(allNotes.length)
} catch (error) {
diff --git a/components/webfield/AreaChairConsole.js b/components/webfield/AreaChairConsole.js
index 2fa081baf..797abd5ea 100644
--- a/components/webfield/AreaChairConsole.js
+++ b/components/webfield/AreaChairConsole.js
@@ -1125,6 +1125,13 @@ const AreaChairConsole = ({ appContext }) => {
}
})
const metaReview = allMetaReviews.find((p) => !p.isByOtherAC)
+ if (typeof note.content?.authors?.value === 'object' && !note.content?.authorids) {
+ // eslint-disable-next-line no-param-reassign
+ note.authorSearchValue = note.content.authors.value.map((p) => ({
+ ...p,
+ type: 'authorObj',
+ }))
+ }
const customStageReviews = customStageInvitations?.reduce((prev, curr) => {
const customStageReview = note.details.replies.find((p) =>
diff --git a/components/webfield/AreaChairConsoleMenuBar.js b/components/webfield/AreaChairConsoleMenuBar.js
index 6d8216333..62415bc59 100644
--- a/components/webfield/AreaChairConsoleMenuBar.js
+++ b/components/webfield/AreaChairConsoleMenuBar.js
@@ -35,7 +35,11 @@ const AreaChairConsoleMenuBar = ({
number: ['note.number'],
id: ['note.id'],
title: ['note.content.title.value'],
- author: ['note.content.authors.value', 'note.content.authorids.value'],
+ author: [
+ 'note.content.authors.value',
+ 'note.content.authorids.value',
+ 'note.authorSearchValue',
+ ],
keywords: ['note.content.keywords.value'],
venue: ['note.content.venue.value'],
[formattedReviewerName]: ['reviewers'],
diff --git a/components/webfield/AuthorConsole.js b/components/webfield/AuthorConsole.js
index 6531b0a5b..6f5a49a70 100644
--- a/components/webfield/AuthorConsole.js
+++ b/components/webfield/AuthorConsole.js
@@ -14,6 +14,7 @@ import {
prettyId,
inflect,
pluralizeString,
+ getNoteAuthorIds,
} from '../../lib/utils'
import ErrorDisplay from '../ErrorDisplay'
import Table from '../Table'
@@ -511,9 +512,8 @@ const AuthorConsole = ({ appContext }) => {
const loadProfiles = async (notes, version) => {
const authorIds = new Set()
notes.forEach((note) => {
- const ids = version === 2 ? note.content.authorids.value : note.content.authorids
+ const ids = getNoteAuthorIds(note, version === 2)
if (!Array.isArray(ids)) return
-
ids.forEach((id) => {
if (!id.includes('@')) {
authorIds.add(id)
diff --git a/components/webfield/EditorWidget.js b/components/webfield/EditorWidget.js
index 5e9600375..8e78c74b4 100644
--- a/components/webfield/EditorWidget.js
+++ b/components/webfield/EditorWidget.js
@@ -50,6 +50,14 @@ const ProfileSearchWidget = dynamic(() => import('../EditorComponents/ProfileSea
loading: () =>
,
})
+const ProfileSearchWithInstitutionWidget = dynamic(
+ () => import('../EditorComponents/ProfileSearchWithInstitutionWidget'),
+ {
+ ssr: false,
+ loading: () =>
,
+ }
+)
+
// #endregion
const EditorWidget = () => {
@@ -117,6 +125,8 @@ const EditorWidget = () => {
case 'profile[]':
case 'profile{}':
return
+ case 'author{}':
+ return
case 'note':
case 'note[]':
case 'edit':
@@ -133,6 +143,12 @@ const EditorWidget = () => {
if (fieldName === 'authorids' && Array.isArray(field.authorids?.value))
return
+ if (
+ fieldName === 'authors' &&
+ (Array.isArray(field.authors?.value) || // reorder only
+ field.authors?.value?.param?.elements) // reorder with institution change
+ )
+ return
if (!field[fieldName].value?.param) {
if (!field[fieldName].value && field[fieldName].readers) {
return null // TODO: an empty widget which shows only readers
diff --git a/components/webfield/EthicsChairConsole/EthicsChairMenuBar.js b/components/webfield/EthicsChairConsole/EthicsChairMenuBar.js
index 34151e1cd..ce1f8148b 100644
--- a/components/webfield/EthicsChairConsole/EthicsChairMenuBar.js
+++ b/components/webfield/EthicsChairConsole/EthicsChairMenuBar.js
@@ -20,7 +20,11 @@ const EthicsChairMenuBar = ({ tableRowsAll, tableRows, setPaperStatusTabData })
number: ['note.number'],
id: ['note.id'],
title: ['note.content.title.value'],
- author: ['note.content.authors.value', 'note.content.authorids.value'],
+ author: [
+ 'note.content.authors.value',
+ 'note.content.authorids.value',
+ 'note.authorSearchValue',
+ ],
keywords: ['note.content.keywords.value'],
reviewer: ['ethicsReviewers'],
numReviewersAssigned: ['numReviewersAssigned'],
diff --git a/components/webfield/EthicsChairConsole/EthicsChairPaperStatus.js b/components/webfield/EthicsChairConsole/EthicsChairPaperStatus.js
index 10c669df0..99d96911e 100644
--- a/components/webfield/EthicsChairConsole/EthicsChairPaperStatus.js
+++ b/components/webfield/EthicsChairConsole/EthicsChairPaperStatus.js
@@ -1,19 +1,17 @@
-/* globals promptError: false */
-import { useContext, useEffect, useState } from 'react'
import Link from 'next/link'
+import { useContext, useEffect, useState } from 'react'
import api from '../../../lib/api-client'
-import WebFieldContext from '../../WebFieldContext'
import {
getIndentifierFromGroup,
getNumberFromGroup,
getProfileName,
} from '../../../lib/utils'
import LoadingSpinner from '../../LoadingSpinner'
-
-import Table from '../../Table'
import PaginationLinks from '../../PaginationLinks'
-import NoteSummary from '../NoteSummary'
+import Table from '../../Table'
+import WebFieldContext from '../../WebFieldContext'
import { EthicsReviewStatus } from '../NoteReviewStatus'
+import NoteSummary from '../NoteSummary'
import EthicsChairMenuBar from './EthicsChairMenuBar'
const EthicsSubmissionRow = ({ rowData }) => {
@@ -198,7 +196,13 @@ const EthicsChairPaperStatus = () => {
return p.invitations.includes(ethicsMetaReviewInvitationId)
})
: null
-
+ if (typeof note.content?.authors?.value === 'object' && !note.content?.authorids) {
+ // eslint-disable-next-line no-param-reassign
+ note.authorSearchValue = note.content.authors.value.map((p) => ({
+ ...p,
+ type: 'authorObj',
+ }))
+ }
return {
note,
ethicsReviews,
diff --git a/components/webfield/NoteMetaReviewStatus.js b/components/webfield/NoteMetaReviewStatus.js
index f150bd539..1c8abaf72 100644
--- a/components/webfield/NoteMetaReviewStatus.js
+++ b/components/webfield/NoteMetaReviewStatus.js
@@ -1,13 +1,13 @@
/* globals promptError,promptMessage: false */
+import copy from 'copy-to-clipboard'
// modified from noteMetaReviewStatus.hbs handlebar template
import React, { useContext, useEffect, useState } from 'react'
-import copy from 'copy-to-clipboard'
-import WebFieldContext from '../WebFieldContext'
import useUser from '../../hooks/useUser'
import api from '../../lib/api-client'
-import { inflect, pluralizeString, prettyField } from '../../lib/utils'
import { getNoteContentValues } from '../../lib/forum-utils'
+import { inflect, pluralizeString, prettyField } from '../../lib/utils'
+import WebFieldContext from '../WebFieldContext'
import ProfileLink from './ProfileLink'
const IEEECopyrightForm = ({ note, isV2Note }) => {
@@ -15,13 +15,14 @@ const IEEECopyrightForm = ({ note, isV2Note }) => {
useContext(WebFieldContext)
const { user, isRefreshing } = useUser(true)
const noteContent = isV2Note ? getNoteContentValues(note.content) : note.content
+ const noteAuthors = getNoteAuthors(note, isV2Note)
if (showIEEECopyright && IEEEPublicationTitle && IEEEArtSourceCode && !isRefreshing) {
return (