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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion app/(app)/[appName]/[objectName]/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import { useClient, useQuery, useView } from "@objectstack/client-react";
import { useTranslation } from "react-i18next";
import { useEffect, useState, useCallback, useMemo } from "react";
import { DetailViewRenderer } from "~/components/renderers";
import type { FormViewMeta } from "~/components/renderers";
import type { FormViewMeta, ActionMeta } from "~/components/renderers";
import { ScreenHeader } from "~/components/common/ScreenHeader";
import { useObjectMeta } from "~/hooks/useObjectMeta";
import { useRecordActions } from "~/hooks/useRecordActions";
import { isActionVisible } from "~/lib/record-actions";
import { renderRecordTitle } from "~/lib/record-title";

export default function ObjectDetailScreen() {
Expand Down Expand Up @@ -104,6 +106,31 @@ export default function ObjectDetailScreen() {
]);
}, [client, objectName, id, router, t]);

/* ---- Object actions (record_header inline, record_more overflow) ---- */
const allActions = useMemo<ActionMeta[]>(
() => ((meta?.actions as ActionMeta[] | undefined) ?? []).filter(isActionVisible),
[meta],
);
const headerActions = useMemo(
() =>
allActions.filter(
(a) => !a.locations || a.locations.includes("record_header"),
),
[allActions],
);
const moreActions = useMemo(
() => allActions.filter((a) => a.locations?.includes("record_more")),
[allActions],
);

const { runAction, busyName, modals } = useRecordActions({
client,
objectName: objectName!,
recordId: id!,
record,
onRefresh: fetchRecord,
});

return (
<SafeAreaView className="flex-1 bg-background" edges={["left", "right"]}>
<ScreenHeader title={String(displayName)} subtitle={positionLabel} />
Expand All @@ -119,12 +146,17 @@ export default function ObjectDetailScreen() {
router.push(`/(app)/${appName}/${objectName}/${id}/edit` as any)
}
onDelete={handleDelete}
actions={headerActions}
moreActions={moreActions}
onAction={runAction}
busyActionName={busyName}
onPrevious={handlePrevious}
onNext={handleNext}
hasPrevious={hasPrevious}
hasNext={hasNext}
positionLabel={positionLabel}
/>
{modals}
</SafeAreaView>
);
}
26 changes: 14 additions & 12 deletions app/(app)/[appName]/[objectName]/[id]/edit.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { SafeAreaView } from "react-native-safe-area-context";
import { View, Text, ActivityIndicator, Pressable } from "react-native";
import { View } from "react-native";
import { useLocalSearchParams, useRouter } from "expo-router";
import { useClient, useMutation } from "@objectstack/client-react";
import { useEffect, useState, useCallback } from "react";
import { AlertCircle } from "lucide-react-native";
import { FormViewRenderer } from "~/components/renderers";
import { ScreenHeader } from "~/components/common/ScreenHeader";
import { EmptyState } from "~/components/ui/EmptyState";
import { ListSkeleton } from "~/components/ui/ListSkeleton";
import { useObjectMeta } from "~/hooks/useObjectMeta";

