Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/publish-owner-radio-default-org.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@inkeep/open-knowledge": patch
---

Publish-to-GitHub: pick the owner with a radio list and default to an org

The "Publish to GitHub" dialog now renders the eligible owners as a radio list (one card per owner) instead of a dropdown, so every choice is visible at a glance. When you belong to one or more organizations the wizard pre-selects the first org rather than your personal account, since a knowledge base is far more likely meant for the team, and accidentally publishing it under a personal login is annoying to undo. With no org available it falls back to your own account as before.
43 changes: 41 additions & 2 deletions packages/app/src/components/PublishToGitHubDialog.dom.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ mock.module('@/lib/share/publish-wizard', () => ({
extractFolderBasename: (path: string) => path.split('/').filter(Boolean).at(-1) ?? '',
fetchPublishNameCheck: fetchPublishNameCheckMock,
fetchPublishOwners: fetchPublishOwnersMock,
pickDefaultOwner: (owners: SharePublishOwner[]) =>
owners.find((o) => o.kind === 'org')?.login ?? owners[0]?.login ?? '',
presentPublishError: () => presentError,
resolveNameCheckStatus: (_result: unknown, owner: string, name: string) => ({
kind: 'available' as const,
Expand Down Expand Up @@ -212,7 +214,13 @@ describe('PublishToGitHubDialog runtime behavior', () => {
await renderDialog();

expect(screen.getByRole('dialog', { name: 'Publish to GitHub' })).toBeTruthy();
expect(await screen.findByTestId('publish-owner-trigger')).toBeTruthy();
expect(await screen.findByTestId('publish-owner-radio')).toBeTruthy();
expect(screen.getByTestId('publish-owner-option-docs-team').getAttribute('data-state')).toBe(
'checked',
);
expect(screen.getByTestId('publish-owner-option-alice').getAttribute('data-state')).toBe(
'unchecked',
);
expect((screen.getByTestId('publish-name') as HTMLInputElement).value).toBe('my-project');
expect(screen.getByText('Will be created as')).toBeTruthy();
expect(screen.getByText('my-project')).toBeTruthy();
Expand All @@ -232,6 +240,37 @@ describe('PublishToGitHubDialog runtime behavior', () => {
expect((screen.getByTestId('publish-submit') as HTMLButtonElement).disabled).toBe(false);
});

test('pre-selects the user account when no org is available', async () => {
ownersQueue = [{ ok: true, owners: [{ kind: 'user', login: 'alice' }] }];
await renderDialog();

expect(await screen.findByTestId('publish-owner-radio')).toBeTruthy();
expect(screen.getByTestId('publish-owner-option-alice').getAttribute('data-state')).toBe(
'checked',
);
await waitForAvailableNameCheck();
await userEvent.click(screen.getByTestId('publish-submit'));

await waitFor(() => expect(submitCalls).toHaveLength(1));
expect(submitCalls[0]?.owner).toBe('alice');
});

test('selecting a different owner radio updates the publish payload', async () => {
const { onOpenChange } = await renderDialog();
await waitForAvailableNameCheck();

await userEvent.click(screen.getByTestId('publish-owner-option-alice'));
expect(screen.getByTestId('publish-owner-option-alice').getAttribute('data-state')).toBe(
'checked',
);
await waitForAvailableNameCheck();
await userEvent.click(screen.getByTestId('publish-submit'));

await waitFor(() => expect(submitCalls).toHaveLength(1));
expect(submitCalls[0]?.owner).toBe('alice');
expect(onOpenChange).not.toHaveBeenCalledWith(false);
});

test('submits through publish helpers, shows success URL, copies from a fresh button click, and Done closes', async () => {
const { onOpenChange } = await renderDialog();
await waitForAvailableNameCheck();
Expand All @@ -246,7 +285,7 @@ describe('PublishToGitHubDialog runtime behavior', () => {
{
description: 'Internal docs',
name: 'my-project',
owner: 'alice',
owner: 'docs-team',
visibility: 'private',
},
]);
Expand Down
85 changes: 40 additions & 45 deletions packages/app/src/components/PublishToGitHubDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,6 @@ import {
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import { useDocumentContext } from '@/editor/DocumentContext';
import { docNameToMarkdownPath } from '@/lib/doc-paths';
Expand All @@ -41,6 +34,7 @@ import {
fetchPublishNameCheck,
fetchPublishOwners,
type NameCheckStatus,
pickDefaultOwner,
presentPublishError,
resolveNameCheckStatus,
sanitizeRepoName,
Expand Down Expand Up @@ -107,7 +101,7 @@ export function PublishToGitHubDialog({ open, onOpenChange }: PublishToGitHubDia
}
setOwners(res.owners);
if (res.owners.length > 0 && selectedOwner === '') {
setSelectedOwner(res.owners[0]?.login ?? '');
setSelectedOwner(pickDefaultOwner(res.owners));
}
} catch {
setOwnersError(t`Couldn't reach GitHub. Try again?`);
Expand All @@ -133,7 +127,7 @@ export function PublishToGitHubDialog({ open, onOpenChange }: PublishToGitHubDia
if (owners === null) {
void loadOwners();
} else if (selectedOwner === '' && owners.length > 0) {
setSelectedOwner(owners[0]?.login ?? '');
setSelectedOwner(pickDefaultOwner(owners));
}
}, [open]);

Expand Down Expand Up @@ -309,7 +303,7 @@ export function PublishToGitHubDialog({ open, onOpenChange }: PublishToGitHubDia

<DialogBody className="flex flex-col gap-6">
<fieldset className="flex flex-col gap-2">
<Label htmlFor="publish-owner">
<Label id="publish-owner-label">
<Trans>Owner</Trans>
</Label>
{ownersLoading && owners === null ? (
Expand All @@ -330,39 +324,40 @@ export function PublishToGitHubDialog({ open, onOpenChange }: PublishToGitHubDia
</Button>
</div>
) : (
<Select value={selectedOwner} onValueChange={setSelectedOwner}>
<SelectTrigger
id="publish-owner"
data-testid="publish-owner-trigger"
aria-label={t`Owner`}
>
<SelectValue placeholder={t`Pick an owner`} />
</SelectTrigger>
<SelectContent>
{(owners ?? []).map((o) => (
<SelectItem
key={o.login}
value={o.login}
data-testid={`publish-owner-option-${o.login}`}
>
<span className="flex items-center gap-2">
{o.avatarUrl ? (
<img
src={o.avatarUrl}
alt=""
aria-hidden
className="size-4 rounded-full"
/>
) : null}
<span>{o.login}</span>
<span className="text-xs text-muted-foreground">
{o.kind === 'user' ? <Trans>you</Trans> : <Trans>org</Trans>}
</span>
</span>
</SelectItem>
))}
</SelectContent>
</Select>
<RadioGroup
value={selectedOwner}
onValueChange={setSelectedOwner}
aria-labelledby="publish-owner-label"
data-testid="publish-owner-radio"
>
{(owners ?? []).map((o) => {
const itemId = `publish-owner-${o.login}`;
return (
<FieldLabel key={o.login} htmlFor={itemId}>
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>
{o.avatarUrl ? (
<img
src={o.avatarUrl}
alt=""
aria-hidden
className="size-4 rounded-full"
/>
) : null}
<span>{o.login}</span>
</FieldTitle>
</FieldContent>
<RadioGroupItem
value={o.login}
id={itemId}
data-testid={`publish-owner-option-${o.login}`}
/>
</Field>
</FieldLabel>
);
})}
</RadioGroup>
)}
</fieldset>

Expand Down Expand Up @@ -401,7 +396,7 @@ export function PublishToGitHubDialog({ open, onOpenChange }: PublishToGitHubDia
</fieldset>

<fieldset className="flex flex-col gap-2">
<Label>
<Label id="publish-visibility-label">
<Trans>Visibility</Trans>
</Label>
<RadioGroup
Expand All @@ -410,7 +405,7 @@ export function PublishToGitHubDialog({ open, onOpenChange }: PublishToGitHubDia
setVisibility(value === 'public' ? 'public' : 'private')
}
className="sm:flex"
aria-label={t`Visibility`}
aria-labelledby="publish-visibility-label"
>
<FieldLabel htmlFor="publish-visibility-private">
<Field orientation="horizontal">
Expand Down
24 changes: 24 additions & 0 deletions packages/app/src/lib/share/publish-wizard.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { describe, expect, mock, test } from 'bun:test';
import type { SharePublishOwner } from '@inkeep/open-knowledge-core';
import {
buildSamlSsoAuthorizeUrl,
canSubmitPublish,
extractFolderBasename,
fetchPublishNameCheck,
fetchPublishOwners,
pickDefaultOwner,
presentPublishError,
resolveNameCheckStatus,
sanitizeRepoName,
Expand Down Expand Up @@ -61,6 +63,28 @@ describe('extractFolderBasename', () => {
});
});

describe('pickDefaultOwner', () => {
const user: SharePublishOwner = { login: 'alice', kind: 'user' };
const orgA: SharePublishOwner = { login: 'docs-team', kind: 'org' };
const orgB: SharePublishOwner = { login: 'platform', kind: 'org' };

test('prefers the first org over the user account', () => {
expect(pickDefaultOwner([user, orgA, orgB])).toBe('docs-team');
});

test('falls back to the user account when there is no org', () => {
expect(pickDefaultOwner([user])).toBe('alice');
});

test('returns the first entry when somehow ordered with no org', () => {
expect(pickDefaultOwner([orgA, orgB])).toBe('docs-team');
});

test('empty list yields empty string', () => {
expect(pickDefaultOwner([])).toBe('');
});
});

describe('buildSamlSsoAuthorizeUrl', () => {
test('builds the GitHub org-policies-applications URL', () => {
expect(buildSamlSsoAuthorizeUrl('inkeep')).toBe(
Expand Down
5 changes: 5 additions & 0 deletions packages/app/src/lib/share/publish-wizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ export function sanitizeRepoName(input: string): string {

export { extractFolderBasename } from '@/lib/path-utils';

export function pickDefaultOwner(owners: SharePublishOwner[]): string {
const firstOrg = owners.find((o) => o.kind === 'org');
return firstOrg?.login ?? owners[0]?.login ?? '';
}

export function buildSamlSsoAuthorizeUrl(orgLogin: string): string {
return `https://github.com/orgs/${encodeURIComponent(orgLogin)}/policies/applications`;
}
Expand Down
3 changes: 0 additions & 3 deletions packages/app/src/locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,6 @@
"8TLE9g": [
"Read-only copy of a MirrorSource block from another doc. Edit at the source — every Mirror reflects the change live."
],
"8XQ7Ib": ["Pick an owner"],
"8XQRON": ["Loading repositories"],
"8_brI5": ["Name is required"],
"8cS0nH": ["Config sharing unavailable: ", ["0"], "."],
Expand Down Expand Up @@ -1114,7 +1113,6 @@
"."
],
"_K2CvV": ["Template"],
"_N2eSU": ["org"],
"_PACVa": ["Release notes"],
"_QwkMu": ["No matching paths"],
"_TDESN": ["Network error - ", ["kind"], " \"", ["createdPath"], "\" still exists on disk"],
Expand Down Expand Up @@ -1156,7 +1154,6 @@
"aiArms": ["We couldn't clone this repository."],
"ajz9F8": ["Couldn't load conflict content for ", ["filePath"], ". Try reloading the page."],
"aoa6xA": ["network error (is `ok ui` running?)"],
"av_PD9": ["you"],
"b3Thhd": ["Upload failed"],
"b4AQFK": ["Open activity panel for ", ["tooltipName"], ", editing ", ["realCurrentDoc"]],
"b4itZn": ["Working"],
Expand Down
12 changes: 0 additions & 12 deletions packages/app/src/locales/en/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -4071,10 +4071,6 @@ msgstr "Ordered list"
msgid "Ordered List"
msgstr "Ordered List"

#: src/components/PublishToGitHubDialog.tsx
msgid "org"
msgstr "org"

#: src/components/GraphPanel.tsx
msgid "Orphan mode"
msgstr "Orphan mode"
Expand Down Expand Up @@ -4246,10 +4242,6 @@ msgstr "Pick a name"
msgid "Pick a starter pack to scaffold folders, templates, and AI-readable rules."
msgstr "Pick a starter pack to scaffold folders, templates, and AI-readable rules."

#: src/components/PublishToGitHubDialog.tsx
msgid "Pick an owner"
msgstr "Pick an owner"

#: src/components/ShareReceiveDialog.tsx
msgid "Pick the folder where you've cloned it."
msgstr "Pick the folder where you've cloned it."
Expand Down Expand Up @@ -6159,10 +6151,6 @@ msgstr "writing"
msgid "yesterday"
msgstr "yesterday"

#: src/components/PublishToGitHubDialog.tsx
msgid "you"
msgstr "you"

#: src/components/TerminalGate.tsx
msgid "You can also manage this in Settings."
msgstr "You can also manage this in Settings."
Expand Down
3 changes: 0 additions & 3 deletions packages/app/src/locales/pseudo/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,6 @@
"8TLE9g": [
"Ŕēàď-ōńĺŷ ćōƥŷ ōƒ à ḾĩŕŕōŕŚōũŕćē ƀĺōćķ ƒŕōḿ àńōţĥēŕ ďōć. Ēďĩţ àţ ţĥē śōũŕćē — ēvēŕŷ Ḿĩŕŕōŕ ŕēƒĺēćţś ţĥē ćĥàńĝē ĺĩvē."
],
"8XQ7Ib": ["Ƥĩćķ àń ōŵńēŕ"],
"8XQRON": ["Ĺōàďĩńĝ ŕēƥōśĩţōŕĩēś"],
"8_brI5": ["Ńàḿē ĩś ŕēǫũĩŕēď"],
"8cS0nH": ["Ćōńƒĩĝ śĥàŕĩńĝ ũńàvàĩĺàƀĺē: ", ["0"], "."],
Expand Down Expand Up @@ -1114,7 +1113,6 @@
"."
],
"_K2CvV": ["Ţēḿƥĺàţē"],
"_N2eSU": ["ōŕĝ"],
"_PACVa": ["Ŕēĺēàśē ńōţēś"],
"_QwkMu": ["Ńō ḿàţćĥĩńĝ ƥàţĥś"],
"_TDESN": ["Ńēţŵōŕķ ēŕŕōŕ - ", ["kind"], " \"", ["createdPath"], "\" śţĩĺĺ ēxĩśţś ōń ďĩśķ"],
Expand Down Expand Up @@ -1156,7 +1154,6 @@
"aiArms": ["Ŵē ćōũĺďń'ţ ćĺōńē ţĥĩś ŕēƥōśĩţōŕŷ."],
"ajz9F8": ["Ćōũĺďń'ţ ĺōàď ćōńƒĺĩćţ ćōńţēńţ ƒōŕ ", ["filePath"], ". Ţŕŷ ŕēĺōàďĩńĝ ţĥē ƥàĝē."],
"aoa6xA": ["ńēţŵōŕķ ēŕŕōŕ (ĩś `ōķ ũĩ` ŕũńńĩńĝ?)"],
"av_PD9": ["ŷōũ"],
"b3Thhd": ["Ũƥĺōàď ƒàĩĺēď"],
"b4AQFK": ["Ōƥēń àćţĩvĩţŷ ƥàńēĺ ƒōŕ ", ["tooltipName"], ", ēďĩţĩńĝ ", ["realCurrentDoc"]],
"b4itZn": ["Ŵōŕķĩńĝ"],
Expand Down
12 changes: 0 additions & 12 deletions packages/app/src/locales/pseudo/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -4066,10 +4066,6 @@ msgstr ""
msgid "Ordered List"
msgstr ""

#: src/components/PublishToGitHubDialog.tsx
msgid "org"
msgstr ""

#: src/components/GraphPanel.tsx
msgid "Orphan mode"
msgstr ""
Expand Down Expand Up @@ -4241,10 +4237,6 @@ msgstr ""
msgid "Pick a starter pack to scaffold folders, templates, and AI-readable rules."
msgstr ""

#: src/components/PublishToGitHubDialog.tsx
msgid "Pick an owner"
msgstr ""

#: src/components/ShareReceiveDialog.tsx
msgid "Pick the folder where you've cloned it."
msgstr ""
Expand Down Expand Up @@ -6154,10 +6146,6 @@ msgstr ""
msgid "yesterday"
msgstr ""

#: src/components/PublishToGitHubDialog.tsx
msgid "you"
msgstr ""

#: src/components/TerminalGate.tsx
msgid "You can also manage this in Settings."
msgstr ""
Expand Down