chore: add code formatting and linting (#952)
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
import { flow, get } from "lodash";
|
||||
import { flow, get } from 'lodash';
|
||||
import {
|
||||
localForage,
|
||||
unsentRequest,
|
||||
@ -6,48 +6,49 @@ import {
|
||||
then,
|
||||
basename,
|
||||
Cursor,
|
||||
APIError
|
||||
} from "netlify-cms-lib-util";
|
||||
APIError,
|
||||
} from 'netlify-cms-lib-util';
|
||||
|
||||
export default class API {
|
||||
constructor(config) {
|
||||
this.api_root = config.api_root || "https://api.bitbucket.org/2.0";
|
||||
this.branch = config.branch || "master";
|
||||
this.repo = config.repo || "";
|
||||
this.api_root = config.api_root || 'https://api.bitbucket.org/2.0';
|
||||
this.branch = config.branch || 'master';
|
||||
this.repo = config.repo || '';
|
||||
this.requestFunction = config.requestFunction || unsentRequest.performRequest;
|
||||
// Allow overriding this.hasWriteAccess
|
||||
this.hasWriteAccess = config.hasWriteAccess || this.hasWriteAccess;
|
||||
this.repoURL = this.repo ? `/repositories/${ this.repo }` : "";
|
||||
this.repoURL = this.repo ? `/repositories/${this.repo}` : '';
|
||||
}
|
||||
|
||||
buildRequest = req => flow([
|
||||
unsentRequest.withRoot(this.api_root),
|
||||
unsentRequest.withTimestamp,
|
||||
])(req);
|
||||
buildRequest = req =>
|
||||
flow([unsentRequest.withRoot(this.api_root), unsentRequest.withTimestamp])(req);
|
||||
|
||||
request = req => flow([
|
||||
this.buildRequest,
|
||||
this.requestFunction,
|
||||
p => p.catch(err => Promise.reject(new APIError(err.message, null, "BitBucket"))),
|
||||
])(req);
|
||||
request = req =>
|
||||
flow([
|
||||
this.buildRequest,
|
||||
this.requestFunction,
|
||||
p => p.catch(err => Promise.reject(new APIError(err.message, null, 'BitBucket'))),
|
||||
])(req);
|
||||
|
||||
requestJSON = req => flow([
|
||||
unsentRequest.withDefaultHeaders({ "Content-Type": "application/json" }),
|
||||
this.request,
|
||||
then(responseParser({ format: "json" })),
|
||||
p => p.catch(err => Promise.reject(new APIError(err.message, null, "BitBucket"))),
|
||||
])(req);
|
||||
requestText = req => flow([
|
||||
unsentRequest.withDefaultHeaders({ "Content-Type": "text/plain" }),
|
||||
this.request,
|
||||
then(responseParser({ format: "text" })),
|
||||
p => p.catch(err => Promise.reject(new APIError(err.message, null, "BitBucket"))),
|
||||
])(req);
|
||||
requestJSON = req =>
|
||||
flow([
|
||||
unsentRequest.withDefaultHeaders({ 'Content-Type': 'application/json' }),
|
||||
this.request,
|
||||
then(responseParser({ format: 'json' })),
|
||||
p => p.catch(err => Promise.reject(new APIError(err.message, null, 'BitBucket'))),
|
||||
])(req);
|
||||
requestText = req =>
|
||||
flow([
|
||||
unsentRequest.withDefaultHeaders({ 'Content-Type': 'text/plain' }),
|
||||
this.request,
|
||||
then(responseParser({ format: 'text' })),
|
||||
p => p.catch(err => Promise.reject(new APIError(err.message, null, 'BitBucket'))),
|
||||
])(req);
|
||||
|
||||
user = () => this.request("/user");
|
||||
hasWriteAccess = user => this.request(this.repoURL).then(res => res.ok);
|
||||
user = () => this.request('/user');
|
||||
hasWriteAccess = () => this.request(this.repoURL).then(res => res.ok);
|
||||
|
||||
isFile = ({ type }) => type === "commit_file";
|
||||
isFile = ({ type }) => type === 'commit_file';
|
||||
processFile = file => ({
|
||||
...file,
|
||||
name: basename(file.path),
|
||||
@ -59,59 +60,75 @@ export default class API {
|
||||
// that will help with caching (though not as well as a normal
|
||||
// SHA, since it will change even if the individual file itself
|
||||
// doesn't.)
|
||||
...(file.commit && file.commit.hash
|
||||
? { id: `${ file.commit.hash }/${ file.path }` }
|
||||
: {}),
|
||||
...(file.commit && file.commit.hash ? { id: `${file.commit.hash}/${file.path}` } : {}),
|
||||
});
|
||||
processFiles = files => files.filter(this.isFile).map(this.processFile);
|
||||
|
||||
readFile = async (path, sha, { ref = this.branch, parseText = true } = {}) => {
|
||||
const cacheKey = parseText ? `bb.${ sha }` : `bb.${ sha }.blob`;
|
||||
const cacheKey = parseText ? `bb.${sha}` : `bb.${sha}.blob`;
|
||||
const cachedFile = sha ? await localForage.getItem(cacheKey) : null;
|
||||
if (cachedFile) { return cachedFile; }
|
||||
if (cachedFile) {
|
||||
return cachedFile;
|
||||
}
|
||||
const result = await this.request({
|
||||
url: `${ this.repoURL }/src/${ ref }/${ path }`,
|
||||
cache: "no-store",
|
||||
}).then(parseText ? responseParser({ format: "text" }) : responseParser({ format: "blob" }));
|
||||
if (sha) { localForage.setItem(cacheKey, result); }
|
||||
url: `${this.repoURL}/src/${ref}/${path}`,
|
||||
cache: 'no-store',
|
||||
}).then(parseText ? responseParser({ format: 'text' }) : responseParser({ format: 'blob' }));
|
||||
if (sha) {
|
||||
localForage.setItem(cacheKey, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
getEntriesAndCursor = jsonResponse => {
|
||||
const { size: count, page: index, pagelen: pageSize, next, previous: prev, values: entries } = jsonResponse;
|
||||
const pageCount = (pageSize && count) ? Math.ceil(count / pageSize) : undefined;
|
||||
const {
|
||||
size: count,
|
||||
page: index,
|
||||
pagelen: pageSize,
|
||||
next,
|
||||
previous: prev,
|
||||
values: entries,
|
||||
} = jsonResponse;
|
||||
const pageCount = pageSize && count ? Math.ceil(count / pageSize) : undefined;
|
||||
return {
|
||||
entries,
|
||||
cursor: Cursor.create({
|
||||
actions: [...(next ? ["next"] : []), ...(prev ? ["prev"] : [])],
|
||||
actions: [...(next ? ['next'] : []), ...(prev ? ['prev'] : [])],
|
||||
meta: { index, count, pageSize, pageCount },
|
||||
data: { links: { next, prev } },
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
listFiles = async path => {
|
||||
const { entries, cursor } = await flow([
|
||||
// sort files by filename ascending
|
||||
unsentRequest.withParams({ sort: "-path" }),
|
||||
unsentRequest.withParams({ sort: '-path' }),
|
||||
this.requestJSON,
|
||||
then(this.getEntriesAndCursor),
|
||||
])(`${ this.repoURL }/src/${ this.branch }/${ path }`);
|
||||
])(`${this.repoURL}/src/${this.branch}/${path}`);
|
||||
return { entries: this.processFiles(entries), cursor };
|
||||
}
|
||||
};
|
||||
|
||||
traverseCursor = async (cursor, action) => flow([
|
||||
this.requestJSON,
|
||||
then(this.getEntriesAndCursor),
|
||||
then(({ cursor: newCursor, entries }) => ({ cursor: newCursor, entries: this.processFiles(entries) })),
|
||||
])(cursor.data.getIn(["links", action]));
|
||||
traverseCursor = async (cursor, action) =>
|
||||
flow([
|
||||
this.requestJSON,
|
||||
then(this.getEntriesAndCursor),
|
||||
then(({ cursor: newCursor, entries }) => ({
|
||||
cursor: newCursor,
|
||||
entries: this.processFiles(entries),
|
||||
})),
|
||||
])(cursor.data.getIn(['links', action]));
|
||||
|
||||
listAllFiles = async path => {
|
||||
const { cursor: initialCursor, entries: initialEntries } = await this.listFiles(path);
|
||||
const entries = [...initialEntries];
|
||||
let currentCursor = initialCursor;
|
||||
while (currentCursor && currentCursor.actions.has("next")) {
|
||||
const { cursor: newCursor, entries: newEntries } = await this.traverseCursor(currentCursor, "next");
|
||||
while (currentCursor && currentCursor.actions.has('next')) {
|
||||
const { cursor: newCursor, entries: newEntries } = await this.traverseCursor(
|
||||
currentCursor,
|
||||
'next',
|
||||
);
|
||||
entries.push(...newEntries);
|
||||
currentCursor = newCursor;
|
||||
}
|
||||
@ -125,40 +142,41 @@ export default class API {
|
||||
formData.append(item.path, contentBlob, basename(item.path));
|
||||
formData.append('branch', branch);
|
||||
if (commitMessage) {
|
||||
formData.append("message", commitMessage);
|
||||
formData.append('message', commitMessage);
|
||||
}
|
||||
if (this.commitAuthor) {
|
||||
const { name, email } = this.commitAuthor;
|
||||
formData.append("author", `${name} <${email}>`);
|
||||
formData.append('author', `${name} <${email}>`);
|
||||
}
|
||||
|
||||
return flow([
|
||||
unsentRequest.withMethod("POST"),
|
||||
unsentRequest.withMethod('POST'),
|
||||
unsentRequest.withBody(formData),
|
||||
this.request,
|
||||
then(() => ({ ...item, uploaded: true })),
|
||||
])(`${ this.repoURL }/src`);
|
||||
])(`${this.repoURL}/src`);
|
||||
};
|
||||
|
||||
persistFiles = (files, { commitMessage }) => Promise.all(
|
||||
files.filter(({ uploaded }) => !uploaded).map(file => this.uploadBlob(file, { commitMessage }))
|
||||
);
|
||||
persistFiles = (files, { commitMessage }) =>
|
||||
Promise.all(
|
||||
files
|
||||
.filter(({ uploaded }) => !uploaded)
|
||||
.map(file => this.uploadBlob(file, { commitMessage })),
|
||||
);
|
||||
|
||||
deleteFile = (path, message, { branch = this.branch } = {}) => {
|
||||
const body = new FormData();
|
||||
body.append('files', path);
|
||||
body.append('branch', branch);
|
||||
if (message) {
|
||||
body.append("message", message);
|
||||
body.append('message', message);
|
||||
}
|
||||
if (this.commitAuthor) {
|
||||
const { name, email } = this.commitAuthor;
|
||||
body.append("author", `${name} <${email}>`);
|
||||
body.append('author', `${name} <${email}>`);
|
||||
}
|
||||
return flow([
|
||||
unsentRequest.withMethod("POST"),
|
||||
unsentRequest.withBody(body),
|
||||
this.request,
|
||||
])(`${ this.repoURL }/src`);
|
||||
return flow([unsentRequest.withMethod('POST'), unsentRequest.withBody(body), this.request])(
|
||||
`${this.repoURL}/src`,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import { AuthenticationPage, Icon } from 'netlify-cms-ui-default';
|
||||
|
||||
const LoginButtonIcon = styled(Icon)`
|
||||
margin-right: 18px;
|
||||
`
|
||||
`;
|
||||
|
||||
export default class BitbucketAuthenticationPage extends React.Component {
|
||||
static propTypes = {
|
||||
@ -16,11 +16,14 @@ export default class BitbucketAuthenticationPage extends React.Component {
|
||||
|
||||
state = {};
|
||||
|
||||
handleLogin = (e) => {
|
||||
handleLogin = e => {
|
||||
e.preventDefault();
|
||||
const cfg = {
|
||||
base_url: this.props.base_url,
|
||||
site_id: (document.location.host.split(':')[0] === 'localhost') ? 'cms.netlify.com' : this.props.site_id,
|
||||
site_id:
|
||||
document.location.host.split(':')[0] === 'localhost'
|
||||
? 'cms.netlify.com'
|
||||
: this.props.site_id,
|
||||
auth_endpoint: this.props.authEndpoint,
|
||||
};
|
||||
const auth = new NetlifyAuthenticator(cfg);
|
||||
@ -44,8 +47,8 @@ export default class BitbucketAuthenticationPage extends React.Component {
|
||||
loginErrorMessage={this.state.loginError}
|
||||
renderButtonContent={() => (
|
||||
<React.Fragment>
|
||||
<LoginButtonIcon type="bitbucket"/>
|
||||
{inProgress ? "Logging in..." : "Login with Bitbucket"}
|
||||
<LoginButtonIcon type="bitbucket" />
|
||||
{inProgress ? 'Logging in...' : 'Login with Bitbucket'}
|
||||
</React.Fragment>
|
||||
)}
|
||||
/>
|
||||
|
@ -1,21 +1,21 @@
|
||||
import semaphore from "semaphore";
|
||||
import { flow, trimStart } from "lodash";
|
||||
import semaphore from 'semaphore';
|
||||
import { flow, trimStart } from 'lodash';
|
||||
import {
|
||||
CURSOR_COMPATIBILITY_SYMBOL,
|
||||
filterByPropExtension,
|
||||
resolvePromiseProperties,
|
||||
then,
|
||||
unsentRequest,
|
||||
} from "netlify-cms-lib-util";
|
||||
} from 'netlify-cms-lib-util';
|
||||
import { NetlifyAuthenticator } from 'netlify-cms-lib-auth';
|
||||
import AuthenticationPage from "./AuthenticationPage";
|
||||
import API from "./API";
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
import API from './API';
|
||||
|
||||
const MAX_CONCURRENT_DOWNLOADS = 10;
|
||||
|
||||
// Implementation wrapper class
|
||||
export default class Bitbucket {
|
||||
constructor(config, options={}) {
|
||||
constructor(config, options = {}) {
|
||||
this.config = config;
|
||||
this.options = {
|
||||
proxied: false,
|
||||
@ -25,23 +25,23 @@ export default class Bitbucket {
|
||||
};
|
||||
|
||||
if (this.options.useWorkflow) {
|
||||
throw new Error("The BitBucket backend does not support the Editorial Workflow.");
|
||||
throw new Error('The BitBucket backend does not support the Editorial Workflow.');
|
||||
}
|
||||
|
||||
if (!this.options.proxied && !config.getIn(["backend", "repo"], false)) {
|
||||
throw new Error("The BitBucket backend needs a \"repo\" in the backend configuration.");
|
||||
if (!this.options.proxied && !config.getIn(['backend', 'repo'], false)) {
|
||||
throw new Error('The BitBucket backend needs a "repo" in the backend configuration.');
|
||||
}
|
||||
|
||||
this.api = this.options.API || null;
|
||||
|
||||
this.updateUserCredentials = this.options.updateUserCredentials;
|
||||
|
||||
this.repo = config.getIn(["backend", "repo"], "");
|
||||
this.branch = config.getIn(["backend", "branch"], "master");
|
||||
this.api_root = config.getIn(["backend", "api_root"], "https://api.bitbucket.org/2.0");
|
||||
this.base_url = config.get("base_url");
|
||||
this.site_id = config.get("site_id");
|
||||
this.token = "";
|
||||
this.repo = config.getIn(['backend', 'repo'], '');
|
||||
this.branch = config.getIn(['backend', 'branch'], 'master');
|
||||
this.api_root = config.getIn(['backend', 'api_root'], 'https://api.bitbucket.org/2.0');
|
||||
this.base_url = config.get('base_url');
|
||||
this.site_id = config.get('site_id');
|
||||
this.token = '';
|
||||
}
|
||||
|
||||
authComponent() {
|
||||
@ -50,7 +50,11 @@ export default class Bitbucket {
|
||||
|
||||
setUser(user) {
|
||||
this.token = user.token;
|
||||
this.api = new API({ requestFunction: this.apiRequestFunction, branch: this.branch, repo: this.repo });
|
||||
this.api = new API({
|
||||
requestFunction: this.apiRequestFunction,
|
||||
branch: this.branch,
|
||||
repo: this.repo,
|
||||
});
|
||||
}
|
||||
|
||||
restoreUser(user) {
|
||||
@ -60,13 +64,19 @@ export default class Bitbucket {
|
||||
authenticate(state) {
|
||||
this.token = state.token;
|
||||
this.refreshToken = state.refresh_token;
|
||||
this.api = new API({ requestFunction: this.apiRequestFunction, branch: this.branch, repo: this.repo, api_root: this.api_root });
|
||||
this.api = new API({
|
||||
requestFunction: this.apiRequestFunction,
|
||||
branch: this.branch,
|
||||
repo: this.repo,
|
||||
api_root: this.api_root,
|
||||
});
|
||||
|
||||
return this.api.user().then(user =>
|
||||
this.api.hasWriteAccess(user).then(isCollab => {
|
||||
if (!isCollab) throw new Error("Your BitBucker user account does not have access to this repo.");
|
||||
if (!isCollab)
|
||||
throw new Error('Your BitBucker user account does not have access to this repo.');
|
||||
return Object.assign({}, user, { token: state.token, refresh_token: state.refresh_token });
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@ -84,7 +94,8 @@ export default class Bitbucket {
|
||||
this.authenticator = new NetlifyAuthenticator(cfg);
|
||||
}
|
||||
|
||||
this.refreshedTokenPromise = this.authenticator.refresh({ provider: "bitbucket", refresh_token: this.refreshToken })
|
||||
this.refreshedTokenPromise = this.authenticator
|
||||
.refresh({ provider: 'bitbucket', refresh_token: this.refreshToken })
|
||||
.then(({ token, refresh_token }) => {
|
||||
this.token = token;
|
||||
this.refreshToken = refresh_token;
|
||||
@ -112,14 +123,17 @@ export default class Bitbucket {
|
||||
apiRequestFunction = async req => {
|
||||
const token = this.refreshedTokenPromise ? await this.refreshedTokenPromise : this.token;
|
||||
return flow([
|
||||
unsentRequest.withHeaders({ Authorization: `Bearer ${ token }` }),
|
||||
unsentRequest.withHeaders({ Authorization: `Bearer ${token}` }),
|
||||
unsentRequest.performRequest,
|
||||
then(async res => {
|
||||
if (res.status === 401) {
|
||||
const json = (await res.json().catch(() => null));
|
||||
if (json && json.type === "error" && /^access token expired/i.test(json.error.message)) {
|
||||
const json = await res.json().catch(() => null);
|
||||
if (json && json.type === 'error' && /^access token expired/i.test(json.error.message)) {
|
||||
const newToken = await this.getRefreshedAccessToken();
|
||||
const reqWithNewToken = unsentRequest.withHeaders({ Authorization: `Bearer ${ newToken }` }, req);
|
||||
const reqWithNewToken = unsentRequest.withHeaders(
|
||||
{ Authorization: `Bearer ${newToken}` },
|
||||
req,
|
||||
);
|
||||
return unsentRequest.performRequest(reqWithNewToken);
|
||||
}
|
||||
}
|
||||
@ -129,11 +143,11 @@ export default class Bitbucket {
|
||||
};
|
||||
|
||||
entriesByFolder(collection, extension) {
|
||||
const listPromise = this.api.listFiles(collection.get("folder"));
|
||||
const listPromise = this.api.listFiles(collection.get('folder'));
|
||||
return resolvePromiseProperties({
|
||||
files: listPromise
|
||||
.then(({ entries }) => entries)
|
||||
.then(filterByPropExtension(extension, "path"))
|
||||
.then(filterByPropExtension(extension, 'path'))
|
||||
.then(this.fetchFiles),
|
||||
cursor: listPromise.then(({ cursor }) => cursor),
|
||||
}).then(({ files, cursor }) => {
|
||||
@ -143,37 +157,46 @@ export default class Bitbucket {
|
||||
}
|
||||
|
||||
allEntriesByFolder(collection, extension) {
|
||||
return this.api.listAllFiles(collection.get("folder"))
|
||||
.then(filterByPropExtension(extension, "path"))
|
||||
return this.api
|
||||
.listAllFiles(collection.get('folder'))
|
||||
.then(filterByPropExtension(extension, 'path'))
|
||||
.then(this.fetchFiles);
|
||||
}
|
||||
|
||||
entriesByFiles(collection) {
|
||||
const files = collection.get("files").map(collectionFile => ({
|
||||
path: collectionFile.get("file"),
|
||||
label: collectionFile.get("label"),
|
||||
const files = collection.get('files').map(collectionFile => ({
|
||||
path: collectionFile.get('file'),
|
||||
label: collectionFile.get('label'),
|
||||
}));
|
||||
return this.fetchFiles(files);
|
||||
}
|
||||
|
||||
fetchFiles = (files) => {
|
||||
fetchFiles = files => {
|
||||
const sem = semaphore(MAX_CONCURRENT_DOWNLOADS);
|
||||
const promises = [];
|
||||
files.forEach((file) => {
|
||||
promises.push(new Promise(resolve => (
|
||||
sem.take(() => this.api.readFile(file.path, file.id).then((data) => {
|
||||
resolve({ file, data });
|
||||
sem.leave();
|
||||
}).catch((error = true) => {
|
||||
sem.leave();
|
||||
console.error(`failed to load file from BitBucket: ${ file.path }`);
|
||||
resolve({ error });
|
||||
}))
|
||||
)));
|
||||
files.forEach(file => {
|
||||
promises.push(
|
||||
new Promise(resolve =>
|
||||
sem.take(() =>
|
||||
this.api
|
||||
.readFile(file.path, file.id)
|
||||
.then(data => {
|
||||
resolve({ file, data });
|
||||
sem.leave();
|
||||
})
|
||||
.catch((error = true) => {
|
||||
sem.leave();
|
||||
console.error(`failed to load file from BitBucket: ${file.path}`);
|
||||
resolve({ error });
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
return Promise.all(promises)
|
||||
.then(loadedEntries => loadedEntries.filter(loadedEntry => !loadedEntry.error));
|
||||
}
|
||||
return Promise.all(promises).then(loadedEntries =>
|
||||
loadedEntries.filter(loadedEntry => !loadedEntry.error),
|
||||
);
|
||||
};
|
||||
|
||||
getEntry(collection, slug, path) {
|
||||
return this.api.readFile(path).then(data => ({
|
||||
@ -185,18 +208,21 @@ export default class Bitbucket {
|
||||
getMedia() {
|
||||
const sem = semaphore(MAX_CONCURRENT_DOWNLOADS);
|
||||
|
||||
return this.api.listAllFiles(this.config.get("media_folder"))
|
||||
.then(files => files.map(({ id, name, download_url, path }) => {
|
||||
const getBlobPromise = () => new Promise((resolve, reject) =>
|
||||
sem.take(() =>
|
||||
this.api.readFile(path, id, { parseText: false })
|
||||
.then(resolve, reject)
|
||||
.finally(() => sem.leave())
|
||||
)
|
||||
);
|
||||
return this.api.listAllFiles(this.config.get('media_folder')).then(files =>
|
||||
files.map(({ id, name, download_url, path }) => {
|
||||
const getBlobPromise = () =>
|
||||
new Promise((resolve, reject) =>
|
||||
sem.take(() =>
|
||||
this.api
|
||||
.readFile(path, id, { parseText: false })
|
||||
.then(resolve, reject)
|
||||
.finally(() => sem.leave()),
|
||||
),
|
||||
);
|
||||
|
||||
return { id, name, getBlobPromise, url: download_url, path };
|
||||
}));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
persistEntry(entry, mediaFiles, options = {}) {
|
||||
@ -215,10 +241,11 @@ export default class Bitbucket {
|
||||
}
|
||||
|
||||
traverseCursor(cursor, action) {
|
||||
return this.api.traverseCursor(cursor, action)
|
||||
.then(async ({ entries, cursor: newCursor }) => ({
|
||||
entries: await Promise.all(entries.map(file => this.api.readFile(file.path, file.id).then(data => ({ file, data })))),
|
||||
cursor: newCursor,
|
||||
}));
|
||||
return this.api.traverseCursor(cursor, action).then(async ({ entries, cursor: newCursor }) => ({
|
||||
entries: await Promise.all(
|
||||
entries.map(file => this.api.readFile(file.path, file.id).then(data => ({ file, data }))),
|
||||
),
|
||||
cursor: newCursor,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
export BitbucketBackend from './implementation';
|
||||
export API from './API';
|
||||
export AuthenticationPage from './AuthenticationPage';
|
||||
|
||||
|
Reference in New Issue
Block a user