33import { useState } from "react" ;
44import Link from "next/link" ;
55import { useRouter } from "next/navigation" ;
6- import { Alert , Button , Fieldset , Group , Stack , Text , TextInput } from "@mantine/core" ;
7- import { IconInfoCircle } from "@tabler/icons-react" ;
8- import type { z } from "zod/v4" ;
6+ import { Alert , Anchor , Button , ButtonGroup , Fieldset , Group , Stack , Text , TextInput } from "@mantine/core" ;
7+ import { IconInfoCircle , IconPencil , IconPlus , IconUnlink } from "@tabler/icons-react" ;
8+ import { z } from "zod/v4" ;
99
1010import type { RouterOutputs } from "@homarr/api" ;
1111import { clientApi } from "@homarr/api/client" ;
12+ import { useSession } from "@homarr/auth/client" ;
1213import { revalidatePathActionAsync } from "@homarr/common/client" ;
1314import { getAllSecretKindOptions , getDefaultSecretKinds } from "@homarr/definitions" ;
1415import { useZodForm } from "@homarr/form" ;
15- import { useConfirmModal } from "@homarr/modals" ;
16+ import { useConfirmModal , useModalAction } from "@homarr/modals" ;
17+ import { AppSelectModal } from "@homarr/modals-collection" ;
1618import { showErrorNotification , showSuccessNotification } from "@homarr/notifications" ;
1719import { useI18n } from "@homarr/translation/client" ;
1820import { integrationUpdateSchema } from "@homarr/validation/integration" ;
@@ -27,6 +29,19 @@ interface EditIntegrationForm {
2729 integration : RouterOutputs [ "integration" ] [ "byId" ] ;
2830}
2931
32+ const formSchema = integrationUpdateSchema . omit ( { id : true , appId : true } ) . and (
33+ z . object ( {
34+ app : z
35+ . object ( {
36+ id : z . string ( ) ,
37+ name : z . string ( ) ,
38+ iconUrl : z . string ( ) ,
39+ href : z . string ( ) . nullable ( ) ,
40+ } )
41+ . nullable ( ) ,
42+ } ) ,
43+ ) ;
44+
3045export const EditIntegrationForm = ( { integration } : EditIntegrationForm ) => {
3146 const t = useI18n ( ) ;
3247 const { openConfirmModal } = useConfirmModal ( ) ;
@@ -40,22 +55,23 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
4055 const hasUrlSecret = initialSecretsKinds . includes ( "url" ) ;
4156
4257 const router = useRouter ( ) ;
43- const form = useZodForm ( integrationUpdateSchema . omit ( { id : true } ) , {
58+ const form = useZodForm ( formSchema , {
4459 initialValues : {
4560 name : integration . name ,
4661 url : integration . url ,
4762 secrets : initialSecretsKinds . map ( ( kind ) => ( {
4863 kind,
4964 value : integration . secrets . find ( ( secret ) => secret . kind === kind ) ?. value ?? "" ,
5065 } ) ) ,
66+ app : integration . app ?? null ,
5167 } ,
5268 } ) ;
5369 const { mutateAsync, isPending } = clientApi . integration . update . useMutation ( ) ;
5470 const [ error , setError ] = useState < null | AnyMappedTestConnectionError > ( null ) ;
5571
5672 const secretsMap = new Map ( integration . secrets . map ( ( secret ) => [ secret . kind , secret ] ) ) ;
5773
58- const handleSubmitAsync = async ( values : FormType ) => {
74+ const handleSubmitAsync = async ( { app , ... values } : FormType ) => {
5975 const url = hasUrlSecret
6076 ? new URL ( values . secrets . find ( ( secret ) => secret . kind === "url" ) ?. value ?? values . url ) . origin
6177 : values . url ;
@@ -68,6 +84,7 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
6884 kind : secret . kind ,
6985 value : secret . value === "" ? null : secret . value ,
7086 } ) ) ,
87+ appId : app ?. id ?? null ,
7188 } ,
7289 {
7390 onSuccess : ( data ) => {
@@ -102,7 +119,7 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
102119 form . values . secrets . length === initialSecretsKinds . length ;
103120
104121 return (
105- < form onSubmit = { form . onSubmit ( ( values ) => void handleSubmitAsync ( values ) ) } >
122+ < form onSubmit = { form . onSubmit ( async ( values ) => await handleSubmitAsync ( values ) ) } >
106123 < Stack >
107124 < TextInput withAsterisk label = { t ( "integration.field.name.label" ) } { ...form . getInputProps ( "name" ) } />
108125
@@ -169,6 +186,8 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
169186 </ Stack >
170187 </ Fieldset >
171188
189+ < IntegrationLinkApp value = { form . values . app } onChange = { ( app ) => form . setFieldValue ( "app" , app ) } />
190+
172191 { error !== null && < IntegrationTestConnectionError error = { error } url = { form . values . url } /> }
173192
174193 < Group justify = "end" align = "center" >
@@ -184,4 +203,80 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
184203 ) ;
185204} ;
186205
187- type FormType = Omit < z . infer < typeof integrationUpdateSchema > , "id" > ;
206+ type FormType = z . infer < typeof formSchema > ;
207+
208+ interface IntegrationAppSelectProps {
209+ value : FormType [ "app" ] ;
210+ onChange : ( app : FormType [ "app" ] ) => void ;
211+ }
212+
213+ const IntegrationLinkApp = ( { value, onChange } : IntegrationAppSelectProps ) => {
214+ const { openModal } = useModalAction ( AppSelectModal ) ;
215+ const t = useI18n ( ) ;
216+ const { data : session } = useSession ( ) ;
217+ const canCreateApps = session ?. user . permissions . includes ( "app-create" ) ?? false ;
218+
219+ const handleChange = ( ) =>
220+ openModal (
221+ {
222+ onSelect : onChange ,
223+ withCreate : canCreateApps ,
224+ } ,
225+ {
226+ title : t ( "integration.page.edit.app.action.select" ) ,
227+ } ,
228+ ) ;
229+
230+ if ( ! value ) {
231+ return (
232+ < Button
233+ variant = "subtle"
234+ color = "gray"
235+ leftSection = { < IconPlus size = { 16 } stroke = { 1.5 } /> }
236+ fullWidth
237+ onClick = { handleChange }
238+ >
239+ { t ( "integration.page.edit.app.action.add" ) }
240+ </ Button >
241+ ) ;
242+ }
243+
244+ return (
245+ < Fieldset legend = { t ( "integration.field.app.sectionTitle" ) } >
246+ < Group justify = "space-between" >
247+ < Group gap = "sm" >
248+ { /* eslint-disable-next-line @next/next/no-img-element */ }
249+ < img src = { value . iconUrl } alt = { value . name } width = { 32 } height = { 32 } />
250+ < Stack gap = { 0 } >
251+ < Text size = "sm" fw = "bold" >
252+ { value . name }
253+ </ Text >
254+ { value . href !== null && (
255+ < Anchor href = { value . href } target = "_blank" rel = "noopener noreferrer" size = "sm" >
256+ { value . href }
257+ </ Anchor >
258+ ) }
259+ </ Stack >
260+ </ Group >
261+ < ButtonGroup >
262+ < Button
263+ variant = "subtle"
264+ color = "gray"
265+ leftSection = { < IconUnlink size = { 16 } stroke = { 1.5 } /> }
266+ onClick = { ( ) => onChange ( null ) }
267+ >
268+ { t ( "integration.page.edit.app.action.remove" ) }
269+ </ Button >
270+ < Button
271+ variant = "subtle"
272+ color = "gray"
273+ leftSection = { < IconPencil size = { 16 } stroke = { 1.5 } /> }
274+ onClick = { handleChange }
275+ >
276+ { t ( "common.action.change" ) }
277+ </ Button >
278+ </ ButtonGroup >
279+ </ Group >
280+ </ Fieldset >
281+ ) ;
282+ } ;
0 commit comments