diff --git a/components/application/repository-select.jsx b/components/application/repository-select.jsx index 156edece..b64a4dc9 100644 --- a/components/application/repository-select.jsx +++ b/components/application/repository-select.jsx @@ -1,19 +1,26 @@ -import React, { useCallback, useState, useEffect } from 'react' +import React, { useCallback, useState, useEffect, useRef } from 'react' import { AppBar, Select } from '@oacore/design' import { useRouter } from 'next/router' import styles from './repository-select.module.css' +import { TextField } from '../../design' +import close from 'components/upload/assets/closeLight.svg' import { withGlobalStore } from 'store' const RepositorySelect = ({ store }) => { const router = useRouter() const [suggestions, setSuggestions] = useState(store.user.dataProviders) const [value, setValue] = useState(store.dataProvider.name) + const [showSecondDropdown, setShowSecondDropdown] = useState(false) + const [isOpen, setIsOpen] = useState(false) + const [selectedItem, setSelectedItem] = useState(null) + const [inputValue, setInputValue] = useState('') + const providerId = router.query['data-provider-id'] + const setsRef = useRef(null) const handleOnChange = useCallback( (data) => { - if (data.value === store.dataProvider.name) return if (!data.id) { setValue(store.dataProvider.name) return @@ -22,6 +29,8 @@ const RepositorySelect = ({ store }) => { const actualPath = `/data-providers/${data.id}` router.push(routePath, actualPath) + setShowSecondDropdown(true) + store.updateSelectedSetSpec(null) }, [router, store.dataProvider.name] ) @@ -36,19 +45,73 @@ const RepositorySelect = ({ store }) => { setValue(data.value) }, []) + const handleDropdownClick = async () => { + setIsOpen(!isOpen) + } + + const handleSelect = (item) => { + setSelectedItem(item) + setIsOpen(false) + setInputValue(item.setName) + store.updateSelectedSetSpec(item) + store.dataProvider?.getDeduplicationData(providerId) + store.dataProvider?.getRrslistData(providerId) + store.dataProvider?.doi?.doiRecords.load() + store.dataProvider.works.resetWorks() + store.dataProvider.depositDates.resetCompliance() + store.dataProvider.doi.resetDoiRecords() + store.dataProvider?.depositDates?.publicReleaseDatesPages?.load() + store.dataProvider.retrieve() + } + + useEffect(() => { + store.getSetsEnabledList() + }, [providerId]) + useEffect(() => { setValue(store.dataProvider.name) }, [store.dataProvider.name]) + useEffect(() => { + const handleClickOutside = (event) => { + if (setsRef.current && !setsRef.current.contains(event.target)) + setIsOpen(false) + } + + document.addEventListener('mousedown', handleClickOutside) + + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, [setsRef]) + + const handleSelectClick = () => { + if (value === store.dataProvider.name) setShowSecondDropdown(true) + } + + const handleClear = () => { + // eslint-disable-next-line no-console + console.log('reset') + } + + const handleSetInputChange = (event) => { + setInputValue(event.target.value) + } + + const filteredData = store.enabledList.filter((item) => + item.setName.toLowerCase().includes(inputValue.toLowerCase()) + ) + return ( + {store.enabledList.length > 0 && showSecondDropdown && ( +
+
+
+ {selectedItem ? ( +
+ {selectedItem?.setName.length > 25 + ? `${selectedItem?.setName.substring(0, 25)}...` + : inputValue} + {/* eslint-disable-next-line max-len */} + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-noninteractive-element-interactions */} + close +
+ ) : ( + + )} +
+
+ {isOpen && ( +
+
    + {filteredData.map((item) => ( + // eslint-disable-next-line max-len + // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-noninteractive-element-interactions +
  • handleSelect(item)} + className={styles.selectItem} + > + {item.setName} +
  • + ))} +
+
+ )} +
+ )}
) } diff --git a/components/application/repository-select.module.css b/components/application/repository-select.module.css index d4ccedda..5be8c260 100644 --- a/components/application/repository-select.module.css +++ b/components/application/repository-select.module.css @@ -8,6 +8,7 @@ display: flex; flex: 2 2 60%; align-items: center; + gap: 20px; } .repository-select { @@ -40,3 +41,64 @@ .repository-select ul { font-weight: initial; } + +.select-form-wrapper { + display: flex; + width: 100%; + gap: 24px; +} + +.select-wrapper { + width: 100%; +} + +.select-input input { + background: #fff; + border: 1px solid #b75400; + border-radius: 2px; +} + +/* stylelint-disable declaration-no-important */ +.select-input label { + color: #9e9e9e !important; +} + +.dropdown-menu-wrapper { + position: relative; + flex: 1; +} + +.dropdown-menu { + position: absolute; + padding: 10px; + background: #fff; + border-radius: 2px; + border-top-left-radius: 0; + border-top-right-radius: 0; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05), 0 1px 2px rgba(0, 0, 0, 0.25); +} + +.select-item { + padding: 10px; + margin: 4px 0; + color: #b75400; + cursor: pointer; +} + +.selected-item { + display: flex; + align-items: center; + justify-content: space-between; + width: max-content; + max-width: 290px; + padding: 2px 10px; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 130%; + color: #fff; + letter-spacing: 0.01px; + background: #5a9216; + border-radius: 4px; + gap: 24px; +} diff --git a/components/textWithTooltip/styles.module.css b/components/textWithTooltip/styles.module.css new file mode 100644 index 00000000..ad801793 --- /dev/null +++ b/components/textWithTooltip/styles.module.css @@ -0,0 +1,12 @@ +.container { + display: inline-flex; + cursor: pointer; +} + +.pure { + color: var(--gray-600); +} + +.popover { + max-width: 20rem; +} diff --git a/components/textWithTooltip/textWithtooltip.jsx b/components/textWithTooltip/textWithtooltip.jsx new file mode 100644 index 00000000..08eea3f2 --- /dev/null +++ b/components/textWithTooltip/textWithtooltip.jsx @@ -0,0 +1,17 @@ +import React from 'react' +import { Popover } from '@oacore/design' +import classNames from '@oacore/design/lib/utils/class-names' + +import styles from './styles.module.css' + +const TextWithTooltip = ({ tag: Tag = 'div', className, text }) => ( + + +
25 ? text : ''}> + {text.length > 25 ? `${text.substring(0, 25)}..` : text} +
+
+
+) + +export default TextWithTooltip diff --git a/components/upload/assets/checkGreen.svg b/components/upload/assets/checkGreen.svg new file mode 100644 index 00000000..ac8ee0b5 --- /dev/null +++ b/components/upload/assets/checkGreen.svg @@ -0,0 +1,3 @@ + + + diff --git a/components/upload/assets/close.svg b/components/upload/assets/close.svg new file mode 100644 index 00000000..9f152db8 --- /dev/null +++ b/components/upload/assets/close.svg @@ -0,0 +1,3 @@ + + + diff --git a/components/upload/assets/closeLight.svg b/components/upload/assets/closeLight.svg new file mode 100644 index 00000000..7eaf3385 --- /dev/null +++ b/components/upload/assets/closeLight.svg @@ -0,0 +1,3 @@ + + + diff --git a/components/upload/assets/dropdownArrow.svg b/components/upload/assets/dropdownArrow.svg new file mode 100644 index 00000000..a7db9999 --- /dev/null +++ b/components/upload/assets/dropdownArrow.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/components/upload/assets/removeBin.svg b/components/upload/assets/removeBin.svg new file mode 100644 index 00000000..a702f6fd --- /dev/null +++ b/components/upload/assets/removeBin.svg @@ -0,0 +1,3 @@ + + + diff --git a/pages/data-providers/[data-provider-id]/content.jsx b/pages/data-providers/[data-provider-id]/content.jsx index dbe86141..1e40d8fe 100644 --- a/pages/data-providers/[data-provider-id]/content.jsx +++ b/pages/data-providers/[data-provider-id]/content.jsx @@ -6,7 +6,7 @@ import ContentTemplate from 'templates/content' const Content = ({ store: { dataProvider }, ...props }) => ( diff --git a/pages/data-providers/[data-provider-id]/doi.jsx b/pages/data-providers/[data-provider-id]/doi.jsx index 5a684914..bd161e58 100644 --- a/pages/data-providers/[data-provider-id]/doi.jsx +++ b/pages/data-providers/[data-provider-id]/doi.jsx @@ -9,8 +9,8 @@ const DoiPage = ({ store: { dataProvider, organisation }, ...props }) => ( doiUrl={dataProvider?.doi?.doiUrl} isExportDisabled={ dataProvider?.doi?.enrichmentSize === 0 || - dataProvider?.doi?.doiRecords.error != null || - dataProvider?.doi?.doiRecords.data.length === 0 + dataProvider?.doi?.doiRecords?.error != null || + dataProvider?.doi?.doiRecords?.data.length === 0 } doiCount={dataProvider?.doi?.originCount} dataProviderName={dataProvider.name} diff --git a/pages/data-providers/[data-provider-id]/repository.jsx b/pages/data-providers/[data-provider-id]/repository.jsx index f1507aed..c0ffc93d 100644 --- a/pages/data-providers/[data-provider-id]/repository.jsx +++ b/pages/data-providers/[data-provider-id]/repository.jsx @@ -11,6 +11,20 @@ const RepositoryPage = ({ store, ...restProps }) => ( dataProviderLogo={store.dataProvider.logo} updateDataProvider={store.updateDataProvider} updateLogo={store.updateLogo} + setsList={store.setsList} + loadingSets={store.loadingSets} + enableSet={store.enableSet} + getSetsEnabledList={store.getSetsEnabledList} + enabledList={store.enabledList} + disabledList={store.disabledList} + deleteSet={store.deleteSet} + getSetsWholeList={store.getSetsWholeList} + wholeSetData={store.wholeSetData} + loadingWholeSets={store.loadingWholeSets} + loadingWholeSetsBtn={store.loadingWholeSetsBtn} + setLoadingWholeSetsBtn={store.setLoadingWholeSetsBtn} + loadingRemoveItem={store.loadingRemoveItem} + setLoadingRemoveAction={store.setLoadingRemoveAction} oaiMapping={store.dataProvider.oaiMapping} setGlobalRorName={store.dataProvider.setGlobalRorName} setGlobalRorId={store.dataProvider.setGlobalRorId} diff --git a/store/data-provider/deposit-dates.js b/store/data-provider/deposit-dates.js index d820b266..8aec0220 100644 --- a/store/data-provider/deposit-dates.js +++ b/store/data-provider/deposit-dates.js @@ -1,4 +1,4 @@ -import { action, computed, observable } from 'mobx' +import { action, computed, observable, reaction } from 'mobx' import { Pages } from '../helpers/pages' import Store from '../store' @@ -7,6 +7,8 @@ import { PaymentRequiredError } from '../errors' import { NotFoundError } from 'api/errors' class DepositDates extends Store { + baseStore = null + @observable isRetrieveDepositDatesInProgress = false @observable timeLagData = null @@ -17,16 +19,54 @@ class DepositDates extends Store { @observable publicationDatesValidate = null - constructor(baseUrl, options) { + constructor(rootStore, baseUrl, options) { super(baseUrl, options) + this.baseStore = rootStore + this.fetchOptions = { + cache: 'no-store', + } + this.updateOaiUrl(baseUrl) + reaction( + () => this.baseStore?.setSelectedItem, + () => { + this.updateOaiUrl(baseUrl) + } + ) + } - const datesUrl = `${baseUrl}/public-release-dates` + @action + resetCompliance() { + this.timeLagData = null + this.publicReleaseDates = null + this.crossDepositLag = null + this.publicationDatesValidate = null + } + + @action + updateOaiUrl = (baseUrl) => { + const datesUrl = `${baseUrl}/public-release-dates${ + this.baseStore.setSelectedItem + ? `?set=${this.baseStore.setSelectedItem.setSpec}` + : '' + }` this.publicReleaseDates = new Pages(datesUrl, this.options) this.datesUrl = `${process.env.API_URL}${datesUrl}?accept=text/csv` - this.depositTimeLagUrl = `${baseUrl}/statistics/deposit-time-lag` - this.crossDepositLagUrl = `${baseUrl}/cross-deposit-lag` + this.depositTimeLagUrl = `${baseUrl}/statistics/deposit-time-lag${ + this.baseStore.setSelectedItem + ? `?set=${this.baseStore.setSelectedItem.setSpec}` + : '' + }` + this.crossDepositLagUrl = `${baseUrl}/cross-deposit-lag${ + this.baseStore.setSelectedItem + ? `?set=${this.baseStore.setSelectedItem.setSpec}` + : '' + }` this.crossDepositLagCsvUrl = `${process.env.API_URL}${this.crossDepositLagUrl}?accept=text/csv` - this.publicationDatesValidateUrl = `${baseUrl}/publication-dates-validate` + this.publicationDatesValidateUrl = `${baseUrl}/publication-dates-validate${ + this.baseStore.setSelectedItem + ? `?set=${this.baseStore.setSelectedItem.setSpec}` + : '' + }` this.retrieve() } diff --git a/store/data-provider/doi.js b/store/data-provider/doi.js index f1461906..5a05fe4f 100644 --- a/store/data-provider/doi.js +++ b/store/data-provider/doi.js @@ -1,13 +1,63 @@ -import { action, observable, computed } from 'mobx' +import { action, computed, observable, reaction } from 'mobx' import { Pages } from '../helpers/pages' import Store from '../store' class DOI extends Store { + baseStore = null + + constructor(rootStore, baseUrl, options) { + super(baseUrl, options) + this.baseStore = rootStore + this.fetchOptions = { + cache: 'no-store', + } + + this.updateStatisticsUrl(baseUrl) + this.updateDoiRecords(baseUrl) + this.retrieveStatistics() + reaction( + () => this.baseStore?.setSelectedItem, + () => { + this.updateDoiRecords(baseUrl) + this.updateStatisticsUrl(baseUrl) + } + ) + } + @observable originCount = null @observable totalCount = null + @observable doiRecords = null + + @action + resetDoiRecords() { + this.originCount = null + this.totalCount = null + this.doiRecords = null + } + + @action + updateStatisticsUrl = (baseUrl) => { + this.statisticsUrl = `${baseUrl}/statistics/doi${ + this.baseStore?.setSelectedItem + ? `?set=${this.baseStore?.setSelectedItem.setSpec}` + : '' + }` + } + + @action + updateDoiRecords = (baseUrl) => { + const DUrl = `${baseUrl}/doi${ + this.baseStore?.setSelectedItem + ? `?set=${this.baseStore?.setSelectedItem.setSpec}` + : '' + }` + this.doiRecords = new Pages(DUrl, this.options) + this.doiUrl = `${process.env.API_URL}${DUrl}?accept=text/csv` + } + @computed get enrichmentSize() { const { totalCount, originCount } = this @@ -25,21 +75,9 @@ class DOI extends Store { return lag } - @observable doiRecords = null - - constructor(baseUrl, options) { - super(baseUrl, options) - - const doiUrl = `${baseUrl}/doi` - this.doiRecords = new Pages(doiUrl, this.options) - this.doiUrl = `${process.env.API_URL}${doiUrl}?accept=text/csv` - this.statisticsUrl = `${baseUrl}/statistics/doi` - this.retrieveStatistics() - } - @action retrieveStatistics = async () => { - const { data } = await this.request(this.statisticsUrl) + const { data } = await this.request(this.statisticsUrl, this.fetchOptions) this.originCount = data.dataProviderDoiCount this.totalCount = data.totalDoiCount } diff --git a/store/data-provider/index.js b/store/data-provider/index.js index 56c44516..b80f48c0 100644 --- a/store/data-provider/index.js +++ b/store/data-provider/index.js @@ -11,6 +11,13 @@ import { NotFoundError as NetworkNotFoundError } from '../../api/errors' import { NotFoundError } from '../errors' class DataProvider extends Resource { + rootStore = null + + constructor(rootStore, init, options) { + super(init, options) + this.rootStore = rootStore + } + @observable id = '' @observable name = '' @@ -154,7 +161,10 @@ class DataProvider extends Resource { getDeduplicationData = async (id, refresh = false) => { this.duplicateDataLoading = true try { - let url = `${process.env.API_URL}/data-providers/${id}/duplicates` + const specData = this.rootStore.setSelectedItem.setSpec + let url = `${process.env.API_URL}/data-providers/${id}/duplicates${ + specData ? `?set=${specData}` : '' + }` if (refresh) url += '?refresh=true' const options = refresh ? { cache: 'reload' } : {} @@ -177,9 +187,14 @@ class DataProvider extends Resource { getRrslistData = async (id) => { this.rrsDataLoading = true try { - const response = await fetch( - `${process.env.API_URL}/data-providers/${id}/rights-retention` - ) + const specData = this.rootStore.setSelectedItem.setSpec + const url = `${ + process.env.API_URL + }/data-providers/${id}/rights-retention${ + specData ? `?set=${specData}` : '' + }` + + const response = await fetch(url) if (response.ok && response.status === 200) { const data = await response.json() @@ -351,9 +366,9 @@ class DataProvider extends Resource { this.retrieveLogo() const url = `/data-providers/${this.id}` - this.works = new Works(url, this.options) - this.depositDates = new DepositDates(url, this.options) - this.doi = new DOI(url, this.options) + this.works = new Works(this.rootStore, url, this.options) + this.depositDates = new DepositDates(this.rootStore, url, this.options) + this.doi = new DOI(this.rootStore, url, this.options) this.issues = new Issues(url, this.options) this.allMembers = new Membership(url, this.options) this.duplicatesUrl = `${process.env.API_URL}${url}/duplicates?accept=text/csv` diff --git a/store/data-provider/works.js b/store/data-provider/works.js index 4985115a..3d584fe8 100644 --- a/store/data-provider/works.js +++ b/store/data-provider/works.js @@ -1,11 +1,32 @@ -import { action } from 'mobx' +import { action, observable } from 'mobx' import { Pages } from '../helpers/pages' +import Store from '../store' -class Works extends Pages { - constructor(baseUrl, options) { - const url = `${baseUrl}/works` - super(url, options) +class Works extends Store { + baseStore = null + + @observable workRecords = null + + @action + resetWorks() { + this.workRecords = null + } + + constructor(rootStore, baseUrl, options) { + super(baseUrl, options) + this.baseStore = rootStore + this.updateWorks(baseUrl) + } + + @action + updateWorks = (baseUrl) => { + const url = `${baseUrl}/works${ + this.baseStore?.setSelectedItem + ? `?set=${this.baseStore?.setSelectedItem.setSpec}` + : '' + }` + this.workRecords = new Pages(url, this.options) this.contentExportUrl = `${process.env.API_URL}${url}?accept=text/csv` } diff --git a/store/helpers/pages/pages.js b/store/helpers/pages/pages.js index ae7bf639..05c96229 100644 --- a/store/helpers/pages/pages.js +++ b/store/helpers/pages/pages.js @@ -1,7 +1,6 @@ import { observable } from 'mobx' import getOrder from '../order' -import invalidatePreviousRequests from '../invalidatePreviousRequests' import Store from '../../store' import { PaymentRequiredError } from '../../errors' @@ -48,7 +47,6 @@ class Pages extends Store { this.type = type } - @invalidatePreviousRequests load(signal) { const order = getOrder(this.columnOrder) @@ -63,7 +61,6 @@ class Pages extends Store { if (this.type) params.type = this.type if (order) params.orderBy = order if (this.searchTerm) params.q = this.searchTerm - const request = this.request(this.url, { searchParams: params, signal }) return new Promise((resolve, reject) => request diff --git a/store/root.js b/store/root.js index 14dcd8b6..0ce049e5 100644 --- a/store/root.js +++ b/store/root.js @@ -91,6 +91,22 @@ class Root extends Store { @observable deduplicationNotifications = null + @observable loadingSets = false + + @observable loadingWholeSets = false + + @observable loadingWholeSetsBtn = false + + @observable loadingRemoveItem = false + + @observable setsList = [] + + @observable enabledList = [] + + @observable disabledList = [] + + @observable wholeSetData = [] + @observable tutorial = { currentStep: 1, isModalOpen: false, @@ -127,6 +143,13 @@ class Root extends Store { @observable responseData = null + @observable setSelectedItem = '' + + @action + updateSelectedSetSpec = (value) => { + this.setSelectedItem = value + } + @action setHarvestNotifications = (data) => { this.harvestNotifications = data @@ -152,6 +175,36 @@ class Root extends Store { return this.organisation ? this.organisation.id : '' } + @action + setSetsList(data) { + this.setsList = data + } + + @action + setEnabledList(data) { + this.enabledList = data + } + + @action + setDisabledList(data) { + this.disabledList = data + } + + @action + setWholeSetData(data) { + this.wholeSetData = data + } + + @action + setLoadingWholeSetsBtn = (value) => { + this.loadingWholeSetsBtn = value + } + + @action + setLoadingRemoveAction = (value, id) => { + this.loadingRemoveItem = { value, id } + } + @computed get dataProviders() { // The current data provider can be loaded asynchronously to the user's @@ -203,7 +256,7 @@ class Root extends Store { const dataProviderInit = this.findDataProvider(id) - this.dataProvider = new DataProvider(dataProviderInit, { + this.dataProvider = new DataProvider(this, dataProviderInit, { ...this.options, prefetch: true, }) @@ -357,6 +410,87 @@ class Root extends Store { } } + @action + getSetsWholeList = async () => { + this.loadingWholeSets = true + try { + const response = await fetch( + `https://api-dev.core.ac.uk/internal/data-providers/${this.dataProvider.id}/set/available` + ) + if (response.ok) { + const data = await response.json() + this.setWholeSetData(data) + } else throw new Error('Failed to fetch rrs data') + } catch (error) { + console.error('Error fetching rrs data:', error) + this.setWholeSetData([]) + } finally { + this.loadingWholeSets = false + } + } + + @action + getSetsEnabledList = async () => { + this.loadingSets = true + try { + const response = await fetch( + `https://api-dev.core.ac.uk/internal/data-providers/${this.dataProvider.id}/set` + ) + if (response.ok) { + const data = await response.json() + this.setEnabledList(data) + } else throw new Error('Failed to fetch rrs data') + } catch (error) { + console.error('Error fetching rrs data:', error) + this.setEnabledList([]) + } finally { + this.loadingSets = false + } + } + + @action + enableSet = async (body) => { + try { + const response = await fetch( + `https://api-dev.core.ac.uk/internal/data-providers/${this.dataProvider.id}/set/settings`, + { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + } + ) + + if (!response.ok) throw new Error('Failed to patch settings') + } catch (error) { + console.error('Error patching settings:', error) + throw error + } + } + + @action + deleteSet = async (idSet) => { + this.loadingSets = true + try { + const response = await fetch( + `https://api-dev.core.ac.uk/internal/data-providers/${this.dataProvider.id}/set/settings/${idSet}`, + { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + } + ) + if (!response.ok) throw new Error('Failed to patch settings') + } catch (error) { + console.error('Error patching settings:', error) + throw error + } finally { + this.loadingSets = false + } + } + @action getNotifications = async (userId, organisationId, type) => { try { diff --git a/templates/content/cards/table-card.jsx b/templates/content/cards/table-card.jsx index f1d0df07..69ff92ed 100644 --- a/templates/content/cards/table-card.jsx +++ b/templates/content/cards/table-card.jsx @@ -1,8 +1,11 @@ -import React from 'react' +import React, { useContext } from 'react' import { observer } from 'mobx-react-lite' import { classNames } from '@oacore/design/lib/utils' import styles from '../styles.module.css' +import checked from '../../../components/upload/assets/checkGreen.svg' +import { GlobalContext } from '../../../store' +import TextWithTooltip from '../../../components/textWithTooltip/textWithtooltip' import { formatDate } from 'utils/helpers' import { Card, DetailList, Icon } from 'design' @@ -80,57 +83,73 @@ const SidebarContent = observer( } ) -const TableCard = ({ works, changeVisibility, exportUrl, ...props }) => { - const [tableProps, fetchData] = useDynamicTableData({ pages: works }) - return ( - - - { - const { oai } = v.identifiers - if (oai) return oai.split(':').pop() - return '-' - }} - className={styles.oaiColumn} - /> - - v.authors.map((a) => a.name).join(' ')} - /> - formatDate(v.lastUpdate)} - /> - - - - - - {texts.exporting.download} - - -
-
- ) -} +const TableCard = observer( + ({ works, changeVisibility, exportUrl, ...props }) => { + const { ...globalStore } = useContext(GlobalContext) + const [tableProps, fetchData] = useDynamicTableData({ pages: works }) + return ( + +
+ {globalStore?.setSelectedItem && ( +
+ + + + +
+ )} +
+ + { + const { oai } = v.identifiers + if (oai) return oai.split(':').pop() + return '-' + }} + className={styles.oaiColumn} + /> + + v.authors.map((a) => a.name).join(' ')} + /> + formatDate(v.lastUpdate)} + /> + + + + + + {texts.exporting.download} + + +
+
+ ) + } +) export default TableCard diff --git a/templates/content/index.jsx b/templates/content/index.jsx index 4046bf1d..18e143e9 100644 --- a/templates/content/index.jsx +++ b/templates/content/index.jsx @@ -1,20 +1,23 @@ import React from 'react' +import { observer } from 'mobx-react-lite' import TableCard from './cards/table-card' import Title from '../../components/title' -const ContentTemplate = ({ works, changeVisibility, exportUrl, ...props }) => ( - <> - - {works && ( - - )} - +const ContentTemplate = observer( + ({ works, changeVisibility, exportUrl, ...props }) => ( + <> + + {works && ( + + )} + + ) ) export default ContentTemplate diff --git a/templates/content/styles.module.css b/templates/content/styles.module.css index f6a89a20..01dd13fb 100644 --- a/templates/content/styles.module.css +++ b/templates/content/styles.module.css @@ -60,3 +60,20 @@ fill: var(--danger); } } + +.set-end-wrapper { + display: flex; + justify-content: flex-end; + width: 100%; + margin-bottom: 12px; +} + +.set-name { + margin-right: 8px; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 130%; + color: var(--success-dark); + letter-spacing: 0.048px; +} diff --git a/templates/deduplication/cards/deduplicationInfo.jsx b/templates/deduplication/cards/deduplicationInfo.jsx index 7f5b1ac1..05905d14 100644 --- a/templates/deduplication/cards/deduplicationInfo.jsx +++ b/templates/deduplication/cards/deduplicationInfo.jsx @@ -1,34 +1,58 @@ -import React from 'react' +import React, { useContext } from 'react' +import { classNames } from '@oacore/design/lib/utils' import styles from '../styles.module.css' import texts from '../../../texts/deduplication/deduplication.yml' import { formatDate, valueOrDefault } from '../../../utils/helpers' +import checked from '../../../components/upload/assets/checkGreen.svg' +import { GlobalContext } from '../../../store' +import TextWithTooltip from '../../../components/textWithTooltip/textWithtooltip' import { Card } from 'design' -const DeduplicationInfoCard = ({ harvestingStatus }) => ( - -
- - {texts.info.title} - -
-
- - {valueOrDefault( - formatDate(harvestingStatus?.lastHarvestingDate), - 'Loading...' - )} - -
- - {texts.info.description} - -
-) +const DeduplicationInfoCard = ({ harvestingStatus }) => { + const { ...globalStore } = useContext(GlobalContext) + return ( + +
+ + {texts.info.title} + +
+ {globalStore?.setSelectedItem && ( +
+ + + + +
+ )} +
+
+
+ + {valueOrDefault( + formatDate(harvestingStatus?.lastHarvestingDate), + 'Loading...' + )} + +
+ + {texts.info.description} + +
+ ) +} export default DeduplicationInfoCard diff --git a/templates/deduplication/cards/deduplicationStatistics.jsx b/templates/deduplication/cards/deduplicationStatistics.jsx index 254661ea..e0c6df56 100644 --- a/templates/deduplication/cards/deduplicationStatistics.jsx +++ b/templates/deduplication/cards/deduplicationStatistics.jsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useContext } from 'react' import { Icon } from '@oacore/design' import styles from '../styles.module.css' @@ -6,6 +6,9 @@ import texts from '../../../texts/deduplication/deduplication.yml' import Actions from '../../../components/actions' import ExportButton from '../../../components/export-button' import { formatNumber } from '../../../utils/helpers' +import { GlobalContext } from '../../../store' +import checked from '../../../components/upload/assets/checkGreen.svg' +import TextWithTooltip from '../../../components/textWithTooltip/textWithtooltip' import { ProgressSpinner, Card } from 'design' @@ -13,47 +16,65 @@ const DeduplicationStatistics = ({ duplicateList, duplicatesUrl, checkBillingType, -}) => ( - -
- - {texts.info.countTitle} - - - } - /> -
-
- {texts.info.subTitle} - {duplicateList.count ? ( - {formatNumber(duplicateList.count)} - ) : ( -
- -

- This may take a while, longer for larger repositories ... -

+}) => { + const { ...globalStore } = useContext(GlobalContext) + return ( + +
+
+ + {texts.info.countTitle} + + {globalStore?.setSelectedItem && ( +
+ + + + +
+ )}
- )} - {!checkBillingType && ( - - {texts.info.action} - - )} -
-
-) + + } + /> +
+
+ {texts.info.subTitle} + {duplicateList.count !== undefined && duplicateList.count !== null ? ( + + {formatNumber(duplicateList.count)} + + ) : ( +
+ +

+ This may take a while, longer for larger repositories ... +

+
+ )} + {!checkBillingType && ( + + {texts.info.action} + + )} +
+ + ) +} export default DeduplicationStatistics diff --git a/templates/deduplication/index.jsx b/templates/deduplication/index.jsx index edaa1756..f3ec6759 100644 --- a/templates/deduplication/index.jsx +++ b/templates/deduplication/index.jsx @@ -85,10 +85,7 @@ const DeduplicationPageTemplate = observer( } />
- + { + const { ...globalStore } = useContext(GlobalContext) const [page, setPage] = useState(0) const [records, setRecords] = useState([]) const [localSearchTerm, setLocalSearchTerm] = useState('') @@ -39,10 +43,13 @@ const DeduplicationListTable = observer( else { const startIndex = page * 10 const endIndex = Math.min(startIndex + 10, list.length) - const newRecords = [...records, ...list.slice(startIndex, endIndex)] + let newRecords + if (globalStore.setSelectedItem) newRecords = list + else newRecords = [...records, ...list.slice(startIndex, endIndex)] + setRecords(newRecords) } - }, [page, list, checkBillingType]) + }, [page, list, checkBillingType, globalStore.setSelectedItem]) const searchChange = (event) => { setLocalSearchTerm(event.target.value) @@ -93,9 +100,22 @@ const DeduplicationListTable = observer( return ( <>
- - List of potential duplicates and alternative versions - +
+ + List of potential duplicates and alternative versions + + {globalStore?.setSelectedItem && ( +
+ + + + +
+ )} +
{checkBillingType ? ( ( - - {texts.dataOverview.title} -
- - {totalCount > 0 && ( +const DataOverviewCard = ({ totalCount, complianceLevel }) => { + const { ...globalStore } = useContext(GlobalContext) + return ( + +
+ {texts.dataOverview.title} + {globalStore?.setSelectedItem && ( +
+ + + + +
+ )} +
+
- )} -
- -
-) + {totalCount > 0 && ( + + )} +
+ +
+ ) +} export default DataOverviewCard diff --git a/templates/deposit-compliance/cards/deposit-time-lag-card.jsx b/templates/deposit-compliance/cards/deposit-time-lag-card.jsx index 23117575..6e1f6dc9 100644 --- a/templates/deposit-compliance/cards/deposit-time-lag-card.jsx +++ b/templates/deposit-compliance/cards/deposit-time-lag-card.jsx @@ -1,4 +1,9 @@ -import React from 'react' +import React, { useContext } from 'react' + +import checked from '../../../components/upload/assets/checkGreen.svg' +import styles from '../styles.module.css' +import { GlobalContext } from '../../../store' +import TextWithTooltip from '../../../components/textWithTooltip/textWithtooltip' import { Card } from 'design' import * as texts from 'texts/depositing' @@ -8,19 +13,35 @@ import Markdown from 'components/markdown' const DepositTimeLagCard = ({ timeLagData, isRetrieveDepositDatesInProgress, -}) => ( - - {texts.chart.title} - {timeLagData?.length > 0 && ( - <> - - {texts.chart.body} - - )} - {!timeLagData?.length && !isRetrieveDepositDatesInProgress && ( -

{texts.noData.body}

- )} -
-) +}) => { + const { ...globalStore } = useContext(GlobalContext) + return ( + +
+ {texts.chart.title} + {globalStore?.setSelectedItem && ( +
+ + + + +
+ )} +
+ {timeLagData?.length > 0 && ( + <> + + {texts.chart.body} + + )} + {!timeLagData?.length && !isRetrieveDepositDatesInProgress && ( +

{texts.noData.body}

+ )} +
+ ) +} export default DepositTimeLagCard diff --git a/templates/deposit-compliance/styles.module.css b/templates/deposit-compliance/styles.module.css index 57288a98..630d83ae 100644 --- a/templates/deposit-compliance/styles.module.css +++ b/templates/deposit-compliance/styles.module.css @@ -22,6 +22,21 @@ width: 100%; } +.set-header-wrapper { + display: flex; + justify-content: space-between; + width: 100%; +} + +.set-name { + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 130%; + color: var(--success-dark); + letter-spacing: 0.048px; +} + .numbers { display: flex; flex-wrap: wrap; diff --git a/templates/doi/cards/coverage-card.jsx b/templates/doi/cards/coverage-card.jsx index fc8532f2..e227fcdb 100644 --- a/templates/doi/cards/coverage-card.jsx +++ b/templates/doi/cards/coverage-card.jsx @@ -1,8 +1,11 @@ -import React from 'react' +import React, { useContext } from 'react' import { classNames } from '@oacore/design/lib/utils' import Parser from 'html-react-parser' import styles from '../styles.module.css' +import { GlobalContext } from '../../../store' +import checked from '../../../components/upload/assets/checkGreen.svg' +import TextWithTooltip from '../../../components/textWithTooltip/textWithtooltip' import { Card } from 'design' import Markdown from 'components/markdown' @@ -54,49 +57,65 @@ const CoverageEnrichmentChart = ({ ) } -const CoverageCard = ({ doiCount, totalCount, enrichmentSize }) => ( - - {texts.coverage.title} -
-
- -
-
- +const CoverageCard = ({ doiCount, totalCount, enrichmentSize }) => { + const { ...globalStore } = useContext(GlobalContext) + return ( + +
+ {texts.coverage.title} + {globalStore?.setSelectedItem && ( +
+ + + + +
+ )}
-
- 0 && styles.enrichment)} - tag="p" - title={Parser(texts.coverage.enrichmentLabelTooltip)} - /> +
+
+ +
+
+ +
+
+ 0 && styles.enrichment)} + tag="p" + title={Parser(texts.coverage.enrichmentLabelTooltip)} + /> +
-
- - {enrichmentSize > 0 && ( - - {texts.coverage.body.render({ count: enrichmentSize })} - - )} -
-) + + {enrichmentSize > 0 && ( + + {texts.coverage.body.render({ count: enrichmentSize })} + + )} + + ) +} export default CoverageCard diff --git a/templates/doi/cards/table-card.jsx b/templates/doi/cards/table-card.jsx index 89c63c27..70a20aff 100644 --- a/templates/doi/cards/table-card.jsx +++ b/templates/doi/cards/table-card.jsx @@ -1,8 +1,11 @@ -import React from 'react' +import React, { useContext } from 'react' import { useObserver } from 'mobx-react-lite' import { classNames } from '@oacore/design/lib/utils' import styles from '../styles.module.css' +import checked from '../../../components/upload/assets/checkGreen.svg' +import { GlobalContext } from '../../../store' +import TextWithTooltip from '../../../components/textWithTooltip/textWithtooltip' import { PaymentRequiredError } from 'store/errors' import { Card, Icon } from 'design' @@ -28,6 +31,7 @@ const formatDOI = (entity) => { } const TableCard = ({ pages, exportUrl }) => { + const { ...globalStore } = useContext(GlobalContext) const [tableProps, fetchData] = useDynamicTableData({ pages, defaultSize: 5, @@ -38,7 +42,20 @@ const TableCard = ({ pages, exportUrl }) => { return ( - Browse DOI records +
+ Browse DOI records + {globalStore?.setSelectedItem && ( +
+ + + + +
+ )} +
{ + const { ...globalStore } = useContext(GlobalContext) const rrsToReviewList = rrsList.filter( (item) => item.validationStatusRRS !== 1 && item.validationStatusRRS !== 2 ) @@ -52,6 +56,17 @@ const RrsReviewCard = ({ rrsList, rrsDataLoading }) => { + {globalStore?.setSelectedItem && ( +
+ + + + +
+ )} ) diff --git a/templates/rrs-policy/cards/rrsStatsCard.jsx b/templates/rrs-policy/cards/rrsStatsCard.jsx index dc249d93..fdc61a5d 100644 --- a/templates/rrs-policy/cards/rrsStatsCard.jsx +++ b/templates/rrs-policy/cards/rrsStatsCard.jsx @@ -1,8 +1,11 @@ -import React from 'react' +import React, { useContext } from 'react' import styles from '../styles.module.css' import { formatNumber } from '../../../utils/helpers' import { Button } from '../../../design' +import { GlobalContext } from '../../../store' +import checked from '../../../components/upload/assets/checkGreen.svg' +import TextWithTooltip from '../../../components/textWithTooltip/textWithtooltip' import rrs from 'texts/rrs-retention' import { Card } from 'design' @@ -12,40 +15,56 @@ const RrsStatsCard = ({ rrsList, rrsDataLoading, checkBillingType, -}) => ( - -
-
- - {rrs.statsCard.title} - -
-
- - {rrs.statsCard.description} - -
-
- {rrsDataLoading ? ( -
-
+}) => { + const { ...globalStore } = useContext(GlobalContext) + return ( + +
+
+ + {rrs.statsCard.title} +
- ) : ( -
{formatNumber(rrsList.length)}
- )} -
-
- {!checkBillingType && ( - - )} -
-
-) +
+ + {rrs.statsCard.description} + +
+
+ {rrsDataLoading ? ( +
+
+
+ ) : ( +
{formatNumber(rrsList.length)}
+ )} +
+
+ {!checkBillingType && ( +
+ + {globalStore?.setSelectedItem && ( +
+ + + + +
+ )} +
+ )} +
+ + ) +} export default RrsStatsCard diff --git a/templates/rrs-policy/styles.module.css b/templates/rrs-policy/styles.module.css index 26d2c077..f1079011 100644 --- a/templates/rrs-policy/styles.module.css +++ b/templates/rrs-policy/styles.module.css @@ -83,6 +83,21 @@ justify-content: space-between; } +.set-header-wrapper { + display: flex; + justify-content: space-between; + width: 100%; +} + +.set-name { + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 130%; + color: var(--success-dark); + letter-spacing: 0.048px; +} + .sub-footer { margin: 28px 0; font-size: 48px; diff --git a/templates/rrs-policy/tables/rrsTable.jsx b/templates/rrs-policy/tables/rrsTable.jsx index 714fbd40..198097a9 100644 --- a/templates/rrs-policy/tables/rrsTable.jsx +++ b/templates/rrs-policy/tables/rrsTable.jsx @@ -1,5 +1,5 @@ import { observer } from 'mobx-react-lite' -import React, { useEffect, useRef, useState } from 'react' +import React, { useContext, useEffect, useRef, useState } from 'react' import { useRouter } from 'next/router' import { Button } from '@oacore/design/lib/elements' import { Popover } from '@oacore/design' @@ -20,6 +20,9 @@ import request from '../../../api' import StatusCard from '../cards/statusCard' import AccessPlaceholder from '../../../components/access-placeholder/AccessPlaceholder' import Tablev2 from '../../../components/tablev2/tablev2' +import { GlobalContext } from '../../../store' +import checked from '../../../components/upload/assets/checkGreen.svg' +import TextWithTooltip from '../../../components/textWithTooltip/textWithtooltip' import Table from 'components/table' @@ -36,6 +39,7 @@ const RrsTable = observer( dataProviderData, rrsUrl, }) => { + const { ...globalStore } = useContext(GlobalContext) const [visibleHelp, setVisibleHelp] = useState( localStorage.getItem('rrsHelp') === 'true' ) @@ -191,7 +195,20 @@ const RrsTable = observer( return ( - {texts.table.title} +
+ {texts.table.title} + {globalStore?.setSelectedItem && ( +
+ + + + +
+ )} +
{texts.table.subTitle}
{rrsDataLoading ? (
diff --git a/templates/settings/repository.jsx b/templates/settings/repository.jsx index d6ddb665..c435501a 100644 --- a/templates/settings/repository.jsx +++ b/templates/settings/repository.jsx @@ -5,7 +5,7 @@ import { Button } from '@oacore/design/lib/elements' import { useRouter } from 'next/router' import styles from './styles.module.css' -import { Card, TextField } from '../../design' +import { Card, ProgressSpinner, TextField } from '../../design' import content from '../../texts/settings' import Markdown from '../../components/markdown' import Upload from '../../components/upload' @@ -16,6 +16,8 @@ import DropdownInput from '../../components/input-select/input-select' import warning from './assets/warning.svg' import { GlobalContext } from '../../store' import infoGreen from '../../components/upload/assets/infoGreen.svg' +import removeBin from '../../components/upload/assets/removeBin.svg' +import toggleArrow from '../../components/upload/assets/dropdownArrow.svg' const UploadSection = ({ className, @@ -76,6 +78,20 @@ const RepositoryPageTemplate = observer( setGlobalRorId, init, status, + setsList, + loadingSets, + enableSet, + enabledList, + disabledList, + deleteSet, + getSetsWholeList, + wholeSetData, + loadingWholeSets, + loadingWholeSetsBtn, + getSetsEnabledList, + setLoadingWholeSetsBtn, + loadingRemoveItem, + setLoadingRemoveAction, tag: Tag = 'main', ...restProps }) => { @@ -93,9 +109,18 @@ const RepositoryPageTemplate = observer( const [isChanged, setChanged] = useState(false) const [isNameOpen, setNameIsOpen] = useState(false) const [isIdOpen, setIdIsOpen] = useState(false) - const [isNameChanged, setNameChanged] = useState(false) const [isFormSubmitted, setFormSubmitted] = useState(false) + const [selectedItem, setSelectedItem] = useState(null) + const dropdownRef = useRef(null) + const [isOpen, setIsOpen] = useState(false) + const [setNameDisplay, setSetNameDisplay] = useState({}) + const [isEditing, setIsEditing] = useState({}) + const [showFullList, setShowFullList] = useState(false) + const [inputValue, setInputValue] = useState('') + + const router = useRouter() + const providerId = router.query['data-provider-id'] useEffect(() => { fetch(`https://api.ror.org/organizations?query=${rorId}`) @@ -119,15 +144,20 @@ const RepositoryPageTemplate = observer( }) }, [rorName]) + useEffect(() => { + getSetsEnabledList() + }, [providerId]) + const uploadRef = useRef(null) const mappingRef = useRef(null) - const router = useRouter() + const setRef = useRef(null) const isStartingMember = membershipPlan.billing_type === 'starting' const scrollTarget = { upload: uploadRef, mapping: mappingRef, + sets: setRef, } useScrollEffect(scrollTarget[router.query.referrer]) @@ -142,6 +172,7 @@ const RepositoryPageTemplate = observer( const present = { 'data-provider': updateDataProvider, 'mapping': mappingSubmit, + 'sets': mappingSubmit, }[scope] const result = await present(data) @@ -206,6 +237,108 @@ const RepositoryPageTemplate = observer( return null } + const handleDropdownClick = async () => { + setIsOpen(!isOpen) + if (!wholeSetData.length) await getSetsWholeList() + } + + const handleSelect = (item) => { + setSelectedItem(item) + setInputValue(item.setName) + setIsOpen(false) + } + + const handleAddClick = async () => { + if (selectedItem) { + try { + setLoadingWholeSetsBtn(true) + await enableSet({ + setSpec: selectedItem.setSpec, + setName: selectedItem.setName, + setNameDisplay: selectedItem.setNameDisplay, + }) + setSelectedItem(null) + await getSetsWholeList() + await getSetsEnabledList() + } catch (error) { + console.error('Error patching settings:', error) + } finally { + setLoadingWholeSetsBtn(false) + } + } + } + + const handleDelete = async (id) => { + try { + setLoadingRemoveAction(true, id) + await deleteSet(id) + await getSetsWholeList() + await getSetsEnabledList() + } catch (error) { + console.error('Error patching settings:', error) + } finally { + setLoadingRemoveAction(false, id) + } + } + + const handleInputChange = (id, event) => { + setSetNameDisplay((prevState) => ({ + ...prevState, + [id]: event.target.value, + })) + } + + const handleEditClick = (id) => { + setIsEditing((prevState) => ({ + ...prevState, + [id]: true, + })) + } + + const handleButtonClick = async (item) => { + try { + await enableSet({ + id: item.id, + setSpec: item.setSpec, + setName: item.setName, + setNameDisplay: setNameDisplay[item.id], + }) + setIsEditing((prevState) => ({ + ...prevState, + [item.id]: false, + })) + } catch (error) { + console.error('Error patching settings:', error) + } + } + + useEffect(() => { + const handleClickOutside = (event) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) + setIsOpen(false) + } + + document.addEventListener('mousedown', handleClickOutside) + + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, [dropdownRef]) + + const displayAllSets = showFullList ? enabledList : enabledList.slice(0, 3) + + const toggleList = () => { + setShowFullList(!showFullList) + } + + const handleSetInputChange = (event) => { + setInputValue(event.target.value) + } + + const filteredData = wholeSetData.filter((item) => + item.setName.toLowerCase().includes(inputValue.toLowerCase()) + ) + return ( )} + {globalStore.enabledList.length > 0 ? ( +
+ +
+
+ {content.sets.title} + + {content.sets.description} + +
+ {displayAllSets.map((item) => ( +
+
+
+ 100 + ? `${setNameDisplay[item.id].substring( + 0, + 100 + )}...` + : setNameDisplay[item.id]) || + (item?.setNameDisplay?.length > 100 + ? `${item.setNameDisplay.substring( + 0, + 100 + )}...` + : item.setNameDisplay) + } + onChange={(event) => + handleInputChange(item.id, event) + } + className={styles.setInnerField} + disabled={!isEditing[item.id]} + /> + {!isEditing[item.id] ? ( + + ) : ( + + )} +
+
+ {loadingRemoveItem.id === item.id && + loadingRemoveItem.value ? ( +
+ +
+ ) : ( + // eslint-disable-next-line max-len + // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-noninteractive-element-interactions + handleDelete(item.id)} + src={removeBin} + alt="" + /> + )} +
+
+
+
+
setName
+ + {item.setName.length > 110 + ? `${item.setName.substring(0, 110)}...` + : item.setName} + +
+
+
setSpec
+ + {item.setSpec.length > 110 + ? `${item.setSpec.substring(0, 110)}...` + : item.setSpec} + +
+
+
+ ))} + {enabledList.length > 3 && ( + + )} +
+
+
+
+ + +
+ +
+ {isOpen && ( +
+ {loadingWholeSets ? ( +

Loading...

+ ) : ( +
    + {filteredData.map((item) => ( + // eslint-disable-next-line max-len + // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-noninteractive-element-interactions +
  • handleSelect(item)} + className={styles.selectItem} + > + {item.setName} +
  • + ))} +
+ )} +
+ )} +
+
+
+
+
+ ) : ( + <> + )}