SubForms kısmında ListFormCode dahil edildi
This commit is contained in:
Sedat Öztürk 2026-06-05 22:05:57 +03:00
parent 119c3650f0
commit 1c472a7d9a
7 changed files with 233 additions and 65 deletions

View file

@ -0,0 +1,9 @@
using Volo.Abp.Application.Dtos;
namespace Sozsoft.Platform.AuditLogs;
public class AuditLogListRequestDto : PagedAndSortedResultRequestDto
{
public string ListFormCode { get; set; }
public string EntityId { get; set; }
}

View file

@ -1,30 +1,43 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions;
using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.ListForms;
using Volo.Abp; using Volo.Abp;
using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services; using Volo.Abp.Application.Services;
using Volo.Abp.AuditLogging; using Volo.Abp.AuditLogging;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Uow; using Volo.Abp.Uow;
using static Sozsoft.Platform.Data.Seeds.SeedConsts; using static Sozsoft.Platform.Data.Seeds.SeedConsts;
namespace Sozsoft.Platform.AuditLogs; namespace Sozsoft.Platform.AuditLogs;
public interface IAuditLogAppService public interface IAuditLogAppService
: ICrudAppService<AuditLogDto, Guid> : ICrudAppService<AuditLogDto, Guid, AuditLogListRequestDto>
{ {
} }
[Authorize(AppCodes.IdentityManagement.AuditLogs)] [Authorize(AppCodes.IdentityManagement.AuditLogs)]
public class AuditLogAppService public class AuditLogAppService : CrudAppService<
: CrudAppService<AuditLog, AuditLogDto, Guid> AuditLog,
, IAuditLogAppService AuditLogDto,
Guid,
AuditLogListRequestDto>, IAuditLogAppService
{ {
public AuditLogAppService(IAuditLogRepository auditLogRepository) : base(auditLogRepository) private readonly IRepository<ListForm, Guid> _listFormRepository;
public AuditLogAppService(
IAuditLogRepository auditLogRepository,
IRepository<ListForm, Guid> listFormRepository
) : base(auditLogRepository)
{ {
_listFormRepository = listFormRepository;
} }
public override async Task<AuditLogDto> GetAsync(Guid id) public override async Task<AuditLogDto> GetAsync(Guid id)
@ -35,23 +48,26 @@ public class AuditLogAppService
} }
[UnitOfWork] [UnitOfWork]
public override async Task<PagedResultDto<AuditLogDto>> GetListAsync(PagedAndSortedResultRequestDto input) public override async Task<PagedResultDto<AuditLogDto>> GetListAsync(AuditLogListRequestDto input)
{ {
var query = await CreateFilteredQueryAsync(input); var query = await Repository.WithDetailsAsync();
if (!input.ListFormCode.IsNullOrWhiteSpace())
{
var filterRules = await GetListFormFilterRulesAsync(input.ListFormCode);
query = ApplyAuditLogActionParametersFilter(query, filterRules, input.EntityId);
}
else if (!input.EntityId.IsNullOrWhiteSpace())
{
query = query.Where(a => a.Actions.Any(action => action.Parameters.Contains(input.EntityId)));
}
var totalCount = await AsyncExecuter.CountAsync(query); var totalCount = await AsyncExecuter.CountAsync(query);
query = ApplySorting(query, input); query = ApplySorting(query, input);
query = ApplyPaging(query, input); query = ApplyPaging(query, input);
// EntityChanges ile birlikte getir (N+1 query önlenir) var auditLogsWithDetails = await AsyncExecuter.ToListAsync(query);
var auditLogRepository = (IAuditLogRepository)Repository;
var auditLogsWithDetails = await auditLogRepository.GetListAsync(
sorting: input.Sorting,
maxResultCount: input.MaxResultCount,
skipCount: input.SkipCount,
includeDetails: true
);
// Mapping tek seferde yap // Mapping tek seferde yap
var entityDtos = ObjectMapper.Map<List<AuditLog>, List<AuditLogDto>>(auditLogsWithDetails); var entityDtos = ObjectMapper.Map<List<AuditLog>, List<AuditLogDto>>(auditLogsWithDetails);
@ -69,6 +85,102 @@ public class AuditLogAppService
); );
} }
private async Task<List<AuditLogListFormFilterRule>> GetListFormFilterRulesAsync(string listFormCode)
{
var rules = new List<AuditLogListFormFilterRule>
{
new(listFormCode, null)
};
var listForm = await _listFormRepository.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode);
if (listForm?.SubFormsJson.IsNullOrWhiteSpace() != false)
{
return rules;
}
try
{
var subForms = JsonSerializer.Deserialize<List<SubFormDto>>(listForm.SubFormsJson) ?? [];
foreach (var subForm in subForms.Where(a => !a.Code.IsNullOrWhiteSpace()))
{
var childFieldNames = subForm.Relation?
.Select(a => a.ChildFieldName)
.Where(a => !a.IsNullOrWhiteSpace())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList() ?? [];
rules.AddRange(childFieldNames.Select(childFieldName => new AuditLogListFormFilterRule(subForm.Code, childFieldName)));
}
}
catch (JsonException)
{
// Invalid subform JSON should not block audit log listing for the main form.
}
return rules
.DistinctBy(a => $"{a.ListFormCode}|{a.ChildFieldName}".ToLowerInvariant())
.ToList();
}
private static IQueryable<AuditLog> ApplyAuditLogActionParametersFilter(
IQueryable<AuditLog> query,
List<AuditLogListFormFilterRule> rules,
string entityId)
{
var validRules = rules
.Where(rule => !rule.ListFormCode.IsNullOrWhiteSpace())
.ToList();
if (validRules.Count == 0)
{
return query;
}
var auditLog = Expression.Parameter(typeof(AuditLog), "auditLog");
var action = Expression.Parameter(typeof(AuditLogAction), "action");
var parameters = Expression.Property(action, nameof(AuditLogAction.Parameters));
Expression actionBody = Expression.Constant(false);
foreach (var rule in validRules)
{
Expression ruleBody = Contains(parameters, rule.ListFormCode);
if (!entityId.IsNullOrWhiteSpace())
{
ruleBody = Expression.AndAlso(ruleBody, Contains(parameters, entityId));
}
if (!rule.ChildFieldName.IsNullOrWhiteSpace())
{
ruleBody = Expression.AndAlso(ruleBody, Contains(parameters, rule.ChildFieldName));
}
actionBody = Expression.OrElse(actionBody, ruleBody);
}
var actions = Expression.Property(auditLog, nameof(AuditLog.Actions));
var actionPredicate = Expression.Lambda<Func<AuditLogAction, bool>>(actionBody, action);
var anyCall = Expression.Call(
typeof(Enumerable),
nameof(Enumerable.Any),
[typeof(AuditLogAction)],
actions,
actionPredicate);
var auditLogPredicate = Expression.Lambda<Func<AuditLog, bool>>(anyCall, auditLog);
return query.Where(auditLogPredicate);
}
private static MethodCallExpression Contains(MemberExpression source, string value)
{
return Expression.Call(
source,
nameof(string.Contains),
Type.EmptyTypes,
Expression.Constant(value));
}
private sealed record AuditLogListFormFilterRule(string ListFormCode, string? ChildFieldName);
// Audit Log kayitlarini gormek istiyoruz fakat degistirmek istemiyoruz // Audit Log kayitlarini gormek istiyoruz fakat degistirmek istemiyoruz
[RemoteService(IsEnabled = false)] [RemoteService(IsEnabled = false)]
public override Task<AuditLogDto> CreateAsync(AuditLogDto input) public override Task<AuditLogDto> CreateAsync(AuditLogDto input)

