@rmdes/indiekit-endpoint-activitypub 0.1.7 → 0.1.9
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/index.js +2 -1
- package/lib/controllers/migrate.js +70 -62
- package/lib/migration.js +51 -11
- package/locales/en.json +2 -0
- package/package.json +1 -1
- package/views/activitypub-migrate.njk +87 -6
package/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { dashboardController } from "./lib/controllers/dashboard.js";
|
|
|
9
9
|
import { followersController } from "./lib/controllers/followers.js";
|
|
10
10
|
import { followingController } from "./lib/controllers/following.js";
|
|
11
11
|
import { activitiesController } from "./lib/controllers/activities.js";
|
|
12
|
-
import { migrateGetController, migratePostController } from "./lib/controllers/migrate.js";
|
|
12
|
+
import { migrateGetController, migratePostController, migrateImportController } from "./lib/controllers/migrate.js";
|
|
13
13
|
|
|
14
14
|
const defaults = {
|
|
15
15
|
mountPath: "/activitypub",
|
|
@@ -180,6 +180,7 @@ export default class ActivityPubEndpoint {
|
|
|
180
180
|
router.get("/admin/activities", activitiesController(mp));
|
|
181
181
|
router.get("/admin/migrate", migrateGetController(mp, this.options));
|
|
182
182
|
router.post("/admin/migrate", migratePostController(mp, this.options));
|
|
183
|
+
router.post("/admin/migrate/import", express.json({ limit: "5mb" }), migrateImportController(mp, this.options));
|
|
183
184
|
|
|
184
185
|
return router;
|
|
185
186
|
}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Migration controller — handles Mastodon account migration UI.
|
|
3
3
|
*
|
|
4
4
|
* GET: shows the 3-step migration page
|
|
5
|
-
* POST:
|
|
5
|
+
* POST /admin/migrate: alias update (small form POST)
|
|
6
|
+
* POST /admin/migrate/import: CSV import (JSON via fetch, bypasses body size limit)
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import {
|
|
@@ -30,69 +31,16 @@ export function migrateGetController(mountPath, pluginOptions) {
|
|
|
30
31
|
export function migratePostController(mountPath, pluginOptions) {
|
|
31
32
|
return async (request, response, next) => {
|
|
32
33
|
try {
|
|
33
|
-
const { application } = request.app.locals;
|
|
34
|
-
const action = request.body.action;
|
|
35
34
|
let result = null;
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (action === "import") {
|
|
50
|
-
const followingCollection =
|
|
51
|
-
application?.collections?.get("ap_following");
|
|
52
|
-
const followersCollection =
|
|
53
|
-
application?.collections?.get("ap_followers");
|
|
54
|
-
|
|
55
|
-
const importFollowing = request.body.importTypes?.includes("following");
|
|
56
|
-
const importFollowers = request.body.importTypes?.includes("followers");
|
|
57
|
-
|
|
58
|
-
// Read file content (submitted as text via client-side FileReader)
|
|
59
|
-
const fileContent = request.body.csvContent?.trim();
|
|
60
|
-
if (!fileContent) {
|
|
61
|
-
result = {
|
|
62
|
-
type: "error",
|
|
63
|
-
text: response.locals.__("activitypub.migrate.errorNoFile"),
|
|
64
|
-
};
|
|
65
|
-
} else {
|
|
66
|
-
let followingResult = { imported: 0, failed: 0 };
|
|
67
|
-
let followersResult = { imported: 0, failed: 0 };
|
|
68
|
-
|
|
69
|
-
if (importFollowing && followingCollection) {
|
|
70
|
-
const handles = parseMastodonFollowingCsv(fileContent);
|
|
71
|
-
followingResult = await bulkImportFollowing(
|
|
72
|
-
handles,
|
|
73
|
-
followingCollection,
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (importFollowers && followersCollection) {
|
|
78
|
-
const entries = parseMastodonFollowersList(fileContent);
|
|
79
|
-
followersResult = await bulkImportFollowers(
|
|
80
|
-
entries,
|
|
81
|
-
followersCollection,
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const totalFailed =
|
|
86
|
-
followingResult.failed + followersResult.failed;
|
|
87
|
-
result = {
|
|
88
|
-
type: "success",
|
|
89
|
-
text: response.locals
|
|
90
|
-
.__("activitypub.migrate.success")
|
|
91
|
-
.replace("%d", followingResult.imported)
|
|
92
|
-
.replace("%d", followersResult.imported)
|
|
93
|
-
.replace("%d", totalFailed),
|
|
94
|
-
};
|
|
95
|
-
}
|
|
36
|
+
// Only handles alias updates (small payload, regular form POST)
|
|
37
|
+
const aliasUrl = request.body.aliasUrl?.trim();
|
|
38
|
+
if (aliasUrl) {
|
|
39
|
+
pluginOptions.alsoKnownAs = aliasUrl;
|
|
40
|
+
result = {
|
|
41
|
+
type: "success",
|
|
42
|
+
text: response.locals.__("activitypub.migrate.aliasSuccess"),
|
|
43
|
+
};
|
|
96
44
|
}
|
|
97
45
|
|
|
98
46
|
response.render("activitypub-migrate", {
|
|
@@ -107,3 +55,63 @@ export function migratePostController(mountPath, pluginOptions) {
|
|
|
107
55
|
};
|
|
108
56
|
}
|
|
109
57
|
|
|
58
|
+
/**
|
|
59
|
+
* JSON endpoint for CSV import — receives { csvContent, importTypes }
|
|
60
|
+
* via fetch() to bypass Express's app-level urlencoded body size limit.
|
|
61
|
+
*/
|
|
62
|
+
export function migrateImportController(mountPath, pluginOptions) {
|
|
63
|
+
return async (request, response, next) => {
|
|
64
|
+
try {
|
|
65
|
+
const { application } = request.app.locals;
|
|
66
|
+
const { csvContent, importTypes } = request.body;
|
|
67
|
+
|
|
68
|
+
if (!csvContent?.trim()) {
|
|
69
|
+
return response.status(400).json({
|
|
70
|
+
type: "error",
|
|
71
|
+
text: "No CSV content provided.",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const followingCollection =
|
|
76
|
+
application?.collections?.get("ap_following");
|
|
77
|
+
const followersCollection =
|
|
78
|
+
application?.collections?.get("ap_followers");
|
|
79
|
+
|
|
80
|
+
const importFollowing = importTypes?.includes("following");
|
|
81
|
+
const importFollowers = importTypes?.includes("followers");
|
|
82
|
+
|
|
83
|
+
let followingResult = { imported: 0, failed: 0, errors: [] };
|
|
84
|
+
let followersResult = { imported: 0, failed: 0, errors: [] };
|
|
85
|
+
|
|
86
|
+
if (importFollowing && followingCollection) {
|
|
87
|
+
const handles = parseMastodonFollowingCsv(csvContent);
|
|
88
|
+
console.log(`[ActivityPub] Migration: parsed ${handles.length} following handles from CSV`);
|
|
89
|
+
followingResult = await bulkImportFollowing(handles, followingCollection);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (importFollowers && followersCollection) {
|
|
93
|
+
const entries = parseMastodonFollowersList(csvContent);
|
|
94
|
+
console.log(`[ActivityPub] Migration: parsed ${entries.length} follower entries from CSV`);
|
|
95
|
+
followersResult = await bulkImportFollowers(entries, followersCollection);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const totalFailed = followingResult.failed + followersResult.failed;
|
|
99
|
+
const totalImported = followingResult.imported + followersResult.imported;
|
|
100
|
+
const allErrors = [...followingResult.errors, ...followersResult.errors];
|
|
101
|
+
|
|
102
|
+
return response.json({
|
|
103
|
+
type: totalFailed > 0 && totalImported === 0 ? "error" : "success",
|
|
104
|
+
followingImported: followingResult.imported,
|
|
105
|
+
followersImported: followersResult.imported,
|
|
106
|
+
failed: totalFailed,
|
|
107
|
+
errors: allErrors,
|
|
108
|
+
});
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error("[ActivityPub] Migration import error:", error.message);
|
|
111
|
+
return response.status(500).json({
|
|
112
|
+
type: "error",
|
|
113
|
+
text: error.message,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
package/lib/migration.js
CHANGED
|
@@ -54,7 +54,10 @@ export function parseMastodonFollowersList(text) {
|
|
|
54
54
|
*/
|
|
55
55
|
export async function resolveHandleViaWebFinger(handle) {
|
|
56
56
|
const [user, domain] = handle.split("@");
|
|
57
|
-
if (!user || !domain)
|
|
57
|
+
if (!user || !domain) {
|
|
58
|
+
console.warn(`[ActivityPub] Migration: invalid handle "${handle}" — skipping`);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
58
61
|
|
|
59
62
|
try {
|
|
60
63
|
// WebFinger lookup
|
|
@@ -64,14 +67,20 @@ export async function resolveHandleViaWebFinger(handle) {
|
|
|
64
67
|
signal: AbortSignal.timeout(10_000),
|
|
65
68
|
});
|
|
66
69
|
|
|
67
|
-
if (!wfResponse.ok)
|
|
70
|
+
if (!wfResponse.ok) {
|
|
71
|
+
console.warn(`[ActivityPub] Migration: WebFinger failed for ${handle} (HTTP ${wfResponse.status})`);
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
68
74
|
|
|
69
75
|
const jrd = await wfResponse.json();
|
|
70
76
|
const selfLink = jrd.links?.find(
|
|
71
77
|
(l) => l.rel === "self" && l.type === "application/activity+json",
|
|
72
78
|
);
|
|
73
79
|
|
|
74
|
-
if (!selfLink?.href)
|
|
80
|
+
if (!selfLink?.href) {
|
|
81
|
+
console.warn(`[ActivityPub] Migration: no ActivityPub self link for ${handle}`);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
75
84
|
|
|
76
85
|
// Fetch actor document for inbox and profile
|
|
77
86
|
const actorResponse = await fetch(selfLink.href, {
|
|
@@ -79,7 +88,10 @@ export async function resolveHandleViaWebFinger(handle) {
|
|
|
79
88
|
signal: AbortSignal.timeout(10_000),
|
|
80
89
|
});
|
|
81
90
|
|
|
82
|
-
if (!actorResponse.ok)
|
|
91
|
+
if (!actorResponse.ok) {
|
|
92
|
+
console.warn(`[ActivityPub] Migration: actor fetch failed for ${handle} (HTTP ${actorResponse.status})`);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
83
95
|
|
|
84
96
|
const actor = await actorResponse.json();
|
|
85
97
|
return {
|
|
@@ -89,7 +101,8 @@ export async function resolveHandleViaWebFinger(handle) {
|
|
|
89
101
|
name: actor.name || actor.preferredUsername || handle,
|
|
90
102
|
handle: actor.preferredUsername || user,
|
|
91
103
|
};
|
|
92
|
-
} catch {
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.warn(`[ActivityPub] Migration: resolve failed for ${handle}: ${error.message}`);
|
|
93
106
|
return null;
|
|
94
107
|
}
|
|
95
108
|
}
|
|
@@ -99,16 +112,23 @@ export async function resolveHandleViaWebFinger(handle) {
|
|
|
99
112
|
*
|
|
100
113
|
* @param {string[]} handles - Array of handles to import
|
|
101
114
|
* @param {Collection} collection - MongoDB ap_following collection
|
|
102
|
-
* @returns {Promise<{imported: number, failed: number}>}
|
|
115
|
+
* @returns {Promise<{imported: number, failed: number, errors: string[]}>}
|
|
103
116
|
*/
|
|
104
117
|
export async function bulkImportFollowing(handles, collection) {
|
|
105
118
|
let imported = 0;
|
|
106
119
|
let failed = 0;
|
|
120
|
+
const errors = [];
|
|
121
|
+
|
|
122
|
+
console.log(`[ActivityPub] Migration: importing ${handles.length} following entries...`);
|
|
123
|
+
|
|
124
|
+
for (let i = 0; i < handles.length; i++) {
|
|
125
|
+
const handle = handles[i];
|
|
126
|
+
console.log(`[ActivityPub] Migration: resolving following ${i + 1}/${handles.length}: ${handle}`);
|
|
107
127
|
|
|
108
|
-
for (const handle of handles) {
|
|
109
128
|
const resolved = await resolveHandleViaWebFinger(handle);
|
|
110
129
|
if (!resolved) {
|
|
111
130
|
failed++;
|
|
131
|
+
errors.push(handle);
|
|
112
132
|
continue;
|
|
113
133
|
}
|
|
114
134
|
|
|
@@ -130,7 +150,12 @@ export async function bulkImportFollowing(handles, collection) {
|
|
|
130
150
|
imported++;
|
|
131
151
|
}
|
|
132
152
|
|
|
133
|
-
|
|
153
|
+
console.log(`[ActivityPub] Migration: following import complete — ${imported} imported, ${failed} failed`);
|
|
154
|
+
if (errors.length > 0) {
|
|
155
|
+
console.log(`[ActivityPub] Migration: failed handles: ${errors.join(", ")}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { imported, failed, errors };
|
|
134
159
|
}
|
|
135
160
|
|
|
136
161
|
/**
|
|
@@ -140,15 +165,24 @@ export async function bulkImportFollowing(handles, collection) {
|
|
|
140
165
|
*
|
|
141
166
|
* @param {string[]} entries - Array of handles or actor URLs
|
|
142
167
|
* @param {Collection} collection - MongoDB ap_followers collection
|
|
143
|
-
* @returns {Promise<{imported: number, failed: number}>}
|
|
168
|
+
* @returns {Promise<{imported: number, failed: number, errors: string[]}>}
|
|
144
169
|
*/
|
|
145
170
|
export async function bulkImportFollowers(entries, collection) {
|
|
146
171
|
let imported = 0;
|
|
147
172
|
let failed = 0;
|
|
173
|
+
const errors = [];
|
|
174
|
+
|
|
175
|
+
console.log(`[ActivityPub] Migration: importing ${entries.length} follower entries...`);
|
|
148
176
|
|
|
149
|
-
for (
|
|
177
|
+
for (let i = 0; i < entries.length; i++) {
|
|
178
|
+
const entry = entries[i];
|
|
150
179
|
// If it's a URL, store directly; if it's a handle, resolve via WebFinger
|
|
151
180
|
const isUrl = entry.startsWith("http");
|
|
181
|
+
|
|
182
|
+
if (!isUrl) {
|
|
183
|
+
console.log(`[ActivityPub] Migration: resolving follower ${i + 1}/${entries.length}: ${entry}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
152
186
|
let actorData;
|
|
153
187
|
|
|
154
188
|
if (isUrl) {
|
|
@@ -159,6 +193,7 @@ export async function bulkImportFollowers(entries, collection) {
|
|
|
159
193
|
|
|
160
194
|
if (!actorData) {
|
|
161
195
|
failed++;
|
|
196
|
+
errors.push(entry);
|
|
162
197
|
continue;
|
|
163
198
|
}
|
|
164
199
|
|
|
@@ -180,5 +215,10 @@ export async function bulkImportFollowers(entries, collection) {
|
|
|
180
215
|
imported++;
|
|
181
216
|
}
|
|
182
217
|
|
|
183
|
-
|
|
218
|
+
console.log(`[ActivityPub] Migration: follower import complete — ${imported} imported, ${failed} failed`);
|
|
219
|
+
if (errors.length > 0) {
|
|
220
|
+
console.log(`[ActivityPub] Migration: failed entries: ${errors.join(", ")}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return { imported, failed, errors };
|
|
184
224
|
}
|
package/locales/en.json
CHANGED
|
@@ -43,6 +43,8 @@
|
|
|
43
43
|
"step3Desc": "Once you have saved your alias and imported your data, go to your Mastodon instance → Preferences → Account → <strong>Move to a different account</strong>. Enter your new fediverse handle and confirm. Mastodon will notify all your followers, and those whose servers support it will automatically re-follow you here. This step is irreversible — your old account will become a redirect.",
|
|
44
44
|
"errorNoFile": "Please select a CSV file before importing.",
|
|
45
45
|
"success": "Imported %d following, %d followers (%d failed).",
|
|
46
|
+
"failedList": "Could not resolve: %s",
|
|
47
|
+
"failedListSummary": "Failed handles",
|
|
46
48
|
"aliasSuccess": "Alias saved — your actor document now includes this account as alsoKnownAs."
|
|
47
49
|
}
|
|
48
50
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmdes/indiekit-endpoint-activitypub",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"indiekit",
|
|
@@ -47,9 +47,7 @@
|
|
|
47
47
|
{{ heading({ text: __("activitypub.migrate.step2Title"), level: 2 }) }}
|
|
48
48
|
{{ prose({ text: __("activitypub.migrate.step2Desc") }) }}
|
|
49
49
|
|
|
50
|
-
<
|
|
51
|
-
<input type="hidden" name="action" value="import">
|
|
52
|
-
<input type="hidden" name="csvContent" :value="csvContent">
|
|
50
|
+
<div x-data="csvImport('{{ mountPath }}')">
|
|
53
51
|
|
|
54
52
|
{{ checkboxes({
|
|
55
53
|
name: "importTypes",
|
|
@@ -86,8 +84,31 @@
|
|
|
86
84
|
</template>
|
|
87
85
|
</div>
|
|
88
86
|
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
<button class="button" type="button"
|
|
88
|
+
:disabled="importing || !csvContent"
|
|
89
|
+
@click="startImport()">
|
|
90
|
+
<span x-show="!importing">{{ __("activitypub.migrate.importButton") }}</span>
|
|
91
|
+
<span x-show="importing" x-text="statusText"></span>
|
|
92
|
+
</button>
|
|
93
|
+
|
|
94
|
+
{# Result notification #}
|
|
95
|
+
<template x-if="resultType">
|
|
96
|
+
<div :class="'notification-banner notification-banner--' + resultType"
|
|
97
|
+
role="alert" style="margin-top: 1em">
|
|
98
|
+
<p x-text="resultText"></p>
|
|
99
|
+
<template x-if="resultErrors.length > 0">
|
|
100
|
+
<details style="margin-top: 0.5em">
|
|
101
|
+
<summary>{{ __("activitypub.migrate.failedListSummary") }} (<span x-text="resultErrors.length"></span>)</summary>
|
|
102
|
+
<ul style="margin-top: 0.5em; font-size: 0.875em">
|
|
103
|
+
<template x-for="err in resultErrors" :key="err">
|
|
104
|
+
<li x-text="err"></li>
|
|
105
|
+
</template>
|
|
106
|
+
</ul>
|
|
107
|
+
</details>
|
|
108
|
+
</template>
|
|
109
|
+
</div>
|
|
110
|
+
</template>
|
|
111
|
+
</div>
|
|
91
112
|
|
|
92
113
|
<hr>
|
|
93
114
|
|
|
@@ -96,12 +117,17 @@
|
|
|
96
117
|
{{ prose({ text: __("activitypub.migrate.step3Desc") }) }}
|
|
97
118
|
|
|
98
119
|
<script>
|
|
99
|
-
function csvImport() {
|
|
120
|
+
function csvImport(mountPath) {
|
|
100
121
|
return {
|
|
101
122
|
csvContent: '',
|
|
102
123
|
fileName: '',
|
|
103
124
|
lineCount: 0,
|
|
104
125
|
fileError: '',
|
|
126
|
+
importing: false,
|
|
127
|
+
statusText: '',
|
|
128
|
+
resultType: '',
|
|
129
|
+
resultText: '',
|
|
130
|
+
resultErrors: [],
|
|
105
131
|
|
|
106
132
|
readFile(event) {
|
|
107
133
|
var self = this;
|
|
@@ -109,6 +135,9 @@
|
|
|
109
135
|
self.fileName = '';
|
|
110
136
|
self.lineCount = 0;
|
|
111
137
|
self.fileError = '';
|
|
138
|
+
self.resultType = '';
|
|
139
|
+
self.resultText = '';
|
|
140
|
+
self.resultErrors = [];
|
|
112
141
|
|
|
113
142
|
var file = event.target.files[0];
|
|
114
143
|
if (!file) return;
|
|
@@ -130,6 +159,58 @@
|
|
|
130
159
|
self.fileError = 'Could not read file';
|
|
131
160
|
};
|
|
132
161
|
reader.readAsText(file);
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
async startImport() {
|
|
165
|
+
var self = this;
|
|
166
|
+
self.importing = true;
|
|
167
|
+
self.resultType = '';
|
|
168
|
+
self.resultText = '';
|
|
169
|
+
self.resultErrors = [];
|
|
170
|
+
self.statusText = 'Importing\u2026';
|
|
171
|
+
|
|
172
|
+
// Collect checked import types
|
|
173
|
+
var checkboxes = document.querySelectorAll('input[name="importTypes"]:checked');
|
|
174
|
+
var importTypes = [];
|
|
175
|
+
for (var i = 0; i < checkboxes.length; i++) {
|
|
176
|
+
importTypes.push(checkboxes[i].value);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (importTypes.length === 0) {
|
|
180
|
+
self.importing = false;
|
|
181
|
+
self.resultType = 'error';
|
|
182
|
+
self.resultText = 'Please select at least one import type.';
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
var res = await fetch(mountPath + '/admin/migrate/import', {
|
|
188
|
+
method: 'POST',
|
|
189
|
+
headers: { 'Content-Type': 'application/json' },
|
|
190
|
+
body: JSON.stringify({
|
|
191
|
+
csvContent: self.csvContent,
|
|
192
|
+
importTypes: importTypes
|
|
193
|
+
})
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
if (!res.ok) {
|
|
197
|
+
var errBody = await res.text();
|
|
198
|
+
throw new Error('Server error (' + res.status + '): ' + errBody);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
var data = await res.json();
|
|
202
|
+
self.resultType = data.type;
|
|
203
|
+
self.resultText = 'Imported ' + (data.followingImported || 0) + ' following, '
|
|
204
|
+
+ (data.followersImported || 0) + ' followers'
|
|
205
|
+
+ (data.failed > 0 ? ' (' + data.failed + ' failed)' : '') + '.';
|
|
206
|
+
self.resultErrors = data.errors || [];
|
|
207
|
+
} catch (err) {
|
|
208
|
+
self.resultType = 'error';
|
|
209
|
+
self.resultText = err.message;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
self.importing = false;
|
|
213
|
+
self.statusText = '';
|
|
133
214
|
}
|
|
134
215
|
};
|
|
135
216
|
}
|