Skip to content

Commit d5a5d68

Browse files
authored
feat: integrate with git service and import from real repositories (#1758)
1 parent 4348ed2 commit d5a5d68

17 files changed

Lines changed: 827 additions & 284 deletions

File tree

packages/manager/src/constants/API_ENDPOINTS.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type APIEndpoints = {
1313
RepositoryService: string;
1414
LocaleService: string;
1515
CustomTypeService: string;
16+
GitService: string;
1617
};
1718

1819
export const API_ENDPOINTS: APIEndpoints = (() => {
@@ -47,6 +48,9 @@ export const API_ENDPOINTS: APIEndpoints = (() => {
4748
process.env.custom_type_api ??
4849
"https://api.internal.wroom.io/custom-type/",
4950
),
51+
GitService: addTrailingSlash(
52+
process.env.git_service_api ?? "https://api.internal.wroom.io/git/",
53+
),
5054
};
5155

5256
const missingAPIEndpoints = Object.keys(apiEndpoints).filter((key) => {
@@ -96,6 +100,7 @@ If you didn't intend to run Slice Machine this way, stop it immediately and unse
96100
RepositoryService: "https://api.internal.wroom.io/repository/",
97101
LocaleService: "https://api.internal.wroom.io/locale/",
98102
CustomTypeService: "https://api.internal.wroom.io/custom-type/",
103+
GitService: "https://api.internal.wroom.io/git/",
99104
};
100105
}
101106

@@ -114,6 +119,7 @@ If you didn't intend to run Slice Machine this way, stop it immediately and unse
114119
RepositoryService: `https://api.internal.${process.env.SM_ENV}-wroom.com/repository/`,
115120
LocaleService: `https://api.internal.${process.env.SM_ENV}-wroom.com/locale/`,
116121
CustomTypeService: `https://api.internal.${process.env.SM_ENV}-wroom.com/custom-type/`,
122+
GitService: `https://api.internal.${process.env.SM_ENV}-wroom.com/git/`,
117123
};
118124
}
119125

@@ -131,6 +137,7 @@ If you didn't intend to run Slice Machine this way, stop it immediately and unse
131137
RepositoryService: "https://api.internal.prismic.io/repository/",
132138
LocaleService: "https://api.internal.prismic.io/locale/",
133139
CustomTypeService: "https://api.internal.prismic.io/custom-type/",
140+
GitService: "https://api.internal.prismic.io/git/",
134141
};
135142
}
136143
}

packages/manager/src/managers/prismicRepository/PrismicRepositoryManager.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,34 @@ type PrismicRepositoryManagerFetchEnvironmentsReturnType = {
8383
environments?: Environment[];
8484
};
8585

86+
const GitIntegrationsSchema = z.object({
87+
integrations: z.array(
88+
z.discriminatedUnion("status", [
89+
z.object({
90+
id: z.string(),
91+
status: z.literal("connected"),
92+
owner: z.string(),
93+
repositories: z.array(
94+
z.object({
95+
name: z.string(),
96+
fullName: z.string(),
97+
}),
98+
),
99+
}),
100+
z.object({
101+
id: z.string(),
102+
status: z.literal("broken"),
103+
owner: z.string().optional(),
104+
repositories: z.tuple([]),
105+
}),
106+
]),
107+
),
108+
});
109+
110+
const GitIntegrationTokenSchema = z.object({
111+
token: z.string(),
112+
});
113+
86114
export class PrismicRepositoryManager extends BaseManager {
87115
// TODO: Add methods for repository-specific actions. E.g. creating a
88116
// new repository.
@@ -705,6 +733,68 @@ export class PrismicRepositoryManager extends BaseManager {
705733
}
706734
}
707735

