diff --git a/scopes/lanes/lanes/lanes.graphql.ts b/scopes/lanes/lanes/lanes.graphql.ts index 6c4f2af78ed2..650e25bc900b 100644 --- a/scopes/lanes/lanes/lanes.graphql.ts +++ b/scopes/lanes/lanes/lanes.graphql.ts @@ -13,6 +13,12 @@ export function lanesSchema(lanesMainRuntime: LanesMain): Schema { diffOutput: String } + type SnapDistance { + onSource: [String!]! + onTarget: [String!]! + common: String + } + type FieldsDiff { fieldName: String! diffOutput: String @@ -60,6 +66,8 @@ export function lanesSchema(lanesMainRuntime: LanesMain): Schema { """ changes: [String!] upToDate: Boolean + snapsDistance: SnapDistance + unrelated: Boolean } type LaneDiffStatus { diff --git a/scopes/lanes/lanes/lanes.main.runtime.ts b/scopes/lanes/lanes/lanes.main.runtime.ts index e52b2b1918c5..8218c39f6d19 100644 --- a/scopes/lanes/lanes/lanes.main.runtime.ts +++ b/scopes/lanes/lanes/lanes.main.runtime.ts @@ -29,6 +29,7 @@ import ComponentWriterAspect, { ComponentWriterMain } from '@teambit/component-w import { SnapsDistance } from '@teambit/legacy/dist/scope/component-ops/snaps-distance'; import { MergingMain, MergingAspect } from '@teambit/merging'; import { ChangeType } from '@teambit/lanes.entities.lane-diff'; +import { NoCommonSnap } from '@teambit/legacy/dist/scope/exceptions/no-common-snap'; import { LanesAspect } from './lanes.aspect'; import { LaneCmd, @@ -52,6 +53,12 @@ import { LanesDeleteRoute } from './lanes.delete.route'; export { Lane }; +export type SnapsDistanceObj = { + onSource: string[]; + onTarget: string[]; + common?: string; +}; + export type LaneResults = { lanes: LaneData[]; currentLane?: string | null; @@ -82,11 +89,12 @@ export type LaneComponentDiffStatus = { changeType?: ChangeType; changes?: ChangeType[]; upToDate?: boolean; + snapsDistance?: SnapsDistanceObj; + unrelated?: boolean; }; export type LaneDiffStatusOptions = { skipChanges?: boolean; - skipUpToDate?: boolean; }; export type LaneDiffStatus = { @@ -416,7 +424,12 @@ export class LanesMain { * @param targetHead head on the target lane. leave empty if the target is main * @returns */ - async getSnapsDistance(componentId: ComponentID, sourceHead?: string, targetHead?: string): Promise { + async getSnapsDistance( + componentId: ComponentID, + sourceHead?: string, + targetHead?: string, + throws?: boolean + ): Promise { if (!sourceHead && !targetHead) throw new Error(`getDivergeData got sourceHead and targetHead empty. at least one of them should be populated`); const modelComponent = await this.scope.legacyScope.getModelComponent(componentId._legacy); @@ -425,6 +438,7 @@ export class LanesMain { repo: this.scope.legacyScope.objects, sourceHead: sourceHead ? Ref.from(sourceHead) : modelComponent.head || null, targetHead: targetHead ? Ref.from(targetHead) : modelComponent.head || null, + throws, }); } @@ -609,21 +623,23 @@ export class LanesMain { targetLaneId?: LaneId, options?: LaneDiffStatusOptions ): Promise { - const sourceLane = await this.loadLane(sourceLaneId); - if (!sourceLane) throw new Error(`unable to find ${sourceLaneId.toString()} in the scope`); + const sourceLaneComponents = sourceLaneId.isDefault() + ? (await this.getLaneDataOfDefaultLane())?.components.map((main) => ({ id: main.id, head: Ref.from(main.head) })) + : (await this.loadLane(sourceLaneId))?.components; + const targetLane = targetLaneId ? await this.loadLane(targetLaneId) : undefined; const targetLaneIds = targetLane?.toBitIds(); const host = this.componentAspect.getHost(); const diffProps = compact( await Promise.all( - sourceLane.components.map(async (comp) => { - const componentId = await host.resolveComponentId(comp.id); - const sourceVersionObj = (await this.scope.legacyScope.objects.load(comp.head)) as Version; - if (sourceVersionObj.isRemoved()) { + (sourceLaneComponents || []).map(async ({ id, head }) => { + const componentId = await host.resolveComponentId(id); + const sourceVersionObj = (await this.scope.legacyScope.objects.load(head)) as Version; + if (sourceVersionObj?.isRemoved()) { return null; } const headOnTargetLane = targetLaneIds - ? targetLaneIds.searchWithoutVersion(comp.id)?.version + ? targetLaneIds.searchWithoutVersion(id)?.version : await this.getHeadOnMain(componentId); if (headOnTargetLane) { @@ -633,7 +649,7 @@ export class LanesMain { } } - const sourceHead = comp.head.toString(); + const sourceHead = head.toString(); const targetHead = headOnTargetLane; return { componentId, sourceHead, targetHead }; @@ -657,16 +673,29 @@ export class LanesMain { sourceHead: string, targetHead?: string, options?: LaneDiffStatusOptions - ) { - const snapsDistance = !options?.skipUpToDate - ? await this.getSnapsDistance(componentId, sourceHead, targetHead) - : undefined; + ): Promise { + const snapsDistance = await this.getSnapsDistance(componentId, sourceHead, targetHead, false); + + if (snapsDistance?.err) { + const noCommonSnap = snapsDistance.err instanceof NoCommonSnap; + + return { + componentId, + sourceHead, + targetHead, + upToDate: snapsDistance?.isUpToDate(), + unrelated: noCommonSnap || undefined, + changes: [], + }; + } + + const commonSnap = snapsDistance?.commonSnapBeforeDiverge; const getChanges = async (): Promise => { - if (!targetHead) return [ChangeType.NEW]; + if (!commonSnap) return [ChangeType.NEW]; const compare = await this.componentCompare.compare( - componentId.changeVersion(targetHead).toString(), + componentId.changeVersion(commonSnap.hash).toString(), componentId.changeVersion(sourceHead).toString() ); @@ -695,7 +724,19 @@ export class LanesMain { const changes = !options?.skipChanges ? await getChanges() : undefined; const changeType = changes ? changes[0] : undefined; - return { componentId, changeType, changes, sourceHead, targetHead, upToDate: snapsDistance?.isUpToDate() }; + return { + componentId, + changeType, + changes, + sourceHead, + targetHead: commonSnap?.hash, + upToDate: snapsDistance?.isUpToDate(), + snapsDistance: { + onSource: snapsDistance?.snapsOnSourceOnly.map((s) => s.hash) ?? [], + onTarget: snapsDistance?.snapsOnTargetOnly.map((s) => s.hash) ?? [], + common: snapsDistance?.commonSnapBeforeDiverge?.hash, + }, + }; } async addLaneReadme(readmeComponentIdStr: string, laneName?: string): Promise<{ result: boolean; message?: string }> { diff --git a/scopes/lanes/lanes/lanes.ui.runtime.tsx b/scopes/lanes/lanes/lanes.ui.runtime.tsx index eab3ba3b8652..97088ef73995 100644 --- a/scopes/lanes/lanes/lanes.ui.runtime.tsx +++ b/scopes/lanes/lanes/lanes.ui.runtime.tsx @@ -153,7 +153,7 @@ export class LanesUI { } getLanesComparePage() { - return ; + return ; } getMenuRoutes() { @@ -209,10 +209,13 @@ export class LanesUI { { props: { href: '~compare', - children: 'Lane Compare', + children: 'Compare', }, order: 2, - hide: () => true, + hide: () => { + const { lanesModel } = useLanes(); + return !lanesModel?.viewedLane || lanesModel?.lanes.length < 2; + }, }, ]); } diff --git a/scopes/lanes/ui/compare/lane-compare-hooks/use-lane-diff-status/use-lane-diff-status.ts b/scopes/lanes/ui/compare/lane-compare-hooks/use-lane-diff-status/use-lane-diff-status.ts index 4f343bf69475..1139b4c70f85 100644 --- a/scopes/lanes/ui/compare/lane-compare-hooks/use-lane-diff-status/use-lane-diff-status.ts +++ b/scopes/lanes/ui/compare/lane-compare-hooks/use-lane-diff-status/use-lane-diff-status.ts @@ -46,6 +46,7 @@ export const QUERY_LANE_DIFF_STATUS = gql` targetHead changes upToDate + unrelated } } } @@ -80,6 +81,7 @@ export const useLaneDiffStatus: UseLaneDiffStatus = ({ baseId, compareId, option targetLane: LaneId.from(data.lanes.diffStatus.target.name, data.lanes.diffStatus.target.scope).toString(), diff: data.lanes.diffStatus.componentsStatus.map((c) => ({ ...c, + changes: c.changes || [], componentId: ComponentID.fromObject(c.componentId).toString(), })), }; diff --git a/scopes/lanes/ui/compare/lane-compare-page/lane-compare-page.module.scss b/scopes/lanes/ui/compare/lane-compare-page/lane-compare-page.module.scss index 6640dbb090fb..e60f0b7030f1 100644 --- a/scopes/lanes/ui/compare/lane-compare-page/lane-compare-page.module.scss +++ b/scopes/lanes/ui/compare/lane-compare-page/lane-compare-page.module.scss @@ -3,9 +3,49 @@ flex-direction: column; height: 100%; width: 100%; - // overflow: scroll; } .top { display: flex; - padding: 32px; + padding: 24px; + padding-bottom: 8px; + align-items: center; + width: 60%; + + > div { + display: flex; + } +} + +.bottom { + display: flex; + overflow: scroll; +} + +.compareLane { + padding: 8px; + background: var(--surface-neutral-focus-color); + border-radius: 6px; + margin: 0px 4px; + width: fit-content; + // same height as the lane selector + height: 34px; + box-sizing: border-box; + font-size: var(--bit-p-xs, 14px); + align-items: center; +} + +.baseSelectorContainer { + padding: 0px 4px; + width: 100%; + min-width: 100px; + max-width: 200px; + > div:first-of-type { + width: 100%; + } +} + +.laneIcon { + padding-right: 4px; + height: 16px; + font-size: var(--bit-p-sm, 16px); } diff --git a/scopes/lanes/ui/compare/lane-compare-page/lane-compare-page.tsx b/scopes/lanes/ui/compare/lane-compare-page/lane-compare-page.tsx index 8e11e9d47713..251d2439099f 100644 --- a/scopes/lanes/ui/compare/lane-compare-page/lane-compare-page.tsx +++ b/scopes/lanes/ui/compare/lane-compare-page/lane-compare-page.tsx @@ -1,26 +1,71 @@ -import React, { HTMLAttributes } from 'react'; +import React, { HTMLAttributes, useState, useEffect, useMemo } from 'react'; import { LaneCompareProps } from '@teambit/lanes'; import { useLanes } from '@teambit/lanes.hooks.use-lanes'; +import { LaneSelector } from '@teambit/lanes.ui.inputs.lane-selector'; +import { LaneModel } from '@teambit/lanes.ui.models.lanes-model'; +import { LaneId } from '@teambit/lane-id'; +import { LaneIcon } from '@teambit/lanes.ui.icons.lane-icon'; import styles from './lane-compare-page.module.scss'; export type LaneComparePageProps = { getLaneCompare: (props: LaneCompareProps) => React.ReactNode; + groupByScope?: boolean; } & HTMLAttributes; -export function LaneComparePage({ getLaneCompare, ...rest }: LaneComparePageProps) { +export function LaneComparePage({ getLaneCompare, groupByScope, ...rest }: LaneComparePageProps) { const { lanesModel } = useLanes(); + const [base, setBase] = useState(); + const defaultLane = lanesModel?.getDefaultLane(); + const compare = lanesModel?.viewedLane; + const nonMainLanes = lanesModel?.getNonMainLanes(); + useEffect(() => { + if (!base && !compare?.id.isDefault() && defaultLane) { + setBase(defaultLane); + } + if (!base && compare?.id.isDefault() && (nonMainLanes?.length ?? 0) > 0) { + setBase(nonMainLanes?.[0]); + } + }, [defaultLane, compare?.id.toString(), nonMainLanes?.length]); - if (!lanesModel) return null; + const LaneCompareComponent = useMemo(() => { + return getLaneCompare({ base, compare }); + }, [base?.id.toString(), compare?.id.toString()]); - const compare = lanesModel.getDefaultLane(); - const base = lanesModel.getNonMainLanes()[0]; + const lanes: Array = useMemo(() => { + const mainLaneId = defaultLane?.id; + const nonMainLaneIds = nonMainLanes?.map((lane) => lane.id) || []; + const allLanes = (mainLaneId && [mainLaneId, ...nonMainLaneIds]) || nonMainLaneIds; + return allLanes.filter((l) => l.toString() !== compare?.id.toString()); + }, [base?.id.toString(), compare?.id.toString(), lanesModel?.lanes.length]); - const LaneCompareComponent = getLaneCompare({ base, compare }); + if (!lanesModel) return null; + if (!lanesModel.viewedLane) return null; + if (!base) return null; return (
- {LaneCompareComponent} +
+
Compare
+
+ + {compare?.id.name} +
+
with
+
+ ''} + onLaneSelected={(laneId) => { + setBase(lanesModel?.lanes.find((l) => l.id.toString() === laneId.toString())); + }} + /> +
+
+
{LaneCompareComponent}
); } diff --git a/scopes/lanes/ui/compare/lane-compare/lane-compare.module.scss b/scopes/lanes/ui/compare/lane-compare/lane-compare.module.scss index c8e71bca25cd..1b861738af22 100644 --- a/scopes/lanes/ui/compare/lane-compare/lane-compare.module.scss +++ b/scopes/lanes/ui/compare/lane-compare/lane-compare.module.scss @@ -1,5 +1,3 @@ -@import '~@teambit/ui-foundation.ui.constants.z-indexes/z-indexes.module.scss'; - .rootLaneCompare { position: relative; height: 100%; @@ -42,7 +40,7 @@ top: 0; left: 0; background-color: var(--background-color); - z-index: $modal-z-index; + z-index: 1; padding: 0; } diff --git a/scopes/lanes/ui/compare/lane-compare/lane-compare.tsx b/scopes/lanes/ui/compare/lane-compare/lane-compare.tsx index ece6d95c9bf4..a4dbc408d934 100644 --- a/scopes/lanes/ui/compare/lane-compare/lane-compare.tsx +++ b/scopes/lanes/ui/compare/lane-compare/lane-compare.tsx @@ -93,7 +93,7 @@ export function LaneCompare({ componentDiff.componentId.changeVersion(componentDiff.sourceHead), ])) || [], - [loadingLaneDiff] + [loadingLaneDiff, base.id.toString(), compare.id.toString()] ); const defaultState = useCallback(() => { @@ -118,7 +118,7 @@ export function LaneCompare({ return value; }, []); - const [state, setState] = useState( + const [laneCompareState, setLaneCompareState] = useState( new Map( allComponents.map(([_base, _compare]) => { const key = computeStateKey(_base, _compare); @@ -128,9 +128,12 @@ export function LaneCompare({ ) ); + const [openDrawerList, setOpenDrawerList] = useState([]); + const [fullScreenDrawerKey, setFullScreen] = useState(undefined); + useEffect(() => { if (allComponents.length > 0) { - setState( + setLaneCompareState( new Map( allComponents.map(([_base, _compare]) => { const key = computeStateKey(_base, _compare); @@ -139,22 +142,21 @@ export function LaneCompare({ }) ) ); + setFullScreen(undefined); + setOpenDrawerList([]); } - }, [allComponents.length]); - - const [openDrawerList, onToggleDrawer] = useState([]); - const [fullScreenDrawerKey, setFullScreen] = useState(undefined); + }, [loadingLaneDiff, allComponents.length, base.id.toString(), compare.id.toString()]); const handleDrawerToggle = (id: string) => { const isDrawerOpen = openDrawerList.includes(id); if (isDrawerOpen) { - onToggleDrawer((list) => list.filter((drawer) => drawer !== id)); + setOpenDrawerList((list) => list.filter((drawer) => drawer !== id)); if (id === fullScreenDrawerKey) { setFullScreen(undefined); } return; } - onToggleDrawer((list) => list.concat(id)); + setOpenDrawerList((list) => list.concat(id)); }; const onFullScreenClicked = useCallback( @@ -162,7 +164,7 @@ export function LaneCompare({ e.stopPropagation(); setFullScreen((fullScreenState) => { if (fullScreenState === key) return undefined; - onToggleDrawer((drawers) => { + setOpenDrawerList((drawers) => { if (!drawers.includes(key)) return [...drawers, key]; return drawers; }); @@ -177,7 +179,7 @@ export function LaneCompare({ const _tabs = extractLazyLoadedData(tabs); const onClicked = (prop: ComponentCompareStateKey) => (id) => - setState((value) => { + setLaneCompareState((value) => { let existingState = value.get(key); const propState = existingState?.[prop]; if (propState) { @@ -210,7 +212,7 @@ export function LaneCompare({ accum.set(next.componentId.toStringWithoutVersion(), next); return accum; }, new Map()); - }, [loadingLaneDiff]); + }, [base.id.toString(), compare.id.toString(), loadingLaneDiff]); const groupedComponents = useMemo(() => { if (laneComponentDiffByCompId.size === 0) return null; @@ -230,12 +232,12 @@ export function LaneCompare({ accum.set(changeType, existing.concat([[baseId, compareId]])); return accum; }, new Map>()); - }, [base.id.toString(), compare.id.toString(), laneComponentDiffByCompId.size]); + }, [base.id.toString(), compare.id.toString(), laneComponentDiffByCompId.size, loadingLaneDiff, laneDiff]); const Loading = useMemo(() => { if (!loadingLaneDiff) return null; return ; - }, [loadingLaneDiff]); + }, [base.id.toString(), compare.id.toString(), loadingLaneDiff]); const ComponentCompares = useMemo(() => { if (!groupedComponents?.size) return []; @@ -292,7 +294,7 @@ export function LaneCompare({ compareId, changes, className: classnames(styles.componentCompareContainer, isFullScreen && styles.fullScreen), - state: state.get(compKey), + state: laneCompareState.get(compKey), hooks: hooks(baseId, compareId), customUseComponent, Loader: ComponentCompareLoader, diff --git a/scopes/lanes/ui/inputs/lane-selector/lane-grouped-menu-item.tsx b/scopes/lanes/ui/inputs/lane-selector/lane-grouped-menu-item.tsx index 5e0c6b4fe8ab..235988fadfb2 100644 --- a/scopes/lanes/ui/inputs/lane-selector/lane-grouped-menu-item.tsx +++ b/scopes/lanes/ui/inputs/lane-selector/lane-grouped-menu-item.tsx @@ -9,14 +9,32 @@ export type LaneGroupedMenuItemProps = { selected?: LaneId; current: LaneId[]; scope: string; + getHref?: (laneId: LaneId) => string; + onLaneSelected?: (laneId: LaneId) => void; } & HTMLAttributes; -export function LaneGroupedMenuItem({ selected, current, className, scope, ...rest }: LaneGroupedMenuItemProps) { +export function LaneGroupedMenuItem({ + selected, + current, + className, + scope, + getHref, + onLaneSelected, + ...rest +}: LaneGroupedMenuItemProps) { if (current.length === 0) return null; - if (current[0].isDefault()) { + if (current.length === 1 && current[0].isDefault()) { const defaultLane = current[0] as LaneId; - return ; + return ( + + ); } const onClickStopPropagation = (e) => e.stopPropagation(); @@ -27,7 +45,13 @@ export function LaneGroupedMenuItem({ selected, current, className, scope, ...re {scope} {current.map((lane) => ( - + ))} ); diff --git a/scopes/lanes/ui/inputs/lane-selector/lane-menu-item.tsx b/scopes/lanes/ui/inputs/lane-selector/lane-menu-item.tsx index 395212062415..344865daac4d 100644 --- a/scopes/lanes/ui/inputs/lane-selector/lane-menu-item.tsx +++ b/scopes/lanes/ui/inputs/lane-selector/lane-menu-item.tsx @@ -9,9 +9,18 @@ import styles from './lane-menu-item.module.scss'; export type LaneMenuItemProps = { selected?: LaneId; current: LaneId; + getHref?: (laneId: LaneId) => string; + onLaneSelected?: (laneId: LaneId) => void; } & HTMLAttributes; -export function LaneMenuItem({ selected, current, className, ...rest }: LaneMenuItemProps) { +export function LaneMenuItem({ + selected, + current, + className, + onLaneSelected, + getHref = LanesModel.getLaneUrl, + ...rest +}: LaneMenuItemProps) { const isCurrent = selected?.toString() === current.toString(); const currentVersionRef = useRef(null); @@ -21,11 +30,16 @@ export function LaneMenuItem({ selected, current, className, ...rest }: LaneMenu } }, [isCurrent]); - const href = LanesModel.getLaneUrl(current); + const href = getHref(current); return (
- + onLaneSelected(current))} + >
{current.name}
{current.isDefault() && ( diff --git a/scopes/lanes/ui/inputs/lane-selector/lane-selector.module.scss b/scopes/lanes/ui/inputs/lane-selector/lane-selector.module.scss index 247ce54c750c..e4e02023f4fc 100644 --- a/scopes/lanes/ui/inputs/lane-selector/lane-selector.module.scss +++ b/scopes/lanes/ui/inputs/lane-selector/lane-selector.module.scss @@ -5,8 +5,6 @@ // (this is bizarre, the tree has no position absolute) z-index: $nav-z-index; border-radius: 8px; - // override dropdown default width: 100% - width: calc(100% - 40px) !important; &.disabled { > div { @@ -28,7 +26,7 @@ } .menu { - width: calc(100% - 24px); + width: 100%; border-radius: 8px; padding: 0; padding-top: 8px; diff --git a/scopes/lanes/ui/inputs/lane-selector/lane-selector.tsx b/scopes/lanes/ui/inputs/lane-selector/lane-selector.tsx index 3929152be10a..5d1913ee75bf 100644 --- a/scopes/lanes/ui/inputs/lane-selector/lane-selector.tsx +++ b/scopes/lanes/ui/inputs/lane-selector/lane-selector.tsx @@ -15,17 +15,27 @@ export type LaneSelectorProps = { lanes: Array; selectedLaneId?: LaneId; groupByScope?: boolean; + getHref?: (laneId: LaneId) => string; + onLaneSelected?: (laneId: LaneId) => void; } & HTMLAttributes; type LaneDropdownItems = Array | Array<[scope: string, lanes: LaneId[]]>; -export function LaneSelector({ className, lanes, selectedLaneId, groupByScope = true, ...rest }: LaneSelectorProps) { +export function LaneSelector({ + className, + lanes, + selectedLaneId, + groupByScope = true, + getHref, + onLaneSelected, + ...rest +}: LaneSelectorProps) { const [filteredLanes, setFilteredLanes] = useState(lanes); const [focus, setFocus] = useState(false); useEffect(() => { setFilteredLanes(lanes); - }, [lanes]); + }, [lanes.length]); const multipleLanes = lanes.length > 1; const laneDropdownItems: LaneDropdownItems = groupByScope @@ -51,7 +61,7 @@ export function LaneSelector({ className, lanes, selectedLaneId, groupByScope = {...rest} open={!multipleLanes ? false : undefined} dropClass={styles.menu} - elevation='none' + elevation="none" onChange={multipleLanes ? onDropdownToggled : undefined} // @ts-ignore - mismatch between @types/react placeholder={ @@ -68,12 +78,25 @@ export function LaneSelector({ className, lanes, selectedLaneId, groupByScope = {multipleLanes && groupByScope && (laneDropdownItems as Array<[scope: string, lanes: LaneId[]]>).map(([scope, lanesByScope]) => ( - + ))} {multipleLanes && !groupByScope && (laneDropdownItems as LaneId[]).map((lane) => ( - + ))} ); diff --git a/scopes/lanes/ui/navigation/lane-switcher/lane-switcher.module.scss b/scopes/lanes/ui/navigation/lane-switcher/lane-switcher.module.scss index 89b71d3afa47..a4aa5d7043fa 100644 --- a/scopes/lanes/ui/navigation/lane-switcher/lane-switcher.module.scss +++ b/scopes/lanes/ui/navigation/lane-switcher/lane-switcher.module.scss @@ -22,6 +22,7 @@ .laneSelector { flex: 1; - width: 100%; padding-right: 8px; + // override dropdown default width: 100% + width: calc(100% - 40px) !important; }