@@ -30,6 +30,7 @@ import { useKeyboardContext } from '@/contexts/KeyboardContext';
3030import { useHotkeys } from '@/hooks/useKeyboardNavigation' ;
3131import { Lock , Users , Globe , Mail , Trash2 , Copy , Check , Link2 , X , Save , Plus , Bell , ChevronDown } from 'lucide-react' ;
3232import { cn } from '@/lib/utils' ;
33+ import { createVaultPublicSlugCandidate , normalizeVaultPublicSlug } from '@/lib/vaultSlug' ;
3334
3435type VaultVisibility = 'private' | 'protected' | 'public' ;
3536
@@ -78,7 +79,7 @@ interface VaultDialogProps {
7879 onOpenChange : ( open : boolean ) => void ;
7980 vault ?: Vault | null ;
8081 initialRequestId ?: string ;
81- onSave : ( data : Partial < Vault > ) => Promise < Vault | void > ;
82+ onSave ? : ( data : Partial < Vault > ) => Promise < Vault | void > ;
8283 onUpdate ?: ( ) => void ;
8384 onDelete ?: ( vault : Vault ) => void ;
8485}
@@ -127,7 +128,7 @@ export function VaultDialog({ open, onOpenChange, vault, initialRequestId, onSav
127128 category,
128129 abstract,
129130 visibility : isForkedVault ? 'public' : visibility ,
130- publicSlug : ( isForkedVault || visibility === 'public' ) ? ( publicSlug || generateSlug ( name ) ) : '' ,
131+ publicSlug : ( isForkedVault || visibility === 'public' ) ? ( publicSlug || createVaultPublicSlugCandidate ( name ) ) : '' ,
131132 } ;
132133 setHasUnsavedChanges ( false ) ;
133134 } , [ name , description , color , category , abstract , visibility , publicSlug , isForkedVault ] ) ;
@@ -236,13 +237,46 @@ export function VaultDialog({ open, onOpenChange, vault, initialRequestId, onSav
236237 return v . visibility || 'private' ;
237238 } ;
238239
239- const generateSlug = ( name : string ) => {
240- return name
241- . toLowerCase ( )
242- . replace ( / [ ^ a - z 0 - 9 ] + / g, '-' )
243- . replace ( / ^ - | - $ / g, '' )
244- . slice ( 0 , 50 ) ;
245- } ;
240+ const ensureUniquePublicSlug = useCallback ( async ( desiredSlug : string ) => {
241+ const baseSlug = normalizeVaultPublicSlug ( desiredSlug ) || createVaultPublicSlugCandidate ( name ) ;
242+
243+ for ( let attempt = 0 ; attempt < 8 ; attempt += 1 ) {
244+ const suffix = attempt === 0 ? '' : `-${ attempt + 1 } ` ;
245+ const candidate = `${ baseSlug . slice ( 0 , 50 - suffix . length ) } ${ suffix } ` ;
246+
247+ const { data, error } = await supabase
248+ . from ( 'vaults' )
249+ . select ( 'id' )
250+ . eq ( 'public_slug' , candidate )
251+ . maybeSingle ( ) ;
252+
253+ if ( error ) throw error ;
254+ if ( ! data || data . id === vault ?. id ) return candidate ;
255+ }
256+
257+ return `${ baseSlug . slice ( 0 , 41 ) } -${ Date . now ( ) . toString ( 36 ) } ` ;
258+ } , [ name , vault ?. id ] ) ;
259+
260+ const buildSavePayload = useCallback ( async ( ) : Promise < Partial < Vault > > => {
261+ const shouldHavePublicSlug = isForkedVault || visibility === 'public' ;
262+ const resolvedPublicSlug = shouldHavePublicSlug
263+ ? await ensureUniquePublicSlug ( publicSlug || name )
264+ : null ;
265+
266+ if ( shouldHavePublicSlug && resolvedPublicSlug !== publicSlug ) {
267+ setPublicSlug ( resolvedPublicSlug ) ;
268+ }
269+
270+ return {
271+ name,
272+ description : description || null ,
273+ color,
274+ category : category || null ,
275+ abstract : abstract || null ,
276+ visibility : isForkedVault ? 'public' : visibility ,
277+ public_slug : resolvedPublicSlug ,
278+ } ;
279+ } , [ abstract , category , color , description , ensureUniquePublicSlug , isForkedVault , name , publicSlug , visibility ] ) ;
246280
247281 const fetchShares = useCallback ( async ( vaultId : string ) => {
248282 const { data, error } = await supabase
@@ -398,7 +432,7 @@ export function VaultDialog({ open, onOpenChange, vault, initialRequestId, onSav
398432 const initialCategory = vault . category || '' ;
399433 const initialAbstract = vault . abstract || '' ;
400434 const initialVisibility = getVisibility ( vault ) ;
401- const initialPublicSlug = vault . public_slug || generateSlug ( vault . name ) ;
435+ const initialPublicSlug = vault . public_slug || createVaultPublicSlugCandidate ( vault . name ) ;
402436
403437 setName ( initialName ) ;
404438 setDescription ( initialDescription ) ;
@@ -568,19 +602,11 @@ export function VaultDialog({ open, onOpenChange, vault, initialRequestId, onSav
568602
569603 // Handle save and close
570604 const handleSaveAndClose = useCallback ( async ( ) => {
571- if ( ! name . trim ( ) ) return ;
605+ if ( ! name . trim ( ) || ! onSave ) return ;
572606
573607 setSaving ( true ) ;
574608 try {
575- await onSave ( {
576- name,
577- description,
578- color,
579- category : category || null ,
580- abstract : abstract || null ,
581- visibility : isForkedVault ? 'public' : visibility ,
582- public_slug : ( isForkedVault || visibility === 'public' ) ? ( publicSlug || generateSlug ( name ) ) : null ,
583- } ) ;
609+ await onSave ( await buildSavePayload ( ) ) ;
584610 syncSavedValues ( ) ;
585611 setShowUnsavedDialog ( false ) ;
586612 onOpenChange ( false ) ;
@@ -589,22 +615,14 @@ export function VaultDialog({ open, onOpenChange, vault, initialRequestId, onSav
589615 } finally {
590616 setSaving ( false ) ;
591617 }
592- } , [ name , description , color , category , abstract , visibility , publicSlug , isForkedVault , onSave , onOpenChange , syncSavedValues ] ) ;
618+ } , [ buildSavePayload , name , onSave , onOpenChange , syncSavedValues ] ) ;
593619
594620 const handleSubmit = useCallback ( async ( ) => {
595- if ( ! open || saving || ! name . trim ( ) ) return ;
621+ if ( ! open || saving || ! name . trim ( ) || ! onSave ) return ;
596622
597623 setSaving ( true ) ;
598624 try {
599- await onSave ( {
600- name,
601- description,
602- color,
603- category : category || null ,
604- abstract : abstract || null ,
605- visibility : isForkedVault ? 'public' : visibility ,
606- public_slug : ( isForkedVault || visibility === 'public' ) ? ( publicSlug || generateSlug ( name ) ) : null ,
607- } ) ;
625+ await onSave ( await buildSavePayload ( ) ) ;
608626 syncSavedValues ( ) ;
609627
610628 if ( ! vault ) {
@@ -613,7 +631,7 @@ export function VaultDialog({ open, onOpenChange, vault, initialRequestId, onSav
613631 } finally {
614632 setSaving ( false ) ;
615633 }
616- } , [ open , saving , name , description , color , category , abstract , visibility , publicSlug , isForkedVault , onSave , syncSavedValues , vault , onOpenChange ] ) ;
634+ } , [ buildSavePayload , open , saving , name , onSave , syncSavedValues , vault , onOpenChange ] ) ;
617635
618636 useHotkeys (
619637 'dialog' ,
@@ -907,7 +925,7 @@ export function VaultDialog({ open, onOpenChange, vault, initialRequestId, onSav
907925 onChange = { ( e ) => setAbstract ( e . target . value ) }
908926 placeholder = "describe_contents_and_purpose..."
909927 rows = { 3 }
910- className = "text-sm"
928+ className = "font-mono text-sm"
911929 />
912930 < p className = "text-xs text-muted-foreground font-mono" >
913931 // shown_on_the_codex_when_published
@@ -975,7 +993,7 @@ export function VaultDialog({ open, onOpenChange, vault, initialRequestId, onSav
975993 < Input
976994 value = { publicSlug }
977995 onChange = { ( e ) => {
978- setPublicSlug ( e . target . value . toLowerCase ( ) . replace ( / [ ^ a - z 0 - 9 - ] / g , '' ) ) ;
996+ setPublicSlug ( normalizeVaultPublicSlug ( e . target . value ) ) ;
979997 setSlugAvailable ( null ) ; // Reset while typing
980998 } }
981999 placeholder = "my-research-vault"
@@ -1273,7 +1291,7 @@ export function VaultDialog({ open, onOpenChange, vault, initialRequestId, onSav
12731291 < Button
12741292 type = "submit"
12751293 variant = "glow"
1276- disabled = { saving || ! name . trim ( ) }
1294+ disabled = { saving || ! name . trim ( ) || ! onSave }
12771295 className = "font-mono w-full sm:w-auto text-xs sm:text-sm h-9 sm:h-10"
12781296 >
12791297 { saving ? (
0 commit comments