diff --git a/packages/library/src/_test/stubs.ts b/packages/library/src/_test/stubs.ts index cd25ff8c..7e5c8af2 100644 --- a/packages/library/src/_test/stubs.ts +++ b/packages/library/src/_test/stubs.ts @@ -5,6 +5,7 @@ import { ThemeFunctions } from 'hooks/useTheme' import { GraphData } from 'data' import { GraphColumnState } from 'modules/Graph/components/GraphColumn' import { GraphContextBag } from 'modules/Graph/context' +import { ThemeContextBag } from 'context/ThemeContext' export const commit = (commit?: Partial): Commit => ({ hash: 'aa2c148', @@ -44,10 +45,8 @@ export const gitContextBag = (bag?: Partial): GitContextBag => ({ setPreviewedCommit: vi.fn(), setSelectedCommit: vi.fn(), showBranchesTags: false, - theme: 'dark', showTable: true, selectedCommit: commit({ hash: 'selected' }), - colours: ['white'], headCommit: commit({ hash: 'HEAD' }), graphData: graphData(), showHeaders: true, @@ -64,6 +63,12 @@ export const gitContextBag = (bag?: Partial): GitContextBag => ({ ...bag }) +export const themeContextBag = (bag?: Partial): ThemeContextBag => ({ + theme: 'dark', + colours: ['white'], + ...bag +}) + export const graphContextBag = (bag?: Partial): GraphContextBag => ({ nodeTheme: 'default', showCommitNodeTooltips: false, diff --git a/packages/library/src/components/GitLogCore/GitLogCore.tsx b/packages/library/src/components/GitLogCore/GitLogCore.tsx index 17e72ba4..61936529 100644 --- a/packages/library/src/components/GitLogCore/GitLogCore.tsx +++ b/packages/library/src/components/GitLogCore/GitLogCore.tsx @@ -1,8 +1,6 @@ import { GitLogCoreProps } from './types' import { Children, isValidElement, PropsWithChildren, ReactElement, useCallback, useMemo, useState } from 'react' import { GitContext, GitContextBag } from 'context/GitContext' -import { neonAuroraDarkColours, neonAuroraLightColours, useTheme } from 'hooks/useTheme' -import { generateRainbowGradient } from 'hooks/useTheme/createRainbowTheme' import { computeNodePositions, computeRelationships, GraphData, temporalTopologicalSort } from 'data' import { Tags } from 'modules/Tags' import { Graph, GraphOrientation } from 'modules/Graph' @@ -10,6 +8,7 @@ import { Table } from 'modules/Table' import { Layout } from 'components/Layout' import { Commit } from 'types/Commit' import { DEFAULT_NODE_SIZE, NODE_BORDER_WIDTH } from 'constants/constants' +import { ThemeContextProvider } from 'context/ThemeContext' export const GitLogCore = ({ children, @@ -89,33 +88,6 @@ export const GitLogCore = ({ // TODO: Are we using graphWidth here or just ditching enableResize? const [, setGraphWidth] = useState(defaultGraphWidth ?? smallestAvailableGraphWidth) - const { shiftAlphaChannel } = useTheme() - - const themeColours = useMemo(() => { - switch (colours) { - case 'rainbow-light': { - return generateRainbowGradient(graphData.graphWidth + 1) - } - case 'rainbow-dark': { - return generateRainbowGradient(graphData.graphWidth + 1) - .map(colour => shiftAlphaChannel(colour, 0.6)) - } - case 'neon-aurora-dark': { - return neonAuroraDarkColours - } - case 'neon-aurora-light': { - return neonAuroraLightColours - } - default: { - if (theme === 'light') { - return colours - } - - return colours.map(colour => shiftAlphaChannel(colour, 0.6)) - } - } - }, [colours, graphData.graphWidth, shiftAlphaChannel, theme]) - const handleSelectCommit = useCallback((commit?: Commit) => { setSelectedCommit(commit) onSelectCommit?.(commit) @@ -183,11 +155,9 @@ export const GitLogCore = ({ }, [defaultGraphWidth, smallestAvailableGraphWidth]) const value = useMemo(() => ({ - colours: themeColours, showTable: Boolean(table), showBranchesTags: Boolean(tags), classes, - theme, selectedCommit, setSelectedCommit: handleSelectCommit, previewedCommit, @@ -211,9 +181,7 @@ export const GitLogCore = ({ setGraphOrientation, indexStatus }), [ - themeColours, classes, - theme, selectedCommit, previewedCommit, handleSelectCommit, @@ -239,7 +207,9 @@ export const GitLogCore = ({ return ( - + + + ) } diff --git a/packages/library/src/context/GitContext/GitContext.ts b/packages/library/src/context/GitContext/GitContext.ts index cb55ae1d..4c3163fe 100644 --- a/packages/library/src/context/GitContext/GitContext.ts +++ b/packages/library/src/context/GitContext/GitContext.ts @@ -1,6 +1,5 @@ import { createContext } from 'react' import { GitContextBag } from './types' -import { neonAuroraDarkColours } from 'hooks/useTheme' import { Commit } from 'types/Commit' import DataIntervalTree from 'node-interval-tree' import { DEFAULT_NODE_SIZE } from 'constants/constants' @@ -18,13 +17,11 @@ const defaultCommit: Commit = { } export const GitContext = createContext({ - colours: neonAuroraDarkColours, headCommit: defaultCommit, indexCommit: defaultCommit, currentBranch: 'master', showTable: true, showBranchesTags: true, - theme: 'light', selectedCommit: undefined, setSelectedCommit: (commit?: Commit) => { console.debug(`Tried to invoke setSelectedCommit(${JSON.stringify(commit)}) before the GitContext was initialised.`) diff --git a/packages/library/src/context/GitContext/types.ts b/packages/library/src/context/GitContext/types.ts index 4ecb1d29..190ceb0d 100644 --- a/packages/library/src/context/GitContext/types.ts +++ b/packages/library/src/context/GitContext/types.ts @@ -1,12 +1,9 @@ import { Commit } from 'types/Commit' -import { ThemeMode } from 'hooks/useTheme' import { GraphData } from 'data' import { GitLogIndexStatus, GitLogStylingProps } from '../../types' import { GraphOrientation } from 'modules/Graph' export interface GitContextBag { - colours: string[] - /** * The name of the branch that is * currently checked out. @@ -177,15 +174,6 @@ export interface GitContextBag { */ classes?: GitLogStylingProps - /** - * The variant of the default colour - * them to apply to the log. - * - * Does not take effect if a custom - * array of {@link colours} are passed. - */ - theme: ThemeMode - /** * The status of changed files in the * Git index. diff --git a/packages/library/src/context/ThemeContext/ThemeContext.ts b/packages/library/src/context/ThemeContext/ThemeContext.ts new file mode 100644 index 00000000..87f2f8a1 --- /dev/null +++ b/packages/library/src/context/ThemeContext/ThemeContext.ts @@ -0,0 +1,8 @@ +import { createContext } from 'react' +import { ThemeContextBag } from 'context/ThemeContext/types' +import { neonAuroraDarkColours } from 'hooks/useTheme' + +export const ThemeContext = createContext({ + colours: neonAuroraDarkColours, + theme: 'light' +}) \ No newline at end of file diff --git a/packages/library/src/context/ThemeContext/ThemeContextProvider.tsx b/packages/library/src/context/ThemeContext/ThemeContextProvider.tsx new file mode 100644 index 00000000..ce12dde3 --- /dev/null +++ b/packages/library/src/context/ThemeContext/ThemeContextProvider.tsx @@ -0,0 +1,46 @@ +import { ThemeContext } from 'context/ThemeContext/ThemeContext' +import { PropsWithChildren, useMemo } from 'react' +import { ThemeContextBag, ThemeContextProviderProps } from 'context/ThemeContext/types' +import { neonAuroraDarkColours, neonAuroraLightColours, useTheme } from 'hooks/useTheme' +import { generateRainbowGradient } from 'hooks/useTheme/createRainbowTheme' + +export const ThemeContextProvider = ({ children, theme, colours, graphWidth }: PropsWithChildren) => { + + const { shiftAlphaChannel } = useTheme() + + const themeColours = useMemo(() => { + switch (colours) { + case 'rainbow-light': { + return generateRainbowGradient(graphWidth + 1) + } + case 'rainbow-dark': { + return generateRainbowGradient(graphWidth + 1) + .map(colour => shiftAlphaChannel(colour, 0.6)) + } + case 'neon-aurora-dark': { + return neonAuroraDarkColours + } + case 'neon-aurora-light': { + return neonAuroraLightColours + } + default: { + if (theme === 'light') { + return colours + } + + return colours.map(colour => shiftAlphaChannel(colour, 0.6)) + } + } + }, [colours, graphWidth, shiftAlphaChannel, theme]) + + const value = useMemo(() => ({ + colours: themeColours, + theme + }), [theme, themeColours]) + + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/packages/library/src/context/ThemeContext/index.ts b/packages/library/src/context/ThemeContext/index.ts new file mode 100644 index 00000000..0debd474 --- /dev/null +++ b/packages/library/src/context/ThemeContext/index.ts @@ -0,0 +1,3 @@ +export * from './types' +export * from './useThemeContext' +export * from './ThemeContextProvider' \ No newline at end of file diff --git a/packages/library/src/context/ThemeContext/types.ts b/packages/library/src/context/ThemeContext/types.ts new file mode 100644 index 00000000..326fbcfc --- /dev/null +++ b/packages/library/src/context/ThemeContext/types.ts @@ -0,0 +1,25 @@ +import { ThemeColours, ThemeMode } from 'hooks/useTheme' + +export interface ThemeContextBag { + /** + * The variant of the default colour + * them to apply to the log. + * + * Does not take effect if a custom + * array of {@link colours} are passed. + */ + theme: ThemeMode + + /** + * An array of colours to be used in + * graph to denote the different branches + * in the git log entries. + */ + colours: string[] +} + +export interface ThemeContextProviderProps { + theme: ThemeMode + graphWidth: number + colours: ThemeColours | string[] +} \ No newline at end of file diff --git a/packages/library/src/context/ThemeContext/useThemeContext.ts b/packages/library/src/context/ThemeContext/useThemeContext.ts new file mode 100644 index 00000000..cf7a82b9 --- /dev/null +++ b/packages/library/src/context/ThemeContext/useThemeContext.ts @@ -0,0 +1,4 @@ +import { useContext } from 'react' +import { ThemeContext } from 'context/ThemeContext/ThemeContext' + +export const useThemeContext = () => useContext(ThemeContext) \ No newline at end of file diff --git a/packages/library/src/hooks/useTheme/useTheme.spec.ts b/packages/library/src/hooks/useTheme/useTheme.spec.ts index 027a29de..6a3f54ee 100644 --- a/packages/library/src/hooks/useTheme/useTheme.spec.ts +++ b/packages/library/src/hooks/useTheme/useTheme.spec.ts @@ -2,7 +2,8 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { renderHook } from '@testing-library/react' import { useTheme } from './useTheme' import * as gitContext from 'context/GitContext/useGitContext' -import { gitContextBag, graphData } from 'test/stubs' +import * as themeContext from 'context/ThemeContext/useThemeContext' +import { gitContextBag, graphData, themeContextBag } from 'test/stubs' import { Commit } from 'types/Commit' describe('useTheme', () => { @@ -22,7 +23,7 @@ describe('useTheme', () => { }) it('returns theme background color when opacity is 0 in dark mode', () => { - vi.spyOn(gitContext, 'useGitContext').mockReturnValue(gitContextBag({ + vi.spyOn(themeContext, 'useThemeContext').mockReturnValue(themeContextBag({ theme: 'dark' })) @@ -32,7 +33,7 @@ describe('useTheme', () => { }) it('correctly blends colors based on opacity in dark mode', () => { - vi.spyOn(gitContext, 'useGitContext').mockReturnValue(gitContextBag({ + vi.spyOn(themeContext, 'useThemeContext').mockReturnValue(themeContextBag({ theme: 'dark' })) @@ -42,7 +43,7 @@ describe('useTheme', () => { }) it('correctly blends colors based on opacity in light mode', () => { - vi.spyOn(gitContext, 'useGitContext').mockReturnValue(gitContextBag({ + vi.spyOn(themeContext, 'useThemeContext').mockReturnValue(themeContextBag({ theme: 'light' })) @@ -66,13 +67,17 @@ describe('useTheme', () => { describe('hoverColour', () => { it('returns correct hover colour for dark theme', () => { - vi.spyOn(gitContext, 'useGitContext').mockReturnValue(gitContextBag({ theme: 'dark' })) + vi.spyOn(themeContext, 'useThemeContext').mockReturnValue(themeContextBag({ + theme: 'dark' + })) const { result } = renderHook(useTheme) expect(result.current.hoverColour).toBe('rgba(70,70,70,0.8)') }) it('returns correct hover colour for light theme', () => { - vi.spyOn(gitContext, 'useGitContext').mockReturnValue(gitContextBag({ theme: 'light' })) + vi.spyOn(themeContext, 'useThemeContext').mockReturnValue(themeContextBag({ + theme: 'light' + })) const { result } = renderHook(useTheme) expect(result.current.hoverColour).toBe('rgba(231, 231, 231, 0.5)') }) @@ -80,13 +85,17 @@ describe('useTheme', () => { describe('textColour', () => { it('returns correct text colour for dark theme', () => { - vi.spyOn(gitContext, 'useGitContext').mockReturnValue(gitContextBag({ theme: 'dark' })) + vi.spyOn(themeContext, 'useThemeContext').mockReturnValue(themeContextBag({ + theme: 'dark' + })) const { result } = renderHook(useTheme) expect(result.current.textColour).toBe('rgb(255, 255, 255)') }) it('returns correct text colour for light theme', () => { - vi.spyOn(gitContext, 'useGitContext').mockReturnValue(gitContextBag({ theme: 'light' })) + vi.spyOn(themeContext, 'useThemeContext').mockReturnValue(themeContextBag({ + theme: 'light' + })) const { result } = renderHook(useTheme) expect(result.current.textColour).toBe('rgb(0, 0, 0)') }) @@ -101,13 +110,17 @@ describe('useTheme', () => { describe('getGraphColumnColour', () => { it('returns the correct colour when the index exists in colours array', () => { - vi.spyOn(gitContext, 'useGitContext').mockReturnValue(gitContextBag({ colours: ['red', 'green', 'blue'] })) + vi.spyOn(themeContext, 'useThemeContext').mockReturnValue(themeContextBag({ + colours: ['red', 'green', 'blue'] + })) const { result } = renderHook(useTheme) expect(result.current.getGraphColumnColour(1)).toBe('green') }) it('returns a repeated colour when index is beyond the array length', () => { - vi.spyOn(gitContext, 'useGitContext').mockReturnValue(gitContextBag({ colours: ['red', 'green', 'blue'] })) + vi.spyOn(themeContext, 'useThemeContext').mockReturnValue(themeContextBag({ + colours: ['red', 'green', 'blue'] + })) const { result } = renderHook(useTheme) expect(result.current.getGraphColumnColour(5)).toBe('blue') // 5 % 3 === 2, so should return 'blue' }) @@ -115,15 +128,20 @@ describe('useTheme', () => { describe('getCommitColour', () => { it('returns the first column colour for index commit', () => { - vi.spyOn(gitContext, 'useGitContext').mockReturnValue(gitContextBag({ colours: ['red', 'green', 'blue'] })) + vi.spyOn(themeContext, 'useThemeContext').mockReturnValue(themeContextBag({ + colours: ['red', 'green', 'blue'] + })) const { result } = renderHook(useTheme) expect(result.current.getCommitColour({ hash: 'index' } as Commit)).toBe('red') }) it('returns correct column colour based on commit position', () => { + vi.spyOn(themeContext, 'useThemeContext').mockReturnValue(themeContextBag({ + colours: ['red', 'green', 'blue'] + })) + vi.spyOn(gitContext, 'useGitContext').mockReturnValue( gitContextBag({ - colours: ['red', 'green', 'blue'], graphData: graphData({ positions: new Map([['abc123', [0, 1]]]) }) @@ -135,10 +153,13 @@ describe('useTheme', () => { }) it('logs a warning and returns default black for unmapped commit', () => { + vi.spyOn(themeContext, 'useThemeContext').mockReturnValue(themeContextBag({ + colours: ['red', 'green', 'blue'] + })) + const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) vi.spyOn(gitContext, 'useGitContext').mockReturnValue( gitContextBag({ - colours: ['red', 'green', 'blue'], graphData: graphData({ positions: new Map() }) @@ -155,10 +176,13 @@ describe('useTheme', () => { describe('getTooltipBackground', () => { it('returns correctly adjusted background colour for dark theme', () => { + vi.spyOn(themeContext, 'useThemeContext').mockReturnValue(themeContextBag({ + colours: ['rgb(100, 150, 200)'], + theme: 'dark' + })) + vi.spyOn(gitContext, 'useGitContext').mockReturnValue( gitContextBag({ - theme: 'dark', - colours: ['rgb(100, 150, 200)'], graphData: graphData({ positions: new Map([['abc123', [0, 0]]]) }) @@ -170,10 +194,13 @@ describe('useTheme', () => { }) it('returns correctly adjusted background colour for light theme', () => { + vi.spyOn(themeContext, 'useThemeContext').mockReturnValue(themeContextBag({ + theme: 'light', + colours: ['rgb(100, 150, 200)'] + })) + vi.spyOn(gitContext, 'useGitContext').mockReturnValue( gitContextBag({ - theme: 'light', - colours: ['rgb(100, 150, 200)'], graphData: graphData({ positions: new Map([['abc123', [0, 0]]]) }) diff --git a/packages/library/src/hooks/useTheme/useTheme.ts b/packages/library/src/hooks/useTheme/useTheme.ts index 8bb8f205..3c244736 100644 --- a/packages/library/src/hooks/useTheme/useTheme.ts +++ b/packages/library/src/hooks/useTheme/useTheme.ts @@ -2,9 +2,11 @@ import { useGitContext } from 'context/GitContext' import { ThemeFunctions } from './types' import { useCallback, useMemo } from 'react' import { Commit } from 'types/Commit' +import { useThemeContext } from 'context/ThemeContext' export const useTheme = (): ThemeFunctions => { - const { theme, colours, graphData } = useGitContext() + const { graphData } = useGitContext() + const { theme, colours } = useThemeContext() const hoverColour = useMemo(() => { if (theme === 'dark') { diff --git a/packages/library/src/modules/Tags/Tags.spec.tsx b/packages/library/src/modules/Tags/Tags.spec.tsx new file mode 100644 index 00000000..a8516f4f --- /dev/null +++ b/packages/library/src/modules/Tags/Tags.spec.tsx @@ -0,0 +1,52 @@ +import * as gitContext from 'context/GitContext' +import { commit, gitContextBag, graphData } from 'test/stubs' +import { Tags } from './Tags' +import { render } from '@testing-library/react' +import { CommitNodeLocation } from 'data' +import { tag } from 'test/elements/Tag' + +describe('Tags', () => { + it('should render a tag for the row that has the selected commit', () => { + const selectedCommit = commit({ + hash: 'selected' + }) + + vi.spyOn(gitContext, 'useGitContext').mockReturnValueOnce(gitContextBag({ + selectedCommit: selectedCommit, + graphData: graphData({ + commits: [selectedCommit], + positions: new Map([['selected', [0, 0]]]) + }), + paging: { + startIndex: 0, + endIndex: 1 + } + })) + + render() + + expect(tag.atRow({ row: 0 })).toBeInTheDocument() + }) + + it('should render a tag for the row that has the previewed commit', () => { + const previewedCommit = commit({ + hash: 'previewed' + }) + + vi.spyOn(gitContext, 'useGitContext').mockReturnValueOnce(gitContextBag({ + previewedCommit, + graphData: graphData({ + commits: [previewedCommit], + positions: new Map([['previewed', [0, 0]]]) + }), + paging: { + startIndex: 0, + endIndex: 1 + } + })) + + render() + + expect(tag.atRow({ row: 0 })).toBeInTheDocument() + }) +}) \ No newline at end of file diff --git a/packages/library/src/types.ts b/packages/library/src/types.ts index deb787ac..6652b3a1 100644 --- a/packages/library/src/types.ts +++ b/packages/library/src/types.ts @@ -186,7 +186,24 @@ export interface GitLogPaging { } export interface GitLogIndexStatus { + /** + * The number of modified files on + * the checked-out branch according + * to the Git index. + */ modified: number + + /** + * The number of added files on + * the checked-out branch according + * to the Git index. + */ added: number + + /** + * The number of deleted files on + * the checked-out branch according + * to the Git index. + */ deleted: number } \ No newline at end of file diff --git a/packages/library/vite.config.ts b/packages/library/vite.config.ts index 7175ddd1..2da43208 100644 --- a/packages/library/vite.config.ts +++ b/packages/library/vite.config.ts @@ -77,7 +77,8 @@ export default defineConfig({ exclude: [ 'node_modules/', 'dist', - 'src/_test' + 'src/_test', + 'src/types' ] } }