Activities of "maliming"

Hi,

This is a bug in Suite's template processing on Windows, not something wrong with your templates — your changes from the previous round were fine.

To fix the output now: remove %%main-entity-tab%% from both customized templates and put these placeholders directly in your panel body instead.

CreateModal:

%%<if:IsChildEntity>%%<abp-input asp-for="%%child-master-entity-name%%Id" />%%</if:IsChildEntity>%%
%%input-properties%%
%%navigation-property-lookup-inputs%%

EditModal:

%%<if:IsChildEntity>%%<abp-input asp-for="%%entity-name%%.%%child-master-entity-name%%Id" type="hidden" />%%</if:IsChildEntity>%%
%%<if:CheckConcurrency>%%<abp-input asp-for="%%entity-name%%.ConcurrencyStamp" hidden="true" suppress-label="true"/>%%</if:CheckConcurrency>%%
%%input-properties%%
%%navigation-property-lookup-inputs%%

These cover everything %%main-entity-tab%% produced (including the hidden master-id input for child entities and the concurrency stamp on the edit modal). After this change the full template is written — markers, footer and closing tags included.

One note for later: if you add a navigation connection to an entity, %%navigation-connection-lookup-tabs%% emits <abp-tab> sections that expect a tabs container, so that part needs its own markup in an off-canvas layout.

Thanks

Great

Hi,

Your JavaScript is actually updating the DOM — it's just updating a copy you can't see, and the badge itself never becomes visible. Three separate things are going on here:

  1. The LeptonX MVC layout renders every toolbar item twice: once in the toolbar and once more inside the hidden mobile menu, and the mobile copy comes first in the DOM. Since your markup uses id attributes, the ids get duplicated and document.getElementById(...) always resolves to the hidden mobile copy — the notification list is injected into the invisible panel while the visible one stays empty.
  2. LeptonX has no data-count based CSS rules, so the inline style="opacity:0;visibility:hidden;" on your badges is never undone. Besides that, the theme only gives .menu-item-badge its badge styling (position, background, size) when the parent link has the active-menu-item class — without that class it's just an unstyled <small> element.
  3. The theme already wraps every toolbar item in <li class="outer-menu-item">. Your component's own outer <li> can't be nested inside it, so the browser closes the theme's <li> early and you end up with an extra empty <li> in the toolbar.

Here is a corrected Default.cshtml: no ids (all DOM lookups go through classes and update both rendered copies), active-menu-item toggled for the badge styling and d-none for its visibility, no outer <li>, and a guard at the top so the inline script (which is also included twice) only runs once per page:

<a href="javascript:;" class="lpx-menu-item-link notification-bell" data-lpx-ctx-toggle="notification-bar">
    <span class="lpx-menu-item-icon" style="position:relative;">
        <i class="lpx-icon action-icon bi bi-bell-fill" aria-hidden="true"></i>
        <small class="menu-item-badge notification-count d-none">0</small>
    </span>
</a>

<div class="lpx-context-menu" data-lpx-context-menu="notification-bar">
    <ul class="lpx-nav-menu-notification notification-menu">
        <li class="outer-menu-item">
            <a class="lpx-menu-item-link lpx-menu-item">
                <span class="lpx-menu-item-icon">
                    <i class="lpx-icon outer-icon bi bi-bell-fill" aria-hidden="true"></i>
                </span>
                <span class="lpx-menu-item-text">Notification
                    <span class="lpx-badge notification-new-badge d-none">0 NEW</span>
                </span>
                <span data-lpx-close="notification-bar">
                    <i class="lpx-icon bi bi-x outer-icon dd-icon" aria-hidden="true"></i>
                </span>
            </a>
        </li>
    </ul>

    <div class="lpx-button-container">
        <button class="lpx-button notif-btn-outline notification-mark-all" type="button">Mark all as read</button>
        <a class="lpx-button notif-btn-primary" href="/notification/private-messages">See All Notifications</a>
    </div>
</div>


