chore: Templates UI updates (#11775)

This commit is contained in:
akash-codemonk 2022-03-31 10:46:04 +05:30 committed by GitHub
parent 2b47e00a71
commit 73c5267d13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 290 additions and 220 deletions

View File

@ -41,3 +41,8 @@ export const setTemplateNotificationSeenAction = (payload: boolean) => ({
export const getTemplateNotificationSeenAction = () => ({
type: ReduxActionTypes.GET_TEMPLATE_NOTIFICATION_SEEN,
});
export const getTemplateInformation = (payload: string) => ({
type: ReduxActionTypes.GET_TEMPLATE_INIT,
payload,
});

View File

@ -18,6 +18,8 @@ export interface Template {
datasources: string[];
}
export type FilterKeys = "widgets" | "datasources";
export interface FetchTemplatesResponse extends ApiResponse {
data: Template[];
}

View File

@ -1,7 +1,7 @@
<svg width="14" height="16" viewBox="0 0 14 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.00016 14.6667C7.92064 14.6667 8.66683 13.9205 8.66683 13C8.66683 12.0795 7.92064 11.3333 7.00016 11.3333C6.07969 11.3333 5.3335 12.0795 5.3335 13C5.3335 13.9205 6.07969 14.6667 7.00016 14.6667Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.83341 4.66668C3.75389 4.66668 4.50008 3.92049 4.50008 3.00001C4.50008 2.07954 3.75389 1.33334 2.83341 1.33334C1.91294 1.33334 1.16675 2.07954 1.16675 3.00001C1.16675 3.92049 1.91294 4.66668 2.83341 4.66668Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.1667 4.66668C12.0871 4.66668 12.8333 3.92049 12.8333 3.00001C12.8333 2.07954 12.0871 1.33334 11.1667 1.33334C10.2462 1.33334 9.5 2.07954 9.5 3.00001C9.5 3.92049 10.2462 4.66668 11.1667 4.66668Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.8335 4.66669V6.33335V6.33335C2.8335 7.25383 3.57969 8.00002 4.50016 8.00002H9.50016V8.00002C10.4206 8.00002 11.1668 7.25383 11.1668 6.33335V4.66669" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 8V11.3333" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 17C11.4477 17 11 17.4477 11 18C11 18.5523 11.4477 19 12 19C12.5523 19 13 18.5523 13 18C13 17.4477 12.5523 17 12 17ZM9 18C9 16.3431 10.3431 15 12 15C13.6569 15 15 16.3431 15 18C15 19.6569 13.6569 21 12 21C10.3431 21 9 19.6569 9 18Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 5C6.44772 5 6 5.44772 6 6C6 6.55228 6.44772 7 7 7C7.55228 7 8 6.55228 8 6C8 5.44772 7.55228 5 7 5ZM4 6C4 4.34315 5.34315 3 7 3C8.65685 3 10 4.34315 10 6C10 7.65685 8.65685 9 7 9C5.34315 9 4 7.65685 4 6Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17 5C16.4477 5 16 5.44772 16 6C16 6.55228 16.4477 7 17 7C17.5523 7 18 6.55228 18 6C18 5.44772 17.5523 5 17 5ZM14 6C14 4.34315 15.3431 3 17 3C18.6569 3 20 4.34315 20 6C20 7.65685 18.6569 9 17 9C15.3431 9 14 7.65685 14 6Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 7C7.55228 7 8 7.44772 8 8V10C8 10.5523 8.44772 11 9 11H15C15.5523 11 16 10.5523 16 10V8C16 7.44772 16.4477 7 17 7C17.5523 7 18 7.44772 18 8V10C18 11.6569 16.6569 13 15 13H9C7.34315 13 6 11.6569 6 10V8C6 7.44772 6.44772 7 7 7Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 11C12.5523 11 13 11.4477 13 12V16C13 16.5523 12.5523 17 12 17C11.4477 17 11 16.5523 11 16V12C11 11.4477 11.4477 11 12 11Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 121 KiB

View File

@ -1113,7 +1113,7 @@ export const REQUEST_TEMPLATE = () => "Request for a template";
export const SEARCH_TEMPLATES = () => "Search templates";
export const INTRODUCING_TEMPLATES = () => "Introducing Templates";
export const TEMPLATE_NOTIFICATION_DESCRIPTION = () =>
"You can browse, fork, and make them your own here";
"Use these templates to learn, create, and build apps even faster";
export const GO_BACK = () => "GO BACK";
export const OVERVIEW = () => "Overview";
export const FUNCTION = () => "Function";
@ -1123,6 +1123,8 @@ export const NOTE = () => "Note:";
export const NOTE_MESSAGE = () => "You can add your datasources as well";
export const WIDGET_USED = () => "Widgets Used";
export const SIMILAR_TEMPLATES = () => "Similar Templates";
export const VIEW_ALL_TEMPLATES = () => "VIEW ALL TEMPLATES";
export const FILTERS = () => "FILTERS";
export const IMAGE_LOAD_ERROR = () => "Unable to display the image";

View File

@ -690,6 +690,8 @@ export const ReduxActionTypes = {
FETCH_PLUGIN_AND_JS_ACTIONS_SUCCESS: "FETCH_PLUGIN_AND_JS_ACTIONS_SUCCESS",
GET_DEFAULT_PLUGINS_REQUEST: "GET_DEFAULT_PLUGINS_REQUEST",
GET_DEFAULT_PLUGINS_SUCCESS: "GET_DEFAULT_PLUGINS_SUCCESS",
GET_TEMPLATE_INIT: "GET_TEMPLATES_INIT",
GET_TEMPLATE_SUCCESS: "GET_TEMPLATES_SUCCESS",
};
export type ReduxActionType = typeof ReduxActionTypes[keyof typeof ReduxActionTypes];
@ -857,6 +859,7 @@ export const ReduxActionErrorTypes = {
IMPORT_TEMPLATE_TO_ORGANISATION_ERROR:
"IMPORT_TEMPLATE_TO_ORGANISATION_ERROR",
GET_DEFAULT_PLUGINS_ERROR: "GET_DEFAULT_PLUGINS_ERROR",
GET_TEMPLATE_ERROR: "GET_TEMPLATE_ERROR",
};
export const ReduxFormActionTypes = {

View File

@ -23,6 +23,10 @@ export const PopoverStyles = createGlobalStyle`
.onboarding-carousel .${Classes.OVERLAY_CONTENT} {
filter: drop-shadow(0px 6px 20px rgba(0, 0, 0, 0.15));
}
.templates-notification .bp3-popover2-arrow {
// !important because top is specified as an inline style in the lib
top: -8px !important;
}
.templates-notification .bp3-popover2-arrow-fill {
fill: ${Colors.SEA_SHELL};
}

View File

@ -19,12 +19,6 @@ import history from "utils/history";
const Cta = styled(Button)`
${(props) => getTypographyByKey(props, "btnLarge")}
height: 21px;
span > svg {
height: 10px;
path {
stroke: white;
}
}
`;
const ForkButton = styled(Cta)`
@ -67,7 +61,7 @@ function GetAppViewerHeaderCTA(props: any) {
<ForkButton
className="t--fork-app"
href={forkUrl}
icon="compasses-line"
icon="fork-2"
text={createMessage(FORK_APP)}
/>
);
@ -79,7 +73,7 @@ function GetAppViewerHeaderCTA(props: any) {
trigger={
<TriggerButton
className="t--fork-app"
icon="compasses-line"
icon="fork-2"
onClick={() => dispatch(getAllApplications())}
size={Size.small}
text={createMessage(FORK_APP)}

View File

@ -477,7 +477,7 @@ export function ApplicationCard(props: ApplicationCardProps) {
moreActionItems.push({
onSelect: forkApplicationInitiate,
text: "Fork",
icon: "compasses-line",
icon: "fork-2",
cypressSelector: "t--fork-app",
});
}

View File

@ -93,7 +93,7 @@ function ForkApplicationModal(props: ForkApplicationModalProps) {
<StyledDialog
canOutsideClickClose
className={"fork-modal"}
headerIcon={{ name: "compasses-line", bgColor: Colors.GEYSER_LIGHT }}
headerIcon={{ name: "fork-2", bgColor: Colors.GEYSER_LIGHT }}
isOpen={isModalOpen || showBasedOnURL}
setModalClose={setModalClose}
title={modalHeading}
@ -107,6 +107,7 @@ function ForkApplicationModal(props: ForkApplicationModalProps) {
!!organizationList.length && (
<>
<Dropdown
boundary="viewport"
dropdownMaxHeight={"200px"}
fillOptions
onSelect={(_, dropdownOption) =>

View File

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React from "react";
import styled from "styled-components";
import { useDispatch, useSelector } from "react-redux";
import { Collapse } from "@blueprintjs/core";
@ -6,7 +6,7 @@ import { Classes } from "components/ads/common";
import Text, { TextType } from "components/ads/Text";
import Icon, { IconSize } from "components/ads/Icon";
import { filterTemplates } from "actions/templateActions";
import { createMessage, MORE, SHOW_LESS } from "@appsmith/constants/messages";
import { createMessage, FILTERS } from "@appsmith/constants/messages";
import {
getFilterListSelector,
getTemplateFilterSelector,
@ -14,6 +14,7 @@ import {
import LeftPaneBottomSection from "pages/Home/LeftPaneBottomSection";
import { thinScrollbar } from "constants/DefaultTheme";
import { Colors } from "constants/Colors";
import AnalyticsUtil from "utils/AnalyticsUtil";
const FilterWrapper = styled.div`
overflow: auto;
@ -38,7 +39,6 @@ const Wrapper = styled.div`
padding-left: ${(props) => props.theme.spaces[7]}px;
padding-top: ${(props) => props.theme.spaces[11]}px;
flex-direction: column;
box-shadow: 1px 0px 0px ${Colors.GALLERY_2};
`;
const SecondWrapper = styled.div`
@ -80,6 +80,12 @@ const StyledFilterItem = styled.div<{ selected: boolean }>`
const StyledFilterCategory = styled(Text)`
margin-bottom: ${(props) => props.theme.spaces[4]}px;
padding-left: ${(props) => props.theme.spaces[6]}px;
font-weight: bold;
&.title {
margin-bottom: ${(props) => props.theme.spaces[12] - 2}px;
display: inline-block;
}
`;
const ListWrapper = styled.div`
@ -110,7 +116,13 @@ interface FilterCategoryProps {
function FilterItem({ item, onSelect, selected }: FilterItemProps) {
const onClick = () => {
const action = selected ? "remove" : "add";
onSelect(item?.value ?? item.label, action);
const filterValue = item?.value ?? item.label;
onSelect(filterValue, action);
if (action === "add") {
AnalyticsUtil.logEvent("TEMPLATE_FILTER_SELECTED", {
filter: filterValue,
});
}
};
return (
@ -128,11 +140,11 @@ function FilterCategory({
label,
selectedFilters,
}: FilterCategoryProps) {
const [expand, setExpand] = useState(false);
// const [expand, setExpand] = useState(!!selectedFilters.length);
const dispatch = useDispatch();
// This indicates how many filter items do we want to show, the rest are hidden
// behind show more.
const FILTERS_TO_SHOW = 3;
const FILTERS_TO_SHOW = 4;
const onSelect = (item: string, type: string) => {
if (type === "add") {
dispatch(filterTemplates(label, [...selectedFilters, item]));
@ -146,9 +158,9 @@ function FilterCategory({
}
};
const toggleExpand = () => {
setExpand((expand) => !expand);
};
// const toggleExpand = () => {
// setExpand((expand) => !expand);
// };
const isSelected = (filter: Filter) => {
return selectedFilters.includes(filter?.value ?? filter.label);
@ -156,7 +168,7 @@ function FilterCategory({
return (
<FilterCategoryWrapper>
<StyledFilterCategory type={TextType.SIDE_HEAD}>
<StyledFilterCategory type={TextType.P4}>
{label.toLocaleUpperCase()}{" "}
{!!selectedFilters.length && `(${selectedFilters.length})`}
</StyledFilterCategory>
@ -171,7 +183,7 @@ function FilterCategory({
/>
);
})}
<Collapse isOpen={expand}>
<Collapse isOpen>
{filterList.slice(FILTERS_TO_SHOW).map((filter) => {
return (
<FilterItem
@ -183,7 +195,8 @@ function FilterCategory({
);
})}
</Collapse>
{!!filterList.slice(FILTERS_TO_SHOW).length && (
{/* We will be adding this back later */}
{/* {!!filterList.slice(FILTERS_TO_SHOW).length && (
<Text
className={`more ${selectedFilters.length && expand && "hide"}`}
onClick={toggleExpand}
@ -192,9 +205,11 @@ function FilterCategory({
>
{expand
? `- ${createMessage(SHOW_LESS)}`
: `+ ${filterList.slice(3).length} ${createMessage(MORE)}`}
: `+ ${filterList.slice(FILTERS_TO_SHOW).length} ${createMessage(
MORE,
)}`}
</Text>
)}
)} */}
</ListWrapper>
</FilterCategoryWrapper>
);
@ -208,6 +223,9 @@ function Filters() {
<Wrapper>
<SecondWrapper>
<FilterWrapper>
<StyledFilterCategory className={"title"} type={TextType.H5}>
{createMessage(FILTERS)}
</StyledFilterCategory>
{Object.keys(filters).map((filter) => {
return (
<FilterCategory

View File

@ -66,13 +66,14 @@ function ForkTemplate({
return (
<StyledDialog
canOutsideClickClose={!isImportingTemplate}
headerIcon={{ name: "compasses-line", bgColor: Colors.GEYSER_LIGHT }}
headerIcon={{ name: "fork-2", bgColor: Colors.GEYSER_LIGHT }}
isOpen={showForkModal}
onClose={isImportingTemplate ? noop : onClose}
title={createMessage(CHOOSE_WHERE_TO_FORK)}
trigger={children}
>
<Dropdown
boundary="viewport"
dropdownMaxHeight={"200px"}
fillOptions
onSelect={(_value, dropdownOption) =>

View File

@ -8,7 +8,6 @@ const LargeTemplate = styled(TemplateLayout)`
display: flex;
flex: 1;
flex-direction: column;
max-width: 50%;
cursor: pointer;
&:hover {
box-shadow: 0px 20px 24px -4px rgba(16, 24, 40, 0.1),
@ -30,22 +29,10 @@ const LargeTemplate = styled(TemplateLayout)`
}
.image-wrapper {
padding: ${(props) =>
`${props.theme.spaces[9]}px ${props.theme.spaces[11]}px ${props.theme.spaces[0]}px`};
transition: all 1s ease-out;
width: 100%;
height: 270px;
}
.fork-button {
height: 38px;
width: 38px;
svg {
height: 18px;
width: 18px;
}
}
`;
export default LargeTemplate;

View File

@ -16,7 +16,7 @@ const Wrapper = styled.div`
display: flex;
flex-direction: column;
padding: ${(props) => props.theme.spaces[11]}px;
background-color: rgba(248, 248, 248, 0.5);
background-color: ${Colors.SEA_SHELL};
transition: all 1s ease-out;
margin-bottom: ${(props) => props.theme.spaces[12]}px;
@ -43,24 +43,25 @@ const Wrapper = styled.div`
`;
const StyledImage = styled.img`
height: 147px;
height: 168px;
object-fit: cover;
`;
const REQUEST_TEMPLATE_URL =
"https://app.appsmith.com/applications/6241b5a8c99df2369931a653/pages/6241b5a8c99df2369931a656";
function RequestTemplate() {
const onClick = () => {
window.open(
"https://github.com/appsmithorg/appsmith/issues/new?assignees=Kocharrahul8&labels=Example+Apps&template=Templates.yaml&title=%5BTemplate%5D%3A+",
);
window.open(REQUEST_TEMPLATE_URL);
};
return (
<Wrapper>
<StyledImage src={RequestTemplateSvg} />
<Text className={"title"} type={TextType.H4}>
<Text className={"title"} type={TextType.H1}>
{createMessage(COULDNT_FIND_TEMPLATE)}
</Text>
<Text className={"description"} type={TextType.P2}>
<Text className={"description"} type={TextType.P1}>
{createMessage(COULDNT_FIND_TEMPLATE_DESCRIPTION)}
</Text>
<Button

View File

@ -26,14 +26,15 @@ const TemplateWrapper = styled.div`
`;
const ImageWrapper = styled.div`
padding: ${(props) =>
`${props.theme.spaces[9]}px ${props.theme.spaces[11]}px`};
padding: ${(props) => props.theme.spaces[9]}px;
overflow: hidden;
`;
const StyledImage = styled.img`
box-shadow: 0px 17.52px 24.82px rgba(0, 0, 0, 0.09);
object-fit: cover;
object-fit: contain;
width: 100%;
height: 236px;
`;
const TemplateContent = styled.div`
@ -44,18 +45,19 @@ const TemplateContent = styled.div`
flex: 1;
.title {
${(props) => getTypographyByKey(props, "h4")}
${(props) => getTypographyByKey(props, "h1")}
color: ${Colors.EBONY_CLAY};
}
.categories {
${(props) => getTypographyByKey(props, "p1")}
${(props) => getTypographyByKey(props, "h4")}
font-weight: normal;
color: var(--appsmith-color-black-800);
margin-top: ${(props) => props.theme.spaces[1]}px;
}
.description {
margin-top: ${(props) => props.theme.spaces[2]}px;
color: var(--appsmith-color-black-700);
${(props) => getTypographyByKey(props, "p2")}
${(props) => getTypographyByKey(props, "p1")}
}
`;
@ -83,8 +85,8 @@ const StyledButton = styled(Button)`
width: 31px;
svg {
height: 15px;
width: 15px;
height: 20px;
width: 20px;
}
`;
@ -159,7 +161,7 @@ export function TemplateLayout(props: TemplateLayoutProps) {
<Tooltip content={createMessage(FORK_THIS_TEMPLATE)}>
<StyledButton
className="t--fork-template fork-button"
icon="compasses-line"
icon="fork-2"
size={Size.medium}
tag="button"
/>

View File

@ -5,6 +5,13 @@ import Template from "./Template";
import { Template as TemplateInterface } from "api/TemplatesApi";
import RequestTemplate from "./Template/RequestTemplate";
const breakpointColumnsObject = {
default: 4,
3000: 3,
1500: 2,
950: 1,
};
const Wrapper = styled.div`
padding-right: ${(props) => props.theme.spaces[12]}px;
padding-left: ${(props) => props.theme.spaces[12]}px;
@ -19,11 +26,6 @@ const Wrapper = styled.div`
}
`;
const FirstRow = styled.div`
display: flex;
flex-direction: row;
gap: ${(props) => props.theme.spaces[8]}px;
`;
interface TemplateListProps {
templates: TemplateInterface[];
}
@ -31,18 +33,13 @@ interface TemplateListProps {
function TemplateList(props: TemplateListProps) {
return (
<Wrapper>
<FirstRow>
{props.templates.slice(0, 2).map((template) => (
<Template key={template.id} size="large" template={template} />
))}
</FirstRow>
<Masonry
breakpointCols={3}
breakpointCols={breakpointColumnsObject}
className="grid"
columnClassName="grid_column"
>
{props.templates.slice(2).map((template) => (
<Template key={template.id} template={template} />
{props.templates.map((template) => (
<Template key={template.id} size="large" template={template} />
))}
<RequestTemplate />
</Masonry>

View File

@ -11,12 +11,14 @@ import Template from "./Template";
import DatasourceChip from "./DatasourceChip";
import WidgetInfo from "./WidgetInfo";
import {
getTemplateById,
isFetchingTemplatesSelector,
getActiveTemplateSelector,
isFetchingTemplateSelector,
} from "selectors/templatesSelectors";
import ForkTemplate from "./ForkTemplate";
import LeftPaneTemplateList from "./LeftPaneTemplateList";
import { getSimilarTemplatesInit } from "actions/templateActions";
import {
getSimilarTemplatesInit,
getTemplateInformation,
} from "actions/templateActions";
import { AppState } from "reducers";
import { Icon, IconSize } from "components/ads";
import history from "utils/history";
@ -35,37 +37,40 @@ import {
WIDGET_USED,
DATASOURCES,
SIMILAR_TEMPLATES,
VIEW_ALL_TEMPLATES,
} from "@appsmith/constants/messages";
const Wrapper = styled.div`
width: calc(100% - ${(props) => props.theme.homePage.sidebar}px);
overflow: auto;
const breakpointColumnsObject = {
default: 4,
1600: 3,
1100: 2,
700: 1,
};
.breadcrumb-placeholder {
margin-top: ${(props) => props.theme.spaces[12]}px;
height: 16px;
width: 195px;
}
.title-placeholder {
margin-top: ${(props) => props.theme.spaces[11]}px;
height: 28px;
width: 269px;
}
.iframe-placeholder {
margin-top: ${(props) => props.theme.spaces[12]}px;
height: 500px;
width: 100%;
}
const Wrapper = styled.div`
overflow: auto;
position: relative;
`;
const TemplateViewWrapper = styled.div`
padding-right: ${(props) => props.theme.spaces[12]}px;
padding-left: ${(props) => props.theme.spaces[12]}px;
padding-right: 132px;
padding-left: 132px;
padding-top: ${(props) => props.theme.spaces[12]}px;
padding-bottom: 80px;
background-color: ${Colors.WHITE};
`;
const HeaderWrapper = styled.div`
display: flex;
align-items: center;
.left,
.right {
flex: 1;
}
`;
const Title = styled(Text)`
margin-top: ${(props) => props.theme.spaces[5]}px;
display: inline-block;
`;
@ -97,7 +102,7 @@ const Section = styled.div`
padding-top: ${(props) => props.theme.spaces[12]}px;
.section-content {
margin-top: ${(props) => props.theme.spaces[9]}px;
margin-top: ${(props) => props.theme.spaces[3]}px;
}
.template-fork-button {
@ -135,8 +140,8 @@ const TemplateDatasources = styled.div`
`;
const SimilarTemplatesWrapper = styled.div`
padding-right: ${(props) => props.theme.spaces[12]}px;
padding-left: ${(props) => props.theme.spaces[12]}px;
padding-right: 132px;
padding-left: 132px;
background-color: rgba(248, 248, 248, 0.5);
.grid {
@ -183,32 +188,47 @@ const PageWrapper = styled.div`
height: calc(100vh - ${(props) => props.theme.homePage.header}px);
`;
const BackButtonWrapper = styled.div`
const BackButtonWrapper = styled.div<{ width?: number }>`
cursor: pointer;
display: flex;
align-items: center;
gap: ${(props) => props.theme.spaces[2]}px;
margin-top: ${(props) => props.theme.spaces[12]}px;
${(props) => props.width && `width: ${props.width};`}
`;
const LoadingWrapper = styled.div`
width: calc(100vw);
.title-placeholder {
margin-top: ${(props) => props.theme.spaces[11]}px;
height: 28px;
width: 100%;
}
.iframe-placeholder {
margin-top: ${(props) => props.theme.spaces[12]}px;
height: 500px;
width: 100%;
}
`;
const SimilarTemplatesTitleWrapper = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
`;
function TemplateViewLoader() {
return (
<Wrapper>
<LoadingWrapper>
<TemplateViewWrapper>
<div className={`breadcrumb-placeholder ${Classes.SKELETON}`} />
<div className={`title-placeholder ${Classes.SKELETON}`} />
<div className={`iframe-placeholder ${Classes.SKELETON}`} />
</TemplateViewWrapper>
</Wrapper>
</LoadingWrapper>
);
}
function TemplateNotFound() {
return (
<Wrapper>
<EntityNotFoundPane />;
</Wrapper>
);
return <EntityNotFoundPane />;
}
function TemplateView() {
@ -216,9 +236,9 @@ function TemplateView() {
const similarTemplates = useSelector(
(state: AppState) => state.ui.templates.similarTemplates,
);
const isFetchingTemplates = useSelector(isFetchingTemplatesSelector);
const isFetchingTemplate = useSelector(isFetchingTemplateSelector);
const params = useParams<{ templateId: string }>();
const currentTemplate = useSelector(getTemplateById(params.templateId));
const currentTemplate = useSelector(getActiveTemplateSelector);
const [showForkModal, setShowForkModal] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
@ -235,30 +255,38 @@ function TemplateView() {
};
useEffect(() => {
dispatch(getTemplateInformation(params.templateId));
dispatch(getSimilarTemplatesInit(params.templateId));
if (containerRef.current) {
containerRef.current.scrollTo({ top: 0, behavior: "smooth" });
dispatch(getSimilarTemplatesInit(params.templateId));
containerRef.current.scrollTo({ top: 0 });
}
}, [params.templateId]);
const goBack = () => {
history.goBack();
};
return (
<PageWrapper>
<LeftPaneTemplateList />
{isFetchingTemplates ? (
{isFetchingTemplate ? (
<TemplateViewLoader />
) : !currentTemplate ? (
<TemplateNotFound />
) : (
<Wrapper ref={containerRef}>
<TemplateViewWrapper>
<BackButtonWrapper onClick={goToTemplateListView}>
<Icon name="view-less" size={IconSize.XL} />
<Text type={TextType.P4}>{createMessage(GO_BACK)}</Text>
</BackButtonWrapper>
<Title type={TextType.DANGER_HEADING}>
{currentTemplate.title}
</Title>
<HeaderWrapper>
<div className="left">
<BackButtonWrapper onClick={goBack}>
<Icon name="view-less" size={IconSize.XL} />
<Text type={TextType.P4}>{createMessage(GO_BACK)}</Text>
</BackButtonWrapper>
</div>
<Title type={TextType.DANGER_HEADING}>
{currentTemplate.title}
</Title>
<div className="right" />
</HeaderWrapper>
<IframeWrapper>
<IframeTopBar>
<div className="round red" />
@ -286,7 +314,7 @@ function TemplateView() {
>
<Button
className="template-fork-button"
icon="compasses-line"
icon="fork-2"
iconPosition={IconPositions.left}
onClick={onForkButtonTrigger}
size={Size.large}
@ -357,11 +385,19 @@ function TemplateView() {
{!!similarTemplates.length && (
<SimilarTemplatesWrapper>
<Section>
<Text type={TextType.H1} weight={FontWeight.BOLD}>
{createMessage(SIMILAR_TEMPLATES)}
</Text>
<SimilarTemplatesTitleWrapper>
<Text type={TextType.H1} weight={FontWeight.BOLD}>
{createMessage(SIMILAR_TEMPLATES)}
</Text>
<BackButtonWrapper onClick={goToTemplateListView}>
<Text type={TextType.P4}>
{createMessage(VIEW_ALL_TEMPLATES)}
</Text>
<Icon name="view-all" size={IconSize.XL} />
</BackButtonWrapper>
</SimilarTemplatesTitleWrapper>
<Masonry
breakpointCols={3}
breakpointCols={breakpointColumnsObject}
className="grid"
columnClassName="grid_column"
>

View File

@ -1,7 +1,7 @@
import { Popover2, Classes as Popover2Classes } from "@blueprintjs/popover2";
import { useLocation } from "react-router";
import { setTemplateNotificationSeenAction } from "actions/templateActions";
import { TextType, Text } from "components/ads";
import { TextType, Text, Classes } from "components/ads";
import Icon, { IconSize } from "components/ads/Icon";
import { Colors } from "constants/Colors";
import { matchTemplatesPath } from "constants/routes";
@ -24,14 +24,20 @@ import { AppState } from "reducers";
const NotificationWrapper = styled.div`
background-color: ${Colors.SEA_SHELL};
padding: ${(props) =>
`${props.theme.spaces[3]}px ${props.theme.spaces[8]}px`};
`${props.theme.spaces[4]}px ${props.theme.spaces[8]}px`};
display: flex;
flex-direction: row;
max-width: 376px;
.${Classes.ICON} {
align-items: unset;
margin-top: ${(props) => props.theme.spaces[0] + 1}px;
}
.text-wrapper {
display: flex;
flex-direction: column;
margin-left: ${(props) => props.theme.spaces[8]}px;
margin-left: ${(props) => props.theme.spaces[3]}px;
}
.description {
@ -51,8 +57,14 @@ export function TemplateFeatureNotification() {
<NotificationWrapper>
<Icon name={"info"} size={IconSize.XXXL} />
<div className={"text-wrapper"}>
<Text type={TextType.H4}>{createMessage(INTRODUCING_TEMPLATES)}</Text>
<Text className="description" type={TextType.P1}>
<Text color={Colors.CODE_GRAY} type={TextType.H4}>
{createMessage(INTRODUCING_TEMPLATES)}
</Text>
<Text
className="description"
color={Colors.CODE_GRAY}
type={TextType.P1}
>
{createMessage(TEMPLATE_NOTIFICATION_DESCRIPTION)}
</Text>
</div>

View File

@ -37,6 +37,7 @@ const PageWrapper = styled.div`
margin-top: ${(props) => props.theme.homePage.header}px;
display: flex;
height: calc(100vh - ${(props) => props.theme.homePage.header}px);
padding-left: 8vw;
`;
export const TemplateListWrapper = styled.div`
@ -44,6 +45,7 @@ export const TemplateListWrapper = styled.div`
width: calc(100% - ${(props) => props.theme.homePage.sidebar}px);
height: calc(100vh - ${(props) => props.theme.headerHeight});
overflow: auto;
padding-right: 8vw;
`;
export const ResultsCount = styled.div`

View File

@ -9,6 +9,8 @@ import { Template } from "api/TemplatesApi";
const initialState: TemplatesReduxState = {
isImportingTemplate: false,
gettingAllTemplates: false,
gettingTemplate: false,
activeTemplate: null,
templates: [],
similarTemplates: [],
filters: {},
@ -23,6 +25,22 @@ const templateReducer = createReducer(initialState, {
gettingAllTemplates: true,
};
},
[ReduxActionTypes.GET_TEMPLATE_INIT]: (state: TemplatesReduxState) => {
return {
...state,
gettingTemplate: true,
};
},
[ReduxActionTypes.GET_TEMPLATE_SUCCESS]: (
state: TemplatesReduxState,
action: ReduxAction<Template>,
) => {
return {
...state,
gettingTemplate: false,
activeTemplate: action.payload,
};
},
[ReduxActionTypes.GET_ALL_TEMPLATES_SUCCESS]: (
state: TemplatesReduxState,
action: ReduxAction<Template[]>,
@ -78,6 +96,12 @@ const templateReducer = createReducer(initialState, {
isImportingTemplate: false,
};
},
[ReduxActionErrorTypes.GET_TEMPLATE_ERROR]: (state: TemplatesReduxState) => {
return {
...state,
gettingTemplate: false,
};
},
[ReduxActionTypes.GET_SIMILAR_TEMPLATES_SUCCESS]: (
state: TemplatesReduxState,
action: ReduxAction<Template[]>,
@ -100,7 +124,9 @@ const templateReducer = createReducer(initialState, {
export interface TemplatesReduxState {
gettingAllTemplates: boolean;
gettingTemplate: boolean;
templates: Template[];
activeTemplate: Template | null;
similarTemplates: Template[];
filters: Record<string, string[]>;
templateSearchQuery: string;

View File

@ -114,9 +114,33 @@ function* getTemplateNotificationSeenSaga() {
}
}
function* getTemplateSaga(action: ReduxAction<string>) {
try {
const response = yield call(
TemplatesAPI.getTemplateInformation,
action.payload,
);
const isValid = yield validateResponse(response);
if (isValid) {
yield put({
type: ReduxActionTypes.GET_TEMPLATE_SUCCESS,
payload: response.data,
});
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.GET_TEMPLATE_ERROR,
payload: {
error,
},
});
}
}
export default function* watchActionSagas() {
yield all([
takeEvery(ReduxActionTypes.GET_ALL_TEMPLATES_INIT, getAllTemplatesSaga),
takeEvery(ReduxActionTypes.GET_TEMPLATE_INIT, getTemplateSaga),
takeEvery(
ReduxActionTypes.GET_SIMILAR_TEMPLATES_INIT,
getSimilarTemplatesSaga,

View File

@ -1,18 +1,14 @@
import { Template } from "api/TemplatesApi";
import { FilterKeys, Template } from "api/TemplatesApi";
import Fuse from "fuse.js";
import { AppState } from "reducers";
import { createSelector } from "reselect";
import { getOrganizationCreateApplication } from "./applicationSelectors";
import { getWidgetCards } from "./editorSelectors";
import { getDefaultPlugins } from "./entitiesSelector";
import {
functions as allIndustries,
useCases as allUseCases,
} from "pages/Templates/constants";
import { Filter } from "pages/Templates/Filters";
const fuzzySearchOptions = {
keys: ["title", "id", "functions", "useCases"],
keys: ["title", "id"],
shouldSort: true,
threshold: 0.5,
location: 0,
@ -51,28 +47,44 @@ export const getTemplateFiltersLength = createSelector(
export const isFetchingTemplatesSelector = (state: AppState) =>
state.ui.templates.gettingAllTemplates;
export const isFetchingTemplateSelector = (state: AppState) =>
state.ui.templates.gettingTemplate;
export const getTemplateById = (id: string) => (state: AppState) => {
return state.ui.templates.templates.find((template) => template.id === id);
};
export const getActiveTemplateSelector = (state: AppState) =>
state.ui.templates.activeTemplate;
export const getFilteredTemplateList = createSelector(
getTemplatesSelector,
getTemplateFilterSelector,
(templates, templatesFilters) => {
if (Object.keys(templatesFilters).length) {
return templates.filter((template) => {
return Object.keys(templatesFilters).every((filterKey) => {
if (!templatesFilters[filterKey].length) return true;
getTemplateFiltersLength,
(templates, templatesFilters, numberOfFiltersApplied) => {
const result: Template[] = [];
return templatesFilters[filterKey].every((value: string) =>
template[filterKey as keyof Template].includes(value),
);
});
});
if (!numberOfFiltersApplied) {
return templates;
}
return templates;
if (!Object.keys(templatesFilters).length) {
return templates;
}
Object.keys(templatesFilters).map((filter) => {
templates.map((template) => {
if (
template[filter as FilterKeys].some((templateFilter) => {
return templatesFilters[filter].includes(templateFilter);
})
) {
result.push(template);
}
});
});
return result;
},
);
@ -113,8 +125,6 @@ export const getFilterListSelector = createSelector(
(widgetConfigs, allDatasources, templates) => {
const filters: Record<string, Filter[]> = {
datasources: [],
useCases: [],
functions: [],
widgets: [],
};
@ -153,8 +163,6 @@ export const getFilterListSelector = createSelector(
templates.map((template) => {
filterFilters("datasources", allDatasources, template);
filterFilters("widgets", allWidgets, template);
filterFilters("useCases", allUseCases, template);
filterFilters("functions", allIndustries, template);
});
return filters;

View File

@ -210,7 +210,8 @@ export type EventName =
| "DATASOURCE_AUTH_COMPLETE"
| "RECONNECTING_DATASOURCE_ITEM_CLICK"
| "ADD_MISSING_DATASOURCE_LINK_CLICK"
| "RECONNECTING_SKIP_TO_APPLICATION_BUTTON_CLICK";
| "RECONNECTING_SKIP_TO_APPLICATION_BUTTON_CLICK"
| "TEMPLATE_FILTER_SELECTED";
function getApplicationId(location: Location) {
const pathSplit = location.pathname.split("/");