1- import { Title , Box , TextInput , Textarea , Switch , Select , Button , Loader } from '@mantine/core' ;
1+ import {
2+ Title ,
3+ Box ,
4+ TextInput ,
5+ Textarea ,
6+ Switch ,
7+ Select ,
8+ Button ,
9+ Loader ,
10+ Group ,
11+ ActionIcon ,
12+ Text ,
13+ } from '@mantine/core' ;
214import { DateTimePicker } from '@mantine/dates' ;
315import { useForm , zodResolver } from '@mantine/form' ;
416import { notifications } from '@mantine/notifications' ;
@@ -7,11 +19,12 @@ import React, { useEffect, useState } from 'react';
719import { useNavigate , useParams } from 'react-router-dom' ;
820import { z } from 'zod' ;
921import { AuthGuard } from '@ui/components/AuthGuard' ;
10- import { getRunEnvironmentConfig } from '@ui/config' ;
1122import { useApi } from '@ui/util/api' ;
1223import { OrganizationList as orgList } from '@common/orgs' ;
1324import { AppRoles } from '@common/roles' ;
1425import { EVENT_CACHED_DURATION } from '@common/config' ;
26+ import { IconPlus , IconTrash } from '@tabler/icons-react' ;
27+ import { MAX_METADATA_KEYS , MAX_STRING_LENGTH , metadataSchema } from '@common/types/events' ;
1528
1629export function capitalizeFirstLetter ( string : string ) {
1730 return string . charAt ( 0 ) . toUpperCase ( ) + string . slice ( 1 ) ;
@@ -29,6 +42,8 @@ const baseBodySchema = z.object({
2942 host : z . string ( ) . min ( 1 , 'Host is required' ) ,
3043 featured : z . boolean ( ) . default ( false ) ,
3144 paidEventId : z . string ( ) . min ( 1 , 'Paid Event ID must be at least 1 character' ) . optional ( ) ,
45+ // Add metadata field
46+ metadata : metadataSchema ,
3247} ) ;
3348
3449const requestBodySchema = baseBodySchema
@@ -68,6 +83,7 @@ export const ManageEventPage: React.FC = () => {
6883 try {
6984 const response = await api . get ( `/api/v1/events/${ eventId } ?ts=${ Date . now ( ) } ` ) ;
7085 const eventData = response . data ;
86+
7187 const formValues = {
7288 title : eventData . title ,
7389 description : eventData . description ,
@@ -80,6 +96,7 @@ export const ManageEventPage: React.FC = () => {
8096 repeats : eventData . repeats ,
8197 repeatEnds : eventData . repeatEnds ? new Date ( eventData . repeatEnds ) : undefined ,
8298 paidEventId : eventData . paidEventId ,
99+ metadata : eventData . metadata || { } ,
83100 } ;
84101 form . setValues ( formValues ) ;
85102 } catch ( error ) {
@@ -107,8 +124,10 @@ export const ManageEventPage: React.FC = () => {
107124 repeats : undefined ,
108125 repeatEnds : undefined ,
109126 paidEventId : undefined ,
127+ metadata : { } , // Initialize empty metadata object
110128 } ,
111129 } ) ;
130+
112131 useEffect ( ( ) => {
113132 if ( form . values . end && form . values . end <= form . values . start ) {
114133 form . setFieldValue ( 'end' , new Date ( form . values . start . getTime ( ) + 3.6e6 ) ) ; // 1 hour after the start date
@@ -124,6 +143,7 @@ export const ManageEventPage: React.FC = () => {
124143 const handleSubmit = async ( values : EventPostRequest ) => {
125144 try {
126145 setIsSubmitting ( true ) ;
146+
127147 const realValues = {
128148 ...values ,
129149 start : dayjs ( values . start ) . format ( 'YYYY-MM-DD[T]HH:mm:00' ) ,
@@ -133,6 +153,7 @@ export const ManageEventPage: React.FC = () => {
133153 ? dayjs ( values . repeatEnds ) . format ( 'YYYY-MM-DD[T]HH:mm:00' )
134154 : undefined ,
135155 repeats : values . repeats ? values . repeats : undefined ,
156+ metadata : Object . keys ( values . metadata || { } ) . length > 0 ? values . metadata : undefined ,
136157 } ;
137158
138159 const eventURL = isEditing ? `/api/v1/events/${ eventId } ` : '/api/v1/events' ;
@@ -151,6 +172,87 @@ export const ManageEventPage: React.FC = () => {
151172 }
152173 } ;
153174
175+ // Function to add a new metadata field
176+ const addMetadataField = ( ) => {
177+ const currentMetadata = { ...form . values . metadata } ;
178+ if ( Object . keys ( currentMetadata ) . length >= MAX_METADATA_KEYS ) {
179+ notifications . show ( {
180+ message : `You can add at most ${ MAX_METADATA_KEYS } metadata keys.` ,
181+ } ) ;
182+ return ;
183+ }
184+
185+ // Generate a temporary key name that doesn't exist yet
186+ let tempKey = `key${ Object . keys ( currentMetadata ) . length + 1 } ` ;
187+ // Make sure it's unique
188+ while ( currentMetadata [ tempKey ] !== undefined ) {
189+ tempKey = `key${ parseInt ( tempKey . replace ( 'key' , '' ) ) + 1 } ` ;
190+ }
191+
192+ // Update the form
193+ form . setValues ( {
194+ ...form . values ,
195+ metadata : {
196+ ...currentMetadata ,
197+ [ tempKey ] : '' ,
198+ } ,
199+ } ) ;
200+ } ;
201+
202+ // Function to update a metadata value
203+ const updateMetadataValue = ( key : string , value : string ) => {
204+ form . setValues ( {
205+ ...form . values ,
206+ metadata : {
207+ ...form . values . metadata ,
208+ [ key ] : value ,
209+ } ,
210+ } ) ;
211+ } ;
212+
213+ const updateMetadataKey = ( oldKey : string , newKey : string ) => {
214+ const metadata = { ...form . values . metadata } ;
215+ if ( oldKey === newKey ) return ;
216+
217+ const value = metadata [ oldKey ] ;
218+ delete metadata [ oldKey ] ;
219+ metadata [ newKey ] = value ;
220+
221+ form . setValues ( {
222+ ...form . values ,
223+ metadata,
224+ } ) ;
225+ } ;
226+
227+ // Function to remove a metadata field
228+ const removeMetadataField = ( key : string ) => {
229+ const currentMetadata = { ...form . values . metadata } ;
230+ delete currentMetadata [ key ] ;
231+
232+ form . setValues ( {
233+ ...form . values ,
234+ metadata : currentMetadata ,
235+ } ) ;
236+ } ;
237+
238+ const [ metadataKeys , setMetadataKeys ] = useState < Record < string , string > > ( { } ) ;
239+
240+ // Initialize metadata keys with unique IDs when form loads or changes
241+ useEffect ( ( ) => {
242+ const newMetadataKeys : Record < string , string > = { } ;
243+
244+ // For existing metadata, create stable IDs
245+ Object . keys ( form . values . metadata || { } ) . forEach ( ( key ) => {
246+ if ( ! metadataKeys [ key ] ) {
247+ newMetadataKeys [ key ] = `meta-${ Math . random ( ) . toString ( 36 ) . substring ( 2 , 9 ) } ` ;
248+ } else {
249+ newMetadataKeys [ key ] = metadataKeys [ key ] ;
250+ }
251+ } ) ;
252+
253+ setMetadataKeys ( newMetadataKeys ) ;
254+ } , [ Object . keys ( form . values . metadata || { } ) . length ] ) ;
255+
154256 return (
155257 < AuthGuard resourceDef = { { service : 'core' , validRoles : [ AppRoles . EVENTS_MANAGER ] } } >
156258 < Box maw = { 400 } mx = "auto" mt = "xl" >
@@ -230,6 +332,71 @@ export const ManageEventPage: React.FC = () => {
230332 placeholder = "Enter Ticketing ID or Merch ID prefixed with merch:"
231333 { ...form . getInputProps ( 'paidEventId' ) }
232334 />
335+
336+ { /* Metadata Section */ }
337+ < Box my = "md" >
338+ < Title order = { 5 } > Metadata</ Title >
339+ < Group justify = "space-between" mb = "xs" >
340+ < Button
341+ size = "xs"
342+ variant = "outline"
343+ leftSection = { < IconPlus size = { 16 } /> }
344+ onClick = { addMetadataField }
345+ disabled = { Object . keys ( form . values . metadata || { } ) . length >= MAX_METADATA_KEYS }
346+ >
347+ Add Field
348+ </ Button >
349+ </ Group >
350+ < Text size = "xs" c = "dimmed" >
351+ These values can be acceessed via the API. Max { MAX_STRING_LENGTH } characters for keys
352+ and values.
353+ </ Text >
354+
355+ { Object . entries ( form . values . metadata || { } ) . map ( ( [ key , value ] , index ) => {
356+ const keyError = key . trim ( ) === '' ? 'Key is required' : undefined ;
357+ const valueError = value . trim ( ) === '' ? 'Value is required' : undefined ;
358+
359+ return (
360+ < Group key = { index } align = "start" gap = { 'sm' } >
361+ < TextInput
362+ label = "Key"
363+ value = { key }
364+ onChange = { ( e ) => updateMetadataKey ( key , e . currentTarget . value ) }
365+ error = { keyError }
366+ style = { { flex : 1 } }
367+ />
368+ < Box style = { { flex : 1 } } >
369+ < TextInput
370+ label = "Value"
371+ value = { value }
372+ onChange = { ( e ) => updateMetadataValue ( key , e . currentTarget . value ) }
373+ error = { valueError }
374+ />
375+ { /* Empty space to maintain consistent height */ }
376+ { valueError && < div style = { { height : '0.75rem' } } /> }
377+ </ Box >
378+ < ActionIcon
379+ color = "red"
380+ variant = "light"
381+ onClick = { ( ) => removeMetadataField ( key ) }
382+ mt = { 30 } // align with inputs when label is present
383+ >
384+ < IconTrash size = { 16 } />
385+ </ ActionIcon >
386+ </ Group >
387+ ) ;
388+ } ) }
389+
390+ { Object . keys ( form . values . metadata || { } ) . length > 0 && (
391+ < Box mt = "xs" size = "xs" ta = "right" >
392+ < small >
393+ { Object . keys ( form . values . metadata || { } ) . length } of { MAX_METADATA_KEYS } fields
394+ used
395+ </ small >
396+ </ Box >
397+ ) }
398+ </ Box >
399+
233400 < Button type = "submit" mt = "md" >
234401 { isSubmitting ? (
235402 < >
0 commit comments