@eventcatalog/core 2.1.0 → 2.2.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/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/scripts/catalog-to-astro-content-directory.js +15 -3
- package/scripts/default-files-for-collections/changelogs.md +5 -0
- package/scripts/watcher.js +6 -1
- package/src/components/Lists/VersionList.astro +1 -0
- package/src/components/SideBars/DomainSideBar.astro +4 -1
- package/src/components/SideBars/MessageSideBar.astro +4 -1
- package/src/components/SideBars/ServiceSideBar.astro +1 -2
- package/src/content/config.ts +21 -0
- package/src/pages/docs/[type]/[id]/[version]/changelog/index.astro +172 -0
- package/src/utils/changelogs/changelogs.ts +34 -0
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -44,7 +44,13 @@ const copyFiles = async ({ source, target, catalogFilesDir, pathToMarkdownFiles,
|
|
|
44
44
|
|
|
45
45
|
//ensure the directory exists
|
|
46
46
|
ensureDirSync(path.dirname(targetPath));
|
|
47
|
-
|
|
47
|
+
|
|
48
|
+
if (file.includes('changelog.md')) {
|
|
49
|
+
const target = targetPath.replace('/content', '/content/changelogs');
|
|
50
|
+
fs.cpSync(file, target.replace('changelog.md', 'changelog.mdx'));
|
|
51
|
+
} else {
|
|
52
|
+
fs.cpSync(file, targetPath.replace('index.md', 'index.mdx').replace('changelog.md', 'changelog.mdx'));
|
|
53
|
+
}
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
// Copy all other files (non markdown) files into catalog-files directory (non collection)
|
|
@@ -121,6 +127,7 @@ export const catalogToAstro = async (source, astroContentDir, catalogFilesDir) =
|
|
|
121
127
|
path.join(source, 'events/**/**/index.md'),
|
|
122
128
|
path.join(source, 'services/**/events/**/index.md'),
|
|
123
129
|
path.join(source, 'domains/**/events/**/index.md'),
|
|
130
|
+
path.join(source, 'events/**/**/changelog.md'),
|
|
124
131
|
],
|
|
125
132
|
pathToAllFiles: [
|
|
126
133
|
path.join(source, 'events/**'),
|
|
@@ -139,6 +146,7 @@ export const catalogToAstro = async (source, astroContentDir, catalogFilesDir) =
|
|
|
139
146
|
path.join(source, 'commands/**/**/index.md'),
|
|
140
147
|
path.join(source, 'services/**/commands/**/index.md'),
|
|
141
148
|
path.join(source, 'domains/**/commands/**/index.md'),
|
|
149
|
+
path.join(source, 'commands/**/**/changelog.md'),
|
|
142
150
|
],
|
|
143
151
|
pathToAllFiles: [
|
|
144
152
|
path.join(source, 'commands/**'),
|
|
@@ -153,7 +161,11 @@ export const catalogToAstro = async (source, astroContentDir, catalogFilesDir) =
|
|
|
153
161
|
source,
|
|
154
162
|
target: astroContentDir,
|
|
155
163
|
catalogFilesDir,
|
|
156
|
-
pathToMarkdownFiles: [
|
|
164
|
+
pathToMarkdownFiles: [
|
|
165
|
+
path.join(source, 'services/**/**/index.md'),
|
|
166
|
+
path.join(source, 'domains/**/services/**/index.md'),
|
|
167
|
+
path.join(source, 'services/**/**/changelog.md'),
|
|
168
|
+
],
|
|
157
169
|
pathToAllFiles: [path.join(source, 'services/**'), path.join(source, 'domains/**/services/**')],
|
|
158
170
|
type: 'services',
|
|
159
171
|
});
|
|
@@ -163,7 +175,7 @@ export const catalogToAstro = async (source, astroContentDir, catalogFilesDir) =
|
|
|
163
175
|
source,
|
|
164
176
|
target: astroContentDir,
|
|
165
177
|
catalogFilesDir,
|
|
166
|
-
pathToMarkdownFiles: [path.join(source, 'domains/**/**/index.md')],
|
|
178
|
+
pathToMarkdownFiles: [path.join(source, 'domains/**/**/index.md'), path.join(source, 'domains/**/**/changelog.md')],
|
|
167
179
|
pathToAllFiles: [path.join(source, 'domains/**')],
|
|
168
180
|
type: 'domains',
|
|
169
181
|
});
|
package/scripts/watcher.js
CHANGED
|
@@ -30,7 +30,12 @@ for (let item of [...verifiedWatchList]) {
|
|
|
30
30
|
for (let event of events) {
|
|
31
31
|
const { path: eventPath, type } = event;
|
|
32
32
|
const file = eventPath.split(item)[1];
|
|
33
|
-
|
|
33
|
+
let newPath = path.join(contentPath, item, extensionReplacer(item, file));
|
|
34
|
+
|
|
35
|
+
// Check if changlogs, they need to go into their own content folder
|
|
36
|
+
if (file.includes('changelog.md')) {
|
|
37
|
+
newPath = newPath.replace('src/content', 'src/content/changelogs');
|
|
38
|
+
}
|
|
34
39
|
|
|
35
40
|
// If config files have changes
|
|
36
41
|
if (eventPath.includes('eventcatalog.config.js') || eventPath.includes('eventcatalog.styles.css')) {
|
|
@@ -26,6 +26,7 @@ const currentPath = Astro.url.pathname;
|
|
|
26
26
|
</option>
|
|
27
27
|
})}
|
|
28
28
|
</select>
|
|
29
|
+
<a href={buildUrl(`/docs/${collectionItem.collection}/${collectionItem.data.id}/${collectionItem.data.latestVersion}/changelog`)} class="text-[10px] text-gray-500">View changelogs</a>
|
|
29
30
|
</div>
|
|
30
31
|
|
|
31
32
|
<script>
|
|
@@ -38,6 +38,9 @@ const ownersList = owners.map((o) => ({
|
|
|
38
38
|
<PillList title={`Services (${services.length})`} pills={serviceList} emptyMessage={`This domain does not contain any services.`} color="pink" client:load />
|
|
39
39
|
<OwnersList title={`Service owners (${ownersList.length})`} owners={ownersList} emptyMessage={`This domain does not have any documented owners.`} client:load />
|
|
40
40
|
{domain.data.versions && <VersionList versions={domain.data.versions} collectionItem={domain} />}
|
|
41
|
-
<
|
|
41
|
+
<div class="space-y-2">
|
|
42
|
+
<a href={buildUrl(`/visualiser/${domain.collection}/${domain.data.id}/${domain.data.version}`)} class="block text-center rounded-md w-full bg-white px-3.5 py-2.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-100/60 hover:text-purple-500">View in Visualiser</a>
|
|
43
|
+
<a href={buildUrl(`${domain.data.version}/changelog`)} class="block text-center rounded-md w-full bg-white px-3.5 py-2.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-100/60 hover:text-purple-500">Changelog</a>
|
|
44
|
+
</div>
|
|
42
45
|
</div>
|
|
43
46
|
</aside>
|
|
@@ -47,7 +47,7 @@ const schemaURL = path.join(publicPath, schemaFilePath || '')
|
|
|
47
47
|
|
|
48
48
|
---
|
|
49
49
|
|
|
50
|
-
<aside class="sticky top-28 left-0 space-y-8 h-full overflow-y-auto">
|
|
50
|
+
<aside class="sticky top-28 left-0 space-y-8 h-full overflow-y-auto pb-20 ">
|
|
51
51
|
<div class="">
|
|
52
52
|
<PillList color="pink" title={`${type} Producers (${producerList.length})`} pills={producerList} emptyMessage={`This ${type} does not get produced by any services.`} client:load />
|
|
53
53
|
<PillList color="pink" title={`${type} Consumers (${consumerList.length})`} pills={consumerList} emptyMessage={`This ${type} does not get consumed by any services.`} client:load />
|
|
@@ -77,6 +77,9 @@ const schemaURL = path.join(publicPath, schemaFilePath || '')
|
|
|
77
77
|
class="block text-center rounded-md w-full bg-white px-3.5 py-2.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-100/60 hover:text-purple-500"
|
|
78
78
|
>View in Visualiser</a
|
|
79
79
|
>
|
|
80
|
+
|
|
81
|
+
<a href={buildUrl(`${message.data.version}/changelog`)} class="block text-center rounded-md w-full bg-white px-3.5 py-2.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-100/60 hover:text-purple-500">Changelog</a>
|
|
82
|
+
|
|
80
83
|
</div>
|
|
81
84
|
</div>
|
|
82
85
|
</aside>
|
|
@@ -46,8 +46,6 @@ const ownersList = owners.map((o) => ({
|
|
|
46
46
|
const publicPath = service?.catalog?.publicPath;
|
|
47
47
|
const schemaFilePath = service?.data?.schemaPath;
|
|
48
48
|
const schemaURL = join(publicPath, schemaFilePath || '')
|
|
49
|
-
|
|
50
|
-
|
|
51
49
|
---
|
|
52
50
|
|
|
53
51
|
<aside class="sticky top-28 left-0 space-y-8 h-full overflow-y-auto">
|
|
@@ -77,6 +75,7 @@ const schemaURL = join(publicPath, schemaFilePath || '')
|
|
|
77
75
|
}
|
|
78
76
|
<a href={buildUrl(`/visualiser/${service.collection}/${service.data.id}/${service.data.version}`)} class="block text-center rounded-md w-full bg-white px-3.5 py-2.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-100/60 hover:text-purple-500">View in Visualiser</a>
|
|
79
77
|
<a id="open-api-button" href={buildUrl(`/docs/${service.collection}/${service.data.id}/${service.data.version}/spec`)} class="hidden text-center rounded-md w-full bg-white px-3.5 py-2.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-100/60 hover:text-purple-500">View API spec</a>
|
|
78
|
+
<a href={buildUrl(`${service.data.version}/changelog`)} class="block text-center rounded-md w-full bg-white px-3.5 py-2.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-100/60 hover:text-purple-500">Changelog</a>
|
|
80
79
|
</div>
|
|
81
80
|
</div>
|
|
82
81
|
</aside>
|
package/src/content/config.ts
CHANGED
|
@@ -13,6 +13,26 @@ const pages = defineCollection({
|
|
|
13
13
|
}),
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
+
const changelogs = defineCollection({
|
|
17
|
+
type: 'content',
|
|
18
|
+
schema: z.object({
|
|
19
|
+
createdAt: z.date().optional(),
|
|
20
|
+
badges: z.array(badge).optional(),
|
|
21
|
+
// Used by eventcatalog
|
|
22
|
+
version: z.string().optional(),
|
|
23
|
+
versions: z.array(z.string()).optional(),
|
|
24
|
+
latestVersion: z.string().optional(),
|
|
25
|
+
catalog: z
|
|
26
|
+
.object({
|
|
27
|
+
path: z.string(),
|
|
28
|
+
filePath: z.string(),
|
|
29
|
+
publicPath: z.string(),
|
|
30
|
+
type: z.string(),
|
|
31
|
+
})
|
|
32
|
+
.optional(),
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
|
|
16
36
|
// Create a union type for owners
|
|
17
37
|
const ownerReference = z.union([reference('users'), reference('teams')]);
|
|
18
38
|
|
|
@@ -123,4 +143,5 @@ export const collections = {
|
|
|
123
143
|
teams,
|
|
124
144
|
domains,
|
|
125
145
|
pages,
|
|
146
|
+
changelogs,
|
|
126
147
|
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { CollectionEntry } from 'astro:content';
|
|
3
|
+
|
|
4
|
+
import { getEvents } from '@utils/events';
|
|
5
|
+
import { getServices } from '@utils/services/services';
|
|
6
|
+
import { getCommands } from '@utils/commands';
|
|
7
|
+
import { getDomains } from '@utils/domains/domains';
|
|
8
|
+
import type { CollectionTypes } from '@types';
|
|
9
|
+
import Layout from '@layouts/DocsLayout.astro';
|
|
10
|
+
import { getChangeLogs } from '@utils/changelogs/changelogs';
|
|
11
|
+
import { EnvelopeIcon, RectangleGroupIcon, ServerIcon } from '@heroicons/react/24/outline';
|
|
12
|
+
|
|
13
|
+
import { buildUrl } from '@utils/url-builder';
|
|
14
|
+
|
|
15
|
+
export async function getStaticPaths() {
|
|
16
|
+
const events = await getEvents();
|
|
17
|
+
const commands = await getCommands();
|
|
18
|
+
const services = await getServices();
|
|
19
|
+
const domains = await getDomains();
|
|
20
|
+
|
|
21
|
+
const buildPages = (collection: CollectionEntry<CollectionTypes>[]) => {
|
|
22
|
+
|
|
23
|
+
return collection.map((item) => ({
|
|
24
|
+
params: {
|
|
25
|
+
type: item.collection,
|
|
26
|
+
id: item.data.id,
|
|
27
|
+
version: item.data.version,
|
|
28
|
+
},
|
|
29
|
+
props: {
|
|
30
|
+
type: item.collection,
|
|
31
|
+
...item,
|
|
32
|
+
},
|
|
33
|
+
}));
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return [...buildPages(domains), ...buildPages(events), ...buildPages(services), ...buildPages(commands)];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const props = Astro.props;
|
|
40
|
+
const logs = await getChangeLogs(props);
|
|
41
|
+
|
|
42
|
+
const { data } = props;
|
|
43
|
+
const latestVersion = data.latestVersion;
|
|
44
|
+
|
|
45
|
+
const renderedLogs = await logs.map(async (log) => {
|
|
46
|
+
const { Content } = await log.render();
|
|
47
|
+
return {
|
|
48
|
+
Content,
|
|
49
|
+
...log
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const logsToRender = await Promise.all(renderedLogs);
|
|
54
|
+
|
|
55
|
+
const logList = logsToRender.map((log, index) => ({
|
|
56
|
+
id: log.id,
|
|
57
|
+
url: buildUrl(`/docs/${props.collection}/${props.data.id}`),
|
|
58
|
+
isLatest: log.data.version === latestVersion,
|
|
59
|
+
version: log.data.version,
|
|
60
|
+
createdAt: log.data.createdAt,
|
|
61
|
+
badges: log.data.badges || [],
|
|
62
|
+
Content: log.Content,
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
const getBadge = () => {
|
|
66
|
+
if (props.collection === 'services') {
|
|
67
|
+
return { backgroundColor: 'pink', textColor: 'pink', content: 'Service', icon: ServerIcon, class: "text-pink-400" };
|
|
68
|
+
}
|
|
69
|
+
if (props.collection === 'events') {
|
|
70
|
+
return { backgroundColor: 'orange', textColor: 'orange', content: 'Event', icon: EnvelopeIcon, class: "text-orange-400" };
|
|
71
|
+
}
|
|
72
|
+
if (props.collection === 'commands') {
|
|
73
|
+
return { backgroundColor: 'blue', textColor: 'blue', content: 'Command', icon: EnvelopeIcon, class: "text-blue-400" };
|
|
74
|
+
}
|
|
75
|
+
if (props.collection === 'domains') {
|
|
76
|
+
return { backgroundColor: 'yellow', textColor: 'yellow', content: 'Domain', icon: RectangleGroupIcon, class: "text-yellow-400" };
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const badges = [
|
|
81
|
+
getBadge()
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
<Layout title="ChangeLog">
|
|
87
|
+
<main class="flex-1 w-full lg:pr-10 md:pt-4">
|
|
88
|
+
<div class="border-b border-gray-200 flex justify-between items-start py-4 w-full ">
|
|
89
|
+
<div>
|
|
90
|
+
<h2 class="text-2xl md:text-4xl font-bold">{props.data.name} (Changelog)</h2>
|
|
91
|
+
<h2 class="text-lg pt-2 text-gray-500 font-light">{props.data.summary}</h2>
|
|
92
|
+
{
|
|
93
|
+
badges && (
|
|
94
|
+
<div class="flex flex-wrap py-2 pt-4">
|
|
95
|
+
{badges.map((badge: any) => (
|
|
96
|
+
<span class={`text-sm font-light text-gray-500 px-2 py-1 rounded-md mr-2 bg-${badge.backgroundColor}-100 space-x-1 border border-${badge.backgroundColor}-200 text-${badge.textColor}-800 flex items-center ${badge.class ? badge.class : ''} `}>
|
|
97
|
+
{badge.icon && <badge.icon className="w-4 h-4 inline-block mr-1 " />}
|
|
98
|
+
<span>{badge.content}</span>
|
|
99
|
+
</span>
|
|
100
|
+
))}
|
|
101
|
+
</div>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
{logList.length === 0 &&
|
|
107
|
+
<div class="py-4 text-gray-400 prose prose-md">
|
|
108
|
+
<p>No changelogs found.</p>
|
|
109
|
+
</div>
|
|
110
|
+
}
|
|
111
|
+
<div class="flow-root py-8">
|
|
112
|
+
<ul role="list" class="-mb-8">
|
|
113
|
+
{logList.map((log, index) => (
|
|
114
|
+
<li>
|
|
115
|
+
<div class="relative pb-8">
|
|
116
|
+
{index !== logList.length - 1 ? (
|
|
117
|
+
<span aria-hidden="true" class="absolute left-6 top-4 -ml-px h-full w-0.5 bg-gray-200" />
|
|
118
|
+
) : null}
|
|
119
|
+
<div class="relative flex space-x-3">
|
|
120
|
+
<div>
|
|
121
|
+
<a
|
|
122
|
+
href={log.isLatest ? `${log.url}` : `${log.url}/${log.version}`}
|
|
123
|
+
class={'bg-purple-500 hover:bg-purple-400 text-white flex h-8 w-14 items-center justify-center rounded-full ring-8 ring-white'}
|
|
124
|
+
>
|
|
125
|
+
{log.version}
|
|
126
|
+
{/* <DocumentTextIcon aria-hidden="true" className="h-5 w-5 text-white" /> */}
|
|
127
|
+
</a>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
</div>
|
|
131
|
+
<div class="pl-[70px] py-2 -mt-10">
|
|
132
|
+
{log.createdAt &&
|
|
133
|
+
<div class="pb-2">
|
|
134
|
+
<h3 class="text-2xl text-gray-800 font-bold">
|
|
135
|
+
<span>{log.createdAt.toISOString().split('T')[0]} {`${log.isLatest ? '(latest)' : ''}`}</span>
|
|
136
|
+
</h3>
|
|
137
|
+
</div>
|
|
138
|
+
}
|
|
139
|
+
{
|
|
140
|
+
log.badges && (
|
|
141
|
+
<div class="flex flex-wrap">
|
|
142
|
+
{log.badges.map((badge: any) => (
|
|
143
|
+
<span class={`text-sm font-light text-gray-500 px-2 py-1 rounded-md mr-2 bg-${badge.backgroundColor}-100 space-x-1 border border-${badge.backgroundColor}-200 text-${badge.textColor}-800 flex items-center ${badge.class ? badge.class : ''} `}>
|
|
144
|
+
{badge.icon && <badge.icon className="w-4 h-4 inline-block mr-1 " />}
|
|
145
|
+
<span>{badge.content}</span>
|
|
146
|
+
</span>
|
|
147
|
+
))}
|
|
148
|
+
</div>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
<div class="prose prose-md !max-w-none py-2">
|
|
152
|
+
<log.Content />
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</li>
|
|
157
|
+
))}
|
|
158
|
+
</ul>
|
|
159
|
+
</div>
|
|
160
|
+
<footer class="py-4 space-y-8 border-t border-gray-300">
|
|
161
|
+
<div class="flex justify-between items-center py-8 text-gray-500 text-sm font-light">
|
|
162
|
+
<div class="flex space-x-5">
|
|
163
|
+
<a href="https://github.com/event-catalog/eventcatalog" target="_blank"><svg class="w-5 h-5 bg-gray-400 hover:bg-purple-500 dark:hover:bg-gray-400" style="mask-image: url("/icons/github.svg"); mask-repeat: no-repeat; mask-position: center center;"></svg></a>
|
|
164
|
+
<a href="https://x.com/event_catalog" target="_blank"><span class="sr-only">x</span><svg class="w-5 h-5 bg-gray-400 hover:bg-purple-500 dark:hover:bg-gray-400" style="mask-image: url("/icons/x-twitter.svg"); mask-repeat: no-repeat; mask-position: center center;"></svg></a>
|
|
165
|
+
</div>
|
|
166
|
+
<a target="_blank" class="hover:text-purple-500 hover:underline text-gray-400 font-light not-prose" href="https://eventcatalog.dev">Powered by EventCatalog</a>
|
|
167
|
+
</div>
|
|
168
|
+
</footer>
|
|
169
|
+
</main>
|
|
170
|
+
|
|
171
|
+
</Layout>
|
|
172
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { CollectionTypes } from '@types';
|
|
2
|
+
import { getCollection, type CollectionEntry } from 'astro:content';
|
|
3
|
+
|
|
4
|
+
export type ChangeLog = CollectionEntry<'changelogs'>;
|
|
5
|
+
|
|
6
|
+
export const getChangeLogs = async (item: CollectionEntry<CollectionTypes>): Promise<ChangeLog[]> => {
|
|
7
|
+
const { collection, data } = item;
|
|
8
|
+
|
|
9
|
+
// Get all logs for collection type and filter by given collection
|
|
10
|
+
const logs = await getCollection('changelogs', (log) => {
|
|
11
|
+
return log.id.includes(`${collection}/`) && log.id.includes(`/${data.id}/`);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const hydratedLogs = logs.map((log) => {
|
|
15
|
+
// Check if there is a version in the url
|
|
16
|
+
const isVersioned = log.id.includes('versioned');
|
|
17
|
+
|
|
18
|
+
const parts = log.id.split('/');
|
|
19
|
+
// hack to get the version of the id (url)
|
|
20
|
+
const version = parts[parts.length - 2];
|
|
21
|
+
return {
|
|
22
|
+
...log,
|
|
23
|
+
data: {
|
|
24
|
+
...log.data,
|
|
25
|
+
version: isVersioned ? version : data.latestVersion || 'latest',
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Order by version string
|
|
31
|
+
return hydratedLogs.sort((a, b) => {
|
|
32
|
+
return b.data.version.localeCompare(a.data.version);
|
|
33
|
+
});
|
|
34
|
+
};
|