Skip to content
This repository was archived by the owner on Mar 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/chilled-houses-study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@commercetools-docs/gatsby-theme-learning': minor
'@commercetools-docs/gatsby-theme-docs': minor
'@commercetools-website/docs-smoke-test': minor
'@commercetools-website/documentation': minor
---

Added status indicator for course and course topics
59 changes: 56 additions & 3 deletions packages/gatsby-theme-docs/gatsby-node.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,40 @@ const debugMem = () => {
}
};

/**
* Given navigation and content nodes, checks if all the content nodes
* belonging to a single self-learning chapter (course) have the same courseId.
*/
const validateSelfLearningContentStructure = (
allNavigationNodes,
allContentNodes
) => {
// get only self learning content
const slugToCourseMap = new Map();
allContentNodes
.filter((contentNode) => contentNode.courseId)
.forEach((contentNode) =>
slugToCourseMap.set(contentNode.slug, contentNode.courseId)
);

allNavigationNodes.forEach((navigationNode) => {
let prevCourseId = undefined;
const chapterTitle = navigationNode.chapterTitle;
navigationNode.pages.forEach((page) => {
if (!slugToCourseMap.has(page.path)) {
return; // the path is not self-learning content
}
const courseId = slugToCourseMap.get(page.path);
if (prevCourseId !== undefined && prevCourseId !== courseId) {
const msg = `Mismatch self-learning courseId property (${courseId}) found in topic with slug ${page.path}. All topics within a single course should referece the same courseId, please check the frontmatter sections within the "${chapterTitle}" course`;
throw new Error(msg);
} else {
prevCourseId = courseId;
}
});
});
};

// Ensure that certain directories exist.
// https://www.gatsbyjs.org/tutorial/building-a-theme/#create-a-data-directory-using-the-onprebootstrap-lifecycle
export const onPreBootstrap = async (gatsbyApi, themeOptions) => {
Expand Down Expand Up @@ -218,6 +252,7 @@ export const createSchemaCustomization = ({ actions, schema }) => {
}),
},
courseId: { type: 'Int' },
topicName: { type: 'String' },
},
interfaces: ['Node'],
}),
Expand Down Expand Up @@ -352,6 +387,9 @@ export const onCreateNode = async (
courseId: node.frontmatter.courseId
? Number(node.frontmatter.courseId)
: null,
topicName: node.frontmatter.topicName
? String(node.frontmatter.topicName)
: null,
};

