From e0b2043c71582f0bad83f7940ca3cd4f79072240 Mon Sep 17 00:00:00 2001 From: ievgenii vdovenko Date: Tue, 6 Jan 2026 17:54:17 +0100 Subject: [PATCH] feature: implements mid trimming long text; implements repo length truncation --- grunt_config.json | 1 + rhodecode/lib/helpers.py | 16 ++ rhodecode/model/pull_request.py | 2 +- rhodecode/public/css/summary.less | 4 + rhodecode/public/css/tables.less | 4 - rhodecode/public/js/src/rhodecode.js | 6 +- .../public/js/src/rhodecode/utils/trimText.js | 153 ++++++++++++++++++ .../templates/admin/integrations/list.mako | 11 +- .../templates/admin/repos/repo_add_base.mako | 6 +- .../admin/repos/repo_edit_advanced.mako | 129 ++++++++------- .../admin/repos/repo_edit_audit.mako | 4 + .../admin/repos/repo_edit_settings.mako | 6 +- rhodecode/templates/admin/repos/repos.mako | 4 + rhodecode/templates/base/base.mako | 7 +- rhodecode/templates/compare/compare_diff.mako | 4 +- .../templates/data_table/_dt_elements.mako | 4 +- rhodecode/templates/files/files.mako | 12 +- rhodecode/templates/files/files_browser.mako | 1 + rhodecode/templates/index_base.mako | 4 +- .../templates/pullrequests/pullrequest.mako | 21 ++- .../pullrequests/pullrequest_show.mako | 8 +- 21 files changed, 311 insertions(+), 96 deletions(-) create mode 100644 rhodecode/public/js/src/rhodecode/utils/trimText.js 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) + //
OR + const perEl = + $el.closest(".truncate").attr("data-maxlen") || + $el.attr("data-maxlen"); + + // 2) manual override passed to function + // 3) auto-detected from width + // 4) fallback default + const autoLen = autoMaxLenForAnchor($el); + const limit = perEl || limitOverride || autoLen || DEFAULT_LIMIT; + + const trimmed = truncateMiddle(full, limit); + if ($span.text() !== trimmed) $span.text(trimmed); + }); +} + +function trimRepoTitleName() { + const $repoNameTitle = $("#repo-name-title"); + let text = $repoNameTitle.text(); + let arr = text.split(": "); + if (arr.length < 2) { + return; + } + let repoName = arr[arr.length - 1]; + let symbolsCnt = autoMaxLenForAnchor($repoNameTitle); + const trimmedName = truncateMiddle(repoName, symbolsCnt); + $repoNameTitle.text(arr[0] + ": " + trimmedName); +} \ No newline at end of file diff --git a/rhodecode/templates/admin/integrations/list.mako b/rhodecode/templates/admin/integrations/list.mako index 2132a9ef..b49e3707 100644 --- a/rhodecode/templates/admin/integrations/list.mako +++ b/rhodecode/templates/admin/integrations/list.mako @@ -37,8 +37,8 @@
-
-

+
+