<script>
(function() {
    'use strict';

    // The theme renders each toolbar item twice (toolbar + mobile menu),
    // so this script is also included twice per page - run it only once.
    if (window.__notificationBadgeInitialized) return;
    window.__notificationBadgeInitialized = true;

    var currentUnread = 0;

    function unwrap(response) {
        if (response && typeof response === 'object' && 'result' in response) return response.result;
        return response;
    }

    function esc(s) {
        return String(s == null ? '' : s).replace(/[&<>"']/g, function(c) {
            return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
        });
    }

    function isPanelOpen() {
        return !!document.querySelector('[data-lpx-context-menu="notification-bar"].show');
    }

    function updateBadge(count) {
        var num = parseInt(count, 10) || 0;
        currentUnread = num;

        document.querySelectorAll('.notification-bell').forEach(function(link) {
            link.classList.toggle('active-menu-item', num > 0);
            var badge = link.querySelector('.notification-count');
            if (badge) {
                badge.textContent = num > 99 ? '99+' : String(num);
                badge.classList.toggle('d-none', num <= 0);
            }
        });

        document.querySelectorAll('.notification-new-badge').forEach(function(badge) {
            badge.textContent = num + ' NEW';
            badge.classList.toggle('d-none', num <= 0);
        });
    }

    function startOfDay(d) { return new Date(d.getFullYear(), d.getMonth(), d.getDate()); }

    function groupLabel(date) {
        var today = startOfDay(new Date());
        var yest = new Date(today);
        yest.setDate(today.getDate() - 1);
        if (date >= today) return 'TODAY';
        if (date >= yest) return 'YESTERDAY';
        var dd = String(date.getDate()).padStart(2, '0');
        var mm = String(date.getMonth() + 1).padStart(2, '0');
        return dd + '.' + mm + '.' + date.getFullYear();
    }

    function formatTime(date) {
        var hh = String(date.getHours()).padStart(2, '0');
        var mm = String(date.getMinutes()).padStart(2, '0');
        var today = startOfDay(new Date());
        var yest = new Date(today);
        yest.setDate(today.getDate() - 1);
        if (date >= today) return hh + ':' + mm;
        if (date >= yest) return 'Yesterday ' + hh + ':' + mm;
        var dd = String(date.getDate()).padStart(2, '0');
        var mm2 = String(date.getMonth() + 1).padStart(2, '0');
        return mm2 + '/' + dd + ' ' + hh + ':' + mm;
    }

    function renderGroups(items) {
        document.querySelectorAll('.notification-menu').forEach(function(menu) {
            menu.querySelectorAll('.notif-group').forEach(function(g) { g.remove(); });

            if (!items || !items.length) {
                var emptyLi = document.createElement('li');
                emptyLi.className = 'outer-menu-item notif-group';
                emptyLi.innerHTML = '<div class="lpx-notification-empty">No notifications</div>';
                menu.appendChild(emptyLi);
                return;
            }

            var groups = [];
            items.forEach(function(it, idx) {
                var d = new Date(it.creationTime);
                var label = groupLabel(d);
                var g = null;
                for (var j = 0; j < groups.length; j++) {
                    if (groups[j].label === label) { g = groups[j]; break; }
                }
                if (!g) { g = { label: label, items: [] }; groups.push(g); }
                g.items.push({ data: it, unread: idx < currentUnread, date: d });
            });

            groups.forEach(function(g) {
                var li = document.createElement('li');
                li.className = 'outer-menu-item notif-group';
                var html = '<div class="lpx-menu-item-title">' + esc(g.label) + '</div>'
                    + '<ul class="lpx-notification-items lpx-inner-menu">';
                g.items.forEach(function(it) {
                    html += '<li class="lpx-notification-item' + (it.unread ? ' unread' : '') + '" data-id="' + esc(it.data.id) + '">'
                        + '<a class="lpx-notification-item-link">'
                        + '<div class="lpx-notification-member-container"><i class="lpx-notification-member-icon bi bi-bell-fill"></i></div>'
                        + '<div><p class="lpx-notification-text"><span class="lpx-notification-text-highlight">' + esc(it.data.title)
                        + '</span> ' + esc(it.data.content) + '</p>'
                        + '<small class="lpx-notification-text-small">' + esc(formatTime(it.date)) + '</small></div>'
                        + '</a></li>';
                });
                html += '</ul>';
                li.innerHTML = html;
                menu.appendChild(li);
            });
        });
    }

    function fetchLatest() {
        if (typeof abp === 'undefined' || !abp || !abp.ajax) return;
        abp.ajax({
            url: (abp.appPath || '') + 'api/notification/private-message?maxResultCount=5&sorting=CreationTime%20DESC',
            type: 'GET',
            success: function(response) {
                var data = unwrap(response);
                var items = (data && data.items) ? data.items : [];
                renderGroups(items);
            },
            error: function() {}
        });
    }

    function fetchCount() {
        if (typeof abp === 'undefined' || !abp || !abp.ajax) return;
        abp.ajax({
            url: (abp.appPath || '') + 'api/notification/private-message/unread-count',
            type: 'GET',
            success: function(response) {
                var num = parseInt(unwrap(response), 10) || 0;
                updateBadge(num);
                if (isPanelOpen()) fetchLatest();
            },
            error: function() {}
        });
    }

    function markOneRead(id) {
        if (typeof abp === 'undefined' || !abp || !abp.ajax) return;
        abp.ajax({
            url: (abp.appPath || '') + 'api/notification/private-message/' + id + '/mark-as-read',
            type: 'POST',
            success: function() {
                document.querySelectorAll('.lpx-notification-item[data-id="' + id + '"]').forEach(function(el) {
                    el.classList.remove('unread');
                });
                fetchCount();
            },
            error: function() {}
        });
    }

    function markAllRead() {
        if (typeof abp === 'undefined' || !abp || !abp.ajax) return;
        abp.ajax({
            url: (abp.appPath || '') + 'api/notification/private-message/mark-all-as-read',
            type: 'POST',
            success: function() {
                document.querySelectorAll('.notification-menu .lpx-notification-item.unread').forEach(function(el) {
                    el.classList.remove('unread');
                });
                updateBadge(0);
            },
            error: function() {}
        });
    }

    window.refreshNotificationBadge = fetchCount;
    window.refreshNotificationList = fetchLatest;

    document.addEventListener('click', function(e) {
        if (!e.target || !e.target.closest) return;

        if (e.target.closest('[data-lpx-ctx-toggle="notification-bar"]')) {
            fetchLatest();
            return;
        }

        if (e.target.closest('.notification-mark-all')) {
            markAllRead();
            return;
        }

        var itemEl = e.target.closest('.lpx-notification-item');
        if (itemEl && itemEl.classList.contains('unread') && itemEl.getAttribute('data-id')) {
            itemEl.classList.remove('unread');
            markOneRead(itemEl.getAttribute('data-id'));
        }
    });

    function tryInit() {
        if (typeof abp === 'undefined' || !abp || !abp.ajax) return false;
        fetchCount();
        setInterval(fetchCount, 30000);
        return true;
    }

    if (!tryInit()) {
        var attempts = 0;
        var readyTimer = setInterval(function() {
            attempts++;
            if (tryInit()) {
                clearInterval(readyTimer);
            } else if (attempts > 40) {
                clearInterval(readyTimer);
            }
        }, 500);
    }
})();
</script>

