+ {!mini &&
}
{subView === 'cur_file' &&
}
{subView === 'overview' &&
}
-
diff --git a/services/web/frontend/js/features/review-panel-new/hooks/use-review-panel-styles.ts b/services/web/frontend/js/features/review-panel-new/hooks/use-review-panel-styles.ts
new file mode 100644
index 0000000000..3bca48a7ea
--- /dev/null
+++ b/services/web/frontend/js/features/review-panel-new/hooks/use-review-panel-styles.ts
@@ -0,0 +1,54 @@
+import { CSSProperties, useCallback, useEffect, useState } from 'react'
+import { useCodeMirrorViewContext } from '@/features/source-editor/components/codemirror-editor'
+
+export const useReviewPanelStyles = (mini: boolean) => {
+ const view = useCodeMirrorViewContext()
+ const [styles, setStyles] = useState
()
+
+ const updateScrollDomVariables = useCallback((element: HTMLDivElement) => {
+ const { top, bottom } = element.getBoundingClientRect()
+
+ setStyles(value => ({
+ ...value,
+ '--review-panel-top': `${top}px`,
+ '--review-panel-bottom': `${bottom}px`,
+ }))
+ }, [])
+
+ const updateContentDomVariables = useCallback((element: HTMLDivElement) => {
+ const { height } = element.getBoundingClientRect()
+
+ setStyles(value => ({
+ ...value,
+ '--review-panel-height': `${height}px`,
+ }))
+ }, [])
+
+ useEffect(() => {
+ setStyles(value => ({
+ ...value,
+ '--review-panel-width': mini ? '22px' : '230px',
+ }))
+ }, [mini])
+
+ useEffect(() => {
+ if ('ResizeObserver' in window) {
+ const scrollDomObserver = new window.ResizeObserver(entries =>
+ updateScrollDomVariables(entries[0]?.target as HTMLDivElement)
+ )
+ scrollDomObserver.observe(view.scrollDOM)
+
+ const contentDomObserver = new window.ResizeObserver(entries =>
+ updateContentDomVariables(entries[0]?.target as HTMLDivElement)
+ )
+ contentDomObserver.observe(view.contentDOM)
+
+ return () => {
+ scrollDomObserver.disconnect()
+ contentDomObserver.disconnect()
+ }
+ }
+ }, [view, updateScrollDomVariables, updateContentDomVariables])
+
+ return styles
+}
diff --git a/services/web/frontend/js/features/review-panel-new/utils/has-active-range.ts b/services/web/frontend/js/features/review-panel-new/utils/has-active-range.ts
new file mode 100644
index 0000000000..0934aef59f
--- /dev/null
+++ b/services/web/frontend/js/features/review-panel-new/utils/has-active-range.ts
@@ -0,0 +1,25 @@
+import { Ranges } from '@/features/review-panel-new/context/ranges-context'
+import { Threads } from '@/features/review-panel-new/context/threads-context'
+
+export const hasActiveRange = (
+ ranges: Ranges | undefined,
+ threads: Threads | undefined
+): boolean | undefined => {
+ if (!ranges || !threads) {
+ // data isn't loaded yet
+ return undefined
+ }
+
+ if (ranges.changes.length > 0) {
+ // at least one tracked change
+ return true
+ }
+
+ for (const thread of Object.values(threads)) {
+ if (!thread.resolved) {
+ return true
+ }
+ }
+
+ return false
+}
diff --git a/services/web/frontend/js/features/review-panel-new/utils/is-in-viewport.tsx b/services/web/frontend/js/features/review-panel-new/utils/is-in-viewport.tsx
deleted file mode 100644
index 9f545d7bd3..0000000000
--- a/services/web/frontend/js/features/review-panel-new/utils/is-in-viewport.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import { EditorView } from '@codemirror/view'
-import { Change } from '../../../../../types/change'
-
-export const isInViewport =
- (view: EditorView) =>
- (change: Change): boolean =>
- change.op.p >= view.viewport.from && change.op.p <= view.viewport.to
diff --git a/services/web/frontend/js/features/review-panel-new/utils/position-items.ts b/services/web/frontend/js/features/review-panel-new/utils/position-items.ts
index f0e3a5cc6a..eb624aa8f5 100644
--- a/services/web/frontend/js/features/review-panel-new/utils/position-items.ts
+++ b/services/web/frontend/js/features/review-panel-new/utils/position-items.ts
@@ -15,34 +15,44 @@ export const positionItems = debounce(
return
}
- let focusedItemIndex = items.findIndex(item =>
- item.classList.contains('review-panel-entry-focused')
+ let activeItemIndex = items.findIndex(item =>
+ item.classList.contains('review-panel-entry-action')
)
- if (focusedItemIndex === -1) {
+
+ if (activeItemIndex === -1) {
+ // if there is no action available
+ // check if there is a focused entry
+ activeItemIndex = items.findIndex(item =>
+ item.classList.contains('review-panel-entry-focused')
+ )
+ }
+
+ if (activeItemIndex === -1) {
// if entry was not focused manually
// check if there is an entry in selection and use that as the focused item
- focusedItemIndex = items.findIndex(item =>
+ activeItemIndex = items.findIndex(item =>
item.classList.contains('review-panel-entry-highlighted')
)
}
- if (focusedItemIndex === -1) {
- focusedItemIndex = previousFocusedItemIndex
+
+ if (activeItemIndex === -1) {
+ activeItemIndex = previousFocusedItemIndex
}
- const focusedItem = items[focusedItemIndex]
- if (!focusedItem) {
+ const activeItem = items[activeItemIndex]
+ if (!activeItem) {
return
}
- const focusedItemTop = getTopPosition(focusedItem, focusedItemIndex === 0)
+ const activeItemTop = getTopPosition(activeItem, activeItemIndex === 0)
- focusedItem.style.top = `${focusedItemTop}px`
- focusedItem.style.visibility = 'visible'
- const focusedItemRect = focusedItem.getBoundingClientRect()
+ activeItem.style.top = `${activeItemTop}px`
+ activeItem.style.visibility = 'visible'
+ const focusedItemRect = activeItem.getBoundingClientRect()
- // above the focused item
- let topLimit = focusedItemTop
- for (let i = focusedItemIndex - 1; i >= 0; i--) {
+ // above the active item
+ let topLimit = activeItemTop
+ for (let i = activeItemIndex - 1; i >= 0; i--) {
const item = items[i]
const rect = item.getBoundingClientRect()
let top = getTopPosition(item, i === 0)
@@ -55,9 +65,9 @@ export const positionItems = debounce(
topLimit = top
}
- // below the focused item
- let bottomLimit = focusedItemTop + focusedItemRect.height
- for (let i = focusedItemIndex + 1; i < items.length; i++) {
+ // below the active item
+ let bottomLimit = activeItemTop + focusedItemRect.height
+ for (let i = activeItemIndex + 1; i < items.length; i++) {
const item = items[i]
const rect = item.getBoundingClientRect()
let top = getTopPosition(item, false)
@@ -70,7 +80,7 @@ export const positionItems = debounce(
}
return {
- focusedItemIndex,
+ activeItemIndex,
min: topLimit,
max: bottomLimit,
}
diff --git a/services/web/frontend/stylesheets/app/editor/review-panel-new.less b/services/web/frontend/stylesheets/app/editor/review-panel-new.less
index 4653fcaf06..ad3270730c 100644
--- a/services/web/frontend/stylesheets/app/editor/review-panel-new.less
+++ b/services/web/frontend/stylesheets/app/editor/review-panel-new.less
@@ -1,17 +1,22 @@
-.review-panel-container {
- height: 100%;
- flex-shrink: 0;
-}
-
.review-panel-new {
- z-index: 6;
- flex-shrink: 0;
- background-color: @neutral-10;
- border-left: solid 0 @neutral-20;
- font-family: @font-family-base;
- line-height: @line-height-base;
- font-size: @font-size-01;
- box-sizing: content-box;
+ &.review-panel-container {
+ height: 100%;
+ flex-shrink: 0;
+ position: relative;
+ }
+
+ .review-panel-inner {
+ z-index: 6;
+ flex-shrink: 0;
+ background-color: @neutral-10;
+ border-left: solid 0 @neutral-20;
+ font-family: @font-family-base;
+ line-height: @line-height-base;
+ font-size: @font-size-01;
+ box-sizing: content-box;
+ width: var(--review-panel-width);
+ min-height: var(--review-panel-height);
+ }
.review-panel-entry {
background-color: white;
@@ -38,6 +43,7 @@
margin-left: @spacing-01;
border: 1px solid @blue-50;
}
+
.review-panel-entry-header {
display: flex;
justify-content: space-between;
@@ -46,9 +52,11 @@
color: @blue;
font-size: 110%;
}
+
.review-panel-entry-time {
color: @content-secondary;
}
+
.review-panel-entry-actions {
display: flex;
align-items: center;
@@ -73,16 +81,19 @@
}
}
}
+
.review-panel-change-body {
display: flex;
align-items: center;
color: @content-secondary;
gap: @spacing-02;
}
+
.review-panel-content-highlight {
color: @content-primary;
text-decoration: none;
}
+
del.review-panel-content-highlight {
text-decoration: line-through;
}
@@ -92,14 +103,17 @@
padding: @spacing-02;
font-size: 16px;
}
+
.review-panel-entry-icon-accept {
background-color: @green-10;
color: @green-50;
}
+
.review-panel-entry-icon-reject {
background-color: @red-10;
color: @red-50;
}
+
.review-panel-entry-icon-changed {
background-color: @neutral-20;
color: @content-secondary;
@@ -107,7 +121,8 @@
.review-panel-header {
position: fixed;
- top: 0;
+ top: calc(var(--review-panel-top) - 40px);
+ width: var(--review-panel-width);
z-index: 2;
display: flex;
flex-direction: column;
@@ -291,19 +306,22 @@
display: flex;
gap: @spacing-04;
}
+
.review-panel-comment {
flex-grow: 1;
}
+
.review-panel-comment-reply-divider {
border-left: 2px solid @yellow-20;
}
+
.review-panel-comment-body {
font-size: @font-size-02;
color: @content-primary;
overflow-wrap: anywhere;
}
- .review-panel-content-expandable {
+ .review-panel-content-expandable {
display: -webkit-box;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
@@ -311,6 +329,7 @@
line-clamp: 3;
overflow: hidden;
}
+
.review-panel-content-expanded {
display: block;
}
@@ -330,6 +349,12 @@
max-height: 400px;
}
+ .review-panel-add-comment-form {
+ display: flex;
+ flex-direction: column;
+ width: var(--review-panel-width);
+ }
+
.review-panel-empty-state {
position: absolute;
top: 0;
@@ -419,46 +444,69 @@
}
.review-panel-footer {
+ position: fixed;
+ top: calc(var(--review-panel-bottom) - 66px);
+ width: var(--review-panel-width);
z-index: 2;
- }
-}
-
-.review-panel-new.review-panel-mini {
- width: 22px !important;
- overflow: visible !important;
-
- .review-panel-entry {
- margin-left: 2px;
- background-color: transparent;
- border: none;
- }
-
- .review-panel-entry-indicator {
- position: absolute;
- left: 0;
- top: 7px;
+ background: @rp-bg-dim-blue;
+ border-top: 1px solid #d9d9d9;
display: flex;
- width: 16px;
- height: 16px;
- color: @content-secondary;
- cursor: pointer;
}
- .review-panel-entry-content {
- display: none;
- background: white;
- border: 1px solid @rp-border-grey;
- border-radius: @border-radius-base-new;
- width: 200px;
- padding: @spacing-02;
- }
-
- .review-panel-entry:hover {
- .review-panel-entry-content {
- display: initial;
- position: absolute;
- left: -200px;
+ &.review-panel-subview-overview {
+ &.review-panel-container {
+ overflow-y: hidden;
+ position: sticky;
top: 0;
}
+
+ .review-panel-inner {
+ min-height: auto;
+ height: 100%;
+ overflow: hidden;
+ }
+ }
+
+ &.review-panel-mini {
+ overflow: visible !important;
+
+ .review-panel-entry {
+ margin-left: 2px;
+ background-color: transparent;
+ border: none;
+ }
+
+ .review-panel-entry-indicator {
+ position: absolute;
+ left: 0;
+ top: 7px;
+ display: flex;
+ width: 16px;
+ height: 16px;
+ color: @content-secondary;
+ cursor: pointer;
+ }
+
+ .review-panel-entry-content {
+ display: none;
+ background: white;
+ border: 1px solid @rp-border-grey;
+ border-radius: @border-radius-base-new;
+ width: 200px;
+ padding: @spacing-02;
+ }
+
+ .review-panel-entry:hover {
+ .review-panel-entry-content {
+ display: initial;
+ position: absolute;
+ left: -200px;
+ top: 0;
+ }
+ }
+
+ .review-panel-footer {
+ display: none;
+ }
}
}