@comergehq/studio 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +134 -9
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +135 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/ChatMessageBubble.tsx +5 -2
- package/src/data/apps/remote.ts +9 -0
- package/src/data/apps/repository.ts +7 -0
- package/src/data/apps/types.ts +15 -0
- package/src/studio/ComergeStudio.tsx +22 -0
- package/src/studio/ui/PreviewPanel.tsx +24 -4
- package/src/studio/ui/StudioOverlay.tsx +9 -0
- package/src/studio/ui/preview-panel/PreviewCollaborateSection.tsx +95 -3
- package/src/studio/ui/preview-panel/usePreviewPanelData.ts +8 -0
package/package.json
CHANGED
|
@@ -25,6 +25,9 @@ export function ChatMessageBubble({ message, renderContent, style }: ChatMessage
|
|
|
25
25
|
const isMergeRejected = metaEvent === 'merge_request.rejected';
|
|
26
26
|
const isMergeCompleted = metaEvent === 'merge.completed';
|
|
27
27
|
|
|
28
|
+
const isSyncStarted = metaEvent === 'sync.started';
|
|
29
|
+
const isSyncCompleted = metaEvent === 'sync.completed';
|
|
30
|
+
|
|
28
31
|
const isHuman = message.author === 'human' || isMergeApproved || isMergeRejected;
|
|
29
32
|
|
|
30
33
|
const align: ViewStyle = { alignSelf: isHuman ? 'flex-end' : 'flex-start' };
|
|
@@ -51,10 +54,10 @@ export function ChatMessageBubble({ message, renderContent, style }: ChatMessage
|
|
|
51
54
|
]}
|
|
52
55
|
>
|
|
53
56
|
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
|
54
|
-
{isMergeCompleted ? (
|
|
57
|
+
{isMergeCompleted || isSyncCompleted ? (
|
|
55
58
|
<CheckCheck size={16} color={theme.colors.success} style={{ marginRight: theme.spacing.sm }} />
|
|
56
59
|
) : null}
|
|
57
|
-
{isMergeApproved ? (
|
|
60
|
+
{isMergeApproved || isSyncStarted ? (
|
|
58
61
|
<GitMerge size={16} color={theme.colors.text} style={{ marginRight: theme.spacing.sm }} />
|
|
59
62
|
) : null}
|
|
60
63
|
<View style={{ flexShrink: 1, minWidth: 0 }}>
|
package/src/data/apps/remote.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
ListLikedAppsParams,
|
|
15
15
|
ListPublicAppsParams,
|
|
16
16
|
LikedAppsList,
|
|
17
|
+
SyncUpstreamResponse,
|
|
17
18
|
} from './types';
|
|
18
19
|
|
|
19
20
|
export interface AppsRemoteDataSource {
|
|
@@ -26,6 +27,7 @@ export interface AppsRemoteDataSource {
|
|
|
26
27
|
getInsights(appId: string): Promise<ServiceResponse<AppInsights>>;
|
|
27
28
|
getAnalytics(appId: string, params: AppAnalyticsParams): Promise<ServiceResponse<AppAnalyticsPoint[]>>;
|
|
28
29
|
importFromGithub(payload: ImportGithubAppRequest): Promise<ServiceResponse<ImportGithubAppResponse>>;
|
|
30
|
+
syncUpstream(appId: string): Promise<ServiceResponse<SyncUpstreamResponse>>;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
class AppsRemoteDataSourceImpl extends BaseRemote implements AppsRemoteDataSource {
|
|
@@ -90,6 +92,13 @@ class AppsRemoteDataSourceImpl extends BaseRemote implements AppsRemoteDataSourc
|
|
|
90
92
|
);
|
|
91
93
|
return data;
|
|
92
94
|
}
|
|
95
|
+
|
|
96
|
+
async syncUpstream(appId: string): Promise<ServiceResponse<SyncUpstreamResponse>> {
|
|
97
|
+
const { data } = await api.post<ServiceResponse<SyncUpstreamResponse>>(
|
|
98
|
+
`/v1/apps/${encodeURIComponent(appId)}/sync-upstream`,
|
|
99
|
+
);
|
|
100
|
+
return data;
|
|
101
|
+
}
|
|
93
102
|
}
|
|
94
103
|
|
|
95
104
|
export const appsRemoteDataSource: AppsRemoteDataSource = new AppsRemoteDataSourceImpl();
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
ListLikedAppsParams,
|
|
12
12
|
ListPublicAppsParams,
|
|
13
13
|
LikedAppsList,
|
|
14
|
+
SyncUpstreamResponse,
|
|
14
15
|
} from './types';
|
|
15
16
|
import { appsRemoteDataSource } from './remote';
|
|
16
17
|
import type { AppsRemoteDataSource } from './remote';
|
|
@@ -82,6 +83,7 @@ export interface AppsRepository {
|
|
|
82
83
|
subscribeCreatedApps(userId: string, handlers: AppSubscriptionHandlers): () => void;
|
|
83
84
|
subscribeApp(appId: string, handlers: AppSubscriptionHandlers): () => void;
|
|
84
85
|
importFromGithub(payload: ImportGithubAppRequest): Promise<ImportGithubAppResponse>;
|
|
86
|
+
syncUpstream(appId: string): Promise<SyncUpstreamResponse>;
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
class AppsRepositoryImpl extends BaseRepository implements AppsRepository {
|
|
@@ -134,6 +136,11 @@ class AppsRepositoryImpl extends BaseRepository implements AppsRepository {
|
|
|
134
136
|
return this.unwrapOrThrow(res);
|
|
135
137
|
}
|
|
136
138
|
|
|
139
|
+
async syncUpstream(appId: string): Promise<SyncUpstreamResponse> {
|
|
140
|
+
const res = await this.remote.syncUpstream(appId);
|
|
141
|
+
return this.unwrapOrThrow(res);
|
|
142
|
+
}
|
|
143
|
+
|
|
137
144
|
subscribeCreatedApps(userId: string, handlers: AppSubscriptionHandlers): () => void {
|
|
138
145
|
if (!userId) return () => {};
|
|
139
146
|
return this.subscribeToAppChannel(`apps:createdBy:${userId}`, `created_by=eq.${userId}`, handlers);
|
package/src/data/apps/types.ts
CHANGED
|
@@ -29,6 +29,8 @@ export type MergeRequestEntry = {
|
|
|
29
29
|
createdAt: string;
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
+
export type SyncRequestEntry = MergeRequestEntry;
|
|
33
|
+
|
|
32
34
|
export type LikeEntry = {
|
|
33
35
|
userId: string;
|
|
34
36
|
createdAt: string;
|
|
@@ -63,6 +65,12 @@ export type AppInsights = {
|
|
|
63
65
|
merged: number;
|
|
64
66
|
entries: MergeRequestEntry[];
|
|
65
67
|
};
|
|
68
|
+
syncs: {
|
|
69
|
+
total: number;
|
|
70
|
+
approved: number;
|
|
71
|
+
merged: number;
|
|
72
|
+
entries: SyncRequestEntry[];
|
|
73
|
+
};
|
|
66
74
|
likes: {
|
|
67
75
|
total: number;
|
|
68
76
|
entries: LikeEntry[];
|
|
@@ -137,6 +145,13 @@ export type ForkAppRequest = {
|
|
|
137
145
|
forkedFromCommitId?: string;
|
|
138
146
|
};
|
|
139
147
|
|
|
148
|
+
export type SyncUpstreamStatus = 'up-to-date' | 'queued';
|
|
149
|
+
|
|
150
|
+
export type SyncUpstreamResponse = {
|
|
151
|
+
status: SyncUpstreamStatus;
|
|
152
|
+
mergeRequestId?: string;
|
|
153
|
+
};
|
|
154
|
+
|
|
140
155
|
export type ImportGithubAppRequest = {
|
|
141
156
|
repoFullName: string;
|
|
142
157
|
branch?: string;
|
|
@@ -16,6 +16,8 @@ import { StudioOverlay } from './ui/StudioOverlay';
|
|
|
16
16
|
import { LiquidGlassResetProvider } from '../components/utils/liquidGlassReset';
|
|
17
17
|
import { useEditQueue } from './hooks/useEditQueue';
|
|
18
18
|
import { useEditQueueActions } from './hooks/useEditQueueActions';
|
|
19
|
+
import { appsRepository } from '../data/apps/repository';
|
|
20
|
+
import type { SyncUpstreamStatus } from '../data/apps/types';
|
|
19
21
|
|
|
20
22
|
export type ComergeStudioProps = {
|
|
21
23
|
appId: string;
|
|
@@ -245,6 +247,8 @@ function ComergeStudioInner({
|
|
|
245
247
|
|
|
246
248
|
const [processingMrId, setProcessingMrId] = React.useState<string | null>(null);
|
|
247
249
|
const [testingMrId, setTestingMrId] = React.useState<string | null>(null);
|
|
250
|
+
const [syncingUpstream, setSyncingUpstream] = React.useState(false);
|
|
251
|
+
const [upstreamSyncStatus, setUpstreamSyncStatus] = React.useState<SyncUpstreamStatus | null>(null);
|
|
248
252
|
|
|
249
253
|
// Show typing dots when the last message isn't an outcome (agent still working).
|
|
250
254
|
const chatShowTypingIndicator = React.useMemo(() => {
|
|
@@ -257,8 +261,23 @@ function ComergeStudioInner({
|
|
|
257
261
|
React.useEffect(() => {
|
|
258
262
|
updateLastEditQueueInfo(null);
|
|
259
263
|
setSuppressQueueUntilResponse(false);
|
|
264
|
+
setUpstreamSyncStatus(null);
|
|
260
265
|
}, [activeAppId, updateLastEditQueueInfo]);
|
|
261
266
|
|
|
267
|
+
const handleSyncUpstream = React.useCallback(async () => {
|
|
268
|
+
if (!app?.id) {
|
|
269
|
+
throw new Error('Missing app');
|
|
270
|
+
}
|
|
271
|
+
setSyncingUpstream(true);
|
|
272
|
+
try {
|
|
273
|
+
const result = await appsRepository.syncUpstream(activeAppId);
|
|
274
|
+
setUpstreamSyncStatus(result.status);
|
|
275
|
+
return result;
|
|
276
|
+
} finally {
|
|
277
|
+
setSyncingUpstream(false);
|
|
278
|
+
}
|
|
279
|
+
}, [activeAppId, app?.id]);
|
|
280
|
+
|
|
262
281
|
React.useEffect(() => {
|
|
263
282
|
if (!lastEditQueueInfo?.queueItemId) return;
|
|
264
283
|
const stillPresent = editQueue.items.some((item) => item.id === lastEditQueueInfo.queueItemId);
|
|
@@ -320,6 +339,9 @@ function ComergeStudioInner({
|
|
|
320
339
|
}
|
|
321
340
|
: undefined
|
|
322
341
|
}
|
|
342
|
+
onSyncUpstream={actions.isOwner && app?.forkedFromAppId ? handleSyncUpstream : undefined}
|
|
343
|
+
syncingUpstream={syncingUpstream}
|
|
344
|
+
upstreamSyncStatus={upstreamSyncStatus}
|
|
323
345
|
onApprove={async (mr) => {
|
|
324
346
|
if (processingMrId) return;
|
|
325
347
|
setProcessingMrId(mr.id);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { ActivityIndicator, Platform, Share, View } from 'react-native';
|
|
3
3
|
|
|
4
|
-
import type { App } from '../../data/apps/types';
|
|
4
|
+
import type { App, SyncUpstreamStatus } from '../../data/apps/types';
|
|
5
5
|
import type { MergeRequest } from '../../data/merge-requests/types';
|
|
6
6
|
import { log } from '../../core/logger';
|
|
7
7
|
import { PreviewPage } from '../../components/preview/PreviewPage';
|
|
@@ -30,6 +30,9 @@ export type PreviewPanelProps = {
|
|
|
30
30
|
onGoToChat: () => void;
|
|
31
31
|
onStartDraw?: () => void;
|
|
32
32
|
onSubmitMergeRequest?: () => void | Promise<void>;
|
|
33
|
+
onSyncUpstream?: () => Promise<{ status: SyncUpstreamStatus }>;
|
|
34
|
+
syncingUpstream?: boolean;
|
|
35
|
+
upstreamSyncStatus?: SyncUpstreamStatus | null;
|
|
33
36
|
onRequestApprove?: (mr: MergeRequest) => void;
|
|
34
37
|
onReject?: (mr: MergeRequest) => void | Promise<void>;
|
|
35
38
|
onTestMr?: (mr: MergeRequest) => void | Promise<void>;
|
|
@@ -54,6 +57,9 @@ export function PreviewPanel({
|
|
|
54
57
|
onGoToChat,
|
|
55
58
|
onStartDraw,
|
|
56
59
|
onSubmitMergeRequest,
|
|
60
|
+
onSyncUpstream,
|
|
61
|
+
syncingUpstream,
|
|
62
|
+
upstreamSyncStatus,
|
|
57
63
|
onRequestApprove,
|
|
58
64
|
onReject,
|
|
59
65
|
onTestMr,
|
|
@@ -63,9 +69,9 @@ export function PreviewPanel({
|
|
|
63
69
|
const handleShare = React.useCallback(async () => {
|
|
64
70
|
if (!app || !app.isPublic) return;
|
|
65
71
|
const shareUrl = `https://remix.one/app/${app.id}`;
|
|
66
|
-
const message = app.name ? `${app.name} on
|
|
72
|
+
const message = app.name ? `${app.name} on Remix\n${shareUrl}` : `Check out this app on Remix\n${shareUrl}`;
|
|
67
73
|
try {
|
|
68
|
-
const title = app.name ?? '
|
|
74
|
+
const title = app.name ?? 'Remix app';
|
|
69
75
|
const payload =
|
|
70
76
|
Platform.OS === 'ios'
|
|
71
77
|
? {
|
|
@@ -83,7 +89,17 @@ export function PreviewPanel({
|
|
|
83
89
|
}
|
|
84
90
|
}, [app]);
|
|
85
91
|
|
|
86
|
-
const {
|
|
92
|
+
const {
|
|
93
|
+
imageUrl,
|
|
94
|
+
imageLoaded,
|
|
95
|
+
setImageLoaded,
|
|
96
|
+
creator,
|
|
97
|
+
insights,
|
|
98
|
+
stats,
|
|
99
|
+
showProcessing,
|
|
100
|
+
canSubmitMergeRequest,
|
|
101
|
+
canSyncUpstream,
|
|
102
|
+
} = usePreviewPanelData({
|
|
87
103
|
app,
|
|
88
104
|
isOwner,
|
|
89
105
|
outgoingMergeRequests,
|
|
@@ -145,6 +161,9 @@ export function PreviewPanel({
|
|
|
145
161
|
|
|
146
162
|
<PreviewCollaborateSection
|
|
147
163
|
canSubmitMergeRequest={canSubmitMergeRequest}
|
|
164
|
+
canSyncUpstream={canSyncUpstream}
|
|
165
|
+
syncingUpstream={syncingUpstream}
|
|
166
|
+
upstreamSyncStatus={upstreamSyncStatus}
|
|
148
167
|
incomingMergeRequests={incomingMergeRequests}
|
|
149
168
|
outgoingMergeRequests={outgoingMergeRequests}
|
|
150
169
|
creatorStatsById={creatorStatsById}
|
|
@@ -153,6 +172,7 @@ export function PreviewPanel({
|
|
|
153
172
|
testingMrId={testingMrId}
|
|
154
173
|
toMergeRequestSummary={toMergeRequestSummary}
|
|
155
174
|
onSubmitMergeRequest={onSubmitMergeRequest}
|
|
175
|
+
onSyncUpstream={onSyncUpstream}
|
|
156
176
|
onRequestApprove={onRequestApprove}
|
|
157
177
|
onReject={onReject}
|
|
158
178
|
onTestMr={onTestMr}
|
|
@@ -46,6 +46,9 @@ export type StudioOverlayProps = {
|
|
|
46
46
|
testingMrId?: string | null;
|
|
47
47
|
toMergeRequestSummary: (mr: MergeRequest) => MergeRequestSummary;
|
|
48
48
|
onSubmitMergeRequest?: () => void | Promise<void>;
|
|
49
|
+
onSyncUpstream?: () => Promise<{ status: import('../../data/apps/types').SyncUpstreamStatus }>;
|
|
50
|
+
syncingUpstream?: boolean;
|
|
51
|
+
upstreamSyncStatus?: import('../../data/apps/types').SyncUpstreamStatus | null;
|
|
49
52
|
onApprove?: (mr: MergeRequest) => void | Promise<void>;
|
|
50
53
|
onReject?: (mr: MergeRequest) => void | Promise<void>;
|
|
51
54
|
onTestMr?: (mr: MergeRequest) => void | Promise<void>;
|
|
@@ -85,6 +88,9 @@ export function StudioOverlay({
|
|
|
85
88
|
testingMrId,
|
|
86
89
|
toMergeRequestSummary,
|
|
87
90
|
onSubmitMergeRequest,
|
|
91
|
+
onSyncUpstream,
|
|
92
|
+
syncingUpstream,
|
|
93
|
+
upstreamSyncStatus,
|
|
88
94
|
onApprove,
|
|
89
95
|
onReject,
|
|
90
96
|
onTestMr,
|
|
@@ -248,6 +254,9 @@ export function StudioOverlay({
|
|
|
248
254
|
onGoToChat={goToChat}
|
|
249
255
|
onStartDraw={isOwner ? startDraw : undefined}
|
|
250
256
|
onSubmitMergeRequest={onSubmitMergeRequest}
|
|
257
|
+
onSyncUpstream={onSyncUpstream}
|
|
258
|
+
syncingUpstream={syncingUpstream}
|
|
259
|
+
upstreamSyncStatus={upstreamSyncStatus}
|
|
251
260
|
onRequestApprove={(mr) => setConfirmMrId(mr.id)}
|
|
252
261
|
onReject={onReject}
|
|
253
262
|
onTestMr={handleTestMr}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { ActivityIndicator, Alert, View } from 'react-native';
|
|
3
|
-
import { Send } from 'lucide-react-native';
|
|
3
|
+
import { RefreshCw, Send } from 'lucide-react-native';
|
|
4
4
|
|
|
5
5
|
import type { MergeRequest } from '../../../data/merge-requests/types';
|
|
6
6
|
import type { UserStats } from '../../../data/users/types';
|
|
7
|
+
import type { SyncUpstreamStatus } from '../../../data/apps/types';
|
|
7
8
|
import { MergeRequestStatusCard } from '../../../components/merge-requests/MergeRequestStatusCard';
|
|
8
9
|
import { ReviewMergeRequestCarousel } from '../../../components/merge-requests/ReviewMergeRequestCarousel';
|
|
9
10
|
import { Text } from '../../../components/primitives/Text';
|
|
@@ -16,6 +17,9 @@ import { MergeIcon } from '../../../components/icons/MergeIcon';
|
|
|
16
17
|
|
|
17
18
|
export type PreviewCollaborateSectionProps = {
|
|
18
19
|
canSubmitMergeRequest: boolean;
|
|
20
|
+
canSyncUpstream: boolean;
|
|
21
|
+
syncingUpstream?: boolean;
|
|
22
|
+
upstreamSyncStatus?: SyncUpstreamStatus | null;
|
|
19
23
|
incomingMergeRequests: MergeRequest[];
|
|
20
24
|
outgoingMergeRequests: MergeRequest[];
|
|
21
25
|
creatorStatsById: Record<string, UserStats>;
|
|
@@ -24,6 +28,7 @@ export type PreviewCollaborateSectionProps = {
|
|
|
24
28
|
testingMrId?: string | null;
|
|
25
29
|
toMergeRequestSummary: (mr: MergeRequest) => import('../../../components/models/types').MergeRequestSummary;
|
|
26
30
|
onSubmitMergeRequest?: () => void | Promise<void>;
|
|
31
|
+
onSyncUpstream?: () => Promise<{ status: SyncUpstreamStatus }>;
|
|
27
32
|
onRequestApprove?: (mr: MergeRequest) => void;
|
|
28
33
|
onReject?: (mr: MergeRequest) => void | Promise<void>;
|
|
29
34
|
onTestMr?: (mr: MergeRequest) => void | Promise<void>;
|
|
@@ -31,6 +36,9 @@ export type PreviewCollaborateSectionProps = {
|
|
|
31
36
|
|
|
32
37
|
export function PreviewCollaborateSection({
|
|
33
38
|
canSubmitMergeRequest,
|
|
39
|
+
canSyncUpstream,
|
|
40
|
+
syncingUpstream,
|
|
41
|
+
upstreamSyncStatus,
|
|
34
42
|
incomingMergeRequests,
|
|
35
43
|
outgoingMergeRequests,
|
|
36
44
|
creatorStatsById,
|
|
@@ -39,17 +47,24 @@ export function PreviewCollaborateSection({
|
|
|
39
47
|
testingMrId,
|
|
40
48
|
toMergeRequestSummary,
|
|
41
49
|
onSubmitMergeRequest,
|
|
50
|
+
onSyncUpstream,
|
|
42
51
|
onRequestApprove,
|
|
43
52
|
onReject,
|
|
44
53
|
onTestMr,
|
|
45
54
|
}: PreviewCollaborateSectionProps) {
|
|
46
55
|
const theme = useTheme();
|
|
47
56
|
const [submittingMr, setSubmittingMr] = React.useState(false);
|
|
57
|
+
const [syncingLocal, setSyncingLocal] = React.useState(false);
|
|
48
58
|
|
|
49
|
-
const hasSection =
|
|
59
|
+
const hasSection =
|
|
60
|
+
canSubmitMergeRequest || canSyncUpstream || incomingMergeRequests.length > 0 || outgoingMergeRequests.length > 0;
|
|
50
61
|
if (!hasSection) return null;
|
|
51
62
|
|
|
52
|
-
const
|
|
63
|
+
const isSyncing = Boolean(syncingUpstream || syncingLocal);
|
|
64
|
+
const showActionsSubtitle =
|
|
65
|
+
(canSubmitMergeRequest && onSubmitMergeRequest) ||
|
|
66
|
+
(canSyncUpstream && onSyncUpstream) ||
|
|
67
|
+
(onTestMr && incomingMergeRequests.length > 0);
|
|
53
68
|
|
|
54
69
|
return (
|
|
55
70
|
<>
|
|
@@ -131,6 +146,83 @@ export function PreviewCollaborateSection({
|
|
|
131
146
|
/>
|
|
132
147
|
) : null}
|
|
133
148
|
|
|
149
|
+
{canSyncUpstream && onSyncUpstream ? (
|
|
150
|
+
<PressableCardRow
|
|
151
|
+
accessibilityLabel="Sync from original"
|
|
152
|
+
disabled={isSyncing}
|
|
153
|
+
onPress={() => {
|
|
154
|
+
Alert.alert(
|
|
155
|
+
'Sync from Original',
|
|
156
|
+
'This will pull the latest upstream changes into your remix.',
|
|
157
|
+
[
|
|
158
|
+
{ text: 'Cancel', style: 'cancel' },
|
|
159
|
+
{
|
|
160
|
+
text: 'Sync',
|
|
161
|
+
style: 'destructive',
|
|
162
|
+
onPress: () => {
|
|
163
|
+
setSyncingLocal(true);
|
|
164
|
+
Promise.resolve(onSyncUpstream())
|
|
165
|
+
.then((result) => {
|
|
166
|
+
if (result?.status === 'up-to-date') {
|
|
167
|
+
Alert.alert('Up to date', 'Your remix already includes the latest upstream changes.');
|
|
168
|
+
} else {
|
|
169
|
+
Alert.alert('Sync started', 'Upstream changes are being merged into your remix.');
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
.catch(() => {
|
|
173
|
+
Alert.alert('Sync failed', 'We could not start the sync. Please try again.');
|
|
174
|
+
})
|
|
175
|
+
.finally(() => setSyncingLocal(false));
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
]
|
|
179
|
+
);
|
|
180
|
+
}}
|
|
181
|
+
style={{
|
|
182
|
+
padding: theme.spacing.lg,
|
|
183
|
+
borderRadius: theme.radii.lg,
|
|
184
|
+
backgroundColor: withAlpha(theme.colors.surfaceRaised, 0.5),
|
|
185
|
+
borderWidth: 1,
|
|
186
|
+
borderColor: withAlpha(theme.colors.primary, 0.25),
|
|
187
|
+
marginBottom: theme.spacing.sm,
|
|
188
|
+
}}
|
|
189
|
+
left={
|
|
190
|
+
<View
|
|
191
|
+
style={{
|
|
192
|
+
width: 40,
|
|
193
|
+
height: 40,
|
|
194
|
+
borderRadius: 999,
|
|
195
|
+
alignItems: 'center',
|
|
196
|
+
justifyContent: 'center',
|
|
197
|
+
backgroundColor: withAlpha(theme.colors.primary, 0.12),
|
|
198
|
+
marginRight: theme.spacing.lg,
|
|
199
|
+
}}
|
|
200
|
+
>
|
|
201
|
+
{isSyncing ? (
|
|
202
|
+
<ActivityIndicator color={theme.colors.primary} size="small" />
|
|
203
|
+
) : (
|
|
204
|
+
<RefreshCw size={18} color={theme.colors.primary} />
|
|
205
|
+
)}
|
|
206
|
+
</View>
|
|
207
|
+
}
|
|
208
|
+
title={
|
|
209
|
+
<Text style={{ color: theme.colors.text, fontSize: 16, lineHeight: 20, fontWeight: theme.typography.fontWeight.semibold }}>
|
|
210
|
+
Sync from Original
|
|
211
|
+
</Text>
|
|
212
|
+
}
|
|
213
|
+
subtitle={
|
|
214
|
+
<Text style={{ color: theme.colors.textMuted, fontSize: 12, lineHeight: 16, marginTop: 2 }}>
|
|
215
|
+
{isSyncing
|
|
216
|
+
? 'Syncing upstream changes...'
|
|
217
|
+
: upstreamSyncStatus === 'up-to-date'
|
|
218
|
+
? 'You are already up to date with the original app'
|
|
219
|
+
: 'Pull the latest upstream changes into this remix'}
|
|
220
|
+
</Text>
|
|
221
|
+
}
|
|
222
|
+
right={<RefreshCw size={16} color={theme.colors.primary} />}
|
|
223
|
+
/>
|
|
224
|
+
) : null}
|
|
225
|
+
|
|
134
226
|
{onTestMr && incomingMergeRequests.length > 0 ? (
|
|
135
227
|
<ReviewMergeRequestCarousel
|
|
136
228
|
mergeRequests={incomingMergeRequests}
|
|
@@ -115,6 +115,13 @@ export function usePreviewPanelData(params: {
|
|
|
115
115
|
return false;
|
|
116
116
|
}, [app, isOwner, outgoingMergeRequests]);
|
|
117
117
|
|
|
118
|
+
const canSyncUpstream = React.useMemo(() => {
|
|
119
|
+
if (!isOwner) return false;
|
|
120
|
+
if (!app) return false;
|
|
121
|
+
if (!app.forkedFromAppId) return false;
|
|
122
|
+
return app.status === 'ready';
|
|
123
|
+
}, [app, isOwner]);
|
|
124
|
+
|
|
118
125
|
const showProcessing = app ? app.status !== 'ready' : false;
|
|
119
126
|
|
|
120
127
|
return {
|
|
@@ -126,6 +133,7 @@ export function usePreviewPanelData(params: {
|
|
|
126
133
|
stats,
|
|
127
134
|
showProcessing,
|
|
128
135
|
canSubmitMergeRequest,
|
|
136
|
+
canSyncUpstream,
|
|
129
137
|
};
|
|
130
138
|
}
|
|
131
139
|
|