|
| 1 | +<script lang="ts"> |
| 2 | + // Promote a bio-page link to an affiliation — AddPersonInline's gate |
| 3 | + // pattern INVERTED: the person is fixed (the link row's owner), the ORG is |
| 4 | + // being resolved. Seeded from the link's hostname, candidates come from |
| 5 | + // resolver.search (whose D4 clause matches domains[*].domain), and the gate |
| 6 | + // is ALWAYS shown: pick an existing org or explicitly create a thin one |
| 7 | + // (name + the bio's domain, so it stays domain-matchable). The edge + its |
| 8 | + // affiliated_with observation come from person.affiliate with the bio URL |
| 9 | + // as source — the link row keeps its identity role; this adds the two |
| 10 | + // facts it was silently dropping. |
| 11 | + // Per context-v/issues/Person-Bio-Pages-Are-Affiliation-Signals-Not-Just-Identity-Links.md. |
| 12 | +
|
| 13 | + import { searchOrgs, affiliatePerson } from './lib/org-client'; |
| 14 | + import type { OrgSuggestion, ShapedLink } from './lib/types'; |
| 15 | +
|
| 16 | + let { |
| 17 | + person_uuid, |
| 18 | + personName, |
| 19 | + entry, |
| 20 | + client, |
| 21 | + onadded, |
| 22 | + oncancel, |
| 23 | + }: { |
| 24 | + person_uuid: string; |
| 25 | + personName: string; |
| 26 | + entry: ShapedLink; |
| 27 | + client: string; |
| 28 | + onadded: () => void; |
| 29 | + oncancel: () => void; |
| 30 | + } = $props(); |
| 31 | +
|
| 32 | + function hostOf(u: string): string { |
| 33 | + try { |
| 34 | + return new URL(u).hostname.replace(/^www\./, ''); |
| 35 | + } catch { |
| 36 | + return ''; |
| 37 | + } |
| 38 | + } |
| 39 | +
|
| 40 | + const domain = $derived(hostOf(entry.url)); |
| 41 | +
|
| 42 | + let orgName = $state(''); |
| 43 | + let role = $state(''); |
| 44 | + let phase = $state<'gate' | 'writing'>('gate'); |
| 45 | + let candidates = $state<OrgSuggestion[]>([]); |
| 46 | + let searched = $state(false); |
| 47 | + let error = $state<string | null>(null); |
| 48 | +
|
| 49 | + // Candidates load from the bio's domain on mount (D4 makes this nearly |
| 50 | + // free); typing a name and re-finding re-queries by name instead. |
| 51 | + $effect(() => { |
| 52 | + if (!searched && domain) void find(domain); |
| 53 | + }); |
| 54 | +
|
| 55 | + async function find(q: string) { |
| 56 | + error = null; |
| 57 | + try { |
| 58 | + candidates = await searchOrgs(q, client); |
| 59 | + } catch (err) { |
| 60 | + error = err instanceof Error ? err.message : String(err); |
| 61 | + } finally { |
| 62 | + searched = true; |
| 63 | + } |
| 64 | + } |
| 65 | +
|
| 66 | + async function resolve(action: 'match' | 'create', org_slug?: string) { |
| 67 | + if (action === 'create' && !orgName.trim()) return; |
| 68 | + phase = 'writing'; |
| 69 | + error = null; |
| 70 | + try { |
| 71 | + await affiliatePerson({ |
| 72 | + person_uuid, |
| 73 | + org_action: action, |
| 74 | + org_slug, |
| 75 | + org_name: action === 'create' ? orgName.trim() : undefined, |
| 76 | + org_domain: action === 'create' ? domain : undefined, |
| 77 | + role: role.trim() || null, |
| 78 | + client, |
| 79 | + source: entry.url, |
| 80 | + }); |
| 81 | + onadded(); // parent bumps + dispatches augment-it:entity-updated |
| 82 | + } catch (err) { |
| 83 | + error = err instanceof Error ? err.message : String(err); |
| 84 | + phase = 'gate'; |
| 85 | + } |
| 86 | + } |
| 87 | +</script> |
| 88 | + |
| 89 | +<div class="ow-addperson"> |
| 90 | + <p class="ow-gate-note"> |
| 91 | + Promote <strong>{domain || entry.url}</strong> to an affiliation for {personName} — pick the |
| 92 | + org this bio lives on, or create it: |
| 93 | + </p> |
| 94 | + |
| 95 | + {#if phase === 'gate'} |
| 96 | + {#if candidates.length > 0} |
| 97 | + <ul class="ow-gate-list"> |
| 98 | + {#each candidates as c (c.slug)} |
| 99 | + <li> |
| 100 | + <button type="button" class="ow-gate-pick" onclick={() => resolve('match', c.slug)}> |
| 101 | + <strong>{c.complete_name ?? c.conventional_name ?? c.slug}</strong> |
| 102 | + <span class="ow-gate-headline">{c.slug}</span> |
| 103 | + </button> |
| 104 | + </li> |
| 105 | + {/each} |
| 106 | + </ul> |
| 107 | + {:else if searched} |
| 108 | + <p class="ow-gate-note">No existing org matches “{domain}”.</p> |
| 109 | + {:else} |
| 110 | + <p class="ow-gate-note">looking for orgs matching “{domain}”…</p> |
| 111 | + {/if} |
| 112 | + |
| 113 | + <form class="ow-addperson-form" onsubmit={(e) => { e.preventDefault(); void resolve('create'); }}> |
| 114 | + <input |
| 115 | + class="ow-add-url" |
| 116 | + type="text" |
| 117 | + placeholder="Org name (required to create)" |
| 118 | + bind:value={orgName} |
| 119 | + /> |
| 120 | + <input |
| 121 | + class="ow-add-kind" |
| 122 | + type="text" |
| 123 | + placeholder="Role (optional)" |
| 124 | + bind:value={role} |
| 125 | + /> |
| 126 | + <span class="ow-addperson-actions"> |
| 127 | + <button type="button" class="ow-add-go" onclick={() => find(orgName.trim() || domain)}> |
| 128 | + Find matches |
| 129 | + </button> |
| 130 | + <button type="submit" class="ow-add-go" disabled={!orgName.trim()}> |
| 131 | + Create + affiliate |
| 132 | + </button> |
| 133 | + <button type="button" class="ow-add-go" onclick={oncancel}>Cancel</button> |
| 134 | + </span> |
| 135 | + </form> |
| 136 | + {:else} |
| 137 | + <p class="ow-gate-note">writing affiliation…</p> |
| 138 | + {/if} |
| 139 | + {#if error}<div class="ow-error">{error}</div>{/if} |
| 140 | +</div> |
0 commit comments