Keep data-lpx-ctx-toggle="notification-bar" / data-lpx-context-menu="notification-bar" as they are — notification-bar is the context menu name LeptonX binds automatically inside the toolbar, so the dropdown toggling in the toolbar keeps working. Note this covers the desktop toolbar; the copy inside the mobile menu is kept in sync data-wise, but the dropdown there is not wired up by the theme.

One more detail for the footer: the theme's lpx-button-container styles a single full-width button (that's also why your notif-btn-outline / notif-btn-primary classes have no effect — they don't exist in LeptonX), so your two buttons overlap. Add this to your styles to stack them properly:

.lpx-context-menu .lpx-button-container .lpx-button {
    display: block;
    text-align: center;
    text-decoration: none;
    box-sizing: border-box;
}

.lpx-context-menu .lpx-button-container .lpx-button + .lpx-button {
    margin-top: 8px;
}

Thanks

Hi,

This is a limitation of how Suite processes the Create/Edit modal templates. Besides placeholder replacement, it runs two extra steps during generation that assume the default template structure, and a customized layout can lose content to them:

  1. When the entity's "Preserve custom code" option is not enabled, Suite removes each @*//<suite-custom-code-block-N>*@ ... @*//</suite-custom-code-block-N>*@ pair together with everything between the markers. In the default template these blocks are empty, so you never notice. If your footer and closing tags are inside a marker pair, they are removed on every generate — that's exactly the truncation you're seeing.

  2. When the entity has no navigation connections, Suite removes every line that starts with <abp-tab or </abp-tab (leading whitespace is ignored, so indentation doesn't matter). This is line-based, so a multi-line <abp-tab ...> opening tag leaves an orphan line with only the attributes (the corrupted markup near the top), and anything that follows </abp-tab> on the same line is removed together with it.

Suite doesn't validate the generated markup at the moment, so a missing closing form tag doesn't fail the generate step — the file is written as-is.

So for a custom off-canvas/panel layout:

  • Keep static markup (footer actions, closing panel tags, the closing form tag) outside the suite-custom-code-block markers. Content inside the markers is only preserved when "Preserve custom code" is enabled for the entity.
  • Use each suite-custom-code-block-N index only once per file.
  • Put <abp-tabs>, <abp-tab ...>, </abp-tab> and </abp-tabs> each on their own line, keep the opening tag on a single line, and don't put other markup on those lines.
  • Keep the ##...## line in the files under .suite/customized-suite-templates — Suite strips the first line of those files when loading, so if you delete the hash header, the first real template line is lost instead.

After updating the two templates this way, save them and regenerate — the off-canvas layout keeps everything after the property fields, including the footer and closing tags.

If it still truncates after these changes, share your customized CreateModal/EditModal .cshtml.txt files and we can point out the exact line that triggers it.

Thanks

Hi,

Yes — it's already fixed and merged, and it'll ship in the next patch release. Thanks again for the detailed report.

Thanks

Hi,

Thanks, that confirms it. In SCMQuartzBackgroundJobManager the Quartz JobKey is built from BackgroundJobNameAttribute.GetName<TArgs>() (which falls back to the args type full name when it has no [BackgroundJobName]), plus _{TenantId} for IMultiTenant args and _{UniqueKey} for IUniqueBackgroundJob args. That works for your own jobs, but the module's IndexDocumentJobArgs implements neither interface, it's just a class with a DataSourceId.

So every reindex job ends up with the same key, Volo.AIManagement.BackgroundJobs.IndexDocumentJobArgs. ReIndex All enqueues one job per document, but while the first job and its trigger still exist in Quartz, your CheckExists branch hits "a job already exists and is not in error, let's not schedule a new job" and returns without scheduling. Nothing throws, so ReIndexAllAsync finishes normally and resets every document (which the UI then shows as Processing), but only the first one actually has a job.

The fix is to give jobs that don't opt into your identity scheme a unique key per enqueue, like stock ABP does. Where you add it matters, it has to be the fallback of the IUniqueBackgroundJob check, not the tenant else, otherwise a non-tenant IUniqueBackgroundJob would get a GUID and its UniqueKey and lose its stable identity:

if (args is IUniqueBackgroundJob uniqueBackgroundJob && !string.IsNullOrEmpty(uniqueBackgroundJob.UniqueKey))
{
    jobName = $"{jobName}_{uniqueBackgroundJob.UniqueKey}";
}
else
{
    jobName = $"{jobName}_{Guid.NewGuid():N}";
}

This makes anything that isn't an IUniqueBackgroundJob run on its own, like ABP's default manager. If some of your own jobs relied on the CheckExists dedup to avoid running twice, mark those with IUniqueBackgroundJob (with a stable UniqueKey) so they keep that behaviour. These jobs aren't durable, so the GUID keys don't accumulate, Quartz removes each one once it finishes and its trigger is gone.

A few practical notes:

  • Documents already stuck in Processing won't recover on their own, run ReIndex All again after deploying.
  • With the fix, every document's job is enqueued to start right away, so how many run in parallel comes down to the Quartz worker threads available across your cluster. Worth a quick check against your embedding provider's rate limits for workspaces with a lot of documents.
  • In 10.5 the indexing job doesn't take a per-document lock, so avoid re-triggering ReIndex on a document that's still indexing, two overlapping runs on the same data source can delete each other's chunks/vectors. That's existing 10.5 behaviour, not something this change introduces.

Unrelated, but I noticed it in the config you sent: quartz.scheduler.instanceId is set to a fixed value. If you run more than one node against this clustered store, each node needs a distinct instanceId (usually AUTO), a shared one interferes with Quartz's cluster and failover handling.

Thanks

Hi,

The DeleteTrigger and Deleting job lines are just Quartz's normal cleanup after a one-shot job finishes, so they don't mean the other documents' jobs were deleted.

The real signal is the JobKey:

DEFAULT.Volo.AIManagement.BackgroundJobs.IndexDocumentJobArgs

That key is the job arguments type name. ABP's stock QuartzBackgroundJobManager never sets a job identity, so Quartz gives every enqueue a fresh GUID key and each document gets its own job. Your log uses the args type name as a fixed key instead, and with one shared key only a single IndexDocumentJob can live in the scheduler at a time, so ReIndex All (which enqueues one job per document) only runs the first.

That fixed key isn't ABP's default, so something in your app is setting it. To pin down exactly where and give you the precise fix, could you share either:

  1. The class that implements or replaces IBackgroundJobManager, plus your AddQuartz / background-job registration code, or
  2. A small project that reproduces it (a fresh app with your Quartz configuration and a couple of documents is enough). A private GitHub repo with https://github.com/maliming invited, or a zip to liming.ma@volosoft.com, both work.

Once I can see where the key is set, the fix is to let each enqueue get its own job (a unique key per enqueue), with a per-document lock or dedup check if the fixed key was added to avoid indexing the same document twice.

Thanks

Hi,

Thanks for the details. Since everything is already on 10.4.1, my earlier upgrade suggestion was wrong — sorry about that. This is a bug in ABP Suite's Angular proxy generation: it names the navigation-property lookups differently from your backend controllers, so the generated URLs don't match and some lookups get dropped.

For now, generate the proxy from the running backend instead of Suite's built-in generation:

  1. Start your HTTP API host.
  2. From the angular folder, run (replace the URL with your host's URL):
abp generate-proxy -t ng -u https://localhost:44300 -m app

This reads your actual API, so employee.service.ts matches your controllers exactly and all the lookups are there. The method names follow the backend (getGradeLookup instead of getEmployeeGradeLookup), so update those call sites once. If you regenerate the entity in Suite later, run this command again afterward.

We'll fix this in Suite's Angular generation. Your ticket has been refunded.

Thanks

Hi,

The differences in employee.service.ts come from how ABP names the navigation-property "lookup" endpoints, and that convention changed in ABP 10.3:

  • Before 10.3: the lookup is named after the related entity (Grade → GetEmployeeGradeLookup / employee-grade-lookup), and multiple relations pointing to the same entity are merged into one — that's why your self-reference GetEmployeeLookup was dropped.
  • 10.3+: the lookup is named after the foreign-key property (GradeId → GetGradeLookup / grade-lookup), and each relation, including each self-reference, keeps its own lookup.

Your committed file has the FK-style routes (grade-lookup), but the regenerated one came out in the old entity-name style (employee-grade-lookup) with the self-reference gone. That's the pre-10.3 convention, so one of the tools you regenerate with is older than your project's ABP version (10.4).

To pin down which one, could you share a few details:

  1. Your ABP Suite version (shown on the Suite start page), and abp --version.
  2. How the proxy gets regenerated — does it happen automatically when you create/save an entity in ABP Suite, or do you run abp generate-proxy (or an @abp/ng.schematics command) yourself?
  3. The @abp/ng.core and @abp/ng.schematics versions from your Angular package.json.
  4. If it's handy, the navigationProperties section of .suite/entities/Employee.json — that shows exactly what the generator will produce for these lookups.

Once we see which piece is behind, the fix is to bring it up to your project's version (for example abp suite update for Suite, or aligning the @abp/ng.* packages) and regenerate. The lookup routes then come back as grade-lookup, the self-reference lookups aren't dropped, and the generated code stays consistent. One thing to expect: the method names line up with the routes too (getEmployeeGradeLookup → getGradeLookup), so you'll update those call sites in your Angular code once.

On the "no selective generation" point — the proxy is regenerated as a whole, not per method, so regenerating always rewrites the file. Keeping the proxy folder in source control and reviewing the diff after each generation is the practical way to manage that.

Thanks

Hi,

The conflict isn't on the refresh token itself. When several tabs renew at the same time - the silent /connect/authorize calls in your screenshot - each one creates a new token under the same authorization. ABP currently treats that foreign key as a navigation change and updates the shared authorization's ConcurrencyStamp on every one of those token inserts.

The first request updates the authorization and wins; the concurrent ones still hold the previous stamp, so their update affects 0 rows and EF Core throws the DbConcurrencyException you're seeing.

The framework already excludes OpenIddictApplication from this, but not OpenIddictAuthorization. You can add it in your *EntityFrameworkCoreModule (the .EntityFrameworkCore project):

using Volo.Abp.Domain.Entities.Events;
using Volo.Abp.OpenIddict.Authorizations;

// inside ConfigureServices
Configure<AbpEntityChangeOptions>(options =>
{
    options.IgnoredNavigationEntitySelectors.Add(
        "DisableOpenIddictAuthorization",
        type => type == typeof(OpenIddictAuthorization));
});

After this, creating a token no longer touches the shared authorization, so concurrent renewals from multiple tabs stop colliding. Nothing needs to change on the Angular side.

We'll also add OpenIddictAuthorization to the framework defaults.

Thanks

Showing 241 to 250 of 12912 entries
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.8.0-preview. Updated on November 02, 2026, 06:56
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.