export default function EditRecordScreen() {
Expand Down Expand Up @@ -55,8 +58,8 @@ export default function EditRecordScreen() {
return (
<SafeAreaView className="flex-1 bg-background" edges={["left", "right"]}>
<ScreenHeader title={`Edit ${displayName}`} />
<View className="flex-1 items-center justify-center">
<ActivityIndicator size="large" color="#1e40af" />
<View className="px-4 pt-4">
<ListSkeleton count={5} />
</View>
</SafeAreaView>
);
Expand All @@ -66,15 +69,14 @@ export default function EditRecordScreen() {
return (
<SafeAreaView className="flex-1 bg-background" edges={["left", "right"]}>
<ScreenHeader title={`Edit ${displayName}`} />
<View className="flex-1 items-center justify-center px-6">
<Text className="text-base text-destructive">{loadError}</Text>
<Pressable
className="mt-4 rounded-xl bg-primary px-5 py-3"
onPress={fetchRecord}
>
<Text className="font-semibold text-primary-foreground">Retry</Text>
</Pressable>
</View>
<EmptyState
icon={AlertCircle}
variant="error"
title="Couldn't Load Record"
description={loadError}
actionLabel="Retry"
onAction={fetchRecord}
/>
</SafeAreaView>
);
}
Expand Down
62 changes: 33 additions & 29 deletions app/(app)/[appName]/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { View, Text, ScrollView, Pressable, ActivityIndicator, Linking } from "react-native";
import { View, Text, ScrollView, Linking } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { useLocalSearchParams, useRouter } from "expo-router";
import { Inbox, ChevronRight } from "lucide-react-native";
import { Card, CardContent } from "~/components/ui/Card";
import { Inbox, ChevronRight, AlertCircle } from "lucide-react-native";
import { PressableCard } from "~/components/ui/PressableCard";
import { EmptyState } from "~/components/ui/EmptyState";
import { ListSkeleton } from "~/components/ui/ListSkeleton";
import { ScreenHeader } from "~/components/common/ScreenHeader";
import { useApp, type NavigationItem } from "~/hooks/useApps";
import { getIcon } from "~/lib/getIcon";
Expand Down Expand Up @@ -59,24 +61,23 @@ export default function AppHomeScreen() {
const Icon = getIcon(item.icon);
const navigable = isNavigable(item);
return (
<Pressable
<PressableCard
key={item.id}
disabled={!navigable}
haptic={navigable}
onPress={() => navigate(item)}
className={navigable ? "" : "opacity-40"}
className={`flex-row items-center p-3.5 ${navigable ? "" : "opacity-40"}`}
accessibilityRole={navigable ? "button" : undefined}
accessibilityLabel={item.label}
>
<Card>
<CardContent className="flex-row items-center py-3.5">
<View className="rounded-xl bg-primary/10 p-2.5">
<Icon size={20} color="#1e40af" />
</View>
<Text className="ml-3 flex-1 text-base font-medium text-card-foreground">
{item.label}
</Text>
{navigable ? <ChevronRight size={18} color="#94a3b8" /> : null}
</CardContent>
</Card>
</Pressable>
<View className="rounded-xl bg-primary/10 p-2.5">
<Icon size={20} color="#1e40af" />
</View>
<Text className="ml-3 flex-1 text-base font-medium text-card-foreground">
{item.label}
</Text>
{navigable ? <ChevronRight size={18} color="#94a3b8" /> : null}
</PressableCard>
);
};

Expand Down Expand Up @@ -105,22 +106,25 @@ export default function AppHomeScreen() {
<ScreenHeader title={displayName} backFallback="/(tabs)/apps" />
<ScrollView className="flex-1" contentContainerClassName="px-5 pb-8 pt-2">
{isLoading ? (
<View className="flex-1 items-center justify-center pt-20">
<ActivityIndicator size="large" color="#1e40af" />
<View className="pt-3">
<ListSkeleton count={6} />
</View>
) : error ? (
<View className="flex-1 items-center justify-center pt-20">
<Text className="text-base text-destructive">{error.message}</Text>
<View className="pt-20">
<EmptyState
icon={AlertCircle}
variant="error"
title="Couldn't Load App"
description={error.message}
/>
</View>
) : navigation.length === 0 ? (
<View className="flex-1 items-center justify-center pt-20">
<View className="rounded-2xl bg-muted p-6">
<Inbox size={40} color="#94a3b8" />
</View>
<Text className="mt-5 text-lg font-semibold text-foreground">No Navigation</Text>
<Text className="mt-2 text-center text-sm text-muted-foreground">
This app hasn&apos;t published a navigation menu yet.
</Text>
<View className="pt-20">
<EmptyState
icon={Inbox}
title="No Navigation"
description="This app hasn't published a navigation menu yet."
/>
</View>
) : (
<View>
Expand Down
48 changes: 27 additions & 21 deletions app/(app)/packages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import {
Text,
ScrollView,
TouchableOpacity,
ActivityIndicator,
Alert,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { Package, ToggleLeft, ToggleRight, Trash2 } from "lucide-react-native";
import { usePackageManagement } from "~/hooks/usePackageManagement";
import { Card, CardHeader, CardTitle, CardContent } from "~/components/ui/Card";
import { ScreenHeader } from "~/components/common/ScreenHeader";
import { EmptyState } from "~/components/ui/EmptyState";
import { ListSkeleton } from "~/components/ui/ListSkeleton";

/**
* Package management screen – list, enable, disable, uninstall packages.
Expand Down Expand Up @@ -58,31 +59,29 @@ export default function PackagesScreen() {
return (
<SafeAreaView className="flex-1 bg-background" edges={["left", "right"]}>
<ScreenHeader title="Packages" />
<ScrollView className="flex-1 bg-background">
<ScrollView className="flex-1 bg-background" contentContainerClassName="pb-4">
{isLoading && !packages.length ? (
<View className="flex-1 items-center justify-center py-20">
<ActivityIndicator size="large" color="#1e40af" />
<View className="p-4">
<ListSkeleton count={5} />
</View>
) : error ? (
<View className="flex-1 items-center justify-center px-6 py-20">
<Text className="text-destructive text-center mb-4">
{error.message}
</Text>
<TouchableOpacity
onPress={refetch}
className="bg-primary px-4 py-2 rounded-lg"
>
<Text className="text-primary-foreground font-medium">
Retry
</Text>
</TouchableOpacity>
<View className="pt-24">
<EmptyState
icon={Package}
variant="error"
title="Couldn't Load Packages"
description={error.message}
actionLabel="Retry"
onAction={refetch}
/>
</View>
) : !packages.length ? (
<View className="flex-1 items-center justify-center px-6 py-20">
<Package size={48} color="#9ca3af" />
<Text className="text-muted-foreground mt-4">
No packages installed
</Text>
<View className="pt-24">
<EmptyState
icon={Package}
title="No Packages"
description="No packages are installed yet."
/>
</View>
) : (
<View className="p-4 gap-3">
Expand All @@ -97,6 +96,10 @@ export default function PackagesScreen() {
<View className="flex-row items-center gap-3">
<TouchableOpacity
onPress={() => handleToggle(pkg.id, pkg.enabled)}
hitSlop={8}
accessibilityRole="switch"
accessibilityState={{ checked: pkg.enabled }}
accessibilityLabel={`${pkg.enabled ? "Disable" : "Enable"} ${pkg.label}`}
>
{pkg.enabled ? (
<ToggleRight size={24} color="#16a34a" />
Expand All @@ -106,6 +109,9 @@ export default function PackagesScreen() {
</TouchableOpacity>
<TouchableOpacity
onPress={() => handleUninstall(pkg.id, pkg.label)}
hitSlop={8}
accessibilityRole="button"
accessibilityLabel={`Uninstall ${pkg.label}`}
>
<Trash2 size={18} color="#dc2626" />
</TouchableOpacity>
Expand Down
12 changes: 8 additions & 4 deletions app/(app)/page/[id].tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, { useEffect, useState } from "react";
import { View, Text } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { useLocalSearchParams } from "expo-router";
import { useClient } from "@objectstack/client-react";
import { AlertCircle } from "lucide-react-native";
import { ScreenHeader } from "~/components/common/ScreenHeader";
import { EmptyState } from "~/components/ui/EmptyState";
import { PageRenderer } from "~/components/renderers/PageRenderer";
import {
validatePageSchema,
Expand Down Expand Up @@ -60,9 +61,12 @@ export default function SDUIPageScreen() {
<SafeAreaView className="flex-1 bg-background" edges={["left", "right"]}>
<ScreenHeader title={schema?.label ?? id ?? "Page"} />
{error && !isLoading ? (
<View className="flex-1 items-center justify-center px-6">
<Text className="text-destructive text-center">{error.message}</Text>
</View>
<EmptyState
icon={AlertCircle}
variant="error"
title="Couldn't Load Page"
description={error.message}
/>
) : schema ? (
<PageRenderer schema={schema} isLoading={isLoading} />
) : (
Expand Down
Loading
Loading