diff --git a/documentation/docs/01-users/05-adding-software.md b/documentation/docs/01-users/05-adding-software.md
index bdae64f70..78965eef9 100644
--- a/documentation/docs/01-users/05-adding-software.md
+++ b/documentation/docs/01-users/05-adding-software.md
@@ -65,12 +65,12 @@ You need to use the full URL of the image and the image needs to send CORS heade
```
-### Document URL
+### Markdown URL
You can link to a remote Markdown file which will be dynamically loaded by the RSD. An often used approach is to link to a readme file on the GitHub repository. In this case you need to link to the `raw` version of the readme file. For example, to link to the readme file of the RSD repository, we used the link https://raw.githubusercontent.com/research-software-directory/RSD-as-a-service/main/README.md. **Note that the URL domain is different** `https://raw.githubusercontent.com/` from the default GitHub domain (https://github.com).
:::warning
-When using a Document URL to point to a remote Markdown file on the GitHub, you need to provide a URL to a raw Markdown file (see the animation below). In addition, all links used in the Markdown document, like **images**, need to use the **full URL** (e.g. use `https://user-images.githubusercontent.com/4195550/136156498-736f915f-7623-43d2-8678-f30b06563a38.png`, not `/4195550/136156498-736f915f-7623-43d2-8678-f30b06563a38.png`). This is required, because the Markdown content is loaded from the GitHub domain into the RSD website.
+When using a Markdown URL to point to a remote Markdown file all links used in the Markdown document, like **images**, need to use the **full URL** (e.g. use `https://user-images.githubusercontent.com/4195550/136156498-736f915f-7623-43d2-8678-f30b06563a38.png`, not `/4195550/136156498-736f915f-7623-43d2-8678-f30b06563a38.png`). This is required, because the Markdown content is loaded into the RSD website.
:::
### Logo
diff --git a/frontend/components/layout/PageErrorMessage.tsx b/frontend/components/layout/PageErrorMessage.tsx
index c143df463..e57e7586c 100644
--- a/frontend/components/layout/PageErrorMessage.tsx
+++ b/frontend/components/layout/PageErrorMessage.tsx
@@ -1,18 +1,28 @@
// SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all)
// SPDX-FileCopyrightText: 2022 dv4all
+// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center)
+// SPDX-FileCopyrightText: 2024 Netherlands eScience Center
//
// SPDX-License-Identifier: Apache-2.0
import ContentInTheMiddle from './ContentInTheMiddle'
-export default function PageErrorMessage({status = 500, message = 'Failed to process you request'}: {
- message:string, status?:number
-}) {
+type PageErrorMessageProps=Readonly<{
+ message:string,
+ status?:number,
+ children?:any
+}>
+
+export default function PageErrorMessage({
+ message = 'Failed to process you request',
+ status = 500,
+ children
+}:PageErrorMessageProps) {
return (
{status}
-
{message}
+ {children ||
{message}
}
)
diff --git a/frontend/components/software/edit/editSoftwareConfig.tsx b/frontend/components/software/edit/editSoftwareConfig.tsx
index f993f419c..1cb9ec196 100644
--- a/frontend/components/software/edit/editSoftwareConfig.tsx
+++ b/frontend/components/software/edit/editSoftwareConfig.tsx
@@ -11,6 +11,8 @@
//
// SPDX-License-Identifier: Apache-2.0
+import {isProperUrl} from '~/utils/fetchHelpers'
+
export const softwareInformation = {
slug: {
label: 'RSD path (admin only)',
@@ -62,14 +64,19 @@ export const softwareInformation = {
description_url: {
label: 'URL of (raw) Markdown file',
help: <>
- Read here how to properly link to a raw Markdown URL
+ Read here how to properly link to a raw Markdown URL
>,
validation: {
- required: 'Valid markdown URL must be provided',
+ required: 'Valid markdown URL is required',
maxLength: {value: 200, message: 'Maximum length is 200'},
- pattern: {
- value: /^https?:\/\/.+\..+\.md$/,
- message: 'URL should start with http(s):// have at least one dot (.) and end with (.md)'
+ // custom validation function for correct url syntax
+ validate: (url:string)=>{
+ // return error message if not correct url syntax
+ if (isProperUrl(url)===false){
+ return 'Invalid url. Please improve your input'
+ }
+ // has correct url syntax
+ return true
}
}
},
@@ -100,70 +107,7 @@ export const contributorInformation = {
infoLink: '/documentation/users/adding-software/#contributors',
label: 'Import contributors',
message: (doi: string) => `Import contributors from datacite.org using DOI ${doi}`
- },
- // We use default config of AggregatedPersonModal
- // Dusan, 2024-09-12
- // is_contact_person: {
- // label: 'Contact person',
- // help: 'Is this contributor the main contact person?'
- // },
- // given_names: {
- // label: 'First name / Given name(s)',
- // help: 'One or more given names',
- // validation: {
- // required: 'First name is required',
- // minLength: {value: 1, message: 'Minimum length is 1'},
- // maxLength: {value: 200, message: 'Maximum length is 200'},
- // }
- // },
- // family_names: {
- // label: 'Last name / Family name(s)',
- // help: 'Include "de/van/van den ...etc."',
- // validation: {
- // required: 'Family name is required',
- // minLength: {value: 2, message: 'Minimum length is 2'},
- // maxLength: {value: 200, message: 'Maximum length is 200'},
- // }
- // },
- // email_address: {
- // label: 'Email',
- // help: 'Contact person should have an email',
- // validation: (required:boolean) => ({
- // required: required ? 'Contact person should have an email' : false,
- // minLength: {value: 5, message: 'Minimum length is 5'},
- // maxLength: {value: 200, message: 'Maximum length is 200'},
- // pattern: {
- // value: /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
- // message: 'Invalid email address'
- // }
- // })
- // },
- // affiliation: {
- // label: 'Affiliation',
- // help: 'Select or type in the current affiliation?',
- // validation: {
- // minLength: {value: 2, message: 'Minimum length is 2'},
- // maxLength: {value: 200, message: 'Maximum length is 200'},
- // }
- // },
- // role: {
- // label: 'Role',
- // help: 'For this software',
- // validation: {
- // minLength: {value: 2, message: 'Minimum length is 2'},
- // maxLength: {value: 200, message: 'Maximum length is 200'},
- // }
- // },
- // orcid: {
- // label: 'ORCID',
- // help: '16 digits, pattern 0000-0000-0000-0000',
- // validation: {
- // pattern: {
- // value: /^\d{4}-\d{4}-\d{4}-\d{3}[0-9X]$/,
- // message: 'Invalid pattern, not a 0000-0000-0000-0000'
- // }
- // }
- // }
+ }
}
diff --git a/frontend/components/software/edit/information/AutosaveRemoteMarkdown.tsx b/frontend/components/software/edit/information/AutosaveRemoteMarkdown.tsx
index 2572dec04..e9fbfbc35 100644
--- a/frontend/components/software/edit/information/AutosaveRemoteMarkdown.tsx
+++ b/frontend/components/software/edit/information/AutosaveRemoteMarkdown.tsx
@@ -8,35 +8,37 @@
// SPDX-License-Identifier: Apache-2.0
import {useState,useEffect} from 'react'
-import {useWatch,useFormState} from 'react-hook-form'
+import Button from '@mui/material/Button'
+import {useWatch,useFormState, useFormContext} from 'react-hook-form'
import {EditSoftwareItem} from '~/types/SoftwareTypes'
-import {getRemoteMarkdown} from '~/utils/getSoftware'
import {useDebounceValid} from '~/utils/useDebounce'
+import {apiRemoteMarkdown} from '~/utils/getSoftware'
import ReactMarkdownWithSettings from '~/components/layout/ReactMarkdownWithSettings'
import PageErrorMessage from '~/components/layout/PageErrorMessage'
import ContentLoader from '~/components/layout/ContentLoader'
import AutosaveControlledTextField, {OnSaveProps} from '~/components/form/AutosaveControlledTextField'
import {ControlledTextFieldOptions} from '~/components/form/ControlledTextField'
+import {softwareInformation as config} from '../editSoftwareConfig'
-type AutosaveRemoteMarkdownProps = {
- control: any,
+type AutosaveRemoteMarkdownProps = Readonly<{
rules: any,
options: ControlledTextFieldOptions,
onSaveField: ({name,value}: OnSaveProps) => void
-}
+}>
-export default function AutosaveRemoteMarkdown({control,rules,options,onSaveField}: AutosaveRemoteMarkdownProps) {
- // watch for change
- const markdownUrl = useWatch({control, name: options.name})
+export default function AutosaveRemoteMarkdown({rules,options,onSaveField}: AutosaveRemoteMarkdownProps) {
+ const {control, setError, setValue} = useFormContext()
const {errors} = useFormState({control})
- const debouncedUrl = useDebounceValid(markdownUrl,errors[options.name],1000)
+ const markdownUrl = useWatch({control, name: options.name})
+ const debouncedUrl = useDebounceValid(markdownUrl,errors[options.name],700)
const [loading, setLoading] = useState(false)
const [state, setState] = useState<{
markdown: string | null,
error: {
status: number | null,
- message: string | null
+ message: string | null,
+ rawUrl?: string
}
}>({
markdown: null,
@@ -45,63 +47,68 @@ export default function AutosaveRemoteMarkdown({control,rules,options,onSaveFiel
message: null
}
})
- // listen for error
- let error:any
- if (errors.hasOwnProperty(options.name) === true) {
- error = errors[options.name]
- }
// console.group(`AutosaveRemoteMarkdownProps...${options.name}`)
// console.log('markdownUrl...',markdownUrl)
// console.log('debouncedUrl...',debouncedUrl)
// console.log('errors...', errors)
- // console.log('error...', error)
+ // console.log('state...', state)
// console.groupEnd()
useEffect(() => {
let abort = false
const getMarkdown=async(url:string)=>{
setLoading(true)
- const markdown = await getRemoteMarkdown(url)
+ const resp = await apiRemoteMarkdown(url)
// exit on abort
if (abort) return
- if (typeof markdown === 'string') {
+ if (resp.status===200) {
+ // debugger
setState({
- markdown,
+ markdown: resp.message,
error: {
status: null,
message: null
}
})
- } else if (typeof markdown === 'object'){
+ } else {
// create error
setState({
markdown: null,
error: {
- ...markdown
+ ...resp
}
})
+ // set error to input form
+ setError(options.name,{
+ type:'custom',
+ message: resp.message
+ })
}
setLoading(false)
}
// if there is markdownUrl value
- if (debouncedUrl &&
- debouncedUrl.length > 9) {
+ if (debouncedUrl && debouncedUrl.length > 9) {
if (abort) return
getMarkdown(debouncedUrl)
- } else if (!debouncedUrl) {
- if (abort) return
- setLoading(false)
- setState({
- markdown: '',
- error: {
- status: 200,
- message: 'Waiting for input'
- }
- })
}
return ()=>{abort=true}
- }, [debouncedUrl])
+ }, [debouncedUrl,options.name,setError])
+
+ function useSuggestedUrl(){
+ if (state.error?.rawUrl){
+ // change input value
+ setValue(options.name,state.error?.rawUrl,{
+ shouldValidate:true,
+ shouldDirty:true
+ })
+ // save change
+ onSaveField({
+ name: options.name,
+ value: state.error?.rawUrl
+ })
+ }
+ }
function renderContent() {
if (loading) {
@@ -112,7 +119,28 @@ export default function AutosaveRemoteMarkdown({control,rules,options,onSaveFiel
+ >
+