diff --git a/grunt_config.json b/grunt_config.json index acd4a24f..882683fa 100644 --- a/grunt_config.json +++ b/grunt_config.json @@ -70,6 +70,7 @@ "<%= dirs.js.src_rc %>/i18n/select2/translations.js", "<%= dirs.js.src %>/rhodecode/utils/array.js", "<%= dirs.js.src %>/rhodecode/utils/string.js", + "<%= dirs.js.src %>/rhodecode/utils/trimText.js", "<%= dirs.js.src %>/rhodecode/utils/pyroutes.js", "<%= dirs.js.src %>/rhodecode/utils/ajax.js", "<%= dirs.js.src %>/rhodecode/utils/autocomplete.js", diff --git a/rhodecode/lib/helpers.py b/rhodecode/lib/helpers.py index 288145d6..9d0fd16d 100644 --- a/rhodecode/lib/helpers.py +++ b/rhodecode/lib/helpers.py @@ -2452,3 +2452,19 @@ def get_directory_statistics(start_path): total_size += dir_size return total_files, total_size, directory_stats + + +def truncate_middle(text_, max_length=100): + ellipsis = "..." + min_limit = 5 + + if len(text_) <= max_length: + return text_ + + max_length = max(min_limit, max_length) + + keep = max_length - len(ellipsis) + left = math.ceil(keep / 2) + right = math.ceil(keep / 2) + + return text_[:left] + ellipsis + text_[-right:] diff --git a/rhodecode/model/pull_request.py b/rhodecode/model/pull_request.py index 7d96d240..ea7e33dd 100644 --- a/rhodecode/model/pull_request.py +++ b/rhodecode/model/pull_request.py @@ -113,7 +113,7 @@ def get_diff_info(source_repo, source_ref, target_repo, target_ref, get_authors= target_scm = target_repo.scm_instance() ancestor_id = target_scm.get_common_ancestor(target_ref, source_ref, source_scm) - if not ancestor_id: + if not ancestor_id or (isinstance(ancestor_id, str) and ancestor_id.lower() == "none"): raise ValueError( "cannot calculate diff info without a common ancestor. " "Make sure both repositories are related, and have a common forking commit." diff --git a/rhodecode/public/css/summary.less b/rhodecode/public/css/summary.less index dd115549..503a67b8 100644 --- a/rhodecode/public/css/summary.less +++ b/rhodecode/public/css/summary.less @@ -208,6 +208,10 @@ .fieldset { + pre { + overflow-x: auto; + } + .left-label { // similar to form legend display: block; margin: 0; diff --git a/rhodecode/public/css/tables.less b/rhodecode/public/css/tables.less index 098e41f8..07789565 100644 --- a/rhodecode/public/css/tables.less +++ b/rhodecode/public/css/tables.less @@ -186,7 +186,6 @@ table.dataTable { &.truncate, .truncate-wrap { white-space: nowrap; overflow: hidden; - text-overflow: ellipsis; max-width: 350px; } } @@ -345,9 +344,6 @@ table.dataTable { max-width: 450px; width: 300px; overflow: hidden; - text-overflow: ellipsis; - -o-text-overflow: ellipsis; - -ms-text-overflow: ellipsis; &.autoexpand { width: 120px; diff --git a/rhodecode/public/js/src/rhodecode.js b/rhodecode/public/js/src/rhodecode.js index 4c87957c..a183cee8 100644 --- a/rhodecode/public/js/src/rhodecode.js +++ b/rhodecode/public/js/src/rhodecode.js @@ -333,7 +333,7 @@ var tooltipActivate = function () { }; // Formatting values in a Select2 dropdown of commit references -var formatSelect2SelectionRefs = function(commit_ref){ +var formatSelect2SelectionRefs = function(commit_ref, tripTextFn){ var tmpl = ''; if (!commit_ref.text || commit_ref.type === 'sha'){ return commit_ref.text; @@ -345,6 +345,10 @@ var formatSelect2SelectionRefs = function(commit_ref){ } else if (commit_ref.type === 'book'){ tmpl = tmpl.concat(' '); } + if (tripTextFn !== undefined) { + return tmpl.concat(tripTextFn(escapeHtml(commit_ref.text))); + } + return tmpl.concat(escapeHtml(commit_ref.text)); }; diff --git a/rhodecode/public/js/src/rhodecode/utils/trimText.js b/rhodecode/public/js/src/rhodecode/utils/trimText.js new file mode 100644 index 00000000..61e1bdd2 --- /dev/null +++ b/rhodecode/public/js/src/rhodecode/utils/trimText.js @@ -0,0 +1,153 @@ +const DEFAULT_LIMIT = 30; // used only if auto can't compute +const MIN_LIMIT = 5; +const ELLIPSIS = "..."; +const FILL = 0.90; // use 90% of available width + +// Cache average char width per font+letterSpacing +const avgCharWidthCache = new Map(); + +function toChars(s) { + return Array.from(s); +} // unicode-safe + +function truncateMiddle(str, maxLen) { + maxLen = parseInt(maxLen, 10); + if (!Number.isFinite(maxLen)) maxLen = DEFAULT_LIMIT; + maxLen = Math.max(MIN_LIMIT, maxLen); + + const full = (str || "").trim(); + const chars = toChars(full); + if (chars.length <= maxLen) return full; + + const keep = maxLen - ELLIPSIS.length; // remaining visible chars besides "..." + const left = Math.ceil(keep / 2); + const right = Math.floor(keep / 2); + + return chars.slice(0, left).join("") + ELLIPSIS + chars.slice(chars.length - right).join(""); +} + +function ensureTextSpan($a) { + let $span = $a.find("span.js-midcut").first(); + if ($span.length) return $span; + + const textNodes = $a.contents().filter(function () { + return this.nodeType === 3 && this.nodeValue && this.nodeValue.trim().length > 0; + }); + if (!textNodes.length) return null; + + const full = textNodes.map(function () { + return this.nodeValue; + }).get().join(" ").trim(); + textNodes.remove(); + + $span = $("", {class: "js-midcut", text: full}) + .attr("data-full", full) + .attr("title", full); + + $a.append($span); + return $span; +} + +function getAvgCharWidthPx(el) { + const cs = window.getComputedStyle(el); + + // Include letter-spacing because it affects width + const key = [ + cs.fontStyle, cs.fontVariant, cs.fontWeight, + cs.fontSize, cs.fontFamily, cs.letterSpacing + ].join("|"); + + if (avgCharWidthCache.has(key)) return avgCharWidthCache.get(key); + + // Measure using a hidden span with same font + letter spacing + const sample = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + const repeat = 6; // 62*6 = 372 chars -> stable average + const text = sample.repeat(repeat); + + const meas = document.createElement("span"); + meas.style.position = "absolute"; + meas.style.visibility = "hidden"; + meas.style.whiteSpace = "nowrap"; + meas.style.left = "-99999px"; + meas.style.top = "-99999px"; + meas.style.fontStyle = cs.fontStyle; + meas.style.fontVariant = cs.fontVariant; + meas.style.fontWeight = cs.fontWeight; + meas.style.fontSize = cs.fontSize; + meas.style.fontFamily = cs.fontFamily; + meas.style.letterSpacing = cs.letterSpacing; + meas.textContent = text; + + document.body.appendChild(meas); + const width = meas.getBoundingClientRect().width; + document.body.removeChild(meas); + + const avg = width / text.length || 8; // fallback if something weird + avgCharWidthCache.set(key, avg); + return avg; +} + +// Auto maxLen from width (px) → chars, using 90% available width +function autoMaxLenForAnchor($a) { + const aEl = $a[0]; + const $truncate = $a.closest(".truncate"); + const containerEl = $truncate[0] || aEl; + + const containerW = containerEl.getBoundingClientRect().width; + if (!containerW) return null; + + // Subtract widths of non-text children (icons etc.), exclude our span + let nonTextW = 0; + $a.children().not("span.js-midcut").each(function () { + nonTextW += $(this).outerWidth(true) || 0; + }); + + const availablePx = Math.max(0, (containerW - nonTextW) * FILL); + if (!availablePx) return null; + + const avgCharW = getAvgCharWidthPx(aEl); + if (!avgCharW) return null; + + const est = Math.floor(availablePx / avgCharW); + return Math.max(MIN_LIMIT, est); +} + +// limitOverride: optional manual max length for all elements in this call +function applyMiddleCut(selector, limitOverride) { + $(selector).each(function () { + const $el = $(this); + + const $span = ensureTextSpan($el); + if (!$span) return; + + const full = $span.attr("data-full") || $span.text(); + + // 1) per-element override (if you can add attributes) + //
${pull_request.source_ref_parts.name}
+ ${h.truncate_middle(pull_request.source_ref_parts.name, 70)}
→
## Target
- ${pull_request.target_ref_parts.name}
+ ${h.truncate_middle(pull_request.target_ref_parts.name, 90)}
${c.pull_request.source_ref_parts.type}:${c.pull_request.source_ref_parts.name}
+ ${c.pull_request.source_ref_parts.type}:${h.truncate_middle(c.pull_request.source_ref_parts.name, 40)}
%else:
- ${'{}:{}'.format(c.pull_request.source_ref_parts.type, c.pull_request.source_ref_parts.name)}
+ ${'{}:{}'.format(c.pull_request.source_ref_parts.type, h.truncate_middle(c.pull_request.source_ref_parts.name, 40))}
%endif
- ${_('of')} ${c.pull_request.source_repo.repo_name}
+ ${_('of')} ${h.truncate_middle(c.pull_request.source_repo.repo_name, 40)}
→
## Target
%if c.pull_request.target_ref_parts.type == 'branch':
@@ -118,7 +118,7 @@
${'{}:{}'.format(c.pull_request.target_ref_parts.type, c.pull_request.target_ref_parts.name)}
%endif
- ${_('of')} ${c.pull_request.target_repo.repo_name}
+ ${_('of')} ${h.truncate_middle(c.pull_request.target_repo.repo_name, 40)}
more details