Skip to content

Commit c3e1550

Browse files
authored
Merge pull request #7 from objectstack-ai/copilot/add-objectql-data-layer
2 parents 8e2e494 + 79eebc7 commit c3e1550

26 files changed

Lines changed: 2219 additions & 19 deletions
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import React from "react";
2+
import { View, Text, Pressable } from "react-native";
3+
import { Trash2, Edit3, X } from "lucide-react-native";
4+
import { cn } from "~/lib/utils";
5+
6+
export interface BatchActionBarProps {
7+
/** Number of selected records */
8+
selectedCount: number;
9+
/** Called when user taps batch-delete */
10+
onBatchDelete?: () => void;
11+
/** Called when user taps batch-edit (optional) */
12+
onBatchEdit?: () => void;
13+
/** Clear selection */
14+
onClearSelection: () => void;
15+
className?: string;
16+
}
17+
18+
/**
19+
* Action bar shown at the bottom when records are multi-selected.
20+
*/
21+
export function BatchActionBar({
22+
selectedCount,
23+
onBatchDelete,
24+
onBatchEdit,
25+
onClearSelection,
26+
className,
27+
}: BatchActionBarProps) {
28+
if (selectedCount === 0) return null;
29+
30+
return (
31+
<View
32+
className={cn(
33+
"flex-row items-center justify-between border-t border-border bg-card px-4 py-3",
34+
className,
35+
)}
36+
>
37+
<View className="flex-row items-center gap-2">
38+
<Pressable onPress={onClearSelection} className="rounded-full bg-muted p-1.5">
39+
<X size={14} color="#64748b" />
40+
</Pressable>
41+
<Text className="text-sm font-medium text-foreground">
42+
{selectedCount} selected
43+
</Text>
44+
</View>
45+
46+
<View className="flex-row items-center gap-3">
47+
{onBatchEdit && (
48+
<Pressable
49+
onPress={onBatchEdit}
50+
className="flex-row items-center rounded-lg bg-primary/10 px-3 py-2"
51+
>
52+
<Edit3 size={14} color="#1e40af" />
53+
<Text className="ml-1.5 text-xs font-semibold text-primary">Edit</Text>
54+
</Pressable>
55+
)}
56+
{onBatchDelete && (
57+
<Pressable
58+
onPress={onBatchDelete}
59+
className="flex-row items-center rounded-lg bg-destructive/10 px-3 py-2"
60+
>
61+
<Trash2 size={14} color="#ef4444" />
62+
<Text className="ml-1.5 text-xs font-semibold text-destructive">Delete</Text>
63+
</Pressable>
64+
)}
65+
</View>
66+
</View>
67+
);
68+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import React from "react";
2+
import { View, Text } from "react-native";
3+
import { cn } from "~/lib/utils";
4+
import type { BatchProgress, BatchResult } from "~/hooks/useBatchOperations";
5+
6+
export interface BatchProgressIndicatorProps {
7+
/** Current progress (while processing) */
8+
progress: BatchProgress | null;
9+
/** Final result (after completion) */
10+
result: BatchResult | null;
11+
className?: string;
12+
}
13+
14+
/**
15+
* Displays a progress bar and summary for batch operations.
16+
*/
17+
export function BatchProgressIndicator({
18+
progress,
19+
result,
20+
className,
21+
}: BatchProgressIndicatorProps) {
22+
if (!progress && !result) return null;
23+
24+
// If we have a final result, show the summary
25+
if (result && (!progress || progress.completed >= progress.total)) {
26+
return (
27+
<View className={cn("rounded-xl border border-border bg-card p-4", className)}>
28+
<Text className="text-sm font-medium text-foreground">
29+
Batch complete: {result.succeeded} succeeded, {result.failed} failed
30+
</Text>
31+
{result.errors.length > 0 && (
32+
<View className="mt-2">
33+
{result.errors.slice(0, 5).map((err, i) => (
34+
<Text key={i} className="text-xs text-destructive">
35+
{err.recordId ? `Record ${err.recordId}: ` : ""}{err.message}
36+
</Text>
37+
))}
38+
{result.errors.length > 5 && (
39+
<Text className="mt-1 text-xs text-muted-foreground">
40+
…and {result.errors.length - 5} more errors
41+
</Text>
42+
)}
43+
</View>
44+
)}
45+
</View>
46+
);
47+
}
48+
49+
// In-progress state
50+
if (!progress) return null;
51+
const pct = progress.total > 0 ? (progress.completed / progress.total) * 100 : 0;
52+
53+
return (
54+
<View className={cn("rounded-xl border border-border bg-card p-4", className)}>
55+
<View className="mb-2 flex-row items-center justify-between">
56+
<Text className="text-sm font-medium text-foreground">
57+
Processing… {progress.completed}/{progress.total}
58+
</Text>
59+
{progress.failed > 0 && (
60+
<Text className="text-xs text-destructive">{progress.failed} failed</Text>
61+
)}
62+
</View>
63+
{/* Progress bar */}
64+
<View className="h-2 rounded-full bg-muted">
65+
<View
66+
className="h-2 rounded-full bg-primary"
67+
style={{ width: `${Math.min(pct, 100)}%` }}
68+
/>
69+
</View>
70+
</View>
71+
);
72+
}

components/batch/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export { BatchActionBar } from "./BatchActionBar";
2+
export type { BatchActionBarProps } from "./BatchActionBar";
3+
export { BatchProgressIndicator } from "./BatchProgressIndicator";
4+
export type { BatchProgressIndicatorProps } from "./BatchProgressIndicator";
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import React from "react";
2+
import { View, Text, Pressable } from "react-native";
3+
import { WifiOff, RefreshCw } from "lucide-react-native";
4+
import { cn } from "~/lib/utils";
5+
6+
export interface OfflineIndicatorProps {
7+
isOffline: boolean;
8+
pendingCount?: number;
9+
isSyncing?: boolean;
10+
onSyncPress?: () => void;
11+
className?: string;
12+
}
13+
14+
/**
15+
* Banner that appears when the device is offline.
16+
* Shows pending mutation count and a manual sync button.
17+
*/
18+
export function OfflineIndicator({
19+
isOffline,
20+
pendingCount = 0,
21+
isSyncing = false,
22+
onSyncPress,
23+
className,
24+
}: OfflineIndicatorProps) {
25+
if (!isOffline && pendingCount === 0) return null;
26+
27+
return (
28+
<View
29+
className={cn(
30+
"flex-row items-center justify-between px-4 py-2",
31+
isOffline ? "bg-amber-100 dark:bg-amber-900/30" : "bg-blue-50 dark:bg-blue-900/20",
32+
className,
33+
)}
34+
>
35+
<View className="flex-row items-center gap-2">
36+
{isOffline && <WifiOff size={14} color="#d97706" />}
37+
<Text className="text-xs font-medium text-foreground">
38+
{isOffline
39+
? "You are offline"
40+
: `Syncing ${pendingCount} change${pendingCount !== 1 ? "s" : ""}…`}
41+
</Text>
42+
{pendingCount > 0 && isOffline && (
43+
<Text className="text-xs text-muted-foreground">
44+
{pendingCount} pending
45+
</Text>
46+
)}
47+
</View>
48+
49+
{!isOffline && pendingCount > 0 && onSyncPress && (
50+
<Pressable
51+
onPress={onSyncPress}
52+
disabled={isSyncing}
53+
className="flex-row items-center rounded-lg bg-primary/10 px-2.5 py-1"
54+
>
55+
<RefreshCw size={12} color="#1e40af" />
56+
<Text className="ml-1 text-xs font-medium text-primary">
57+
{isSyncing ? "Syncing…" : "Sync"}
58+
</Text>
59+
</Pressable>
60+
)}
61+
</View>
62+
);
63+
}

components/query/FilterRow.tsx

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import React, { useMemo } from "react";
2+
import { View, Text, TextInput, Pressable } from "react-native";
3+
import { X } from "lucide-react-native";
4+
import type { FieldDefinition } from "~/components/renderers/types";
5+
import {
6+
type SimpleFilter,
7+
type FilterOperator,
8+
OPERATOR_META,
9+
operatorsForFieldType,
10+
} from "~/lib/query-builder";
11+
12+
/* ------------------------------------------------------------------ */
13+
/* Props */
14+
/* ------------------------------------------------------------------ */
15+
16+
export interface FilterRowProps {
17+
filter: SimpleFilter;
18+
fields: FieldDefinition[];
19+
onUpdate: (patch: Partial<SimpleFilter>) => void;
20+
onRemove: () => void;
21+
}
22+
23+
/* ------------------------------------------------------------------ */
24+
/* Component */
25+
/* ------------------------------------------------------------------ */
26+
27+
export function FilterRow({ filter, fields, onUpdate, onRemove }: FilterRowProps) {
28+
const selectedFieldDef = fields.find((f) => f.name === filter.field);
29+
const fieldType = selectedFieldDef?.type ?? "text";
30+
31+
const availableOperators = useMemo(
32+
() => operatorsForFieldType(fieldType),
33+
[fieldType],
34+
);
35+
36+
const operatorMeta = OPERATOR_META[filter.operator];
37+
const needsValue = operatorMeta?.valueCount !== 0;
38+
const needsSecondValue = operatorMeta?.valueCount === 2;
39+
40+
return (
41+
<View className="mb-2 flex-row items-center gap-2">
42+
{/* Field picker (simplified as a scrollable row of chips) */}
43+
<Pressable
44+
className="flex-1 rounded-lg border border-input bg-background px-3 py-2"
45+
onPress={() => {
46+
// Cycle through fields for simplicity
47+
const currentIdx = fields.findIndex((f) => f.name === filter.field);
48+
const nextField = fields[(currentIdx + 1) % fields.length];
49+
if (nextField) {
50+
onUpdate({ field: nextField.name });
51+
}
52+
}}
53+
>
54+
<Text className="text-xs text-foreground" numberOfLines={1}>
55+
{selectedFieldDef?.label ?? (filter.field || "Field…")}
56+
</Text>
57+
</Pressable>
58+
59+
{/* Operator picker */}
60+
<Pressable
61+
className="rounded-lg border border-input bg-background px-2 py-2"
62+
onPress={() => {
63+
const currentIdx = availableOperators.indexOf(filter.operator);
64+
const next = availableOperators[(currentIdx + 1) % availableOperators.length];
65+
if (next) onUpdate({ operator: next });
66+
}}
67+
>
68+
<Text className="text-xs text-muted-foreground" numberOfLines={1}>
69+
{operatorMeta?.label ?? filter.operator}
70+
</Text>
71+
</Pressable>
72+
73+
{/* Value input */}
74+
{needsValue && (
75+
<TextInput
76+
className="flex-1 rounded-lg border border-input bg-background px-3 py-2 text-xs text-foreground"
77+
value={String(filter.value ?? "")}
78+
onChangeText={(text) => onUpdate({ value: text })}
79+
placeholder="Value"
80+
placeholderTextColor="#94a3b8"
81+
/>
82+
)}
83+
84+
{/* Second value (between) */}
85+
{needsSecondValue && (
86+
<TextInput
87+
className="flex-1 rounded-lg border border-input bg-background px-3 py-2 text-xs text-foreground"
88+
value={String(filter.value2 ?? "")}
89+
onChangeText={(text) => onUpdate({ value2: text } as Partial<SimpleFilter>)}
90+
placeholder="To"
91+
placeholderTextColor="#94a3b8"
92+
/>
93+
)}
94+
95+
{/* Remove */}
96+
<Pressable onPress={onRemove} className="p-1">
97+
<X size={16} color="#ef4444" />
98+
</Pressable>
99+
</View>
100+
);
101+
}

components/query/GlobalSearch.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import React from "react";
2+
import { View, TextInput } from "react-native";
3+
import { Search } from "lucide-react-native";
4+
import { cn } from "~/lib/utils";
5+
6+
export interface GlobalSearchProps {
7+
value: string;
8+
onChangeText: (text: string) => void;
9+
placeholder?: string;
10+
className?: string;
11+
}
12+
13+
/**
14+
* Global full-text search input.
15+
* Designed to sit above list views for cross-field filtering.
16+
*/
17+
export function GlobalSearch({
18+
value,
19+
onChangeText,
20+
placeholder = "Search across all fields…",
21+
className,
22+
}: GlobalSearchProps) {
23+
return (
24+
<View
25+
className={cn(
26+
"flex-row items-center gap-2 rounded-xl border border-primary/30 bg-primary/5 px-3",
27+
className,
28+
)}
29+
>
30+
<Search size={18} color="#1e40af" />
31+
<TextInput
32+
className="h-11 flex-1 text-sm text-foreground placeholder:text-muted-foreground"
33+
value={value}
34+
onChangeText={onChangeText}
35+
placeholder={placeholder}
36+
placeholderTextColor="#94a3b8"
37+
returnKeyType="search"
38+
/>
39+
</View>
40+
);
41+
}

0 commit comments

Comments
 (0)