Skip to content

Commit 27dfd55

Browse files
committed
feat: initial changes for multi org template creation
1 parent de2585b commit 27dfd55

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy