-
Notifications
You must be signed in to change notification settings - Fork 5
#397: Highlight States #412
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d968793
3709c14
cc905ad
1b0a3d5
bd02d6b
f729b69
2d61856
f840404
1479f36
36476f5
2abb827
288d961
f66e39f
d69ef67
1448234
e2ec2ed
6eba979
7700a63
e083d73
04e0314
ffb4767
3a9efd9
cb0e9d4
82b93f8
0554d37
a75c21e
5c814cf
e53cac7
2941455
897ca27
f9c88f3
f3d58d1
3ea658c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,13 +22,21 @@ | |
| import { css } from '@emotion/react'; | ||
| import type { Theme } from '../'; | ||
|
|
||
| export const fieldRowStyles = css` | ||
| export const fieldRowStyles = (theme: Theme, isForeignKey: boolean, isEven: boolean, isHighlighted: boolean = false) => css` | ||
| padding: 12px 12px; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: space-between; | ||
| transition: background-color 0.2s; | ||
| position: relative; | ||
| background-color: ${isHighlighted ? theme.colors.secondary_1 : isEven ? '#e5edf3' : 'transparent'}; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider using css selectors for alternating colours instead of JS props: |
||
| border-block: 1.5px solid ${isHighlighted ? theme.colors.secondary_dark : isEven ? '#d4dce2' : 'transparent'}; | ||
| ${isForeignKey ? 'cursor: pointer;' : ''} | ||
|
|
||
| &:hover { | ||
| background-color: ${isForeignKey ? theme.colors.secondary_1 : theme.colors.grey_3}; | ||
| border-block: 1.5px solid ${isForeignKey ? theme.colors.secondary_dark : theme.colors.grey_4}; | ||
| } | ||
| `; | ||
|
|
||
| export const fieldContentStyles = css` | ||
|
|
@@ -61,16 +69,18 @@ export const dataTypeBadgeStyles = (theme: Theme) => css` | |
| } | ||
| `; | ||
|
|
||
| export const nodeContainerStyles = css` | ||
| export const nodeContainerStyles = (isInactive: boolean = false) => css` | ||
| background: white; | ||
| border: 1px solid black; | ||
| border-radius: 8px; | ||
| box-shadow: | ||
| 0 10px 20px -3px rgba(0, 0, 0, 0.30), | ||
| 0 10px 20px -3px rgba(0, 0, 0, 0.3), | ||
| 0 4px 10px -2px rgba(0, 0, 0, 0.35); | ||
| min-width: 280px; | ||
| max-width: 350px; | ||
| overflow: hidden; | ||
| transition: opacity 0.2s; | ||
| ${isInactive ? 'opacity: 0.4;' : ''} | ||
| `; | ||
|
|
||
| export const nodeHeaderStyles = (theme: Theme) => css` | ||
|
|
@@ -102,10 +112,6 @@ export const nodeSubtitleTextStyle = css` | |
| export const fieldsListStyles = css` | ||
| background: #f8fafc; | ||
| overflow-y: auto; | ||
| & > div:nth-child(even) { | ||
| background-color: #e5edf3; | ||
| border-block: 1.5px solid #d4dce2; | ||
| } | ||
| `; | ||
|
|
||
| export const fieldNameContainerStyles = css` | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| /* | ||
| * | ||
| * Copyright (c) 2026 The Ontario Institute for Cancer Research. All rights reserved | ||
| * | ||
| * This program and the accompanying materials are made available under the terms of | ||
| * the GNU Affero General Public License v3.0. You should have received a copy of the | ||
| * GNU Affero General Public License along with this program. | ||
| * If not, see <http://www.gnu.org/licenses/>. | ||
| * | ||
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY | ||
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES | ||
| * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT | ||
| * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, | ||
| * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED | ||
| * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; | ||
| * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER | ||
| * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN | ||
| * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
| * | ||
| */ | ||
|
|
||
| import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from 'react'; | ||
| import { traceChain, type RelationshipMap } from './diagramUtils'; | ||
|
|
||
| type ActiveRelationshipState = { | ||
| edgeIds: Set<string>; | ||
| fieldKeys: Set<string>; | ||
| schemaChain: string[]; | ||
| } | null; | ||
|
Comment on lines
+25
to
+29
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Recommend defining the type without the union to null. That way we can use this type to represent known data, and we can always present union with null if the value could be missing. |
||
|
|
||
| type ActiveRelationshipContextValue = { | ||
| activeEdgeIds: Set<string> | null; | ||
| activeFieldKeys: Set<string> | null; | ||
| activeSchemaNames: Set<string> | null; | ||
| activeSchemaChain: string[] | null; | ||
|
Comment on lines
+32
to
+35
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why union with null instead of just using optional fields? TS (and the underlying JS) is built to use undefined for missing values with syntax like: One reason to define it as you have done is if you want to ensure that any variable |
||
| activateRelationship: (fkIndex: number, mappingIndex?: number) => void; | ||
| deactivateRelationship: () => void; | ||
| relationshipMap: RelationshipMap; | ||
| isFieldInActiveRelationship: (schemaName: string, fieldName: string) => boolean; | ||
| }; | ||
|
|
||
| const ActiveRelationshipContext = createContext<ActiveRelationshipContextValue | null>(null); | ||
|
|
||
| type ActiveRelationshipProviderProps = { | ||
| relationshipMap: RelationshipMap; | ||
| children: ReactNode; | ||
| }; | ||
|
|
||
| /** | ||
| * Provides active relationship state and actions to the ERD component tree. | ||
| * Wraps children with context that tracks which FK chain is currently highlighted, | ||
| * and exposes methods to activate/deactivate highlighting via traceChain. | ||
| * | ||
| * @param {RelationshipMap} relationshipMap — The FK adjacency graph used for chain tracing | ||
| * @param {ReactNode} children — Child components that can consume the active relationship context | ||
| */ | ||
| export function ActiveRelationshipProvider({ relationshipMap, children }: ActiveRelationshipProviderProps) { | ||
| const [activeState, setActiveState] = useState<ActiveRelationshipState>(null); | ||
|
|
||
| const activateRelationship = useCallback( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i appreciate the usage of |
||
| (fkIndex: number, mappingIndex?: number) => { | ||
| const result = traceChain(fkIndex, relationshipMap, mappingIndex); | ||
| setActiveState(result); | ||
| }, | ||
| [relationshipMap], | ||
| ); | ||
|
|
||
| const deactivateRelationship = useCallback(() => { | ||
| setActiveState(null); | ||
| }, []); | ||
|
|
||
| const isFieldInActiveRelationship = useCallback( | ||
| (schemaName: string, fieldName: string): boolean => { | ||
| if (!activeState) return false; | ||
| return activeState.fieldKeys.has(`${schemaName}::${fieldName}`); | ||
| }, | ||
| [activeState], | ||
| ); | ||
|
|
||
| const activeSchemaNames = useMemo(() => { | ||
| if (!activeState) return null; | ||
| const names = new Set<string>(); | ||
| for (const key of activeState.fieldKeys) { | ||
| const schemaName = key.split('::')[0]; | ||
| if (schemaName) names.add(schemaName); | ||
| } | ||
| return names; | ||
| }, [activeState]); | ||
|
|
||
| return ( | ||
| <ActiveRelationshipContext.Provider | ||
| value={{ | ||
| activeEdgeIds: activeState?.edgeIds ?? null, | ||
| activeFieldKeys: activeState?.fieldKeys ?? null, | ||
|
Comment on lines
+93
to
+94
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Defining the type of activeEdgeIds as an optional field would simplify this assignment to not require the |
||
| activeSchemaNames, | ||
| activeSchemaChain: activeState?.schemaChain ?? null, | ||
| activateRelationship, | ||
| deactivateRelationship, | ||
| relationshipMap, | ||
| isFieldInActiveRelationship, | ||
| }} | ||
| > | ||
| {children} | ||
| </ActiveRelationshipContext.Provider> | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Consumes the active relationship context. Must be called from a component | ||
| * that is a descendant of ActiveRelationshipProvider. | ||
| * | ||
| * @returns {ActiveRelationshipContextValue} The current active relationship state and actions | ||
| */ | ||
| export function useActiveRelationship(): ActiveRelationshipContextValue { | ||
| const context = useContext(ActiveRelationshipContext); | ||
| if (!context) { | ||
| throw new Error('useActiveRelationship must be used within an ActiveRelationshipProvider'); | ||
| } | ||
| return context; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.