feat: add UTM parameter generator to LinkEditForm#4270
Conversation
|
@KAUSHALCODER123 is attempting to deploy a commit to the Umami Software Team on Vercel. A member of the Team first needs to authorize it. |
|
accept my pr |
Greptile SummaryThis PR adds a collapsible UTM parameter generator to the
Confidence Score: 3/5The UTM section's auto-expand feature is broken for existing links and the UTM fields can silently overwrite manual URL edits; these should be fixed before merging. The src/app/(main)/links/LinkEditForm.tsx — specifically the Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant Form
participant UTMField
participant URLField
participant API
User->>Form: Open edit form (existing link)
Form->>API: useLinkQuery(linkId)
API-->>Form: data (url with utm params)
Note over Form: useState initializer already ran with data=undefined - showUtm=false (BUG)
User->>Form: Toggle UTM section (button)
Form->>Form: setShowUtm(true)
User->>UTMField: Type value
UTMField->>URLField: setValue url with utm param
Note over URLField: URL field updated
User->>URLField: Manually edit URL
URLField-->>UTMField: No update - stale UTM fields (BUG)
User->>Form: Submit
Form->>Form: Strip utmSource/Medium/etc from payload
Form->>API: mutateAsync with updated url
API-->>Form: success
Reviews (1): Last reviewed commit: "feat: add UTM parameter generator to Lin..." | Re-trigger Greptile |
| const [showUtm, setShowUtm] = useState(() => { | ||
| const utm = parseUtm(data?.url); | ||
| return !!(utm.utmSource || utm.utmMedium || utm.utmCampaign || utm.utmContent || utm.utmTerm); | ||
| }); |
There was a problem hiding this comment.
showUtm initializer always sees undefined data
useState's initializer runs once on mount, but at that point useLinkQuery hasn't resolved yet — data is undefined. So parseUtm(undefined) returns {}, and showUtm is always initialized to false. When the query does resolve and the <Loading> early-return lifts, useState does not re-run its initializer. As a result, the UTM section never auto-expands for existing links that already contain UTM params in their URL, breaking the "automatic parsing from existing URLs" behavior described in the PR.
| const [showUtm, setShowUtm] = useState(() => { | |
| const utm = parseUtm(data?.url); | |
| return !!(utm.utmSource || utm.utmMedium || utm.utmCampaign || utm.utmContent || utm.utmTerm); | |
| }); | |
| const [showUtm, setShowUtm] = useState(false); | |
| useEffect(() => { | |
| if (data?.url) { | |
| const utm = parseUtm(data.url); | |
| if (utm.utmSource || utm.utmMedium || utm.utmCampaign || utm.utmContent || utm.utmTerm) { | |
| setShowUtm(true); | |
| } | |
| } | |
| }, [data]); |
| <FormField label={t(labels.utmSource)} name="utmSource"> | ||
| <TextField | ||
| autoComplete="off" | ||
| onChange={val => { | ||
| const url = watch('url'); | ||
| try { | ||
| const u = new URL(url); | ||
| if (val) u.searchParams.set('utm_source', val); | ||
| else u.searchParams.delete('utm_source'); | ||
| setValue('url', u.toString(), { shouldDirty: true }); | ||
| } catch { | ||
| // ignore | ||
| } | ||
| }} | ||
| /> | ||
| </FormField> | ||
| <FormField label={t(labels.utmMedium)} name="utmMedium"> | ||
| <TextField | ||
| autoComplete="off" | ||
| onChange={val => { | ||
| const url = watch('url'); | ||
| try { | ||
| const u = new URL(url); | ||
| if (val) u.searchParams.set('utm_medium', val); | ||
| else u.searchParams.delete('utm_medium'); | ||
| setValue('url', u.toString(), { shouldDirty: true }); | ||
| } catch { | ||
| // ignore | ||
| } | ||
| }} | ||
| /> | ||
| </FormField> | ||
| <FormField label={t(labels.utmCampaign)} name="utmCampaign"> | ||
| <TextField | ||
| autoComplete="off" | ||
| onChange={val => { | ||
| const url = watch('url'); | ||
| try { | ||
| const u = new URL(url); | ||
| if (val) u.searchParams.set('utm_campaign', val); | ||
| else u.searchParams.delete('utm_campaign'); | ||
| setValue('url', u.toString(), { shouldDirty: true }); | ||
| } catch { | ||
| // ignore | ||
| } | ||
| }} | ||
| /> | ||
| </FormField> | ||
| <FormField label={t(labels.utmContent)} name="utmContent"> | ||
| <TextField | ||
| autoComplete="off" | ||
| onChange={val => { | ||
| const url = watch('url'); | ||
| try { | ||
| const u = new URL(url); | ||
| if (val) u.searchParams.set('utm_content', val); | ||
| else u.searchParams.delete('utm_content'); | ||
| setValue('url', u.toString(), { shouldDirty: true }); | ||
| } catch { | ||
| // ignore | ||
| } | ||
| }} | ||
| /> | ||
| </FormField> | ||
| <FormField label={t(labels.utmTerm)} name="utmTerm" colSpan="2"> | ||
| <TextField | ||
| autoComplete="off" | ||
| onChange={val => { | ||
| const url = watch('url'); | ||
| try { | ||
| const u = new URL(url); | ||
| if (val) u.searchParams.set('utm_term', val); | ||
| else u.searchParams.delete('utm_term'); | ||
| setValue('url', u.toString(), { shouldDirty: true }); | ||
| } catch { | ||
| // ignore | ||
| } | ||
| }} | ||
| /> | ||
| </FormField> |
There was a problem hiding this comment.
No reverse sync from URL field to UTM fields
Every UTM TextField pushes its value into the url field (UTM → URL), but the reverse path is absent. If a user manually edits the url field and changes or adds a UTM query param directly, the individual UTM fields (utmSource, utmMedium, etc.) remain stale and will silently overwrite whatever the user typed when they next touch one of those fields.
Changes:
Added a togglable UTM generator section to the Link Add/Edit form.
Automatic parsing of UTM parameters from existing URLs.
Real-time synchronization between UTM fields and the Destination URL.
Automatic filtering of helper fields before form submission