| author | |
| committer | |
| log | a4c8745abee3f73f960c914800974dee530dbc3b |
| tree | f1ada9ba9f459c155b6b3ccaf9dd67f7215ebd0c |
| parent | 9b40d3746388e573370488f73b50e1e869b79a45 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>13 files changed, 257 insertions(+), 67 deletions(-)
apps/mobile/src/features/projects/AddProjectScreen.tsx+52-17| ... | @@ -15,9 +15,10 @@ import { | ... | @@ -15,9 +15,10 @@ import { |
| 15 | getCloneDestinationPath, | 15 | getCloneDestinationPath, |
| 16 | getCloneDirectoryName, | 16 | getCloneDirectoryName, |
| 17 | getDefaultCloneUrl, | 17 | getDefaultCloneUrl, |
| 18 | getNewProjectGitHubRepository, | ||
| 19 | getNewProjectGitHubTarget, | ||
| 20 | getNewProjectPathPreview, | 18 | getNewProjectPathPreview, |
| 19 | getNewProjectPublishTarget, | ||
| 20 | getNewProjectPublishTargets, | ||
| 21 | getNewProjectRepository, | ||
| 21 | normalizePastedCloneUrl, | 22 | normalizePastedCloneUrl, |
| 22 | parseAddProjectRemoteSource, | 23 | parseAddProjectRemoteSource, |
| 23 | resolveAddProjectPath, | 24 | resolveAddProjectPath, |
| ... | @@ -995,9 +996,12 @@ export function AddProjectNewScreen(props: { readonly environmentId?: string | s | ... | @@ -995,9 +996,12 @@ export function AddProjectNewScreen(props: { readonly environmentId?: string | s |
| 995 | input: {}, | 996 | input: {}, |
| 996 | }), | 997 | }), |
| 997 | ); | 998 | ); |
| 998 | const githubTarget = getNewProjectGitHubTarget(discoveryState.data); | 999 | const publishTargets = getNewProjectPublishTargets(discoveryState.data); |
| 999 | const [name, setName] = useState(""); | 1000 | const [name, setName] = useState(""); |
| 1000 | const [publishesToGitHub, setPublishesToGitHub] = useState(false); | 1001 | const [publishes, setPublishes] = useState(false); |
| 1002 | // Null takes the first ready host, GitHub when it is ready. | ||
| 1003 | const [publishKind, setPublishKind] = useState<SourceControlProviderKind | null>(null); | ||
| 1004 | const publishTarget = getNewProjectPublishTarget(publishTargets, publishKind); | ||
| 1001 | const [isSubmitting, setIsSubmitting] = useState(false); | 1005 | const [isSubmitting, setIsSubmitting] = useState(false); |
| 1002 | const [error, setError] = useState<string | null>(null); | 1006 | const [error, setError] = useState<string | null>(null); |
| 1003 | const trimmedName = name.trim(); | 1007 | const trimmedName = name.trim(); |
| ... | @@ -1066,19 +1070,19 @@ export function AddProjectNewScreen(props: { readonly environmentId?: string | s | ... | @@ -1066,19 +1070,19 @@ export function AddProjectNewScreen(props: { readonly environmentId?: string | s |
| 1066 | if (commitError !== undefined) { | 1070 | if (commitError !== undefined) { |
| 1067 | Alert.alert("Created without a first commit", commitError); | 1071 | Alert.alert("Created without a first commit", commitError); |
| 1068 | } | 1072 | } |
| 1069 | if (publishesToGitHub && githubTarget !== null) { | 1073 | if (publishes && publishTarget !== null) { |
| 1070 | void publishRepository({ | 1074 | void publishRepository({ |
| 1071 | environmentId: environment.environmentId, | 1075 | environmentId: environment.environmentId, |
| 1072 | input: { | 1076 | input: { |
| 1073 | cwd: workspaceRoot, | 1077 | cwd: workspaceRoot, |
| 1074 | provider: SourceControlProviderKind.make("github"), | 1078 | provider: publishTarget.definition.kind, |
| 1075 | repository: getNewProjectGitHubRepository(githubTarget, workspaceRoot), | 1079 | repository: getNewProjectRepository(publishTarget, workspaceRoot), |
| 1076 | visibility: "private", | 1080 | visibility: "private", |
| 1077 | }, | 1081 | }, |
| 1078 | }).then((publishResult) => { | 1082 | }).then((publishResult) => { |
| 1079 | if (AsyncResult.isFailure(publishResult)) { | 1083 | if (AsyncResult.isFailure(publishResult)) { |
| 1080 | Alert.alert( | 1084 | Alert.alert( |
| 1081 | "Could not create the GitHub repository", | 1085 | `Could not create the ${publishTarget.definition.label} repository`, |
| 1082 | errorMessage(Cause.squash(publishResult.cause)), | 1086 | errorMessage(Cause.squash(publishResult.cause)), |
| 1083 | ); | 1087 | ); |
| 1084 | } | 1088 | } |
| ... | @@ -1133,18 +1137,18 @@ export function AddProjectNewScreen(props: { readonly environmentId?: string | s | ... | @@ -1133,18 +1137,18 @@ export function AddProjectNewScreen(props: { readonly environmentId?: string | s |
| 1133 | </Text> | 1137 | </Text> |
| 1134 | ) : null} | 1138 | ) : null} |
| 1135 | {machineRows} | 1139 | {machineRows} |
| 1136 | {githubTarget !== null ? ( | 1140 | {publishTarget !== null ? ( |
| 1137 | <ListSection> | 1141 | <ListSection> |
| 1138 | <ListRow | 1142 | <ListRow |
| 1139 | title="Create private repository on GitHub" | 1143 | title={`Create private repository on ${publishTarget.definition.label}`} |
| 1140 | subtitle={ | 1144 | subtitle={ |
| 1141 | trimmedName.length > 0 && pathPreview !== null | 1145 | trimmedName.length > 0 && pathPreview !== null |
| 1142 | ? getNewProjectGitHubRepository(githubTarget, pathPreview) | 1146 | ? getNewProjectRepository(publishTarget, pathPreview) |
| 1143 | : githubTarget.account | 1147 | : publishTarget.owner |
| 1144 | } | 1148 | } |
| 1145 | icon={ | 1149 | icon={ |
| 1146 | <SourceControlIcon | 1150 | <SourceControlIcon |
| 1147 | kind={sourceControlClients.get("github").icon} | 1151 | kind={publishTarget.definition.icon} |
| 1148 | size={Platform.OS === "android" ? 24 : 18} | 1152 | size={Platform.OS === "android" ? 24 : 18} |
| 1149 | colorClassName="accent-icon" | 1153 | colorClassName="accent-icon" |
| 1150 | /> | 1154 | /> |
| ... | @@ -1152,13 +1156,44 @@ export function AddProjectNewScreen(props: { readonly environmentId?: string | s | ... | @@ -1152,13 +1156,44 @@ export function AddProjectNewScreen(props: { readonly environmentId?: string | s |
| 1152 | isFirst | 1156 | isFirst |
| 1153 | right={ | 1157 | right={ |
| 1154 | <ThemedSwitch | 1158 | <ThemedSwitch |
| 1155 | accessibilityLabel="Create private repository on GitHub" | 1159 | accessibilityLabel={`Create private repository on ${publishTarget.definition.label}`} |
| 1156 | value={publishesToGitHub} | 1160 | value={publishes} |
| 1157 | onValueChange={setPublishesToGitHub} | 1161 | onValueChange={setPublishes} |
| 1158 | /> | 1162 | /> |
| 1159 | } | 1163 | } |
| 1160 | onPress={() => setPublishesToGitHub((publishes) => !publishes)} | 1164 | onPress={() => setPublishes((current) => !current)} |
| 1161 | /> | 1165 | /> |
| 1166 | {publishes && publishTargets.length > 1 | ||
| 1167 | ? publishTargets.map((target) => { | ||
| 1168 | const selected = target.definition.kind === publishTarget.definition.kind; | ||
| 1169 | return ( | ||
| 1170 | <ListRow | ||
| 1171 | key={target.definition.kind} | ||
| 1172 | title={target.definition.label} | ||
| 1173 | icon={ | ||
| 1174 | <SourceControlIcon | ||
| 1175 | kind={target.definition.icon} | ||
| 1176 | size={Platform.OS === "android" ? 24 : 17} | ||
| 1177 | colorClassName="accent-icon" | ||
| 1178 | /> | ||
| 1179 | } | ||
| 1180 | selected={selected} | ||
| 1181 | disabled={isSubmitting} | ||
| 1182 | right={ | ||
| 1183 | selected ? ( | ||
| 1184 | <SymbolView | ||
| 1185 | name="checkmark" | ||
| 1186 | size={Platform.OS === "android" ? 20 : 14} | ||
| 1187 | tintColorClassName="accent-icon" | ||
| 1188 | type="monochrome" | ||
| 1189 | /> | ||
| 1190 | ) : null | ||
| 1191 | } | ||
| 1192 | onPress={() => setPublishKind(target.definition.kind)} | ||
| 1193 | /> | ||
| 1194 | ); | ||
| 1195 | }) | ||
| 1196 | : null} | ||
| 1162 | </ListSection> | 1197 | </ListSection> |
| 1163 | ) : null} | 1198 | ) : null} |
| 1164 | <PrimaryActionButton | 1199 | <PrimaryActionButton |
apps/web/src/components/CommandPalette.tsx+97-19| ... | @@ -10,9 +10,11 @@ import { | ... | @@ -10,9 +10,11 @@ import { |
| 10 | getCloneDestinationPath, | 10 | getCloneDestinationPath, |
| 11 | getCloneDirectoryName, | 11 | getCloneDirectoryName, |
| 12 | getDefaultCloneUrl, | 12 | getDefaultCloneUrl, |
| 13 | getNewProjectGitHubRepository, | ||
| 14 | getNewProjectGitHubTarget, | ||
| 15 | getNewProjectPathPreview, | 13 | getNewProjectPathPreview, |
| 14 | getNewProjectPublishTarget, | ||
| 15 | getNewProjectPublishTargets, | ||
| 16 | getNewProjectRepository, | ||
| 17 | type NewProjectPublishTarget, | ||
| 16 | normalizePastedCloneUrl, | 18 | normalizePastedCloneUrl, |
| 17 | addProjectRemoteSourceLabel, | 19 | addProjectRemoteSourceLabel, |
| 18 | addProjectRemoteSourcePathHint, | 20 | addProjectRemoteSourcePathHint, |
| ... | @@ -195,7 +197,6 @@ import { | ... | @@ -195,7 +197,6 @@ import { |
| 195 | CommandPaletteVirtualizedResults, | 197 | CommandPaletteVirtualizedResults, |
| 196 | scrollCommandPaletteRowIntoView, | 198 | scrollCommandPaletteRowIntoView, |
| 197 | } from "./CommandPaletteResults"; | 199 | } from "./CommandPaletteResults"; |
| 198 | import { GitHubIcon } from "./Icons"; | ||
| 199 | import { sourceControlIcon } from "~/sourceControlPresentation"; | 200 | import { sourceControlIcon } from "~/sourceControlPresentation"; |
| 200 | import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon"; | 201 | import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon"; |
| 201 | import { Checkbox } from "./ui/checkbox"; | 202 | import { Checkbox } from "./ui/checkbox"; |
| ... | @@ -221,6 +222,7 @@ import { resolveShortcutCommand, threadJumpIndexFromCommand } from "../keybindin | ... | @@ -221,6 +222,7 @@ import { resolveShortcutCommand, threadJumpIndexFromCommand } from "../keybindin |
| 221 | import { CommandDialog, CommandDialogPopup, CommandFooterAction } from "./ui/command"; | 222 | import { CommandDialog, CommandDialogPopup, CommandFooterAction } from "./ui/command"; |
| 222 | import { Button } from "./ui/button"; | 223 | import { Button } from "./ui/button"; |
| 223 | import { Kbd, KbdGroup } from "./ui/kbd"; | 224 | import { Kbd, KbdGroup } from "./ui/kbd"; |
| 225 | import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "./ui/select"; | ||
| 224 | import { stackedThreadToast, toastManager } from "./ui/toast"; | 226 | import { stackedThreadToast, toastManager } from "./ui/toast"; |
| 225 | import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; | 227 | import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; |
| 226 | import { ComposerHandleContext, useComposerHandleContext } from "../composerHandleContext"; | 228 | import { ComposerHandleContext, useComposerHandleContext } from "../composerHandleContext"; |
| ... | @@ -280,6 +282,14 @@ function remoteProjectSourceIcon(source: AddProjectRemoteSource, className: stri | ... | @@ -280,6 +282,14 @@ function remoteProjectSourceIcon(source: AddProjectRemoteSource, className: stri |
| 280 | return <Icon className={className} />; | 282 | return <Icon className={className} />; |
| 281 | } | 283 | } |
| 282 | 284 | ||
| 285 | function sourceControlHostIcon( | ||
| 286 | definition: NewProjectPublishTarget["definition"], | ||
| 287 | className: string, | ||
| 288 | ): ReactNode { | ||
| 289 | const Icon = sourceControlIcon(definition); | ||
| 290 | return <Icon className={className} />; | ||
| 291 | } | ||
| 292 | |||
| 283 | function projectFaviconIcon(project: Project): ReactNode { | 293 | function projectFaviconIcon(project: Project): ReactNode { |
| 284 | return <ProjectFavicon project={project} className={ITEM_ICON_CLASS} />; | 294 | return <ProjectFavicon project={project} className={ITEM_ICON_CLASS} />; |
| 285 | } | 295 | } |
| ... | @@ -762,7 +772,11 @@ function OpenCommandPaletteDialog(props: { | ... | @@ -762,7 +772,11 @@ function OpenCommandPaletteDialog(props: { |
| 762 | /** Machine of the Add project sources view under this step; null from the palette root. */ | 772 | /** Machine of the Add project sources view under this step; null from the palette root. */ |
| 763 | readonly sourcesEnvironmentId: EnvironmentId | null; | 773 | readonly sourcesEnvironmentId: EnvironmentId | null; |
| 764 | } | null>(null); | 774 | } | null>(null); |
| 765 | const [newProjectPublishesToGitHub, setNewProjectPublishesToGitHub] = useState(false); | 775 | const [newProjectPublishes, setNewProjectPublishes] = useState(false); |
| 776 | // The host picked beside the publish toggle; null takes the first ready host, GitHub when ready. | ||
| 777 | const [newProjectPublishKind, setNewProjectPublishKind] = | ||
| 778 | useState<SourceControlProviderKind | null>(null); | ||
| 779 | const [newProjectHostPickerOpen, setNewProjectHostPickerOpen] = useState(false); | ||
| 766 | const [isCreatingNewProject, setIsCreatingNewProject] = useState(false); | 780 | const [isCreatingNewProject, setIsCreatingNewProject] = useState(false); |
| 767 | // State lags a render behind, so a repeated Enter could start a second create. | 781 | // State lags a render behind, so a repeated Enter could start a second create. |
| 768 | const newProjectSubmittingRef = useRef(false); | 782 | const newProjectSubmittingRef = useRef(false); |
| ... | @@ -1482,7 +1496,8 @@ function OpenCommandPaletteDialog(props: { | ... | @@ -1482,7 +1496,8 @@ function OpenCommandPaletteDialog(props: { |
| 1482 | setAddProjectEnvironmentId(environmentId); | 1496 | setAddProjectEnvironmentId(environmentId); |
| 1483 | setAddProjectCloneFlow(null); | 1497 | setAddProjectCloneFlow(null); |
| 1484 | setNewProjectFlow({ environmentId, sourcesEnvironmentId }); | 1498 | setNewProjectFlow({ environmentId, sourcesEnvironmentId }); |
| 1485 | setNewProjectPublishesToGitHub(false); | 1499 | setNewProjectPublishes(false); |
| 1500 | setNewProjectPublishKind(null); | ||
| 1486 | pushPaletteView({ | 1501 | pushPaletteView({ |
| 1487 | addonIcon: <FolderGit2Icon className={ADDON_ICON_CLASS} />, | 1502 | addonIcon: <FolderGit2Icon className={ADDON_ICON_CLASS} />, |
| 1488 | groups: [], | 1503 | groups: [], |
| ... | @@ -2417,8 +2432,12 @@ function OpenCommandPaletteDialog(props: { | ... | @@ -2417,8 +2432,12 @@ function OpenCommandPaletteDialog(props: { |
| 2417 | ], | 2432 | ], |
| 2418 | ); | 2433 | ); |
| 2419 | 2434 | ||
| 2420 | const newProjectGitHubTarget = | 2435 | const newProjectPublishTargets = |
| 2421 | newProjectFlow === null ? null : getNewProjectGitHubTarget(sourceControlDiscovery.data ?? null); | 2436 | newProjectFlow === null ? [] : getNewProjectPublishTargets(sourceControlDiscovery.data ?? null); |
| 2437 | const newProjectPublishTarget = getNewProjectPublishTarget( | ||
| 2438 | newProjectPublishTargets, | ||
| 2439 | newProjectPublishKind, | ||
| 2440 | ); | ||
| 2422 | const newProjectName = query.trim(); | 2441 | const newProjectName = query.trim(); |
| 2423 | const canSubmitNewProject = | 2442 | const canSubmitNewProject = |
| 2424 | newProjectFlow !== null && | 2443 | newProjectFlow !== null && |
| ... | @@ -2436,7 +2455,7 @@ function OpenCommandPaletteDialog(props: { | ... | @@ -2436,7 +2455,7 @@ function OpenCommandPaletteDialog(props: { |
| 2436 | const created = await createNewProject({ | 2455 | const created = await createNewProject({ |
| 2437 | environmentId: newProjectFlow.environmentId, | 2456 | environmentId: newProjectFlow.environmentId, |
| 2438 | name: newProjectName, | 2457 | name: newProjectName, |
| 2439 | github: newProjectPublishesToGitHub ? newProjectGitHubTarget : null, | 2458 | publishTo: newProjectPublishes ? newProjectPublishTarget : null, |
| 2440 | }); | 2459 | }); |
| 2441 | if (created) setOpen(false); | 2460 | if (created) setOpen(false); |
| 2442 | } finally { | 2461 | } finally { |
| ... | @@ -2742,7 +2761,7 @@ function OpenCommandPaletteDialog(props: { | ... | @@ -2742,7 +2761,7 @@ function OpenCommandPaletteDialog(props: { |
| 2742 | const newProjectsRoot = newProjectFlow ? newProjectsRootFor(newProjectFlow.environmentId) : null; | 2761 | const newProjectsRoot = newProjectFlow ? newProjectsRootFor(newProjectFlow.environmentId) : null; |
| 2743 | const newProjectPathPreview = | 2762 | const newProjectPathPreview = |
| 2744 | newProjectsRoot === null ? null : getNewProjectPathPreview(newProjectsRoot, newProjectName); | 2763 | newProjectsRoot === null ? null : getNewProjectPathPreview(newProjectsRoot, newProjectName); |
| 2745 | const newProjectGitHubToggleValue = "new-project:github"; | 2764 | const newProjectPublishToggleValue = "new-project:publish"; |
| 2746 | // The name step's way out to folders and clones, for the selected machine. | 2765 | // The name step's way out to folders and clones, for the selected machine. |
| 2747 | // It replaces the name step (and a sources view for another machine under | 2766 | // It replaces the name step (and a sources view for another machine under |
| 2748 | // it), so Back returns to wherever New project was opened from. | 2767 | // it), so Back returns to wherever New project was opened from. |
| ... | @@ -2829,7 +2848,7 @@ function OpenCommandPaletteDialog(props: { | ... | @@ -2829,7 +2848,7 @@ function OpenCommandPaletteDialog(props: { |
| 2829 | ], | 2848 | ], |
| 2830 | }; | 2849 | }; |
| 2831 | const newProjectOptionGroups: CommandPaletteView["groups"] = | 2850 | const newProjectOptionGroups: CommandPaletteView["groups"] = |
| 2832 | newProjectGitHubTarget === null || newProjectPathPreview === null | 2851 | newProjectPublishTarget === null || newProjectPathPreview === null |
| 2833 | ? [] | 2852 | ? [] |
| 2834 | : [ | 2853 | : [ |
| 2835 | { | 2854 | { |
| ... | @@ -2838,22 +2857,81 @@ function OpenCommandPaletteDialog(props: { | ... | @@ -2838,22 +2857,81 @@ function OpenCommandPaletteDialog(props: { |
| 2838 | items: [ | 2857 | items: [ |
| 2839 | { | 2858 | { |
| 2840 | kind: "action", | 2859 | kind: "action", |
| 2841 | value: newProjectGitHubToggleValue, | 2860 | value: newProjectPublishToggleValue, |
| 2842 | searchTerms: [], | 2861 | searchTerms: [], |
| 2843 | title: "Create private repository on GitHub", | 2862 | title: |
| 2863 | newProjectPublishTargets.length > 1 | ||
| 2864 | ? "Create private repository" | ||
| 2865 | : `Create private repository on ${newProjectPublishTarget.definition.label}`, | ||
| 2844 | description: | 2866 | description: |
| 2845 | newProjectName.length > 0 | 2867 | newProjectName.length > 0 |
| 2846 | ? getNewProjectGitHubRepository(newProjectGitHubTarget, newProjectPathPreview) | 2868 | ? getNewProjectRepository(newProjectPublishTarget, newProjectPathPreview) |
| 2847 | : (newProjectGitHubTarget.account ?? "Your GitHub account"), | 2869 | : (newProjectPublishTarget.owner ?? |
| 2848 | icon: <GitHubIcon className={ITEM_ICON_CLASS} />, | 2870 | `Your ${newProjectPublishTarget.definition.label} account`), |
| 2871 | icon: sourceControlHostIcon(newProjectPublishTarget.definition, ITEM_ICON_CLASS), | ||
| 2849 | titleTrailingContent: ( | 2872 | titleTrailingContent: ( |
| 2850 | <span className="pointer-events-none ms-auto flex"> | 2873 | <span className="ms-auto flex items-center gap-2"> |
| 2851 | <Checkbox checked={newProjectPublishesToGitHub} tabIndex={-1} aria-hidden /> | 2874 | {newProjectPublishTargets.length > 1 ? ( |
| 2875 | // The row toggles on click, so the picker keeps its clicks to itself. The | ||
| 2876 | // row also cancels pointerdown to keep focus in the input, which drops the | ||
| 2877 | // mousedown Select opens on, so the picker opens on click instead. | ||
| 2878 | <span | ||
| 2879 | className="flex" | ||
| 2880 | onClick={(event) => { | ||
| 2881 | event.stopPropagation(); | ||
| 2882 | // React bubbles the popup's clicks here through its portal; only the | ||
| 2883 | // trigger opens the picker. | ||
| 2884 | if (!event.currentTarget.contains(event.target as Node)) return; | ||
| 2885 | if (!isCreatingNewProject) setNewProjectHostPickerOpen(true); | ||
| 2886 | }} | ||
| 2887 | > | ||
| 2888 | <Select | ||
| 2889 | disabled={isCreatingNewProject} | ||
| 2890 | open={newProjectHostPickerOpen} | ||
| 2891 | onOpenChange={setNewProjectHostPickerOpen} | ||
| 2892 | value={newProjectPublishTarget.definition.kind} | ||
| 2893 | onValueChange={(kind) => { | ||
| 2894 | if (kind !== null) setNewProjectPublishKind(kind); | ||
| 2895 | }} | ||
| 2896 | > | ||
| 2897 | <SelectTrigger | ||
| 2898 | size="xs" | ||
| 2899 | variant="ghost" | ||
| 2900 | aria-label="Host for the new repository" | ||
| 2901 | > | ||
| 2902 | <SelectValue> | ||
| 2903 | {(kind: SourceControlProviderKind) => | ||
| 2904 | sourceControlClients.get(kind).label | ||
| 2905 | } | ||
| 2906 | </SelectValue> | ||
| 2907 | </SelectTrigger> | ||
| 2908 | <SelectPopup align="end" alignItemWithTrigger={false}> | ||
| 2909 | {newProjectPublishTargets.map((target) => ( | ||
| 2910 | <SelectItem | ||
| 2911 | key={target.definition.kind} | ||
| 2912 | hideIndicator | ||
| 2913 | value={target.definition.kind} | ||
| 2914 | > | ||
| 2915 | <span className="inline-flex items-center gap-1.5"> | ||
| 2916 | {sourceControlHostIcon(target.definition, "size-3.5")} | ||
| 2917 | {target.definition.label} | ||
| 2918 | </span> | ||
| 2919 | </SelectItem> | ||
| 2920 | ))} | ||
| 2921 | </SelectPopup> | ||
| 2922 | </Select> | ||
| 2923 | </span> | ||
| 2924 | ) : null} | ||
| 2925 | <span className="pointer-events-none flex"> | ||
| 2926 | <Checkbox checked={newProjectPublishes} tabIndex={-1} aria-hidden /> | ||
| 2927 | </span> | ||
| 2852 | </span> | 2928 | </span> |
| 2853 | ), | 2929 | ), |
| 2930 | // The create in flight keeps the choice it started with. | ||
| 2931 | ...(isCreatingNewProject ? { disabled: true } : {}), | ||
| 2854 | keepOpen: true, | 2932 | keepOpen: true, |
| 2855 | run: async () => { | 2933 | run: async () => { |
| 2856 | setNewProjectPublishesToGitHub((publishes) => !publishes); | 2934 | setNewProjectPublishes((publishes) => !publishes); |
| 2857 | }, | 2935 | }, |
| 2858 | }, | 2936 | }, |
| 2859 | ], | 2937 | ], |
| ... | @@ -3327,7 +3405,7 @@ function OpenCommandPaletteDialog(props: { | ... | @@ -3327,7 +3405,7 @@ function OpenCommandPaletteDialog(props: { |
| 3327 | newProjectFlow !== null | 3405 | newProjectFlow !== null |
| 3328 | ? highlightedItemValue === null | 3406 | ? highlightedItemValue === null |
| 3329 | ? "Create" | 3407 | ? "Create" |
| 3330 | : highlightedItemValue === newProjectGitHubToggleValue | 3408 | : highlightedItemValue === newProjectPublishToggleValue |
| 3331 | ? "Toggle" | 3409 | ? "Toggle" |
| 3332 | : "Select" | 3410 | : "Select" |
| 3333 | : addProjectCloneFlow?.step === "repository" | 3411 | : addProjectCloneFlow?.step === "repository" |
apps/web/src/components/ui/checkbox.tsx+4-3| ... | @@ -14,7 +14,8 @@ function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) { | ... | @@ -14,7 +14,8 @@ function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) { |
| 14 | data-slot="checkbox" | 14 | data-slot="checkbox" |
| 15 | {...props} | 15 | {...props} |
| 16 | > | 16 | > |
| 17 | {/* Leave a contrasting inner gap inside the 2px focus outline. */} | 17 | {/* Leave a contrasting inner gap inside the 2px focus outline. The glyphs name their color so |
| 18 | rows that mute stray icons, such as list items, leave the indicator's color alone. */} | ||
| 18 | <CheckboxPrimitive.Indicator | 19 | <CheckboxPrimitive.Indicator |
| 19 | className="-inset-px absolute flex items-center justify-center rounded-[.25rem] text-primary-foreground data-unchecked:hidden data-checked:bg-primary data-indeterminate:text-foreground in-[[data-slot=checkbox]:focus-visible]:inset-shadow-[0_0_0_3px_var(--background)] in-[[data-slot=checkbox]:focus-visible]:outline-2 in-[[data-slot=checkbox]:focus-visible]:outline-solid in-[[data-slot=checkbox]:focus-visible]:-outline-offset-2 in-[[data-slot=checkbox]:focus-visible]:outline-inherit" | 20 | className="-inset-px absolute flex items-center justify-center rounded-[.25rem] text-primary-foreground data-unchecked:hidden data-checked:bg-primary data-indeterminate:text-foreground in-[[data-slot=checkbox]:focus-visible]:inset-shadow-[0_0_0_3px_var(--background)] in-[[data-slot=checkbox]:focus-visible]:outline-2 in-[[data-slot=checkbox]:focus-visible]:outline-solid in-[[data-slot=checkbox]:focus-visible]:-outline-offset-2 in-[[data-slot=checkbox]:focus-visible]:outline-inherit" |
| 20 | data-slot="checkbox-indicator" | 21 | data-slot="checkbox-indicator" |
| ... | @@ -22,7 +23,7 @@ function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) { | ... | @@ -22,7 +23,7 @@ function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) { |
| 22 | <span {...props}> | 23 | <span {...props}> |
| 23 | {state.indeterminate ? ( | 24 | {state.indeterminate ? ( |
| 24 | <svg | 25 | <svg |
| 25 | className="size-3.5 sm:size-3" | 26 | className="size-3.5 text-current sm:size-3" |
| 26 | fill="none" | 27 | fill="none" |
| 27 | height="24" | 28 | height="24" |
| 28 | stroke="currentColor" | 29 | stroke="currentColor" |
| ... | @@ -37,7 +38,7 @@ function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) { | ... | @@ -37,7 +38,7 @@ function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) { |
| 37 | </svg> | 38 | </svg> |
| 38 | ) : ( | 39 | ) : ( |
| 39 | <svg | 40 | <svg |
| 40 | className="size-3.5 sm:size-3" | 41 | className="size-3.5 text-current sm:size-3" |
| 41 | fill="none" | 42 | fill="none" |
| 42 | height="24" | 43 | height="24" |
| 43 | stroke="currentColor" | 44 | stroke="currentColor" |
apps/web/src/hooks/useNewProject.ts+18-15| ... | @@ -1,6 +1,8 @@ | ... | @@ -1,6 +1,8 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 2 | import { scopeProjectRef } from "@t3tools/client-runtime/environment"; | 1 | import { scopeProjectRef } from "@t3tools/client-runtime/environment"; |
| 3 | import { getNewProjectGitHubRepository } from "@t3tools/client-runtime/operations/projects"; | 2 | import { |
| 3 | getNewProjectRepository, | ||
| 4 | type NewProjectPublishTarget, | ||
| 5 | } from "@t3tools/client-runtime/operations/projects"; | ||
| 4 | import { | 6 | import { |
| 5 | isAtomCommandInterrupted, | 7 | isAtomCommandInterrupted, |
| 6 | squashAtomCommandFailure, | 8 | squashAtomCommandFailure, |
| ... | @@ -24,8 +26,8 @@ function errorMessage(error: unknown): string { | ... | @@ -24,8 +26,8 @@ function errorMessage(error: unknown): string { |
| 24 | /** | 26 | /** |
| 25 | * Starts a project from just a name. The server makes a folder under its | 27 | * Starts a project from just a name. The server makes a folder under its |
| 26 | * `newProjectsRoot` with a README, an icon, and a first commit; this then | 28 | * `newProjectsRoot` with a README, an icon, and a first commit; this then |
| 27 | * opens a new thread draft in it. With `github`, it also publishes the | 29 | * opens a new thread draft in it. With `publishTo`, it also publishes the |
| 28 | * repository as private, without holding up the draft. | 30 | * repository as private on that host, without holding up the draft. |
| 29 | * | 31 | * |
| 30 | * Resolves to whether the project was created. | 32 | * Resolves to whether the project was created. |
| 31 | */ | 33 | */ |
| ... | @@ -36,18 +38,19 @@ export function useNewProject() { | ... | @@ -36,18 +38,19 @@ export function useNewProject() { |
| 36 | }); | 38 | }); |
| 37 | const handleNewThread = useNewThreadHandler(); | 39 | const handleNewThread = useNewThreadHandler(); |
| 38 | 40 | ||
| 39 | const publishToGitHub = useCallback( | 41 | const publishNewRepository = useCallback( |
| 40 | async (input: { | 42 | async (input: { |
| 41 | readonly environmentId: EnvironmentId; | 43 | readonly environmentId: EnvironmentId; |
| 42 | readonly workspaceRoot: string; | 44 | readonly workspaceRoot: string; |
| 43 | readonly account: string | null; | 45 | readonly target: NewProjectPublishTarget; |
| 44 | }) => { | 46 | }) => { |
| 47 | const { label } = input.target.definition; | ||
| 45 | const result = await publishRepository({ | 48 | const result = await publishRepository({ |
| 46 | environmentId: input.environmentId, | 49 | environmentId: input.environmentId, |
| 47 | input: { | 50 | input: { |
| 48 | cwd: input.workspaceRoot, | 51 | cwd: input.workspaceRoot, |
| 49 | provider: SourceControlProviderKind.make("github"), | 52 | provider: input.target.definition.kind, |
| 50 | repository: getNewProjectGitHubRepository(input, input.workspaceRoot), | 53 | repository: getNewProjectRepository(input.target, input.workspaceRoot), |
| 51 | visibility: "private", | 54 | visibility: "private", |
| 52 | }, | 55 | }, |
| 53 | }); | 56 | }); |
| ... | @@ -56,7 +59,7 @@ export function useNewProject() { | ... | @@ -56,7 +59,7 @@ export function useNewProject() { |
| 56 | toastManager.add( | 59 | toastManager.add( |
| 57 | stackedThreadToast({ | 60 | stackedThreadToast({ |
| 58 | type: "error", | 61 | type: "error", |
| 59 | title: "Could not create the GitHub repository", | 62 | title: `Could not create the ${label} repository`, |
| 60 | description: `${errorMessage(squashAtomCommandFailure(result))} Use Publish Repository in the Git menu to try again.`, | 63 | description: `${errorMessage(squashAtomCommandFailure(result))} Use Publish Repository in the Git menu to try again.`, |
| 61 | }), | 64 | }), |
| 62 | ); | 65 | ); |
| ... | @@ -66,7 +69,7 @@ export function useNewProject() { | ... | @@ -66,7 +69,7 @@ export function useNewProject() { |
| 66 | toastManager.add( | 69 | toastManager.add( |
| 67 | stackedThreadToast({ | 70 | stackedThreadToast({ |
| 68 | type: "success", | 71 | type: "success", |
| 69 | title: "Published to GitHub", | 72 | title: `Published to ${label}`, |
| 70 | description: result.value.repository.nameWithOwner, | 73 | description: result.value.repository.nameWithOwner, |
| 71 | }), | 74 | }), |
| 72 | ); | 75 | ); |
| ... | @@ -78,7 +81,7 @@ export function useNewProject() { | ... | @@ -78,7 +81,7 @@ export function useNewProject() { |
| 78 | async (input: { | 81 | async (input: { |
| 79 | readonly environmentId: EnvironmentId; | 82 | readonly environmentId: EnvironmentId; |
| 80 | readonly name: string; | 83 | readonly name: string; |
| 81 | readonly github: { readonly account: string | null } | null; | 84 | readonly publishTo: NewProjectPublishTarget | null; |
| 82 | }): Promise<boolean> => { | 85 | }): Promise<boolean> => { |
| 83 | const result = await createNew({ | 86 | const result = await createNew({ |
| 84 | environmentId: input.environmentId, | 87 | environmentId: input.environmentId, |
| ... | @@ -110,11 +113,11 @@ export function useNewProject() { | ... | @@ -110,11 +113,11 @@ export function useNewProject() { |
| 110 | }, | 113 | }, |
| 111 | ), | 114 | ), |
| 112 | ); | 115 | ); |
| 113 | if (input.github) { | 116 | if (input.publishTo) { |
| 114 | void publishToGitHub({ | 117 | void publishNewRepository({ |
| 115 | environmentId: input.environmentId, | 118 | environmentId: input.environmentId, |
| 116 | workspaceRoot, | 119 | workspaceRoot, |
| 117 | account: input.github.account, | 120 | target: input.publishTo, |
| 118 | }); | 121 | }); |
| 119 | } | 122 | } |
| 120 | 123 | ||
| ... | @@ -143,6 +146,6 @@ export function useNewProject() { | ... | @@ -143,6 +146,6 @@ export function useNewProject() { |
| 143 | }); | 146 | }); |
| 144 | return true; | 147 | return true; |
| 145 | }, | 148 | }, |
| 146 | [createNew, handleNewThread, publishToGitHub], | 149 | [createNew, handleNewThread, publishNewRepository], |
| 147 | ); | 150 | ); |
| 148 | } | 151 | } |
packages/client-runtime/src/operations/projects.test.ts+35| ... | @@ -18,6 +18,8 @@ import { | ... | @@ -18,6 +18,8 @@ import { |
| 18 | getCloneDestinationPath, | 18 | getCloneDestinationPath, |
| 19 | getCloneDirectoryName, | 19 | getCloneDirectoryName, |
| 20 | getDefaultCloneUrl, | 20 | getDefaultCloneUrl, |
| 21 | getNewProjectPublishTarget, | ||
| 22 | getNewProjectPublishTargets, | ||
| 21 | normalizePastedCloneUrl, | 23 | normalizePastedCloneUrl, |
| 22 | resolveAddProjectPath, | 24 | resolveAddProjectPath, |
| 23 | sortAddProjectProviderSources, | 25 | sortAddProjectProviderSources, |
| ... | @@ -279,3 +281,36 @@ describe("add project shared logic", () => { | ... | @@ -279,3 +281,36 @@ describe("add project shared logic", () => { |
| 279 | }); | 281 | }); |
| 280 | }); | 282 | }); |
| 281 | }); | 283 | }); |
| 284 | |||
| 285 | describe("new project publish targets", () => { | ||
| 286 | const readyHost = (kind: string, account: string | null) => ({ | ||
| 287 | kind: SourceControlProviderKind.make(kind), | ||
| 288 | label: kind, | ||
| 289 | status: "available" as const, | ||
| 290 | installHint: "", | ||
| 291 | version: Option.none(), | ||
| 292 | detail: Option.none(), | ||
| 293 | auth: { | ||
| 294 | status: "authenticated" as const, | ||
| 295 | account: Option.fromNullOr(account), | ||
| 296 | host: Option.none(), | ||
| 297 | detail: Option.none(), | ||
| 298 | }, | ||
| 299 | }); | ||
| 300 | |||
| 301 | it("offers ready hosts that can place the repository, GitHub first, and keeps the pick", () => { | ||
| 302 | const targets = getNewProjectPublishTargets({ | ||
| 303 | versionControlSystems: [], | ||
| 304 | sourceControlProviders: [ | ||
| 305 | readyHost("gitlab", "group"), | ||
| 306 | readyHost("github", null), | ||
| 307 | readyHost("azure-devops", "me"), | ||
| 308 | ], | ||
| 309 | }); | ||
| 310 | expect(targets.map((target) => target.definition.kind)).toEqual(["github", "gitlab"]); | ||
| 311 | expect(getNewProjectPublishTarget(targets, null)?.definition.kind).toBe("github"); | ||
| 312 | expect( | ||
| 313 | getNewProjectPublishTarget(targets, SourceControlProviderKind.make("gitlab"))?.owner, | ||
| 314 | ).toBe("group"); | ||
| 315 | }); | ||
| 316 | }); |
packages/client-runtime/src/operations/projects.ts+36-13| ... | @@ -25,6 +25,7 @@ import { | ... | @@ -25,6 +25,7 @@ import { |
| 25 | } from "../state/projects.ts"; | 25 | } from "../state/projects.ts"; |
| 26 | import type { EnvironmentProject } from "../state/models.ts"; | 26 | import type { EnvironmentProject } from "../state/models.ts"; |
| 27 | import { sourceControlClients } from "../sourceControlClients.ts"; | 27 | import { sourceControlClients } from "../sourceControlClients.ts"; |
| 28 | import type { SourceControlClientDefinition } from "../sourceControlClients.ts"; | ||
| 28 | 29 | ||
| 29 | /** A host to clone from, or `url` for a pasted clone URL. */ | 30 | /** A host to clone from, or `url` for a pasted clone URL. */ |
| 30 | export type AddProjectRemoteSource = SourceControlProviderKind | "url"; | 31 | export type AddProjectRemoteSource = SourceControlProviderKind | "url"; |
| ... | @@ -225,27 +226,49 @@ export function getNewProjectPathPreview(newProjectsRoot: string, name: string): | ... | @@ -225,27 +226,49 @@ export function getNewProjectPathPreview(newProjectsRoot: string, name: string): |
| 225 | return getCloneDestinationPath(newProjectsRoot, newProjectFolderName(name)); | 226 | return getCloneDestinationPath(newProjectsRoot, newProjectFolderName(name)); |
| 226 | } | 227 | } |
| 227 | 228 | ||
| 229 | /** A host a new project can be published to, and who the repository goes under there. */ | ||
| 230 | export interface NewProjectPublishTarget { | ||
| 231 | readonly definition: SourceControlClientDefinition; | ||
| 232 | /** Null lets the host's CLI place it under the signed-in user. */ | ||
| 233 | readonly owner: string | null; | ||
| 234 | } | ||
| 235 | |||
| 228 | /** | 236 | /** |
| 229 | * The GitHub account a new project would be published under, or null when | 237 | * The hosts a new project can be published to on that environment, in definition order so |
| 230 | * GitHub is not ready on that environment. A ready GitHub with an unknown | 238 | * GitHub leads. A host is offered when it is ready and its account names where the repository |
| 231 | * account still publishes; `gh` picks the signed-in user. | 239 | * goes. |
| 232 | */ | 240 | */ |
| 233 | export function getNewProjectGitHubTarget( | 241 | export function getNewProjectPublishTargets( |
| 234 | discovery: SourceControlDiscoveryResult | null, | 242 | discovery: SourceControlDiscoveryResult | null, |
| 235 | ): { readonly account: string | null } | null { | 243 | ): ReadonlyArray<NewProjectPublishTarget> { |
| 236 | const kind = SourceControlProviderKind.make("github"); | 244 | const readiness = buildAddProjectRemoteSourceReadiness(discovery); |
| 237 | if (!buildAddProjectRemoteSourceReadiness(discovery)(kind).ready) return null; | 245 | return sourceControlClients.definitions.flatMap((definition) => { |
| 238 | const github = discovery?.sourceControlProviders.find((provider) => provider.kind === kind); | 246 | if (!readiness(definition.kind).ready) return []; |
| 239 | return { account: github ? Option.getOrNull(github.auth.account) : null }; | 247 | const provider = discovery?.sourceControlProviders.find( |
| 248 | (candidate) => candidate.kind === definition.kind, | ||
| 249 | ); | ||
| 250 | const placement = definition.newRepositoryOwner( | ||
| 251 | provider ? Option.getOrNull(provider.auth.account) : null, | ||
| 252 | ); | ||
| 253 | return placement === null ? [] : [{ definition, owner: placement.owner }]; | ||
| 254 | }); | ||
| 255 | } | ||
| 256 | |||
| 257 | /** The chosen host's target, or the first one when the choice is not on offer. */ | ||
| 258 | export function getNewProjectPublishTarget( | ||
| 259 | targets: ReadonlyArray<NewProjectPublishTarget>, | ||
| 260 | kind: SourceControlProviderKind | null, | ||
| 261 | ): NewProjectPublishTarget | null { | ||
| 262 | return targets.find((target) => target.definition.kind === kind) ?? targets[0] ?? null; | ||
| 240 | } | 263 | } |
| 241 | 264 | ||
| 242 | /** `owner/folder` for publishing a new project, or just the folder for `gh` to place. */ | 265 | /** `owner/folder` for publishing a new project, or just the folder for the host to place. */ |
| 243 | export function getNewProjectGitHubRepository( | 266 | export function getNewProjectRepository( |
| 244 | target: { readonly account: string | null }, | 267 | target: Pick<NewProjectPublishTarget, "owner">, |
| 245 | workspaceRoot: string, | 268 | workspaceRoot: string, |
| 246 | ): string { | 269 | ): string { |
| 247 | const folderName = workspaceRoot.split(/[\\/]/).filter(Boolean).at(-1) ?? ""; | 270 | const folderName = workspaceRoot.split(/[\\/]/).filter(Boolean).at(-1) ?? ""; |
| 248 | return target.account ? `${target.account}/${folderName}` : folderName; | 271 | return target.owner ? `${target.owner}/${folderName}` : folderName; |
| 249 | } | 272 | } |
| 250 | 273 | ||
| 251 | /** | 274 | /** |
packages/source-control-azure-devops/src/client/definition.ts+2| ... | @@ -37,6 +37,8 @@ export const definition = defineSourceControlClient({ | ... | @@ -37,6 +37,8 @@ export const definition = defineSourceControlClient({ |
| 37 | publicHost: null, | 37 | publicHost: null, |
| 38 | publishDescription: "dev.azure.com", | 38 | publishDescription: "dev.azure.com", |
| 39 | publishHost: () => "dev.azure.com", | 39 | publishHost: () => "dev.azure.com", |
| 40 | // A new repository belongs to a project, which the account does not name. | ||
| 41 | newRepositoryOwner: () => null, | ||
| 40 | defaultCloneTransport: "ssh", | 42 | defaultCloneTransport: "ssh", |
| 41 | changeRequestUrl: ({ host, repository, number }) => | 43 | changeRequestUrl: ({ host, repository, number }) => |
| 42 | `https://${canonicalRepositoryKey(`${host}/${repository}`.toLowerCase())}/pullrequest/${number}`, | 44 | `https://${canonicalRepositoryKey(`${host}/${repository}`.toLowerCase())}/pullrequest/${number}`, |
packages/source-control-bitbucket/src/client/definition.ts+2| ... | @@ -26,6 +26,8 @@ export const definition = defineSourceControlClient({ | ... | @@ -26,6 +26,8 @@ export const definition = defineSourceControlClient({ |
| 26 | publicHost: "bitbucket.org", | 26 | publicHost: "bitbucket.org", |
| 27 | publishDescription: "bitbucket.org", | 27 | publishDescription: "bitbucket.org", |
| 28 | publishHost: () => "bitbucket.org", | 28 | publishHost: () => "bitbucket.org", |
| 29 | // A new repository belongs to a workspace, which the signed-in account does not name. | ||
| 30 | newRepositoryOwner: () => null, | ||
| 29 | defaultCloneTransport: "ssh", | 31 | defaultCloneTransport: "ssh", |
| 30 | changeRequestUrl: ({ host, repository, number }) => | 32 | changeRequestUrl: ({ host, repository, number }) => |
| 31 | `https://${host}/${repository}/pull-requests/${number}`, | 33 | `https://${host}/${repository}/pull-requests/${number}`, |
packages/source-control-core/src/client/definition.ts+7| ... | @@ -74,6 +74,12 @@ export interface SourceControlClientDefinition { | ... | @@ -74,6 +74,12 @@ export interface SourceControlClientDefinition { |
| 74 | readonly publishDescription: string; | 74 | readonly publishDescription: string; |
| 75 | /** Where a publish lands, given the host the server is signed in to, if it reported one. */ | 75 | /** Where a publish lands, given the host the server is signed in to, if it reported one. */ |
| 76 | readonly publishHost: (signedInHost: string | null) => string; | 76 | readonly publishHost: (signedInHost: string | null) => string; |
| 77 | /** | ||
| 78 | * Who a new project's repository is published under, given the signed-in account: an owner to | ||
| 79 | * name, `null` owner to let the host's CLI pick the signed-in user, or null when the host needs | ||
| 80 | * a choice an account cannot make, such as Azure DevOps' project. | ||
| 81 | */ | ||
| 82 | readonly newRepositoryOwner: (account: string | null) => { readonly owner: string | null } | null; | ||
| 77 | /** Which of a repository's clone URLs a new clone uses. */ | 83 | /** Which of a repository's clone URLs a new clone uses. */ |
| 78 | readonly defaultCloneTransport: "https" | "ssh"; | 84 | readonly defaultCloneTransport: "https" | "ssh"; |
| 79 | /** The web URL of a change request, or null when this client cannot build one for the host. */ | 85 | /** The web URL of a change request, or null when this client cannot build one for the host. */ |
| ... | @@ -224,6 +230,7 @@ export const UNKNOWN_SOURCE_CONTROL_CLIENT: SourceControlClientDefinition = { | ... | @@ -224,6 +230,7 @@ export const UNKNOWN_SOURCE_CONTROL_CLIENT: SourceControlClientDefinition = { |
| 224 | publicHost: null, | 230 | publicHost: null, |
| 225 | publishDescription: "Your signed-in server", | 231 | publishDescription: "Your signed-in server", |
| 226 | publishHost: (signedInHost) => signedInHost ?? "your server", | 232 | publishHost: (signedInHost) => signedInHost ?? "your server", |
| 233 | newRepositoryOwner: () => null, | ||
| 227 | defaultCloneTransport: "ssh", | 234 | defaultCloneTransport: "ssh", |
| 228 | changeRequestUrl: () => null, | 235 | changeRequestUrl: () => null, |
| 229 | changeRequestActions: new Set(), | 236 | changeRequestActions: new Set(), |
packages/source-control-forgejo/src/client/definition.ts+1| ... | @@ -44,6 +44,7 @@ export const definition = defineSourceControlClient({ | ... | @@ -44,6 +44,7 @@ export const definition = defineSourceControlClient({ |
| 44 | publicHost: null, | 44 | publicHost: null, |
| 45 | publishDescription: "Your signed-in server", | 45 | publishDescription: "Your signed-in server", |
| 46 | publishHost: (signedInHost) => signedInHost ?? "your server", | 46 | publishHost: (signedInHost) => signedInHost ?? "your server", |
| 47 | newRepositoryOwner: (account) => (account ? { owner: account } : null), | ||
| 47 | defaultCloneTransport: "https", | 48 | defaultCloneTransport: "https", |
| 48 | // The server's resolved web URL wins; otherwise an HTTP remote on the same host names the | 49 | // The server's resolved web URL wins; otherwise an HTTP remote on the same host names the |
| 49 | // origin, which may carry a port. SSH remotes say nothing about the web origin. | 50 | // origin, which may carry a port. SSH remotes say nothing about the web origin. |
packages/source-control-gitcafe/src/client/definition.ts+1| ... | @@ -28,6 +28,7 @@ export const definition = defineSourceControlClient({ | ... | @@ -28,6 +28,7 @@ export const definition = defineSourceControlClient({ |
| 28 | publicHost: null, | 28 | publicHost: null, |
| 29 | publishDescription: "git.cafe", | 29 | publishDescription: "git.cafe", |
| 30 | publishHost: () => "git.cafe", | 30 | publishHost: () => "git.cafe", |
| 31 | newRepositoryOwner: (account) => (account ? { owner: account } : null), | ||
| 31 | defaultCloneTransport: "ssh", | 32 | defaultCloneTransport: "ssh", |
| 32 | changeRequestUrl: ({ host, repository, number }) => | 33 | changeRequestUrl: ({ host, repository, number }) => |
| 33 | `https://${host}/${repository}/pulls/${number}`, | 34 | `https://${host}/${repository}/pulls/${number}`, |
packages/source-control-github/src/client/definition.ts+1| ... | @@ -27,6 +27,7 @@ export const definition = defineSourceControlClient({ | ... | @@ -27,6 +27,7 @@ export const definition = defineSourceControlClient({ |
| 27 | publicHost: "github.com", | 27 | publicHost: "github.com", |
| 28 | publishDescription: "github.com", | 28 | publishDescription: "github.com", |
| 29 | publishHost: () => "github.com", | 29 | publishHost: () => "github.com", |
| 30 | newRepositoryOwner: (account) => ({ owner: account }), | ||
| 30 | defaultCloneTransport: "https", | 31 | defaultCloneTransport: "https", |
| 31 | changeRequestUrl: ({ host, repository, number }) => | 32 | changeRequestUrl: ({ host, repository, number }) => |
| 32 | `https://${host}/${repository}/pull/${number}`, | 33 | `https://${host}/${repository}/pull/${number}`, |
packages/source-control-gitlab/src/client/definition.ts+1| ... | @@ -27,6 +27,7 @@ export const definition = defineSourceControlClient({ | ... | @@ -27,6 +27,7 @@ export const definition = defineSourceControlClient({ |
| 27 | publicHost: "gitlab.com", | 27 | publicHost: "gitlab.com", |
| 28 | publishDescription: "gitlab.com", | 28 | publishDescription: "gitlab.com", |
| 29 | publishHost: () => "gitlab.com", | 29 | publishHost: () => "gitlab.com", |
| 30 | newRepositoryOwner: (account) => ({ owner: account }), | ||
| 30 | defaultCloneTransport: "ssh", | 31 | defaultCloneTransport: "ssh", |
| 31 | changeRequestUrl: ({ host, repository, number }) => | 32 | changeRequestUrl: ({ host, repository, number }) => |
| 32 | `https://${host}/${repository}/-/merge_requests/${number}`, | 33 | `https://${host}/${repository}/-/merge_requests/${number}`, |