Skip to content

Commit e2646cd

Browse files
Merge pull request #8758 from ProcessMaker/task/FOUR-29764
Connect Audit Logs UI to Case Retention API with Sorting, Search, and Overflow Handling
2 parents 17ecd4f + 5a6c16f commit e2646cd

8 files changed

Lines changed: 288 additions & 74 deletions

File tree

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
namespace ProcessMaker\Http\Controllers\Api;
4+
5+
use Illuminate\Http\Request;
6+
use Illuminate\Support\Facades\DB;
7+
use ProcessMaker\Http\Controllers\Controller;
8+
use ProcessMaker\Http\Resources\ApiCollection;
9+
use ProcessMaker\Models\CaseRetentionPolicyLog;
10+
11+
class CasesRetentionController extends Controller
12+
{
13+
private const LOG_SORT_COLUMNS = [
14+
'id',
15+
'process_id',
16+
'case_ids',
17+
'deleted_count',
18+
'total_time_taken',
19+
'deleted_at',
20+
'created_at',
21+
];
22+
23+
/**
24+
* Search log id, process_id, numeric columns, and JSON case_ids — not date columns.
25+
*/
26+
private function applyLogsFilter($query, string $term): void
27+
{
28+
$term = trim($term);
29+
if ($term === '') {
30+
return;
31+
}
32+
33+
$like = '%' . $term . '%';
34+
$driver = $query->getConnection()->getDriverName();
35+
36+
$query->where(function ($q) use ($like, $driver) {
37+
$q->where('id', 'like', $like)
38+
->orWhere('process_id', 'like', $like)
39+
->orWhere('deleted_count', 'like', $like)
40+
->orWhere('total_time_taken', 'like', $like);
41+
42+
if ($driver === 'pgsql') {
43+
$q->orWhereRaw('case_ids::text ILIKE ?', [$like]);
44+
} else {
45+
$q->orWhereRaw('CAST(case_ids AS CHAR) LIKE ?', [$like]);
46+
}
47+
});
48+
}
49+
50+
public function logs(Request $request): ApiCollection
51+
{
52+
$query = CaseRetentionPolicyLog::query();
53+
54+
if ($request->filled('filter')) {
55+
$this->applyLogsFilter($query, (string) $request->input('filter'));
56+
}
57+
58+
$orderBy = $request->input('order_by');
59+
if ($orderBy && in_array($orderBy, self::LOG_SORT_COLUMNS, true)) {
60+
$orderBy = DB::raw(preg_replace('/\.(.+)/', "->>'\$.$1'", $orderBy, 1));
61+
62+
$orderDirection = strtolower((string) $request->input('order_direction', 'asc'));
63+
if (!in_array($orderDirection, ['asc', 'desc'], true)) {
64+
$orderDirection = 'asc';
65+
}
66+
67+
$query->orderBy($orderBy, $orderDirection);
68+
} else {
69+
$query->orderByDesc('created_at');
70+
}
71+
72+
$response = $query->paginate($request->input('per_page', 10));
73+
74+
return new ApiCollection($response);
75+
}
76+
}

ProcessMaker/Jobs/EvaluateProcessRetentionJob.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ public function handle(): void
184184

185185
CaseRetentionPolicyLog::create([
186186
'process_id' => $this->processId,
187-
'case_ids' => json_encode($caseIds),
187+
'case_ids' => $caseIds,
188188
'deleted_count' => $chunkSize,
189189
'total_time_taken' => $chunkTimeMs,
190190
'deleted_at' => Carbon::now(),

ProcessMaker/Models/CaseRetentionPolicyLog.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,8 @@ class CaseRetentionPolicyLog extends ProcessMakerModel
2525
'total_time_taken',
2626
'deleted_at',
2727
];
28+
29+
protected $casts = [
30+
'case_ids' => 'array',
31+
];
2832
}

