Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 77cfb7c

Browse files
committed
feat: initial changes for multi org template creation
1 parent 80cbffe commit 77cfb7c

File tree

10 files changed

+440
-149
lines changed

10 files changed

+440
-149
lines changed
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { css } from "@emotion/css";
2+
import Autocomplete from "@mui/material/Autocomplete";
3+
import CircularProgress from "@mui/material/CircularProgress";
4+
import TextField from "@mui/material/TextField";
5+
import {
6+
type ChangeEvent,
7+
type ComponentProps,
8+
type FC,
9+
useState,
10+
} from "react";
11+
import { useQuery } from "react-query";
12+
import { myOrganizations } from "api/queries/users";
13+
import type { Organization } from "api/typesGenerated";
14+
import { Avatar } from "components/Avatar/Avatar";
15+
import { AvatarData } from "components/AvatarData/AvatarData";
16+
import { useDebouncedFunction } from "hooks/debounce";
17+
// import { prepareQuery } from "utils/filters";
18+
19+
export type OrganizationAutocompleteProps = {
20+
value: Organization | null;
21+
onChange: (organization: Organization | null) => void;
22+
label?: string;
23+
className?: string;
24+
size?: ComponentProps<typeof TextField>["size"];
25+
};
26+
27+
export const OrganizationAutocomplete: FC<OrganizationAutocompleteProps> = ({
28+
value,
29+
onChange,
30+
label,
31+
className,
32+
size = "small",
33+
}) => {
34+
const [autoComplete, setAutoComplete] = useState<{
35+
value: string;
36+
open: boolean;
37+
}>({
38+
value: value?.name ?? "",
39+
open: false,
40+
});
41+
// const usersQuery = useQuery({
42+
// ...users({
43+
// q: prepareQuery(encodeURI(autoComplete.value)),
44+
// limit: 25,
45+
// }),
46+
// enabled: autoComplete.open,
47+
// keepPreviousData: true,
48+
// });
49+
const organizationsQuery = useQuery(myOrganizations());
50+
51+
const { debounced: debouncedInputOnChange } = useDebouncedFunction(
52+
(event: ChangeEvent<HTMLInputElement>) => {
53+
setAutoComplete((state) => ({
54+
...state,
55+
value: event.target.value,
56+
}));
57+
},
58+
750,
59+
);
60+
61+
return (
62+
<Autocomplete
63+
// Since the values are filtered by the API we don't need to filter them
64+
// in the FE because it can causes some mismatches.
65+
filterOptions={(organization) => organization}
66+
noOptionsText="No users found"
67+
className={className}
68+
options={organizationsQuery.data ?? []}
69+
loading={organizationsQuery.isLoading}
70+
value={value}
71+
id="organization-autocomplete"
72+
open={autoComplete.open}
73+
onOpen={() => {
74+
setAutoComplete((state) => ({
75+
...state,
76+
open: true,
77+
}));
78+
}}
79+
onClose={() => {
80+
setAutoComplete({
81+
value: value?.name ?? "",
82+
open: false,
83+
});
84+
}}
85+
onChange={(_, newValue) => {
86+
onChange(newValue);
87+
}}
88+
isOptionEqualToValue={(option: Organization, value: Organization) =>
89+
option.name === value.name
90+
}
91+
getOptionLabel={(option) => option.name}
92+
renderOption={(props, option) => (
93+
<li {...props}>
94+
<AvatarData
95+
title={option.name}
96+
subtitle={option.display_name}
97+
src={option.icon}
98+
/>
99+
</li>
100+
)}
101+
renderInput={(params) => (
102+
<TextField
103+
{...params}
104+
fullWidth
105+
size={size}
106+
label={label}
107+
placeholder="Organization name"
108+
css={{
109+
"&:not(:has(label))": {
110+
margin: 0,
111+
},
112+
}}
113+
InputProps={{
114+
...params.InputProps,
115+
onChange: debouncedInputOnChange,
116+
startAdornment: value && (
117+
<Avatar size="sm" src={value.icon}>
118+
{value.name}
119+
</Avatar>
120+
),
121+
endAdornment: (
122+
<>
123+
{organizationsQuery.isFetching && autoComplete.open ? (
124+
<CircularProgress size={16} />
125+
) : null}
126+
{params.InputProps.endAdornment}
127+
</>
128+
),
129+
classes: { root },
130+
}}
131+
InputLabelProps={{
132+
shrink: true,
133+
}}
134+
/>
135+
)}
136+
/>
137+
);
138+
};
139+
140+
const root = css`
141+
padding-left: 14px !important; // Same padding left as input
142+
gap: 4px;
143+
`;

site/src/pages/StarterTemplatesPage/StarterTemplatesPage.tsx renamed to site/src/pages/CreateTemplatesGalleryPage/CreateTemplatesGalleryPage.tsx

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,34 @@ import type { TemplateExample } from "api/typesGenerated";
66
import { useDashboard } from "modules/dashboard/useDashboard";
77
import { pageTitle } from "utils/page";
88
import { getTemplatesByTag } from "utils/starterTemplates";
9+
import { CreateTemplatesPageView } from "./CreateTemplatesPageView";
910
import { StarterTemplatesPageView } from "./StarterTemplatesPageView";
1011

11-
const StarterTemplatesPage: FC = () => {
12-
const { organizationId } = useDashboard();
12+
const CreateTemplatesGalleryPage: FC = () => {
13+
const { organizationId, experiments } = useDashboard();
1314
const templateExamplesQuery = useQuery(templateExamples(organizationId));
1415
const starterTemplatesByTag = templateExamplesQuery.data
1516
? // Currently, the scratch template should not be displayed on the starter templates page.
1617
getTemplatesByTag(removeScratchExample(templateExamplesQuery.data))
1718
: undefined;
19+
const multiOrgExperimentEnabled = experiments.includes("multi-organization");
1820

1921
return (
2022
<>
2123
<Helmet>
22-
<title>{pageTitle("Starter Templates")}</title>
24+
<title>{pageTitle("Create a Template")}</title>
2325
</Helmet>
24-
25-
<StarterTemplatesPageView
26-
error={templateExamplesQuery.error}
27-
starterTemplatesByTag={starterTemplatesByTag}
28-
/>
26+
{multiOrgExperimentEnabled ? (
27+
<CreateTemplatesPageView
28+
error={templateExamplesQuery.error}
29+
starterTemplatesByTag={starterTemplatesByTag}
30+
/>
31+
) : (
32+
<StarterTemplatesPageView
33+
error={templateExamplesQuery.error}
34+
starterTemplatesByTag={starterTemplatesByTag}
35+
/>
36+
)}
2937
</>
3038
);
3139
};
@@ -34,4 +42,4 @@ const removeScratchExample = (data: TemplateExample[]) => {
3442
return data.filter((example) => example.id !== "scratch");
3543
};
3644

37-
export default StarterTemplatesPage;
45+
export default CreateTemplatesGalleryPage;
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import type { Interpolation, Theme } from "@emotion/react";
2+
import { useState, type FC } from "react";
3+
import { useQuery } from "react-query";
4+
import { Link, useSearchParams } from "react-router-dom";
5+
import { templateExamples } from "api/queries/templates";
6+
import type { Organization, TemplateExample } from "api/typesGenerated";
7+
import { ErrorAlert } from "components/Alert/ErrorAlert";
8+
import { Loader } from "components/Loader/Loader";
9+
import { Margins } from "components/Margins/Margins";
10+
import { OrganizationAutocomplete } from "components/OrganizationAutocomplete/OrganizationAutocomplete";
11+
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
12+
import { Stack } from "components/Stack/Stack";
13+
import { useDashboard } from "modules/dashboard/useDashboard";
14+
import { TemplateExampleCard } from "modules/templates/TemplateExampleCard/TemplateExampleCard";
15+
import {
16+
getTemplatesByTag,
17+
type StarterTemplatesByTag,
18+
} from "utils/starterTemplates";
19+
import { StarterTemplates } from "./StarterTemplates";
20+
21+
// const getTagLabel = (tag: string) => {
22+
// const labelByTag: Record<string, string> = {
23+
// all: "All templates",
24+
// digitalocean: "DigitalOcean",
25+
// aws: "AWS",
26+
// google: "Google Cloud",
27+
// };
28+
// // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- this can be undefined
29+
// return labelByTag[tag] ?? tag;
30+
// };
31+
32+
// const selectTags = (starterTemplatesByTag: StarterTemplatesByTag) => {
33+
// return starterTemplatesByTag
34+
// ? Object.keys(starterTemplatesByTag).sort((a, b) => a.localeCompare(b))
35+
// : undefined;
36+
// };
37+
38+
export interface CreateTemplatePageViewProps {
39+
starterTemplatesByTag?: StarterTemplatesByTag;
40+
error?: unknown;
41+
}
42+
43+
// const removeScratchExample = (data: TemplateExample[]) => {
44+
// return data.filter((example) => example.id !== "scratch");
45+
// };
46+
47+
export const CreateTemplatesPageView: FC<CreateTemplatePageViewProps> = ({
48+
starterTemplatesByTag,
49+
error,
50+
}) => {
51+
const [selectedOrg, setSelectedOrg] = useState<Organization | null>(null);
52+
// const { organizationId } = useDashboard();
53+
// const templateExamplesQuery = useQuery(templateExamples(organizationId));
54+
// const starterTemplatesByTag = templateExamplesQuery.data
55+
// ? // Currently, the scratch template should not be displayed on the starter templates page.
56+
// getTemplatesByTag(removeScratchExample(templateExamplesQuery.data))
57+
// : undefined;
58+
59+
return (
60+
<Margins>
61+
<PageHeader>
62+
<PageHeaderTitle>Create a Template</PageHeaderTitle>
63+
</PageHeader>
64+
65+
<OrganizationAutocomplete
66+
css={styles.autoComplete}
67+
value={selectedOrg}
68+
onChange={(newValue) => {
69+
setSelectedOrg(newValue);
70+
}}
71+
/>
72+
73+
{Boolean(error) && <ErrorAlert error={error} />}
74+
75+
{Boolean(!starterTemplatesByTag) && <Loader />}
76+
77+
<StarterTemplates starterTemplatesByTag={starterTemplatesByTag} />
78+
</Margins>
79+
);
80+
};
81+
82+
const styles = {
83+
autoComplete: {
84+
width: 300,
85+
},
86+
87+
filterCaption: (theme) => ({
88+
textTransform: "uppercase",
89+
fontWeight: 600,
90+
fontSize: 12,
91+
color: theme.palette.text.secondary,
92+
letterSpacing: "0.1em",
93+
}),
94+
95+
tagLink: (theme) => ({
96+
color: theme.palette.text.secondary,
97+
textDecoration: "none",
98+
fontSize: 14,
99+
textTransform: "capitalize",
100+
101+
"&:hover": {
102+
color: theme.palette.text.primary,
103+
},
104+
}),
105+
106+
tagLinkActive: (theme) => ({
107+
color: theme.palette.text.primary,
108+
fontWeight: 600,
109+
}),
110+
} satisfies Record<string, Interpolation<Theme>>;

0 commit comments

Comments
 (0)