index.tsx 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. 'use client'
  2. import React, { useCallback, useState } from 'react'
  3. import Modal from '@/app/components/base/modal'
  4. import type { Item } from '@/app/components/base/select'
  5. import type { InstallState } from '@/app/components/plugins/types'
  6. import { useGitHubReleases } from '../hooks'
  7. import { convertRepoToUrl, parseGitHubUrl } from '../utils'
  8. import type { PluginDeclaration, UpdateFromGitHubPayload } from '../../types'
  9. import { InstallStepFromGitHub } from '../../types'
  10. import Toast from '@/app/components/base/toast'
  11. import SetURL from './steps/setURL'
  12. import SelectPackage from './steps/selectPackage'
  13. import Installed from '../base/installed'
  14. import Loaded from './steps/loaded'
  15. import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
  16. import { useTranslation } from 'react-i18next'
  17. import useRefreshPluginList from '../hooks/use-refresh-plugin-list'
  18. import cn from '@/utils/classnames'
  19. import useHideLogic from '../hooks/use-hide-logic'
  20. const i18nPrefix = 'plugin.installFromGitHub'
  21. type InstallFromGitHubProps = {
  22. updatePayload?: UpdateFromGitHubPayload
  23. onClose: () => void
  24. onSuccess: () => void
  25. }
  26. const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, onClose, onSuccess }) => {
  27. const { t } = useTranslation()
  28. const { getIconUrl } = useGetIcon()
  29. const { fetchReleases } = useGitHubReleases()
  30. const { refreshPluginList } = useRefreshPluginList()
  31. const {
  32. modalClassName,
  33. foldAnimInto,
  34. setIsInstalling,
  35. handleStartToInstall,
  36. } = useHideLogic(onClose)
  37. const [state, setState] = useState<InstallState>({
  38. step: updatePayload ? InstallStepFromGitHub.selectPackage : InstallStepFromGitHub.setUrl,
  39. repoUrl: updatePayload?.originalPackageInfo?.repo
  40. ? convertRepoToUrl(updatePayload.originalPackageInfo.repo)
  41. : '',
  42. selectedVersion: '',
  43. selectedPackage: '',
  44. releases: updatePayload ? updatePayload.originalPackageInfo.releases : [],
  45. })
  46. const [uniqueIdentifier, setUniqueIdentifier] = useState<string | null>(null)
  47. const [manifest, setManifest] = useState<PluginDeclaration | null>(null)
  48. const [errorMsg, setErrorMsg] = useState<string | null>(null)
  49. const versions: Item[] = state.releases.map(release => ({
  50. value: release.tag_name,
  51. name: release.tag_name,
  52. }))
  53. const packages: Item[] = state.selectedVersion
  54. ? (state.releases
  55. .find(release => release.tag_name === state.selectedVersion)
  56. ?.assets
  57. .map(asset => ({
  58. value: asset.name,
  59. name: asset.name,
  60. })) || [])
  61. : []
  62. const getTitle = useCallback(() => {
  63. if (state.step === InstallStepFromGitHub.installed)
  64. return t(`${i18nPrefix}.installedSuccessfully`)
  65. if (state.step === InstallStepFromGitHub.installFailed)
  66. return t(`${i18nPrefix}.installFailed`)
  67. return updatePayload ? t(`${i18nPrefix}.updatePlugin`) : t(`${i18nPrefix}.installPlugin`)
  68. }, [state.step, t, updatePayload])
  69. const handleUrlSubmit = async () => {
  70. const { isValid, owner, repo } = parseGitHubUrl(state.repoUrl)
  71. if (!isValid || !owner || !repo) {
  72. Toast.notify({
  73. type: 'error',
  74. message: t('plugin.error.inValidGitHubUrl'),
  75. })
  76. return
  77. }
  78. try {
  79. const fetchedReleases = await fetchReleases(owner, repo)
  80. if (fetchedReleases.length > 0) {
  81. setState(prevState => ({
  82. ...prevState,
  83. releases: fetchedReleases,
  84. step: InstallStepFromGitHub.selectPackage,
  85. }))
  86. }
  87. else {
  88. Toast.notify({
  89. type: 'error',
  90. message: t('plugin.error.noReleasesFound'),
  91. })
  92. }
  93. }
  94. catch (error) {
  95. Toast.notify({
  96. type: 'error',
  97. message: t('plugin.error.fetchReleasesError'),
  98. })
  99. }
  100. }
  101. const handleError = (e: any, isInstall: boolean) => {
  102. const message = e?.response?.message || t('plugin.installModal.installFailedDesc')
  103. setErrorMsg(message)
  104. setState(prevState => ({ ...prevState, step: isInstall ? InstallStepFromGitHub.installFailed : InstallStepFromGitHub.uploadFailed }))
  105. }
  106. const handleUploaded = async (GitHubPackage: any) => {
  107. try {
  108. const icon = await getIconUrl(GitHubPackage.manifest.icon)
  109. setManifest({
  110. ...GitHubPackage.manifest,
  111. icon,
  112. })
  113. setUniqueIdentifier(GitHubPackage.uniqueIdentifier)
  114. setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.readyToInstall }))
  115. }
  116. catch (e) {
  117. handleError(e, false)
  118. }
  119. }
  120. const handleUploadFail = useCallback((errorMsg: string) => {
  121. setErrorMsg(errorMsg)
  122. setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.uploadFailed }))
  123. }, [])
  124. const handleInstalled = useCallback((notRefresh?: boolean) => {
  125. setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.installed }))
  126. if (!notRefresh)
  127. refreshPluginList(manifest)
  128. setIsInstalling(false)
  129. onSuccess()
  130. }, [manifest, onSuccess, refreshPluginList, setIsInstalling])
  131. const handleFailed = useCallback((errorMsg?: string) => {
  132. setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.installFailed }))
  133. setIsInstalling(false)
  134. if (errorMsg)
  135. setErrorMsg(errorMsg)
  136. }, [setIsInstalling])
  137. const handleBack = () => {
  138. setState((prevState) => {
  139. switch (prevState.step) {
  140. case InstallStepFromGitHub.selectPackage:
  141. return { ...prevState, step: InstallStepFromGitHub.setUrl }
  142. case InstallStepFromGitHub.readyToInstall:
  143. return { ...prevState, step: InstallStepFromGitHub.selectPackage }
  144. default:
  145. return prevState
  146. }
  147. })
  148. }
  149. return (
  150. <Modal
  151. isShow={true}
  152. onClose={foldAnimInto}
  153. className={cn(modalClassName, `flex min-w-[560px] p-0 flex-col items-start rounded-2xl border-[0.5px]
  154. border-components-panel-border bg-components-panel-bg shadows-shadow-xl`)}
  155. closable
  156. >
  157. <div className='flex pt-6 pl-6 pb-3 pr-14 items-start gap-2 self-stretch'>
  158. <div className='flex flex-col items-start gap-1 grow'>
  159. <div className='self-stretch text-text-primary title-2xl-semi-bold'>
  160. {getTitle()}
  161. </div>
  162. <div className='self-stretch text-text-tertiary system-xs-regular'>
  163. {!([InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installed, InstallStepFromGitHub.installFailed].includes(state.step)) && t('plugin.installFromGitHub.installNote')}
  164. </div>
  165. </div>
  166. </div>
  167. {([InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installed, InstallStepFromGitHub.installFailed].includes(state.step))
  168. ? <Installed
  169. payload={manifest}
  170. isFailed={[InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installFailed].includes(state.step)}
  171. errMsg={errorMsg}
  172. onCancel={onClose}
  173. />
  174. : <div className={`flex px-6 py-3 flex-col justify-center items-start self-stretch ${state.step === InstallStepFromGitHub.installed ? 'gap-2' : 'gap-4'}`}>
  175. {state.step === InstallStepFromGitHub.setUrl && (
  176. <SetURL
  177. repoUrl={state.repoUrl}
  178. onChange={value => setState(prevState => ({ ...prevState, repoUrl: value }))}
  179. onNext={handleUrlSubmit}
  180. onCancel={onClose}
  181. />
  182. )}
  183. {state.step === InstallStepFromGitHub.selectPackage && (
  184. <SelectPackage
  185. updatePayload={updatePayload!}
  186. repoUrl={state.repoUrl}
  187. selectedVersion={state.selectedVersion}
  188. versions={versions}
  189. onSelectVersion={item => setState(prevState => ({ ...prevState, selectedVersion: item.value as string }))}
  190. selectedPackage={state.selectedPackage}
  191. packages={packages}
  192. onSelectPackage={item => setState(prevState => ({ ...prevState, selectedPackage: item.value as string }))}
  193. onUploaded={handleUploaded}
  194. onFailed={handleUploadFail}
  195. onBack={handleBack}
  196. />
  197. )}
  198. {state.step === InstallStepFromGitHub.readyToInstall && (
  199. <Loaded
  200. updatePayload={updatePayload!}
  201. uniqueIdentifier={uniqueIdentifier!}
  202. payload={manifest as any}
  203. repoUrl={state.repoUrl}
  204. selectedVersion={state.selectedVersion}
  205. selectedPackage={state.selectedPackage}
  206. onBack={handleBack}
  207. onStartToInstall={handleStartToInstall}
  208. onInstalled={handleInstalled}
  209. onFailed={handleFailed}
  210. />
  211. )}
  212. </div>}
  213. </Modal>
  214. )
  215. }
  216. export default InstallFromGitHub