@apptegy/nuxt-navigation 0.2.6-alpha.0 → 0.2.8
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/dist/module.json +1 -1
- package/dist/runtime/components/Nav/Header.vue +4 -1
- package/dist/runtime/components/Nav/NotificationItem.vue +150 -81
- package/dist/runtime/components/Nav/SidebarItem.vue +1 -1
- package/dist/runtime/types/index.d.ts +1 -1
- package/dist/runtime/types/notifications.d.ts +23 -1
- package/dist/runtime/utils/notification-api.d.ts +10 -0
- package/dist/runtime/utils/notification-api.js +42 -1
- package/locale/en.json +1 -0
- package/package.json +2 -2
package/dist/module.json
CHANGED
|
@@ -200,7 +200,7 @@ const notificationsPanel = computed(() => {
|
|
|
200
200
|
return null;
|
|
201
201
|
}
|
|
202
202
|
return mapNotificationsApiResponse(notificationsResponse.value, {
|
|
203
|
-
locale:
|
|
203
|
+
locale: $intl.locale,
|
|
204
204
|
dateLabels: notificationDateLabels.value,
|
|
205
205
|
retentionLabel: $t("navigation.notificationsPanel.retentionLabel"),
|
|
206
206
|
tabs: [
|
|
@@ -216,6 +216,9 @@ function patchNotifications(patch) {
|
|
|
216
216
|
}
|
|
217
217
|
emit("update:notifications", {
|
|
218
218
|
...props.notifications,
|
|
219
|
+
panelOpen: panelOpen.value,
|
|
220
|
+
activeTabId: activeTabId.value,
|
|
221
|
+
activeFilterId: activeFilterId.value,
|
|
219
222
|
...patch
|
|
220
223
|
});
|
|
221
224
|
}
|
|
@@ -3,95 +3,127 @@
|
|
|
3
3
|
class="notification-item"
|
|
4
4
|
:class="{ 'notification-item--in-badge': showBadgeActions }"
|
|
5
5
|
>
|
|
6
|
-
<div
|
|
7
|
-
class="notification-item__main"
|
|
8
|
-
role="button"
|
|
9
|
-
tabindex="0"
|
|
10
|
-
@click="handleClick"
|
|
11
|
-
@keydown.enter.prevent="handleClick"
|
|
12
|
-
>
|
|
6
|
+
<div class="notification-item__header">
|
|
13
7
|
<div
|
|
14
|
-
class="notification-
|
|
15
|
-
|
|
8
|
+
class="notification-item__main"
|
|
9
|
+
role="button"
|
|
10
|
+
tabindex="0"
|
|
11
|
+
@click="handleClick"
|
|
12
|
+
@keydown.enter.prevent="handleClick"
|
|
16
13
|
>
|
|
17
|
-
<div
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
<div
|
|
15
|
+
class="notification-item__avatar"
|
|
16
|
+
:style="{ '--type-accent': typeAccent }"
|
|
17
|
+
>
|
|
18
|
+
<div class="notification-item__avatar-ring">
|
|
19
|
+
<NavAvatar
|
|
20
|
+
:name="primaryActor.name"
|
|
21
|
+
:src="primaryActor.avatarUrl"
|
|
22
|
+
class="notification-item__avatar-image"
|
|
23
|
+
/>
|
|
24
|
+
</div>
|
|
25
|
+
<span
|
|
26
|
+
v-if="groupBadgeLabel"
|
|
27
|
+
class="notification-item__group-badge"
|
|
28
|
+
aria-hidden="true"
|
|
29
|
+
>
|
|
30
|
+
{{ groupBadgeLabel }}
|
|
31
|
+
</span>
|
|
32
|
+
<span
|
|
33
|
+
v-else
|
|
34
|
+
class="notification-item__type-badge"
|
|
35
|
+
aria-hidden="true"
|
|
36
|
+
>
|
|
37
|
+
<component :is="typeIcon" />
|
|
38
|
+
</span>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div class="notification-item__content">
|
|
42
|
+
<p class="notification-item__message">
|
|
43
|
+
{{ item.actionText }}
|
|
44
|
+
</p>
|
|
45
|
+
<p class="notification-item__meta">
|
|
46
|
+
<span>{{ item.timestampLabel }}</span>
|
|
47
|
+
<template v-if="item.contextLabel">
|
|
48
|
+
<span
|
|
49
|
+
class="notification-item__meta-separator"
|
|
50
|
+
aria-hidden="true"
|
|
51
|
+
>·</span
|
|
52
|
+
>
|
|
53
|
+
<span>{{ item.contextLabel }}</span>
|
|
54
|
+
</template>
|
|
55
|
+
</p>
|
|
56
|
+
<Tag
|
|
57
|
+
v-if="item.stateLabel"
|
|
58
|
+
:value="item.stateLabel"
|
|
59
|
+
severity="secondary"
|
|
60
|
+
class="notification-item__state-chip"
|
|
22
61
|
/>
|
|
23
62
|
</div>
|
|
24
|
-
<span
|
|
25
|
-
v-if="groupBadgeLabel"
|
|
26
|
-
class="notification-item__group-badge"
|
|
27
|
-
aria-hidden="true"
|
|
28
|
-
>
|
|
29
|
-
{{ groupBadgeLabel }}
|
|
30
|
-
</span>
|
|
31
|
-
<span
|
|
32
|
-
class="notification-item__type-badge"
|
|
33
|
-
aria-hidden="true"
|
|
34
|
-
>
|
|
35
|
-
<component :is="typeIcon" />
|
|
36
|
-
</span>
|
|
37
63
|
</div>
|
|
38
64
|
|
|
39
|
-
<div class="notification-
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
<span>{{ item.timestampLabel }}</span>
|
|
45
|
-
<template v-if="item.contextLabel">
|
|
46
|
-
<span
|
|
47
|
-
class="notification-item__meta-separator"
|
|
48
|
-
aria-hidden="true"
|
|
49
|
-
>·</span
|
|
50
|
-
>
|
|
51
|
-
<span>{{ item.contextLabel }}</span>
|
|
52
|
-
</template>
|
|
53
|
-
</p>
|
|
54
|
-
<Tag
|
|
55
|
-
v-if="item.stateLabel"
|
|
56
|
-
:value="item.stateLabel"
|
|
57
|
-
severity="secondary"
|
|
58
|
-
class="notification-item__state-chip"
|
|
65
|
+
<div class="notification-item__actions">
|
|
66
|
+
<span
|
|
67
|
+
v-if="showBadgeActions"
|
|
68
|
+
class="notification-item__unread-dot"
|
|
69
|
+
aria-hidden="true"
|
|
59
70
|
/>
|
|
71
|
+
<Button
|
|
72
|
+
v-if="showBadgeActions"
|
|
73
|
+
type="button"
|
|
74
|
+
severity="secondary"
|
|
75
|
+
text
|
|
76
|
+
rounded
|
|
77
|
+
class="notification-item__dismiss"
|
|
78
|
+
:aria-label="$t('navigation.notificationsPanel.dismiss')"
|
|
79
|
+
@click="handleDismiss"
|
|
80
|
+
>
|
|
81
|
+
<svg
|
|
82
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
83
|
+
width="16"
|
|
84
|
+
height="16"
|
|
85
|
+
viewBox="0 0 24 24"
|
|
86
|
+
fill="none"
|
|
87
|
+
stroke="currentColor"
|
|
88
|
+
stroke-width="2"
|
|
89
|
+
stroke-linecap="round"
|
|
90
|
+
stroke-linejoin="round"
|
|
91
|
+
aria-hidden="true"
|
|
92
|
+
>
|
|
93
|
+
<path d="M18 6 6 18" />
|
|
94
|
+
<path d="m6 6 12 12" />
|
|
95
|
+
</svg>
|
|
96
|
+
</Button>
|
|
60
97
|
</div>
|
|
61
98
|
</div>
|
|
62
99
|
|
|
63
|
-
<div
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
rounded
|
|
75
|
-
class="notification-item__dismiss"
|
|
76
|
-
:aria-label="$t('navigation.notificationsPanel.dismiss')"
|
|
77
|
-
@click="handleDismiss"
|
|
78
|
-
>
|
|
79
|
-
<svg
|
|
80
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
81
|
-
width="16"
|
|
82
|
-
height="16"
|
|
83
|
-
viewBox="0 0 24 24"
|
|
84
|
-
fill="none"
|
|
85
|
-
stroke="currentColor"
|
|
86
|
-
stroke-width="2"
|
|
87
|
-
stroke-linecap="round"
|
|
88
|
-
stroke-linejoin="round"
|
|
100
|
+
<div
|
|
101
|
+
v-if="item.summary"
|
|
102
|
+
class="notification-item__summary"
|
|
103
|
+
role="button"
|
|
104
|
+
tabindex="0"
|
|
105
|
+
@click="handleClick"
|
|
106
|
+
@keydown.enter.prevent="handleClick"
|
|
107
|
+
>
|
|
108
|
+
<template v-if="reactionEmojis.length">
|
|
109
|
+
<span
|
|
110
|
+
class="notification-item__reactions"
|
|
89
111
|
aria-hidden="true"
|
|
90
112
|
>
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
113
|
+
<span
|
|
114
|
+
v-for="(emoji, index) in reactionEmojis"
|
|
115
|
+
:key="`${emoji}-${index}`"
|
|
116
|
+
class="notification-item__reaction"
|
|
117
|
+
>{{ emoji }}</span>
|
|
118
|
+
</span>
|
|
119
|
+
<span class="notification-item__summary-on">
|
|
120
|
+
{{ " " }}{{ $t("navigation.notificationsPanel.summaryOn") }}{{ " " }}
|
|
121
|
+
</span>
|
|
122
|
+
</template>
|
|
123
|
+
<span class="notification-item__summary-text">
|
|
124
|
+
<template v-if="reactionEmojis.length">"{{ item.summary }}"</template>
|
|
125
|
+
<template v-else>{{ item.summary }}</template>
|
|
126
|
+
</span>
|
|
95
127
|
</div>
|
|
96
128
|
</div>
|
|
97
129
|
</template>
|
|
@@ -108,6 +140,7 @@ const emit = defineEmits(["dismiss", "select"]);
|
|
|
108
140
|
const primaryActor = computed(() => props.item.actors[0] || { name: "" });
|
|
109
141
|
const typeAccent = computed(() => getTypeAccent(props.item.type));
|
|
110
142
|
const showBadgeActions = computed(() => isInBadge(props.item));
|
|
143
|
+
const reactionEmojis = computed(() => props.item.reactionEmojis ?? []);
|
|
111
144
|
const groupBadgeLabel = computed(() => {
|
|
112
145
|
const count = props.item.groupCount ?? Math.max(props.item.actors.length - 1, 0);
|
|
113
146
|
return count > 0 ? `+${count}` : "";
|
|
@@ -189,10 +222,8 @@ function handleDismiss(event) {
|
|
|
189
222
|
|
|
190
223
|
<style scoped>
|
|
191
224
|
.notification-item {
|
|
192
|
-
display:
|
|
193
|
-
|
|
194
|
-
gap: 12px;
|
|
195
|
-
align-items: start;
|
|
225
|
+
display: flex;
|
|
226
|
+
flex-direction: column;
|
|
196
227
|
width: 100%;
|
|
197
228
|
padding: 12px;
|
|
198
229
|
border: 1px solid rgb(232, 232, 236);
|
|
@@ -203,6 +234,12 @@ function handleDismiss(event) {
|
|
|
203
234
|
box-shadow: none;
|
|
204
235
|
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
|
205
236
|
}
|
|
237
|
+
.notification-item__header {
|
|
238
|
+
display: grid;
|
|
239
|
+
grid-template-columns: 1fr auto;
|
|
240
|
+
gap: 12px;
|
|
241
|
+
align-items: start;
|
|
242
|
+
}
|
|
206
243
|
.notification-item__main {
|
|
207
244
|
display: grid;
|
|
208
245
|
grid-template-columns: auto 1fr;
|
|
@@ -308,6 +345,38 @@ function handleDismiss(event) {
|
|
|
308
345
|
.notification-item__state-chip {
|
|
309
346
|
margin-top: 8px;
|
|
310
347
|
}
|
|
348
|
+
.notification-item__summary {
|
|
349
|
+
margin-top: 12px;
|
|
350
|
+
padding-top: 12px;
|
|
351
|
+
border-top: 1px solid rgb(232, 232, 236);
|
|
352
|
+
cursor: pointer;
|
|
353
|
+
font-size: 13px;
|
|
354
|
+
line-height: 18px;
|
|
355
|
+
color: var(--grey-70, #4b5563);
|
|
356
|
+
}
|
|
357
|
+
.notification-item__summary:focus-visible {
|
|
358
|
+
outline: 2px solid var(--purple-60, #662e80);
|
|
359
|
+
outline-offset: 2px;
|
|
360
|
+
border-radius: 4px;
|
|
361
|
+
}
|
|
362
|
+
.notification-item__reactions {
|
|
363
|
+
display: inline;
|
|
364
|
+
white-space: nowrap;
|
|
365
|
+
}
|
|
366
|
+
.notification-item__reaction {
|
|
367
|
+
font-size: 14px;
|
|
368
|
+
line-height: 1;
|
|
369
|
+
}
|
|
370
|
+
.notification-item__summary-on {
|
|
371
|
+
margin-inline: 0.25em;
|
|
372
|
+
color: var(--grey-50, #747474);
|
|
373
|
+
}
|
|
374
|
+
.notification-item__summary-text {
|
|
375
|
+
font-style: italic;
|
|
376
|
+
color: var(--grey-70, #4b5563);
|
|
377
|
+
white-space: normal;
|
|
378
|
+
overflow-wrap: anywhere;
|
|
379
|
+
}
|
|
311
380
|
.notification-item__actions {
|
|
312
381
|
display: flex;
|
|
313
382
|
align-items: center;
|
|
@@ -12,4 +12,4 @@ export interface IUser {
|
|
|
12
12
|
client: string;
|
|
13
13
|
admin_email: string;
|
|
14
14
|
}
|
|
15
|
-
export type { NotificationType, NotificationApiState, INotificationApiActor, INotificationApiRecord, INotificationsPagination, INotificationsApiResponse, INotificationActor, INotificationItem, INotificationGroup, INotificationTab, INotificationFilter, INotificationsPanelData, INotificationsHeaderModel, } from './notifications.js';
|
|
15
|
+
export type { NotificationType, NotificationApiState, INotificationApiActor, INotificationApiDetailObject, INotificationApiRecord, INotificationsPagination, INotificationsApiResponse, INotificationActor, INotificationItem, INotificationGroup, INotificationTab, INotificationFilter, INotificationsPanelData, INotificationsHeaderModel, } from './notifications.js';
|
|
@@ -5,6 +5,15 @@ export interface INotificationApiActor {
|
|
|
5
5
|
first_name: string;
|
|
6
6
|
last_name: string;
|
|
7
7
|
}
|
|
8
|
+
export interface INotificationApiDetailObject {
|
|
9
|
+
resource_type?: string | null;
|
|
10
|
+
resource_id?: string | null;
|
|
11
|
+
parent_type?: string | null;
|
|
12
|
+
parent_id?: string | null;
|
|
13
|
+
/** Parent display name (e.g. course/room); shown in the meta line. */
|
|
14
|
+
parent_name?: string | null;
|
|
15
|
+
ward_ids?: string[] | null;
|
|
16
|
+
}
|
|
8
17
|
export interface INotificationApiRecord {
|
|
9
18
|
id: number | string;
|
|
10
19
|
/**
|
|
@@ -14,7 +23,15 @@ export interface INotificationApiRecord {
|
|
|
14
23
|
kind?: string | null;
|
|
15
24
|
source_record_type: string;
|
|
16
25
|
title: string;
|
|
17
|
-
summary
|
|
26
|
+
/** Post/body preview shown in the card summary row. */
|
|
27
|
+
summary?: string | null;
|
|
28
|
+
/**
|
|
29
|
+
* Reaction tallies for aggregate reaction notifications (e.g. `{ CLAP: 2, LIKE: 1 }`).
|
|
30
|
+
* When present, corresponding emojis are shown beside the summary.
|
|
31
|
+
*/
|
|
32
|
+
reaction_counts?: Record<string, number> | null;
|
|
33
|
+
/** Navigation/context payload; `parent_name` is shown in the meta line. */
|
|
34
|
+
detail_object?: INotificationApiDetailObject | null;
|
|
18
35
|
actors?: INotificationApiActor[] | null;
|
|
19
36
|
detail_link?: string | null;
|
|
20
37
|
notification_state: NotificationApiState | string;
|
|
@@ -58,7 +75,12 @@ export interface INotificationItem {
|
|
|
58
75
|
* Derived from API `last_activity_at`, falling back to `created_at`.
|
|
59
76
|
*/
|
|
60
77
|
createdAt?: string;
|
|
78
|
+
/** Parent name shown after the relative timestamp (e.g. "Algebra I"). */
|
|
61
79
|
contextLabel?: string;
|
|
80
|
+
/** Post/body preview shown in the card summary row. */
|
|
81
|
+
summary?: string;
|
|
82
|
+
/** Emoji list derived from API `reaction_counts`, shown before the summary when present. */
|
|
83
|
+
reactionEmojis?: string[];
|
|
62
84
|
/** When false, the notification counts toward the bell badge and appears in the New tab. */
|
|
63
85
|
dismissed?: boolean;
|
|
64
86
|
/** Read receipt — announcement and message types only. */
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { INotificationApiActor, INotificationApiRecord, INotificationGroup, INotificationItem, INotificationsApiResponse, INotificationsPanelData, INotificationTab, NotificationType } from '../types/index.js';
|
|
2
2
|
import { type INotificationDateLabels } from './notification-dates.js';
|
|
3
|
+
/**
|
|
4
|
+
* Map `reaction_counts` to unique display emojis (one per reaction type).
|
|
5
|
+
* Count is ignored beyond requiring it to be >= 1. Unknown keys are skipped.
|
|
6
|
+
*/
|
|
7
|
+
export declare function mapReactionCountsToEmojis(reactionCounts: Record<string, number> | null | undefined): string[];
|
|
3
8
|
export declare const DEFAULT_NOTIFICATION_TABS: INotificationTab[];
|
|
4
9
|
/** Normalize kind values for filter matching and `filter[kind][]` query params. */
|
|
5
10
|
export declare function normalizeNotificationKind(value: string): string;
|
|
@@ -14,6 +19,11 @@ export declare function mapNotificationApiActor(actor: INotificationApiActor): {
|
|
|
14
19
|
name: string;
|
|
15
20
|
avatarUrl: string | undefined;
|
|
16
21
|
};
|
|
22
|
+
export declare const NOTIFICATION_SUMMARY_MAX_LENGTH = 200;
|
|
23
|
+
/**
|
|
24
|
+
* Collapse whitespace/newlines into a single line and truncate for card display.
|
|
25
|
+
*/
|
|
26
|
+
export declare function formatNotificationSummary(summary: string | null | undefined, maxLength?: number): string | undefined;
|
|
17
27
|
export declare function mapNotificationApiRecord(record: INotificationApiRecord, options: {
|
|
18
28
|
locale?: string;
|
|
19
29
|
dateLabels: INotificationDateLabels;
|
|
@@ -15,6 +15,29 @@ const SOURCE_RECORD_TYPE_MAP = {
|
|
|
15
15
|
"school-post": "school-post",
|
|
16
16
|
system: "system"
|
|
17
17
|
};
|
|
18
|
+
const REACTION_EMOJI_MAP = {
|
|
19
|
+
clap: "\u{1F44F}",
|
|
20
|
+
like: "\u{1F44D}",
|
|
21
|
+
heart: "\u2764\uFE0F",
|
|
22
|
+
dislike: "\u{1F44E}",
|
|
23
|
+
laugh: "\u{1F602}",
|
|
24
|
+
wow: "\u{1F62E}"
|
|
25
|
+
};
|
|
26
|
+
export function mapReactionCountsToEmojis(reactionCounts) {
|
|
27
|
+
if (!reactionCounts) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
const emojis = [];
|
|
31
|
+
for (const [key, count] of Object.entries(reactionCounts)) {
|
|
32
|
+
const emoji = REACTION_EMOJI_MAP[key.toLowerCase()];
|
|
33
|
+
const safeCount = Number(count);
|
|
34
|
+
if (!emoji || !Number.isFinite(safeCount) || safeCount < 1) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
emojis.push(emoji);
|
|
38
|
+
}
|
|
39
|
+
return emojis;
|
|
40
|
+
}
|
|
18
41
|
export const DEFAULT_NOTIFICATION_TABS = [
|
|
19
42
|
{ id: "new", label: "New", unreadOnly: true },
|
|
20
43
|
{ id: "all", label: "All" }
|
|
@@ -42,10 +65,26 @@ export function mapNotificationApiActor(actor) {
|
|
|
42
65
|
avatarUrl: actor.avatar_url
|
|
43
66
|
};
|
|
44
67
|
}
|
|
68
|
+
export const NOTIFICATION_SUMMARY_MAX_LENGTH = 200;
|
|
69
|
+
export function formatNotificationSummary(summary, maxLength = NOTIFICATION_SUMMARY_MAX_LENGTH) {
|
|
70
|
+
if (!summary) {
|
|
71
|
+
return void 0;
|
|
72
|
+
}
|
|
73
|
+
const normalized = summary.replace(/\s+/g, " ").trim();
|
|
74
|
+
if (!normalized) {
|
|
75
|
+
return void 0;
|
|
76
|
+
}
|
|
77
|
+
if (normalized.length <= maxLength) {
|
|
78
|
+
return normalized;
|
|
79
|
+
}
|
|
80
|
+
return `${normalized.slice(0, maxLength).trimEnd()}\u2026`;
|
|
81
|
+
}
|
|
45
82
|
export function mapNotificationApiRecord(record, options) {
|
|
46
83
|
const state = record.notification_state;
|
|
47
84
|
const type = mapSourceRecordType(record.source_record_type);
|
|
48
85
|
const activityAt = resolveNotificationActivityAt(record);
|
|
86
|
+
const summary = formatNotificationSummary(record.summary);
|
|
87
|
+
const reactionEmojis = mapReactionCountsToEmojis(record.reaction_counts);
|
|
49
88
|
return {
|
|
50
89
|
id: String(record.id),
|
|
51
90
|
type,
|
|
@@ -61,7 +100,9 @@ export function mapNotificationApiRecord(record, options) {
|
|
|
61
100
|
}
|
|
62
101
|
}),
|
|
63
102
|
createdAt: activityAt,
|
|
64
|
-
contextLabel: record.
|
|
103
|
+
contextLabel: record.detail_object?.parent_name?.trim() || void 0,
|
|
104
|
+
summary,
|
|
105
|
+
reactionEmojis: reactionEmojis.length > 0 ? reactionEmojis : void 0,
|
|
65
106
|
dismissed: state === "dismissed",
|
|
66
107
|
read: state === "read",
|
|
67
108
|
unread: state === "unread",
|
package/locale/en.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apptegy/nuxt-navigation",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -51,5 +51,5 @@
|
|
|
51
51
|
"vue": "^3.5.13",
|
|
52
52
|
"vue-tsc": "^3.0.3"
|
|
53
53
|
},
|
|
54
|
-
"gitHead": "
|
|
54
|
+
"gitHead": "58c15c01250814b62a3afb2b260d93b63af3a53e"
|
|
55
55
|
}
|