From 7050ab0f0192db8b52d2f55e58071f72e6cb76fe Mon Sep 17 00:00:00 2001
From: Jack <32645451+Jacxk@users.noreply.github.com>
Date: Tue, 27 Jan 2026 13:37:55 -0500
Subject: [PATCH 1/9] feat: implement team selection in setup process and
settings
---
app/index.tsx | 7 +--
app/main/settings/index.tsx | 69 ++++++++++++++++++++++----
app/setup/team.tsx | 77 +++++++++++++++++++++++++++++
components/SetupScreenContainer.tsx | 8 ++-
constants/StorageKeys.ts | 1 +
hooks/useSetup.ts | 12 +++++
6 files changed, 160 insertions(+), 14 deletions(-)
create mode 100644 app/setup/team.tsx
diff --git a/app/index.tsx b/app/index.tsx
index c62994d..73f2e16 100644
--- a/app/index.tsx
+++ b/app/index.tsx
@@ -5,10 +5,11 @@ import * as SplashScreen from "expo-splash-screen";
SplashScreen.preventAutoHideAsync();
export default function RootIndex() {
- const { setupComplete } = useSetup();
+ const { setupComplete, team } = useSetup();
- if (setupComplete === null) return null;
+ if (setupComplete === null || team === null) return null;
+ if (team === "NO_TEAM_SELECTED") return ;
if (setupComplete) return ;
- else return ;
+ return ;
}
diff --git a/app/main/settings/index.tsx b/app/main/settings/index.tsx
index 182d50c..2b4b848 100644
--- a/app/main/settings/index.tsx
+++ b/app/main/settings/index.tsx
@@ -1,3 +1,4 @@
+import { useTeams } from "@/api/teams";
import { ArrowUpRight } from "@/components/icons/ArrowUpRight";
import Info from "@/components/icons/Info";
import { MessageCircle } from "@/components/icons/MessageCircle";
@@ -10,29 +11,76 @@ import {
SettingsButtonProps,
} from "@/components/SettingsButton";
import { SettingsLink, SettingsLinkProps } from "@/components/SettingsLink";
+import { Label } from "@/components/ui/label";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
import { H3 } from "@/components/ui/typography";
import { LOG_REFETCH_INTERVAL_STORAGE_KEY } from "@/constants/StorageKeys";
import { useColorScheme } from "@/hooks/useColorScheme";
+import useSetup from "@/hooks/useSetup";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { Alert, Linking, SectionList, View } from "react-native";
import { Switch } from "react-native-gesture-handler";
type SettingsItem =
- | (SettingsLinkProps & { isLink: true })
- | (SettingsButtonProps & { isLink: false });
+ | SettingsLinkProps
+ | SettingsButtonProps
+ | { label: string; render: React.ReactElement | null };
type SettingsSections = {
title: string;
data: SettingsItem[];
};
+function TeamSelect() {
+ const { team, setTeam } = useSetup();
+ const { data: teams } = useTeams();
+
+ if (!teams || !team) return null;
+
+ const selectedTeam = {
+ value: team,
+ label: teams.find((t) => t.id.toString() === team)?.name ?? team,
+ };
+
+ return (
+
+
+
+
+ );
+}
+
export default function Settings() {
const { toggleColorScheme, isDarkColorScheme } = useColorScheme();
const appearanceItems: SettingsItem[] = [
{
label: "Dark Mode",
- isLink: false,
onPress: toggleColorScheme,
icon: ,
rightComponent: (
@@ -42,9 +90,12 @@ export default function Settings() {
];
const appConfigItems: SettingsItem[] = [
+ {
+ label: "Team Selection",
+ render: ,
+ },
{
label: "Reconfigure Linked Instance",
- isLink: true,
href: {
pathname: "/setup/serverAddress",
params: { reconfigure: "true" },
@@ -55,7 +106,6 @@ export default function Settings() {
},
{
label: "Logs Refetch Interval",
- isLink: false,
onPress: async () => {
const savedInterval = await AsyncStorage.getItem(
LOG_REFETCH_INTERVAL_STORAGE_KEY,
@@ -71,7 +121,7 @@ export default function Settings() {
{
isPreferred: true,
text: "Save",
- onPress: (text) => {
+ onPress: (text: string | undefined) => {
AsyncStorage.setItem(
LOG_REFETCH_INTERVAL_STORAGE_KEY,
text || "2000",
@@ -89,7 +139,6 @@ export default function Settings() {
{
label: "Clear Cache",
labelClassName: "text-destructive",
- isLink: false,
onPress: () => {
// TODO: Clear cache
},
@@ -100,7 +149,6 @@ export default function Settings() {
const appInfoItems: SettingsItem[] = [
{
label: "Share Feedback",
- isLink: false,
onPress: () => {
Linking.openURL("https://github.com/Jacxk/Coolifynager/issues");
},
@@ -109,7 +157,6 @@ export default function Settings() {
},
{
label: "About",
- isLink: true,
href: "/main/settings/about",
icon: ,
},
@@ -140,7 +187,9 @@ export default function Settings() {
{title}
)}
renderItem={({ item }) => {
- if (item.isLink) {
+ if ("render" in item) {
+ return item.render ?? null;
+ } else if ("href" in item) {
return ;
} else {
return ;
diff --git a/app/setup/team.tsx b/app/setup/team.tsx
new file mode 100644
index 0000000..c78a99f
--- /dev/null
+++ b/app/setup/team.tsx
@@ -0,0 +1,77 @@
+import { useTeams } from "@/api/teams";
+import LoadingScreen from "@/components/LoadingScreen";
+import SetupScreenContainer from "@/components/SetupScreenContainer";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Text } from "@/components/ui/text";
+import { H1, P } from "@/components/ui/typography";
+import useSetup from "@/hooks/useSetup";
+import { cn } from "@/lib/utils";
+import { Href, router, useLocalSearchParams } from "expo-router";
+import { View } from "react-native";
+import { Gesture, GestureDetector } from "react-native-gesture-handler";
+import { scheduleOnRN } from "react-native-worklets";
+
+export default function TeamStep() {
+ const { redirect } = useLocalSearchParams<{ redirect: Href }>();
+ const { team, setTeam } = useSetup();
+ const { data, isPending } = useTeams();
+
+ if (isPending) {
+ return ;
+ }
+
+ if (!data || data.length === 0) {
+ return (
+
+
+ No teams found
+
+
+ );
+ }
+
+ return (
+
+
+ Select Team
+
+ Select the primary team you want to use. You can change this later in
+ the settings.
+
+
+ {data?.map((eachTeam) => (
+ {
+ scheduleOnRN(setTeam, eachTeam.id.toString());
+ })}
+ >
+
+
+ {eachTeam.name}
+ {eachTeam.description}
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/components/SetupScreenContainer.tsx b/components/SetupScreenContainer.tsx
index 45d1de0..bf6b85f 100644
--- a/components/SetupScreenContainer.tsx
+++ b/components/SetupScreenContainer.tsx
@@ -1,10 +1,13 @@
+import { cn } from "@/lib/utils";
import * as SplashScreen from "expo-splash-screen";
import { KeyboardAvoidingView, ScrollView } from "react-native";
export default function SetupScreenContainer({
children,
+ scrollviewClassName,
}: {
children: React.ReactNode;
+ scrollviewClassName?: string;
}) {
return (
{children}
diff --git a/constants/StorageKeys.ts b/constants/StorageKeys.ts
index e038aa2..df1e26b 100644
--- a/constants/StorageKeys.ts
+++ b/constants/StorageKeys.ts
@@ -1,5 +1,6 @@
export const FAVORITES_STORAGE_KEY = "FAVORITES";
export const SETUP_COMPLETE_STORAGE_KEY = "SETUP_COMPLETE";
+export const TEAM_STORAGE_KEY = "CURRENT_TEAM";
export const PERMISSIONS_SAVED_STORAGE_KEY = "PERMISSIONS_SAVED";
export const LOGS_LINES_STORAGE_KEY = "LOGS_LINES";
export const LOG_REFETCH_INTERVAL_STORAGE_KEY = "LOG_REFETCH_INTERVAL";
diff --git a/hooks/useSetup.ts b/hooks/useSetup.ts
index 913d712..1d0090d 100644
--- a/hooks/useSetup.ts
+++ b/hooks/useSetup.ts
@@ -3,6 +3,7 @@ import { Secrets } from "@/constants/Secrets";
import {
PERMISSIONS_SAVED_STORAGE_KEY,
SETUP_COMPLETE_STORAGE_KEY,
+ TEAM_STORAGE_KEY,
} from "@/constants/StorageKeys";
import { isValidUrl } from "@/lib/utils";
import SecureStore from "@/utils/SecureStorage";
@@ -14,6 +15,7 @@ export default function useSetup() {
const [setupComplete, setSetupCompleteState] = useState(null);
const [serverAddress, setServerAddressState] = useState(null);
const [permissions, setPermissionsState] = useState(null);
+ const [team, setTeamState] = useState("NO_TEAM_SELECTED");
const setApiToken = async (key: string) => {
const { success, message } = await validateToken(key);
@@ -47,6 +49,11 @@ export default function useSetup() {
setPermissionsState(saved);
};
+ const setTeam = async (team: string) => {
+ await AsyncStorage.setItem(TEAM_STORAGE_KEY, team);
+ setTeamState(team);
+ };
+
const resetSetup = async () => {
await SecureStore.deleteItemAsync(Secrets.API_TOKEN);
await SecureStore.deleteItemAsync(Secrets.SERVER_ADDRESS);
@@ -75,16 +82,21 @@ export default function useSetup() {
AsyncStorage.getItem(PERMISSIONS_SAVED_STORAGE_KEY).then((value) => {
setPermissionsState(value === "true");
});
+ AsyncStorage.getItem(TEAM_STORAGE_KEY).then((value) => {
+ if (value) setTeamState(value);
+ });
}, []);
return {
setupComplete,
serverAddress,
permissions,
+ team,
setApiToken,
setServerAddress,
setSetupComplete,
setPermissions,
+ setTeam,
resetSetup,
};
}
From fe787976181fcf7a69bd32ba540644d58bde74e8 Mon Sep 17 00:00:00 2001
From: Jack <32645451+Jacxk@users.noreply.github.com>
Date: Tue, 27 Jan 2026 14:27:24 -0500
Subject: [PATCH 2/9] feat: implement resource filtering by team_id across all
API endpoints
---
api/application.ts | 18 +++++-
api/databases.ts | 22 +++++--
api/private-keys.ts | 15 ++++-
api/projects.ts | 46 ++++++++++++++-
api/resources.ts | 10 +++-
api/servers.ts | 15 ++++-
api/services.ts | 15 ++++-
api/types/project.types.ts | 4 +-
lib/storage.ts | 8 +++
lib/utils.ts | 116 +++++++++++++++++++++++++++++++++++++
10 files changed, 247 insertions(+), 22 deletions(-)
create mode 100644 lib/storage.ts
diff --git a/api/application.ts b/api/application.ts
index 8a6fe5d..4a4940a 100644
--- a/api/application.ts
+++ b/api/application.ts
@@ -1,4 +1,8 @@
import { queryClient } from "@/app/_layout";
+import {
+ filterResourceByTeam,
+ filterResourcesByTeam,
+} from "@/lib/utils";
import {
useMutation,
UseMutationOptions,
@@ -98,10 +102,14 @@ export const ApplicationKeys = {
// Fetch functions
export const getApplications = async () => {
const data = await coolifyFetch("/applications");
- data.forEach((app) =>
+ const filtered = await filterResourcesByTeam(
+ data,
+ (app) => app.destination.server.team_id,
+ );
+ filtered.forEach((app) =>
optimisticUpdateOne(ApplicationKeys.queries.single(app.uuid), app),
);
- return data;
+ return filtered;
};
export const getApplication = async (uuid: string) => {
@@ -109,7 +117,11 @@ export const getApplication = async (uuid: string) => {
queryKey: ApplicationKeys.queries.all(),
exact: true,
});
- return coolifyFetch(`/applications/${uuid}`);
+ const application = await coolifyFetch(`/applications/${uuid}`);
+ return filterResourceByTeam(
+ application,
+ (app) => app.destination.server.team_id,
+ );
};
export const getApplicationLogs = async (uuid: string, lines = 100) => {
diff --git a/api/databases.ts b/api/databases.ts
index bce5b4e..f09631b 100644
--- a/api/databases.ts
+++ b/api/databases.ts
@@ -1,4 +1,8 @@
import { queryClient } from "@/app/_layout";
+import {
+ filterResourceByTeam,
+ filterResourcesByTeam,
+} from "@/lib/utils";
import {
useMutation,
UseMutationOptions,
@@ -85,8 +89,12 @@ export const DatabaseKeys = {
*/
export const getDatabases = async () => {
const data = await coolifyFetch("/databases");
+ const filtered = await filterResourcesByTeam(
+ data,
+ (db) => db.destination.server.team_id,
+ );
// Set individual database cache entries
- data.forEach((database) => {
+ filtered.forEach((database) => {
optimisticUpdateInsertOneToMany(DatabaseKeys.queries.all(), database);
optimisticUpdateOne(DatabaseKeys.queries.single(database.uuid), database);
});
@@ -97,9 +105,15 @@ export const getDatabases = async () => {
export const getDatabase = async (uuid: string) => {
const data = await coolifyFetch(`/databases/${uuid}`);
- // Update the databases list cache with the new database
- optimisticUpdateInsertOneToMany(DatabaseKeys.queries.all(), data);
- return data;
+ const filtered = await filterResourceByTeam(
+ data,
+ (db) => db.destination.server.team_id,
+ );
+ if (filtered) {
+ // Update the databases list cache with the new database
+ optimisticUpdateInsertOneToMany(DatabaseKeys.queries.all(), filtered);
+ }
+ return filtered;
};
export const getDatabaseLogs = async (uuid: string, lines = 100) => {
diff --git a/api/private-keys.ts b/api/private-keys.ts
index 4ce3cee..b3dddea 100644
--- a/api/private-keys.ts
+++ b/api/private-keys.ts
@@ -1,3 +1,7 @@
+import {
+ filterResourceByTeam,
+ filterResourcesByTeam,
+} from "@/lib/utils";
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { coolifyFetch, optimisticUpdateOne } from "./client";
import { PrivateKey } from "./types/private-keys.types";
@@ -14,14 +18,19 @@ export const PrivateKeyKeys = {
// Fetch functions
export const getPrivateKeys = async () => {
const res = await coolifyFetch("/security/keys");
- res.forEach((key) =>
+ const filtered = await filterResourcesByTeam(
+ res,
+ (key) => key.team_id,
+ );
+ filtered.forEach((key) =>
optimisticUpdateOne(PrivateKeyKeys.queries.single(key.uuid), key)
);
- return res;
+ return filtered;
};
export const getPrivateKey = async (uuid: string) => {
- return coolifyFetch(`/security/keys/${uuid}`);
+ const key = await coolifyFetch(`/security/keys/${uuid}`);
+ return filterResourceByTeam(key, (k) => k.team_id);
};
// Query hooks
diff --git a/api/projects.ts b/api/projects.ts
index 0b6c3d3..e273f4b 100644
--- a/api/projects.ts
+++ b/api/projects.ts
@@ -1,4 +1,8 @@
import { queryClient } from "@/app/_layout";
+import {
+ filterResourceByTeam,
+ filterResourcesByTeam,
+} from "@/lib/utils";
import {
useMutation,
UseMutationOptions,
@@ -32,17 +36,53 @@ export const ProjectKeys = {
// Fetch functions
export const getProjects = async () => {
const data = await coolifyFetch("/projects");
- data.forEach((project) => {
+
+ // Check if team_id exists in the response (API might return it even if type doesn't)
+ const hasTeamId = data.length > 0 && 'team_id' in data[0];
+
+ let filtered: PartialProject[];
+ if (hasTeamId) {
+ // Filter directly if team_id is present
+ filtered = await filterResourcesByTeam(
+ data,
+ (project) => (project as PartialProject & { team_id?: number }).team_id,
+ );
+ } else {
+ // If team_id is not in the list response, fetch full projects to filter
+ // This is less efficient but necessary if API doesn't return team_id in list
+ const projectsWithTeam = await Promise.all(
+ data.map(async (project) => {
+ try {
+ const fullProject = await getProject(project.uuid);
+ return fullProject;
+ } catch {
+ return null;
+ }
+ })
+ );
+ const validProjects = projectsWithTeam.filter(
+ (p): p is Project => p !== null
+ );
+ const filteredProjects = await filterResourcesByTeam(
+ validProjects,
+ (project) => project.team_id,
+ );
+ // Convert back to PartialProject format
+ filtered = filteredProjects.map(({ team_id, created_at, updated_at, environments, ...rest }) => rest);
+ }
+
+ filtered.forEach((project) => {
queryClient.prefetchQuery({
queryKey: ProjectKeys.queries.single(project.uuid),
queryFn: () => getProject(project.uuid),
});
});
- return data;
+ return filtered;
};
export const getProject = async (uuid: string) => {
- return coolifyFetch(`/projects/${uuid}`);
+ const project = await coolifyFetch(`/projects/${uuid}`);
+ return filterResourceByTeam(project, (p) => p.team_id);
};
export const createProject = async (data: ProjectCreateBody) => {
diff --git a/api/resources.ts b/api/resources.ts
index e978a8f..f06356f 100644
--- a/api/resources.ts
+++ b/api/resources.ts
@@ -1,3 +1,4 @@
+import { filterResourcesByTeam } from "@/lib/utils";
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { ApplicationKeys, useApplication } from "./application";
import {
@@ -39,13 +40,18 @@ const getQueryKeyFromType = (type: ResourceFromListType) => {
// Fetch functions
export const getResources = async () => {
const data = await coolifyFetch("/resources");
- data.forEach((resource) => {
+ // Resources have team_id in destination.server.team_id (similar to applications/databases)
+ const filtered = await filterResourcesByTeam(
+ data,
+ (resource) => resource.destination?.server?.team_id,
+ );
+ filtered.forEach((resource) => {
const key = getQueryKeyFromType(resource.type);
optimisticUpdateInsertOneToMany(key, resource);
optimisticUpdateOne([...key, resource.uuid], resource);
});
- return data;
+ return filtered;
};
// Query hooks
diff --git a/api/servers.ts b/api/servers.ts
index b6fd7fc..e2b575c 100644
--- a/api/servers.ts
+++ b/api/servers.ts
@@ -1,4 +1,8 @@
import { queryClient } from "@/app/_layout";
+import {
+ filterResourceByTeam,
+ filterResourcesByTeam,
+} from "@/lib/utils";
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { coolifyFetch } from "./client";
import { Server, SingleServer } from "./types/server.types";
@@ -15,14 +19,19 @@ export const ServerKeys = {
// Fetch functions
export const getServers = async () => {
const data = await coolifyFetch("/servers");
- data.forEach((server) => {
+ const filtered = await filterResourcesByTeam(
+ data,
+ (server) => server.team_id,
+ );
+ filtered.forEach((server) => {
queryClient.setQueryData(ServerKeys.queries.single(server.uuid), server);
});
- return data;
+ return filtered;
};
export const getServer = async (uuid: string) => {
- return coolifyFetch(`/servers/${uuid}`);
+ const server = await coolifyFetch(`/servers/${uuid}`);
+ return filterResourceByTeam(server, (s) => s.team_id);
};
// Query hooks
diff --git a/api/services.ts b/api/services.ts
index 4d53ce2..ed09ca7 100644
--- a/api/services.ts
+++ b/api/services.ts
@@ -1,4 +1,8 @@
import { queryClient } from "@/app/_layout";
+import {
+ filterResourceByTeam,
+ filterResourcesByTeam,
+} from "@/lib/utils";
import {
useMutation,
UseMutationOptions,
@@ -80,10 +84,14 @@ export const ServiceKeys = {
// Fetch functions
export const getServices = async () => {
const data = await coolifyFetch("/services");
- data.forEach((service) =>
+ const filtered = await filterResourcesByTeam(
+ data,
+ (service) => service.server.team_id,
+ );
+ filtered.forEach((service) =>
optimisticUpdateOne(ServiceKeys.queries.single(service.uuid), service)
);
- return data;
+ return filtered;
};
export const getService = async (uuid: string) => {
@@ -91,7 +99,8 @@ export const getService = async (uuid: string) => {
queryKey: ServiceKeys.queries.all(),
exact: true,
});
- return coolifyFetch(`/services/${uuid}`);
+ const service = await coolifyFetch(`/services/${uuid}`);
+ return filterResourceByTeam(service, (s) => s.server.team_id);
};
export const getServiceLogs = async (uuid: string, lines = 100) => {
diff --git a/api/types/project.types.ts b/api/types/project.types.ts
index 3d26828..8fdc87c 100644
--- a/api/types/project.types.ts
+++ b/api/types/project.types.ts
@@ -15,7 +15,9 @@ export type ProjectBase = {
description: string;
};
-export type PartialProject = ProjectBase;
+export type PartialProject = ProjectBase & {
+ team_id?: number;
+};
export type Project = ProjectBase & {
team_id: number;
diff --git a/lib/storage.ts b/lib/storage.ts
new file mode 100644
index 0000000..62ed780
--- /dev/null
+++ b/lib/storage.ts
@@ -0,0 +1,8 @@
+import { TEAM_STORAGE_KEY } from "@/constants/StorageKeys";
+import AsyncStorage from "@react-native-async-storage/async-storage";
+
+export async function getCurrentTeam(apply?: (team: string | null) => void) {
+ const team = await AsyncStorage.getItem(TEAM_STORAGE_KEY);
+ apply?.(team);
+ return team;
+}
diff --git a/lib/utils.ts b/lib/utils.ts
index e1c6cfb..6b943f6 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -1,5 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
+import { getCurrentTeam } from "./storage";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
@@ -32,3 +33,118 @@ export function getDirtyData(
return acc;
}, {} as Partial);
}
+
+/**
+ * Filters a single resource by team_id. Returns null if the resource doesn't belong to the selected team.
+ * @overload
+ * @param resource - The resource to filter
+ * @param getTeamId - Function to extract team_id from the resource (currentTeam will be fetched automatically)
+ * @returns The resource if it belongs to the team, null otherwise
+ */
+export function filterResourceByTeam(
+ resource: T,
+ getTeamId: (resource: T) => number | string | undefined
+): Promise;
+/**
+ * Filters a single resource by team_id. Returns null if the resource doesn't belong to the selected team.
+ * @overload
+ * @param resource - The resource to filter
+ * @param currentTeam - The currently selected team ID (as string)
+ * @param getTeamId - Function to extract team_id from the resource
+ * @returns The resource if it belongs to the team, null otherwise
+ */
+export function filterResourceByTeam(
+ resource: T,
+ currentTeam: string | null,
+ getTeamId: (resource: T) => number | string | undefined
+): Promise;
+export async function filterResourceByTeam(
+ resource: T,
+ currentTeamOrGetTeamId: string | null | ((resource: T) => number | string | undefined),
+ getTeamId?: (resource: T) => number | string | undefined
+): Promise {
+ let currentTeam: string | null;
+ let teamIdExtractor: (resource: T) => number | string | undefined;
+
+ // Determine which overload was called
+ if (typeof currentTeamOrGetTeamId === 'function') {
+ // First overload: (resource, getTeamId)
+ currentTeam = await getCurrentTeam();
+ teamIdExtractor = currentTeamOrGetTeamId;
+ } else {
+ // Second overload: (resource, currentTeam, getTeamId)
+ currentTeam = currentTeamOrGetTeamId;
+ teamIdExtractor = getTeamId!;
+ }
+
+ if (!currentTeam) {
+ return resource;
+ }
+
+ const resourceTeamId = teamIdExtractor(resource);
+ if (resourceTeamId === undefined) {
+ return resource;
+ }
+
+ if (resourceTeamId.toString() !== currentTeam) {
+ return null;
+ }
+
+ return resource;
+}
+
+/**
+ * Filters an array of resources by team_id. Returns only resources that belong to the selected team.
+ * @overload
+ * @param resources - Array of resources to filter
+ * @param getTeamId - Function to extract team_id from each resource (currentTeam will be fetched automatically)
+ * @returns Array of resources that belong to the team
+ */
+export function filterResourcesByTeam(
+ resources: T[],
+ getTeamId: (resource: T) => number | string | undefined
+): Promise;
+/**
+ * Filters an array of resources by team_id. Returns only resources that belong to the selected team.
+ * @overload
+ * @param resources - Array of resources to filter
+ * @param currentTeam - The currently selected team ID (as string)
+ * @param getTeamId - Function to extract team_id from each resource
+ * @returns Array of resources that belong to the team
+ */
+export function filterResourcesByTeam(
+ resources: T[],
+ currentTeam: string | null,
+ getTeamId: (resource: T) => number | string | undefined
+): Promise;
+export async function filterResourcesByTeam(
+ resources: T[],
+ currentTeamOrGetTeamId: string | null | ((resource: T) => number | string | undefined),
+ getTeamId?: (resource: T) => number | string | undefined
+): Promise {
+ let currentTeam: string | null;
+ let teamIdExtractor: (resource: T) => number | string | undefined;
+
+ // Determine which overload was called
+ if (typeof currentTeamOrGetTeamId === 'function') {
+ // First overload: (resources, getTeamId)
+ currentTeam = await getCurrentTeam();
+ teamIdExtractor = currentTeamOrGetTeamId;
+ } else {
+ // Second overload: (resources, currentTeam, getTeamId)
+ currentTeam = currentTeamOrGetTeamId;
+ teamIdExtractor = getTeamId!;
+ }
+
+ if (!currentTeam) {
+ return resources;
+ }
+
+ return resources.filter((resource) => {
+ const resourceTeamId = teamIdExtractor(resource);
+ if (resourceTeamId === undefined) {
+ return true;
+ }
+ return resourceTeamId.toString() === currentTeam;
+ });
+}
\ No newline at end of file
From 8d29c6c15592ad00f5b003d3b0b500f70619c26f Mon Sep 17 00:00:00 2001
From: Jack <32645451+Jacxk@users.noreply.github.com>
Date: Tue, 27 Jan 2026 14:32:16 -0500
Subject: [PATCH 3/9] fix: update query options to allow null values on API
endpoints
---
api/application.ts | 15 +++++++--------
api/client.ts | 7 +++----
api/databases.ts | 12 +++++++-----
api/private-keys.ts | 2 +-
api/projects.ts | 2 +-
api/resources.ts | 2 +-
api/servers.ts | 4 ++--
api/services.ts | 2 +-
api/types/server.types.ts | 1 +
9 files changed, 24 insertions(+), 23 deletions(-)
diff --git a/api/application.ts b/api/application.ts
index 4a4940a..fee3f3d 100644
--- a/api/application.ts
+++ b/api/application.ts
@@ -1,8 +1,5 @@
import { queryClient } from "@/app/_layout";
-import {
- filterResourceByTeam,
- filterResourcesByTeam,
-} from "@/lib/utils";
+import { filterResourceByTeam, filterResourcesByTeam } from "@/lib/utils";
import {
useMutation,
UseMutationOptions,
@@ -229,7 +226,7 @@ export const useApplications = (
export const useApplication = (
uuid: string,
- options?: Omit, "queryKey">,
+ options?: Omit, "queryKey">,
) => {
return useQuery({
queryKey: ApplicationKeys.queries.single(uuid),
@@ -284,7 +281,7 @@ export const useCreateApplicationEnv = (
return update;
},
onError: onOptimisticUpdateError,
- onSettled: onOptimisticUpdateSettled(),
+ onSettled: onOptimisticUpdateSettled(ApplicationKeys.queries.envs(uuid)),
});
};
@@ -351,8 +348,10 @@ export const useUpdateApplication: UseUpdateApplication = (
return { update, insert };
},
onError: (error, variables, context) => {
- onOptimisticUpdateError(error, variables, context?.update);
- onOptimisticUpdateError(error, variables, context?.insert);
+ if (context) {
+ onOptimisticUpdateError(error, variables, context.update);
+ onOptimisticUpdateError(error, variables, context.insert);
+ }
},
onSettled: () => onOptimisticUpdateSettled(ApplicationKeys.queries.all())(),
});
diff --git a/api/client.ts b/api/client.ts
index 7e88d79..a99d6d9 100644
--- a/api/client.ts
+++ b/api/client.ts
@@ -107,10 +107,9 @@ export async function optimisticUpdateOne(
return { previousData, queryKey };
}
-export const onOptimisticUpdateError = (
- data: unknown,
- error: unknown,
- variables: unknown,
+export const onOptimisticUpdateError = (
+ error: Error,
+ variables: TVariables,
context?: {
queryKey: (string | number)[];
previousData: unknown;
diff --git a/api/databases.ts b/api/databases.ts
index f09631b..c873c9b 100644
--- a/api/databases.ts
+++ b/api/databases.ts
@@ -180,7 +180,7 @@ export const useDatabases = (
export const useDatabase = (
uuid: string,
- options?: Omit, "queryKey">
+ options?: Omit, "queryKey">
) => {
return useQuery({
queryKey: DatabaseKeys.queries.single(uuid),
@@ -250,10 +250,12 @@ export const useUpdateDatabase: UseUpdateDatabase = (uuid: string, options) => {
return { update, insert };
},
onError: (error, variables, context) => {
- onOptimisticUpdateError(error, variables, context?.update);
- onOptimisticUpdateError(error, variables, context?.insert);
+ if (context) {
+ onOptimisticUpdateError(error, variables, context.update);
+ onOptimisticUpdateError(error, variables, context.insert);
+ }
},
- onSettled: () => onOptimisticUpdateSettled(DatabaseKeys.queries.all())(),
+ onSettled: onOptimisticUpdateSettled(DatabaseKeys.queries.all()),
});
};
@@ -274,6 +276,6 @@ export const useCreateDatabase = (
type: CoolifyDatabases;
}) => createDatabase(body, type),
...options,
- onSettled: () => onOptimisticUpdateSettled(DatabaseKeys.queries.all())(),
+ onSettled: onOptimisticUpdateSettled(DatabaseKeys.queries.all()),
});
};
diff --git a/api/private-keys.ts b/api/private-keys.ts
index b3dddea..273e161 100644
--- a/api/private-keys.ts
+++ b/api/private-keys.ts
@@ -46,7 +46,7 @@ export const usePrivateKeys = (
export const usePrivateKey = (
uuid: string,
- options?: Omit, "queryKey">
+ options?: Omit, "queryKey">
) => {
return useQuery({
queryKey: PrivateKeyKeys.queries.single(uuid),
diff --git a/api/projects.ts b/api/projects.ts
index e273f4b..ef07799 100644
--- a/api/projects.ts
+++ b/api/projects.ts
@@ -117,7 +117,7 @@ export const useProjects = (
export const useProject = (
uuid: string,
- options?: Omit, "queryKey">
+ options?: Omit, "queryKey">
) => {
return useQuery({
queryKey: ProjectKeys.queries.single(uuid),
diff --git a/api/resources.ts b/api/resources.ts
index f06356f..1c49f20 100644
--- a/api/resources.ts
+++ b/api/resources.ts
@@ -58,7 +58,7 @@ export const getResources = async () => {
export const useResource = (
uuid: string,
type: ResourceType,
- options?: Omit, "queryKey">
+ options?: Omit, "queryKey">
) => {
if (!type) {
throw new Error("Resource type is required");
diff --git a/api/servers.ts b/api/servers.ts
index e2b575c..1ec6bc7 100644
--- a/api/servers.ts
+++ b/api/servers.ts
@@ -21,7 +21,7 @@ export const getServers = async () => {
const data = await coolifyFetch("/servers");
const filtered = await filterResourcesByTeam(
data,
- (server) => server.team_id,
+ (server) => server.team_id ?? undefined,
);
filtered.forEach((server) => {
queryClient.setQueryData(ServerKeys.queries.single(server.uuid), server);
@@ -47,7 +47,7 @@ export const useServers = (
export const useServer = (
uuid: string,
- options?: Omit, "queryKey">
+ options?: Omit, "queryKey">
) => {
return useQuery({
queryKey: ServerKeys.queries.single(uuid),
diff --git a/api/services.ts b/api/services.ts
index ed09ca7..65ccc93 100644
--- a/api/services.ts
+++ b/api/services.ts
@@ -176,7 +176,7 @@ export const useServices = (
export const useService = (
uuid: string,
- options?: Omit, "queryKey">
+ options?: Omit, "queryKey">
) => {
return useQuery({
queryKey: ServiceKeys.queries.single(uuid),
diff --git a/api/types/server.types.ts b/api/types/server.types.ts
index 780640d..a32719a 100644
--- a/api/types/server.types.ts
+++ b/api/types/server.types.ts
@@ -74,6 +74,7 @@ export type ServerBase = {
export type Server = ServerBase & {
proxy: ServerProxy;
+ team_id?: number;
};
export type SingleServer = ServerBase & {
From 0254699b87e52a12019b573436a41e17d314b222 Mon Sep 17 00:00:00 2001
From: Jack <32645451+Jacxk@users.noreply.github.com>
Date: Wed, 28 Jan 2026 09:19:01 -0500
Subject: [PATCH 4/9] fix: update team state initialization to allow null
values
---
hooks/useSetup.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/hooks/useSetup.ts b/hooks/useSetup.ts
index 1d0090d..9f2f4cb 100644
--- a/hooks/useSetup.ts
+++ b/hooks/useSetup.ts
@@ -15,7 +15,7 @@ export default function useSetup() {
const [setupComplete, setSetupCompleteState] = useState(null);
const [serverAddress, setServerAddressState] = useState(null);
const [permissions, setPermissionsState] = useState(null);
- const [team, setTeamState] = useState("NO_TEAM_SELECTED");
+ const [team, setTeamState] = useState(null);
const setApiToken = async (key: string) => {
const { success, message } = await validateToken(key);
@@ -83,7 +83,7 @@ export default function useSetup() {
setPermissionsState(value === "true");
});
AsyncStorage.getItem(TEAM_STORAGE_KEY).then((value) => {
- if (value) setTeamState(value);
+ setTeamState(value ?? "NO_TEAM_SELECTED");
});
}, []);
From ba15284535e060d8650d2ccbcf186462f9daebfe Mon Sep 17 00:00:00 2001
From: Jack <32645451+Jacxk@users.noreply.github.com>
Date: Wed, 28 Jan 2026 09:42:17 -0500
Subject: [PATCH 5/9] feat: add team selection validation and enhanced no teams
found screen
---
app/setup/team.tsx | 36 +++++++++++++++++++++++++------
lib/utils.ts | 53 +++++++++++++++++++++++++++-------------------
2 files changed, 60 insertions(+), 29 deletions(-)
diff --git a/app/setup/team.tsx b/app/setup/team.tsx
index c78a99f..7b5fbec 100644
--- a/app/setup/team.tsx
+++ b/app/setup/team.tsx
@@ -13,13 +13,14 @@ import { H1, P } from "@/components/ui/typography";
import useSetup from "@/hooks/useSetup";
import { cn } from "@/lib/utils";
import { Href, router, useLocalSearchParams } from "expo-router";
+import { openBrowserAsync } from "expo-web-browser";
import { View } from "react-native";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import { scheduleOnRN } from "react-native-worklets";
export default function TeamStep() {
const { redirect } = useLocalSearchParams<{ redirect: Href }>();
- const { team, setTeam } = useSetup();
+ const { serverAddress, team, setTeam } = useSetup();
const { data, isPending } = useTeams();
if (isPending) {
@@ -30,12 +31,32 @@ export default function TeamStep() {
return (
- No teams found
+ No teams found
+
+ We couldn't find any teams on the server. Please contact your
+ administrator. If you are the administrator, you can create a team
+ by{" "}
+
+
);
}
+ const isTeamSelected = team && team !== "NO_TEAM_SELECTED";
+ const onContinue = () => {
+ if (isTeamSelected) {
+ router.dismissTo(redirect ?? "/main");
+ }
+ };
+
return (
@@ -65,11 +86,12 @@ export default function TeamStep() {
))}
-
diff --git a/lib/utils.ts b/lib/utils.ts
index 6b943f6..7201505 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -13,18 +13,21 @@ export function isValidUrl(url: string) {
export function groupBy(
list: T[],
- getKey: (item: T) => K
+ getKey: (item: T) => K,
): Record {
- return list.reduce((acc, item) => {
- const key = getKey(item);
- (acc[key] ||= []).push(item);
- return acc;
- }, {} as Record);
+ return list.reduce(
+ (acc, item) => {
+ const key = getKey(item);
+ (acc[key] ||= []).push(item);
+ return acc;
+ },
+ {} as Record,
+ );
}
export function getDirtyData(
data: T,
- dirtyFields: Record
+ dirtyFields: Record,
): Partial {
return Object.keys(dirtyFields).reduce((acc, key) => {
if (dirtyFields[key]) {
@@ -43,7 +46,7 @@ export function getDirtyData(
*/
export function filterResourceByTeam(
resource: T,
- getTeamId: (resource: T) => number | string | undefined
+ getTeamId: (resource: T) => number | string | undefined,
): Promise;
/**
* Filters a single resource by team_id. Returns null if the resource doesn't belong to the selected team.
@@ -56,18 +59,21 @@ export function filterResourceByTeam(
export function filterResourceByTeam(
resource: T,
currentTeam: string | null,
- getTeamId: (resource: T) => number | string | undefined
+ getTeamId: (resource: T) => number | string | undefined,
): Promise;
export async function filterResourceByTeam(
resource: T,
- currentTeamOrGetTeamId: string | null | ((resource: T) => number | string | undefined),
- getTeamId?: (resource: T) => number | string | undefined
+ currentTeamOrGetTeamId:
+ | string
+ | null
+ | ((resource: T) => number | string | undefined),
+ getTeamId?: (resource: T) => number | string | undefined,
): Promise {
let currentTeam: string | null;
let teamIdExtractor: (resource: T) => number | string | undefined;
// Determine which overload was called
- if (typeof currentTeamOrGetTeamId === 'function') {
+ if (typeof currentTeamOrGetTeamId === "function") {
// First overload: (resource, getTeamId)
currentTeam = await getCurrentTeam();
teamIdExtractor = currentTeamOrGetTeamId;
@@ -77,8 +83,8 @@ export async function filterResourceByTeam(
teamIdExtractor = getTeamId!;
}
- if (!currentTeam) {
- return resource;
+ if (!currentTeam || currentTeam === "NO_TEAM_SELECTED") {
+ return null;
}
const resourceTeamId = teamIdExtractor(resource);
@@ -102,7 +108,7 @@ export async function filterResourceByTeam(
*/
export function filterResourcesByTeam(
resources: T[],
- getTeamId: (resource: T) => number | string | undefined
+ getTeamId: (resource: T) => number | string | undefined,
): Promise;
/**
* Filters an array of resources by team_id. Returns only resources that belong to the selected team.
@@ -115,18 +121,21 @@ export function filterResourcesByTeam(
export function filterResourcesByTeam(
resources: T[],
currentTeam: string | null,
- getTeamId: (resource: T) => number | string | undefined
+ getTeamId: (resource: T) => number | string | undefined,
): Promise;
export async function filterResourcesByTeam(
resources: T[],
- currentTeamOrGetTeamId: string | null | ((resource: T) => number | string | undefined),
- getTeamId?: (resource: T) => number | string | undefined
+ currentTeamOrGetTeamId:
+ | string
+ | null
+ | ((resource: T) => number | string | undefined),
+ getTeamId?: (resource: T) => number | string | undefined,
): Promise {
let currentTeam: string | null;
let teamIdExtractor: (resource: T) => number | string | undefined;
// Determine which overload was called
- if (typeof currentTeamOrGetTeamId === 'function') {
+ if (typeof currentTeamOrGetTeamId === "function") {
// First overload: (resources, getTeamId)
currentTeam = await getCurrentTeam();
teamIdExtractor = currentTeamOrGetTeamId;
@@ -136,8 +145,8 @@ export async function filterResourcesByTeam(
teamIdExtractor = getTeamId!;
}
- if (!currentTeam) {
- return resources;
+ if (!currentTeam || currentTeam === "NO_TEAM_SELECTED") {
+ return [];
}
return resources.filter((resource) => {
@@ -147,4 +156,4 @@ export async function filterResourcesByTeam(
}
return resourceTeamId.toString() === currentTeam;
});
-}
\ No newline at end of file
+}
From 8014bc4e3a0f35ab63b7c03ec7eb985d3193047a Mon Sep 17 00:00:00 2001
From: Jack <32645451+Jacxk@users.noreply.github.com>
Date: Wed, 28 Jan 2026 09:58:04 -0500
Subject: [PATCH 6/9] refactor: update navigation flow for team selection and
permissions step
---
app/setup/api_token.tsx | 5 ++++-
app/setup/team.tsx | 8 +++++---
2 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/app/setup/api_token.tsx b/app/setup/api_token.tsx
index 6c96423..b34a131 100644
--- a/app/setup/api_token.tsx
+++ b/app/setup/api_token.tsx
@@ -28,7 +28,10 @@ export default function ApiTokenStep() {
if (reconfigure) {
router.dismissTo("/main/settings");
} else {
- router.navigate("/setup/permissions");
+ router.navigate({
+ pathname: "/setup/team",
+ params: { nextStep: "permissions" },
+ });
}
})
.catch((e) => setError(e.message))
diff --git a/app/setup/team.tsx b/app/setup/team.tsx
index 7b5fbec..c65bac2 100644
--- a/app/setup/team.tsx
+++ b/app/setup/team.tsx
@@ -12,14 +12,14 @@ import { Text } from "@/components/ui/text";
import { H1, P } from "@/components/ui/typography";
import useSetup from "@/hooks/useSetup";
import { cn } from "@/lib/utils";
-import { Href, router, useLocalSearchParams } from "expo-router";
+import { router, useLocalSearchParams } from "expo-router";
import { openBrowserAsync } from "expo-web-browser";
import { View } from "react-native";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import { scheduleOnRN } from "react-native-worklets";
export default function TeamStep() {
- const { redirect } = useLocalSearchParams<{ redirect: Href }>();
+ const { nextStep } = useLocalSearchParams<{ nextStep: string }>();
const { serverAddress, team, setTeam } = useSetup();
const { data, isPending } = useTeams();
@@ -53,7 +53,9 @@ export default function TeamStep() {
const isTeamSelected = team && team !== "NO_TEAM_SELECTED";
const onContinue = () => {
if (isTeamSelected) {
- router.dismissTo(redirect ?? "/main");
+ router.dismissTo(
+ nextStep === "permissions" ? "/setup/permissions" : "/main",
+ );
}
};
From 6f060323bdb09f1ce45ddbcf1db8ef97a655baea Mon Sep 17 00:00:00 2001
From: Jack <32645451+Jacxk@users.noreply.github.com>
Date: Wed, 28 Jan 2026 17:00:46 -0500
Subject: [PATCH 7/9] refactor: change project types and fetching on API
---
api/projects.ts | 82 ++++++++++++------------------------
api/servers.ts | 11 ++---
api/types/project.types.ts | 4 --
app/main/projects/create.tsx | 6 +--
4 files changed, 34 insertions(+), 69 deletions(-)
diff --git a/api/projects.ts b/api/projects.ts
index ef07799..4e90864 100644
--- a/api/projects.ts
+++ b/api/projects.ts
@@ -1,8 +1,5 @@
import { queryClient } from "@/app/_layout";
-import {
- filterResourceByTeam,
- filterResourcesByTeam,
-} from "@/lib/utils";
+import { filterResourceByTeam } from "@/lib/utils";
import {
useMutation,
UseMutationOptions,
@@ -10,11 +7,7 @@ import {
UseQueryOptions,
} from "@tanstack/react-query";
import { coolifyFetch, onOptimisticUpdateError } from "./client";
-import {
- PartialProject,
- Project,
- ProjectCreateBody,
-} from "./types/project.types";
+import { Project, ProjectBase, ProjectCreateBody } from "./types/project.types";
import {
ResourceActionResponse,
ResourceCreateResponse,
@@ -35,49 +28,24 @@ export const ProjectKeys = {
// Fetch functions
export const getProjects = async () => {
- const data = await coolifyFetch("/projects");
-
- // Check if team_id exists in the response (API might return it even if type doesn't)
- const hasTeamId = data.length > 0 && 'team_id' in data[0];
-
- let filtered: PartialProject[];
- if (hasTeamId) {
- // Filter directly if team_id is present
- filtered = await filterResourcesByTeam(
- data,
- (project) => (project as PartialProject & { team_id?: number }).team_id,
- );
- } else {
- // If team_id is not in the list response, fetch full projects to filter
- // This is less efficient but necessary if API doesn't return team_id in list
- const projectsWithTeam = await Promise.all(
- data.map(async (project) => {
- try {
- const fullProject = await getProject(project.uuid);
- return fullProject;
- } catch {
- return null;
- }
- })
- );
- const validProjects = projectsWithTeam.filter(
- (p): p is Project => p !== null
- );
- const filteredProjects = await filterResourcesByTeam(
- validProjects,
- (project) => project.team_id,
- );
- // Convert back to PartialProject format
- filtered = filteredProjects.map(({ team_id, created_at, updated_at, environments, ...rest }) => rest);
- }
-
- filtered.forEach((project) => {
- queryClient.prefetchQuery({
- queryKey: ProjectKeys.queries.single(project.uuid),
- queryFn: () => getProject(project.uuid),
- });
+ const data = await coolifyFetch("/projects");
+
+ const projectsWithTeam = await Promise.all(
+ data.map(async (project) => {
+ try {
+ return await getProject(project.uuid);
+ } catch {
+ return null;
+ }
+ }),
+ );
+ const validProjects = projectsWithTeam.filter((p) => p !== null);
+
+ validProjects.forEach((project) => {
+ queryClient.setQueryData(ProjectKeys.queries.single(project.uuid), project);
});
- return filtered;
+
+ return validProjects;
};
export const getProject = async (uuid: string) => {
@@ -106,7 +74,7 @@ export const deleteProject = async (uuid: string) => {
// Query hooks
export const useProjects = (
- options?: Omit, "queryKey">
+ options?: Omit, "queryKey">,
) => {
return useQuery({
queryKey: ProjectKeys.queries.all(),
@@ -117,7 +85,7 @@ export const useProjects = (
export const useProject = (
uuid: string,
- options?: Omit, "queryKey">
+ options?: Omit, "queryKey">,
) => {
return useQuery({
queryKey: ProjectKeys.queries.single(uuid),
@@ -128,7 +96,11 @@ export const useProject = (
// Mutation hooks
export const useCreateProject = (
- options?: UseMutationOptions
+ options?: UseMutationOptions<
+ ResourceCreateResponse,
+ Error,
+ ProjectCreateBody
+ >,
) => {
return useMutation({
mutationKey: ProjectKeys.mutations.create(),
@@ -145,7 +117,7 @@ export const useCreateProject = (
export const useDeleteProject = (
uuid: string,
- options?: UseMutationOptions
+ options?: UseMutationOptions,
) => {
return useMutation({
mutationKey: ProjectKeys.mutations.delete(uuid),
diff --git a/api/servers.ts b/api/servers.ts
index 1ec6bc7..9d06545 100644
--- a/api/servers.ts
+++ b/api/servers.ts
@@ -1,8 +1,5 @@
import { queryClient } from "@/app/_layout";
-import {
- filterResourceByTeam,
- filterResourcesByTeam,
-} from "@/lib/utils";
+import { filterResourceByTeam, filterResourcesByTeam } from "@/lib/utils";
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { coolifyFetch } from "./client";
import { Server, SingleServer } from "./types/server.types";
@@ -21,7 +18,7 @@ export const getServers = async () => {
const data = await coolifyFetch("/servers");
const filtered = await filterResourcesByTeam(
data,
- (server) => server.team_id ?? undefined,
+ (server) => server.team_id,
);
filtered.forEach((server) => {
queryClient.setQueryData(ServerKeys.queries.single(server.uuid), server);
@@ -36,7 +33,7 @@ export const getServer = async (uuid: string) => {
// Query hooks
export const useServers = (
- options?: Omit, "queryKey">
+ options?: Omit, "queryKey">,
) => {
return useQuery({
queryKey: ServerKeys.queries.all(),
@@ -47,7 +44,7 @@ export const useServers = (
export const useServer = (
uuid: string,
- options?: Omit, "queryKey">
+ options?: Omit, "queryKey">,
) => {
return useQuery({
queryKey: ServerKeys.queries.single(uuid),
diff --git a/api/types/project.types.ts b/api/types/project.types.ts
index 8fdc87c..f15c2e8 100644
--- a/api/types/project.types.ts
+++ b/api/types/project.types.ts
@@ -15,10 +15,6 @@ export type ProjectBase = {
description: string;
};
-export type PartialProject = ProjectBase & {
- team_id?: number;
-};
-
export type Project = ProjectBase & {
team_id: number;
created_at: string;
diff --git a/app/main/projects/create.tsx b/app/main/projects/create.tsx
index a4efb20..b1b72d3 100644
--- a/app/main/projects/create.tsx
+++ b/app/main/projects/create.tsx
@@ -1,5 +1,5 @@
import { useCreateProject } from "@/api/projects";
-import { PartialProject } from "@/api/types/project.types";
+import { ProjectBase } from "@/api/types/project.types";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Text } from "@/components/ui/text";
@@ -13,7 +13,7 @@ export default function CreateProject() {
control,
formState: { isValid },
handleSubmit,
- } = useForm({
+ } = useForm({
defaultValues: {
name: "",
description: "",
@@ -22,7 +22,7 @@ export default function CreateProject() {
const { mutateAsync, isPending } = useCreateProject();
- const onSubmit = (data: PartialProject) => {
+ const onSubmit = (data: ProjectBase) => {
toast.promise(mutateAsync(data), {
loading: "Creating project...",
success: ({ uuid }) => {
From e80f2445efc3a41aa2cbf386149cb47f94b36fde Mon Sep 17 00:00:00 2001
From: Jack <32645451+Jacxk@users.noreply.github.com>
Date: Thu, 5 Feb 2026 20:42:33 -0600
Subject: [PATCH 8/9] fix: update redirect logic for setup and main routes
---
app/index.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/index.tsx b/app/index.tsx
index 73f2e16..a9921f2 100644
--- a/app/index.tsx
+++ b/app/index.tsx
@@ -9,7 +9,7 @@ export default function RootIndex() {
if (setupComplete === null || team === null) return null;
+ if (!setupComplete) return ;
if (team === "NO_TEAM_SELECTED") return ;
- if (setupComplete) return ;
- return ;
+ return ;
}
From 0ba1c7e959dd566be1dea65af0b8318724a44a91 Mon Sep 17 00:00:00 2001
From: Jack <32645451+Jacxk@users.noreply.github.com>
Date: Thu, 5 Feb 2026 20:44:25 -0600
Subject: [PATCH 9/9] chore: update project curosr rules to not alwaysApply
---
.cursor/rules/expo.mdc | 3 ++-
.cursor/rules/file-structure.mdc | 21 +++++++++++----------
2 files changed, 13 insertions(+), 11 deletions(-)
diff --git a/.cursor/rules/expo.mdc b/.cursor/rules/expo.mdc
index c71be58..dbae860 100644
--- a/.cursor/rules/expo.mdc
+++ b/.cursor/rules/expo.mdc
@@ -1,5 +1,6 @@
---
-alwaysApply: true
+description: Framework related rules for the project
+alwaysApply: false
---
This project is built using **Expo** and **NativeWind** with **TypeScript**. Please adhere to the following conventions and rules when writing, editing, or suggesting code:
diff --git a/.cursor/rules/file-structure.mdc b/.cursor/rules/file-structure.mdc
index 077a424..36aa294 100644
--- a/.cursor/rules/file-structure.mdc
+++ b/.cursor/rules/file-structure.mdc
@@ -1,5 +1,6 @@
---
-alwaysApply: true
+description: File structure related rules for the project
+alwaysApply: false
---
## Component Naming & Structure
@@ -33,15 +34,15 @@ This project uses Expo with Expo Router and NativeWind. Please follow these nami
### Examples:
-| File Path | Component Name |
-| -------------------------------------------- | -------------------------- |
-| `app/index.tsx` | `Index` |
-| `app/applications/index.tsx` | `ApplicationsIndex` |
-| `app/applications/[uuid]/index.tsx` | `ApplicationIndex` |
-| `app/applications/[uuid]/settings/index.tsx` | `ApplicationSettingsIndex` |
-| `app/auth/login.tsx` | `AuthLogin` |
-| `app/(user)/dashboard/overview.tsx` | `DashboardOverview` |
-| `app/main/applications/[uuid]/(tabs)/logs.tsx` | `MainApplicationsLogs` |
+| File Path | Component Name |
+| ---------------------------------------------- | -------------------------- |
+| `app/index.tsx` | `Index` |
+| `app/applications/index.tsx` | `ApplicationsIndex` |
+| `app/applications/[uuid]/index.tsx` | `ApplicationIndex` |
+| `app/applications/[uuid]/settings/index.tsx` | `ApplicationSettingsIndex` |
+| `app/auth/login.tsx` | `AuthLogin` |
+| `app/(user)/dashboard/overview.tsx` | `DashboardOverview` |
+| `app/main/applications/[uuid]/(tabs)/logs.tsx` | `MainApplicationsLogs` |
### Hooks