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' ;
2
14
import { DateTimePicker } from '@mantine/dates' ;
3
15
import { useForm , zodResolver } from '@mantine/form' ;
4
16
import { notifications } from '@mantine/notifications' ;
@@ -7,11 +19,12 @@ import React, { useEffect, useState } from 'react';
7
19
import { useNavigate , useParams } from 'react-router-dom' ;
8
20
import { z } from 'zod' ;
9
21
import { AuthGuard } from '@ui/components/AuthGuard' ;
10
- import { getRunEnvironmentConfig } from '@ui/config' ;
11
22
import { useApi } from '@ui/util/api' ;
12
23
import { OrganizationList as orgList } from '@common/orgs' ;
13
24
import { AppRoles } from '@common/roles' ;
14
25
import { 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' ;
15
28
16
29
export function capitalizeFirstLetter ( string : string ) {
17
30
return string . charAt ( 0 ) . toUpperCase ( ) + string . slice ( 1 ) ;
@@ -29,6 +42,8 @@ const baseBodySchema = z.object({
29
42
host : z . string ( ) . min ( 1 , 'Host is required' ) ,
30
43
featured : z . boolean ( ) . default ( false ) ,
31
44
paidEventId : z . string ( ) . min ( 1 , 'Paid Event ID must be at least 1 character' ) . optional ( ) ,
45
+ // Add metadata field
46
+ metadata : metadataSchema ,
32
47
} ) ;
33
48
34
49
const requestBodySchema = baseBodySchema
@@ -68,6 +83,7 @@ export const ManageEventPage: React.FC = () => {
68
83
try {
69
84
const response = await api . get ( `/api/v1/events/${ eventId } ?ts=${ Date . now ( ) } ` ) ;
70
85
const eventData = response . data ;
86
+
71
87
const formValues = {
72
88
title : eventData . title ,
73
89
description : eventData . description ,
@@ -80,6 +96,7 @@ export const ManageEventPage: React.FC = () => {
80
96
repeats : eventData . repeats ,
81
97
repeatEnds : eventData . repeatEnds ? new Date ( eventData . repeatEnds ) : undefined ,
82
98
paidEventId : eventData . paidEventId ,
99
+ metadata : eventData . metadata || { } ,
83
100
} ;
84
101
form . setValues ( formValues ) ;
85
102
} catch ( error ) {
@@ -107,8 +124,10 @@ export const ManageEventPage: React.FC = () => {
107
124
repeats : undefined ,
108
125
repeatEnds : undefined ,
109
126
paidEventId : undefined ,
127
+ metadata : { } , // Initialize empty metadata object
110
128
} ,
111
129
} ) ;
130
+
112
131
useEffect ( ( ) => {
113
132
if ( form . values . end && form . values . end <= form . values . start ) {
114
133
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 = () => {
124
143
const handleSubmit = async ( values : EventPostRequest ) => {
125
144
try {
126
145
setIsSubmitting ( true ) ;
146
+
127
147
const realValues = {
128
148
...values ,
129
149
start : dayjs ( values . start ) . format ( 'YYYY-MM-DD[T]HH:mm:00' ) ,
@@ -133,6 +153,7 @@ export const ManageEventPage: React.FC = () => {
133
153
? dayjs ( values . repeatEnds ) . format ( 'YYYY-MM-DD[T]HH:mm:00' )
134
154
: undefined ,
135
155
repeats : values . repeats ? values . repeats : undefined ,
156
+ metadata : Object . keys ( values . metadata || { } ) . length > 0 ? values . metadata : undefined ,
136
157
} ;
137
158
138
159
const eventURL = isEditing ? `/api/v1/events/${ eventId } ` : '/api/v1/events' ;
@@ -151,6 +172,87 @@ export const ManageEventPage: React.FC = () => {
151
172
}
152
173
} ;
153
174
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
+
154
256
return (
155
257
< AuthGuard resourceDef = { { service : 'core' , validRoles : [ AppRoles . EVENTS_MANAGER ] } } >
156
258
< Box maw = { 400 } mx = "auto" mt = "xl" >
@@ -230,6 +332,71 @@ export const ManageEventPage: React.FC = () => {
230
332
placeholder = "Enter Ticketing ID or Merch ID prefixed with merch:"
231
333
{ ...form . getInputProps ( 'paidEventId' ) }
232
334
/>
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
+
233
400
< Button type = "submit" mt = "md" >
234
401
{ isSubmitting ? (
235
402
< >
0 commit comments