%if c.repo: ${_('Current Integrations for Repository: {repo_name}').format(repo_name=c.repo.repo_name)} %elif c.repo_group: @@ -81,7 +81,7 @@ %if c.repo: - ${_('No {type} integrations for repo {repo} exist yet.').format(type=integration_type, repo=c.repo.repo_name)} + ${_('No {type} integrations for repo {repo} exist yet.').format(type=integration_type, repo=h.truncate_middle(c.repo.repo_name))} %elif c.repo_group: ${_('No {type} integrations for repogroup {repogroup} exist yet.').format(type=integration_type, repogroup=c.repo_group.group_name)} %else: @@ -91,7 +91,7 @@ %if c.current_IntegrationType: <% if c.repo: - create_url = h.route_path('repo_integrations_create', repo_name=c.repo.repo_name, integration=c.current_IntegrationType.key) + create_url = h.route_path('repo_integrations_create', repo_name=h.truncate_middle(c.repo.repo_name), integration=c.current_IntegrationType.key) elif c.repo_group: create_url = h.route_path('repo_group_integrations_create', repo_group_name=c.repo_group.group_name, integration=c.current_IntegrationType.key) else: @@ -128,7 +128,7 @@ %if integration.repo: - ${_('repo')}:${integration.repo.repo_name} + ${_('repo')}:${h.truncate_middle(integration.repo.repo_name)} %elif integration.repo_group: @@ -216,4 +216,5 @@ e.preventDefault(); delete_integration(this); }); + trimRepoTitleName(); \ No newline at end of file diff --git a/rhodecode/templates/admin/repos/repo_add_base.mako b/rhodecode/templates/admin/repos/repo_add_base.mako index 7b46e8cd..181cdea9 100644 --- a/rhodecode/templates/admin/repos/repo_add_base.mako +++ b/rhodecode/templates/admin/repos/repo_add_base.mako @@ -19,7 +19,8 @@ ${h.secure_form(h.route_path('repo_create'), request=request)} ${h.select('repo_group', request.GET.get('parent_group'), c.repo_groups, class_="medium")}
/
- ${h.text('repo_name', class_="medium", id="repo_name_input")} + ${h.text('repo_name', class_="medium", id="repo_name_input", maxlength=192)} +

@@ -282,6 +283,7 @@ ${h.secure_form(h.route_path('repo_create'), request=request)} } }) - }) + trimRepoTitleName(); + }); ${h.end_form()} diff --git a/rhodecode/templates/admin/repos/repo_edit_advanced.mako b/rhodecode/templates/admin/repos/repo_edit_advanced.mako index d2925688..02e143d2 100644 --- a/rhodecode/templates/admin/repos/repo_edit_advanced.mako +++ b/rhodecode/templates/admin/repos/repo_edit_advanced.mako @@ -17,8 +17,8 @@ %>
-
-

${_('Repository: %s') % c.rhodecode_db_repo.repo_name}

+
+

${_('Repository: %s') % c.rhodecode_db_repo.repo_name}

${base.dt_info_panel(elems)} @@ -241,7 +241,7 @@
@@ -263,73 +263,72 @@ diff --git a/rhodecode/templates/admin/repos/repo_edit_audit.mako b/rhodecode/templates/admin/repos/repo_edit_audit.mako index f8401b4d..9f0bb9d6 100644 --- a/rhodecode/templates/admin/repos/repo_edit_audit.mako +++ b/rhodecode/templates/admin/repos/repo_edit_audit.mako @@ -21,3 +21,7 @@
+ + diff --git a/rhodecode/templates/admin/repos/repo_edit_settings.mako b/rhodecode/templates/admin/repos/repo_edit_settings.mako index effc29b7..7e04c8bb 100644 --- a/rhodecode/templates/admin/repos/repo_edit_settings.mako +++ b/rhodecode/templates/admin/repos/repo_edit_settings.mako @@ -1,8 +1,8 @@ <%namespace name="base" file="/base/base.mako"/>
-
-

${_('Settings for Repository: %s') % c.rhodecode_db_repo.repo_name}

+
+

${_('Settings for Repository: %s') % c.rhodecode_db_repo.repo_name}