database/factories/ProcessMaker/Models/CaseRetentionPolicyLogFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public function definition()
2222
'process_id' => function () {
2323
return Process::factory()->create()->id;
2424
},
25-
'case_ids' => json_encode(CaseNumber::factory()->count($this->faker->numberBetween(1, 1000))->create()->pluck('id')->toArray()),
25+
'case_ids' => CaseNumber::factory()->count($this->faker->numberBetween(1, 1000))->create()->pluck('id')->toArray(),
2626
'deleted_count' => $this->faker->numberBetween(1, 1000),
2727
'total_time_taken' => $this->faker->numberBetween(1, 1000000),
2828
'deleted_at' => $this->faker->dateTimeBetween('-1 year', 'now'),
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
<template>
2+
<div
3+
v-uni-id="'case-id-' + rowId"
4+
class="case-ids-cell"
5+
>
6+
<template v-if="!ids.length">
7+
8+
</template>
9+
<template v-else-if="!hasOverflow">
10+
<span class="case-ids-preview">{{ ids.join(", ") }}</span>
11+
</template>
12+
<template v-else>
13+
<button
14+
:id="popoverTriggerId"
15+
type="button"
16+
class="case-ids-trigger"
17+
:title="$t('Show full list')"
18+
>
19+
<span class="case-ids-preview">{{ previewHead }}</span><span class="case-ids-ellipsis">…</span><span class="case-ids-more-count text-muted">+{{ moreHiddenCount }}</span>
20+
</button>
21+
<b-popover
22+
:target="popoverTriggerId"
23+
triggers="click"
24+
placement="auto"
25+
boundary="viewport"
26+
container="body"
27+
custom-class="case-ids-popover"
28+
>
29+
<template #title>
30+
{{ $t("Case IDs") }}
31+
<span class="text-muted font-weight-normal">({{ ids.length }})</span>
32+
</template>
33+
<div class="case-ids-popover-inner">
34+
<pre class="case-ids-popover-pre mb-0">{{ fullListText }}</pre>
35+
</div>
36+
</b-popover>
37+
</template>
38+
</div>
39+
</template>
40+
41+
<script>
42+
import { createUniqIdsMixin } from "vue-uniq-ids";
43+
44+
const uniqIdsMixin = createUniqIdsMixin();
45+
46+
export default {
47+
name: "CaseIdsTableCell",
48+
mixins: [uniqIdsMixin],
49+
props: {
50+
caseIds: {
51+
type: [Array, String, Number],
52+
default: null,
53+
},
54+
rowId: {
55+
type: [Number, String],
56+
required: true,
57+
},
58+
previewLimit: {
59+
type: Number,
60+
default: 5,
61+
},
62+
},
63+
computed: {
64+
ids() {
65+
return this.parseCaseIdsArray(this.caseIds);
66+
},
67+
hasOverflow() {
68+
return this.ids.length > this.previewLimit;
69+
},
70+
previewHead() {
71+
return this.ids.slice(0, this.previewLimit).join(", ");
72+
},
73+
moreHiddenCount() {
74+
return this.ids.length - this.previewLimit;
75+
},
76+
fullListText() {
77+
return this.ids.join(", ");
78+
},
79+
popoverTriggerId() {
80+
return `retention-case-ids-pop-${this.rowId}`;
81+
},
82+
},
83+
methods: {
84+
parseCaseIdsArray(caseIds) {
85+
if (caseIds == null || caseIds === "") {
86+
return [];
87+
}
88+
if (Array.isArray(caseIds)) {
89+
return caseIds.map(String);
90+
}
91+
if (typeof caseIds === "string") {
92+
try {
93+
const parsed = JSON.parse(caseIds);
94+
return Array.isArray(parsed) ? parsed.map(String) : [];
95+
} catch {
96+
return [];
97+
}
98+
}
99+
return [String(caseIds)];
100+
},
101+
},
102+
};
103+
</script>
104+
105+
<style lang="scss" scoped>
106+
.case-ids-preview {
107+
color: #4e5663;
108+
}
109+
110+
.case-ids-trigger {
111+
cursor: pointer;
112+
border: none;
113+
background: transparent;
114+
font: inherit;
115+
color: inherit;
116+
border-radius: 4px;
117+
margin: -2px -4px;
118+
padding: 2px 4px;
119+
display: inline;
120+
text-align: left;
121+
line-height: inherit;
122+
vertical-align: baseline;
123+
124+
&:hover,
125+
&:focus {
126+
background-color: rgba(0, 0, 0, 0.04);
127+
}
128+
129+
&:focus {
130+
outline: none;
131+
box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25);
132+
}
133+
}
134+
135+
.case-ids-ellipsis {
136+
color: #4e5663;
137+
letter-spacing: 0.02em;
138+
}
139+
140+
.case-ids-more-count {
141+
font-size: 12px;
142+
margin-left: 2px;
143+
font-weight: 500;
144+
white-space: nowrap;
145+
}
146+
</style>
147+
148+
<!-- Popover is teleported to body; scoped styles do not apply. -->
149+
<style lang="scss">
150+
.popover.case-ids-popover {
151+
max-width: min(440px, 92vw);
152+
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12);
153+
}
154+
155+
.popover.case-ids-popover .popover-body {
156+
padding: 0;
157+
}
158+
159+
.case-ids-popover-inner {
160+
max-height: min(320px, 50vh);
161+
overflow: auto;
162+
padding: 0.75rem 1rem;
163+
}
164+
165+
.case-ids-popover-pre {
166+
white-space: pre-wrap;
167+
word-break: break-all;
168+
font-size: 13px;
169+
line-height: 1.45;
170+
color: #4e5663;
171+
}
172+
</style>

0 commit comments

Comments
 (0)