mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-10 23:15:40 +00:00
feat(web): Community link (GitHub Discussions) in the user menu and auth footer (TASK-2888) (#1251)
Dave's day-57 call: no Discord; the repo's GitHub Discussions tab is the community channel. COMMUNITY_URL in $lib/brand/links; Community right after GitHub in the user-menu Resources block (Cloud + self-hosted) and the Cloud auth footer; docs/brand.md §7 order updated; AuthFooter and AuthHeader now take their URLs from the links module and sit under the single-source guard. Four codex rounds (two findings fixed, rounds 3-4 clean on the tip), CI 7/7 green. Claude-Session: https://claude.ai/code/session_015A7n836r64Y9THC8UWsDFF
This commit is contained in:
+9
-8
@@ -284,14 +284,15 @@ because Tailwind/Svelte tooling will warn without it.
|
||||
Always the same order. This is the contract:
|
||||
|
||||
1. GitHub *(external)*
|
||||
2. Docs
|
||||
3. Changelog
|
||||
4. Contribute
|
||||
5. FAQ
|
||||
6. Security
|
||||
7. Privacy
|
||||
8. Terms
|
||||
9. Sub-processors
|
||||
2. Community *(external — the repo's GitHub Discussions; TASK-2888)*
|
||||
3. Docs
|
||||
4. Changelog
|
||||
5. Contribute
|
||||
6. FAQ
|
||||
7. Security
|
||||
8. Privacy
|
||||
9. Terms
|
||||
10. Sub-processors
|
||||
|
||||
If a surface omits a link (e.g. self-hosted footer skips Changelog
|
||||
because there's no Changelog page on a self-hosted install), keep the
|
||||
|
||||
@@ -15,6 +15,13 @@
|
||||
/** The public repository. Shown in the app so a user never has to search for it. */
|
||||
export const GITHUB_REPO_URL = 'https://github.com/PerpetualSoftware/pad';
|
||||
|
||||
/**
|
||||
* Community channel: the repo's GitHub Discussions tab (TASK-2888 — Dave's
|
||||
* day-57 call: no Discord; Discussions, or r/getpad, is the channel for now).
|
||||
* Project-wide, so shown on Cloud and self-hosted alike.
|
||||
*/
|
||||
export const COMMUNITY_URL = `${GITHUB_REPO_URL}/discussions`;
|
||||
|
||||
/** Canonical project documentation — the same for Cloud and self-hosted. */
|
||||
export const DOCS_URL = 'https://getpad.dev/docs';
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
// Replaces the prior LegalFooter + SupportFooter pair — the brand spec
|
||||
// describes a single footer pattern, not two separate strips.
|
||||
|
||||
import { CHANGELOG_URL, COMMUNITY_URL, DOCS_URL, GITHUB_REPO_URL } from '$lib/brand/links';
|
||||
|
||||
let { cloudMode = false }: { cloudMode?: boolean } = $props();
|
||||
|
||||
// Canonical Cloud-mode link order from docs/brand.md §7. Do not reorder.
|
||||
@@ -29,9 +31,10 @@
|
||||
// anchor opens in a new tab — the user is mid-auth-flow and we don't want
|
||||
// to lose their form state by navigating away.
|
||||
const cloudLinks: Array<{ label: string; href: string }> = [
|
||||
{ label: 'GitHub', href: 'https://github.com/PerpetualSoftware/pad' },
|
||||
{ label: 'Docs', href: 'https://getpad.dev/docs' },
|
||||
{ label: 'Changelog', href: 'https://getpad.dev/changelog' },
|
||||
{ label: 'GitHub', href: GITHUB_REPO_URL },
|
||||
{ label: 'Community', href: COMMUNITY_URL },
|
||||
{ label: 'Docs', href: DOCS_URL },
|
||||
{ label: 'Changelog', href: CHANGELOG_URL },
|
||||
{ label: 'Contribute', href: 'https://getpad.dev/contribute' },
|
||||
{ label: 'FAQ', href: 'https://getpad.dev/faq' },
|
||||
{ label: 'Security', href: 'https://getpad.dev/security' },
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
// under their own brand and must not get getpad.dev chrome imposed on them.
|
||||
// Same pattern as the companion AuthFooter.svelte in this directory.
|
||||
|
||||
import { DOCS_URL, GITHUB_REPO_URL } from '$lib/brand/links';
|
||||
|
||||
let { cloudMode = false }: { cloudMode?: boolean } = $props();
|
||||
|
||||
let mobileMenuOpen = $state(false);
|
||||
@@ -21,11 +23,13 @@
|
||||
// Link list mirrors pad-web's marketing nav (Docs / Blog / GitHub) but
|
||||
// points at absolute marketing URLs explicitly — auth pages are pre-login,
|
||||
// the user has no workspace context, and these are "go back to the
|
||||
// marketing site" links rather than in-app nav.
|
||||
// marketing site" links rather than in-app nav. The Docs and GitHub
|
||||
// addresses come from $lib/brand/links (single source, guarded by
|
||||
// Sidebar.svelte.test.ts); Blog has no constant yet.
|
||||
const navLinks: Array<{ label: string; href: string }> = [
|
||||
{ label: 'Docs', href: 'https://getpad.dev/docs' },
|
||||
{ label: 'Docs', href: DOCS_URL },
|
||||
{ label: 'Blog', href: 'https://getpad.dev/blog' },
|
||||
{ label: 'GitHub', href: 'https://github.com/PerpetualSoftware/pad' }
|
||||
{ label: 'GitHub', href: GITHUB_REPO_URL }
|
||||
];
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
|
||||
@@ -290,7 +290,7 @@ describe('sidebar — GitHub repo link', () => {
|
||||
// re-hardcoded the same string renders an identical href and passes every
|
||||
// assertion above; it fails this one.
|
||||
//
|
||||
// Deliberately narrow: it names the two consumers rather than sweeping the
|
||||
// Deliberately narrow: it names the consumers rather than sweeping the
|
||||
// tree, so it cannot fail for an unrelated file (a doc, a fixture, a
|
||||
// changelog entry legitimately quoting the URL) and cannot quietly stop
|
||||
// covering them by drifting into a glob that no longer matches.
|
||||
@@ -300,7 +300,9 @@ describe('sidebar — GitHub repo link', () => {
|
||||
const root = process.cwd();
|
||||
for (const rel of [
|
||||
'src/lib/components/layout/Sidebar.svelte',
|
||||
'src/lib/components/layout/UserMenuResources.svelte'
|
||||
'src/lib/components/layout/UserMenuResources.svelte',
|
||||
'src/lib/components/auth/AuthFooter.svelte',
|
||||
'src/lib/components/auth/AuthHeader.svelte'
|
||||
]) {
|
||||
const source = readFileSync(join(root, rel), 'utf8');
|
||||
expect(source, `${rel} hardcodes the repo URL`).not.toContain(
|
||||
|
||||
@@ -8,12 +8,13 @@
|
||||
// menu (just above "Connect a project…" and "Sign out") and gives them
|
||||
// a quiet escape hatch back out to the surrounding ecosystem.
|
||||
//
|
||||
// Cloud (cloudMode=true): Docs / Changelog / GitHub / Status / Support
|
||||
// Self-hosted (cloudMode=false): Docs / GitHub only — Changelog and
|
||||
// Cloud (cloudMode=true): GitHub / Community / Docs / Changelog / Status / Support
|
||||
// Self-hosted (cloudMode=false): GitHub / Community / Docs — Changelog and
|
||||
// Status are Cloud-specific surfaces, and getpad.dev's support@getpad.dev
|
||||
// mailbox is not the operator's to direct people to. The Docs link still
|
||||
// points at getpad.dev because that's the canonical project documentation
|
||||
// even for self-hosted deployments.
|
||||
// even for self-hosted deployments, and Community (the repo's GitHub
|
||||
// Discussions) is the project's channel regardless of who hosts it.
|
||||
//
|
||||
// All links open in a new tab so a user mid-task doesn't lose state.
|
||||
// Replaces the prior inline Cloud-only Support/Status block in
|
||||
@@ -25,6 +26,7 @@
|
||||
|
||||
import {
|
||||
CHANGELOG_URL,
|
||||
COMMUNITY_URL,
|
||||
DOCS_URL,
|
||||
GITHUB_REPO_URL,
|
||||
STATUS_URL,
|
||||
@@ -52,13 +54,16 @@
|
||||
};
|
||||
|
||||
// Order follows the canonical link order from docs/brand.md §7: GitHub,
|
||||
// Docs, Changelog, then anything else. Status and Support are user-menu-
|
||||
// specific additions (the brand-spec footer doesn't carry them) so they
|
||||
// land at the end. Keeping the same relative order as the marketing
|
||||
// Community, Docs, Changelog, then anything else. Status and Support are
|
||||
// user-menu-specific additions (the brand-spec footer doesn't carry them)
|
||||
// so they land at the end. Community sits second, as in the footer: the
|
||||
// brand doc's rule is that a surface may omit links but never reorders
|
||||
// the ones it keeps (codex round 1 on TASK-2888 caught the drift). Keeping the same relative order as the marketing
|
||||
// footer means a user visiting both surfaces sees the same linear pattern
|
||||
// — small cohesion win that Codex flagged on first review.
|
||||
const cloudLinks: ResourceLink[] = [
|
||||
{ label: 'GitHub', href: GITHUB_REPO_URL },
|
||||
{ label: 'Community', href: COMMUNITY_URL },
|
||||
{ label: 'Docs', href: DOCS_URL },
|
||||
{ label: 'Changelog', href: CHANGELOG_URL },
|
||||
{ label: 'Status', href: STATUS_URL },
|
||||
@@ -66,11 +71,12 @@
|
||||
];
|
||||
|
||||
// Self-hosted is a subset that preserves the relative canonical order —
|
||||
// GitHub before Docs. Changelog/Status/Support are Cloud-only surfaces
|
||||
// GitHub, Community, Docs. Changelog/Status/Support are Cloud-only surfaces
|
||||
// (operators have their own changelog/status if any; getpad.dev's
|
||||
// support@ mailbox is not theirs to direct people to).
|
||||
const selfHostedLinks: ResourceLink[] = [
|
||||
{ label: 'GitHub', href: GITHUB_REPO_URL },
|
||||
{ label: 'Community', href: COMMUNITY_URL },
|
||||
{ label: 'Docs', href: DOCS_URL }
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// @vitest-environment jsdom
|
||||
//
|
||||
// TASK-2888: the Community link (the repo's GitHub Discussions tab) is a
|
||||
// project-wide channel, so it must appear in BOTH the Cloud and the
|
||||
// self-hosted Resources lists, sit immediately after GitHub (docs/brand.md
|
||||
// §7 canonical order — a surface may omit links but never reorders the
|
||||
// ones it keeps), and take its href from the shared brand module rather
|
||||
// than a re-typed literal.
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import { render, cleanup } from '@testing-library/svelte';
|
||||
import UserMenuResources from './UserMenuResources.svelte';
|
||||
import AuthFooter from '../auth/AuthFooter.svelte';
|
||||
import { COMMUNITY_URL, GITHUB_REPO_URL } from '$lib/brand/links';
|
||||
|
||||
afterEach(() => cleanup());
|
||||
|
||||
function labels(container: HTMLElement): (string | undefined)[] {
|
||||
return Array.from(container.querySelectorAll('a')).map((a) => a.textContent?.trim());
|
||||
}
|
||||
|
||||
function communityLink(container: HTMLElement): HTMLAnchorElement | null {
|
||||
return (
|
||||
Array.from(container.querySelectorAll('a')).find(
|
||||
(a) => a.textContent?.trim() === 'Community'
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
describe('Community link (TASK-2888)', () => {
|
||||
it('points at the repo Discussions tab, derived from the repo URL', () => {
|
||||
expect(COMMUNITY_URL).toBe(`${GITHUB_REPO_URL}/discussions`);
|
||||
});
|
||||
|
||||
it.each([true, false])('user menu renders Community (cloudMode=%s), external', (cloudMode) => {
|
||||
const { container } = render(UserMenuResources, { props: { cloudMode } });
|
||||
const a = communityLink(container);
|
||||
expect(a, 'Community link missing').not.toBeNull();
|
||||
expect(a?.getAttribute('href')).toBe(COMMUNITY_URL);
|
||||
expect(a?.getAttribute('target')).toBe('_blank');
|
||||
expect(a?.getAttribute('rel')).toBe('noopener noreferrer');
|
||||
});
|
||||
|
||||
it.each([true, false])(
|
||||
'user menu places Community immediately after GitHub (cloudMode=%s)',
|
||||
(cloudMode) => {
|
||||
const { container } = render(UserMenuResources, { props: { cloudMode } });
|
||||
const order = labels(container);
|
||||
expect(order.indexOf('GitHub')).toBeGreaterThanOrEqual(0);
|
||||
expect(order.indexOf('Community')).toBe(order.indexOf('GitHub') + 1);
|
||||
}
|
||||
);
|
||||
|
||||
it('auth footer renders Community on Cloud, immediately after GitHub', () => {
|
||||
const { container } = render(AuthFooter, { props: { cloudMode: true } });
|
||||
const order = labels(container);
|
||||
expect(order.indexOf('GitHub')).toBeGreaterThanOrEqual(0);
|
||||
expect(order.indexOf('Community')).toBe(order.indexOf('GitHub') + 1);
|
||||
expect(communityLink(container)?.getAttribute('href')).toBe(COMMUNITY_URL);
|
||||
});
|
||||
|
||||
it('auth footer renders nothing on self-hosted (unchanged)', () => {
|
||||
const { container } = render(AuthFooter, { props: { cloudMode: false } });
|
||||
expect(container.querySelector('a')).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user