actions.createNode({
Expand Down Expand Up @@ -411,6 +449,7 @@ async function createContentPages(
nodes {
slug
courseId
topicName
}
}
allReleaseNotePage(sort: { date: DESC }) {
Expand Down Expand Up @@ -441,13 +480,18 @@ async function createContentPages(
if (navigationYamlResult.errors) {
reporter.panicOnBuild('🚨 ERROR: Loading "allNavigationYaml" query');
}
// validate self-learning content structure
validateSelfLearningContentStructure(
navigationYamlResult.data.allNavigationYaml.nodes,
result.data.allContentPage.nodes
);
const pages = result.data.allContentPage.nodes;
const navigationPages =
navigationYamlResult.data.allNavigationYaml.nodes.reduce(
(pageLinks, node) => [...pageLinks, ...(node.pages || [])],
[]
);
pages.forEach(({ slug, courseId }) => {
pages.forEach(({ slug, courseId, topicName }) => {
const matchingNavigationPage = navigationPages.find(
(page) => trimTrailingSlash(page.path) === trimTrailingSlash(slug)
);
Expand Down Expand Up @@ -489,9 +533,18 @@ async function createContentPages(
let contentPageData = {
...pageData,
component: require.resolve('./src/templates/page-content.js'),
}
};
if (courseId) {
contentPageData = {...contentPageData, context: {...contentPageData.context, courseId}}
contentPageData = {
...contentPageData,
context: { ...contentPageData.context, courseId },
};
}
if (topicName) {
contentPageData = {
...contentPageData,
context: { ...contentPageData.context, topicName },
};
}
actions.createPage(contentPageData);

Expand Down
14 changes: 10 additions & 4 deletions packages/gatsby-theme-docs/src/hooks/use-course-pages.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const useCoursePages = () => {
nodes {
slug
courseId
topicName
}
}
}
Expand All @@ -25,17 +26,22 @@ var isIndexed = false;
const courseMapToPages = (coursePagesData) => {
if (!isIndexed) {
coursePagesData.forEach((element) => {
coursePageMap.set(element.slug, { courseId: element.courseId });
const { courseId, topicName } = element;
coursePageMap.set(element.slug, { courseId, topicName });
});
}
isIndexed = true;
};

/**
* Given a page slug, it returns the courseId where the page belongs
* Given an array of page slugs, it returns an array with course info
* if exists, otherwise undefined
*/
export const useCourseInfoByPageSlug = (pageSlug) => {
export const useCourseInfoByPageSlugs = (pageSlugs) => {
courseMapToPages(useCoursePages());
return coursePageMap.get(pageSlug);
const courseInfo = pageSlugs.reduce(
(prev, curr) => ({ ...prev, [curr]: coursePageMap.get(curr) }),
{}
);
return courseInfo;
};
4 changes: 0 additions & 4 deletions packages/gatsby-theme-docs/src/layouts/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import LayoutPageNavigation from './internals/layout-page-navigation';
import LayoutPageContent from './internals/layout-page-content';
import PageContentInset from './internals/page-content-inset';
import PageReadTime from './internals/page-read-time-estimation';
import { PageCourseStatus } from '@commercetools-docs/gatsby-theme-learning';

const LayoutContent = (props) => {
const { ref, inView, entry } = useInView();
Expand Down Expand Up @@ -82,9 +81,6 @@ const LayoutContent = (props) => {
{props.pageData.showTimeToRead && (
<PageReadTime data={props.pageData} />
)}
{props.pageContext.courseId && (
<PageCourseStatus courseId={props.pageContext.courseId} />
)}
</LayoutPageHeader>
<LayoutPageHeaderSide>
<SpacingsStack scale="m">
Expand Down
101 changes: 80 additions & 21 deletions packages/gatsby-theme-docs/src/layouts/internals/sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ import SiteIcon from '../../overrides/site-icon';
import useScrollPosition from '../../hooks/use-scroll-position';
import { BetaFlag } from '../../components';
import LayoutHeaderLogo from './layout-header-logo';
import { useCourseInfoByPageSlug } from '../../hooks/use-course-pages';
import { PageCourseStatus } from '@commercetools-docs/gatsby-theme-learning';
import { useCourseInfoByPageSlugs } from '../../hooks/use-course-pages';
import {
SidebarCourseStatus,
SidebarTopicStatus,
} from '@commercetools-docs/gatsby-theme-learning';

const ReleaseNotesIcon = createStyledIcon(Icons.ReleaseNotesSvgIcon);

Expand Down Expand Up @@ -91,6 +94,19 @@ const LinkItem = styled.div`
display: flex;
flex-direction: row;
align-items: flex-end;
vertical-align: middle;
`;
const LinkItemWithIcon = styled.div`
padding: 0 0 0 ${designSystem.dimensions.spacings.m};
display: flex;
flex-direction: row;
vertical-align: middle;
svg {
margin-right: 2px;
}
div {
line-height: ${designSystem.typography.lineHeights.cardSmallTitle};
}
`;
const linkStyles = css`
border-left: ${designSystem.dimensions.spacings.xs} solid
Expand Down Expand Up @@ -121,6 +137,24 @@ const activeLinkStyles = css`
color: ${designSystem.colors.light.linkNavigation} !important;
`;

const StatusIconWrapper = styled.span`
display: flex;
vertical-align: middle;
padding-left: 10px; // change this setting to remove the indentation
svg {
margin-right: 5px;
}
`;

const LinkSubtitleWithIcon = (props) => (
<LinkSubtitle>
<StatusIconWrapper>{props.children}</StatusIconWrapper>
</LinkSubtitle>
);
LinkSubtitleWithIcon.propTypes = {
children: PropTypes.any,
};

const SidebarLink = (props) => {
const { locationPath, customStyles, customActiveStyles, ...forwardProps } =
props;
Expand Down Expand Up @@ -234,7 +268,10 @@ SidebarLinkWrapper.propTypes = {
};

const SidebarChapter = (props) => {
const courseInfo = useCourseInfoByPageSlug(props.chapter.pages[0].path);
const courseInfo = useCourseInfoByPageSlugs(
props.chapter.pages.map((page) => page.path)
);
const courseId = Object.values(courseInfo)[0]?.courseId;
const elemId = `sidebar-chapter-${props.index}`;
const getChapterDOMElement = React.useCallback(
() => document.getElementById(elemId),
Expand All @@ -244,26 +281,48 @@ const SidebarChapter = (props) => {
return (
<div role="sidebar-chapter" id={elemId}>
<SpacingsStack scale="s">
<LinkItem>
<LinkTitle>{props.chapter.chapterTitle}</LinkTitle>
{courseInfo?.courseId && (
<PageCourseStatus courseId={courseInfo.courseId} />
)}
</LinkItem>
{courseId ? (
<LinkItemWithIcon>
<SidebarCourseStatus courseId={courseId} />
<LinkTitle>{props.chapter.chapterTitle}</LinkTitle>
</LinkItemWithIcon>
) : (
<LinkItem>
<LinkTitle>{props.chapter.chapterTitle}</LinkTitle>
</LinkItem>
)}

<SpacingsStack scale="s">
{props.chapter.pages &&
props.chapter.pages.map((pageLink, pageIndex) => (
<SidebarLinkWrapper
key={`${props.index}-${pageIndex}-${pageLink.path}`}
to={pageLink.path}
onClick={props.onLinkClick}
location={props.location}
nextScrollPosition={props.nextScrollPosition}
getChapterDOMElement={getChapterDOMElement}
>
<LinkSubtitle>{pageLink.title}</LinkSubtitle>
</SidebarLinkWrapper>
))}
props.chapter.pages.map((pageLink, pageIndex) => {
const currTopicName = courseInfo[pageLink.path]?.topicName;
const TopicIcon =
courseId && currTopicName ? (
<SidebarTopicStatus
courseId={courseId}
pageTitle={currTopicName}
/>
) : null;
return (
<SidebarLinkWrapper
key={`${props.index}-${pageIndex}-${pageLink.path}`}
to={pageLink.path}
onClick={props.onLinkClick}
location={props.location}
nextScrollPosition={props.nextScrollPosition}
getChapterDOMElement={getChapterDOMElement}
>
{TopicIcon ? (
<LinkSubtitleWithIcon>
{TopicIcon}
{pageLink.title}
</LinkSubtitleWithIcon>
) : (
<LinkSubtitle>{pageLink.title}</LinkSubtitle>
)}
</SidebarLinkWrapper>
);
})}
</SpacingsStack>
</SpacingsStack>
</div>
Expand Down
4 changes: 2 additions & 2 deletions packages/gatsby-theme-learning/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ Works against the learning-api.

- `auth0Domain`: the auth0 application domain url (it is defined in the auth0 management app)
- `learnApiBaseUrl`: the learn API base url. It can be omitted if the host running the site matches the api host.
- `features`: a list of feature flags (boolean values) to enable/disable specific functionalities
- `courseStatusIndicator`: feature flag to toggle the course status indicator.
- `features`: an array of strings representing feature flags used to enable/disable specific functionalities. Expected values:
- `status-indicator`: feature flag to toggle the course and topics status indicator.

In order to enable the plugin, at least the following configuration should be added to the `gatsby-config.js` plugin section:

Expand Down
5 changes: 1 addition & 4 deletions packages/gatsby-theme-learning/gatsby-browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ export const wrapRootElement = ({ element }, pluginOptions) => {
value={{
learnApiBaseUrl: pluginOptions.learnApiBaseUrl,
auth0Domain: pluginOptions.auth0Domain,
features: {
courseStatusIndicator:
pluginOptions?.features?.courseStatusIndicator || false,
},
features: pluginOptions?.features || [],
}}
>
{element}
Expand Down
4 changes: 1 addition & 3 deletions packages/gatsby-theme-learning/gatsby-node.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ export const pluginOptionsSchema = ({ Joi }) => {
.allow('')
.default('')
.description(`Learn API base url`),
features: Joi.object({
courseStatusIndicator: Joi.boolean().default(false),
})
features: Joi.array().items(Joi.string()),
});
};
15 changes: 15 additions & 0 deletions packages/gatsby-theme-learning/gatsby-ssr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import ConfigContext from './src/components/config-context';

export const wrapRootElement = ({ element }, pluginOptions) => {
return (
<ConfigContext.Provider
value={{
learnApiBaseUrl: pluginOptions.learnApiBaseUrl,
auth0Domain: pluginOptions.auth0Domain,
features: pluginOptions?.features || [],
}}
>
{element}
</ConfigContext.Provider>
);
};
1 change: 1 addition & 0 deletions packages/gatsby-theme-learning/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export {
useFetchCourses,
getCourseStatusByCourseId,
} from './src/hooks/use-course-status';
export { useFetchCourseDetails } from './src/hooks/use-course-details';
17 changes: 11 additions & 6 deletions packages/gatsby-theme-learning/src/components/config-context.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import { createContext } from 'react';

export enum EFeatureFlag {
CourseStatus = 'status-indicator',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if a data structure that has defaults might be better suited. I can't really wrap my head around why exactly to be honest, it's just a feeling.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure what you mean here. but the feature flag feature is working and I would descope this discussion from this PR. We can have a discussion about it and then create a separate ticket

}

export type Config = {
learnApiBaseUrl: string;
auth0Domain: string;
features: {
courseStatusIndicator: boolean;
};
features: Array<EFeatureFlag>;
};

const ConfigContext = createContext<Config>({
learnApiBaseUrl: '',
auth0Domain: '',
features: {
courseStatusIndicator: false,
},
features: [],
});

export const isFeatureEnabled = (
feature: EFeatureFlag,
features: EFeatureFlag[]
) => features.includes(feature);

export default ConfigContext;
Loading