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