mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-31 13:42:50 +00:00
* feat(app): add toolbar and loading states to the GitHub PR panel Adds a toolbar under the PR title with a refresh button (refreshes git and GitHub status + timeline in one tap), an always-visible View Pull Request button, and a Merge control with a method dropdown that is disabled-with-reason when the PR can't be merged yet. The merge control reuses the existing git-actions policy verbatim via a new buildPrPanelMergeActions selector fed into GitActionsSplitButton, so no merge readiness/squash/auto-merge logic is duplicated. Also replaces the empty-state flash with skeletons: a full-panel skeleton while PR status loads and an activity skeleton while the timeline loads, with empty states shown only when loaded-and-empty and an error state with retry. * refactor(app): tidy PR panel skeleton (i18n, shared pulse, index) Addresses review feedback on the PR panel work: - Localize the skeleton's "Checks" label via t() instead of hardcoding. - Hoist the shared SkeletonPulse + useSkeletonPulse into activity-skeleton so pane-skeleton reuses them instead of duplicating the pulse driver. - Export PullRequestPaneError/Skeleton from the module index and import them through it, matching the module's existing public surface. * test(app): remove mock-based PR pane test * fix(app): simplify PR panel toolbar * fix(app): report PR panel retry failures
88 lines
2.3 KiB
TypeScript
88 lines
2.3 KiB
TypeScript
import { useEffect, useMemo, useRef } from "react";
|
|
import { Animated, View, type StyleProp, type ViewStyle } from "react-native";
|
|
import { StyleSheet } from "react-native-unistyles";
|
|
|
|
const ROW_KEYS = [0, 1, 2].map((i) => `pr-activity-skeleton-row-${i}`);
|
|
|
|
export function useSkeletonPulse(): Animated.Value {
|
|
const pulse = useRef(new Animated.Value(0)).current;
|
|
useEffect(() => {
|
|
const animation = Animated.loop(
|
|
Animated.sequence([
|
|
Animated.timing(pulse, { toValue: 1, duration: 1000, useNativeDriver: true }),
|
|
Animated.timing(pulse, { toValue: 0, duration: 1000, useNativeDriver: true }),
|
|
]),
|
|
);
|
|
animation.start();
|
|
return () => animation.stop();
|
|
}, [pulse]);
|
|
return pulse;
|
|
}
|
|
|
|
export function SkeletonPulse({
|
|
pulse,
|
|
style,
|
|
}: {
|
|
pulse: Animated.Value;
|
|
style: StyleProp<ViewStyle>;
|
|
}) {
|
|
const opacity = pulse.interpolate({
|
|
inputRange: [0, 1],
|
|
outputRange: [0.4, 0.8],
|
|
});
|
|
const pulseStyle = useMemo(() => [style, { opacity }], [style, opacity]);
|
|
return <Animated.View style={pulseStyle} />;
|
|
}
|
|
|
|
export function PrActivitySkeleton() {
|
|
const pulse = useSkeletonPulse();
|
|
|
|
return (
|
|
<View style={styles.container} testID="pr-pane-activity-skeleton">
|
|
{ROW_KEYS.map((key) => (
|
|
<View key={key} style={styles.row}>
|
|
<SkeletonPulse pulse={pulse} style={styles.avatar} />
|
|
<View style={styles.lines}>
|
|
<SkeletonPulse pulse={pulse} style={styles.lineWide} />
|
|
<SkeletonPulse pulse={pulse} style={styles.lineNarrow} />
|
|
</View>
|
|
</View>
|
|
))}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create((theme) => ({
|
|
container: {
|
|
paddingHorizontal: theme.spacing[3],
|
|
gap: theme.spacing[3],
|
|
},
|
|
row: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: theme.spacing[2],
|
|
},
|
|
avatar: {
|
|
width: 20,
|
|
height: 20,
|
|
borderRadius: theme.borderRadius.full,
|
|
backgroundColor: theme.colors.surface2,
|
|
},
|
|
lines: {
|
|
flex: 1,
|
|
gap: theme.spacing[1],
|
|
},
|
|
lineWide: {
|
|
width: "70%",
|
|
height: 12,
|
|
borderRadius: theme.borderRadius.sm,
|
|
backgroundColor: theme.colors.surface2,
|
|
},
|
|
lineNarrow: {
|
|
width: "45%",
|
|
height: 10,
|
|
borderRadius: theme.borderRadius.sm,
|
|
backgroundColor: theme.colors.surface2,
|
|
},
|
|
}));
|