${h.secure_form(h.route_path('edit_repo', repo_name=c.rhodecode_db_repo.repo_name), request=request)} @@ -332,5 +332,7 @@ }; UsersAutoComplete('repo_owner', '${c.rhodecode_user.user_id}'); + + trimRepoTitleName(); }); diff --git a/rhodecode/templates/admin/repos/repos.mako b/rhodecode/templates/admin/repos/repos.mako index 2d7c2f75..c9276ade 100644 --- a/rhodecode/templates/admin/repos/repos.mako +++ b/rhodecode/templates/admin/repos/repos.mako @@ -141,6 +141,10 @@ $(document).ready(function() { }) ); + $(document).on("draw.dt", function () { + applyMiddleCut('#repo_list_table .truncate a', 60); + applyMiddleCut('.truncate-wrap'); + }); }); diff --git a/rhodecode/templates/base/base.mako b/rhodecode/templates/base/base.mako index 244a63d9..802a1430 100644 --- a/rhodecode/templates/base/base.mako +++ b/rhodecode/templates/base/base.mako @@ -854,7 +854,7 @@ var tmpl = ''; var repoType = data['repo_type']; - var repoName = data['text']; + var repoName = truncateMiddle(data['text'], 60); if(data && data.type == 'repo'){ if(repoType === 'hg'){ @@ -1222,6 +1222,9 @@ window.updateStickyHeader(); } } - })() + $(function () { + applyMiddleCut('#context-bar .title a'); + }); + })(); diff --git a/rhodecode/templates/compare/compare_diff.mako b/rhodecode/templates/compare/compare_diff.mako index e42f62a0..a1bbd1cc 100644 --- a/rhodecode/templates/compare/compare_diff.mako +++ b/rhodecode/templates/compare/compare_diff.mako @@ -219,7 +219,7 @@ var enable_fields = ${"false" if c.preview_mode else "true"}; $("#compare_source").select2({ - placeholder: "${'%s@%s' % (c.source_repo.repo_name, c.source_ref)}", + placeholder: "${'%s@%s' % (h.truncate_middle(c.source_repo.repo_name), c.source_ref)}", containerCssClass: "drop-menu", dropdownCssClass: "drop-menu-dropdown", formatSelection: formatSelection("${c.source_repo.repo_name}"), @@ -238,7 +238,7 @@ }).select2("enable", enable_fields); $("#compare_target").select2({ - placeholder: "${'%s@%s' % (c.target_repo.repo_name, c.target_ref)}", + placeholder: "${'%s@%s' % (h.truncate_middle(c.target_repo.repo_name), c.target_ref)}", dropdownAutoWidth: true, containerCssClass: "drop-menu", dropdownCssClass: "drop-menu-dropdown", diff --git a/rhodecode/templates/data_table/_dt_elements.mako b/rhodecode/templates/data_table/_dt_elements.mako index 5b13c975..19f0b8f6 100644 --- a/rhodecode/templates/data_table/_dt_elements.mako +++ b/rhodecode/templates/data_table/_dt_elements.mako @@ -402,10 +402,10 @@ ${h.style_metatag(tag_type, tag)|n,trim} %> ## Source - ${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)}
diff --git a/rhodecode/templates/files/files.mako b/rhodecode/templates/files/files.mako index fb904989..63f36e6c 100644 --- a/rhodecode/templates/files/files.mako +++ b/rhodecode/templates/files/files.mako @@ -286,9 +286,17 @@ query.callback(data); }; + const truncateSelection = function(item) { + return truncateMiddle(item, 35); + } + + const truncateResult = function(item) { + return truncateMiddle(item, 60); + } + var select2RefFileSwitcher = function (targetElement, loadUrl, initialData) { var formatResult = function (result, container, query) { - return formatSelect2SelectionRefs(result); + return formatSelect2SelectionRefs(result, truncateResult); }; var formatSelection = function (data, container) { @@ -299,7 +307,7 @@ tmpl = (commit_ref.raw_id || "").substr(0,8); } else if (commit_ref.type === 'branch') { tmpl = tmpl.concat(' '); - tmpl = tmpl.concat(escapeHtml(commit_ref.text)); + tmpl = tmpl.concat(truncateSelection(escapeHtml(commit_ref.text))); } else if (commit_ref.type === 'tag') { tmpl = tmpl.concat(' '); tmpl = tmpl.concat(escapeHtml(commit_ref.text)); diff --git a/rhodecode/templates/files/files_browser.mako b/rhodecode/templates/files/files_browser.mako index 6289b24f..9eced895 100644 --- a/rhodecode/templates/files/files_browser.mako +++ b/rhodecode/templates/files/files_browser.mako @@ -10,6 +10,7 @@
${h.hidden('refs_filter')} +