View file

@ -2302,7 +2302,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"), InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"),
PagerOptionJson = DefaultPagerOptionJson, PagerOptionJson = DefaultPagerOptionJson,
EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 300, true, true, true, false, false), EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 550, true, true, true, false, false),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>() { EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>() {
new() { new() {
Order=1, ColCount=1, ColSpan=1, ItemType="group", Items=[ Order=1, ColCount=1, ColSpan=1, ItemType="group", Items=[

View file

@ -0,0 +1,24 @@
import { PagedResultDto } from '@/proxy'
import { AuditLogDto } from '@/proxy/auditLog/audit-log'
import apiService from '@/services/api.service'
export interface AuditLogListRequestDto {
skipCount?: number
maxResultCount?: number
sorting?: string
listFormCode?: string
entityId?: string
}
class AuditLogService {
async getList(params?: AuditLogListRequestDto): Promise<PagedResultDto<AuditLogDto>> {
const response = await apiService.fetchData<PagedResultDto<AuditLogDto>>({
url: '/api/app/audit-log',
method: 'GET',
params,
})
return response.data
}
}
export const auditLogService = new AuditLogService()

View file

@ -17,16 +17,17 @@ import TabNav from '@/components/ui/Tabs/TabNav'
import { NoteDto } from '@/proxy/note/models' import { NoteDto } from '@/proxy/note/models'
import { AVATAR_URL } from '@/constants/app.constant' import { AVATAR_URL } from '@/constants/app.constant'
import { useStoreState } from '@/store/store' import { useStoreState } from '@/store/store'
import apiService from '@/services/api.service'
import { PagedResultDto } from '@/proxy'
import { AuditLogActionDto, AuditLogDto } from '@/proxy/auditLog/audit-log' import { AuditLogActionDto, AuditLogDto } from '@/proxy/auditLog/audit-log'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { auditLogService } from '@/services/auditLog.service'
interface NoteListProps { interface NoteListProps {
notes: NoteDto[] notes: NoteDto[]
entityName: string entityName: string
entityId: string entityId: string
isVisible?: boolean
onAddNote?: () => void onAddNote?: () => void
onRefreshNotes?: () => void
onDeleteNote?: (noteId: string) => void onDeleteNote?: (noteId: string) => void
onDownloadFile?: (fileData: any) => void onDownloadFile?: (fileData: any) => void
} }
@ -35,7 +36,9 @@ export const NoteList: React.FC<NoteListProps> = ({
notes, notes,
entityName, entityName,
entityId, entityId,
isVisible = true,
onAddNote, onAddNote,
onRefreshNotes,
onDeleteNote, onDeleteNote,
onDownloadFile, onDownloadFile,
}) => { }) => {
@ -215,7 +218,18 @@ export const NoteList: React.FC<NoteListProps> = ({
const getRowLabelIfMatches = (input: any): string | null => { const getRowLabelIfMatches = (input: any): string | null => {
if (!entityIdNormalized) return null if (!entityIdNormalized) return null
const inputFormCode = normalize(input?.listFormCode) const inputFormCode = normalize(input?.listFormCode)
if (!inputFormCode || inputFormCode !== listFormCodeNormalized) return null if (!inputFormCode) return null
const data = input?.data
const isMainListForm = inputFormCode === listFormCodeNormalized
if (!isMainListForm) {
if (!data || typeof data !== 'object') return null
const hit = findMatchingValueInData(data, entityIdNormalized)
if (!hit) return null
const nameValue = (data as any)?.Name ?? (data as any)?.name
return nameValue ? String(nameValue) : String(hit.value)
}
const keys = getKeysFromInput(input) const keys = getKeysFromInput(input)
.map((k) => normalize(k)) .map((k) => normalize(k))
@ -227,7 +241,6 @@ export const NoteList: React.FC<NoteListProps> = ({
} }
// Some entities may use a different PK than the visible row key; allow strict match via input.data too. // Some entities may use a different PK than the visible row key; allow strict match via input.data too.
const data = input?.data
if (data && typeof data === 'object') { if (data && typeof data === 'object') {
const hit = findMatchingValueInData(data, entityIdNormalized) const hit = findMatchingValueInData(data, entityIdNormalized)
if (hit) { if (hit) {
@ -240,7 +253,6 @@ export const NoteList: React.FC<NoteListProps> = ({
} }
// insert: keys is null, match by scanning input.data for entity id/name/code/etc. // insert: keys is null, match by scanning input.data for entity id/name/code/etc.
const data = input?.data
if (data && typeof data === 'object') { if (data && typeof data === 'object') {
const hit = findMatchingValueInData(data, entityIdNormalized) const hit = findMatchingValueInData(data, entityIdNormalized)
if (!hit) return null if (!hit) return null
@ -277,17 +289,15 @@ export const NoteList: React.FC<NoteListProps> = ({
setAuditLoading(true) setAuditLoading(true)
setAuditError(null) setAuditError(null)
try { try {
const response = await apiService.fetchData<PagedResultDto<AuditLogDto>>({ const response = await auditLogService.getList({
method: 'GET', skipCount: 0,
url: '/api/app/audit-log', maxResultCount: 200,
params: { sorting: 'ExecutionTime DESC',
skipCount: 0, listFormCode: entityName,
maxResultCount: 200, entityId,
sorting: 'ExecutionTime DESC',
},
}) })
const items = response.data?.items ?? [] const items = response?.items ?? []
const filtered = items const filtered = items
.map((log) => ({ log, matchedActions: buildMatchedActions(log) })) .map((log) => ({ log, matchedActions: buildMatchedActions(log) }))
.filter((x) => x.matchedActions.length > 0) .filter((x) => x.matchedActions.length > 0)
@ -305,13 +315,13 @@ export const NoteList: React.FC<NoteListProps> = ({
} }
useEffect(() => { useEffect(() => {
if (currentTab !== 'audit') return if (!isVisible) return
if (!listFormCodeNormalized && !entityIdNormalized) return if (!listFormCodeNormalized && !entityIdNormalized) return
const key = `${listFormCodeNormalized}|${entityIdNormalized}` const key = `${listFormCodeNormalized}|${entityIdNormalized}`
if (auditLoadedKey === key) return if (auditLoadedKey === key) return
loadAuditLogs() loadAuditLogs()
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentTab, listFormCodeNormalized, entityIdNormalized]) }, [isVisible, listFormCodeNormalized, entityIdNormalized])
const getStatusBadge = (statusCode?: number) => { const getStatusBadge = (statusCode?: number) => {
if (!statusCode) return <Badge className="bg-gray-500" content="?" /> if (!statusCode) return <Badge className="bg-gray-500" content="?" />
@ -330,7 +340,7 @@ export const NoteList: React.FC<NoteListProps> = ({
onChange={(val) => setCurrentTab(val as 'notes' | 'audit')} onChange={(val) => setCurrentTab(val as 'notes' | 'audit')}
variant="underline" variant="underline"
> >
<TabList className="mb-4 border-0 dark:bg-gray-800"> <TabList className="mb-2 border-0 dark:bg-gray-800">
<TabNav value="notes"> <TabNav value="notes">
{translate('::ListForms.ListForm.Notes')} {translate('::ListForms.ListForm.Notes')}
<Badge className="ml-2 bg-blue-500" content={`${notes?.length ?? 0}`} /> <Badge className="ml-2 bg-blue-500" content={`${notes?.length ?? 0}`} />
@ -341,21 +351,48 @@ export const NoteList: React.FC<NoteListProps> = ({
</TabNav> </TabNav>
</TabList> </TabList>
<TabContent value="notes"> <div className="mb-2 flex min-h-10 items-center justify-end border-y border-gray-200 bg-gray-50 px-1 py-1 dark:border-gray-700 dark:bg-gray-900">
<div className="flex items-center justify-end mb-2"> {currentTab === 'notes' ? (
<div className="flex items-center gap-2">
<Button
variant="default"
size="xs"
icon={<FaPlus />}
type="button"
onClick={onAddNote}
disabled={!onAddNote}
className="flex items-center"
>
{translate('::ListForms.ListForm.AddNote')}
</Button>
<Button
size="xs"
variant="default"
type="button"
icon={<FaSyncAlt />}
onClick={onRefreshNotes}
disabled={!onRefreshNotes}
className="flex items-center"
>
{translate('::ListForms.ListForm.Refresh')}
</Button>
</div>
) : (
<Button <Button
size="xs"
variant="default" variant="default"
size="sm"
icon={<FaPlus className="mr-1" />}
type="button" type="button"
onClick={onAddNote} icon={<FaSyncAlt />}
disabled={!onAddNote} onClick={loadAuditLogs}
disabled={auditLoading}
className="flex items-center" className="flex items-center"
> >
{translate('::ListForms.ListForm.AddNote')} {translate('::ListForms.ListForm.Refresh')}
</Button> </Button>
</div> )}
</div>
<TabContent value="notes">
{(notes?.length ?? 0) === 0 ? ( {(notes?.length ?? 0) === 0 ? (
<div className="flex flex-col items-center justify-center h-32 text-gray-500"> <div className="flex flex-col items-center justify-center h-32 text-gray-500">
<FaStickyNote className="text-4xl mb-2 opacity-50" /> <FaStickyNote className="text-4xl mb-2 opacity-50" />
@ -449,20 +486,6 @@ export const NoteList: React.FC<NoteListProps> = ({
</TabContent> </TabContent>
<TabContent value="audit"> <TabContent value="audit">
<div className="flex items-center justify-end mb-2">
<Button
size="sm"
variant="default"
type="button"
icon={<FaSyncAlt className="mr-1" />}
onClick={loadAuditLogs}
disabled={auditLoading}
className="flex items-center"
>
{translate('::ListForms.ListForm.Refresh')}
</Button>
</div>
{auditLoading ? ( {auditLoading ? (
<div className="flex items-center justify-center py-10"> <div className="flex items-center justify-center py-10">
<Spinner size={32} /> <Spinner size={32} />

View file

@ -92,7 +92,7 @@ function NoteModalContent({
{/* Başlık */} {/* Başlık */}
<div className="flex items-center justify-between mb-5 flex-shrink-0"> <div className="flex items-center justify-between mb-5 flex-shrink-0">
<h3 className="text-xl font-semibold flex items-center gap-3"> <h3 className="text-xl font-semibold flex items-center gap-3">
<div className="p-2 bg-purple-100 rounded-full"> <div className="p-1 bg-purple-100 rounded-full">
<FaPlus className="text-purple-600 text-lg" /> <FaPlus className="text-purple-600 text-lg" />
</div> </div>
{translate('::ListForms.ListForm.AddNote')} {translate('::ListForms.ListForm.AddNote')}
@ -116,7 +116,7 @@ function NoteModalContent({
{types.map((t) => ( {types.map((t) => (
<label <label
key={t.value} key={t.value}
className="flex items-center gap-2 px-2 py-1 text-black rounded-md cursor-pointer transition-all duration-200 dark:bg-gray-800 dark:text-gray-300" className="flex items-center gap-2 px-1 py-1 text-black rounded-md cursor-pointer transition-all duration-200 dark:bg-gray-800 dark:text-gray-300"
> >
<Radio <Radio
value={t.value} value={t.value}
@ -139,8 +139,9 @@ function NoteModalContent({
<Field <Field
type="text" type="text"
name="subject" name="subject"
as={Input}
placeholder={translate('::ListForms.ListForm.NoteModal.Subject')} placeholder={translate('::ListForms.ListForm.NoteModal.Subject')}
component={Input}
autoFocus
/> />
</FormItem> </FormItem>
@ -198,7 +199,7 @@ function NoteModalContent({
{/* DOSYA YÜKLEME */} {/* DOSYA YÜKLEME */}
<FormItem> <FormItem>
<div className="border-2 border-dashed border-gray-300 rounded-lg p-3 text-center hover:border-purple-400 transition-colors duration-200"> <div className="border-2 border-dashed border-gray-300 rounded-lg p-2 text-center hover:border-purple-400 transition-colors duration-200">
<Upload <Upload
className="cursor-pointer" className="cursor-pointer"
showList={false} showList={false}
@ -255,7 +256,7 @@ function NoteModalContent({
</FormContainer> </FormContainer>
{/* ALT BUTONLAR */} {/* ALT BUTONLAR */}
<Dialog.Footer className="mt-5 flex justify-between items-center pt-4 border-t border-gray-200"> <Dialog.Footer className="mt-2 flex justify-between items-center pt-4 border-t border-gray-200">
<Button variant="default" size="md" onClick={onClose} disabled={uploading}> <Button variant="default" size="md" onClick={onClose} disabled={uploading}>
{translate('::Cancel')} {translate('::Cancel')}
</Button> </Button>

View file

@ -41,7 +41,7 @@ export const NotePanel: React.FC<NotePanelProps> = ({
useEffect(() => { useEffect(() => {
if (isVisible) fetchActivities() if (isVisible) fetchActivities()
}, [isVisible]) }, [isVisible, entityName, entityId])
const handleDownloadFile = async (fileData: any) => { const handleDownloadFile = async (fileData: any) => {
if (!fileData?.SavedFileName) return if (!fileData?.SavedFileName) return
@ -62,8 +62,6 @@ export const NotePanel: React.FC<NotePanelProps> = ({
} }
} }
const getTotalCount = () => activities.length
// Draggable button handlers // Draggable button handlers
const handleMouseDown = (e: React.MouseEvent) => { const handleMouseDown = (e: React.MouseEvent) => {
if (!buttonRef.current) return if (!buttonRef.current) return
@ -134,7 +132,6 @@ export const NotePanel: React.FC<NotePanelProps> = ({
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{isVisible ? <FaChevronRight /> : <FaChevronLeft />} {isVisible ? <FaChevronRight /> : <FaChevronLeft />}
{getTotalCount() > 0 && <Badge content={getTotalCount()} />}
</div> </div>
</Button> </Button>
</div> </div>
@ -187,7 +184,9 @@ export const NotePanel: React.FC<NotePanelProps> = ({
notes={activities} notes={activities}
entityName={entityName} entityName={entityName}
entityId={entityId} entityId={entityId}
isVisible={isVisible}
onAddNote={() => setShowAddModal(true)} onAddNote={() => setShowAddModal(true)}
onRefreshNotes={fetchActivities}
onDeleteNote={handleDeleteActivity} onDeleteNote={handleDeleteActivity}
onDownloadFile={handleDownloadFile} onDownloadFile={handleDownloadFile}
/> />