Skip to content

Commit

Permalink
Improve custom components modal
Browse files Browse the repository at this point in the history
- add support for multiple collections
- add support for featured collections (from GitHub)
- add support for loading collections from a custom URL

Signed-off-by: Zvonimir Fras <[email protected]>
  • Loading branch information
zvonimirfras committed Jan 24, 2024
1 parent 3e4b780 commit eed26a9
Show file tree
Hide file tree
Showing 7 changed files with 279 additions and 47 deletions.
22 changes: 18 additions & 4 deletions app/src/components/custom-components-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,27 @@ import {
} from '@carbon/react';
import { TrashCan, DataEnrichmentAdd } from '@carbon/icons-react';
import { CustomComponentsCollectionEditor, getNewCustomComponentsCollection } from '@carbon-builder/sdk-react';
import { GlobalStateContext, ModalContext } from '../context';
import { GithubContext, GlobalStateContext, ModalContext } from '../context';
import { css } from 'emotion';
import { capitalize } from 'lodash';

export const CustomComponentsModal = () => {
const { customComponentsModal, hideCustomComponentsModal } = useContext(ModalContext);
const { customComponentsCollections, setCustomComponentsCollections } = useContext(GlobalStateContext);
const { getFeaturedCustomComponentsCollections } = useContext(GithubContext);

const [isDeleteOpen, setIsDeleteOpen] = useState({} as any);
const [featuredCollectionsItems, setFeaturedCollectionsItems] = useState([] as any[]);

useEffect(() => {
getFeaturedCustomComponentsCollections().then((value: any) => {
setFeaturedCollectionsItems(value.map((v: any) => ({
id: v.name,
text: capitalize(v.name)
})));
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

useEffect(() => {
if (!customComponentsCollections || !Array.isArray(customComponentsCollections) || customComponentsCollections.length === 0) {
Expand All @@ -37,7 +50,7 @@ export const CustomComponentsModal = () => {
<Accordion className={css`.cds--accordion__content { position: relative; }`}>
{
customComponentsCollections.map((collection: any, index: number) =>
<AccordionItem title={collection.name} key={collection.name}>
<AccordionItem title={collection.name} key={index}>
<Button
kind='danger'
className={css`position: absolute; right: 0;`}
Expand All @@ -48,11 +61,12 @@ export const CustomComponentsModal = () => {
<CustomComponentsCollectionEditor
key={collection.name}
collection={collection}
featuredCollectionsItems={featuredCollectionsItems}
setCollection={(c: any) => {
setCustomComponentsCollections([
...(index > 0 ? [customComponentsCollections.slice(0, index - 1)] : []),
...customComponentsCollections.slice(0, index),
c,
...[customComponentsCollections.slice(index + 1)]
...customComponentsCollections.slice(index + 1)
]);
}} />
<Modal
Expand Down
19 changes: 15 additions & 4 deletions app/src/components/fragment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
updatedState
} from '@carbon-builder/sdk-react';
import './fragment-preview.scss';
import { useRemoteCustomComponentsCollections } from '../utils/misc-tools';

const canvas = css`
border: 2px solid #d8d8d8;
Expand Down Expand Up @@ -105,6 +106,8 @@ export const Fragment = ({ fragment, setFragment, outline }: any) => {
const [showDragOverIndicator, setShowDragOverIndicator] = useState(false);
const [customComponentsStyles, setCustomComponentsStyles] = useState(css``);
const [customComponentsStylesUrls, _setCustomComponentsStylesUrls] = useState<string[]>([]);
const [remoteCustomComponentsCollections] = useRemoteCustomComponentsCollections();
const [allCustomComponentsCollections, setAllCustomComponentsCollections] = useState([] as any[]);
const containerRef = useRef(null as any);
const holderRef = useRef(null as any);

Expand All @@ -129,9 +132,16 @@ export const Fragment = ({ fragment, setFragment, outline }: any) => {

// update requested urls
useEffect(() => {
setCustomComponentsStylesUrls(getUsedCollectionsStyleUrls(globalState.customComponentsCollections, fragment.data));
setCustomComponentsStylesUrls(getUsedCollectionsStyleUrls(allCustomComponentsCollections, fragment.data) || []);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fragment.data, globalState.customComponentsCollections]);
}, [fragment.data, allCustomComponentsCollections]);

useEffect(() => {
setAllCustomComponentsCollections([
...(remoteCustomComponentsCollections as any[] || []).flat(),
...globalState.customComponentsCollections
]);
}, [remoteCustomComponentsCollections, globalState.customComponentsCollections]);

const resize = throttle((e: any) => {
const rect = containerRef.current.getBoundingClientRect();
Expand Down Expand Up @@ -261,8 +271,9 @@ export const Fragment = ({ fragment, setFragment, outline }: any) => {
// ///////////////////////////////////////////////
if (componentObj.componentsCollection) {
// our component belongs to one of the custom components collections
const customComponentsCollection = Array.isArray(globalState.customComponentsCollections)
&& globalState.customComponentsCollections?.find((ccc: any) => ccc.name === componentObj.componentsCollection);
const customComponentsCollection = Array.isArray(allCustomComponentsCollections)
&& allCustomComponentsCollections?.find((ccc: any) => ccc.name === componentObj.componentsCollection);

if (customComponentsCollection) {
const customComponent = customComponentsCollection.components.find((cc: any) => cc.type === componentObj.type);

Expand Down
45 changes: 37 additions & 8 deletions app/src/context/github-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const GithubContextProvider = ({ children }: any) => {
_setGithubToken(t);
};

const getContent = async (owner: string, repo: string, contentPath: string, format: 'raw' | 'object' = 'object') => {
const getGithubContent = async (owner: string, repo: string, contentPath: string, format: 'raw' | 'object' = 'object') => {
let path = contentPath;
if (path.endsWith('/')) {
// github doesn't like when we're fetching with a slash, so we remove it
Expand Down Expand Up @@ -123,13 +123,13 @@ const GithubContextProvider = ({ children }: any) => {
};

const getContentWithFolder = async (owner: string, repo: string, contentPath: string, format: 'raw' | 'object' = 'object') => {
const contentState: any = await getContent(owner, repo, contentPath, format);
const contentState: any = await getGithubContent(owner, repo, contentPath, format);

const pathParts = contentPath.split('/');

if (contentState.fileContent || contentState.fileContentBase64 || contentState.fragmentState) {
return {
...(await getContent(owner, repo, pathParts.slice(0, pathParts.length - 1).join('/'), format)),
...(await getGithubContent(owner, repo, pathParts.slice(0, pathParts.length - 1).join('/'), format)),
fileContent: contentState.fileContent,
fileContentBase64: contentState.fileContentBase64,
fragmentState: contentState.fragmentState
Expand All @@ -151,10 +151,10 @@ const GithubContextProvider = ({ children }: any) => {
};

const getFeaturedFragments = async () => {
const featuredFragmentsResponse = await getContent('IBM', 'carbon-ui-builder-featured-fragments', 'featured-fragments', 'raw');
const featuredFragmentsResponse = await getGithubContent('IBM', 'carbon-ui-builder-featured-fragments', 'featured-fragments', 'raw');

const allFeaturedFragments = await Promise.all(((featuredFragmentsResponse as any).data as any[]).map(async (item) => {
const fragmentFileResponse = await getContent('IBM', 'carbon-ui-builder-featured-fragments', item.path, 'raw');
const fragmentFileResponse = await getGithubContent('IBM', 'carbon-ui-builder-featured-fragments', item.path, 'raw');
try {
const data = JSON.parse((fragmentFileResponse as any).data.toString());

Expand All @@ -177,11 +177,38 @@ const GithubContextProvider = ({ children }: any) => {
return allFeaturedFragments.filter(fragment => fragment !== null);
};

const getFeaturedCustomComponentsCollections = async () => {
const featuredFragmentsResponse = await getGithubContent('IBM', 'carbon-ui-builder-featured-fragments', 'custom-components', 'raw');

const allFeaturedFragments = await Promise.all(((featuredFragmentsResponse as any).data as any[]).map(async (item) => {
const customCollectionsResponse = await getGithubContent('IBM', 'carbon-ui-builder-featured-fragments', item.path, 'raw');
try {
const data = JSON.parse((customCollectionsResponse as any).data.toString());
return data;
} catch (error) {
return null;
}
}));

return allFeaturedFragments.filter(fragment => fragment !== null);
};

const getFeaturedCustomComponentsCollection = async (name: string) => {
const customCollectionResponse =
await getGithubContent('IBM', 'carbon-ui-builder-featured-fragments', `custom-components/${name}.json`, 'raw');
try {
const data = JSON.parse((customCollectionResponse as any).data.toString());
return data;
} catch (error) {
return null;
}
};

const getBuiltInTemplates = async () => {
const featuredFragmentsResponse = await getContent('IBM', 'carbon-ui-builder-featured-fragments', 'built-in-templates', 'raw');
const featuredFragmentsResponse = await getGithubContent('IBM', 'carbon-ui-builder-featured-fragments', 'built-in-templates', 'raw');

const allFeaturedFragments = await Promise.all(((featuredFragmentsResponse as any).data as any[]).map(async (item) => {
const fragmentFileResponse = await getContent('IBM', 'carbon-ui-builder-featured-fragments', item.path, 'raw');
const fragmentFileResponse = await getGithubContent('IBM', 'carbon-ui-builder-featured-fragments', item.path, 'raw');
try {
const data = JSON.parse((fragmentFileResponse as any).data.toString());

Expand Down Expand Up @@ -209,8 +236,10 @@ const GithubContextProvider = ({ children }: any) => {
token: githubToken,
setToken: setGithubToken,
getFeaturedFragments,
getFeaturedCustomComponentsCollections,
getFeaturedCustomComponentsCollection,
getBuiltInTemplates,
getContent,
getGithubContent,
getContentWithFolder,
getUser,
getRepos
Expand Down
45 changes: 32 additions & 13 deletions app/src/routes/edit/elements-pane.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import React, { useContext, useRef, useState } from 'react';
import React, {
useContext,
useEffect,
useRef,
useState
} from 'react';
import { css, cx } from 'emotion';
import { Button, Search } from '@carbon/react';
import { ChevronDown, ChevronUp } from '@carbon/react/icons';
Expand All @@ -10,6 +15,7 @@ import { leftPane, leftPaneContent, leftPaneHeader } from '.';
import { GlobalStateContext, useFragment } from '../../context';
import { getEditScreenParams } from '../../utils/fragment-tools';
import { accordionButtonStyle } from './settings-context-pane';
import { useRemoteCustomComponentsCollections } from '../../utils/misc-tools';

const elementTileListStyleBase = css`
display: flex;
Expand Down Expand Up @@ -52,6 +58,16 @@ export const ElementsPane = ({ isActive }: any) => {
customComponentsCollections
} = useContext(GlobalStateContext);

const [remoteCustomComponentsCollections] = useRemoteCustomComponentsCollections();
const [allCustomComponentsCollections, setAllCustomComponentsCollections] = useState([] as any[]);

useEffect(() => {
setAllCustomComponentsCollections([
...(remoteCustomComponentsCollections as any[] || []).flat(),
...customComponentsCollections
]);
}, [remoteCustomComponentsCollections, customComponentsCollections]);

const isLayoutWidgetOpen = settings.layoutWidget?.isAccordionOpen === undefined
? true // open by default
: settings.layoutWidget?.isAccordionOpen;
Expand Down Expand Up @@ -144,19 +160,22 @@ export const ElementsPane = ({ isActive }: any) => {
}
</div>
{
customComponentsCollections && customComponentsCollections.length > 0
&& customComponentsCollections.map((customComponentsCollection: any) => <>
allCustomComponentsCollections && allCustomComponentsCollections.length > 0
&& allCustomComponentsCollections
.filter((customComponentsCollection: any) => !!customComponentsCollection.components)
.map((customComponentsCollection: any) => <>
<h4>{customComponentsCollection.name}</h4>
<div className={elementTileListStyleMicroLayouts}>
{
// components from JSON
customComponentsCollection.components?.map((component: any) =>
<ElementTile
componentObj={{
...component.defaultInputs,
type: component.type,
componentsCollection: customComponentsCollection.name
}}
key={component.type}>
componentObj={{
...component.defaultInputs,
type: component.type,
componentsCollection: customComponentsCollection.name
}}
key={component.type}>
<span className={css`padding: 1rem; pointer-events: none; height: 100px; overflow: hidden;`}>
{renderHandlebars(component)}
</span>
Expand Down Expand Up @@ -203,10 +222,10 @@ export const ElementsPane = ({ isActive }: any) => {
window.addEventListener('mouseup', stopResize);
}} />
<Button
kind='ghost'
className={accordionButtonStyle}
renderIcon={isLayoutWidgetOpen ? ChevronDown : ChevronUp}
onClick={() => setIsLayoutWidgetOpen(!isLayoutWidgetOpen)}>
kind='ghost'
className={accordionButtonStyle}
renderIcon={isLayoutWidgetOpen ? ChevronDown : ChevronUp}
onClick={() => setIsLayoutWidgetOpen(!isLayoutWidgetOpen)}>
Layout tree
</Button>
{
Expand Down
20 changes: 15 additions & 5 deletions app/src/routes/edit/share-options/exports/export-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { createAngularApp } from './frameworks/angular/latest/fragment';

import { ModalContext } from '../../../../context/modal-context';
import { saveBlob } from '../../../../utils/file-tools';
import { useRemoteCustomComponentsCollections } from '../../../../utils/misc-tools';
import { GlobalStateContext } from '../../../../context';
import { ExportImageComponent } from './export-image-component';
import { filenameToLanguage, getFragmentJsonExportString } from '@carbon-builder/sdk-react';
Expand Down Expand Up @@ -188,6 +189,8 @@ const findTreeItemByPath = (node: any, path: string, currentPath = ''): any => {
export const ExportModal = () => {
const { fragments, settings, setSettings, styleClasses, customComponentsCollections } = useContext(GlobalStateContext);
const { fragmentExportModal, hideFragmentExportModal } = useContext(ModalContext);
const [remoteCustomComponentsCollections] = useRemoteCustomComponentsCollections();
const [allCustomComponentsCollections, setAllCustomComponentsCollections] = useState([] as any[]);
const [selectedAngularFileItem, setSelectedAngularFileItem] = useState({
id: 'src/app/app.component.ts'
} as any);
Expand All @@ -202,6 +205,13 @@ export const ExportModal = () => {

const monaco = useMonaco();

useEffect(() => {
setAllCustomComponentsCollections([
...(remoteCustomComponentsCollections as any[] || []).flat(),
...customComponentsCollections
]);
}, [remoteCustomComponentsCollections, customComponentsCollections]);

const carbonVersions = [
{ id: 'v10' },
{ id: 'v11' }
Expand Down Expand Up @@ -229,20 +239,20 @@ export const ExportModal = () => {
if (fragmentExportModal?.fragment) {
switch (version) {
case 'v10':
setReactCode(createReactAppV10(fragmentExportModal.fragment, fragments, styleClasses, customComponentsCollections));
setAngularCode(createAngularAppV10(fragmentExportModal.fragment, fragments, styleClasses, customComponentsCollections));
setReactCode(createReactAppV10(fragmentExportModal.fragment, fragments, styleClasses, allCustomComponentsCollections));
setAngularCode(createAngularAppV10(fragmentExportModal.fragment, fragments, styleClasses, allCustomComponentsCollections));
break;
default:
setReactCode(createReactApp(fragmentExportModal.fragment, fragments, styleClasses, customComponentsCollections));
setAngularCode(createAngularApp(fragmentExportModal.fragment, fragments, styleClasses, customComponentsCollections));
setReactCode(createReactApp(fragmentExportModal.fragment, fragments, styleClasses, allCustomComponentsCollections));
setAngularCode(createAngularApp(fragmentExportModal.fragment, fragments, styleClasses, allCustomComponentsCollections));
break;
}
return;
}

setReactCode([]);
setAngularCode([]);
}, [customComponentsCollections, fragmentExportModal.fragment, fragmentExportModal.isVisible, fragments, styleClasses, version]);
}, [allCustomComponentsCollections, fragmentExportModal.fragment, fragmentExportModal.isVisible, fragments, styleClasses, version]);

useEffect(() => {
monaco?.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
Expand Down
29 changes: 29 additions & 0 deletions app/src/utils/misc-tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useContext, useEffect, useState } from 'react';
import { GithubContext, GlobalStateContext } from '../context';
import axios from 'axios';

export const useRemoteCustomComponentsCollections = () => {
const { customComponentsCollections } = useContext(GlobalStateContext);
const { getFeaturedCustomComponentsCollection } = useContext(GithubContext);

const [collections, setCollections] = useState([] as any[]);
const promises: any[] = [];
customComponentsCollections.forEach((collection: any) => {
if (collection.type === 'featured') {
promises.push(getFeaturedCustomComponentsCollection(collection.collection));
}

if (collection.type === 'url') {
promises.push(axios.get(collection.url, { responseType: 'text' }).then((value: any) => value.data));
}
});

useEffect(() => {
Promise.all(promises).then((data: any[]) => {
setCollections(data);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return [collections, setCollections];
};
Loading

0 comments on commit eed26a9

Please sign in to comment.