Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
b562049
feat: add store settings management for store owners
Copilot May 17, 2026
f57c54d
fix: move admin sitemap migration from version 2.5 to 2.4
Copilot May 17, 2026
1d7b448
Potential fix for pull request finding 'Generic catch clause'
KrzysztofPajak May 17, 2026
d8a2037
feat: make merchandise return reasons and actions per-store
Copilot May 18, 2026
7cd454c
Add PermissionNames to Vendor settings menu item
KrzysztofPajak May 19, 2026
8ad8344
Merge branch 'copilot/allow-store-owner-settings' of https://github.c…
KrzysztofPajak May 19, 2026
a36b145
feat: add Stores selector to admin views and access control to store …
Copilot May 19, 2026
fed6a1c
feat: filter admin merchandise return reason/action lists by selected…
Copilot May 19, 2026
a3aafbe
fix: admin store pre-selection and store owner all-stores access cont…
Copilot May 19, 2026
e47017b
fix: enforce store assignment server-side in Store area create POST a…
Copilot May 19, 2026
47ad1a2
fix: preserve Stores/LimitedToStores on edit POST in Store area for r…
Copilot May 19, 2026
38966b9
fix: clear cache after DeleteMerchandiseReturnAction
Copilot May 19, 2026
418eabf
fix: update mapping test snapshots for LimitedToStores field on Merch…
Copilot May 22, 2026
c5e82a0
Fix Sales settings invalid ModelState flow
Copilot May 22, 2026
cf28a1a
fix: add Vendor settings to migration adminOnlyItems to match Standar…
Copilot May 22, 2026
0019b67
Fix store filtering at IQueryable level before ToList in MerchandiseR…
Copilot May 22, 2026
98785e0
Fix CS0266: explicitly type query as IQueryable to allow Where reassi…
Copilot May 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -117,15 +117,20 @@ public virtual async Task<IPagedList<MerchandiseReturn>> SearchMerchandiseReturn
/// <summary>
/// Gets all merchandise return actions
/// </summary>
/// <param name="storeId">Store identifier; empty to load all entries</param>
/// <returns>Merchandise return actions</returns>
public virtual async Task<IList<MerchandiseReturnAction>> GetAllMerchandiseReturnActions()
public virtual async Task<IList<MerchandiseReturnAction>> GetAllMerchandiseReturnActions(string storeId = "")
{
return await _cacheBase.GetAsync(CacheKey.MERCHANDISE_RETURN_ACTIONS_ALL_KEY, async () =>
var key = string.Format(CacheKey.MERCHANDISE_RETURN_ACTIONS_ALL_KEY, storeId);
return await _cacheBase.GetAsync(key, async () =>
{
var query = from rra in _merchandiseReturnActionRepository.Table
IQueryable<MerchandiseReturnAction> query = from rra in _merchandiseReturnActionRepository.Table
orderby rra.DisplayOrder
select rra;
return await Task.FromResult(query.ToList());
if (!string.IsNullOrEmpty(storeId))
query = query.Where(x => !x.LimitedToStores || x.Stores.Contains(storeId));
var actions = query.ToList();
return await Task.FromResult(actions);
});
}

Expand Down Expand Up @@ -203,7 +208,7 @@ public virtual async Task InsertMerchandiseReturnAction(MerchandiseReturnAction
await _mediator.EntityInserted(merchandiseReturnAction);

//clear cache
await _cacheBase.RemoveByPrefix(CacheKey.MERCHANDISE_RETURN_ACTIONS_ALL_KEY);
await _cacheBase.RemoveByPrefix(CacheKey.MERCHANDISE_RETURN_ACTIONS_PATTERN_KEY);
}

/// <summary>
Expand All @@ -220,7 +225,7 @@ public virtual async Task UpdateMerchandiseReturnAction(MerchandiseReturnAction
await _mediator.EntityUpdated(merchandiseReturnAction);

//clear cache
await _cacheBase.RemoveByPrefix(CacheKey.MERCHANDISE_RETURN_ACTIONS_ALL_KEY);
await _cacheBase.RemoveByPrefix(CacheKey.MERCHANDISE_RETURN_ACTIONS_PATTERN_KEY);
}

/// <summary>
Expand All @@ -235,6 +240,9 @@ public virtual async Task DeleteMerchandiseReturnAction(MerchandiseReturnAction

//event notification
await _mediator.EntityDeleted(merchandiseReturnAction);

//clear cache
await _cacheBase.RemoveByPrefix(CacheKey.MERCHANDISE_RETURN_ACTIONS_PATTERN_KEY);
}

/// <summary>
Expand All @@ -251,21 +259,26 @@ public virtual async Task DeleteMerchandiseReturnReason(MerchandiseReturnReason
await _mediator.EntityDeleted(merchandiseReturnReason);

//clear cache
await _cacheBase.RemoveByPrefix(CacheKey.MERCHANDISE_RETURN_REASONS_ALL_KEY);
await _cacheBase.RemoveByPrefix(CacheKey.MERCHANDISE_RETURN_REASONS_PATTERN_KEY);
}

/// <summary>
/// Gets all merchandise return reasons
/// </summary>
/// <param name="storeId">Store identifier; empty to load all entries</param>
/// <returns>Merchandise return reasons</returns>
public virtual async Task<IList<MerchandiseReturnReason>> GetAllMerchandiseReturnReasons()
public virtual async Task<IList<MerchandiseReturnReason>> GetAllMerchandiseReturnReasons(string storeId = "")
{
return await _cacheBase.GetAsync(CacheKey.MERCHANDISE_RETURN_REASONS_ALL_KEY, async () =>
var key = string.Format(CacheKey.MERCHANDISE_RETURN_REASONS_ALL_KEY, storeId);
return await _cacheBase.GetAsync(key, async () =>
{
var query = from rra in _merchandiseReturnReasonRepository.Table
IQueryable<MerchandiseReturnReason> query = from rra in _merchandiseReturnReasonRepository.Table
orderby rra.DisplayOrder
select rra;
return await Task.FromResult(query.ToList());
if (!string.IsNullOrEmpty(storeId))
query = query.Where(x => !x.LimitedToStores || x.Stores.Contains(storeId));
var reasons = query.ToList();
return await Task.FromResult(reasons);
});
}

Expand Down Expand Up @@ -293,7 +306,7 @@ public virtual async Task InsertMerchandiseReturnReason(MerchandiseReturnReason
await _mediator.EntityInserted(merchandiseReturnReason);

//clear cache
await _cacheBase.RemoveByPrefix(CacheKey.MERCHANDISE_RETURN_REASONS_ALL_KEY);
await _cacheBase.RemoveByPrefix(CacheKey.MERCHANDISE_RETURN_REASONS_PATTERN_KEY);
}

/// <summary>
Expand All @@ -310,7 +323,7 @@ public virtual async Task UpdateMerchandiseReturnReason(MerchandiseReturnReason
await _mediator.EntityUpdated(merchandiseReturnReason);

//clear cache
await _cacheBase.RemoveByPrefix(CacheKey.MERCHANDISE_RETURN_REASONS_ALL_KEY);
await _cacheBase.RemoveByPrefix(CacheKey.MERCHANDISE_RETURN_REASONS_PATTERN_KEY);
}

#region Merchandise return notes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ Task<IPagedList<MerchandiseReturn>> SearchMerchandiseReturns(string storeId = ""
/// <summary>
/// Gets all merchandise return actions
/// </summary>
/// <param name="storeId">Store identifier; empty to load all entries</param>
/// <returns>Merchandise return actions</returns>
Task<IList<MerchandiseReturnAction>> GetAllMerchandiseReturnActions();
Task<IList<MerchandiseReturnAction>> GetAllMerchandiseReturnActions(string storeId = "");

/// <summary>
/// Gets a merchandise return action
Expand Down Expand Up @@ -93,8 +94,9 @@ Task<IPagedList<MerchandiseReturn>> SearchMerchandiseReturns(string storeId = ""
/// <summary>
/// Gets all merchandise return reasons
/// </summary>
/// <param name="storeId">Store identifier; empty to load all entries</param>
/// <returns>Merchandise return reasons</returns>
Task<IList<MerchandiseReturnReason>> GetAllMerchandiseReturnReasons();
Task<IList<MerchandiseReturnReason>> GetAllMerchandiseReturnReasons(string storeId = "");

/// <summary>
/// Gets a merchandise return reasons
Expand Down
13 changes: 12 additions & 1 deletion src/Core/Grand.Domain/Orders/MerchandiseReturnAction.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using Grand.Domain.Localization;
using Grand.Domain.Stores;

namespace Grand.Domain.Orders;

/// <summary>
/// Represents a merchandise return action
/// </summary>
public class MerchandiseReturnAction : BaseEntity, ITranslationEntity
public class MerchandiseReturnAction : BaseEntity, ITranslationEntity, IStoreLinkEntity
{
/// <summary>
/// Gets or sets the name
Expand All @@ -21,4 +22,14 @@ public class MerchandiseReturnAction : BaseEntity, ITranslationEntity
/// Gets or sets the collection of locales
/// </summary>
public IList<TranslationEntity> Locales { get; set; } = new List<TranslationEntity>();

/// <summary>
/// Gets or sets a value indicating whether the entity is limited/restricted to certain stores
/// </summary>
public bool LimitedToStores { get; set; }

/// <summary>
/// Gets or sets the stores
/// </summary>
public IList<string> Stores { get; set; } = new List<string>();
}
13 changes: 12 additions & 1 deletion src/Core/Grand.Domain/Orders/MerchandiseReturnReason.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using Grand.Domain.Localization;
using Grand.Domain.Stores;

namespace Grand.Domain.Orders;

/// <summary>
/// Represents a merchandise return reason
/// </summary>
public class MerchandiseReturnReason : BaseEntity, ITranslationEntity
public class MerchandiseReturnReason : BaseEntity, ITranslationEntity, IStoreLinkEntity
{
/// <summary>
/// Gets or sets the name
Expand All @@ -21,4 +22,14 @@ public class MerchandiseReturnReason : BaseEntity, ITranslationEntity
/// Gets or sets the collection of locales
/// </summary>
public IList<TranslationEntity> Locales { get; set; } = new List<TranslationEntity>();

/// <summary>
/// Gets or sets a value indicating whether the entity is limited/restricted to certain stores
/// </summary>
public bool LimitedToStores { get; set; }

/// <summary>
/// Gets or sets the stores
/// </summary>
public IList<string> Stores { get; set; } = new List<string>();
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,22 @@
public static partial class CacheKey
{
/// <summary>
/// Key for all merchandise return reason
/// Key for all merchandise return reasons. {0} - store ID
/// </summary>
public static string MERCHANDISE_RETURN_REASONS_ALL_KEY => "Grand.merchandisereturn.reasons.all";
public static string MERCHANDISE_RETURN_REASONS_ALL_KEY => "Grand.merchandisereturn.reasons.all-{0}";

/// <summary>
/// Key for all merchandise return actions
/// Key pattern to clear merchandise return reasons cache
/// </summary>
public static string MERCHANDISE_RETURN_ACTIONS_ALL_KEY => "Grand.merchandisereturn.actions.all";
public static string MERCHANDISE_RETURN_REASONS_PATTERN_KEY => "Grand.merchandisereturn.reasons";

/// <summary>
/// Key for all merchandise return actions. {0} - store ID
/// </summary>
public static string MERCHANDISE_RETURN_ACTIONS_ALL_KEY => "Grand.merchandisereturn.actions.all-{0}";

/// <summary>
/// Key pattern to clear merchandise return actions cache
/// </summary>
public static string MERCHANDISE_RETURN_ACTIONS_PATTERN_KEY => "Grand.merchandisereturn.actions";
}
Original file line number Diff line number Diff line change
Expand Up @@ -967,6 +967,7 @@ public static class StandardAdminSiteMap
new() {
SystemName = "Vendor settings",
ResourceName = "Admin.Settings.Vendor",
PermissionNames = new List<string> { PermissionSystemName.System },
ControllerName = "Setting",
ActionName = "Vendor",
DisplayOrder = 6,
Comment thread
KrzysztofPajak marked this conversation as resolved.
Expand All @@ -975,6 +976,7 @@ public static class StandardAdminSiteMap
new() {
SystemName = "Push notifications settings",
ResourceName = "Admin.Settings.PushNotifications",
PermissionNames = new List<string> { PermissionSystemName.System },
ControllerName = "Setting",
ActionName = "PushNotifications",
DisplayOrder = 7,
Expand All @@ -983,6 +985,7 @@ public static class StandardAdminSiteMap
new() {
SystemName = "Admin search settings",
ResourceName = "Admin.Settings.AdminSearch",
PermissionNames = new List<string> { PermissionSystemName.System },
ControllerName = "Setting",
ActionName = "AdminSearch",
DisplayOrder = 8,
Expand All @@ -991,6 +994,7 @@ public static class StandardAdminSiteMap
new() {
SystemName = "System settings",
ResourceName = "Admin.Settings.System",
PermissionNames = new List<string> { PermissionSystemName.System },
ControllerName = "Setting",
ActionName = "SystemSetting",
DisplayOrder = 9,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using Grand.Data;
using Grand.Domain.Admin;
using Grand.Domain.Permissions;
using Grand.Infrastructure.Migrations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace Grand.Module.Migration.Migrations._2._4;

public class MigrationUpdateAdminSiteMap : IMigration
{
public int Priority => 1;
public DbVersion Version => new(2, 4);
public Guid Identity => new("B7C3D8E2-F1A4-4B9E-8C62-197E3F5A4D21");
public string Name => "Update standard admin site map - restrict Vendor, Push notifications, Admin search, System settings to System permission";

/// <summary>
/// Upgrade process
/// </summary>
/// <param name="serviceProvider"></param>
/// <returns></returns>
public bool UpgradeProcess(IServiceProvider serviceProvider)
{
var repository = serviceProvider.GetRequiredService<IRepository<AdminSiteMap>>();
var logService = serviceProvider.GetRequiredService<ILogger<MigrationUpdateAdminSiteMap>>();

try
{
var sitemapSettings = repository.Table.FirstOrDefault(x => x.SystemName == "Settings");
if (sitemapSettings == null) return true;

var adminOnlyItems = new[] {
"Vendor settings",
"Push notifications settings",
"Admin search settings",
"System settings"
};

foreach (var itemName in adminOnlyItems)
{
var item = sitemapSettings.ChildNodes.FirstOrDefault(x => x.SystemName == itemName);
if (item != null && !item.PermissionNames.Contains(PermissionSystemName.System))
item.PermissionNames.Add(PermissionSystemName.System);
}

repository.Update(sitemapSettings);
}
catch (InvalidOperationException ex)
{
logService.LogError(ex, "UpgradeProcess - UpdateAdminSiteMap (2.4)");
}

return true;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
Name: Refund,
DisplayOrder: 1,
LimitedToStores: false,
Id: ObjectId_1
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
Name: Defective,
DisplayOrder: 1,
LimitedToStores: false,
Id: ObjectId_1
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,12 @@
<span asp-validation-for="DisplayOrder"></span>
</div>
</div>
<div class="form-group">
<admin-label asp-for="Stores"/>
<div class="col-md-9 col-sm-9">
<admin-input asp-for="Stores"/>
<span asp-validation-for="Stores"></span>
</div>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,12 @@
<span asp-validation-for="DisplayOrder"></span>
</div>
</div>
<div class="form-group">
<admin-label asp-for="Stores"/>
<div class="col-md-9 col-sm-9">
<admin-input asp-for="Stores"/>
<span asp-validation-for="Stores"></span>
</div>
</div>
</div>
</div>
16 changes: 12 additions & 4 deletions src/Web/Grand.Web.Admin/Controllers/SettingController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,8 @@ public async Task<IActionResult> MerchandiseReturnReasonList()
[HttpPost]
public async Task<IActionResult> MerchandiseReturnReasonList(DataSourceRequest command)
{
var reasons = await merchandiseReturnService.GetAllMerchandiseReturnReasons();
var storeId = await GetActiveStore();
var reasons = await merchandiseReturnService.GetAllMerchandiseReturnReasons(storeId);
var gridModel = new DataSourceResult {
Data = reasons.Select(x => x.ToModel()),
Total = reasons.Count
Expand All @@ -317,7 +318,10 @@ public async Task<IActionResult> MerchandiseReturnReasonList(DataSourceRequest c
//create
public async Task<IActionResult> MerchandiseReturnReasonCreate()
{
var model = new MerchandiseReturnReasonModel();
var storeId = await GetActiveStore();
var model = new MerchandiseReturnReasonModel {
Stores = !string.IsNullOrEmpty(storeId) ? [storeId] : []
};
//locales
await AddLocales(languageService, model.Locales);
return View(model);
Expand Down Expand Up @@ -417,7 +421,8 @@ public async Task<IActionResult> MerchandiseReturnActionList()
[HttpPost]
public async Task<IActionResult> MerchandiseReturnActionList(DataSourceRequest command)
{
var actions = await merchandiseReturnService.GetAllMerchandiseReturnActions();
var storeId = await GetActiveStore();
var actions = await merchandiseReturnService.GetAllMerchandiseReturnActions(storeId);
var gridModel = new DataSourceResult {
Data = actions.Select(x => x.ToModel()),
Total = actions.Count
Expand All @@ -428,7 +433,10 @@ public async Task<IActionResult> MerchandiseReturnActionList(DataSourceRequest c
//create
public async Task<IActionResult> MerchandiseReturnActionCreate()
{
var model = new MerchandiseReturnActionModel();
var storeId = await GetActiveStore();
var model = new MerchandiseReturnActionModel {
Stores = !string.IsNullOrEmpty(storeId) ? [storeId] : []
};
//locales
await AddLocales(languageService, model.Locales);
return View(model);
Expand Down
Loading
Loading