@shipfox/client-workflows 19.0.0 → 20.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +14 -0
- package/dist/components/workflow-run-duration-label.d.ts +8 -7
- package/dist/components/workflow-run-duration-label.d.ts.map +1 -1
- package/dist/components/workflow-run-duration-label.js +15 -31
- package/dist/components/workflow-run-duration-label.js.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-row.d.ts.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-row.js +9 -6
- package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -1
- package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
- package/dist/components/workflow-run-summary/workflow-run-summary.js +7 -34
- package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
- package/dist/components/workflow-status/status-visuals.d.ts.map +1 -1
- package/dist/components/workflow-status/status-visuals.js +0 -9
- package/dist/components/workflow-status/status-visuals.js.map +1 -1
- package/dist/components/workflow-status/workflow-status-icon.d.ts.map +1 -1
- package/dist/components/workflow-status/workflow-status-icon.js +0 -11
- package/dist/components/workflow-status/workflow-status-icon.js.map +1 -1
- package/dist/core/entities/workflow-run.d.ts +10 -55
- package/dist/core/entities/workflow-run.d.ts.map +1 -1
- package/dist/core/entities/workflow-run.js +1 -94
- package/dist/core/entities/workflow-run.js.map +1 -1
- package/dist/core/workflow-run.d.ts +2 -2
- package/dist/core/workflow-run.d.ts.map +1 -1
- package/dist/core/workflow-run.js +1 -1
- package/dist/core/workflow-run.js.map +1 -1
- package/dist/hooks/api/workflow-run-mapper.d.ts.map +1 -1
- package/dist/hooks/api/workflow-run-mapper.js +3 -1
- package/dist/hooks/api/workflow-run-mapper.js.map +1 -1
- package/dist/hooks/api/workflow-runs.js +1 -0
- package/dist/hooks/api/workflow-runs.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/components/workflow-run-duration-label.tsx +21 -44
- package/src/components/workflow-run-list/workflow-run-list-view.test.tsx +48 -0
- package/src/components/workflow-run-list/workflow-run-row.tsx +7 -6
- package/src/components/workflow-run-summary/workflow-run-summary.stories.tsx +0 -38
- package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +73 -21
- package/src/components/workflow-run-summary/workflow-run-summary.tsx +6 -27
- package/src/components/workflow-status/status-visuals.ts +0 -4
- package/src/components/workflow-status/workflow-status-icon.stories.tsx +0 -1
- package/src/components/workflow-status/workflow-status-icon.tsx +0 -18
- package/src/core/entities/workflow-run.ts +9 -136
- package/src/core/workflow-run.test.ts +2 -0
- package/src/core/workflow-run.ts +0 -8
- package/src/hooks/api/workflow-run-mapper.ts +2 -0
- package/src/hooks/api/workflow-runs.ts +1 -1
- package/test/fixtures/workflow-run.ts +30 -3
- package/tsconfig.build.tsbuildinfo +1 -1
- package/src/core/entities/workflow-run-display.test.ts +0 -187
|
@@ -6,21 +6,17 @@ import {
|
|
|
6
6
|
type ListenerStatus,
|
|
7
7
|
WORKFLOW_JOB_STATUSES,
|
|
8
8
|
} from './job.js';
|
|
9
|
-
import {
|
|
10
|
-
elapsedTimeFromTimestamps,
|
|
11
|
-
type JobExecutionDisplayDuration,
|
|
12
|
-
type JobExecutionStatus,
|
|
13
|
-
} from './job-execution.js';
|
|
9
|
+
import type {JobExecutionStatus} from './job-execution.js';
|
|
14
10
|
import type {WorkflowRunAttempt, WorkflowRunAttemptSummary} from './workflow-run-attempt.js';
|
|
15
11
|
|
|
16
12
|
export type WorkflowRunStatus = 'pending' | 'running' | 'succeeded' | 'failed' | 'cancelled';
|
|
17
13
|
export type WorkflowRunRerunMode = 'all' | 'failed';
|
|
18
14
|
export type WorkflowStatus = WorkflowRunStatus | (typeof WORKFLOW_JOB_STATUSES)[number];
|
|
19
15
|
/**
|
|
20
|
-
* `listening`
|
|
21
|
-
*
|
|
16
|
+
* `listening` is display-only: the API never returns it, and it is derived from the listener
|
|
17
|
+
* state a job already carries.
|
|
22
18
|
*/
|
|
23
|
-
export type WorkflowDisplayStatus = WorkflowStatus | 'listening'
|
|
19
|
+
export type WorkflowDisplayStatus = WorkflowStatus | 'listening';
|
|
24
20
|
|
|
25
21
|
export const WORKFLOW_RUN_STATUSES = [
|
|
26
22
|
'pending',
|
|
@@ -34,7 +30,6 @@ export const WORKFLOW_DISPLAY_STATUSES = [
|
|
|
34
30
|
...WORKFLOW_RUN_STATUSES,
|
|
35
31
|
...WORKFLOW_JOB_STATUSES,
|
|
36
32
|
'listening',
|
|
37
|
-
'queued',
|
|
38
33
|
] as const satisfies readonly WorkflowDisplayStatus[];
|
|
39
34
|
|
|
40
35
|
export const TERMINAL_WORKFLOW_RUN_STATUSES = [
|
|
@@ -87,11 +82,14 @@ export interface WorkflowRunJobStatusCount {
|
|
|
87
82
|
* of them.
|
|
88
83
|
*
|
|
89
84
|
* `preview` is capped by the API, so `preview.length` is not the job count and the strip
|
|
90
|
-
* reads `total` and `statusCounts` for anything it says rather than anything it draws.
|
|
85
|
+
* reads `total` and `statusCounts` for anything it says rather than anything it draws. The API
|
|
86
|
+
* also reports whether any job execution has started because a terminal `cancelled` job can have
|
|
87
|
+
* started work even though its job status does not preserve that distinction.
|
|
91
88
|
*/
|
|
92
89
|
export interface WorkflowRunJobs {
|
|
93
90
|
preview: WorkflowRunJobSummary[];
|
|
94
91
|
statusCounts: WorkflowRunJobStatusCount[];
|
|
92
|
+
hasStartedJobExecution: boolean;
|
|
95
93
|
total: number;
|
|
96
94
|
}
|
|
97
95
|
|
|
@@ -132,6 +130,7 @@ export interface WorkflowRunDetail extends WorkflowRun {
|
|
|
132
130
|
latestAttempt: number;
|
|
133
131
|
runAttempt: WorkflowRunAttempt;
|
|
134
132
|
jobs: Job[];
|
|
133
|
+
hasStartedJobExecution: boolean;
|
|
135
134
|
}
|
|
136
135
|
|
|
137
136
|
export interface WorkflowRunListPage {
|
|
@@ -198,129 +197,3 @@ export function isWorkflowRunTerminal(status: WorkflowRunStatus): boolean {
|
|
|
198
197
|
export function isWorkflowStatus(status: string): status is WorkflowStatus {
|
|
199
198
|
return WORKFLOW_STATUSES.has(status as WorkflowStatus);
|
|
200
199
|
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* A run's headline duration, split the way a job's already is. An attempt's `startedAt` marks
|
|
204
|
-
* when the orchestrator picked the run up, not when work began, so a run whose jobs are all
|
|
205
|
-
* still waiting for a runner reads as queue time rather than as run time.
|
|
206
|
-
*/
|
|
207
|
-
export type WorkflowRunDisplayDuration = JobExecutionDisplayDuration;
|
|
208
|
-
|
|
209
|
-
/** What every surface shows for a run, derived together so status and duration cannot disagree. */
|
|
210
|
-
export interface WorkflowRunDisplay {
|
|
211
|
-
status: WorkflowDisplayStatus;
|
|
212
|
-
duration: WorkflowRunDisplayDuration | null;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* The evidence a surface can offer about a run's progress.
|
|
217
|
-
*
|
|
218
|
-
* `jobStatuses` is what every surface has: the list carries status counts, the detail carries
|
|
219
|
-
* whole jobs. `firstStartedAt` is the detail's extra precision, and where it is absent the
|
|
220
|
-
* attempt's own mark stands in.
|
|
221
|
-
*/
|
|
222
|
-
export interface WorkflowRunProgress {
|
|
223
|
-
runStatus: WorkflowRunStatus;
|
|
224
|
-
startedAt: string | null;
|
|
225
|
-
finishedAt: string | null;
|
|
226
|
-
jobStatuses: JobDisplayStatus[];
|
|
227
|
-
firstStartedAt?: string | null | undefined;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/** When the run's earliest execution began, or null while none of them has. */
|
|
231
|
-
export function workflowRunFirstStartedAt(jobs: Job[]): string | null {
|
|
232
|
-
let earliest: string | null = null;
|
|
233
|
-
let earliestMs = Number.POSITIVE_INFINITY;
|
|
234
|
-
|
|
235
|
-
for (const job of jobs) {
|
|
236
|
-
for (const {startedAt} of job.jobExecutions) {
|
|
237
|
-
if (startedAt === null) continue;
|
|
238
|
-
const startedMs = new Date(startedAt).getTime();
|
|
239
|
-
if (!Number.isFinite(startedMs) || startedMs >= earliestMs) continue;
|
|
240
|
-
earliest = startedAt;
|
|
241
|
-
earliestMs = startedMs;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return earliest;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Whether any work has begun. A run with no jobs on hand is a run whose jobs were not fetched,
|
|
250
|
-
* which is no evidence that nothing started, so it counts as started rather than claiming a
|
|
251
|
-
* queue this surface cannot see.
|
|
252
|
-
*/
|
|
253
|
-
function workflowRunHasStarted({jobStatuses, firstStartedAt}: WorkflowRunProgress): boolean {
|
|
254
|
-
if (firstStartedAt != null) return true;
|
|
255
|
-
if (jobStatuses.length === 0) return true;
|
|
256
|
-
return jobStatuses.some((status) => status !== 'pending' && status !== 'skipped');
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* The single rule behind every run readout. An attempt's `startedAt` marks when the
|
|
261
|
-
* orchestrator picked the run up, not when work began, so a run whose jobs are all still
|
|
262
|
-
* waiting reads as `Queued` for queue time. Calling that "running for 2h" sends an operator to
|
|
263
|
-
* debug a build that never began.
|
|
264
|
-
*/
|
|
265
|
-
export function deriveWorkflowRunDisplay(progress: WorkflowRunProgress): WorkflowRunDisplay {
|
|
266
|
-
const {runStatus, startedAt, finishedAt, firstStartedAt} = progress;
|
|
267
|
-
const hasStarted = workflowRunHasStarted(progress);
|
|
268
|
-
const status = runStatus === 'running' && !hasStarted ? 'queued' : runStatus;
|
|
269
|
-
|
|
270
|
-
if (startedAt === null) return {status, duration: null};
|
|
271
|
-
|
|
272
|
-
// A run cancelled before anything started keeps its queue reading rather than reporting a
|
|
273
|
-
// run that never was.
|
|
274
|
-
const time = elapsedTimeFromTimestamps({from: firstStartedAt ?? startedAt, to: finishedAt});
|
|
275
|
-
if (time === null) return {status, duration: null};
|
|
276
|
-
|
|
277
|
-
return {status, duration: {kind: hasStarted ? 'run' : 'queue', ...time}};
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/** The run detail's reading: whole jobs, so queue and run time split on the first execution. */
|
|
281
|
-
export function workflowRunDetailDisplay(run: {
|
|
282
|
-
runAttempt: Pick<WorkflowRunAttemptSummary, 'status' | 'startedAt' | 'finishedAt'>;
|
|
283
|
-
jobs: Job[];
|
|
284
|
-
}): WorkflowRunDisplay {
|
|
285
|
-
return deriveWorkflowRunDisplay({
|
|
286
|
-
runStatus: run.runAttempt.status,
|
|
287
|
-
startedAt: run.runAttempt.startedAt,
|
|
288
|
-
finishedAt: run.runAttempt.finishedAt,
|
|
289
|
-
jobStatuses: run.jobs.map((job) => job.status),
|
|
290
|
-
firstStartedAt: workflowRunFirstStartedAt(run.jobs),
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* The run list's reading: status counts cover every job, not just the drawn preview, so a row
|
|
296
|
-
* reaches the same verdict as the detail page without fetching a single timestamp more.
|
|
297
|
-
*/
|
|
298
|
-
export function workflowRunListItemDisplay(run: WorkflowRunListItem): WorkflowRunDisplay {
|
|
299
|
-
return deriveWorkflowRunDisplay({
|
|
300
|
-
runStatus: run.status,
|
|
301
|
-
startedAt: run.runAttempt.startedAt,
|
|
302
|
-
finishedAt: run.runAttempt.finishedAt,
|
|
303
|
-
jobStatuses: run.jobs.statusCounts.filter(({count}) => count > 0).map(({status}) => status),
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* The job the run is waiting on: the one queued longest without starting. Named only so a
|
|
309
|
-
* queued run says what it waits for instead of showing a number with no subject.
|
|
310
|
-
*/
|
|
311
|
-
export function workflowRunBlockingJob(jobs: Job[]): Job | null {
|
|
312
|
-
let blocking: Job | null = null;
|
|
313
|
-
let queuedMs = Number.POSITIVE_INFINITY;
|
|
314
|
-
|
|
315
|
-
for (const job of jobs) {
|
|
316
|
-
for (const {queuedAt, startedAt, finishedAt} of job.jobExecutions) {
|
|
317
|
-
if (queuedAt === null || startedAt !== null || finishedAt !== null) continue;
|
|
318
|
-
const candidateMs = new Date(queuedAt).getTime();
|
|
319
|
-
if (!Number.isFinite(candidateMs) || candidateMs >= queuedMs) continue;
|
|
320
|
-
blocking = job;
|
|
321
|
-
queuedMs = candidateMs;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
return blocking;
|
|
326
|
-
}
|
|
@@ -103,6 +103,7 @@ describe('workflow run model mapping', () => {
|
|
|
103
103
|
latest_attempt: 4,
|
|
104
104
|
started_at: '2026-05-07T01:01:10.000Z',
|
|
105
105
|
finished_at: null,
|
|
106
|
+
has_started_job_execution: true,
|
|
106
107
|
});
|
|
107
108
|
|
|
108
109
|
const run = toWorkflowRunListItem(dto);
|
|
@@ -122,6 +123,7 @@ describe('workflow run model mapping', () => {
|
|
|
122
123
|
expect(run).not.toHaveProperty('startedAt');
|
|
123
124
|
expect(run).not.toHaveProperty('finishedAt');
|
|
124
125
|
expect(run).not.toHaveProperty('displayDuration');
|
|
126
|
+
expect(run.jobs.hasStartedJobExecution).toBe(true);
|
|
125
127
|
expect(run.runAttempt.displayDuration).toEqual({
|
|
126
128
|
state: 'live',
|
|
127
129
|
fromIso: '2026-05-07T01:01:10.000Z',
|
package/src/core/workflow-run.ts
CHANGED
|
@@ -59,14 +59,11 @@ export type {
|
|
|
59
59
|
WorkflowDisplayStatus,
|
|
60
60
|
WorkflowRun,
|
|
61
61
|
WorkflowRunDetail,
|
|
62
|
-
WorkflowRunDisplay,
|
|
63
|
-
WorkflowRunDisplayDuration,
|
|
64
62
|
WorkflowRunJobStatusCount,
|
|
65
63
|
WorkflowRunJobSummary,
|
|
66
64
|
WorkflowRunJobs,
|
|
67
65
|
WorkflowRunListItem,
|
|
68
66
|
WorkflowRunListPage,
|
|
69
|
-
WorkflowRunProgress,
|
|
70
67
|
WorkflowRunRecord,
|
|
71
68
|
WorkflowRunRerunMode,
|
|
72
69
|
WorkflowRunStatus,
|
|
@@ -75,19 +72,14 @@ export type {
|
|
|
75
72
|
WorkflowStatus,
|
|
76
73
|
} from './entities/workflow-run.js';
|
|
77
74
|
export {
|
|
78
|
-
deriveWorkflowRunDisplay,
|
|
79
75
|
isWorkflowRunTerminal,
|
|
80
76
|
isWorkflowStatus,
|
|
81
77
|
TERMINAL_WORKFLOW_RUN_STATUSES,
|
|
82
78
|
WORKFLOW_DISPLAY_STATUSES,
|
|
83
79
|
WORKFLOW_RUN_STATUSES,
|
|
84
80
|
workflowRunActor,
|
|
85
|
-
workflowRunBlockingJob,
|
|
86
81
|
workflowRunBranchLabel,
|
|
87
82
|
workflowRunCommitLabel,
|
|
88
|
-
workflowRunDetailDisplay,
|
|
89
|
-
workflowRunFirstStartedAt,
|
|
90
|
-
workflowRunListItemDisplay,
|
|
91
83
|
workflowRunTriggerDisplayLabel,
|
|
92
84
|
workflowRunTriggerLabel,
|
|
93
85
|
} from './entities/workflow-run.js';
|
|
@@ -119,6 +119,7 @@ export function toWorkflowRunListItem(dto: WorkflowRunListItemDto): WorkflowRunL
|
|
|
119
119
|
position: job.position,
|
|
120
120
|
})),
|
|
121
121
|
statusCounts,
|
|
122
|
+
hasStartedJobExecution: dto.has_started_job_execution ?? true,
|
|
122
123
|
// Derived rather than sent: the counts already cover every job, and a separate total
|
|
123
124
|
// would be a second source of truth that could disagree with them.
|
|
124
125
|
total: statusCounts.reduce((sum, entry) => sum + entry.count, 0),
|
|
@@ -146,6 +147,7 @@ export function toWorkflowRunDetail(dto: WorkflowRunDetailResponseDto): Workflow
|
|
|
146
147
|
latestAttempt: dto.latest_attempt,
|
|
147
148
|
runAttempt: toWorkflowRunAttempt(dto.run_attempt),
|
|
148
149
|
jobs: dto.jobs.map(toJob),
|
|
150
|
+
hasStartedJobExecution: dto.has_started_job_execution ?? true,
|
|
149
151
|
};
|
|
150
152
|
}
|
|
151
153
|
|
|
@@ -358,7 +358,7 @@ function buildTempRun({
|
|
|
358
358
|
updatedAt: createdAt,
|
|
359
359
|
isTemporary: true,
|
|
360
360
|
// The optimistic row genuinely has no jobs yet: the server has not planned the graph.
|
|
361
|
-
jobs: {preview: [], statusCounts: [], total: 0},
|
|
361
|
+
jobs: {preview: [], statusCounts: [], hasStartedJobExecution: false, total: 0},
|
|
362
362
|
runAttempt: new WorkflowRunAttemptSummary({
|
|
363
363
|
workflowRunId: id,
|
|
364
364
|
attempt: 1,
|
|
@@ -82,6 +82,7 @@ export function workflowRunDto(
|
|
|
82
82
|
jobs: [],
|
|
83
83
|
job_status_counts: [],
|
|
84
84
|
job_display_status_counts: [],
|
|
85
|
+
has_started_job_execution: false,
|
|
85
86
|
...overrides,
|
|
86
87
|
};
|
|
87
88
|
}
|
|
@@ -136,10 +137,18 @@ function executionStatusForFixtureStatus(
|
|
|
136
137
|
*
|
|
137
138
|
* Built from one list so the two can never disagree. A fixture whose counts contradicted its
|
|
138
139
|
* preview would hide exactly the bug the split exists to prevent.
|
|
140
|
+
*
|
|
141
|
+
* `has_started_job_execution` follows the statuses where they settle it: `succeeded` and `failed`
|
|
142
|
+
* cannot be reached without running. `pending`, `skipped`, and `cancelled` leave it open, which is
|
|
143
|
+
* the ambiguity the server flag exists to resolve, so those default to not started and a case that
|
|
144
|
+
* needs the other reading overrides it.
|
|
139
145
|
*/
|
|
140
146
|
export function workflowRunJobsFixture(
|
|
141
147
|
statuses: readonly JobStatusDto[],
|
|
142
|
-
): Pick<
|
|
148
|
+
): Pick<
|
|
149
|
+
WorkflowRunListItemDto,
|
|
150
|
+
'jobs' | 'job_status_counts' | 'job_display_status_counts' | 'has_started_job_execution'
|
|
151
|
+
> {
|
|
143
152
|
const counts = new Map<JobStatusDto, number>();
|
|
144
153
|
for (const status of statuses) counts.set(status, (counts.get(status) ?? 0) + 1);
|
|
145
154
|
|
|
@@ -152,6 +161,9 @@ export function workflowRunJobsFixture(
|
|
|
152
161
|
jobs: preview,
|
|
153
162
|
job_status_counts: [...counts.entries()].map(([status, count]) => ({status, count})),
|
|
154
163
|
job_display_status_counts: [...counts.entries()].map(([status, count]) => ({status, count})),
|
|
164
|
+
has_started_job_execution: statuses.some(
|
|
165
|
+
(status) => status === 'running' || status === 'succeeded' || status === 'failed',
|
|
166
|
+
),
|
|
155
167
|
};
|
|
156
168
|
}
|
|
157
169
|
|
|
@@ -159,7 +171,10 @@ export function workflowRunJobsFixture(
|
|
|
159
171
|
export function workflowRunJobsOfStatus(
|
|
160
172
|
count: number,
|
|
161
173
|
status: JobStatusDto = 'succeeded',
|
|
162
|
-
): Pick<
|
|
174
|
+
): Pick<
|
|
175
|
+
WorkflowRunListItemDto,
|
|
176
|
+
'jobs' | 'job_status_counts' | 'job_display_status_counts' | 'has_started_job_execution'
|
|
177
|
+
> {
|
|
163
178
|
return workflowRunJobsFixture(Array.from({length: count}, () => status));
|
|
164
179
|
}
|
|
165
180
|
|
|
@@ -189,7 +204,12 @@ export function workflowRunListPage(
|
|
|
189
204
|
export function workflowRunDetailDto(
|
|
190
205
|
overrides: Partial<WorkflowRunDetailResponseDto> = {},
|
|
191
206
|
): WorkflowRunDetailResponseDto {
|
|
192
|
-
const {
|
|
207
|
+
const {
|
|
208
|
+
jobs,
|
|
209
|
+
run_attempt: runAttemptOverride,
|
|
210
|
+
has_started_job_execution: hasStartedOverride,
|
|
211
|
+
...runOverrides
|
|
212
|
+
} = overrides;
|
|
193
213
|
const run = workflowRunDto(runOverrides);
|
|
194
214
|
|
|
195
215
|
return {
|
|
@@ -205,6 +225,13 @@ export function workflowRunDetailDto(
|
|
|
205
225
|
finished_at: run.finished_at,
|
|
206
226
|
}),
|
|
207
227
|
jobs: jobs ?? [],
|
|
228
|
+
// Follows the executions the case actually built, so the fixture cannot claim a run started
|
|
229
|
+
// while carrying no execution that did.
|
|
230
|
+
has_started_job_execution:
|
|
231
|
+
hasStartedOverride ??
|
|
232
|
+
(jobs ?? []).some((job) =>
|
|
233
|
+
job.job_executions.some((execution) => execution.started_at != null),
|
|
234
|
+
),
|
|
208
235
|
};
|
|
209
236
|
}
|
|
210
237
|
|