History

fix(clients): disable add project while disconnected (#4834)

Wout Stiens committed 3 months ago

Commit 758deca1 parent 2d9066e7 files · +243 −56Browse files

Unified · Split

7 files
  1. apps/mobile/src/features/projects/AddProjectScreen.logic.test.ts +41 −0
  2. apps/mobile/src/features/projects/AddProjectScreen.logic.ts +26 −0
  3. apps/mobile/src/features/projects/AddProjectScreen.tsx +79 −40
  4. apps/mobile/src/features/threads/NewTaskRouteScreen.tsx +18 −12
  5. apps/web/src/components/CommandPalette.tsx +62 −4
  6. packages/client-runtime/src/operations/projects.test.ts +10 −0
  7. packages/client-runtime/src/operations/projects.ts +7 −0
apps/mobile/src/features/projects/AddProjectScreen.logic.test.ts+41−0
@@ -0,0 +1,41 @@
1+import type { EnvironmentConnectionPhase } from "@t3tools/client-runtime/connection";
2+import { EnvironmentId } from "@t3tools/contracts";
3+import { describe, expect, it } from "vite-plus/test";
4+
5+import { resolveAddProjectEnvironment } from "./AddProjectScreen.logic";
6+
7+const ENVIRONMENT_A = EnvironmentId.make("environment-a");
8+const ENVIRONMENT_B = EnvironmentId.make("environment-b");
9+
10+function environment(environmentId: EnvironmentId, connectionState: EnvironmentConnectionPhase) {
11+ return { environmentId, connectionState };
12+}
13+
14+describe("resolveAddProjectEnvironment", () => {
15+ it("does not redirect an explicit unavailable environment to another environment", () => {
16+ expect(
17+ resolveAddProjectEnvironment(
18+ [environment(ENVIRONMENT_A, "offline"), environment(ENVIRONMENT_B, "connected")],
19+ ENVIRONMENT_A,
20+ ),
21+ ).toBeNull();
22+ });
23+
24+ it("resolves an explicit connected environment", () => {
25+ expect(
26+ resolveAddProjectEnvironment(
27+ [environment(ENVIRONMENT_A, "connected"), environment(ENVIRONMENT_B, "connected")],
28+ ENVIRONMENT_A,
29+ )?.environmentId,
30+ ).toBe(ENVIRONMENT_A);
31+ });
32+
33+ it("defaults to the first connected environment when no environment is requested", () => {
34+ expect(
35+ resolveAddProjectEnvironment(
36+ [environment(ENVIRONMENT_A, "offline"), environment(ENVIRONMENT_B, "connected")],
37+ null,
38+ )?.environmentId,
39+ ).toBe(ENVIRONMENT_B);
40+ });
41+});
apps/mobile/src/features/projects/AddProjectScreen.logic.ts+26−0
@@ -0,0 +1,26 @@
1+import { canCreateProjectInEnvironment } from "@t3tools/client-runtime/operations/projects";
2+import type { EnvironmentConnectionPhase } from "@t3tools/client-runtime/connection";
3+import type { EnvironmentId } from "@t3tools/contracts";
4+
5+export function resolveAddProjectEnvironment<
6+ T extends {
7+ readonly environmentId: EnvironmentId;
8+ readonly connectionState: EnvironmentConnectionPhase;
9+ },
10+>(environmentOptions: ReadonlyArray<T>, requestedEnvironmentId: EnvironmentId | null): T | null {
11+ if (requestedEnvironmentId !== null) {
12+ return (
13+ environmentOptions.find(
14+ (environment) =>
15+ environment.environmentId === requestedEnvironmentId &&
16+ canCreateProjectInEnvironment(environment.connectionState),
17+ ) ?? null
18+ );
19+ }
20+
21+ return (
22+ environmentOptions.find((environment) =>
23+ canCreateProjectInEnvironment(environment.connectionState),
24+ ) ?? null
25+ );
26+}
apps/mobile/src/features/projects/AddProjectScreen.tsx+79−40
@@ -4,12 +4,17 @@ import { · 3 unmodified lines
44 addProjectRemoteSourceProvider,
55 buildAddProjectRemoteSourceReadiness,
66 buildProjectCreateCommand,
7+ canCreateProjectInEnvironment,
78 findExistingAddProject,
89 getAddProjectInitialQuery,
910 resolveAddProjectPath,
1011 sortAddProjectProviderSources,
1112 type AddProjectRemoteSource,
1213 } from "@t3tools/client-runtime/operations/projects";
14+import {
15+ connectionStatusText,
16+ type EnvironmentConnectionPhase,
17+} from "@t3tools/client-runtime/connection";
1318 import {
1419 canPreloadBrowsePath,
1520 createBrowseNavigationCoordinator,
@@ -24,7 +29,7 @@ import { · 8 unmodified lines
2429 import { CommandId, type EnvironmentId, ProjectId } from "@t3tools/contracts";
2530 import { StackActions, useNavigation } from "@react-navigation/native";
2631 import { SymbolView } from "../../components/AppSymbol";
27−import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react";
32+import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
2833 import { ActivityIndicator, Alert, Pressable, ScrollView, View } from "react-native";
2934 import { useSafeAreaInsets } from "react-native-safe-area-context";
3035 import * as Arr from "effect/Array";
@@ -46,15 +51,20 @@ import { uuidv4 } from "../../lib/uuid"; · 15 unmodified lines
4651 import { useAtomCommand } from "../../state/use-atom-command";
4752 import { useAtomQueryRunner } from "../../state/use-atom-query-runner";
4853 import {
54+ useRemoteConnectionStatus,
4955 useRemoteEnvironmentRuntime,
5056 useSavedRemoteConnections,
5157 } from "../../state/use-remote-environment-registry";
58+import { resolveAddProjectEnvironment } from "./AddProjectScreen.logic";
5259
5360 interface EnvironmentOption {
5461 readonly environmentId: EnvironmentId;
5562 readonly label: string;
5663 readonly platform: string;
5764 readonly baseDirectory: string | null;
65+ readonly connectionState: EnvironmentConnectionPhase;
66+ readonly connectionError: string | null;
67+ readonly connectionErrorTraceId: string | null;
5868 }
5969
6070 const environmentOptionOrder = Order.mapInput(
@@ -227,10 +237,13 @@ function ProjectPathInput(props: { · 166 unmodified lines
227237 }
228238
229239 function useBrowsePathInput(environment: EnvironmentOption | null) {
240+ const environmentId = environment?.environmentId ?? null;
241+ const environmentBaseDirectory = environment?.baseDirectory ?? null;
230242 const [pathInput, commitPathInput] = useState(() =>
231− getAddProjectInitialQuery(environment?.baseDirectory),
243+ getAddProjectInitialQuery(environmentBaseDirectory),
232244 );
233− const environmentRuntime = useRemoteEnvironmentRuntime(environment?.environmentId ?? null);
245+ const previousEnvironmentIdRef = useRef(environmentId);
246+ const environmentRuntime = useRemoteEnvironmentRuntime(environmentId);
234247 const loadBrowsePath = useAtomQueryRunner(filesystemEnvironment.browse, {
235248 reportFailure: false,
236249 reportDefect: false,
@@ -268,10 +281,11 @@ function useBrowsePathInput(environment: EnvironmentOption | null) { · 31 unmodified lines
268281 );
269282
270283 useEffect(() => {
271− if (environment) {
272− setPathInput(getAddProjectInitialQuery(environment.baseDirectory));
284+ if (environmentId !== null && environmentId !== previousEnvironmentIdRef.current) {
285+ previousEnvironmentIdRef.current = environmentId;
286+ setPathInput(getAddProjectInitialQuery(environmentBaseDirectory));
273287 }
274− }, [environment, setPathInput]);
288+ }, [environmentBaseDirectory, environmentId, setPathInput]);
275289
276290 useEffect(
277291 () => () => {
@@ -286,19 +300,27 @@ function useBrowsePathInput(environment: EnvironmentOption | null) { · 8 unmodified lines
286300 function useEnvironmentOptions(): ReadonlyArray<EnvironmentOption> {
287301 const serverConfigByEnvironmentId = useServerConfigs();
288302 const { savedConnectionsById } = useSavedRemoteConnections();
303+ const { connectedEnvironments } = useRemoteConnectionStatus();
289304
290305 return useMemo<ReadonlyArray<EnvironmentOption>>(() => {
306+ const runtimeByEnvironmentId = new Map(
307+ connectedEnvironments.map((environment) => [environment.environmentId, environment] as const),
308+ );
291309 const options = Object.values(savedConnectionsById).map((connection) => {
292310 const config = serverConfigByEnvironmentId.get(connection.environmentId);
311+ const runtime = runtimeByEnvironmentId.get(connection.environmentId);
293312 return {
294313 environmentId: connection.environmentId,
295314 label: connection.environmentLabel,
296315 platform: platformFromOs(config?.environment.platform.os ?? null),
297316 baseDirectory: config?.settings.addProjectBaseDirectory ?? null,
317+ connectionState: runtime?.connectionState ?? "available",
318+ connectionError: runtime?.connectionError ?? null,
319+ connectionErrorTraceId: runtime?.connectionErrorTraceId ?? null,
298320 };
299321 });
300322 return Arr.sort(options, environmentOptionOrder);
301− }, [savedConnectionsById, serverConfigByEnvironmentId]);
323+ }, [connectedEnvironments, savedConnectionsById, serverConfigByEnvironmentId]);
302324 }
303325
304326 function useSelectedEnvironment(): {
@@ -309,8 +331,14 @@ function useSelectedEnvironment(): { · 4 unmodified lines
309331 const [selectedEnvironmentId, setSelectedEnvironmentId] = useState<EnvironmentId | null>(null);
310332 const environmentOptions = useEnvironmentOptions();
311333 const selectedEnvironment =
312− environmentOptions.find((environment) => environment.environmentId === selectedEnvironmentId) ??
313− environmentOptions[0] ??
334+ environmentOptions.find(
335+ (environment) =>
336+ environment.environmentId === selectedEnvironmentId &&
337+ canCreateProjectInEnvironment(environment.connectionState),
338+ ) ??
339+ environmentOptions.find((environment) =>
340+ canCreateProjectInEnvironment(environment.connectionState),
341+ ) ??
314342 null;
315343
316344 return {
@@ -325,9 +353,9 @@ function EmptyEnvironmentState() { · 8 unmodified lines
325353
326354 return (
327355 <View className="items-center gap-3 rounded-2xl bg-card px-5 py-8">
328− <Text className="text-center text-lg font-t3-bold">No environments connected</Text>
356+ <Text className="text-center text-lg font-t3-bold">Environment unavailable</Text>
329357 <Text className="text-center text-sm leading-normal text-foreground-muted">
330− Add an environment before adding a project.
358+ Start or reconnect an environment before adding a project.
331359 </Text>
332360 <Pressable
333361 onPress={() => navigation.dispatch(StackActions.replace("ConnectionsNew"))}
@@ -407,17 +435,25 @@ export function AddProjectSourceScreen() { · 73 unmodified lines
407435
408436 return (
409437 <AddProjectShell>
410− {environmentOptions.length === 0 ? <EmptyEnvironmentState /> : null}
438+ {selectedEnvironment === null ? <EmptyEnvironmentState /> : null}
411439
412440 {environmentOptions.length > 1 ? (
413441 <>
414− <SectionTitle>Connected environments</SectionTitle>
442+ <SectionTitle>Environments</SectionTitle>
415443 <ListSection>
416444 {environmentOptions.map((environment, index) => (
417445 <ListRow
418446 key={environment.environmentId}
419447 title={environment.label}
420− subtitle={environment.environmentId}
448+ subtitle={
449+ canCreateProjectInEnvironment(environment.connectionState)
450+ ? environment.environmentId
451+ : connectionStatusText({
452+ phase: environment.connectionState,
453+ error: environment.connectionError,
454+ traceId: environment.connectionErrorTraceId,
455+ })
456+ }
421457 icon={
422458 <SymbolView
423459 name="server.rack"
@@ -427,6 +463,7 @@ export function AddProjectSourceScreen() { · 3 unmodified lines
427463 />
428464 }
429465 selected={environment.environmentId === selectedEnvironment?.environmentId}
466+ disabled={!canCreateProjectInEnvironment(environment.connectionState)}
430467 isFirst={index === 0}
431468 right={
432469 environment.environmentId === selectedEnvironment?.environmentId ? (
@@ -500,7 +537,7 @@ function useCreateProject(environment: EnvironmentOption | null) { · 67 unmodified lines
500537
501538 return useCallback(
502539 async (workspaceRoot: string) => {
503− if (!environment) return;
540+ if (!environment || !canCreateProjectInEnvironment(environment.connectionState)) return;
504541
505542 const existing = findExistingAddProject({
506543 projects,
@@ -551,11 +588,7 @@ function useEnvironmentFromParam( · 44 unmodified lines
551588 ): EnvironmentOption | null {
552589 const environmentOptions = useEnvironmentOptions();
553590 const environmentId = stringParam(environmentIdParam) as EnvironmentId | null;
554− return (
555− environmentOptions.find((environment) => environment.environmentId === environmentId) ??
556− environmentOptions[0] ??
557− null
558− );
591+ return resolveAddProjectEnvironment(environmentOptions, environmentId);
559592 }
560593
561594 export function AddProjectRepositoryScreen(props: {
@@ -619,26 +652,32 @@ export function AddProjectRepositoryScreen(props: { · 57 unmodified lines
619652 return (
620653 <AddProjectShell>
621654 {error ? <ErrorBanner message={error} /> : null}
622− <TextInput
623− className="h-12 min-h-12 rounded-[24px] px-4 py-0 text-base leading-snug"
624− value={repositoryInput}
625− onChangeText={setRepositoryInput}
626− autoCapitalize="none"
627− autoCorrect={false}
628− placeholder={
629− source === "url"
630− ? "https://github.com/org/repo.git"
631− : addProjectRemoteSourcePathHint(source)
632− }
633− returnKeyType="next"
634− onSubmitEditing={() => void lookupRepository()}
635− />
636− <PrimaryActionButton
637− label={source === "url" ? "Continue" : "Lookup repository"}
638− disabled={isSubmitting || repositoryInput.trim().length === 0}
639− onPress={() => void lookupRepository()}
640− loading={isSubmitting}
641− />
655+ {environment ? (
656+ <>
657+ <TextInput
658+ className="h-12 min-h-12 rounded-[24px] px-4 py-0 text-base leading-snug"
659+ value={repositoryInput}
660+ onChangeText={setRepositoryInput}
661+ autoCapitalize="none"
662+ autoCorrect={false}
663+ placeholder={
664+ source === "url"
665+ ? "https://github.com/org/repo.git"
666+ : addProjectRemoteSourcePathHint(source)
667+ }
668+ returnKeyType="next"
669+ onSubmitEditing={() => void lookupRepository()}
670+ />
671+ <PrimaryActionButton
672+ label={source === "url" ? "Continue" : "Lookup repository"}
673+ disabled={isSubmitting || repositoryInput.trim().length === 0}
674+ onPress={() => void lookupRepository()}
675+ loading={isSubmitting}
676+ />
677+ </>
678+ ) : (
679+ <EmptyEnvironmentState />
680+ )}
642681 </AddProjectShell>
643682 );
644683 }
apps/mobile/src/features/threads/NewTaskRouteScreen.tsx+18−12
@@ -203,13 +203,17 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara · 202 unmodified lines
203203 title={screenTitle}
204204 subtitle={incomingShareSubtitle}
205205 onBack={layout.usesSplitView ? () => navigation.goBack() : undefined}
206− actions={[
207− {
208− accessibilityLabel: "Add project",
209− icon: "plus",
210− onPress: () => navigation.navigate("NewTaskSheet", { screen: "AddProject" }),
211− },
212− ]}
206+ actions={
207+ catalogState.hasReadyEnvironment
208+ ? [
209+ {
210+ accessibilityLabel: "Add project",
211+ icon: "plus",
212+ onPress: () => navigation.navigate("NewTaskSheet", { screen: "AddProject" }),
213+ },
214+ ]
215+ : []
216+ }
213217 />
214218 </>
215219 ) : (
@@ -229,11 +233,13 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara · 13 unmodified lines
229233 separateBackground
230234 />
231235 ) : null}
232− <NativeHeaderToolbar.Button
233− icon="plus"
234− onPress={() => navigation.navigate("NewTaskSheet", { screen: "AddProject" })}
235− separateBackground
236− />
236+ {catalogState.hasReadyEnvironment ? (
237+ <NativeHeaderToolbar.Button
238+ icon="plus"
239+ onPress={() => navigation.navigate("NewTaskSheet", { screen: "AddProject" })}
240+ separateBackground
241+ />
242+ ) : null}
237243 </NativeHeaderToolbar>
238244 </>
239245 )}
apps/web/src/components/CommandPalette.tsx+62−4
@@ -1,6 +1,8 @@
11 "use client";
22
33 import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime/environment";
4+import { canCreateProjectInEnvironment } from "@t3tools/client-runtime/operations/projects";
5+import { connectionStatusText } from "@t3tools/client-runtime/connection";
46 import {
57 canPreloadBrowsePath,
68 createBrowseNavigationCoordinator,
@@ -163,6 +165,8 @@ interface AddProjectEnvironmentOption { · 156 unmodified lines
163165 readonly environmentId: EnvironmentId;
164166 readonly label: string;
165167 readonly isPrimary: boolean;
168+ readonly isConnected: boolean;
169+ readonly status: string;
166170 }
167171
168172 type AddProjectRemoteProviderKind = Extract<
@@ -620,6 +624,8 @@ function OpenCommandPaletteDialog(props: { · 451 unmodified lines
620624 runtimeLabel: environment.label,
621625 }),
622626 isPrimary,
627+ isConnected: canCreateProjectInEnvironment(environment.connection.phase),
628+ status: connectionStatusText(environment.connection),
623629 };
624630 });
625631
@@ -632,10 +638,14 @@ function OpenCommandPaletteDialog(props: { · 6 unmodified lines
632638
633639 return options;
634640 }, [environments]);
635− const defaultAddProjectEnvironmentId = addProjectEnvironmentOptions[0]?.environmentId ?? null;
641+ const defaultAddProjectEnvironmentId =
642+ addProjectEnvironmentOptions.find((option) => option.isConnected)?.environmentId ?? null;
636643 const wslAddProjectEnvironmentOption = useMemo(
637644 () =>
638645 addProjectEnvironmentOptions.find((option) => {
646+ if (!option.isConnected) {
647+ return false;
648+ }
639649 const environment = environments.find(
640650 (candidate) => candidate.environmentId === option.environmentId,
641651 );
@@ -1108,6 +1118,19 @@ function OpenCommandPaletteDialog(props: { · 466 unmodified lines
11081118
11091119 const startAddProjectSourceSelection = useCallback(
11101120 (environmentId: EnvironmentId): void => {
1121+ const environment = environments.find(
1122+ (candidate) => candidate.environmentId === environmentId,
1123+ );
1124+ if (!canCreateProjectInEnvironment(environment?.connection.phase)) {
1125+ toastManager.add(
1126+ stackedThreadToast({
1127+ type: "error",
1128+ title: "Environment unavailable",
1129+ description: `${environment?.label ?? "The selected environment"} is not connected.`,
1130+ }),
1131+ );
1132+ return;
1133+ }
11111134 setAddProjectEnvironmentId(environmentId);
11121135 setAddProjectCloneFlow(null);
11131136 pushPaletteView({
@@ -1123,6 +1146,7 @@ function OpenCommandPaletteDialog(props: { · 9 unmodified lines
11231146 [
11241147 browseEnvironmentId,
11251148 buildAddProjectSourceGroups,
1149+ environments,
11261150 pushPaletteView,
11271151 sourceControlDiscovery.data,
11281152 ],
@@ -1134,7 +1158,12 @@ function OpenCommandPaletteDialog(props: { · 5 unmodified lines
11341158 value: `action:add-project:environment:${option.environmentId}`,
11351159 searchTerms: [option.label, option.environmentId, option.isPrimary ? "this device" : ""],
11361160 title: option.label,
1137− description: option.isPrimary ? "This device" : option.environmentId,
1161+ description: option.isConnected
1162+ ? option.isPrimary
1163+ ? "This device"
1164+ : option.environmentId
1165+ : option.status,
1166+ disabled: !option.isConnected,
11381167 icon: <FolderPlusIcon className={ITEM_ICON_CLASS} />,
11391168 keepOpen: true,
11401169 run: async () => {
@@ -1155,7 +1184,7 @@ function OpenCommandPaletteDialog(props: { · 14 unmodified lines
11551184 );
11561185
11571186 const openAddProjectFlow = useCallback(() => {
1158− if (addProjectEnvironmentOptions.length > 1) {
1187+ if (addProjectEnvironmentOptions.length > 1 || defaultAddProjectEnvironmentId === null) {
11591188 pushPaletteView({
11601189 addonIcon: <FolderPlusIcon className={ADDON_ICON_CLASS} />,
11611190 groups: addProjectEnvironmentGroups,
@@ -1294,6 +1323,7 @@ function OpenCommandPaletteDialog(props: { · 132 unmodified lines
12941323 "environment",
12951324 ],
12961325 title: "Add project",
1326+ disabled: defaultAddProjectEnvironmentId === null,
12971327 icon: <FolderPlusIcon className={ITEM_ICON_CLASS} />,
12981328 keepOpen: true,
12991329 run: async () => {
@@ -1355,6 +1385,19 @@ function OpenCommandPaletteDialog(props: { · 55 unmodified lines
13551385 readonly platform: string;
13561386 readonly currentProjectCwd: string | null;
13571387 }) => {
1388+ const environment = environments.find(
1389+ (candidate) => candidate.environmentId === input.environmentId,
1390+ );
1391+ if (!canCreateProjectInEnvironment(environment?.connection.phase)) {
1392+ toastManager.add(
1393+ stackedThreadToast({
1394+ type: "error",
1395+ title: "Environment unavailable",
1396+ description: `${environment?.label ?? "The selected environment"} is not connected.`,
1397+ }),
1398+ );
1399+ return;
1400+ }
13581401 const rawCwd = input.rawCwd;
13591402
13601403 if (isUnsupportedWindowsProjectPath(rawCwd.trim(), input.platform)) {
@@ -1507,6 +1550,16 @@ function OpenCommandPaletteDialog(props: { · 146 unmodified lines
15071550 if (!addProjectCloneFlow) {
15081551 return;
15091552 }
1553+ if (!canCreateProjectInEnvironment(browseEnvironment?.connection.phase)) {
1554+ toastManager.add(
1555+ stackedThreadToast({
1556+ type: "error",
1557+ title: "Environment unavailable",
1558+ description: `${browseEnvironment?.label ?? "The selected environment"} is not connected.`,
1559+ }),
1560+ );
1561+ return;
1562+ }
15101563
15111564 if (addProjectCloneFlow.step === "repository") {
15121565 const rawRepository = query.trim();
@@ -1711,7 +1764,10 @@ function OpenCommandPaletteDialog(props: { · 198 unmodified lines
17111764 getCommandPaletteInputPlaceholder(paletteMode);
17121765 const isSubmenu = paletteMode === "submenu" || paletteMode === "submenu-browse";
17131766 const hasHighlightedBrowseItem = highlightedItemValue?.startsWith("browse:") ?? false;
1714− const canSubmitBrowsePath = isBrowsing && !relativePathNeedsActiveProject;
1767+ const canSubmitBrowsePath =
1768+ isBrowsing &&
1769+ !relativePathNeedsActiveProject &&
1770+ canCreateProjectInEnvironment(browseEnvironment?.connection.phase);
17151771 const willCreateProjectPath =
17161772 canSubmitBrowsePath &&
17171773 !isBrowsePending &&
@@ -1738,6 +1794,7 @@ function OpenCommandPaletteDialog(props: { · 20 unmodified lines
17381794 const canSubmitRemoteProjectFlow =
17391795 addProjectCloneFlow?.step === "repository" &&
17401796 query.trim().length > 0 &&
1797+ canCreateProjectInEnvironment(browseEnvironment?.connection.phase) &&
17411798 !isRemoteProjectPending;
17421799 const fileManagerName = getLocalFileManagerName(navigator.platform);
17431800 const canOpenProjectFromFileManager =
@@ -2063,6 +2120,7 @@ function OpenCommandPaletteDialog(props: { · 319 unmodified lines
20632120 )}
20642121 aria-label={`${submitActionLabel} (${addShortcutLabel})`}
20652122 disabled={
2123+ !canCreateProjectInEnvironment(browseEnvironment?.connection.phase) ||
20662124 relativePathNeedsActiveProject ||
20672125 (isCloneDestinationStep && isRemoteProjectPending)
20682126 }
packages/client-runtime/src/operations/projects.test.ts+10−0
@@ -10,6 +10,7 @@ import * as Option from "effect/Option"; · 9 unmodified lines
1010 import {
1111 buildAddProjectRemoteSourceReadiness,
1212 buildProjectCreateCommand,
13+ canCreateProjectInEnvironment,
1314 findExistingAddProject,
1415 getAddProjectInitialQuery,
1516 resolveAddProjectPath,
@@ -18,6 +19,15 @@ import { · 2 unmodified lines
1819 import type { EnvironmentProject } from "../state/models.ts";
1920
2021 describe("add project shared logic", () => {
22+ it("only allows project creation in connected environments", () => {
23+ expect(canCreateProjectInEnvironment("connected")).toBe(true);
24+ expect(canCreateProjectInEnvironment("available")).toBe(false);
25+ expect(canCreateProjectInEnvironment("offline")).toBe(false);
26+ expect(canCreateProjectInEnvironment("connecting")).toBe(false);
27+ expect(canCreateProjectInEnvironment("reconnecting")).toBe(false);
28+ expect(canCreateProjectInEnvironment("error")).toBe(false);
29+ });
30+
2131 it("resolves initial browse paths from settings", () => {
2232 expect(getAddProjectInitialQuery("")).toBe("~/");
2333 expect(getAddProjectInitialQuery("/work")).toBe("/work/");
packages/client-runtime/src/operations/projects.ts+7−0
@@ -1,3 +1,4 @@
1+import type { EnvironmentConnectionPhase } from "../connection/presentation.ts";
12 import type {
23 CommandId,
34 EnvironmentId,
@@ -27,6 +28,12 @@ export type AddProjectRemoteProviderKind = Extract< · 23 unmodified lines
2728 >;
2829 export type AddProjectRemoteSource = AddProjectRemoteProviderKind | "url";
2930
31+export function canCreateProjectInEnvironment(
32+ connectionPhase: EnvironmentConnectionPhase | null | undefined,
33+): boolean {
34+ return connectionPhase === "connected";
35+}
36+
3037 export type AddProjectRemoteSourceReadiness = Record<
3138 AddProjectRemoteSource,
3239 { readonly ready: boolean; readonly hint: string | null }

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t