736+
async fetchGitIntegrations(): Promise<z.infer<typeof GitIntegrationsSchema>> {
737+
const repositoryName = await this.project.getRepositoryName();
738+
739+
const url = new URL("integrations", API_ENDPOINTS.GitService);
740+
url.searchParams.set("repository", repositoryName);
741+
742+
const res = await this._fetch({ url });
743+
744+
if (!res.ok) {
745+
const text = await res.text();
746+
throw new Error(
747+
`Failed to fetch integrations for repository ${repositoryName}`,
748+
{ cause: text },
749+
);
750+
}
751+
752+
const json = await res.json();
753+
const { value, error } = decode(GitIntegrationsSchema, json);
754+
755+
if (error) {
756+
throw new UnexpectedDataError(
757+
`Failed to decode integrations: ${error.errors.join(", ")}`,
758+
);
759+
}
760+
761+
return value;
762+
}
763+
764+
async fetchGitIntegrationToken(args: {
765+
integrationId: string;
766+
}): Promise<z.infer<typeof GitIntegrationTokenSchema>> {
767+
const { integrationId } = args;
768+
const repositoryName = await this.project.getRepositoryName();
769+
770+
const url = new URL(
771+
`integrations/${integrationId}/token`,
772+
API_ENDPOINTS.GitService,
773+
);
774+
url.searchParams.set("repository", repositoryName);
775+
776+
const res = await this._fetch({ url, method: "POST" });
777+
778+
if (!res.ok) {
779+
const text = await res.text();
780+
throw new Error(
781+
`Failed to fetch token for integration ${integrationId}`,
782+
{ cause: text },
783+
);
784+
}
785+
786+
const json = await res.json();
787+
const { value, error } = decode(GitIntegrationTokenSchema, json);
788+
789+
if (error) {
790+
throw new UnexpectedDataError(
791+
`Failed to decode integration token: ${error.errors.join(", ")}`,
792+
);
793+
}
794+
795+
return value;
796+
}
797+
708798
private _decodeLimitOrThrow(
709799
potentialLimit: unknown,
710800
statusCode: number,

packages/manager/test/SliceMachineManager-getState.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ it("returns global Slice Machine state", async () => {
2828
RepositoryService: "https://api.internal.prismic.io/repository/",
2929
LocaleService: "https://api.internal.prismic.io/locale/",
3030
CustomTypeService: "https://api.internal.prismic.io/custom-type/",
31+
GitService: "https://api.internal.prismic.io/git/",
3132
});
3233
expect(result.clientError).toStrictEqual({
3334
name: new UnauthenticatedError().name,

packages/slice-machine/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@
4242
"@emotion/react": "11.11.1",
4343
"@extractus/oembed-extractor": "3.1.8",
4444
"@prismicio/client": "7.17.0",
45-
"@prismicio/editor-fields": "0.4.89-alpha.jp-box-display-none.1",
46-
"@prismicio/editor-support": "0.4.89-alpha.jp-box-display-none.1",
47-
"@prismicio/editor-ui": "0.4.89-alpha.jp-box-display-none.1",
45+
"@prismicio/editor-fields": "0.4.90",
46+
"@prismicio/editor-support": "0.4.90",
47+
"@prismicio/editor-ui": "0.4.90",
4848
"@prismicio/mock": "0.7.1",
4949
"@prismicio/mocks": "2.14.0",
5050
"@prismicio/simulator": "0.1.4",

packages/slice-machine/src/features/customTypes/customTypesBuilder/ImportSlicesFromLibraryModal/DialogButtons.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
DialogActionButton,
33
DialogActions,
44
DialogCancelButton,
5+
Skeleton,
56
} from "@prismicio/editor-ui";
67

78
import { CommonDialogProps } from "./types";
@@ -13,6 +14,15 @@ type DialogButtonsProps = {
1314
typeName: CommonDialogProps["typeName"];
1415
};
1516

17+
export function DialogButtonsSkeleton() {
18+
return (
19+
<DialogActions>
20+
<Skeleton height={32} width={58.48} />
21+
<Skeleton height={32} width={110.38} />
22+
</DialogActions>
23+
);
24+
}
25+
1626
export function DialogButtons(props: DialogButtonsProps) {
1727
const { totalSelected, onSubmit, isSubmitting, typeName } = props;
1828

packages/slice-machine/src/features/customTypes/customTypesBuilder/ImportSlicesFromLibraryModal/DialogTabs.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ export function DialogTabs(props: DialogTabsProps) {
1313
const { selectedTab, onSelectTab, rightContent } = props;
1414

1515
return (
16-
<Box justifyContent="space-between" padding={16} border={{ bottom: true }}>
16+
<Box
17+
justifyContent="space-between"
18+
padding={16}
19+
border={{ bottom: true }}
20+
gap={8}
21+
>
1722
<Box gap={8}>
1823
<Tab
1924
selected={selectedTab === "local"}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import {
2+
BlankSlate,
3+
BlankSlateActions,
4+
BlankSlateDescription,
5+
BlankSlateIcon,
6+
BlankSlateTitle,
7+
Box,
8+
ThemeKeys,
9+
} from "@prismicio/editor-ui";
10+
import { ReactNode } from "react";
11+
12+
type EmptyViewProps = {
13+
title: string;
14+
description?: string;
15+
icon: "github" | "alert" | "logout" | "viewDay";
16+
color?: "purple" | "tomato";
17+
actions?: ReactNode;
18+
};
19+
20+
export function EmptyView(props: EmptyViewProps) {
21+
const { title, description, icon, actions, color = "purple" } = props;
22+
23+
let iconColor: ThemeKeys<"color">;
24+
let iconBackgroundColor: ThemeKeys<"color">;
25+
26+
switch (color) {
27+
case "purple":
28+
iconColor = "purple11";
29+
iconBackgroundColor = "purple3";
30+
break;
31+
case "tomato":
32+
iconColor = "tomato11";
33+
iconBackgroundColor = "tomato3";
34+
break;
35+
}
36+
37+
return (
38+
<Box
39+
flexDirection="column"
40+
justifyContent="center"
41+
alignItems="center"
42+
gap={16}
43+
flexGrow={1}
44+
>
45+
<BlankSlate>
46+
<BlankSlateIcon
47+
lineColor={iconColor}
48+
backgroundColor={iconBackgroundColor}
49+
name={icon}
50+
/>
51+
<BlankSlateTitle size="medium">{title}</BlankSlateTitle>
52+
{description !== undefined && (
53+
<BlankSlateDescription>{description}</BlankSlateDescription>
54+
)}
55+
{actions !== undefined && (
56+
<BlankSlateActions>{actions}</BlankSlateActions>
57+
)}
58+
</BlankSlate>
59+
</Box>
60+
);
61+
}

packages/slice-machine/src/features/customTypes/customTypesBuilder/ImportSlicesFromLibraryModal/ImportSlicesFromLibraryModal.tsx

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ import {
55
DialogHeader,
66
} from "@prismicio/editor-ui";
77
import { SharedSlice } from "@prismicio/types-internal/lib/customtypes";
8-
import { useState } from "react";
8+
import { useEffect, useState } from "react";
99

1010
import { LibrarySlicesDialogContent } from "./LibrarySlicesDialogContent";
1111
import { LocalSlicesDialogContent } from "./LocalSlicesDialogContent";
1212
import { CommonDialogProps, DialogTab } from "./types";
1313

1414
type ImportSlicesFromLibraryModalProps = CommonDialogProps & {
15-
availableSlices: (SharedSlice & { thumbnailUrl?: string })[];
15+
open: boolean;
16+
localSlices: (SharedSlice & { thumbnailUrl?: string })[];
17+
isEveryLocalSliceAdded: boolean;
1618
onSuccess: (args: {
1719
slices: { model: SharedSlice; langSmithUrl?: string }[];
1820
library?: string;
@@ -22,34 +24,44 @@ type ImportSlicesFromLibraryModalProps = CommonDialogProps & {
2224
export function ImportSlicesFromLibraryModal(
2325
props: ImportSlicesFromLibraryModalProps,
2426
) {
25-
const { open, availableSlices = [], onClose, ...commonProps } = props;
27+
const {
28+
open,
29+
localSlices,
30+
isEveryLocalSliceAdded,
31+
onClose,
32+
...contentProps
33+
} = props;
34+
2635
const [selectedTab, setSelectedTab] = useState<DialogTab>("local");
2736

28-
const onOpenChange = (open: boolean) => {
37+
useEffect(() => {
2938
if (!open) {
30-
onClose();
31-
setTimeout(() => setSelectedTab("local"), 250); // wait for the modal fade animation
39+
// wait for the modal fade animation
40+
const timeout = setTimeout(() => {
41+
setSelectedTab("local");
42+
}, 250);
43+
44+
return () => clearTimeout(timeout);
3245
}
33-
};
46+
}, [open]);
3447

3548
return (
36-
<Dialog open={open} onOpenChange={onOpenChange}>
49+
<Dialog open={open} onOpenChange={(open) => !open && onClose()}>
3750
<DialogHeader title="Reuse an existing slice" />
3851
<DialogContent gap={0}>
3952
<DialogDescription hidden>
4053
Select existing slices or import slices from a GitHub repository
4154
</DialogDescription>
4255
<LocalSlicesDialogContent
43-
{...commonProps}
44-
open={open}
56+
{...contentProps}
4557
selected={selectedTab === "local"}
4658
onSelectTab={setSelectedTab}
47-
availableSlices={availableSlices}
59+
slices={localSlices}
60+
isEveryLocalSliceAdded={isEveryLocalSliceAdded}
4861
onClose={onClose}
4962
/>
5063
<LibrarySlicesDialogContent
51-
{...commonProps}
52-
open={open}
64+
{...contentProps}
5365
selected={selectedTab === "library"}
5466
onSelectTab={setSelectedTab}
5567
onClose={onClose}

0 commit comments

Comments
 (0)