index.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. 'use client'
  2. import type { MouseEventHandler } from 'react'
  3. import { useMemo, useRef, useState } from 'react'
  4. import { useRouter } from 'next/navigation'
  5. import { useContext } from 'use-context-selector'
  6. import { useTranslation } from 'react-i18next'
  7. import { RiCloseLine } from '@remixicon/react'
  8. import Uploader from './uploader'
  9. import Button from '@/app/components/base/button'
  10. import Modal from '@/app/components/base/modal'
  11. import { ToastContext } from '@/app/components/base/toast'
  12. import {
  13. importApp,
  14. importAppFromUrl,
  15. } from '@/service/apps'
  16. import { useAppContext } from '@/context/app-context'
  17. import { useProviderContext } from '@/context/provider-context'
  18. import AppsFull from '@/app/components/billing/apps-full-in-dialog'
  19. import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
  20. import { getRedirection } from '@/utils/app-redirection'
  21. import cn from '@/utils/classnames'
  22. type CreateFromDSLModalProps = {
  23. show: boolean
  24. onSuccess?: () => void
  25. onClose: () => void
  26. activeTab?: string
  27. dslUrl?: string
  28. }
  29. export enum CreateFromDSLModalTab {
  30. FROM_FILE = 'from-file',
  31. FROM_URL = 'from-url',
  32. }
  33. const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDSLModalTab.FROM_FILE, dslUrl = '' }: CreateFromDSLModalProps) => {
  34. const { push } = useRouter()
  35. const { t } = useTranslation()
  36. const { notify } = useContext(ToastContext)
  37. const [currentFile, setDSLFile] = useState<File>()
  38. const [fileContent, setFileContent] = useState<string>()
  39. const [currentTab, setCurrentTab] = useState(activeTab)
  40. const [dslUrlValue, setDslUrlValue] = useState(dslUrl)
  41. const readFile = (file: File) => {
  42. const reader = new FileReader()
  43. reader.onload = function (event) {
  44. const content = event.target?.result
  45. setFileContent(content as string)
  46. }
  47. reader.readAsText(file)
  48. }
  49. const handleFile = (file?: File) => {
  50. setDSLFile(file)
  51. if (file)
  52. readFile(file)
  53. if (!file)
  54. setFileContent('')
  55. }
  56. const { isCurrentWorkspaceEditor } = useAppContext()
  57. const { plan, enableBilling } = useProviderContext()
  58. const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps)
  59. const isCreatingRef = useRef(false)
  60. const onCreate: MouseEventHandler = async () => {
  61. if (currentTab === CreateFromDSLModalTab.FROM_FILE && !currentFile)
  62. return
  63. if (currentTab === CreateFromDSLModalTab.FROM_URL && !dslUrlValue)
  64. return
  65. if (isCreatingRef.current)
  66. return
  67. isCreatingRef.current = true
  68. try {
  69. let app
  70. if (currentTab === CreateFromDSLModalTab.FROM_FILE) {
  71. app = await importApp({
  72. data: fileContent || '',
  73. })
  74. }
  75. if (currentTab === CreateFromDSLModalTab.FROM_URL) {
  76. app = await importAppFromUrl({
  77. url: dslUrlValue || '',
  78. })
  79. }
  80. if (onSuccess)
  81. onSuccess()
  82. if (onClose)
  83. onClose()
  84. notify({ type: 'success', message: t('app.newApp.appCreated') })
  85. localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
  86. getRedirection(isCurrentWorkspaceEditor, app, push)
  87. }
  88. catch (e) {
  89. notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
  90. }
  91. isCreatingRef.current = false
  92. }
  93. const tabs = [
  94. {
  95. key: CreateFromDSLModalTab.FROM_FILE,
  96. label: t('app.importFromDSLFile'),
  97. },
  98. {
  99. key: CreateFromDSLModalTab.FROM_URL,
  100. label: t('app.importFromDSLUrl'),
  101. },
  102. ]
  103. const buttonDisabled = useMemo(() => {
  104. if (isAppsFull)
  105. return true
  106. if (currentTab === CreateFromDSLModalTab.FROM_FILE)
  107. return !currentFile
  108. if (currentTab === CreateFromDSLModalTab.FROM_URL)
  109. return !dslUrlValue
  110. return false
  111. }, [isAppsFull, currentTab, currentFile, dslUrlValue])
  112. return (
  113. <Modal
  114. className='p-0 w-[520px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl'
  115. isShow={show}
  116. onClose={() => { }}
  117. >
  118. <div className='flex items-center justify-between pt-6 pl-6 pr-5 pb-3 text-text-primary title-2xl-semi-bold'>
  119. {t('app.importFromDSL')}
  120. <div
  121. className='flex items-center w-8 h-8 cursor-pointer'
  122. onClick={() => onClose()}
  123. >
  124. <RiCloseLine className='w-5 h-5 text-text-tertiary' />
  125. </div>
  126. </div>
  127. <div className='flex items-center px-6 h-9 space-x-6 system-md-semibold text-text-tertiary border-b border-divider-subtle'>
  128. {
  129. tabs.map(tab => (
  130. <div
  131. key={tab.key}
  132. className={cn(
  133. 'relative flex items-center h-full cursor-pointer',
  134. currentTab === tab.key && 'text-text-primary',
  135. )}
  136. onClick={() => setCurrentTab(tab.key)}
  137. >
  138. {tab.label}
  139. {
  140. currentTab === tab.key && (
  141. <div className='absolute bottom-0 w-full h-[2px] bg-util-colors-blue-brand-blue-brand-600'></div>
  142. )
  143. }
  144. </div>
  145. ))
  146. }
  147. </div>
  148. <div className='px-6 py-4'>
  149. {
  150. currentTab === CreateFromDSLModalTab.FROM_FILE && (
  151. <Uploader
  152. className='mt-0'
  153. file={currentFile}
  154. updateFile={handleFile}
  155. />
  156. )
  157. }
  158. {
  159. currentTab === CreateFromDSLModalTab.FROM_URL && (
  160. <div>
  161. <div className='mb-1 system-md-semibold leading6'>DSL URL</div>
  162. <input
  163. placeholder={t('app.importFromDSLUrlPlaceholder') || ''}
  164. className='px-2 w-full h-8 border border-components-input-border-active bg-components-input-bg-active rounded-lg outline-none appearance-none placeholder:text-components-input-text-placeholder system-sm-regular'
  165. value={dslUrlValue}
  166. onChange={e => setDslUrlValue(e.target.value)}
  167. />
  168. </div>
  169. )
  170. }
  171. </div>
  172. {isAppsFull && (
  173. <div className='px-6'>
  174. <AppsFull className='mt-0' loc='app-create-dsl' />
  175. </div>
  176. )}
  177. <div className='flex justify-end px-6 py-5'>
  178. <Button className='mr-2' onClick={onClose}>{t('app.newApp.Cancel')}</Button>
  179. <Button disabled={buttonDisabled} variant="primary" onClick={onCreate}>{t('app.newApp.Create')}</Button>
  180. </div>
  181. </Modal>
  182. )
  183. }
  184. export default CreateFromDSLModal