@bongos/core 1.21.56 → 1.21.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +106 -46
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +6 -0
- package/clients/bongos-client/index.cjs +6 -0
- package/clients/bongos-client/index.d.ts +13 -2
- package/clients/bongos-client/index.mjs +6 -0
- package/docs/adr/0336-a-guilds-record-is-the-sum-of-what-its-members-already-show.md +2 -0
- package/docs/adr/0365-a-guild-engages-a-project-through-each-members-own-door.md +58 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +233 -6
- package/docs/api-reference.md +8 -5
- package/docs/copy-inventory.md +361 -315
- package/docs/copy-registry.json +868 -419
- package/docs/file-map.md +4 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +38 -4
- package/docs/page-readings.json +856 -771
- package/modules/platform-identity/application-echo.js +11 -5
- package/modules/platform-identity/guild-engagements.js +255 -0
- package/modules/platform-identity/guilds.js +3 -1
- package/modules/platform-identity/migrations/platform_identity_030_guild_engagements.sql +64 -0
- package/modules/platform-identity/routes/guilds.js +35 -1
- package/modules/platform-identity/routes/my-projects.js +27 -3
- package/modules/platform-identity/routes/projects.js +44 -0
- package/modules/platform-identity/routes/sso.js +21 -3
- package/modules/public-landing/public/assets/cosmos.css +7 -0
- package/modules/public-landing/public/guild.html +633 -0
- package/modules/public-landing/public/guild.probes.json +63 -0
- package/modules/public-landing/public/guild.states.json +43 -0
- package/modules/public-landing/public/projects.html +252 -16
- package/modules/public-landing/public/projects.probes.json +26 -1
- package/modules/public-landing/public/projects.states.json +4 -0
- package/modules/ui-design/kit/fixtures/guilds.json +73 -0
- package/modules/ui-design/kit/fixtures/me__guild-engagements.json +7 -0
- package/modules/ui-design/kit/serve.js +99 -5
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/run-unit-tests.js +5 -0
- package/src/module-api.js +1 -1
- package/src/platform-server.js +13 -0
- package/tests/applicant_profile_boundary.mjs +104 -1
- package/tests/application_echo.mjs +91 -5
- package/tests/guild_engagement.mjs +881 -0
- package/tests/guild_engagement_db.mjs +240 -0
- package/tests/hub_engagement_cards.mjs +307 -0
- package/tests/hub_guild_page.mjs +624 -0
- package/tests/privacy_matrix.mjs +216 -14
- package/tests/project_invite_ui.mjs +187 -3
- package/tests/render_check.mjs +2 -2
- package/tests/ui_design_kit.mjs +3 -3
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "The guild page's interaction contract for modules/ui-design/kit/probe.js (task 1002301): signed out, the public page and the sign-in doors pointing back at the guild; signed in, the member's two switches driven end to end against the stub's held guilds (serve.js keeps fixtures/guilds.json for the run and refuses what PATCH /guilds/:slug/membership refuses), so a save the page re-reads comes back saved. The probes are a SEQUENCE over one stub: the prompt's yes lists the builder, later probes put them back off, and the 'not now' probe runs in a fresh browser (no remembered answer). The owner of night-shift (task 1002303, R27) puts the guild forward for a project: an empty field sends nothing, a project not public on the map is one honest line, and a bare host is taken as https:// — the stub mirrors POST /guilds/:slug/engagements' refusals (a member's 403, one 404 for any project not public on the map). A probe with `continue` keeps the previous page.",
|
|
3
|
+
"page": "/guild",
|
|
4
|
+
"surface": "public-landing",
|
|
5
|
+
"probes": [
|
|
6
|
+
{ "name": "signed out: the public guild, and both sign-in doors return to it", "auth": false, "width": 1440, "url": "/g/forge-crew",
|
|
7
|
+
"steps": [["wait", 400]],
|
|
8
|
+
"expect": { "visible": ["#guildBody", "#signinTop"], "hidden": ["#gYou", "#missing"], "count": [["#gRoster li", 4]],
|
|
9
|
+
"href": [["#signinTop", "/api/bongos/auth/web/start?return=/g/forge-crew&repos=1"], ["#missingSignin", "/api/bongos/auth/web/start?return=/g/forge-crew&repos=1"]] } },
|
|
10
|
+
{ "name": "signed out: an unlisted guild is 'no guild here', and sign-in returns to it", "auth": false, "width": 1440, "url": "/g/night-shift",
|
|
11
|
+
"steps": [["wait", 400]],
|
|
12
|
+
"expect": { "visible": ["#missing", "#missingSignin"], "hidden": ["#guildBody"],
|
|
13
|
+
"href": [["#missingSignin", "/api/bongos/auth/web/start?return=/g/night-shift&repos=1"]] } },
|
|
14
|
+
{ "name": "a path that cannot be a slug is 'no guild here'", "auth": false, "width": 1440, "url": "/g/-bad-",
|
|
15
|
+
"steps": [["wait", 400]],
|
|
16
|
+
"expect": { "visible": ["#missing"], "hidden": ["#guildBody"] } },
|
|
17
|
+
{ "name": "signed in, a member not on the public page: asked first, the pair waits", "auth": true, "width": 1440, "url": "/g/forge-crew",
|
|
18
|
+
"steps": [["wait", 500]],
|
|
19
|
+
"expect": { "visible": ["#gYou", "#showAsk", "#askYes", "#askNo"], "hidden": ["#shownBand", "#gApply"], "count": [["#gRoster li", 6]],
|
|
20
|
+
"text": [["#countWhat", "Your numbers count toward this guild’s rounded totals — even while your record is hidden or your account is private. They are never shown on their own: only inside totals rounded down to a band (10+, 20+, 50+ …) and counted once a week. Leave them out of any guild you’d rather not share them with."]] } },
|
|
21
|
+
{ "name": "show me: the switch saves, the prompt gives way to the pair, set to Show me", "continue": true,
|
|
22
|
+
"steps": [["click", "#askYes"], ["wait", 500]],
|
|
23
|
+
"expect": { "text": [["#youMsg", "Saved — you’re listed on its public page."]], "hidden": ["#showAsk"], "visible": ["#shownBand"], "checked": "input[name=shown][value=on]" } },
|
|
24
|
+
{ "name": "nothing changed saves nothing", "continue": true,
|
|
25
|
+
"steps": [["click", "#youSave"], ["wait", 150]],
|
|
26
|
+
"expect": { "text": [["#youMsg", "Nothing has changed."]] } },
|
|
27
|
+
{ "name": "picking Leave them out says what that means before anything is saved", "continue": true,
|
|
28
|
+
"steps": [["click", "#countBand .aOpt:nth-child(2)"], ["wait", 150]],
|
|
29
|
+
"expect": { "checked": "input[name=counted][value=off]", "text": [["#countWhat", "Your numbers are left out of this guild’s totals, from its next weekly count."], ["#youMsg", "Nothing has changed."]] } },
|
|
30
|
+
{ "name": "both switches in one save: only what moved is sent, and the page re-reads", "continue": true,
|
|
31
|
+
"steps": [["click", "#shownBand .aOpt:nth-child(2)"], ["click", "#youSave"], ["wait", 500]],
|
|
32
|
+
"expect": { "text": [["#youMsg", "Saved — you’re off its public page, and your numbers are left out of its totals."]], "checked": "input[name=shown][value=off]", "hidden": ["#showAsk"] } },
|
|
33
|
+
{ "name": "a fresh visit, off the public page again: asked again; not now keeps you off and shows the pair", "auth": true, "width": 1440, "url": "/g/forge-crew",
|
|
34
|
+
"steps": [["wait", 500], ["click", "#askNo"], ["wait", 150]],
|
|
35
|
+
"expect": { "hidden": ["#showAsk"], "visible": ["#shownBand"], "checked": "input[name=shown][value=off]",
|
|
36
|
+
"text": [["#youMsg", "You’re staying off its public page. You can change that here at any time."]] } },
|
|
37
|
+
{ "name": "the owner of an unlisted guild: no public page, so no pair and no record", "auth": true, "width": 1440, "url": "/g/night-shift",
|
|
38
|
+
"steps": [["wait", 500]],
|
|
39
|
+
"expect": { "visible": ["#gYou", "#shownUnlisted", "#gApply", "#applyOrigin"], "hidden": ["#shownBand", "#showAsk", "#gFigs"], "text": [["#gKicker", "Guild · unlisted"], ["#gYouSide", "You own this guild. Only you see this, and only you can change it."]] } },
|
|
40
|
+
{ "name": "apply to a project: an empty field asks for the address and sends nothing", "continue": true,
|
|
41
|
+
"steps": [["click", "#applySend"], ["wait", 150]],
|
|
42
|
+
"expect": { "text": [["#applyMsg", "Enter the project’s address."]], "hasClass": [["#applyMsg", "err"]] } },
|
|
43
|
+
{ "name": "apply to a project: a project that is not public on the map is one honest line, and the address stays", "continue": true,
|
|
44
|
+
"steps": [["fill", "#applyOrigin", "https://hidden.example.com"], ["click", "#applySend"], ["wait", 400]],
|
|
45
|
+
"expect": { "text": [["#applyMsg", "No public project on the map at that address."]], "value": [["#applyOrigin", "https://hidden.example.com"]] } },
|
|
46
|
+
{ "name": "apply to a project: a bare host is taken as https://, and the line says it applied for nobody", "continue": true,
|
|
47
|
+
"steps": [["fill", "#applyOrigin", "demo.cloudbongos.com"], ["click", "#applySend"], ["wait", 400]],
|
|
48
|
+
"expect": { "hasClass": [["#applyMsg", "ok"]], "value": [["#applyOrigin", ""]],
|
|
49
|
+
"text": [["#applyMsg", "Done. Night Shift’s members will see Demo in their invitations, and each one applies as themselves. That includes you — this didn’t apply for you."]] } },
|
|
50
|
+
{ "name": "a public guild you are not in: the public page, nothing of yours", "auth": true, "width": 1440, "url": "/g/open-hands",
|
|
51
|
+
"steps": [["wait", 500]],
|
|
52
|
+
"expect": { "visible": ["#guildBody"], "hidden": ["#gYou", "#missing"], "text": [["#gRecordNote", "Not counted yet. A guild’s record is counted once a week."]] } },
|
|
53
|
+
{ "name": "the hub's bar: the chip names the person, both bands closed, the page starts under the bar", "auth": true, "width": 1440, "url": "/g/open-hands",
|
|
54
|
+
"steps": [["wait", 400]],
|
|
55
|
+
"expect": { "expanded": false, "top": [["#guild", 64]], "visible": ["#projTrig", "#who"], "hidden": ["#signinTop", "#menuProjects", "#menuProfile"], "text": [["#whoLogin", "builder"]], "count": [["#mapQ", 0], ["[aria-current=\"page\"]", 0]] } },
|
|
56
|
+
{ "name": "Enter opens Projects and lands on the first item; Escape closes and returns focus", "continue": true,
|
|
57
|
+
"steps": [["focus", "#projTrig"], ["press", "Enter"], ["wait", 150], ["press", "Escape"], ["wait", 100]],
|
|
58
|
+
"expect": { "closed": "#menuProjects", "activeId": "projTrig", "expanded": false } },
|
|
59
|
+
{ "name": "phone width: the bar stays one line and nothing scrolls sideways", "auth": true, "width": 320, "url": "/g/forge-crew",
|
|
60
|
+
"steps": [["wait", 500]],
|
|
61
|
+
"expect": { "noOverflow": true, "visible": ["#guildBody"] } }
|
|
62
|
+
]
|
|
63
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "The guild page's states for modules/ui-design/kit/render.js (task 1002301, R25; ADR 0336 D4, ADR 0337 D4.4). One shell serves every /g/<slug>; the stub answers three guilds from fixtures/guilds.json. Signed out: forge-crew is the public page (four listed members, the rounded record), and night-shift — unlisted — is the same 'no guild here' an unknown slug gets, with sign-in. Signed in as the fixture builder: forge-crew is the members' view (everyone, six, with the prompt to be listed because they are a member who is not on its public page; the pair for that switch waits until the prompt is answered), night-shift is their unlisted guild (no public page, no published record), open-hands is a public guild they are not in (the public view, not counted yet), and an unknown slug is 'no guild here' without the sign-in. As night-shift's owner they also see 'apply to a project' (task 1002303, R27): the field, and the guild put forward for a project on the map — it applies for nobody, so the line says each member applies as themselves. Dark only, like every apex page (ADR 0204).",
|
|
3
|
+
"page": "/guild",
|
|
4
|
+
"surface": "public-landing",
|
|
5
|
+
"modes": ["dark"],
|
|
6
|
+
"modeQuery": false,
|
|
7
|
+
"ignoreRequests": ["/api/(gds|bongos)/me$", "/api/(gds|bongos)/guilds/[a-z0-9-]+(/members)?$"],
|
|
8
|
+
"ignoreConsole": ["/api/(gds|bongos)/me", "/api/(gds|bongos)/guilds/", "\\b401\\b", "\\b404\\b", "Unauthorized", "Not Found"],
|
|
9
|
+
"ignoreControls": [".vh"],
|
|
10
|
+
"states": {
|
|
11
|
+
"out-public": { "url": "/g/forge-crew", "auth": false, "actions": [["wait", 400]],
|
|
12
|
+
"expect": { "visible": ["#guildBody", "#gName", "#gRoster", "#gFigs", "#signinTop"], "hidden": ["#gYou", "#gApply", "#missing", "#loading", "#who"],
|
|
13
|
+
"text": [["#gName", "Forge Crew"], ["#gKicker", "Guild"], ["#gMeta", "4 members · since Sep 2026"], ["#tShipped", "100+"], ["#tCredits", "2,000+"], ["#tKarma", "50+"], ["#tProjects", "under 10"], ["#gRecordNote", "Counted on 1 Oct 2026. Projects are the public projects its listed members build on."]],
|
|
14
|
+
"count": [["#gRoster li", 4]] } },
|
|
15
|
+
"out-unlisted": { "url": "/g/night-shift", "auth": false, "actions": [["wait", 400]],
|
|
16
|
+
"expect": { "visible": ["#missing", "#missingSignin"], "hidden": ["#guildBody", "#loading"],
|
|
17
|
+
"text": [["#missingWhy", "There is no public guild at this address. If it is an unlisted guild you belong to, sign in to see it."]] } },
|
|
18
|
+
"in-member": { "url": "/g/forge-crew", "auth": true, "actions": [["wait", 500]],
|
|
19
|
+
"expect": { "visible": ["#guildBody", "#gYou", "#showAsk", "#countWhat", "#who"], "hidden": ["#shownBand", "#shownUnlisted", "#gApply", "#missing", "#signinTop"],
|
|
20
|
+
"text": [["#gKicker", "Guild · public"], ["#gMeta", "6 members · 4 on the public page · since Sep 2026"], ["#gYouSide", "You’re a member of this guild. Only you see this, and only you can change it."]],
|
|
21
|
+
"checked": "input[name=shown][value=off]",
|
|
22
|
+
"count": [["#gRoster li", 6]] } },
|
|
23
|
+
"in-unlisted-owner": { "url": "/g/night-shift", "auth": true, "actions": [["wait", 500]],
|
|
24
|
+
"expect": { "visible": ["#guildBody", "#gYou", "#shownUnlisted", "#gApply"], "hidden": ["#showAsk", "#shownBand", "#gFigs", "#gDesc"],
|
|
25
|
+
"text": [["#gKicker", "Guild · unlisted"], ["#shownUnlisted", "This guild is unlisted, so it has no public page. If you make it public, you’ll be listed on it as its owner, and each member will be asked before they are."], ["#gMeta", "3 members"], ["#gRecordNote", "This guild is unlisted, so its record isn’t published anywhere."], ["#countWhat", "Your numbers are left out of this guild’s totals, from its next weekly count."]],
|
|
26
|
+
"checked": "input[name=counted][value=off]" } },
|
|
27
|
+
"in-not-member": { "url": "/g/open-hands", "auth": true, "actions": [["wait", 500]],
|
|
28
|
+
"expect": { "visible": ["#guildBody", "#gRoster"], "hidden": ["#gYou", "#gApply", "#missing"],
|
|
29
|
+
"text": [["#gKicker", "Guild"], ["#gMeta", "2 members · since Oct 2026"], ["#tShipped", "—"], ["#gRecordNote", "Not counted yet. A guild’s record is counted once a week."]],
|
|
30
|
+
"count": [["#gRoster li", 2]] } },
|
|
31
|
+
"in-missing": { "url": "/g/no-such-guild", "auth": true, "actions": [["wait", 500]],
|
|
32
|
+
"expect": { "visible": ["#missing"], "hidden": ["#guildBody", "#missingSignin"],
|
|
33
|
+
"text": [["#missingWhy", "There is no guild at this address that you can see. A guild that is unlisted is shown only to its members."]] } },
|
|
34
|
+
"in-owner-apply": { "url": "/g/night-shift", "auth": true, "actions": [["wait", 500], ["focus", "#applyOrigin"], ["wait", 150]],
|
|
35
|
+
"expect": { "visible": ["#gApply", "#applyOrigin", "#applySend"], "text": [["#gApply-h", "apply to a project"], ["#applyMsg", ""]], "value": [["#applyOrigin", ""]] } },
|
|
36
|
+
"in-owner-applied": { "url": "/g/night-shift", "auth": true, "actions": [["wait", 500], ["fill", "#applyOrigin", "example.com"], ["click", "#applySend"], ["wait", 400]],
|
|
37
|
+
"expect": { "visible": ["#gApply", "#applyMsg"], "hasClass": [["#applyMsg", "ok"]], "value": [["#applyOrigin", ""]],
|
|
38
|
+
"text": [["#applyMsg", "Done. Night Shift’s members will see Example World in their invitations, and each one applies as themselves. That includes you — this didn’t apply for you."]] } },
|
|
39
|
+
"in-profile-band": { "url": "/g/forge-crew", "auth": true, "actions": [["wait", 500], ["click", "#who"], ["wait", 300]],
|
|
40
|
+
"expect": { "visible": ["#menuProfile"], "hidden": ["#menuProjects"], "text": [["#bandWhoLogin", "builder"]] } }
|
|
41
|
+
},
|
|
42
|
+
"reducedMotion": { "url": "/g/forge-crew", "auth": true, "actions": [["wait", 400]] }
|
|
43
|
+
}
|
|
@@ -963,6 +963,11 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
963
963
|
<section id="mineInvites" hidden aria-labelledby="mineInvitesH">
|
|
964
964
|
<h2 id="mineInvitesH">Invitations</h2>
|
|
965
965
|
<ul class="mineRows" id="invitesList"></ul>
|
|
966
|
+
<!-- a guild you are in, engaging a project (task 1002303, R27; ADR 0365):
|
|
967
|
+
"G is applying to P" and "P invited G", from GET /me/guild-engagements.
|
|
968
|
+
Each card's Apply is the ordinary join request with `guild` added, so
|
|
969
|
+
you apply as yourself, never as the guild. -->
|
|
970
|
+
<ul class="mineRows" id="engageList"></ul>
|
|
966
971
|
</section>
|
|
967
972
|
|
|
968
973
|
<section aria-labelledby="mineBuildingH">
|
|
@@ -1281,6 +1286,17 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1281
1286
|
<ul class="suggList" id="mInviteSugg"></ul>
|
|
1282
1287
|
<ul class="inviteList" id="mInviteList"></ul>
|
|
1283
1288
|
<div class="projMsg" id="mInviteMsg" role="status" aria-live="polite"></div>
|
|
1289
|
+
<!-- invite a guild (task 1002303, R27; ADR 0365 D3): POST /projects/invite-guild
|
|
1290
|
+
{ client_id, guild } records that the project invited the guild and
|
|
1291
|
+
answers one hall hand-off link per member on the guild's public page,
|
|
1292
|
+
drawn as the same lines as a single invite above. -->
|
|
1293
|
+
<p class="finePrint">Or invite a guild, by its name or its /g/ address. Each member gets their own invite, and only the members shown on the guild’s public page are listed.</p>
|
|
1294
|
+
<div class="attach">
|
|
1295
|
+
<label class="vh" for="mInviteGuild">Guild name or address</label>
|
|
1296
|
+
<input type="text" id="mInviteGuild" placeholder="Guild name or /g/ address" autocomplete="off" autocapitalize="off" spellcheck="false" maxlength="200">
|
|
1297
|
+
<button type="button" class="btn small ghost" data-invite-guild="mInvite">Invite the guild</button>
|
|
1298
|
+
</div>
|
|
1299
|
+
<div class="projMsg" id="mInviteGuildMsg" role="status" aria-live="polite"></div>
|
|
1284
1300
|
</div>
|
|
1285
1301
|
</div>
|
|
1286
1302
|
|
|
@@ -3616,10 +3632,9 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3616
3632
|
hub has no accept route at all. Decline still works; it only clears the
|
|
3617
3633
|
hub's own pending row. */
|
|
3618
3634
|
function renderInvitesList(invites) {
|
|
3619
|
-
var section = $('mineInvites');
|
|
3620
3635
|
var ul = $('invitesList');
|
|
3621
3636
|
ul.textContent = '';
|
|
3622
|
-
if (!invites || !invites.length) {
|
|
3637
|
+
if (!invites || !invites.length) { paintInvitesSection(); return; }
|
|
3623
3638
|
invites.forEach(function (inv) {
|
|
3624
3639
|
var row = mineRow(inv);
|
|
3625
3640
|
var hint = mineEl('p', 'mineRow__hint');
|
|
@@ -3643,21 +3658,151 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3643
3658
|
decline.addEventListener('click', function () {
|
|
3644
3659
|
postThenRemoveRow(
|
|
3645
3660
|
API + '/my-projects/invites/' + encodeURIComponent(inv.client_id) + '/decline', row, decline, null,
|
|
3646
|
-
|
|
3661
|
+
paintInvitesSection
|
|
3647
3662
|
);
|
|
3648
3663
|
});
|
|
3649
3664
|
actions.appendChild(decline);
|
|
3650
3665
|
row.appendChild(actions);
|
|
3651
3666
|
ul.appendChild(row);
|
|
3652
3667
|
});
|
|
3653
|
-
|
|
3668
|
+
paintInvitesSection();
|
|
3669
|
+
}
|
|
3670
|
+
|
|
3671
|
+
/* the Invitations section holds two lists — the projects' own invites and the
|
|
3672
|
+
guild cards below — and shows while either has a row */
|
|
3673
|
+
function paintInvitesSection() {
|
|
3674
|
+
$('mineInvites').hidden = !$('invitesList').children.length && !$('engageList').children.length;
|
|
3654
3675
|
}
|
|
3655
3676
|
|
|
3656
3677
|
function loadInvites() {
|
|
3657
|
-
return
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3678
|
+
return Promise.all([
|
|
3679
|
+
fetch(API + '/my-projects/invites', { credentials: 'same-origin' })
|
|
3680
|
+
.then(function (res) { return res.ok ? res.json() : { invites: [] }; })
|
|
3681
|
+
.then(function (d) { renderInvitesList(d.invites || []); })
|
|
3682
|
+
.catch(function () { renderInvitesList([]); }),
|
|
3683
|
+
loadEngagements()
|
|
3684
|
+
]);
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
/* ── a guild you are in, engaging a project (task 1002303, R27; ADR 0365) ──
|
|
3688
|
+
GET /me/guild-engagements names what the guilds you are in have going: "G is
|
|
3689
|
+
applying to P" (the guild's owner put it forward) and "P invited G" (the
|
|
3690
|
+
project invited the guild; its hall sends each member on the guild's public
|
|
3691
|
+
page their own invite, and the rest of the guild hears it here). The read only
|
|
3692
|
+
names a project that is on the map and public, and leaves out one you already
|
|
3693
|
+
build on, are invited to, or have applied to. Each card's Apply is the SAME
|
|
3694
|
+
relay as everything else in this view — postJoinRequest — with `guild` added:
|
|
3695
|
+
you apply as yourself, never as the guild, with your own note if you like. A
|
|
3696
|
+
guild that no longer holds for you (you left it, or its 30 days ran out) is a
|
|
3697
|
+
409 and nothing is relayed; the card then offers the plain application.
|
|
3698
|
+
textContent / createElement only. Proof: tests/hub_engagement_cards.mjs */
|
|
3699
|
+
var ENGAGE_GONE = 'That guild application has ended — you can still apply on your own.';
|
|
3700
|
+
function engageLine(e) {
|
|
3701
|
+
var g = e.guild.name || e.guild.slug;
|
|
3702
|
+
var p = (e.project && e.project.name) || hostOf(e.project.origin);
|
|
3703
|
+
return e.kind === 'invited' ? p + ' invited ' + g : g + ' is applying to ' + p + ' — apply with them?';
|
|
3704
|
+
}
|
|
3705
|
+
function sessionExpired(el) {
|
|
3706
|
+
el.className = 'projMsg err';
|
|
3707
|
+
el.textContent = 'Your session expired — ';
|
|
3708
|
+
var a = document.createElement('a');
|
|
3709
|
+
a.href = API + '/auth/web/start?return=/projects%3Fview%3Dmine&repos=1';
|
|
3710
|
+
a.textContent = 'sign in again';
|
|
3711
|
+
el.appendChild(a);
|
|
3712
|
+
el.appendChild(document.createTextNode('.'));
|
|
3713
|
+
}
|
|
3714
|
+
function engageRow(e) {
|
|
3715
|
+
var origin = e.project.origin;
|
|
3716
|
+
var slug = e.guild.slug;
|
|
3717
|
+
var row = mineRow({ name: engageLine(e), origin: origin });
|
|
3718
|
+
row.className = 'mineRow engageRow';
|
|
3719
|
+
var main = row.querySelector('.mineRow__main');
|
|
3720
|
+
var hint = mineEl('p', 'mineRow__hint');
|
|
3721
|
+
hint.textContent = 'You apply as yourself — add a note for the owner if you like.';
|
|
3722
|
+
main.appendChild(hint);
|
|
3723
|
+
var note = document.createElement('textarea');
|
|
3724
|
+
note.className = 'mineJoinNote';
|
|
3725
|
+
note.rows = 2;
|
|
3726
|
+
note.maxLength = 400;
|
|
3727
|
+
note.placeholder = 'A note for the owner (optional) — what you’d like to work on, who you are.';
|
|
3728
|
+
note.setAttribute('aria-label', 'A note for the owner (optional)');
|
|
3729
|
+
note.hidden = true;
|
|
3730
|
+
main.appendChild(note);
|
|
3731
|
+
var msg = mineEl('p', 'projMsg');
|
|
3732
|
+
msg.setAttribute('role', 'status');
|
|
3733
|
+
msg.setAttribute('aria-live', 'polite');
|
|
3734
|
+
main.appendChild(msg);
|
|
3735
|
+
var actions = mineEl('div', 'mineRow__actions');
|
|
3736
|
+
var noteBtn = document.createElement('button');
|
|
3737
|
+
noteBtn.type = 'button';
|
|
3738
|
+
noteBtn.className = 'linklike';
|
|
3739
|
+
noteBtn.textContent = 'Add a note';
|
|
3740
|
+
noteBtn.addEventListener('click', function () { note.hidden = false; noteBtn.hidden = true; note.focus(); });
|
|
3741
|
+
var apply = document.createElement('button');
|
|
3742
|
+
apply.type = 'button';
|
|
3743
|
+
apply.className = 'btn small';
|
|
3744
|
+
apply.textContent = 'Apply';
|
|
3745
|
+
/* with the guild until the server says it no longer holds, then on your own */
|
|
3746
|
+
var withGuild = true;
|
|
3747
|
+
function idle() { apply.disabled = false; apply.textContent = withGuild ? 'Apply' : 'Apply on my own'; }
|
|
3748
|
+
apply.addEventListener('click', function () {
|
|
3749
|
+
apply.disabled = true;
|
|
3750
|
+
apply.textContent = 'Applying…';
|
|
3751
|
+
msg.className = 'projMsg';
|
|
3752
|
+
msg.textContent = '';
|
|
3753
|
+
postJoinRequest(origin, (note.value || '').trim() || null, withGuild ? slug : null).then(function (res) {
|
|
3754
|
+
if (res.unauthenticated) { idle(); sessionExpired(msg); return; }
|
|
3755
|
+
if (res.ok && res.data && res.data.status === 'requested') {
|
|
3756
|
+
/* no longer pressable, so no longer the accent (the One Warm Thing rule) */
|
|
3757
|
+
apply.className = 'btn small ghost';
|
|
3758
|
+
apply.textContent = 'Applied';
|
|
3759
|
+
hint.hidden = true;
|
|
3760
|
+
note.hidden = true;
|
|
3761
|
+
noteBtn.hidden = true;
|
|
3762
|
+
msg.className = 'projMsg ok';
|
|
3763
|
+
msg.textContent = res.data.message || 'Requested.';
|
|
3764
|
+
return;
|
|
3765
|
+
}
|
|
3766
|
+
var err = apiError(res.data, res.status);
|
|
3767
|
+
if (res.status === 409 && err.code === 'guild_engagement_gone') {
|
|
3768
|
+
withGuild = false;
|
|
3769
|
+
idle();
|
|
3770
|
+
msg.textContent = ENGAGE_GONE;
|
|
3771
|
+
return;
|
|
3772
|
+
}
|
|
3773
|
+
idle();
|
|
3774
|
+
/* the relay's own answer (invite_only, already_member …) in its own words */
|
|
3775
|
+
msg.className = 'projMsg' + (res.ok ? '' : ' err');
|
|
3776
|
+
msg.textContent = (res.ok && res.data && res.data.message) || err.message || 'Could not reach that project — try again in a moment.';
|
|
3777
|
+
}).catch(function () {
|
|
3778
|
+
idle();
|
|
3779
|
+
msg.className = 'projMsg err';
|
|
3780
|
+
msg.textContent = 'Couldn’t reach the platform — check your connection and try again.';
|
|
3781
|
+
});
|
|
3782
|
+
});
|
|
3783
|
+
actions.appendChild(noteBtn);
|
|
3784
|
+
actions.appendChild(apply);
|
|
3785
|
+
row.appendChild(actions);
|
|
3786
|
+
return row;
|
|
3787
|
+
}
|
|
3788
|
+
/* a row this page cannot act on (an unknown kind, no guild to name, no address
|
|
3789
|
+
to apply to) is dropped rather than drawn as a card whose button fails */
|
|
3790
|
+
function renderEngagements(rows) {
|
|
3791
|
+
var ul = $('engageList');
|
|
3792
|
+
ul.textContent = '';
|
|
3793
|
+
(rows || []).forEach(function (e) {
|
|
3794
|
+
if (!e || (e.kind !== 'apply' && e.kind !== 'invited')) return;
|
|
3795
|
+
if (!e.guild || typeof e.guild.slug !== 'string' || !e.guild.slug) return;
|
|
3796
|
+
if (!e.project || !safeHttpUrl(e.project.origin)) return;
|
|
3797
|
+
ul.appendChild(engageRow(e));
|
|
3798
|
+
});
|
|
3799
|
+
paintInvitesSection();
|
|
3800
|
+
}
|
|
3801
|
+
function loadEngagements() {
|
|
3802
|
+
return fetch(API + '/me/guild-engagements', { credentials: 'same-origin' })
|
|
3803
|
+
.then(function (res) { return res.ok ? res.json() : { engagements: [] }; })
|
|
3804
|
+
.then(function (d) { renderEngagements(d.engagements || []); })
|
|
3805
|
+
.catch(function () { renderEngagements([]); });
|
|
3661
3806
|
}
|
|
3662
3807
|
|
|
3663
3808
|
/* the one relay both the map's per-card Apply (submitApply) and the
|
|
@@ -3665,11 +3810,17 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3665
3810
|
the 'requested' echo update kept in ONE place (task 1003774). Keys the
|
|
3666
3811
|
echo off the server's OWN resolved origin where the response carries one:
|
|
3667
3812
|
it need not match what the visitor typed byte-for-byte (a trailing
|
|
3668
|
-
slash, a different case) to be the same project the map's catalog names.
|
|
3669
|
-
|
|
3813
|
+
slash, a different case) to be the same project the map's catalog names.
|
|
3814
|
+
`guild` (a slug, optional) is a guild card's: the application is still the
|
|
3815
|
+
caller's own, and the hub records which guild they came with (task 1002303,
|
|
3816
|
+
R27; ADR 0365 D1/D2). */
|
|
3817
|
+
function postJoinRequest(origin, note, guild) {
|
|
3818
|
+
var body = { origin: origin };
|
|
3819
|
+
if (note) body.note = note;
|
|
3820
|
+
if (guild) body.guild = guild;
|
|
3670
3821
|
return fetch(API + '/my-projects/join', {
|
|
3671
3822
|
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' },
|
|
3672
|
-
body: JSON.stringify(
|
|
3823
|
+
body: JSON.stringify(body),
|
|
3673
3824
|
}).then(function (res) {
|
|
3674
3825
|
if (res.status === 401) return { unauthenticated: true };
|
|
3675
3826
|
return jsonOrEmpty(res).then(function (data) {
|
|
@@ -10296,11 +10447,17 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
10296
10447
|
here, because the project notifies the hub (POST /sso/invites/notify).
|
|
10297
10448
|
|
|
10298
10449
|
WHAT IT PUBLISHES OF AN INVITEE is the login the inviter typed and nothing else.
|
|
10450
|
+
|
|
10451
|
+
A GUILD (task 1002303, R27; ADR 0365 D3), on the project's own page only: the
|
|
10452
|
+
inviter names a public guild and POST /projects/invite-guild { client_id, guild }
|
|
10453
|
+
answers one hand-off link per member on the guild's PUBLIC page — the people the
|
|
10454
|
+
inviter could see, and their handle and display name, which that page already
|
|
10455
|
+
shows. They become the same lines, each finished on the hall like any other.
|
|
10299
10456
|
Proof: ../../../tests/project_invite_ui.mjs ══ */
|
|
10300
10457
|
|
|
10301
10458
|
var INVITE_MOUNTS = {
|
|
10302
10459
|
wizInvite: { who: 'wizInviteWho', list: 'wizInviteList', msg: 'wizInviteMsg', after: 'doneStepsHead', why: 'wizInviteWhy', sugg: 'wizInviteSugg' },
|
|
10303
|
-
mInvite: { who: 'mInviteWho', list: 'mInviteList', msg: 'mInviteMsg', after: null, why: 'mInviteWhy', sugg: 'mInviteSugg' }
|
|
10460
|
+
mInvite: { who: 'mInviteWho', list: 'mInviteList', msg: 'mInviteMsg', after: null, why: 'mInviteWhy', sugg: 'mInviteSugg', guild: 'mInviteGuild', guildMsg: 'mInviteGuildMsg' }
|
|
10304
10461
|
};
|
|
10305
10462
|
var inviteSlug = {}; /* mount id → the slug it invites to; '' means no step at all */
|
|
10306
10463
|
var inviteRows = {}; /* mount id → the invites filed on THIS project this session */
|
|
@@ -10320,6 +10477,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
10320
10477
|
inviteRows[id] = [];
|
|
10321
10478
|
inviteSugg[id] = null;
|
|
10322
10479
|
inviteSay(id, '', '');
|
|
10480
|
+
inviteSay(id, '', '', false, 'guildMsg');
|
|
10323
10481
|
}
|
|
10324
10482
|
var box = $(id);
|
|
10325
10483
|
if (box) box.hidden = !next;
|
|
@@ -10389,14 +10547,15 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
10389
10547
|
}
|
|
10390
10548
|
|
|
10391
10549
|
/* One line per name handed to the hall this session: the login the inviter
|
|
10392
|
-
typed,
|
|
10550
|
+
typed (or, for a guild's member, the display name its public page shows), and
|
|
10551
|
+
the link that finishes its invite there. The line says the invite is
|
|
10393
10552
|
NOT sent yet, because it is not — the hall's Invite button is what sends it. */
|
|
10394
10553
|
function renderInviteRows(id) {
|
|
10395
10554
|
var m = INVITE_MOUNTS[id];
|
|
10396
10555
|
var list = m && $(m.list);
|
|
10397
10556
|
if (!list) return;
|
|
10398
10557
|
list.innerHTML = (inviteRows[id] || []).map(function (r) {
|
|
10399
|
-
return '<li><b>' + esc(r.login) + '</b><small>not sent yet — finish it on your project’s hall.</small>'
|
|
10558
|
+
return '<li><b>' + esc(r.name || r.login) + '</b><small>not sent yet — finish it on your project’s hall.</small>'
|
|
10400
10559
|
+ '<a class="btn small" href="' + esc(r.url) + '" target="_blank" rel="noopener">Send the invite →</a></li>';
|
|
10401
10560
|
}).join('');
|
|
10402
10561
|
/* the two lists are one answer: a name that just became a pending row is no
|
|
@@ -10404,9 +10563,10 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
10404
10563
|
renderInviteSugg(id);
|
|
10405
10564
|
}
|
|
10406
10565
|
|
|
10407
|
-
|
|
10566
|
+
/* `key` names the line: the mount's message (the default) or the guild row's */
|
|
10567
|
+
function inviteSay(id, text, cls, isHtml, key) {
|
|
10408
10568
|
var m = INVITE_MOUNTS[id];
|
|
10409
|
-
var el = m && $(m
|
|
10569
|
+
var el = m && m[key || 'msg'] ? $(m[key || 'msg']) : null;
|
|
10410
10570
|
if (!el) return;
|
|
10411
10571
|
el.className = 'projMsg' + (cls ? ' ' + cls : '');
|
|
10412
10572
|
if (isHtml) el.innerHTML = text; else el.textContent = text;
|
|
@@ -10461,6 +10621,73 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
10461
10621
|
});
|
|
10462
10622
|
}
|
|
10463
10623
|
|
|
10624
|
+
/* inviteGuildSlug(raw) — a guild named the ways a person pastes one: its name in
|
|
10625
|
+
the address ("forge-crew"), "/g/forge-crew", or the guild page's whole address.
|
|
10626
|
+
PURE. Anything else is sent as typed, and the route answers for it. */
|
|
10627
|
+
function inviteGuildSlug(raw) {
|
|
10628
|
+
var s = String(raw == null ? '' : raw).replace(/^\s+|\s+$/g, '');
|
|
10629
|
+
var m = /(?:^|\/)g\/([^\/?#\s]+)\/?(?:[?#].*)?$/i.exec(s);
|
|
10630
|
+
if (m) {
|
|
10631
|
+
s = m[1];
|
|
10632
|
+
try { s = decodeURIComponent(s); } catch (e) { /* sent as typed */ }
|
|
10633
|
+
}
|
|
10634
|
+
return s.toLowerCase();
|
|
10635
|
+
}
|
|
10636
|
+
|
|
10637
|
+
/* doInviteGuild(id) — one public guild, one request: a pending line per member on
|
|
10638
|
+
its public page, each finished on the hall like a single invite. The route
|
|
10639
|
+
records that the project invited the guild and sends nothing, and nor does
|
|
10640
|
+
this. Only a mount that names a guild field has this control. */
|
|
10641
|
+
function doInviteGuild(id) {
|
|
10642
|
+
var m = INVITE_MOUNTS[id];
|
|
10643
|
+
var slug = inviteSlug[id] || '';
|
|
10644
|
+
if (!m || !m.guild || !slug || inviteBusy[id]) return;
|
|
10645
|
+
var field = $(m.guild);
|
|
10646
|
+
var guild = inviteGuildSlug(field && field.value);
|
|
10647
|
+
if (!guild) {
|
|
10648
|
+
inviteSay(id, 'Type the guild’s name or its /g/ address.', 'err', false, 'guildMsg');
|
|
10649
|
+
if (field && field.focus) field.focus();
|
|
10650
|
+
return;
|
|
10651
|
+
}
|
|
10652
|
+
inviteBusy[id] = true;
|
|
10653
|
+
inviteSay(id, 'Finding the guild’s members…', '', false, 'guildMsg');
|
|
10654
|
+
fetch(API + '/projects/invite-guild', {
|
|
10655
|
+
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' },
|
|
10656
|
+
body: JSON.stringify({ client_id: slug, guild: guild })
|
|
10657
|
+
}).then(function (res) {
|
|
10658
|
+
return jsonOrEmpty(res).then(function (data) {
|
|
10659
|
+
inviteBusy[id] = false;
|
|
10660
|
+
if (res.status === 401) { inviteSay(id, signInAgain(), 'err', true, 'guildMsg'); return; }
|
|
10661
|
+
var g = res.ok && data && data.guild;
|
|
10662
|
+
if (g) {
|
|
10663
|
+
/* a member already on a line (the same guild twice) is not drawn again */
|
|
10664
|
+
var listed = 0;
|
|
10665
|
+
(data.invites || []).forEach(function (r) {
|
|
10666
|
+
var url = r ? safeHttpUrl(r.invite_url) : '';
|
|
10667
|
+
if (!url || !r.handle) return;
|
|
10668
|
+
listed++;
|
|
10669
|
+
if ((inviteRows[id] || []).some(function (x) { return x.url === url; })) return;
|
|
10670
|
+
inviteRows[id] = (inviteRows[id] || []).concat([{ login: String(r.handle), name: r.display_name ? String(r.display_name) : '', url: url }]);
|
|
10671
|
+
});
|
|
10672
|
+
renderInviteRows(id);
|
|
10673
|
+
if (field) field.value = '';
|
|
10674
|
+
var name = g.name || guild;
|
|
10675
|
+
inviteSay(id, listed
|
|
10676
|
+
? name + ': ' + listed + (listed === 1 ? ' member' : ' members') + ' to invite. Send each invite from your project’s hall — every line above opens it with their name filled in.'
|
|
10677
|
+
: 'Nobody is listed on ' + name + '’s public page yet, so there is no one to invite from here.', listed ? 'ok' : '', false, 'guildMsg');
|
|
10678
|
+
return;
|
|
10679
|
+
}
|
|
10680
|
+
var e = apiError(data, res.status);
|
|
10681
|
+
/* an unknown guild and an unlisted one are one answer on the server */
|
|
10682
|
+
if (res.status === 404 && e.code === 'guild_not_found') { inviteSay(id, 'No public guild at that name.', 'err', false, 'guildMsg'); return; }
|
|
10683
|
+
inviteSay(id, inviteRefusal(e.code, res.status, e.message), 'err', false, 'guildMsg');
|
|
10684
|
+
});
|
|
10685
|
+
}).catch(function () {
|
|
10686
|
+
inviteBusy[id] = false;
|
|
10687
|
+
inviteSay(id, 'Couldn’t reach the platform — check your connection and try again.', 'err', false, 'guildMsg');
|
|
10688
|
+
});
|
|
10689
|
+
}
|
|
10690
|
+
|
|
10464
10691
|
/* skipInvite(id) — the step steps aside having sent NOTHING. Focus moves on
|
|
10465
10692
|
rather than being stranded inside the box that just went away. */
|
|
10466
10693
|
function skipInvite(id) {
|
|
@@ -10479,6 +10706,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
10479
10706
|
var box = $(id);
|
|
10480
10707
|
if (box) {
|
|
10481
10708
|
box.addEventListener('click', function (ev) {
|
|
10709
|
+
if (ev.target.closest && ev.target.closest('[data-invite-guild]')) { doInviteGuild(id); return; }
|
|
10482
10710
|
var t = ev.target.closest && ev.target.closest('[data-invite-send],[data-invite-skip],[data-invite-pick]');
|
|
10483
10711
|
if (!t) return;
|
|
10484
10712
|
/* a picked suggestion fills the field and takes the SAME path as a typed
|
|
@@ -10501,6 +10729,14 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
10501
10729
|
doInvite(id);
|
|
10502
10730
|
});
|
|
10503
10731
|
}
|
|
10732
|
+
var guildField = INVITE_MOUNTS[id].guild ? $(INVITE_MOUNTS[id].guild) : null;
|
|
10733
|
+
if (guildField) {
|
|
10734
|
+
guildField.addEventListener('keydown', function (ev) {
|
|
10735
|
+
if (ev.key !== 'Enter') return;
|
|
10736
|
+
ev.preventDefault();
|
|
10737
|
+
doInviteGuild(id);
|
|
10738
|
+
});
|
|
10739
|
+
}
|
|
10504
10740
|
});
|
|
10505
10741
|
|
|
10506
10742
|
/* ── boot: who am I → my instances ────────────────────────────────────── */
|
|
@@ -175,6 +175,31 @@
|
|
|
175
175
|
"expect": {"visible":["#panel-fork",".doorPill","#wizDemo","#wizStart"]} },
|
|
176
176
|
{ "name": "the wizard driver at 390: the review rows stack and the walk still reaches Create", "auth": true, "width": 390, "url": "/projects?view=new",
|
|
177
177
|
"steps": [["wait",600],["click","#termsAck"],["wait",100],["click","#wizStart"],["wait",400],["fill","#newName","Mercury"],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-what button[type=submit]"],["wait",400],["select","#physCountry","US"],["wait",100],["click","[data-phys=\"builder_geography\"][data-val=\"global\"]"],["click","[data-phys=\"minors_on_board\"][data-val=\"false\"]"],["click","[data-phys=\"entity\"][data-val=\"intent-solo\"]"],["click","#physNext"],["wait",700],["click","#reviewRows .setRow[data-row=\"address\"] [data-row-chg]"],["wait",200],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#reviewRows .setRow[data-row=\"address\"] [data-row-chg]"],["wait",200],["click","#reviewGo"],["wait",500]],
|
|
178
|
-
"expect": {"visible":["#panel-look","#wizLook","#doCreate"],"hidden":["#panel-review"],"text":[["#look-h","Give it a look."]]} }
|
|
178
|
+
"expect": {"visible":["#panel-look","#wizLook","#doCreate"],"hidden":["#panel-review"],"text":[["#look-h","Give it a look."]]} },
|
|
179
|
+
{ "name": "guild cards (task 1002303, R27): the mine view lists what your guilds have going, each card with its own Apply", "auth": true, "width": 1440, "url": "/projects?view=mine",
|
|
180
|
+
"steps": [["wait",900]],
|
|
181
|
+
"expect": { "visible": ["#mineInvites", "#engageList"], "count": [["#engageList .engageRow", 2], ["#engageList .engageRow .mineJoinNote:not([hidden])", 0]],
|
|
182
|
+
"text": [["#engageList .engageRow:nth-child(1) .mineRow__name", "Forge Crew is applying to Tidepool — apply with them?"], ["#engageList .engageRow:nth-child(2) .mineRow__name", "Lantern Works invited Forge Crew"]] } },
|
|
183
|
+
{ "name": "guild cards: Add a note opens the card's own note; Apply sends it with the guild (the stub refuses a guild with no engagement) and the card says it went through", "continue": true,
|
|
184
|
+
"steps": [["click","#engageList .engageRow:first-child .linklike"],["fill","#engageList .engageRow:first-child .mineJoinNote","Happy to take the migrations."],["click","#engageList .engageRow:first-child .btn"],["wait",400]],
|
|
185
|
+
"expect": { "hidden": ["#engageList .engageRow:first-child .mineJoinNote"], "hasClass": [["#engageList .engageRow:first-child .projMsg", "ok"]],
|
|
186
|
+
"text": [["#engageList .engageRow:first-child .btn", "Applied"], ["#engageList .engageRow:first-child .projMsg", "Requested access. The project’s owner admits new builders; once approved, sign in to enter."]] } },
|
|
187
|
+
{ "name": "guild cards: the invited card applies on its own, with no note", "continue": true,
|
|
188
|
+
"steps": [["click","#engageList .engageRow:nth-child(2) .btn"],["wait",400]],
|
|
189
|
+
"expect": { "text": [["#engageList .engageRow:nth-child(2) .btn", "Applied"]], "hasClass": [["#engageList .engageRow:nth-child(2) .projMsg", "ok"]] } },
|
|
190
|
+
{ "name": "invite a guild (task 1002303, R27): a /g/ address gives one pending line per member on its public page, each a link to the hall", "auth": true, "width": 1440, "url": "/projects?view=manage&id=101",
|
|
191
|
+
"steps": [["wait",900],["fill","#mInviteGuild","/g/forge-crew"],["click","#mInvite [data-invite-guild]"],["wait",400]],
|
|
192
|
+
"expect": { "count": [["#mInviteList li", 4]], "value": [["#mInviteGuild", ""]],
|
|
193
|
+
"href": [["#mInviteList li:first-child a", "https://mercury.example.test/builders/watch?invite=mara-quinn"]],
|
|
194
|
+
"text": [["#mInviteGuildMsg", "Forge Crew: 4 members to invite. Send each invite from your project’s hall — every line above opens it with their name filled in."]] } },
|
|
195
|
+
{ "name": "invite a guild: the same guild again draws no second line", "continue": true,
|
|
196
|
+
"steps": [["fill","#mInviteGuild","forge-crew"],["click","#mInvite [data-invite-guild]"],["wait",400]],
|
|
197
|
+
"expect": { "count": [["#mInviteList li", 4]] } },
|
|
198
|
+
{ "name": "invite a guild: an unlisted guild is the same one line as an unknown one", "continue": true,
|
|
199
|
+
"steps": [["fill","#mInviteGuild","night-shift"],["click","#mInvite [data-invite-guild]"],["wait",400]],
|
|
200
|
+
"expect": { "text": [["#mInviteGuildMsg", "No public guild at that name."]], "hasClass": [["#mInviteGuildMsg", "err"]], "count": [["#mInviteList li", 4]], "value": [["#mInviteGuild", "night-shift"]] } },
|
|
201
|
+
{ "name": "invite a guild: an empty field is a question, not a request; Enter in the field sends too", "continue": true,
|
|
202
|
+
"steps": [["fill","#mInviteGuild",""],["focus","#mInviteGuild"],["press","Enter"],["wait",150]],
|
|
203
|
+
"expect": { "text": [["#mInviteGuildMsg", "Type the guild’s name or its /g/ address."]], "activeId": "mInviteGuild" } }
|
|
179
204
|
]
|
|
180
205
|
}
|
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
"in-manage-kind": { "auth": true, "url": "/projects?view=manage&id=101", "expect": {"visible":["#mKind","#mKindPick .modBand","#mKindWhat summary"],"count":[["#mKindPick .modOpt",5],["#mKindPick .modOpt[aria-pressed=\"true\"]",0]],"text":[["#mKindPick .modOpt b","Game"],["#mKindPick .finePrint","This project has never said what kind it is. Saying so puts the word beside its name wherever it is listed; it changes nothing else."]]} },
|
|
27
27
|
"in-manage-invite": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mInvite", "#mInvite h2", "#mInviteWho", "#mInvite [data-invite-send]"] } },
|
|
28
28
|
"in-manage-invite-handoff": { "auth": true, "url": "/projects?view=manage&id=101", "actions": [["wait",900],["fill","#mInviteWho","octo-builder"],["click","#mInvite [data-invite-send]"],["wait",400]], "expect": {"visible":["#mInviteList a","#mInviteMsg"],"hidden":[]} },
|
|
29
|
+
"in-manage-invite-guild": { "auth": true, "url": "/projects?view=manage&id=101", "actions": [["wait",900],["fill","#mInviteGuild","https://cloudbongos.com/g/forge-crew"],["click","#mInvite [data-invite-guild]"],["wait",400]], "expect": { "visible": ["#mInviteGuild", "#mInviteList a", "#mInviteGuildMsg"], "count": [["#mInviteList li", 4]], "value": [["#mInviteGuild", ""]], "hasClass": [["#mInviteGuildMsg", "ok"]], "text": [["#mInviteList li:first-child b", "Mara Quinn"], ["#mInviteGuildMsg", "Forge Crew: 4 members to invite. Send each invite from your project’s hall — every line above opens it with their name filled in."]] } },
|
|
30
|
+
"in-mine-engagements": { "auth": true, "url": "/projects?view=mine", "actions": [["wait",900],["focus","#engageList .engageRow:first-child .btn"],["wait",200]], "expect": { "visible": ["#mineInvites", "#invitesList", "#engageList"], "count": [["#engageList .engageRow", 2], ["#engageList .engageRow .mineJoinNote:not([hidden])", 0]], "text": [["#engageList .engageRow:nth-child(1) .mineRow__name", "Forge Crew is applying to Tidepool — apply with them?"], ["#engageList .engageRow:nth-child(1) .mineRow__host", "tidepool.cloudbongos.com"], ["#engageList .engageRow:nth-child(2) .mineRow__name", "Lantern Works invited Forge Crew"], ["#engageList .engageRow:nth-child(1) .mineRow__hint", "You apply as yourself — add a note for the owner if you like."], ["#engageList .engageRow:nth-child(1) .btn", "Apply"]] } },
|
|
31
|
+
"in-mine-engagement-note": { "auth": true, "url": "/projects?view=mine", "actions": [["wait",900],["click","#engageList .engageRow:first-child .linklike"],["fill","#engageList .engageRow:first-child .mineJoinNote","I like the boring parts too: migrations and the deploy path."],["wait",150]], "expect": { "visible": ["#engageList .engageRow:first-child .mineJoinNote"], "hidden": ["#engageList .engageRow:first-child .linklike"], "value": [["#engageList .engageRow:first-child .mineJoinNote", "I like the boring parts too: migrations and the deploy path."]] } },
|
|
32
|
+
"in-mine-engagement-applied": { "auth": true, "url": "/projects?view=mine", "actions": [["wait",900],["click","#engageList .engageRow:first-child .linklike"],["fill","#engageList .engageRow:first-child .mineJoinNote","I like the boring parts too: migrations and the deploy path."],["click","#engageList .engageRow:first-child .btn"],["wait",400]], "expect": { "hidden": ["#engageList .engageRow:first-child .mineJoinNote", "#engageList .engageRow:first-child .linklike"], "hasClass": [["#engageList .engageRow:first-child .projMsg", "ok"], ["#engageList .engageRow:first-child .btn", "ghost"]], "text": [["#engageList .engageRow:first-child .btn", "Applied"], ["#engageList .engageRow:first-child .projMsg", "Requested access. The project’s owner admits new builders; once approved, sign in to enter."], ["#engageList .engageRow:nth-child(2) .btn", "Apply"]] } },
|
|
29
33
|
"in-manage-app-live": { "auth": true, "url": "/projects?view=manage&id=101", "actions": [["wait",900],["focus","#mApp a"]], "expect": {"visible":["#mAppCard","#mApp .appStat","#mApp .appVars"],"hidden":[]} },
|
|
30
34
|
"in-manage-app-building": { "auth": true, "url": "/projects?view=manage&id=102", "actions": [["wait",900],["focus","#mApp a"]], "expect": {"visible":["#mAppCard","#mApp .appStages"],"hidden":[]} },
|
|
31
35
|
"in-manage-app-failed": { "auth": true, "url": "/projects?view=manage&id=105", "actions": [["wait",900],["focus","#mApp .appRetryBtn"]], "expect": {"visible":["#mAppCard","#mApp p.err","#mApp .appRetryBtn"],"hidden":["#mApp .appRetry"]} },
|